mcp: document handlers and route embed_text via libKI (OllamaProvider) with synchronous QFuture wait; ki: respect OLLAMA_BASE for tags/generate/embeddings

This commit is contained in:
Χγφτ Kompanion 2025-10-19 10:23:38 +02:00
parent 50ecdcf56b
commit 6ff51da0e2
2 changed files with 88 additions and 5 deletions

View File

@ -33,9 +33,15 @@ KICapabilities* OllamaProvider::caps() const
return m_caps; 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() 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")); req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
auto rep = m_manager->get(req); auto rep = m_manager->get(req);
connect(rep, &QNetworkReply::finished, this, [this, rep] { connect(rep, &QNetworkReply::finished, this, [this, rep] {
@ -56,7 +62,7 @@ void OllamaProvider::reload()
QFuture<KIReply*> OllamaProvider::chat(const KIThread& thread, const KIChatOptions& opts) 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")); req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
QJsonObject data; QJsonObject data;
@ -118,7 +124,7 @@ QFuture<KIEmbeddingResult> OllamaProvider::embed(const QStringList& texts, const
acc->vectors.resize(texts.size()); acc->vectors.resize(texts.size());
acc->remaining = 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) { for (int i = 0; i < texts.size(); ++i) {
QNetworkRequest req{url}; QNetworkRequest req{url};
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));

View File

@ -15,8 +15,20 @@
#include <vector> #include <vector>
#include "PgDal.hpp" #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 { 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 { namespace detail {
inline ki::PgDal& database() { inline ki::PgDal& database() {
@ -443,6 +455,11 @@ inline std::string upsert_memory(const std::string& reqJson) {
return os.str(); 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) { inline std::string search_memory(const std::string& reqJson) {
const std::string nsName = detail::extract_string_field(reqJson, "namespace"); const std::string nsName = detail::extract_string_field(reqJson, "namespace");
if (nsName.empty()) { if (nsName.empty()) {
@ -500,6 +517,11 @@ inline std::string search_memory(const std::string& reqJson) {
return detail::serialize_matches(matches); 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) { inline std::string save_context(const std::string& reqJson) {
const std::string nsName = detail::extract_string_field(reqJson, "namespace"); const std::string nsName = detail::extract_string_field(reqJson, "namespace");
if (nsName.empty()) { if (nsName.empty()) {
@ -570,6 +592,11 @@ inline std::string save_context(const std::string& reqJson) {
return os.str(); 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) { inline std::string recall_context(const std::string& reqJson) {
const std::string nsName = detail::extract_string_field(reqJson, "namespace"); const std::string nsName = detail::extract_string_field(reqJson, "namespace");
if (nsName.empty()) { if (nsName.empty()) {
@ -634,15 +661,65 @@ inline std::string recall_context(const std::string& reqJson) {
return os.str(); 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) { inline std::string embed_text(const std::string& reqJson) {
const std::string nsName = detail::extract_string_field(reqJson, "namespace"); const std::string nsName = detail::extract_string_field(reqJson, "namespace");
if (nsName.empty()) { if (nsName.empty()) {
return detail::error_response("bad_request", "namespace is required"); 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) { inline std::string warm_cache(const std::string& reqJson) {
const std::string nsName = detail::extract_string_field(reqJson, "namespace"); const std::string nsName = detail::extract_string_field(reqJson, "namespace");
if (nsName.empty()) { if (nsName.empty()) {