From 17ea8ff7d926de9ceaea14588995092f364faa0f Mon Sep 17 00:00:00 2001 From: Adam Treat Date: Thu, 7 Nov 2024 18:14:29 -0500 Subject: [PATCH] WIP Signed-off-by: Adam Treat --- gpt4all-chat/qml/ChatItemView.qml | 52 +++++++++--- gpt4all-chat/qml/ChatView.qml | 1 - gpt4all-chat/src/chatmodel.h | 122 ++++++++++++++++++++++++++- gpt4all-chat/src/codeinterpreter.cpp | 44 +++++++--- 4 files changed, 187 insertions(+), 32 deletions(-) diff --git a/gpt4all-chat/qml/ChatItemView.qml b/gpt4all-chat/qml/ChatItemView.qml index 5858d474e749..28af49d0e8d3 100644 --- a/gpt4all-chat/qml/ChatItemView.qml +++ b/gpt4all-chat/qml/ChatItemView.qml @@ -18,7 +18,8 @@ GridLayout { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.preferredWidth: 32 Layout.preferredHeight: 32 - Layout.topMargin: model.index > 0 ? 25 : 0 + Layout.topMargin: name !== "ToolResponse: " && model.index > 0 ? 25 : 0 + visible: content !== "" Image { id: logo @@ -34,6 +35,7 @@ GridLayout { anchors.fill: logo source: logo color: theme.conversationHeader + visible: name !== "ToolResponse: " RotationAnimation { id: rotationAnimation target: colorOver @@ -52,7 +54,8 @@ GridLayout { Layout.column: 1 Layout.fillWidth: true Layout.preferredHeight: 38 - Layout.topMargin: model.index > 0 ? 25 : 0 + Layout.topMargin: name !== "ToolResponse: " && model.index > 0 ? 25 : 0 + visible: content !== "" RowLayout { spacing: 5 @@ -61,23 +64,46 @@ GridLayout { anchors.bottom: parent.bottom TextArea { - text: name === "Response: " ? qsTr("GPT4All") : qsTr("You") + text: { + if (name === "Response: ") + if (!isToolCall) + return qsTr("GPT4All") + else if (currentChat.responseInProgress) + return qsTr("Analyzing") + else + return qsTr("Analyzed") + else if (name === "ToolResponse: ") + return qsTr("Computed result: ") + return qsTr("You") + } padding: 0 - font.pixelSize: theme.fontSizeLarger - font.bold: true - color: theme.conversationHeader + font.pixelSize: { + if (name === "ToolResponse: ") + return theme.fontSizeLarge + return theme.fontSizeLarger + } + font.bold: { + if (name === "ToolResponse: ") + return false + return true + } + color: { + if (name === "ToolResponse: ") + return theme.textColor + return theme.conversationHeader + } enabled: false focus: false readOnly: true } Text { - visible: name === "Response: " + visible: name === "Response: " && !isToolCall font.pixelSize: theme.fontSizeLarger text: currentModelName() color: theme.mutedTextColor } RowLayout { - visible: currentResponse && (value === "" && currentChat.responseInProgress) + visible: currentResponse && (content === "" && currentChat.responseInProgress) Text { color: theme.mutedTextColor font.pixelSize: theme.fontSizeLarger @@ -217,7 +243,7 @@ GridLayout { height: enabled ? implicitHeight : 0 onTriggered: { textProcessor.shouldProcessText = !textProcessor.shouldProcessText; - textProcessor.setValue(value); + textProcessor.setValue(content); } } } @@ -238,14 +264,14 @@ GridLayout { textProcessor.codeColors.headerColor = theme.codeHeaderColor textProcessor.codeColors.backgroundColor = theme.codeBackgroundColor textProcessor.textDocument = textDocument - textProcessor.setValue(value); + textProcessor.setValue(content); } Component.onCompleted: { resetChatViewTextProcessor(); - chatModel.valueChanged.connect(function(i, value) { + chatModel.contentChanged.connect(function(i) { if (model.index === i) - textProcessor.setValue(value); + textProcessor.setValue(content); } ); } @@ -271,7 +297,7 @@ GridLayout { y: Math.round((parent.height - height) / 2) width: 640 height: 300 - property string text: value + property string text: content response: newResponse === undefined || newResponse === "" ? text : newResponse onAccepted: { var responseHasChanged = response !== text && response !== newResponse diff --git a/gpt4all-chat/qml/ChatView.qml b/gpt4all-chat/qml/ChatView.qml index 4379619e5921..dd11053bf499 100644 --- a/gpt4all-chat/qml/ChatView.qml +++ b/gpt4all-chat/qml/ChatView.qml @@ -803,7 +803,6 @@ Rectangle { delegate: ChatItemView { width: listView.contentItem.width - 15 - visible: name !== "ToolResponse: " height: visible ? implicitHeight : 0 } diff --git a/gpt4all-chat/src/chatmodel.h b/gpt4all-chat/src/chatmodel.h index 0cd741d4c7da..5095ab51e8fc 100644 --- a/gpt4all-chat/src/chatmodel.h +++ b/gpt4all-chat/src/chatmodel.h @@ -2,6 +2,7 @@ #define CHATMODEL_H #include "database.h" +#include "toolcallparser.h" #include "utils.h" #include "xlsxtomd.h" @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +84,8 @@ struct ChatItem Q_PROPERTY(bool stopped MEMBER stopped) Q_PROPERTY(bool thumbsUpState MEMBER thumbsUpState) Q_PROPERTY(bool thumbsDownState MEMBER thumbsDownState) + Q_PROPERTY(QString content READ content) + Q_PROPERTY(QString toolResult READ toolResult) public: enum class Type { System, Prompt, Response, ToolResponse }; @@ -138,6 +142,104 @@ struct ChatItem return value; } + bool isToolCall() const + { + if (type() != Type::Response) + return false; + + ToolCallParser parser; + parser.update(value); + return parser.state() != ToolCallParser::None; + } + + QString content() const + { + if (type() == Type::System || type() == Type::Prompt) + return value; + + // This only returns a string for the code interpreter + if (type() == Type::ToolResponse) + return toolResult(); + + // Otherwise we parse if this is a toolcall + ToolCallParser parser; + parser.update(value); + + // If no tool call is detected, return the original value + if (parser.startIndex() < 0) + return value; + + // Constants for identifying and formatting the code interpreter tool call + static const QString codeInterpreterPrefix = "{\"name\": \"javascript_interpret\", \"parameters\": {\"code\":\""; + static const QString codeInterpreterSuffix = "\"}}"; + static const QString formattedPrefix = "```javascript\n"; + static const QString formattedSuffix = "```"; + + QString beforeToolCall = value.left(parser.startIndex()); + QString toolCallString = value.mid(parser.startIndex()); + + // Check if the tool call is a JavaScript interpreter tool call + if (toolCallString.startsWith(codeInterpreterPrefix)) { + int startCodeIndex = codeInterpreterPrefix.length(); + int endCodeIndex = toolCallString.indexOf(codeInterpreterSuffix); + + // Handle partial matches for codeInterpreterSuffix + if (endCodeIndex == -1) { + // If no complete match for suffix, search for a partial match + for (int i = codeInterpreterSuffix.length() - 1; i >= 0; --i) { + QString partialSuffix = codeInterpreterSuffix.left(i); + if (toolCallString.endsWith(partialSuffix)) { + endCodeIndex = toolCallString.length() - partialSuffix.length(); + break; + } + } + } + + // If a partial or full suffix match was found, adjust the `code` extraction + if (endCodeIndex > startCodeIndex) { + QString code = toolCallString.mid(startCodeIndex, endCodeIndex - startCodeIndex); + + // Decode escaped JSON characters + code.replace("\\n", "\n"); + code.replace("\\t", "\t"); + code.replace("\\r", "\r"); + code.replace("\\\"", "\""); + code.replace("\\'", "'"); + code.replace("\\\\", "\\"); + code.replace("\\b", "\b"); + code.replace("\\f", "\f"); + code.replace("\\v", "\v"); + code.replace("\\/", "/"); + + // Format the code with JavaScript styling + QString formattedCode = formattedPrefix + code + formattedSuffix; + + // Return the text before the tool call and the formatted code + return beforeToolCall + formattedCode; + } + } + + // If it's not a code interpreter tool call, return the text before the tool call + return beforeToolCall; + } + + QString toolResult() const + { + QJsonDocument doc = QJsonDocument::fromJson(value.toUtf8()); + if (doc.isNull() || !doc.isObject()) + return QString(); + + QJsonObject obj = doc.object(); + if (!obj.contains("tool")) + return QString(); + + if (obj.value("tool").toString() != "javascript_interpret") + return QString(); + + Q_ASSERT(obj.contains("result")); + return "```" + obj.value("result").toString() + "```"; + } + // TODO: Maybe we should include the model name here as well as timestamp? QString name; QString value; @@ -188,7 +290,9 @@ class ChatModel : public QAbstractListModel SourcesRole, ConsolidatedSourcesRole, PromptAttachmentsRole, - IsErrorRole + IsErrorRole, + ContentRole, + IsToolCallRole, }; int rowCount(const QModelIndex &parent = QModelIndex()) const override @@ -228,6 +332,10 @@ class ChatModel : public QAbstractListModel return QVariant::fromValue(item.promptAttachments); case IsErrorRole: return item.type() == ChatItem::Type::Response && item.isError; + case ContentRole: + return item.content(); + case IsToolCallRole: + return item.isToolCall(); } return QVariant(); @@ -247,6 +355,8 @@ class ChatModel : public QAbstractListModel roles[ConsolidatedSourcesRole] = "consolidatedSources"; roles[PromptAttachmentsRole] = "promptAttachments"; roles[IsErrorRole] = "isError"; + roles[ContentRole] = "content"; + roles[IsToolCallRole] = "isToolCall"; return roles; } @@ -406,8 +516,12 @@ class ChatModel : public QAbstractListModel } } if (changed) { - emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ValueRole}); - emit valueChanged(index, value); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ValueRole, ContentRole, IsToolCallRole}); + // FIXME: This should be eliminated. It is necessary right now because of how we handle + // display of text of chat items via ChatViewTextProcessor, but should go away when we + // switch to a model/view arch and stop relying upon QTextDocument to display all the model's + // content + emit contentChanged(index); } } @@ -760,7 +874,7 @@ class ChatModel : public QAbstractListModel Q_SIGNALS: void countChanged(); - void valueChanged(int index, const QString &value); + void contentChanged(int index); void hasErrorChanged(bool value); private: diff --git a/gpt4all-chat/src/codeinterpreter.cpp b/gpt4all-chat/src/codeinterpreter.cpp index b5492db78384..9921f32c71f5 100644 --- a/gpt4all-chat/src/codeinterpreter.cpp +++ b/gpt4all-chat/src/codeinterpreter.cpp @@ -41,7 +41,7 @@ QString CodeInterpreter::run(const QJsonObject ¶meters, qint64 timeout) engine.globalObject().setProperty("console", consoleObject); QJSValue result = engine.evaluate(code); - QString resultString = result.toString(); + QString resultString = result.isUndefined() ? QString() : result.toString(); // NOTE: We purposely do not set the m_error or m_errorString which for the code interpreter since // 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 ¶meters, qint64 timeout) + ":" + result.toString(); } + if (resultString.isEmpty()) + resultString = consoleCapture.output; + else if (!consoleCapture.output.isEmpty()) + resultString += "\n" + consoleCapture.output; + QJsonObject jsonObject; + jsonObject.insert("tool", function()); jsonObject.insert("code", code); jsonObject.insert("result", resultString); - jsonObject.insert("output", consoleCapture.output); QJsonDocument doc(jsonObject); Q_ASSERT(!doc.isNull() && doc.isObject()); return doc.toJson(QJsonDocument::Compact); @@ -68,7 +73,7 @@ QJsonObject CodeInterpreter::paramSchema() const static const QString paramSchema = R"({ "code": { "type": "string", - "description": "The javascript code to run", + "description": "The javascript code to run with comments using console.log() to output a result.", "required": true } })"; @@ -81,17 +86,28 @@ QJsonObject CodeInterpreter::paramSchema() const QJsonObject CodeInterpreter::exampleParams() const { static const QString example = R"( - function isPrime(num) { - if (num <= 1) return false; - if (num === 2) return true; - if (num % 2 === 0) return false; - for (let i = 3; i <= Math.sqrt(num); i += 2) { - if (num % i === 0) return false; - } - return true; - } - const number = 7; - console.log(`${number} is prime: ${isPrime(number)}`); +// A prime number is a natural number greater than one that is only divisible by one and itself +function isPrime(num) { + if (num <= 1) return false; // Only values greater than 1 can be prime + if (num === 2) return true; + if (num % 2 === 0) return false; + for (let i = 3; i <= Math.sqrt(num); i += 2) { + if (num % i === 0) + return false; + } + return true; +} + +// Loop through numbers from 87 to 902 and add only the primes +const primes = [] +for (let num=87; num<=902; num++) { + if (isPrime(num)) { + primes.push(`${num}`); + } +} + +// Output the result +console.log(`The prime numbers between 87 and 902 are: ${primes.join(', ')}`); )"; QJsonObject jsonObject;