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 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 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 #include "PgDal.hpp" +// libKI (central embedding + model provider) +#include "Client/KIClient.h" +#include "Provider/OllamaProvider.h" +#include "Embedding/KIEmbedding.h" +#include +#include 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(texts.size())); + for (const auto &t : texts) qtexts.push_back(QString::fromStdString(t)); + + QEventLoop loop; + QFuture fut = client.embed(qtexts, opts); + QFutureWatcher watcher; + QObject::connect(&watcher, &QFutureWatcher::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()) {