1
0
mirror of https://github.com/nomic-ai/gpt4all.git synced 2025-05-03 22:17:22 +00:00

Code interpreter ()

Signed-off-by: Adam Treat <treat.adam@gmail.com>
This commit is contained in:
AT 2024-12-19 16:31:37 -05:00 committed by GitHub
parent 2efb336b8a
commit 1c89447d63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 2243 additions and 623 deletions

View File

@ -11,7 +11,6 @@ function(gpt4all_add_warning_options target)
-Wextra-semi
-Wformat=2
-Wmissing-include-dirs
-Wstrict-overflow=2
-Wsuggest-override
-Wvla
# errors

View File

@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
### Added
- Built-in javascript code interpreter tool plus model ([#3173](https://github.com/nomic-ai/gpt4all/pull/3173))
### Fixed
- Fix remote model template to allow for XML in messages ([#3318](https://github.com/nomic-ai/gpt4all/pull/3318))
- Fix Jinja2Cpp bug that broke system message detection in chat templates ([#3325](https://github.com/nomic-ai/gpt4all/pull/3325))

View File

@ -193,8 +193,9 @@ qt_add_executable(chat
src/chatapi.cpp src/chatapi.h
src/chatlistmodel.cpp src/chatlistmodel.h
src/chatllm.cpp src/chatllm.h
src/chatmodel.h
src/chatmodel.h src/chatmodel.cpp
src/chatviewtextprocessor.cpp src/chatviewtextprocessor.h
src/codeinterpreter.cpp src/codeinterpreter.h
src/database.cpp src/database.h
src/download.cpp src/download.h
src/embllm.cpp src/embllm.h
@ -207,6 +208,9 @@ qt_add_executable(chat
src/mysettings.cpp src/mysettings.h
src/network.cpp src/network.h
src/server.cpp src/server.h
src/tool.cpp src/tool.h
src/toolcallparser.cpp src/toolcallparser.h
src/toolmodel.cpp src/toolmodel.h
src/xlsxtomd.cpp src/xlsxtomd.h
${CHAT_EXE_RESOURCES}
${MACOS_SOURCES}
@ -225,8 +229,10 @@ qt_add_qml_module(chat
qml/AddHFModelView.qml
qml/ApplicationSettings.qml
qml/ChatDrawer.qml
qml/ChatCollapsibleItem.qml
qml/ChatItemView.qml
qml/ChatMessageButton.qml
qml/ChatTextItem.qml
qml/ChatView.qml
qml/CollectionsDrawer.qml
qml/HomeView.qml

View File

@ -1,6 +1,22 @@
[
{
"order": "a",
"md5sum": "a54c08a7b90e4029a8c2ab5b5dc936aa",
"name": "Reasoner v1",
"filename": "qwen2.5-coder-7b-instruct-q4_0.gguf",
"filesize": "4431390720",
"requires": "3.5.4-dev0",
"ramrequired": "8",
"parameters": "8 billion",
"quant": "q4_0",
"type": "qwen2",
"description": "<ul><li>Based on <a href=\"https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct\">Qwen2.5-Coder 7B</a></li><li>Uses built-in javascript code interpreter</li><li>Use for complex reasoning tasks that can be aided by computation analysis</li><li>License: <a href=\"https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct/blob/main/LICENSE\">Apache License Version 2.0</a></li><li>#reasoning</li></ul>",
"url": "https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-GGUF/resolve/main/qwen2.5-coder-7b-instruct-q4_0.gguf",
"chatTemplate": "{{- '<|im_start|>system\\n' }}\n{% if toolList|length > 0 %}You have access to the following functions:\n{% for tool in toolList %}\nUse the function '{{tool.function}}' to: '{{tool.description}}'\n{% if tool.parameters|length > 0 %}\nparameters:\n{% for info in tool.parameters %}\n {{info.name}}:\n type: {{info.type}}\n description: {{info.description}}\n required: {{info.required}}\n{% endfor %}\n{% endif %}\n# Tool Instructions\nIf you CHOOSE to call this function ONLY reply with the following format:\n'{{tool.symbolicFormat}}'\nHere is an example. If the user says, '{{tool.examplePrompt}}', then you reply\n'{{tool.exampleCall}}'\nAfter the result you might reply with, '{{tool.exampleReply}}'\n{% endfor %}\nYou MUST include both the start and end tags when you use a function.\n\nYou are a helpful AI assistant who uses the functions to break down, analyze, perform, and verify complex reasoning tasks. You SHOULD try to verify your answers using the functions where possible.\n{% endif %}\n{{- '<|im_end|>\\n' }}\n{% for message in messages %}\n{{'<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>' + '\\n' }}\n{% endfor %}\n{% if add_generation_prompt %}\n{{ '<|im_start|>assistant\\n' }}\n{% endif %}\n",
"systemPrompt": ""
},
{
"order": "aa",
"md5sum": "c87ad09e1e4c8f9c35a5fcef52b6f1c9",
"name": "Llama 3 8B Instruct",
"filename": "Meta-Llama-3-8B-Instruct.Q4_0.gguf",

View File

@ -56,6 +56,52 @@ ColumnLayout {
Accessible.description: qsTr("Displayed when the models request is ongoing")
}
RowLayout {
ButtonGroup {
id: buttonGroup
exclusive: true
}
MyButton {
text: qsTr("All")
checked: true
borderWidth: 0
backgroundColor: checked ? theme.lightButtonBackground : "transparent"
backgroundColorHovered: theme.lighterButtonBackgroundHovered
backgroundRadius: 5
padding: 15
topPadding: 8
bottomPadding: 8
textColor: theme.lighterButtonForeground
fontPixelSize: theme.fontSizeLarge
fontPixelBold: true
checkable: true
ButtonGroup.group: buttonGroup
onClicked: {
ModelList.gpt4AllDownloadableModels.filter("");
}
}
MyButton {
text: qsTr("Reasoning")
borderWidth: 0
backgroundColor: checked ? theme.lightButtonBackground : "transparent"
backgroundColorHovered: theme.lighterButtonBackgroundHovered
backgroundRadius: 5
padding: 15
topPadding: 8
bottomPadding: 8
textColor: theme.lighterButtonForeground
fontPixelSize: theme.fontSizeLarge
fontPixelBold: true
checkable: true
ButtonGroup.group: buttonGroup
onClicked: {
ModelList.gpt4AllDownloadableModels.filter("#reasoning");
}
}
Layout.bottomMargin: 10
}
ScrollView {
id: scrollView
ScrollBar.vertical.policy: ScrollBar.AsNeeded

View File

@ -0,0 +1,160 @@
import Qt5Compat.GraphicalEffects
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import gpt4all
import mysettings
import toolenums
ColumnLayout {
property alias textContent: innerTextItem.textContent
property bool isCurrent: false
property bool isError: false
Layout.topMargin: 10
Layout.bottomMargin: 10
Item {
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: 38
RowLayout {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
Item {
width: myTextArea.width
height: myTextArea.height
TextArea {
id: myTextArea
text: {
if (isError)
return qsTr("Analysis encountered error");
if (isCurrent)
return qsTr("Analyzing");
return qsTr("Analyzed");
}
padding: 0
font.pixelSize: theme.fontSizeLarger
enabled: false
focus: false
readOnly: true
color: headerMA.containsMouse ? theme.mutedDarkTextColorHovered : theme.mutedTextColor
hoverEnabled: false
}
Item {
id: textColorOverlay
anchors.fill: parent
clip: true
visible: false
Rectangle {
id: animationRec
width: myTextArea.width * 0.3
anchors.top: parent.top
anchors.bottom: parent.bottom
color: theme.textColor
SequentialAnimation {
running: isCurrent
loops: Animation.Infinite
NumberAnimation {
target: animationRec;
property: "x";
from: -animationRec.width;
to: myTextArea.width * 3;
duration: 2000
}
}
}
}
OpacityMask {
visible: isCurrent
anchors.fill: parent
maskSource: myTextArea
source: textColorOverlay
}
}
Item {
id: caret
Layout.preferredWidth: contentCaret.width
Layout.preferredHeight: contentCaret.height
Image {
id: contentCaret
anchors.centerIn: parent
visible: false
sourceSize.width: theme.fontSizeLarge
sourceSize.height: theme.fontSizeLarge
mipmap: true
source: {
if (contentLayout.state === "collapsed")
return "qrc:/gpt4all/icons/caret_right.svg";
else
return "qrc:/gpt4all/icons/caret_down.svg";
}
}
ColorOverlay {
anchors.fill: contentCaret
source: contentCaret
color: headerMA.containsMouse ? theme.mutedDarkTextColorHovered : theme.mutedTextColor
}
}
}
MouseArea {
id: headerMA
hoverEnabled: true
anchors.fill: parent
onClicked: {
if (contentLayout.state === "collapsed")
contentLayout.state = "expanded";
else
contentLayout.state = "collapsed";
}
}
}
ColumnLayout {
id: contentLayout
spacing: 0
state: "collapsed"
clip: true
states: [
State {
name: "expanded"
PropertyChanges { target: contentLayout; Layout.preferredHeight: innerContentLayout.height }
},
State {
name: "collapsed"
PropertyChanges { target: contentLayout; Layout.preferredHeight: 0 }
}
]
transitions: [
Transition {
SequentialAnimation {
PropertyAnimation {
target: contentLayout
property: "Layout.preferredHeight"
duration: 300
easing.type: Easing.InOutQuad
}
}
}
]
ColumnLayout {
id: innerContentLayout
Layout.leftMargin: 30
ChatTextItem {
id: innerTextItem
}
}
}
}

View File

@ -4,9 +4,11 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import Qt.labs.qmlmodels
import gpt4all
import mysettings
import toolenums
ColumnLayout {
@ -33,6 +35,9 @@ GridLayout {
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.preferredWidth: 32
Layout.preferredHeight: 32
Layout.topMargin: model.index > 0 ? 25 : 0
visible: content !== "" || childItems.length > 0
Image {
id: logo
sourceSize: Qt.size(32, 32)
@ -65,6 +70,9 @@ GridLayout {
Layout.column: 1
Layout.fillWidth: true
Layout.preferredHeight: 38
Layout.topMargin: model.index > 0 ? 25 : 0
visible: content !== "" || childItems.length > 0
RowLayout {
spacing: 5
anchors.left: parent.left
@ -72,7 +80,11 @@ GridLayout {
anchors.bottom: parent.bottom
TextArea {
text: name === "Response: " ? qsTr("GPT4All") : qsTr("You")
text: {
if (name === "Response: ")
return qsTr("GPT4All");
return qsTr("You");
}
padding: 0
font.pixelSize: theme.fontSizeLarger
font.bold: true
@ -88,7 +100,7 @@ GridLayout {
color: theme.mutedTextColor
}
RowLayout {
visible: isCurrentResponse && (value === "" && currentChat.responseInProgress)
visible: isCurrentResponse && (content === "" && currentChat.responseInProgress)
Text {
color: theme.mutedTextColor
font.pixelSize: theme.fontSizeLarger
@ -156,131 +168,36 @@ GridLayout {
}
}
TextArea {
id: myTextArea
Repeater {
model: childItems
DelegateChooser {
id: chooser
role: "name"
DelegateChoice {
roleValue: "Text: ";
ChatTextItem {
Layout.fillWidth: true
textContent: modelData.content
}
}
DelegateChoice {
roleValue: "ToolCall: ";
ChatCollapsibleItem {
Layout.fillWidth: true
textContent: modelData.content
isCurrent: modelData.isCurrentResponse
isError: modelData.isToolCallError
}
}
}
delegate: chooser
}
ChatTextItem {
Layout.fillWidth: true
padding: 0
color: {
if (!currentChat.isServer)
return theme.textColor
return theme.white
}
wrapMode: Text.WordWrap
textFormat: TextEdit.PlainText
focus: false
readOnly: true
font.pixelSize: theme.fontSizeLarge
cursorVisible: isCurrentResponse ? currentChat.responseInProgress : false
cursorPosition: text.length
TapHandler {
id: tapHandler
onTapped: function(eventPoint, button) {
var clickedPos = myTextArea.positionAt(eventPoint.position.x, eventPoint.position.y);
var success = textProcessor.tryCopyAtPosition(clickedPos);
if (success)
copyCodeMessage.open();
}
}
MouseArea {
id: conversationMouseArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
conversationContextMenu.x = conversationMouseArea.mouseX
conversationContextMenu.y = conversationMouseArea.mouseY
conversationContextMenu.open()
}
}
}
onLinkActivated: function(link) {
if (!isCurrentResponse || !currentChat.responseInProgress)
Qt.openUrlExternally(link)
}
onLinkHovered: function (link) {
if (!isCurrentResponse || !currentChat.responseInProgress)
statusBar.externalHoveredLink = link
}
MyMenu {
id: conversationContextMenu
MyMenuItem {
text: qsTr("Copy")
enabled: myTextArea.selectedText !== ""
height: enabled ? implicitHeight : 0
onTriggered: myTextArea.copy()
}
MyMenuItem {
text: qsTr("Copy Message")
enabled: myTextArea.selectedText === ""
height: enabled ? implicitHeight : 0
onTriggered: {
myTextArea.selectAll()
myTextArea.copy()
myTextArea.deselect()
}
}
MyMenuItem {
text: textProcessor.shouldProcessText ? qsTr("Disable markdown") : qsTr("Enable markdown")
height: enabled ? implicitHeight : 0
onTriggered: {
textProcessor.shouldProcessText = !textProcessor.shouldProcessText;
textProcessor.setValue(value);
}
}
}
ChatViewTextProcessor {
id: textProcessor
}
function resetChatViewTextProcessor() {
textProcessor.fontPixelSize = myTextArea.font.pixelSize
textProcessor.codeColors.defaultColor = theme.codeDefaultColor
textProcessor.codeColors.keywordColor = theme.codeKeywordColor
textProcessor.codeColors.functionColor = theme.codeFunctionColor
textProcessor.codeColors.functionCallColor = theme.codeFunctionCallColor
textProcessor.codeColors.commentColor = theme.codeCommentColor
textProcessor.codeColors.stringColor = theme.codeStringColor
textProcessor.codeColors.numberColor = theme.codeNumberColor
textProcessor.codeColors.headerColor = theme.codeHeaderColor
textProcessor.codeColors.backgroundColor = theme.codeBackgroundColor
textProcessor.textDocument = textDocument
textProcessor.setValue(value);
}
property bool textProcessorReady: false
Component.onCompleted: {
resetChatViewTextProcessor();
textProcessorReady = true;
}
Connections {
target: chatModel
function onValueChanged(i, value) {
if (myTextArea.textProcessorReady && index === i)
textProcessor.setValue(value);
}
}
Connections {
target: MySettings
function onFontSizeChanged() {
myTextArea.resetChatViewTextProcessor();
}
function onChatThemeChanged() {
myTextArea.resetChatViewTextProcessor();
}
}
Accessible.role: Accessible.Paragraph
Accessible.name: text
Accessible.description: name === "Response: " ? "The response by the model" : "The prompt by the user"
textContent: content
}
ThumbsDownDialog {
@ -289,16 +206,16 @@ 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
if (thumbsDownState && !thumbsUpState && !responseHasChanged)
return
chatModel.updateNewResponse(index, response)
chatModel.updateThumbsUpState(index, false)
chatModel.updateThumbsDownState(index, true)
chatModel.updateNewResponse(model.index, response)
chatModel.updateThumbsUpState(model.index, false)
chatModel.updateThumbsDownState(model.index, true)
Network.sendConversation(currentChat.id, getConversationJson());
}
}
@ -416,7 +333,7 @@ GridLayout {
states: [
State {
name: "expanded"
PropertyChanges { target: sourcesLayout; Layout.preferredHeight: flow.height }
PropertyChanges { target: sourcesLayout; Layout.preferredHeight: sourcesFlow.height }
},
State {
name: "collapsed"
@ -438,7 +355,7 @@ GridLayout {
]
Flow {
id: flow
id: sourcesFlow
Layout.fillWidth: true
spacing: 10
visible: consolidatedSources.length !== 0
@ -617,9 +534,7 @@ GridLayout {
name: qsTr("Copy")
source: "qrc:/gpt4all/icons/copy.svg"
onClicked: {
myTextArea.selectAll();
myTextArea.copy();
myTextArea.deselect();
chatModel.copyToClipboard(index);
}
}

View File

@ -0,0 +1,139 @@
import Qt5Compat.GraphicalEffects
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import gpt4all
import mysettings
import toolenums
TextArea {
id: myTextArea
property string textContent: ""
visible: textContent != ""
Layout.fillWidth: true
padding: 0
color: {
if (!currentChat.isServer)
return theme.textColor
return theme.white
}
wrapMode: Text.WordWrap
textFormat: TextEdit.PlainText
focus: false
readOnly: true
font.pixelSize: theme.fontSizeLarge
cursorVisible: isCurrentResponse ? currentChat.responseInProgress : false
cursorPosition: text.length
TapHandler {
id: tapHandler
onTapped: function(eventPoint, button) {
var clickedPos = myTextArea.positionAt(eventPoint.position.x, eventPoint.position.y);
var success = textProcessor.tryCopyAtPosition(clickedPos);
if (success)
copyCodeMessage.open();
}
}
MouseArea {
id: conversationMouseArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
conversationContextMenu.x = conversationMouseArea.mouseX
conversationContextMenu.y = conversationMouseArea.mouseY
conversationContextMenu.open()
}
}
}
onLinkActivated: function(link) {
if (!isCurrentResponse || !currentChat.responseInProgress)
Qt.openUrlExternally(link)
}
onLinkHovered: function (link) {
if (!isCurrentResponse || !currentChat.responseInProgress)
statusBar.externalHoveredLink = link
}
MyMenu {
id: conversationContextMenu
MyMenuItem {
text: qsTr("Copy")
enabled: myTextArea.selectedText !== ""
height: enabled ? implicitHeight : 0
onTriggered: myTextArea.copy()
}
MyMenuItem {
text: qsTr("Copy Message")
enabled: myTextArea.selectedText === ""
height: enabled ? implicitHeight : 0
onTriggered: {
myTextArea.selectAll()
myTextArea.copy()
myTextArea.deselect()
}
}
MyMenuItem {
text: textProcessor.shouldProcessText ? qsTr("Disable markdown") : qsTr("Enable markdown")
height: enabled ? implicitHeight : 0
onTriggered: {
textProcessor.shouldProcessText = !textProcessor.shouldProcessText;
textProcessor.setValue(textContent);
}
}
}
ChatViewTextProcessor {
id: textProcessor
}
function resetChatViewTextProcessor() {
textProcessor.fontPixelSize = myTextArea.font.pixelSize
textProcessor.codeColors.defaultColor = theme.codeDefaultColor
textProcessor.codeColors.keywordColor = theme.codeKeywordColor
textProcessor.codeColors.functionColor = theme.codeFunctionColor
textProcessor.codeColors.functionCallColor = theme.codeFunctionCallColor
textProcessor.codeColors.commentColor = theme.codeCommentColor
textProcessor.codeColors.stringColor = theme.codeStringColor
textProcessor.codeColors.numberColor = theme.codeNumberColor
textProcessor.codeColors.headerColor = theme.codeHeaderColor
textProcessor.codeColors.backgroundColor = theme.codeBackgroundColor
textProcessor.textDocument = textDocument
textProcessor.setValue(textContent);
}
property bool textProcessorReady: false
Component.onCompleted: {
resetChatViewTextProcessor();
textProcessorReady = true;
}
Connections {
target: myTextArea
function onTextContentChanged() {
if (myTextArea.textProcessorReady)
textProcessor.setValue(textContent);
}
}
Connections {
target: MySettings
function onFontSizeChanged() {
myTextArea.resetChatViewTextProcessor();
}
function onChatThemeChanged() {
myTextArea.resetChatViewTextProcessor();
}
}
Accessible.role: Accessible.Paragraph
Accessible.name: text
Accessible.description: name === "Response: " ? "The response by the model" : "The prompt by the user"
}

View File

@ -824,6 +824,8 @@ Rectangle {
textInput.forceActiveFocus();
textInput.cursorPosition = text.length;
}
height: visible ? implicitHeight : 0
visible: name !== "ToolResponse: "
}
remove: Transition {

View File

@ -3,12 +3,19 @@
#include "chatlistmodel.h"
#include "network.h"
#include "server.h"
#include "tool.h"
#include "toolcallparser.h"
#include "toolmodel.h"
#include <QBuffer>
#include <QDataStream>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QLatin1String>
#include <QMap>
#include <QRegularExpression>
#include <QString>
#include <QVariant>
#include <Qt>
@ -16,6 +23,8 @@
#include <utility>
using namespace ToolEnums;
Chat::Chat(QObject *parent)
: QObject(parent)
, m_id(Network::globalInstance()->generateUniqueId())
@ -54,7 +63,6 @@ void Chat::connectLLM()
// Should be in different threads
connect(m_llmodel, &ChatLLM::modelLoadingPercentageChanged, this, &Chat::handleModelLoadingPercentageChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::responseChanged, this, &Chat::handleResponseChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::responseFailed, this, &Chat::handleResponseFailed, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::promptProcessing, this, &Chat::promptProcessing, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::generatingQuestions, this, &Chat::generatingQuestions, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::responseStopped, this, &Chat::responseStopped, Qt::QueuedConnection);
@ -181,23 +189,12 @@ Chat::ResponseState Chat::responseState() const
return m_responseState;
}
void Chat::handleResponseChanged(const QString &response)
void Chat::handleResponseChanged()
{
if (m_responseState != Chat::ResponseGeneration) {
m_responseState = Chat::ResponseGeneration;
emit responseStateChanged();
}
const int index = m_chatModel->count() - 1;
m_chatModel->updateValue(index, response);
}
void Chat::handleResponseFailed(const QString &error)
{
const int index = m_chatModel->count() - 1;
m_chatModel->updateValue(index, error);
m_chatModel->setError();
responseStopped(0);
}
void Chat::handleModelLoadingPercentageChanged(float loadingPercentage)
@ -242,9 +239,54 @@ void Chat::responseStopped(qint64 promptResponseMs)
m_responseState = Chat::ResponseStopped;
emit responseInProgressChanged();
emit responseStateChanged();
const QString possibleToolcall = m_chatModel->possibleToolcall();
ToolCallParser parser;
parser.update(possibleToolcall);
if (parser.state() == ToolEnums::ParseState::Complete) {
const QString toolCall = parser.toolCall();
// Regex to remove the formatting around the code
static const QRegularExpression regex("^\\s*```javascript\\s*|\\s*```\\s*$");
QString code = toolCall;
code.remove(regex);
code = code.trimmed();
// Right now the code interpreter is the only available tool
Tool *toolInstance = ToolModel::globalInstance()->get(ToolCallConstants::CodeInterpreterFunction);
Q_ASSERT(toolInstance);
// The param is the code
const ToolParam param = { "code", ToolEnums::ParamType::String, code };
const QString result = toolInstance->run({param}, 10000 /*msecs to timeout*/);
const ToolEnums::Error error = toolInstance->error();
const QString errorString = toolInstance->errorString();
// Update the current response with meta information about toolcall and re-parent
m_chatModel->updateToolCall({
ToolCallConstants::CodeInterpreterFunction,
{ param },
result,
error,
errorString
});
++m_consecutiveToolCalls;
// We limit the number of consecutive toolcalls otherwise we get into a potentially endless loop
if (m_consecutiveToolCalls < 3 || error == ToolEnums::Error::NoError) {
resetResponseState();
emit promptRequested(m_collections); // triggers a new response
return;
}
}
if (m_generatedName.isEmpty())
emit generateNameRequested();
m_consecutiveToolCalls = 0;
Network::globalInstance()->trackChatEvent("response_complete", {
{"first", m_firstResponse},
{"message_count", chatModel()->count()},

View File

@ -161,8 +161,7 @@ Q_SIGNALS:
void generatedQuestionsChanged();
private Q_SLOTS:
void handleResponseChanged(const QString &response);
void handleResponseFailed(const QString &error);
void handleResponseChanged();
void handleModelLoadingPercentageChanged(float);
void promptProcessing();
void generatingQuestions();
@ -205,6 +204,7 @@ private:
// - The chat was freshly created during this launch.
// - The chat was changed after loading it from disk.
bool m_needsSave = true;
int m_consecutiveToolCalls = 0;
};
#endif // CHAT_H

View File

@ -20,7 +20,7 @@
#include <memory>
static constexpr quint32 CHAT_FORMAT_MAGIC = 0xF5D553CC;
static constexpr qint32 CHAT_FORMAT_VERSION = 11;
static constexpr qint32 CHAT_FORMAT_VERSION = 12;
class MyChatListModel: public ChatListModel { };
Q_GLOBAL_STATIC(MyChatListModel, chatListModelInstance)

View File

@ -7,6 +7,9 @@
#include "localdocs.h"
#include "mysettings.h"
#include "network.h"
#include "tool.h"
#include "toolmodel.h"
#include "toolcallparser.h"
#include <fmt/format.h>
@ -55,6 +58,7 @@
#include <vector>
using namespace Qt::Literals::StringLiterals;
using namespace ToolEnums;
namespace ranges = std::ranges;
//#define DEBUG
@ -643,40 +647,16 @@ bool isAllSpace(R &&r)
void ChatLLM::regenerateResponse(int index)
{
Q_ASSERT(m_chatModel);
int promptIdx;
{
auto items = m_chatModel->chatItems(); // holds lock
if (index < 1 || index >= items.size() || items[index].type() != ChatItem::Type::Response)
return;
promptIdx = m_chatModel->getPeerUnlocked(index).value_or(-1);
if (m_chatModel->regenerateResponse(index)) {
emit responseChanged();
prompt(m_chat->collectionList());
}
emit responseChanged({});
m_chatModel->truncate(index + 1);
m_chatModel->updateCurrentResponse(index, true );
m_chatModel->updateNewResponse (index, {} );
m_chatModel->updateStopped (index, false);
m_chatModel->updateThumbsUpState (index, false);
m_chatModel->updateThumbsDownState(index, false);
m_chatModel->setError(false);
if (promptIdx >= 0)
m_chatModel->updateSources(promptIdx, {});
prompt(m_chat->collectionList());
}
std::optional<QString> ChatLLM::popPrompt(int index)
{
Q_ASSERT(m_chatModel);
QString content;
{
auto items = m_chatModel->chatItems(); // holds lock
if (index < 0 || index >= items.size() || items[index].type() != ChatItem::Type::Prompt)
return std::nullopt;
content = items[index].value;
}
m_chatModel->truncate(index);
return content;
return m_chatModel->popPrompt(index);
}
ModelInfo ChatLLM::modelInfo() const
@ -737,28 +717,28 @@ void ChatLLM::prompt(const QStringList &enabledCollections)
promptInternalChat(enabledCollections, promptContextFromSettings(m_modelInfo));
} catch (const std::exception &e) {
// FIXME(jared): this is neither translated nor serialized
emit responseFailed(u"Error: %1"_s.arg(QString::fromUtf8(e.what())));
m_chatModel->setResponseValue(u"Error: %1"_s.arg(QString::fromUtf8(e.what())));
m_chatModel->setError();
emit responseStopped(0);
}
}
// FIXME(jared): We can avoid this potentially expensive copy if we use ChatItem pointers, but this is only safe if we
// hold the lock while generating. We can't do that now because Chat is actually in charge of updating the response, not
// ChatLLM.
std::vector<ChatItem> ChatLLM::forkConversation(const QString &prompt) const
std::vector<MessageItem> ChatLLM::forkConversation(const QString &prompt) const
{
Q_ASSERT(m_chatModel);
if (m_chatModel->hasError())
throw std::logic_error("cannot continue conversation with an error");
std::vector<ChatItem> conversation;
std::vector<MessageItem> conversation;
{
auto items = m_chatModel->chatItems(); // holds lock
Q_ASSERT(items.size() >= 2); // should be prompt/response pairs
auto items = m_chatModel->messageItems();
// It is possible the main thread could have erased the conversation while the llm thread,
// is busy forking the conversatoin but it must have set stop generating first
Q_ASSERT(items.size() >= 2 || m_stopGenerating); // should be prompt/response pairs
conversation.reserve(items.size() + 1);
conversation.assign(items.begin(), items.end());
}
conversation.emplace_back(ChatItem::prompt_tag, prompt);
conversation.emplace_back(MessageItem::Type::Prompt, prompt.toUtf8());
return conversation;
}
@ -793,7 +773,7 @@ std::optional<std::string> ChatLLM::checkJinjaTemplateError(const std::string &s
return std::nullopt;
}
std::string ChatLLM::applyJinjaTemplate(std::span<const ChatItem> items) const
std::string ChatLLM::applyJinjaTemplate(std::span<const MessageItem> items) const
{
Q_ASSERT(items.size() >= 1);
@ -820,25 +800,33 @@ std::string ChatLLM::applyJinjaTemplate(std::span<const ChatItem> items) const
uint version = parseJinjaTemplateVersion(chatTemplate);
auto makeMap = [version](const ChatItem &item) {
auto makeMap = [version](const MessageItem &item) {
return jinja2::GenericMap([msg = std::make_shared<JinjaMessage>(version, item)] { return msg.get(); });
};
std::unique_ptr<ChatItem> systemItem;
std::unique_ptr<MessageItem> systemItem;
bool useSystem = !isAllSpace(systemMessage);
jinja2::ValuesList messages;
messages.reserve(useSystem + items.size());
if (useSystem) {
systemItem = std::make_unique<ChatItem>(ChatItem::system_tag, systemMessage);
systemItem = std::make_unique<MessageItem>(MessageItem::Type::System, systemMessage.toUtf8());
messages.emplace_back(makeMap(*systemItem));
}
for (auto &item : items)
messages.emplace_back(makeMap(item));
jinja2::ValuesList toolList;
const int toolCount = ToolModel::globalInstance()->count();
for (int i = 0; i < toolCount; ++i) {
Tool *t = ToolModel::globalInstance()->get(i);
toolList.push_back(t->jinjaValue());
}
jinja2::ValuesMap params {
{ "messages", std::move(messages) },
{ "add_generation_prompt", true },
{ "toolList", toolList },
};
for (auto &[name, token] : model->specialTokens())
params.emplace(std::move(name), std::move(token));
@ -852,48 +840,44 @@ std::string ChatLLM::applyJinjaTemplate(std::span<const ChatItem> items) const
}
auto ChatLLM::promptInternalChat(const QStringList &enabledCollections, const LLModel::PromptContext &ctx,
std::optional<std::pair<int, int>> subrange) -> ChatPromptResult
qsizetype startOffset) -> ChatPromptResult
{
Q_ASSERT(isModelLoaded());
Q_ASSERT(m_chatModel);
// Return a (ChatModelAccessor, std::span) pair where the span represents the relevant messages for this chat.
// "subrange" is used to select only local server messages from the current chat session.
// Return a vector of relevant messages for this chat.
// "startOffset" is used to select only local server messages from the current chat session.
auto getChat = [&]() {
auto items = m_chatModel->chatItems(); // holds lock
std::span view(items);
if (subrange)
view = view.subspan(subrange->first, subrange->second);
Q_ASSERT(view.size() >= 2);
return std::pair(std::move(items), view);
auto items = m_chatModel->messageItems();
if (startOffset > 0)
items.erase(items.begin(), items.begin() + startOffset);
Q_ASSERT(items.size() >= 2);
return items;
};
// copy messages for safety (since we can't hold the lock the whole time)
std::optional<std::pair<int, QString>> query;
{
// Find the prompt that represents the query. Server chats are flexible and may not have one.
auto [_, view] = getChat(); // holds lock
if (auto peer = m_chatModel->getPeer(view, view.end() - 1)) // peer of response
query = { *peer - view.begin(), (*peer)->value };
}
QList<ResultInfo> databaseResults;
if (query && !enabledCollections.isEmpty()) {
auto &[promptIndex, queryStr] = *query;
const int retrievalSize = MySettings::globalInstance()->localDocsRetrievalSize();
emit requestRetrieveFromDB(enabledCollections, queryStr, retrievalSize, &databaseResults); // blocks
m_chatModel->updateSources(promptIndex, databaseResults);
emit databaseResultsChanged(databaseResults);
if (!enabledCollections.isEmpty()) {
std::optional<std::pair<int, QString>> query;
{
// Find the prompt that represents the query. Server chats are flexible and may not have one.
auto items = getChat();
if (auto peer = m_chatModel->getPeer(items, items.end() - 1)) // peer of response
query = { *peer - items.begin(), (*peer)->content() };
}
if (query) {
auto &[promptIndex, queryStr] = *query;
const int retrievalSize = MySettings::globalInstance()->localDocsRetrievalSize();
emit requestRetrieveFromDB(enabledCollections, queryStr, retrievalSize, &databaseResults); // blocks
m_chatModel->updateSources(promptIndex, databaseResults);
emit databaseResultsChanged(databaseResults);
}
}
// copy messages for safety (since we can't hold the lock the whole time)
std::vector<ChatItem> chatItems;
{
auto [_, view] = getChat(); // holds lock
chatItems.assign(view.begin(), view.end() - 1); // exclude new response
}
auto messageItems = getChat();
messageItems.pop_back(); // exclude new response
auto result = promptInternal(chatItems, ctx, !databaseResults.isEmpty());
auto result = promptInternal(messageItems, ctx, !databaseResults.isEmpty());
return {
/*PromptResult*/ {
.response = std::move(result.response),
@ -905,7 +889,7 @@ auto ChatLLM::promptInternalChat(const QStringList &enabledCollections, const LL
}
auto ChatLLM::promptInternal(
const std::variant<std::span<const ChatItem>, std::string_view> &prompt,
const std::variant<std::span<const MessageItem>, std::string_view> &prompt,
const LLModel::PromptContext &ctx,
bool usedLocalDocs
) -> PromptResult
@ -915,14 +899,14 @@ auto ChatLLM::promptInternal(
auto *mySettings = MySettings::globalInstance();
// unpack prompt argument
const std::span<const ChatItem> *chatItems = nullptr;
const std::span<const MessageItem> *messageItems = nullptr;
std::string jinjaBuffer;
std::string_view conversation;
if (auto *nonChat = std::get_if<std::string_view>(&prompt)) {
conversation = *nonChat; // complete the string without a template
} else {
chatItems = &std::get<std::span<const ChatItem>>(prompt);
jinjaBuffer = applyJinjaTemplate(*chatItems);
messageItems = &std::get<std::span<const MessageItem>>(prompt);
jinjaBuffer = applyJinjaTemplate(*messageItems);
conversation = jinjaBuffer;
}
@ -930,8 +914,8 @@ auto ChatLLM::promptInternal(
if (!dynamic_cast<const ChatAPI *>(m_llModelInfo.model.get())) {
auto nCtx = m_llModelInfo.model->contextLength();
std::string jinjaBuffer2;
auto lastMessageRendered = (chatItems && chatItems->size() > 1)
? std::string_view(jinjaBuffer2 = applyJinjaTemplate({ &chatItems->back(), 1 }))
auto lastMessageRendered = (messageItems && messageItems->size() > 1)
? std::string_view(jinjaBuffer2 = applyJinjaTemplate({ &messageItems->back(), 1 }))
: conversation;
int32_t lastMessageLength = m_llModelInfo.model->countPromptTokens(lastMessageRendered);
if (auto limit = nCtx - 4; lastMessageLength > limit) {
@ -951,14 +935,42 @@ auto ChatLLM::promptInternal(
return !m_stopGenerating;
};
auto handleResponse = [this, &result](LLModel::Token token, std::string_view piece) -> bool {
ToolCallParser toolCallParser;
auto handleResponse = [this, &result, &toolCallParser](LLModel::Token token, std::string_view piece) -> bool {
Q_UNUSED(token)
result.responseTokens++;
m_timer->inc();
// FIXME: This is *not* necessarily fully formed utf data because it can be partial at this point
// handle this like below where we have a QByteArray
toolCallParser.update(QString::fromStdString(piece.data()));
// Create a toolcall and split the response if needed
if (!toolCallParser.hasSplit() && toolCallParser.state() == ToolEnums::ParseState::Partial) {
const QPair<QString, QString> pair = toolCallParser.split();
m_chatModel->splitToolCall(pair);
}
result.response.append(piece.data(), piece.size());
auto respStr = QString::fromUtf8(result.response);
emit responseChanged(removeLeadingWhitespace(respStr));
return !m_stopGenerating;
try {
if (toolCallParser.hasSplit())
m_chatModel->setResponseValue(toolCallParser.buffer());
else
m_chatModel->setResponseValue(removeLeadingWhitespace(respStr));
} catch (const std::exception &e) {
// We have a try/catch here because the main thread might have removed the response from
// the chatmodel by erasing the conversation during the response... the main thread sets
// m_stopGenerating before doing so, but it doesn't wait after that to reset the chatmodel
Q_ASSERT(m_stopGenerating);
return false;
}
emit responseChanged();
const bool foundToolCall = toolCallParser.state() == ToolEnums::ParseState::Complete;
return !foundToolCall && !m_stopGenerating;
};
QElapsedTimer totalTime;
@ -978,13 +990,20 @@ auto ChatLLM::promptInternal(
m_timer->stop();
qint64 elapsed = totalTime.elapsed();
const bool foundToolCall = toolCallParser.state() == ToolEnums::ParseState::Complete;
// trim trailing whitespace
auto respStr = QString::fromUtf8(result.response);
if (!respStr.isEmpty() && std::as_const(respStr).back().isSpace())
emit responseChanged(respStr.trimmed());
if (!respStr.isEmpty() && (std::as_const(respStr).back().isSpace() || foundToolCall)) {
if (toolCallParser.hasSplit())
m_chatModel->setResponseValue(toolCallParser.buffer());
else
m_chatModel->setResponseValue(respStr.trimmed());
emit responseChanged();
}
bool doQuestions = false;
if (!m_isServer && chatItems) {
if (!m_isServer && messageItems && !foundToolCall) {
switch (mySettings->suggestionMode()) {
case SuggestionMode::On: doQuestions = true; break;
case SuggestionMode::LocalDocsOnly: doQuestions = usedLocalDocs; break;

View File

@ -220,8 +220,8 @@ Q_SIGNALS:
void modelLoadingPercentageChanged(float);
void modelLoadingError(const QString &error);
void modelLoadingWarning(const QString &warning);
void responseChanged(const QString &response);
void responseFailed(const QString &error);
void responseChanged();
void responseFailed();
void promptProcessing();
void generatingQuestions();
void responseStopped(qint64 promptResponseMs);
@ -251,20 +251,20 @@ protected:
};
ChatPromptResult promptInternalChat(const QStringList &enabledCollections, const LLModel::PromptContext &ctx,
std::optional<std::pair<int, int>> subrange = {});
qsizetype startOffset = 0);
// passing a string_view directly skips templating and uses the raw string
PromptResult promptInternal(const std::variant<std::span<const ChatItem>, std::string_view> &prompt,
PromptResult promptInternal(const std::variant<std::span<const MessageItem>, std::string_view> &prompt,
const LLModel::PromptContext &ctx,
bool usedLocalDocs);
private:
bool loadNewModel(const ModelInfo &modelInfo, QVariantMap &modelLoadProps);
std::vector<ChatItem> forkConversation(const QString &prompt) const;
std::vector<MessageItem> forkConversation(const QString &prompt) const;
// Applies the Jinja template. Query mode returns only the last message without special tokens.
// Returns a (# of messages, rendered prompt) pair.
std::string applyJinjaTemplate(std::span<const ChatItem> items) const;
std::string applyJinjaTemplate(std::span<const MessageItem> items) const;
void generateQuestions(qint64 elapsed);

View File

@ -0,0 +1,345 @@
#include "chatmodel.h"
QList<ResultInfo> ChatItem::consolidateSources(const QList<ResultInfo> &sources)
{
QMap<QString, ResultInfo> groupedData;
for (const ResultInfo &info : sources) {
if (groupedData.contains(info.file)) {
groupedData[info.file].text += "\n---\n" + info.text;
} else {
groupedData[info.file] = info;
}
}
QList<ResultInfo> consolidatedSources = groupedData.values();
return consolidatedSources;
}
void ChatItem::serializeResponse(QDataStream &stream, int version)
{
stream << value;
}
void ChatItem::serializeToolCall(QDataStream &stream, int version)
{
stream << value;
toolCallInfo.serialize(stream, version);
}
void ChatItem::serializeToolResponse(QDataStream &stream, int version)
{
stream << value;
}
void ChatItem::serializeText(QDataStream &stream, int version)
{
stream << value;
}
void ChatItem::serializeSubItems(QDataStream &stream, int version)
{
stream << name;
switch (auto typ = type()) {
using enum ChatItem::Type;
case Response: { serializeResponse(stream, version); break; }
case ToolCall: { serializeToolCall(stream, version); break; }
case ToolResponse: { serializeToolResponse(stream, version); break; }
case Text: { serializeText(stream, version); break; }
case System:
case Prompt:
throw std::invalid_argument(fmt::format("cannot serialize subitem type {}", int(typ)));
}
stream << qsizetype(subItems.size());
for (ChatItem *item :subItems)
item->serializeSubItems(stream, version);
}
void ChatItem::serialize(QDataStream &stream, int version)
{
stream << name;
stream << value;
stream << newResponse;
stream << isCurrentResponse;
stream << stopped;
stream << thumbsUpState;
stream << thumbsDownState;
if (version >= 11 && type() == ChatItem::Type::Response)
stream << isError;
if (version >= 8) {
stream << sources.size();
for (const ResultInfo &info : sources) {
Q_ASSERT(!info.file.isEmpty());
stream << info.collection;
stream << info.path;
stream << info.file;
stream << info.title;
stream << info.author;
stream << info.date;
stream << info.text;
stream << info.page;
stream << info.from;
stream << info.to;
}
} else if (version >= 3) {
QList<QString> references;
QList<QString> referencesContext;
int validReferenceNumber = 1;
for (const ResultInfo &info : sources) {
if (info.file.isEmpty())
continue;
QString reference;
{
QTextStream stream(&reference);
stream << (validReferenceNumber++) << ". ";
if (!info.title.isEmpty())
stream << "\"" << info.title << "\". ";
if (!info.author.isEmpty())
stream << "By " << info.author << ". ";
if (!info.date.isEmpty())
stream << "Date: " << info.date << ". ";
stream << "In " << info.file << ". ";
if (info.page != -1)
stream << "Page " << info.page << ". ";
if (info.from != -1) {
stream << "Lines " << info.from;
if (info.to != -1)
stream << "-" << info.to;
stream << ". ";
}
stream << "[Context](context://" << validReferenceNumber - 1 << ")";
}
references.append(reference);
referencesContext.append(info.text);
}
stream << references.join("\n");
stream << referencesContext;
}
if (version >= 10) {
stream << promptAttachments.size();
for (const PromptAttachment &a : promptAttachments) {
Q_ASSERT(!a.url.isEmpty());
stream << a.url;
stream << a.content;
}
}
if (version >= 12) {
stream << qsizetype(subItems.size());
for (ChatItem *item :subItems)
item->serializeSubItems(stream, version);
}
}
bool ChatItem::deserializeToolCall(QDataStream &stream, int version)
{
stream >> value;
return toolCallInfo.deserialize(stream, version);;
}
bool ChatItem::deserializeToolResponse(QDataStream &stream, int version)
{
stream >> value;
return true;
}
bool ChatItem::deserializeText(QDataStream &stream, int version)
{
stream >> value;
return true;
}
bool ChatItem::deserializeResponse(QDataStream &stream, int version)
{
stream >> value;
return true;
}
bool ChatItem::deserializeSubItems(QDataStream &stream, int version)
{
stream >> name;
try {
type(); // check name
} catch (const std::exception &e) {
qWarning() << "ChatModel ERROR:" << e.what();
return false;
}
switch (auto typ = type()) {
using enum ChatItem::Type;
case Response: { deserializeResponse(stream, version); break; }
case ToolCall: { deserializeToolCall(stream, version); break; }
case ToolResponse: { deserializeToolResponse(stream, version); break; }
case Text: { deserializeText(stream, version); break; }
case System:
case Prompt:
throw std::invalid_argument(fmt::format("cannot serialize subitem type {}", int(typ)));
}
qsizetype count;
stream >> count;
for (int i = 0; i < count; ++i) {
ChatItem *c = new ChatItem(this);
if (!c->deserializeSubItems(stream, version)) {
delete c;
return false;
}
subItems.push_back(c);
}
return true;
}
bool ChatItem::deserialize(QDataStream &stream, int version)
{
if (version < 12) {
int id;
stream >> id;
}
stream >> name;
try {
type(); // check name
} catch (const std::exception &e) {
qWarning() << "ChatModel ERROR:" << e.what();
return false;
}
stream >> value;
if (version < 10) {
// This is deprecated and no longer used
QString prompt;
stream >> prompt;
}
stream >> newResponse;
stream >> isCurrentResponse;
stream >> stopped;
stream >> thumbsUpState;
stream >> thumbsDownState;
if (version >= 11 && type() == ChatItem::Type::Response)
stream >> isError;
if (version >= 8) {
qsizetype count;
stream >> count;
for (int i = 0; i < count; ++i) {
ResultInfo info;
stream >> info.collection;
stream >> info.path;
stream >> info.file;
stream >> info.title;
stream >> info.author;
stream >> info.date;
stream >> info.text;
stream >> info.page;
stream >> info.from;
stream >> info.to;
sources.append(info);
}
consolidatedSources = ChatItem::consolidateSources(sources);
} else if (version >= 3) {
QString references;
QList<QString> referencesContext;
stream >> references;
stream >> referencesContext;
if (!references.isEmpty()) {
QList<QString> referenceList = references.split("\n");
// Ignore empty lines and those that begin with "---" which is no longer used
for (auto it = referenceList.begin(); it != referenceList.end();) {
if (it->trimmed().isEmpty() || it->trimmed().startsWith("---"))
it = referenceList.erase(it);
else
++it;
}
Q_ASSERT(referenceList.size() == referencesContext.size());
for (int j = 0; j < referenceList.size(); ++j) {
QString reference = referenceList[j];
QString context = referencesContext[j];
ResultInfo info;
QTextStream refStream(&reference);
QString dummy;
int validReferenceNumber;
refStream >> validReferenceNumber >> dummy;
// Extract title (between quotes)
if (reference.contains("\"")) {
int startIndex = reference.indexOf('"') + 1;
int endIndex = reference.indexOf('"', startIndex);
info.title = reference.mid(startIndex, endIndex - startIndex);
}
// Extract author (after "By " and before the next period)
if (reference.contains("By ")) {
int startIndex = reference.indexOf("By ") + 3;
int endIndex = reference.indexOf('.', startIndex);
info.author = reference.mid(startIndex, endIndex - startIndex).trimmed();
}
// Extract date (after "Date: " and before the next period)
if (reference.contains("Date: ")) {
int startIndex = reference.indexOf("Date: ") + 6;
int endIndex = reference.indexOf('.', startIndex);
info.date = reference.mid(startIndex, endIndex - startIndex).trimmed();
}
// Extract file name (after "In " and before the "[Context]")
if (reference.contains("In ") && reference.contains(". [Context]")) {
int startIndex = reference.indexOf("In ") + 3;
int endIndex = reference.indexOf(". [Context]", startIndex);
info.file = reference.mid(startIndex, endIndex - startIndex).trimmed();
}
// Extract page number (after "Page " and before the next space)
if (reference.contains("Page ")) {
int startIndex = reference.indexOf("Page ") + 5;
int endIndex = reference.indexOf(' ', startIndex);
if (endIndex == -1) endIndex = reference.length();
info.page = reference.mid(startIndex, endIndex - startIndex).toInt();
}
// Extract lines (after "Lines " and before the next space or hyphen)
if (reference.contains("Lines ")) {
int startIndex = reference.indexOf("Lines ") + 6;
int endIndex = reference.indexOf(' ', startIndex);
if (endIndex == -1) endIndex = reference.length();
int hyphenIndex = reference.indexOf('-', startIndex);
if (hyphenIndex != -1 && hyphenIndex < endIndex) {
info.from = reference.mid(startIndex, hyphenIndex - startIndex).toInt();
info.to = reference.mid(hyphenIndex + 1, endIndex - hyphenIndex - 1).toInt();
} else {
info.from = reference.mid(startIndex, endIndex - startIndex).toInt();
}
}
info.text = context;
sources.append(info);
}
consolidatedSources = ChatItem::consolidateSources(sources);
}
}
if (version >= 10) {
qsizetype count;
stream >> count;
QList<PromptAttachment> attachments;
for (int i = 0; i < count; ++i) {
PromptAttachment a;
stream >> a.url;
stream >> a.content;
attachments.append(a);
}
promptAttachments = attachments;
}
if (version >= 12) {
qsizetype count;
stream >> count;
for (int i = 0; i < count; ++i) {
ChatItem *c = new ChatItem(this);
if (!c->deserializeSubItems(stream, version)) {
delete c;
return false;
}
subItems.push_back(c);
}
}
return true;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,125 @@
#include "codeinterpreter.h"
#include <QJSValue>
#include <QStringList>
#include <QThread>
#include <QVariant>
QString CodeInterpreter::run(const QList<ToolParam> &params, qint64 timeout)
{
m_error = ToolEnums::Error::NoError;
m_errorString = QString();
Q_ASSERT(params.count() == 1
&& params.first().name == "code"
&& params.first().type == ToolEnums::ParamType::String);
const QString code = params.first().value.toString();
QThread workerThread;
CodeInterpreterWorker worker;
worker.moveToThread(&workerThread);
connect(&worker, &CodeInterpreterWorker::finished, &workerThread, &QThread::quit, Qt::DirectConnection);
connect(&workerThread, &QThread::started, [&worker, code]() {
worker.request(code);
});
workerThread.start();
bool timedOut = !workerThread.wait(timeout);
if (timedOut) {
worker.interrupt(); // thread safe
m_error = ToolEnums::Error::TimeoutError;
}
workerThread.quit();
workerThread.wait();
if (!timedOut) {
m_error = worker.error();
m_errorString = worker.errorString();
}
return worker.response();
}
QList<ToolParamInfo> CodeInterpreter::parameters() const
{
return {{
"code",
ToolEnums::ParamType::String,
"javascript code to compute",
true
}};
}
QString CodeInterpreter::symbolicFormat() const
{
return "{human readable plan to complete the task}\n" + ToolCallConstants::CodeInterpreterPrefix + "{code}\n" + ToolCallConstants::CodeInterpreterSuffix;
}
QString CodeInterpreter::examplePrompt() const
{
return R"(Write code to check if a number is prime, use that to see if the number 7 is prime)";
}
QString CodeInterpreter::exampleCall() const
{
static const QString example = R"(function isPrime(n) {
if (n <= 1) {
return false;
}
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) {
return false;
}
}
return true;
}
const number = 7;
console.log(`The number ${number} is prime: ${isPrime(number)}`);
)";
return "Certainly! Let's compute the answer to whether the number 7 is prime.\n" + ToolCallConstants::CodeInterpreterPrefix + example + ToolCallConstants::CodeInterpreterSuffix;
}
QString CodeInterpreter::exampleReply() const
{
return R"("The computed result shows that 7 is a prime number.)";
}
CodeInterpreterWorker::CodeInterpreterWorker()
: QObject(nullptr)
{
}
void CodeInterpreterWorker::request(const QString &code)
{
JavaScriptConsoleCapture consoleCapture;
QJSValue consoleObject = m_engine.newQObject(&consoleCapture);
m_engine.globalObject().setProperty("console", consoleObject);
const QJSValue result = m_engine.evaluate(code);
QString resultString = result.isUndefined() ? QString() : result.toString();
// NOTE: We purposely do not set the m_error or m_errorString for the code interpreter since
// we *want* the model to see the response has an error so it can hopefully correct itself. The
// error member variables are intended for tools that have error conditions that cannot be corrected.
// For instance, a tool depending upon the network might set these error variables if the network
// is not available.
if (result.isError()) {
const QStringList lines = code.split('\n');
const int line = result.property("lineNumber").toInt();
const int index = line - 1;
const QString lineContent = (index >= 0 && index < lines.size()) ? lines.at(index) : "Line not found in code.";
resultString = QString("Uncaught exception at line %1: %2\n\t%3")
.arg(line)
.arg(result.toString())
.arg(lineContent);
m_error = ToolEnums::Error::UnknownError;
m_errorString = resultString;
}
if (resultString.isEmpty())
resultString = consoleCapture.output;
else if (!consoleCapture.output.isEmpty())
resultString += "\n" + consoleCapture.output;
m_response = resultString;
emit finished();
}

View File

@ -0,0 +1,84 @@
#ifndef CODEINTERPRETER_H
#define CODEINTERPRETER_H
#include "tool.h"
#include "toolcallparser.h"
#include <QJSEngine>
#include <QObject>
#include <QString>
#include <QtGlobal>
class JavaScriptConsoleCapture : public QObject
{
Q_OBJECT
public:
QString output;
Q_INVOKABLE void log(const QString &message)
{
const int maxLength = 1024;
if (output.length() >= maxLength)
return;
if (output.length() + message.length() + 1 > maxLength) {
static const QString trunc = "\noutput truncated at " + QString::number(maxLength) + " characters...";
int remainingLength = maxLength - output.length();
if (remainingLength > 0)
output.append(message.left(remainingLength));
output.append(trunc);
Q_ASSERT(output.length() > maxLength);
} else {
output.append(message + "\n");
}
}
};
class CodeInterpreterWorker : public QObject {
Q_OBJECT
public:
CodeInterpreterWorker();
virtual ~CodeInterpreterWorker() {}
QString response() const { return m_response; }
void request(const QString &code);
void interrupt() { m_engine.setInterrupted(true); }
ToolEnums::Error error() const { return m_error; }
QString errorString() const { return m_errorString; }
Q_SIGNALS:
void finished();
private:
QJSEngine m_engine;
QString m_response;
ToolEnums::Error m_error = ToolEnums::Error::NoError;
QString m_errorString;
};
class CodeInterpreter : public Tool
{
Q_OBJECT
public:
explicit CodeInterpreter() : Tool(), m_error(ToolEnums::Error::NoError) {}
virtual ~CodeInterpreter() {}
QString run(const QList<ToolParam> &params, qint64 timeout = 2000) override;
ToolEnums::Error error() const override { return m_error; }
QString errorString() const override { return m_errorString; }
QString name() const override { return tr("Code Interpreter"); }
QString description() const override { return tr("compute javascript code using console.log as output"); }
QString function() const override { return ToolCallConstants::CodeInterpreterFunction; }
QList<ToolParamInfo> parameters() const override;
virtual QString symbolicFormat() const override;
QString examplePrompt() const override;
QString exampleCall() const override;
QString exampleReply() const override;
private:
ToolEnums::Error m_error = ToolEnums::Error::NoError;
QString m_errorString;
};
#endif // CODEINTERPRETER_H

View File

@ -51,12 +51,14 @@ auto JinjaMessage::keys() const -> const std::unordered_set<std::string_view> &
static const std::unordered_set<std::string_view> userKeys
{ "role", "content", "sources", "prompt_attachments" };
switch (m_item->type()) {
using enum ChatItem::Type;
using enum MessageItem::Type;
case System:
case Response:
case ToolResponse:
return baseKeys;
case Prompt:
return userKeys;
break;
}
Q_UNREACHABLE();
}
@ -67,16 +69,18 @@ bool operator==(const JinjaMessage &a, const JinjaMessage &b)
return true;
const auto &[ia, ib] = std::tie(*a.m_item, *b.m_item);
auto type = ia.type();
if (type != ib.type() || ia.value != ib.value)
if (type != ib.type() || ia.content() != ib.content())
return false;
switch (type) {
using enum ChatItem::Type;
using enum MessageItem::Type;
case System:
case Response:
case ToolResponse:
return true;
case Prompt:
return ia.sources == ib.sources && ia.promptAttachments == ib.promptAttachments;
return ia.sources() == ib.sources() && ia.promptAttachments() == ib.promptAttachments();
break;
}
Q_UNREACHABLE();
}
@ -84,26 +88,28 @@ bool operator==(const JinjaMessage &a, const JinjaMessage &b)
const JinjaFieldMap<JinjaMessage> JinjaMessage::s_fields = {
{ "role", [](auto &m) {
switch (m.item().type()) {
using enum ChatItem::Type;
using enum MessageItem::Type;
case System: return "system"sv;
case Prompt: return "user"sv;
case Response: return "assistant"sv;
case ToolResponse: return "tool"sv;
break;
}
Q_UNREACHABLE();
} },
{ "content", [](auto &m) {
if (m.version() == 0 && m.item().type() == ChatItem::Type::Prompt)
if (m.version() == 0 && m.item().type() == MessageItem::Type::Prompt)
return m.item().bakedPrompt().toStdString();
return m.item().value.toStdString();
return m.item().content().toStdString();
} },
{ "sources", [](auto &m) {
auto sources = m.item().sources | views::transform([](auto &r) {
auto sources = m.item().sources() | views::transform([](auto &r) {
return jinja2::GenericMap([map = std::make_shared<JinjaResultInfo>(r)] { return map.get(); });
});
return jinja2::ValuesList(sources.begin(), sources.end());
} },
{ "prompt_attachments", [](auto &m) {
auto attachments = m.item().promptAttachments | views::transform([](auto &pa) {
auto attachments = m.item().promptAttachments() | views::transform([](auto &pa) {
return jinja2::GenericMap([map = std::make_shared<JinjaPromptAttachment>(pa)] { return map.get(); });
});
return jinja2::ValuesList(attachments.begin(), attachments.end());

View File

@ -86,12 +86,12 @@ private:
class JinjaMessage : public JinjaHelper<JinjaMessage> {
public:
explicit JinjaMessage(uint version, const ChatItem &item) noexcept
explicit JinjaMessage(uint version, const MessageItem &item) noexcept
: m_version(version), m_item(&item) {}
const JinjaMessage &value () const { return *this; }
uint version() const { return m_version; }
const ChatItem &item () const { return *m_item; }
const MessageItem &item () const { return *m_item; }
size_t GetSize() const override { return keys().size(); }
bool HasValue(const std::string &name) const override { return keys().contains(name); }
@ -107,7 +107,7 @@ private:
private:
static const JinjaFieldMap<JinjaMessage> s_fields;
uint m_version;
const ChatItem *m_item;
const MessageItem *m_item;
friend class JinjaHelper<JinjaMessage>;
friend bool operator==(const JinjaMessage &a, const JinjaMessage &b);

View File

@ -7,6 +7,7 @@
#include "modellist.h"
#include "mysettings.h"
#include "network.h"
#include "toolmodel.h"
#include <gpt4all-backend/llmodel.h>
#include <singleapplication.h>
@ -116,6 +117,8 @@ int main(int argc, char *argv[])
qmlRegisterSingletonInstance("download", 1, 0, "Download", Download::globalInstance());
qmlRegisterSingletonInstance("network", 1, 0, "Network", Network::globalInstance());
qmlRegisterSingletonInstance("localdocs", 1, 0, "LocalDocs", LocalDocs::globalInstance());
qmlRegisterSingletonInstance("toollist", 1, 0, "ToolList", ToolModel::globalInstance());
qmlRegisterUncreatableMetaObject(ToolEnums::staticMetaObject, "toolenums", 1, 0, "ToolEnums", "Error: only enums");
qmlRegisterUncreatableMetaObject(MySettingsEnums::staticMetaObject, "mysettingsenums", 1, 0, "MySettingsEnums", "Error: only enums");
{

View File

@ -473,14 +473,24 @@ GPT4AllDownloadableModels::GPT4AllDownloadableModels(QObject *parent)
connect(this, &GPT4AllDownloadableModels::modelReset, this, &GPT4AllDownloadableModels::countChanged);
}
void GPT4AllDownloadableModels::filter(const QVector<QString> &keywords)
{
m_keywords = keywords;
invalidateFilter();
}
bool GPT4AllDownloadableModels::filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
bool hasDescription = !sourceModel()->data(index, ModelList::DescriptionRole).toString().isEmpty();
const QString description = sourceModel()->data(index, ModelList::DescriptionRole).toString();
bool hasDescription = !description.isEmpty();
bool isClone = sourceModel()->data(index, ModelList::IsCloneRole).toBool();
bool isDiscovered = sourceModel()->data(index, ModelList::IsDiscoveredRole).toBool();
return !isDiscovered && hasDescription && !isClone;
bool satisfiesKeyword = m_keywords.isEmpty();
for (const QString &k : m_keywords)
satisfiesKeyword = description.contains(k) ? true : satisfiesKeyword;
return !isDiscovered && hasDescription && !isClone && satisfiesKeyword;
}
int GPT4AllDownloadableModels::count() const

View File

@ -302,11 +302,16 @@ public:
explicit GPT4AllDownloadableModels(QObject *parent);
int count() const;
Q_INVOKABLE void filter(const QVector<QString> &keywords);
Q_SIGNALS:
void countChanged();
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
private:
QVector<QString> m_keywords;
};
class HuggingFaceDownloadableModels : public QSortFilterProxyModel

View File

@ -694,7 +694,8 @@ auto Server::handleCompletionRequest(const CompletionRequest &request)
promptCtx,
/*usedLocalDocs*/ false);
} catch (const std::exception &e) {
emit responseChanged(e.what());
m_chatModel->setResponseValue(e.what());
m_chatModel->setError();
emit responseStopped(0);
return makeError(QHttpServerResponder::StatusCode::InternalServerError);
}
@ -772,16 +773,16 @@ auto Server::handleChatRequest(const ChatRequest &request)
Q_ASSERT(!request.messages.isEmpty());
// adds prompt/response items to GUI
QList<ChatItem> chatItems;
std::vector<MessageInput> messages;
for (auto &message : request.messages) {
using enum ChatRequest::Message::Role;
switch (message.role) {
case System: chatItems.emplace_back(ChatItem::system_tag, message.content); break;
case User: chatItems.emplace_back(ChatItem::prompt_tag, message.content); break;
case Assistant: chatItems.emplace_back(ChatItem::response_tag, message.content); break;
case System: messages.push_back({ MessageInput::Type::System, message.content }); break;
case User: messages.push_back({ MessageInput::Type::Prompt, message.content }); break;
case Assistant: messages.push_back({ MessageInput::Type::Response, message.content }); break;
}
}
auto subrange = m_chatModel->appendResponseWithHistory(chatItems);
auto startOffset = m_chatModel->appendResponseWithHistory(messages);
// FIXME(jared): taking parameters from the UI inhibits reproducibility of results
LLModel::PromptContext promptCtx {
@ -801,9 +802,10 @@ auto Server::handleChatRequest(const ChatRequest &request)
for (int i = 0; i < request.n; ++i) {
ChatPromptResult result;
try {
result = promptInternalChat(m_collections, promptCtx, subrange);
result = promptInternalChat(m_collections, promptCtx, startOffset);
} catch (const std::exception &e) {
emit responseChanged(e.what());
m_chatModel->setResponseValue(e.what());
m_chatModel->setError();
emit responseStopped(0);
return makeError(QHttpServerResponder::StatusCode::InternalServerError);
}

74
gpt4all-chat/src/tool.cpp Normal file
View File

@ -0,0 +1,74 @@
#include "tool.h"
#include <jinja2cpp/value.h>
#include <string>
jinja2::Value Tool::jinjaValue() const
{
jinja2::ValuesList paramList;
const QList<ToolParamInfo> p = parameters();
for (auto &info : p) {
std::string typeStr;
switch (info.type) {
using enum ToolEnums::ParamType;
case String: typeStr = "string"; break;
case Number: typeStr = "number"; break;
case Integer: typeStr = "integer"; break;
case Object: typeStr = "object"; break;
case Array: typeStr = "array"; break;
case Boolean: typeStr = "boolean"; break;
case Null: typeStr = "null"; break;
}
jinja2::ValuesMap infoMap {
{ "name", info.name.toStdString() },
{ "type", typeStr},
{ "description", info.description.toStdString() },
{ "required", info.required }
};
paramList.push_back(infoMap);
}
jinja2::ValuesMap params {
{ "name", name().toStdString() },
{ "description", description().toStdString() },
{ "function", function().toStdString() },
{ "parameters", paramList },
{ "symbolicFormat", symbolicFormat().toStdString() },
{ "examplePrompt", examplePrompt().toStdString() },
{ "exampleCall", exampleCall().toStdString() },
{ "exampleReply", exampleReply().toStdString() }
};
return params;
}
void ToolCallInfo::serialize(QDataStream &stream, int version)
{
stream << name;
stream << params.size();
for (auto param : params) {
stream << param.name;
stream << param.type;
stream << param.value;
}
stream << result;
stream << error;
stream << errorString;
}
bool ToolCallInfo::deserialize(QDataStream &stream, int version)
{
stream >> name;
qsizetype count;
stream >> count;
for (int i = 0; i < count; ++i) {
ToolParam p;
stream >> p.name;
stream >> p.type;
stream >> p.value;
}
stream >> result;
stream >> error;
stream >> errorString;
return true;
}

127
gpt4all-chat/src/tool.h Normal file
View File

@ -0,0 +1,127 @@
#ifndef TOOL_H
#define TOOL_H
#include <QList>
#include <QObject>
#include <QString>
#include <QVariant>
#include <QtGlobal>
#include <jinja2cpp/value.h>
namespace ToolEnums
{
Q_NAMESPACE
enum class Error
{
NoError = 0,
TimeoutError = 2,
UnknownError = 499,
};
Q_ENUM_NS(Error)
enum class ParamType { String, Number, Integer, Object, Array, Boolean, Null }; // json schema types
Q_ENUM_NS(ParamType)
enum class ParseState {
None,
InStart,
Partial,
Complete,
};
Q_ENUM_NS(ParseState)
}
struct ToolParamInfo
{
QString name;
ToolEnums::ParamType type;
QString description;
bool required;
};
Q_DECLARE_METATYPE(ToolParamInfo)
struct ToolParam
{
QString name;
ToolEnums::ParamType type;
QVariant value;
bool operator==(const ToolParam& other) const
{
return name == other.name && type == other.type && value == other.value;
}
};
Q_DECLARE_METATYPE(ToolParam)
struct ToolCallInfo
{
QString name;
QList<ToolParam> params;
QString result;
ToolEnums::Error error = ToolEnums::Error::NoError;
QString errorString;
void serialize(QDataStream &stream, int version);
bool deserialize(QDataStream &stream, int version);
bool operator==(const ToolCallInfo& other) const
{
return name == other.name && result == other.result && params == other.params
&& error == other.error && errorString == other.errorString;
}
};
Q_DECLARE_METATYPE(ToolCallInfo)
class Tool : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString description READ description CONSTANT)
Q_PROPERTY(QString function READ function CONSTANT)
Q_PROPERTY(QList<ToolParamInfo> parameters READ parameters CONSTANT)
Q_PROPERTY(QString examplePrompt READ examplePrompt CONSTANT)
Q_PROPERTY(QString exampleCall READ exampleCall CONSTANT)
Q_PROPERTY(QString exampleReply READ exampleReply CONSTANT)
public:
Tool() : QObject(nullptr) {}
virtual ~Tool() {}
virtual QString run(const QList<ToolParam> &params, qint64 timeout = 2000) = 0;
// Tools should set these if they encounter errors. For instance, a tool depending upon the network
// might set these error variables if the network is not available.
virtual ToolEnums::Error error() const { return ToolEnums::Error::NoError; }
virtual QString errorString() const { return QString(); }
// [Required] Human readable name of the tool.
virtual QString name() const = 0;
// [Required] Human readable description of what the tool does. Use this tool to: {{description}}
virtual QString description() const = 0;
// [Required] Must be unique. Name of the function to invoke. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.
virtual QString function() const = 0;
// [Optional] List describing the tool's parameters. An empty list specifies no parameters.
virtual QList<ToolParamInfo> parameters() const { return {}; }
// [Optional] The symbolic format of the toolcall.
virtual QString symbolicFormat() const { return QString(); }
// [Optional] A human generated example of a prompt that could result in this tool being called.
virtual QString examplePrompt() const { return QString(); }
// [Optional] An example of this tool call that pairs with the example query. It should be the
// complete string that the model must generate.
virtual QString exampleCall() const { return QString(); }
// [Optional] An example of the reply the model might generate given the result of the tool call.
virtual QString exampleReply() const { return QString(); }
bool operator==(const Tool &other) const { return function() == other.function(); }
jinja2::Value jinjaValue() const;
};
#endif // TOOL_H

View File

@ -0,0 +1,111 @@
#include "toolcallparser.h"
#include <QDebug>
#include <QtGlobal>
#include <QtLogging>
#include <cstddef>
static const QString ToolCallStart = ToolCallConstants::CodeInterpreterTag;
static const QString ToolCallEnd = ToolCallConstants::CodeInterpreterEndTag;
ToolCallParser::ToolCallParser()
{
reset();
}
void ToolCallParser::reset()
{
// Resets the search state, but not the buffer or global state
resetSearchState();
// These are global states maintained between update calls
m_buffer.clear();
m_hasSplit = false;
}
void ToolCallParser::resetSearchState()
{
m_expected = ToolCallStart.at(0);
m_expectedIndex = 0;
m_state = ToolEnums::ParseState::None;
m_toolCall.clear();
m_endTagBuffer.clear();
m_startIndex = -1;
}
// This method is called with an arbitrary string and a current state. This method should take the
// current state into account and then parse through the update character by character to arrive at
// the new state.
void ToolCallParser::update(const QString &update)
{
Q_ASSERT(m_state != ToolEnums::ParseState::Complete);
if (m_state == ToolEnums::ParseState::Complete) {
qWarning() << "ERROR: ToolCallParser::update already found a complete toolcall!";
return;
}
m_buffer.append(update);
for (size_t i = m_buffer.size() - update.size(); i < m_buffer.size(); ++i) {
const QChar c = m_buffer[i];
const bool foundMatch = m_expected.isNull() || c == m_expected;
if (!foundMatch) {
resetSearchState();
continue;
}
switch (m_state) {
case ToolEnums::ParseState::None:
{
m_expectedIndex = 1;
m_expected = ToolCallStart.at(1);
m_state = ToolEnums::ParseState::InStart;
m_startIndex = i;
break;
}
case ToolEnums::ParseState::InStart:
{
if (m_expectedIndex == ToolCallStart.size() - 1) {
m_expectedIndex = 0;
m_expected = QChar();
m_state = ToolEnums::ParseState::Partial;
} else {
++m_expectedIndex;
m_expected = ToolCallStart.at(m_expectedIndex);
}
break;
}
case ToolEnums::ParseState::Partial:
{
m_toolCall.append(c);
m_endTagBuffer.append(c);
if (m_endTagBuffer.size() > ToolCallEnd.size())
m_endTagBuffer.remove(0, 1);
if (m_endTagBuffer == ToolCallEnd) {
m_toolCall.chop(ToolCallEnd.size());
m_state = ToolEnums::ParseState::Complete;
m_endTagBuffer.clear();
}
}
case ToolEnums::ParseState::Complete:
{
// Already complete, do nothing further
break;
}
}
}
}
QPair<QString, QString> ToolCallParser::split()
{
Q_ASSERT(m_state == ToolEnums::ParseState::Partial
|| m_state == ToolEnums::ParseState::Complete);
Q_ASSERT(m_startIndex >= 0);
m_hasSplit = true;
const QString beforeToolCall = m_buffer.left(m_startIndex);
m_buffer = m_buffer.mid(m_startIndex);
m_startIndex = 0;
return { beforeToolCall, m_buffer };
}

View File

@ -0,0 +1,47 @@
#ifndef TOOLCALLPARSER_H
#define TOOLCALLPARSER_H
#include "tool.h"
#include <QChar>
#include <QString>
#include <QPair>
namespace ToolCallConstants
{
const QString CodeInterpreterFunction = R"(javascript_interpret)";
const QString CodeInterpreterTag = R"(<)" + CodeInterpreterFunction + R"(>)";
const QString CodeInterpreterEndTag = R"(</)" + CodeInterpreterFunction + R"(>)";
const QString CodeInterpreterPrefix = CodeInterpreterTag + "\n```javascript\n";
const QString CodeInterpreterSuffix = "```\n" + CodeInterpreterEndTag;
}
class ToolCallParser
{
public:
ToolCallParser();
void reset();
void update(const QString &update);
QString buffer() const { return m_buffer; }
QString toolCall() const { return m_toolCall; }
int startIndex() const { return m_startIndex; }
ToolEnums::ParseState state() const { return m_state; }
// Splits
QPair<QString, QString> split();
bool hasSplit() const { return m_hasSplit; }
private:
void resetSearchState();
QChar m_expected;
int m_expectedIndex;
ToolEnums::ParseState m_state;
QString m_buffer;
QString m_toolCall;
QString m_endTagBuffer;
int m_startIndex;
bool m_hasSplit;
};
#endif // TOOLCALLPARSER_H

View File

@ -0,0 +1,31 @@
#include "toolmodel.h"
#include "codeinterpreter.h"
#include <QCoreApplication>
#include <QEvent>
#include <QGlobalStatic>
class MyToolModel: public ToolModel { };
Q_GLOBAL_STATIC(MyToolModel, toolModelInstance)
ToolModel *ToolModel::globalInstance()
{
return toolModelInstance();
}
ToolModel::ToolModel()
: QAbstractListModel(nullptr) {
QCoreApplication::instance()->installEventFilter(this);
Tool* codeInterpreter = new CodeInterpreter;
m_tools.append(codeInterpreter);
m_toolMap.insert(codeInterpreter->function(), codeInterpreter);
}
bool ToolModel::eventFilter(QObject *obj, QEvent *ev)
{
if (obj == QCoreApplication::instance() && ev->type() == QEvent::LanguageChange)
emit dataChanged(index(0, 0), index(m_tools.size() - 1, 0));
return false;
}

View File

@ -0,0 +1,110 @@
#ifndef TOOLMODEL_H
#define TOOLMODEL_H
#include "tool.h"
#include <QAbstractListModel>
#include <QByteArray>
#include <QHash>
#include <QList>
#include <QString>
#include <QVariant>
#include <QtGlobal>
class ToolModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
static ToolModel *globalInstance();
enum Roles {
NameRole = Qt::UserRole + 1,
DescriptionRole,
FunctionRole,
ParametersRole,
SymbolicFormatRole,
ExamplePromptRole,
ExampleCallRole,
ExampleReplyRole,
};
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent)
return m_tools.size();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_tools.size())
return QVariant();
const Tool *item = m_tools.at(index.row());
switch (role) {
case NameRole:
return item->name();
case DescriptionRole:
return item->description();
case FunctionRole:
return item->function();
case ParametersRole:
return QVariant::fromValue(item->parameters());
case SymbolicFormatRole:
return item->symbolicFormat();
case ExamplePromptRole:
return item->examplePrompt();
case ExampleCallRole:
return item->exampleCall();
case ExampleReplyRole:
return item->exampleReply();
}
return QVariant();
}
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[DescriptionRole] = "description";
roles[FunctionRole] = "function";
roles[ParametersRole] = "parameters";
roles[SymbolicFormatRole] = "symbolicFormat";
roles[ExamplePromptRole] = "examplePrompt";
roles[ExampleCallRole] = "exampleCall";
roles[ExampleReplyRole] = "exampleReply";
return roles;
}
Q_INVOKABLE Tool* get(int index) const
{
if (index < 0 || index >= m_tools.size()) return nullptr;
return m_tools.at(index);
}
Q_INVOKABLE Tool *get(const QString &id) const
{
if (!m_toolMap.contains(id)) return nullptr;
return m_toolMap.value(id);
}
int count() const { return m_tools.size(); }
Q_SIGNALS:
void countChanged();
void valueChanged(int index, const QString &value);
protected:
bool eventFilter(QObject *obj, QEvent *ev) override;
private:
explicit ToolModel();
~ToolModel() {}
friend class MyToolModel;
QList<Tool*> m_tools;
QHash<QString, Tool*> m_toolMap;
};
#endif // TOOLMODEL_H