Refactor and make use of jinja templates.

Signed-off-by: Adam Treat <treat.adam@gmail.com>
This commit is contained in:
Adam Treat 2024-08-09 11:02:10 -04:00
parent 00ecbb75b4
commit c3cfaff803
14 changed files with 244 additions and 120 deletions

View File

@ -306,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

@ -42,6 +42,56 @@ QString BraveSearch::run(const QJsonObject &parameters, qint64 timeout)
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();
}
bool BraveSearch::isEnabled() const
{
// FIXME: Refer to mysettings
return true;
}
bool BraveSearch::forceUsage() const
{
// FIXME: Refer to mysettings
return false;
}
void BraveAPIWorker::request(const QString &apiKey, const QString &query, int count)
{
// Documentation on the brave web search:

View File

@ -48,6 +48,16 @@ public:
ToolEnums::Error error() const override { return m_error; }
QString errorString() const override { return m_errorString; }
QString name() const override { return tr("Brave web search"); }
QString description() const override { return tr("Search the web using brave"); }
QString function() const override { return "brave_search"; }
QJsonObject paramSchema() const override;
QJsonObject exampleParams() const override;
bool isEnabled() const override;
bool isBuiltin() const override { return true; }
bool forceUsage() const override;
bool excerpts() const override { return true; }
private:
ToolEnums::Error m_error;
QString m_errorString;

View File

@ -6,6 +6,8 @@
#include "localdocssearch.h"
#include "mysettings.h"
#include "network.h"
#include "tool.h"
#include "toolmodel.h"
#include <QDataStream>
#include <QDebug>
@ -29,6 +31,7 @@
#include <cmath>
#include <cstddef>
#include <functional>
#include <jinja2cpp/template.h>
#include <limits>
#include <optional>
#include <string_view>
@ -1332,7 +1335,35 @@ void ChatLLM::processSystemPrompt()
if (!isModelLoaded() || m_processedSystemPrompt || m_restoreStateFromText || m_isServer)
return;
const std::string systemPrompt = MySettings::globalInstance()->modelSystemPrompt(m_modelInfo).toStdString();
const std::string systemPromptTemplate = MySettings::globalInstance()->modelSystemPromptTemplate(m_modelInfo).toStdString();
// FIXME: This needs to be moved to settings probably and the same code used for validation
jinja2::ValuesMap params;
params.insert({"currentDate", QDate::currentDate().toString().toStdString()});
jinja2::ValuesList toolList;
int c = ToolModel::globalInstance()->count();
for (int i = 0; i < c; ++i) {
Tool *t = ToolModel::globalInstance()->get(i);
if (t->isEnabled() && !t->forceUsage())
toolList.push_back(t->jinjaValue());
}
params.insert({"toolList", toolList});
std::string systemPrompt;
jinja2::Template t;
t.Load(systemPromptTemplate);
const auto renderResult = t.RenderAsString(params);
// 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(renderResult);
if (renderResult)
systemPrompt = renderResult.value();
else
qWarning() << "ERROR: Could not parse system prompt template:" << renderResult.error().ToString();
if (QString::fromStdString(systemPrompt).trimmed().isEmpty()) {
m_processedSystemPrompt = true;
return;

View File

@ -6,6 +6,7 @@
#include <QDebug>
#include <QGuiApplication>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>
@ -39,6 +40,37 @@ QString LocalDocsSearch::run(const QJsonObject &parameters, qint64 timeout)
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();
}
LocalDocsWorker::LocalDocsWorker()
: QObject(nullptr)
{

View File

@ -34,6 +34,15 @@ public:
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"; }
QJsonObject paramSchema() const override;
bool isEnabled() const override { return true; }
bool isBuiltin() const override { return true; }
bool forceUsage() const override { return true; }
bool excerpts() const override { return true; }
private:
ToolEnums::Error m_error;
QString m_errorString;

View File

@ -334,15 +334,15 @@ void ModelInfo::setToolTemplate(const QString &t)
m_toolTemplate = t;
}
QString ModelInfo::systemPrompt() const
QString ModelInfo::systemPromptTemplate() const
{
return MySettings::globalInstance()->modelSystemPrompt(*this);
return MySettings::globalInstance()->modelSystemPromptTemplate(*this);
}
void ModelInfo::setSystemPrompt(const QString &p)
void ModelInfo::setSystemPromptTemplate(const QString &p)
{
if (shouldSaveMetadata()) MySettings::globalInstance()->setModelSystemPrompt(*this, p, true /*force*/);
m_systemPrompt = p;
if (shouldSaveMetadata()) MySettings::globalInstance()->setModelSystemPromptTemplate(*this, p, true /*force*/);
m_systemPromptTemplate = p;
}
QString ModelInfo::chatNamePrompt() const
@ -397,7 +397,7 @@ QVariantMap ModelInfo::getFields() const
{ "repeatPenaltyTokens", m_repeatPenaltyTokens },
{ "promptTemplate", m_promptTemplate },
{ "toolTemplate", m_toolTemplate },
{ "systemPrompt", m_systemPrompt },
{ "systemPromptTemplate",m_systemPromptTemplate },
{ "chatNamePrompt", m_chatNamePrompt },
{ "suggestedFollowUpPrompt", m_suggestedFollowUpPrompt },
};
@ -792,7 +792,7 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const
case ToolTemplateRole:
return info->toolTemplate();
case SystemPromptRole:
return info->systemPrompt();
return info->systemPromptTemplate();
case ChatNamePromptRole:
return info->chatNamePrompt();
case SuggestedFollowUpPromptRole:
@ -970,7 +970,7 @@ void ModelList::updateData(const QString &id, const QVector<QPair<int, QVariant>
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:
@ -1125,7 +1125,7 @@ QString ModelList::clone(const ModelInfo &model)
{ ModelList::RepeatPenaltyTokensRole, model.repeatPenaltyTokens() },
{ ModelList::PromptTemplateRole, model.promptTemplate() },
{ ModelList::ToolTemplateRole, model.toolTemplate() },
{ ModelList::SystemPromptRole, model.systemPrompt() },
{ ModelList::SystemPromptRole, model.systemPromptTemplate() },
{ ModelList::ChatNamePromptRole, model.chatNamePrompt() },
{ ModelList::SuggestedFollowUpPromptRole, model.suggestedFollowUpPrompt() },
};

View File

@ -69,7 +69,7 @@ struct ModelInfo {
Q_PROPERTY(int repeatPenaltyTokens READ repeatPenaltyTokens WRITE setRepeatPenaltyTokens)
Q_PROPERTY(QString promptTemplate READ promptTemplate WRITE setPromptTemplate)
Q_PROPERTY(QString toolTemplate READ toolTemplate WRITE setToolTemplate)
Q_PROPERTY(QString systemPrompt READ systemPrompt WRITE setSystemPrompt)
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)
@ -181,8 +181,9 @@ public:
void setPromptTemplate(const QString &t);
QString toolTemplate() const;
void setToolTemplate(const QString &t);
QString systemPrompt() const;
void setSystemPrompt(const QString &p);
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;
@ -219,7 +220,7 @@ private:
int m_repeatPenaltyTokens = 64;
QString m_promptTemplate = "### Human:\n%1\n\n### Assistant:\n";
QString m_toolTemplate = "";
QString m_systemPrompt = "### System:\nYou are an AI assistant who gives a quality response to whatever humans ask of you.\n\n";
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.";
friend class MySettings;

View File

@ -195,7 +195,7 @@ void MySettings::restoreModelDefaults(const ModelInfo &info)
setModelRepeatPenaltyTokens(info, info.m_repeatPenaltyTokens);
setModelPromptTemplate(info, info.m_promptTemplate);
setModelToolTemplate(info, info.m_toolTemplate);
setModelSystemPrompt(info, info.m_systemPrompt);
setModelSystemPromptTemplate(info, info.m_systemPromptTemplate);
setModelChatNamePrompt(info, info.m_chatNamePrompt);
setModelSuggestedFollowUpPrompt(info, info.m_suggestedFollowUpPrompt);
}
@ -298,7 +298,7 @@ double MySettings::modelRepeatPenalty (const ModelInfo &info) const
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::modelToolTemplate (const ModelInfo &info) const { return getModelSetting("toolTemplate", info).toString(); }
QString MySettings::modelSystemPrompt (const ModelInfo &info) const { return getModelSetting("systemPrompt", info).toString(); }
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(); }
@ -412,7 +412,7 @@ void MySettings::setModelToolTemplate(const ModelInfo &info, const QString &valu
setModelSetting("toolTemplate", info, value, force, true);
}
void MySettings::setModelSystemPrompt(const ModelInfo &info, const QString &value, bool force)
void MySettings::setModelSystemPromptTemplate(const ModelInfo &info, const QString &value, bool force)
{
setModelSetting("systemPrompt", info, value, force, true);
}

View File

@ -128,8 +128,8 @@ public:
Q_INVOKABLE void setModelPromptTemplate(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);
QString modelSystemPrompt(const ModelInfo &info) const;
Q_INVOKABLE void setModelSystemPrompt(const ModelInfo &info, const QString &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;

View File

@ -1 +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;
}

View File

@ -3,6 +3,7 @@
#include <QObject>
#include <QJsonObject>
#include <jinja2cpp/value.h>
using namespace Qt::Literals::StringLiterals;
@ -18,15 +19,16 @@ namespace ToolEnums {
class Tool : public QObject {
Q_OBJECT
Q_PROPERTY(QString name MEMBER name)
Q_PROPERTY(QString description MEMBER description)
Q_PROPERTY(QString function MEMBER function)
Q_PROPERTY(QJsonObject paramSchema MEMBER paramSchema)
Q_PROPERTY(QUrl url MEMBER url)
Q_PROPERTY(bool isEnabled MEMBER isEnabled)
Q_PROPERTY(bool isBuiltin MEMBER isBuiltin)
Q_PROPERTY(bool forceUsage MEMBER forceUsage)
Q_PROPERTY(bool excerpts MEMBER excerpts)
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString description READ description CONSTANT)
Q_PROPERTY(QString function READ function 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 isEnabled READ isEnabled NOTIFY isEnabledChanged)
Q_PROPERTY(bool isBuiltin READ isBuiltin CONSTANT)
Q_PROPERTY(bool forceUsage READ forceUsage NOTIFY forceUsageChanged)
Q_PROPERTY(bool excerpts READ excerpts CONSTANT)
public:
Tool() : QObject(nullptr) {}
@ -36,28 +38,54 @@ public:
virtual ToolEnums::Error error() const { return ToolEnums::Error::NoError; }
virtual QString errorString() const { return QString(); }
QString name; // [Required] Human readable name of the tool.
QString description; // [Required] Human readable description of the tool.
QString function; // [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.
QJsonObject paramSchema; // [Optional] Json schema describing the tool's parameters. An empty object specifies no parameters.
// https://json-schema.org/understanding-json-schema/
QUrl url; // [Optional] The local file or remote resource use to invoke the tool.
bool isEnabled = false; // [Optional] Whether the tool is currently enabled
bool isBuiltin = false; // [Optional] Whether the tool is built-in
bool forceUsage = false; // [Optional] Whether we should attempt to force usage of the tool rather than let the LLM decide. NOTE: Not always possible.
bool excerpts = false; // [Optional] Whether json result produces source excerpts.
// [Required] Human readable name of the tool.
virtual QString name() const = 0;
// FIXME: Should we go with essentially the OpenAI/ollama consensus for these tool
// info files? If you install a tool in GPT4All should it need to meet the spec for these:
// [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;
// [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 currently enabled
virtual bool isEnabled() const { return false; }
// [Optional] Whether the tool is built-in
virtual bool isBuiltin() const { return false; }
// [Optional] Whether we should attempt to force usage of the tool rather than let the LLM decide. NOTE: Not always possible.
virtual bool forceUsage() const { return false; }
// [Optional] Whether json result produces source excerpts.
virtual bool excerpts() const { return false; }
bool operator==(const Tool &other) const {
return function == other.function;
return function() == other.function();
}
bool operator!=(const Tool &other) const {
return !(*this == other);
}
jinja2::Value jinjaValue() const;
Q_SIGNALS:
void isEnabledChanged();
void forceUsageChanged();
};
#endif // TOOL_H

View File

@ -20,79 +20,12 @@ ToolModel::ToolModel()
QCoreApplication::instance()->installEventFilter(this);
Tool* localDocsSearch = new LocalDocsSearch;
localDocsSearch->name = tr("LocalDocs search");
localDocsSearch->description = tr("Search the local docs");
localDocsSearch->function = "localdocs_search";
localDocsSearch->isBuiltin = true;
localDocsSearch->excerpts = true;
localDocsSearch->forceUsage = true; // FIXME: persistent setting
localDocsSearch->isEnabled = true; // FIXME: persistent setting
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
}
})";
QJsonDocument localJsonDoc = QJsonDocument::fromJson(localParamSchema.toUtf8());
Q_ASSERT(!localJsonDoc.isNull() && localJsonDoc.isObject());
localDocsSearch->paramSchema = localJsonDoc.object();
m_tools.append(localDocsSearch);
m_toolMap.insert(localDocsSearch->function, localDocsSearch);
m_toolMap.insert(localDocsSearch->function(), localDocsSearch);
Tool *braveSearch = new BraveSearch;
braveSearch->name = tr("Brave web search");
braveSearch->description = tr("Search the web using brave.com");
braveSearch->function = "brave_search";
braveSearch->isBuiltin = true;
braveSearch->excerpts = true;
braveSearch->forceUsage = false; // FIXME: persistent setting
braveSearch->isEnabled = false; // FIXME: persistent setting
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
}
})";
QJsonDocument braveJsonDoc = QJsonDocument::fromJson(braveParamSchema.toUtf8());
Q_ASSERT(!braveJsonDoc.isNull() && braveJsonDoc.isObject());
braveSearch->paramSchema = braveJsonDoc.object();
m_tools.append(braveSearch);
m_toolMap.insert(braveSearch->function, braveSearch);
m_toolMap.insert(braveSearch->function(), braveSearch);
}
bool ToolModel::eventFilter(QObject *obj, QEvent *ev)

View File

@ -41,23 +41,23 @@ public:
const Tool *item = m_tools.at(index.row());
switch (role) {
case NameRole:
return item->name;
return item->name();
case DescriptionRole:
return item->description;
return item->description();
case FunctionRole:
return item->function;
return item->function();
case ParametersRole:
return item->paramSchema;
return item->paramSchema();
case UrlRole:
return item->url;
return item->url();
case IsEnabledRole:
return item->isEnabled;
return item->isEnabled();
case IsBuiltinRole:
return item->isBuiltin;
return item->isBuiltin();
case ForceUsageRole:
return item->forceUsage;
return item->forceUsage();
case ExcerptsRole:
return item->excerpts;
return item->excerpts();
}
return QVariant();