Skip to content

Commit b40c860

Browse files
committedFeb 13, 2025
Convert tool calls to CommandRequests on boxel message events, and support multiple command requests per message
1 parent 22d317b commit b40c860

18 files changed

+544
-418
lines changed
 

‎packages/ai-bot/.eslintrc.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
module.exports = {
4+
root: true,
5+
parser: '@typescript-eslint/parser',
6+
parserOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: 'module',
9+
ecmaFeatures: {
10+
legacyDecorators: true,
11+
},
12+
},
13+
plugins: ['ember'],
14+
extends: [
15+
'eslint:recommended',
16+
'plugin:@typescript-eslint/recommended',
17+
'plugin:prettier/recommended',
18+
],
19+
rules: {
20+
// this doesn't work well with the monorepo. Typescript already complains if you try to import something that's not found
21+
'import/no-unresolved': 'off',
22+
'prefer-const': 'off',
23+
'@typescript-eslint/ban-ts-comment': 'off',
24+
'@typescript-eslint/ban-types': 'off',
25+
'@typescript-eslint/prefer-as-const': 'off',
26+
'@typescript-eslint/no-explicit-any': 'off',
27+
'@typescript-eslint/no-non-null-assertion': 'off',
28+
'@typescript-eslint/no-unused-vars': [
29+
'error',
30+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
31+
],
32+
},
33+
};

‎packages/ai-bot/helpers.ts

+69-68
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,31 @@ import {
33
type LooseSingleCardDocument,
44
type CardResource,
55
} from '@cardstack/runtime-common';
6-
import { ToolChoice } from '@cardstack/runtime-common/helpers/ai';
6+
import {
7+
CommandRequestContent,
8+
ToolChoice,
9+
} from '@cardstack/runtime-common/helpers/ai';
710
import type {
811
MatrixEvent as DiscreteMatrixEvent,
912
CardFragmentContent,
10-
CommandEvent,
1113
Tool,
1214
SkillsConfigEvent,
1315
ActiveLLMEvent,
16+
CardMessageEvent,
1417
CommandResultEvent,
1518
} from 'https://cardstack.com/base/matrix-event';
1619
import { MatrixEvent, type IRoomEvent } from 'matrix-js-sdk';
1720
import { ChatCompletionMessageToolCall } from 'openai/resources/chat/completions';
1821
import * as Sentry from '@sentry/node';
1922
import { logger } from '@cardstack/runtime-common';
2023
import {
24+
APP_BOXEL_COMMAND_REQUESTS_KEY,
2125
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
2226
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE,
2327
} from '../runtime-common/matrix-constants';
2428
import {
2529
APP_BOXEL_CARDFRAGMENT_MSGTYPE,
2630
APP_BOXEL_MESSAGE_MSGTYPE,
27-
APP_BOXEL_COMMAND_MSGTYPE,
2831
APP_BOXEL_ROOM_SKILLS_EVENT_TYPE,
2932
DEFAULT_LLM,
3033
APP_BOXEL_ACTIVE_LLM,
@@ -169,8 +172,8 @@ export function constructHistory(
169172
if (event.content.msgtype === APP_BOXEL_MESSAGE_MSGTYPE) {
170173
let { attachedCardsEventIds } = event.content.data;
171174
if (attachedCardsEventIds && attachedCardsEventIds.length > 0) {
172-
event.content.data.attachedCards = attachedCardsEventIds.map((id) =>
173-
serializedCardFromFragments(id, cardFragments),
175+
event.content.data.attachedCards = attachedCardsEventIds.map(
176+
(id: string) => serializedCardFromFragments(id, cardFragments),
174177
);
175178
}
176179
}
@@ -377,57 +380,69 @@ export function getToolChoice(
377380
return 'auto';
378381
}
379382

380-
function getCommandResult(
381-
commandEvent: CommandEvent,
383+
function getCommandResults(
384+
cardMessageEvent: CardMessageEvent,
382385
history: DiscreteMatrixEvent[],
383386
) {
384-
let commandResultEvent = history.find((e) => {
387+
let commandResultEvents = history.filter((e) => {
385388
if (
386389
isCommandResultEvent(e) &&
387-
e.content['m.relates_to']?.event_id === commandEvent.event_id
390+
e.content['m.relates_to']?.event_id === cardMessageEvent.event_id
388391
) {
389392
return true;
390393
}
391394
return false;
392-
}) as CommandResultEvent | undefined;
393-
return commandResultEvent;
395+
}) as CommandResultEvent[];
396+
return commandResultEvents;
394397
}
395398

396-
function toToolCall(event: CommandEvent): ChatCompletionMessageToolCall {
397-
return {
398-
id: event.content.data.toolCall.id,
399-
function: {
400-
name: event.content.data.toolCall.name,
401-
arguments: JSON.stringify(event.content.data.toolCall.arguments),
399+
function toToolCalls(event: CardMessageEvent): ChatCompletionMessageToolCall[] {
400+
return (event.content[APP_BOXEL_COMMAND_REQUESTS_KEY] ?? []).map(
401+
(commandRequest: CommandRequestContent) => {
402+
return {
403+
id: commandRequest.id,
404+
function: {
405+
name: commandRequest.name,
406+
arguments: JSON.stringify(commandRequest.arguments),
407+
},
408+
type: 'function',
409+
};
402410
},
403-
type: 'function',
404-
};
411+
);
405412
}
406413

407-
function toPromptMessageWithToolResult(
408-
event: CommandEvent,
414+
function toPromptMessageWithToolResults(
415+
event: CardMessageEvent,
409416
history: DiscreteMatrixEvent[],
410-
): OpenAIPromptMessage {
411-
let commandResult = getCommandResult(event, history);
412-
let content = 'pending';
413-
if (commandResult) {
414-
let status = commandResult.content['m.relates_to']?.key;
415-
if (
416-
commandResult.content.msgtype ===
417-
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE
418-
) {
419-
content = `Command ${status}, with result card: ${JSON.stringify(
420-
commandResult.content.data.card,
421-
)}.\n`;
422-
} else {
423-
content = `Command ${status}.\n`;
424-
}
425-
}
426-
return {
427-
role: 'tool',
428-
content,
429-
tool_call_id: event.content.data.toolCall.id,
430-
};
417+
): OpenAIPromptMessage[] {
418+
let commandResults = getCommandResults(event, history);
419+
return (event.content[APP_BOXEL_COMMAND_REQUESTS_KEY] ?? []).map(
420+
(commandRequest: CommandRequestContent) => {
421+
let content = 'pending';
422+
let commandResult = commandResults.find(
423+
(commandResult) =>
424+
commandResult.content.data.commandRequestId === commandRequest.id,
425+
);
426+
if (commandResult) {
427+
let status = commandResult.content['m.relates_to']?.key;
428+
if (
429+
commandResult.content.msgtype ===
430+
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE
431+
) {
432+
content = `Command ${status}, with result card: ${JSON.stringify(
433+
commandResult.content.data.card,
434+
)}.\n`;
435+
} else {
436+
content = `Command ${status}.\n`;
437+
}
438+
}
439+
return {
440+
role: 'tool',
441+
tool_call_id: commandRequest.id,
442+
content,
443+
};
444+
},
445+
);
431446
}
432447

433448
export function getModifyPrompt(
@@ -460,20 +475,19 @@ export function getModifyPrompt(
460475
let body = event.content.body;
461476
if (body) {
462477
if (event.sender === aiBotUserId) {
463-
if (isCommandEvent(event)) {
464-
historicalMessages.push({
465-
role: 'assistant',
466-
content: body,
467-
tool_calls: [toToolCall(event)],
468-
});
469-
historicalMessages.push(
470-
toPromptMessageWithToolResult(event, history),
478+
let toolCalls = toToolCalls(event);
479+
let historicalMessage: OpenAIPromptMessage = {
480+
role: 'assistant',
481+
content: body,
482+
};
483+
if (toolCalls.length) {
484+
historicalMessage.tool_calls = toolCalls;
485+
}
486+
historicalMessages.push(historicalMessage);
487+
if (toolCalls.length) {
488+
toPromptMessageWithToolResults(event, history).forEach((message) =>
489+
historicalMessages.push(message),
471490
);
472-
} else {
473-
historicalMessages.push({
474-
role: 'assistant',
475-
content: body,
476-
});
477491
}
478492
} else {
479493
if (
@@ -570,19 +584,6 @@ export const isCommandResultStatusApplied = (event?: MatrixEvent) => {
570584
);
571585
};
572586

573-
export function isCommandEvent(
574-
event: DiscreteMatrixEvent,
575-
): event is CommandEvent {
576-
return (
577-
event.type === 'm.room.message' &&
578-
typeof event.content === 'object' &&
579-
event.content.msgtype === APP_BOXEL_COMMAND_MSGTYPE &&
580-
event.content.format === 'org.matrix.custom.html' &&
581-
typeof event.content.data === 'object' &&
582-
typeof event.content.data.toolCall === 'object'
583-
);
584-
}
585-
586587
function getModel(eventlist: DiscreteMatrixEvent[]): string {
587588
let activeLLMEvent = eventlist.findLast(
588589
(event) => event.type === APP_BOXEL_ACTIVE_LLM,

‎packages/ai-bot/lib/debug.ts

+9-17
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { setTitle } from './set-title';
2-
import {
3-
sendErrorEvent,
4-
sendCommandEvent,
5-
sendMessageEvent,
6-
MatrixClient,
7-
} from './matrix';
2+
import { sendErrorEvent, sendMessageEvent, MatrixClient } from './matrix';
83
import OpenAI from 'openai';
94

105
import * as Sentry from '@sentry/node';
@@ -62,19 +57,16 @@ export async function handleDebugCommands(
6257
roomId,
6358
`Error parsing your debug patch, ${error} ${patchMessage}`,
6459
undefined,
60+
{},
61+
[
62+
{
63+
id: 'patchCard-debug',
64+
name: 'patchCard',
65+
arguments: toolArguments,
66+
},
67+
],
6568
);
6669
}
67-
return await sendCommandEvent(
68-
client,
69-
roomId,
70-
{
71-
id: 'patchCard-debug',
72-
name: 'patchCard',
73-
type: 'function',
74-
arguments: toolArguments,
75-
},
76-
undefined,
77-
);
7870
}
7971
return;
8072
}

0 commit comments

Comments
 (0)