Skip to content

Commit f68e044

Browse files
committed
Merge branch 'main' into cs-7598-ai-assistant-chat-should-automatically-display-return-value
2 parents 7e85f7c + 4ae59cc commit f68e044

File tree

277 files changed

+13386
-2851
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

277 files changed

+13386
-2851
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
"deploy:boxel-motion:preview-staging": "cd packages/boxel-motion/addon && pnpm build && cd ../test-app && pnpm exec ember deploy s3-preview-staging --verbose",
1616
"deploy:boxel-ui": "pnpm run build-common-deps && cd packages/boxel-ui/test-app && pnpm exec ember deploy",
1717
"deploy:boxel-ui:preview-staging": "pnpm run build-common-deps && cd packages/boxel-ui/test-app && pnpm exec ember deploy s3-preview-staging --verbose",
18-
"lint": "pnpm run --filter './packages/**' --if-present -r lint",
19-
"lint:fix": "pnpm run --filter './packages/**' --if-present -r lint:fix"
18+
"lint": "pnpm run --filter './packages/**' --filter '!./packages/boxel-motion/**' --if-present -r lint",
19+
"lint:fix": "pnpm run --filter './packages/**' --filter '!./packages/boxel-motion/**' --if-present -r lint:fix"
2020
},
2121
"pnpm": {
2222
"allowedDeprecatedVersions": {

packages/ai-bot/helpers.ts

+67-29
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
type LooseSingleCardDocument,
44
type CardResource,
55
} from '@cardstack/runtime-common';
6-
import { getSearchTool } from '@cardstack/runtime-common/helpers/ai';
6+
import { ToolChoice } from '@cardstack/runtime-common/helpers/ai';
77
import type {
88
MatrixEvent as DiscreteMatrixEvent,
99
CardFragmentContent,
@@ -17,6 +17,13 @@ import { MatrixEvent, type IRoomEvent } from 'matrix-js-sdk';
1717
import { ChatCompletionMessageToolCall } from 'openai/resources/chat/completions';
1818
import * as Sentry from '@sentry/node';
1919
import { logger } from '@cardstack/runtime-common';
20+
import {
21+
APP_BOXEL_CARDFRAGMENT_MSGTYPE,
22+
APP_BOXEL_MESSAGE_MSGTYPE,
23+
APP_BOXEL_COMMAND_MSGTYPE,
24+
APP_BOXEL_COMMAND_RESULT_MSGTYPE,
25+
APP_BOXEL_ROOM_SKILLS_EVENT_TYPE,
26+
} from '@cardstack/runtime-common/matrix-constants';
2027

2128
let log = logger('ai-bot');
2229

@@ -51,7 +58,7 @@ export interface PromptParts {
5158
messages: OpenAIPromptMessage[];
5259
model: string;
5360
history: DiscreteMatrixEvent[];
54-
toolChoice: 'auto' | 'none';
61+
toolChoice: ToolChoice;
5562
}
5663

5764
export type Message = CommandMessage | TextMessage;
@@ -75,13 +82,14 @@ export function getPromptParts(
7582
);
7683
let skills = getEnabledSkills(eventList, cardFragments);
7784
let tools = getTools(history, aiBotUserId);
85+
let toolChoice = getToolChoice(history, aiBotUserId);
7886
let messages = getModifyPrompt(history, aiBotUserId, tools, skills);
7987
return {
8088
tools,
8189
messages,
8290
model: 'openai/gpt-4o',
8391
history,
84-
toolChoice: 'auto',
92+
toolChoice: toolChoice,
8593
};
8694
}
8795

@@ -91,7 +99,7 @@ export function extractCardFragmentsFromEvents(
9199
const fragments = new Map<string, CardFragmentContent>(); // eventId => fragment
92100
for (let event of eventList) {
93101
if (event.type === 'm.room.message') {
94-
if (event.content.msgtype === 'org.boxel.cardFragment') {
102+
if (event.content.msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE) {
95103
fragments.set(event.event_id, event.content as CardFragmentContent);
96104
}
97105
}
@@ -136,10 +144,10 @@ export function constructHistory(
136144
continue;
137145
}
138146
let eventId = event.event_id!;
139-
if (event.content.msgtype === 'org.boxel.cardFragment') {
147+
if (event.content.msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE) {
140148
continue;
141149
}
142-
if (event.content.msgtype === 'org.boxel.message') {
150+
if (event.content.msgtype === APP_BOXEL_MESSAGE_MSGTYPE) {
143151
let { attachedCardsEventIds } = event.content.data;
144152
if (attachedCardsEventIds && attachedCardsEventIds.length > 0) {
145153
event.content.data.attachedCards = attachedCardsEventIds.map((id) =>
@@ -174,7 +182,7 @@ function getEnabledSkills(
174182
cardFragments: Map<string, CardFragmentContent>,
175183
): LooseCardResource[] {
176184
let skillsConfigEvent = eventlist.findLast(
177-
(event) => event.type === 'com.cardstack.boxel.room.skills',
185+
(event) => event.type === APP_BOXEL_ROOM_SKILLS_EVENT_TYPE,
178186
) as SkillsConfigEvent;
179187
if (!skillsConfigEvent) {
180188
return [];
@@ -272,7 +280,7 @@ export function getRelevantCards(
272280
}
273281
if (event.sender !== aiBotUserId) {
274282
let { content } = event;
275-
if (content.msgtype === 'org.boxel.message') {
283+
if (content.msgtype === APP_BOXEL_MESSAGE_MSGTYPE) {
276284
setRelevantCards(attachedCardMap, content.data?.attachedCards);
277285
if (content.data?.attachedCards) {
278286
mostRecentlyAttachedCard = getMostRecentlyAttachedCard(
@@ -297,27 +305,57 @@ export function getTools(
297305
history: DiscreteMatrixEvent[],
298306
aiBotUserId: string,
299307
): Tool[] {
300-
// TODO: there should be no default tools defined in the ai-bot, tools must be determined by the host
301-
let searchTool = getSearchTool();
302-
let tools = [searchTool as Tool];
303-
// Just get the users messages
304-
const userMessages = history.filter((event) => event.sender !== aiBotUserId);
305-
// Get the last message
306-
if (userMessages.length === 0) {
307-
// If the user has sent no messages, return tools that are available by default
308-
return tools;
308+
// Build map directly from messages
309+
let toolMap = new Map<string, Tool>();
310+
for (let event of history) {
311+
if (event.type !== 'm.room.message' || event.sender == aiBotUserId) {
312+
continue;
313+
}
314+
if (event.content.msgtype === APP_BOXEL_MESSAGE_MSGTYPE) {
315+
let eventTools = event.content.data.context.tools;
316+
if (eventTools?.length) {
317+
for (let tool of eventTools) {
318+
toolMap.set(tool.function.name, tool);
319+
}
320+
}
321+
}
309322
}
310-
const lastMessage = userMessages[userMessages.length - 1];
323+
return Array.from(toolMap.values()).sort((a, b) =>
324+
a.function.name.localeCompare(b.function.name),
325+
);
326+
}
327+
328+
export function getToolChoice(
329+
history: DiscreteMatrixEvent[],
330+
aiBotUserId: string,
331+
): ToolChoice {
332+
const lastUserMessage = history.findLast(
333+
(event) => event.sender !== aiBotUserId,
334+
);
335+
311336
if (
312-
lastMessage.type === 'm.room.message' &&
313-
lastMessage.content.msgtype === 'org.boxel.message' &&
314-
lastMessage.content.data?.context?.tools?.length
337+
!lastUserMessage ||
338+
lastUserMessage.type !== 'm.room.message' ||
339+
lastUserMessage.content.msgtype !== APP_BOXEL_MESSAGE_MSGTYPE
315340
) {
316-
return lastMessage.content.data.context.tools;
317-
} else {
318-
// If it's a different message type, or there are no tools, return tools that are available by default
319-
return tools;
341+
// If the last message is not a user message, auto is safe
342+
return 'auto';
343+
}
344+
345+
const messageContext = lastUserMessage.content.data.context;
346+
if (messageContext?.requireToolCall) {
347+
let tools = messageContext.tools || [];
348+
if (tools.length != 1) {
349+
throw new Error('Forced tool calls only work with a single tool');
350+
}
351+
return {
352+
type: 'function',
353+
function: {
354+
name: tools[0].function.name,
355+
},
356+
};
320357
}
358+
return 'auto';
321359
}
322360

323361
export function isCommandResultEvent(
@@ -326,7 +364,7 @@ export function isCommandResultEvent(
326364
return (
327365
event.type === 'm.room.message' &&
328366
typeof event.content === 'object' &&
329-
event.content.msgtype === 'org.boxel.commandResult'
367+
event.content.msgtype === APP_BOXEL_COMMAND_RESULT_MSGTYPE
330368
);
331369
}
332370

@@ -449,7 +487,7 @@ export function getModifyPrompt(
449487
}
450488
} else {
451489
if (
452-
event.content.msgtype === 'org.boxel.message' &&
490+
event.content.msgtype === APP_BOXEL_MESSAGE_MSGTYPE &&
453491
event.content.data?.context?.openCardIds
454492
) {
455493
body = `User message: ${body}
@@ -489,7 +527,7 @@ export function getModifyPrompt(
489527

490528
if (tools.length == 0) {
491529
systemMessage +=
492-
'You are unable to edit any cards, the user has not given you access, they need to open the card on the stack and let it be auto-attached. However, you are allowed to search for cards.';
530+
'You are unable to edit any cards, the user has not given you access, they need to open the card on the stack and let it be auto-attached.';
493531
}
494532

495533
let messages: OpenAIPromptMessage[] = [
@@ -559,7 +597,7 @@ export function isCommandEvent(
559597
return (
560598
event.type === 'm.room.message' &&
561599
typeof event.content === 'object' &&
562-
event.content.msgtype === 'org.boxel.command' &&
600+
event.content.msgtype === APP_BOXEL_COMMAND_MSGTYPE &&
563601
event.content.format === 'org.matrix.custom.html' &&
564602
typeof event.content.data === 'object' &&
565603
typeof event.content.data.toolCall === 'object'

packages/ai-bot/lib/matrix.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { logger } from '@cardstack/runtime-common';
33
import { OpenAIError } from 'openai/error';
44
import * as Sentry from '@sentry/node';
55
import { FunctionToolCall } from '@cardstack/runtime-common/helpers/ai';
6+
import { APP_BOXEL_COMMAND_MSGTYPE } from '@cardstack/runtime-common/matrix-constants';
67

78
let log = logger('ai-bot');
89

@@ -129,7 +130,7 @@ export const toMatrixMessageCommandContent = (
129130
const body = payload['description'] || 'Issuing command';
130131
let messageObject: IContent = {
131132
body: body,
132-
msgtype: 'org.boxel.command',
133+
msgtype: APP_BOXEL_COMMAND_MSGTYPE,
133134
formatted_body: body,
134135
format: 'org.matrix.custom.html',
135136
data: {

packages/ai-bot/main.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
getPromptParts,
1616
extractCardFragmentsFromEvents,
1717
} from './helpers';
18+
import { APP_BOXEL_CARDFRAGMENT_MSGTYPE } from '@cardstack/runtime-common/matrix-constants';
19+
1820
import {
1921
shouldSetRoomTitle,
2022
setTitle,
@@ -167,7 +169,7 @@ Common issues are:
167169
if (event.getType() !== 'm.room.message') {
168170
return; // only print messages
169171
}
170-
if (event.getContent().msgtype === 'org.boxel.cardFragment') {
172+
if (event.getContent().msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE) {
171173
return; // don't respond to card fragments, we just gather these in our history
172174
}
173175

packages/ai-bot/tests/chat-titling-test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { module, test, assert } from 'qunit';
22
import { shouldSetRoomTitle } from '../lib/set-title';
33
import type { MatrixEvent as DiscreteMatrixEvent } from 'https://cardstack.com/base/matrix-event';
4+
import { APP_BOXEL_COMMAND_MSGTYPE } from '@cardstack/runtime-common/matrix-constants';
45

56
module('shouldSetRoomTitle', () => {
67
test('Do not set a title when there is no content', () => {
@@ -370,7 +371,7 @@ module('shouldSetRoomTitle', () => {
370371
event_id: '2',
371372
origin_server_ts: 1234567890,
372373
content: {
373-
msgtype: 'org.boxel.command',
374+
msgtype: APP_BOXEL_COMMAND_MSGTYPE,
374375
format: 'org.matrix.custom.html',
375376
body: 'patching card',
376377
formatted_body: 'patching card',
@@ -439,7 +440,7 @@ module('shouldSetRoomTitle', () => {
439440
event_id: '2',
440441
origin_server_ts: 1234567890,
441442
content: {
442-
msgtype: 'org.boxel.command',
443+
msgtype: APP_BOXEL_COMMAND_MSGTYPE,
443444
format: 'org.matrix.custom.html',
444445
body: 'patching card',
445446
formatted_body: 'patching card',

packages/ai-bot/tests/history-construction-test.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import {
44
extractCardFragmentsFromEvents,
55
HistoryConstructionError,
66
} from '../helpers';
7+
import {
8+
APP_BOXEL_CARD_FORMAT,
9+
APP_BOXEL_CARDFRAGMENT_MSGTYPE,
10+
APP_BOXEL_MESSAGE_MSGTYPE,
11+
} from '@cardstack/runtime-common/matrix-constants';
12+
713
import { type IRoomEvent } from 'matrix-js-sdk';
814
import type { MatrixEvent as DiscreteMatrixEvent } from 'https://cardstack.com/base/matrix-event';
915

@@ -383,8 +389,8 @@ module('constructHistory', () => {
383389
event_id: '1',
384390
origin_server_ts: 1234567900,
385391
content: {
386-
msgtype: 'org.boxel.cardFragment',
387-
format: 'org.boxel.card',
392+
msgtype: APP_BOXEL_CARDFRAGMENT_MSGTYPE,
393+
format: APP_BOXEL_CARD_FORMAT,
388394
formatted_body: '',
389395
body: '',
390396
data: JSON.stringify({
@@ -405,8 +411,8 @@ module('constructHistory', () => {
405411
event_id: '2',
406412
origin_server_ts: 1234567890,
407413
content: {
408-
msgtype: 'org.boxel.cardFragment',
409-
format: 'org.boxel.card',
414+
msgtype: APP_BOXEL_CARDFRAGMENT_MSGTYPE,
415+
format: APP_BOXEL_CARD_FORMAT,
410416
formatted_body: '',
411417
body: '',
412418
data: JSON.stringify({
@@ -428,8 +434,8 @@ module('constructHistory', () => {
428434
event_id: '3',
429435
origin_server_ts: 1234567910,
430436
content: {
431-
msgtype: 'org.boxel.cardFragment',
432-
format: 'org.boxel.card',
437+
msgtype: APP_BOXEL_CARDFRAGMENT_MSGTYPE,
438+
format: APP_BOXEL_CARD_FORMAT,
433439
formatted_body: '',
434440
body: '',
435441
data: JSON.stringify({
@@ -450,7 +456,7 @@ module('constructHistory', () => {
450456
event_id: '4',
451457
origin_server_ts: 1234567920,
452458
content: {
453-
msgtype: 'org.boxel.message',
459+
msgtype: APP_BOXEL_MESSAGE_MSGTYPE,
454460
format: 'org.matrix.custom.html',
455461
body: 'Hey',
456462
formatted_body: 'Hey',
@@ -479,7 +485,7 @@ module('constructHistory', () => {
479485
event_id: '4',
480486
origin_server_ts: 1234567920,
481487
content: {
482-
msgtype: 'org.boxel.message',
488+
msgtype: APP_BOXEL_MESSAGE_MSGTYPE,
483489
format: 'org.matrix.custom.html',
484490
body: 'Hey',
485491
formatted_body: 'Hey',
@@ -542,8 +548,8 @@ module('constructHistory', () => {
542548
room_id: 'room1',
543549
sender: '@user:localhost',
544550
content: {
545-
msgtype: 'org.boxel.cardFragment',
546-
format: 'org.boxel.card',
551+
msgtype: APP_BOXEL_CARDFRAGMENT_MSGTYPE,
552+
format: APP_BOXEL_CARD_FORMAT,
547553
body: 'card fragment 1 of 1',
548554
formatted_body: 'card fragment 1 of 1',
549555
// data should be a JSON string

0 commit comments

Comments
 (0)