Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
Signed-off-by: Adam Treat <[email protected]>
  • Loading branch information
manyoso committed Nov 7, 2024
1 parent 55875eb commit 17ea8ff
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 32 deletions.
52 changes: 39 additions & 13 deletions gpt4all-chat/qml/ChatItemView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,6 +35,7 @@ GridLayout {
anchors.fill: logo
source: logo
color: theme.conversationHeader
visible: name !== "ToolResponse: "
RotationAnimation {
id: rotationAnimation
target: colorOver
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -217,7 +243,7 @@ GridLayout {
height: enabled ? implicitHeight : 0
onTriggered: {
textProcessor.shouldProcessText = !textProcessor.shouldProcessText;
textProcessor.setValue(value);
textProcessor.setValue(content);
}
}
}
Expand All @@ -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);
}
);
}
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion gpt4all-chat/qml/ChatView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,6 @@ Rectangle {

delegate: ChatItemView {
width: listView.contentItem.width - 15
visible: name !== "ToolResponse: "
height: visible ? implicitHeight : 0
}

Expand Down
122 changes: 118 additions & 4 deletions gpt4all-chat/src/chatmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define CHATMODEL_H

#include "database.h"
#include "toolcallparser.h"
#include "utils.h"
#include "xlsxtomd.h"

Expand All @@ -11,6 +12,7 @@
#include <QBuffer>
#include <QByteArray>
#include <QDataStream>
#include <QJsonDocument>
#include <QHash>
#include <QList>
#include <QObject>
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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 = "<tool_call>{\"name\": \"javascript_interpret\", \"parameters\": {\"code\":\"";
static const QString codeInterpreterSuffix = "\"}}</tool_call>";
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;
Expand Down Expand Up @@ -188,7 +290,9 @@ class ChatModel : public QAbstractListModel
SourcesRole,
ConsolidatedSourcesRole,
PromptAttachmentsRole,
IsErrorRole
IsErrorRole,
ContentRole,
IsToolCallRole,
};

int rowCount(const QModelIndex &parent = QModelIndex()) const override
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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:
Expand Down
44 changes: 30 additions & 14 deletions gpt4all-chat/src/codeinterpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ QString CodeInterpreter::run(const QJsonObject &parameters, 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
Expand All @@ -54,10 +54,15 @@ QString CodeInterpreter::run(const QJsonObject &parameters, 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);
Expand All @@ -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
}
})";
Expand All @@ -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;
Expand Down

0 comments on commit 17ea8ff

Please sign in to comment.