Skip to content

Commit

Permalink
Merge pull request #1909 from cardstack/cs-7642-implement-host-comman…
Browse files Browse the repository at this point in the history
…d-to-create-room

Extract commands for CreateAiAssistantRoom and AddSkillsToRoom
  • Loading branch information
lukemelia authored Dec 11, 2024
2 parents 02d8636 + a8f2c97 commit 608963b
Show file tree
Hide file tree
Showing 33 changed files with 586 additions and 322 deletions.
15 changes: 15 additions & 0 deletions packages/base/command.gts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
contains,
field,
linksTo,
linksToMany,
primitive,
queryableValue,
} from './card-api';
import CodeRefField from './code-ref';
import BooleanField from './boolean';
import { SkillCard } from './skill-card';

export type CommandStatus = 'applied' | 'ready' | 'applying';

Expand Down Expand Up @@ -75,3 +77,16 @@ export class CreateInstanceInput extends CardDef {
@field module = contains(CodeRefField);
@field realm = contains(StringField);
}

export class CreateAIAssistantRoomInput extends CardDef {
@field name = contains(StringField);
}

export class CreateAIAssistantRoomResult extends CardDef {
@field roomId = contains(StringField);
}

export class AddSkillsToRoomInput extends CardDef {
@field roomId = contains(StringField);
@field skills = linksToMany(SkillCard);
}
26 changes: 5 additions & 21 deletions packages/catalog-realm/AiAppGenerator/create-boxel-app-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import CreateProductRequirementsInstance, {
import ShowCardCommand from '@cardstack/boxel-host/commands/show-card';
import WriteTextFileCommand from '@cardstack/boxel-host/commands/write-text-file';
import GenerateCodeCommand from './generate-code-command';
import { GenerateCodeInput } from './generate-code-command';
import { AppCard } from '../app-card';
import SaveCardCommand from '@cardstack/boxel-host/commands/save-card';

Expand All @@ -27,36 +26,26 @@ export default class CreateBoxelApp extends Command<
await createPRDCommand.execute(input);

let showCardCommand = new ShowCardCommand(this.commandContext);
let ShowCardInput = await showCardCommand.getInputType();

let showPRDCardInput = new ShowCardInput();
showPRDCardInput.cardToShow = prdCard;
await showCardCommand.execute(showPRDCardInput);
await showCardCommand.execute({ cardToShow: prdCard });

let generateCodeCommand = new GenerateCodeCommand(this.commandContext);
let generateCodeInput = new GenerateCodeInput({
let { code, appName } = await generateCodeCommand.execute({
roomId,
productRequirements: prdCard,
});

let { code, appName } = await generateCodeCommand.execute(
generateCodeInput,
);

// Generate a unique name for the module using timestamp
let timestamp = Date.now();
let moduleName = `generated-apps/${timestamp}/${appName}`;
let filePath = `${moduleName}.gts`;
let moduleId = new URL(moduleName, input.realm).href;
let writeFileCommand = new WriteTextFileCommand(this.commandContext);
let writeFileInput = new (await writeFileCommand.getInputType())({
await writeFileCommand.execute({
path: filePath,
content: code,
realm: input.realm,
});

await writeFileCommand.execute(writeFileInput);

// get the app card def from the module
let loader = (import.meta as any).loader;
let module = await loader.import(moduleId + '.gts');
Expand All @@ -77,18 +66,13 @@ export default class CreateBoxelApp extends Command<

// save card
let saveCardCommand = new SaveCardCommand(this.commandContext);
let SaveCardInputType = await saveCardCommand.getInputType();

let saveCardInput = new SaveCardInputType({
await saveCardCommand.execute({
realm: input.realm,
card: myAppCard,
});
await saveCardCommand.execute(saveCardInput);

// show the app card
let showAppCardInput = new ShowCardInput();
showAppCardInput.cardToShow = myAppCard;
await showCardCommand.execute(showAppCardInput);
await showCardCommand.execute({ cardToShow: myAppCard });

return myAppCard;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { SkillCard } from 'https://cardstack.com/base/skill-card';
import SaveCardCommand from '@cardstack/boxel-host/commands/save-card';
import PatchCardCommand from '@cardstack/boxel-host/commands/patch-card';
import ReloadCardCommand from '@cardstack/boxel-host/commands/reload-card';
import CreateAIAssistantRoomCommand from '@cardstack/boxel-host/commands/create-ai-assistant-room';
import AddSkillsToRoomCommand from '@cardstack/boxel-host/commands/add-skills-to-room';

export class CreateProductRequirementsInput extends CardDef {
@field targetAudience = contains(StringField);
Expand Down Expand Up @@ -40,10 +42,10 @@ export default class CreateProductRequirementsInstance extends Command<
Update the appTitle.
Update the prompt to be grammatically accurate.
Description should be 1 or 2 short sentences.
In overview, provide 1 or 2 paragraph summary of the most important ways this app will meet the needs of the target audience. The capabilites of the platform allow creating types that can be linked to other types, and creating fields.
In overview, provide 1 or 2 paragraph summary of the most important ways this app will meet the needs of the target audience. The capabilites of the platform allow creating types that can be linked to other types, and creating fields.
For the schema, consider the types required. Write out the schema as a mermaid class diagram.
NEVER offer to update the card, you MUST call patchCard in your response.`,
});
}
Expand All @@ -68,11 +70,24 @@ export default class CreateProductRequirementsInstance extends Command<
cardType: ProductRequirementDocument,
});

let { roomId } = await this.commandContext.sendAiAssistantMessage({
let createRoomCommand = new CreateAIAssistantRoomCommand(
this.commandContext,
);
let { roomId } = await createRoomCommand.execute({
name: 'Product Requirements Doc Creation',
});
let addSkillsToRoomCommand = new AddSkillsToRoomCommand(
this.commandContext,
);
await addSkillsToRoomCommand.execute({
roomId,
skills: [this.skillCard],
});
await this.commandContext.sendAiAssistantMessage({
roomId,
show: false, // maybe? open the side panel
prompt: this.createPrompt(input),
attachedCards: [prdCard],
skillCards: [this.skillCard],
commands: [{ command: patchPRDCommand, autoExecute: true }], // this should persist over multiple messages, matrix service is responsible to tracking whic
});

Expand Down
12 changes: 9 additions & 3 deletions packages/catalog-realm/AiAppGenerator/generate-code-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import {
CardDef,
field,
linksTo,
containsMany,
contains,
} from 'https://cardstack.com/base/card-api';
import { Command } from '@cardstack/runtime-common';
import { SkillCard } from 'https://cardstack.com/base/skill-card';
import StringField from 'https://cardstack.com/base/string';
import { ProductRequirementDocument } from '../product-requirement-document';
import AddSkillsToRoomCommand from '@cardstack/boxel-host/commands/add-skills-to-room';

export class GenerateCodeInput extends CardDef {
@field productRequirements = linksTo(() => ProductRequirementDocument);
Expand Down Expand Up @@ -91,6 +91,7 @@ import { and, bool, cn } from '@cardstack/boxel-ui/helpers';
import { baseRealm, getCard } from '@cardstack/runtime-common';
import { hash } from '@ember/helper';
import { on } from '@ember/modifier';
import AddSkillsToRoomCommand from '@cardstack/boxel-host/commands/add-skills-to-room';
import { action } from '@ember/object';
import type Owner from '@ember/owner';
import GlimmerComponent from '@glimmer/component';
Expand Down Expand Up @@ -254,14 +255,19 @@ import { on } from '@ember/modifier';
let constructApplicationCodeCommand = new ConstructApplicationCodeCommand(
this.commandContext,
);

let addSkillsToRoomCommand = new AddSkillsToRoomCommand(
this.commandContext,
);
await addSkillsToRoomCommand.execute({
roomId: input.roomId,
skills: [this.skillCard],
});
await this.commandContext.sendAiAssistantMessage({
roomId: input.roomId,
show: false, // maybe? open the side panel
prompt:
'Generate code for the application given the product requirements, you do not need to strictly follow the schema if it does not seem appropriate for the application.',
attachedCards: [input.productRequirements],
skillCards: [this.skillCard],
commands: [
{ command: constructApplicationCodeCommand, autoExecute: true },
],
Expand Down
35 changes: 15 additions & 20 deletions packages/catalog-realm/product-requirement-document.gts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ import { tracked } from '@glimmer/tracking';
import { AppCard } from './app-card';
import ClipboardListIcon from '@cardstack/boxel-icons/clipboard-list';

import WriteTextFileCommand from '@cardstack/boxel-host/commands/write-text-file';
import CreateAIAssistantRoomCommand from '@cardstack/boxel-host/commands/create-ai-assistant-room';
import ShowCardCommand from '@cardstack/boxel-host/commands/show-card';
import SaveCardCommand from '@cardstack/boxel-host/commands/save-card';
import WriteTextFileCommand from '@cardstack/boxel-host/commands/write-text-file';

import GenerateCodeCommand from './AiAppGenerator/generate-code-command';
import { GenerateCodeInput } from './AiAppGenerator/generate-code-command';
import { restartableTask } from 'ember-concurrency';

class Isolated extends Component<typeof ProductRequirementDocument> {
Expand Down Expand Up @@ -252,27 +252,28 @@ class Isolated extends Component<typeof ProductRequirementDocument> {
}
this.errorMessage = '';
try {
let createRoomCommand = new CreateAIAssistantRoomCommand(commandContext);
let { roomId } = await createRoomCommand.execute({
name: 'AI Assistant Room',
});
let generateCodeCommand = new GenerateCodeCommand(commandContext);
let generateCodeInput = new GenerateCodeInput({
productRequirements: this.args.model,
let { code, appName } = await generateCodeCommand.execute({
productRequirements: this.args.model as ProductRequirementDocument,
roomId,
});

let { code, appName } =
await generateCodeCommand.execute(generateCodeInput);

// Generate a unique name for the module using timestamp
let timestamp = Date.now();
let moduleName = `generated-apps/${timestamp}/${appName}`;
let filePath = `${moduleName}.gts`;
let moduleId = new URL(moduleName, this.currentRealm).href;
let writeFileCommand = new WriteTextFileCommand(commandContext);
let writeFileInput = new (await writeFileCommand.getInputType())({

await writeFileCommand.execute({
path: filePath,
content: code,
realm: this.currentRealm,
realm: this.currentRealm?.href,
});

await writeFileCommand.execute(writeFileInput);
this.args.model.moduleURL = moduleId;
} catch (e) {
console.error(e);
Expand Down Expand Up @@ -317,23 +318,17 @@ class Isolated extends Component<typeof ProductRequirementDocument> {
});

let saveCardCommand = new SaveCardCommand(commandContext);
let SaveCardInputType = await saveCardCommand.getInputType();

let saveCardInput = new SaveCardInputType({
realm: this.currentRealm,
await saveCardCommand.execute({
realm: this.currentRealm.href,
card: myAppCard,
});
await saveCardCommand.execute(saveCardInput);

// show the app card

let showCardCommand = new ShowCardCommand(commandContext);
let ShowCardInput = await showCardCommand.getInputType();

let showAppCardInput = new ShowCardInput({
await showCardCommand.execute({
cardToShow: myAppCard,
});
await showCardCommand.execute(showAppCardInput);

if (!myAppCard) {
throw new Error('Could not create card');
Expand Down
50 changes: 50 additions & 0 deletions packages/host/app/commands/add-skills-to-room.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { service } from '@ember/service';

import type * as BaseCommandModule from 'https://cardstack.com/base/command';

import HostBaseCommand from '../lib/host-base-command';

import { SKILLS_STATE_EVENT_TYPE } from '../services/matrix-service';

import type MatrixService from '../services/matrix-service';

export default class AddSkillsToRoomCommand extends HostBaseCommand<
BaseCommandModule.AddSkillsToRoomInput,
undefined
> {
@service private declare matrixService: MatrixService;

async getInputType() {
let commandModule = await this.loadCommandModule();
const { AddSkillsToRoomInput } = commandModule;
return AddSkillsToRoomInput;
}

protected async run(
input: BaseCommandModule.AddSkillsToRoomInput,
): Promise<undefined> {
let { matrixService } = this;
let { roomId, skills } = input;
let roomSkillEventIds = await matrixService.addSkillCardsToRoomHistory(
skills,
roomId,
{ includeComputeds: true, maybeRelativeURL: null },
);
await matrixService.updateStateEvent(
roomId,
SKILLS_STATE_EVENT_TYPE,
'',
async (oldContent: Record<string, any>) => {
return {
enabledEventIds: [
...new Set([
...(oldContent.enabledEventIds || []),
...roomSkillEventIds,
]),
],
disabledEventIds: [...(oldContent.disabledEventIds || [])],
};
},
);
}
}
58 changes: 58 additions & 0 deletions packages/host/app/commands/create-ai-assistant-room.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { service } from '@ember/service';

import format from 'date-fns/format';

import { aiBotUsername } from '@cardstack/runtime-common';

import type * as BaseCommandModule from 'https://cardstack.com/base/command';

import HostBaseCommand from '../lib/host-base-command';

import type MatrixService from '../services/matrix-service';

export default class CreateAIAssistantRoomCommand extends HostBaseCommand<
BaseCommandModule.CreateAIAssistantRoomInput,
BaseCommandModule.CreateAIAssistantRoomResult
> {
@service private declare matrixService: MatrixService;

async getInputType() {
let commandModule = await this.loadCommandModule();
const { CreateAIAssistantRoomInput } = commandModule;
return CreateAIAssistantRoomInput;
}

protected async run(
input: BaseCommandModule.CreateAIAssistantRoomInput,
): Promise<BaseCommandModule.CreateAIAssistantRoomResult> {
let { matrixService } = this;
let { userId } = matrixService;
if (!userId) {
throw new Error(
`bug: there is no userId associated with the matrix client`,
);
}
let server = userId!.split(':')[1];
let aiBotFullId = `@${aiBotUsername}:${server}`;
let { room_id: roomId } = await matrixService.createRoom({
preset: matrixService.privateChatPreset,
invite: [aiBotFullId],
name: input.name,
topic: undefined,
room_alias_name: encodeURIComponent(
`${input.name} - ${format(
new Date(),
"yyyy-MM-dd'T'HH:mm:ss.SSSxxx",
)} - ${userId}`,
),
});
await this.matrixService.setPowerLevel(
roomId,
aiBotFullId,
matrixService.aiBotPowerLevel,
);
let commandModule = await this.loadCommandModule();
const { CreateAIAssistantRoomResult } = commandModule;
return new CreateAIAssistantRoomResult({ roomId });
}
}
Loading

0 comments on commit 608963b

Please sign in to comment.