From c1d8c582f9b683e14d43ff543398fe39d3fb825a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=A7=CE=B3=CF=86=CF=84=20Kompanion?= Date: Mon, 20 Oct 2025 00:17:33 +0200 Subject: [PATCH] Move HandlerMemory up again. -- --- AGENTS.md | 3 ++ src/cli/.KompanionApp.cpp.swp | Bin 16384 -> 0 bytes src/mcp/HandlersMemory.hpp | 95 +++++++++++++++++----------------- tests/CMakeLists.txt | 4 ++ 4 files changed, 55 insertions(+), 47 deletions(-) delete mode 100644 src/cli/.KompanionApp.cpp.swp diff --git a/AGENTS.md b/AGENTS.md index 09b6f99..5b1303e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,9 @@ # Repository Guidelines This guide supports new agents contributing to `metal-kompanion`, the MCP backend for Kompanion. Follow these practices to keep the service buildable, testable, and easy to review. + +source dev.env for envrionment variables. + ## MCP Usage - This project uses agentic-control-framework Use this for task planning diff --git a/src/cli/.KompanionApp.cpp.swp b/src/cli/.KompanionApp.cpp.swp deleted file mode 100644 index 69bea14c218038bdd36489bb503ea0cc882d7b5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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 F`X^_9l*Ir5 diff --git a/src/mcp/HandlersMemory.hpp b/src/mcp/HandlersMemory.hpp index 8ff7bea..35a7fb1 100644 --- a/src/mcp/HandlersMemory.hpp +++ b/src/mcp/HandlersMemory.hpp @@ -594,6 +594,53 @@ inline std::string save_context(const std::string& reqJson) { return os.str(); } +/** + * upsert_and_embed + * Request: { namespace, model?, items: [{id?, text, tags?, metadata?}] } + * Response: { upserted, embedded } + */ +inline std::string upsert_and_embed(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"); + auto nsRow = detail::database().ensureNamespace(nsName); + if (!nsRow) return detail::error_response("internal_error","failed to ensure namespace"); + + auto items = detail::parse_items(reqJson); + if (items.empty()) return detail::error_response("bad_request","items array must contain at least one entry"); + std::string model = detail::extract_string_field(reqJson, "model"); + + // Upsert items first and collect texts/ids + std::vector itemIds; itemIds.reserve(items.size()); + std::vector texts; texts.reserve(items.size()); + for (auto &it : items) { + ki::ItemRow row; row.id = it.id; row.namespace_id = nsRow->id; row.text = it.text; + row.tags = it.tags; row.revision = 1; row.metadata_json = it.metadataJson.empty()?"{}":it.metadataJson; row.content_json = it.rawJson; + const std::string id = detail::database().upsertItem(row); + itemIds.push_back(id); texts.push_back(it.text); + } + + // Embed via libKI + KI::KIClient client; KI::OllamaProvider provider; client.setProvider(&provider); + KI::KIEmbedOptions opts; if (!model.empty()) opts.model = QString::fromStdString(model); + QStringList qtexts; for (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(); + + // Upsert chunks + embeddings (ord=0) + int embedded = 0; + const int n = std::min(itemIds.size(), (std::size_t)result.vectors.size()); + for (int i = 0; i < n; ++i) { + ki::ChunkRow chunk; chunk.item_id = itemIds[(size_t)i]; chunk.ord = 0; chunk.text = texts[(size_t)i]; + auto chunkIds = detail::database().upsertChunks(std::vector{chunk}); if (chunkIds.empty()) continue; + ki::EmbeddingRow emb; emb.chunk_id = chunkIds.front(); emb.model = result.model.toStdString(); emb.dim = result.vectors[i].size(); + emb.vector.assign(result.vectors[i].begin(), result.vectors[i].end()); + detail::database().upsertEmbeddings(std::vector{emb}); embedded++; + } + + std::ostringstream os; os << "{\"upserted\":" << itemIds.size() << ",\"embedded\":" << embedded << "}"; return os.str(); +} + /** * recall_context * Request: { "namespace": string, "key?": string, "tags?": string[], "limit?": int, "since?": iso8601 } @@ -771,7 +818,7 @@ inline std::string warm_cache(const std::string& reqJson) { watcher.setFuture(fut); loop.exec(); const KI::KIEmbeddingResult result = watcher.result(); // Persist - int persisted = 0; const int n = std::min(result.vectors.size(), (int)toEmbed.size()); + int persisted = 0; const int n = std::min((size_t)result.vectors.size(), toEmbed.size()); for (int i = 0; i < n; ++i) { const auto &pair = toEmbed[(size_t)i]; ki::ChunkRow chunk; chunk.item_id = pair.first; chunk.ord = 0; chunk.text = pair.second; @@ -798,49 +845,3 @@ inline std::string delete_context(const std::string& reqJson) { } } // namespace Handlers -/** - * upsert_and_embed - * Request: { namespace, model?, items: [{id?, text, tags?, metadata?}] } - * Response: { upserted, embedded } - */ -inline std::string upsert_and_embed(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"); - auto nsRow = detail::database().ensureNamespace(nsName); - if (!nsRow) return detail::error_response("internal_error","failed to ensure namespace"); - - auto items = detail::parse_items(reqJson); - if (items.empty()) return detail::error_response("bad_request","items array must contain at least one entry"); - std::string model = detail::extract_string_field(reqJson, "model"); - - // Upsert items first and collect texts/ids - std::vector itemIds; itemIds.reserve(items.size()); - std::vector texts; texts.reserve(items.size()); - for (auto &it : items) { - ki::ItemRow row; row.id = it.id; row.namespace_id = nsRow->id; row.text = it.text; - row.tags = it.tags; row.revision = 1; row.metadata_json = it.metadataJson.empty()?"{}":it.metadataJson; row.content_json = it.rawJson; - const std::string id = detail::database().upsertItem(row); - itemIds.push_back(id); texts.push_back(it.text); - } - - // Embed via libKI - KI::KIClient client; KI::OllamaProvider provider; client.setProvider(&provider); - KI::KIEmbedOptions opts; if (!model.empty()) opts.model = QString::fromStdString(model); - QStringList qtexts; for (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(); - - // Upsert chunks + embeddings (ord=0) - int embedded = 0; - const int n = std::min((int)itemIds.size(), result.vectors.size()); - for (int i = 0; i < n; ++i) { - ki::ChunkRow chunk; chunk.item_id = itemIds[(size_t)i]; chunk.ord = 0; chunk.text = texts[(size_t)i]; - auto chunkIds = detail::database().upsertChunks(std::vector{chunk}); if (chunkIds.empty()) continue; - ki::EmbeddingRow emb; emb.chunk_id = chunkIds.front(); emb.model = result.model.toStdString(); emb.dim = result.vectors[i].size(); - emb.vector.assign(result.vectors[i].begin(), result.vectors[i].end()); - detail::database().upsertEmbeddings(std::vector{emb}); embedded++; - } - - std::ostringstream os; os << "{\"upserted\":" << itemIds.size() << ",\"embedded\":" << embedded << "}"; return os.str(); -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d0b1884..a644bec 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,3 +17,7 @@ qt_add_executable(test_snapshot ) target_link_libraries(test_snapshot PRIVATE Qt6::Core Qt6::Network Qt6::Test kompanion_mw) add_test(NAME test_snapshot COMMAND test_snapshot) + +add_test(NAME cli_smoke + COMMAND sh ${CMAKE_SOURCE_DIR}/tests/cli_smoke.sh $) +set_tests_properties(cli_smoke PROPERTIES ENVIRONMENT "KOMPANION_SKIP_CLI_SMOKE=1")