Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Brave search tool #2737

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f4fb3df
Brave search tool calling.
manyoso Jul 25, 2024
a71db10
Change the name to announce the beta.
manyoso Jul 25, 2024
1c9911a
Fix problem with only displaying one source for tool call excerpts.
manyoso Jul 25, 2024
c78c95a
Add the extra snippets to the source excerpts.
manyoso Jul 25, 2024
dda59a9
Fix the way we're injecting the context back into the model for web s…
manyoso Jul 27, 2024
d2ee235
Change the suggestion mode to turn on for tool calls by default.
manyoso Jul 27, 2024
b0578e2
Change the name to inc the beta.
manyoso Jul 27, 2024
b9684ff
Inc again for new beta.
manyoso Jul 28, 2024
7c7558e
Stop hardcoding the tool call checking and rely upon the format advoc…
manyoso Jul 30, 2024
fffd9f3
Refactor to handle errors in tool calling better and add source comme…
manyoso Jul 30, 2024
dfe3e95
Refactor the brave search and introduce an abstraction for tool calls.
manyoso Jul 31, 2024
01f67c7
Begin converting the localdocs to a tool.
manyoso Aug 1, 2024
27b86da
Serialize the source excerpts from and to pure json
manyoso Aug 1, 2024
5fc2ff8
Use parameters which is in keeping with other standard practices.
manyoso Aug 1, 2024
244b826
Implement error handling for tool calls.
manyoso Aug 7, 2024
cedba6c
Tool model.
manyoso Aug 8, 2024
f93b764
Move the usearch submodule to third_party dir.
manyoso Aug 10, 2024
00ecbb7
Add jinja third party dependency.
manyoso Aug 10, 2024
c3cfaff
Refactor and make use of jinja templates.
manyoso Aug 9, 2024
587dd55
Get rid of the name change now that 3.2.0 has been released.
manyoso Aug 13, 2024
227dbfd
Use an enum for tool usage mode.
manyoso Aug 13, 2024
48117cd
Move the jinja processing to mysettings and validation.
manyoso Aug 13, 2024
a673087
Move to a brave search specific settings page.
manyoso Aug 13, 2024
f118720
Don't advertise brave.
manyoso Aug 14, 2024
75dbf9d
Handle the forced usage of tool calls outside of the recursive prompt…
manyoso Aug 14, 2024
991afc6
Abstract the built-in web search completely away from ChatLLM.
manyoso Aug 14, 2024
4cb9569
Breakout the ask before running which can be thought of as a security…
manyoso Aug 14, 2024
054ca43
Display the antenna by introducing notion of privacy scopes to tools.
manyoso Aug 14, 2024
3a56468
Implement all the settings for the web search.
manyoso Aug 14, 2024
4ae6acd
Force tool usage and refactor.
manyoso Aug 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 10 additions & 4 deletions gpt4all-chat/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 --
Expand Down
234 changes: 234 additions & 0 deletions gpt4all-chat/bravesearch.cpp
Original file line number Diff line number Diff line change
@@ -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();
}
67 changes: 67 additions & 0 deletions gpt4all-chat/bravesearch.h
Original file line number Diff line number Diff line change
@@ -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
Loading