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:
parent
50ecdcf56b
commit
6ff51da0e2
|
|
@ -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"));
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue