Skip to content

Commit 0d84b50

Browse files
committed
Fix how command results are added to prompt when there is no body
1 parent 53a898d commit 0d84b50

File tree

5 files changed

+101
-83
lines changed

5 files changed

+101
-83
lines changed

packages/ai-bot/helpers.ts

+34-61
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,17 @@ import { MatrixEvent, type IRoomEvent } from 'matrix-js-sdk';
1919
import { ChatCompletionMessageToolCall } from 'openai/resources/chat/completions';
2020
import * as Sentry from '@sentry/node';
2121
import { logger } from '@cardstack/runtime-common';
22-
import {
23-
APP_BOXEL_COMMAND_REQUESTS_KEY,
24-
APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
25-
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
26-
APP_BOXEL_COMMAND_RESULT_REL_TYPE,
27-
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE,
28-
} from '../runtime-common/matrix-constants';
2922
import {
3023
APP_BOXEL_CARDFRAGMENT_MSGTYPE,
3124
APP_BOXEL_MESSAGE_MSGTYPE,
3225
APP_BOXEL_ROOM_SKILLS_EVENT_TYPE,
3326
DEFAULT_LLM,
3427
APP_BOXEL_ACTIVE_LLM,
28+
APP_BOXEL_COMMAND_REQUESTS_KEY,
29+
APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
30+
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
31+
APP_BOXEL_COMMAND_RESULT_REL_TYPE,
32+
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE,
3533
} from '@cardstack/runtime-common/matrix-constants';
3634

3735
let log = logger('ai-bot');
@@ -654,7 +652,7 @@ export async function getModifyPrompt(
654652
continue;
655653
}
656654
if (isCommandResultEvent(event)) {
657-
continue;
655+
continue; // we'll include these with the tool calls
658656
}
659657
if (
660658
'isStreamingFinished' in event.content &&
@@ -663,40 +661,39 @@ export async function getModifyPrompt(
663661
continue;
664662
}
665663
let body = event.content.body;
666-
if (body) {
667-
if (event.sender === aiBotUserId) {
668-
let toolCalls = toToolCalls(event);
669-
let historicalMessage: OpenAIPromptMessage = {
670-
role: 'assistant',
671-
content: body,
672-
};
673-
if (toolCalls.length) {
674-
historicalMessage.tool_calls = toolCalls;
675-
}
676-
historicalMessages.push(historicalMessage);
677-
if (toolCalls.length) {
678-
toPromptMessageWithToolResults(event, history).forEach((message) =>
679-
historicalMessages.push(message),
680-
);
681-
}
682-
} else {
683-
if (
684-
event.content.msgtype === APP_BOXEL_MESSAGE_MSGTYPE &&
685-
event.content.data?.context?.openCardIds
686-
) {
687-
body = `User message: ${body}
664+
if (event.sender === aiBotUserId) {
665+
let toolCalls = toToolCalls(event);
666+
let historicalMessage: OpenAIPromptMessage = {
667+
role: 'assistant',
668+
content: body,
669+
};
670+
if (toolCalls.length) {
671+
historicalMessage.tool_calls = toolCalls;
672+
}
673+
historicalMessages.push(historicalMessage);
674+
if (toolCalls.length) {
675+
toPromptMessageWithToolResults(event, history).forEach((message) =>
676+
historicalMessages.push(message),
677+
);
678+
}
679+
}
680+
if (body && event.sender !== aiBotUserId) {
681+
if (
682+
event.content.msgtype === APP_BOXEL_MESSAGE_MSGTYPE &&
683+
event.content.data?.context?.openCardIds
684+
) {
685+
body = `User message: ${body}
688686
Context: the user has the following cards open: ${JSON.stringify(
689687
event.content.data.context.openCardIds,
690688
)}`;
691-
} else {
692-
body = `User message: ${body}
689+
} else {
690+
body = `User message: ${body}
693691
Context: the user has no open cards.`;
694-
}
695-
historicalMessages.push({
696-
role: 'user',
697-
content: body,
698-
});
699692
}
693+
historicalMessages.push({
694+
role: 'user',
695+
content: body,
696+
});
700697
}
701698
}
702699

@@ -799,27 +796,3 @@ export function isCommandResultEvent(
799796
APP_BOXEL_COMMAND_RESULT_REL_TYPE
800797
);
801798
}
802-
803-
export function eventRequiresResponse(event: MatrixEvent) {
804-
// If it's a message, we should respond unless it's a card fragment
805-
if (event.getType() === 'm.room.message') {
806-
if (
807-
event.getContent().msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE ||
808-
event.getContent().msgtype === APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE
809-
) {
810-
return false;
811-
}
812-
return true;
813-
}
814-
815-
// If it's a command result with output, we should respond
816-
if (
817-
event.getType() === APP_BOXEL_COMMAND_RESULT_EVENT_TYPE &&
818-
event.getContent().msgtype === APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE
819-
) {
820-
return true;
821-
}
822-
823-
// If it's a different type, or a command result without output, we should not respond
824-
return false;
825-
}

packages/ai-bot/lib/responder.ts

+50-10
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,47 @@ import { CommandRequest } from '@cardstack/runtime-common/commands';
1212
import { thinkingMessage } from '../constants';
1313
import type OpenAI from 'openai';
1414
import type { ChatCompletionSnapshot } from 'openai/lib/ChatCompletionStream';
15+
import {
16+
APP_BOXEL_CARDFRAGMENT_MSGTYPE,
17+
APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
18+
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
19+
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE,
20+
} from '@cardstack/runtime-common/matrix-constants';
1521

1622
let log = logger('ai-bot');
1723

1824
export class Responder {
19-
// internally has a debounced function that will send the matrix messages
25+
static eventMayTriggerResponse(event: DiscreteMatrixEvent) {
26+
// If it's a message, we should respond unless it's a card fragment
27+
if (event.getType() === 'm.room.message') {
28+
if (
29+
event.getContent().msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE ||
30+
event.getContent().msgtype === APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE
31+
) {
32+
return false;
33+
}
34+
return true;
35+
}
36+
37+
// If it's a command result with output, we might respond
38+
if (
39+
event.getType() === APP_BOXEL_COMMAND_RESULT_EVENT_TYPE &&
40+
event.getContent().msgtype ===
41+
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE
42+
) {
43+
return true;
44+
}
45+
46+
// If it's a different type, or a command result without output, we should not respond
47+
return false;
48+
}
49+
50+
static eventWillDefinitelyTriggerResponse(event: DiscreteMatrixEvent) {
51+
return (
52+
this.eventMayTriggerResponse(event) &&
53+
event.getType() !== APP_BOXEL_COMMAND_RESULT_EVENT_TYPE
54+
);
55+
}
2056

2157
constructor(
2258
readonly client: MatrixClient,
@@ -25,6 +61,7 @@ export class Responder {
2561

2662
messagePromises: Promise<ISendEventResponse | void>[] = [];
2763

64+
initialMessageSent = false;
2865
responseEventId: string | undefined;
2966
latestContent = '';
3067
toolCalls: ChatCompletionSnapshot.Choice.Message.ToolCall[] = [];
@@ -58,15 +95,18 @@ export class Responder {
5895
await messagePromise;
5996
};
6097

61-
async initialize() {
62-
let initialMessage = await sendMessageEvent(
63-
this.client,
64-
this.roomId,
65-
thinkingMessage,
66-
undefined,
67-
{ isStreamingFinished: false },
68-
);
69-
this.responseEventId = initialMessage.event_id;
98+
async ensureThinkingMessageSent() {
99+
if (!this.initialMessageSent) {
100+
let initialMessage = await sendMessageEvent(
101+
this.client,
102+
this.roomId,
103+
thinkingMessage,
104+
undefined,
105+
{ isStreamingFinished: false },
106+
);
107+
this.responseEventId = initialMessage.event_id;
108+
this.initialMessageSent = true;
109+
}
70110
}
71111

72112
async onChunk(

packages/ai-bot/main.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
isCommandResultStatusApplied,
1010
getPromptParts,
1111
extractCardFragmentsFromEvents,
12-
eventRequiresResponse,
1312
} from './helpers';
1413

1514
import {
@@ -188,13 +187,15 @@ Common issues are:
188187
if (toStartOfTimeline) {
189188
return; // don't print paginated results
190189
}
191-
if (!eventRequiresResponse(event)) {
192-
return; // only print messages
193-
}
194190

195191
if (senderMatrixUserId === aiBotUserId) {
196192
return;
197193
}
194+
195+
if (!Responder.eventMayTriggerResponse(event)) {
196+
return; // early exit for events that will not trigger a response
197+
}
198+
198199
log.info(
199200
'(%s) (Room: "%s" %s) (Message: %s %s)',
200201
event.getType(),
@@ -205,7 +206,10 @@ Common issues are:
205206
);
206207

207208
const responder = new Responder(client, room.roomId);
208-
await responder.initialize();
209+
210+
if (Responder.eventWillDefinitelyTriggerResponse(event)) {
211+
await responder.ensureThinkingMessageSent();
212+
}
209213

210214
let promptParts: PromptParts;
211215
let initial = await client.roomInitialSync(room!.roomId, 1000);
@@ -216,6 +220,7 @@ Common issues are:
216220
if (!promptParts.shouldRespond) {
217221
return;
218222
}
223+
await responder.ensureThinkingMessageSent();
219224
} catch (e) {
220225
log.error(e);
221226
await responder.onError(

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {
1414
import {
1515
APP_BOXEL_MESSAGE_MSGTYPE,
1616
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
17-
APP_BOXEL_COMMAND_RESULT_REL_TYPE,
1817
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE,
18+
APP_BOXEL_COMMAND_RESULT_REL_TYPE,
1919
DEFAULT_LLM,
2020
APP_BOXEL_COMMAND_REQUESTS_KEY,
2121
} from '@cardstack/runtime-common/matrix-constants';

packages/ai-bot/tests/responding-test.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ module('Responding', (hooks) => {
131131
});
132132

133133
test('Sends thinking message', async () => {
134-
await responder.initialize();
134+
await responder.ensureThinkingMessageSent();
135135

136136
const sentEvents = fakeMatrixClient.getSentEvents();
137137
assert.equal(sentEvents.length, 1, 'One event should be sent');
@@ -153,7 +153,7 @@ module('Responding', (hooks) => {
153153
});
154154

155155
test('Sends first content message immediately, replace the thinking message', async () => {
156-
await responder.initialize();
156+
await responder.ensureThinkingMessageSent();
157157

158158
// Send several messages
159159
for (let i = 0; i < 10; i++) {
@@ -188,7 +188,7 @@ module('Responding', (hooks) => {
188188
});
189189

190190
test('Sends first content message immediately, only sends new content updates after 250ms, replacing the thinking message', async () => {
191-
await responder.initialize();
191+
await responder.ensureThinkingMessageSent();
192192

193193
// Send several messages
194194
for (let i = 0; i < 10; i++) {
@@ -260,7 +260,7 @@ module('Responding', (hooks) => {
260260
},
261261
};
262262

263-
await responder.initialize();
263+
await responder.ensureThinkingMessageSent();
264264

265265
await responder.onChunk(
266266
{} as any,
@@ -326,7 +326,7 @@ module('Responding', (hooks) => {
326326
},
327327
},
328328
};
329-
await responder.initialize();
329+
await responder.ensureThinkingMessageSent();
330330

331331
await responder.onChunk({} as any, snapshotWithContent('some content'));
332332

@@ -449,7 +449,7 @@ module('Responding', (hooks) => {
449449
zipCode: '90210',
450450
},
451451
};
452-
await responder.initialize();
452+
await responder.ensureThinkingMessageSent();
453453

454454
await responder.onChunk({} as any, snapshotWithContent('some content'));
455455

0 commit comments

Comments
 (0)