3
3
type LooseSingleCardDocument ,
4
4
type CardResource ,
5
5
} from '@cardstack/runtime-common' ;
6
- import { getSearchTool } from '@cardstack/runtime-common/helpers/ai' ;
6
+ import { ToolChoice } from '@cardstack/runtime-common/helpers/ai' ;
7
7
import type {
8
8
MatrixEvent as DiscreteMatrixEvent ,
9
9
CardFragmentContent ,
@@ -17,6 +17,13 @@ import { MatrixEvent, type IRoomEvent } from 'matrix-js-sdk';
17
17
import { ChatCompletionMessageToolCall } from 'openai/resources/chat/completions' ;
18
18
import * as Sentry from '@sentry/node' ;
19
19
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' ;
20
27
21
28
let log = logger ( 'ai-bot' ) ;
22
29
@@ -51,7 +58,7 @@ export interface PromptParts {
51
58
messages : OpenAIPromptMessage [ ] ;
52
59
model : string ;
53
60
history : DiscreteMatrixEvent [ ] ;
54
- toolChoice : 'auto' | 'none' ;
61
+ toolChoice : ToolChoice ;
55
62
}
56
63
57
64
export type Message = CommandMessage | TextMessage ;
@@ -75,13 +82,14 @@ export function getPromptParts(
75
82
) ;
76
83
let skills = getEnabledSkills ( eventList , cardFragments ) ;
77
84
let tools = getTools ( history , aiBotUserId ) ;
85
+ let toolChoice = getToolChoice ( history , aiBotUserId ) ;
78
86
let messages = getModifyPrompt ( history , aiBotUserId , tools , skills ) ;
79
87
return {
80
88
tools,
81
89
messages,
82
90
model : 'openai/gpt-4o' ,
83
91
history,
84
- toolChoice : 'auto' ,
92
+ toolChoice : toolChoice ,
85
93
} ;
86
94
}
87
95
@@ -91,7 +99,7 @@ export function extractCardFragmentsFromEvents(
91
99
const fragments = new Map < string , CardFragmentContent > ( ) ; // eventId => fragment
92
100
for ( let event of eventList ) {
93
101
if ( event . type === 'm.room.message' ) {
94
- if ( event . content . msgtype === 'org.boxel.cardFragment' ) {
102
+ if ( event . content . msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE ) {
95
103
fragments . set ( event . event_id , event . content as CardFragmentContent ) ;
96
104
}
97
105
}
@@ -136,10 +144,10 @@ export function constructHistory(
136
144
continue ;
137
145
}
138
146
let eventId = event . event_id ! ;
139
- if ( event . content . msgtype === 'org.boxel.cardFragment' ) {
147
+ if ( event . content . msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE ) {
140
148
continue ;
141
149
}
142
- if ( event . content . msgtype === 'org.boxel.message' ) {
150
+ if ( event . content . msgtype === APP_BOXEL_MESSAGE_MSGTYPE ) {
143
151
let { attachedCardsEventIds } = event . content . data ;
144
152
if ( attachedCardsEventIds && attachedCardsEventIds . length > 0 ) {
145
153
event . content . data . attachedCards = attachedCardsEventIds . map ( ( id ) =>
@@ -174,7 +182,7 @@ function getEnabledSkills(
174
182
cardFragments : Map < string , CardFragmentContent > ,
175
183
) : LooseCardResource [ ] {
176
184
let skillsConfigEvent = eventlist . findLast (
177
- ( event ) => event . type === 'com.cardstack.boxel.room.skills' ,
185
+ ( event ) => event . type === APP_BOXEL_ROOM_SKILLS_EVENT_TYPE ,
178
186
) as SkillsConfigEvent ;
179
187
if ( ! skillsConfigEvent ) {
180
188
return [ ] ;
@@ -272,7 +280,7 @@ export function getRelevantCards(
272
280
}
273
281
if ( event . sender !== aiBotUserId ) {
274
282
let { content } = event ;
275
- if ( content . msgtype === 'org.boxel.message' ) {
283
+ if ( content . msgtype === APP_BOXEL_MESSAGE_MSGTYPE ) {
276
284
setRelevantCards ( attachedCardMap , content . data ?. attachedCards ) ;
277
285
if ( content . data ?. attachedCards ) {
278
286
mostRecentlyAttachedCard = getMostRecentlyAttachedCard (
@@ -297,27 +305,57 @@ export function getTools(
297
305
history : DiscreteMatrixEvent [ ] ,
298
306
aiBotUserId : string ,
299
307
) : 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
+ }
309
322
}
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
+
311
336
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
315
340
) {
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
+ } ;
320
357
}
358
+ return 'auto' ;
321
359
}
322
360
323
361
export function isCommandResultEvent (
@@ -326,7 +364,7 @@ export function isCommandResultEvent(
326
364
return (
327
365
event . type === 'm.room.message' &&
328
366
typeof event . content === 'object' &&
329
- event . content . msgtype === 'org.boxel.commandResult'
367
+ event . content . msgtype === APP_BOXEL_COMMAND_RESULT_MSGTYPE
330
368
) ;
331
369
}
332
370
@@ -449,7 +487,7 @@ export function getModifyPrompt(
449
487
}
450
488
} else {
451
489
if (
452
- event . content . msgtype === 'org.boxel.message' &&
490
+ event . content . msgtype === APP_BOXEL_MESSAGE_MSGTYPE &&
453
491
event . content . data ?. context ?. openCardIds
454
492
) {
455
493
body = `User message: ${ body }
@@ -489,7 +527,7 @@ export function getModifyPrompt(
489
527
490
528
if ( tools . length == 0 ) {
491
529
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.' ;
493
531
}
494
532
495
533
let messages : OpenAIPromptMessage [ ] = [
@@ -559,7 +597,7 @@ export function isCommandEvent(
559
597
return (
560
598
event . type === 'm.room.message' &&
561
599
typeof event . content === 'object' &&
562
- event . content . msgtype === 'org.boxel.command' &&
600
+ event . content . msgtype === APP_BOXEL_COMMAND_MSGTYPE &&
563
601
event . content . format === 'org.matrix.custom.html' &&
564
602
typeof event . content . data === 'object' &&
565
603
typeof event . content . data . toolCall === 'object'
0 commit comments