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 69bea14..0000000 Binary files a/src/cli/.KompanionApp.cpp.swp and /dev/null differ 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")