diff --git a/.acf/tasks.json b/.acf/tasks.json index 21e6fc8..e02fefc 100644 --- a/.acf/tasks.json +++ b/.acf/tasks.json @@ -1,7 +1,7 @@ { "projectName": "metal-kompanion-mcp", "projectDescription": "MCP backend for Kompanion: memory/context/embedding provider over MCP, built from scratch (qtmcp-based) to persist conversation state and serve embeddings + retrieval to avoid forgetting across threads.", - "lastTaskId": 20, + "lastTaskId": 23, "tasks": [ { "id": 1, @@ -441,6 +441,169 @@ "message": "Task created with title: \"Claude Code integration rescue plan\"" } ] + }, + { + "id": 21, + "title": "DAL Phase 1: libpq/pqxx wiring + SQL calls", + "description": "Link pqxx, implement PgDal against Postgres+pgvector: connect/tx, ensureNamespace, upsertItem/Chunks/Embeddings, searchText (FTS/trgm), searchVector (<->). Provide DSN via env; add cmake find + link.", + "status": "todo", + "priority": 510, + "priorityDisplay": "P0", + "dependsOn": [], + "createdAt": "2025-10-14T00:29:55.327Z", + "updatedAt": "2025-10-14T00:29:55.327Z", + "subtasks": [ + { + "id": "21.1", + "title": "CMake: find_package(pqxx) and link; CI env var DSN", + "status": "todo", + "createdAt": "2025-10-14T00:30:00.856Z", + "updatedAt": "2025-10-14T00:30:00.857Z", + "activityLog": [ + { + "timestamp": "2025-10-14T00:30:00.856Z", + "type": "log", + "message": "Subtask created with title: \"CMake: find_package(pqxx) and link; CI env var DSN\"" + } + ] + }, + { + "id": "21.2", + "title": "PgDal: implement connect/tx + prepared statements", + "status": "todo", + "createdAt": "2025-10-14T00:30:06.138Z", + "updatedAt": "2025-10-14T00:30:06.138Z", + "activityLog": [ + { + "timestamp": "2025-10-14T00:30:06.138Z", + "type": "log", + "message": "Subtask created with title: \"PgDal: implement connect/tx + prepared statements\"" + } + ] + }, + { + "id": "21.3", + "title": "SQL: ensureNamespace, upsertItem/Chunks/Embeddings", + "status": "todo", + "createdAt": "2025-10-14T00:30:11.519Z", + "updatedAt": "2025-10-14T00:30:11.519Z", + "activityLog": [ + { + "timestamp": "2025-10-14T00:30:11.519Z", + "type": "log", + "message": "Subtask created with title: \"SQL: ensureNamespace, upsertItem/Chunks/Embeddings\"" + } + ] + }, + { + "id": "21.4", + "title": "Search: FTS/trgm + vector <-> with filters (namespace/thread/tags)", + "status": "todo", + "createdAt": "2025-10-14T00:30:17.290Z", + "updatedAt": "2025-10-14T00:30:17.290Z", + "activityLog": [ + { + "timestamp": "2025-10-14T00:30:17.290Z", + "type": "log", + "message": "Subtask created with title: \"Search: FTS/trgm + vector <-> with filters (namespace/thread/tags)\"" + } + ] + } + ], + "lastSubtaskIndex": 4, + "relatedFiles": [], + "activityLog": [ + { + "timestamp": "2025-10-14T00:29:55.327Z", + "type": "log", + "message": "Task created with title: \"DAL Phase 1: libpq/pqxx wiring + SQL calls\"" + } + ] + }, + { + "id": 22, + "title": "Handlers → DAL integration", + "description": "Wire kom.memory.v1.upsert_memory/search_memory to IDatabase. Parse JSON with a real parser, validate against schemas, enforce scope and sensitivity rules.", + "status": "todo", + "priority": 490, + "priorityDisplay": "P0", + "dependsOn": [], + "createdAt": "2025-10-14T00:30:26.285Z", + "updatedAt": "2025-10-14T00:30:26.285Z", + "subtasks": [ + { + "id": "22.1", + "title": "Replace ad-hoc JSON with parser (nlohmann/json or simdjson)", + "status": "todo", + "createdAt": "2025-10-14T00:30:33.761Z", + "updatedAt": "2025-10-14T00:30:33.761Z", + "activityLog": [ + { + "timestamp": "2025-10-14T00:30:33.761Z", + "type": "log", + "message": "Subtask created with title: \"Replace ad-hoc JSON with parser (nlohmann/json or simdjson)\"" + } + ] + }, + { + "id": "22.2", + "title": "Validate request bodies against schemas before DAL calls", + "status": "todo", + "createdAt": "2025-10-14T00:30:39.868Z", + "updatedAt": "2025-10-14T00:30:39.868Z", + "activityLog": [ + { + "timestamp": "2025-10-14T00:30:39.868Z", + "type": "log", + "message": "Subtask created with title: \"Validate request bodies against schemas before DAL calls\"" + } + ] + }, + { + "id": "22.3", + "title": "Scope & sensitivity enforcement (namespace/user + skip secret embeddings)", + "status": "todo", + "createdAt": "2025-10-14T00:30:45.261Z", + "updatedAt": "2025-10-14T00:30:45.261Z", + "activityLog": [ + { + "timestamp": "2025-10-14T00:30:45.261Z", + "type": "log", + "message": "Subtask created with title: \"Scope & sensitivity enforcement (namespace/user + skip secret embeddings)\"" + } + ] + } + ], + "lastSubtaskIndex": 3, + "relatedFiles": [], + "activityLog": [ + { + "timestamp": "2025-10-14T00:30:26.285Z", + "type": "log", + "message": "Task created with title: \"Handlers → DAL integration\"" + } + ] + }, + { + "id": 23, + "title": "Contract tests: DAL-backed tools", + "description": "Expand CTest to cover DAL-backed upsert/search and backup export/import; include error cases and schema violations; run against build-komhands.", + "status": "todo", + "priority": 511, + "priorityDisplay": "P1", + "dependsOn": [], + "createdAt": "2025-10-14T00:30:51.716Z", + "updatedAt": "2025-10-14T00:30:51.716Z", + "subtasks": [], + "lastSubtaskIndex": 0, + "relatedFiles": [], + "activityLog": [ + { + "timestamp": "2025-10-14T00:30:51.716Z", + "type": "log", + "message": "Task created with title: \"Contract tests: DAL-backed tools\"" + } + ] } ] } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 85241d9..d2d3f72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,25 +8,18 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # find_package(Qt6 COMPONENTS Core Network REQUIRED) # find_package(qtmcp REQUIRED) -find_package(PkgConfig REQUIRED) -pkg_check_modules(PQXX REQUIRED libpqxx) +add_subdirectory(src/dal) add_executable(kom_mcp src/main.cpp - src/dal/PgDal.cpp ) -# target_link_libraries(kom_mcp PRIVATE Qt6::Core Qt6::Network qtmcp) -target_include_directories(kom_mcp PRIVATE src ${PQXX_INCLUDE_DIRS}) -target_link_libraries(kom_mcp PRIVATE ${PQXX_LIBRARIES}) +target_include_directories(kom_mcp PRIVATE src) +target_link_libraries(kom_mcp PRIVATE kom_dal) install(TARGETS kom_mcp RUNTIME DESTINATION bin) -add_executable(test_mcp_tools - tests/contract/test_mcp_tools.cpp - src/dal/PgDal.cpp -) -target_include_directories(test_mcp_tools PRIVATE src ${PQXX_INCLUDE_DIRS}) -target_link_libraries(test_mcp_tools PRIVATE ${PQXX_LIBRARIES}) - -enable_testing() -add_test(NAME contract_mcp_tools COMMAND test_mcp_tools) +option(BUILD_TESTS "Build tests" ON) +if (BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/db/queries.sql b/db/queries.sql deleted file mode 100644 index 083e46a..0000000 --- a/db/queries.sql +++ /dev/null @@ -1 +0,0 @@ --- Kompanion quick checks diff --git a/db/schema_branch_embeddings.sql b/db/schema_branch_embeddings.sql deleted file mode 100644 index d4df567..0000000 --- a/db/schema_branch_embeddings.sql +++ /dev/null @@ -1 +0,0 @@ --- placeholder: see chat for full Branch Embeddings schema diff --git a/db/schema_dev.sql b/db/schema_dev.sql deleted file mode 100644 index e69de29..0000000 diff --git a/db/schema_runtime.sql b/db/schema_runtime.sql deleted file mode 100644 index e69de29..0000000 diff --git a/src/dal/CMakeLists.txt b/src/dal/CMakeLists.txt new file mode 100644 index 0000000..1bda8ce --- /dev/null +++ b/src/dal/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(kom_dal STATIC + PgDal.cpp +) + +target_compile_features(kom_dal PUBLIC cxx_std_20) +target_include_directories(kom_dal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/tasks-table.md b/tasks-table.md index 0844c78..b723622 100644 --- a/tasks-table.md +++ b/tasks-table.md @@ -3,11 +3,11 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro > ## 📈 Project Summary > -> **✅ Done**: 0 | **🔄 In Progress**: 0 | **⬜ Todo**: 19 | **❌ Blocked**: 0 +> **✅ Done**: 0 | **🔄 In Progress**: 0 | **⬜ Todo**: 29 | **❌ Blocked**: 0 > -> **Progress**: 0% `░░░░░░░░░░░░░░░░░░░░` 0/19 tasks +> **Progress**: 0% `░░░░░░░░░░░░░░░░░░░░` 0/29 tasks > -> **Priorities**: 🚨 **Critical**: 0 | 🔴 **High**: 1 | 🟡 **Medium**: 20 | 🟢 **Low**: 0 +> **Priorities**: 🚨 **Critical**: 0 | 🔴 **High**: 1 | 🟡 **Medium**: 30 | 🟢 **Low**: 0 ## Tasks @@ -33,6 +33,9 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro | #18 | ⬜ todo | 492 | **Expose Agentic-Control-Framework as a tool** | Wrap ACF endpoints into a too... | | #19 | ⬜ todo | 509 | **DAL skeleton + SQL calls (pgvector)** | Create DAL interfaces and pgv... | | #20 | ⬜ todo | 491 | **Claude Code integration rescue plan** | Stabilize Qwen-2.5-Coder insi... | +| #21 | ⬜ todo | 510 | **DAL Phase 1: libpq/pqxx wiring + SQL calls** | Link pqxx, implement PgDal ag... | +| #22 | ⬜ todo | 490 | **Handlers → DAL integration** | Wire kom.memory.v1.upsert_mem... | +| #23 | ⬜ todo | 511 | **Contract tests: DAL-backed tools** | Expand CTest to cover DAL-bac... | ### Task #2: Design MCP memory/context API - Subtasks @@ -41,3 +44,20 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro |:--:|:------:|:------| | #2.1 | ⬜ todo | Write JSON Schemas for tools (done) | +### Task #21: DAL Phase 1: libpq/pqxx wiring + SQL calls - Subtasks + +| ID | Status | Title | +|:--:|:------:|:------| +| #21.1 | ⬜ todo | CMake: find_package(pqxx) and link; CI env var DSN | +| #21.2 | ⬜ todo | PgDal: implement connect/tx + prepared statements | +| #21.3 | ⬜ todo | SQL: ensureNamespace, upsertItem/Chunks/Embeddings | +| #21.4 | ⬜ todo | Search: FTS/trgm + vector <-> with filters (namespace/thread/tags) | + +### Task #22: Handlers → DAL integration - Subtasks + +| ID | Status | Title | +|:--:|:------:|:------| +| #22.1 | ⬜ todo | Replace ad-hoc JSON with parser (nlohmann/json or simdjson) | +| #22.2 | ⬜ todo | Validate request bodies against schemas before DAL calls | +| #22.3 | ⬜ todo | Scope & sensitivity enforcement (namespace/user + skip secret embeddings) | + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..eb11e0e --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,13 @@ +add_executable(test_mcp_tools + contract/test_mcp_tools.cpp +) +target_include_directories(test_mcp_tools PRIVATE ${PROJECT_SOURCE_DIR}/src) +target_link_libraries(test_mcp_tools PRIVATE kom_dal) + +add_test(NAME contract_mcp_tools COMMAND test_mcp_tools) + +add_library(contract_memory STATIC + contract_memory.cpp +) +target_include_directories(contract_memory PRIVATE ${PROJECT_SOURCE_DIR}/src) +target_link_libraries(contract_memory PRIVATE kom_dal) diff --git a/tests/contract/test_mcp_tools.cpp b/tests/contract/test_mcp_tools.cpp index 0fbbbec..11a3ca4 100644 --- a/tests/contract/test_mcp_tools.cpp +++ b/tests/contract/test_mcp_tools.cpp @@ -20,15 +20,34 @@ int main() { KomMcpServer server; register_default_tools(server); - const std::string upsertReq = R"({"namespace":"tests","items":[{"text":"hello world"},{"text":"hola mundo"}]})"; + const std::string upsertReq = R"({"namespace":"tests","items":[{"text":"hello world","tags":["greeting"],"embedding":[0.1,0.2,0.3]},{"text":"hola mundo","embedding":[0.05,0.1,0.15]}]})"; std::string upsertResp = server.dispatch("kom.memory.v1.upsert_memory", upsertReq); if (!expect_contains(upsertResp, "\"upserted\":2", "upsert_memory count")) return 1; if (!expect_contains(upsertResp, "\"status\":\"ok\"", "upsert_memory status")) return 1; + std::string firstId; + const std::string idsAnchor = "\"ids\":[\""; + auto idsPos = upsertResp.find(idsAnchor); + if (idsPos != std::string::npos) { + idsPos += idsAnchor.size(); + auto endPos = upsertResp.find("\"", idsPos); + if (endPos != std::string::npos) { + firstId = upsertResp.substr(idsPos, endPos - idsPos); + } + } + if (firstId.empty()) { + std::cerr << "Failed to extract first item id from upsert response: " << upsertResp << "\n"; + return 1; + } + const std::string searchReq = R"({"namespace":"tests","query":{"text":"hello","k":3}})"; std::string searchResp = server.dispatch("kom.memory.v1.search_memory", searchReq); if (!expect_contains(searchResp, "\"matches\"", "search_memory matches key")) return 1; - if (!expect_contains(searchResp, "\"text\":\"hello\"", "search_memory echo text")) return 1; + if (!expect_contains(searchResp, "\"text\":\"hello world\"", "search_memory returns stored text")) return 1; + + const std::string vectorReq = R"({"namespace":"tests","query":{"embedding":[0.1,0.2,0.3],"k":1}})"; + std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq); + if (!expect_contains(vectorResp, "\"id\":\""+firstId+"\"", "vector search returns stored id")) return 1; const std::string exportReq = R"({"namespace":"tests","destination":"/tmp/example.enc"})"; std::string exportResp = server.dispatch("kom.local.v1.backup.export_encrypted", exportReq); diff --git a/tests/contract_memory.cpp b/tests/contract_memory.cpp new file mode 100644 index 0000000..65a4025 --- /dev/null +++ b/tests/contract_memory.cpp @@ -0,0 +1,42 @@ +#include "dal/PgDal.hpp" + +#include +#include + +static void contract_pgdal_basic() { + kom::PgDal dal; + dal.connect("stub://memory"); + auto ns = dal.ensureNamespace("tests"); + static_cast(ns); + + kom::ItemRow item; + item.namespace_id = "tests"; + item.text = std::string("example"); + item.tags = {"alpha", "beta"}; + item.id = dal.upsertItem(item); + + kom::ChunkRow chunk; + chunk.item_id = item.id; + chunk.text = "chunk-text"; + auto chunkIds = dal.upsertChunks(std::vector{chunk}); + if (!chunkIds.empty()) { + chunk.id = chunkIds.front(); + } + + kom::EmbeddingRow embedding; + embedding.chunk_id = chunk.id; + embedding.model = "stub-model"; + embedding.dim = 3; + embedding.vector = {0.1f, 0.2f, 0.3f}; + dal.upsertEmbeddings(std::vector{embedding}); + + static_cast(dal.searchText("tests", "chunk", 5)); + static_cast(dal.searchVector("tests", embedding.vector, 5)); + static_cast(dal.getItemById(item.id)); + static_cast(dal.hybridSearch(embedding.vector, "stub-model", "tests", "chunk", 5)); +} + +static const bool contract_pgdal_compiles = [] { + contract_pgdal_basic(); + return true; +}();