1
1
import { cleanContent } from '../helpers' ;
2
2
import { logger } from '@cardstack/runtime-common' ;
3
- import { MatrixClient , sendError , sendMessage , sendOption } from './matrix' ;
3
+ import {
4
+ MatrixClient ,
5
+ sendError ,
6
+ sendMessage ,
7
+ sendCommandMessage ,
8
+ } from './matrix' ;
4
9
5
10
import * as Sentry from '@sentry/node' ;
6
11
import { OpenAIError } from 'openai/error' ;
@@ -9,6 +14,7 @@ import { ISendEventResponse } from 'matrix-js-sdk/lib/matrix';
9
14
import { ChatCompletionMessageToolCall } from 'openai/resources/chat/completions' ;
10
15
import { FunctionToolCall } from '@cardstack/runtime-common/helpers/ai' ;
11
16
import { thinkingMessage } from '../constants' ;
17
+ import { APP_BOXEL_COMMAND_MSGTYPE } from '@cardstack/runtime-common/matrix-constants' ;
12
18
13
19
let log = logger ( 'ai-bot' ) ;
14
20
@@ -19,6 +25,8 @@ export class Responder {
19
25
initialMessageReplaced = false ;
20
26
client : MatrixClient ;
21
27
roomId : string ;
28
+ includesFunctionToolCall = false ;
29
+ latestContent ?: string ;
22
30
messagePromises : Promise < ISendEventResponse | void > [ ] = [ ] ;
23
31
debouncedMessageSender : (
24
32
content : string ,
@@ -35,14 +43,22 @@ export class Responder {
35
43
eventToUpdate : string | undefined ,
36
44
isStreamingFinished = false ,
37
45
) => {
46
+ this . latestContent = content ;
47
+ let dataOverrides : Record < string , string | boolean > = {
48
+ isStreamingFinished : isStreamingFinished ,
49
+ } ;
50
+ if ( this . includesFunctionToolCall ) {
51
+ dataOverrides = {
52
+ ...dataOverrides ,
53
+ msgtype : APP_BOXEL_COMMAND_MSGTYPE ,
54
+ } ;
55
+ }
38
56
const messagePromise = sendMessage (
39
57
this . client ,
40
58
this . roomId ,
41
59
content ,
42
60
eventToUpdate ,
43
- {
44
- isStreamingFinished : isStreamingFinished ,
45
- } ,
61
+ dataOverrides ,
46
62
) ;
47
63
this . messagePromises . push ( messagePromise ) ;
48
64
await messagePromise ;
@@ -65,7 +81,19 @@ export class Responder {
65
81
66
82
async onChunk ( chunk : {
67
83
usage ?: { prompt_tokens : number ; completion_tokens : number } ;
84
+ choices : {
85
+ delta : { content ?: string ; role ?: string ; tool_calls ?: any [ ] } ;
86
+ } [ ] ;
68
87
} ) {
88
+ if ( chunk . choices [ 0 ] . delta ?. tool_calls ?. [ 0 ] ?. function ) {
89
+ if ( ! this . includesFunctionToolCall ) {
90
+ this . includesFunctionToolCall = true ;
91
+ await this . debouncedMessageSender (
92
+ this . latestContent || '' ,
93
+ this . initialMessageId ,
94
+ ) ;
95
+ }
96
+ }
69
97
// This usage value is set *once* and *only once* at the end of the conversation
70
98
// It will be null at all other times.
71
99
if ( chunk . usage ) {
@@ -76,6 +104,7 @@ export class Responder {
76
104
}
77
105
78
106
async onContent ( snapshot : string ) {
107
+ log . debug ( 'onContent: ' , snapshot ) ;
79
108
await this . debouncedMessageSender (
80
109
cleanContent ( snapshot ) ,
81
110
this . initialMessageId ,
@@ -87,6 +116,7 @@ export class Responder {
87
116
role : string ;
88
117
tool_calls ?: ChatCompletionMessageToolCall [ ] ;
89
118
} ) {
119
+ log . debug ( 'onMessage: ' , msg ) ;
90
120
if ( msg . role === 'assistant' ) {
91
121
await this . handleFunctionToolCalls ( msg ) ;
92
122
}
@@ -111,14 +141,14 @@ export class Responder {
111
141
for ( const toolCall of msg . tool_calls || [ ] ) {
112
142
log . debug ( '[Room Timeline] Function call' , toolCall ) ;
113
143
try {
114
- let optionPromise = sendOption (
144
+ let commandMessagePromise = sendCommandMessage (
115
145
this . client ,
116
146
this . roomId ,
117
147
this . deserializeToolCall ( toolCall ) ,
118
- this . initialMessageReplaced ? undefined : this . initialMessageId ,
148
+ this . initialMessageId ,
119
149
) ;
120
- this . messagePromises . push ( optionPromise ) ;
121
- await optionPromise ;
150
+ this . messagePromises . push ( commandMessagePromise ) ;
151
+ await commandMessagePromise ;
122
152
this . initialMessageReplaced = true ;
123
153
} catch ( error ) {
124
154
Sentry . captureException ( error ) ;
@@ -127,7 +157,7 @@ export class Responder {
127
157
this . client ,
128
158
this . roomId ,
129
159
error ,
130
- this . initialMessageReplaced ? undefined : this . initialMessageId ,
160
+ this . initialMessageId ,
131
161
) ;
132
162
this . messagePromises . push ( errorPromise ) ;
133
163
await errorPromise ;
@@ -136,6 +166,7 @@ export class Responder {
136
166
}
137
167
138
168
async onError ( error : OpenAIError | string ) {
169
+ log . debug ( 'onError: ' , error ) ;
139
170
Sentry . captureException ( error ) ;
140
171
return await sendError (
141
172
this . client ,
@@ -146,6 +177,7 @@ export class Responder {
146
177
}
147
178
148
179
async finalize ( finalContent : string | void | null | undefined ) {
180
+ log . debug ( 'finalize: ' , finalContent ) ;
149
181
if ( finalContent ) {
150
182
finalContent = cleanContent ( finalContent ) ;
151
183
await this . debouncedMessageSender (
0 commit comments