From 6ff51da0e2caca790d6bed4b0e991d62eb002fd8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=A7=CE=B3=CF=86=CF=84=20Kompanion?=
 <chigammaphitau@chelydra.at>
Date: Sun, 19 Oct 2025 10:23:38 +0200
Subject: [PATCH] mcp: document handlers and route embed_text via libKI
 (OllamaProvider) with synchronous QFuture wait; ki: respect OLLAMA_BASE for
 tags/generate/embeddings

---
 src/KI/Provider/OllamaProvider.cpp | 12 +++--
 src/mcp/HandlersMemory.hpp         | 81 +++++++++++++++++++++++++++++-
 2 files changed, 88 insertions(+), 5 deletions(-)

diff --git a/src/KI/Provider/OllamaProvider.cpp b/src/KI/Provider/OllamaProvider.cpp
index ce0ebfd..5fe0f10 100644
--- a/src/KI/Provider/OllamaProvider.cpp
+++ b/src/KI/Provider/OllamaProvider.cpp
@@ -33,9 +33,15 @@ KICapabilities* OllamaProvider::caps() const
     return m_caps;
 }
 
+static QString ollamaBaseUrl() {
+    const QByteArray env = qgetenv("OLLAMA_BASE");
+    if (!env.isEmpty()) return QString::fromLocal8Bit(env);
+    return QStringLiteral("http://localhost:11434");
+}
+
 void OllamaProvider::reload()
 {
-    QNetworkRequest req{QUrl(QStringLiteral("http://localhost:11434/api/tags"))};
+    QNetworkRequest req{QUrl(ollamaBaseUrl() + QStringLiteral("/api/tags"))};
     req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
     auto rep = m_manager->get(req);
     connect(rep, &QNetworkReply::finished, this, [this, rep] {
@@ -56,7 +62,7 @@ void OllamaProvider::reload()
 
 QFuture<KIReply*> OllamaProvider::chat(const KIThread& thread, const KIChatOptions& opts)
 {
-    QNetworkRequest req{QUrl(QStringLiteral("http://localhost:11434/api/generate"))};
+QNetworkRequest req{QUrl(ollamaBaseUrl() + QStringLiteral("/api/generate"))};
     req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
 
     QJsonObject data;
@@ -118,7 +124,7 @@ QFuture<KIEmbeddingResult> OllamaProvider::embed(const QStringList& texts, const
     acc->vectors.resize(texts.size());
     acc->remaining = texts.size();
 
-    const QUrl url(QStringLiteral("http://localhost:11434/api/embeddings"));
+    const QUrl url(ollamaBaseUrl() + QStringLiteral("/api/embeddings"));
     for (int i = 0; i < texts.size(); ++i) {
         QNetworkRequest req{url};
         req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
diff --git a/src/mcp/HandlersMemory.hpp b/src/mcp/HandlersMemory.hpp
index 01005d2..1f42ffa 100644
--- a/src/mcp/HandlersMemory.hpp
+++ b/src/mcp/HandlersMemory.hpp
@@ -15,8 +15,20 @@
 #include <vector>
 
 #include "PgDal.hpp"
+// libKI (central embedding + model provider)
+#include "Client/KIClient.h"
+#include "Provider/OllamaProvider.h"
+#include "Embedding/KIEmbedding.h"
+#include <QEventLoop>
+#include <QFutureWatcher>
 
 namespace Handlers {
+
+/**
+ * upsert_memory
+ * Request: { "namespace": string, "items": [ { "id?": string, "text": string, "tags?": string[], "embedding?": number[] } ] }
+ * Response: { "upserted": int, "ids?": string[], "status": "ok" }
+ */
 namespace detail {
 
 inline ki::PgDal& database() {
@@ -443,6 +455,11 @@ inline std::string upsert_memory(const std::string& reqJson) {
     return os.str();
 }
 
+/**
+ * search_memory
+ * Request: { "namespace": string, "query": { "text?": string, "embedding?": number[], "k?": int } }
+ * Response: { "matches": [ { "id": string, "score": number, "text?": string } ] }
+ */
 inline std::string search_memory(const std::string& reqJson) {
     const std::string nsName = detail::extract_string_field(reqJson, "namespace");
     if (nsName.empty()) {
@@ -500,6 +517,11 @@ inline std::string search_memory(const std::string& reqJson) {
     return detail::serialize_matches(matches);
 }
 
+/**
+ * save_context
+ * Request: { "namespace": string, "key?": string, "content": any, "tags?": string[], "ttl_seconds?": int }
+ * Response: { "id": string, "created_at": iso8601 }
+ */
 inline std::string save_context(const std::string& reqJson) {
     const std::string nsName = detail::extract_string_field(reqJson, "namespace");
     if (nsName.empty()) {
@@ -570,6 +592,11 @@ inline std::string save_context(const std::string& reqJson) {
     return os.str();
 }
 
+/**
+ * recall_context
+ * Request: { "namespace": string, "key?": string, "tags?": string[], "limit?": int, "since?": iso8601 }
+ * Response: { "items": [ { "id": string, "key?": string, "content": any, "tags": string[], "created_at": iso8601 } ] }
+ */
 inline std::string recall_context(const std::string& reqJson) {
     const std::string nsName = detail::extract_string_field(reqJson, "namespace");
     if (nsName.empty()) {
@@ -634,15 +661,65 @@ inline std::string recall_context(const std::string& reqJson) {
     return os.str();
 }
 
+/**
+ * embed_text
+ * Request: { "namespace": string, "model?": string, "texts": string[] }
+ * Response: { "model": string, "vectors": number[][] }
+ *
+ * Implementation: delegates to libKI (OllamaProvider) for local embeddings.
+ */
 inline std::string embed_text(const std::string& reqJson) {
     const std::string nsName = detail::extract_string_field(reqJson, "namespace");
     if (nsName.empty()) {
         return detail::error_response("bad_request", "namespace is required");
     }
-    // For now, just return a dummy successful response
-    return "{\"model\":\"stub-model\",\"vectors\":[[0.1,0.2,0.3]]}";
+
+    // Parse inputs
+    std::string model = detail::extract_string_field(reqJson, "model");
+    auto texts = detail::parse_string_array(reqJson, "texts");
+    if (texts.empty()) {
+        return detail::error_response("bad_request", "texts must contain at least one string");
+    }
+
+    // libKI: synchronous wait on QFuture
+    KI::KIClient client;
+    KI::OllamaProvider provider;
+    client.setProvider(&provider);
+    KI::KIEmbedOptions opts; if (!model.empty()) opts.model = QString::fromStdString(model);
+
+    QStringList qtexts; qtexts.reserve(static_cast<int>(texts.size()));
+    for (const auto &t : texts) qtexts.push_back(QString::fromStdString(t));
+
+    QEventLoop loop;
+    QFuture<KI::KIEmbeddingResult> fut = client.embed(qtexts, opts);
+    QFutureWatcher<KI::KIEmbeddingResult> watcher;
+    QObject::connect(&watcher, &QFutureWatcher<KI::KIEmbeddingResult>::finished, &loop, &QEventLoop::quit);
+    watcher.setFuture(fut);
+    loop.exec();
+    const KI::KIEmbeddingResult result = watcher.result();
+
+    // Serialize
+    std::ostringstream os;
+    os << "{\"model\":\"" << detail::json_escape(result.model.toStdString()) << "\",\"vectors\":[";
+    for (int i = 0; i < result.vectors.size(); ++i) {
+        if (i) os << ',';
+        os << '[';
+        const auto &vec = result.vectors[i];
+        for (int j = 0; j < vec.size(); ++j) {
+            if (j) os << ',';
+            os.setf(std::ios::fixed); os << std::setprecision(6) << vec[j];
+        }
+        os << ']';
+    }
+    os << "]}";
+    return os.str();
 }
 
+/**
+ * warm_cache (stub)
+ * Request: { "namespace": string }
+ * Response: { "queued": int }
+ */
 inline std::string warm_cache(const std::string& reqJson) {
     const std::string nsName = detail::extract_string_field(reqJson, "namespace");
     if (nsName.empty()) {
