From 06475dd113917c6eedbe5755e006ad30a39cd973 Mon Sep 17 00:00:00 2001 From: Jared Van Bortel Date: Wed, 26 Feb 2025 15:22:13 -0500 Subject: [PATCH] WIP: use Boost::json for incremental parsing and reflection --- gpt4all-backend-test/CMakeLists.txt | 2 +- gpt4all-backend-test/src/main.cpp | 4 +- gpt4all-backend/CMakeLists.txt | 3 +- gpt4all-backend/deps/CMakeLists.txt | 14 +++++++ .../include/gpt4all-backend/main.h | 18 +++++---- gpt4all-backend/src/CMakeLists.txt | 18 ++++++++- gpt4all-backend/src/json_helpers.cpp | 12 ++++++ gpt4all-backend/src/json_helpers.h | 11 +++++ gpt4all-backend/src/main.cpp | 40 ++++++++++--------- 9 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 gpt4all-backend/deps/CMakeLists.txt create mode 100644 gpt4all-backend/src/json_helpers.cpp create mode 100644 gpt4all-backend/src/json_helpers.h diff --git a/gpt4all-backend-test/CMakeLists.txt b/gpt4all-backend-test/CMakeLists.txt index 25d32eec..2647bc94 100644 --- a/gpt4all-backend-test/CMakeLists.txt +++ b/gpt4all-backend-test/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.29) +cmake_minimum_required(VERSION 3.28...3.31) project(gpt4all-backend-test VERSION 0.1 LANGUAGES CXX) set(G4A_TEST_OLLAMA_URL "http://localhost:11434/" CACHE STRING "The base URL of the Ollama server to use.") diff --git a/gpt4all-backend-test/src/main.cpp b/gpt4all-backend-test/src/main.cpp index ad1ac48b..f12fa0f9 100644 --- a/gpt4all-backend-test/src/main.cpp +++ b/gpt4all-backend-test/src/main.cpp @@ -23,9 +23,9 @@ static void run() LLMProvider provider(OLLAMA_URL); auto version = QCoro::waitFor(provider.getVersion()); if (version) { - fmt::print("Server version: {}\n", *version); + fmt::print("Server version: {}\n", version->version); } else { - fmt::print("Network error: {}\n", version.error().errorString); + fmt::print("Error retrieving version: {}\n", version.error().errorString); return QCoreApplication::exit(1); } QCoreApplication::exit(0); diff --git a/gpt4all-backend/CMakeLists.txt b/gpt4all-backend/CMakeLists.txt index e2fe3b8c..d5b9812b 100644 --- a/gpt4all-backend/CMakeLists.txt +++ b/gpt4all-backend/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.28) +cmake_minimum_required(VERSION 3.28...3.31) project(gpt4all-backend VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) # make sure fmt is compiled with the same C++ version as us @@ -7,6 +7,7 @@ include(../common/common.cmake) find_package(Qt6 6.8 COMPONENTS Concurrent Core Network REQUIRED) add_subdirectory(../deps common_deps) +add_subdirectory(deps) add_subdirectory(src) target_sources(gpt4all-backend PUBLIC diff --git a/gpt4all-backend/deps/CMakeLists.txt b/gpt4all-backend/deps/CMakeLists.txt new file mode 100644 index 00000000..382e83e3 --- /dev/null +++ b/gpt4all-backend/deps/CMakeLists.txt @@ -0,0 +1,14 @@ +include(FetchContent) + +set(BUILD_SHARED_LIBS OFF) + +# suppress warnings during boost build +add_compile_definitions($<$:BOOST_ALLOW_DEPRECATED_HEADERS>) + +set(GPT4ALL_BOOST_TAG 1.87.0) +FetchContent_Declare( + boost + URL "https://github.com/boostorg/boost/releases/download/boost-${GPT4ALL_BOOST_TAG}/boost-${GPT4ALL_BOOST_TAG}-cmake.tar.xz" + URL_HASH "SHA256=7da75f171837577a52bbf217e17f8ea576c7c246e4594d617bfde7fafd408be5" +) +FetchContent_MakeAvailable(boost) diff --git a/gpt4all-backend/include/gpt4all-backend/main.h b/gpt4all-backend/include/gpt4all-backend/main.h index cd2137a3..fcd62cc1 100644 --- a/gpt4all-backend/include/gpt4all-backend/main.h +++ b/gpt4all-backend/include/gpt4all-backend/main.h @@ -1,6 +1,7 @@ #pragma once #include // IWYU pragma: keep +#include #include #include @@ -19,7 +20,7 @@ struct ResponseError { private: using ErrorCode = std::variant< QNetworkReply::NetworkError, - QJsonParseError::ParseError + std::exception_ptr >; public: @@ -33,17 +34,19 @@ public: assert(reply->error()); } - ResponseError(const QJsonParseError &err) - : error(err.error) - , errorString(err.errorString()) + ResponseError(const std::exception &e, std::exception_ptr err) + : error(std::move(err)) + , errorString(e.what()) { - assert(err.error); + assert(std::get(error)); } }; template using DataOrRespErr = std::expected; +struct VersionResponse { QString version; }; +BOOST_DESCRIBE_STRUCT(VersionResponse, (), (version)) class LLMProvider { public: @@ -55,10 +58,11 @@ public: void getBaseUrl(QUrl value) { m_baseUrl = std::move(value); } /// Retrieve the Ollama version, e.g. "0.5.1" - auto getVersion() const -> QCoro::Task>; + auto getVersion() -> QCoro::Task>; private: - QUrl m_baseUrl; + QUrl m_baseUrl; + QNetworkAccessManager m_nam; }; } // namespace gpt4all::backend diff --git a/gpt4all-backend/src/CMakeLists.txt b/gpt4all-backend/src/CMakeLists.txt index 22e2bce2..b83f306d 100644 --- a/gpt4all-backend/src/CMakeLists.txt +++ b/gpt4all-backend/src/CMakeLists.txt @@ -1,13 +1,27 @@ set(TARGET gpt4all-backend) add_library(${TARGET} STATIC + json_helpers.cpp main.cpp ) target_compile_features(${TARGET} PUBLIC cxx_std_23) gpt4all_add_warning_options(${TARGET}) +target_include_directories(${TARGET} PRIVATE + . + ../include/gpt4all-backend +) target_link_libraries(${TARGET} PUBLIC - QCoro6::Coro Qt6::Core Qt6::Network + Boost::describe + QCoro6::Coro + Qt6::Core + Qt6::Network ) target_link_libraries(${TARGET} PRIVATE - QCoro6::Network fmt::fmt + QCoro6::Network + fmt::fmt ) + +# link Boost::json as -isystem to suppress -Wundef +get_target_property(LIB_INCLUDE_DIRS Boost::json INTERFACE_INCLUDE_DIRECTORIES) +target_include_directories(${TARGET} SYSTEM PRIVATE ${LIB_INCLUDE_DIRS}) +target_link_libraries(${TARGET} PRIVATE Boost::json) diff --git a/gpt4all-backend/src/json_helpers.cpp b/gpt4all-backend/src/json_helpers.cpp new file mode 100644 index 00000000..42d7ef89 --- /dev/null +++ b/gpt4all-backend/src/json_helpers.cpp @@ -0,0 +1,12 @@ +#include "json_helpers.h" + +#include + +#include + + +QString tag_invoke(const boost::json::value_to_tag &, const boost::json::value &value) +{ + auto &s = value.as_string(); + return QString::fromUtf8(s.data(), s.size()); +} diff --git a/gpt4all-backend/src/json_helpers.h b/gpt4all-backend/src/json_helpers.h new file mode 100644 index 00000000..5990a4cb --- /dev/null +++ b/gpt4all-backend/src/json_helpers.h @@ -0,0 +1,11 @@ +#pragma once + +class QString; +namespace boost::json { + class value; + template struct value_to_tag; +} + + +/// Allows JSON strings to be deserialized as QString. +QString tag_invoke(const boost::json::value_to_tag &, const boost::json::value &value); diff --git a/gpt4all-backend/src/main.cpp b/gpt4all-backend/src/main.cpp index 1bf3f7a2..fcfdcf82 100644 --- a/gpt4all-backend/src/main.cpp +++ b/gpt4all-backend/src/main.cpp @@ -1,43 +1,45 @@ -#include +#include "main.h" +#include "json_helpers.h" + +#include // IWYU pragma: keep #include // IWYU pragma: keep +#include #include -#include #include #include -#include -#include #include #include #include using namespace Qt::Literals::StringLiterals; +namespace json = boost::json; namespace gpt4all::backend { -auto LLMProvider::getVersion() const -> QCoro::Task> +auto LLMProvider::getVersion() -> QCoro::Task> { - QNetworkAccessManager nam; - std::unique_ptr reply(co_await nam.get(QNetworkRequest(m_baseUrl.resolved(u"/api/version"_s)))); + std::unique_ptr reply(m_nam.get(QNetworkRequest(m_baseUrl.resolved(u"/api/version"_s)))); if (reply->error()) co_return std::unexpected(reply.get()); - QJsonParseError error; - auto doc = QJsonDocument::fromJson(reply->readAll(), &error); - if (doc.isNull()) - co_return std::unexpected(error); + try { + json::parser p; + auto coroReply = qCoro(*reply); + do { + auto chunk = co_await coroReply.readAll(); + if (reply->error()) + co_return std::unexpected(reply.get()); + p.write(chunk.data(), chunk.size()); + } while (!reply->atEnd()); - assert(doc.isObject()); - auto obj = doc.object(); - - auto version = std::as_const(obj).find("version"_L1); - assert(version != obj.constEnd()); - - assert(version->isString()); - co_return version->toString(); + co_return json::value_to(p.release()); + } catch (const std::exception &e) { + co_return std::unexpected(ResponseError(e, std::current_exception())); + } } } // namespace gpt4all::backend