diff --git a/packages/ai-bot/README.md b/packages/ai-bot/README.md index c90213cd2b..9c39e91744 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/helpers.ts b/packages/ai-bot/helpers.ts index d6907ef583..5ac0211855 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,6 @@ export function getModifyPrompt( mostRecentlyAttachedCard, attachedCards, ); - if (skillCards.length) { systemMessage += SKILL_INSTRUCTIONS_MESSAGE; systemMessage += skillCardsToMessage(skillCards); 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/main.ts b/packages/ai-bot/main.ts index 910a3851ee..6431c8a378 100644 --- a/packages/ai-bot/main.ts +++ b/packages/ai-bot/main.ts @@ -44,16 +44,17 @@ class Assistant { getResponse(history: DiscreteMatrixEvent[]) { let tools = getTools(history, this.id); let messages = getModifyPrompt(history, this.id, tools); + if (tools.length === 0) { return this.openai.beta.chat.completions.stream({ model: 'openai/gpt-4o', - messages: messages, + messages, }); } else { return this.openai.beta.chat.completions.stream({ model: 'openai/gpt-4o', - messages: messages, - tools: tools, + messages, + tools, tool_choice: 'auto', }); } 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..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'; @@ -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', @@ -891,13 +893,13 @@ module('getModifyPrompt', () => { parameters: { type: 'object', properties: { - description: { - type: 'string', - }, card_id: { type: 'string', const: 'http://localhost:4201/experiments/Friend/1', }, + description: { + type: 'string', + }, attributes: { type: 'object', properties: { @@ -928,7 +930,12 @@ module('getModifyPrompt', () => { openCardIds: ['http://localhost:4201/experiments/Friend/1'], tools: [ getPatchTool('http://localhost:4201/experiments/Friend/1', { - firstName: { type: 'string' }, + attributes: { + type: 'object', + properties: { + firstName: { type: 'string' }, + }, + }, }), ], submode: 'interact', @@ -960,15 +967,30 @@ module('getModifyPrompt', () => { description: { type: 'string', }, - card_id: { - type: 'string', - const: 'http://localhost:4201/experiments/Friend/1', - }, - firstName: { - type: 'string', + attributes: { + type: 'object', + properties: { + cardId: { + type: 'string', + const: 'http://localhost:4201/experiments/Friend/1', + }, + patch: { + type: 'object', + properties: { + attributes: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + }, + }, + }, + }, + }, }, }, - required: ['card_id', 'attributes', 'description'], + required: ['attributes', 'description'], }, }, }); @@ -989,7 +1011,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 +1042,12 @@ module('getModifyPrompt', () => { openCardIds: ['http://localhost:4201/experiments/Meeting/2'], tools: [ getPatchTool('http://localhost:4201/experiments/Meeting/2', { - location: { type: 'string' }, + attributes: { + type: 'object', + properties: { + location: { type: 'string' }, + }, + }, }), ], submode: 'interact', @@ -1048,18 +1077,33 @@ module('getModifyPrompt', () => { parameters: { type: 'object', properties: { - card_id: { - type: 'string', - const: 'http://localhost:4201/experiments/Meeting/2', - }, description: { type: 'string', }, - location: { - type: 'string', + attributes: { + type: 'object', + properties: { + cardId: { + type: 'string', + const: 'http://localhost:4201/experiments/Meeting/2', + }, + patch: { + type: 'object', + properties: { + attributes: { + type: 'object', + properties: { + location: { + type: 'string', + }, + }, + }, + }, + }, + }, }, }, - required: ['card_id', 'attributes', 'description'], + required: ['attributes', 'description'], }, }, }); @@ -1867,11 +1911,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..5efcae9e4f 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', + patch: { + attributes: { + some: 'thing', + }, + }, + }, }; await responder.initialize(); @@ -225,10 +231,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', + patch: { + attributes: { + some: 'thing', + }, + }, }, }, }, @@ -252,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', + patch: { + attributes: { + some: 'thing', + }, + }, + }, }; await responder.initialize(); @@ -293,10 +309,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', + patch: { + attributes: { + some: 'thing', + }, + }, }, }, }, 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-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/base/command.gts b/packages/base/command.gts index 931daad66b..5c183af9c5 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,40 @@ 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); +} + +class JsonField extends FieldDef { + static [primitive]: Record; +} + +export class PatchCardInput extends CardDef { + @field cardId = contains(StringField); + @field patch = contains(JsonField); //TODO: JSONField ? +} + +export class ShowCardInput extends CardDef { + @field cardToShow = linksTo(CardDef); +} + +export class SwitchSubmodeInput extends CardDef { + @field submode = contains(StringField); +} + +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/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..41834d1352 100644 --- a/packages/base/matrix-event.gts +++ b/packages/base/matrix-event.gts @@ -2,7 +2,7 @@ import { LooseSingleCardDocument } from '@cardstack/runtime-common'; import type { EventStatus, MatrixError } from 'matrix-js-sdk'; import { FunctionToolCall, - type Schema, + type AttributesSchema, } from '@cardstack/runtime-common/helpers/ai'; interface BaseMatrixEvent { @@ -166,7 +166,7 @@ export interface Tool { function: { name: string; description: string; - parameters: Schema; + parameters: AttributesSchema; }; } @@ -196,7 +196,7 @@ export interface CardMessageContent { context: { openCardIds?: string[]; tools: Tool[]; - submode: string | undefined; + submode?: string; }; }; } 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(