From cedba6cd10042bcdbc749501e8dbe066e137d3a5 Mon Sep 17 00:00:00 2001 From: Adam Treat Date: Thu, 8 Aug 2024 10:37:53 -0400 Subject: [PATCH] Tool model. Signed-off-by: Adam Treat --- gpt4all-chat/CMakeLists.txt | 2 +- gpt4all-chat/chatlistmodel.cpp | 2 +- gpt4all-chat/qml/ToolSettings.qml | 2 +- gpt4all-chat/tool.h | 82 +++++++++------------- gpt4all-chat/toolmodel.cpp | 103 +++++++++++++++++++++++++++ gpt4all-chat/toolmodel.h | 112 ++++++++++++++++++++++++++++++ 6 files changed, 252 insertions(+), 51 deletions(-) create mode 100644 gpt4all-chat/toolmodel.cpp create mode 100644 gpt4all-chat/toolmodel.h diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt index 1ab4db20..08030af6 100644 --- a/gpt4all-chat/CMakeLists.txt +++ b/gpt4all-chat/CMakeLists.txt @@ -124,7 +124,7 @@ qt_add_executable(chat sourceexcerpt.h sourceexcerpt.cpp server.h server.cpp logger.h logger.cpp - tool.h tool.cpp + tool.h tool.cpp toolmodel.h toolmodel.cpp ${APP_ICON_RESOURCE} ${CHAT_EXE_RESOURCES} ) diff --git a/gpt4all-chat/chatlistmodel.cpp b/gpt4all-chat/chatlistmodel.cpp index c5be4338..d5547e3b 100644 --- a/gpt4all-chat/chatlistmodel.cpp +++ b/gpt4all-chat/chatlistmodel.cpp @@ -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) diff --git a/gpt4all-chat/qml/ToolSettings.qml b/gpt4all-chat/qml/ToolSettings.qml index 2fc1cd32..f9b5e727 100644 --- a/gpt4all-chat/qml/ToolSettings.qml +++ b/gpt4all-chat/qml/ToolSettings.qml @@ -67,5 +67,5 @@ MySettingsTab { height: 1 color: theme.settingsDivider } - } + } } diff --git a/gpt4all-chat/tool.h b/gpt4all-chat/tool.h index aa8ea3b2..7598af68 100644 --- a/gpt4all-chat/tool.h +++ b/gpt4all-chat/tool.h @@ -8,63 +8,26 @@ using namespace Qt::Literals::StringLiterals; namespace ToolEnums { Q_NAMESPACE - enum class ConnectionType { - BuiltinConnection = 0, // A built-in tool with bespoke connection type - LocalConnection = 1, // Starts a local process and communicates via stdin/stdout/stderr - LocalServerConnection = 2, // Connects to an existing local process and communicates via stdin/stdout/stderr - RemoteConnection = 3, // Starts a remote process and communicates via some networking protocol TBD - RemoteServerConnection = 4 // Connects to an existing remote process and communicates via some networking protocol TBD - }; - Q_ENUM_NS(ConnectionType) - enum class Error { NoError = 0, TimeoutError = 2, UnknownError = 499, }; + Q_ENUM_NS(Error) } -struct ToolInfo { - Q_GADGET - Q_PROPERTY(QString name MEMBER name) - Q_PROPERTY(QString description MEMBER description) - Q_PROPERTY(QJsonObject parameters MEMBER parameters) - Q_PROPERTY(bool isEnabled MEMBER isEnabled) - Q_PROPERTY(ToolEnums::ConnectionType connectionType MEMBER connectionType) - -public: - QString name; - QString description; - QJsonObject parameters; - bool isEnabled; - ToolEnums::ConnectionType connectionType; - - // 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: - // 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 - QJsonObject toJson() const - { - QJsonObject result; - result.insert("name", name); - result.insert("description", description); - result.insert("parameters", parameters); - return result; - } - - static ToolInfo fromJson(const QString &json); - - bool operator==(const ToolInfo &other) const { - return name == other.name; - } - bool operator!=(const ToolInfo &other) const { - return !(*this == other); - } -}; -Q_DECLARE_METATYPE(ToolInfo) - 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) + public: Tool() : QObject(nullptr) {} virtual ~Tool() {} @@ -72,6 +35,29 @@ public: virtual QString run(const QJsonObject ¶meters, qint64 timeout = 2000) = 0; 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. + + // 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: + // 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 + + bool operator==(const Tool &other) const { + return function == other.function; + } + bool operator!=(const Tool &other) const { + return !(*this == other); + } }; #endif // TOOL_H diff --git a/gpt4all-chat/toolmodel.cpp b/gpt4all-chat/toolmodel.cpp new file mode 100644 index 00000000..4a6d61c3 --- /dev/null +++ b/gpt4all-chat/toolmodel.cpp @@ -0,0 +1,103 @@ +#include "toolmodel.h" + +#include +#include +#include + +#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; + 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); + + 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); +} + +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; +} diff --git a/gpt4all-chat/toolmodel.h b/gpt4all-chat/toolmodel.h new file mode 100644 index 00000000..3da55793 --- /dev/null +++ b/gpt4all-chat/toolmodel.h @@ -0,0 +1,112 @@ +#ifndef TOOLMODEL_H +#define TOOLMODEL_H + +#include "tool.h" + +#include + +class ToolModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + static ToolModel *globalInstance(); + + enum Roles { + NameRole = Qt::UserRole + 1, + DescriptionRole, + FunctionRole, + ParametersRole, + UrlRole, + ApiKeyRole, + KeyRequiredRole, + IsEnabledRole, + IsBuiltinRole, + ForceUsageRole, + 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 ParametersRole: + return item->paramSchema; + case UrlRole: + return item->url; + case IsEnabledRole: + return item->isEnabled; + case IsBuiltinRole: + return item->isBuiltin; + case ForceUsageRole: + return item->forceUsage; + case ExcerptsRole: + return item->excerpts; + } + + return QVariant(); + } + + QHash roleNames() const override + { + QHash roles; + roles[NameRole] = "name"; + roles[DescriptionRole] = "description"; + roles[FunctionRole] = "function"; + roles[ParametersRole] = "parameters"; + roles[UrlRole] = "url"; + roles[ApiKeyRole] = "apiKey"; + roles[KeyRequiredRole] = "keyRequired"; + roles[IsEnabledRole] = "isEnabled"; + roles[IsBuiltinRole] = "isBuiltin"; + roles[ForceUsageRole] = "forceUsage"; + 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(); } + +Q_SIGNALS: + void countChanged(); + void valueChanged(int index, const QString &value); + +protected: + bool eventFilter(QObject *obj, QEvent *ev) override; + +private: + explicit ToolModel(); + ~ToolModel() {} + friend class MyToolModel; + QList m_tools; + QHash m_toolMap; +}; + +#endif // TOOLMODEL_H