Skip to content

Commit 17ea8ff

Browse files
committed
WIP
Signed-off-by: Adam Treat <[email protected]>
1 parent 55875eb commit 17ea8ff

File tree

4 files changed

+187
-32
lines changed

4 files changed

+187
-32
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: currentResponse && (value === "" && currentChat.responseInProgress)
106+
visible: currentResponse && (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/chatmodel.h

+118-4
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>
@@ -82,6 +84,8 @@ struct ChatItem
8284
Q_PROPERTY(bool stopped MEMBER stopped)
8385
Q_PROPERTY(bool thumbsUpState MEMBER thumbsUpState)
8486
Q_PROPERTY(bool thumbsDownState MEMBER thumbsDownState)
87+
Q_PROPERTY(QString content READ content)
88+
Q_PROPERTY(QString toolResult READ toolResult)
8589

8690
public:
8791
enum class Type { System, Prompt, Response, ToolResponse };
@@ -138,6 +142,104 @@ struct ChatItem
138142
return value;
139143
}
140144

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+
141243
// TODO: Maybe we should include the model name here as well as timestamp?
142244
QString name;
143245
QString value;
@@ -188,7 +290,9 @@ class ChatModel : public QAbstractListModel
188290
SourcesRole,
189291
ConsolidatedSourcesRole,
190292
PromptAttachmentsRole,
191-
IsErrorRole
293+
IsErrorRole,
294+
ContentRole,
295+
IsToolCallRole,
192296
};
193297

194298
int rowCount(const QModelIndex &parent = QModelIndex()) const override
@@ -228,6 +332,10 @@ class ChatModel : public QAbstractListModel
228332
return QVariant::fromValue(item.promptAttachments);
229333
case IsErrorRole:
230334
return item.type() == ChatItem::Type::Response && item.isError;
335+
case ContentRole:
336+
return item.content();
337+
case IsToolCallRole:
338+
return item.isToolCall();
231339
}
232340

233341
return QVariant();
@@ -247,6 +355,8 @@ class ChatModel : public QAbstractListModel
247355
roles[ConsolidatedSourcesRole] = "consolidatedSources";
248356
roles[PromptAttachmentsRole] = "promptAttachments";
249357
roles[IsErrorRole] = "isError";
358+
roles[ContentRole] = "content";
359+
roles[IsToolCallRole] = "isToolCall";
250360
return roles;
251361
}
252362

@@ -406,8 +516,12 @@ class ChatModel : public QAbstractListModel
406516
}
407517
}
408518
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);
411525
}
412526
}
413527

@@ -760,7 +874,7 @@ class ChatModel : public QAbstractListModel
760874

761875
Q_SIGNALS:
762876
void countChanged();
763-
void valueChanged(int index, const QString &value);
877+
void contentChanged(int index);
764878
void hasErrorChanged(bool value);
765879

766880
private:

gpt4all-chat/src/codeinterpreter.cpp

+30-14
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ QString CodeInterpreter::run(const QJsonObject &parameters, qint64 timeout)
4141
engine.globalObject().setProperty("console", consoleObject);
4242

4343
QJSValue result = engine.evaluate(code);
44-
QString resultString = result.toString();
44+
QString resultString = result.isUndefined() ? QString() : result.toString();
4545

4646
// NOTE: We purposely do not set the m_error or m_errorString which for the code interpreter since
4747
// we *want* the model to see the response is an error so it can hopefully correct itself. The
@@ -54,10 +54,15 @@ QString CodeInterpreter::run(const QJsonObject &parameters, qint64 timeout)
5454
+ ":" + result.toString();
5555
}
5656

57+
if (resultString.isEmpty())
58+
resultString = consoleCapture.output;
59+
else if (!consoleCapture.output.isEmpty())
60+
resultString += "\n" + consoleCapture.output;
61+
5762
QJsonObject jsonObject;
63+
jsonObject.insert("tool", function());
5864
jsonObject.insert("code", code);
5965
jsonObject.insert("result", resultString);
60-
jsonObject.insert("output", consoleCapture.output);
6166
QJsonDocument doc(jsonObject);
6267
Q_ASSERT(!doc.isNull() && doc.isObject());
6368
return doc.toJson(QJsonDocument::Compact);
@@ -68,7 +73,7 @@ QJsonObject CodeInterpreter::paramSchema() const
6873
static const QString paramSchema = R"({
6974
"code": {
7075
"type": "string",
71-
"description": "The javascript code to run",
76+
"description": "The javascript code to run with comments using console.log() to output a result.",
7277
"required": true
7378
}
7479
})";
@@ -81,17 +86,28 @@ QJsonObject CodeInterpreter::paramSchema() const
8186
QJsonObject CodeInterpreter::exampleParams() const
8287
{
8388
static const QString example = R"(
84-
function isPrime(num) {
85-
if (num <= 1) return false;
86-
if (num === 2) return true;
87-
if (num % 2 === 0) return false;
88-
for (let i = 3; i <= Math.sqrt(num); i += 2) {
89-
if (num % i === 0) return false;
90-
}
91-
return true;
92-
}
93-
const number = 7;
94-
console.log(`${number} is prime: ${isPrime(number)}`);
89+
// A prime number is a natural number greater than one that is only divisible by one and itself
90+
function isPrime(num) {
91+
if (num <= 1) return false; // Only values greater than 1 can be prime
92+
if (num === 2) return true;
93+
if (num % 2 === 0) return false;
94+
for (let i = 3; i <= Math.sqrt(num); i += 2) {
95+
if (num % i === 0)
96+
return false;
97+
}
98+
return true;
99+
}
100+
101+
// Loop through numbers from 87 to 902 and add only the primes
102+
const primes = []
103+
for (let num=87; num<=902; num++) {
104+
if (isPrime(num)) {
105+
primes.push(`${num}`);
106+
}
107+
}
108+
109+
// Output the result
110+
console.log(`The prime numbers between 87 and 902 are: ${primes.join(', ')}`);
95111
)";
96112

97113
QJsonObject jsonObject;

0 commit comments

Comments
 (0)