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>
@@ -82,6 +84,8 @@ struct ChatItem
82
84
Q_PROPERTY (bool stopped MEMBER stopped)
83
85
Q_PROPERTY (bool thumbsUpState MEMBER thumbsUpState)
84
86
Q_PROPERTY (bool thumbsDownState MEMBER thumbsDownState)
87
+ Q_PROPERTY (QString content READ content)
88
+ Q_PROPERTY (QString toolResult READ toolResult)
85
89
86
90
public:
87
91
enum class Type { System, Prompt, Response, ToolResponse };
@@ -138,6 +142,104 @@ struct ChatItem
138
142
return value;
139
143
}
140
144
145
+ bool isToolCall () const
146
+ {
147
+ if (type () != Type::Response)
148
+ return false ;
149
+
150
+ ToolCallParser parser;
151
+ parser.update (value);
152
+ return parser.state () != ToolCallParser::None;
153
+ }
154
+
155
+ QString content () const
156
+ {
157
+ if (type () == Type::System || type () == Type::Prompt)
158
+ return value;
159
+
160
+ // This only returns a string for the code interpreter
161
+ if (type () == Type::ToolResponse)
162
+ return toolResult ();
163
+
164
+ // Otherwise we parse if this is a toolcall
165
+ ToolCallParser parser;
166
+ parser.update (value);
167
+
168
+ // If no tool call is detected, return the original value
169
+ if (parser.startIndex () < 0 )
170
+ return value;
171
+
172
+ // Constants for identifying and formatting the code interpreter tool call
173
+ static const QString codeInterpreterPrefix = " <tool_call>{\" name\" : \" javascript_interpret\" , \" parameters\" : {\" code\" :\" " ;
174
+ static const QString codeInterpreterSuffix = " \" }}</tool_call>" ;
175
+ static const QString formattedPrefix = " ```javascript\n " ;
176
+ static const QString formattedSuffix = " ```" ;
177
+
178
+ QString beforeToolCall = value.left (parser.startIndex ());
179
+ QString toolCallString = value.mid (parser.startIndex ());
180
+
181
+ // Check if the tool call is a JavaScript interpreter tool call
182
+ if (toolCallString.startsWith (codeInterpreterPrefix)) {
183
+ int startCodeIndex = codeInterpreterPrefix.length ();
184
+ int endCodeIndex = toolCallString.indexOf (codeInterpreterSuffix);
185
+
186
+ // Handle partial matches for codeInterpreterSuffix
187
+ if (endCodeIndex == -1 ) {
188
+ // If no complete match for suffix, search for a partial match
189
+ for (int i = codeInterpreterSuffix.length () - 1 ; i >= 0 ; --i) {
190
+ QString partialSuffix = codeInterpreterSuffix.left (i);
191
+ if (toolCallString.endsWith (partialSuffix)) {
192
+ endCodeIndex = toolCallString.length () - partialSuffix.length ();
193
+ break ;
194
+ }
195
+ }
196
+ }
197
+
198
+ // If a partial or full suffix match was found, adjust the `code` extraction
199
+ if (endCodeIndex > startCodeIndex) {
200
+ QString code = toolCallString.mid (startCodeIndex, endCodeIndex - startCodeIndex);
201
+
202
+ // Decode escaped JSON characters
203
+ code.replace (" \\ n" , " \n " );
204
+ code.replace (" \\ t" , " \t " );
205
+ code.replace (" \\ r" , " \r " );
206
+ code.replace (" \\\" " , " \" " );
207
+ code.replace (" \\ '" , " '" );
208
+ code.replace (" \\\\ " , " \\ " );
209
+ code.replace (" \\ b" , " \b " );
210
+ code.replace (" \\ f" , " \f " );
211
+ code.replace (" \\ v" , " \v " );
212
+ code.replace (" \\ /" , " /" );
213
+
214
+ // Format the code with JavaScript styling
215
+ QString formattedCode = formattedPrefix + code + formattedSuffix;
216
+
217
+ // Return the text before the tool call and the formatted code
218
+ return beforeToolCall + formattedCode;
219
+ }
220
+ }
221
+
222
+ // If it's not a code interpreter tool call, return the text before the tool call
223
+ return beforeToolCall;
224
+ }
225
+
226
+ QString toolResult () const
227
+ {
228
+ QJsonDocument doc = QJsonDocument::fromJson (value.toUtf8 ());
229
+ if (doc.isNull () || !doc.isObject ())
230
+ return QString ();
231
+
232
+ QJsonObject obj = doc.object ();
233
+ if (!obj.contains (" tool" ))
234
+ return QString ();
235
+
236
+ if (obj.value (" tool" ).toString () != " javascript_interpret" )
237
+ return QString ();
238
+
239
+ Q_ASSERT (obj.contains (" result" ));
240
+ return " ```" + obj.value (" result" ).toString () + " ```" ;
241
+ }
242
+
141
243
// TODO: Maybe we should include the model name here as well as timestamp?
142
244
QString name;
143
245
QString value;
@@ -188,7 +290,9 @@ class ChatModel : public QAbstractListModel
188
290
SourcesRole,
189
291
ConsolidatedSourcesRole,
190
292
PromptAttachmentsRole,
191
- IsErrorRole
293
+ IsErrorRole,
294
+ ContentRole,
295
+ IsToolCallRole,
192
296
};
193
297
194
298
int rowCount (const QModelIndex &parent = QModelIndex()) const override
@@ -228,6 +332,10 @@ class ChatModel : public QAbstractListModel
228
332
return QVariant::fromValue (item.promptAttachments );
229
333
case IsErrorRole:
230
334
return item.type () == ChatItem::Type::Response && item.isError ;
335
+ case ContentRole:
336
+ return item.content ();
337
+ case IsToolCallRole:
338
+ return item.isToolCall ();
231
339
}
232
340
233
341
return QVariant ();
@@ -247,6 +355,8 @@ class ChatModel : public QAbstractListModel
247
355
roles[ConsolidatedSourcesRole] = " consolidatedSources" ;
248
356
roles[PromptAttachmentsRole] = " promptAttachments" ;
249
357
roles[IsErrorRole] = " isError" ;
358
+ roles[ContentRole] = " content" ;
359
+ roles[IsToolCallRole] = " isToolCall" ;
250
360
return roles;
251
361
}
252
362
@@ -406,8 +516,12 @@ class ChatModel : public QAbstractListModel
406
516
}
407
517
}
408
518
if (changed) {
409
- emit dataChanged (createIndex (index , 0 ), createIndex (index , 0 ), {ValueRole});
410
- emit valueChanged (index , value);
519
+ emit dataChanged (createIndex (index , 0 ), createIndex (index , 0 ), {ValueRole, ContentRole, IsToolCallRole});
520
+ // FIXME: This should be eliminated. It is necessary right now because of how we handle
521
+ // display of text of chat items via ChatViewTextProcessor, but should go away when we
522
+ // switch to a model/view arch and stop relying upon QTextDocument to display all the model's
523
+ // content
524
+ emit contentChanged (index );
411
525
}
412
526
}
413
527
@@ -760,7 +874,7 @@ class ChatModel : public QAbstractListModel
760
874
761
875
Q_SIGNALS:
762
876
void countChanged ();
763
- void valueChanged (int index, const QString &value );
877
+ void contentChanged (int index);
764
878
void hasErrorChanged (bool value);
765
879
766
880
private:
0 commit comments