Skip to content

Commit 8eb2549

Browse files
committed
WIP
Signed-off-by: Adam Treat <[email protected]>
1 parent 221b85e commit 8eb2549

File tree

5 files changed

+194
-37
lines changed

5 files changed

+194
-37
lines changed

gpt4all-chat/qml/ChatItemView.qml

+39-13
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ GridLayout {
1818
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
1919
Layout.preferredWidth: 32
2020
Layout.preferredHeight: 32
21-
Layout.topMargin: model.index > 0 ? 25 : 0
21+
Layout.topMargin: name !== "ToolResponse: " && model.index > 0 ? 25 : 0
22+
visible: content !== ""
2223

2324
Image {
2425
id: logo
@@ -34,6 +35,7 @@ GridLayout {
3435
anchors.fill: logo
3536
source: logo
3637
color: theme.conversationHeader
38+
visible: name !== "ToolResponse: "
3739
RotationAnimation {
3840
id: rotationAnimation
3941
target: colorOver
@@ -52,7 +54,8 @@ GridLayout {
5254
Layout.column: 1
5355
Layout.fillWidth: true
5456
Layout.preferredHeight: 38
55-
Layout.topMargin: model.index > 0 ? 25 : 0
57+
Layout.topMargin: name !== "ToolResponse: " && model.index > 0 ? 25 : 0
58+
visible: content !== ""
5659

5760
RowLayout {
5861
spacing: 5
@@ -61,23 +64,46 @@ GridLayout {
6164
anchors.bottom: parent.bottom
6265

6366
TextArea {
64-
text: name === "Response: " ? qsTr("GPT4All") : qsTr("You")
67+
text: {
68+
if (name === "Response: ")
69+
if (!isToolCall)
70+
return qsTr("GPT4All")
71+
else if (currentChat.responseInProgress)
72+
return qsTr("Analyzing")
73+
else
74+
return qsTr("Analyzed")
75+
else if (name === "ToolResponse: ")
76+
return qsTr("Computed result: ")
77+
return qsTr("You")
78+
}
6579
padding: 0
66-
font.pixelSize: theme.fontSizeLarger
67-
font.bold: true
68-
color: theme.conversationHeader
80+
font.pixelSize: {
81+
if (name === "ToolResponse: ")
82+
return theme.fontSizeLarge
83+
return theme.fontSizeLarger
84+
}
85+
font.bold: {
86+
if (name === "ToolResponse: ")
87+
return false
88+
return true
89+
}
90+
color: {
91+
if (name === "ToolResponse: ")
92+
return theme.textColor
93+
return theme.conversationHeader
94+
}
6995
enabled: false
7096
focus: false
7197
readOnly: true
7298
}
7399
Text {
74-
visible: name === "Response: "
100+
visible: name === "Response: " && !isToolCall
75101
font.pixelSize: theme.fontSizeLarger
76102
text: currentModelName()
77103
color: theme.mutedTextColor
78104
}
79105
RowLayout {
80-
visible: isCurrentResponse && (value === "" && currentChat.responseInProgress)
106+
visible: isCurrentResponse && (content === "" && currentChat.responseInProgress)
81107
Text {
82108
color: theme.mutedTextColor
83109
font.pixelSize: theme.fontSizeLarger
@@ -217,7 +243,7 @@ GridLayout {
217243
height: enabled ? implicitHeight : 0
218244
onTriggered: {
219245
textProcessor.shouldProcessText = !textProcessor.shouldProcessText;
220-
textProcessor.setValue(value);
246+
textProcessor.setValue(content);
221247
}
222248
}
223249
}
@@ -238,14 +264,14 @@ GridLayout {
238264
textProcessor.codeColors.headerColor = theme.codeHeaderColor
239265
textProcessor.codeColors.backgroundColor = theme.codeBackgroundColor
240266
textProcessor.textDocument = textDocument
241-
textProcessor.setValue(value);
267+
textProcessor.setValue(content);
242268
}
243269

244270
Component.onCompleted: {
245271
resetChatViewTextProcessor();
246-
chatModel.valueChanged.connect(function(i, value) {
272+
chatModel.contentChanged.connect(function(i) {
247273
if (model.index === i)
248-
textProcessor.setValue(value);
274+
textProcessor.setValue(content);
249275
}
250276
);
251277
}
@@ -271,7 +297,7 @@ GridLayout {
271297
y: Math.round((parent.height - height) / 2)
272298
width: 640
273299
height: 300
274-
property string text: value
300+
property string text: content
275301
response: newResponse === undefined || newResponse === "" ? text : newResponse
276302
onAccepted: {
277303
var responseHasChanged = response !== text && response !== newResponse

gpt4all-chat/qml/ChatView.qml

-1
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,6 @@ Rectangle {
803803

804804
delegate: ChatItemView {
805805
width: listView.contentItem.width - 15
806-
visible: name !== "ToolResponse: "
807806
height: visible ? implicitHeight : 0
808807
}
809808

gpt4all-chat/src/chat.cpp

+5-2
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,12 @@ void Chat::responseStopped(qint64 promptResponseMs)
288288
const QString toolResponse = executeToolCall(toolCall, errorString);
289289
qDebug() << toolCall << toolResponse;
290290
resetResponseState();
291-
m_chatModel->updateCurrentResponse(m_chatModel->count() - 1, false);
291+
292+
qsizetype prevMsgIndex = m_chatModel->count() - 1;
293+
if (prevMsgIndex >= 0)
294+
m_chatModel->updateCurrentResponse(prevMsgIndex, false);
292295
m_chatModel->appendToolResponse(toolResponse);
293-
m_chatModel->appendResponse();
296+
m_chatModel->appendResponse(prevMsgIndex + 1);
294297
emit promptRequested(m_collections); // triggers a new response
295298
return;
296299
} else if (m_generatedName.isEmpty()) {

gpt4all-chat/src/chatmodel.h

+120-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#define CHATMODEL_H
33

44
#include "database.h"
5+
#include "toolcallparser.h"
56
#include "utils.h"
67
#include "xlsxtomd.h"
78

@@ -11,6 +12,7 @@
1112
#include <QBuffer>
1213
#include <QByteArray>
1314
#include <QDataStream>
15+
#include <QJsonDocument>
1416
#include <QHash>
1517
#include <QList>
1618
#include <QObject>
@@ -73,15 +75,16 @@ struct ChatItem
7375
Q_PROPERTY(QString value MEMBER value)
7476

7577
// prompts and responses
76-
Q_PROPERTY(int peerIndex MEMBER peerIndex)
78+
Q_PROPERTY(int peerIndex MEMBER peerIndex)
79+
Q_PROPERTY(QString content READ content )
7780

7881
// prompts
7982
Q_PROPERTY(QList<PromptAttachment> promptAttachments MEMBER promptAttachments)
8083
Q_PROPERTY(QString bakedPrompt READ bakedPrompt )
8184

8285
// 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 )
8588

8689
// responses (DataLake)
8790
Q_PROPERTY(QString newResponse MEMBER newResponse )
@@ -151,6 +154,104 @@ struct ChatItem
151154
return parts.join(QString());
152155
}
153156

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+
154255
// TODO: Maybe we should include the model name here as well as timestamp?
155256
QString name;
156257
QString value;
@@ -205,6 +306,7 @@ class ChatModel : public QAbstractListModel
205306

206307
// prompts and responses
207308
PeerRole,
309+
ContentRole,
208310

209311
// prompts
210312
PromptAttachmentsRole,
@@ -215,6 +317,7 @@ class ChatModel : public QAbstractListModel
215317
ConsolidatedSourcesRole,
216318
IsCurrentResponseRole,
217319
IsErrorRole,
320+
IsToolCallRole,
218321

219322
// responses (DataLake)
220323
NewResponseRole,
@@ -291,6 +394,10 @@ class ChatModel : public QAbstractListModel
291394
return item.thumbsDownState;
292395
case IsErrorRole:
293396
return item.type() == ChatItem::Type::Response && item.isError;
397+
case ContentRole:
398+
return item.content();
399+
case IsToolCallRole:
400+
return item.isToolCall();
294401
}
295402

296403
return QVariant();
@@ -311,6 +418,8 @@ class ChatModel : public QAbstractListModel
311418
{ StoppedRole, "stopped" },
312419
{ ThumbsUpStateRole, "thumbsUpState" },
313420
{ ThumbsDownStateRole, "thumbsDownState" },
421+
{ ContentRole, "content" },
422+
{ IsToolCallRole, "isToolCall" },
314423
};
315424
}
316425

@@ -350,7 +459,7 @@ class ChatModel : public QAbstractListModel
350459
if (promptIndex >= m_chatItems.size())
351460
throw std::out_of_range(fmt::format("index {} is out of range", promptIndex));
352461
auto &promptItem = m_chatItems[promptIndex];
353-
if (promptItem.type() != ChatItem::Type::Prompt)
462+
if (promptItem.type() != ChatItem::Type::Prompt && promptItem.type() != ChatItem::Type::ToolResponse)
354463
throw std::invalid_argument(fmt::format("item at index {} is not a prompt", promptIndex));
355464
promptItem.peerIndex = m_chatItems.count();
356465
}
@@ -489,8 +598,12 @@ class ChatModel : public QAbstractListModel
489598
}
490599
}
491600
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);
494607
}
495608
}
496609

@@ -907,7 +1020,7 @@ class ChatModel : public QAbstractListModel
9071020

9081021
Q_SIGNALS:
9091022
void countChanged();
910-
void valueChanged(int index, const QString &value);
1023+
void contentChanged(int index);
9111024
void hasErrorChanged(bool value);
9121025

9131026
private:

0 commit comments

Comments
 (0)