mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2025-05-03 22:17:22 +00:00
Code interpreter (#3173)
Signed-off-by: Adam Treat <treat.adam@gmail.com>
This commit is contained in:
parent
2efb336b8a
commit
1c89447d63
@ -11,7 +11,6 @@ function(gpt4all_add_warning_options target)
|
||||
-Wextra-semi
|
||||
-Wformat=2
|
||||
-Wmissing-include-dirs
|
||||
-Wstrict-overflow=2
|
||||
-Wsuggest-override
|
||||
-Wvla
|
||||
# errors
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
160
gpt4all-chat/qml/ChatCollapsibleItem.qml
Normal file
160
gpt4all-chat/qml/ChatCollapsibleItem.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
139
gpt4all-chat/qml/ChatTextItem.qml
Normal file
139
gpt4all-chat/qml/ChatTextItem.qml
Normal 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"
|
||||
}
|
@ -824,6 +824,8 @@ Rectangle {
|
||||
textInput.forceActiveFocus();
|
||||
textInput.cursorPosition = text.length;
|
||||
}
|
||||
height: visible ? implicitHeight : 0
|
||||
visible: name !== "ToolResponse: "
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
|
@ -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()},
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
345
gpt4all-chat/src/chatmodel.cpp
Normal file
345
gpt4all-chat/src/chatmodel.cpp
Normal 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
125
gpt4all-chat/src/codeinterpreter.cpp
Normal file
125
gpt4all-chat/src/codeinterpreter.cpp
Normal file
@ -0,0 +1,125 @@
|
||||
#include "codeinterpreter.h"
|
||||
|
||||
#include <QJSValue>
|
||||
#include <QStringList>
|
||||
#include <QThread>
|
||||
#include <QVariant>
|
||||
|
||||
QString CodeInterpreter::run(const QList<ToolParam> ¶ms, 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();
|
||||
}
|
84
gpt4all-chat/src/codeinterpreter.h
Normal file
84
gpt4all-chat/src/codeinterpreter.h
Normal 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> ¶ms, 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
|
@ -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());
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
74
gpt4all-chat/src/tool.cpp
Normal 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
127
gpt4all-chat/src/tool.h
Normal 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> ¶ms, 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
|
111
gpt4all-chat/src/toolcallparser.cpp
Normal file
111
gpt4all-chat/src/toolcallparser.cpp
Normal 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 };
|
||||
}
|
47
gpt4all-chat/src/toolcallparser.h
Normal file
47
gpt4all-chat/src/toolcallparser.h
Normal 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
|
31
gpt4all-chat/src/toolmodel.cpp
Normal file
31
gpt4all-chat/src/toolmodel.cpp
Normal 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;
|
||||
}
|
110
gpt4all-chat/src/toolmodel.h
Normal file
110
gpt4all-chat/src/toolmodel.h
Normal 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
|
Loading…
Reference in New Issue
Block a user