From 610e3afabeae67c4e018960f12124561938e7ef4 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 31 Oct 2024 18:21:21 -0400 Subject: [PATCH 01/40] Implement pieces of planned Commands API in support of AI App Generator card --- packages/base/command.gts | 32 +++ .../catalog-realm/AiAppGenerator/commands.ts | 218 ++++++++++++++++++ packages/catalog-realm/ai-app-generator.gts | 128 +++++----- packages/runtime-common/commands.ts | 106 +++++++++ packages/runtime-common/index.ts | 1 + 5 files changed, 432 insertions(+), 53 deletions(-) create mode 100644 packages/catalog-realm/AiAppGenerator/commands.ts create mode 100644 packages/runtime-common/commands.ts diff --git a/packages/base/command.gts b/packages/base/command.gts index 931daad66b..8bdc188dcf 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -5,9 +5,11 @@ import { StringField, contains, field, + linksTo, primitive, queryableValue, } from './card-api'; +import CodeRefField from './code-ref'; export type CommandStatus = 'applied' | 'ready'; @@ -49,3 +51,33 @@ export class CommandCard extends CardDef { @field eventId = contains(StringField); @field status = contains(CommandStatusField); } + +export class SaveCardInput extends CardDef { + @field realm = contains(StringField); + @field card = linksTo(CardDef); +} + +export class PatchCommandInput extends CardDef { + @field card = linksTo(CardDef); + @field patch = contains(FieldDef); //TODO: JSONField ? +} + +export class ShowCardInput extends CardDef { + @field cardToShow = linksTo(CardDef); + @field placement = contains(StringField); // TODO: nicer if enum, likely need to specify stackIndex too? +} + +export class CreateModuleInput extends CardDef { + @field code = contains(StringField); + @field realm = contains(StringField); + @field modulePath = contains(StringField); +} + +export class ModuleCard extends CardDef { + @field module = contains(CodeRefField); +} + +export class CreateInstanceInput extends CardDef { + @field module = contains(CodeRefField); + @field realm = contains(StringField); +} diff --git a/packages/catalog-realm/AiAppGenerator/commands.ts b/packages/catalog-realm/AiAppGenerator/commands.ts new file mode 100644 index 0000000000..a948e7a8ac --- /dev/null +++ b/packages/catalog-realm/AiAppGenerator/commands.ts @@ -0,0 +1,218 @@ +import { + CardDef, + field, + contains, + linksTo, + StringField, +} from 'https://cardstack.com/base/card-api'; +import { SkillCard } from 'https://cardstack.com/base/skill-card'; +import { + CreateInstanceInput, + CreateModuleInput, + ModuleCard, + PatchCommandInput, + SaveCardInput, + ShowCardInput, +} from 'https://cardstack.com/base/command'; +import { Command } from '@cardstack/runtime-common'; +import { ProductRequirementDocument } from '../product-requirement-document'; +import CodeRefField from 'https://cardstack.com/base/code-ref'; + +export class CreateProductRequirementsInput extends CardDef { + @field targetAudience = contains(StringField); + @field productDescription = contains(StringField); + @field features = contains(StringField); + @field realm = contains(StringField); +} + +export class CreateProductRequirementsResult extends CardDef { + @field productRequirements = linksTo(ProductRequirementDocument); + @field sessionId = contains(StringField); +} + +export class CreateProductRequirementsInstance extends Command< + CreateProductRequirementsInput, + CreateProductRequirementsResult +> { + inputType = CreateProductRequirementsInput; + + get skillCard() { + return new SkillCard({ + name: 'PRD Helper', + description: + 'This skill card can be used to help with creating product requirements', + instructions: + 'You are a helpful assistant that can help with creating product requirements, etc. Use patchCard to apply changes, do not ask, just do it, etc.', + }); + } + + createPrompt(input: CreateProductRequirementsInput) { + return `Create product requirements for ${input.targetAudience} with the following description: ${input.productDescription}. Focus on the following features: ${input.features}`; + } + + protected async run( + input: CreateProductRequirementsInput, + ): Promise { + // Create new card + let prdCard = new ProductRequirementDocument(); + + let saveCardCommand = this.commandContext.lookupCommand< + SaveCardInput, + undefined + >('saveCard'); // lookupCommand creates the instance and passes in the context + + // todo: use real carddef so we can do this in the constructor + let saveCardInput = new SaveCardInput(); + saveCardInput.realm = input.realm; + saveCardInput.card = prdCard; + + await saveCardCommand.execute(saveCardInput); + + // Get patch command, this takes the card and returns a command that can be used to patch the card + let patchPRDCommand = this.commandContext.lookupCommand< + PatchCommandInput, + undefined, + ProductRequirementDocument + >('patchCard'); + + // This should return a session ID so that we can potentially send followup messages + // This should delegate to a matrix service method. Besides actually sending the message, + // with attached cards, skill cards, and commands, it should also be responsible for assigning + // ids to the commands which are used to give the tools unique names (e.g. patchCard_ABC) + // service should maintain a mapping of commandIds to command instances in order to map an apply + // back to the correct command instance + // Auto execute commands are commands that should be executed automatically if they are returned + // as tool calls from the AI. + + let { sessionId } = await this.commandContext.sendAiAssistantMessage({ + show: true, // maybe? open the side panel + prompt: this.createPrompt(input), + attachedCards: [prdCard], + skillCards: [this.skillCard], + autoExecuteCommands: [patchPRDCommand], // this should persist over multiple messages, matrix service is responsible to tracking whic + }); + + // Wait for the PRD command to have been applied + await patchPRDCommand.waitForNextCompletion(); + let result = new CreateProductRequirementsResult(); + result.productRequirements = prdCard; + result.sessionId = sessionId; + return result; + } +} + +export class GenerateAppInput extends CardDef { + @field productRequirements = linksTo(ProductRequirementDocument); + @field realm = contains(StringField); + @field sessionId = contains(StringField); +} + +class GenerateCodeFromPRDResult extends CardDef { + @field module = contains(CodeRefField); + @field sessionId = contains(StringField); +} + +export class GenerateCodeFromPRDCommand extends Command< + GenerateAppInput, + GenerateCodeFromPRDResult +> { + inputType = GenerateAppInput; + + get skillCard() { + return new SkillCard({ + name: 'Boxel App Generator', + description: + 'This skill card helps generate code from product requirements', + instructions: + 'You are an expert programmer. Given product requirements, generate appropriate code that implements those requirements. Use the createModule command to create the module with the generated code.', + }); + } + + createPrompt(prdCard: ProductRequirementDocument) { + // TODO: use this PRD card value? + return `Please analyze the provided product requirements and generate appropriate code to implement them. Consider best practices, maintainability, and performance.`; + } + + protected async run( + input: GenerateAppInput, + ): Promise { + // Get the create module command + let createModuleCommand = this.commandContext.lookupCommand< + CreateModuleInput, + ModuleCard + >('createModule'); + + // Send message to AI assistant with the PRD card and wait for it to generate code + let { sessionId } = await this.commandContext.sendAiAssistantMessage({ + sessionId: input.sessionId, + show: true, + prompt: this.createPrompt(input.productRequirements), + attachedCards: [input.productRequirements], + skillCards: [this.skillCard], + autoExecuteCommands: [createModuleCommand], + }); + + // Wait for the module to be created + const moduleCard = await createModuleCommand.waitForNextCompletion(); + + let result = new GenerateCodeFromPRDResult(); + result.module = moduleCard.module; + result.sessionId = sessionId; + return result; + } +} + +export class CreateBoxelApp extends Command< + CreateProductRequirementsInput, + CardDef +> { + inputType = CreateProductRequirementsInput; + + protected async run(input: CreateProductRequirementsInput): Promise { + // Create PRD + let createPRDCommand = new CreateProductRequirementsInstance( + this.commandContext, + undefined, + ); + let { productRequirements: prdCard, sessionId } = + await createPRDCommand.execute(input); + let showCardCommand = this.commandContext.lookupCommand< + ShowCardInput, + undefined + >('showCard'); + let showPRDCardInput = new ShowCardInput(); + showPRDCardInput.cardToShow = prdCard; + showPRDCardInput.placement = 'addToStack'; // probably want to be able to lookup what stack to add this to, based on where the app card is, if visible + await showCardCommand.execute(showPRDCardInput); + // Generate App + let generateAppCommand = new GenerateCodeFromPRDCommand( + this.commandContext, + undefined, + ); + let generateAppInput = new GenerateAppInput(); + generateAppInput.productRequirements = prdCard; + generateAppInput.realm = input.realm; + generateAppInput.sessionId = sessionId; + let { module: moduleCard } = await generateAppCommand.execute( + generateAppInput, + ); + // Create instance + let createInstanceCommand = this.commandContext.lookupCommand< + CreateInstanceInput, + CardDef + >('createInstance'); + let createInstanceInput = new CreateInstanceInput(); + createInstanceInput.module = moduleCard.module; + createInstanceInput.realm = input.realm; + let appCard = await createInstanceCommand.execute(createInstanceInput); + // open new app card + let showCardInput = new ShowCardInput(); + showCardInput.cardToShow = appCard; + showCardInput.placement = 'addToStack'; // probably want to be able to lookup what stack to add this to, based on where the app card is, if visible + await showCardCommand.execute(showCardInput); + // // generate some sample data + // // Notes: + // // - We're going to need to look through the module and get the types? + return appCard; + } +} diff --git a/packages/catalog-realm/ai-app-generator.gts b/packages/catalog-realm/ai-app-generator.gts index 70d8b4821d..74d6eb8908 100644 --- a/packages/catalog-realm/ai-app-generator.gts +++ b/packages/catalog-realm/ai-app-generator.gts @@ -25,6 +25,10 @@ import { } from '@cardstack/runtime-common'; import { AppCard, AppCardTemplate, CardsGrid } from './app-card'; import CPU from '@cardstack/boxel-icons/cpu'; +import { + CreateBoxelApp, + CreateProductRequirementsInput, +} from './AiAppGenerator/commands'; const getCardTypeQuery = (cardRef: CodeRef, excludedId?: string): Query => { let filter: Query['filter']; @@ -297,7 +301,7 @@ class DashboardTab extends GlimmerComponent<{ @prompt={{this.prompt}} @setPrompt={{this.setPrompt}} @generateProductRequirementsDoc={{this.generateProductRequirementsDoc}} - @isLoading={{this.generateRequirements.isRunning}} + @isLoading={{this.isGenerating}} /> {{#if this.errorMessage}}

{{this.errorMessage}}

@@ -347,66 +351,84 @@ class DashboardTab extends GlimmerComponent<{ }; @tracked errorMessage = ''; @tracked prompt: Prompt = this.promptReset; + @tracked isGenerating = false; @action setPrompt(key: string, value: string) { this.prompt = { ...this.prompt, [key]: value }; } - @action generateProductRequirementsDoc() { - let appTitle = `${this.prompt.domain} ${this.prompt.appType}`; - let requirements = this.prompt.customRequirements - ? `that has these features: ${this.prompt.customRequirements}` - : ''; - let prompt = `I want to make a ${this.prompt.appType} tailored for a ${this.prompt.domain} ${requirements}`; - this.generateRequirements.perform(this.prdCardRef, { - data: { - attributes: { appTitle, prompt }, - meta: { - adoptsFrom: this.prdCardRef, - realmURL: this.args.currentRealm?.href, - }, - }, - }); - } - private generateRequirements = restartableTask( - async (ref: CodeRef, doc: LooseSingleCardDocument) => { - try { - this.errorMessage = ''; - let { createCard, viewCard, runCommand } = - this.args.context?.actions ?? {}; - if (!createCard || !viewCard || !runCommand) { - throw new Error('Missing required card actions'); - } + generateProductRequirementsDoc = async () => { + let command = new CreateBoxelApp( + this.args.context.commandContext, + undefined, + ); + this.isGenerating = true; + try { + await command.execute( + new CreateProductRequirementsInput({ + appType: this.prompt.appType, + domain: this.prompt.domain, + customRequirements: this.prompt.customRequirements, + realm: this.args.currentRealm, + }), + ); + } finally { + this.isGenerating = false; + } + // let appTitle = `${this.prompt.domain} ${this.prompt.appType}`; + // let requirements = this.prompt.customRequirements + // ? `that has these features: ${this.prompt.customRequirements}` + // : ''; + // let prompt = `I want to make a ${this.prompt.appType} tailored for a ${this.prompt.domain} ${requirements}`; + // this.generateRequirements.perform(this.prdCardRef, { + // data: { + // attributes: { appTitle, prompt }, + // meta: { + // adoptsFrom: this.prdCardRef, + // realmURL: this.args.currentRealm?.href, + // }, + // }, + // }); + }; + // private generateRequirements = restartableTask( + // async (ref: CodeRef, doc: LooseSingleCardDocument) => { + // try { + // this.errorMessage = ''; + // let { createCard, viewCard, runCommand } = + // this.args.context?.actions ?? {}; + // if (!createCard || !viewCard || !runCommand) { + // throw new Error('Missing required card actions'); + // } - let card = await createCard(ref, this.args.currentRealm, { - doc, - cardModeAfterCreation: 'isolated', - }); - if (!card) { - throw new Error('Error: Failed to create card'); - } + // let card = await createCard(ref, this.args.currentRealm, { + // doc, + // cardModeAfterCreation: 'isolated', + // }); + // if (!card) { + // throw new Error('Error: Failed to create card'); + // } - // Construct the relative URL for the SkillCard - let skillCardUrl = new URL( - './SkillCard/generate-product-requirements', - import.meta.url, - ).href; + // // Construct the relative URL for the SkillCard + // let skillCardUrl = new URL( + // './SkillCard/generate-product-requirements', + // import.meta.url, + // ).href; - // Update the runCommand call to use the constructed URL - await runCommand( - card, - skillCardUrl, - 'Generate product requirements document', - ); - this.prompt = this.promptReset; - this.args.setActiveTab?.('requirements'); - } catch (e) { - this.errorMessage = - e instanceof Error ? `${e.name}: ${e.message}` : 'An error occurred.'; - throw e; - } - }, - ); + // // Update the runCommand call to use the constructed URL + // await runCommand( + // card, + // skillCardUrl, + // 'Generate product requirements document', + // ); + // this.prompt = this.promptReset; + // this.args.setActiveTab?.('requirements'); + // } catch (e) { + // this.errorMessage = + // e instanceof Error ? `${e.name}: ${e.message}` : 'An error occurred.'; + // throw e; + // } + // }, + // ); } class RequirementsTab extends GlimmerComponent<{ diff --git a/packages/runtime-common/commands.ts b/packages/runtime-common/commands.ts new file mode 100644 index 0000000000..230a0804c6 --- /dev/null +++ b/packages/runtime-common/commands.ts @@ -0,0 +1,106 @@ +import { Deferred } from './deferred'; +import { CardDef } from 'https://cardstack.com/base/card-api'; +import { SkillCard } from 'https://cardstack.com/base/skill-card'; +// import { Schema } from './helpers/ai'; + +export interface CommandContext { + lookupCommand< + CardInputType extends CardDef | undefined, + CardResultType extends CardDef | undefined, + CommandConfiguration extends any | undefined = undefined, + >( + name: string, + ): Command; + + sendAiAssistantMessage: (params: { + sessionId?: string; // if falsy we create a new session + show?: boolean; // if truthy, ensure the side panel to the session + prompt: string; + attachedCards?: CardDef[]; + skillCards?: SkillCard[]; + autoExecuteCommands?: Command[]; + }) => Promise<{ sessionId: string }>; +} + +export class CommandInvocation< + CardInputType extends CardDef | undefined, + CardResultType extends CardDef | undefined, +> { + result?: CardResultType; + error: Error | undefined; + status: 'pending' | 'success' | 'error' = 'pending'; + private deferred: Deferred = new Deferred(); + + constructor(public readonly input: CardInputType) {} + + get promise(): Promise { + return this.deferred.promise; + } + + fulfill(result: CardResultType): void { + this.status = 'success'; + this.deferred.fulfill(result); + } + + reject(error: unknown): void { + this.status = 'error'; + this.deferred.reject(error); + } +} + +export abstract class Command< + CardInputType extends CardDef | undefined, + CardResultType extends CardDef | undefined, + CommandConfiguration extends any | undefined = undefined, +> { + // Is this actually type checking ? + abstract inputType: new () => NonNullable; + + invocations: CommandInvocation[] = []; + + nextCompletionDeferred: Deferred = + new Deferred(); + + name: string = this.constructor.name; + description = ''; + + constructor( + protected readonly commandContext: CommandContext, + protected readonly configuration: CommandConfiguration, // we'd like this to be required *if* CommandConfiguration is defined, and allow the user to skip it when CommandConfiguration is undefined + ) {} + + async execute(input: CardInputType): Promise { + // internal bookkeeping + // todo: support for this.runTask being defined + // runTask would be an ember task, run would just be a normal function + + let invocation = new CommandInvocation( + input, + ); + + this.invocations.push(invocation); + this.nextCompletionDeferred.fulfill(invocation.promise); + + try { + let result = await this.run(input); + invocation.fulfill(result); + return result; + } catch (error) { + invocation.reject(error); + throw error; + } finally { + this.nextCompletionDeferred = new Deferred(); + } + } + + protected abstract run(input: CardInputType): Promise; + + waitForNextCompletion(): Promise { + return this.nextCompletionDeferred.promise; + } + + //TODO: figure out how to do this, and if here is the right place + // async getInputJsonSchema(): Promise { + // return await getJSONSchema(this.inputType); + // } +} diff --git a/packages/runtime-common/index.ts b/packages/runtime-common/index.ts index 1f2e93542d..89ee529273 100644 --- a/packages/runtime-common/index.ts +++ b/packages/runtime-common/index.ts @@ -61,6 +61,7 @@ export interface RealmPrerenderedCards { import { RealmPaths, type LocalPath } from './paths'; import { CardTypeFilter, EveryFilter, Query } from './query'; import { Loader } from './loader'; +export * from './commands'; export * from './constants'; export * from './queue'; export * from './expression'; From 04ced18545dddefc630550e9345110434d1bc366 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Mon, 4 Nov 2024 18:06:36 -0500 Subject: [PATCH 02/40] WIP commands implementation --- packages/base/card-api.gts | 2 + packages/base/command.gts | 2 +- packages/base/field-component.gts | 1 + packages/base/matrix-event.gts | 2 +- .../catalog-realm/AiAppGenerator/commands.ts | 40 ++++---- packages/catalog-realm/ai-app-generator.gts | 9 +- .../4165a295-e5ed-4922-a271-3fe49c79b482.json | 4 +- .../6724b1e4-e620-4e62-86c4-4cab1c132170.json | 2 +- .../8a2268d9-89f9-4d4c-ab56-dfb4471814a4.json | 2 +- packages/host/app/commands/patch-card.ts | 66 +++++++++++++ packages/host/app/commands/save-card.ts | 31 +++++++ .../operator-mode/interact-submode.gts | 54 ++++++++++- .../components/operator-mode/stack-item.gts | 9 +- .../app/components/operator-mode/stack.gts | 4 +- packages/host/app/services/card-service.ts | 6 +- packages/host/app/services/matrix-service.ts | 62 ++++++++++++- .../tests/unit/ai-function-generation-test.ts | 92 ++++--------------- packages/runtime-common/commands.ts | 30 ++++-- packages/runtime-common/helpers/ai.ts | 29 +++--- 19 files changed, 312 insertions(+), 135 deletions(-) create mode 100644 packages/host/app/commands/patch-card.ts create mode 100644 packages/host/app/commands/save-card.ts diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index ae13a75b27..362cdb5832 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -44,6 +44,7 @@ import { type Actions, type RealmInfo, CodeRef, + CommandContext, } from '@cardstack/runtime-common'; import type { ComponentLike } from '@glint/template'; import { initSharedState } from './shared-state'; @@ -130,6 +131,7 @@ interface NotLoadedValue { export interface CardContext { actions?: Actions; + commandContext?: CommandContext; cardComponentModifier?: typeof Modifier<{ Args: { Named: { diff --git a/packages/base/command.gts b/packages/base/command.gts index 8bdc188dcf..a628b9d7e7 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -57,7 +57,7 @@ export class SaveCardInput extends CardDef { @field card = linksTo(CardDef); } -export class PatchCommandInput extends CardDef { +export class PatchCardInput extends CardDef { @field card = linksTo(CardDef); @field patch = contains(FieldDef); //TODO: JSONField ? } diff --git a/packages/base/field-component.gts b/packages/base/field-component.gts index cad24e4d2a..ea8c4b24dd 100644 --- a/packages/base/field-component.gts +++ b/packages/base/field-component.gts @@ -49,6 +49,7 @@ const DEFAULT_CARD_CONTEXT = { modify() {} }, actions: undefined, + commandContext: undefined, }; export class CardContextConsumer extends Component { diff --git a/packages/base/matrix-event.gts b/packages/base/matrix-event.gts index 65e5d5dfa0..620fc0cb0b 100644 --- a/packages/base/matrix-event.gts +++ b/packages/base/matrix-event.gts @@ -196,7 +196,7 @@ export interface CardMessageContent { context: { openCardIds?: string[]; tools: Tool[]; - submode: string | undefined; + submode?: string; }; }; } diff --git a/packages/catalog-realm/AiAppGenerator/commands.ts b/packages/catalog-realm/AiAppGenerator/commands.ts index a948e7a8ac..69e8226453 100644 --- a/packages/catalog-realm/AiAppGenerator/commands.ts +++ b/packages/catalog-realm/AiAppGenerator/commands.ts @@ -10,7 +10,7 @@ import { CreateInstanceInput, CreateModuleInput, ModuleCard, - PatchCommandInput, + PatchCardInput, SaveCardInput, ShowCardInput, } from 'https://cardstack.com/base/command'; @@ -27,7 +27,7 @@ export class CreateProductRequirementsInput extends CardDef { export class CreateProductRequirementsResult extends CardDef { @field productRequirements = linksTo(ProductRequirementDocument); - @field sessionId = contains(StringField); + @field roomId = contains(StringField); } export class CreateProductRequirementsInstance extends Command< @@ -59,21 +59,21 @@ export class CreateProductRequirementsInstance extends Command< let saveCardCommand = this.commandContext.lookupCommand< SaveCardInput, undefined - >('saveCard'); // lookupCommand creates the instance and passes in the context + >('save-card'); // lookupCommand creates the instance and passes in the context - // todo: use real carddef so we can do this in the constructor - let saveCardInput = new SaveCardInput(); - saveCardInput.realm = input.realm; - saveCardInput.card = prdCard; - - await saveCardCommand.execute(saveCardInput); + await saveCardCommand.execute( + new SaveCardInput({ + realm: input.realm, + card: prdCard, + }), + ); // Get patch command, this takes the card and returns a command that can be used to patch the card let patchPRDCommand = this.commandContext.lookupCommand< - PatchCommandInput, + PatchCardInput, undefined, ProductRequirementDocument - >('patchCard'); + >('patch-card'); // This should return a session ID so that we can potentially send followup messages // This should delegate to a matrix service method. Besides actually sending the message, @@ -84,7 +84,7 @@ export class CreateProductRequirementsInstance extends Command< // Auto execute commands are commands that should be executed automatically if they are returned // as tool calls from the AI. - let { sessionId } = await this.commandContext.sendAiAssistantMessage({ + let { roomId } = await this.commandContext.sendAiAssistantMessage({ show: true, // maybe? open the side panel prompt: this.createPrompt(input), attachedCards: [prdCard], @@ -96,7 +96,7 @@ export class CreateProductRequirementsInstance extends Command< await patchPRDCommand.waitForNextCompletion(); let result = new CreateProductRequirementsResult(); result.productRequirements = prdCard; - result.sessionId = sessionId; + result.roomId = roomId; return result; } } @@ -104,12 +104,12 @@ export class CreateProductRequirementsInstance extends Command< export class GenerateAppInput extends CardDef { @field productRequirements = linksTo(ProductRequirementDocument); @field realm = contains(StringField); - @field sessionId = contains(StringField); + @field roomId = contains(StringField); } class GenerateCodeFromPRDResult extends CardDef { @field module = contains(CodeRefField); - @field sessionId = contains(StringField); + @field roomId = contains(StringField); } export class GenerateCodeFromPRDCommand extends Command< @@ -143,8 +143,8 @@ export class GenerateCodeFromPRDCommand extends Command< >('createModule'); // Send message to AI assistant with the PRD card and wait for it to generate code - let { sessionId } = await this.commandContext.sendAiAssistantMessage({ - sessionId: input.sessionId, + let { roomId } = await this.commandContext.sendAiAssistantMessage({ + roomId: input.roomId, show: true, prompt: this.createPrompt(input.productRequirements), attachedCards: [input.productRequirements], @@ -157,7 +157,7 @@ export class GenerateCodeFromPRDCommand extends Command< let result = new GenerateCodeFromPRDResult(); result.module = moduleCard.module; - result.sessionId = sessionId; + result.roomId = roomId; return result; } } @@ -174,7 +174,7 @@ export class CreateBoxelApp extends Command< this.commandContext, undefined, ); - let { productRequirements: prdCard, sessionId } = + let { productRequirements: prdCard, roomId } = await createPRDCommand.execute(input); let showCardCommand = this.commandContext.lookupCommand< ShowCardInput, @@ -192,7 +192,7 @@ export class CreateBoxelApp extends Command< let generateAppInput = new GenerateAppInput(); generateAppInput.productRequirements = prdCard; generateAppInput.realm = input.realm; - generateAppInput.sessionId = sessionId; + generateAppInput.roomId = roomId; let { module: moduleCard } = await generateAppCommand.execute( generateAppInput, ); diff --git a/packages/catalog-realm/ai-app-generator.gts b/packages/catalog-realm/ai-app-generator.gts index 74d6eb8908..a8a126d2a9 100644 --- a/packages/catalog-realm/ai-app-generator.gts +++ b/packages/catalog-realm/ai-app-generator.gts @@ -358,10 +358,11 @@ class DashboardTab extends GlimmerComponent<{ } generateProductRequirementsDoc = async () => { - let command = new CreateBoxelApp( - this.args.context.commandContext, - undefined, - ); + let commandContext = this.args.context?.commandContext; + if (!commandContext) { + throw new Error('Missing commandContext'); + } + let command = new CreateBoxelApp(commandContext, undefined); this.isGenerating = true; try { await command.execute( diff --git a/packages/experiments-realm/ProductRequirementDocument/4165a295-e5ed-4922-a271-3fe49c79b482.json b/packages/experiments-realm/ProductRequirementDocument/4165a295-e5ed-4922-a271-3fe49c79b482.json index 77cbc1c5fa..86f5e8e2cc 100644 --- a/packages/experiments-realm/ProductRequirementDocument/4165a295-e5ed-4922-a271-3fe49c79b482.json +++ b/packages/experiments-realm/ProductRequirementDocument/4165a295-e5ed-4922-a271-3fe49c79b482.json @@ -27,9 +27,9 @@ }, "meta": { "adoptsFrom": { - "module": "../product-requirement-document", + "module": "/catalog/product-requirement-document", "name": "ProductRequirementDocument" } } } -} \ No newline at end of file +} diff --git a/packages/experiments-realm/ProductRequirementDocument/6724b1e4-e620-4e62-86c4-4cab1c132170.json b/packages/experiments-realm/ProductRequirementDocument/6724b1e4-e620-4e62-86c4-4cab1c132170.json index f03dd076f1..a5e6999396 100644 --- a/packages/experiments-realm/ProductRequirementDocument/6724b1e4-e620-4e62-86c4-4cab1c132170.json +++ b/packages/experiments-realm/ProductRequirementDocument/6724b1e4-e620-4e62-86c4-4cab1c132170.json @@ -20,7 +20,7 @@ }, "meta": { "adoptsFrom": { - "module": "../product-requirement-document", + "module": "/catalog/product-requirement-document", "name": "ProductRequirementDocument" } } diff --git a/packages/experiments-realm/ProductRequirementDocument/8a2268d9-89f9-4d4c-ab56-dfb4471814a4.json b/packages/experiments-realm/ProductRequirementDocument/8a2268d9-89f9-4d4c-ab56-dfb4471814a4.json index 027866a03b..8de49344f4 100644 --- a/packages/experiments-realm/ProductRequirementDocument/8a2268d9-89f9-4d4c-ab56-dfb4471814a4.json +++ b/packages/experiments-realm/ProductRequirementDocument/8a2268d9-89f9-4d4c-ab56-dfb4471814a4.json @@ -20,7 +20,7 @@ }, "meta": { "adoptsFrom": { - "module": "../product-requirement-document", + "module": "/catalog/product-requirement-document", "name": "ProductRequirementDocument" } } diff --git a/packages/host/app/commands/patch-card.ts b/packages/host/app/commands/patch-card.ts new file mode 100644 index 0000000000..073ac24cec --- /dev/null +++ b/packages/host/app/commands/patch-card.ts @@ -0,0 +1,66 @@ +import { service } from '@ember/service'; + +import { Command, baseRealm } from '@cardstack/runtime-common'; + +import { + RelationshipsSchema, + Schema, + generateJsonSchemaForCardType, +} from '@cardstack/runtime-common/helpers/ai'; + +import type { CardDef } from 'https://cardstack.com/base/card-api'; +import type * as CardAPI from 'https://cardstack.com/base/card-api'; +import type * as BaseCommandModule from 'https://cardstack.com/base/command'; + +// import type CardService from '../services/card-service'; +import type LoaderService from '../services/loader-service'; + +export default class PatchCardCommand extends Command< + BaseCommandModule.PatchCardInput, + undefined, + { cardType: typeof CardDef } +> { + // @service private declare cardService: CardService; + @service private declare loaderService: LoaderService; + + description = `Propose a patch to an existing card to change its contents. Any attributes specified will be fully replaced, return the minimum required to make the change. If a relationship field value is removed, set the self property of the specific item to null. When editing a relationship array, display the full array in the patch code. Ensure the description explains what change you are making.`; + + async getInputType() { + let commandModule = await this.loaderService.loader.import< + typeof BaseCommandModule + >(`${baseRealm.url}card-api`); + const { PatchCardInput } = commandModule; + return PatchCardInput; + } + + protected async run( + _input: BaseCommandModule.PatchCardInput, + ): Promise { + // await this.cardService.saveModel(this, input.card, input.realm); + // TODO: delegate to cardService patchCard incoporating OperatorModeStateService#patchCard + throw new Error('Not implemented'); + } + + async getInputJsonSchema( + cardApi: typeof CardAPI, + mappings: Map, + ): Promise<{ + attributes: Schema; + relationships: RelationshipsSchema; + }> { + let cardTypeToPatch = this.configuration.cardType; + let cardTypeToPatchSchema = generateJsonSchemaForCardType( + cardTypeToPatch, + cardApi, + mappings, + ); + let inputTypeSchema = generateJsonSchemaForCardType( + await this.getInputType(), + cardApi, + mappings, + ); + // TODO: merge cardTypeToPatchSchema into inputTypeSchema specifying the schema of the "patch" attribute + debugger; + return inputTypeSchema; + } +} diff --git a/packages/host/app/commands/save-card.ts b/packages/host/app/commands/save-card.ts new file mode 100644 index 0000000000..7eb6511222 --- /dev/null +++ b/packages/host/app/commands/save-card.ts @@ -0,0 +1,31 @@ +import { service } from '@ember/service'; + +import { Command, baseRealm } from '@cardstack/runtime-common'; + +import type * as BaseCommandModule from 'https://cardstack.com/base/command'; + +import type CardService from '../services/card-service'; +import type LoaderService from '../services/loader-service'; + +export default class SaveCardCommand extends Command< + BaseCommandModule.SaveCardInput, + undefined +> { + @service private declare cardService: CardService; + @service private declare loaderService: LoaderService; + + async getInputType() { + let commandModule = await this.loaderService.loader.import< + typeof BaseCommandModule + >(`${baseRealm.url}card-api`); + const { SaveCardInput } = commandModule; + return SaveCardInput; + } + + protected async run( + input: BaseCommandModule.SaveCardInput, + ): Promise { + // TODO: handle case where card is already saved and a different input.realm is provided + await this.cardService.saveModel(this, input.card, input.realm); + } +} diff --git a/packages/host/app/components/operator-mode/interact-submode.gts b/packages/host/app/components/operator-mode/interact-submode.gts index 8f36a3403c..7e00434efa 100644 --- a/packages/host/app/components/operator-mode/interact-submode.gts +++ b/packages/host/app/components/operator-mode/interact-submode.gts @@ -1,6 +1,7 @@ import { concat, fn } from '@ember/helper'; import { on } from '@ember/modifier'; import { action } from '@ember/object'; +import { getOwner } from '@ember/owner'; import { inject as service } from '@ember/service'; import { htmlSafe } from '@ember/template'; import { buildWaiter } from '@ember/test-waiters'; @@ -32,6 +33,8 @@ import { type Actions, type CodeRef, type LooseSingleCardDocument, + Command, + CommandContext, } from '@cardstack/runtime-common'; import { StackItem, isIndexCard } from '@cardstack/host/lib/stack-item'; @@ -40,7 +43,11 @@ import { stackBackgroundsResource } from '@cardstack/host/resources/stack-backgr import type MatrixService from '@cardstack/host/services/matrix-service'; -import { type CardDef, type Format } from 'https://cardstack.com/base/card-api'; +import type { + CardContext, + CardDef, + Format, +} from 'https://cardstack.com/base/card-api'; import CopyButton from './copy-button'; import DeleteModal from './delete-modal'; @@ -53,6 +60,7 @@ import type OperatorModeStateService from '../../services/operator-mode-state-se import type Realm from '../../services/realm'; import type { Submode } from '../submode-switcher'; +import { setOwner } from '@ember/owner'; const waiter = buildWaiter('operator-mode:interact-submode-waiter'); @@ -708,11 +716,52 @@ export default class InteractSubmode extends Component { openSearchCallback(); } + lookupCommand = < + CardInputType extends CardDef | undefined, + CardResultType extends CardDef | undefined, + CommandConfiguration, + >( + name: string, + ): Command => { + let owner = getOwner(this)!; + let commandFactory = owner.factoryFor(`command:${name}`); + if (!commandFactory) { + throw new Error(`Could not find command "${name}"`); + } + let CommandClass = commandFactory.class as unknown as { + new ( + commandContext: CommandContext, + ): Command; + }; + let instance = new CommandClass(this.commandContext) as Command< + CardInputType, + CardResultType, + CommandConfiguration + >; + setOwner(instance, owner); + return instance; + }; + + private get commandContext() { + return { + lookupCommand: this.lookupCommand, + sendAiAssistantMessage: this.matrixService.sendAiAssistantMessage, + }; + } + @provide(CardContextName) // @ts-ignore "cardContext is declared but not used" - private get cardContext() { + private get cardContext(): Omit< + CardContext, + 'prerenderedCardSearchComponent' + > { return { actions: this.publicAPI(this, 0), + // TODO: should we include this here?? + commandContext: { + lookupCommand: this.lookupCommand, + sendAiAssistantMessage: this.matrixService.sendAiAssistantMessage, + }, }; } @@ -772,6 +821,7 @@ export default class InteractSubmode extends Component { @stackItems={{stack}} @stackIndex={{stackIndex}} @publicAPI={{this.publicAPI this stackIndex}} + @commandContext={{this.commandContext}} @close={{perform this.close}} @onSelectedCards={{this.onSelectedCards}} @setupStackItem={{this.setupStackItem}} diff --git a/packages/host/app/components/operator-mode/stack-item.gts b/packages/host/app/components/operator-mode/stack-item.gts index e0473ff5ee..d0512c4d90 100644 --- a/packages/host/app/components/operator-mode/stack-item.gts +++ b/packages/host/app/components/operator-mode/stack-item.gts @@ -44,6 +44,7 @@ import { type Permissions, Deferred, cardTypeIcon, + CommandContext, } from '@cardstack/runtime-common'; import config from '@cardstack/host/config/environment'; @@ -51,6 +52,7 @@ import config from '@cardstack/host/config/environment'; import { type StackItem, isIndexCard } from '@cardstack/host/lib/stack-item'; import type { + CardContext, CardDef, Format, FieldType, @@ -72,6 +74,7 @@ interface Signature { stackItems: StackItem[]; index: number; publicAPI: Actions; + commandContext: CommandContext; close: (item: StackItem) => void; dismissStackedCardsAbove: (stackIndex: number) => void; onSelectedCards: ( @@ -214,10 +217,14 @@ export default class OperatorModeStackItem extends Component { return !this.isBuried; } - private get cardContext() { + private get cardContext(): Omit< + CardContext, + 'prerenderedCardSearchComponent' + > { return { cardComponentModifier: this.cardTracker.trackElement, actions: this.args.publicAPI, + commandContext: this.args.commandContext, }; } diff --git a/packages/host/app/components/operator-mode/stack.gts b/packages/host/app/components/operator-mode/stack.gts index 023bfd31d0..b39fe8992f 100644 --- a/packages/host/app/components/operator-mode/stack.gts +++ b/packages/host/app/components/operator-mode/stack.gts @@ -3,7 +3,7 @@ import Component from '@glimmer/component'; import { task } from 'ember-concurrency'; import perform from 'ember-concurrency/helpers/perform'; -import type { Actions } from '@cardstack/runtime-common'; +import type { Actions, CommandContext } from '@cardstack/runtime-common'; import type { StackItem } from '@cardstack/host/lib/stack-item'; @@ -16,6 +16,7 @@ interface Signature { stackItems: StackItem[]; stackIndex: number; publicAPI: Actions; + commandContext: CommandContext; close: (stackItem: StackItem) => void; onSelectedCards: ( selectedCards: CardDefOrId[], @@ -75,6 +76,7 @@ export default class OperatorModeStack extends Component { @index={{i}} @stackItems={{@stackItems}} @publicAPI={{@publicAPI}} + @commandContext={{@commandContext}} @dismissStackedCardsAbove={{perform this.dismissStackedCardsAbove}} @close={{@close}} @onSelectedCards={{@onSelectedCards}} diff --git a/packages/host/app/services/card-service.ts b/packages/host/app/services/card-service.ts index 1d43790d4c..672c1a906e 100644 --- a/packages/host/app/services/card-service.ts +++ b/packages/host/app/services/card-service.ts @@ -159,6 +159,8 @@ export default class CardService extends Service { async saveModel( owner: T, card: CardDef, + defaultRealmHref: string | undefined = this.realm.defaultWritableRealm + ?.path, ): Promise { let cardChanged = false; function onCardChange() { @@ -179,10 +181,10 @@ export default class CardService extends Service { // in the case where we get no realm URL from the card, we are dealing with // a new card instance that does not have a realm URL yet. if (!realmURL) { - if (!this.realm.defaultWritableRealm) { + if (!defaultRealmHref) { throw new Error('Could not find a writable realm'); } - realmURL = new URL(this.realm.defaultWritableRealm.path); + realmURL = new URL(defaultRealmHref); } let json = await this.saveCardDocument(doc, realmURL); let isNew = !card.id; diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 4d6aea0573..8de4d11e10 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -31,10 +31,11 @@ import { loaderFor, LooseCardResource, ResolvedCodeRef, + Command, } from '@cardstack/runtime-common'; import { basicMappings, - generateCardPatchCallSpecification, + generateJsonSchemaForCardType, getSearchTool, getGenerateAppModuleTool, } from '@cardstack/runtime-common/helpers/ai'; @@ -577,7 +578,7 @@ export default class MatrixService extends Service { ); // Generate tool calls for patching currently open cards permitted for modification for (let attachedOpenCard of attachedOpenCards) { - let patchSpec = generateCardPatchCallSpecification( + let patchSpec = generateJsonSchemaForCardType( attachedOpenCard.constructor as typeof CardDef, this.cardAPI, mappings, @@ -621,6 +622,63 @@ export default class MatrixService extends Service { } as CardMessageContent); } + public async sendAiAssistantMessage(params: { + roomId?: string; // if falsy we create a new room + show?: boolean; // if truthy, ensure the side panel to the room + prompt: string; + attachedCards?: CardDef[]; + skillCards?: SkillCard[]; + autoExecuteCommands?: Command[]; + }): Promise<{ roomId: string }> { + let roomId = params.roomId; + if (!roomId) { + roomId = await this.createRoom('AI Assistant', [aiBotUsername]); + } + + let html = markdownToHtml(params.prompt); + let mappings = await basicMappings(this.loaderService.loader); + let tools = []; + for (let command of params.autoExecuteCommands ?? []) { + tools.push({ + type: 'function', + function: { + name: command.name, + description: command.description, + parameters: await command.getInputJsonSchema(this.cardAPI, mappings), + }, + }); + } + + let attachedCardsEventIds = await this.getCardEventIds( + params.attachedCards ?? [], + roomId, + this.cardHashes, + { maybeRelativeURL: null }, + ); + let attachedSkillEventIds = await this.getCardEventIds( + params.skillCards ?? [], + roomId, + this.skillCardHashes, + { includeComputeds: true, maybeRelativeURL: null }, + ); + + await this.sendEvent(roomId, 'm.room.message', { + msgtype: 'org.boxel.message', + body: params.prompt || '', + format: 'org.matrix.custom.html', + formatted_body: html, + clientGeneratedId, + data: { + attachedCardsEventIds, + attachedSkillEventIds, + context: { + tools, + }, + }, + } as CardMessageContent); + return { roomId }; + } + private generateCardHashKey(roomId: string, card: LooseSingleCardDocument) { return md5(roomId + JSON.stringify(card)); } diff --git a/packages/host/tests/unit/ai-function-generation-test.ts b/packages/host/tests/unit/ai-function-generation-test.ts index 3125345128..9ff2edb0db 100644 --- a/packages/host/tests/unit/ai-function-generation-test.ts +++ b/packages/host/tests/unit/ai-function-generation-test.ts @@ -4,7 +4,7 @@ import { module, test } from 'qunit'; import { baseRealm } from '@cardstack/runtime-common'; import { - generateCardPatchCallSpecification, + generateJsonSchemaForCardType, basicMappings, type LinksToSchema, type RelationshipSchema, @@ -78,11 +78,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field bigIntegerField = contains(BigIntegerField); } - let schema = generateCardPatchCallSpecification( - BasicCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(BasicCard, cardApi, mappings); assert.deepEqual(schema, { attributes: { type: 'object', @@ -117,11 +113,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field containerField = contains(InternalField); } - let schema = generateCardPatchCallSpecification( - BasicCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(BasicCard, cardApi, mappings); const links: LinksToSchema['properties']['links'] = { type: 'object', properties: { @@ -177,11 +169,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field containerField = contains(InternalField); } - let schema = generateCardPatchCallSpecification( - TestCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(TestCard, cardApi, mappings); assert.deepEqual(schema, { attributes: { type: 'object', @@ -214,11 +202,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field linkedCard2 = linksTo(OtherCard); } - let schema = generateCardPatchCallSpecification( - TestCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(TestCard, cardApi, mappings); let attributes: ObjectSchema = { type: 'object', @@ -265,11 +249,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field linkedCards = linksToMany(OtherCard); } - let schema = generateCardPatchCallSpecification( - TestCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(TestCard, cardApi, mappings); let attributes: ObjectSchema = { type: 'object', @@ -326,11 +306,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field child = contains(ChildField); } - let schema = generateCardPatchCallSpecification( - ParentCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(ParentCard, cardApi, mappings); let attributes: ObjectSchema = { type: 'object', @@ -399,11 +375,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field traveler = contains(Traveler); } - let schema = generateCardPatchCallSpecification( - TripInfo, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(TripInfo, cardApi, mappings); const links: LinksToSchema['properties']['links'] = { type: 'object', properties: { @@ -478,11 +450,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field skipField = contains(NewField); } - let schema = generateCardPatchCallSpecification( - TestCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(TestCard, cardApi, mappings); assert.deepEqual(schema, { attributes: { type: 'object', @@ -509,11 +477,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field keepField = contains(NewField); } - let schema = generateCardPatchCallSpecification( - TestCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(TestCard, cardApi, mappings); assert.deepEqual(schema, { attributes: { type: 'object', @@ -544,11 +508,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field containingField = contains(ContainingField); } - let schema = generateCardPatchCallSpecification( - TestCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(TestCard, cardApi, mappings); assert.deepEqual(schema, { attributes: { @@ -581,11 +541,7 @@ module('Unit | ai-function-generation-test', function (hooks) { }); } - let schema = generateCardPatchCallSpecification( - BasicCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(BasicCard, cardApi, mappings); assert.deepEqual(schema, { attributes: { type: 'object', @@ -630,11 +586,7 @@ module('Unit | ai-function-generation-test', function (hooks) { }); } - let schema = generateCardPatchCallSpecification( - BasicCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(BasicCard, cardApi, mappings); const links: LinksToSchema['properties']['links'] = { type: 'object', properties: { @@ -714,11 +666,7 @@ module('Unit | ai-function-generation-test', function (hooks) { @field linkedCard2 = linksTo(OtherCard, { description: 'linked card' }); } - let schema = generateCardPatchCallSpecification( - TestCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(TestCard, cardApi, mappings); let attributes: ObjectSchema = { type: 'object', @@ -780,11 +728,7 @@ module('Unit | ai-function-generation-test', function (hooks) { }); } - let schema = generateCardPatchCallSpecification( - TestCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(TestCard, cardApi, mappings); let attributes: ObjectSchema = { type: 'object', @@ -834,11 +778,7 @@ module('Unit | ai-function-generation-test', function (hooks) { }); } - let schema = generateCardPatchCallSpecification( - BasicCard, - cardApi, - mappings, - ); + let schema = generateJsonSchemaForCardType(BasicCard, cardApi, mappings); assert.deepEqual(schema, { attributes: { type: 'object', diff --git a/packages/runtime-common/commands.ts b/packages/runtime-common/commands.ts index 230a0804c6..56888eab9d 100644 --- a/packages/runtime-common/commands.ts +++ b/packages/runtime-common/commands.ts @@ -1,7 +1,12 @@ import { Deferred } from './deferred'; +import type * as CardAPI from 'https://cardstack.com/base/card-api'; import { CardDef } from 'https://cardstack.com/base/card-api'; import { SkillCard } from 'https://cardstack.com/base/skill-card'; -// import { Schema } from './helpers/ai'; +import { + Schema, + generateJsonSchemaForCardType, + RelationshipsSchema, +} from './helpers/ai'; export interface CommandContext { lookupCommand< @@ -13,8 +18,8 @@ export interface CommandContext { ): Command; sendAiAssistantMessage: (params: { - sessionId?: string; // if falsy we create a new session - show?: boolean; // if truthy, ensure the side panel to the session + roomId?: string; // if falsy we create a new room + show?: boolean; // if truthy, ensure the side panel to the room prompt: string; attachedCards?: CardDef[]; skillCards?: SkillCard[]; @@ -54,7 +59,7 @@ export abstract class Command< CommandConfiguration extends any | undefined = undefined, > { // Is this actually type checking ? - abstract inputType: new () => NonNullable; + abstract getInputType(): Promise; invocations: CommandInvocation[] = []; @@ -99,8 +104,17 @@ export abstract class Command< return this.nextCompletionDeferred.promise; } - //TODO: figure out how to do this, and if here is the right place - // async getInputJsonSchema(): Promise { - // return await getJSONSchema(this.inputType); - // } + async getInputJsonSchema( + cardApi: typeof CardAPI, + mappings: Map, + ): Promise<{ + attributes: Schema; + relationships: RelationshipsSchema; + }> { + return generateJsonSchemaForCardType( + await this.getInputType(), + cardApi, + mappings, + ); + } } diff --git a/packages/runtime-common/helpers/ai.ts b/packages/runtime-common/helpers/ai.ts index 8ed51e9d3a..bb9cb16c26 100644 --- a/packages/runtime-common/helpers/ai.ts +++ b/packages/runtime-common/helpers/ai.ts @@ -180,7 +180,7 @@ function getPrimitiveType( * @param mappings - A map of field definitions to JSON schema * @returns The generated patch call specification as JSON schema */ -function generatePatchCallSpecification( +function generateJsonSchemaForContainsFields( def: typeof CardAPI.BaseDef, cardApi: typeof CardAPI, mappings: Map, @@ -212,7 +212,7 @@ function generatePatchCallSpecification( continue; } - let fieldSchemaForSingleItem = generatePatchCallSpecification( + let fieldSchemaForSingleItem = generateJsonSchemaForContainsFields( field.card, cardApi, mappings, @@ -244,7 +244,7 @@ type RelationshipFieldInfo = { description?: string; }; -function generatePatchCallRelationshipsSpecification( +function generateJsonSchemaForLinksToFields( def: typeof CardAPI.BaseDef, cardApi: typeof CardAPI, ): RelationshipsSchema | undefined { @@ -335,14 +335,12 @@ function generateRelationshipFieldsInfo( * @param mappings - A map of field definitions to JSON schema * @returns The generated patch call specification as JSON schema */ -export function generateCardPatchCallSpecification( +export function generateJsonSchemaForCardType( def: typeof CardAPI.CardDef, cardApi: typeof CardAPI, mappings: Map, -): - | { attributes: Schema } - | { attributes: Schema; relationships: RelationshipsSchema } { - let schema = generatePatchCallSpecification(def, cardApi, mappings) as +): { attributes: Schema; relationships: RelationshipsSchema } { + let schema = generateJsonSchemaForContainsFields(def, cardApi, mappings) as | Schema | undefined; if (schema == undefined) { @@ -351,18 +349,23 @@ export function generateCardPatchCallSpecification( type: 'object', properties: {}, }, + relationships: { + type: 'object', + properties: {}, + required: [], + }, }; } else { - let relationships = generatePatchCallRelationshipsSpecification( - def, - cardApi, - ); + let relationships = generateJsonSchemaForLinksToFields(def, cardApi); if ( !relationships || !('required' in relationships) || !relationships.required.length ) { - return { attributes: schema }; + return { + attributes: schema, + relationships: { type: 'object', properties: {}, required: [] }, + }; } return { attributes: schema, From 1703c755de61cd0c7d6b66b2ed1d29b3bdb133bd Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 5 Nov 2024 13:03:55 +0000 Subject: [PATCH 03/40] Getting up to returning a patchcardcommand function call --- packages/ai-bot/helpers.ts | 7 ++++++- packages/ai-bot/main.ts | 20 +++++++++++++++++++ .../catalog-realm/AiAppGenerator/commands.ts | 6 ++++-- packages/catalog-realm/ai-app-generator.gts | 7 ++++--- packages/host/app/commands/patch-card.ts | 6 +----- .../operator-mode/interact-submode.gts | 19 +++++++++--------- packages/host/app/services/matrix-service.ts | 10 +++++++++- 7 files changed, 53 insertions(+), 22 deletions(-) diff --git a/packages/ai-bot/helpers.ts b/packages/ai-bot/helpers.ts index d6907ef583..ab574e43f1 100644 --- a/packages/ai-bot/helpers.ts +++ b/packages/ai-bot/helpers.ts @@ -246,6 +246,7 @@ export function getTools( history: DiscreteMatrixEvent[], aiBotUserId: string, ): Tool[] { + // TODO: there should be no default tools defined in the ai-bot, tools must be determined by the host let searchTool = getSearchTool(); let tools = [searchTool]; // Just get the users messages @@ -426,7 +427,7 @@ export function getModifyPrompt( mostRecentlyAttachedCard, attachedCards, ); - + console.log('skillCards', skillCards); if (skillCards.length) { systemMessage += SKILL_INSTRUCTIONS_MESSAGE; systemMessage += skillCardsToMessage(skillCards); @@ -467,6 +468,10 @@ export const attachedCardsToMessage = ( }; export const skillCardsToMessage = (cards: CardResource[]) => { + for (let card of cards) { + console.log('card', card); + console.log('card.attributes', card.attributes); + } return `${JSON.stringify( cards.map((card) => card.attributes?.instructions), )}`; diff --git a/packages/ai-bot/main.ts b/packages/ai-bot/main.ts index 8f404a751b..96a73f6b6c 100644 --- a/packages/ai-bot/main.ts +++ b/packages/ai-bot/main.ts @@ -24,9 +24,15 @@ import { handleDebugCommands } from './lib/debug'; import { MatrixClient } from './lib/matrix'; import type { MatrixEvent as DiscreteMatrixEvent } from 'https://cardstack.com/base/matrix-event'; import * as Sentry from '@sentry/node'; +import fs from 'fs'; let log = logger('ai-bot'); +// Ensure logs directory exists +if (!fs.existsSync('ai-bot-logs')) { + fs.mkdirSync('ai-bot-logs'); +} + class Assistant { private openai: OpenAI; private client: MatrixClient; @@ -41,6 +47,20 @@ class Assistant { getResponse(history: DiscreteMatrixEvent[]) { let tools = getTools(history, this.id); let messages = getModifyPrompt(history, this.id, tools); + + // Write out tools and messages to a log file + const fs = require('fs'); + const logData = { + timestamp: new Date().toISOString(), + tools, + messages, + }; + + fs.writeFileSync( + `ai-bot-logs/${Date.now()}-prompt.json`, + JSON.stringify(logData, null, 2), + ); + if (tools.length === 0) { return this.openai.beta.chat.completions.stream({ model: 'gpt-4o', diff --git a/packages/catalog-realm/AiAppGenerator/commands.ts b/packages/catalog-realm/AiAppGenerator/commands.ts index 69e8226453..d70e52fc4e 100644 --- a/packages/catalog-realm/AiAppGenerator/commands.ts +++ b/packages/catalog-realm/AiAppGenerator/commands.ts @@ -38,11 +38,12 @@ export class CreateProductRequirementsInstance extends Command< get skillCard() { return new SkillCard({ + id: 'SkillCard1', name: 'PRD Helper', description: 'This skill card can be used to help with creating product requirements', instructions: - 'You are a helpful assistant that can help with creating product requirements, etc. Use patchCard to apply changes, do not ask, just do it, etc.', + 'You are a helpful assistant that can help with creating product requirements, etc. You *MUST* make the patchCard function call', }); } @@ -53,6 +54,7 @@ export class CreateProductRequirementsInstance extends Command< protected async run( input: CreateProductRequirementsInput, ): Promise { + console.log('Input into the run', input); // Create new card let prdCard = new ProductRequirementDocument(); @@ -73,7 +75,7 @@ export class CreateProductRequirementsInstance extends Command< PatchCardInput, undefined, ProductRequirementDocument - >('patch-card'); + >('patch-card', { cardType: ProductRequirementDocument }); // This should return a session ID so that we can potentially send followup messages // This should delegate to a matrix service method. Besides actually sending the message, diff --git a/packages/catalog-realm/ai-app-generator.gts b/packages/catalog-realm/ai-app-generator.gts index a8a126d2a9..896ac0b0ff 100644 --- a/packages/catalog-realm/ai-app-generator.gts +++ b/packages/catalog-realm/ai-app-generator.gts @@ -354,6 +354,7 @@ class DashboardTab extends GlimmerComponent<{ @tracked isGenerating = false; @action setPrompt(key: string, value: string) { + console.log('setPrompt', key, value); this.prompt = { ...this.prompt, [key]: value }; } @@ -367,9 +368,9 @@ class DashboardTab extends GlimmerComponent<{ try { await command.execute( new CreateProductRequirementsInput({ - appType: this.prompt.appType, - domain: this.prompt.domain, - customRequirements: this.prompt.customRequirements, + productDescription: this.prompt.appType, + targetAudience: this.prompt.domain, + features: this.prompt.customRequirements, realm: this.args.currentRealm, }), ); diff --git a/packages/host/app/commands/patch-card.ts b/packages/host/app/commands/patch-card.ts index 073ac24cec..37fee0fc02 100644 --- a/packages/host/app/commands/patch-card.ts +++ b/packages/host/app/commands/patch-card.ts @@ -54,11 +54,7 @@ export default class PatchCardCommand extends Command< cardApi, mappings, ); - let inputTypeSchema = generateJsonSchemaForCardType( - await this.getInputType(), - cardApi, - mappings, - ); + return cardTypeToPatchSchema; // TODO: merge cardTypeToPatchSchema into inputTypeSchema specifying the schema of the "patch" attribute debugger; return inputTypeSchema; diff --git a/packages/host/app/components/operator-mode/interact-submode.gts b/packages/host/app/components/operator-mode/interact-submode.gts index 7e00434efa..7c02919ca3 100644 --- a/packages/host/app/components/operator-mode/interact-submode.gts +++ b/packages/host/app/components/operator-mode/interact-submode.gts @@ -722,6 +722,7 @@ export default class InteractSubmode extends Component { CommandConfiguration, >( name: string, + configuration: CommandConfiguration = undefined, ): Command => { let owner = getOwner(this)!; let commandFactory = owner.factoryFor(`command:${name}`); @@ -733,11 +734,10 @@ export default class InteractSubmode extends Component { commandContext: CommandContext, ): Command; }; - let instance = new CommandClass(this.commandContext) as Command< - CardInputType, - CardResultType, - CommandConfiguration - >; + let instance = new CommandClass( + this.commandContext, + configuration, + ) as Command; setOwner(instance, owner); return instance; }; @@ -745,7 +745,9 @@ export default class InteractSubmode extends Component { private get commandContext() { return { lookupCommand: this.lookupCommand, - sendAiAssistantMessage: this.matrixService.sendAiAssistantMessage, + sendAiAssistantMessage: ( + ...args: Parameters + ) => this.matrixService.sendAiAssistantMessage(...args), }; } @@ -758,10 +760,7 @@ export default class InteractSubmode extends Component { return { actions: this.publicAPI(this, 0), // TODO: should we include this here?? - commandContext: { - lookupCommand: this.lookupCommand, - sendAiAssistantMessage: this.matrixService.sendAiAssistantMessage, - }, + commandContext: this.commandContext, }; } diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 8de4d11e10..eb81adafa9 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -644,7 +644,13 @@ export default class MatrixService extends Service { function: { name: command.name, description: command.description, - parameters: await command.getInputJsonSchema(this.cardAPI, mappings), + parameters: { + type: 'object', + properties: await command.getInputJsonSchema( + this.cardAPI, + mappings, + ), + }, }, }); } @@ -662,6 +668,8 @@ export default class MatrixService extends Service { { includeComputeds: true, maybeRelativeURL: null }, ); + let clientGeneratedId = uuidv4(); + await this.sendEvent(roomId, 'm.room.message', { msgtype: 'org.boxel.message', body: params.prompt || '', From f8e5e71c1cbde115618a223963c97c6c19b598dd Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 5 Nov 2024 17:30:14 +0000 Subject: [PATCH 04/40] Track registered commands --- .../catalog-realm/AiAppGenerator/commands.ts | 4 +-- packages/host/app/services/command-service.ts | 33 ++++++++++++++++++- packages/host/app/services/matrix-service.ts | 11 +++++-- packages/runtime-common/commands.ts | 2 +- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/catalog-realm/AiAppGenerator/commands.ts b/packages/catalog-realm/AiAppGenerator/commands.ts index d70e52fc4e..24f77c917e 100644 --- a/packages/catalog-realm/AiAppGenerator/commands.ts +++ b/packages/catalog-realm/AiAppGenerator/commands.ts @@ -91,7 +91,7 @@ export class CreateProductRequirementsInstance extends Command< prompt: this.createPrompt(input), attachedCards: [prdCard], skillCards: [this.skillCard], - autoExecuteCommands: [patchPRDCommand], // this should persist over multiple messages, matrix service is responsible to tracking whic + commands: [{ command: patchPRDCommand, autoExecute: true }], // this should persist over multiple messages, matrix service is responsible to tracking whic }); // Wait for the PRD command to have been applied @@ -151,7 +151,7 @@ export class GenerateCodeFromPRDCommand extends Command< prompt: this.createPrompt(input.productRequirements), attachedCards: [input.productRequirements], skillCards: [this.skillCard], - autoExecuteCommands: [createModuleCommand], + commands: [{ command: createModuleCommand, autoExecute: true }], }); // Wait for the module to be created diff --git a/packages/host/app/services/command-service.ts b/packages/host/app/services/command-service.ts index 95bdb2a707..74500f3997 100644 --- a/packages/host/app/services/command-service.ts +++ b/packages/host/app/services/command-service.ts @@ -5,6 +5,7 @@ import { task } from 'ember-concurrency'; import flatMap from 'lodash/flatMap'; import { + Command, type LooseSingleCardDocument, type PatchData, baseRealm, @@ -37,13 +38,43 @@ export default class CommandService extends Service { @service private declare realm: Realm; @service private declare realmServer: RealmServerService; + private commandNonce = 0; + private commands: Map< + string, + { command: Command; autoExecute: boolean } + > = new Map(); + + public registerCommand( + command: Command, + autoExecute: boolean, + ) { + console.log('registerCommand', command.name); + let name = `${command.name}_${this.commandNonce++}`; + this.commands.set(name, { command, autoExecute }); + return name; + } + //TODO: Convert to non-EC async method after fixing CS-6987 run = task(async (command: CommandCard, roomId: string) => { let { payload, eventId } = command; let res: any; try { this.matrixService.failedCommandState.delete(eventId); - if (command.name === 'patchCard') { + + // lookup command + let { command: commandToRun } = this.commands.get(command.name) ?? {}; + + console.log('run', command.name, commandToRun); + + if (commandToRun) { + // Get the input type and validate/construct the payload + let InputType = await commandToRun.getInputType(); + // Construct a new instance of the input type with the payload + // Here the input type is undefined? + debugger; + let typedInput = new InputType(payload); + res = await commandToRun.execute(typedInput); + } else if (command.name === 'patchCard') { if (!hasPatchData(payload)) { throw new Error( "Patch command can't run because it doesn't have all the fields in arguments returned by open ai", diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index eb81adafa9..c9313287db 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -77,6 +77,7 @@ import { RoomResource, getRoom } from '../resources/room'; import { type SerializedState as OperatorModeSerializedState } from './operator-mode-state-service'; import type CardService from './card-service'; +import type CommandService from './command-service'; import type LoaderService from './loader-service'; import type MatrixSDKLoader from './matrix-sdk-loader'; import type { ExtendedClient, ExtendedMatrixSDK } from './matrix-sdk-loader'; @@ -105,6 +106,7 @@ export type OperatorModeContext = { export default class MatrixService extends Service { @service private declare loaderService: LoaderService; @service private declare cardService: CardService; + @service private declare commandService: CommandService; @service private declare realm: RealmService; @service private declare matrixSdkLoader: MatrixSDKLoader; @service private declare realmServer: RealmServerService; @@ -117,6 +119,7 @@ export default class MatrixService extends Service { profile = getMatrixProfile(this, () => this.client.getUserId()); private rooms: TrackedMap = new TrackedMap(); + roomResourcesCache: TrackedMap = new TrackedMap(); messagesToSend: TrackedMap = new TrackedMap(); cardsToSend: TrackedMap = new TrackedMap(); @@ -628,7 +631,7 @@ export default class MatrixService extends Service { prompt: string; attachedCards?: CardDef[]; skillCards?: SkillCard[]; - autoExecuteCommands?: Command[]; + commands?: { command: Command; autoExecute: boolean }[]; }): Promise<{ roomId: string }> { let roomId = params.roomId; if (!roomId) { @@ -638,11 +641,13 @@ export default class MatrixService extends Service { let html = markdownToHtml(params.prompt); let mappings = await basicMappings(this.loaderService.loader); let tools = []; - for (let command of params.autoExecuteCommands ?? []) { + for (let { command, autoExecute } of params.commands ?? []) { + // get a registered name for the command + let name = this.commandService.registerCommand(command, autoExecute); tools.push({ type: 'function', function: { - name: command.name, + name, description: command.description, parameters: { type: 'object', diff --git a/packages/runtime-common/commands.ts b/packages/runtime-common/commands.ts index 56888eab9d..11e9ab7792 100644 --- a/packages/runtime-common/commands.ts +++ b/packages/runtime-common/commands.ts @@ -23,7 +23,7 @@ export interface CommandContext { prompt: string; attachedCards?: CardDef[]; skillCards?: SkillCard[]; - autoExecuteCommands?: Command[]; + commands?: { command: Command; autoExecute: boolean }[]; }) => Promise<{ sessionId: string }>; } From e7280667fa969bbb8200cf70cfb43e13433814ea Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 5 Nov 2024 18:51:59 -0500 Subject: [PATCH 05/40] WIP Add tests for sending commands to AI Assistant and having them become tools --- packages/base/command.gts | 4 + packages/host/app/commands/patch-card.ts | 5 +- packages/host/app/commands/save-card.ts | 2 +- packages/host/app/commands/switch-submode.ts | 63 +++ .../host/app/components/submode-switcher.gts | 2 +- packages/host/app/services/command-service.ts | 4 +- packages/host/app/services/matrix-service.ts | 12 +- .../host/tests/acceptance/commands-test.gts | 324 ++++++++++++++++ packages/host/tests/cards/person.gts | 21 + .../helpers/mock-matrix/_server-state.ts | 2 +- .../host/tests/helpers/mock-matrix/_utils.ts | 3 + packages/matrix/helpers/index.ts | 32 +- packages/matrix/tests/commands.spec.ts | 365 ++++++++++++++++++ packages/matrix/tests/messages.spec.ts | 286 +------------- 14 files changed, 816 insertions(+), 309 deletions(-) create mode 100644 packages/host/app/commands/switch-submode.ts create mode 100644 packages/host/tests/acceptance/commands-test.gts create mode 100644 packages/matrix/tests/commands.spec.ts diff --git a/packages/base/command.gts b/packages/base/command.gts index a628b9d7e7..e4085e02c9 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -67,6 +67,10 @@ export class ShowCardInput extends CardDef { @field placement = contains(StringField); // TODO: nicer if enum, likely need to specify stackIndex too? } +export class SwitchSubmodeInput extends CardDef { + @field submode = contains(StringField); +} + export class CreateModuleInput extends CardDef { @field code = contains(StringField); @field realm = contains(StringField); diff --git a/packages/host/app/commands/patch-card.ts b/packages/host/app/commands/patch-card.ts index 37fee0fc02..dbc1d21f2a 100644 --- a/packages/host/app/commands/patch-card.ts +++ b/packages/host/app/commands/patch-card.ts @@ -28,7 +28,7 @@ export default class PatchCardCommand extends Command< async getInputType() { let commandModule = await this.loaderService.loader.import< typeof BaseCommandModule - >(`${baseRealm.url}card-api`); + >(`${baseRealm.url}command`); const { PatchCardInput } = commandModule; return PatchCardInput; } @@ -56,7 +56,6 @@ export default class PatchCardCommand extends Command< ); return cardTypeToPatchSchema; // TODO: merge cardTypeToPatchSchema into inputTypeSchema specifying the schema of the "patch" attribute - debugger; - return inputTypeSchema; + // return inputTypeSchema; } } diff --git a/packages/host/app/commands/save-card.ts b/packages/host/app/commands/save-card.ts index 7eb6511222..612067b83c 100644 --- a/packages/host/app/commands/save-card.ts +++ b/packages/host/app/commands/save-card.ts @@ -17,7 +17,7 @@ export default class SaveCardCommand extends Command< async getInputType() { let commandModule = await this.loaderService.loader.import< typeof BaseCommandModule - >(`${baseRealm.url}card-api`); + >(`${baseRealm.url}command`); const { SaveCardInput } = commandModule; return SaveCardInput; } diff --git a/packages/host/app/commands/switch-submode.ts b/packages/host/app/commands/switch-submode.ts new file mode 100644 index 0000000000..eb6b730a09 --- /dev/null +++ b/packages/host/app/commands/switch-submode.ts @@ -0,0 +1,63 @@ +import { service } from '@ember/service'; + +import { Command, baseRealm } from '@cardstack/runtime-common'; + +import { CardDef } from 'https://cardstack.com/base/card-api'; +import type * as BaseCommandModule from 'https://cardstack.com/base/command'; + +import { Submodes } from '../components/submode-switcher'; + +import type LoaderService from '../services/loader-service'; +import type OperatorModeStateService from '../services/operator-mode-state-service'; + +export default class SwitchSubmodeCommand extends Command< + BaseCommandModule.SwitchSubmodeInput, + undefined +> { + @service private declare operatorModeStateService: OperatorModeStateService; + @service private declare loaderService: LoaderService; + + description = + 'Navigate the UI to another submode. Possible values for submode are "interact" and "code".'; + + async getInputType() { + let commandModule = await this.loaderService.loader.import< + typeof BaseCommandModule + >(`${baseRealm.url}command`); + const { SwitchSubmodeInput } = commandModule; + return SwitchSubmodeInput; + } + + private get allStackItems() { + return this.operatorModeStateService.state?.stacks.flat() ?? []; + } + + private get lastCardInRightMostStack(): CardDef | null { + if (this.allStackItems.length <= 0) { + return null; + } + + return this.allStackItems[this.allStackItems.length - 1].card; + } + + protected async run( + input: BaseCommandModule.SwitchSubmodeInput, + ): Promise { + switch (input.submode) { + case Submodes.Interact: + this.operatorModeStateService.updateCodePath(null); + break; + case Submodes.Code: + this.operatorModeStateService.updateCodePath( + this.lastCardInRightMostStack + ? new URL(this.lastCardInRightMostStack.id + '.json') + : null, + ); + break; + default: + throw new Error(`invalid submode specified: ${input.submode}`); + } + + this.operatorModeStateService.updateSubmode(input.submode); + } +} diff --git a/packages/host/app/components/submode-switcher.gts b/packages/host/app/components/submode-switcher.gts index 3fd24e0eeb..5aae5885c3 100644 --- a/packages/host/app/components/submode-switcher.gts +++ b/packages/host/app/components/submode-switcher.gts @@ -36,7 +36,7 @@ interface Signature { export default class SubmodeSwitcher extends Component { diff --git a/packages/host/tests/cards/person.gts b/packages/host/tests/cards/person.gts index cdd2381261..499df65f63 100644 --- a/packages/host/tests/cards/person.gts +++ b/packages/host/tests/cards/person.gts @@ -1,4 +1,5 @@ import { on } from '@ember/modifier'; + import { contains, field, From f961bedaba6ce1968c7b95bf9fddb464bf2f34f6 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 12 Nov 2024 16:46:58 +0000 Subject: [PATCH 16/40] Smaller test setup for commands, add basic create module command --- packages/host/app/commands/create-module.ts | 43 +++++++++++++++ packages/host/app/commands/patch-card.ts | 1 + .../operator-mode/interact-submode.gts | 45 ++-------------- packages/host/app/services/command-service.ts | 49 ++++++++++++++++- packages/host/tests/helpers/index.gts | 6 +++ .../commands/switch-submode-test.gts | 54 +++++++++++++++++++ 6 files changed, 156 insertions(+), 42 deletions(-) create mode 100644 packages/host/app/commands/create-module.ts create mode 100644 packages/host/tests/integration/commands/switch-submode-test.gts diff --git a/packages/host/app/commands/create-module.ts b/packages/host/app/commands/create-module.ts new file mode 100644 index 0000000000..819eb633d7 --- /dev/null +++ b/packages/host/app/commands/create-module.ts @@ -0,0 +1,43 @@ +import { service } from '@ember/service'; + +import { Command, baseRealm } from '@cardstack/runtime-common'; + +import type { CardDef } from 'https://cardstack.com/base/card-api'; +import type * as BaseCommandModule from 'https://cardstack.com/base/command'; + +import type CardService from '../services/card-service'; + +import type LoaderService from '../services/loader-service'; + +export default class CreateModuleCommand extends Command< + BaseCommandModule.CreateModuleInput, + undefined, + { cardType: typeof CardDef } +> { + @service private declare cardService: CardService; + @service private declare loaderService: LoaderService; + + description = `Create a new module, errors if there is an existing module with the same name.`; + + async getInputType() { + let commandModule = await this.loaderService.loader.import< + typeof BaseCommandModule + >(`${baseRealm.url}command`); + const { CreateModuleInput } = commandModule; + // Can we define one on the fly? + return CreateModuleInput; + } + + protected async run( + input: BaseCommandModule.CreateModuleInput, + ): Promise { + // TODO: check if currently exists using authedFetch + await this.cardService.saveSource( + new URL(input.modulePath, input.realm), + input.code, + ); + // We have no return here, we need to return a reference to the module created + // and potentially also identify exported cards from it + // We should also return error messages if any + } +} diff --git a/packages/host/app/commands/patch-card.ts b/packages/host/app/commands/patch-card.ts index 1d36069d21..2a11818e9f 100644 --- a/packages/host/app/commands/patch-card.ts +++ b/packages/host/app/commands/patch-card.ts @@ -42,6 +42,7 @@ export default class PatchCardCommand extends Command< ): Promise { console.log('input', input); if (!input.cardId || !input.patch) { + console.log(input.cardId, input.patch); throw new Error( "Patch command can't run because it doesn't have all the fields in arguments returned by open ai", ); diff --git a/packages/host/app/components/operator-mode/interact-submode.gts b/packages/host/app/components/operator-mode/interact-submode.gts index f790c21c06..69e9362474 100644 --- a/packages/host/app/components/operator-mode/interact-submode.gts +++ b/packages/host/app/components/operator-mode/interact-submode.gts @@ -57,6 +57,7 @@ import { CardDefOrId } from './stack-item'; import SubmodeLayout from './submode-layout'; import type CardService from '../../services/card-service'; +import type CommandService from '../../services/command-service'; import type OperatorModeStateService from '../../services/operator-mode-state-service'; import type Realm from '../../services/realm'; @@ -153,6 +154,7 @@ interface Signature { export default class InteractSubmode extends Component { @service private declare cardService: CardService; + @service private declare commandService: CommandService; @service private declare matrixService: MatrixService; @service private declare operatorModeStateService: OperatorModeStateService; @service private declare realm: Realm; @@ -672,45 +674,6 @@ export default class InteractSubmode extends Component { openSearchCallback(); } - //TODO use create[CommandName] methods to create commands instead of lookupCommand to solve type issues and avoid embroider issues - // OR - //TODO use imports and leverage with custom loader maybe import SaveCard from 'http://cardstack.com/host/commannds/save-card' - lookupCommand = < - CardInputType extends CardDef | undefined, - CardResultType extends CardDef | undefined, - CommandConfiguration extends any | undefined, - >( - name: string, - configuration: CommandConfiguration | undefined, - ): Command => { - let owner = getOwner(this)!; - let commandFactory = owner.factoryFor(`command:${name}`); - if (!commandFactory) { - throw new Error(`Could not find command "${name}"`); - } - let CommandClass = commandFactory.class as unknown as { - new ( - commandContext: CommandContext, - commandConfiguration: CommandConfiguration | undefined, - ): Command; - }; - let instance = new CommandClass( - this.commandContext, - configuration, - ) as Command; - setOwner(instance, owner); - return instance; - }; - - private get commandContext(): CommandContext { - return { - lookupCommand: this.lookupCommand, - sendAiAssistantMessage: ( - ...args: Parameters - ) => this.matrixService.sendAiAssistantMessage(...args), - }; - } - @provide(CardContextName) // @ts-ignore "cardContext is declared but not used" private get cardContext(): Omit< @@ -720,7 +683,7 @@ export default class InteractSubmode extends Component { return { actions: this.publicAPI(this, 0), // TODO: should we include this here?? - commandContext: this.commandContext, + commandContext: this.commandService.commandContext, }; } @@ -780,7 +743,7 @@ export default class InteractSubmode extends Component { @stackItems={{stack}} @stackIndex={{stackIndex}} @publicAPI={{this.publicAPI this stackIndex}} - @commandContext={{this.commandContext}} + @commandContext={{this.commandService.commandContext}} @close={{perform this.close}} @onSelectedCards={{this.onSelectedCards}} @setupStackItem={{this.setupStackItem}} diff --git a/packages/host/app/services/command-service.ts b/packages/host/app/services/command-service.ts index ba81b2d78f..ac868f9869 100644 --- a/packages/host/app/services/command-service.ts +++ b/packages/host/app/services/command-service.ts @@ -1,4 +1,5 @@ import Service, { service } from '@ember/service'; +import { getOwner, setOwner } from '@ember/owner'; import { task } from 'ember-concurrency'; @@ -11,6 +12,7 @@ import { type LooseSingleCardDocument, type PatchData, baseRealm, + CommandContext, } from '@cardstack/runtime-common'; import { type CardTypeFilter, @@ -84,6 +86,46 @@ export default class CommandService extends Service { ); } + //TODO use create[CommandName] methods to create commands instead of lookupCommand to solve type issues and avoid embroider issues + // OR + //TODO use imports and leverage with custom loader maybe import SaveCard from 'http://cardstack.com/host/commannds/save-card' + + lookupCommand = < + CardInputType extends CardDef | undefined, + CardResultType extends CardDef | undefined, + CommandConfiguration extends any | undefined, + >( + name: string, + configuration: CommandConfiguration | undefined, + ): Command => { + let owner = getOwner(this)!; + let commandFactory = owner.factoryFor(`command:${name}`); + if (!commandFactory) { + throw new Error(`Could not find command "${name}"`); + } + let CommandClass = commandFactory.class as unknown as { + new ( + commandContext: CommandContext, + commandConfiguration: CommandConfiguration | undefined, + ): Command; + }; + let instance = new CommandClass( + this.commandContext, + configuration, + ) as Command; + setOwner(instance, owner); + return instance; + }; + + get commandContext(): CommandContext { + return { + lookupCommand: this.lookupCommand, + sendAiAssistantMessage: ( + ...args: Parameters + ) => this.matrixService.sendAiAssistantMessage(...args), + }; + } + //TODO: Convert to non-EC async method after fixing CS-6987 run = task(async (command: CommandCard, roomId: string) => { let { payload, eventId } = command; @@ -100,7 +142,12 @@ export default class CommandService extends Service { // Get the input type and validate/construct the payload let InputType = await commandToRun.getInputType(); // Construct a new instance of the input type with the payload - let typedInput = new InputType(payload); + console.log('payload', payload); + let typedInput = new InputType({ + ...payload.attributes, + ...payload.relationships, + }); + console.log('typedInput', typedInput); res = await commandToRun.execute(typedInput); } else if (command.name === 'patchCard') { if (!hasPatchData(payload)) { diff --git a/packages/host/tests/helpers/index.gts b/packages/host/tests/helpers/index.gts index 53d40d30c5..ed48685a14 100644 --- a/packages/host/tests/helpers/index.gts +++ b/packages/host/tests/helpers/index.gts @@ -43,6 +43,7 @@ import SQLiteAdapter from '@cardstack/host/lib/sqlite-adapter'; import type CardService from '@cardstack/host/services/card-service'; import type { CardSaveSubscriber } from '@cardstack/host/services/card-service'; +import type CommandService from '@cardstack/host/services/command-service'; import type LoaderService from '@cardstack/host/services/loader-service'; import type MessageService from '@cardstack/host/services/message-service'; import type NetworkService from '@cardstack/host/services/network'; @@ -465,6 +466,11 @@ export function lookupLoaderService(): LoaderService { return owner.lookup('service:loader-service') as LoaderService; } +export function lookupService(name: string): T { + let owner = (getContext() as TestContext).owner; + return owner.lookup(`service:${name}`) as T; +} + export function lookupNetworkService(): NetworkService { let owner = (getContext() as TestContext).owner; return owner.lookup('service:network') as NetworkService; diff --git a/packages/host/tests/integration/commands/switch-submode-test.gts b/packages/host/tests/integration/commands/switch-submode-test.gts new file mode 100644 index 0000000000..5bb4a618d1 --- /dev/null +++ b/packages/host/tests/integration/commands/switch-submode-test.gts @@ -0,0 +1,54 @@ +import { RenderingTestContext } from '@ember/test-helpers'; + +import { module, test } from 'qunit'; + +import { Loader } from '@cardstack/runtime-common'; +import type CommandService from '@cardstack/host/services/command-service'; +import type OperatorModeStateService from '@cardstack/host/services/operator-mode-state-service'; + +import { + setupIntegrationTestRealm, + setupLocalIndexing, + lookupLoaderService, + lookupService, +} from '../../helpers'; +import { setupRenderingTest } from '../../helpers/setup'; + +import type { SwitchSubmodeInput } from 'https://cardstack.com/base/command'; + +let loader: Loader; + +module('Integration | commands | switch-submode', function (hooks) { + setupRenderingTest(hooks); + setupLocalIndexing(hooks); + + hooks.beforeEach(function (this: RenderingTestContext) { + loader = lookupLoaderService().loader; + }); + + hooks.beforeEach(async function () { + await setupIntegrationTestRealm({ + loader, + contents: {}, + }); + }); + + test('switch to code submode', async function (assert) { + let commandService = lookupService('command-service'); + let operatorModeStateService = lookupService( + 'operator-mode-state-service', + ); + let switchSubmodeCommand = commandService.lookupCommand< + SwitchSubmodeInput, + undefined, + undefined + >('switch-submode', undefined); + const InputType = await switchSubmodeCommand.getInputType(); + let input = new InputType({ + submode: 'code', + }); + assert.strictEqual(operatorModeStateService.state?.submode, 'interact'); + await switchSubmodeCommand.execute(input); + assert.strictEqual(operatorModeStateService.state?.submode, 'code'); + }); +}); From 9006a8f40af142b924fa0c3c101051c15f453f67 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 12 Nov 2024 12:33:54 -0500 Subject: [PATCH 17/40] Lint fixes --- .../host/app/components/operator-mode/interact-submode.gts | 4 ---- packages/host/app/services/command-service.ts | 2 +- packages/host/tests/helpers/index.gts | 1 - .../host/tests/integration/commands/switch-submode-test.gts | 5 +++-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/host/app/components/operator-mode/interact-submode.gts b/packages/host/app/components/operator-mode/interact-submode.gts index 69e9362474..432d476b20 100644 --- a/packages/host/app/components/operator-mode/interact-submode.gts +++ b/packages/host/app/components/operator-mode/interact-submode.gts @@ -1,8 +1,6 @@ import { concat, fn } from '@ember/helper'; import { on } from '@ember/modifier'; import { action } from '@ember/object'; -import { getOwner } from '@ember/owner'; -import { setOwner } from '@ember/owner'; import { inject as service } from '@ember/service'; import { htmlSafe } from '@ember/template'; import { buildWaiter } from '@ember/test-waiters'; @@ -34,8 +32,6 @@ import { type Actions, type CodeRef, type LooseSingleCardDocument, - Command, - CommandContext, } from '@cardstack/runtime-common'; import { StackItem, isIndexCard } from '@cardstack/host/lib/stack-item'; diff --git a/packages/host/app/services/command-service.ts b/packages/host/app/services/command-service.ts index ac868f9869..8cb1a1ef96 100644 --- a/packages/host/app/services/command-service.ts +++ b/packages/host/app/services/command-service.ts @@ -1,5 +1,5 @@ -import Service, { service } from '@ember/service'; import { getOwner, setOwner } from '@ember/owner'; +import Service, { service } from '@ember/service'; import { task } from 'ember-concurrency'; diff --git a/packages/host/tests/helpers/index.gts b/packages/host/tests/helpers/index.gts index ed48685a14..87c1011c61 100644 --- a/packages/host/tests/helpers/index.gts +++ b/packages/host/tests/helpers/index.gts @@ -43,7 +43,6 @@ import SQLiteAdapter from '@cardstack/host/lib/sqlite-adapter'; import type CardService from '@cardstack/host/services/card-service'; import type { CardSaveSubscriber } from '@cardstack/host/services/card-service'; -import type CommandService from '@cardstack/host/services/command-service'; import type LoaderService from '@cardstack/host/services/loader-service'; import type MessageService from '@cardstack/host/services/message-service'; import type NetworkService from '@cardstack/host/services/network'; diff --git a/packages/host/tests/integration/commands/switch-submode-test.gts b/packages/host/tests/integration/commands/switch-submode-test.gts index 5bb4a618d1..cd4a6d4dd1 100644 --- a/packages/host/tests/integration/commands/switch-submode-test.gts +++ b/packages/host/tests/integration/commands/switch-submode-test.gts @@ -3,9 +3,12 @@ import { RenderingTestContext } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { Loader } from '@cardstack/runtime-common'; + import type CommandService from '@cardstack/host/services/command-service'; import type OperatorModeStateService from '@cardstack/host/services/operator-mode-state-service'; +import type { SwitchSubmodeInput } from 'https://cardstack.com/base/command'; + import { setupIntegrationTestRealm, setupLocalIndexing, @@ -14,8 +17,6 @@ import { } from '../../helpers'; import { setupRenderingTest } from '../../helpers/setup'; -import type { SwitchSubmodeInput } from 'https://cardstack.com/base/command'; - let loader: Loader; module('Integration | commands | switch-submode', function (hooks) { From d7b69f923db3416f8d79f670c79265051d518d64 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 12 Nov 2024 17:31:46 -0500 Subject: [PATCH 18/40] Test fixes and improve command payload consistency --- packages/ai-bot/README.md | 2 +- packages/ai-bot/lib/debug.ts | 29 +- packages/ai-bot/tests/chat-titling-test.ts | 16 +- .../ai-bot/tests/prompt-construction-test.ts | 28 +- packages/ai-bot/tests/responding-test.ts | 20 +- packages/host/app/commands/patch-card.ts | 4 - .../app/components/matrix/room-message.gts | 1 - packages/host/app/services/command-service.ts | 28 +- .../services/operator-mode-state-service.ts | 11 + .../host/tests/acceptance/commands-test.gts | 35 +-- .../components/ai-assistant-panel-test.gts | 255 +++++++++--------- packages/matrix/tests/commands.spec.ts | 22 +- packages/runtime-common/helpers/ai.ts | 21 +- 13 files changed, 250 insertions(+), 222 deletions(-) diff --git a/packages/ai-bot/README.md b/packages/ai-bot/README.md index 735d1f1c26..e2b02e177b 100644 --- a/packages/ai-bot/README.md +++ b/packages/ai-bot/README.md @@ -42,7 +42,7 @@ It will be able to see any cards shared in the chat and can respond using GPT4 i You can deliberately trigger a specific patch by sending a message that starts `debug:patch:` and has the JSON patch you want returned. For example: ``` -debug:patch:{"card_id":"http://localhost:4200/experiments/Author/1", "description": "message", "attributes": {"firstName": "David"}} +debug:patch:{"attributes": {"cardId":"http://localhost:4200/experiments/Author/1", "patch": { "attributes": {"firstName": "David"}}}} ``` This will return a patch with the ID of the last card you uploaded. This does not hit GPT4 and is useful for testing the integration of the two components without waiting for streaming responses. diff --git a/packages/ai-bot/lib/debug.ts b/packages/ai-bot/lib/debug.ts index c1019a5b55..b833d74d9e 100644 --- a/packages/ai-bot/lib/debug.ts +++ b/packages/ai-bot/lib/debug.ts @@ -33,16 +33,21 @@ export async function handleDebugCommands( let patchMessage = eventBody.split('debug:patch:')[1]; // If there's a card attached, we need to split it off to parse the json patchMessage = patchMessage.split('(Card')[0]; - let command: { - card_id?: string; + let toolArguments: { + attributes?: { + cardId?: string; + patch?: any; + }; description?: string; - attributes?: any; } = {}; try { - command = JSON.parse(patchMessage); - if (!command.card_id || !command.description || !command.attributes) { + toolArguments = JSON.parse(patchMessage); + if ( + !toolArguments.attributes?.cardId || + !toolArguments.attributes?.patch + ) { throw new Error( - 'Invalid debug patch: card_id, description, or attributes is missing.', + 'Invalid debug patch: attributes.cardId, or attributes.patch is missing.', ); } } catch (error) { @@ -54,7 +59,17 @@ export async function handleDebugCommands( undefined, ); } - return await sendOption(client, roomId, command, undefined); + return await sendOption( + client, + roomId, + { + id: 'patchCard-debug', + name: 'patchCard', + type: 'function', + arguments: toolArguments, + }, + undefined, + ); } return; } diff --git a/packages/ai-bot/tests/chat-titling-test.ts b/packages/ai-bot/tests/chat-titling-test.ts index 910208e7c0..87e8beda2f 100644 --- a/packages/ai-bot/tests/chat-titling-test.ts +++ b/packages/ai-bot/tests/chat-titling-test.ts @@ -378,9 +378,13 @@ module('shouldSetRoomTitle', () => { toolCall: { name: 'patchCard', arguments: { - card_id: 'http://localhost:4201/experiments/Friend/1', attributes: { - firstName: 'Dave', + cardId: 'http://localhost:4201/experiments/Friend/1', + patch: { + attributes: { + firstName: 'Dave', + }, + }, }, }, }, @@ -443,9 +447,13 @@ module('shouldSetRoomTitle', () => { toolCall: { name: 'patchCard', arguments: { - card_id: 'http://localhost:4201/drafts/Friend/1', attributes: { - firstName: 'Dave', + cardId: 'http://localhost:4201/drafts/Friend/1', + patch: { + attributes: { + firstName: 'Dave', + }, + }, }, }, }, diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index 9d1a0c3190..e9aea546c1 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -769,7 +769,9 @@ module('getModifyPrompt', () => { openCardIds: ['http://localhost:4201/experiments/Friend/1'], tools: [ getPatchTool('http://localhost:4201/experiments/Friend/1', { - firstName: { type: 'string' }, + attributes: { + firstName: { type: 'string' }, + }, }), ], submode: 'interact', @@ -928,7 +930,9 @@ module('getModifyPrompt', () => { openCardIds: ['http://localhost:4201/experiments/Friend/1'], tools: [ getPatchTool('http://localhost:4201/experiments/Friend/1', { - firstName: { type: 'string' }, + attributes: { + firstName: { type: 'string' }, + }, }), ], submode: 'interact', @@ -989,7 +993,9 @@ module('getModifyPrompt', () => { openCardIds: ['http://localhost:4201/experiments/Friend/1'], tools: [ getPatchTool('http://localhost:4201/experiments/Friend/1', { - firstName: { type: 'string' }, + attributes: { + firstName: { type: 'string' }, + }, }), ], submode: 'interact', @@ -1018,7 +1024,9 @@ module('getModifyPrompt', () => { openCardIds: ['http://localhost:4201/experiments/Meeting/2'], tools: [ getPatchTool('http://localhost:4201/experiments/Meeting/2', { - location: { type: 'string' }, + attributes: { + location: { type: 'string' }, + }, }), ], submode: 'interact', @@ -1867,11 +1875,13 @@ module('getModifyPrompt', () => { id: 'tool-call-id-1', name: 'searchCard', arguments: { - description: "Search for card instances of type 'Author'", - filter: { - type: { - module: 'http://localhost:4201/drafts/author', - name: 'Author', + attributes: { + description: "Search for card instances of type 'Author'", + filter: { + type: { + module: 'http://localhost:4201/drafts/author', + name: 'Author', + }, }, }, }, diff --git a/packages/ai-bot/tests/responding-test.ts b/packages/ai-bot/tests/responding-test.ts index 3ca3ff6ea5..77c5b32cd1 100644 --- a/packages/ai-bot/tests/responding-test.ts +++ b/packages/ai-bot/tests/responding-test.ts @@ -225,10 +225,14 @@ module('Responding', (hooks) => { id: 'some-tool-call-id', name: 'patchCard', arguments: { - card_id: 'card/1', - description: 'A new thing', attributes: { - some: 'thing', + cardId: 'card/1', + description: 'A new thing', + patch: { + attributes: { + some: 'thing', + }, + }, }, }, }, @@ -293,10 +297,14 @@ module('Responding', (hooks) => { id: 'some-tool-call-id', name: 'patchCard', arguments: { - card_id: 'card/1', - description: 'A new thing', attributes: { - some: 'thing', + cardId: 'card/1', + description: 'A new thing', + patch: { + attributes: { + some: 'thing', + }, + }, }, }, }, diff --git a/packages/host/app/commands/patch-card.ts b/packages/host/app/commands/patch-card.ts index 2a11818e9f..3488d5def9 100644 --- a/packages/host/app/commands/patch-card.ts +++ b/packages/host/app/commands/patch-card.ts @@ -40,9 +40,7 @@ export default class PatchCardCommand extends Command< protected async run( input: BaseCommandModule.PatchCardInput, ): Promise { - console.log('input', input); if (!input.cardId || !input.patch) { - console.log(input.cardId, input.patch); throw new Error( "Patch command can't run because it doesn't have all the fields in arguments returned by open ai", ); @@ -82,8 +80,6 @@ export default class PatchCardCommand extends Command< properties: {}, } as RelationshipsSchema, }; - console.log('inputTypeCardSchema', inputTypeCardSchema); - //debugger; return inputTypeCardSchema; } } diff --git a/packages/host/app/components/matrix/room-message.gts b/packages/host/app/components/matrix/room-message.gts index 33e3f0cc86..60f81ae01d 100644 --- a/packages/host/app/components/matrix/room-message.gts +++ b/packages/host/app/components/matrix/room-message.gts @@ -311,7 +311,6 @@ export default class RoomMessage extends Component { if (this.failedCommandState) { return `Failed to apply changes. ${this.failedCommandState.message}`; } - if (this.args.message.errorMessage) { return this.args.message.errorMessage; } diff --git a/packages/host/app/services/command-service.ts b/packages/host/app/services/command-service.ts index 8cb1a1ef96..bb821c1860 100644 --- a/packages/host/app/services/command-service.ts +++ b/packages/host/app/services/command-service.ts @@ -89,7 +89,6 @@ export default class CommandService extends Service { //TODO use create[CommandName] methods to create commands instead of lookupCommand to solve type issues and avoid embroider issues // OR //TODO use imports and leverage with custom loader maybe import SaveCard from 'http://cardstack.com/host/commannds/save-card' - lookupCommand = < CardInputType extends CardDef | undefined, CardResultType extends CardDef | undefined, @@ -156,10 +155,10 @@ export default class CommandService extends Service { ); } res = await this.operatorModeStateService.patchCard.perform( - payload.card_id, + payload?.attributes?.cardId, { - attributes: payload.attributes, - relationships: payload.relationships, + attributes: payload?.attributes?.patch?.attributes, + relationships: payload?.attributes?.patch?.relationships, }, ); } else if (command.name === 'searchCard') { @@ -168,7 +167,7 @@ export default class CommandService extends Service { "Search command can't run because it doesn't have all the arguments returned by open ai", ); } - let query = { filter: payload.filter }; + let query = { filter: payload.attributes.filter }; let realmUrls = this.realmServer.availableRealmURLs; let instances: CardDef[] = flatMap( await Promise.all( @@ -277,23 +276,20 @@ export default class CommandService extends Service { } } -type PatchPayload = { card_id: string } & PatchData; -type SearchPayload = { card_id: string; filter: CardTypeFilter | EqFilter }; +type PatchPayload = { attributes: { cardId: string; patch: PatchData } }; +type SearchPayload = { + attributes: { cardId: string; filter: CardTypeFilter | EqFilter }; +}; function hasPatchData(payload: any): payload is PatchPayload { return ( - (typeof payload === 'object' && - payload !== null && - 'card_id' in payload && - 'attributes' in payload) || - (typeof payload === 'object' && - payload !== null && - 'card_id' in payload && - 'relationships' in payload) + payload.attributes?.cardId && + (payload.attributes?.patch?.attributes || + payload.attributes?.patch?.relationships) ); } function hasSearchData(payload: any): payload is SearchPayload { - assertQuery({ filter: payload.filter }); + assertQuery({ filter: payload.attributes.filter }); return payload; } diff --git a/packages/host/app/services/operator-mode-state-service.ts b/packages/host/app/services/operator-mode-state-service.ts index d9595633fd..9f53d3907b 100644 --- a/packages/host/app/services/operator-mode-state-service.ts +++ b/packages/host/app/services/operator-mode-state-service.ts @@ -144,8 +144,19 @@ export default class OperatorModeStateService extends Service { } } await this.cardService.patchCard(card, document, patch); + // TODO: if we introduce an identity map, we would not need this + await this.reloadCardIfOpen(card.id); }); + async reloadCardIfOpen(id: string) { + let stackItems = this.state?.stacks.flat() ?? []; + for (let item of stackItems) { + if ('card' in item && item.card.id == id) { + this.cardService.reloadCard(item.card); + } + } + } + async deleteCard(card: CardDef) { // remove all stack items for the deleted card let items: StackItem[] = []; diff --git a/packages/host/tests/acceptance/commands-test.gts b/packages/host/tests/acceptance/commands-test.gts index 078c4aadf8..dbaa2c0cde 100644 --- a/packages/host/tests/acceptance/commands-test.gts +++ b/packages/host/tests/acceptance/commands-test.gts @@ -125,29 +125,23 @@ module('Acceptance | Commands tests', function (hooks) { return ScheduleMeetingInput; } protected async run(input: ScheduleMeetingInput) { - console.log('input', input); let meeting = new Meeting({ topic: 'unset topic', participants: input.participants, }); - console.log('meeting', meeting); let saveCardCommand = this.commandContext.lookupCommand< SaveCardInput, undefined >('save-card'); - console.log('saveCardCommand', saveCardCommand); - console.log('this.loaderService', this.loaderService); const { SaveCardInput } = await this.loaderService.loader.import< typeof BaseCommandModule >(`${baseRealm.url}command`); - console.log('SaveCardInput', SaveCardInput); await saveCardCommand.execute( new SaveCardInput({ card: meeting, realm: testRealmURL, }), ); - console.log('saved'); // Mutate and save again let patchCardCommand = this.commandContext.lookupCommand< @@ -168,7 +162,6 @@ module('Acceptance | Commands tests', function (hooks) { ShowCardInput, undefined >('show-card'); - console.log('showCardCommand', showCardCommand); const { ShowCardInput } = await this.loaderService.loader.import< typeof BaseCommandModule >(`${baseRealm.url}command`); @@ -357,7 +350,6 @@ module('Acceptance | Commands tests', function (hooks) { }, relationships: { properties: {}, - required: [], type: 'object', }, }, @@ -371,7 +363,9 @@ module('Acceptance | Commands tests', function (hooks) { toolCall: { name: toolName, arguments: { - submode: 'code', + attributes: { + submode: 'code', + }, }, }, eventId: '__EVENT_ID__', @@ -457,7 +451,6 @@ module('Acceptance | Commands tests', function (hooks) { }, relationships: { properties: {}, - required: [], type: 'object', }, }, @@ -471,7 +464,9 @@ module('Acceptance | Commands tests', function (hooks) { toolCall: { name: toolName, arguments: { - submode: 'code', + attributes: { + submode: 'code', + }, }, }, eventId: '__EVENT_ID__', @@ -542,20 +537,12 @@ module('Acceptance | Commands tests', function (hooks) { assert.strictEqual(boxelMessageData.context.tools[0].type, 'function'); let toolName = boxelMessageData.context.tools[0].function.name; let meetingCardEventId = boxelMessageData.attachedCardsEventIds[0]; - console.log('meetingCardEventId', meetingCardEventId); - console.log('getRoomEvents(roomId)', getRoomEvents(roomId)); - console.log( - getRoomEvents(roomId).find( - (event) => event.event_id === meetingCardEventId, - ), - ); let cardFragment = getRoomEvents(roomId).find( (event) => event.event_id === meetingCardEventId, )!.content.data.cardFragment; let parsedCard = JSON.parse(cardFragment); let meetingCardId = parsedCard.data.id; - console.log('meetingCardId', meetingCardId); simulateRemoteMessage(roomId, '@aibot:localhost', { body: 'Update card', @@ -566,10 +553,12 @@ module('Acceptance | Commands tests', function (hooks) { toolCall: { name: toolName, arguments: { - cardId: meetingCardId, - patch: { - attributes: { - topic: 'Meeting with Hassan', + attributes: { + cardId: meetingCardId, + patch: { + attributes: { + topic: 'Meeting with Hassan', + }, }, }, }, diff --git a/packages/host/tests/integration/components/ai-assistant-panel-test.gts b/packages/host/tests/integration/components/ai-assistant-panel-test.gts index c083c62bec..a1fe7ac186 100644 --- a/packages/host/tests/integration/components/ai-assistant-panel-test.gts +++ b/packages/host/tests/integration/components/ai-assistant-panel-test.gts @@ -390,8 +390,12 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: `${testRealmURL}Person/fadhlan`, - attributes: { firstName: 'Dave' }, + attributes: { + cardId: `${testRealmURL}Person/fadhlan`, + patch: { + attributes: { firstName: 'Dave' }, + }, + }, }, }, eventId: '__EVENT_ID__', @@ -458,8 +462,12 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: `${testRealmURL}Person/fadhlan`, - attributes: { firstName: 'Evie' }, + attributes: { + cardId: `${testRealmURL}Person/fadhlan`, + patch: { + attributes: { firstName: 'Evie' }, + }, + }, }, }, eventId: '__EVENT_ID__', @@ -479,8 +487,12 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: `${testRealmURL}Person/fadhlan`, - attributes: { firstName: 'Jackie' }, + attributes: { + cardId: `${testRealmURL}Person/fadhlan`, + patch: { + attributes: { firstName: 'Jackie' }, + }, + }, }, }, eventId: '__EVENT_ID__', @@ -542,82 +554,6 @@ module('Integration | ai-assistant-panel', function (hooks) { .exists(); }); - test('it only applies changes from the chat if the stack contains a card with that ID', async function (assert) { - let roomId = await renderAiAssistantPanel(`${testRealmURL}Person/fadhlan`); - - await waitFor('[data-test-person]'); - assert.dom('[data-test-boxel-card-header-title]').hasText('Person'); - assert.dom('[data-test-person]').hasText('Fadhlan'); - - let otherCardID = `${testRealmURL}Person/burcu`; - await simulateRemoteMessage(roomId, '@aibot:localhost', { - body: 'i am the body', - msgtype: 'org.boxel.command', - formatted_body: - 'A patch
https://www.example.com/path/to/resource?query=param1value&anotherQueryParam=anotherValue&additionalParam=additionalValue&longparameter1=someLongValue1
', - format: 'org.matrix.custom.html', - data: JSON.stringify({ - toolCall: { - name: 'patchCard', - arguments: { - card_id: otherCardID, - attributes: { firstName: 'Dave' }, - }, - }, - eventId: '__EVENT_ID__', - }), - 'm.relates_to': { - rel_type: 'm.replace', - event_id: '__EVENT_ID__', - }, - }); - - await waitFor('[data-test-command-apply="ready"]'); - await click('[data-test-command-apply]'); - - await waitFor('[data-test-command-card-idle]'); - assert - .dom('[data-test-card-error]') - .containsText(`Please open card '${otherCardID}' to make changes to it.`); - assert.dom('[data-test-apply-state="failed"]').exists(); - assert.dom('[data-test-ai-bot-retry-button]').exists(); - assert.dom('[data-test-command-apply]').doesNotExist(); - assert.dom('[data-test-person]').hasText('Fadhlan'); - - await triggerEvent( - `[data-test-stack-card="${testRealmURL}Person/fadhlan"] [data-test-field-component-card][data-test-card-format="fitted"]`, - 'mouseenter', - ); - await waitFor('[data-test-overlay-card] [data-test-overlay-more-options]'); - await percySnapshot( - 'Integration | ai-assistant-panel | it only applies changes from the chat if the stack contains a card with that ID | error', - ); - - await setCardInOperatorModeState(otherCardID); - await waitFor('[data-test-person="Burcu"]'); - click('[data-test-ai-bot-retry-button]'); // retry the command with correct card - await waitFor('[data-test-apply-state="applying"]'); - assert.dom('[data-test-apply-state="applying"]').exists(); - - await waitFor('[data-test-command-card-idle]'); - await waitFor('[data-test-apply-state="applied"]'); - assert.dom('[data-test-apply-state="applied"]').exists(); - assert.dom('[data-test-person]').hasText('Dave'); - assert.dom('[data-test-command-apply]').doesNotExist(); - assert.dom('[data-test-ai-bot-retry-button]').doesNotExist(); - - await triggerEvent( - `[data-test-stack-card="${testRealmURL}Person/burcu"] [data-test-plural-view="linksToMany"] [data-test-plural-view-item="0"]`, - 'mouseenter', - ); - assert - .dom('[data-test-overlay-card] [data-test-overlay-more-options]') - .exists(); - await percySnapshot( - 'Integration | ai-assistant-panel | it only applies changes from the chat if the stack contains a card with that ID | error fixed', - ); - }); - test('it can apply change to nested contains field', async function (assert) { let roomId = await renderAiAssistantPanel(`${testRealmURL}Person/fadhlan`); @@ -627,10 +563,14 @@ module('Integration | ai-assistant-panel', function (hooks) { let payload = { name: 'patchCard', arguments: { - card_id: `${testRealmURL}Person/fadhlan`, attributes: { - firstName: 'Joy', - address: { shippingInfo: { preferredCarrier: 'UPS' } }, + cardId: `${testRealmURL}Person/fadhlan`, + patch: { + attributes: { + firstName: 'Joy', + address: { shippingInfo: { preferredCarrier: 'UPS' } }, + }, + }, }, }, eventId: 'event1', @@ -658,14 +598,18 @@ module('Integration | ai-assistant-panel', function (hooks) { name: 'patchCard', payload: { attributes: { - address: { - shippingInfo: { - preferredCarrier: 'UPS', + cardId: 'http://test-realm/test/Person/fadhlan', + patch: { + attributes: { + address: { + shippingInfo: { + preferredCarrier: 'UPS', + }, + }, + firstName: 'Joy', }, }, - firstName: 'Joy', }, - card_id: 'http://test-realm/test/Person/fadhlan', }, }, 'it can preview code when a change is proposed', @@ -698,12 +642,16 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: id, attributes: { - address: { shippingInfo: { preferredCarrier: 'Fedex' } }, - }, - relationships: { - pet: { links: { self: null } }, + cardId: id, + patch: { + attributes: { + address: { shippingInfo: { preferredCarrier: 'Fedex' } }, + }, + relationships: { + pet: { links: { self: null } }, + }, + }, }, }, }, @@ -736,13 +684,17 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: id, attributes: { - address: { shippingInfo: { preferredCarrier: 'UPS' } }, - }, - relationships: { - pet: { - links: { self: `${testRealmURL}Pet/mango` }, + cardId: id, + patch: { + attributes: { + address: { shippingInfo: { preferredCarrier: 'UPS' } }, + }, + relationships: { + pet: { + links: { self: `${testRealmURL}Pet/mango` }, + }, + }, }, }, }, @@ -785,8 +737,16 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: id, - attributes: { trips: { tripTitle: 'Trip to Japan' } }, + attributes: { + cardId: id, + patch: { + attributes: { + trips: { + tripTitle: 'Trip to Japan', + }, + }, + }, + }, }, }, eventId: '__EVENT_ID__', @@ -819,9 +779,12 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: id, - - attributes: { firstName: 'Dave' }, + attributes: { + cardId: id, + patch: { + attributes: { firstName: 'Dave' }, + }, + }, }, }, eventId: '__EVENT_ID__', @@ -862,8 +825,12 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: id, - attributes: { firstName: 'Jackie' }, + attributes: { + cardId: id, + patch: { + attributes: { firstName: 'Jackie' }, + }, + }, }, }, eventId: '__EVENT_ID__', @@ -923,8 +890,12 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: id, - attributes: { firstName: 'Dave' }, + attributes: { + cardId: id, + patch: { + attributes: { firstName: 'Dave' }, + }, + }, }, }, eventId: '__EVENT_ID__', @@ -1193,12 +1164,14 @@ module('Integration | ai-assistant-panel', function (hooks) { assert.dom('[data-test-ai-assistant-message]').doesNotExist(); click('[data-test-send-message-btn]'); + await this.pauseTest(); await waitFor('[data-test-ai-assistant-message].is-pending'); assert.dom('[data-test-message-field]').hasValue(''); assert.dom('[data-test-send-message-btn]').isDisabled(); assert.dom('[data-test-ai-assistant-message]').exists({ count: 1 }); assert.dom('[data-test-ai-assistant-message]').hasClass('is-pending'); + await this.pauseTest(); await waitFor('[data-test-ai-assistant-message].is-error'); await waitUntil( () => @@ -1231,8 +1204,12 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: `${testRealmURL}Person/fadhlan`, - attributes: { firstName: 'Dave' }, + attributes: { + cardId: `${testRealmURL}Person/fadhlan`, + patch: { + attributes: { firstName: 'Dave' }, + }, + }, }, }, eventId: '__EVENT_ID__', @@ -1892,8 +1869,12 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'patchCard', arguments: { - card_id: `${testRealmURL}Person/fadhlan`, - attributes: { firstName: 'Evie' }, + attributes: { + cardId: `${testRealmURL}Person/fadhlan`, + patch: { + attributes: { firstName: 'Evie' }, + }, + }, }, }, eventId: '__EVENT_ID__', @@ -1961,11 +1942,13 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'searchCard', arguments: { - description: 'Searching for card', - filter: { - type: { - module: `${testRealmURL}pet`, - name: 'Pet', + attributes: { + description: 'Searching for card', + filter: { + type: { + module: `${testRealmURL}pet`, + name: 'Pet', + }, }, }, }, @@ -2025,11 +2008,13 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'searchCard', arguments: { - description: 'Searching for card', - filter: { - type: { - module: `${testRealmURL}pet`, - name: 'Pet', + attributes: { + description: 'Searching for card', + filter: { + type: { + module: `${testRealmURL}pet`, + name: 'Pet', + }, }, }, }, @@ -2084,10 +2069,12 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'searchCard', arguments: { - description: 'Searching for card', - filter: { - contains: { - title: 'Mango', + attributes: { + description: 'Searching for card', + filter: { + contains: { + title: 'Mango', + }, }, }, }, @@ -2140,11 +2127,13 @@ module('Integration | ai-assistant-panel', function (hooks) { toolCall: { name: 'searchCard', arguments: { - description: 'Searching for card', - filter: { - type: { - module: `${testRealmURL}person`, - name: 'Person', + attributes: { + description: 'Searching for card', + filter: { + type: { + module: `${testRealmURL}person`, + name: 'Person', + }, }, }, }, diff --git a/packages/matrix/tests/commands.spec.ts b/packages/matrix/tests/commands.spec.ts index 81b3e23f84..e39dab60b0 100644 --- a/packages/matrix/tests/commands.spec.ts +++ b/packages/matrix/tests/commands.spec.ts @@ -226,7 +226,7 @@ test.describe('Commands', () => { }) => { await login(page, 'user1', 'pass'); let room1 = await getRoomId(page); - let card_id = `${testHost}/hassan`; + let cardId = `${testHost}/hassan`; let content = { msgtype: 'org.boxel.command', format: 'org.matrix.custom.html', @@ -236,9 +236,13 @@ test.describe('Commands', () => { toolCall: { name: 'patchCard', arguments: { - card_id, attributes: { - firstName: 'Dave', + cardId, + patch: { + attributes: { + firstName: 'Dave', + }, + }, }, }, }, @@ -279,11 +283,13 @@ test.describe('Commands', () => { toolCall: { name: 'searchCard', arguments: { - description: 'Searching for card', - filter: { - type: { - module: `${testHost}person`, - name: 'Person', + attributes: { + description: 'Searching for card', + filter: { + type: { + module: `${testHost}person`, + name: 'Person', + }, }, }, }, diff --git a/packages/runtime-common/helpers/ai.ts b/packages/runtime-common/helpers/ai.ts index 28fddb4a49..5dcc6bef01 100644 --- a/packages/runtime-common/helpers/ai.ts +++ b/packages/runtime-common/helpers/ai.ts @@ -263,7 +263,6 @@ function generateJsonSchemaForLinksToFields( let schema: RelationshipsSchema = { type: 'object', properties: {}, - required: [], }; for (let rel of relationships) { let relSchema: LinksToSchema = { @@ -370,7 +369,7 @@ export function generateJsonSchemaForCardType( ) { return { attributes: schema, - relationships: { type: 'object', properties: {}, required: [] }, + relationships: { type: 'object', properties: {} }, }; } return { @@ -392,16 +391,18 @@ export function getPatchTool( parameters: { type: 'object', properties: { - card_id: { - type: 'string', - const: attachedOpenCardId, // Force the valid card_id to be the id of the card being patched - }, - description: { - type: 'string', + attributes: { + cardId: { + type: 'string', + const: attachedOpenCardId, // Force the valid card_id to be the id of the card being patched + }, + description: { + type: 'string', + }, + ...patchSpec, }, - ...patchSpec, }, - required: ['card_id', 'attributes', 'description'], + required: ['attributes', 'description'], }, }, }; From fe8a51fb44256409ff52ac6e0d7df8ef6d54cf9c Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 13 Nov 2024 00:34:02 -0500 Subject: [PATCH 19/40] Minor fixes --- packages/ai-bot/main.ts | 1 - .../tests/integration/components/ai-assistant-panel-test.gts | 2 -- packages/matrix/tests/commands.spec.ts | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/ai-bot/main.ts b/packages/ai-bot/main.ts index 96a73f6b6c..bf56497a1d 100644 --- a/packages/ai-bot/main.ts +++ b/packages/ai-bot/main.ts @@ -49,7 +49,6 @@ class Assistant { let messages = getModifyPrompt(history, this.id, tools); // Write out tools and messages to a log file - const fs = require('fs'); const logData = { timestamp: new Date().toISOString(), tools, diff --git a/packages/host/tests/integration/components/ai-assistant-panel-test.gts b/packages/host/tests/integration/components/ai-assistant-panel-test.gts index a1fe7ac186..108495dc4c 100644 --- a/packages/host/tests/integration/components/ai-assistant-panel-test.gts +++ b/packages/host/tests/integration/components/ai-assistant-panel-test.gts @@ -1164,14 +1164,12 @@ module('Integration | ai-assistant-panel', function (hooks) { assert.dom('[data-test-ai-assistant-message]').doesNotExist(); click('[data-test-send-message-btn]'); - await this.pauseTest(); await waitFor('[data-test-ai-assistant-message].is-pending'); assert.dom('[data-test-message-field]').hasValue(''); assert.dom('[data-test-send-message-btn]').isDisabled(); assert.dom('[data-test-ai-assistant-message]').exists({ count: 1 }); assert.dom('[data-test-ai-assistant-message]').hasClass('is-pending'); - await this.pauseTest(); await waitFor('[data-test-ai-assistant-message].is-error'); await waitUntil( () => diff --git a/packages/matrix/tests/commands.spec.ts b/packages/matrix/tests/commands.spec.ts index e39dab60b0..1006bf34e6 100644 --- a/packages/matrix/tests/commands.spec.ts +++ b/packages/matrix/tests/commands.spec.ts @@ -252,7 +252,7 @@ test.describe('Commands', () => { await showAllCards(page); await page .locator( - `[data-test-stack-card="${testHost}/index"] [data-test-cards-grid-item="${card_id}"]`, + `[data-test-stack-card="${testHost}/index"] [data-test-cards-grid-item="${cardId}"]`, ) .click(); await putEvent(userCred.accessToken, room1, 'm.room.message', '1', content); From fe5c2b400936553e501c3424f3380963e6a56489 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 13 Nov 2024 00:50:22 -0500 Subject: [PATCH 20/40] Update tests for newer command/tool schema --- .../ai-bot/tests/prompt-construction-test.ts | 48 ++++++---- packages/ai-bot/tests/responding-test.ts | 24 +++-- packages/matrix/tests/commands.spec.ts | 89 ++++++++++--------- 3 files changed, 97 insertions(+), 64 deletions(-) diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index e9aea546c1..221b5fe5c3 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -896,20 +896,24 @@ module('getModifyPrompt', () => { description: { type: 'string', }, - card_id: { - type: 'string', - const: 'http://localhost:4201/experiments/Friend/1', - }, attributes: { - type: 'object', - properties: { - firstName: { - type: 'string', + cardId: { + type: 'string', + const: 'http://localhost:4201/experiments/Friend/1', + }, + patch: { + attributes: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + }, }, }, }, }, - required: ['card_id', 'attributes', 'description'], + required: ['attributes', 'description'], }, }, }); @@ -930,8 +934,10 @@ module('getModifyPrompt', () => { openCardIds: ['http://localhost:4201/experiments/Friend/1'], tools: [ getPatchTool('http://localhost:4201/experiments/Friend/1', { - attributes: { - firstName: { type: 'string' }, + patch: { + attributes: { + firstName: { type: 'string' }, + }, }, }), ], @@ -961,18 +967,24 @@ module('getModifyPrompt', () => { parameters: { type: 'object', properties: { - description: { - type: 'string', + attributes: { + cardId: { + type: 'string', + const: 'http://localhost:4201/experiments/Friend/1', + }, }, - card_id: { + description: { type: 'string', - const: 'http://localhost:4201/experiments/Friend/1', }, - firstName: { - type: 'string', + patch: { + attributes: { + firstName: { + type: 'string', + }, + }, }, }, - required: ['card_id', 'attributes', 'description'], + required: ['attributes', 'description'], }, }, }); diff --git a/packages/ai-bot/tests/responding-test.ts b/packages/ai-bot/tests/responding-test.ts index 77c5b32cd1..680543d24e 100644 --- a/packages/ai-bot/tests/responding-test.ts +++ b/packages/ai-bot/tests/responding-test.ts @@ -184,9 +184,15 @@ module('Responding', (hooks) => { test('Sends tool call event and replaces thinking message when tool call happens with no content', async () => { const patchArgs = { - card_id: 'card/1', - description: 'A new thing', - attributes: { some: 'thing' }, + attributes: { + cardId: 'card/1', + description: 'A new thing', + patch: { + attributes: { + some: 'thing', + }, + }, + }, }; await responder.initialize(); @@ -256,9 +262,15 @@ module('Responding', (hooks) => { test('Sends tool call event separately when content is sent before tool call', async () => { const patchArgs = { - card_id: 'card/1', - description: 'A new thing', - attributes: { some: 'thing' }, + attributes: { + cardId: 'card/1', + description: 'A new thing', + patch: { + attributes: { + some: 'thing', + }, + }, + }, }; await responder.initialize(); diff --git a/packages/matrix/tests/commands.spec.ts b/packages/matrix/tests/commands.spec.ts index 1006bf34e6..55309c563f 100644 --- a/packages/matrix/tests/commands.spec.ts +++ b/packages/matrix/tests/commands.spec.ts @@ -61,32 +61,39 @@ test.describe('Commands', () => { description: { type: 'string', }, - card_id: { - type: 'string', - const: `${testHost}/mango`, - }, attributes: { - type: 'object', - properties: { - firstName: { - type: 'string', - }, - lastName: { - type: 'string', - }, - email: { - type: 'string', - }, - posts: { - type: 'number', - }, - thumbnailURL: { - type: 'string', + cardId: { + type: 'string', + const: `${testHost}/mango`, + }, + patch: { + type: 'object', + properties: { + attributes: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + lastName: { + type: 'string', + }, + email: { + type: 'string', + }, + posts: { + type: 'number', + }, + thumbnailURL: { + type: 'string', + }, + }, + }, }, }, }, }, - required: ['card_id', 'attributes', 'description'], + required: ['attributes', 'description'], }, }, }, @@ -102,33 +109,35 @@ test.describe('Commands', () => { description: { type: 'string', }, - filter: { - type: 'object', - properties: { - contains: { - type: 'object', - properties: { - title: { - type: 'string', - description: 'title of the card', + attributes: { + filter: { + type: 'object', + properties: { + contains: { + type: 'object', + properties: { + title: { + type: 'string', + description: 'title of the card', + }, }, + required: ['title'], }, - required: ['title'], - }, - eq: { - type: 'object', - properties: { - _cardType: { - type: 'string', - description: 'name of the card type', + eq: { + type: 'object', + properties: { + _cardType: { + type: 'string', + description: 'name of the card type', + }, }, + required: ['_cardType'], }, - required: ['_cardType'], }, }, }, }, - required: ['filter', 'description'], + required: ['attributes', 'description'], }, }, }, From 00cb956b5bff515a9c86a13bd2ae7dcf816e7c0a Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 13 Nov 2024 16:14:04 -0500 Subject: [PATCH 21/40] Fix two host tests --- packages/base/command-result.gts | 8 +++++-- .../components/ai-assistant-panel-test.gts | 24 +++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/base/command-result.gts b/packages/base/command-result.gts index 361bbfa950..4bb10ffbde 100644 --- a/packages/base/command-result.gts +++ b/packages/base/command-result.gts @@ -395,10 +395,14 @@ class CommandResultIsolated extends CommandResultEmbeddedView { @tracked showAllResults = true; get filterString() { - if (!this.args.model.toolCallArgs?.filter) { + if (!this.args.model.toolCallArgs?.attributes.filter) { return; } - return JSON.stringify(this.args.model.toolCallArgs.filter, null, 2); + return JSON.stringify( + this.args.model.toolCallArgs.attributes.filter, + null, + 2, + ); } } diff --git a/packages/host/tests/integration/components/ai-assistant-panel-test.gts b/packages/host/tests/integration/components/ai-assistant-panel-test.gts index 108495dc4c..f521a9854b 100644 --- a/packages/host/tests/integration/components/ai-assistant-panel-test.gts +++ b/packages/host/tests/integration/components/ai-assistant-panel-test.gts @@ -2177,10 +2177,12 @@ module('Integration | ai-assistant-panel', function (hooks) { const roomId = await renderAiAssistantPanel(id); const toolArgs = { description: 'Search for Person cards', - filter: { - type: { - module: `${testRealmURL}person`, - name: 'Person', + attributes: { + filter: { + type: { + module: `${testRealmURL}person`, + name: 'Person', + }, }, }, }; @@ -2236,7 +2238,7 @@ module('Integration | ai-assistant-panel', function (hooks) { .hasText(`Description ${toolArgs.description}`); assert .dom(`${savedCard} [data-test-boxel-field]:nth-child(2)`) - .hasText(`Filter ${JSON.stringify(toolArgs.filter, null, 2)}`); + .hasText(`Filter ${JSON.stringify(toolArgs.attributes.filter, null, 2)}`); resultListItem = `${savedCard} ${resultListItem}`; assert.dom(resultListItem).exists({ count: 8 }); @@ -2250,10 +2252,12 @@ module('Integration | ai-assistant-panel', function (hooks) { const roomId = await renderAiAssistantPanel(id); const toolArgs = { description: 'Search for Person cards', - filter: { - type: { - module: `${testRealmURL}person`, - name: 'Person', + attributes: { + filter: { + type: { + module: `${testRealmURL}person`, + name: 'Person', + }, }, }, }; @@ -2311,7 +2315,7 @@ module('Integration | ai-assistant-panel', function (hooks) { .hasText(`Description ${toolArgs.description}`); assert .dom(`${savedCard} [data-test-boxel-field]:nth-child(2)`) - .hasText(`Filter ${JSON.stringify(toolArgs.filter, null, 2)}`); + .hasText(`Filter ${JSON.stringify(toolArgs.attributes.filter, null, 2)}`); resultListItem = `${savedCard} ${resultListItem}`; assert.dom(resultListItem).exists({ count: 8 }); From 6ca260edcb044a03e30f9bcbbb0aef0e94f7645c Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 13 Nov 2024 19:51:38 -0500 Subject: [PATCH 22/40] Defer attempt to access defaultWritableRealm until needed --- packages/host/app/services/card-service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/host/app/services/card-service.ts b/packages/host/app/services/card-service.ts index 8c93455fce..5c4e12623f 100644 --- a/packages/host/app/services/card-service.ts +++ b/packages/host/app/services/card-service.ts @@ -200,8 +200,7 @@ export default class CardService extends Service { async saveModel( owner: T, card: CardDef, - defaultRealmHref: string | undefined = this.realm.defaultWritableRealm - ?.path, + defaultRealmHref: string | undefined, ): Promise { let cardChanged = false; function onCardChange() { @@ -222,6 +221,8 @@ export default class CardService extends Service { // in the case where we get no realm URL from the card, we are dealing with // a new card instance that does not have a realm URL yet. if (!realmURL) { + defaultRealmHref = + defaultRealmHref ?? this.realm.defaultWritableRealm?.path; if (!defaultRealmHref) { throw new Error('Could not find a writable realm'); } From fad785935c1dbb2221cb1632f40c89fe8b9ea2f5 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 13 Nov 2024 19:52:17 -0500 Subject: [PATCH 23/40] Avoid duplicated events in room.events and RoomResource#_messageCache --- packages/host/app/resources/room.ts | 13 ++++++++++--- packages/host/app/services/matrix-service.ts | 18 +++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/host/app/resources/room.ts b/packages/host/app/resources/room.ts index cde430e130..4309bd64c3 100644 --- a/packages/host/app/resources/room.ts +++ b/packages/host/app/resources/room.ts @@ -331,13 +331,20 @@ export class RoomResource extends Resource { if (messageField) { // if the message is a replacement for other messages, // use `created` from the oldest one. - if (this._messageCache.has(event_id)) { - let d1 = this._messageCache.get(event_id)!.created!; + let existingMessage = + this._messageCache.get(event_id) || + (messageField.clientGeneratedId && + this._messageCache.get(messageField.clientGeneratedId)); + if (existingMessage) { + let d1 = existingMessage.created!; let d2 = messageField.created!; messageField.created = d1 < d2 ? d1 : d2; + this._messageCache.delete(existingMessage.eventId); + existingMessage.clientGeneratedId && + this._messageCache.delete(existingMessage.clientGeneratedId!); } this._messageCache.set( - (event.content as CardMessageContent).clientGeneratedId ?? event_id, + messageField.clientGeneratedId ?? event_id, messageField as any, ); } diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index b20709c8f9..e4d58c9b1a 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -1198,9 +1198,21 @@ export default class MatrixService extends Service { `bug: unknown room for event ${JSON.stringify(event, null, 2)}`, ); } - let oldEventIndex = room.events.findIndex((e) => e.event_id === oldEventId); - if (oldEventIndex >= 0) { - room.events[oldEventIndex] = event as unknown as DiscreteMatrixEvent; + let oldEvent = room.events.find((e) => e.event_id === oldEventId); + if (oldEvent) { + // If the event was already added with the new ID, remove it since the original will be updated + let alreadyPresentNewEventIndex = room.events.findIndex( + (e) => e.event_id === event.event_id, + ); + if (alreadyPresentNewEventIndex >= 0) { + room.events.splice(alreadyPresentNewEventIndex, 1); + } + + // Update the old event being updated + room.events[room.events.indexOf(oldEvent)] = + event as unknown as DiscreteMatrixEvent; + + // This triggers reactivity that is based on the @tracked room.events room.events = [...room.events]; } } From aea643472ad33b0c78cb87460b20b847a7033b28 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Thu, 14 Nov 2024 13:22:00 +0000 Subject: [PATCH 24/40] Extra fallback option for body description supporting old and new formats --- packages/ai-bot/lib/matrix.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/ai-bot/lib/matrix.ts b/packages/ai-bot/lib/matrix.ts index 5d129994e1..29a5580814 100644 --- a/packages/ai-bot/lib/matrix.ts +++ b/packages/ai-bot/lib/matrix.ts @@ -126,7 +126,13 @@ export const toMatrixMessageCommandContent = ( eventToUpdate: string | undefined, ): IContent | undefined => { let { arguments: payload } = functionCall; - const body = payload['description'] || 'Issuing command'; + // Take the description if it exists (old format) + // Otherwise take the description from the attributes (new format, responses are cards) + // If neither exists, use a default + const body = + payload['description'] || + payload['attributes']?.description || + 'Issuing command'; let messageObject: IContent = { body: body, msgtype: 'org.boxel.command', From 6c9dec85b5c16a69181d5261c77200c5b1ffef16 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Thu, 14 Nov 2024 13:22:40 +0000 Subject: [PATCH 25/40] Test fixes, check test for old & newer formats --- .../ai-bot/tests/prompt-construction-test.ts | 62 +++++++++---------- packages/ai-bot/tests/responding-test.ts | 2 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index 221b5fe5c3..b3b0246011 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -893,27 +893,23 @@ module('getModifyPrompt', () => { parameters: { type: 'object', properties: { + card_id: { + type: 'string', + const: 'http://localhost:4201/experiments/Friend/1', + }, description: { type: 'string', }, attributes: { - cardId: { - type: 'string', - const: 'http://localhost:4201/experiments/Friend/1', - }, - patch: { - attributes: { - type: 'object', - properties: { - firstName: { - type: 'string', - }, - }, + type: 'object', + properties: { + firstName: { + type: 'string', }, }, }, }, - required: ['attributes', 'description'], + required: ['card_id', 'attributes', 'description'], }, }, }); @@ -972,14 +968,14 @@ module('getModifyPrompt', () => { type: 'string', const: 'http://localhost:4201/experiments/Friend/1', }, - }, - description: { - type: 'string', - }, - patch: { - attributes: { - firstName: { - type: 'string', + description: { + type: 'string', + }, + patch: { + attributes: { + firstName: { + type: 'string', + }, }, }, }, @@ -1068,18 +1064,22 @@ module('getModifyPrompt', () => { parameters: { type: 'object', properties: { - card_id: { - type: 'string', - const: 'http://localhost:4201/experiments/Meeting/2', - }, - description: { - type: 'string', - }, - location: { - type: 'string', + attributes: { + cardId: { + type: 'string', + const: 'http://localhost:4201/experiments/Meeting/2', + }, + description: { + type: 'string', + }, + attributes: { + location: { + type: 'string', + }, + }, }, }, - required: ['card_id', 'attributes', 'description'], + required: ['attributes', 'description'], }, }, }); diff --git a/packages/ai-bot/tests/responding-test.ts b/packages/ai-bot/tests/responding-test.ts index 680543d24e..cd1f86f650 100644 --- a/packages/ai-bot/tests/responding-test.ts +++ b/packages/ai-bot/tests/responding-test.ts @@ -247,7 +247,7 @@ module('Responding', (hooks) => { ); assert.deepEqual( sentEvents[1].content.body, - patchArgs.description, + patchArgs.attributes.description, 'Body text should be the description', ); assert.deepEqual( From 7fe1a5e44b9a9e66cb36c7b42fe2bc29f66284af Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Thu, 14 Nov 2024 14:11:53 +0000 Subject: [PATCH 26/40] add type layer in properties --- packages/runtime-common/helpers/ai.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/runtime-common/helpers/ai.ts b/packages/runtime-common/helpers/ai.ts index 5dcc6bef01..012e11f4ce 100644 --- a/packages/runtime-common/helpers/ai.ts +++ b/packages/runtime-common/helpers/ai.ts @@ -392,17 +392,20 @@ export function getPatchTool( type: 'object', properties: { attributes: { - cardId: { - type: 'string', - const: attachedOpenCardId, // Force the valid card_id to be the id of the card being patched - }, - description: { - type: 'string', + type: 'object', + properties: { + cardId: { + type: 'string', + const: attachedOpenCardId, // Force the valid card_id to be the id of the card being patched + }, + description: { + type: 'string', + }, + ...patchSpec, }, - ...patchSpec, }, }, - required: ['attributes', 'description'], + required: ['attributes'], }, }, }; From d8ba2477658c0f4950cdf7198b8767ff57703acf Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Thu, 14 Nov 2024 15:06:53 +0000 Subject: [PATCH 27/40] Update to patch structure --- .../ai-bot/tests/prompt-construction-test.ts | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index b3b0246011..33fd8b80df 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -964,23 +964,26 @@ module('getModifyPrompt', () => { type: 'object', properties: { attributes: { - cardId: { - type: 'string', - const: 'http://localhost:4201/experiments/Friend/1', - }, - description: { - type: 'string', - }, - patch: { - attributes: { - firstName: { - type: 'string', + type: 'object', + properties: { + cardId: { + type: 'string', + const: 'http://localhost:4201/experiments/Friend/1', + }, + description: { + type: 'string', + }, + patch: { + attributes: { + firstName: { + type: 'string', + }, }, }, }, }, }, - required: ['attributes', 'description'], + required: ['attributes'], }, }, }); @@ -1065,21 +1068,24 @@ module('getModifyPrompt', () => { type: 'object', properties: { attributes: { - cardId: { - type: 'string', - const: 'http://localhost:4201/experiments/Meeting/2', - }, - description: { - type: 'string', - }, - attributes: { - location: { + type: 'object', + properties: { + cardId: { + type: 'string', + const: 'http://localhost:4201/experiments/Meeting/2', + }, + description: { type: 'string', }, + attributes: { + location: { + type: 'string', + }, + }, }, }, }, - required: ['attributes', 'description'], + required: ['attributes'], }, }, }); From 9f0e8f0abea7de49b7c77cffc0a070d28f137048 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 14 Nov 2024 15:37:57 -0500 Subject: [PATCH 28/40] Fix lint and patchCard issues --- packages/host/app/services/card-service.ts | 2 +- packages/matrix/tests/commands.spec.ts | 126 +++++++++++---------- packages/runtime-common/helpers/ai.ts | 26 +++-- 3 files changed, 88 insertions(+), 66 deletions(-) diff --git a/packages/host/app/services/card-service.ts b/packages/host/app/services/card-service.ts index 5c4e12623f..78824f7035 100644 --- a/packages/host/app/services/card-service.ts +++ b/packages/host/app/services/card-service.ts @@ -200,7 +200,7 @@ export default class CardService extends Service { async saveModel( owner: T, card: CardDef, - defaultRealmHref: string | undefined, + defaultRealmHref?: string, ): Promise { let cardChanged = false; function onCardChange() { diff --git a/packages/matrix/tests/commands.spec.ts b/packages/matrix/tests/commands.spec.ts index 55309c563f..152385d244 100644 --- a/packages/matrix/tests/commands.spec.ts +++ b/packages/matrix/tests/commands.spec.ts @@ -48,20 +48,22 @@ test.describe('Commands', () => { expect(message.content.msgtype).toStrictEqual('org.boxel.message'); let boxelMessageData = JSON.parse(message.content.data); - expect(boxelMessageData.context.tools).toMatchObject([ - { - type: 'function', - function: { - name: 'patchCard', - description: - 'Propose a patch to an existing card to change its contents. Any attributes specified will be fully replaced, return the minimum required to make the change. If a relationship field value is removed, set the self property of the specific item to null. When editing a relationship array, display the full array in the patch code. Ensure the description explains what change you are making.', - parameters: { - type: 'object', - properties: { - description: { - type: 'string', - }, - attributes: { + expect(boxelMessageData.context.tools.length).toEqual(3); + let patchCardTool = boxelMessageData.context.tools.find( + (t: any) => t.function.name === 'patchCard', + ); + expect(patchCardTool).toMatchObject({ + type: 'function', + function: { + name: 'patchCard', + description: + 'Propose a patch to an existing card to change its contents. Any attributes specified will be fully replaced, return the minimum required to make the change. If a relationship field value is removed, set the self property of the specific item to null. When editing a relationship array, display the full array in the patch code. Ensure the description explains what change you are making.', + parameters: { + type: 'object', + properties: { + attributes: { + type: 'object', + properties: { cardId: { type: 'string', const: `${testHost}/mango`, @@ -93,23 +95,29 @@ test.describe('Commands', () => { }, }, }, - required: ['attributes', 'description'], }, + required: ['attributes', 'description'], }, }, - { - type: 'function', - function: { - name: 'searchCard', - description: - 'Propose a query to search for a card instance filtered by type. If a card was shared with you, always prioritise search based upon the card that was last shared. If you do not have information on card module and name, do the search using the `_cardType` attribute.', - parameters: { - type: 'object', - properties: { - description: { - type: 'string', - }, - attributes: { + }); + let searchCardTool = boxelMessageData.context.tools.find( + (t: any) => t.function.name === 'searchCard', + ); + expect(searchCardTool).toMatchObject({ + type: 'function', + function: { + name: 'searchCard', + description: + 'Propose a query to search for a card instance filtered by type. If a card was shared with you, always prioritise search based upon the card that was last shared. If you do not have information on card module and name, do the search using the `_cardType` attribute.', + parameters: { + type: 'object', + properties: { + description: { + type: 'string', + }, + attributes: { + type: 'object', + properties: { filter: { type: 'object', properties: { @@ -137,42 +145,45 @@ test.describe('Commands', () => { }, }, }, - required: ['attributes', 'description'], }, + required: ['attributes', 'description'], }, }, - { - type: 'function', - function: { - name: 'generateAppModule', - description: `Propose a post request to generate a new app module. Insert the module code in the 'moduleCode' property of the payload and the title for the module in the 'appTitle' property. Ensure the description explains what change you are making.`, - parameters: { - type: 'object', - properties: { - attached_card_id: { - type: 'string', - const: `${testHost}/mango`, - }, - description: { - type: 'string', - }, - appTitle: { - type: 'string', - }, - moduleCode: { - type: 'string', - }, + }); + let generateAppModuleTool = boxelMessageData.context.tools.find( + (t: any) => t.function.name === 'generateAppModule', + ); + expect(generateAppModuleTool).toMatchObject({ + type: 'function', + function: { + name: 'generateAppModule', + description: `Propose a post request to generate a new app module. Insert the module code in the 'moduleCode' property of the payload and the title for the module in the 'appTitle' property. Ensure the description explains what change you are making.`, + parameters: { + type: 'object', + properties: { + attached_card_id: { + type: 'string', + const: `${testHost}/mango`, + }, + description: { + type: 'string', + }, + appTitle: { + type: 'string', + }, + moduleCode: { + type: 'string', }, - required: [ - 'attached_card_id', - 'description', - 'appTitle', - 'moduleCode', - ], }, + required: [ + 'attached_card_id', + 'description', + 'appTitle', + 'moduleCode', + ], }, }, - ]); + }); }); test(`it does not include patch tool in message event for an open card that is not attached`, async ({ @@ -247,6 +258,7 @@ test.describe('Commands', () => { arguments: { attributes: { cardId, + description: 'Patching card', patch: { attributes: { firstName: 'Dave', diff --git a/packages/runtime-common/helpers/ai.ts b/packages/runtime-common/helpers/ai.ts index 012e11f4ce..4812432b8b 100644 --- a/packages/runtime-common/helpers/ai.ts +++ b/packages/runtime-common/helpers/ai.ts @@ -391,6 +391,9 @@ export function getPatchTool( parameters: { type: 'object', properties: { + description: { + type: 'string', + }, attributes: { type: 'object', properties: { @@ -398,14 +401,16 @@ export function getPatchTool( type: 'string', const: attachedOpenCardId, // Force the valid card_id to be the id of the card being patched }, - description: { - type: 'string', + patch: { + type: 'object', + properties: { + ...patchSpec, + }, }, - ...patchSpec, }, }, }, - required: ['attributes'], + required: ['attributes', 'description'], }, }, }; @@ -445,15 +450,20 @@ export function getSearchTool() { description: { type: 'string', }, - filter: { + attributes: { type: 'object', properties: { - contains: containsFilterProperty, - eq: eqCardTypeFilterProperty, + filter: { + type: 'object', + properties: { + contains: containsFilterProperty, + eq: eqCardTypeFilterProperty, + }, + }, }, }, }, - required: ['filter', 'description'], + required: ['attributes', 'description'], }, }, }; From 6bfefb8d6a826b5e8b08116190820ae25b26c45c Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 14 Nov 2024 15:57:11 -0500 Subject: [PATCH 29/40] Lint fix --- packages/host/app/resources/room.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/host/app/resources/room.ts b/packages/host/app/resources/room.ts index 4309bd64c3..9e6019ddfe 100644 --- a/packages/host/app/resources/room.ts +++ b/packages/host/app/resources/room.ts @@ -11,7 +11,6 @@ import type { LooseSingleCardDocument } from '@cardstack/runtime-common'; import { CommandStatus } from 'https://cardstack.com/base/command'; import type { CardFragmentContent, - CardMessageContent, CommandEvent, CommandResultEvent, MatrixEvent as DiscreteMatrixEvent, From cf5dff027e67c04d019aae809b6b9dd21fe5ab09 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 14 Nov 2024 16:18:49 -0500 Subject: [PATCH 30/40] Test fixes --- .../ai-bot/tests/prompt-construction-test.ts | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index 33fd8b80df..9a236a60f9 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -15,7 +15,7 @@ import type { Tool, CardMessageContent, } from 'https://cardstack.com/base/matrix-event'; -import { EventStatus } from 'matrix-js-sdk'; +import { EventStatus, IRoomEvent } from 'matrix-js-sdk'; import type { SingleCardDocument } from '@cardstack/runtime-common'; import { CardDef } from 'https://cardstack.com/base/card-api'; @@ -930,8 +930,9 @@ module('getModifyPrompt', () => { openCardIds: ['http://localhost:4201/experiments/Friend/1'], tools: [ getPatchTool('http://localhost:4201/experiments/Friend/1', { - patch: { - attributes: { + attributes: { + type: 'object', + properties: { firstName: { type: 'string' }, }, }, @@ -963,6 +964,9 @@ module('getModifyPrompt', () => { parameters: { type: 'object', properties: { + description: { + type: 'string', + }, attributes: { type: 'object', properties: { @@ -970,20 +974,23 @@ module('getModifyPrompt', () => { type: 'string', const: 'http://localhost:4201/experiments/Friend/1', }, - description: { - type: 'string', - }, patch: { - attributes: { - firstName: { - type: 'string', + type: 'object', + properties: { + attributes: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + }, }, }, }, }, }, }, - required: ['attributes'], + required: ['attributes', 'description'], }, }, }); @@ -1036,7 +1043,10 @@ module('getModifyPrompt', () => { tools: [ getPatchTool('http://localhost:4201/experiments/Meeting/2', { attributes: { - location: { type: 'string' }, + type: 'object', + properties: { + location: { type: 'string' }, + }, }, }), ], @@ -1067,6 +1077,9 @@ module('getModifyPrompt', () => { parameters: { type: 'object', properties: { + description: { + type: 'string', + }, attributes: { type: 'object', properties: { @@ -1074,18 +1087,23 @@ module('getModifyPrompt', () => { type: 'string', const: 'http://localhost:4201/experiments/Meeting/2', }, - description: { - type: 'string', - }, - attributes: { - location: { - type: 'string', + patch: { + type: 'object', + properties: { + attributes: { + type: 'object', + properties: { + location: { + type: 'string', + }, + }, + }, }, }, }, }, }, - required: ['attributes'], + required: ['attributes', 'description'], }, }, }); From 0f308db5aadb5967b6c4e3a2dbab3f5a3f15d304 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 14 Nov 2024 17:00:06 -0500 Subject: [PATCH 31/40] Remove extraneous logging --- packages/ai-bot/helpers.ts | 5 ----- .../integration/components/resizable-panel-group-test.gts | 1 - packages/catalog-realm/ai-app-generator.gts | 1 - packages/host/app/services/command-service.ts | 5 ----- .../integration/components/prerendered-card-search-test.gts | 1 - packages/host/tests/integration/resources/search-test.ts | 1 - 6 files changed, 14 deletions(-) diff --git a/packages/ai-bot/helpers.ts b/packages/ai-bot/helpers.ts index ab574e43f1..5ac0211855 100644 --- a/packages/ai-bot/helpers.ts +++ b/packages/ai-bot/helpers.ts @@ -427,7 +427,6 @@ export function getModifyPrompt( mostRecentlyAttachedCard, attachedCards, ); - console.log('skillCards', skillCards); if (skillCards.length) { systemMessage += SKILL_INSTRUCTIONS_MESSAGE; systemMessage += skillCardsToMessage(skillCards); @@ -468,10 +467,6 @@ export const attachedCardsToMessage = ( }; export const skillCardsToMessage = (cards: CardResource[]) => { - for (let card of cards) { - console.log('card', card); - console.log('card.attributes', card.attributes); - } return `${JSON.stringify( cards.map((card) => card.attributes?.instructions), )}`; diff --git a/packages/boxel-ui/test-app/tests/integration/components/resizable-panel-group-test.gts b/packages/boxel-ui/test-app/tests/integration/components/resizable-panel-group-test.gts index 18187bda59..feec0a96f6 100644 --- a/packages/boxel-ui/test-app/tests/integration/components/resizable-panel-group-test.gts +++ b/packages/boxel-ui/test-app/tests/integration/components/resizable-panel-group-test.gts @@ -121,7 +121,6 @@ orientationPropertiesToTest.forEach((orientationProperties) => { max-${orientationProperties.dimension}: 100%; } `; - console.log(renderController.panels[1]); await render(