Compare commits

...

30 Commits

Author SHA1 Message Date
Adam Treat
4ae6acdedc Force tool usage and refactor.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-15 16:25:26 -04:00
Adam Treat
3a564688b1 Implement all the settings for the web search.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 16:19:28 -04:00
Adam Treat
054ca43d52 Display the antenna by introducing notion of privacy scopes to tools.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 12:14:31 -04:00
Adam Treat
4cb95694ff Breakout the ask before running which can be thought of as a security feature.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 11:22:22 -04:00
Adam Treat
991afc6ef2 Abstract the built-in web search completely away from ChatLLM.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 10:24:06 -04:00
Adam Treat
75dbf9de7d Handle the forced usage of tool calls outside of the recursive prompt method.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 09:39:01 -04:00
Adam Treat
f118720717 Don't advertise brave.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:58:31 -04:00
Adam Treat
a6730879dd Move to a brave search specific settings page.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
48117cda46 Move the jinja processing to mysettings and validation.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
227dbfd18b Use an enum for tool usage mode.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
587dd55b73 Get rid of the name change now that 3.2.0 has been released.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
c3cfaff803 Refactor and make use of jinja templates.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
00ecbb75b4 Add jinja third party dependency.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
f93b76438e Move the usearch submodule to third_party dir.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
cedba6cd10 Tool model.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
244b82622c Implement error handling for tool calls.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
5fc2ff8e69 Use parameters which is in keeping with other standard practices.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
27b86dae21 Serialize the source excerpts from and to pure json
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
01f67c74ea Begin converting the localdocs to a tool.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
dfe3e951d4 Refactor the brave search and introduce an abstraction for tool calls.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
fffd9f341a Refactor to handle errors in tool calling better and add source comments.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
7c7558eed3 Stop hardcoding the tool call checking and rely upon the format advocated by ollama for tool calling.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
b9684ff741 Inc again for new beta.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
b0578e28b9 Change the name to inc the beta.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
d2ee235388 Change the suggestion mode to turn on for tool calls by default.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
dda59a97a6 Fix the way we're injecting the context back into the model for web search.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
c78c95ab42 Add the extra snippets to the source excerpts.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
1c9911a4ac Fix problem with only displaying one source for tool call excerpts.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
a71db10124 Change the name to announce the beta.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
Adam Treat
f4fb3df437 Brave search tool calling.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-08-14 07:43:48 -04:00
36 changed files with 2025 additions and 370 deletions

5
.gitmodules vendored
View File

@@ -3,5 +3,8 @@
url = https://github.com/nomic-ai/llama.cpp.git
branch = master
[submodule "gpt4all-chat/usearch"]
path = gpt4all-chat/usearch
path = gpt4all-chat/third_party/usearch
url = https://github.com/nomic-ai/usearch.git
[submodule "gpt4all-chat/third_party/jinja2cpp"]
path = gpt4all-chat/third_party/jinja2cpp
url = https://github.com/nomic-ai/jinja2cpp.git

View File

@@ -107,6 +107,7 @@ endif()
qt_add_executable(chat
main.cpp
bravesearch.h bravesearch.cpp
chat.h chat.cpp
chatllm.h chatllm.cpp
chatmodel.h chatlistmodel.h chatlistmodel.cpp
@@ -115,13 +116,15 @@ qt_add_executable(chat
database.h database.cpp
download.h download.cpp
embllm.cpp embllm.h
localdocs.h localdocs.cpp localdocsmodel.h localdocsmodel.cpp
localdocs.h localdocs.cpp localdocsmodel.h localdocsmodel.cpp localdocssearch.h localdocssearch.cpp
llm.h llm.cpp
modellist.h modellist.cpp
mysettings.h mysettings.cpp
network.h network.cpp
sourceexcerpt.h sourceexcerpt.cpp
server.h server.cpp
logger.h logger.cpp
tool.h tool.cpp toolmodel.h toolmodel.cpp
${APP_ICON_RESOURCE}
${CHAT_EXE_RESOURCES}
)
@@ -174,6 +177,7 @@ qt_add_qml_module(chat
qml/MyTextField.qml
qml/MyToolButton.qml
qml/MyWelcomeButton.qml
qml/WebSearchSettings.qml
RESOURCES
icons/antenna_1.svg
icons/antenna_2.svg
@@ -289,8 +293,10 @@ target_compile_definitions(chat
# usearch uses the identifier 'slots' which conflicts with Qt's 'slots' keyword
target_compile_definitions(chat PRIVATE QT_NO_SIGNALS_SLOTS_KEYWORDS)
target_include_directories(chat PRIVATE usearch/include
usearch/fp16/include)
target_include_directories(chat PRIVATE third_party/usearch/include
third_party/usearch/fp16/include)
add_subdirectory(third_party/jinja2cpp ${CMAKE_BINARY_DIR}/jinja2cpp)
if(LINUX)
target_link_libraries(chat
@@ -300,7 +306,7 @@ else()
PRIVATE Qt6::Quick Qt6::Svg Qt6::HttpServer Qt6::Sql Qt6::Pdf)
endif()
target_link_libraries(chat
PRIVATE llmodel)
PRIVATE llmodel jinja2cpp)
# -- install --

View File

@@ -0,0 +1,234 @@
#include "bravesearch.h"
#include "mysettings.h"
#include <QCoreApplication>
#include <QDebug>
#include <QGuiApplication>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QThread>
#include <QUrl>
#include <QUrlQuery>
using namespace Qt::Literals::StringLiterals;
BraveSearch::BraveSearch()
: Tool(), m_error(ToolEnums::Error::NoError)
{
connect(MySettings::globalInstance(), &MySettings::webSearchUsageModeChanged,
this, &Tool::usageModeChanged);
connect(MySettings::globalInstance(), &MySettings::webSearchConfirmationModeChanged,
this, &Tool::confirmationModeChanged);
}
QString BraveSearch::run(const QJsonObject &parameters, qint64 timeout)
{
// Reset the error state
m_error = ToolEnums::Error::NoError;
m_errorString = QString();
const QString apiKey = MySettings::globalInstance()->braveSearchAPIKey();
const QString query = parameters["query"].toString();
const int count = MySettings::globalInstance()->webSearchRetrievalSize();
QThread workerThread;
BraveAPIWorker worker;
worker.moveToThread(&workerThread);
connect(&worker, &BraveAPIWorker::finished, &workerThread, &QThread::quit, Qt::DirectConnection);
connect(&workerThread, &QThread::started, [&worker, apiKey, query, count]() {
worker.request(apiKey, query, count);
});
workerThread.start();
bool timedOut = !workerThread.wait(timeout);
if (timedOut) {
m_error = ToolEnums::Error::TimeoutError;
m_errorString = tr("ERROR: brave search timeout");
} else {
m_error = worker.error();
m_errorString = worker.errorString();
}
workerThread.quit();
workerThread.wait();
return worker.response();
}
QJsonObject BraveSearch::paramSchema() const
{
static const QString braveParamSchema = R"({
"apiKey": {
"type": "string",
"description": "The api key to use",
"required": true,
"modelGenerated": false,
"userConfigured": true
},
"query": {
"type": "string",
"description": "The query to search",
"required": true
},
"count": {
"type": "integer",
"description": "The number of excerpts to return",
"required": true,
"modelGenerated": false
}
})";
static const QJsonDocument braveJsonDoc = QJsonDocument::fromJson(braveParamSchema.toUtf8());
Q_ASSERT(!braveJsonDoc.isNull() && braveJsonDoc.isObject());
return braveJsonDoc.object();
}
QJsonObject BraveSearch::exampleParams() const
{
static const QString example = R"({
"query": "the 44th president of the United States"
})";
static const QJsonDocument exampleDoc = QJsonDocument::fromJson(example.toUtf8());
Q_ASSERT(!exampleDoc.isNull() && exampleDoc.isObject());
return exampleDoc.object();
}
ToolEnums::UsageMode BraveSearch::usageMode() const
{
return MySettings::globalInstance()->webSearchUsageMode();
}
ToolEnums::ConfirmationMode BraveSearch::confirmationMode() const
{
return MySettings::globalInstance()->webSearchConfirmationMode();
}
void BraveAPIWorker::request(const QString &apiKey, const QString &query, int count)
{
// Documentation on the brave web search:
// https://api.search.brave.com/app/documentation/web-search/get-started
QUrl jsonUrl("https://api.search.brave.com/res/v1/web/search");
// Documentation on the query options:
//https://api.search.brave.com/app/documentation/web-search/query
QUrlQuery urlQuery;
urlQuery.addQueryItem("q", query);
urlQuery.addQueryItem("count", QString::number(count));
urlQuery.addQueryItem("result_filter", "web");
urlQuery.addQueryItem("extra_snippets", "true");
jsonUrl.setQuery(urlQuery);
QNetworkRequest request(jsonUrl);
QSslConfiguration conf = request.sslConfiguration();
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
request.setSslConfiguration(conf);
request.setRawHeader("X-Subscription-Token", apiKey.toUtf8());
request.setRawHeader("Accept", "application/json");
m_networkManager = new QNetworkAccessManager(this);
QNetworkReply *reply = m_networkManager->get(request);
connect(qGuiApp, &QCoreApplication::aboutToQuit, reply, &QNetworkReply::abort);
connect(reply, &QNetworkReply::finished, this, &BraveAPIWorker::handleFinished);
connect(reply, &QNetworkReply::errorOccurred, this, &BraveAPIWorker::handleErrorOccurred);
}
QString BraveAPIWorker::cleanBraveResponse(const QByteArray& jsonResponse)
{
// This parses the response from brave and formats it in json that conforms to the de facto
// standard in SourceExcerpts::fromJson(...)
QJsonParseError err;
QJsonDocument document = QJsonDocument::fromJson(jsonResponse, &err);
if (err.error != QJsonParseError::NoError) {
m_error = ToolEnums::Error::UnknownError;
m_errorString = QString(tr("ERROR: brave search could not parse json response: %1")).arg(jsonResponse);
return QString();
}
QString query;
QJsonObject searchResponse = document.object();
QJsonArray cleanArray;
if (searchResponse.contains("query")) {
QJsonObject queryObj = searchResponse["query"].toObject();
if (queryObj.contains("original"))
query = queryObj["original"].toString();
}
if (searchResponse.contains("mixed")) {
QJsonObject mixedResults = searchResponse["mixed"].toObject();
QJsonArray mainResults = mixedResults["main"].toArray();
QJsonObject resultsObject = searchResponse["web"].toObject();
QJsonArray resultsArray = resultsObject["results"].toArray();
for (int i = 0; i < std::min(mainResults.size(), resultsArray.size()); ++i) {
QJsonObject m = mainResults[i].toObject();
QString r_type = m["type"].toString();
Q_ASSERT(r_type == "web");
const int idx = m["index"].toInt();
QJsonObject resultObj = resultsArray[idx].toObject();
QStringList selectedKeys = {"type", "title", "url"};
QJsonObject result;
for (const auto& key : selectedKeys)
if (resultObj.contains(key))
result.insert(key, resultObj[key]);
if (resultObj.contains("page_age"))
result.insert("date", resultObj["page_age"]);
else
result.insert("date", QDate::currentDate().toString());
QJsonArray excerpts;
if (resultObj.contains("extra_snippets")) {
QJsonArray snippets = resultObj["extra_snippets"].toArray();
for (int i = 0; i < snippets.size(); ++i) {
QString snippet = snippets[i].toString();
QJsonObject excerpt;
excerpt.insert("text", snippet);
excerpts.append(excerpt);
}
if (resultObj.contains("description"))
result.insert("description", resultObj["description"]);
} else {
QJsonObject excerpt;
excerpt.insert("text", resultObj["description"]);
}
if (!excerpts.isEmpty()) {
result.insert("excerpts", excerpts);
cleanArray.append(QJsonValue(result));
}
}
}
QJsonObject cleanResponse;
cleanResponse.insert("query", query);
cleanResponse.insert("results", cleanArray);
QJsonDocument cleanedDoc(cleanResponse);
// qDebug().noquote() << document.toJson(QJsonDocument::Indented);
// qDebug().noquote() << cleanedDoc.toJson(QJsonDocument::Indented);
return cleanedDoc.toJson(QJsonDocument::Compact);
}
void BraveAPIWorker::handleFinished()
{
QNetworkReply *jsonReply = qobject_cast<QNetworkReply *>(sender());
Q_ASSERT(jsonReply);
if (jsonReply->error() == QNetworkReply::NoError && jsonReply->isFinished()) {
QByteArray jsonData = jsonReply->readAll();
jsonReply->deleteLater();
m_response = cleanBraveResponse(jsonData);
} else {
QByteArray jsonData = jsonReply->readAll();
jsonReply->deleteLater();
}
emit finished();
}
void BraveAPIWorker::handleErrorOccurred(QNetworkReply::NetworkError code)
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
Q_ASSERT(reply);
m_error = ToolEnums::Error::UnknownError;
m_errorString = QString(tr("ERROR: brave search code: %1 response: %2")).arg(code).arg(reply->errorString());
emit finished();
}

View File

@@ -0,0 +1,67 @@
#ifndef BRAVESEARCH_H
#define BRAVESEARCH_H
#include "tool.h"
#include <QObject>
#include <QString>
#include <QNetworkAccessManager>
#include <QNetworkReply>
class BraveAPIWorker : public QObject {
Q_OBJECT
public:
BraveAPIWorker()
: QObject(nullptr)
, m_networkManager(nullptr) {}
virtual ~BraveAPIWorker() {}
QString response() const { return m_response; }
ToolEnums::Error error() const { return m_error; }
QString errorString() const { return m_errorString; }
public Q_SLOTS:
void request(const QString &apiKey, const QString &query, int count);
Q_SIGNALS:
void finished();
private Q_SLOTS:
QString cleanBraveResponse(const QByteArray& jsonResponse);
void handleFinished();
void handleErrorOccurred(QNetworkReply::NetworkError code);
private:
QNetworkAccessManager *m_networkManager;
QString m_response;
ToolEnums::Error m_error = ToolEnums::Error::NoError;
QString m_errorString;
};
class BraveSearch : public Tool {
Q_OBJECT
public:
BraveSearch();
virtual ~BraveSearch() {}
QString run(const QJsonObject &parameters, 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("Web Search"); }
QString description() const override { return tr("Search the web"); }
QString function() const override { return "web_search"; }
ToolEnums::PrivacyScope privacyScope() const override { return ToolEnums::PrivacyScope::None; }
QJsonObject paramSchema() const override;
QJsonObject exampleParams() const override;
bool isBuiltin() const override { return true; }
ToolEnums::UsageMode usageMode() const override;
ToolEnums::ConfirmationMode confirmationMode() const override;
bool excerpts() const override { return true; }
private:
ToolEnums::Error m_error;
QString m_errorString;
};
#endif // BRAVESEARCH_H

View File

@@ -59,6 +59,7 @@ void Chat::connectLLM()
connect(m_llmodel, &ChatLLM::responseChanged, this, &Chat::handleResponseChanged, 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::toolCalled, this, &Chat::toolCalled, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::responseStopped, this, &Chat::responseStopped, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::modelLoadingError, this, &Chat::handleModelLoadingError, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::modelLoadingWarning, this, &Chat::modelLoadingWarning, Qt::QueuedConnection);
@@ -67,7 +68,7 @@ void Chat::connectLLM()
connect(m_llmodel, &ChatLLM::generatedQuestionFinished, this, &Chat::generatedQuestionFinished, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::reportSpeed, this, &Chat::handleTokenSpeedChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::loadedModelInfoChanged, this, &Chat::loadedModelInfoChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::databaseResultsChanged, this, &Chat::handleDatabaseResultsChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::sourceExcerptsChanged, this, &Chat::handleSourceExcerptsChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::modelInfoChanged, this, &Chat::handleModelInfoChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::trySwitchContextOfLoadedModelCompleted, this, &Chat::handleTrySwitchContextOfLoadedModelCompleted, Qt::QueuedConnection);
@@ -115,12 +116,14 @@ void Chat::resetResponseState()
if (m_responseInProgress && m_responseState == Chat::LocalDocsRetrieval)
return;
m_sourceExcerpts = QList<SourceExcerpt>();
m_generatedQuestions = QList<QString>();
emit generatedQuestionsChanged();
m_tokenSpeed = QString();
emit tokenSpeedChanged();
m_responseInProgress = true;
m_responseState = m_collections.empty() ? Chat::PromptProcessing : Chat::LocalDocsRetrieval;
m_toolDescription = QString();
emit responseInProgressChanged();
emit responseStateChanged();
}
@@ -134,7 +137,8 @@ void Chat::prompt(const QString &prompt)
void Chat::regenerateResponse()
{
const int index = m_chatModel->count() - 1;
m_chatModel->updateSources(index, QList<ResultInfo>());
m_sourceExcerpts = QList<SourceExcerpt>();
m_chatModel->updateSources(index, QList<SourceExcerpt>());
emit regenerateResponseRequested();
}
@@ -189,8 +193,13 @@ void Chat::handleModelLoadingPercentageChanged(float loadingPercentage)
void Chat::promptProcessing()
{
m_responseState = !databaseResults().isEmpty() ? Chat::LocalDocsProcessing : Chat::PromptProcessing;
emit responseStateChanged();
if (sourceExcerpts().isEmpty())
m_responseState = Chat::PromptProcessing;
else if (m_responseState == Chat::ToolCalled)
m_responseState = Chat::ToolProcessing;
else
m_responseState = Chat::LocalDocsProcessing;
emit responseStateChanged();
}
void Chat::generatingQuestions()
@@ -199,6 +208,14 @@ void Chat::generatingQuestions()
emit responseStateChanged();
}
void Chat::toolCalled(const QString &description)
{
m_responseState = Chat::ToolCalled;
m_toolDescription = description;
emit toolDescriptionChanged();
emit responseStateChanged();
}
void Chat::responseStopped(qint64 promptResponseMs)
{
m_tokenSpeed = QString();
@@ -357,11 +374,11 @@ QString Chat::fallbackReason() const
return m_llmodel->fallbackReason();
}
void Chat::handleDatabaseResultsChanged(const QList<ResultInfo> &results)
void Chat::handleSourceExcerptsChanged(const QList<SourceExcerpt> &sourceExcerpts)
{
m_databaseResults = results;
m_sourceExcerpts = sourceExcerpts;
const int index = m_chatModel->count() - 1;
m_chatModel->updateSources(index, m_databaseResults);
m_chatModel->updateSources(index, m_sourceExcerpts);
}
void Chat::handleModelInfoChanged(const ModelInfo &modelInfo)

View File

@@ -40,6 +40,7 @@ class Chat : public QObject
// 0=no, 1=waiting, 2=working
Q_PROPERTY(int trySwitchContextInProgress READ trySwitchContextInProgress NOTIFY trySwitchContextInProgressChanged)
Q_PROPERTY(QList<QString> generatedQuestions READ generatedQuestions NOTIFY generatedQuestionsChanged)
Q_PROPERTY(QString toolDescription READ toolDescription NOTIFY toolDescriptionChanged)
QML_ELEMENT
QML_UNCREATABLE("Only creatable from c++!")
@@ -50,7 +51,9 @@ public:
LocalDocsProcessing,
PromptProcessing,
GeneratingQuestions,
ResponseGeneration
ResponseGeneration,
ToolCalled,
ToolProcessing
};
Q_ENUM(ResponseState)
@@ -81,9 +84,10 @@ public:
Q_INVOKABLE void stopGenerating();
Q_INVOKABLE void newPromptResponsePair(const QString &prompt);
QList<ResultInfo> databaseResults() const { return m_databaseResults; }
QList<SourceExcerpt> sourceExcerpts() const { return m_sourceExcerpts; }
QString response() const;
QString toolDescription() const { return m_toolDescription; }
bool responseInProgress() const { return m_responseInProgress; }
ResponseState responseState() const;
ModelInfo modelInfo() const;
@@ -158,19 +162,21 @@ Q_SIGNALS:
void trySwitchContextInProgressChanged();
void loadedModelInfoChanged();
void generatedQuestionsChanged();
void toolDescriptionChanged();
private Q_SLOTS:
void handleResponseChanged(const QString &response);
void handleModelLoadingPercentageChanged(float);
void promptProcessing();
void generatingQuestions();
void toolCalled(const QString &description);
void responseStopped(qint64 promptResponseMs);
void generatedNameChanged(const QString &name);
void generatedQuestionFinished(const QString &question);
void handleRestoringFromText();
void handleModelLoadingError(const QString &error);
void handleTokenSpeedChanged(const QString &tokenSpeed);
void handleDatabaseResultsChanged(const QList<ResultInfo> &results);
void handleSourceExcerptsChanged(const QList<SourceExcerpt> &sourceExcerpts);
void handleModelInfoChanged(const ModelInfo &modelInfo);
void handleTrySwitchContextOfLoadedModelCompleted(int value);
@@ -185,6 +191,7 @@ private:
QString m_device;
QString m_fallbackReason;
QString m_response;
QString m_toolDescription;
QList<QString> m_collections;
QList<QString> m_generatedQuestions;
ChatModel *m_chatModel;
@@ -192,7 +199,7 @@ private:
ResponseState m_responseState;
qint64 m_creationDate;
ChatLLM *m_llmodel;
QList<ResultInfo> m_databaseResults;
QList<SourceExcerpt> m_sourceExcerpts;
bool m_isServer = false;
bool m_shouldDeleteLater = false;
float m_modelLoadingPercentage = 0.0f;

View File

@@ -19,7 +19,7 @@
#include <algorithm>
#define CHAT_FORMAT_MAGIC 0xF5D553CC
#define CHAT_FORMAT_VERSION 9
#define CHAT_FORMAT_VERSION 10
class MyChatListModel: public ChatListModel { };
Q_GLOBAL_STATIC(MyChatListModel, chatListModelInstance)
@@ -31,7 +31,7 @@ ChatListModel *ChatListModel::globalInstance()
ChatListModel::ChatListModel()
: QAbstractListModel(nullptr) {
QCoreApplication::instance()->installEventFilter(this);
QCoreApplication::instance()->installEventFilter(this);
}
bool ChatListModel::eventFilter(QObject *obj, QEvent *ev)

View File

@@ -2,15 +2,19 @@
#include "chat.h"
#include "chatapi.h"
#include "localdocs.h"
#include "localdocssearch.h"
#include "mysettings.h"
#include "network.h"
#include "tool.h"
#include "toolmodel.h"
#include <QDataStream>
#include <QDebug>
#include <QFile>
#include <QGlobalStatic>
#include <QGuiApplication>
#include <QIODevice>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMutex>
@@ -33,6 +37,7 @@
#include <vector>
using namespace Qt::Literals::StringLiterals;
using namespace ToolEnums;
//#define DEBUG
//#define DEBUG_MODEL_LOADING
@@ -113,6 +118,9 @@ ChatLLM::ChatLLM(Chat *parent, bool isServer)
, m_reloadingToChangeVariant(false)
, m_processedSystemPrompt(false)
, m_restoreStateFromText(false)
, m_checkToolCall(false)
, m_maybeToolCall(false)
, m_foundToolCall(false)
{
moveToThread(&m_llmThread);
connect(this, &ChatLLM::shouldBeLoadedChanged, this, &ChatLLM::handleShouldBeLoadedChanged,
@@ -123,11 +131,6 @@ ChatLLM::ChatLLM(Chat *parent, bool isServer)
connect(&m_llmThread, &QThread::started, this, &ChatLLM::handleThreadStarted);
connect(MySettings::globalInstance(), &MySettings::forceMetalChanged, this, &ChatLLM::handleForceMetalChanged);
connect(MySettings::globalInstance(), &MySettings::deviceChanged, this, &ChatLLM::handleDeviceChanged);
// The following are blocking operations and will block the llm thread
connect(this, &ChatLLM::requestRetrieveFromDB, LocalDocs::globalInstance()->database(), &Database::retrieveFromDB,
Qt::BlockingQueuedConnection);
m_llmThread.setObjectName(parent->id());
m_llmThread.start();
}
@@ -708,7 +711,28 @@ bool ChatLLM::handleResponse(int32_t token, const std::string &response)
m_timer->inc();
Q_ASSERT(!response.empty());
m_response.append(response);
emit responseChanged(QString::fromStdString(remove_leading_whitespace(m_response)));
// If we're checking for a tool called and the response is equal or exceeds 11 chars
// then we check
if (m_checkToolCall && m_response.size() >= 11) {
if (m_response.starts_with("<tool_call>")) {
m_maybeToolCall = true;
m_response.erase(0, 11);
}
m_checkToolCall = false;
}
// Check if we're at the end of tool call and erase the end tag
if (m_maybeToolCall && m_response.ends_with("</tool_call>")) {
m_foundToolCall = true;
m_response.erase(m_response.length() - 12);
return false;
}
// If we're not checking for tool call and haven't detected one, then send along the response
if (!m_checkToolCall && !m_maybeToolCall)
emit responseChanged(QString::fromStdString(remove_leading_whitespace(m_response)));
return !m_stopGenerating;
}
@@ -737,27 +761,109 @@ bool ChatLLM::prompt(const QList<QString> &collectionList, const QString &prompt
bool ChatLLM::promptInternal(const QList<QString> &collectionList, const QString &prompt, const QString &promptTemplate,
int32_t n_predict, int32_t top_k, float top_p, float min_p, float temp, int32_t n_batch, float repeat_penalty,
int32_t repeat_penalty_tokens)
{
QString toolCallingTemplate = MySettings::globalInstance()->modelToolTemplate(m_modelInfo);
Q_ASSERT(toolCallingTemplate.isEmpty() || toolCallingTemplate.contains("%1"));
if (toolCallingTemplate.isEmpty() || !toolCallingTemplate.contains("%1"))
toolCallingTemplate = u"### Context:\n%1\n\n"_s;
const bool isToolCallingModel = MySettings::globalInstance()->modelIsToolCalling(m_modelInfo);
// Iterate over the list of tools and if force usage is set, then we *try* and force usage here
QList<QString> toolResponses;
qint64 totalTime = 0;
bool producedSourceExcerpts = false;
const int toolCount = ToolModel::globalInstance()->count();
for (int i = 0; i < toolCount; ++i) {
Tool *t = ToolModel::globalInstance()->get(i);
if (t->usageMode() != UsageMode::ForceUsage)
continue;
// Local docs search is unique. It is the _only_ tool where we try and force usage even if
// the model does not support tool calling.
if (!isToolCallingModel && t->function() != "localdocs_search")
continue;
// If this is the localdocs tool call, then we perform the search with the entire prompt as
// the query
if (t->function() == "localdocs_search") {
if (collectionList.isEmpty())
continue;
QJsonObject parameters;
parameters.insert("collections", QJsonArray::fromStringList(collectionList));
parameters.insert("query", prompt);
parameters.insert("count", MySettings::globalInstance()->localDocsRetrievalSize());
// FIXME: Honor the confirmation mode feature
const QString response = t->run(parameters, 2000 /*msecs to timeout*/);
if (t->error() != Error::NoError) {
qWarning() << "ERROR: LocalDocs call produced error:" << t->errorString();
continue;
}
QString parseError;
QList<SourceExcerpt> localDocsExcerpts = SourceExcerpt::fromJson(response, parseError);
if (!parseError.isEmpty()) {
qWarning() << "ERROR: Could not parse source excerpts for localdocs response:" << parseError;
} else {
producedSourceExcerpts = true;
emit sourceExcerptsChanged(localDocsExcerpts);
}
toolResponses << QString(toolCallingTemplate).arg(response);
continue;
}
// For all other cases we should have a tool calling model
Q_ASSERT(isToolCallingModel);
// Create the tool calling response as if the model has chosen this particular tool
const QString toolCallingResponse = QString("<tool_call>{\"name\": \"%1\", \"parameters\": {\"").arg(t->function());
// Mimic that the model has already responded like this to trigger our tool calling detection
// code and then rely upon it to complete the parameters correctly
m_response = toolCallingResponse.toStdString();
// Insert this response as the tool prompt
const QString toolPrompt = QString(promptTemplate).arg(prompt, toolCallingResponse);
const QString toolCall = completeToolCall(toolPrompt, n_predict, top_k, top_p,
min_p, temp, n_batch, repeat_penalty, repeat_penalty_tokens, totalTime);
// If the tool call is empty, then we failed in our attempt to force usage
if (toolCall.isEmpty()) {
qWarning() << "WARNING: Attempt to force usage of toolcall" << t->function() << "failed:"
<< "model could not complete parameters for" << toolPrompt;
continue;
}
QString errorString;
const QString response = executeToolCall(toolCall, producedSourceExcerpts, errorString);
if (response.isEmpty()) {
qWarning() << "WARNING: Attempt to force usage of toolcall" << t->function() << "failed:" << errorString;
continue;
}
toolResponses << QString(toolCallingTemplate).arg(response);
}
bool success = promptRecursive({ toolResponses }, prompt, promptTemplate, n_predict, top_k, top_p,
min_p, temp, n_batch, repeat_penalty, repeat_penalty_tokens, totalTime, producedSourceExcerpts);
Q_ASSERT(success);
SuggestionMode mode = MySettings::globalInstance()->suggestionMode();
if (mode == SuggestionMode::On || (mode == SuggestionMode::SourceExcerptsOnly && producedSourceExcerpts))
generateQuestions(totalTime);
else
emit responseStopped(totalTime);
return success;
}
QString ChatLLM::completeToolCall(const QString &prompt, int32_t n_predict, int32_t top_k, float top_p,
float min_p, float temp, int32_t n_batch, float repeat_penalty, int32_t repeat_penalty_tokens,
qint64 &totalTime)
{
if (!isModelLoaded())
return false;
QList<ResultInfo> databaseResults;
const int retrievalSize = MySettings::globalInstance()->localDocsRetrievalSize();
if (!collectionList.isEmpty()) {
emit requestRetrieveFromDB(collectionList, prompt, retrievalSize, &databaseResults); // blocks
emit databaseResultsChanged(databaseResults);
}
// Augment the prompt template with the results if any
QString docsContext;
if (!databaseResults.isEmpty()) {
QStringList results;
for (const ResultInfo &info : databaseResults)
results << u"Collection: %1\nPath: %2\nExcerpt: %3"_s.arg(info.collection, info.path, info.text);
// FIXME(jared): use a Jinja prompt template instead of hardcoded Alpaca-style localdocs template
docsContext = u"### Context:\n%1\n\n"_s.arg(results.join("\n\n"));
}
return QString();
int n_threads = MySettings::globalInstance()->threadCount();
@@ -779,37 +885,188 @@ bool ChatLLM::promptInternal(const QList<QString> &collectionList, const QString
printf("%s", qPrintable(prompt));
fflush(stdout);
#endif
QElapsedTimer totalTime;
totalTime.start();
QElapsedTimer elapsedTimer;
elapsedTimer.start();
m_timer->start();
if (!docsContext.isEmpty()) {
auto old_n_predict = std::exchange(m_ctx.n_predict, 0); // decode localdocs context without a response
m_llModelInfo.model->prompt(docsContext.toStdString(), "%1", promptFunc, responseFunc,
m_checkToolCall = true;
// We pass in the prompt as the completed template as we're mimicking that the respone has already
// started
LLModel::PromptContext ctx = m_ctx;
m_llModelInfo.model->prompt(prompt.toStdString(), "%1", promptFunc, responseFunc,
/*allowContextShift*/ false, ctx);
// After the response has been handled reset this state
m_checkToolCall = false;
m_maybeToolCall = false;
m_timer->stop();
totalTime = elapsedTimer.elapsed();
const QString toolCall = QString::fromStdString(trim_whitespace(m_response));
m_promptResponseTokens = 0;
m_promptTokens = 0;
m_response = std::string();
if (!m_foundToolCall)
return QString();
m_foundToolCall = false;
return toolCall;
}
QString ChatLLM::executeToolCall(const QString &toolCall, bool &producedSourceExcerpts, QString &errorString)
{
const QString toolTemplate = MySettings::globalInstance()->modelToolTemplate(m_modelInfo);
if (toolTemplate.isEmpty()) {
errorString = QString("ERROR: No valid tool template for this model %1").arg(toolCall);
return QString();
}
QJsonParseError err;
const QJsonDocument toolCallDoc = QJsonDocument::fromJson(toolCall.toUtf8(), &err);
if (toolCallDoc.isNull() || err.error != QJsonParseError::NoError || !toolCallDoc.isObject()) {
errorString = QString("ERROR: The tool call had null or invalid json %1").arg(toolCall);
return QString();
}
QJsonObject rootObject = toolCallDoc.object();
if (!rootObject.contains("name") || !rootObject.contains("parameters")) {
errorString = QString("ERROR: The tool call did not have required name and argument objects %1").arg(toolCall);
return QString();
}
const QString tool = toolCallDoc["name"].toString();
const QJsonObject args = toolCallDoc["parameters"].toObject();
Tool *toolInstance = ToolModel::globalInstance()->get(tool);
if (!toolInstance) {
errorString = QString("ERROR: Could not find the tool for %1").arg(toolCall);
return QString();
}
// FIXME: Honor the confirmation mode feature
// Inform the chat that we're executing a tool call
emit toolCalled(toolInstance->name().toLower());
const QString response = toolInstance->run(args, 2000 /*msecs to timeout*/);
if (toolInstance->error() != Error::NoError) {
errorString = QString("ERROR: Tool call produced error: %1").arg(toolInstance->errorString());
return QString();
}
// If the tool supports excerpts then try to parse them here, but it isn't strictly an error
// but rather a warning
if (toolInstance->excerpts()) {
QString parseError;
QList<SourceExcerpt> sourceExcerpts = SourceExcerpt::fromJson(response, parseError);
if (!parseError.isEmpty()) {
qWarning() << "WARNING: Could not parse source excerpts for response:" << parseError;
} else if (!sourceExcerpts.isEmpty()) {
producedSourceExcerpts = true;
emit sourceExcerptsChanged(sourceExcerpts);
}
}
return response;
}
bool ChatLLM::promptRecursive(const QList<QString> &toolResponses, const QString &prompt,
const QString &promptTemplate, int32_t n_predict, int32_t top_k, float top_p, float min_p, float temp,
int32_t n_batch, float repeat_penalty, int32_t repeat_penalty_tokens, qint64 &totalTime, bool &producedSourceExcerpts, bool isRecursiveCall)
{
if (!isModelLoaded())
return false;
int n_threads = MySettings::globalInstance()->threadCount();
m_stopGenerating = false;
auto promptFunc = std::bind(&ChatLLM::handlePrompt, this, std::placeholders::_1);
auto responseFunc = std::bind(&ChatLLM::handleResponse, this, std::placeholders::_1,
std::placeholders::_2);
emit promptProcessing();
m_ctx.n_predict = n_predict;
m_ctx.top_k = top_k;
m_ctx.top_p = top_p;
m_ctx.min_p = min_p;
m_ctx.temp = temp;
m_ctx.n_batch = n_batch;
m_ctx.repeat_penalty = repeat_penalty;
m_ctx.repeat_last_n = repeat_penalty_tokens;
m_llModelInfo.model->setThreadCount(n_threads);
#if defined(DEBUG)
printf("%s", qPrintable(prompt));
fflush(stdout);
#endif
QElapsedTimer elapsedTimer;
elapsedTimer.start();
m_timer->start();
// The list of possible additional responses that come from previous usage of tool calls
for (const QString &context : toolResponses) {
auto old_n_predict = std::exchange(m_ctx.n_predict, 0); // decode context without a response
m_llModelInfo.model->prompt(context.toStdString(), "%1", promptFunc, responseFunc,
/*allowContextShift*/ true, m_ctx);
m_ctx.n_predict = old_n_predict; // now we are ready for a response
}
// We can't handle recursive tool calls right now due to the possibility of the model causing
// infinite recursion through repeated tool calls
m_checkToolCall = !isRecursiveCall;
m_llModelInfo.model->prompt(prompt.toStdString(), promptTemplate.toStdString(), promptFunc, responseFunc,
/*allowContextShift*/ true, m_ctx);
// After the response has been handled reset this state
m_checkToolCall = false;
m_maybeToolCall = false;
#if defined(DEBUG)
printf("\n");
fflush(stdout);
#endif
m_timer->stop();
qint64 elapsed = totalTime.elapsed();
totalTime = elapsedTimer.elapsed();
std::string trimmed = trim_whitespace(m_response);
if (trimmed != m_response) {
m_response = trimmed;
emit responseChanged(QString::fromStdString(m_response));
// If we found a tool call, then deal with it
if (m_foundToolCall) {
m_foundToolCall = false;
QString errorString;
const QString toolCall = QString::fromStdString(trimmed);
const QString toolResponse = executeToolCall(toolCall, producedSourceExcerpts, errorString);
if (toolResponse.isEmpty()) {
// FIXME: Need to surface errors to the UI
// Restore the strings that we excluded previously when detecting the tool call
qWarning() << errorString;
m_response = "<tool_call>" + toolCall.toStdString() + "</tool_call>";
emit responseChanged(QString::fromStdString(m_response));
emit responseStopped(totalTime);
m_pristineLoadedState = false;
return false;
}
// Reset the state now that we've had a successful tool call response
m_promptResponseTokens = 0;
m_promptTokens = 0;
m_response = std::string();
// This is a recursive call but flag is checked above to arrest infinite recursive tool calls
return promptRecursive({ toolResponse }, prompt, promptTemplate,
n_predict, top_k, top_p, min_p, temp, n_batch, repeat_penalty, repeat_penalty_tokens, totalTime,
producedSourceExcerpts, true /*isRecursiveCall*/);
} else {
if (trimmed != m_response) {
m_response = trimmed;
emit responseChanged(QString::fromStdString(m_response));
}
m_pristineLoadedState = false;
return true;
}
SuggestionMode mode = MySettings::globalInstance()->suggestionMode();
if (mode == SuggestionMode::On || (!databaseResults.isEmpty() && mode == SuggestionMode::LocalDocsOnly))
generateQuestions(elapsed);
else
emit responseStopped(elapsed);
m_pristineLoadedState = false;
return true;
}
void ChatLLM::setShouldBeLoaded(bool b)
@@ -1209,7 +1466,15 @@ void ChatLLM::processSystemPrompt()
if (!isModelLoaded() || m_processedSystemPrompt || m_restoreStateFromText || m_isServer)
return;
const std::string systemPrompt = MySettings::globalInstance()->modelSystemPrompt(m_modelInfo).toStdString();
QString error;
const std::string systemPrompt = MySettings::globalInstance()->modelSystemPrompt(m_modelInfo, error).toStdString();
// The GUI should not allow setting an improper template, but it is always possible someone hand
// edits the settings file to produce an improper one.
Q_ASSERT(error.isEmpty());
if (!error.isEmpty())
qWarning() << "ERROR: Could not parse system prompt template:" << error;
if (QString::fromStdString(systemPrompt).trimmed().isEmpty()) {
m_processedSystemPrompt = true;
return;

View File

@@ -180,6 +180,7 @@ Q_SIGNALS:
void responseChanged(const QString &response);
void promptProcessing();
void generatingQuestions();
void toolCalled(const QString &description);
void responseStopped(qint64 promptResponseMs);
void generatedNameChanged(const QString &name);
void generatedQuestionFinished(const QString &generatedQuestion);
@@ -188,14 +189,14 @@ Q_SIGNALS:
void shouldBeLoadedChanged();
void trySwitchContextRequested(const ModelInfo &modelInfo);
void trySwitchContextOfLoadedModelCompleted(int value);
void requestRetrieveFromDB(const QList<QString> &collections, const QString &text, int retrievalSize, QList<ResultInfo> *results);
void reportSpeed(const QString &speed);
void reportDevice(const QString &device);
void reportFallbackReason(const QString &fallbackReason);
void databaseResultsChanged(const QList<ResultInfo>&);
void sourceExcerptsChanged(const QList<SourceExcerpt>&);
void modelInfoChanged(const ModelInfo &modelInfo);
protected:
// FIXME: This is only available because of server which sucks
bool promptInternal(const QList<QString> &collectionList, const QString &prompt, const QString &promptTemplate,
int32_t n_predict, int32_t top_k, float top_p, float min_p, float temp, int32_t n_batch, float repeat_penalty,
int32_t repeat_penalty_tokens);
@@ -218,6 +219,13 @@ protected:
quint32 m_promptResponseTokens;
private:
QString completeToolCall(const QString &promptTemplate, int32_t n_predict, int32_t top_k, float top_p,
float min_p, float temp, int32_t n_batch, float repeat_penalty, int32_t repeat_penalty_tokens,
qint64 &totalTime);
QString executeToolCall(const QString &toolCall, bool &producedSourceExcerpts, QString &errorString);
bool promptRecursive(const QList<QString> &toolContexts, const QString &prompt, const QString &promptTemplate,
int32_t n_predict, int32_t top_k, float top_p, float min_p, float temp, int32_t n_batch, float repeat_penalty,
int32_t repeat_penalty_tokens, qint64 &totalTime, bool &producedSourceExcerpts, bool isRecursiveCall = false);
bool loadNewModel(const ModelInfo &modelInfo, QVariantMap &modelLoadProps);
std::string m_response;
@@ -239,11 +247,15 @@ private:
bool m_reloadingToChangeVariant;
bool m_processedSystemPrompt;
bool m_restoreStateFromText;
bool m_checkToolCall;
bool m_maybeToolCall;
bool m_foundToolCall;
// m_pristineLoadedState is set if saveSate is unnecessary, either because:
// - an unload was queued during LLModel::restoreState()
// - the chat will be restored from text and hasn't been interacted with yet
bool m_pristineLoadedState = false;
QVector<QPair<QString, QString>> m_stateFromText;
QNetworkAccessManager m_networkManager; // FIXME REMOVE
};
#endif // CHATLLM_H

View File

@@ -28,8 +28,7 @@ struct ChatItem
Q_PROPERTY(bool stopped MEMBER stopped)
Q_PROPERTY(bool thumbsUpState MEMBER thumbsUpState)
Q_PROPERTY(bool thumbsDownState MEMBER thumbsDownState)
Q_PROPERTY(QList<ResultInfo> sources MEMBER sources)
Q_PROPERTY(QList<ResultInfo> consolidatedSources MEMBER consolidatedSources)
Q_PROPERTY(QList<SourceExcerpt> sources MEMBER sources)
public:
// TODO: Maybe we should include the model name here as well as timestamp?
@@ -38,8 +37,7 @@ public:
QString value;
QString prompt;
QString newResponse;
QList<ResultInfo> sources;
QList<ResultInfo> consolidatedSources;
QList<SourceExcerpt> sources;
bool currentResponse = false;
bool stopped = false;
bool thumbsUpState = false;
@@ -65,8 +63,7 @@ public:
StoppedRole,
ThumbsUpStateRole,
ThumbsDownStateRole,
SourcesRole,
ConsolidatedSourcesRole
SourcesRole
};
int rowCount(const QModelIndex &parent = QModelIndex()) const override
@@ -102,8 +99,6 @@ public:
return item.thumbsDownState;
case SourcesRole:
return QVariant::fromValue(item.sources);
case ConsolidatedSourcesRole:
return QVariant::fromValue(item.consolidatedSources);
}
return QVariant();
@@ -122,7 +117,6 @@ public:
roles[ThumbsUpStateRole] = "thumbsUpState";
roles[ThumbsDownStateRole] = "thumbsDownState";
roles[SourcesRole] = "sources";
roles[ConsolidatedSourcesRole] = "consolidatedSources";
return roles;
}
@@ -200,28 +194,17 @@ public:
}
}
QList<ResultInfo> 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;
}
Q_INVOKABLE void updateSources(int index, const QList<ResultInfo> &sources)
Q_INVOKABLE void updateSources(int index, const QList<SourceExcerpt> &sources)
{
if (index < 0 || index >= m_chatItems.size()) return;
ChatItem &item = m_chatItems[index];
item.sources = sources;
item.consolidatedSources = consolidateSources(sources);
if (sources.isEmpty()) {
item.sources.clear();
} else {
item.sources << sources;
}
emit dataChanged(createIndex(index, 0), createIndex(index, 0), {SourcesRole});
emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ConsolidatedSourcesRole});
}
Q_INVOKABLE void updateThumbsUpState(int index, bool b)
@@ -272,57 +255,7 @@ public:
stream << c.stopped;
stream << c.thumbsUpState;
stream << c.thumbsDownState;
if (version > 7) {
stream << c.sources.size();
for (const ResultInfo &info : c.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 > 2) {
QList<QString> references;
QList<QString> referencesContext;
int validReferenceNumber = 1;
for (const ResultInfo &info : c.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;
}
stream << SourceExcerpt::toJson(c.sources);
}
return stream.status() == QDataStream::Ok;
}
@@ -342,34 +275,43 @@ public:
stream >> c.stopped;
stream >> c.thumbsUpState;
stream >> c.thumbsDownState;
if (version > 7) {
if (version > 9) {
QList<SourceExcerpt> sources;
QString json;
stream >> json;
QString errorString;
sources = SourceExcerpt::fromJson(json, errorString);
Q_ASSERT(errorString.isEmpty());
c.sources = sources;
} else if (version > 7) {
qsizetype count;
stream >> count;
QList<ResultInfo> sources;
QList<SourceExcerpt> sources;
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);
SourceExcerpt source;
stream >> source.collection;
stream >> source.path;
stream >> source.file;
stream >> source.title;
stream >> source.author;
stream >> source.date;
Excerpt excerpt;
stream >> excerpt.text;
stream >> excerpt.page;
stream >> excerpt.from;
stream >> excerpt.to;
source.excerpts = QList{ excerpt };
sources.append(source);
}
c.sources = sources;
c.consolidatedSources = consolidateSources(sources);
}else if (version > 2) {
} else if (version > 2) {
QString references;
QList<QString> referencesContext;
stream >> references;
stream >> referencesContext;
if (!references.isEmpty()) {
QList<ResultInfo> sources;
QList<SourceExcerpt> sources;
QList<QString> referenceList = references.split("\n");
// Ignore empty lines and those that begin with "---" which is no longer used
@@ -384,7 +326,8 @@ public:
for (int j = 0; j < referenceList.size(); ++j) {
QString reference = referenceList[j];
QString context = referencesContext[j];
ResultInfo info;
SourceExcerpt source;
Excerpt excerpt;
QTextStream refStream(&reference);
QString dummy;
int validReferenceNumber;
@@ -393,28 +336,28 @@ public:
if (reference.contains("\"")) {
int startIndex = reference.indexOf('"') + 1;
int endIndex = reference.indexOf('"', startIndex);
info.title = reference.mid(startIndex, endIndex - startIndex);
source.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();
source.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();
source.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();
source.file = reference.mid(startIndex, endIndex - startIndex).trimmed();
}
// Extract page number (after "Page " and before the next space)
@@ -422,7 +365,7 @@ public:
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();
excerpt.page = reference.mid(startIndex, endIndex - startIndex).toInt();
}
// Extract lines (after "Lines " and before the next space or hyphen)
@@ -432,18 +375,18 @@ public:
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();
excerpt.from = reference.mid(startIndex, hyphenIndex - startIndex).toInt();
excerpt.to = reference.mid(hyphenIndex + 1, endIndex - hyphenIndex - 1).toInt();
} else {
info.from = reference.mid(startIndex, endIndex - startIndex).toInt();
excerpt.from = reference.mid(startIndex, endIndex - startIndex).toInt();
}
}
info.text = context;
sources.append(info);
excerpt.text = context;
source.excerpts = QList{ excerpt };
sources.append(source);
}
c.sources = sources;
c.consolidatedSources = consolidateSources(sources);
}
}
beginInsertRows(QModelIndex(), m_chatItems.size(), m_chatItems.size());

View File

@@ -1938,7 +1938,7 @@ QList<int> Database::searchEmbeddings(const std::vector<float> &query, const QLi
}
void Database::retrieveFromDB(const QList<QString> &collections, const QString &text, int retrievalSize,
QList<ResultInfo> *results)
QString &jsonResult)
{
#if defined(DEBUG)
qDebug() << "retrieveFromDB" << collections << text << retrievalSize;
@@ -1960,37 +1960,49 @@ void Database::retrieveFromDB(const QList<QString> &collections, const QString &
return;
}
QMap<QString, QJsonObject> results;
while (q.next()) {
#if defined(DEBUG)
const int rowid = q.value(0).toInt();
#endif
const QString document_path = q.value(2).toString();
const QString chunk_text = q.value(3).toString();
const QString date = QDateTime::fromMSecsSinceEpoch(q.value(1).toLongLong()).toString("yyyy, MMMM dd");
const QString file = q.value(4).toString();
const QString title = q.value(5).toString();
const QString author = q.value(6).toString();
const int page = q.value(7).toInt();
const int from = q.value(8).toInt();
const int to = q.value(9).toInt();
const QString collectionName = q.value(10).toString();
ResultInfo info;
info.collection = collectionName;
info.path = document_path;
info.file = file;
info.title = title;
info.author = author;
info.date = date;
info.text = chunk_text;
info.page = page;
info.from = from;
info.to = to;
results->append(info);
QJsonObject resultObject = results.value(file);
resultObject.insert("file", file);
resultObject.insert("path", q.value(2).toString());
resultObject.insert("date", QDateTime::fromMSecsSinceEpoch(q.value(1).toLongLong()).toString("yyyy, MMMM dd"));
resultObject.insert("title", q.value(5).toString());
resultObject.insert("author", q.value(6).toString());
resultObject.insert("collection", q.value(10).toString());
QJsonArray excerpts;
if (resultObject.contains("excerpts"))
excerpts = resultObject["excerpts"].toArray();
QJsonObject excerptObject;
excerptObject.insert("text", q.value(3).toString());
excerptObject.insert("page", q.value(7).toInt());
excerptObject.insert("from", q.value(8).toInt());
excerptObject.insert("to", q.value(9).toInt());
excerpts.append(excerptObject);
resultObject.insert("excerpts", excerpts);
results.insert(file, resultObject);
#if defined(DEBUG)
qDebug() << "retrieve rowid:" << rowid
<< "chunk_text:" << chunk_text;
#endif
}
QJsonArray resultsArray;
QList<QJsonObject> resultsList = results.values();
for (const QJsonObject &result : resultsList)
resultsArray.append(QJsonValue(result));
QJsonObject response;
response.insert("results", resultsArray);
QJsonDocument document(response);
// qDebug().noquote() << document.toJson(QJsonDocument::Indented);
jsonResult = document.toJson(QJsonDocument::Compact);
}
// FIXME This is very slow and non-interruptible and when we close the application and we're

View File

@@ -2,6 +2,7 @@
#define DATABASE_H
#include "embllm.h" // IWYU pragma: keep
#include "sourceexcerpt.h"
#include <QDateTime>
#include <QFileInfo>
@@ -49,64 +50,6 @@ struct DocumentInfo
}
};
struct ResultInfo {
Q_GADGET
Q_PROPERTY(QString collection MEMBER collection)
Q_PROPERTY(QString path MEMBER path)
Q_PROPERTY(QString file MEMBER file)
Q_PROPERTY(QString title MEMBER title)
Q_PROPERTY(QString author MEMBER author)
Q_PROPERTY(QString date MEMBER date)
Q_PROPERTY(QString text MEMBER text)
Q_PROPERTY(int page MEMBER page)
Q_PROPERTY(int from MEMBER from)
Q_PROPERTY(int to MEMBER to)
Q_PROPERTY(QString fileUri READ fileUri STORED false)
public:
QString collection; // [Required] The name of the collection
QString path; // [Required] The full path
QString file; // [Required] The name of the file, but not the full path
QString title; // [Optional] The title of the document
QString author; // [Optional] The author of the document
QString date; // [Required] The creation or the last modification date whichever is latest
QString text; // [Required] The text actually used in the augmented context
int page = -1; // [Optional] The page where the text was found
int from = -1; // [Optional] The line number where the text begins
int to = -1; // [Optional] The line number where the text ends
QString fileUri() const {
// QUrl reserved chars that are not UNSAFE_PATH according to glib/gconvert.c
static const QByteArray s_exclude = "!$&'()*+,/:=@~"_ba;
Q_ASSERT(!QFileInfo(path).isRelative());
#ifdef Q_OS_WINDOWS
Q_ASSERT(!path.contains('\\')); // Qt normally uses forward slash as path separator
#endif
auto escaped = QString::fromUtf8(QUrl::toPercentEncoding(path, s_exclude));
if (escaped.front() != '/')
escaped = '/' + escaped;
return u"file://"_s + escaped;
}
bool operator==(const ResultInfo &other) const {
return file == other.file &&
title == other.title &&
author == other.author &&
date == other.date &&
text == other.text &&
page == other.page &&
from == other.from &&
to == other.to;
}
bool operator!=(const ResultInfo &other) const {
return !(*this == other);
}
};
Q_DECLARE_METATYPE(ResultInfo)
struct CollectionItem {
// -- Fields persisted to database --
@@ -158,7 +101,7 @@ public Q_SLOTS:
void forceRebuildFolder(const QString &path);
bool addFolder(const QString &collection, const QString &path, const QString &embedding_model);
void removeFolder(const QString &collection, const QString &path);
void retrieveFromDB(const QList<QString> &collections, const QString &text, int retrievalSize, QList<ResultInfo> *results);
void retrieveFromDB(const QList<QString> &collections, const QString &text, int retrievalSize, QString &jsonResult);
void changeChunkSize(int chunkSize);
void changeFileExtensions(const QStringList &extensions);
@@ -225,7 +168,6 @@ private:
QStringList m_scannedFileExtensions;
QTimer *m_scanTimer;
QMap<int, QQueue<DocumentInfo>> m_docsToScan;
QList<ResultInfo> m_retrieve;
QThread m_dbThread;
QFileSystemWatcher *m_watcher;
QSet<QString> m_watchedPaths;

View File

@@ -0,0 +1,103 @@
#include "localdocssearch.h"
#include "database.h"
#include "localdocs.h"
#include "mysettings.h"
#include <QCoreApplication>
#include <QDebug>
#include <QGuiApplication>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>
using namespace Qt::Literals::StringLiterals;
QString LocalDocsSearch::run(const QJsonObject &parameters, qint64 timeout)
{
// Reset the error state
m_error = ToolEnums::Error::NoError;
m_errorString = QString();
QList<QString> collections;
QJsonArray collectionsArray = parameters["collections"].toArray();
for (int i = 0; i < collectionsArray.size(); ++i)
collections.append(collectionsArray[i].toString());
const QString text = parameters["query"].toString();
const int count = MySettings::globalInstance()->localDocsRetrievalSize();
QThread workerThread;
LocalDocsWorker worker;
worker.moveToThread(&workerThread);
connect(&worker, &LocalDocsWorker::finished, &workerThread, &QThread::quit, Qt::DirectConnection);
connect(&workerThread, &QThread::started, [&worker, collections, text, count]() {
worker.request(collections, text, count);
});
workerThread.start();
bool timedOut = !workerThread.wait(timeout);
if (timedOut) {
m_error = ToolEnums::Error::TimeoutError;
m_errorString = tr("ERROR: localdocs timeout");
}
workerThread.wait(timeout);
workerThread.quit();
workerThread.wait();
return worker.response();
}
QJsonObject LocalDocsSearch::paramSchema() const
{
static const QString localParamSchema = R"({
"collections": {
"type": "array",
"items": {
"type": "string"
},
"description": "The collections to search",
"required": true,
"modelGenerated": false,
"userConfigured": false
},
"query": {
"type": "string",
"description": "The query to search",
"required": true
},
"count": {
"type": "integer",
"description": "The number of excerpts to return",
"required": true,
"modelGenerated": false
}
})";
static const QJsonDocument localJsonDoc = QJsonDocument::fromJson(localParamSchema.toUtf8());
Q_ASSERT(!localJsonDoc.isNull() && localJsonDoc.isObject());
return localJsonDoc.object();
}
QJsonObject LocalDocsSearch::exampleParams() const
{
static const QString example = R"({
"query": "the 44th president of the United States"
})";
static const QJsonDocument exampleDoc = QJsonDocument::fromJson(example.toUtf8());
Q_ASSERT(!exampleDoc.isNull() && exampleDoc.isObject());
return exampleDoc.object();
}
LocalDocsWorker::LocalDocsWorker()
: QObject(nullptr)
{
// The following are blocking operations and will block the calling thread
connect(this, &LocalDocsWorker::requestRetrieveFromDB, LocalDocs::globalInstance()->database(),
&Database::retrieveFromDB, Qt::BlockingQueuedConnection);
}
void LocalDocsWorker::request(const QList<QString> &collections, const QString &text, int count)
{
QString jsonResult;
emit requestRetrieveFromDB(collections, text, count, jsonResult); // blocks
m_response = jsonResult;
emit finished();
}

View File

@@ -0,0 +1,52 @@
#ifndef LOCALDOCSSEARCH_H
#define LOCALDOCSSEARCH_H
#include "tool.h"
#include <QObject>
#include <QString>
class LocalDocsWorker : public QObject {
Q_OBJECT
public:
LocalDocsWorker();
virtual ~LocalDocsWorker() {}
QString response() const { return m_response; }
void request(const QList<QString> &collections, const QString &text, int count);
Q_SIGNALS:
void requestRetrieveFromDB(const QList<QString> &collections, const QString &text, int count, QString &jsonResponse);
void finished();
private:
QString m_response;
};
class LocalDocsSearch : public Tool {
Q_OBJECT
public:
LocalDocsSearch() : Tool(), m_error(ToolEnums::Error::NoError) {}
virtual ~LocalDocsSearch() {}
QString run(const QJsonObject &parameters, 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("LocalDocs Search"); }
QString description() const override { return tr("Search the local docs"); }
QString function() const override { return "localdocs_search"; }
ToolEnums::PrivacyScope privacyScope() const override { return ToolEnums::PrivacyScope::Local; }
QJsonObject exampleParams() const override;
QJsonObject paramSchema() const override;
bool isBuiltin() const override { return true; }
ToolEnums::UsageMode usageMode() const override { return ToolEnums::UsageMode::ForceUsage; }
bool excerpts() const override { return true; }
private:
ToolEnums::Error m_error;
QString m_errorString;
};
#endif // LOCALDOCSSEARCH_H

View File

@@ -7,6 +7,7 @@
#include "modellist.h"
#include "mysettings.h"
#include "network.h"
#include "toolmodel.h"
#include "../gpt4all-backend/llmodel.h"
@@ -67,6 +68,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");
const QUrl url(u"qrc:/gpt4all/main.qml"_qs);

View File

@@ -12,6 +12,8 @@ import network
import gpt4all
import localdocs
import mysettings
import toollist
import toolenums
Window {
id: window
@@ -413,7 +415,11 @@ Window {
ColorOverlay {
id: antennaColored
visible: ModelList.selectableModels.count !== 0 && (currentChat.isServer || currentChat.modelInfo.isOnline || MySettings.networkIsActive)
visible: ModelList.selectableModels.count !== 0
&& (MySettings.networkIsActive
|| currentChat.modelInfo.isOnline
|| currentChat.isServer
|| ToolList.privacyScope === ToolEnums.PrivacyScope.None)
anchors.fill: antennaImage
source: antennaImage
color: theme.styledTextColor
@@ -422,8 +428,10 @@ Window {
return qsTr("The datalake is enabled")
else if (currentChat.modelInfo.isOnline)
return qsTr("Using a network model")
else if (currentChat.modelInfo.isOnline)
else if (currentChat.isServer)
return qsTr("Server mode is enabled")
else if (ToolList.privacyScope === ToolEnums.PrivacyScope.None)
return qsTr("One or more enabled tools is not private")
return ""
}
ToolTip.visible: maAntenna.containsMouse

View File

@@ -323,15 +323,26 @@ void ModelInfo::setPromptTemplate(const QString &t)
m_promptTemplate = t;
}
QString ModelInfo::systemPrompt() const
QString ModelInfo::toolTemplate() const
{
return MySettings::globalInstance()->modelSystemPrompt(*this);
return MySettings::globalInstance()->modelToolTemplate(*this);
}
void ModelInfo::setSystemPrompt(const QString &p)
void ModelInfo::setToolTemplate(const QString &t)
{
if (shouldSaveMetadata()) MySettings::globalInstance()->setModelSystemPrompt(*this, p, true /*force*/);
m_systemPrompt = p;
if (shouldSaveMetadata()) MySettings::globalInstance()->setModelToolTemplate(*this, t, true /*force*/);
m_toolTemplate = t;
}
QString ModelInfo::systemPromptTemplate() const
{
return MySettings::globalInstance()->modelSystemPromptTemplate(*this);
}
void ModelInfo::setSystemPromptTemplate(const QString &p)
{
if (shouldSaveMetadata()) MySettings::globalInstance()->setModelSystemPromptTemplate(*this, p, true /*force*/);
m_systemPromptTemplate = p;
}
QString ModelInfo::chatNamePrompt() const
@@ -356,6 +367,17 @@ void ModelInfo::setSuggestedFollowUpPrompt(const QString &p)
m_suggestedFollowUpPrompt = p;
}
bool ModelInfo::isToolCalling() const
{
return MySettings::globalInstance()->modelIsToolCalling(*this);
}
void ModelInfo::setIsToolCalling(bool b)
{
if (shouldSaveMetadata()) MySettings::globalInstance()->setModelIsToolCalling(*this, b, true /*force*/);
m_isToolCalling = b;
}
bool ModelInfo::shouldSaveMetadata() const
{
return installed && (isClone() || isDiscovered() || description() == "" /*indicates sideloaded*/);
@@ -385,9 +407,11 @@ QVariantMap ModelInfo::getFields() const
{ "repeatPenalty", m_repeatPenalty },
{ "repeatPenaltyTokens", m_repeatPenaltyTokens },
{ "promptTemplate", m_promptTemplate },
{ "systemPrompt", m_systemPrompt },
{ "toolTemplate", m_toolTemplate },
{ "systemPromptTemplate",m_systemPromptTemplate },
{ "chatNamePrompt", m_chatNamePrompt },
{ "suggestedFollowUpPrompt", m_suggestedFollowUpPrompt },
{ "isToolCalling", m_isToolCalling },
};
}
@@ -504,7 +528,9 @@ ModelList::ModelList()
connect(MySettings::globalInstance(), &MySettings::repeatPenaltyChanged, this, &ModelList::updateDataForSettings);
connect(MySettings::globalInstance(), &MySettings::repeatPenaltyTokensChanged, this, &ModelList::updateDataForSettings);;
connect(MySettings::globalInstance(), &MySettings::promptTemplateChanged, this, &ModelList::updateDataForSettings);
connect(MySettings::globalInstance(), &MySettings::toolTemplateChanged, this, &ModelList::updateDataForSettings);
connect(MySettings::globalInstance(), &MySettings::systemPromptChanged, this, &ModelList::updateDataForSettings);
connect(MySettings::globalInstance(), &MySettings::isToolCallingChanged, this, &ModelList::updateDataForSettings);
connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this, &ModelList::handleSslErrors);
updateModelsFromJson();
@@ -776,8 +802,10 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const
return info->repeatPenaltyTokens();
case PromptTemplateRole:
return info->promptTemplate();
case ToolTemplateRole:
return info->toolTemplate();
case SystemPromptRole:
return info->systemPrompt();
return info->systemPromptTemplate();
case ChatNamePromptRole:
return info->chatNamePrompt();
case SuggestedFollowUpPromptRole:
@@ -788,7 +816,8 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const
return info->downloads();
case RecencyRole:
return info->recency();
case IsToolCallingRole:
return info->isToolCalling();
}
return QVariant();
@@ -952,8 +981,10 @@ void ModelList::updateData(const QString &id, const QVector<QPair<int, QVariant>
info->setRepeatPenaltyTokens(value.toInt()); break;
case PromptTemplateRole:
info->setPromptTemplate(value.toString()); break;
case ToolTemplateRole:
info->setToolTemplate(value.toString()); break;
case SystemPromptRole:
info->setSystemPrompt(value.toString()); break;
info->setSystemPromptTemplate(value.toString()); break;
case ChatNamePromptRole:
info->setChatNamePrompt(value.toString()); break;
case SuggestedFollowUpPromptRole:
@@ -982,6 +1013,8 @@ void ModelList::updateData(const QString &id, const QVector<QPair<int, QVariant>
}
break;
}
case IsToolCallingRole:
info->setIsToolCalling(value.toBool()); break;
}
}
@@ -1107,7 +1140,8 @@ QString ModelList::clone(const ModelInfo &model)
{ ModelList::RepeatPenaltyRole, model.repeatPenalty() },
{ ModelList::RepeatPenaltyTokensRole, model.repeatPenaltyTokens() },
{ ModelList::PromptTemplateRole, model.promptTemplate() },
{ ModelList::SystemPromptRole, model.systemPrompt() },
{ ModelList::ToolTemplateRole, model.toolTemplate() },
{ ModelList::SystemPromptRole, model.systemPromptTemplate() },
{ ModelList::ChatNamePromptRole, model.chatNamePrompt() },
{ ModelList::SuggestedFollowUpPromptRole, model.suggestedFollowUpPrompt() },
};
@@ -1551,8 +1585,12 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
data.append({ ModelList::RepeatPenaltyTokensRole, obj["repeatPenaltyTokens"].toInt() });
if (obj.contains("promptTemplate"))
data.append({ ModelList::PromptTemplateRole, obj["promptTemplate"].toString() });
if (obj.contains("toolTemplate"))
data.append({ ModelList::ToolTemplateRole, obj["toolTemplate"].toString() });
if (obj.contains("systemPrompt"))
data.append({ ModelList::SystemPromptRole, obj["systemPrompt"].toString() });
if (obj.contains("isToolCalling"))
data.append({ ModelList::IsToolCallingRole, obj["isToolCalling"].toBool() });
updateData(id, data);
}
@@ -1852,6 +1890,10 @@ void ModelList::updateModelsFromSettings()
const QString promptTemplate = settings.value(g + "/promptTemplate").toString();
data.append({ ModelList::PromptTemplateRole, promptTemplate });
}
if (settings.contains(g + "/toolTemplate")) {
const QString toolTemplate = settings.value(g + "/toolTemplate").toString();
data.append({ ModelList::ToolTemplateRole, toolTemplate });
}
if (settings.contains(g + "/systemPrompt")) {
const QString systemPrompt = settings.value(g + "/systemPrompt").toString();
data.append({ ModelList::SystemPromptRole, systemPrompt });
@@ -1864,6 +1906,10 @@ void ModelList::updateModelsFromSettings()
const QString suggestedFollowUpPrompt = settings.value(g + "/suggestedFollowUpPrompt").toString();
data.append({ ModelList::SuggestedFollowUpPromptRole, suggestedFollowUpPrompt });
}
if (settings.contains(g + "/isToolCalling")) {
const bool isToolCalling = settings.value(g + "/isToolCalling").toBool();
data.append({ ModelList::IsToolCallingRole, isToolCalling });
}
updateData(id, data);
}
}

View File

@@ -68,12 +68,14 @@ struct ModelInfo {
Q_PROPERTY(double repeatPenalty READ repeatPenalty WRITE setRepeatPenalty)
Q_PROPERTY(int repeatPenaltyTokens READ repeatPenaltyTokens WRITE setRepeatPenaltyTokens)
Q_PROPERTY(QString promptTemplate READ promptTemplate WRITE setPromptTemplate)
Q_PROPERTY(QString systemPrompt READ systemPrompt WRITE setSystemPrompt)
Q_PROPERTY(QString toolTemplate READ toolTemplate WRITE setToolTemplate)
Q_PROPERTY(QString systemPromptTemplate READ systemPromptTemplate WRITE setSystemPromptTemplate)
Q_PROPERTY(QString chatNamePrompt READ chatNamePrompt WRITE setChatNamePrompt)
Q_PROPERTY(QString suggestedFollowUpPrompt READ suggestedFollowUpPrompt WRITE setSuggestedFollowUpPrompt)
Q_PROPERTY(int likes READ likes WRITE setLikes)
Q_PROPERTY(int downloads READ downloads WRITE setDownloads)
Q_PROPERTY(QDateTime recency READ recency WRITE setRecency)
Q_PROPERTY(bool isToolCalling READ isToolCalling WRITE setIsToolCalling)
public:
enum HashAlgorithm {
@@ -117,6 +119,9 @@ public:
QDateTime recency() const;
void setRecency(const QDateTime &r);
bool isToolCalling() const;
void setIsToolCalling(bool b);
QString dirpath;
QString filesize;
QByteArray hash;
@@ -178,8 +183,11 @@ public:
void setRepeatPenaltyTokens(int t);
QString promptTemplate() const;
void setPromptTemplate(const QString &t);
QString systemPrompt() const;
void setSystemPrompt(const QString &p);
QString toolTemplate() const;
void setToolTemplate(const QString &t);
QString systemPromptTemplate() const;
void setSystemPromptTemplate(const QString &p);
// FIXME (adam): The chatname and suggested follow-up should also be templates I guess?
QString chatNamePrompt() const;
void setChatNamePrompt(const QString &p);
QString suggestedFollowUpPrompt() const;
@@ -215,9 +223,11 @@ private:
double m_repeatPenalty = 1.18;
int m_repeatPenaltyTokens = 64;
QString m_promptTemplate = "### Human:\n%1\n\n### Assistant:\n";
QString m_systemPrompt = "### System:\nYou are an AI assistant who gives a quality response to whatever humans ask of you.\n\n";
QString m_toolTemplate = "";
QString m_systemPromptTemplate = "### System:\nYou are an AI assistant who gives a quality response to whatever humans ask of you.\n\n";
QString m_chatNamePrompt = "Describe the above conversation in seven words or less.";
QString m_suggestedFollowUpPrompt = "Suggest three very short factual follow-up questions that have not been answered yet or cannot be found inspired by the previous conversation and excerpts.";
bool m_isToolCalling = false;
friend class MySettings;
};
Q_DECLARE_METATYPE(ModelInfo)
@@ -339,13 +349,15 @@ public:
RepeatPenaltyRole,
RepeatPenaltyTokensRole,
PromptTemplateRole,
ToolTemplateRole,
SystemPromptRole,
ChatNamePromptRole,
SuggestedFollowUpPromptRole,
MinPRole,
LikesRole,
DownloadsRole,
RecencyRole
RecencyRole,
IsToolCallingRole
};
QHash<int, QByteArray> roleNames() const override
@@ -393,6 +405,7 @@ public:
roles[RepeatPenaltyRole] = "repeatPenalty";
roles[RepeatPenaltyTokensRole] = "repeatPenaltyTokens";
roles[PromptTemplateRole] = "promptTemplate";
roles[ToolTemplateRole] = "toolTemplate";
roles[SystemPromptRole] = "systemPrompt";
roles[ChatNamePromptRole] = "chatNamePrompt";
roles[SuggestedFollowUpPromptRole] = "suggestedFollowUpPrompt";

View File

@@ -1,6 +1,8 @@
#include "mysettings.h"
#include "../gpt4all-backend/llmodel.h"
#include "tool.h"
#include "toolmodel.h"
#include <QDebug>
#include <QDir>
@@ -18,11 +20,13 @@
#include <QtLogging>
#include <algorithm>
#include <jinja2cpp/template.h>
#include <string>
#include <thread>
#include <vector>
using namespace Qt::Literals::StringLiterals;
using namespace ToolEnums;
// used only for settings serialization, do not translate
static const QStringList suggestionModeNames { "LocalDocsOnly", "On", "Off" };
@@ -44,22 +48,26 @@ static const QString languageAndLocale = "System Locale";
} // namespace defaults
static const QVariantMap basicDefaults {
{ "chatTheme", QVariant::fromValue(ChatTheme::Light) },
{ "fontSize", QVariant::fromValue(FontSize::Small) },
{ "lastVersionStarted", "" },
{ "networkPort", 4891, },
{ "saveChatsContext", false },
{ "serverChat", false },
{ "userDefaultModel", "Application default" },
{ "suggestionMode", QVariant::fromValue(SuggestionMode::LocalDocsOnly) },
{ "localdocs/chunkSize", 512 },
{ "localdocs/retrievalSize", 3 },
{ "localdocs/showReferences", true },
{ "localdocs/fileExtensions", QStringList { "txt", "pdf", "md", "rst" } },
{ "localdocs/useRemoteEmbed", false },
{ "localdocs/nomicAPIKey", "" },
{ "localdocs/embedDevice", "Auto" },
{ "network/attribution", "" },
{ "chatTheme", QVariant::fromValue(ChatTheme::Light) },
{ "fontSize", QVariant::fromValue(FontSize::Small) },
{ "lastVersionStarted", "" },
{ "networkPort", 4891, },
{ "saveChatsContext", false },
{ "serverChat", false },
{ "userDefaultModel", "Application default" },
{ "suggestionMode", QVariant::fromValue(SuggestionMode::SourceExcerptsOnly) },
{ "localdocs/chunkSize", 512 },
{ "localdocs/retrievalSize", 3 },
{ "localdocs/showReferences", true },
{ "localdocs/fileExtensions", QStringList { "txt", "pdf", "md", "rst" } },
{ "localdocs/useRemoteEmbed", false },
{ "localdocs/nomicAPIKey", "" },
{ "localdocs/embedDevice", "Auto" },
{ "network/attribution", "" },
{ "websearch/retrievalSize", 2 },
{ "websearch/usageMode", QVariant::fromValue(UsageMode::Disabled) },
{ "websearch/confirmationMode", QVariant::fromValue(ConfirmationMode::NoConfirmation) },
{ "bravesearch/APIKey", "" },
};
static QString defaultLocalModelsPath()
@@ -194,7 +202,9 @@ void MySettings::restoreModelDefaults(const ModelInfo &info)
setModelRepeatPenalty(info, info.m_repeatPenalty);
setModelRepeatPenaltyTokens(info, info.m_repeatPenaltyTokens);
setModelPromptTemplate(info, info.m_promptTemplate);
setModelSystemPrompt(info, info.m_systemPrompt);
setModelToolTemplate(info, info.m_toolTemplate);
setModelIsToolCalling(info, info.m_isToolCalling);
setModelSystemPromptTemplate(info, info.m_systemPromptTemplate);
setModelChatNamePrompt(info, info.m_chatNamePrompt);
setModelSuggestedFollowUpPrompt(info, info.m_suggestedFollowUpPrompt);
}
@@ -226,6 +236,14 @@ void MySettings::restoreLocalDocsDefaults()
setLocalDocsEmbedDevice(basicDefaults.value("localdocs/embedDevice").toString());
}
void MySettings::restoreWebSearchDefaults()
{
setWebSearchUsageMode(basicDefaults.value("websearch/usageMode").value<UsageMode>());
setWebSearchRetrievalSize(basicDefaults.value("websearch/retrievalSize").toInt());
setWebSearchConfirmationMode(basicDefaults.value("websearch/confirmationMode").value<ConfirmationMode>());
setBraveSearchAPIKey(basicDefaults.value("bravesearch/APIKey").toString());
}
void MySettings::eraseModel(const ModelInfo &info)
{
m_settings.remove(u"model-%1"_s.arg(info.id()));
@@ -296,7 +314,9 @@ int MySettings::modelGpuLayers (const ModelInfo &info) const
double MySettings::modelRepeatPenalty (const ModelInfo &info) const { return getModelSetting("repeatPenalty", info).toDouble(); }
int MySettings::modelRepeatPenaltyTokens (const ModelInfo &info) const { return getModelSetting("repeatPenaltyTokens", info).toInt(); }
QString MySettings::modelPromptTemplate (const ModelInfo &info) const { return getModelSetting("promptTemplate", info).toString(); }
QString MySettings::modelSystemPrompt (const ModelInfo &info) const { return getModelSetting("systemPrompt", info).toString(); }
QString MySettings::modelToolTemplate (const ModelInfo &info) const { return getModelSetting("toolTemplate", info).toString(); }
bool MySettings::modelIsToolCalling (const ModelInfo &info) const { return getModelSetting("isToolCalling", info).toBool(); }
QString MySettings::modelSystemPromptTemplate (const ModelInfo &info) const { return getModelSetting("systemPrompt", info).toString(); }
QString MySettings::modelChatNamePrompt (const ModelInfo &info) const { return getModelSetting("chatNamePrompt", info).toString(); }
QString MySettings::modelSuggestedFollowUpPrompt(const ModelInfo &info) const { return getModelSetting("suggestedFollowUpPrompt", info).toString(); }
@@ -405,7 +425,17 @@ void MySettings::setModelPromptTemplate(const ModelInfo &info, const QString &va
setModelSetting("promptTemplate", info, value, force, true);
}
void MySettings::setModelSystemPrompt(const ModelInfo &info, const QString &value, bool force)
void MySettings::setModelToolTemplate(const ModelInfo &info, const QString &value, bool force)
{
setModelSetting("toolTemplate", info, value, force, true);
}
void MySettings::setModelIsToolCalling(const ModelInfo &info, bool value, bool force)
{
setModelSetting("isToolCalling", info, value, force, true);
}
void MySettings::setModelSystemPromptTemplate(const ModelInfo &info, const QString &value, bool force)
{
setModelSetting("systemPrompt", info, value, force, true);
}
@@ -456,6 +486,10 @@ bool MySettings::localDocsUseRemoteEmbed() const { return getBasicSetting
QString MySettings::localDocsNomicAPIKey() const { return getBasicSetting("localdocs/nomicAPIKey" ).toString(); }
QString MySettings::localDocsEmbedDevice() const { return getBasicSetting("localdocs/embedDevice" ).toString(); }
QString MySettings::networkAttribution() const { return getBasicSetting("network/attribution" ).toString(); }
QString MySettings::braveSearchAPIKey() const { return getBasicSetting("bravesearch/APIKey" ).toString(); }
int MySettings::webSearchRetrievalSize() const { return getBasicSetting("websearch/retrievalSize").toInt(); }
UsageMode MySettings::webSearchUsageMode() const { return getBasicSetting("websearch/usageMode").value<UsageMode>(); }
ConfirmationMode MySettings::webSearchConfirmationMode() const { return getBasicSetting("websearch/confirmationMode").value<ConfirmationMode>(); }
ChatTheme MySettings::chatTheme() const { return ChatTheme (getEnumSetting("chatTheme", chatThemeNames)); }
FontSize MySettings::fontSize() const { return FontSize (getEnumSetting("fontSize", fontSizeNames)); }
@@ -474,6 +508,10 @@ void MySettings::setLocalDocsUseRemoteEmbed(bool value) { setBasic
void MySettings::setLocalDocsNomicAPIKey(const QString &value) { setBasicSetting("localdocs/nomicAPIKey", value, "localDocsNomicAPIKey"); }
void MySettings::setLocalDocsEmbedDevice(const QString &value) { setBasicSetting("localdocs/embedDevice", value, "localDocsEmbedDevice"); }
void MySettings::setNetworkAttribution(const QString &value) { setBasicSetting("network/attribution", value, "networkAttribution"); }
void MySettings::setBraveSearchAPIKey(const QString &value) { setBasicSetting("bravesearch/APIKey", value, "braveSearchAPIKey"); }
void MySettings::setWebSearchRetrievalSize(int value) { setBasicSetting("websearch/retrievalSize", value, "webSearchRetrievalSize"); }
void MySettings::setWebSearchUsageMode(ToolEnums::UsageMode value) { setBasicSetting("websearch/usageMode", int(value), "webSearchUsageMode"); }
void MySettings::setWebSearchConfirmationMode(ToolEnums::ConfirmationMode value) { setBasicSetting("websearch/confirmationMode", int(value), "webSearchConfirmationMode"); }
void MySettings::setChatTheme(ChatTheme value) { setBasicSetting("chatTheme", chatThemeNames .value(int(value))); }
void MySettings::setFontSize(FontSize value) { setBasicSetting("fontSize", fontSizeNames .value(int(value))); }
@@ -667,3 +705,49 @@ void MySettings::setLanguageAndLocale(const QString &bcp47Name)
QLocale::setDefault(locale);
emit languageAndLocaleChanged();
}
QString MySettings::validateModelSystemPromptTemplate(const QString &proposedTemplate)
{
QString error;
systemPromptInternal(proposedTemplate, error);
return error;
}
QString MySettings::modelSystemPrompt(const ModelInfo &info, QString &error)
{
return systemPromptInternal(modelSystemPromptTemplate(info), error);
}
QString MySettings::systemPromptInternal(const QString &proposedTemplate, QString &error)
{
jinja2::ValuesMap params;
params.insert({"currentDate", QDate::currentDate().toString().toStdString()});
jinja2::ValuesList toolList;
const int toolCount = ToolModel::globalInstance()->count();
for (int i = 0; i < toolCount; ++i) {
Tool *t = ToolModel::globalInstance()->get(i);
// FIXME: For now we don't tell the model about the localdocs search in the system prompt because
// it will try to call the localdocs search even if no collection is selected. Ideally, we need
// away to update model to whether a tool is enabled/disabled either via reprocessing the system
// prompt or sending a system message as it happens
if (t->usageMode() != UsageMode::Disabled && t->function() != "localdocs_search")
toolList.push_back(t->jinjaValue());
}
params.insert({"toolList", toolList});
QString systemPrompt;
jinja2::Template t;
const auto loadResult = t.Load(proposedTemplate.toStdString(), "systemPromptTemplate" /*Used in error messages*/);
if (!loadResult) {
error = QString::fromStdString(loadResult.error().ToString());
return systemPrompt;
}
const auto renderResult = t.RenderAsString(params);
if (renderResult)
systemPrompt = QString::fromStdString(renderResult.value());
else
error = QString::fromStdString(renderResult.error().ToString());
return systemPrompt;
}

View File

@@ -2,6 +2,7 @@
#define MYSETTINGS_H
#include "modellist.h" // IWYU pragma: keep
#include "tool.h"
#include <QDateTime>
#include <QObject>
@@ -21,9 +22,9 @@ namespace MySettingsEnums {
* ApplicationSettings.qml, as well as the corresponding name lists in mysettings.cpp */
enum class SuggestionMode {
LocalDocsOnly = 0,
On = 1,
Off = 2,
SourceExcerptsOnly = 0,
On = 1,
Off = 2,
};
Q_ENUM_NS(SuggestionMode)
@@ -72,6 +73,10 @@ class MySettings : public QObject
Q_PROPERTY(int networkPort READ networkPort WRITE setNetworkPort NOTIFY networkPortChanged)
Q_PROPERTY(SuggestionMode suggestionMode READ suggestionMode WRITE setSuggestionMode NOTIFY suggestionModeChanged)
Q_PROPERTY(QStringList uiLanguages MEMBER m_uiLanguages CONSTANT)
Q_PROPERTY(int webSearchRetrievalSize READ webSearchRetrievalSize WRITE setWebSearchRetrievalSize NOTIFY webSearchRetrievalSizeChanged)
Q_PROPERTY(ToolEnums::UsageMode webSearchUsageMode READ webSearchUsageMode WRITE setWebSearchUsageMode NOTIFY webSearchUsageModeChanged)
Q_PROPERTY(ToolEnums::ConfirmationMode webSearchConfirmationMode READ webSearchConfirmationMode WRITE setWebSearchConfirmationMode NOTIFY webSearchConfirmationModeChanged)
Q_PROPERTY(QString braveSearchAPIKey READ braveSearchAPIKey WRITE setBraveSearchAPIKey NOTIFY braveSearchAPIKeyChanged)
public:
static MySettings *globalInstance();
@@ -80,6 +85,7 @@ public:
Q_INVOKABLE void restoreModelDefaults(const ModelInfo &info);
Q_INVOKABLE void restoreApplicationDefaults();
Q_INVOKABLE void restoreLocalDocsDefaults();
Q_INVOKABLE void restoreWebSearchDefaults();
// Model/Character settings
void eraseModel(const ModelInfo &info);
@@ -125,8 +131,12 @@ public:
Q_INVOKABLE void setModelRepeatPenaltyTokens(const ModelInfo &info, int value, bool force = false);
QString modelPromptTemplate(const ModelInfo &info) const;
Q_INVOKABLE void setModelPromptTemplate(const ModelInfo &info, const QString &value, bool force = false);
QString modelSystemPrompt(const ModelInfo &info) const;
Q_INVOKABLE void setModelSystemPrompt(const ModelInfo &info, const QString &value, bool force = false);
QString modelToolTemplate(const ModelInfo &info) const;
Q_INVOKABLE void setModelToolTemplate(const ModelInfo &info, const QString &value, bool force = false);
bool modelIsToolCalling(const ModelInfo &info) const;
Q_INVOKABLE void setModelIsToolCalling(const ModelInfo &info, bool value, bool force = false);
QString modelSystemPromptTemplate(const ModelInfo &info) const;
Q_INVOKABLE void setModelSystemPromptTemplate(const ModelInfo &info, const QString &value, bool force = false);
int modelContextLength(const ModelInfo &info) const;
Q_INVOKABLE void setModelContextLength(const ModelInfo &info, int value, bool force = false);
int modelGpuLayers(const ModelInfo &info) const;
@@ -185,6 +195,16 @@ public:
QString localDocsEmbedDevice() const;
void setLocalDocsEmbedDevice(const QString &value);
// Web search settings
int webSearchRetrievalSize() const;
void setWebSearchRetrievalSize(int value);
ToolEnums::UsageMode webSearchUsageMode() const;
void setWebSearchUsageMode(ToolEnums::UsageMode value);
ToolEnums::ConfirmationMode webSearchConfirmationMode() const;
void setWebSearchConfirmationMode(ToolEnums::ConfirmationMode value);
QString braveSearchAPIKey() const;
void setBraveSearchAPIKey(const QString &value);
// Network settings
QString networkAttribution() const;
void setNetworkAttribution(const QString &value);
@@ -197,6 +217,10 @@ public:
int networkPort() const;
void setNetworkPort(int value);
// Jinja aware methods for validating and parsing/rendering the system prompt
Q_INVOKABLE QString validateModelSystemPromptTemplate(const QString &proposedTemplate);
QString modelSystemPrompt(const ModelInfo &info, QString &error);
Q_SIGNALS:
void nameChanged(const ModelInfo &info);
void filenameChanged(const ModelInfo &info);
@@ -212,9 +236,11 @@ Q_SIGNALS:
void repeatPenaltyChanged(const ModelInfo &info);
void repeatPenaltyTokensChanged(const ModelInfo &info);
void promptTemplateChanged(const ModelInfo &info);
void toolTemplateChanged(const ModelInfo &info);
void systemPromptChanged(const ModelInfo &info);
void chatNamePromptChanged(const ModelInfo &info);
void suggestedFollowUpPromptChanged(const ModelInfo &info);
void isToolCallingChanged(const ModelInfo &info);
void threadCountChanged();
void saveChatsContextChanged();
void serverChatChanged();
@@ -239,6 +265,11 @@ Q_SIGNALS:
void deviceChanged();
void suggestionModeChanged();
void languageAndLocaleChanged();
void webSearchRetrievalSizeChanged();
// FIXME: These are never emitted along with a lot of the signals above probably with all kinds of bugs!!
void webSearchUsageModeChanged();
void webSearchConfirmationModeChanged();
void braveSearchAPIKeyChanged();
private:
QSettings m_settings;
@@ -260,6 +291,7 @@ private:
void setModelSetting(const QString &name, const ModelInfo &info, const QVariant &value, bool force,
bool signal = false);
QString filePathForLocale(const QLocale &locale);
QString systemPromptInternal(const QString &proposedTemplate, QString &error);
};
#endif // MYSETTINGS_H

View File

@@ -350,17 +350,26 @@ MySettingsTab {
Layout.alignment: Qt.AlignRight
// NOTE: indices match values of SuggestionMode enum, keep them in sync
model: ListModel {
ListElement { name: qsTr("When chatting with LocalDocs") }
ListElement { name: qsTr("When source excerpts are cited") }
ListElement { name: qsTr("Whenever possible") }
ListElement { name: qsTr("Never") }
}
function updateModel() {
suggestionModeBox.currentIndex = MySettings.suggestionMode;
}
Accessible.name: suggestionModeLabel.text
Accessible.description: suggestionModeLabel.helpText
onActivated: {
MySettings.suggestionMode = suggestionModeBox.currentIndex;
}
Component.onCompleted: {
suggestionModeBox.currentIndex = MySettings.suggestionMode;
suggestionModeBox.updateModel();
}
Connections {
target: MySettings
function onSuggestionModeChanged() {
suggestionModeBox.updateModel();
}
}
}
MySettingsLabel {

View File

@@ -881,6 +881,8 @@ Rectangle {
case Chat.PromptProcessing: return qsTr("processing ...")
case Chat.ResponseGeneration: return qsTr("generating response ...");
case Chat.GeneratingQuestions: return qsTr("generating questions ...");
case Chat.ToolCalled: return qsTr("executing %1 ...").arg(currentChat.toolDescription);
case Chat.ToolProcessing: return qsTr("processing %1 results ...").arg(currentChat.toolDescription);
default: return ""; // handle unexpected values
}
}
@@ -1104,7 +1106,7 @@ Rectangle {
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: childrenRect.height
visible: {
if (consolidatedSources.length === 0)
if (sources.length === 0)
return false
if (!MySettings.localDocsShowReferences)
return false
@@ -1131,7 +1133,14 @@ Rectangle {
sourceSize.width: 24
sourceSize.height: 24
mipmap: true
source: "qrc:/gpt4all/icons/db.svg"
source: {
if (typeof sources === 'undefined'
|| typeof sources[0] === 'undefined'
|| sources[0].url === "")
return "qrc:/gpt4all/icons/db.svg";
else
return "qrc:/gpt4all/icons/globe.svg";
}
}
ColorOverlay {
@@ -1142,7 +1151,7 @@ Rectangle {
}
Text {
text: qsTr("%1 Sources").arg(consolidatedSources.length)
text: qsTr("%1 Sources").arg(sources.length)
padding: 0
font.pixelSize: theme.fontSizeLarge
font.bold: true
@@ -1190,7 +1199,7 @@ Rectangle {
Layout.column: 1
Layout.topMargin: 5
visible: {
if (consolidatedSources.length === 0)
if (sources.length === 0)
return false
if (!MySettings.localDocsShowReferences)
return false
@@ -1231,9 +1240,9 @@ Rectangle {
id: flow
Layout.fillWidth: true
spacing: 10
visible: consolidatedSources.length !== 0
visible: sources.length !== 0
Repeater {
model: consolidatedSources
model: sources
delegate: Rectangle {
radius: 10
@@ -1243,11 +1252,15 @@ Rectangle {
MouseArea {
id: ma
enabled: modelData.path !== ""
enabled: modelData.path !== "" || modelData.url !== ""
anchors.fill: parent
hoverEnabled: true
onClicked: function() {
Qt.openUrlExternally(modelData.fileUri)
if (modelData.url !== "") {
console.log("opening url")
Qt.openUrlExternally(modelData.url)
} else
Qt.openUrlExternally(modelData.fileUri)
}
}
@@ -1287,22 +1300,27 @@ Rectangle {
Image {
id: fileIcon
anchors.fill: parent
visible: false
visible: modelData.favicon !== ""
sourceSize.width: 24
sourceSize.height: 24
mipmap: true
source: {
if (modelData.file.toLowerCase().endsWith(".txt"))
if (modelData.favicon !== "")
return modelData.favicon;
else if (modelData.file.toLowerCase().endsWith(".txt"))
return "qrc:/gpt4all/icons/file-txt.svg"
else if (modelData.file.toLowerCase().endsWith(".pdf"))
return "qrc:/gpt4all/icons/file-pdf.svg"
else if (modelData.file.toLowerCase().endsWith(".md"))
return "qrc:/gpt4all/icons/file-md.svg"
else
else if (modelData.file !== "")
return "qrc:/gpt4all/icons/file.svg"
else
return "qrc:/gpt4all/icons/globe.svg"
}
}
ColorOverlay {
visible: !fileIcon.visible
anchors.fill: fileIcon
source: fileIcon
color: theme.textColor
@@ -1310,7 +1328,7 @@ Rectangle {
}
Text {
Layout.maximumWidth: 156
text: modelData.collection !== "" ? modelData.collection : qsTr("LocalDocs")
text: modelData.collection !== "" ? modelData.collection : modelData.title
font.pixelSize: theme.fontSizeLarge
font.bold: true
color: theme.styledTextColor
@@ -1326,7 +1344,7 @@ Rectangle {
Layout.fillHeight: true
Layout.maximumWidth: 180
Layout.maximumHeight: 55 - title.height
text: modelData.file
text: modelData.file !== "" ? modelData.file : modelData.url
color: theme.textColor
font.pixelSize: theme.fontSizeSmall
elide: Qt.ElideRight
@@ -1343,7 +1361,7 @@ Rectangle {
return false;
if (MySettings.suggestionMode === 2) // Off
return false;
if (MySettings.suggestionMode === 0 && consolidatedSources.length === 0) // LocalDocs only
if (MySettings.suggestionMode === 0 && sources.length === 0) // LocalDocs only
return false;
return currentChat.responseState === Chat.GeneratingQuestions || currentChat.generatedQuestions.length !== 0;
}

View File

@@ -255,13 +255,14 @@ MySettingsTab {
MySettingsLabel {
id: chunkLabel
Layout.fillWidth: true
text: qsTr("Document snippet size (characters)")
helpText: qsTr("Number of characters per document snippet. Larger numbers increase likelihood of factual responses, but also result in slower generation.")
text: qsTr("Document excerpt size (characters)")
helpText: qsTr("Number of characters per document excerpt. Larger numbers increase likelihood of factual responses, but also result in slower generation.")
}
MyTextField {
id: chunkSizeTextField
text: MySettings.localDocsChunkSize
font.pixelSize: theme.fontSizeLarge
validator: IntValidator {
bottom: 1
}
@@ -281,13 +282,14 @@ MySettingsTab {
Layout.topMargin: 15
MySettingsLabel {
id: contextItemsPerPrompt
text: qsTr("Max document snippets per prompt")
helpText: qsTr("Max best N matches of retrieved document snippets to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation.")
text: qsTr("Max source excerpts per prompt")
helpText: qsTr("Max best N matches of retrieved source excerpts to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation.")
}
MyTextField {
text: MySettings.localDocsRetrievalSize
font.pixelSize: theme.fontSizeLarge
validator: IntValidator {
bottom: 1
}

View File

@@ -154,18 +154,56 @@ MySettingsTab {
}
MySettingsLabel {
visible: !root.currentModelInfo.isOnline
text: qsTr("System Prompt")
helpText: qsTr("Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens.")
Layout.row: 7
Layout.column: 0
Layout.columnSpan: 1
Layout.topMargin: 15
id: isToolCallingLabel
text: qsTr("Is Tool Calling Model")
helpText: qsTr("Whether the model is capable of tool calling and has tool calling instructions in system prompt.")
}
MyCheckBox {
Layout.row: 7
Layout.column: 1
Layout.topMargin: 15
id: isToolCallingBox
checked: root.currentModelInfo.isToolCalling
onClicked: {
MySettings.setModelIsToolCalling(root.currentModelInfo, isToolCallingBox.checked);
}
}
RowLayout {
Layout.row: 8
Layout.column: 0
Layout.columnSpan: 2
Layout.topMargin: 15
spacing: 10
MySettingsLabel {
text: qsTr("System Prompt Template")
helpText: qsTr("Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens.")
}
MySettingsLabel {
id: systemPromptTemplateError
color: theme.textErrorColor
wrapMode: TextArea.Wrap
Timer {
id: errorTimer
interval: 500 // 500 ms delay
repeat: false
property string text: ""
onTriggered: {
systemPromptTemplateError.text = errorTimer.text;
}
}
}
}
Rectangle {
id: systemPrompt
visible: !root.currentModelInfo.isOnline
Layout.row: 8
Layout.row: 9
Layout.column: 0
Layout.columnSpan: 2
Layout.fillWidth: true
@@ -174,28 +212,36 @@ MySettingsTab {
MyTextArea {
id: systemPromptArea
anchors.fill: parent
text: root.currentModelInfo.systemPrompt
text: root.currentModelInfo.systemPromptTemplate
Connections {
target: MySettings
function onSystemPromptChanged() {
systemPromptArea.text = root.currentModelInfo.systemPrompt;
systemPromptArea.text = root.currentModelInfo.systemPromptTemplate;
}
}
Connections {
target: root
function onCurrentModelInfoChanged() {
systemPromptArea.text = root.currentModelInfo.systemPrompt;
systemPromptArea.text = root.currentModelInfo.systemPromptTemplate;
}
}
onTextChanged: {
MySettings.setModelSystemPrompt(root.currentModelInfo, text)
var errorString = MySettings.validateModelSystemPromptTemplate(text);
if (errorString === "") {
errorTimer.stop();
systemPromptTemplateError.text = ""; // Clear any previous error
MySettings.setModelSystemPromptTemplate(root.currentModelInfo, text);
} else {
errorTimer.text = errorString;
errorTimer.restart();
}
}
Accessible.role: Accessible.EditableText
}
}
RowLayout {
Layout.row: 9
Layout.row: 10
Layout.column: 0
Layout.columnSpan: 2
Layout.topMargin: 15
@@ -209,38 +255,38 @@ MySettingsTab {
id: promptTemplateLabelHelp
text: qsTr("Must contain the string \"%1\" to be replaced with the user's input.")
color: theme.textErrorColor
visible: templateTextArea.text.indexOf("%1") === -1
visible: promptTemplateTextArea.text.indexOf("%1") === -1
wrapMode: TextArea.Wrap
}
}
Rectangle {
id: promptTemplate
Layout.row: 10
Layout.row: 11
Layout.column: 0
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.minimumHeight: Math.max(100, templateTextArea.contentHeight + 20)
Layout.minimumHeight: Math.max(100, promptTemplateTextArea.contentHeight + 20)
color: "transparent"
clip: true
MyTextArea {
id: templateTextArea
id: promptTemplateTextArea
anchors.fill: parent
text: root.currentModelInfo.promptTemplate
Connections {
target: MySettings
function onPromptTemplateChanged() {
templateTextArea.text = root.currentModelInfo.promptTemplate;
promptTemplateTextArea.text = root.currentModelInfo.promptTemplate;
}
}
Connections {
target: root
function onCurrentModelInfoChanged() {
templateTextArea.text = root.currentModelInfo.promptTemplate;
promptTemplateTextArea.text = root.currentModelInfo.promptTemplate;
}
}
onTextChanged: {
if (templateTextArea.text.indexOf("%1") !== -1) {
if (promptTemplateTextArea.text.indexOf("%1") !== -1) {
MySettings.setModelPromptTemplate(root.currentModelInfo, text)
}
}
@@ -250,18 +296,65 @@ MySettingsTab {
}
}
MySettingsLabel {
Layout.row: 12
Layout.column: 0
Layout.columnSpan: 2
Layout.topMargin: 15
id: toolTemplateLabel
text: qsTr("Tool Template")
helpText: qsTr("The template that allows tool calls to inject information into the context. Only enabled for tool calling models.")
}
Rectangle {
id: toolTemplate
enabled: root.currentModelInfo.isToolCalling
Layout.row: 13
Layout.column: 0
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.minimumHeight: Math.max(100, toolTemplateTextArea.contentHeight + 20)
color: "transparent"
clip: true
MyTextArea {
id: toolTemplateTextArea
anchors.fill: parent
text: root.currentModelInfo.toolTemplate
Connections {
target: MySettings
function onToolTemplateChanged() {
toolTemplateTextArea.text = root.currentModelInfo.toolTemplate;
}
}
Connections {
target: root
function onCurrentModelInfoChanged() {
toolTemplateTextArea.text = root.currentModelInfo.toolTemplate;
}
}
onTextChanged: {
if (toolTemplateTextArea.text.indexOf("%1") !== -1) {
MySettings.setModelToolTemplate(root.currentModelInfo, text)
}
}
Accessible.role: Accessible.EditableText
Accessible.name: toolTemplateLabel.text
Accessible.description: toolTemplateLabel.text
}
}
MySettingsLabel {
id: chatNamePromptLabel
text: qsTr("Chat Name Prompt")
helpText: qsTr("Prompt used to automatically generate chat names.")
Layout.row: 11
Layout.row: 14
Layout.column: 0
Layout.topMargin: 15
}
Rectangle {
id: chatNamePrompt
Layout.row: 12
Layout.row: 15
Layout.column: 0
Layout.columnSpan: 2
Layout.fillWidth: true
@@ -297,14 +390,14 @@ MySettingsTab {
id: suggestedFollowUpPromptLabel
text: qsTr("Suggested FollowUp Prompt")
helpText: qsTr("Prompt used to generate suggested follow-up questions.")
Layout.row: 13
Layout.row: 16
Layout.column: 0
Layout.topMargin: 15
}
Rectangle {
id: suggestedFollowUpPrompt
Layout.row: 14
Layout.row: 17
Layout.column: 0
Layout.columnSpan: 2
Layout.fillWidth: true
@@ -337,7 +430,7 @@ MySettingsTab {
}
GridLayout {
Layout.row: 15
Layout.row: 18
Layout.column: 0
Layout.columnSpan: 2
Layout.topMargin: 15
@@ -833,7 +926,7 @@ MySettingsTab {
}
Rectangle {
Layout.row: 16
Layout.row: 19
Layout.column: 0
Layout.columnSpan: 2
Layout.topMargin: 15

View File

@@ -34,6 +34,9 @@ Rectangle {
ListElement {
title: qsTr("LocalDocs")
}
ListElement {
title: qsTr("Web Search")
}
}
ColumnLayout {
@@ -152,6 +155,12 @@ Rectangle {
Component { LocalDocsSettings { } }
]
}
MySettingsStack {
tabs: [
Component { WebSearchSettings { } }
]
}
}
}
}

View File

@@ -0,0 +1,151 @@
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import QtQuick.Dialogs
import localdocs
import modellist
import mysettings
import network
MySettingsTab {
onRestoreDefaultsClicked: {
MySettings.restoreWebSearchDefaults();
}
showRestoreDefaultsButton: true
title: qsTr("Web Search")
contentItem: ColumnLayout {
id: root
spacing: 30
ColumnLayout {
spacing: 10
Label {
color: theme.grayRed900
font.pixelSize: theme.fontSizeLarge
font.bold: true
text: qsTr("Web Search")
}
Rectangle {
Layout.fillWidth: true
height: 1
color: theme.grayRed500
}
}
RowLayout {
MySettingsLabel {
id: usageModeLabel
text: qsTr("Usage Mode")
helpText: qsTr("When and how the brave search tool is executed.")
}
MyComboBox {
id: usageModeBox
Layout.minimumWidth: 400
Layout.maximumWidth: 400
Layout.alignment: Qt.AlignRight
// NOTE: indices match values of UsageMode enum, keep them in sync
model: ListModel {
ListElement { name: qsTr("Never") }
ListElement { name: qsTr("Model decides") }
ListElement { name: qsTr("Force usage for every response where possible") }
}
function updateModel() {
usageModeBox.currentIndex = MySettings.webSearchUsageMode;
}
Accessible.name: usageModeLabel.text
Accessible.description: usageModeLabel.helpText
onActivated: {
MySettings.webSearchUsageMode = usageModeBox.currentIndex;
}
Component.onCompleted: {
usageModeBox.updateModel();
}
Connections {
target: MySettings
function onWebSearchUsageModeChanged() {
usageModeBox.updateModel();
}
}
}
}
RowLayout {
MySettingsLabel {
id: apiKeyLabel
text: qsTr("Brave AI API key")
helpText: qsTr('The API key to use for Brave Web Search. Get one from the Brave for free <a href="https://brave.com/search/api/">API keys page</a>.')
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
}
MyTextField {
id: apiKeyField
enabled: usageModeBox.currentIndex !== 0
text: MySettings.braveSearchAPIKey
color: theme.textColor
font.pixelSize: theme.fontSizeLarge
Layout.alignment: Qt.AlignRight
Layout.minimumWidth: 400
Layout.maximumWidth: 400
onEditingFinished: {
MySettings.braveSearchAPIKey = apiKeyField.text;
}
Accessible.role: Accessible.EditableText
Accessible.name: apiKeyLabel.text
Accessible.description: apiKeyLabel.helpText
}
}
RowLayout {
MySettingsLabel {
id: contextItemsPerPrompt
text: qsTr("Max source excerpts per prompt")
helpText: qsTr("Max best N matches of retrieved source excerpts to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation.")
}
MyTextField {
text: MySettings.webSearchRetrievalSize
font.pixelSize: theme.fontSizeLarge
validator: IntValidator {
bottom: 1
}
onEditingFinished: {
var val = parseInt(text)
if (!isNaN(val)) {
MySettings.webSearchRetrievalSize = val
focus = false
} else {
text = MySettings.webSearchRetrievalSize
}
}
}
}
// FIXME:
// RowLayout {
// MySettingsLabel {
// id: askBeforeRunningLabel
// text: qsTr("Ask before running")
// helpText: qsTr("The user is queried whether they want the tool to run in every instance.")
// }
// MyCheckBox {
// id: askBeforeRunningBox
// checked: MySettings.webSearchConfirmationMode
// onClicked: {
// MySettings.webSearchConfirmationMode = !MySettings.webSearchAskBeforeRunning
// }
// }
// }
Rectangle {
Layout.topMargin: 15
Layout.fillWidth: true
height: 1
color: theme.settingsDivider
}
}
}

View File

@@ -56,27 +56,13 @@ static inline QJsonObject modelToJson(const ModelInfo &info)
return model;
}
static inline QJsonObject resultToJson(const ResultInfo &info)
{
QJsonObject result;
result.insert("file", info.file);
result.insert("title", info.title);
result.insert("author", info.author);
result.insert("date", info.date);
result.insert("text", info.text);
result.insert("page", info.page);
result.insert("from", info.from);
result.insert("to", info.to);
return result;
}
Server::Server(Chat *chat)
: ChatLLM(chat, true /*isServer*/)
, m_chat(chat)
, m_server(nullptr)
{
connect(this, &Server::threadStarted, this, &Server::start);
connect(this, &Server::databaseResultsChanged, this, &Server::handleDatabaseResultsChanged);
connect(this, &Server::sourceExcerptsChanged, this, &Server::handleSourceExcerptsChanged);
connect(chat, &Chat::collectionListChanged, this, &Server::handleCollectionListChanged, Qt::QueuedConnection);
}
@@ -373,7 +359,7 @@ QHttpServerResponse Server::handleCompletionRequest(const QHttpServerRequest &re
int promptTokens = 0;
int responseTokens = 0;
QList<QPair<QString, QList<ResultInfo>>> responses;
QList<QPair<QString, QList<SourceExcerpt>>> responses;
for (int i = 0; i < n; ++i) {
if (!promptInternal(
m_collections,
@@ -394,7 +380,7 @@ QHttpServerResponse Server::handleCompletionRequest(const QHttpServerRequest &re
QString echoedPrompt = actualPrompt;
if (!echoedPrompt.endsWith("\n"))
echoedPrompt += "\n";
responses.append(qMakePair((echo ? u"%1\n"_s.arg(actualPrompt) : QString()) + response(), m_databaseResults));
responses.append(qMakePair((echo ? u"%1\n"_s.arg(actualPrompt) : QString()) + response(), m_sourceExcerpts));
if (!promptTokens)
promptTokens += m_promptTokens;
responseTokens += m_promptResponseTokens - m_promptTokens;
@@ -414,7 +400,7 @@ QHttpServerResponse Server::handleCompletionRequest(const QHttpServerRequest &re
int index = 0;
for (const auto &r : responses) {
QString result = r.first;
QList<ResultInfo> infos = r.second;
QList<SourceExcerpt> infos = r.second;
QJsonObject choice;
choice.insert("index", index++);
choice.insert("finish_reason", responseTokens == max_tokens ? "length" : "stop");
@@ -422,30 +408,22 @@ QHttpServerResponse Server::handleCompletionRequest(const QHttpServerRequest &re
message.insert("role", "assistant");
message.insert("content", result);
choice.insert("message", message);
if (MySettings::globalInstance()->localDocsShowReferences()) {
QJsonArray references;
for (const auto &ref : infos)
references.append(resultToJson(ref));
choice.insert("references", references);
}
if (MySettings::globalInstance()->localDocsShowReferences())
choice.insert("references", SourceExcerpt::toJson(infos));
choices.append(choice);
}
} else {
int index = 0;
for (const auto &r : responses) {
QString result = r.first;
QList<ResultInfo> infos = r.second;
QList<SourceExcerpt> infos = r.second;
QJsonObject choice;
choice.insert("text", result);
choice.insert("index", index++);
choice.insert("logprobs", QJsonValue::Null); // We don't support
choice.insert("finish_reason", responseTokens == max_tokens ? "length" : "stop");
if (MySettings::globalInstance()->localDocsShowReferences()) {
QJsonArray references;
for (const auto &ref : infos)
references.append(resultToJson(ref));
choice.insert("references", references);
}
if (MySettings::globalInstance()->localDocsShowReferences())
choice.insert("references", SourceExcerpt::toJson(infos));
choices.append(choice);
}
}

View File

@@ -2,7 +2,7 @@
#define SERVER_H
#include "chatllm.h"
#include "database.h"
#include "sourceexcerpt.h"
#include <QHttpServerRequest>
#include <QHttpServerResponse>
@@ -29,13 +29,13 @@ Q_SIGNALS:
private Q_SLOTS:
QHttpServerResponse handleCompletionRequest(const QHttpServerRequest &request, bool isChat);
void handleDatabaseResultsChanged(const QList<ResultInfo> &results) { m_databaseResults = results; }
void handleSourceExcerptsChanged(const QList<SourceExcerpt> &sourceExcerpts) { m_sourceExcerpts = sourceExcerpts; }
void handleCollectionListChanged(const QList<QString> &collectionList) { m_collections = collectionList; }
private:
Chat *m_chat;
QHttpServer *m_server;
QList<ResultInfo> m_databaseResults;
QList<SourceExcerpt> m_sourceExcerpts;
QList<QString> m_collections;
};

View File

@@ -0,0 +1,131 @@
#include "sourceexcerpt.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
QString SourceExcerpt::toJson(const QList<SourceExcerpt> &sources)
{
if (sources.isEmpty())
return QString();
QJsonArray resultsArray;
for (const auto &source : sources) {
QJsonObject sourceObj;
sourceObj["date"] = source.date;
sourceObj["collection"] = source.collection;
sourceObj["path"] = source.path;
sourceObj["file"] = source.file;
sourceObj["url"] = source.url;
sourceObj["favicon"] = source.favicon;
sourceObj["title"] = source.title;
sourceObj["author"] = source.author;
sourceObj["description"] = source.description;
QJsonArray excerptsArray;
for (const auto &excerpt : source.excerpts) {
QJsonObject excerptObj;
excerptObj["text"] = excerpt.text;
if (excerpt.page != -1)
excerptObj["page"] = excerpt.page;
if (excerpt.from != -1)
excerptObj["from"] = excerpt.from;
if (excerpt.to != -1)
excerptObj["to"] = excerpt.to;
excerptsArray.append(excerptObj);
}
sourceObj["excerpts"] = excerptsArray;
resultsArray.append(sourceObj);
}
QJsonObject jsonObj;
jsonObj["results"] = resultsArray;
QJsonDocument doc(jsonObj);
return doc.toJson(QJsonDocument::Compact);
}
QList<SourceExcerpt> SourceExcerpt::fromJson(const QString &json, QString &errorString)
{
if (json.isEmpty())
return QList<SourceExcerpt>();
QJsonParseError err;
QJsonDocument document = QJsonDocument::fromJson(json.toUtf8(), &err);
if (err.error != QJsonParseError::NoError) {
errorString = err.errorString();
return QList<SourceExcerpt>();
}
QJsonObject jsonObject = document.object();
Q_ASSERT(jsonObject.contains("results"));
if (!jsonObject.contains("results")) {
errorString = "json does not contain results array";
return QList<SourceExcerpt>();
}
QList<SourceExcerpt> excerpts;
QJsonArray results = jsonObject["results"].toArray();
for (int i = 0; i < results.size(); ++i) {
QJsonObject result = results[i].toObject();
if (!result.contains("date")) {
errorString = "result does not contain required date field";
return QList<SourceExcerpt>();
}
if (!result.contains("excerpts") || !result["excerpts"].isArray()) {
errorString = "result does not contain required excerpts array";
return QList<SourceExcerpt>();
}
QJsonArray textExcerpts = result["excerpts"].toArray();
if (textExcerpts.isEmpty()) {
errorString = "result excerpts array is empty";
return QList<SourceExcerpt>();
}
SourceExcerpt source;
source.date = result["date"].toString();
if (result.contains("collection"))
source.collection = result["collection"].toString();
if (result.contains("path"))
source.path = result["path"].toString();
if (result.contains("file"))
source.file = result["file"].toString();
if (result.contains("url"))
source.url = result["url"].toString();
if (result.contains("favicon"))
source.favicon = result["favicon"].toString();
if (result.contains("title"))
source.title = result["title"].toString();
if (result.contains("author"))
source.author = result["author"].toString();
if (result.contains("description"))
source.author = result["description"].toString();
for (int i = 0; i < textExcerpts.size(); ++i) {
if (!textExcerpts[i].isObject()) {
errorString = "result excerpt is not an object";
return QList<SourceExcerpt>();
}
QJsonObject excerptObj = textExcerpts[i].toObject();
if (!excerptObj.contains("text")) {
errorString = "result excerpt is does not have text field";
return QList<SourceExcerpt>();
}
Excerpt excerpt;
excerpt.text = excerptObj["text"].toString();
if (excerptObj.contains("page"))
excerpt.page = excerptObj["page"].toInt();
if (excerptObj.contains("from"))
excerpt.from = excerptObj["from"].toInt();
if (excerptObj.contains("to"))
excerpt.to = excerptObj["to"].toInt();
source.excerpts.append(excerpt);
}
excerpts.append(source);
}
return excerpts;
}

View File

@@ -0,0 +1,99 @@
#ifndef SOURCEEXCERT_H
#define SOURCEEXCERT_H
#include <QObject>
#include <QJsonObject>
#include <QFileInfo>
#include <QUrl>
using namespace Qt::Literals::StringLiterals;
struct Excerpt {
QString text; // [Required] The text actually used in the augmented context
int page = -1; // [Optional] The page where the text was found
int from = -1; // [Optional] The line number where the text begins
int to = -1; // [Optional] The line number where the text ends
bool operator==(const Excerpt &other) const {
return text == other.text && page == other.page && from == other.from && to == other.to;
}
bool operator!=(const Excerpt &other) const {
return !(*this == other);
}
};
Q_DECLARE_METATYPE(Excerpt)
struct SourceExcerpt {
Q_GADGET
Q_PROPERTY(QString date MEMBER date)
Q_PROPERTY(QString collection MEMBER collection)
Q_PROPERTY(QString path MEMBER path)
Q_PROPERTY(QString file MEMBER file)
Q_PROPERTY(QString url MEMBER url)
Q_PROPERTY(QString favicon MEMBER favicon)
Q_PROPERTY(QString title MEMBER title)
Q_PROPERTY(QString author MEMBER author)
Q_PROPERTY(QString description MEMBER description)
Q_PROPERTY(QString fileUri READ fileUri STORED false)
Q_PROPERTY(QString text READ text STORED false)
Q_PROPERTY(QList<Excerpt> excerpts MEMBER excerpts)
public:
QString date; // [Required] The creation or the last modification date whichever is latest
QString collection; // [Optional] The name of the collection
QString path; // [Optional] The full path
QString file; // [Optional] The name of the file, but not the full path
QString url; // [Optional] The name of the remote url
QString favicon; // [Optional] The favicon
QString title; // [Optional] The title of the document
QString author; // [Optional] The author of the document
QString description; // [Optional] The description of the source
QList<Excerpt> excerpts;// [Required] The list of excerpts
// Returns a human readable string containing all the excerpts
QString text() const {
QStringList formattedExcerpts;
for (const auto& excerpt : excerpts) {
QString formattedExcerpt = excerpt.text;
if (excerpt.page != -1) {
formattedExcerpt += QStringLiteral(" (Page: %1").arg(excerpt.page);
if (excerpt.from != -1 && excerpt.to != -1) {
formattedExcerpt += QStringLiteral(", Lines: %1-%2").arg(excerpt.from).arg(excerpt.to);
}
formattedExcerpt += QStringLiteral(")");
} else if (excerpt.from != -1 && excerpt.to != -1) {
formattedExcerpt += QStringLiteral(" (Lines: %1-%2)").arg(excerpt.from).arg(excerpt.to);
}
formattedExcerpts.append(formattedExcerpt);
}
return formattedExcerpts.join(QStringLiteral("\n---\n"));
}
QString fileUri() const {
// QUrl reserved chars that are not UNSAFE_PATH according to glib/gconvert.c
static const QByteArray s_exclude = "!$&'()*+,/:=@~"_ba;
Q_ASSERT(!QFileInfo(path).isRelative());
#ifdef Q_OS_WINDOWS
Q_ASSERT(!path.contains('\\')); // Qt normally uses forward slash as path separator
#endif
auto escaped = QString::fromUtf8(QUrl::toPercentEncoding(path, s_exclude));
if (escaped.front() != '/')
escaped = '/' + escaped;
return u"file://"_s + escaped;
}
static QString toJson(const QList<SourceExcerpt> &sources);
static QList<SourceExcerpt> fromJson(const QString &json, QString &errorString);
bool operator==(const SourceExcerpt &other) const {
return file == other.file || url == other.url;
}
bool operator!=(const SourceExcerpt &other) const {
return !(*this == other);
}
};
Q_DECLARE_METATYPE(SourceExcerpt)
#endif // SOURCEEXCERT_H

31
gpt4all-chat/tool.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include "tool.h"
#include <QJsonDocument>
QJsonObject filterModelGeneratedProperties(const QJsonObject &inputObject) {
QJsonObject filteredObject;
for (const QString &key : inputObject.keys()) {
QJsonObject propertyObject = inputObject.value(key).toObject();
if (!propertyObject.contains("modelGenerated") || propertyObject["modelGenerated"].toBool())
filteredObject.insert(key, propertyObject);
}
return filteredObject;
}
jinja2::Value Tool::jinjaValue() const
{
QJsonDocument doc(filterModelGeneratedProperties(paramSchema()));
QString p(doc.toJson(QJsonDocument::Compact));
QJsonDocument exampleDoc(exampleParams());
QString e(exampleDoc.toJson(QJsonDocument::Compact));
jinja2::ValuesMap params {
{ "name", name().toStdString() },
{ "description", description().toStdString() },
{ "function", function().toStdString() },
{ "paramSchema", p.toStdString() },
{ "exampleParams", e.toStdString() }
};
return params;
}

116
gpt4all-chat/tool.h Normal file
View File

@@ -0,0 +1,116 @@
#ifndef TOOL_H
#define TOOL_H
#include <QObject>
#include <QJsonObject>
#include <jinja2cpp/value.h>
using namespace Qt::Literals::StringLiterals;
namespace ToolEnums {
Q_NAMESPACE
enum class Error {
NoError = 0,
TimeoutError = 2,
UnknownError = 499,
};
Q_ENUM_NS(Error)
enum class UsageMode {
Disabled = 0, // Completely disabled
Enabled = 1, // Enabled and the model decides whether to run
ForceUsage = 2, // Attempt to force usage of the tool rather than let the LLM decide. NOTE: Not always possible.
};
Q_ENUM_NS(UsageMode)
enum class ConfirmationMode {
NoConfirmation = 0, // No confirmation required
AskBeforeRunning = 1, // User is queried on every execution
AskBeforeRunningRecursive = 2, // User is queried if the tool is invoked in a recursive tool call
};
Q_ENUM_NS(ConfirmationMode)
// Ordered in increasing levels of privacy
enum class PrivacyScope {
None = 0, // Tool call data does not have any privacy scope
LocalOrg = 1, // Tool call data does not leave the local organization
Local = 2 // Tool call data does not leave the machine
};
Q_ENUM_NS(PrivacyScope)
}
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(ToolEnums::PrivacyScope privacyScope READ privacyScope CONSTANT)
Q_PROPERTY(QJsonObject paramSchema READ paramSchema CONSTANT)
Q_PROPERTY(QJsonObject exampleParams READ exampleParams CONSTANT)
Q_PROPERTY(QUrl url READ url CONSTANT)
Q_PROPERTY(bool isBuiltin READ isBuiltin CONSTANT)
Q_PROPERTY(ToolEnums::UsageMode usageMode READ usageMode NOTIFY usageModeChanged)
Q_PROPERTY(ToolEnums::ConfirmationMode confirmationMode READ confirmationMode NOTIFY confirmationModeChanged)
Q_PROPERTY(bool excerpts READ excerpts CONSTANT)
public:
Tool() : QObject(nullptr) {}
virtual ~Tool() {}
virtual QString run(const QJsonObject &parameters, qint64 timeout = 2000) = 0;
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 the tool.
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;
// [Required] The privacy scope.
virtual ToolEnums::PrivacyScope privacyScope() const = 0;
// [Optional] Json schema describing the tool's parameters. An empty object specifies no parameters.
// https://json-schema.org/understanding-json-schema/
// https://platform.openai.com/docs/api-reference/runs/createRun#runs-createrun-tools
// https://github.com/ollama/ollama/blob/main/docs/api.md#chat-request-with-tools
// FIXME: This should be validated against json schema
virtual QJsonObject paramSchema() const { return QJsonObject(); }
// [Optional] An example of the parameters for this tool call. NOTE: This should only include parameters
// that the model is responsible for generating.
virtual QJsonObject exampleParams() const { return QJsonObject(); }
// [Optional] The local file or remote resource use to invoke the tool.
virtual QUrl url() const { return QUrl(); }
// [Optional] Whether the tool is built-in.
virtual bool isBuiltin() const { return false; }
// [Optional] The usage mode.
virtual ToolEnums::UsageMode usageMode() const { return ToolEnums::UsageMode::Disabled; }
// [Optional] The confirmation mode.
virtual ToolEnums::ConfirmationMode confirmationMode() const { return ToolEnums::ConfirmationMode::NoConfirmation; }
// [Optional] Whether json result produces source excerpts.
virtual bool excerpts() const { return false; }
bool operator==(const Tool &other) const {
return function() == other.function();
}
bool operator!=(const Tool &other) const {
return !(*this == other);
}
jinja2::Value jinjaValue() const;
Q_SIGNALS:
void usageModeChanged();
void confirmationModeChanged();
};
#endif // TOOL_H

View File

@@ -0,0 +1,47 @@
#include "toolmodel.h"
#include <QGlobalStatic>
#include <QGuiApplication>
#include <QJsonDocument>
#include "bravesearch.h"
#include "localdocssearch.h"
class MyToolModel: public ToolModel { };
Q_GLOBAL_STATIC(MyToolModel, toolModelInstance)
ToolModel *ToolModel::globalInstance()
{
return toolModelInstance();
}
ToolModel::ToolModel()
: QAbstractListModel(nullptr) {
QCoreApplication::instance()->installEventFilter(this);
Tool* localDocsSearch = new LocalDocsSearch;
m_tools.append(localDocsSearch);
m_toolMap.insert(localDocsSearch->function(), localDocsSearch);
connect(localDocsSearch, &Tool::usageModeChanged, this, &ToolModel::privacyScopeChanged);
Tool *braveSearch = new BraveSearch;
m_tools.append(braveSearch);
m_toolMap.insert(braveSearch->function(), braveSearch);
connect(braveSearch, &Tool::usageModeChanged, this, &ToolModel::privacyScopeChanged);
}
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;
}
ToolEnums::PrivacyScope ToolModel::privacyScope() const
{
ToolEnums::PrivacyScope scope = ToolEnums::PrivacyScope::Local; // highest scope
for (const Tool *t : m_tools)
if (t->usageMode() != ToolEnums::UsageMode::Disabled)
scope = std::min(scope, t->privacyScope());
return scope;
}

121
gpt4all-chat/toolmodel.h Normal file
View File

@@ -0,0 +1,121 @@
#ifndef TOOLMODEL_H
#define TOOLMODEL_H
#include "tool.h"
#include <QAbstractListModel>
class ToolModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(ToolEnums::PrivacyScope privacyScope READ privacyScope NOTIFY privacyScopeChanged)
public:
static ToolModel *globalInstance();
enum Roles {
NameRole = Qt::UserRole + 1,
DescriptionRole,
FunctionRole,
PrivacyScopeRole,
ParametersRole,
UrlRole,
ApiKeyRole,
KeyRequiredRole,
IsBuiltinRole,
UsageModeRole,
ConfirmationModeRole,
ExcerptsRole,
};
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 PrivacyScopeRole:
return QVariant::fromValue(item->privacyScope());
case ParametersRole:
return item->paramSchema();
case UrlRole:
return item->url();
case IsBuiltinRole:
return item->isBuiltin();
case UsageModeRole:
return QVariant::fromValue(item->usageMode());
case ConfirmationModeRole:
return QVariant::fromValue(item->confirmationMode());
case ExcerptsRole:
return item->excerpts();
}
return QVariant();
}
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[DescriptionRole] = "description";
roles[FunctionRole] = "function";
roles[PrivacyScopeRole] = "privacyScope";
roles[ParametersRole] = "parameters";
roles[UrlRole] = "url";
roles[ApiKeyRole] = "apiKey";
roles[KeyRequiredRole] = "keyRequired";
roles[IsBuiltinRole] = "isBuiltin";
roles[UsageModeRole] = "usageMode";
roles[ConfirmationModeRole] = "confirmationMode";
roles[ExcerptsRole] = "excerpts";
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(); }
// Returns the least private scope of all enabled tools
ToolEnums::PrivacyScope privacyScope() const;
Q_SIGNALS:
void countChanged();
void privacyScopeChanged();
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