From a805ba105d34735531283c900e578e3d95a58675 Mon Sep 17 00:00:00 2001 From: rfprod Date: Sun, 4 Oct 2026 00:44:49 +0400 Subject: [PATCH] feat(llama): add basic ollama integration - [x] implement a program to communicate with ollama installed locally; - [x] remove redundant shell scripts; --- .gitignore | 5 ++ .vscode/setting.json | 7 ++- CMakeLists.txt | 14 ++++- README.md | 20 +++++++ conanfile.py | 2 + src/main.cpp | 46 +++++++++++----- src/ollama_client.cpp | 120 ++++++++++++++++++++++++++++++++++++++++++ src/ollama_client.hpp | 25 +++++++++ tools/shell/build.sh | 14 ++--- 9 files changed, 229 insertions(+), 24 deletions(-) create mode 100644 src/ollama_client.cpp create mode 100644 src/ollama_client.hpp diff --git a/.gitignore b/.gitignore index 7336de9..9902b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,8 @@ core # environment .env + +# ignore all files except `*.cpp` and `*.hpp` in the source directory +src/* +!src/*.cpp +!src/*.hpp diff --git a/.vscode/setting.json b/.vscode/setting.json index 3282641..7e3d281 100644 --- a/.vscode/setting.json +++ b/.vscode/setting.json @@ -30,8 +30,7 @@ "shellcheck.executablePath": "shellcheck", "shellcheck.ignorePatterns": { "**/.git/objects/**": true, - "**/.git/subtree-cache/**": true, - "**/node_modules/*/**": true + "**/.git/subtree-cache/**": true }, "shellcheck.run": "onType", "shellcheck.useWorkspaceRootAsCwd": true, @@ -46,6 +45,10 @@ "clang-format.fallbackStyle": "LLVM", "clang-format.language.cpp.enable": true, "clang-format.style": "file", + "C_Cpp.default.cppStandard": "gnu++20", + "C_Cpp.default.cStandard": "gnu23", + "C_Cpp.default.compileCommands": "${workspaceFolder}/build/Release/compile_commands.json", + "C_Cpp.intelliSenseEngine": "Tag Parser", "files.defaultLanguage": "cpp", "files.hotExit": "onExit", "yaml.format.enable": true, diff --git a/CMakeLists.txt b/CMakeLists.txt index 60af9cd..3fc6567 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,14 +6,24 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) add_executable(cpp-workspace-app src/main.cpp + src/ollama_client.cpp ) -find_package(fmt CONFIG REQUIRED) -target_link_libraries(cpp-workspace-app PRIVATE fmt::fmt) +find_package(fmt REQUIRED) +find_package(CURL REQUIRED) +find_package(nlohmann_json REQUIRED) +target_link_libraries(cpp-workspace-app + PRIVATE + fmt::fmt + CURL::libcurl + nlohmann_json::nlohmann_json +) # enable_testing() diff --git a/README.md b/README.md index 7f828f4..099a0ce 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,23 @@ or ```bash cz c ``` + +## FAQ + +### Manual build + +```bash +rm -rf ./build/* +conan install . --build=missing +cmake --preset conan-release +cmake --build --preset conan-release +``` + +or + +```bash +rm -rf ./build/* ./CMakeUserPresets.json +conan install . --build=missing +cmake --preset conan-release +cmake --build --preset conan-release +``` diff --git a/conanfile.py b/conanfile.py index 031e85b..7faefe6 100644 --- a/conanfile.py +++ b/conanfile.py @@ -9,6 +9,8 @@ class AppConan(ConanFile): def requirements(self): self.requires("fmt/11.2.0") + self.requires("libcurl/8.5.0") + self.requires("nlohmann_json/3.11.2") def layout(self): cmake_layout(self) diff --git a/src/main.cpp b/src/main.cpp index 0e3d46b..d8de3f1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,21 +1,41 @@ -#include +#include "ollama_client.hpp" #include -using std::cout; -using std::format; +int main(int argc, const char* argv[]) { + std::string model_arg = "gemma3n:latest"; + std::string prompt_arg = "Hey! What's up!"; + bool stream_arg = false; -int main() { - const char string[]{"this is a string"}; - - for (auto* p = string; *p; p++) { - cout << format("char = {}\n", *p); + for (int i = 1; i < argc; i += 1) { + std::string arg = argv[i]; + if (arg.starts_with("--model=") == 0) { + model_arg = arg.substr(8); + } else if (arg.starts_with("--prompt=") == 0) { + prompt_arg = arg.substr(9); + } else if (arg.starts_with("--stream=") == 0) { + stream_arg = arg.substr(9) == "true" ? true : false; + } } - for (int i{0}; string[i]; i++) { - cout << format("{} char = {}\n", i, string[i]); - } + printf("\n"); + printf("model_arg = %s\n", model_arg.c_str()); + printf("prompt_arg = %s\n", prompt_arg.c_str()); + printf("stream_arg = %s\n", stream_arg ? "true" : "false"); + printf("\n"); - for (const auto& el : string) { - cout << format("element = {}\n", el); + if (prompt_arg.length() == 0) { + printf("Enter your prompt: "); + getline(std::cin, prompt_arg); } + + OllamaClient client = OllamaClient(); + + struct OllamaPromptConfig config; + config.model = model_arg; + config.prompt = prompt_arg; + config.stream = stream_arg; + + client.curl_request(config); + + return 0; } diff --git a/src/ollama_client.cpp b/src/ollama_client.cpp new file mode 100644 index 0000000..d140391 --- /dev/null +++ b/src/ollama_client.cpp @@ -0,0 +1,120 @@ +#include "ollama_client.hpp" +#include +#include +#include +#include + +using nlohmann::json_abi_v3_11_2::json; + +static size_t WriteCallback(char* contents, size_t size, size_t nmemb, std::string* userp) { + userp->append(contents, size * nmemb); + return size * nmemb; +} + +// Create an instance of OllamaClient. +OllamaClient::OllamaClient() { + this->url = "http://localhost:11434/api/generate"; + this->headers = nullptr; + + this->check_ollama(); + + this->init_curl(); + + printf("\n"); +} + +// Checks whether Ollama binary exists on the file system. +void OllamaClient::check_ollama() { + if (system("which ollama > /dev/null 2>&1")) { + fprintf(stderr, "Ollama binary does not exist.\nInstallation instructions: " + "https://docs.ollama.com/linux\n"); + exit(0); + } else { + printf("[ok] Ollama binary exists.\n"); + } +}; + +// Initialize CURL. +void OllamaClient::init_curl() { + this->curl = curl_easy_init(); + if (!this->curl) { + fprintf(stderr, "Failed to initialize CURL.\n"); + exit(1); + } else { + printf("[ok] CURL initialized.\n"); + } +} + +// Process a response. +void OllamaClient::process_response(std::string response) { + try { + json response_json = json::parse(response); + std::cout << "Response: " << response_json["response"].get() << std::endl; + } catch (const json::exception& err) { + fprintf(stderr, "JSON parse error: %s\n", err.what()); + } +} + +// Process a streaming response. +void OllamaClient::process_response_stream(std::string response) { + std::istringstream stream(response); + std::string line; + + std::cout << "Response: "; + std::cout.flush(); + + while (std::getline(stream, line)) { + if (!line.empty()) { + try { + json chunk = json::parse(line); + std::string text = chunk["response"].get(); + std::cout << text; + std::cout.flush(); + + if (chunk["done"].get()) { + break; + } + } catch (const json::exception& err) { + fprintf(stderr, "JSON parse error: %s\n", err.what()); + } + } + } + + std::cout << std::endl; +} + +// Send a request to the Ollama endpoint using CURL. +void OllamaClient::curl_request(OllamaPromptConfig config) { + json request_payload = { + {"model", config.model}, {"prompt", config.prompt}, {"stream", config.stream}}; + + std::string post_data = request_payload.dump(); + + std::string response; + + curl_easy_setopt(this->curl, CURLOPT_URL, this->url.c_str()); + curl_easy_setopt(this->curl, CURLOPT_POSTFIELDS, post_data.c_str()); + curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, &response); + + this->headers = curl_slist_append(this->headers, "Content-Type: application/json"); + curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, this->headers); + + CURLcode res = curl_easy_perform(curl); + + if (res != CURLE_OK) { + fprintf(stderr, "CURL error: %s\n", curl_easy_strerror(res)); + curl_slist_free_all(this->headers); + curl_easy_cleanup(this->curl); + exit(1); + } + + if (config.stream) { + this->process_response_stream(response); + } else { + this->process_response(response); + } + + curl_slist_free_all(this->headers); + curl_easy_cleanup(this->curl); +} diff --git a/src/ollama_client.hpp b/src/ollama_client.hpp new file mode 100644 index 0000000..04f84f6 --- /dev/null +++ b/src/ollama_client.hpp @@ -0,0 +1,25 @@ +#include +#include + +struct OllamaPromptConfig { + std::string model; + std::string prompt; + bool stream; +}; + +class OllamaClient { + private: + CURL* curl; + std::string url; + struct curl_slist* headers; + + void check_ollama(); + void init_curl(); + void process_response(std::string response); + void process_response_stream(std::string response); + + public: + OllamaClient(); + + void curl_request(OllamaPromptConfig config); +}; diff --git a/tools/shell/build.sh b/tools/shell/build.sh index 050d59f..e2948eb 100644 --- a/tools/shell/build.sh +++ b/tools/shell/build.sh @@ -88,7 +88,7 @@ log_error() { # Extract version from CMakeLists.txt get_version() { - grep -oP 'project\(\s*\w+\s+VERSION\s+\K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt 2>/dev/null || echo "0.0.0" + grep -oP 'project\(\s*[\w-]+\s+VERSION\s+\K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt 2>/dev/null || echo "0.0.0" } # Main build script @@ -133,7 +133,7 @@ main() { fi log_info "Building project..." - if cmake --build "$BUILD_DIR" --config "$BUILD_TYPE"; then + if cmake --build "$BUILD_DIR/$BUILD_TYPE" --config "$BUILD_TYPE"; then log_success "Build completed" else log_error "Build failed" @@ -141,7 +141,7 @@ main() { fi log_info "Running tests..." - if ctest --test-dir "$BUILD_DIR" --output-on-failure; then + if ctest --test-dir "$BUILD_DIR/$BUILD_TYPE" --output-on-failure; then log_success "All tests passed" else log_warning "Some tests failed (continuing)" @@ -150,9 +150,9 @@ main() { local BIN_NAME BIN_NAME=cpp-workspace-app - if [ -f "$BUILD_DIR/bin/$BIN_NAME" ]; then + if [ -f "$BUILD_DIR/$BUILD_TYPE/bin/$BIN_NAME" ]; then log_info "Running executable..." - "$BUILD_DIR/bin/$BIN_NAME" + "$BUILD_DIR/$BUILD_TYPE/bin/$BIN_NAME" log_success "Executable ran successfully" else log_warning "Executable not found at $BUILD_DIR/bin/$BIN_NAME" @@ -179,8 +179,8 @@ EOF mkdir -p "$RELEASE_DIR" # Copy executable - if [ -f "$BUILD_DIR/bin/$BIN_NAME" ]; then - cp "$BUILD_DIR/bin/$BIN_NAME" "$RELEASE_DIR/" + if [ -f "$BUILD_DIR/$BUILD_TYPE/bin/$BIN_NAME" ]; then + cp "$BUILD_DIR/$BUILD_TYPE/bin/$BIN_NAME" "$RELEASE_DIR/" fi # Copy documentation