2
2
#define CHATMODEL_H
3
3
4
4
#include " database.h"
5
+ #include " toolcallparser.h"
5
6
#include " utils.h"
6
7
#include " xlsxtomd.h"
7
8
11
12
#include < QBuffer>
12
13
#include < QByteArray>
13
14
#include < QDataStream>
15
+ #include < QJsonDocument>
14
16
#include < QHash>
15
17
#include < QList>
16
18
#include < QObject>
@@ -73,15 +75,16 @@ struct ChatItem
73
75
Q_PROPERTY (QString value MEMBER value)
74
76
75
77
// prompts and responses
76
- Q_PROPERTY (int peerIndex MEMBER peerIndex)
78
+ Q_PROPERTY (int peerIndex MEMBER peerIndex)
79
+ Q_PROPERTY (QString content READ content )
77
80
78
81
// prompts
79
82
Q_PROPERTY (QList<PromptAttachment> promptAttachments MEMBER promptAttachments)
80
83
Q_PROPERTY (QString bakedPrompt READ bakedPrompt )
81
84
82
85
// responses
83
- Q_PROPERTY (bool isCurrentResponse MEMBER isCurrentResponse)
84
- Q_PROPERTY (bool isError MEMBER isError )
86
+ Q_PROPERTY (bool isCurrentResponse MEMBER isCurrentResponse)
87
+ Q_PROPERTY (bool isError MEMBER isError )
85
88
86
89
// responses (DataLake)
87
90
Q_PROPERTY (QString newResponse MEMBER newResponse )
@@ -151,6 +154,104 @@ struct ChatItem
151
154
return parts.join (QString ());
152
155
}
153
156
157
+ bool isToolCall () const
158
+ {
159
+ if (type () != Type::Response)
160
+ return false ;
161
+
162
+ ToolCallParser parser;
163
+ parser.update (value);
164
+ return parser.state () != ToolCallParser::None;
165
+ }
166
+
167
+ QString content () const
168
+ {
169
+ if (type () == Type::System || type () == Type::Prompt)
170
+ return value;
171
+
172
+ // This only returns a string for the code interpreter
173
+ if (type () == Type::ToolResponse)
174
+ return toolResult ();
175
+
176
+ // Otherwise we parse if this is a toolcall
177
+ ToolCallParser parser;
178
+ parser.update (value);
179
+
180
+ // If no tool call is detected, return the original value
181
+ if (parser.startIndex () < 0 )
182
+ return value;
183
+
184
+ // Constants for identifying and formatting the code interpreter tool call
185
+ static const QString codeInterpreterPrefix = " <tool_call>{\" name\" : \" javascript_interpret\" , \" parameters\" : {\" code\" :\" " ;
186
+ static const QString codeInterpreterSuffix = " \" }}</tool_call>" ;
187
+ static const QString formattedPrefix = " ```javascript\n " ;
188
+ static const QString formattedSuffix = " ```" ;
189
+
190
+ QString beforeToolCall = value.left (parser.startIndex ());
191
+ QString toolCallString = value.mid (parser.startIndex ());
192
+
193
+ // Check if the tool call is a JavaScript interpreter tool call
194
+ if (toolCallString.startsWith (codeInterpreterPrefix)) {
195
+ int startCodeIndex = codeInterpreterPrefix.length ();
196
+ int endCodeIndex = toolCallString.indexOf (codeInterpreterSuffix);
197
+
198
+ // Handle partial matches for codeInterpreterSuffix
199
+ if (endCodeIndex == -1 ) {
200
+ // If no complete match for suffix, search for a partial match
201
+ for (int i = codeInterpreterSuffix.length () - 1 ; i >= 0 ; --i) {
202
+ QString partialSuffix = codeInterpreterSuffix.left (i);
203
+ if (toolCallString.endsWith (partialSuffix)) {
204
+ endCodeIndex = toolCallString.length () - partialSuffix.length ();
205
+ break ;
206
+ }
207
+ }
208
+ }
209
+
210
+ // If a partial or full suffix match was found, adjust the `code` extraction
211
+ if (endCodeIndex > startCodeIndex) {
212
+ QString code = toolCallString.mid (startCodeIndex, endCodeIndex - startCodeIndex);
213
+
214
+ // Decode escaped JSON characters
215
+ code.replace (" \\ n" , " \n " );
216
+ code.replace (" \\ t" , " \t " );
217
+ code.replace (" \\ r" , " \r " );
218
+ code.replace (" \\\" " , " \" " );
219
+ code.replace (" \\ '" , " '" );
220
+ code.replace (" \\\\ " , " \\ " );
221
+ code.replace (" \\ b" , " \b " );
222
+ code.replace (" \\ f" , " \f " );
223
+ code.replace (" \\ v" , " \v " );
224
+ code.replace (" \\ /" , " /" );
225
+
226
+ // Format the code with JavaScript styling
227
+ QString formattedCode = formattedPrefix + code + formattedSuffix;
228
+
229
+ // Return the text before the tool call and the formatted code
230
+ return beforeToolCall + formattedCode;
231
+ }
232
+ }
233
+
234
+ // If it's not a code interpreter tool call, return the text before the tool call
235
+ return beforeToolCall;
236
+ }
237
+
238
+ QString toolResult () const
239
+ {
240
+ QJsonDocument doc = QJsonDocument::fromJson (value.toUtf8 ());
241
+ if (doc.isNull () || !doc.isObject ())
242
+ return QString ();
243
+
244
+ QJsonObject obj = doc.object ();
245
+ if (!obj.contains (" tool" ))
246
+ return QString ();
247
+
248
+ if (obj.value (" tool" ).toString () != " javascript_interpret" )
249
+ return QString ();
250
+
251
+ Q_ASSERT (obj.contains (" result" ));
252
+ return " ```" + obj.value (" result" ).toString () + " ```" ;
253
+ }
254
+
154
255
// TODO: Maybe we should include the model name here as well as timestamp?
155
256
QString name;
156
257
QString value;
@@ -205,6 +306,7 @@ class ChatModel : public QAbstractListModel
205
306
206
307
// prompts and responses
207
308
PeerRole,
309
+ ContentRole,
208
310
209
311
// prompts
210
312
PromptAttachmentsRole,
@@ -215,6 +317,7 @@ class ChatModel : public QAbstractListModel
215
317
ConsolidatedSourcesRole,
216
318
IsCurrentResponseRole,
217
319
IsErrorRole,
320
+ IsToolCallRole,
218
321
219
322
// responses (DataLake)
220
323
NewResponseRole,
@@ -291,6 +394,10 @@ class ChatModel : public QAbstractListModel
291
394
return item.thumbsDownState ;
292
395
case IsErrorRole:
293
396
return item.type () == ChatItem::Type::Response && item.isError ;
397
+ case ContentRole:
398
+ return item.content ();
399
+ case IsToolCallRole:
400
+ return item.isToolCall ();
294
401
}
295
402
296
403
return QVariant ();
@@ -311,6 +418,8 @@ class ChatModel : public QAbstractListModel
311
418
{ StoppedRole, " stopped" },
312
419
{ ThumbsUpStateRole, " thumbsUpState" },
313
420
{ ThumbsDownStateRole, " thumbsDownState" },
421
+ { ContentRole, " content" },
422
+ { IsToolCallRole, " isToolCall" },
314
423
};
315
424
}
316
425
@@ -350,7 +459,7 @@ class ChatModel : public QAbstractListModel
350
459
if (promptIndex >= m_chatItems.size ())
351
460
throw std::out_of_range (fmt::format (" index {} is out of range" , promptIndex));
352
461
auto &promptItem = m_chatItems[promptIndex];
353
- if (promptItem.type () != ChatItem::Type::Prompt)
462
+ if (promptItem.type () != ChatItem::Type::Prompt && promptItem. type () != ChatItem::Type::ToolResponse )
354
463
throw std::invalid_argument (fmt::format (" item at index {} is not a prompt" , promptIndex));
355
464
promptItem.peerIndex = m_chatItems.count ();
356
465
}
@@ -489,8 +598,12 @@ class ChatModel : public QAbstractListModel
489
598
}
490
599
}
491
600
if (changed) {
492
- emit dataChanged (createIndex (index , 0 ), createIndex (index , 0 ), {ValueRole});
493
- emit valueChanged (index , value);
601
+ emit dataChanged (createIndex (index , 0 ), createIndex (index , 0 ), {ValueRole, ContentRole, IsToolCallRole});
602
+ // FIXME: This should be eliminated. It is necessary right now because of how we handle
603
+ // display of text of chat items via ChatViewTextProcessor, but should go away when we
604
+ // switch to a model/view arch and stop relying upon QTextDocument to display all the model's
605
+ // content
606
+ emit contentChanged (index );
494
607
}
495
608
}
496
609
@@ -907,7 +1020,7 @@ class ChatModel : public QAbstractListModel
907
1020
908
1021
Q_SIGNALS:
909
1022
void countChanged ();
910
- void valueChanged (int index, const QString &value );
1023
+ void contentChanged (int index);
911
1024
void hasErrorChanged (bool value);
912
1025
913
1026
private:
0 commit comments