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 8, 2024
1 parent 221b85e commit 8eb2549
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 37 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: isCurrentResponse && (value === "" && currentChat.responseInProgress)
visible: isCurrentResponse && (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
7 changes: 5 additions & 2 deletions gpt4all-chat/src/chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,12 @@ void Chat::responseStopped(qint64 promptResponseMs)
const QString toolResponse = executeToolCall(toolCall, errorString);
qDebug() << toolCall << toolResponse;
resetResponseState();
m_chatModel->updateCurrentResponse(m_chatModel->count() - 1, false);

qsizetype prevMsgIndex = m_chatModel->count() - 1;
if (prevMsgIndex >= 0)
m_chatModel->updateCurrentResponse(prevMsgIndex, false);
m_chatModel->appendToolResponse(toolResponse);
m_chatModel->appendResponse();
m_chatModel->appendResponse(prevMsgIndex + 1);
emit promptRequested(m_collections); // triggers a new response
return;
} else if (m_generatedName.isEmpty()) {
Expand Down
127 changes: 120 additions & 7 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 @@ -73,15 +75,16 @@ struct ChatItem
Q_PROPERTY(QString value MEMBER value)

// prompts and responses
Q_PROPERTY(int peerIndex MEMBER peerIndex)
Q_PROPERTY(int peerIndex MEMBER peerIndex)
Q_PROPERTY(QString content READ content )

// prompts
Q_PROPERTY(QList<PromptAttachment> promptAttachments MEMBER promptAttachments)
Q_PROPERTY(QString bakedPrompt READ bakedPrompt )

// responses
Q_PROPERTY(bool isCurrentResponse MEMBER isCurrentResponse)
Q_PROPERTY(bool isError MEMBER isError )
Q_PROPERTY(bool isCurrentResponse MEMBER isCurrentResponse)
Q_PROPERTY(bool isError MEMBER isError )

// responses (DataLake)
Q_PROPERTY(QString newResponse MEMBER newResponse )
Expand Down Expand Up @@ -151,6 +154,104 @@ struct ChatItem
return parts.join(QString());
}

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 @@ -205,6 +306,7 @@ class ChatModel : public QAbstractListModel

// prompts and responses
PeerRole,
ContentRole,

// prompts
PromptAttachmentsRole,
Expand All @@ -215,6 +317,7 @@ class ChatModel : public QAbstractListModel
ConsolidatedSourcesRole,
IsCurrentResponseRole,
IsErrorRole,
IsToolCallRole,

// responses (DataLake)
NewResponseRole,
Expand Down Expand Up @@ -291,6 +394,10 @@ class ChatModel : public QAbstractListModel
return item.thumbsDownState;
case IsErrorRole:
return item.type() == ChatItem::Type::Response && item.isError;
case ContentRole:
return item.content();
case IsToolCallRole:
return item.isToolCall();
}

return QVariant();
Expand All @@ -311,6 +418,8 @@ class ChatModel : public QAbstractListModel
{ StoppedRole, "stopped" },
{ ThumbsUpStateRole, "thumbsUpState" },
{ ThumbsDownStateRole, "thumbsDownState" },
{ ContentRole, "content" },
{ IsToolCallRole, "isToolCall" },
};
}

Expand Down Expand Up @@ -350,7 +459,7 @@ class ChatModel : public QAbstractListModel
if (promptIndex >= m_chatItems.size())
throw std::out_of_range(fmt::format("index {} is out of range", promptIndex));
auto &promptItem = m_chatItems[promptIndex];
if (promptItem.type() != ChatItem::Type::Prompt)
if (promptItem.type() != ChatItem::Type::Prompt && promptItem.type() != ChatItem::Type::ToolResponse)
throw std::invalid_argument(fmt::format("item at index {} is not a prompt", promptIndex));
promptItem.peerIndex = m_chatItems.count();
}
Expand Down Expand Up @@ -489,8 +598,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 @@ -907,7 +1020,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
Loading

0 comments on commit 8eb2549

Please sign in to comment.