Skip to content

Commit ce4ffb2

Browse files
committed
WIP BoxelCommandResultEvent instead of ReactionEvent
1 parent f68e044 commit ce4ffb2

22 files changed

+501
-450
lines changed

packages/ai-bot/helpers.ts

+44-73
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,19 @@ import type {
88
MatrixEvent as DiscreteMatrixEvent,
99
CardFragmentContent,
1010
CommandEvent,
11-
CommandResultEvent,
12-
ReactionEvent,
1311
Tool,
1412
SkillsConfigEvent,
13+
CommandResultEvent,
1514
} from 'https://cardstack.com/base/matrix-event';
1615
import { MatrixEvent, type IRoomEvent } from 'matrix-js-sdk';
1716
import { ChatCompletionMessageToolCall } from 'openai/resources/chat/completions';
1817
import * as Sentry from '@sentry/node';
1918
import { logger } from '@cardstack/runtime-common';
19+
import { APP_BOXEL_COMMAND_RESULT_EVENT_TYPE } from '../runtime-common/matrix-constants';
2020
import {
2121
APP_BOXEL_CARDFRAGMENT_MSGTYPE,
2222
APP_BOXEL_MESSAGE_MSGTYPE,
2323
APP_BOXEL_COMMAND_MSGTYPE,
24-
APP_BOXEL_COMMAND_RESULT_MSGTYPE,
2524
APP_BOXEL_ROOM_SKILLS_EVENT_TYPE,
2625
} from '@cardstack/runtime-common/matrix-constants';
2726

@@ -140,6 +139,15 @@ export function constructHistory(
140139
}
141140
}
142141
let event = { ...rawEvent } as DiscreteMatrixEvent;
142+
if (event.type === APP_BOXEL_COMMAND_RESULT_EVENT_TYPE) {
143+
let { cardEventId } = event.content.data;
144+
if (cardEventId) {
145+
event.content.data.card = serializedCardFromFragments(
146+
cardEventId,
147+
cardFragments,
148+
);
149+
}
150+
}
143151
if (event.type !== 'm.room.message') {
144152
continue;
145153
}
@@ -358,60 +366,20 @@ export function getToolChoice(
358366
return 'auto';
359367
}
360368

361-
export function isCommandResultEvent(
362-
event: DiscreteMatrixEvent,
363-
): event is CommandResultEvent {
364-
return (
365-
event.type === 'm.room.message' &&
366-
typeof event.content === 'object' &&
367-
event.content.msgtype === APP_BOXEL_COMMAND_RESULT_MSGTYPE
368-
);
369-
}
370-
371-
export function isReactionEvent(
372-
event: DiscreteMatrixEvent,
373-
): event is ReactionEvent {
374-
return (
375-
event.type === 'm.reaction' &&
376-
event.content['m.relates_to'].rel_type === 'm.annotation'
377-
);
378-
}
379-
380-
function getReactionStatus(
381-
commandEvent: DiscreteMatrixEvent,
382-
history: DiscreteMatrixEvent[],
383-
) {
384-
let maybeReactionEvent = history.find((e) => {
385-
if (
386-
isReactionEvent(e) &&
387-
e.content['m.relates_to']?.event_id === commandEvent.event_id
388-
) {
389-
return true;
390-
}
391-
return false;
392-
});
393-
return maybeReactionEvent && isReactionEvent(maybeReactionEvent)
394-
? maybeReactionEvent.content['m.relates_to'].key
395-
: undefined;
396-
}
397-
398369
function getCommandResult(
399370
commandEvent: CommandEvent,
400371
history: DiscreteMatrixEvent[],
401372
) {
402-
let maybeCommandResultEvent = history.find((e) => {
373+
let commandResultEvent = history.find((e) => {
403374
if (
404375
isCommandResultEvent(e) &&
405376
e.content['m.relates_to']?.event_id === commandEvent.event_id
406377
) {
407378
return true;
408379
}
409380
return false;
410-
});
411-
return maybeCommandResultEvent &&
412-
isCommandResultEvent(maybeCommandResultEvent)
413-
? maybeCommandResultEvent.content.result
414-
: undefined;
381+
}) as CommandResultEvent | undefined;
382+
return commandResultEvent;
415383
}
416384

417385
function toToolCall(event: CommandEvent): ChatCompletionMessageToolCall {
@@ -429,21 +397,23 @@ function toPromptMessageWithToolResult(
429397
event: CommandEvent,
430398
history: DiscreteMatrixEvent[],
431399
): OpenAIPromptMessage {
432-
let commandResult = getCommandResult(event as CommandEvent, history);
400+
let commandResult = getCommandResult(event, history);
401+
let content = 'pending';
433402
if (commandResult) {
434-
return {
435-
role: 'tool',
436-
content: commandResult,
437-
tool_call_id: event.content.data.toolCall.id,
438-
};
439-
} else {
440-
let reactionStatus = getReactionStatus(event, history);
441-
return {
442-
role: 'tool',
443-
content: reactionStatus ?? 'pending',
444-
tool_call_id: event.content.data.toolCall.id,
445-
};
403+
let status = commandResult.content['m.relates_to']?.key;
404+
if (commandResult.content.data.card) {
405+
content = `Command ${status}, with result card: ${JSON.stringify(
406+
commandResult.content.data.card,
407+
)}.\n`;
408+
} else {
409+
content = `Command ${status}.\n`;
410+
}
446411
}
412+
return {
413+
role: 'tool',
414+
content,
415+
tool_call_id: event.content.data.toolCall.id,
416+
};
447417
}
448418

449419
export function getModifyPrompt(
@@ -570,24 +540,13 @@ export function cleanContent(content: string) {
570540
return content.trim();
571541
}
572542

573-
export const isCommandReactionEvent = (event?: MatrixEvent) => {
574-
if (event === undefined) {
575-
return false;
576-
}
577-
let content = event.getContent();
578-
return (
579-
event.getType() === 'm.reaction' &&
580-
content['m.relates_to']?.rel_type === 'm.annotation'
581-
);
582-
};
583-
584-
export const isCommandReactionStatusApplied = (event?: MatrixEvent) => {
543+
export const isCommandResultStatusApplied = (event?: MatrixEvent) => {
585544
if (event === undefined) {
586545
return false;
587546
}
588-
let content = event.getContent();
589547
return (
590-
isCommandReactionEvent(event) && content['m.relates_to']?.key === 'applied'
548+
isCommandResultEvent(event.event as DiscreteMatrixEvent) &&
549+
event.getContent()['m.relates_to']?.key === 'applied'
591550
);
592551
};
593552

@@ -603,3 +562,15 @@ export function isCommandEvent(
603562
typeof event.content.data.toolCall === 'object'
604563
);
605564
}
565+
566+
export function isCommandResultEvent(
567+
event?: DiscreteMatrixEvent,
568+
): event is CommandResultEvent {
569+
if (event === undefined) {
570+
return false;
571+
}
572+
return (
573+
event.type === APP_BOXEL_COMMAND_RESULT_EVENT_TYPE &&
574+
event.content['m.relates_to']?.rel_type === 'm.annotation'
575+
);
576+
}

packages/ai-bot/lib/set-title.ts

+14-9
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
import { type MatrixEvent, type IEventRelation } from 'matrix-js-sdk';
1+
import {
2+
type MatrixEvent,
3+
type IEventRelation,
4+
IRoomEvent,
5+
} from 'matrix-js-sdk';
26
import OpenAI from 'openai';
37
import {
48
type OpenAIPromptMessage,
5-
isCommandReactionStatusApplied,
9+
isCommandResultStatusApplied,
610
attachedCardsToMessage,
711
isCommandEvent,
812
getRelevantCards,
913
} from '../helpers';
1014
import { MatrixClient } from './matrix';
1115
import type { MatrixEvent as DiscreteMatrixEvent } from 'https://cardstack.com/base/matrix-event';
16+
import { ChatCompletionMessageParam } from 'openai/resources';
1217

1318
const SET_TITLE_SYSTEM_MESSAGE = `You are a chat titling system, you must read the conversation and return a suggested title of no more than six words.
14-
Do NOT say talk or discussion or discussing or chat or chatting, this is implied by the context.
15-
The user can optionally apply 'patchCard' by sending data about fields to update.
19+
Do NOT say talk or discussion or discussing or chat or chatting, this is implied by the context.
20+
The user can optionally apply 'patchCard' by sending data about fields to update.
1621
Explain the general actions and user intent. If 'patchCard' was used, express the title in an active sentence. Do NOT use the word "patch" in the title.`;
1722

1823
export async function setTitle(
@@ -39,7 +44,7 @@ export async function setTitle(
3944
let result = await openai.chat.completions.create(
4045
{
4146
model: 'gpt-4o',
42-
messages: startOfConversation,
47+
messages: startOfConversation as ChatCompletionMessageParam[],
4348
stream: false,
4449
},
4550
{
@@ -120,15 +125,15 @@ export const getLatestCommandApplyMessage = (
120125
return [];
121126
};
122127

123-
export const roomTitleAlreadySet = (rawEventLog: DiscreteMatrixEvent[]) => {
128+
export const roomTitleAlreadySet = (rawEventLog: IRoomEvent[]) => {
124129
return (
125130
rawEventLog.filter((event) => event.type === 'm.room.name').length > 1 ??
126131
false
127132
);
128133
};
129134

130135
const userAlreadyHasSentNMessages = (
131-
rawEventLog: DiscreteMatrixEvent[],
136+
rawEventLog: IRoomEvent[],
132137
botUserId: string,
133138
n = 5,
134139
) => {
@@ -140,12 +145,12 @@ const userAlreadyHasSentNMessages = (
140145
};
141146

142147
export function shouldSetRoomTitle(
143-
rawEventLog: DiscreteMatrixEvent[],
148+
rawEventLog: IRoomEvent[],
144149
aiBotUserId: string,
145150
event?: MatrixEvent,
146151
) {
147152
return (
148-
(isCommandReactionStatusApplied(event) ||
153+
(isCommandResultStatusApplied(event) ||
149154
userAlreadyHasSentNMessages(rawEventLog, aiBotUserId)) &&
150155
!roomTitleAlreadySet(rawEventLog)
151156
);

packages/ai-bot/main.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { logger, aiBotUsername } from '@cardstack/runtime-common';
1111
import {
1212
type PromptParts,
1313
constructHistory,
14-
isCommandReactionStatusApplied,
14+
isCommandResultStatusApplied,
1515
getPromptParts,
1616
extractCardFragmentsFromEvents,
1717
} from './helpers';
@@ -30,6 +30,8 @@ import * as Sentry from '@sentry/node';
3030

3131
import { getAvailableCredits, saveUsageCost } from './lib/ai-billing';
3232
import { PgAdapter } from '@cardstack/postgres';
33+
import { ChatCompletionMessageParam } from 'openai/resources';
34+
import { OpenAIError } from 'openai/error';
3335

3436
let log = logger('ai-bot');
3537

@@ -69,12 +71,12 @@ class Assistant {
6971
if (prompt.tools.length === 0) {
7072
return this.openai.beta.chat.completions.stream({
7173
model: prompt.model,
72-
messages: prompt.messages,
74+
messages: prompt.messages as ChatCompletionMessageParam[],
7375
});
7476
} else {
7577
return this.openai.beta.chat.completions.stream({
7678
model: prompt.model,
77-
messages: prompt.messages,
79+
messages: prompt.messages as ChatCompletionMessageParam[],
7880
tools: prompt.tools,
7981
tool_choice: prompt.toolChoice,
8082
});
@@ -250,7 +252,7 @@ Common issues are:
250252
finalContent = await runner.finalContent();
251253
await responder.finalize(finalContent);
252254
} catch (error) {
253-
await responder.onError(error);
255+
await responder.onError(error as OpenAIError);
254256
} finally {
255257
if (generationId) {
256258
assistant.trackAiUsageCost(senderMatrixUserId, generationId);
@@ -278,7 +280,7 @@ Common issues are:
278280
if (!room) {
279281
return;
280282
}
281-
if (!isCommandReactionStatusApplied(event)) {
283+
if (!isCommandResultStatusApplied(event)) {
282284
return;
283285
}
284286
log.info(

packages/ai-bot/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"devDependencies": {
2020
"@sinonjs/fake-timers": "^11.2.2",
21+
"@types/qunit": "^2.19.12",
2122
"@types/sinonjs__fake-timers": "^8.1.5",
2223
"qunit": "^2.18.0"
2324
},

0 commit comments

Comments
 (0)