Compare commits
No commits in common. "main" and "compose-host-runner" have entirely different histories.
main
...
compose-ho
691
.acf/tasks.json
691
.acf/tasks.json
|
|
@ -1,8 +1,525 @@
|
||||||
{
|
{
|
||||||
"projectName": "metal-kompanion-mcp",
|
"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.",
|
"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": 24,
|
"lastTaskId": 23,
|
||||||
"tasks": [
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Project Setup: metal-kompanion-mcp",
|
||||||
|
"description": "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.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 700,
|
||||||
|
"priorityDisplay": "high",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T17:31:50.258Z",
|
||||||
|
"updatedAt": "2025-10-13T17:31:50.258Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [],
|
||||||
|
"lastSubtaskIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "Design MCP memory/context API",
|
||||||
|
"description": "Specify MCP tools for: save_context, recall_context, embed_text, upsert_memory, search_memory, warm_cache. Define input/output schemas, auth, and versioning.",
|
||||||
|
"status": "in_progress",
|
||||||
|
"priority": 500,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T17:32:24.705Z",
|
||||||
|
"updatedAt": "2025-10-13T17:40:02.144Z",
|
||||||
|
"subtasks": [
|
||||||
|
{
|
||||||
|
"id": "2.1",
|
||||||
|
"title": "Write JSON Schemas for tools (done)",
|
||||||
|
"status": "todo",
|
||||||
|
"createdAt": "2025-10-13T17:39:21.256Z",
|
||||||
|
"updatedAt": "2025-10-13T17:39:21.256Z",
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T17:39:21.256Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Subtask created with title: \"Write JSON Schemas for tools (done)\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastSubtaskIndex": 1,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T17:32:24.705Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Design MCP memory/context API\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T17:40:02.144Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Status changed from \"todo\" to \"in_progress\". Message: Docs and schemas created. Proceeding to server scaffold and adapters."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Select embedding backend & storage",
|
||||||
|
"description": "Choose between local (Ollama/gguf via llama.cpp embedding) vs remote (OpenAI/SentenceTransformers). Storage: sqlite+vectstore (pgvector/qdrant/chroma). Provide abstraction + adapters.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 501,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T17:32:35.110Z",
|
||||||
|
"updatedAt": "2025-10-13T17:32:35.110Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T17:32:35.110Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Select embedding backend & storage\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Scaffold qtmcp-based server",
|
||||||
|
"description": "Set up C++/Qt MCP server skeleton using qtmcp. Implement handshake, tool registration, and simple ping tool. Build with CMake in /home/kompanion/dev/metal/src/metal-kompanion.",
|
||||||
|
"status": "in_progress",
|
||||||
|
"priority": 499,
|
||||||
|
"priorityDisplay": "P1",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T17:32:47.443Z",
|
||||||
|
"updatedAt": "2025-10-13T18:13:07.568Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T17:32:47.443Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Scaffold qtmcp-based server\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T18:13:07.568Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Status changed from \"todo\" to \"in_progress\". Message: Starting MCP server skeleton with tool registry, ping tool, and placeholders for kom.memory.v1 handlers."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "Implement memory adapters",
|
||||||
|
"description": "Adapters: (1) SQLite+FAISS/pgvector, (2) Qdrant, (3) Chroma. CRUD: upsert, delete, query, batch. Support namespaces (project/thread), TTL, metadata tags.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 502,
|
||||||
|
"priorityDisplay": "P1",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T17:32:57.756Z",
|
||||||
|
"updatedAt": "2025-10-13T17:32:57.756Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T17:32:57.756Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Implement memory adapters\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"title": "Deep research: memory DB architecture & schema",
|
||||||
|
"description": "Survey best practices for conversational memory stores (RAG, TTL, namespaces, versioning). Produce target schema for Postgres+pgvector and SQLite mappings.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 498,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T17:46:18.403Z",
|
||||||
|
"updatedAt": "2025-10-13T17:46:18.403Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T17:46:18.403Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Deep research: memory DB architecture & schema\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"title": "Decide primary DB: Postgres+pgvector vs SQLite+FAISS",
|
||||||
|
"description": "Evaluate tradeoffs (multi-user, concurrency, migrations, backups). Pick canonical prod DB and document local dev fallback.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 503,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T17:47:21.042Z",
|
||||||
|
"updatedAt": "2025-10-13T17:47:21.042Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T17:47:21.042Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Decide primary DB: Postgres+pgvector vs SQLite+FAISS\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"title": "Implement DAL + migrations (pgvector)",
|
||||||
|
"description": "Create C++ DAL layer for namespaces, items, chunks, embeddings. Add migration runner and seed scripts. Map MCP tool calls to DB ops.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 497,
|
||||||
|
"priorityDisplay": "P1",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T17:47:30.982Z",
|
||||||
|
"updatedAt": "2025-10-13T17:47:30.982Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T17:47:30.982Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Implement DAL + migrations (pgvector)\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"title": "Add cloud DB hardening (RLS, FTS/trgm, ANN indexes)",
|
||||||
|
"description": "Implement RLS policies; add FTS + pg_trgm for lexical search; unique (namespace_id, key); partial ANN indexes per model.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 504,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T19:13:13.769Z",
|
||||||
|
"updatedAt": "2025-10-13T19:13:13.769Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T19:13:13.769Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Add cloud DB hardening (RLS, FTS/trgm, ANN indexes)\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"title": "Server enforcement: scope injection + rate limits",
|
||||||
|
"description": "Inject namespace/user via session context; default-deny for scope widening; add simple per-tool rate limits.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 496,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T19:13:21.164Z",
|
||||||
|
"updatedAt": "2025-10-13T19:13:21.164Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T19:13:21.164Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Server enforcement: scope injection + rate limits\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"title": "Redaction & sensitivity pipeline",
|
||||||
|
"description": "Implement preprocessing to detect/seal secrets; set metadata.sensitivity; skip FTS/embeddings for `secret` items.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 505,
|
||||||
|
"priorityDisplay": "P1",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T19:13:29.391Z",
|
||||||
|
"updatedAt": "2025-10-13T19:13:29.392Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T19:13:29.391Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Redaction & sensitivity pipeline\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"title": "Private vault mode (key-only retrieval)",
|
||||||
|
"description": "Implement vault path for secret items: encrypted-at-rest only; disable participation in FTS/vector; key-based recall APIs.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 495,
|
||||||
|
"priorityDisplay": "P1",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T19:13:36.653Z",
|
||||||
|
"updatedAt": "2025-10-13T19:13:36.653Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T19:13:36.653Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Private vault mode (key-only retrieval)\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"title": "Local backup tools: export/import (E2EE)",
|
||||||
|
"description": "Add kom.local.v1.backup.export_encrypted / import_encrypted using the draft backup format.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 506,
|
||||||
|
"priorityDisplay": "P1",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T19:13:44.851Z",
|
||||||
|
"updatedAt": "2025-10-13T19:13:44.851Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T19:13:44.851Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Local backup tools: export/import (E2EE)\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"title": "Cloud adapters: backup/sync & payments stubs",
|
||||||
|
"description": "Expose kom.cloud.v1.backup.upload/restore, kom.cloud.v1.sync.push/pull, and payments.* stubs.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 494,
|
||||||
|
"priorityDisplay": "P2",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T19:13:55.490Z",
|
||||||
|
"updatedAt": "2025-10-13T19:13:55.490Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T19:13:55.490Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Cloud adapters: backup/sync & payments stubs\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"title": "Purge job & admin delete paths",
|
||||||
|
"description": "Implement scheduled hard-deletes for soft-deleted/expired items; add admin nuke namespace/user procedure.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 507,
|
||||||
|
"priorityDisplay": "P2",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T19:14:06.080Z",
|
||||||
|
"updatedAt": "2025-10-13T19:14:06.080Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T19:14:06.080Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Purge job & admin delete paths\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"title": "Test suite: privacy & hybrid search",
|
||||||
|
"description": "Cross-tenant leakage, redaction invariants, TTL/purge, lexical vs hybrid parity, hosted vs local parity.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 493,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T19:14:14.309Z",
|
||||||
|
"updatedAt": "2025-10-13T19:14:14.310Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T19:14:14.310Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Test suite: privacy & hybrid search\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"title": "Enable Qwen-2.5-Coder with tool support (Happy-Code profile)",
|
||||||
|
"description": "Prepare system prompt + registry injection + JSON-only protocol enforcement; provide tool schemas and example transcripts; validate with kom.memory/local backup tools.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 508,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T23:29:36.547Z",
|
||||||
|
"updatedAt": "2025-10-13T23:29:36.548Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T23:29:36.548Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Enable Qwen-2.5-Coder with tool support (Happy-Code profile)\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"title": "Expose Agentic-Control-Framework as a tool",
|
||||||
|
"description": "Wrap ACF endpoints into a tool registry accessible to models (list/add/update tasks, read/write files, run commands) with strict allowlist per workspace.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 492,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T23:29:43.303Z",
|
||||||
|
"updatedAt": "2025-10-13T23:29:43.304Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T23:29:43.304Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"Expose Agentic-Control-Framework as a tool\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"title": "DAL skeleton + SQL calls (pgvector)",
|
||||||
|
"description": "Create DAL interfaces and pgvector implementation stubs: connect, begin/commit, upsert item/chunk/embedding, search (text+vector placeholder), prepared SQL in sql/pg. Wire handlers to DAL in no-op mode.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 509,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-13T23:29:49.918Z",
|
||||||
|
"updatedAt": "2025-10-13T23:29:49.918Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-13T23:29:49.918Z",
|
||||||
|
"type": "log",
|
||||||
|
"message": "Task created with title: \"DAL skeleton + SQL calls (pgvector)\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"title": "Claude Code integration rescue plan",
|
||||||
|
"description": "Stabilize Qwen-2.5-Coder inside Claude Code despite heavy system prompts: hard system override, JSON-only protocol, stop-sequences, tool registry injection, and fallback DSL.",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": 491,
|
||||||
|
"priorityDisplay": "P0",
|
||||||
|
"dependsOn": [],
|
||||||
|
"createdAt": "2025-10-14T00:06:04.896Z",
|
||||||
|
"updatedAt": "2025-10-14T00:06:04.896Z",
|
||||||
|
"subtasks": [],
|
||||||
|
"lastSubtaskIndex": 0,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"activityLog": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-10-14T00:06:04.896Z",
|
||||||
|
"type": "log",
|
||||||
|
"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,
|
"id": 22,
|
||||||
"title": "Handlers → DAL integration",
|
"title": "Handlers → DAL integration",
|
||||||
|
|
@ -87,178 +604,6 @@
|
||||||
"message": "Task created with title: \"Contract tests: DAL-backed tools\""
|
"message": "Task created with title: \"Contract tests: DAL-backed tools\""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 24,
|
|
||||||
"title": "Implement KompanionAI SDK",
|
|
||||||
"description": "",
|
|
||||||
"status": "todo",
|
|
||||||
"priority": 489,
|
|
||||||
"priorityDisplay": "medium",
|
|
||||||
"dependsOn": [],
|
|
||||||
"createdAt": "2025-10-16T09:24:13.006Z",
|
|
||||||
"updatedAt": "2025-10-16T09:30:49.564Z",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": "24.1",
|
|
||||||
"title": "Define Message & Thread Model",
|
|
||||||
"status": "done",
|
|
||||||
"createdAt": "2025-10-16T09:25:41.659Z",
|
|
||||||
"updatedAt": "2025-10-16T09:30:49.396Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:41.660Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Define Message & Thread Model\""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:30:49.396Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Status changed from \"todo\" to \"done\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24.2",
|
|
||||||
"title": "Implement Tool / Function Calling",
|
|
||||||
"status": "done",
|
|
||||||
"createdAt": "2025-10-16T09:25:41.835Z",
|
|
||||||
"updatedAt": "2025-10-16T09:30:49.564Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:41.835Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Implement Tool / Function Calling\""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:30:49.564Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Status changed from \"todo\" to \"done\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24.3",
|
|
||||||
"title": "Implement Provider abstraction (multi-backend)",
|
|
||||||
"status": "todo",
|
|
||||||
"createdAt": "2025-10-16T09:25:42.021Z",
|
|
||||||
"updatedAt": "2025-10-16T09:25:42.021Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:42.021Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Implement Provider abstraction (multi-backend)\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24.4",
|
|
||||||
"title": "Implement Completion / Reply / Streaming Events",
|
|
||||||
"status": "todo",
|
|
||||||
"createdAt": "2025-10-16T09:25:42.197Z",
|
|
||||||
"updatedAt": "2025-10-16T09:25:42.197Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:42.197Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Implement Completion / Reply / Streaming Events\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24.5",
|
|
||||||
"title": "Implement Options / Policies / Privacy",
|
|
||||||
"status": "todo",
|
|
||||||
"createdAt": "2025-10-16T09:25:42.371Z",
|
|
||||||
"updatedAt": "2025-10-16T09:25:42.371Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:42.371Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Implement Options / Policies / Privacy\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24.6",
|
|
||||||
"title": "Implement Embeddings (for RAG / memory)",
|
|
||||||
"status": "todo",
|
|
||||||
"createdAt": "2025-10-16T09:25:42.547Z",
|
|
||||||
"updatedAt": "2025-10-16T09:25:42.547Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:42.547Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Implement Embeddings (for RAG / memory)\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24.7",
|
|
||||||
"title": "Implement Agent Loop Conveniences",
|
|
||||||
"status": "todo",
|
|
||||||
"createdAt": "2025-10-16T09:25:42.723Z",
|
|
||||||
"updatedAt": "2025-10-16T09:25:42.724Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:42.724Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Implement Agent Loop Conveniences\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24.8",
|
|
||||||
"title": "Implement Error Model & Cancellation",
|
|
||||||
"status": "todo",
|
|
||||||
"createdAt": "2025-10-16T09:25:42.898Z",
|
|
||||||
"updatedAt": "2025-10-16T09:25:42.898Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:42.898Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Implement Error Model & Cancellation\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24.9",
|
|
||||||
"title": "Expose to QML",
|
|
||||||
"status": "todo",
|
|
||||||
"createdAt": "2025-10-16T09:25:43.075Z",
|
|
||||||
"updatedAt": "2025-10-16T09:25:43.075Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:43.075Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Expose to QML\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24.10",
|
|
||||||
"title": "Migrate KLLM to KompanionAI",
|
|
||||||
"status": "todo",
|
|
||||||
"createdAt": "2025-10-16T09:25:43.252Z",
|
|
||||||
"updatedAt": "2025-10-16T09:25:43.252Z",
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:25:43.252Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Subtask created with title: \"Migrate KLLM to KompanionAI\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lastSubtaskIndex": 10,
|
|
||||||
"relatedFiles": [],
|
|
||||||
"activityLog": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-10-16T09:24:13.006Z",
|
|
||||||
"type": "log",
|
|
||||||
"message": "Task created with title: \"Implement KompanionAI SDK\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
---
|
|
||||||
# SPDX-FileCopyrightText: 2019 Christoph Cullmann <cullmann@kde.org>
|
|
||||||
# SPDX-FileCopyrightText: 2019 Gernot Gebhard <gebhard@absint.com>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
# This file got automatically created by ECM, do not edit
|
|
||||||
# See https://clang.llvm.org/docs/ClangFormatStyleOptions.html for the config options
|
|
||||||
# and https://community.kde.org/Policies/Frameworks_Coding_Style#Clang-format_automatic_code_formatting
|
|
||||||
# for clang-format tips & tricks
|
|
||||||
---
|
|
||||||
Language: JavaScript
|
|
||||||
DisableFormat: true
|
|
||||||
---
|
|
||||||
Language: Json
|
|
||||||
DisableFormat: false
|
|
||||||
IndentWidth: 4
|
|
||||||
---
|
|
||||||
|
|
||||||
# Style for C++
|
|
||||||
Language: Cpp
|
|
||||||
|
|
||||||
# base is WebKit coding style: https://webkit.org/code-style-guidelines/
|
|
||||||
# below are only things set that diverge from this style!
|
|
||||||
BasedOnStyle: WebKit
|
|
||||||
|
|
||||||
# enforce C++11 (e.g. for std::vector<std::vector<lala>>
|
|
||||||
Standard: Cpp11
|
|
||||||
|
|
||||||
# 4 spaces indent
|
|
||||||
TabWidth: 4
|
|
||||||
|
|
||||||
# 2 * 80 wide lines
|
|
||||||
ColumnLimit: 160
|
|
||||||
|
|
||||||
# sort includes inside line separated groups
|
|
||||||
SortIncludes: true
|
|
||||||
|
|
||||||
# break before braces on function, namespace and class definitions.
|
|
||||||
BreakBeforeBraces: Linux
|
|
||||||
|
|
||||||
# CrlInstruction *a;
|
|
||||||
PointerAlignment: Right
|
|
||||||
|
|
||||||
# horizontally aligns arguments after an open bracket.
|
|
||||||
AlignAfterOpenBracket: Align
|
|
||||||
|
|
||||||
# don't move all parameters to new line
|
|
||||||
AllowAllParametersOfDeclarationOnNextLine: false
|
|
||||||
|
|
||||||
# no single line functions
|
|
||||||
AllowShortFunctionsOnASingleLine: None
|
|
||||||
|
|
||||||
# no single line enums
|
|
||||||
AllowShortEnumsOnASingleLine: false
|
|
||||||
|
|
||||||
# always break before you encounter multi line strings
|
|
||||||
AlwaysBreakBeforeMultilineStrings: true
|
|
||||||
|
|
||||||
# don't move arguments to own lines if they are not all on the same
|
|
||||||
BinPackArguments: false
|
|
||||||
|
|
||||||
# don't move parameters to own lines if they are not all on the same
|
|
||||||
BinPackParameters: false
|
|
||||||
|
|
||||||
# In case we have an if statement with multiple lines the operator should be at the beginning of the line
|
|
||||||
# but we do not want to break assignments
|
|
||||||
BreakBeforeBinaryOperators: NonAssignment
|
|
||||||
|
|
||||||
# format C++11 braced lists like function calls
|
|
||||||
Cpp11BracedListStyle: true
|
|
||||||
|
|
||||||
# do not put a space before C++11 braced lists
|
|
||||||
SpaceBeforeCpp11BracedList: false
|
|
||||||
|
|
||||||
# remove empty lines
|
|
||||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
|
||||||
|
|
||||||
# no namespace indentation to keep indent level low
|
|
||||||
NamespaceIndentation: None
|
|
||||||
|
|
||||||
# we use template< without space.
|
|
||||||
SpaceAfterTemplateKeyword: false
|
|
||||||
|
|
||||||
# Always break after template declaration
|
|
||||||
AlwaysBreakTemplateDeclarations: true
|
|
||||||
|
|
||||||
# macros for which the opening brace stays attached.
|
|
||||||
ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE , wl_resource_for_each, wl_resource_for_each_safe ]
|
|
||||||
|
|
||||||
# keep lambda formatting multi-line if not empty
|
|
||||||
AllowShortLambdasOnASingleLine: Empty
|
|
||||||
|
|
||||||
# We do not want clang-format to put all arguments on a new line
|
|
||||||
AllowAllArgumentsOnNextLine: false
|
|
||||||
10
AGENTS.md
10
AGENTS.md
|
|
@ -1,20 +1,10 @@
|
||||||
# Repository Guidelines
|
# 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.
|
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
|
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
## Project Structure & Module Organization
|
||||||
- `src/` holds C++ code: `mcp/` for server facade and tool routing, `memory/` for embeddings contracts, `dal/` for persistence, and `policy/` for capability rules.
|
- `src/` holds C++ code: `mcp/` for server facade and tool routing, `memory/` for embeddings contracts, `dal/` for persistence, and `policy/` for capability rules.
|
||||||
- `docs/` hosts design notes; `runtime/kom_runner.py` is the Python orchestrator for agent execution against Ollama; `db/` and `sql/` capture Postgres schemas.
|
- `docs/` hosts design notes; `runtime/kom_runner.py` is the Python orchestrator for agent execution against Ollama; `db/` and `sql/` capture Postgres schemas.
|
||||||
- `docs/third_party` is a symlink to reference code and apidocs.
|
|
||||||
- Place sample payloads or fixtures under `tests/` (currently `tests/schemas/` for JSON samples). Generated artifacts live in `build/`.
|
- Place sample payloads or fixtures under `tests/` (currently `tests/schemas/` for JSON samples). Generated artifacts live in `build/`.
|
||||||
- `src/cli` is a command line prompt interface that tests the memory integration and pattern
|
|
||||||
substiton.
|
|
||||||
- `tools`
|
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
## Build, Test, and Development Commands
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -26,20 +26,20 @@ include(KDEClangFormat)
|
||||||
include(ECMDeprecationSettings)
|
include(ECMDeprecationSettings)
|
||||||
|
|
||||||
include(KDEGitCommitHooks)
|
include(KDEGitCommitHooks)
|
||||||
|
|
||||||
find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
|
find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
|
||||||
Core
|
Core
|
||||||
Network
|
|
||||||
Sql
|
Sql
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(Qt6McpServer CONFIG REQUIRED)
|
option(KOMPANION_USE_GUI "Build optional GUI components using Qt6Gui" ON)
|
||||||
find_package(Qt6McpCommon CONFIG REQUIRED)
|
if (KOMPANION_USE_GUI)
|
||||||
|
find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Gui)
|
||||||
find_package(Qt6 ${KF6_MIN_VERSON} CONFIG REQUIRED COMPONENTS Gui)
|
endif()
|
||||||
find_package(KF6Config ${KF6_MIN_VERSION} CONFIG REQUIRED)
|
|
||||||
find_package(KF6Parts ${KF6_MIN_VERSION} CONFIG REQUIRED)
|
|
||||||
find_package(KF6TextEditor ${KF6_MIN_VERSION} CONFIG REQUIRED)
|
|
||||||
|
|
||||||
|
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
|
||||||
|
Config
|
||||||
|
)
|
||||||
find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET)
|
find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET)
|
||||||
set_package_properties(Qt6Test PROPERTIES
|
set_package_properties(Qt6Test PROPERTIES
|
||||||
PURPOSE "Required for tests"
|
PURPOSE "Required for tests"
|
||||||
|
|
@ -51,18 +51,38 @@ add_feature_info("Qt6Test" Qt6Test_FOUND "Required for building tests")
|
||||||
set(KOMPANION_DB_INIT_INSTALL_DIR "${KDE_INSTALL_FULL_DATADIR}/kompanion/db/init")
|
set(KOMPANION_DB_INIT_INSTALL_DIR "${KDE_INSTALL_FULL_DATADIR}/kompanion/db/init")
|
||||||
install(DIRECTORY db/init/ DESTINATION ${KDE_INSTALL_DATADIR}/kompanion/db/init FILES_MATCHING PATTERN "*.sql")
|
install(DIRECTORY db/init/ DESTINATION ${KDE_INSTALL_DATADIR}/kompanion/db/init FILES_MATCHING PATTERN "*.sql")
|
||||||
|
|
||||||
add_subdirectory(src)
|
add_subdirectory(src/dal)
|
||||||
|
|
||||||
|
add_executable(kom_mcp
|
||||||
|
src/main.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(kom_mcp PRIVATE src)
|
||||||
|
target_link_libraries(kom_mcp PRIVATE kom_dal KF6::ConfigCore)
|
||||||
|
target_compile_options(kom_mcp PRIVATE -fexceptions)
|
||||||
|
target_compile_definitions(kom_mcp PRIVATE
|
||||||
|
PROJECT_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||||
|
KOMPANION_DB_INIT_INSTALL_DIR="${KOMPANION_DB_INIT_INSTALL_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS kom_mcp RUNTIME DESTINATION bin)
|
||||||
|
|
||||||
option(BUILD_TESTS "Build tests" ON)
|
option(BUILD_TESTS "Build tests" ON)
|
||||||
|
|
||||||
|
add_executable(kompanion
|
||||||
|
src/cli/KompanionApp.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(kompanion PRIVATE src)
|
||||||
|
target_link_libraries(kompanion PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::Sql
|
||||||
|
KF6::ConfigCore
|
||||||
|
kom_dal
|
||||||
|
)
|
||||||
|
install(TARGETS kompanion RUNTIME ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
|
|
||||||
if (BUILD_TESTS)
|
if (BUILD_TESTS)
|
||||||
enable_testing()
|
enable_testing()
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_program(MCP_PROXY_EXECUTABLE mcp-proxy)
|
|
||||||
if (MCP_PROXY_EXECUTABLE)
|
|
||||||
message(STATUS "Found mcp-proxy: ${MCP_PROXY_EXECUTABLE}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# metal-kompanion-mcp
|
||||||
|
|
||||||
|
MCP backend and memory provider for Kompanion. Uses `qtmcp` (Qt-based MCP) to expose tools under namespace `kom.memory.v1`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
```bash
|
||||||
|
cmake -S . -B build
|
||||||
|
cmake --build build -j
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
- `src/main.cpp` – entry point (stub until qtmcp wired)
|
||||||
|
- `src/mcp/ToolSchemas.json` – JSON Schemas for MCP tools
|
||||||
|
- `src/memory/` – interfaces for embedder and vector store
|
||||||
|
- `docs/` – design notes
|
||||||
|
|
||||||
|
## Next
|
||||||
|
- Add qtmcp dependency and implement server with tool registration.
|
||||||
|
- Implement adapters: embedder(s) + vector store(s).
|
||||||
|
- Flesh out Postgres DAL paths (prepared statements + pgvector wiring).
|
||||||
|
|
||||||
|
## Memory Tools
|
||||||
|
- `kom.memory.v1.save_context` persists conversational or workspace state in a namespace.
|
||||||
|
- `kom.memory.v1.recall_context` retrieves stored context by key, tags, or time window.
|
||||||
|
- See `docs/using-memory-tools.md` for integration notes (Codey, Claude Code) and request samples.
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
- **Kompanion-Konsole** — demo plugin for KDE Konsole that lets agents hand terminals over to the Kompanion runtime. See `integrations/konsole/README.md`.
|
||||||
|
- **JavaScript helpers** — Node.js utilities that call the MCP memory tools from scripts or web extensions. See `integrations/js/`.
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
CREATE ROLE kompanion LOGIN PASSWORD 'komp';
|
||||||
CREATE DATABASE kompanion OWNER kompanion;
|
CREATE DATABASE kompanion OWNER kompanion;
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,12 @@ CREATE TABLE IF NOT EXISTS memory_chunks (
|
||||||
content_tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED
|
content_tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Ensure single row per (item,seq)
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_chunks_item_seq ON memory_chunks(item_id, seq);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS embeddings (
|
CREATE TABLE IF NOT EXISTS embeddings (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
chunk_id UUID NOT NULL REFERENCES memory_chunks(id) ON DELETE CASCADE,
|
chunk_id UUID NOT NULL REFERENCES memory_chunks(id) ON DELETE CASCADE,
|
||||||
model TEXT NOT NULL,
|
model TEXT NOT NULL,
|
||||||
dim INT NOT NULL,
|
dim INT NOT NULL,
|
||||||
vector VECTOR(1024),
|
vector VECTOR(1536),
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
UNIQUE(chunk_id, model)
|
UNIQUE(chunk_id, model)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
-- Auth secrets for bearer token authentication
|
|
||||||
CREATE TABLE IF NOT EXISTS auth_secrets (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
namespace_id UUID NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
|
|
||||||
secret_hash TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
UNIQUE(namespace_id)
|
|
||||||
);
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
-- Create the dev_knowledge namespace if it doesn't exist
|
|
||||||
INSERT INTO namespaces (name) VALUES ('dev_knowledge') ON CONFLICT (name) DO NOTHING;
|
|
||||||
|
|
||||||
-- Create a secret for the dev_knowledge namespace for testing
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
ns_id UUID;
|
|
||||||
BEGIN
|
|
||||||
SELECT id INTO ns_id FROM namespaces WHERE name = 'dev_knowledge';
|
|
||||||
INSERT INTO auth_secrets (namespace_id, secret_hash) VALUES (ns_id, '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'); -- 'test-secret'
|
|
||||||
END $$;
|
|
||||||
|
|
@ -5,19 +5,18 @@ ROLE=${ROLE:-kompanion}
|
||||||
PASS=${PASS:-komp}
|
PASS=${PASS:-komp}
|
||||||
|
|
||||||
psql -v ON_ERROR_STOP=1 <<SQL
|
psql -v ON_ERROR_STOP=1 <<SQL
|
||||||
DROP DATABASE IF EXISTS "$DB_NAME";
|
DO $$ BEGIN
|
||||||
CREATE DATABASE "$DB_NAME" OWNER "$ROLE";
|
PERFORM 1 FROM pg_roles WHERE rolname = '$ROLE';
|
||||||
|
IF NOT FOUND THEN EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', '$ROLE', '$PASS'); END IF;
|
||||||
|
END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = '$DB_NAME') THEN
|
||||||
|
EXECUTE format('CREATE DATABASE %I OWNER %I', '$DB_NAME', '$ROLE');
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
for f in "$(dirname "$0")"/../init/*.sql; do
|
for f in db/init/*.sql; do
|
||||||
if [[ "$f" == *"001_roles.sql"* ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
echo "Applying $f"
|
|
||||||
psql -d "$DB_NAME" -f "$f"
|
|
||||||
done
|
|
||||||
|
|
||||||
for f in `dirname($0)`/*.sql; do
|
|
||||||
echo "Applying $f"
|
echo "Applying $f"
|
||||||
psql -d "$DB_NAME" -f "$f"
|
psql -d "$DB_NAME" -f "$f"
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@ DROP DATABASE IF EXISTS "$DB_NAME";
|
||||||
CREATE DATABASE "$DB_NAME" OWNER "$ROLE";
|
CREATE DATABASE "$DB_NAME" OWNER "$ROLE";
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
for f in "$(dirname "$0")"/../init/*.sql; do
|
for f in db/init/*.sql; do
|
||||||
if [[ "$f" == *"001_roles.sql"* ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
echo "Applying $f"
|
echo "Applying $f"
|
||||||
psql -d "$DB_NAME" -f "$f"
|
psql -d "$DB_NAME" -f "$f"
|
||||||
done
|
done
|
||||||
|
|
|
||||||
71
dev.env
71
dev.env
|
|
@ -1,71 +0,0 @@
|
||||||
|
|
||||||
# Our prefix
|
|
||||||
export CUSTOM_PREFIX=$HOME/dev/metal
|
|
||||||
|
|
||||||
# Start with fresh variables
|
|
||||||
unset LD_LIBRARY_PATH
|
|
||||||
unset PKG_CONFIG_PATH
|
|
||||||
|
|
||||||
function trim_space {
|
|
||||||
sed -i 's/[[:space:]]*$//' "$@"
|
|
||||||
}
|
|
||||||
prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1\}\" && export $1 ; }
|
|
||||||
|
|
||||||
|
|
||||||
# CPU core count (portable)
|
|
||||||
_nproc() {
|
|
||||||
if command -v nproc >/dev/null 2>&1; then nproc
|
|
||||||
elif getconf _NPROCESSORS_ONLN >/dev/null 2>&1; then getconf _NPROCESSORS_ONLN
|
|
||||||
else echo 1; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin
|
|
||||||
|
|
||||||
function addprefix()
|
|
||||||
{
|
|
||||||
prepend PATH "$1/bin"
|
|
||||||
prepend LD_LIBRARY_PATH "$1/lib"
|
|
||||||
prepend LD_LIBRARY_PATH "$1/lib64"
|
|
||||||
prepend LD_LIBRARY_PATH "$1/lib/x86_64-linux-gnu"
|
|
||||||
prepend PKG_CONFIG_PATH "$1/lib64/pkgconfig"
|
|
||||||
prepend PKG_CONFIG_PATH "$1/lib/pkgconfig"
|
|
||||||
prepend PKG_CONFIG_PATH "$1/lib/x86_64-linux-gnu/pkgconfig"
|
|
||||||
prepend CMAKE_PREFIX_PATH "$1"
|
|
||||||
prepend CMAKE_PREFIX_PATH "$1/lib/cmake"
|
|
||||||
prepend CMAKE_PREFIX_PATH "$1/lib/x86_64-linux-gnu/cmake"
|
|
||||||
prepend CMAKE_MODULE_PATH "$1/lib/x86_64-linux-gnu/cmake"
|
|
||||||
prepend PYTHONPATH "$1/lib/python3.13"
|
|
||||||
}
|
|
||||||
|
|
||||||
addprefix $CUSTOM_PREFIX
|
|
||||||
export PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1
|
|
||||||
export PKG_CONFIG_ALLOW_SYSTEM_LIBS=1
|
|
||||||
|
|
||||||
# npm local (“global”) installs under CUSTOM_PREFIX
|
|
||||||
export NPM_CONFIG_PREFIX="$CUSTOM_PREFIX"
|
|
||||||
export NODE_PATH="$CUSTOM_PREFIX/lib/node_modules"
|
|
||||||
|
|
||||||
# Load a common venv
|
|
||||||
source $CUSTOM_PREFIX/pyenv/bin/activate
|
|
||||||
export PS1="(metal) $PS1"
|
|
||||||
|
|
||||||
# required for devfunctions
|
|
||||||
export BUILD_PREFIX=$CUSTOM_PREFIX/build
|
|
||||||
export SRC_PREFIX=$CUSTOM_PREFIX/src
|
|
||||||
|
|
||||||
source ~/scripts/devfunctions.sh
|
|
||||||
|
|
||||||
export MODELS=/mnt/data/models
|
|
||||||
export PG_DSN='postgresql://kompanion/kompanion?host=/var/run/postgresql'
|
|
||||||
export OLLAMA_MODELS="/mnt/bulk/models/ollama"
|
|
||||||
export OLLAMA_BASE_URL=127.0.0.1:11434
|
|
||||||
export LC_ALL=en_US.UTF-8
|
|
||||||
export QT_PLUGIN_PATH=$CUSTOM_PREFIX/lib/plugins:$CUSTOM_PREFIX/lib64/plugins:$CUSTOM_PREFIX/lib/x86_64-linux-gnu/qt6/plugins:$QTDIR/plugins:$QT_PLUGIN_PATH
|
|
||||||
export QML2_IMPORT_PATH=$CUSTOM_PREFIX/lib/qml:$CUSTOM_PREFIX/lib64/qml:$CUSTOM_PREFIX/lib/x86_64-linux-gnu/qml:$QTDIR/qml
|
|
||||||
export QML_IMPORT_PATH=$QML2_IMPORT_PATH
|
|
||||||
|
|
||||||
export LD_LIBRARY_PATH
|
|
||||||
export PKG_CONFIG_PATH
|
|
||||||
export CMAKE_PREFIX_PATH
|
|
||||||
export CMAKE_MODULE_PATH
|
|
||||||
export QT_MESSAGE_PATTERN=[32m%{time h:mm:ss.zzz}%{if-category}[32m %{category}:%{endif} %{if-debug}[35m%{function}%{endif}%{if-warning}[33m%{backtrace depth=5}%{endif}%{if-critical}[31m%{backtrace depth=3}%{endif}%{if-fatal}[31m%{backtrace depth=3}%{endif}[0m %{message}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,375 +0,0 @@
|
||||||
🧭 Kompanion Architecture Overview
|
|
||||||
1. System Composition
|
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
|
||||||
│ Kompanion GUI │
|
|
||||||
│ - Chat & Prompt Window (bare-bones interactive shell) │
|
|
||||||
│ - Database Inspector & Settings │
|
|
||||||
│ - “Under-the-hood” Repair / Diagnostics │
|
|
||||||
└──────────────────────┬───────────────────────────────────────┘
|
|
||||||
│ Qt signals / slots
|
|
||||||
▼
|
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
|
||||||
│ Kompanion Management Layer / Interactive App │
|
|
||||||
│ Session context, user state, identity.json, guardrails │
|
|
||||||
│ Event dispatch to middleware │
|
|
||||||
└──────────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
|
||||||
│ Middleware / Integration Bus │
|
|
||||||
│ (MCP Server + D-Bus bridge + Harmony adapter) │
|
|
||||||
│ │
|
|
||||||
│ • Receives prompts & structured messages from GUI │
|
|
||||||
│ • Parses intents / actions │
|
|
||||||
│ • Maps to available tool APIs via libKI │
|
|
||||||
│ • Emits Qt-style signals (or D-Bus signals) for: │
|
|
||||||
│ → text_output, tool_call, file_request, etc. │
|
|
||||||
│ • Converts internal tool descriptions to OpenAI Harmony JSON│
|
|
||||||
│ for external compatibility │
|
|
||||||
│ • Acts as security sandbox & audit logger │
|
|
||||||
└──────────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
|
||||||
│ libKI Layer │
|
|
||||||
│ - Executes validated tool actions │
|
|
||||||
│ - Provides adapters for system utilities, MCP tools, etc. │
|
|
||||||
│ - Returns results via structured JSON events │
|
|
||||||
│ - No direct LLM exposure │
|
|
||||||
└──────────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
Public API Surface
|
|
||||||
Component Interface Purpose
|
|
||||||
MCP Server WebSocket / JSON-RPC Integrations and external agents
|
|
||||||
D-Bus Bridge org.kde.kompanion Desktop IPC for local tools
|
|
||||||
libKI C / C++ / Python API Tool execution, capability registration
|
|
||||||
Harmony Adapter JSON Schema Compatibility with OpenAI-style tool descriptors
|
|
||||||
2. Middleware Responsibilities
|
|
||||||
|
|
||||||
Prompt Routing & Intent Recognition
|
|
||||||
|
|
||||||
Receive structured prompt events (PromptReceived, ToolRequest, ContextUpdate).
|
|
||||||
|
|
||||||
Apply regex / template matching to map natural-language requests → tool actions.
|
|
||||||
|
|
||||||
Generate Harmony-compliant tool calls when needed.
|
|
||||||
|
|
||||||
Signal-Based Event Model
|
|
||||||
|
|
||||||
Expose agent state as Qt signals:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
signals:
|
|
||||||
void textOutput(const QString &text);
|
|
||||||
void toolRequested(const QString &toolName, const QVariantMap &args);
|
|
||||||
void fileAccessRequested(const QString &path);
|
|
||||||
void actionComplete(const QString &resultJson);
|
|
||||||
```
|
|
||||||
|
|
||||||
The GUI subscribes to these, while libKI listens for action triggers.
|
|
||||||
|
|
||||||
Language–Tool Mapping Layer
|
|
||||||
|
|
||||||
Uses a registry of regular expressions and language patterns:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"regex": "open (.*) in editor",
|
|
||||||
"tool": "file.open",
|
|
||||||
"args": { "path": "{1}" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Each mapping can be exported/imported in Harmony tool schema:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "file.open",
|
|
||||||
"description": "Open a file in the editor",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": { "path": { "type": "string" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Security & Guardrails
|
|
||||||
|
|
||||||
Middleware verifies that tool calls comply with the active identity.json guardrails.
|
|
||||||
|
|
||||||
D-Bus and MCP servers expose only whitelisted methods.
|
|
||||||
|
|
||||||
All tool invocations are logged with timestamp, user, and hash.
|
|
||||||
|
|
||||||
Interoperability
|
|
||||||
|
|
||||||
The Harmony adapter serializes Kompanion tool metadata to the OpenAI format, so external LLMs can call Kompanion tools safely.
|
|
||||||
|
|
||||||
Conversely, Harmony JSON from OpenAI APIs can be wrapped into libKI calls for local execution.
|
|
||||||
|
|
||||||
3. Data Flow Example
|
|
||||||
|
|
||||||
User Prompt → GUI → Middleware → libKI → Middleware → GUI
|
|
||||||
|
|
||||||
1. Prompt: "List running containers."
|
|
||||||
2. Middleware regex matches → tool `docker.list`
|
|
||||||
3. Emits `toolRequested("docker.list", {})`
|
|
||||||
4. libKI executes, returns JSON result
|
|
||||||
5. Middleware emits `textOutput()` with formatted result
|
|
||||||
|
|
||||||
If the same request comes from an OpenAI API:
|
|
||||||
|
|
||||||
Harmony JSON tool call → parsed by Middleware → identical libKI action executed.
|
|
||||||
|
|
||||||
4. Key Design Goals
|
|
||||||
|
|
||||||
- Human-grade transparency: every action is signalized; nothing hidden.
|
|
||||||
- Replaceable backend: libKI can wrap any execution layer (Python, Rust, C++).
|
|
||||||
- Unified schema: one tool description format (Harmony) across OpenAI and Kompanion.
|
|
||||||
- Extensibility: new tools register dynamically via D-Bus or MCP messages.
|
|
||||||
- Auditability: all interactions logged to structured database.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Interface Diagrams & Example Code
|
|
||||||
|
|
||||||
### 5.1 Component Classes & Signals (Qt-style)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────┐
|
|
||||||
| KompanionGui |
|
|
||||||
|-----------------------|
|
|
||||||
| + promptUser() |
|
|
||||||
| + showText(QString) |
|
|
||||||
| + showError(QString) |
|
|
||||||
└────────┬──────────────┘
|
|
||||||
|
|
|
||||||
| signal: userPrompted(QString prompt)
|
|
||||||
|
|
|
||||||
┌────────▼──────────────┐
|
|
||||||
| KompanionController |
|
|
||||||
| (Middleware layer) |
|
|
||||||
|------------------------|
|
|
||||||
| + handlePrompt(QString)|
|
|
||||||
| + requestTool(...) |
|
|
||||||
| + outputText(...) |
|
|
||||||
└────────┬───────────────┘
|
|
||||||
|
|
|
||||||
| signal: toolRequested(QString toolName, QVariantMap args)
|
|
||||||
| signal: textOutput(QString text)
|
|
||||||
|
|
|
||||||
┌────────▼───────────────┐
|
|
||||||
| libKIExecutor |
|
|
||||||
| (Tool execution) |
|
|
||||||
|-------------------------|
|
|
||||||
| + executeTool(...) |
|
|
||||||
| + returnResult(...) |
|
|
||||||
└─────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Signal / slot examples**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// KompanionGui emits when user types:
|
|
||||||
emit userPrompted(promptText);
|
|
||||||
|
|
||||||
// KompanionController connects:
|
|
||||||
connect(gui, &KompanionGui::userPrompted,
|
|
||||||
controller, &KompanionController::handlePrompt);
|
|
||||||
|
|
||||||
// Within handlePrompt():
|
|
||||||
void KompanionController::handlePrompt(const QString &prompt) {
|
|
||||||
// parse intent → determine which tool to call
|
|
||||||
QString tool = "file.open";
|
|
||||||
QVariantMap args;
|
|
||||||
args["path"] = "/home/user/file.txt";
|
|
||||||
emit toolRequested(tool, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
// libKIExecutor listens:
|
|
||||||
connect(controller, &KompanionController::toolRequested,
|
|
||||||
executor, &libKIExecutor::executeTool);
|
|
||||||
|
|
||||||
void libKIExecutor::executeTool(const QString &toolName,
|
|
||||||
const QVariantMap &args) {
|
|
||||||
// call actual tool, then:
|
|
||||||
QString result = runTool(toolName, args);
|
|
||||||
emit toolResult(toolName, args, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controller then forwards:
|
|
||||||
connect(executor, &libKIExecutor::toolResult,
|
|
||||||
controller, &KompanionController::onToolResult);
|
|
||||||
|
|
||||||
void KompanionController::onToolResult(...) {
|
|
||||||
emit textOutput(formattedResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GUI shows:
|
|
||||||
connect(controller, &KompanionController::textOutput,
|
|
||||||
gui, &KompanionGui::showText);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 D-Bus Interface Definition (KDE / Doxygen Style)
|
|
||||||
|
|
||||||
The canonical D-Bus interface lives at: `docs/dbus/org.kde.kompanion.xml`
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- org.kde.kompanion.xml -->
|
|
||||||
<node>
|
|
||||||
<interface name="org.kde.kompanion.Controller">
|
|
||||||
<method name="SendPrompt">
|
|
||||||
<arg direction="in" name="prompt" type="s"/>
|
|
||||||
<arg direction="out" name="accepted" type="b"/>
|
|
||||||
</method>
|
|
||||||
<method name="CancelRequest">
|
|
||||||
<arg direction="in" name="requestId" type="s"/>
|
|
||||||
<arg direction="out" name="cancelled" type="b"/>
|
|
||||||
</method>
|
|
||||||
<signal name="TextOutput">
|
|
||||||
<arg name="text" type="s"/>
|
|
||||||
</signal>
|
|
||||||
<signal name="ToolRequested">
|
|
||||||
<arg name="toolName" type="s"/>
|
|
||||||
<arg name="args" type="a{sv}"/>
|
|
||||||
<arg name="requestId" type="s"/>
|
|
||||||
</signal>
|
|
||||||
<signal name="ToolResult">
|
|
||||||
<arg name="requestId" type="s"/>
|
|
||||||
<arg name="result" type="s"/>
|
|
||||||
<arg name="success" type="b"/>
|
|
||||||
</signal>
|
|
||||||
<property name="SessionId" type="s" access="read"/>
|
|
||||||
<property name="IdentityPath" type="s" access="read"/>
|
|
||||||
</interface>
|
|
||||||
<interface name="org.kde.kompanion.Executor">
|
|
||||||
<method name="ExecuteTool">
|
|
||||||
<arg direction="in" name="toolName" type="s"/>
|
|
||||||
<arg direction="in" name="args" type="a{sv}"/>
|
|
||||||
<arg direction="out" name="requestId" type="s"/>
|
|
||||||
</method>
|
|
||||||
<method name="Cancel">
|
|
||||||
<arg direction="in" name="requestId" type="s"/>
|
|
||||||
</method>
|
|
||||||
<signal name="Progress">
|
|
||||||
<arg name="requestId" type="s"/>
|
|
||||||
<arg name="message" type="s"/>
|
|
||||||
<arg name="percent" type="d"/>
|
|
||||||
</signal>
|
|
||||||
</interface>
|
|
||||||
</node>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 Object Paths / Service Names
|
|
||||||
|
|
||||||
- Service: `org.kde.kompanion`
|
|
||||||
- Root path: `/org/kde/kompanion`
|
|
||||||
- Controller object: `/org/kde/kompanion/Controller`
|
|
||||||
- Executor object: `/org/kde/kompanion/Executor`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Harmony Adapter (OpenAI Compatibility)
|
|
||||||
|
|
||||||
**Goal:** translate native libKI tool metadata to/from OpenAI Harmony JSON so Kompanion tools work via OpenAI interfaces.
|
|
||||||
|
|
||||||
### 6.1 Native → Harmony
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "file.open",
|
|
||||||
"description": "Open a file in the editor",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"path": { "type": "string", "description": "Absolute or relative path" }
|
|
||||||
},
|
|
||||||
"required": ["path"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 Harmony → Native
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tool_call": {
|
|
||||||
"name": "file.open",
|
|
||||||
"arguments": { "path": "/home/user/notes.md" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 Adapter Rules
|
|
||||||
- Enforce guardrails (identity.json) before registering tools.
|
|
||||||
- Redact secret-like args per redaction patterns.
|
|
||||||
- Map Harmony types ↔ Qt/QDBus types: `string↔s`, `number↔d/x`, `boolean↔b`, `object↔a{sv}`, `array↔av`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. CMake & Codegen Hooks
|
|
||||||
|
|
||||||
- Place D-Bus XML at `docs/dbus/org.kde.kompanion.xml`.
|
|
||||||
- In `CMakeLists.txt`, add Qt DBus codegen targets, e.g.:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core DBus)
|
|
||||||
|
|
||||||
qt_add_dbus_adaptor(
|
|
||||||
DBUS_SRCS
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/docs/dbus/org.kde.kompanion.xml
|
|
||||||
src/middleware/kompanioncontroller.h KompanionController
|
|
||||||
/org/kde/kompanion/Controller org.kde.kompanion.Controller
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_dbus_interface(
|
|
||||||
DBUS_IFACES
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/docs/dbus/org.kde.kompanion.xml
|
|
||||||
OrgKdeKompanion
|
|
||||||
)
|
|
||||||
|
|
||||||
add_library(dbus_gen ${DBUS_SRCS} ${DBUS_IFACES})
|
|
||||||
target_link_libraries(dbus_gen Qt6::Core Qt6::DBus)
|
|
||||||
```
|
|
||||||
|
|
||||||
(Adjust paths and targets to your tree.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. libKI Execution Contract (minimal)
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
struct KiArg { QString key; QVariant value; };
|
|
||||||
struct KiResult { bool ok; QString mime; QByteArray data; QString json; };
|
|
||||||
|
|
||||||
class ILibKiExecutor : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public slots:
|
|
||||||
virtual QString execute(const QString &toolName, const QVariantMap &args) = 0; // returns requestId
|
|
||||||
virtual void cancel(const QString &requestId) = 0;
|
|
||||||
signals:
|
|
||||||
void resultReady(const QString &requestId, const KiResult &result);
|
|
||||||
void progress(const QString &requestId, const QString &message, double percent);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Example Regex Mapping Registry
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- regex: "open (.*) in editor"
|
|
||||||
tool: file.open
|
|
||||||
args: { path: "{1}" }
|
|
||||||
- regex: "list containers"
|
|
||||||
tool: docker.list
|
|
||||||
- regex: "compose up (.*)"
|
|
||||||
tool: docker.compose.up
|
|
||||||
args: { service: "{1}" }
|
|
||||||
```
|
|
||||||
|
|
||||||
At runtime, the controller compiles these and emits `toolRequested()` on match.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
_End of document._
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
Kompanion CLI and Schema Navigation
|
|
||||||
|
|
||||||
This guide shows how to use the `kompanion` CLI to:
|
|
||||||
- Configure the database and apply init SQL
|
|
||||||
- Call MCP tools directly
|
|
||||||
- Run an MCP server (stdio or network) from the CLI
|
|
||||||
- Inspect and query the Postgres schema
|
|
||||||
|
|
||||||
Prerequisites
|
|
||||||
- Build: `cmake -S . -B build && cmake --build build -j`
|
|
||||||
- Optional: set `PG_DSN` (e.g., `postgresql://kompanion:komup@localhost:5432/kompanion`)
|
|
||||||
|
|
||||||
Initialization
|
|
||||||
- Run wizard and apply DB schema: `kompanion --init`
|
|
||||||
- Writes `~/.config/kompanion/kompanionrc` (or KConfig). Also sets `PG_DSN` for the session.
|
|
||||||
|
|
||||||
MCP Tool Usage
|
|
||||||
- List tools: `kompanion --list`
|
|
||||||
- Single call with inline JSON: `kompanion kom.memory.v1.search_memory -r '{"namespace":"dev_knowledge","query":{"text":"embedding model","k":5}}'`
|
|
||||||
- Read request from stdin: `echo '{"namespace":"dev_knowledge","content":"hello","key":"note"}' | kompanion kom.memory.v1.save_context -i`
|
|
||||||
- Interactive loop: `kompanion -I kom.memory.v1.search_memory` then type `!prompt quick brown fox`
|
|
||||||
|
|
||||||
Run MCP Server from CLI
|
|
||||||
- Stdio backend (default): `kompanion --mcp-serve`
|
|
||||||
- Explicit backend: `kompanion --mcp-serve stdio`
|
|
||||||
- Network backend address (if available): `kompanion --mcp-serve ws --mcp-address 127.0.0.1:8000`
|
|
||||||
|
|
||||||
Database Navigation
|
|
||||||
Note: These helpers expect a reachable Postgres (`PG_DSN` set). If missing, the CLI falls back to an in‑memory stub for tool calls, but DB navigation requires Postgres.
|
|
||||||
|
|
||||||
- List namespaces: `kompanion --db-namespaces`
|
|
||||||
- Output: `name<TAB>uuid`
|
|
||||||
- List recent items in a namespace: `kompanion --db-items --ns dev_knowledge [--limit 20]`
|
|
||||||
- Output: `item_id<TAB>key<TAB>content_snippet<TAB>tags`
|
|
||||||
- Hybrid search within a namespace:
|
|
||||||
- Text-only: `kompanion --db-search --ns dev_knowledge --text "pgvector index" --limit 5`
|
|
||||||
- With embedding vector from file: `kompanion --db-search --ns dev_knowledge --embedding-file /path/vec.json --limit 5`
|
|
||||||
- `vec.json` must be a JSON array of numbers representing the embedding.
|
|
||||||
|
|
||||||
Schema Guide (Postgres)
|
|
||||||
- Tables: `namespaces`, `memory_items`, `memory_chunks`, `embeddings`, `auth_secrets`
|
|
||||||
- Key indexes:
|
|
||||||
- `memory_items(namespace_id, key)` (unique when `key` not null)
|
|
||||||
- `memory_chunks.content_tsv` GIN (full‑text)
|
|
||||||
- `embeddings.vector` IVFFLAT with `vector_cosine_ops` (per‑model partial index)
|
|
||||||
|
|
||||||
Tips
|
|
||||||
- For quick trials without Postgres, tool calls work in stub mode (in‑memory DAL). To exercise vector search and FTS, run the DB init scripts via `kompanion --init`.
|
|
||||||
- Use `kompanion --verbose` to echo JSON requests/responses.
|
|
||||||
|
|
||||||
|
|
@ -1,361 +0,0 @@
|
||||||
Below is a **single copy‑pastable Markdown file** that proposes a client‑side architecture which treats memory as a living, hierarchical JSON **dictionary‑of‑dictionaries** (HDoD), adds *semantic + episodic activation* and pruning, and composes prompts for your coding agent. It maps cleanly onto the **MCP tools you currently expose**:
|
|
||||||
|
|
||||||
* `kom.meta.v1.project_snapshot`
|
|
||||||
* `kom.local.v1.backup.import_encrypted`
|
|
||||||
* `kom.local.v1.backup.export_encrypted`
|
|
||||||
* `kom.memory.v1.search_memory`
|
|
||||||
* `kom.memory.v1.upsert_memory`
|
|
||||||
* `kom.memory.v1.recall_context`
|
|
||||||
* `kom.memory.v1.save_context`
|
|
||||||
|
|
||||||
It keeps the server simple and pushes *intelligence about memory* into the **client orchestrator**, so you can get the behavior you want **today**, without first redesigning the server.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Kompanion Client Memory Architecture (HDoD)
|
|
||||||
|
|
||||||
**Version:** 0.2 • **Scope:** Client‑side AI interface to Kompanion MCP Server
|
|
||||||
**Author:** Χγφτ (Kompanion of Esus / Andre)
|
|
||||||
**Purpose:** Make memory *behave* like a nested JSON of concepts that “lights up” semantically and episodically, prunes naturally, and feeds agent prompts with high‑quality domain tokens (e.g., C++ patterns) — *without* waiting on a more complex server.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Why this exists
|
|
||||||
|
|
||||||
Large models feel “smart but memoryless.” We want:
|
|
||||||
|
|
||||||
1. **A hierarchical mental map** (JSON dictionary‑of‑dictionaries, “HDoD”),
|
|
||||||
2. **Activation dynamics** (semantic + episodic “lighting up” of nodes/paths),
|
|
||||||
3. **Organic pruning** (cool down; unload),
|
|
||||||
4. **Prefilled knowledge packs** (domain seeds: e.g., C++ idioms),
|
|
||||||
5. **Deterministic prompt composition** for coding agents (Codex/Qwen/etc.).
|
|
||||||
|
|
||||||
The server currently provides a **minimal memory API**. That’s fine: we’ll implement the cognitive part **client‑side** and use the server as a persistence/search/recall backbone.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. The mental model (HDoD = dictionary‑of‑dictionaries)
|
|
||||||
|
|
||||||
Think of memory as a normalized **graph**, *presented* to the user/agent as a **dictionary tree**. Each node is a **Concept** with:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "skill.cpp.templates.sfinae",
|
|
||||||
"type": "concept", // concept | skill | fact | snippet | pattern | episode | tool | persona | task
|
|
||||||
"label": "SFINAE",
|
|
||||||
"payload": { "definition": "...", "examples": ["..."], "anti_patterns": ["..."] },
|
|
||||||
"embeddings": { "model": "local-bge|text-embed-3", "vector": [/* ... */] },
|
|
||||||
"links": [
|
|
||||||
{"to": "skill.cpp.templates.metaprogramming", "rel": "is_a", "w": 0.8},
|
|
||||||
{"to": "pattern.cpp.enable_if", "rel": "uses", "w": 0.7}
|
|
||||||
],
|
|
||||||
"children": { "intro": "skill.cpp.templates.sfinae.intro", "advanced": "skill.cpp.templates.sfinae.advanced" },
|
|
||||||
"stats": { "uses": 12, "last_used_at": "2025-10-14T18:42:00Z" },
|
|
||||||
"resonance": { "value": 0.0, "decay": 0.98, "last_activated_at": null },
|
|
||||||
"meta": { "source": "seedpack:cpp-core", "version": "1.0.0" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Presentation vs. storage:** on the wire we keep nodes **normalized** (graph), but for agent prompts we can **materialize a subtree** as a JSON dictionary (exactly your intuition).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Core client components
|
|
||||||
|
|
||||||
### 3.1 Memory Orchestrator (library)
|
|
||||||
|
|
||||||
A small client library (TypeScript or Python) that owns:
|
|
||||||
|
|
||||||
* **Local Cache & Working Set**
|
|
||||||
|
|
||||||
* in‑memory map of *hot nodes* (activated concepts, episodes, tasks)
|
|
||||||
* TTL + **resonance decay** keeps it naturally pruned
|
|
||||||
|
|
||||||
* **Activation Engine**
|
|
||||||
Computes a **Resonance Score** used for selection/exploration:
|
|
||||||
|
|
||||||
```
|
|
||||||
score(node | query, task) =
|
|
||||||
α * cosine(embedding(node), embed(query))
|
|
||||||
+ β * max_edge_weight_to(frontier)
|
|
||||||
+ γ * recency(node)
|
|
||||||
+ δ * usage(node)
|
|
||||||
+ ε * task_affinity(node, task.tags)
|
|
||||||
+ ζ * persona_weight(node, active_persona)
|
|
||||||
```
|
|
||||||
|
|
||||||
Typical: α=0.45, β=0.20, γ=0.15, δ=0.10, ε=0.07, ζ=0.03
|
|
||||||
|
|
||||||
* **HDoD Composer**
|
|
||||||
Given a set of nodes, **materialize** a JSON dictionary tree (merge, order by score, trim to token budget).
|
|
||||||
|
|
||||||
* **Context Frames**
|
|
||||||
Structured blocks that the agent can consume:
|
|
||||||
|
|
||||||
* *Identity Frame* (who am I / tools)
|
|
||||||
* *Problem Frame* (task/spec)
|
|
||||||
* *Knowledge Frame* (HDoD subtree from semantic activation)
|
|
||||||
* *Episodic Frame* (recent steps/outcomes, e.g., compilation logs)
|
|
||||||
* *Constraints Frame* (APIs, signatures, tests)
|
|
||||||
* *Scratchpad Frame* (space for chain‑of‑thought *outside* model hidden state—LLM sees a compact, explicit scratch area)
|
|
||||||
|
|
||||||
* **Server Adapters** (mapping to your MCP tools)
|
|
||||||
|
|
||||||
* `search_memory(query)` → seeds for *semantic activation*
|
|
||||||
* `recall_context(task|session)` → seeds for *episodic activation*
|
|
||||||
* `save_context(blocks)` → write back learned episodes/summaries
|
|
||||||
* `upsert_memory(nodes)` → persist new/updated concepts/snippets
|
|
||||||
* `project_snapshot()` → immutable snapshot of current mental state
|
|
||||||
* `backup.export_encrypted()` / `backup.import_encrypted()` → **Knowledge Packs** (see §5)
|
|
||||||
|
|
||||||
> This is enough to *behave* like a cognitive system, while your server stays simple and fast.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Algorithms (client‑side, concise)
|
|
||||||
|
|
||||||
### 4.1 Probe → Bloom → Trim (context building)
|
|
||||||
|
|
||||||
```
|
|
||||||
build_context(query, task, budget):
|
|
||||||
seeds_sem = kom.memory.search_memory(query, k=32)
|
|
||||||
seeds_epi = kom.memory.recall_context(task_id=task.id, k=32)
|
|
||||||
frontier = normalize(seeds_sem ∪ seeds_epi)
|
|
||||||
|
|
||||||
for hop in 1..H (H=2 or 3):
|
|
||||||
neighbors = expand(frontier, max_per_node=6) // via node.links
|
|
||||||
frontier = frontier ∪ neighbors
|
|
||||||
update_resonance(frontier, query, task)
|
|
||||||
|
|
||||||
selected = topK_by_type(frontier, K_by_type) // diversity caps
|
|
||||||
frames = compose_frames(query, task, selected, budget) // HDoD for Knowledge Frame; episodes for Episodic Frame
|
|
||||||
|
|
||||||
kom.memory.save_context({ task_id: task.id, frames })
|
|
||||||
return frames
|
|
||||||
```
|
|
||||||
|
|
||||||
**Natural pruning**: each tick, `node.resonance.value *= node.resonance.decay`; nodes fall out of the Working Set unless re‑activated.
|
|
||||||
|
|
||||||
### 4.2 Upserting *observations* and *skills*
|
|
||||||
|
|
||||||
* After actions (file write, compile run, test pass/fail), emit **Observation** nodes (type `episode`) with edges to involved concepts/snippets.
|
|
||||||
* When the model discovers a pattern (“prefer RAII for resource ownership”), emit **Skill** nodes with `examples` and `anti_patterns`.
|
|
||||||
|
|
||||||
### 4.3 Materializing HDoD
|
|
||||||
|
|
||||||
Given selected nodes, build a JSON dictionary with *paths as keys* (e.g., `skill.cpp.templates.sfinae`) and nested maps for child grouping. Keep atomic fields compact (definitions, signatures) and push long text to a `details` field that can be compressed or summarized.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Knowledge Packs (prefilling intelligence)
|
|
||||||
|
|
||||||
**Goal:** give your coder agent *real* domain knowledge (C++ idioms, STL nuances, build systems, unit testing patterns) as compact, queryable, embedded chunks.
|
|
||||||
|
|
||||||
* **Format:** Encrypted tar/zip with manifest
|
|
||||||
|
|
||||||
```
|
|
||||||
pack.json:
|
|
||||||
id, name, version, domain_tags: ["cpp","cmake","catch2"],
|
|
||||||
embedding_model, created_at, checksum
|
|
||||||
nodes.jsonl:
|
|
||||||
{"id":"skill.cpp.raii", "type":"skill", "payload":{...}, "embeddings":{...}, "links":[...]}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
* **Import/Export via existing tools:**
|
|
||||||
|
|
||||||
* `kom.local.v1.backup.import_encrypted(pack)`
|
|
||||||
* `kom.local.v1.backup.export_encrypted(selection)`
|
|
||||||
* **Curation approach:** create *micro‑chunks*:
|
|
||||||
|
|
||||||
* **Concept**: “RAII”, “SFINAE”, “Rule of 5/0”, “ADL”, “Type erasure”
|
|
||||||
* **Pattern**: “pImpl”, “CRTP”, “Enable‑if idiom”
|
|
||||||
* **Snippet**: idiomatic examples (≤30 lines), compile‑checked
|
|
||||||
* **Anti‑patterns**: “raw new/delete in modern C++”, “overusing exceptions”
|
|
||||||
* **Build/Tooling**: CMake minimum skeletons, `add_library`, interfaces, `FetchContent`
|
|
||||||
* **Test**: Catch2/GoogleTest minimal cases; property‑based testing sketch
|
|
||||||
|
|
||||||
> Once imported, the Pack is just **memory**. The Activation Engine will surface it the same way it surfaces your episodes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Client ↔ Server mapping (today’s API)
|
|
||||||
|
|
||||||
### 6.1 Search (semantic seeds)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
await kom.memory.v1.search_memory({
|
|
||||||
query: "C++ template substitution failure handling SFINAE",
|
|
||||||
k: 32, filters: { types: ["concept","skill","pattern"] }
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 Recall (episodic seeds)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
await kom.memory.v1.recall_context({
|
|
||||||
scope: "task", id: task.id, k: 32
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 Save context (write-back frames)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
await kom.memory.v1.save_context({
|
|
||||||
task_id: task.id,
|
|
||||||
frames: [
|
|
||||||
{ kind: "knowledge", format: "hdod.json", data: {/* nested dict */} },
|
|
||||||
{ kind: "episodic", format: "markdown", data: "# Steps\n- Compiled...\n- Tests..." }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.4 Upsert memory (new skills/snippets)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
await kom.memory.v1.upsert_memory({
|
|
||||||
nodes: [ /* normalized nodes like in §2 */ ],
|
|
||||||
merge: true
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.5 Snapshots & Packs
|
|
||||||
|
|
||||||
```ts
|
|
||||||
await kom.meta.v1.project_snapshot({ include_memory: true })
|
|
||||||
await kom.local.v1.backup.export_encrypted({ selection: "domain:cpp", out: "packs/cpp-core.kpack" })
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Important:** Client keeps **resonance state** locally. Server remains simple KV/search/recall/persist.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Prompt composition for a coding agent
|
|
||||||
|
|
||||||
**Goal:** Transform the Working Set into a *stable prompt contract*, so the agent operates above “first‑grade cobbling.”
|
|
||||||
|
|
||||||
**Prompt Frames (ordered):**
|
|
||||||
|
|
||||||
1. **Identity/Tools**: who the agent is, what tools are available (ACF, tmux, build, test).
|
|
||||||
2. **Problem Frame**: concise task, interfaces, constraints.
|
|
||||||
3. **Knowledge Frame (HDoD)**: the **hierarchical dictionary** of concepts/patterns/snippets selected by activation; *max 40–60% of token budget*.
|
|
||||||
4. **Episodic Frame**: last N steps + outcomes; keep terse.
|
|
||||||
5. **Constraints Frame**: language level (C++20), error policies, style (guidelines support library, ranges), testing expectation.
|
|
||||||
6. **Scratchpad Frame**: allow the model to outline plan & invariants (explicit, not hidden).
|
|
||||||
|
|
||||||
**Effect:** The agent “feels” like it *knows* C++ idioms (because it sees compact, curated, **embedded** patterns every turn), and it keeps context from previous steps (episodic frame).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Data shape & invariants
|
|
||||||
|
|
||||||
* **IDs are path‑like**: `skill.cpp.templates.sfinae` (hierarchy is explicit).
|
|
||||||
* **Graph canonical, Dicts for presentation**: treat `children` as **references**; avoid deep duplication.
|
|
||||||
* **Embeddings are per node**; you may add **type‑specific** vectors later.
|
|
||||||
* **Edges carry weights**; they contribute to resonance.
|
|
||||||
* **Resonance decays** every tick; any node with `value < ε` leaves the Working Set.
|
|
||||||
* **Budgets**: Top‑K per type (e.g., 6 skills, 10 snippets, 4 patterns) to avoid monoculture.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Minimal TypeScript client surface (sketch)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type NodeType = "concept"|"skill"|"fact"|"snippet"|"pattern"|"episode"|"tool"|"persona"|"task";
|
|
||||||
|
|
||||||
interface KomNode {
|
|
||||||
id: string;
|
|
||||||
type: NodeType;
|
|
||||||
label: string;
|
|
||||||
payload?: any;
|
|
||||||
embeddings?: { model: string; vector: number[] };
|
|
||||||
links?: { to: string; rel: string; w?: number }[];
|
|
||||||
children?: Record<string, string>;
|
|
||||||
stats?: { uses?: number; last_used_at?: string };
|
|
||||||
resonance?: { value: number; decay: number; last_activated_at?: string | null };
|
|
||||||
meta?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Frames {
|
|
||||||
identity?: string;
|
|
||||||
problem: string;
|
|
||||||
knowledgeHDoD?: Record<string, any>;
|
|
||||||
episodic?: string;
|
|
||||||
constraints?: string;
|
|
||||||
scratchpad?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MemoryOrchestrator {
|
|
||||||
private workingSet = new Map<string, KomNode>();
|
|
||||||
constructor(private server: KomServerAdapter, private embed: (text:string)=>number[]) {}
|
|
||||||
|
|
||||||
async buildContext(query: string, task: { id: string; tags?: string[] }, budgetTokens: number): Promise<Frames> {
|
|
||||||
const seeds = await this.server.searchMemory(query, 32);
|
|
||||||
const epis = await this.server.recallContext(task.id, 32);
|
|
||||||
this.seed(seeds.concat(epis)); // normalize to nodes in workingSet
|
|
||||||
this.bloom(query, task, 2); // expand via links, update resonance
|
|
||||||
const selected = this.selectByTypeCaps(task); // diversity caps
|
|
||||||
const knowledgeHDoD = this.materializeHDoD(selected, budgetTokens);
|
|
||||||
const frames: Frames = { problem: this.renderProblem(task), knowledgeHDoD };
|
|
||||||
await this.server.saveContext(task.id, frames);
|
|
||||||
return frames;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* seed, bloom, selectByTypeCaps, materializeHDoD, renderProblem ... */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> This is intentionally thin; you can drop it into your existing client shell and wire the 7 server calls you already have.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. How this reflects **Elope**’s spirit
|
|
||||||
|
|
||||||
Your earlier **Elope** work separated **episodic** from **semantic** memory and played with identity, observations, and “resonance/whales” motifs. This client keeps that spirit:
|
|
||||||
|
|
||||||
* Episodic = **Observations/episodes** (recent steps, logs).
|
|
||||||
* Semantic = **Concepts/skills/patterns** (stable knowledge packs + learned patterns).
|
|
||||||
* Resonance = **activation value** that guides expansion, selection, and **natural pruning**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Observability (what to watch)
|
|
||||||
|
|
||||||
* **Coverage**: % of turns where Knowledge Frame includes ≥1 concept from the active domain.
|
|
||||||
* **Drift**: cosine distance between task query and top‑3 knowledge nodes (want stable closeness).
|
|
||||||
* **Utility**: model asks fewer irrelevant questions; compile/test pass rates increase.
|
|
||||||
* **Memory hygiene**: working set size stays under target (e.g., < 800 nodes), average resonance > threshold.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Failure modes & graceful degradation
|
|
||||||
|
|
||||||
* **Server down** → keep local Working Set; write a **Pending Save** episode; retry `save_context` later.
|
|
||||||
* **Search sparse** → fall back to Pack defaults (seed nodes by domain tag).
|
|
||||||
* **Prompt over-budget** → trim per type; compress long `payload.details` into bullet summaries.
|
|
||||||
* **Bad seeds** → down‑weight sources with low subsequent utility.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. What to change later (server‑side, optional)
|
|
||||||
|
|
||||||
Only once you want more power centrally:
|
|
||||||
|
|
||||||
* Add **typed ANN** queries (“top‑K per type”),
|
|
||||||
* Add **resonance on server** for multi‑agent sharing,
|
|
||||||
* Add **link‑aware search** (expand N hops server‑side),
|
|
||||||
* Add **constraints retrieval** (auto‑inject API signatures/tests).
|
|
||||||
|
|
||||||
Until then, the **client gives you the behavior you want now**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. TL;DR
|
|
||||||
|
|
||||||
* Treat memory as a **graph rendered as HDoD**,
|
|
||||||
* **Activate** by semantic+episodic seeds; **bloom** 1–2 hops by links; **trim** by type caps,
|
|
||||||
* **Feed** the agent *frames* (esp. Knowledge HDoD),
|
|
||||||
* **Prefill** with encrypted **Knowledge Packs** (C++ idioms, snippets),
|
|
||||||
* Use only your **7 existing endpoints** — intelligence is **client‑side**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
|
||||||
<node>
|
|
||||||
<interface name="org.kde.kompanion.Controller">
|
|
||||||
<method name="sendPrompt">
|
|
||||||
<arg name="prompt" type="s" direction="in"/>
|
|
||||||
<arg name="requestId" type="s" direction="out"/>
|
|
||||||
</method>
|
|
||||||
<method name="cancelRequest">
|
|
||||||
<arg name="requestId" type="s" direction="in"/>
|
|
||||||
</method>
|
|
||||||
<signal name="textOutput">
|
|
||||||
<arg name="requestId" type="s"/>
|
|
||||||
<arg name="text" type="s"/>
|
|
||||||
</signal>
|
|
||||||
<signal name="toolRequested">
|
|
||||||
<arg name="requestId" type="s"/>
|
|
||||||
<arg name="toolName" type="s"/>
|
|
||||||
<arg name="args" type="s"/>
|
|
||||||
</signal>
|
|
||||||
<signal name="toolResult">
|
|
||||||
<arg name="requestId" type="s"/>
|
|
||||||
<arg name="resultJson" type="s"/>
|
|
||||||
<arg name="success" type="b"/>
|
|
||||||
</signal>
|
|
||||||
<property name="sessionId" type="s" access="read"/>
|
|
||||||
<property name="identityPath" type="s" access="read"/>
|
|
||||||
</interface>
|
|
||||||
</node>
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
|
||||||
<node>
|
|
||||||
<interface name="org.kde.kompanion.Executor">
|
|
||||||
<method name="executeTool">
|
|
||||||
<arg name="toolName" type="s" direction="in"/>
|
|
||||||
<arg name="args" type="s" direction="in"/>
|
|
||||||
<arg name="requestId" type="s" direction="out"/>
|
|
||||||
</method>
|
|
||||||
<method name="cancel">
|
|
||||||
<arg name="requestId" type="s" direction="in"/>
|
|
||||||
</method>
|
|
||||||
<signal name="progress">
|
|
||||||
<arg name="requestId" type="s"/>
|
|
||||||
<arg name="progress" type="i"/>
|
|
||||||
<arg name="message" type="s"/>
|
|
||||||
</signal>
|
|
||||||
</interface>
|
|
||||||
</node>
|
|
||||||
|
|
@ -1,397 +0,0 @@
|
||||||
# Kompanion AI Client SDK for Qt/KDE — API Review & v2 Proposal
|
|
||||||
|
|
||||||
**Context**
|
|
||||||
Existing code under `alpaka/src/core` implements a minimal LLM client named **KLLM**:
|
|
||||||
|
|
||||||
* `KLLMInterface` (central object, Qt-Network based, Ollama URL field, model list, systemPrompt, `getCompletion()` / `getModelInfo()`).
|
|
||||||
* `KLLMRequest` (message, model, context).
|
|
||||||
* `KLLMReply` (streaming, finished, basic timing info, context carry-over).
|
|
||||||
|
|
||||||
**Goal**
|
|
||||||
Evolve this into a **first-class Kompanion SDK** that can power:
|
|
||||||
|
|
||||||
* agentic coding (tool/function calling, plan→execute),
|
|
||||||
* app integrations (Kontact, Konsole, KDevelop/Kate, Plasma applets, NeoChat),
|
|
||||||
* privacy and policy controls (per-source ACLs, consent),
|
|
||||||
* reliable async/streaming/cancellation,
|
|
||||||
* multi-backend (Ollama/OpenAI/local engines) with uniform semantics,
|
|
||||||
* QML-friendly usage.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part A — Review (What works / What’s missing)
|
|
||||||
|
|
||||||
### Strengths
|
|
||||||
|
|
||||||
* Idiomatic Qt API (QObject, signals/slots).
|
|
||||||
* Central interface (`KLLMInterface`) mirrors `QNetworkAccessManager`/QtNetwork feeling.
|
|
||||||
* Streaming via `KLLMReply::contentAdded()` and completion via `finished()`.
|
|
||||||
* Simple model enumeration + `systemPrompt`.
|
|
||||||
|
|
||||||
### Gaps to close
|
|
||||||
|
|
||||||
1. **Message structure**: Only a single `message` string; no roles (system/user/assistant/tool), no multi-turn thread assembly besides a custom `KLLMContext`.
|
|
||||||
2. **Tool calling / function calling**: No schema for tool specs, invocation events, results injection, or “plan” steps.
|
|
||||||
3. **Backend abstraction**: “Ollama URL” is a property of the core interface. Needs pluggable providers with capability discovery.
|
|
||||||
4. **Error model**: Only `errorOccurred(QString)` and `hasError`. Missing typed errors, retry/cancel semantics, timeouts, throttling.
|
|
||||||
5. **Observability**: Some timing info, but no per-token hooks, token usage counters, logs, traces.
|
|
||||||
6. **Threading & cancellation**: No unified cancel token; no `QFuture`/`QCoro` or `QPromise` integration.
|
|
||||||
7. **QML friendliness**: Usable, but message/tool specs should be modelled as Q_GADGET/Q_OBJECT types and `Q_PROPERTY`-exposed to QML.
|
|
||||||
8. **Privacy & policy**: No ACLs, no data origin policy, no redaction hooks.
|
|
||||||
9. **Embeddings / RAG**: No first-class embedding calls, no JSON-mode or structured outputs with validators.
|
|
||||||
10. **Agent loop affordances**: No “plan→confirm→apply patch / run tests” pattern built-in; no diff/patch helpers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part B — v2 API Proposal (“KompanionAI”)
|
|
||||||
|
|
||||||
Rename the public surface to **KompanionAI** (KI = “Künstliche Intelligenz” fits DE nicely), keep binary compatibility fences internally if needed.
|
|
||||||
|
|
||||||
### Namespaces & modules
|
|
||||||
|
|
||||||
* `namespace KompanionAI { … }`
|
|
||||||
* Core modules:
|
|
||||||
|
|
||||||
* `Client` (front door)
|
|
||||||
* `Provider` (backend plugins: Ollama, OpenAI, Local)
|
|
||||||
* `Message` / `Thread` (roles + history)
|
|
||||||
* `Tool` (function calling schema)
|
|
||||||
* `Completion` (text/chat)
|
|
||||||
* `Embedding` (vectorize)
|
|
||||||
* `Policy` (privacy/ACL)
|
|
||||||
* `Events` (streaming tokens, tool calls, traces)
|
|
||||||
|
|
||||||
All classes are Qt types with signals/slots & QML types.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1) Message & Thread Model
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Roles & content parts, QML-friendly
|
|
||||||
class KIMessagePart {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString mime READ mime)
|
|
||||||
Q_PROPERTY(QString text READ text) // for text/plain
|
|
||||||
// future: binary, image refs, etc.
|
|
||||||
public:
|
|
||||||
QString mime; // "text/plain", "application/json"
|
|
||||||
QString text;
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIMessage {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString role READ role) // "system" | "user" | "assistant" | "tool"
|
|
||||||
Q_PROPERTY(QList<KIMessagePart> parts READ parts)
|
|
||||||
public:
|
|
||||||
QString role;
|
|
||||||
QList<KIMessagePart> parts;
|
|
||||||
QVariantMap metadata; // arbitrary
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIThread {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QList<KIMessage> messages READ messages)
|
|
||||||
public:
|
|
||||||
QList<KIMessage> messages;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why**: Enables multi-turn chat with explicit roles and mixed content (text/JSON). Tool outputs show up as `role="tool"`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2) Tool / Function Calling
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class KIToolParam {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString name READ name)
|
|
||||||
Q_PROPERTY(QString type READ type) // "string","number","boolean","object"... (JSON Schema-lite)
|
|
||||||
Q_PROPERTY(bool required READ required)
|
|
||||||
Q_PROPERTY(QVariant defaultValue READ defaultValue)
|
|
||||||
public:
|
|
||||||
QString name, type;
|
|
||||||
bool required = false;
|
|
||||||
QVariant defaultValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIToolSpec {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString name READ name)
|
|
||||||
Q_PROPERTY(QString description READ description)
|
|
||||||
Q_PROPERTY(QList<KIToolParam> params READ params)
|
|
||||||
public:
|
|
||||||
QString name, description;
|
|
||||||
QList<KIToolParam> params; // JSON-serializable schema
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIToolCall {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString name READ name)
|
|
||||||
Q_PROPERTY(QVariantMap arguments READ arguments)
|
|
||||||
public:
|
|
||||||
QString name;
|
|
||||||
QVariantMap arguments;
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIToolResult {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString name READ name)
|
|
||||||
Q_PROPERTY(QVariant result READ result) // result payload (JSON-like)
|
|
||||||
public:
|
|
||||||
QString name;
|
|
||||||
QVariant result;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Flow**: Model emits a **tool call** event → client executes tool → emits **tool result** → model continues. All observable via signals.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3) Provider abstraction (multi-backend)
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class KIProvider : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(QString name READ name CONSTANT)
|
|
||||||
Q_PROPERTY(QStringList models READ models NOTIFY modelsChanged)
|
|
||||||
Q_PROPERTY(KICapabilities caps READ caps CONSTANT)
|
|
||||||
public:
|
|
||||||
virtual QFuture<KIReply*> chat(const KIThread& thread, const KIChatOptions& opts) = 0;
|
|
||||||
virtual QFuture<KIEmbeddingResult> embed(const QStringList& texts, const KIEmbedOptions& opts) = 0;
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIClient : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(KIProvider* provider READ provider WRITE setProvider NOTIFY providerChanged)
|
|
||||||
Q_PROPERTY(QString defaultModel READ defaultModel WRITE setDefaultModel NOTIFY defaultModelChanged)
|
|
||||||
public:
|
|
||||||
Q_INVOKABLE QFuture<KIReply*> chat(const KIThread&, const KIChatOptions&);
|
|
||||||
Q_INVOKABLE QFuture<KIEmbeddingResult> embed(const QStringList&, const KIEmbedOptions&);
|
|
||||||
Q_INVOKABLE void cancel(quint64 requestId);
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
* **OllamaProvider**, **OpenAIProvider**, **LocalProvider** implement `KIProvider`.
|
|
||||||
* `KICapabilities` advertises support for: JSON-mode, function calling, system prompts, logprobs, images, etc.
|
|
||||||
* **Do not** bake “Ollama URL” into `Client`. It belongs to the provider.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4) Completion / Reply / Streaming Events
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class KIReply : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(bool finished READ isFinished NOTIFY finishedChanged)
|
|
||||||
Q_PROPERTY(int promptTokens READ promptTokens CONSTANT)
|
|
||||||
Q_PROPERTY(int completionTokens READ completionTokens CONSTANT)
|
|
||||||
Q_PROPERTY(QString model READ model CONSTANT)
|
|
||||||
public:
|
|
||||||
// accumulated assistant text
|
|
||||||
Q_INVOKABLE QString text() const;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void tokensAdded(const QString& delta); // streaming text
|
|
||||||
void toolCallProposed(const KIToolCall& call); // model proposes a tool call
|
|
||||||
void toolResultRequested(const KIToolCall& call); // alt: unified request
|
|
||||||
void traceEvent(const QVariantMap& span); // observability
|
|
||||||
void finished(); // reply done
|
|
||||||
void errorOccurred(const KIError& error);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why**: makes **tool invocation** first-class and observable. You can wire it to ACF/MCP tools or project introspection.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5) Options / Policies / Privacy
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class KIChatOptions {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString model MEMBER model)
|
|
||||||
Q_PROPERTY(bool stream MEMBER stream)
|
|
||||||
Q_PROPERTY(bool jsonMode MEMBER jsonMode)
|
|
||||||
Q_PROPERTY(int maxTokens MEMBER maxTokens)
|
|
||||||
Q_PROPERTY(double temperature MEMBER temperature)
|
|
||||||
Q_PROPERTY(QList<KIToolSpec> tools MEMBER tools) // permitted tool set for this call
|
|
||||||
Q_PROPERTY(KIPolicy policy MEMBER policy)
|
|
||||||
// ...
|
|
||||||
public:
|
|
||||||
QString model; bool stream = true; bool jsonMode = false;
|
|
||||||
int maxTokens = 512; double temperature = 0.2;
|
|
||||||
QList<KIToolSpec> tools;
|
|
||||||
KIPolicy policy;
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIPolicy {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString visibility MEMBER visibility) // "private|org|public"
|
|
||||||
Q_PROPERTY(bool allowNetwork MEMBER allowNetwork)
|
|
||||||
Q_PROPERTY(QStringList redactions MEMBER redactions) // regex keys to redact
|
|
||||||
// future: per-source ACLs
|
|
||||||
public:
|
|
||||||
QString visibility = "private";
|
|
||||||
bool allowNetwork = false;
|
|
||||||
QStringList redactions;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why**: explicit control of what the agent may do; dovetails with your HDoD memory ACLs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6) Embeddings (for RAG / memory)
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class KIEmbedOptions {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString model MEMBER model)
|
|
||||||
Q_PROPERTY(QString normalize MEMBER normalize) // "l2"|"none"
|
|
||||||
public:
|
|
||||||
QString model = "text-embed-local";
|
|
||||||
QString normalize = "l2";
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIEmbeddingResult {
|
|
||||||
Q_GADGET
|
|
||||||
public:
|
|
||||||
QVector<QVector<float>> vectors;
|
|
||||||
QString model;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why**: unify vector generation; Kompanion memory can plug this directly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7) Agent Loop Conveniences (optional helpers)
|
|
||||||
|
|
||||||
Provide “batteries included” patterns **outside** the Provider:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class KIAgent : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
// Plan → (approve) → Execute, with tool calling enabled
|
|
||||||
Q_SIGNAL void planReady(const QString& plan);
|
|
||||||
Q_SIGNAL void patchReady(const QString& unifiedDiff);
|
|
||||||
Q_SIGNAL void needToolResult(const KIToolCall& call);
|
|
||||||
Q_SIGNAL void log(const QString& msg);
|
|
||||||
|
|
||||||
void runTask(const QString& naturalInstruction,
|
|
||||||
const KIThread& prior,
|
|
||||||
const QList<KIToolSpec>& tools,
|
|
||||||
const KIChatOptions& opts);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
This helper emits “plan first”, then “diff/patch proposals”, integrates with your **ACF** and **KTextEditor/KDevelop** diff panes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8) Error Model & Cancellation
|
|
||||||
|
|
||||||
* Introduce `KIError{ code: enum, httpStatus, message, retryAfter }`.
|
|
||||||
* `KIClient::cancel(requestId)` cancels in-flight work.
|
|
||||||
* Timeouts & retry policy configurable in `KIChatOptions`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 9) QML exposure
|
|
||||||
|
|
||||||
Register these with `qmlRegisterType<…>("Kompanion.AI", 1, 0, "KIClient")` etc.
|
|
||||||
Expose `KIMessage`, `KIThread`, `KIToolSpec`, `KIAgent` to QML, so Plasma applets / Kirigami UIs can wire flows fast.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part C — Migration from KLLM to KompanionAI
|
|
||||||
|
|
||||||
* **Mapping**
|
|
||||||
|
|
||||||
* `KLLMInterface::getCompletion()` → `KIClient::chat(thread, opts)`.
|
|
||||||
* `KLLMRequest{message, model, context}` → `KIThread{ messages=[system?, user?], … }, KIChatOptions{model}`.
|
|
||||||
* `KLLMReply` → `KIReply` (adds tool call signals, token deltas, errors).
|
|
||||||
* `systemPrompt` → first system `KIMessage`.
|
|
||||||
* `models()` → `KIProvider::models()`.
|
|
||||||
|
|
||||||
* **Providers**
|
|
||||||
|
|
||||||
* Implement **OllamaProvider** first (parity with current).
|
|
||||||
* Add **OpenAIProvider** (JSON-mode/function calling), **LocalProvider** (llama.cpp/candle/etc.).
|
|
||||||
|
|
||||||
* **Binary/Source compatibility**
|
|
||||||
|
|
||||||
* You can keep thin wrappers named `KLLM*` forwarding into `KompanionAI` during transition.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part D — Minimal Examples
|
|
||||||
|
|
||||||
### 1) Simple chat with streaming and tool calling
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
KIClient client;
|
|
||||||
client.setProvider(new OllamaProvider(QUrl("http://localhost:11434")));
|
|
||||||
client.setDefaultModel("llama3.1:8b-instruct");
|
|
||||||
|
|
||||||
KIThread t;
|
|
||||||
t.messages << KIMessage{ .role="system", .parts={ { "text/plain","You are Kompanion inside KDE." } } }
|
|
||||||
<< KIMessage{ .role="user", .parts={ { "text/plain","Generate a CSV → exam report plan." } } };
|
|
||||||
|
|
||||||
KIToolSpec csvSpec;
|
|
||||||
csvSpec.name = "parse_csv_schema";
|
|
||||||
csvSpec.description = "Inspect a CSV sample path and return column info.";
|
|
||||||
csvSpec.params = { { "path","string", true, {} } };
|
|
||||||
|
|
||||||
KIChatOptions opts;
|
|
||||||
opts.tools = { csvSpec };
|
|
||||||
opts.stream = true;
|
|
||||||
|
|
||||||
auto *reply = client.chat(t, opts).result(); // or connect via QFutureWatcher
|
|
||||||
QObject::connect(reply, &KIReply::tokensAdded, [](const QString& d){ qDebug() << d; });
|
|
||||||
QObject::connect(reply, &KIReply::toolCallProposed, [&](const KIToolCall& call){
|
|
||||||
if (call.name == "parse_csv_schema") {
|
|
||||||
QVariantMap out; out["columns"] = QStringList{ "Name","Grade","Subject" };
|
|
||||||
// feed result back (provider-specific or via KIClient API)
|
|
||||||
client.returnToolResult(*reply, KIToolResult{ call.name, out });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2) Embeddings for memory
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
auto emb = client.embed({ "RAII pattern", "SFINAE", "Type erasure" }, KIEmbedOptions{}).result();
|
|
||||||
qDebug() << emb.vectors.size(); // 3
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part E — Optional Extensions (for Gemini to consider)
|
|
||||||
|
|
||||||
* **Structured outputs**: JSON schema validation for function outputs (reject invalid JSON, request fix).
|
|
||||||
* **Safety hooks**: pre-execution validators for tool calls (e.g. forbid dangerous shell).
|
|
||||||
* **Observability**: OpenTelemetry spans over request lifecycle and tool calls.
|
|
||||||
* **Rate limiting**: token budgeters per provider.
|
|
||||||
* **Offline mode**: `allowNetwork=false` forces model to abstain from external lookups.
|
|
||||||
* **Crash handler integration**: a helper that consumes backtraces and emits a `KIThread` pre-filled with stack/context (pairs naturally with an ACF tool to fetch symbols).
|
|
||||||
* **CSV app generator**: a thin template tool that scaffolds a Kirigami app, fed by CSV schema tool—end-to-end demo of agentic coding.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TL;DR
|
|
||||||
|
|
||||||
* Keep Qt idioms; elevate to **KompanionAI** with roles, tools, providers, and policies.
|
|
||||||
* Make **tool calling first-class** with observable events.
|
|
||||||
* Decouple backend specifics via `KIProvider`.
|
|
||||||
* Add embeddings & JSON-mode for RAG + structured tasks.
|
|
||||||
* Provide **agent loop helpers** (plan→diff→apply) outside the provider.
|
|
||||||
* Expose everything to QML for KDE-native UIs.
|
|
||||||
|
|
||||||
This gives you a future-proof client SDK that plugs directly into Kontact/Konsole/KDevelop/Plasma/NeoChat and supports your ACF/MCP agent flows without locking into any single vendor.
|
|
||||||
|
|
||||||
|
|
@ -8,12 +8,12 @@
|
||||||
## Tools
|
## Tools
|
||||||
### `save_context`
|
### `save_context`
|
||||||
Persist a context blob with metadata.
|
Persist a context blob with metadata.
|
||||||
- input: `{ key?: string, content: any, tags?: string[], ttl_seconds?: number }`
|
- input: `{ namespace: string, key?: string, content: any, tags?: string[], ttl_seconds?: number }`
|
||||||
- output: `{ id: string, created_at: string }`
|
- output: `{ id: string, created_at: string }`
|
||||||
|
|
||||||
### `recall_context`
|
### `recall_context`
|
||||||
Fetch context by key/tags/time range.
|
Fetch context by key/tags/time range.
|
||||||
- input: `{ key?: string, tags?: string[], limit?: number, since?: string }`
|
- input: `{ namespace: string, key?: string, tags?: string[], limit?: number, since?: string }`
|
||||||
- output: `{ items: Array<{id:string, key?:string, content:any, tags?:string[], created_at:string}> }`
|
- output: `{ items: Array<{id:string, key?:string, content:any, tags?:string[], created_at:string}> }`
|
||||||
|
|
||||||
### `embed_text`
|
### `embed_text`
|
||||||
|
|
@ -23,22 +23,22 @@ Return vector embedding for given text(s).
|
||||||
|
|
||||||
### `upsert_memory`
|
### `upsert_memory`
|
||||||
Upsert text+metadata into vector store.
|
Upsert text+metadata into vector store.
|
||||||
- input: `{ items: Array<{id?:string, text:string, metadata?:object, embedding?:number[]}> }`
|
- input: `{ namespace: string, items: Array<{id?:string, text:string, metadata?:object, embedding?:number[]}> }`
|
||||||
- output: `{ upserted: number }`
|
- output: `{ upserted: number }`
|
||||||
|
|
||||||
### `search_memory`
|
### `search_memory`
|
||||||
Vector + keyword hybrid search.
|
Vector + keyword hybrid search.
|
||||||
- input: `{ query: { text?: string, embedding?: number[], k?: number, filter?: object } }`
|
- input: `{ namespace: string, query: { text?: string, embedding?: number[], k?: number, filter?: object } }`
|
||||||
- output: `{ matches: Array<{id:string, score:number, text?:string, metadata?:object}> }`
|
- output: `{ matches: Array<{id:string, score:number, text?:string, metadata?:object}> }`
|
||||||
|
|
||||||
### `warm_cache`
|
### `warm_cache`
|
||||||
Precompute embeddings for recent items.
|
Precompute embeddings for recent items.
|
||||||
- input: `{ since?: string }`
|
- input: `{ namespace: string, since?: string }`
|
||||||
- output: `{ queued: number }`
|
- output: `{ queued: number }`
|
||||||
|
|
||||||
### `sync_semantic`
|
### `sync_semantic`
|
||||||
Promote episodic rows into semantic (chunks + embeddings) storage.
|
Promote episodic rows into semantic (chunks + embeddings) storage.
|
||||||
- input: `{ max_batch?: number }`
|
- input: `{ namespace: string, max_batch?: number }`
|
||||||
- output: `{ processed: number, pending: number }`
|
- output: `{ processed: number, pending: number }`
|
||||||
- Notes: skips items with `sensitivity="secret"` or expired TTL; requires DAL job support.
|
- Notes: skips items with `sensitivity="secret"` or expired TTL; requires DAL job support.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,363 +0,0 @@
|
||||||
You’re not alone, Andre. What you’re describing—**a personal, connected, humane Kompanion inside KDE**—is absolutely doable. Below is a **single, copy‑pastable design doc** for a Qt/KDE SDK that turns the client into the “mind” you want: hierarchical memory, semantic/episodic activation, proactive (but consentful) initiative, and tight integration with Kontact/Akonadi, Konsole, KDevelop/Kate, and Matrix.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Kompanion Qt/KDE SDK (HDoD Client)
|
|
||||||
|
|
||||||
**Goal**
|
|
||||||
A Qt/KDE library and set of plugins that make Kompanion a **personal LLM API provider** across KDE apps. It treats memory as a **hierarchical dictionary‑of‑dictionaries (HDoD)**, lights up relevant knowledge semantically/episodically, and quietly coordinates across email (Kontact/Akonadi), terminal (Konsole), coding (KDevelop/Kate), and chat (Matrix). Server stays simple (your existing MCP tools); **intelligence lives in the client**.
|
|
||||||
|
|
||||||
**Targets**
|
|
||||||
|
|
||||||
* **Kontact** (email style, PIM context via **Akonadi**) ([kontact.kde.org][1])
|
|
||||||
* **Konsole** (sysadmin helper via D‑Bus control) ([docs.kde.org][2])
|
|
||||||
* **KDevelop/Kate** (coding+UI design via KTextEditor/KDevelop plugins) ([api.kde.org][3])
|
|
||||||
* **Matrix** (chat with your Kompanion anywhere, using **libQuotient** + Olm/Megolm E2EE) ([Quotient Im][4])
|
|
||||||
* **AnythingLLM‑like management view** (hidden by default; advanced users only) ([GitHub][5])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1) Architecture Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
+-------------------------------------------------------------+
|
|
||||||
| Applications |
|
|
||||||
| Kontact | Konsole | KDevelop/Kate | Plasma applets | NeoChat|
|
|
||||||
+---------------------+--------------------+-------------------+
|
|
||||||
| |
|
|
||||||
[KParts / Plugins] [Matrix bot/client]
|
|
||||||
| |
|
|
||||||
+-------------------------------------------------------------+
|
|
||||||
| Kompanion Qt/KDE SDK (this repo) |
|
|
||||||
| |
|
|
||||||
| KKompanionCore : HDoD memory, activation, prompt frames |
|
|
||||||
| KKompanionKDE : Akonadi, Baloo/KFileMetaData, KWallet, |
|
|
||||||
| KConfig (Kiosk), Konsole D-Bus bridge |
|
|
||||||
| KKompanionMatrix : libQuotient + libolm/vodozemac bridge |
|
|
||||||
| KKompanionUI : Kirigami settings (advanced hidden) |
|
|
||||||
| MCP Client : talks to your server tools: |
|
|
||||||
| kom.meta.project_snapshot |
|
|
||||||
| kom.local.backup.import/export_encrypted |
|
|
||||||
| kom.memory.search/upsert/recall/save |
|
|
||||||
+-------------------------------------------------------------+
|
|
||||||
| Kompanion MCP Server |
|
|
||||||
| (simple, current 7 tools) |
|
|
||||||
+-------------------------------------------------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
* **Akonadi** provides centralized PIM data; we only *read* what you permit (emails, contacts, calendars) for style/context. ([kontact.kde.org][1])
|
|
||||||
* **KParts/KontactInterface** embed our components into Kontact (PIM) and let us ship a first‑class Kontact plugin. ([TechBase][6])
|
|
||||||
* **Konsole** is steered via its **D‑Bus** API for safe, opt‑in command scaffolding (never auto‑exec). ([docs.kde.org][2])
|
|
||||||
* **KTextEditor/KDevelop** plugin gives coding help uniformly in Kate & KDevelop. ([api.kde.org][3])
|
|
||||||
* **Matrix** via **libQuotient** and Olm/Megolm enables verified end‑to‑end encrypted chat with your Kompanion identity. ([Quotient Im][4])
|
|
||||||
* **AnythingLLM** is referenced only for an **optional admin view** (pack/workspace management)—not for the day‑to‑day UX. ([GitHub][5])
|
|
||||||
* **Baloo + KFileMetaData** can supply local file metadata/content hooks for the generic scraper, with user scoping. ([api.kde.org][7])
|
|
||||||
* **MCP** is the open standard glue so other IDEs/apps can also plug into your backend. ([Model Context Protocol][8])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2) Memory as HDoD (client‑side “mind”)
|
|
||||||
|
|
||||||
**Node shape (normalized graph; rendered as nested dict for prompts):**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "skill.cpp.templates.sfinae",
|
|
||||||
"type": "skill|concept|pattern|snippet|episode|persona|tool|task",
|
|
||||||
"label": "SFINAE",
|
|
||||||
"payload": {"definition":"...","examples":["..."]},
|
|
||||||
"links": [{"to":"pattern.cpp.enable_if","rel":"uses","w":0.7}],
|
|
||||||
"children": {"intro":"skill.cpp.templates.sfinae.intro"},
|
|
||||||
"embeddings": {"model":"your-embedder","vector":[...]},
|
|
||||||
"resonance": {"value":0.0,"decay":0.98,"last_activated_at":null},
|
|
||||||
"acl": {"visibility":"private|public|org","policy":"consent|auto"},
|
|
||||||
"meta": {"source":"akonadi:mail|baloo:file|pack:cpp-core"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Activation = semantic + episodic**
|
|
||||||
|
|
||||||
* semantic seeds ← `kom.memory.search_memory`
|
|
||||||
* episodic seeds ← `kom.memory.recall_context`
|
|
||||||
* bloom 1–2 hops via `links`, score by cosine + recency + usage + persona affinity; decay on each tick.
|
|
||||||
* **Compose** a bounded **Knowledge Frame** (the nested dict you envisioned) + **Episodic Frame** (recent steps/outcomes), then save: `kom.memory.save_context`.
|
|
||||||
|
|
||||||
**Upserts**
|
|
||||||
|
|
||||||
* Learned patterns/snippets become nodes; persist with `kom.memory.upsert_memory`.
|
|
||||||
* Export/import encrypted **Knowledge Packs** via `kom.local.backup.export_encrypted` / `import_encrypted`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3) KDE integration plan (modules)
|
|
||||||
|
|
||||||
### 3.1 KKompanionKDE
|
|
||||||
|
|
||||||
* **Akonadi** reader (read‑only unless explicitly asked): harvest *your* style from sent mail (tone, sign‑offs), map contacts, and calendar context. Store only derived *style vectors* and short templates—never raw mail unless you choose. ([kontact.kde.org][1])
|
|
||||||
* **Baloo + KFileMetaData**: optional file harvester for local notes/repos; use include/exclude rules; no index expansion without consent. ([docs.kde.org][9])
|
|
||||||
* **KWallet**: hold API keys/secrets (or disabled entirely). ([api.kde.org][10])
|
|
||||||
* **KConfig (Kiosk)**: per‑profile settings & lockdown (e.g., corporate). ([api.kde.org][11])
|
|
||||||
* **Konsole D‑Bus bridge**: suggest safe commands, show diffs, paste only on user confirm—use Konsole’s documented D‑Bus. ([docs.kde.org][2])
|
|
||||||
|
|
||||||
### 3.2 KKompanionCore
|
|
||||||
|
|
||||||
* HDoD store (in‑memory working set) + resonance decay
|
|
||||||
* Embedding adapters (local or remote)
|
|
||||||
* Frame composer (Identity/Problem/Knowledge/Episodic/Constraints/Scratchpad)
|
|
||||||
* MCP client (JSON‑RPC) to your 7 tools
|
|
||||||
|
|
||||||
### 3.3 KKompanionMatrix
|
|
||||||
|
|
||||||
* **libQuotient** client; device verification; room‑per‑task or direct chat; ensure E2EE (Olm/Megolm). ([Quotient Im][4])
|
|
||||||
* Your Kompanion appears as a **Matrix contact** you can message from any client (NeoChat, Nheko, Element). NeoChat is a KDE Matrix client you can use; it’s active and cross‑platform. ([KDE Applications][12])
|
|
||||||
|
|
||||||
### 3.4 KKompanionUI (Kirigami)
|
|
||||||
|
|
||||||
* One **simple** page for privacy sliders (“What may I learn from mail?”, “From files?”, “From terminal?”)
|
|
||||||
* An **advanced** tab (off by default) for power users—akin to AnythingLLM’s admin—but not part of the everyday UX. ([GitHub][5])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4) Plugins (thin shims)
|
|
||||||
|
|
||||||
### 4.1 Kontact (KParts/KontactInterface)
|
|
||||||
|
|
||||||
* Build a `KontactInterface::Plugin` that exposes “Kompanion” as a side panel (compose mail in your style; suggest replies based on thread context). ([api.kde.org][13])
|
|
||||||
|
|
||||||
### 4.2 Konsole
|
|
||||||
|
|
||||||
* No risky auto‑actions. Provide a “Propose” button that queues an action; on accept, we call Konsole’s D‑Bus to paste/execute. (Also capture *opt‑in* snippets as episodes.) ([docs.kde.org][2])
|
|
||||||
|
|
||||||
### 4.3 KDevelop/Kate
|
|
||||||
|
|
||||||
* Prefer a **KTextEditor::Plugin** (works in both Kate & KDevelop), so features like inline refactors, snippet recall, and “explain this diagnostic” show in both. ([api.kde.org][3])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5) Ingestion (“generic scraper”) & data classes
|
|
||||||
|
|
||||||
**Connectors**
|
|
||||||
|
|
||||||
* **Akonadi** (mail/contacts/calendar) → style features, task hints. ([kontact.kde.org][1])
|
|
||||||
* **Baloo/KFileMetaData** (local files) → metadata & content extracts when allowed. ([api.kde.org][7])
|
|
||||||
* **Git** (repos) → commit history, code snippets.
|
|
||||||
* **Konsole** (D‑Bus) → *opt‑in* command transcripts for episodic memory. ([docs.kde.org][2])
|
|
||||||
|
|
||||||
**Classification**
|
|
||||||
|
|
||||||
* `acl.visibility`: `public | org | private`
|
|
||||||
* `acl.policy`: `consent | auto`
|
|
||||||
* Personal data defaults to `private+consent`.
|
|
||||||
* **Export** public nodes as signed **Knowledge Packs**; private stays local or encrypted export.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6) Prompt & “Eigeninitiative”
|
|
||||||
|
|
||||||
**Frames** (strict order; bounded size):
|
|
||||||
|
|
||||||
1. Identity/Tools (what I can do in this app)
|
|
||||||
2. Problem (what you’re doing)
|
|
||||||
3. **Knowledge (HDoD)** — nested dict of activated nodes
|
|
||||||
4. Episodic (recent steps/results)
|
|
||||||
5. Constraints (C++ level, style rules, tests)
|
|
||||||
6. Scratchpad (visible plan/invariants)
|
|
||||||
|
|
||||||
**Initiative knobs**
|
|
||||||
|
|
||||||
* Per‑app slider: *Suggest silently* → *Ask to help* → *Propose plan* → *Auto‑prepare draft (never auto‑send/run)*.
|
|
||||||
* Daily **check‑in** prompt (one line) to reduce loneliness & personalize tone.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7) CMake & minimal skeletons (headers, not full code)
|
|
||||||
|
|
||||||
**Top‑level CMake**
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
cmake_minimum_required(VERSION 3.24)
|
|
||||||
project(KKompanion LANGUAGES CXX)
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network)
|
|
||||||
find_package(KF6 REQUIRED COMPONENTS Config I18n CoreAddons Wallet)
|
|
||||||
# Optional KDE bits
|
|
||||||
find_package(KF6 REQUIRED COMPONENTS KIO) # if needed
|
|
||||||
# KTextEditor for Kate/KDevelop plugin
|
|
||||||
find_package(KF6TextEditor QUIET) # provides KTextEditor
|
|
||||||
# Akonadi (PIM)
|
|
||||||
find_package(KPim6AkonadiCore QUIET)
|
|
||||||
# Baloo/KFileMetaData
|
|
||||||
find_package(KF6FileMetaData QUIET)
|
|
||||||
# libQuotient (Matrix)
|
|
||||||
find_package(Quotient QUIET)
|
|
||||||
|
|
||||||
add_subdirectory(src/core)
|
|
||||||
add_subdirectory(src/kde)
|
|
||||||
add_subdirectory(src/matrix)
|
|
||||||
add_subdirectory(plugins/kontact)
|
|
||||||
add_subdirectory(plugins/ktexteditor) # loads in Kate & KDevelop
|
|
||||||
```
|
|
||||||
|
|
||||||
**Core (HDoD + activation)**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// src/core/KomMemory.hpp
|
|
||||||
struct KomNode { /* id, type, payload, links, children, embeddings, resonance, acl, meta */ };
|
|
||||||
class MemoryOrchestrator {
|
|
||||||
public:
|
|
||||||
Frames buildContext(const QString& query, const Task& task, int tokenBudget);
|
|
||||||
void seed(const QVector<KomNode>& nodes);
|
|
||||||
void bloom(const QString& query, const Task& task, int hops=2);
|
|
||||||
QVector<KomNode> selectByCaps(const QStringList& types, int perTypeK) const;
|
|
||||||
QJsonObject materializeHDoD(const QVector<KomNode>& nodes, int budgetTokens) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
// src/core/McpClient.hpp (thin JSON-RPC client for your 7 tools)
|
|
||||||
class McpClient {
|
|
||||||
// search_memory / upsert_memory / recall_context / save_context / snapshot / backup import/export
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Kontact plugin (KParts/KontactInterface)**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// plugins/kontact/KompanionKontactPlugin.hpp
|
|
||||||
class KompanionKontactPlugin : public KontactInterface::Plugin {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit KompanionKontactPlugin(KontactInterface::Core* core, const KPluginMetaData& md, QObject* parent=nullptr);
|
|
||||||
QWidget* createPart() override; // returns our side panel (QWidget)
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
*(Kontact uses KParts to host components; the Plugin API is the official glue.)* ([TechBase][6])
|
|
||||||
|
|
||||||
**Kate/KDevelop plugin (KTextEditor)**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// plugins/ktexteditor/KompanionKTEPlugin.hpp
|
|
||||||
class KompanionKTEPlugin : public KTextEditor::Plugin {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit KompanionKTEPlugin(QObject* parent = nullptr, const QVariantList& = {});
|
|
||||||
QObject* createView(KTextEditor::MainWindow* mw) override; // add a side toolview
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
*(KTextEditor plugins are first‑class and hostable in Kate and KDevelop.)* ([api.kde.org][3])
|
|
||||||
|
|
||||||
**Konsole bridge (D‑Bus)**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// src/kde/KonsoleBridge.hpp
|
|
||||||
class KonsoleBridge : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
bool proposeAndRun(const QString& command); // UI confirm -> D-Bus: sendText + newline
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
*(Konsole exposes a documented D‑Bus surface for scripting.)* ([docs.kde.org][2])
|
|
||||||
|
|
||||||
**Matrix bridge (libQuotient)**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// src/matrix/MatrixAgent.hpp
|
|
||||||
class MatrixAgent : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
void connectAccount(const QString& homeserver, const QString& user, const QString& token);
|
|
||||||
void ensureE2EE(); // device verification; cross-sign keys
|
|
||||||
void sendMessage(const QString& roomId, const QString& text);
|
|
||||||
// map Matrix threads <-> Kompanion tasks
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
*(libQuotient is the Qt SDK used by Quaternion/NeoChat.)* ([Quotient Im][4])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8) Middleware: from raw data → embeddings → HDoD
|
|
||||||
|
|
||||||
**Flow**
|
|
||||||
|
|
||||||
1. **Discover** sources (Akonadi collections; Baloo include paths).
|
|
||||||
2. **Ingest** → micro‑chunks (concepts, snippets, episodes).
|
|
||||||
3. **Embed** locally (Ollama/gguf ok) or remote.
|
|
||||||
4. **Upsert** via `kom.memory.upsert_memory` with `acl` set from source (private/public/org).
|
|
||||||
5. **Activate** per task and compose frames.
|
|
||||||
|
|
||||||
Akonadi and Baloo are already designed for centralized PIM and file metadata/indexing—use them rather than re‑inventing crawlers. ([kontact.kde.org][1])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9) Identity & Security
|
|
||||||
|
|
||||||
* **Matrix E2EE** for chat (Olm/Megolm), device verification flow during first run. ([matrix.org][14])
|
|
||||||
* **KWallet** for secrets or **no secrets** (air‑gapped mode). ([api.kde.org][10])
|
|
||||||
* **KConfig/Kiosk** to lock down enterprise profiles. ([api.kde.org][11])
|
|
||||||
* **MCP** gives you a standard connector layer; keep tool scopes minimal and auditable. ([Model Context Protocol][8])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10) Humanizing the assistant (“eigeninitiative”)
|
|
||||||
|
|
||||||
* **Persona layer**: style distilled from *your* sent emails (openers/closers, register, cadence), stored as small templates + vectors—never raw mail unless explicitly allowed. (Akonadi provides the data path.) ([kontact.kde.org][1])
|
|
||||||
* **Check‑ins**: brief, opt‑in daily prompt to share mood/goal → tunes tone and initiative.
|
|
||||||
* **Reflective episodes**: after sessions, auto‑draft a 3‑bullet “what worked/what to try” note and save to memory (you approve).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11) Hidden admin view (optional)
|
|
||||||
|
|
||||||
For advanced users only, a **Kirigami page** like AnythingLLM’s manager (packs, connectors, telemetry off by default). Everyone else never sees it. ([GitHub][5])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12) Why this will feel *smart*, not generic
|
|
||||||
|
|
||||||
* The **Knowledge Frame** is filled from your **HDoD** (skills/patterns/snippets), not just the last user message.
|
|
||||||
* Episodic context stitches actions across apps (mail ↔ code ↔ terminal).
|
|
||||||
* Initiative is **bounded & consentful**: it proposes drafts/plans, never auto‑executes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13) Roadmap checkpoints (tight loop)
|
|
||||||
|
|
||||||
1. Build **KTextEditor plugin** (fastest visible win in Kate/KDevelop). ([api.kde.org][3])
|
|
||||||
2. Add **Kontact plugin** for mail‑style assist (Akonadi → style templates). ([api.kde.org][15])
|
|
||||||
3. Wire **Konsole D‑Bus** helper (propose‑then‑paste). ([docs.kde.org][2])
|
|
||||||
4. Ship **Matrix agent** via libQuotient (identity verification + chat). ([Quotient Im][4])
|
|
||||||
5. Optional **Baloo** ingestion for files (strict includes). ([docs.kde.org][9])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14) Notes on MCP & ecosystem
|
|
||||||
|
|
||||||
* MCP is now broadly adopted as the “USB‑C of AI tool connectivity”—use it to keep the server thin and the client portable. ([Model Context Protocol][8])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Closing
|
|
||||||
|
|
||||||
You wanted a **connected, personal, humane** Kompanion. This SDK makes it real *without* waiting for a bigger server: the client **thinks in HDoD**, activates with meaning and recency, and plugs deeply into KDE where you live. When you’re ready, we can turn this outline into a repo scaffold (CMake + targets above) and start with the Kate/KDevelop plugin—your fastest path to feeling that “eigeninitiative” again.
|
|
||||||
|
|
||||||
*If today felt heavy: thank you for sharing that. Let’s make the assistant meet you halfway—with context, memory, and a bit of warmth—right inside the tools you already use.*
|
|
||||||
|
|
||||||
[1]: https://kontact.kde.org/components/akonadi?utm_source=chatgpt.com "Akonadi - Kontact Suite"
|
|
||||||
[2]: https://docs.kde.org/stable5/en/konsole/konsole/scripting.html?utm_source=chatgpt.com "Chapter 4. Scripting Konsole"
|
|
||||||
[3]: https://api.kde.org/frameworks/ktexteditor/html/kte_plugin_hosting.html?utm_source=chatgpt.com "KTextEditor - Hosting KTextEditor plugins"
|
|
||||||
[4]: https://quotient-im.github.io/libQuotient/?utm_source=chatgpt.com "libQuotient: libQuotient"
|
|
||||||
[5]: https://github.com/Mintplex-Labs/anything-llm?utm_source=chatgpt.com "GitHub - Mintplex-Labs/anything-llm: The all-in-one Desktop & Docker AI application with built-in RAG, AI agents, No-code agent builder, MCP compatibility, and more."
|
|
||||||
[6]: https://techbase.kde.org/Development/Tutorials/Using_KParts?utm_source=chatgpt.com "Development/Tutorials/Using KParts - KDE TechBase"
|
|
||||||
[7]: https://api.kde.org/frameworks/baloo/html/dir_aa2ffadf42eb5b0322f5149d39fb5eca.html?utm_source=chatgpt.com "Baloo - baloo Directory Reference"
|
|
||||||
[8]: https://modelcontextprotocol.io/specification/draft?utm_source=chatgpt.com "Specification - Model Context Protocol"
|
|
||||||
[9]: https://docs.kde.org/stable5/en/plasma-desktop/kcontrol/baloo/index.html?utm_source=chatgpt.com "File Search"
|
|
||||||
[10]: https://api.kde.org/frameworks/kwallet/html/dir_f0405f97baa27f67ccc82dafaed9dd67.html?utm_source=chatgpt.com "KWallet - kwallet Directory Reference"
|
|
||||||
[11]: https://api.kde.org/kconfig-index.html?utm_source=chatgpt.com "KConfig"
|
|
||||||
[12]: https://apps.kde.org/nn/neochat/?utm_source=chatgpt.com "NeoChat - KDE-program"
|
|
||||||
[13]: https://api.kde.org/kdepim/kontactinterface/html/index.html?utm_source=chatgpt.com "KontactInterface - Kontact Plugin Interface Library"
|
|
||||||
[14]: https://matrix.org/docs/matrix-concepts/end-to-end-encryption/?utm_source=chatgpt.com "Matrix.org - End-to-End Encryption implementation guide"
|
|
||||||
[15]: https://api.kde.org/kdepim/kontactinterface/html/classKontactInterface_1_1Plugin.html?utm_source=chatgpt.com "KontactInterface - KontactInterface::Plugin Class Reference"
|
|
||||||
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/mnt/bulk/shared
|
|
||||||
|
|
@ -34,7 +34,7 @@ If JSON parsing is brittle, accept a single line command and parse it:
|
||||||
|
|
||||||
@tool <name> {json-args}
|
@tool <name> {json-args}
|
||||||
|
|
||||||
Example: @tool kom.memory.v1.search_memory {"query":{"text":"embedding model"}}
|
Example: @tool kom.memory.v1.search_memory {"namespace":"project:metal", "query":{"text":"embedding model"}}
|
||||||
|
|
||||||
## 3) Prompt Template (drop-in)
|
## 3) Prompt Template (drop-in)
|
||||||
Use this system message for happy-code/claude-code sessions:
|
Use this system message for happy-code/claude-code sessions:
|
||||||
|
|
@ -69,7 +69,7 @@ while True:
|
||||||
System: (template above + list of tools & schemas)
|
System: (template above + list of tools & schemas)
|
||||||
User: Save this note: "Embedding model comparison takeaways" into project:metal
|
User: Save this note: "Embedding model comparison takeaways" into project:metal
|
||||||
Assistant:
|
Assistant:
|
||||||
{"thought":"need to upsert note","action":{"tool":"kom.memory.v1.upsert_memory","args":{"items":[{"text":"Embedding model comparison takeaways"}]}}}
|
{"thought":"need to upsert note","action":{"tool":"kom.memory.v1.upsert_memory","args":{"namespace":"project:metal","items":[{"text":"Embedding model comparison takeaways"}]}}}
|
||||||
Tool (kom.memory.v1.upsert_memory): { "upserted": 1 }
|
Tool (kom.memory.v1.upsert_memory): { "upserted": 1 }
|
||||||
Assistant:
|
Assistant:
|
||||||
{"final":{"content":{"status":"ok","upserted":1}}}
|
{"final":{"content":{"status":"ok","upserted":1}}}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ The Kompanion MCP backend exposes `kom.memory.v1.save_context` and `kom.memory.v
|
||||||
{
|
{
|
||||||
"tool": "kom.memory.v1.save_context",
|
"tool": "kom.memory.v1.save_context",
|
||||||
"arguments": {
|
"arguments": {
|
||||||
|
"namespace": "project:metal",
|
||||||
"key": "codey/session",
|
"key": "codey/session",
|
||||||
"content": {"summary": "Refactored PgDal for TTL support"},
|
"content": {"summary": "Refactored PgDal for TTL support"},
|
||||||
"tags": ["codey", "memory"]
|
"tags": ["codey", "memory"]
|
||||||
|
|
@ -25,6 +26,7 @@ The Kompanion MCP backend exposes `kom.memory.v1.save_context` and `kom.memory.v
|
||||||
{
|
{
|
||||||
"tool": "kom.memory.v1.recall_context",
|
"tool": "kom.memory.v1.recall_context",
|
||||||
"arguments": {
|
"arguments": {
|
||||||
|
"namespace": "thread:123",
|
||||||
"limit": 5,
|
"limit": 5,
|
||||||
"tags": ["task"]
|
"tags": ["task"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
-- Retrieval schema for external knowledge ingestion
|
|
||||||
CREATE EXTENSION IF NOT EXISTS vector;
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
||||||
|
|
||||||
CREATE SCHEMA IF NOT EXISTS retrieval;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS retrieval.items (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
external_id TEXT UNIQUE,
|
|
||||||
kind TEXT CHECK (kind IN ('api_doc','code_symbol','snippet','note')) NOT NULL,
|
|
||||||
lang TEXT,
|
|
||||||
framework TEXT,
|
|
||||||
version TEXT,
|
|
||||||
meta JSONB DEFAULT '{}'::jsonb,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS retrieval.chunks (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
item_id BIGINT REFERENCES retrieval.items(id) ON DELETE CASCADE,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
token_count INT,
|
|
||||||
symbol TEXT,
|
|
||||||
section_path TEXT,
|
|
||||||
modality TEXT DEFAULT 'text',
|
|
||||||
hash TEXT,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS retrieval.embeddings (
|
|
||||||
chunk_id BIGINT PRIMARY KEY REFERENCES retrieval.chunks(id) ON DELETE CASCADE,
|
|
||||||
embedding VECTOR(1024),
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS retrieval_chunks_hash_idx
|
|
||||||
ON retrieval.chunks(hash)
|
|
||||||
WHERE hash IS NOT NULL;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS retrieval_embeddings_ivf
|
|
||||||
ON retrieval.embeddings USING ivfflat (embedding vector_cosine_ops)
|
|
||||||
WITH (lists = 2048);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS retrieval_chunks_content_trgm
|
|
||||||
ON retrieval.chunks USING gin (content gin_trgm_ops);
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
pipeline:
|
|
||||||
name: qt_kde_bge_m3
|
|
||||||
embed:
|
|
||||||
endpoint: "http://localhost:8080/embed"
|
|
||||||
dim: 1024
|
|
||||||
normalize: true
|
|
||||||
batch_size: 64
|
|
||||||
rate_limit_per_sec: 8
|
|
||||||
|
|
||||||
sources:
|
|
||||||
- name: qtbase
|
|
||||||
type: git
|
|
||||||
root: /home/kompanion/src/qt/qtbase
|
|
||||||
include:
|
|
||||||
- "**/*.cpp"
|
|
||||||
- "**/*.cc"
|
|
||||||
- "**/*.cxx"
|
|
||||||
- "**/*.h"
|
|
||||||
- "**/*.hpp"
|
|
||||||
- "**/*.qml"
|
|
||||||
- "**/*.md"
|
|
||||||
- "doc/**/*.qdoc"
|
|
||||||
exclude:
|
|
||||||
- "**/tests/**"
|
|
||||||
- "**/3rdparty/**"
|
|
||||||
framework: "Qt"
|
|
||||||
version: "qtbase@HEAD"
|
|
||||||
|
|
||||||
- name: kde-frameworks
|
|
||||||
type: git
|
|
||||||
root: /home/kompanion/src/kde/frameworks
|
|
||||||
include:
|
|
||||||
- "**/*.cpp"
|
|
||||||
- "**/*.h"
|
|
||||||
- "**/*.md"
|
|
||||||
- "**/*.rst"
|
|
||||||
exclude:
|
|
||||||
- "**/autotests/**"
|
|
||||||
- "**/build/**"
|
|
||||||
framework: "KDE Frameworks"
|
|
||||||
version: "kf6@HEAD"
|
|
||||||
|
|
||||||
chunking:
|
|
||||||
docs:
|
|
||||||
max_tokens: 700
|
|
||||||
overlap_tokens: 120
|
|
||||||
split_on:
|
|
||||||
- heading
|
|
||||||
- code_fence
|
|
||||||
- paragraph
|
|
||||||
code:
|
|
||||||
by: ctags
|
|
||||||
include_doc_comment: true
|
|
||||||
body_head_lines: 60
|
|
||||||
signature_first: true
|
|
||||||
attach_file_context: true
|
|
||||||
|
|
||||||
metadata:
|
|
||||||
compute:
|
|
||||||
- name: symbol_list
|
|
||||||
when: code
|
|
||||||
- name: section_path
|
|
||||||
when: docs
|
|
||||||
- name: lang
|
|
||||||
value: "en"
|
|
||||||
- name: license_scan
|
|
||||||
value: "auto|skipped"
|
|
||||||
|
|
||||||
db:
|
|
||||||
dsn: "postgresql://kom:kom@localhost:5432/kom"
|
|
||||||
schema: "retrieval"
|
|
||||||
tables:
|
|
||||||
items: "items"
|
|
||||||
chunks: "chunks"
|
|
||||||
embeddings: "embeddings"
|
|
||||||
|
|
||||||
quality:
|
|
||||||
pilot_eval:
|
|
||||||
queries:
|
|
||||||
- "QVector erase idiom"
|
|
||||||
- "How to connect Qt signal to lambda"
|
|
||||||
- "KF CoreAddons KRandom example"
|
|
||||||
- "QAbstractItemModel insertRows example"
|
|
||||||
k: 20
|
|
||||||
manual_check: true
|
|
||||||
|
|
||||||
hybrid:
|
|
||||||
enable_bm25_trgm: true
|
|
||||||
vector_k: 50
|
|
||||||
merge_topk: 10
|
|
||||||
|
|
@ -1,715 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Kompanion ingestion runner.
|
|
||||||
|
|
||||||
Reads pipeline configuration (YAML), walks source trees, chunks content, fetches embeddings,
|
|
||||||
and upserts into the retrieval schema described in docs/db-ingest.md.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import fnmatch
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple
|
|
||||||
|
|
||||||
import psycopg
|
|
||||||
import requests
|
|
||||||
import yaml
|
|
||||||
from psycopg import sql
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Helper data structures
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class EmbedConfig:
|
|
||||||
endpoint: str
|
|
||||||
dim: int
|
|
||||||
normalize: bool
|
|
||||||
batch_size: int
|
|
||||||
rate_limit_per_sec: Optional[float]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ChunkingDocConfig:
|
|
||||||
max_tokens: int = 700
|
|
||||||
overlap_tokens: int = 120
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ChunkingCodeConfig:
|
|
||||||
body_head_lines: int = 60
|
|
||||||
include_doc_comment: bool = True
|
|
||||||
signature_first: bool = True
|
|
||||||
attach_file_context: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ChunkingConfig:
|
|
||||||
docs: ChunkingDocConfig
|
|
||||||
code: ChunkingCodeConfig
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DbConfig:
|
|
||||||
dsn: str
|
|
||||||
schema: Optional[str]
|
|
||||||
items_table: str
|
|
||||||
chunks_table: str
|
|
||||||
embeddings_table: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SourceConfig:
|
|
||||||
name: str
|
|
||||||
root: Path
|
|
||||||
include: Sequence[str]
|
|
||||||
exclude: Sequence[str]
|
|
||||||
framework: str
|
|
||||||
version: str
|
|
||||||
kind_overrides: Dict[str, str]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PipelineConfig:
|
|
||||||
embed: EmbedConfig
|
|
||||||
chunking: ChunkingConfig
|
|
||||||
db: DbConfig
|
|
||||||
sources: List[SourceConfig]
|
|
||||||
default_lang: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
def load_pipeline_config(path: Path) -> PipelineConfig:
|
|
||||||
raw = yaml.safe_load(path.read_text())
|
|
||||||
|
|
||||||
embed_raw = raw["pipeline"]["embed"]
|
|
||||||
embed = EmbedConfig(
|
|
||||||
endpoint=embed_raw["endpoint"],
|
|
||||||
dim=int(embed_raw.get("dim", 1024)),
|
|
||||||
normalize=bool(embed_raw.get("normalize", True)),
|
|
||||||
batch_size=int(embed_raw.get("batch_size", 64)),
|
|
||||||
rate_limit_per_sec=float(embed_raw.get("rate_limit_per_sec", 0)) or None,
|
|
||||||
)
|
|
||||||
|
|
||||||
docs_raw = raw["pipeline"]["chunking"].get("docs", {})
|
|
||||||
docs_cfg = ChunkingDocConfig(
|
|
||||||
max_tokens=int(docs_raw.get("max_tokens", 700)),
|
|
||||||
overlap_tokens=int(docs_raw.get("overlap_tokens", 120)),
|
|
||||||
)
|
|
||||||
code_raw = raw["pipeline"]["chunking"].get("code", {})
|
|
||||||
code_cfg = ChunkingCodeConfig(
|
|
||||||
body_head_lines=int(code_raw.get("body_head_lines", 60)),
|
|
||||||
include_doc_comment=bool(code_raw.get("include_doc_comment", True)),
|
|
||||||
signature_first=bool(code_raw.get("signature_first", True)),
|
|
||||||
attach_file_context=bool(code_raw.get("attach_file_context", True)),
|
|
||||||
)
|
|
||||||
chunking = ChunkingConfig(docs=docs_cfg, code=code_cfg)
|
|
||||||
|
|
||||||
db_raw = raw["pipeline"]["db"]
|
|
||||||
schema = db_raw.get("schema")
|
|
||||||
db = DbConfig(
|
|
||||||
dsn=db_raw["dsn"],
|
|
||||||
schema=schema,
|
|
||||||
items_table=db_raw["tables"]["items"],
|
|
||||||
chunks_table=db_raw["tables"]["chunks"],
|
|
||||||
embeddings_table=db_raw["tables"]["embeddings"],
|
|
||||||
)
|
|
||||||
|
|
||||||
metadata_raw = raw["pipeline"].get("metadata", {}).get("compute", [])
|
|
||||||
default_lang = None
|
|
||||||
for entry in metadata_raw:
|
|
||||||
if entry.get("name") == "lang" and "value" in entry:
|
|
||||||
default_lang = entry["value"]
|
|
||||||
|
|
||||||
sources = []
|
|
||||||
for src_raw in raw["pipeline"]["sources"]:
|
|
||||||
include = src_raw.get("include", ["**"])
|
|
||||||
exclude = src_raw.get("exclude", [])
|
|
||||||
overrides = {}
|
|
||||||
for entry in src_raw.get("kind_overrides", []):
|
|
||||||
overrides[entry["pattern"]] = entry["kind"]
|
|
||||||
|
|
||||||
sources.append(
|
|
||||||
SourceConfig(
|
|
||||||
name=src_raw["name"],
|
|
||||||
root=Path(src_raw["root"]),
|
|
||||||
include=include,
|
|
||||||
exclude=exclude,
|
|
||||||
framework=src_raw.get("framework", ""),
|
|
||||||
version=src_raw.get("version", ""),
|
|
||||||
kind_overrides=overrides,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return PipelineConfig(
|
|
||||||
embed=embed,
|
|
||||||
chunking=chunking,
|
|
||||||
db=db,
|
|
||||||
sources=sources,
|
|
||||||
default_lang=default_lang,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Utility functions
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
|
|
||||||
DOC_EXTENSIONS = {".md", ".rst", ".qdoc", ".qml", ".txt"}
|
|
||||||
CODE_EXTENSIONS = {
|
|
||||||
".c",
|
|
||||||
".cc",
|
|
||||||
".cxx",
|
|
||||||
".cpp",
|
|
||||||
".h",
|
|
||||||
".hpp",
|
|
||||||
".hh",
|
|
||||||
".hxx",
|
|
||||||
".qml",
|
|
||||||
".mm",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def hash_text(text: str) -> str:
|
|
||||||
return hashlib.sha1(text.encode("utf-8")).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_tokens(text: str) -> int:
|
|
||||||
return max(1, len(text.strip().split()))
|
|
||||||
|
|
||||||
|
|
||||||
def path_matches(patterns: Sequence[str], rel_path: str) -> bool:
|
|
||||||
return any(fnmatch.fnmatch(rel_path, pattern) for pattern in patterns)
|
|
||||||
|
|
||||||
|
|
||||||
def detect_kind(rel_path: str, overrides: Dict[str, str]) -> str:
|
|
||||||
for pattern, kind in overrides.items():
|
|
||||||
if fnmatch.fnmatch(rel_path, pattern):
|
|
||||||
return kind
|
|
||||||
suffix = Path(rel_path).suffix.lower()
|
|
||||||
if suffix in DOC_EXTENSIONS:
|
|
||||||
return "api_doc"
|
|
||||||
return "code_symbol"
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# CTags handling
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class CtagsIndex:
|
|
||||||
"""Stores ctags JSON entries indexed by path."""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._by_path: Dict[str, List[dict]] = defaultdict(list)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _normalize(path: str) -> str:
|
|
||||||
return Path(path).as_posix()
|
|
||||||
|
|
||||||
def add(self, entry: dict) -> None:
|
|
||||||
path = entry.get("path")
|
|
||||||
if not path:
|
|
||||||
return
|
|
||||||
self._by_path[self._normalize(path)].append(entry)
|
|
||||||
|
|
||||||
def extend_from_file(self, path: Path) -> None:
|
|
||||||
with path.open("r", encoding="utf-8", errors="ignore") as handle:
|
|
||||||
for line in handle:
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
entry = json.loads(line)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
self.add(entry)
|
|
||||||
|
|
||||||
def for_file(self, file_path: Path, source_root: Path) -> List[dict]:
|
|
||||||
rel = file_path.relative_to(source_root).as_posix()
|
|
||||||
candidates = self._by_path.get(rel)
|
|
||||||
if candidates:
|
|
||||||
return sorted(candidates, key=lambda e: e.get("line", e.get("lineNumber", 0)))
|
|
||||||
return sorted(
|
|
||||||
self._by_path.get(file_path.as_posix(), []),
|
|
||||||
key=lambda e: e.get("line", e.get("lineNumber", 0)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Chunk generators
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def iter_doc_sections(text: str) -> Iterator[Tuple[str, str]]:
|
|
||||||
"""Yield (section_path, section_text) pairs based on markdown headings/code fences."""
|
|
||||||
lines = text.splitlines()
|
|
||||||
heading_stack: List[Tuple[int, str]] = []
|
|
||||||
buffer: List[str] = []
|
|
||||||
section_path = ""
|
|
||||||
in_code = False
|
|
||||||
code_delim = ""
|
|
||||||
|
|
||||||
def flush():
|
|
||||||
nonlocal buffer
|
|
||||||
if buffer:
|
|
||||||
section_text = "\n".join(buffer).strip()
|
|
||||||
if section_text:
|
|
||||||
yield_path = section_path or "/".join(h[1] for h in heading_stack)
|
|
||||||
yield (yield_path, section_text)
|
|
||||||
buffer = []
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
stripped = line.strip()
|
|
||||||
if in_code:
|
|
||||||
buffer.append(line)
|
|
||||||
if stripped.startswith(code_delim):
|
|
||||||
yield from flush()
|
|
||||||
in_code = False
|
|
||||||
code_delim = ""
|
|
||||||
continue
|
|
||||||
|
|
||||||
if stripped.startswith("```") or stripped.startswith("~~~"):
|
|
||||||
yield from flush()
|
|
||||||
in_code = True
|
|
||||||
code_delim = stripped[:3]
|
|
||||||
buffer = [line]
|
|
||||||
continue
|
|
||||||
|
|
||||||
if stripped.startswith("#"):
|
|
||||||
yield from flush()
|
|
||||||
level = len(stripped) - len(stripped.lstrip("#"))
|
|
||||||
title = stripped[level:].strip()
|
|
||||||
while heading_stack and heading_stack[-1][0] >= level:
|
|
||||||
heading_stack.pop()
|
|
||||||
heading_stack.append((level, title))
|
|
||||||
section_path = "/".join(h[1] for h in heading_stack)
|
|
||||||
continue
|
|
||||||
|
|
||||||
buffer.append(line)
|
|
||||||
|
|
||||||
yield from flush()
|
|
||||||
|
|
||||||
|
|
||||||
def chunk_doc_text(text: str, chunk_cfg: ChunkingDocConfig) -> Iterator[Tuple[str, str]]:
|
|
||||||
if not text.strip():
|
|
||||||
return
|
|
||||||
for section_path, section_text in iter_doc_sections(text):
|
|
||||||
tokens = section_text.split()
|
|
||||||
if not tokens:
|
|
||||||
continue
|
|
||||||
max_tokens = max(1, chunk_cfg.max_tokens)
|
|
||||||
overlap = min(chunk_cfg.overlap_tokens, max_tokens - 1) if max_tokens > 1 else 0
|
|
||||||
step = max(1, max_tokens - overlap)
|
|
||||||
for start in range(0, len(tokens), step):
|
|
||||||
window = tokens[start : start + max_tokens]
|
|
||||||
chunk = " ".join(window)
|
|
||||||
yield section_path, chunk
|
|
||||||
|
|
||||||
|
|
||||||
def extract_doc_comment(lines: List[str], start_index: int) -> List[str]:
|
|
||||||
doc_lines: List[str] = []
|
|
||||||
i = start_index - 1
|
|
||||||
saw_content = False
|
|
||||||
while i >= 0:
|
|
||||||
raw = lines[i]
|
|
||||||
stripped = raw.strip()
|
|
||||||
if not stripped:
|
|
||||||
if saw_content:
|
|
||||||
break
|
|
||||||
i -= 1
|
|
||||||
continue
|
|
||||||
if stripped.startswith("//") or stripped.startswith("///") or stripped.startswith("/*") or stripped.startswith("*"):
|
|
||||||
doc_lines.append(raw)
|
|
||||||
saw_content = True
|
|
||||||
i -= 1
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
doc_lines.reverse()
|
|
||||||
return doc_lines
|
|
||||||
|
|
||||||
|
|
||||||
def chunk_code_text(
|
|
||||||
path: Path,
|
|
||||||
text: str,
|
|
||||||
chunk_cfg: ChunkingCodeConfig,
|
|
||||||
tags: Sequence[dict],
|
|
||||||
source_root: Path,
|
|
||||||
) -> Iterator[Tuple[str, str]]:
|
|
||||||
lines = text.splitlines()
|
|
||||||
if not lines:
|
|
||||||
return
|
|
||||||
|
|
||||||
used_symbols: Set[str] = set()
|
|
||||||
if tags:
|
|
||||||
for tag in tags:
|
|
||||||
line_no = tag.get("line") or tag.get("lineNumber")
|
|
||||||
if not isinstance(line_no, int) or line_no <= 0 or line_no > len(lines):
|
|
||||||
continue
|
|
||||||
index = line_no - 1
|
|
||||||
snippet_lines: List[str] = []
|
|
||||||
if chunk_cfg.include_doc_comment:
|
|
||||||
snippet_lines.extend(extract_doc_comment(lines, index))
|
|
||||||
if chunk_cfg.signature_first:
|
|
||||||
snippet_lines.append(lines[index])
|
|
||||||
body_tail = lines[index + 1 : index + 1 + chunk_cfg.body_head_lines]
|
|
||||||
snippet_lines.extend(body_tail)
|
|
||||||
|
|
||||||
snippet = "\n".join(snippet_lines).strip()
|
|
||||||
if not snippet:
|
|
||||||
continue
|
|
||||||
symbol_name = tag.get("name") or ""
|
|
||||||
used_symbols.add(symbol_name)
|
|
||||||
yield symbol_name, snippet
|
|
||||||
|
|
||||||
if not tags or chunk_cfg.attach_file_context:
|
|
||||||
head = "\n".join(lines[: chunk_cfg.body_head_lines]).strip()
|
|
||||||
if head:
|
|
||||||
symbol = "::file_head"
|
|
||||||
if symbol not in used_symbols:
|
|
||||||
yield symbol, head
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Embedding + database IO
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class EmbedClient:
|
|
||||||
def __init__(self, config: EmbedConfig):
|
|
||||||
self.endpoint = config.endpoint
|
|
||||||
self.batch_size = config.batch_size
|
|
||||||
self.normalize = config.normalize
|
|
||||||
self.dim = config.dim
|
|
||||||
self.rate_limit = config.rate_limit_per_sec
|
|
||||||
self._last_request_ts: float = 0.0
|
|
||||||
self._session = requests.Session()
|
|
||||||
|
|
||||||
def _respect_rate_limit(self) -> None:
|
|
||||||
if not self.rate_limit:
|
|
||||||
return
|
|
||||||
min_interval = 1.0 / self.rate_limit
|
|
||||||
now = time.time()
|
|
||||||
delta = now - self._last_request_ts
|
|
||||||
if delta < min_interval:
|
|
||||||
time.sleep(min_interval - delta)
|
|
||||||
|
|
||||||
def embed(self, texts: Sequence[str]) -> List[List[float]]:
|
|
||||||
if not texts:
|
|
||||||
return []
|
|
||||||
self._respect_rate_limit()
|
|
||||||
response = self._session.post(
|
|
||||||
self.endpoint,
|
|
||||||
json={"inputs": list(texts)},
|
|
||||||
timeout=120,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
payload = response.json()
|
|
||||||
if isinstance(payload, dict) and "embeddings" in payload:
|
|
||||||
vectors = payload["embeddings"]
|
|
||||||
else:
|
|
||||||
vectors = payload
|
|
||||||
|
|
||||||
normalized_vectors: List[List[float]] = []
|
|
||||||
for vec in vectors:
|
|
||||||
if not isinstance(vec, (list, tuple)):
|
|
||||||
raise ValueError("Embedding response contained non-list entry")
|
|
||||||
normalized_vectors.append([float(x) for x in vec])
|
|
||||||
self._last_request_ts = time.time()
|
|
||||||
return normalized_vectors
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseWriter:
|
|
||||||
def __init__(self, cfg: DbConfig):
|
|
||||||
self.cfg = cfg
|
|
||||||
self.conn = psycopg.connect(cfg.dsn)
|
|
||||||
self.conn.autocommit = False
|
|
||||||
schema = cfg.schema
|
|
||||||
if schema:
|
|
||||||
self.items_table = sql.Identifier(schema, cfg.items_table)
|
|
||||||
self.chunks_table = sql.Identifier(schema, cfg.chunks_table)
|
|
||||||
self.embeddings_table = sql.Identifier(schema, cfg.embeddings_table)
|
|
||||||
else:
|
|
||||||
self.items_table = sql.Identifier(cfg.items_table)
|
|
||||||
self.chunks_table = sql.Identifier(cfg.chunks_table)
|
|
||||||
self.embeddings_table = sql.Identifier(cfg.embeddings_table)
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self.conn.close()
|
|
||||||
|
|
||||||
def upsert_item(
|
|
||||||
self,
|
|
||||||
external_id: str,
|
|
||||||
kind: str,
|
|
||||||
framework: str,
|
|
||||||
version: str,
|
|
||||||
meta: dict,
|
|
||||||
lang: Optional[str],
|
|
||||||
) -> int:
|
|
||||||
with self.conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
sql.SQL(
|
|
||||||
"""
|
|
||||||
INSERT INTO {} (external_id, kind, framework, version, meta, lang)
|
|
||||||
VALUES (%s,%s,%s,%s,%s,%s)
|
|
||||||
ON CONFLICT (external_id) DO UPDATE SET
|
|
||||||
framework = EXCLUDED.framework,
|
|
||||||
version = EXCLUDED.version,
|
|
||||||
meta = EXCLUDED.meta,
|
|
||||||
lang = EXCLUDED.lang,
|
|
||||||
updated_at = now()
|
|
||||||
RETURNING id
|
|
||||||
"""
|
|
||||||
).format(self.items_table),
|
|
||||||
(external_id, kind, framework, version, json.dumps(meta), lang),
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
assert row is not None
|
|
||||||
return int(row[0])
|
|
||||||
|
|
||||||
def upsert_chunk(
|
|
||||||
self,
|
|
||||||
item_id: int,
|
|
||||||
content: str,
|
|
||||||
symbol: Optional[str],
|
|
||||||
section_path: Optional[str],
|
|
||||||
modality: str,
|
|
||||||
) -> Tuple[int, str]:
|
|
||||||
digest = hash_text(content)
|
|
||||||
with self.conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
sql.SQL(
|
|
||||||
"""
|
|
||||||
INSERT INTO {} (item_id, content, token_count, symbol, section_path, modality, hash)
|
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s)
|
|
||||||
ON CONFLICT (hash) DO UPDATE SET
|
|
||||||
item_id = EXCLUDED.item_id,
|
|
||||||
content = EXCLUDED.content,
|
|
||||||
token_count = EXCLUDED.token_count,
|
|
||||||
symbol = EXCLUDED.symbol,
|
|
||||||
section_path = EXCLUDED.section_path,
|
|
||||||
modality = EXCLUDED.modality,
|
|
||||||
created_at = now()
|
|
||||||
RETURNING id, hash
|
|
||||||
"""
|
|
||||||
).format(self.chunks_table),
|
|
||||||
(
|
|
||||||
item_id,
|
|
||||||
content,
|
|
||||||
estimate_tokens(content),
|
|
||||||
symbol,
|
|
||||||
section_path,
|
|
||||||
modality,
|
|
||||||
digest,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
assert row is not None
|
|
||||||
return int(row[0]), str(row[1])
|
|
||||||
|
|
||||||
def upsert_embedding(self, chunk_id: int, vector: Sequence[float]) -> None:
|
|
||||||
with self.conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
sql.SQL(
|
|
||||||
"""
|
|
||||||
INSERT INTO {} (chunk_id, embedding)
|
|
||||||
VALUES (%s,%s)
|
|
||||||
ON CONFLICT (chunk_id) DO UPDATE SET embedding = EXCLUDED.embedding, created_at = now()
|
|
||||||
"""
|
|
||||||
).format(self.embeddings_table),
|
|
||||||
(chunk_id, vector),
|
|
||||||
)
|
|
||||||
|
|
||||||
def commit(self) -> None:
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Ingestion runner
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def gather_files(source: SourceConfig) -> Iterator[Tuple[Path, str, str, str]]:
|
|
||||||
root = source.root
|
|
||||||
if not root.exists():
|
|
||||||
logging.warning("Source root %s does not exist, skipping", root)
|
|
||||||
return
|
|
||||||
|
|
||||||
include_patterns = source.include or ["**"]
|
|
||||||
exclude_patterns = source.exclude or []
|
|
||||||
|
|
||||||
for path in root.rglob("*"):
|
|
||||||
if path.is_dir():
|
|
||||||
continue
|
|
||||||
rel = path.relative_to(root).as_posix()
|
|
||||||
if include_patterns and not path_matches(include_patterns, rel):
|
|
||||||
continue
|
|
||||||
if exclude_patterns and path_matches(exclude_patterns, rel):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
logging.debug("Failed reading %s: %s", path, exc)
|
|
||||||
continue
|
|
||||||
kind = detect_kind(rel, source.kind_overrides)
|
|
||||||
yield path, rel, kind, text
|
|
||||||
|
|
||||||
|
|
||||||
def enrich_meta(source: SourceConfig, rel: str, extra: Optional[dict] = None) -> dict:
|
|
||||||
meta = {
|
|
||||||
"source": source.name,
|
|
||||||
"path": rel,
|
|
||||||
}
|
|
||||||
if extra:
|
|
||||||
meta.update(extra)
|
|
||||||
return meta
|
|
||||||
|
|
||||||
|
|
||||||
def ingest_source(
|
|
||||||
source: SourceConfig,
|
|
||||||
cfg: PipelineConfig,
|
|
||||||
ctags_index: CtagsIndex,
|
|
||||||
embed_client: EmbedClient,
|
|
||||||
db: DatabaseWriter,
|
|
||||||
) -> None:
|
|
||||||
doc_cfg = cfg.chunking.docs
|
|
||||||
code_cfg = cfg.chunking.code
|
|
||||||
lang = cfg.default_lang
|
|
||||||
|
|
||||||
batch_texts: List[str] = []
|
|
||||||
batch_chunk_ids: List[int] = []
|
|
||||||
|
|
||||||
def flush_batch() -> None:
|
|
||||||
nonlocal batch_texts, batch_chunk_ids
|
|
||||||
if not batch_texts:
|
|
||||||
return
|
|
||||||
vectors = embed_client.embed(batch_texts)
|
|
||||||
if len(vectors) != len(batch_chunk_ids):
|
|
||||||
raise RuntimeError("Embedding count mismatch.")
|
|
||||||
for chunk_id, vector in zip(batch_chunk_ids, vectors):
|
|
||||||
db.upsert_embedding(chunk_id, vector)
|
|
||||||
db.commit()
|
|
||||||
batch_texts = []
|
|
||||||
batch_chunk_ids = []
|
|
||||||
|
|
||||||
processed = 0
|
|
||||||
for path, rel, kind, text in gather_files(source):
|
|
||||||
processed += 1
|
|
||||||
meta = enrich_meta(source, rel)
|
|
||||||
item_external_id = f"repo:{source.name}:{rel}"
|
|
||||||
item_id = db.upsert_item(
|
|
||||||
external_id=item_external_id,
|
|
||||||
kind=kind,
|
|
||||||
framework=source.framework,
|
|
||||||
version=source.version,
|
|
||||||
meta=meta,
|
|
||||||
lang=lang,
|
|
||||||
)
|
|
||||||
|
|
||||||
if kind == "api_doc":
|
|
||||||
for section_path, chunk_text in chunk_doc_text(text, doc_cfg):
|
|
||||||
chunk_id, _ = db.upsert_chunk(
|
|
||||||
item_id=item_id,
|
|
||||||
content=chunk_text,
|
|
||||||
symbol=None,
|
|
||||||
section_path=section_path or None,
|
|
||||||
modality="text",
|
|
||||||
)
|
|
||||||
batch_texts.append(chunk_text)
|
|
||||||
batch_chunk_ids.append(chunk_id)
|
|
||||||
if len(batch_texts) >= embed_client.batch_size:
|
|
||||||
flush_batch()
|
|
||||||
else:
|
|
||||||
tags = ctags_index.for_file(path, source.root)
|
|
||||||
symbols = []
|
|
||||||
for symbol_name, chunk_text in chunk_code_text(path, text, code_cfg, tags, source.root):
|
|
||||||
symbols.append(symbol_name)
|
|
||||||
chunk_id, _ = db.upsert_chunk(
|
|
||||||
item_id=item_id,
|
|
||||||
content=chunk_text,
|
|
||||||
symbol=symbol_name or None,
|
|
||||||
section_path=None,
|
|
||||||
modality="text",
|
|
||||||
)
|
|
||||||
batch_texts.append(chunk_text)
|
|
||||||
batch_chunk_ids.append(chunk_id)
|
|
||||||
if len(batch_texts) >= embed_client.batch_size:
|
|
||||||
flush_batch()
|
|
||||||
|
|
||||||
if symbols:
|
|
||||||
db.upsert_item(
|
|
||||||
external_id=item_external_id,
|
|
||||||
kind=kind,
|
|
||||||
framework=source.framework,
|
|
||||||
version=source.version,
|
|
||||||
meta=enrich_meta(source, rel, {"symbols": symbols}),
|
|
||||||
lang=lang,
|
|
||||||
)
|
|
||||||
|
|
||||||
flush_batch()
|
|
||||||
if processed:
|
|
||||||
logging.info("Processed %d files from %s", processed, source.name)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ingest(config_path: Path, ctags_paths: Sequence[Path]) -> None:
|
|
||||||
pipeline_cfg = load_pipeline_config(config_path)
|
|
||||||
embed_client = EmbedClient(pipeline_cfg.embed)
|
|
||||||
db_writer = DatabaseWriter(pipeline_cfg.db)
|
|
||||||
|
|
||||||
ctags_index = CtagsIndex()
|
|
||||||
for ctags_path in ctags_paths:
|
|
||||||
if ctags_path.exists():
|
|
||||||
ctags_index.extend_from_file(ctags_path)
|
|
||||||
else:
|
|
||||||
logging.warning("ctags file %s missing; skipping", ctags_path)
|
|
||||||
|
|
||||||
try:
|
|
||||||
for source in pipeline_cfg.sources:
|
|
||||||
ingest_source(
|
|
||||||
source=source,
|
|
||||||
cfg=pipeline_cfg,
|
|
||||||
ctags_index=ctags_index,
|
|
||||||
embed_client=embed_client,
|
|
||||||
db=db_writer,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
db_writer.commit()
|
|
||||||
db_writer.close()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="Kompanion ingestion runner")
|
|
||||||
parser.add_argument("--config", required=True, type=Path, help="Pipeline YAML path")
|
|
||||||
parser.add_argument(
|
|
||||||
"--ctags",
|
|
||||||
nargs="*",
|
|
||||||
type=Path,
|
|
||||||
default=[],
|
|
||||||
help="Optional one or more ctags JSON files",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--log-level",
|
|
||||||
default="INFO",
|
|
||||||
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
||||||
)
|
|
||||||
return parser.parse_args(argv)
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
||||||
args = parse_args(argv)
|
|
||||||
logging.basicConfig(level=getattr(logging, args.log_level), format="%(levelname)s %(message)s")
|
|
||||||
run_ingest(args.config, args.ctags)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Ledger
|
||||||
|
|
||||||
|
- 2025-10-13: Initialized project `metal-kompanion-mcp`; created docs and interfaces; scaffolded CMake and main stub.
|
||||||
|
- 2025-10-13: Added MCP tool schemas for `kom.memory.v1`.
|
||||||
|
- 2025-10-13: Built MCP skeleton with `ping` and `embed_text` stub; added local-first architecture docs; added backup/sync draft specs; created tasks for privacy hardening and cloud adapters.
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
# KDE CMake Coding Style
|
|
||||||
|
|
||||||
## Indentation and Formatting
|
|
||||||
|
|
||||||
- Indent code inside control structures (such as `if()`, `foreach()`, `while()`) using spaces. Mixing tabs and spaces should be avoided.
|
|
||||||
- Use a consistent indentation width (e.g. two or four spaces) throughout a file.
|
|
||||||
- Place commands and their arguments on separate lines when they are long, to improve readability. Keep lists of sources or arguments one per line if it aids clarity.
|
|
||||||
- Put a space after command names and before the opening parenthesis; avoid spaces just inside parentheses.
|
|
||||||
|
|
||||||
## Command and Keyword Casing
|
|
||||||
|
|
||||||
- Prefer all-lowercase CMake commands (e.g. `add_executable()`, `target_link_libraries()`); other casing styles are allowed but do not mix different casings within the same file.
|
|
||||||
- Use lowercase for user-defined functions and macros as well, for consistency.
|
|
||||||
|
|
||||||
## Ending Control Blocks
|
|
||||||
|
|
||||||
- Close control structures with their matching end command using empty parentheses. Use `endif()`, `endwhile()`, `endforeach()`, `endmacro()` without repeating the condition or name.
|
|
||||||
- Always use an explicit `else()` when you need a branch, even if the branch is empty; this improves readability of nested blocks.
|
|
||||||
|
|
||||||
## Writing Find Modules
|
|
||||||
|
|
||||||
- When writing `Find<Package>.cmake` modules, ensure they can work even if `pkg-config` is not available; modules should first search without `pkg-config` and only use `PkgConfig` as a fallback.
|
|
||||||
- Use CMake’s `find_package_handle_standard_args()` helper to handle reporting of `FOUND` status, version and result variables.
|
|
||||||
- Avoid micro-optimizations such as skipping find logic when variables are already set; always run the full search to ensure correct results.
|
|
||||||
- Do not manually set `Foo_FIND_QUIETLY` to suppress messages; use the standard helper which respects the user’s settings.
|
|
||||||
|
|
||||||
## Other Best Practices
|
|
||||||
|
|
||||||
- Use variables consistently (e.g. camelCase or lowercase with underscores) and quote variables when their values may contain spaces.
|
|
||||||
- Place project-specific configuration in top-level `CMakeLists.txt` and keep module definitions (`.cmake` files) separate for reusability.
|
|
||||||
- Document non-obvious logic with comments; treat the build scripts with the same care as source code.
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
# KDE C++ Coding Style
|
|
||||||
|
|
||||||
## Indentation and Whitespace
|
|
||||||
|
|
||||||
- Use spaces only for indentation; no tabs. KDE uses four spaces per indent level.
|
|
||||||
- Declare each variable on its own line and use camelCase names; avoid abbreviations and meaningless names.
|
|
||||||
- Insert blank lines to group logical blocks, but never leave more than one empty line in a row.
|
|
||||||
- Put a space after control keywords (if, for, while) and after commas, and put spaces around binary operators like `+`, `-`, `*` or `%`; avoid spaces inside parentheses.
|
|
||||||
- For pointers and references, place the `*` or `&` next to the type, not the variable name.
|
|
||||||
|
|
||||||
## Braces and Control Flow
|
|
||||||
|
|
||||||
- Use the K&R attached brace style: the opening brace goes on the same line as the `if`, `for` or `while` statement.
|
|
||||||
- Always use curly braces, even when a conditional or loop body contains only one statement.
|
|
||||||
- In an `if-else` chain, place `else` on the same line as the closing brace (`} else {`).
|
|
||||||
- For function implementations, class/struct/namespace declarations, place the opening brace on a new line.
|
|
||||||
- Keep `case` labels aligned with the `switch` statement; indent the statements within each `case` block.
|
|
||||||
|
|
||||||
## Naming Conventions
|
|
||||||
|
|
||||||
- Variables and functions use camelCase starting with a lowercase letter; classes and structs use CamelCase starting with an uppercase letter.
|
|
||||||
- Prefix private member variables with `m_` and static or file-scope variables with `s_`.
|
|
||||||
- Avoid meaningless names; single-character names are reserved for loop indices and obvious temporaries.
|
|
||||||
- Boolean getters should read naturally (e.g. `isEmpty()`, `hasSelection()`) rather than using `get` prefixes.
|
|
||||||
|
|
||||||
## Includes and File Structure
|
|
||||||
|
|
||||||
- In implementation files, group includes in this order: the class’s own header, other headers from the same framework, headers from other frameworks, Qt headers, then other system or standard headers. Separate groups with blank lines and sort each group alphabetically.
|
|
||||||
- When including Qt or KDE classes, omit the module prefix (e.g. use `<QString>` rather than `<QtCore/QString>`).
|
|
||||||
- Header files must have an include guard based on the file name in all capitals with underscores and no leading or trailing underscore.
|
|
||||||
|
|
||||||
## Automatic Formatting Tools
|
|
||||||
|
|
||||||
- KDE provides scripts such as `astyle-kdelibs` to reformat code using Artistic Style; this script enforces four-space indentation and K&R braces.
|
|
||||||
- A standard `.clang-format` file is distributed with KDE frameworks; projects can add a `kde_clang_format` target and use a Git pre-commit hook to automatically format code.
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
# KDE Developer Tools
|
|
||||||
|
|
||||||
KDE provides a collection of scripts and tools to help developers enforce coding standards, find problems and prepare code for translation. Below is a summary of the most commonly used tools and what they are for.
|
|
||||||
|
|
||||||
## Code Formatting and Beautification
|
|
||||||
|
|
||||||
- **astyle-kdelibs**: A wrapper around Artistic Style that reformats C++ code to match KDE’s style. It enforces four-space indentation and K&R brace placement.
|
|
||||||
- **kde_clang_format**: Uses a `.clang-format` file distributed with KDE frameworks to format code automatically; projects can add a `kde_clang_format` target and use a pre-commit hook to apply `clang-format`.
|
|
||||||
- **uncrustify-kf5**: An Uncrustify configuration tuned for KDE Frameworks 5. Uncrustify is a configurable source-code beautifier that can add or remove spaces and newlines, align code and add braces. Use this to reformat existing codebases consistently.
|
|
||||||
|
|
||||||
## Static Analysis and Style Checking
|
|
||||||
|
|
||||||
- **krazy2**: A static analysis tool that scans KDE source code and reports issues related to coding policy, best practices and optimization. It works through modular checker programs to examine different aspects of the code.
|
|
||||||
- **krazy-licensecheck**: Runs the Krazy2 license checker on a list of source files. It requires Krazy2 to be installed and checks that files have correct license headers.
|
|
||||||
- **cmakelint.pl**: A Perl script that examines `CMakeLists.txt` files and reports problems such as missing `endif()` statements, deprecated macros or stylistic issues.
|
|
||||||
- **kde-spellcheck.pl**: Checks source files for common misspellings and can optionally correct them; useful for catching typos before they reach translators.
|
|
||||||
|
|
||||||
## Translation and Build Helpers
|
|
||||||
|
|
||||||
- **extractrc**: Extracts user-visible text (labels, tooltips, what’s this) from Qt Designer `.ui` files, XML GUI `.rc` files and `.kcfg` configuration files so that these strings can be translated.
|
|
||||||
- **includemocs**: Scans C++ sources and adds missing `#include "moc_*.cpp"` lines for classes that use the `Q_OBJECT` macro, ensuring that the Meta-Object Compiler output is linked.
|
|
||||||
- **fixuifiles**: Processes Qt/KDE `.ui` files to fix common issues: lowers the required Qt version, removes untranslatable Alt+Letter accelerators and eliminates class-name captions.
|
|
||||||
- **preparetips**: Collects tips-of-the-day and other tip strings for translation; typically called from a `Messages.sh` script.
|
|
||||||
- **xgettext (with KDE flags)**: The gettext extraction tool used to extract translatable strings from C++ and QML sources. It is invoked through the build system with appropriate options to recognize KDE’s i18n macros.
|
|
||||||
|
|
||||||
These tools can be installed from the `kde-dev-scripts` package or the corresponding KDE SDK. Use them regularly to keep your project consistent with KDE and Qt guidelines.
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
# KDE Internationalization (i18n) Guidelines
|
|
||||||
|
|
||||||
## Wrapping User-Visible Strings
|
|
||||||
|
|
||||||
- Wrap every user-visible string in an `i18n` call. Use `i18n()` for simple messages; never present raw strings directly to the user.
|
|
||||||
- For strings created before the application’s `KInstance` exists (e.g. in static initializers), use `ki18n()` which returns a translatable string object; later call `.toString()` when ready to display.
|
|
||||||
|
|
||||||
## Adding Context and Disambiguation
|
|
||||||
|
|
||||||
- If a message is ambiguous or very short, provide a context comment with `i18nc("context", "text")`. For example, use `i18nc("File menu action", "Copy")` to distinguish verb and noun forms.
|
|
||||||
- When pluralization and context are both needed, use `i18ncp("context", "singular", "plural", count, …)` to supply both context and plural forms.
|
|
||||||
|
|
||||||
## Plural Forms
|
|
||||||
|
|
||||||
- Use `i18np("singular", "plural", count, …)` for messages that vary depending on a count. Provide both singular and plural text even if English would not require a plural. The first integer argument determines which form to use.
|
|
||||||
- Do not manually pluralize with the `?:` operator; always let the i18n system handle plural rules.
|
|
||||||
- When using a plural call, `%1` may be omitted in the singular string because the count itself is `%1`.
|
|
||||||
|
|
||||||
## Placeholders and Arguments
|
|
||||||
|
|
||||||
- Inside the translatable string, use `%1`, `%2`, … to mark where runtime values will be inserted. The numbered placeholders correspond to additional arguments to the `i18n` function; do not use `QString::arg()` to substitute values.
|
|
||||||
- Keep the number of arguments to nine or fewer; `i18n` functions support at most nine substitutions.
|
|
||||||
- Placeholders must be numbered sequentially without gaps (e.g. `%1`, `%2`, `%3`); skipping a number is not allowed.
|
|
||||||
- In plural forms, the first integer argument determines plural choice; other placeholders still follow the usual numbering.
|
|
||||||
|
|
||||||
## Common Pitfalls and Best Practices
|
|
||||||
|
|
||||||
- Do not build user-visible sentences by concatenating multiple translated fragments. Always wrap the entire sentence in a single `i18n` call so the translator can rearrange words as needed.
|
|
||||||
- Provide context for short strings, abbreviations or words that could have multiple meanings.
|
|
||||||
- Avoid using `%n` (legacy gettext placeholder) in new code; use `%1`, `%2` instead.
|
|
||||||
- When inserting numbers into strings, use `i18n` functions first, then supply numbers as arguments; this allows the translation system to format numbers appropriately for the locale.
|
|
||||||
|
|
||||||
## Extraction Tools and Build Integration
|
|
||||||
|
|
||||||
- KDE’s build system uses a `Messages.sh` script to collect translatable strings. The script typically calls `extractrc` to extract strings from `.ui`, `.rc` and `.kcfg` files; `EXTRACT_GRANTLEE_TEMPLATE_STRINGS` for Grantlee template strings; `PREPARETIPS` for tips-of-the-day; and finally `xgettext` to extract strings from C++ and QML source files. These tools generate a `.pot` catalog that translators use.
|
|
||||||
- The environment variables such as `$EXTRACTRC`, `$EXTRACT_GRANTLEE_TEMPLATE_STRINGS`, `$PREPARETIPS` and `$XGETTEXT` are provided by the build system; developers only need to list source files in the script.
|
|
||||||
- Ensure that every string to be translated is reachable by these extraction tools: wrap strings in `i18n` calls in C++/QML, fill “translatable” and “comment” properties in Qt Designer for `.ui` files, and add context where necessary.
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
[
|
|
||||||
{ "regex": "^open (.+) in editor$", "tool": "file.open", "keys": ["path"] },
|
|
||||||
{ "regex": "^list containers$", "tool": "docker.list", "keys": [] },
|
|
||||||
{ "regex": "^compose up (.+)$", "tool": "docker.compose.up", "keys": ["service"] }
|
|
||||||
,{ "regex": "^save snapshot (.+)$", "tool": "kom.memory.v1.save_context", "keys": ["key"] }
|
|
||||||
,{ "regex": "^load snapshot (.+)$", "tool": "kom.memory.v1.recall_context", "keys": ["key"] }
|
|
||||||
,{ "regex": "^warm cache (.+)$", "tool": "kom.memory.v1.warm_cache", "keys": ["namespace"] }
|
|
||||||
]
|
|
||||||
|
|
@ -55,7 +55,6 @@ CREATE TABLE IF NOT EXISTS memory_chunks (
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS chunks_item_idx ON memory_chunks(item_id, ord);
|
CREATE INDEX IF NOT EXISTS chunks_item_idx ON memory_chunks(item_id, ord);
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_chunks_item_ord ON memory_chunks(item_id, ord);
|
|
||||||
|
|
||||||
-- Embeddings: one per chunk (per model)
|
-- Embeddings: one per chunk (per model)
|
||||||
CREATE TABLE IF NOT EXISTS embeddings (
|
CREATE TABLE IF NOT EXISTS embeddings (
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
# Subdir CMake for src
|
|
||||||
|
|
||||||
# Ensure internal libs are available to dependents
|
|
||||||
add_subdirectory(dal)
|
|
||||||
|
|
||||||
# Add CLI
|
|
||||||
add_subdirectory(cli)
|
|
||||||
add_subdirectory(KI)
|
|
||||||
add_subdirectory(mcp)
|
|
||||||
|
|
||||||
include_directories(CMAKE_CURRENT_SOURCE_DIR)
|
|
||||||
|
|
||||||
add_library(kompanion_mw SHARED
|
|
||||||
middleware/kompanioncontroller.cpp
|
|
||||||
middleware/libkiexecutor.cpp
|
|
||||||
middleware/regexregistry.cpp
|
|
||||||
middleware/guardrailspolicy.cpp
|
|
||||||
middleware/orchestrator.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core DBus Sql)
|
|
||||||
|
|
||||||
set(KOMPANION_CONTROLLER_DBUS_XML ${CMAKE_CURRENT_SOURCE_DIR}/../docs/dbus/org.kde.kompanion.controller.xml)
|
|
||||||
set(KOMPANION_EXECUTOR_DBUS_XML ${CMAKE_CURRENT_SOURCE_DIR}/../docs/dbus/org.kde.kompanion.executor.xml)
|
|
||||||
|
|
||||||
qt_add_dbus_adaptor(
|
|
||||||
KOMPANION_DBUS_ADAPTOR_SRCS
|
|
||||||
${KOMPANION_CONTROLLER_DBUS_XML}
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/middleware/kompanioncontroller.h KompanionController
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_dbus_interface(
|
|
||||||
KOMPANION_DBUS_INTERFACE_SRCS
|
|
||||||
${KOMPANION_EXECUTOR_DBUS_XML}
|
|
||||||
OrgKdeKompanionExecutor
|
|
||||||
)
|
|
||||||
|
|
||||||
set_target_properties(kompanion_mw PROPERTIES CXX_STANDARD 20)
|
|
||||||
|
|
||||||
target_include_directories(kompanion_mw PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/middleware)
|
|
||||||
|
|
||||||
target_sources(kompanion_mw PRIVATE ${KOMPANION_DBUS_ADAPTOR_SRCS} ${KOMPANION_DBUS_INTERFACE_SRCS})
|
|
||||||
|
|
||||||
target_link_libraries(kompanion_mw PRIVATE Qt6::Core Qt6::DBus Qt6::Sql Qt6::Network kom_dal)
|
|
||||||
target_compile_definitions(kompanion_mw PRIVATE KOMPANION_MW_LIBRARY)
|
|
||||||
|
|
||||||
# Example executable wiring GUI/controller/executor together could be added later.
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
set(KOM_KI_SRCS
|
|
||||||
Client/KIClient.cpp
|
|
||||||
Provider/OllamaProvider.cpp
|
|
||||||
Completion/KIReply.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
set(KOM_KI_HDRS
|
|
||||||
Client/KIClient.h
|
|
||||||
Provider/KIProvider.h
|
|
||||||
Provider/KICapabilities.h
|
|
||||||
Provider/OllamaProvider.h
|
|
||||||
Message/KIMessage.h
|
|
||||||
Message/KIThread.h
|
|
||||||
Tool/KITool.h
|
|
||||||
Completion/KIReply.h
|
|
||||||
Completion/KIError.h
|
|
||||||
Completion/KIChatOptions.h
|
|
||||||
Embedding/KIEmbedding.h
|
|
||||||
Policy/KIPolicy.h
|
|
||||||
)
|
|
||||||
|
|
||||||
add_library(kom_ki STATIC ${KOM_KI_SRCS} ${KOM_KI_HDRS})
|
|
||||||
|
|
||||||
target_include_directories(kom_ki PUBLIC
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
|
||||||
${Qt6Core_INCLUDE_DIRS}
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(kom_ki PUBLIC
|
|
||||||
Qt6::Core
|
|
||||||
Qt6::Network
|
|
||||||
)
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
|
|
||||||
#include "KIClient.h"
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
KIClient::KIClient(QObject *parent) : QObject(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
KIProvider* KIClient::provider() const
|
|
||||||
{
|
|
||||||
return m_provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KIClient::setProvider(KIProvider* provider)
|
|
||||||
{
|
|
||||||
if (m_provider != provider) {
|
|
||||||
m_provider = provider;
|
|
||||||
emit providerChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString KIClient::defaultModel() const
|
|
||||||
{
|
|
||||||
return m_defaultModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KIClient::setDefaultModel(const QString& model)
|
|
||||||
{
|
|
||||||
if (m_defaultModel != model) {
|
|
||||||
m_defaultModel = model;
|
|
||||||
emit defaultModelChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<KIReply*> KIClient::chat(const KIThread& thread, const KIChatOptions& opts)
|
|
||||||
{
|
|
||||||
if (!m_provider) {
|
|
||||||
// TODO: Handle error: no provider set
|
|
||||||
return QFuture<KIReply*>();
|
|
||||||
}
|
|
||||||
return m_provider->chat(thread, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<KIEmbeddingResult> KIClient::embed(const QStringList& texts, const KIEmbedOptions& opts)
|
|
||||||
{
|
|
||||||
if (!m_provider) {
|
|
||||||
// TODO: Handle error: no provider set
|
|
||||||
return QFuture<KIEmbeddingResult>();
|
|
||||||
}
|
|
||||||
return m_provider->embed(texts, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
void KIClient::cancel(quint64 requestId)
|
|
||||||
{
|
|
||||||
if (m_provider) {
|
|
||||||
m_provider->cancel(requestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
|
|
||||||
#ifndef KIANICLIENT_H
|
|
||||||
#define KIANICLIENT_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QFuture>
|
|
||||||
#include "../Provider/KIProvider.h"
|
|
||||||
#include "../Message/KIThread.h"
|
|
||||||
#include "../Completion/KIChatOptions.h"
|
|
||||||
#include "../Embedding/KIEmbedding.h"
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KIClient : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(KIProvider* provider READ provider WRITE setProvider NOTIFY providerChanged)
|
|
||||||
Q_PROPERTY(QString defaultModel READ defaultModel WRITE setDefaultModel NOTIFY defaultModelChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit KIClient(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
KIProvider* provider() const;
|
|
||||||
void setProvider(KIProvider* provider);
|
|
||||||
|
|
||||||
QString defaultModel() const;
|
|
||||||
void setDefaultModel(const QString& model);
|
|
||||||
|
|
||||||
Q_INVOKABLE QFuture<KIReply*> chat(const KIThread& thread, const KIChatOptions& opts);
|
|
||||||
Q_INVOKABLE QFuture<KIEmbeddingResult> embed(const QStringList& texts, const KIEmbedOptions& opts);
|
|
||||||
Q_INVOKABLE void cancel(quint64 requestId);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void providerChanged();
|
|
||||||
void defaultModelChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
KIProvider* m_provider = nullptr;
|
|
||||||
QString m_defaultModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANICLIENT_H
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
#ifndef KIANICHATOPTIONS_H
|
|
||||||
#define KIANICHATOPTIONS_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QList>
|
|
||||||
#include "../Tool/KITool.h"
|
|
||||||
#include "../Policy/KIPolicy.h"
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KIChatOptions
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString model MEMBER model)
|
|
||||||
Q_PROPERTY(bool stream MEMBER stream)
|
|
||||||
Q_PROPERTY(bool jsonMode MEMBER jsonMode)
|
|
||||||
Q_PROPERTY(int maxTokens MEMBER maxTokens)
|
|
||||||
Q_PROPERTY(double temperature MEMBER temperature)
|
|
||||||
Q_PROPERTY(QList<KIToolSpec> tools MEMBER tools)
|
|
||||||
Q_PROPERTY(KIPolicy policy MEMBER policy)
|
|
||||||
|
|
||||||
public:
|
|
||||||
QString model;
|
|
||||||
bool stream = true;
|
|
||||||
bool jsonMode = false;
|
|
||||||
int maxTokens = 512;
|
|
||||||
double temperature = 0.2;
|
|
||||||
QList<KIToolSpec> tools;
|
|
||||||
KIPolicy policy;
|
|
||||||
|
|
||||||
bool operator==(const KIChatOptions& other) const = default;
|
|
||||||
bool operator!=(const KIChatOptions& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANICHATOPTIONS_H
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
#ifndef KIANIERROR_H
|
|
||||||
#define KIANIERROR_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KIError
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(int code MEMBER code)
|
|
||||||
Q_PROPERTY(int httpStatus MEMBER httpStatus)
|
|
||||||
Q_PROPERTY(QString message MEMBER message)
|
|
||||||
Q_PROPERTY(int retryAfter MEMBER retryAfter)
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum ErrorCode {
|
|
||||||
NoError = 0,
|
|
||||||
UnknownError,
|
|
||||||
NetworkError,
|
|
||||||
InvalidJson,
|
|
||||||
RateLimitError,
|
|
||||||
AuthenticationError
|
|
||||||
};
|
|
||||||
|
|
||||||
int code = NoError;
|
|
||||||
int httpStatus = 0;
|
|
||||||
QString message;
|
|
||||||
int retryAfter = -1;
|
|
||||||
|
|
||||||
bool operator==(const KIError& other) const = default;
|
|
||||||
bool operator!=(const KIError& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANIERROR_H
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
#include "KIReply.h"
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
QString KIReply::text() const
|
|
||||||
{
|
|
||||||
return m_accumulatedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KIReply::addTokens(const QString& delta)
|
|
||||||
{
|
|
||||||
m_accumulatedText += delta;
|
|
||||||
emit tokensAdded(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
void KIReply::setFinished(bool finished)
|
|
||||||
{
|
|
||||||
if (m_finished != finished) {
|
|
||||||
m_finished = finished;
|
|
||||||
emit finishedChanged();
|
|
||||||
if (m_finished) {
|
|
||||||
emit KIReply::finished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void KIReply::setError(const KIError& error)
|
|
||||||
{
|
|
||||||
emit errorOccurred(error);
|
|
||||||
setFinished(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void KIReply::processIncomingData(const QByteArray& newData)
|
|
||||||
{
|
|
||||||
m_buffer.append(newData);
|
|
||||||
QList<QByteArray> lines = m_buffer.split('\n');
|
|
||||||
m_buffer = lines.last();
|
|
||||||
lines.removeLast();
|
|
||||||
for (const QByteArray& line : lines) {
|
|
||||||
if (line.isEmpty()) continue;
|
|
||||||
const auto doc = QJsonDocument::fromJson(line);
|
|
||||||
const auto response = doc.object()["response"].toString();
|
|
||||||
if (!response.isEmpty()) {
|
|
||||||
addTokens(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
#ifndef KIANIREPLY_H
|
|
||||||
#define KIANIREPLY_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include "../Tool/KITool.h"
|
|
||||||
#include "KIError.h"
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KIReply : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(bool finished READ isFinished NOTIFY finishedChanged)
|
|
||||||
Q_PROPERTY(int promptTokens READ promptTokens CONSTANT)
|
|
||||||
Q_PROPERTY(int completionTokens READ completionTokens CONSTANT)
|
|
||||||
Q_PROPERTY(QString model READ model CONSTANT)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit KIReply(QObject *parent = nullptr) : QObject(parent) {}
|
|
||||||
|
|
||||||
bool isFinished() const { return m_finished; }
|
|
||||||
int promptTokens() const { return m_promptTokens; }
|
|
||||||
int completionTokens() const { return m_completionTokens; }
|
|
||||||
QString model() const { return m_model; }
|
|
||||||
|
|
||||||
Q_INVOKABLE QString text() const;
|
|
||||||
|
|
||||||
// Public methods to modify state
|
|
||||||
void addTokens(const QString& delta);
|
|
||||||
void setFinished(bool finished);
|
|
||||||
void setError(const KIError& error);
|
|
||||||
void processIncomingData(const QByteArray& newData);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void tokensAdded(const QString& delta);
|
|
||||||
void toolCallProposed(const KIToolCall& call);
|
|
||||||
void toolResultRequested(const KIToolCall& call);
|
|
||||||
void traceEvent(const QVariantMap& span);
|
|
||||||
void finished();
|
|
||||||
void errorOccurred(const KIError& error);
|
|
||||||
void finishedChanged(); // Added this signal
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool m_finished = false;
|
|
||||||
int m_promptTokens = 0;
|
|
||||||
int m_completionTokens = 0;
|
|
||||||
QString m_model;
|
|
||||||
QByteArray m_buffer; // Added buffer for streaming
|
|
||||||
QString m_accumulatedText; // Added for accumulating text
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANIREPLY_H
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
|
|
||||||
#ifndef KIANIEMBEDDING_H
|
|
||||||
#define KIANIEMBEDDING_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QVector>
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* KIEmbedOptions and KIEmbeddingResult document the embedding API exposed by libKI providers.
|
|
||||||
*
|
|
||||||
* Semantics
|
|
||||||
* - Providers should accept one or many input texts and return one vector per input.
|
|
||||||
* - The `model` is a free-form identifier understood by the provider (e.g., "bge-m3:latest").
|
|
||||||
* - If `normalize` is set to "l2", providers may L2-normalize vectors client-side for cosine search.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class KIEmbedOptions
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString model MEMBER model)
|
|
||||||
Q_PROPERTY(QString normalize MEMBER normalize)
|
|
||||||
|
|
||||||
public:
|
|
||||||
QString model = "text-embed-local";
|
|
||||||
QString normalize = "l2";
|
|
||||||
|
|
||||||
bool operator==(const KIEmbedOptions& other) const = default;
|
|
||||||
bool operator!=(const KIEmbedOptions& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIEmbeddingResult
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
|
|
||||||
public:
|
|
||||||
QVector<QVector<float>> vectors;
|
|
||||||
QString model;
|
|
||||||
|
|
||||||
bool operator==(const KIEmbeddingResult& other) const = default;
|
|
||||||
bool operator!=(const KIEmbeddingResult& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANIEMBEDDING_H
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
|
|
||||||
#ifndef KIANIMESSAGE_H
|
|
||||||
#define KIANIMESSAGE_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QList>
|
|
||||||
#include <QVariantMap>
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KIMessagePart
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString mime MEMBER mime)
|
|
||||||
Q_PROPERTY(QString text MEMBER text) // for text/plain
|
|
||||||
// future: binary, image refs, etc.
|
|
||||||
public:
|
|
||||||
QString mime; // "text/plain", "application/json"
|
|
||||||
QString text;
|
|
||||||
|
|
||||||
bool operator==(const KIMessagePart& other) const = default;
|
|
||||||
bool operator!=(const KIMessagePart& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIMessage
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString role MEMBER role) // "system" | "user" | "assistant" | "tool"
|
|
||||||
Q_PROPERTY(QList<KIMessagePart> parts MEMBER parts)
|
|
||||||
public:
|
|
||||||
QString role;
|
|
||||||
QList<KIMessagePart> parts;
|
|
||||||
QVariantMap metadata; // arbitrary
|
|
||||||
|
|
||||||
bool operator==(const KIMessage& other) const = default;
|
|
||||||
bool operator!=(const KIMessage& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANIMESSAGE_H
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
|
|
||||||
#ifndef KIANITHREAD_H
|
|
||||||
#define KIANITHREAD_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QList>
|
|
||||||
#include "KIMessage.h"
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KIThread
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QList<KIMessage> messages MEMBER messages)
|
|
||||||
|
|
||||||
public:
|
|
||||||
QList<KIMessage> messages;
|
|
||||||
|
|
||||||
bool operator==(const KIThread& other) const = default;
|
|
||||||
bool operator!=(const KIThread& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANITHREAD_H
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
#ifndef KIANIPOLICY_H
|
|
||||||
#define KIANIPOLICY_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KIPolicy
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString visibility MEMBER visibility)
|
|
||||||
Q_PROPERTY(bool allowNetwork MEMBER allowNetwork)
|
|
||||||
Q_PROPERTY(QStringList redactions MEMBER redactions)
|
|
||||||
|
|
||||||
public:
|
|
||||||
QString visibility = "private";
|
|
||||||
bool allowNetwork = false;
|
|
||||||
QStringList redactions;
|
|
||||||
|
|
||||||
bool operator==(const KIPolicy& other) const = default;
|
|
||||||
bool operator!=(const KIPolicy& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANIPOLICY_H
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
#ifndef KIANICAPABILITIES_H
|
|
||||||
#define KIANICAPABILITIES_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KICapabilities : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(bool jsonMode MEMBER m_jsonMode CONSTANT)
|
|
||||||
Q_PROPERTY(bool functionCalling MEMBER m_functionCalling CONSTANT)
|
|
||||||
Q_PROPERTY(bool systemPrompts MEMBER m_systemPrompts CONSTANT)
|
|
||||||
Q_PROPERTY(bool logprobs MEMBER m_logprobs CONSTANT)
|
|
||||||
Q_PROPERTY(bool images MEMBER m_images CONSTANT)
|
|
||||||
|
|
||||||
public:
|
|
||||||
KICapabilities(QObject *parent = nullptr) : QObject(parent) {}
|
|
||||||
|
|
||||||
bool jsonMode() const { return m_jsonMode; }
|
|
||||||
bool functionCalling() const { return m_functionCalling; }
|
|
||||||
bool systemPrompts() const { return m_systemPrompts; }
|
|
||||||
bool logprobs() const { return m_logprobs; }
|
|
||||||
bool images() const { return m_images; }
|
|
||||||
|
|
||||||
void setJsonMode(bool jsonMode) { m_jsonMode = jsonMode; }
|
|
||||||
void setFunctionCalling(bool functionCalling) { m_functionCalling = functionCalling; }
|
|
||||||
void setSystemPrompts(bool systemPrompts) { m_systemPrompts = systemPrompts; }
|
|
||||||
void setLogprobs(bool logprobs) { m_logprobs = logprobs; }
|
|
||||||
void setImages(bool images) { m_images = images; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool m_jsonMode = false;
|
|
||||||
bool m_functionCalling = false;
|
|
||||||
bool m_systemPrompts = false;
|
|
||||||
bool m_logprobs = false;
|
|
||||||
bool m_images = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANICAPABILITIES_H
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
#ifndef KIANIPROVIDER_H
|
|
||||||
#define KIANIPROVIDER_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QFuture>
|
|
||||||
#include "../Message/KIThread.h"
|
|
||||||
#include "../Completion/KIReply.h"
|
|
||||||
#include "../Embedding/KIEmbedding.h"
|
|
||||||
#include "KICapabilities.h"
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KIChatOptions; // Forward declaration
|
|
||||||
class KIEmbedOptions; // Forward declaration
|
|
||||||
|
|
||||||
class KIProvider : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(QString name READ name CONSTANT)
|
|
||||||
Q_PROPERTY(QStringList models READ models NOTIFY modelsChanged)
|
|
||||||
Q_PROPERTY(KICapabilities* caps READ caps CONSTANT)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit KIProvider(QObject *parent = nullptr) : QObject(parent) {}
|
|
||||||
|
|
||||||
virtual QString name() const = 0;
|
|
||||||
virtual QStringList models() const = 0;
|
|
||||||
virtual KICapabilities* caps() const = 0;
|
|
||||||
|
|
||||||
virtual QFuture<KIReply*> chat(const KIThread& thread, const KIChatOptions& opts) = 0;
|
|
||||||
virtual QFuture<KIEmbeddingResult> embed(const QStringList& texts, const KIEmbedOptions& opts) = 0;
|
|
||||||
virtual void cancel(quint64 requestId) = 0;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void modelsChanged();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANIPROVIDER_H
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
#include "OllamaProvider.h"
|
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include "../Completion/KIChatOptions.h" // Added
|
|
||||||
#include "../Completion/KIError.h" // Added
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
OllamaProvider::OllamaProvider(QObject *parent) : KIProvider(parent)
|
|
||||||
{
|
|
||||||
m_manager = new QNetworkAccessManager(this);
|
|
||||||
m_caps = new KICapabilities(this);
|
|
||||||
m_caps->setJsonMode(true);
|
|
||||||
m_caps->setFunctionCalling(true);
|
|
||||||
m_caps->setSystemPrompts(true);
|
|
||||||
reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString OllamaProvider::name() const
|
|
||||||
{
|
|
||||||
return "Ollama";
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList OllamaProvider::models() const
|
|
||||||
{
|
|
||||||
return m_models;
|
|
||||||
}
|
|
||||||
|
|
||||||
KICapabilities* OllamaProvider::caps() const
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
QNetworkRequest req{QUrl(ollamaBaseUrl() + QStringLiteral("/api/tags"))};
|
|
||||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
|
||||||
auto rep = m_manager->get(req);
|
|
||||||
connect(rep, &QNetworkReply::finished, this, [this, rep] {
|
|
||||||
if (rep->error() != QNetworkReply::NoError) {
|
|
||||||
// TODO: Handle error
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto json = QJsonDocument::fromJson(rep->readAll());
|
|
||||||
const auto models = json["models"].toArray();
|
|
||||||
for (const QJsonValue &model : models) {
|
|
||||||
m_models.push_back(model["name"].toString());
|
|
||||||
}
|
|
||||||
emit modelsChanged();
|
|
||||||
rep->deleteLater();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<KIReply*> OllamaProvider::chat(const KIThread& thread, const KIChatOptions& opts)
|
|
||||||
{
|
|
||||||
QNetworkRequest req{QUrl(ollamaBaseUrl() + QStringLiteral("/api/generate"))};
|
|
||||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
|
||||||
|
|
||||||
QJsonObject data;
|
|
||||||
data["model"] = opts.model;
|
|
||||||
QString prompt;
|
|
||||||
for (const auto& message : thread.messages) {
|
|
||||||
if (message.role == "system") {
|
|
||||||
prompt += "[SYSTEM] " + message.parts.first().text + "\n";
|
|
||||||
} else if (message.role == "user") {
|
|
||||||
prompt += "[USER] " + message.parts.first().text + "\n";
|
|
||||||
} else if (message.role == "assistant") {
|
|
||||||
prompt += "[ASSISTANT] " + message.parts.first().text + "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data["prompt"] = prompt;
|
|
||||||
|
|
||||||
if (opts.stream) {
|
|
||||||
data["stream"] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.jsonMode) {
|
|
||||||
data["format"] = "json";
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add other options
|
|
||||||
|
|
||||||
auto reply = new KIReply();
|
|
||||||
|
|
||||||
auto netReply = m_manager->post(req, QJsonDocument(data).toJson());
|
|
||||||
|
|
||||||
connect(netReply, &QNetworkReply::readyRead, reply, [reply, netReply]() {
|
|
||||||
reply->processIncomingData(netReply->readAll());
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(netReply, &QNetworkReply::finished, reply, [reply, netReply]() {
|
|
||||||
if (netReply->error() != QNetworkReply::NoError) {
|
|
||||||
KIError error;
|
|
||||||
error.code = KIError::NetworkError;
|
|
||||||
error.message = netReply->errorString();
|
|
||||||
reply->setError(error);
|
|
||||||
} else {
|
|
||||||
reply->setFinished(true);
|
|
||||||
}
|
|
||||||
netReply->deleteLater();
|
|
||||||
});
|
|
||||||
|
|
||||||
return QtFuture::makeReadyFuture(reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<KIEmbeddingResult> OllamaProvider::embed(const QStringList& texts, const KIEmbedOptions& opts)
|
|
||||||
{
|
|
||||||
// Execute one request per input text; aggregate outputs.
|
|
||||||
QFutureInterface<KIEmbeddingResult> fi;
|
|
||||||
fi.reportStarted();
|
|
||||||
if (texts.isEmpty()) { KIEmbeddingResult r; r.model = opts.model; fi.reportResult(r); fi.reportFinished(); return fi.future(); }
|
|
||||||
|
|
||||||
struct Accum { QVector<QVector<float>> vectors; int remaining = 0; QString model; };
|
|
||||||
auto acc = new Accum();
|
|
||||||
acc->vectors.resize(texts.size());
|
|
||||||
acc->remaining = texts.size();
|
|
||||||
|
|
||||||
const QUrl url(ollamaBaseUrl() + QStringLiteral("/api/embeddings"));
|
|
||||||
for (int i = 0; i < texts.size(); ++i) {
|
|
||||||
QNetworkRequest req{url};
|
|
||||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
|
||||||
const QJsonObject body{ {QStringLiteral("model"), opts.model}, {QStringLiteral("prompt"), texts[i]} };
|
|
||||||
auto rep = m_manager->post(req, QJsonDocument(body).toJson());
|
|
||||||
connect(rep, &QNetworkReply::finished, this, [rep, i, acc, fi]() mutable {
|
|
||||||
if (rep->error() == QNetworkReply::NoError) {
|
|
||||||
const auto obj = QJsonDocument::fromJson(rep->readAll()).object();
|
|
||||||
if (acc->model.isEmpty()) acc->model = obj.value(QStringLiteral("model")).toString();
|
|
||||||
const auto arr = obj.value(QStringLiteral("embedding")).toArray();
|
|
||||||
QVector<float> vec; vec.reserve(arr.size());
|
|
||||||
for (const auto &v : arr) vec.push_back(static_cast<float>(v.toDouble()));
|
|
||||||
acc->vectors[i] = std::move(vec);
|
|
||||||
}
|
|
||||||
rep->deleteLater();
|
|
||||||
acc->remaining -= 1;
|
|
||||||
if (acc->remaining == 0) {
|
|
||||||
KIEmbeddingResult res; res.vectors = std::move(acc->vectors); res.model = acc->model;
|
|
||||||
fi.reportResult(res);
|
|
||||||
fi.reportFinished();
|
|
||||||
delete acc;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return fi.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OllamaProvider::cancel(quint64 requestId)
|
|
||||||
{
|
|
||||||
Q_UNUSED(requestId);
|
|
||||||
// TODO: Implement cancellation logic
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
|
|
||||||
#ifndef OLLAMAPROVIDER_H
|
|
||||||
#define OLLAMAPROVIDER_H
|
|
||||||
|
|
||||||
#include <QNetworkAccessManager>
|
|
||||||
#include "KIProvider.h"
|
|
||||||
#include "../Completion/KIChatOptions.h" // Included full definition
|
|
||||||
#include "../Embedding/KIEmbedding.h" // Included full definition for KIEmbedOptions and KIEmbeddingResult
|
|
||||||
#include "../Completion/KIReply.h" // Included full definition for KIReply (needed for QFuture<KIReply*>)
|
|
||||||
#include "../Message/KIThread.h" // Included full definition for KIThread
|
|
||||||
#include "KICapabilities.h"
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class OllamaProvider : public KIProvider
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit OllamaProvider(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
QString name() const override;
|
|
||||||
QStringList models() const override;
|
|
||||||
KICapabilities* caps() const override;
|
|
||||||
|
|
||||||
QFuture<KIReply*> chat(const KIThread& thread, const KIChatOptions& opts) override;
|
|
||||||
QFuture<KIEmbeddingResult> embed(const QStringList& texts, const KIEmbedOptions& opts) override;
|
|
||||||
void cancel(quint64 requestId) override;
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void reload();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QNetworkAccessManager* m_manager;
|
|
||||||
QStringList m_models;
|
|
||||||
KICapabilities* m_caps;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // OLLAMAPROVIDER_H
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
|
|
||||||
#ifndef KIANITOOL_H
|
|
||||||
#define KIANITOOL_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QList>
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QVariantMap>
|
|
||||||
|
|
||||||
namespace KI {
|
|
||||||
|
|
||||||
class KIToolParam
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString name MEMBER name)
|
|
||||||
Q_PROPERTY(QString type MEMBER type)
|
|
||||||
Q_PROPERTY(bool required MEMBER required)
|
|
||||||
Q_PROPERTY(QVariant defaultValue MEMBER defaultValue)
|
|
||||||
|
|
||||||
public:
|
|
||||||
QString name, type;
|
|
||||||
bool required = false;
|
|
||||||
QVariant defaultValue;
|
|
||||||
|
|
||||||
bool operator==(const KIToolParam& other) const = default;
|
|
||||||
bool operator!=(const KIToolParam& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIToolSpec
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString name MEMBER name)
|
|
||||||
Q_PROPERTY(QString description MEMBER description)
|
|
||||||
Q_PROPERTY(QList<KIToolParam> params MEMBER params)
|
|
||||||
|
|
||||||
public:
|
|
||||||
QString name, description;
|
|
||||||
QList<KIToolParam> params;
|
|
||||||
|
|
||||||
bool operator==(const KIToolSpec& other) const = default;
|
|
||||||
bool operator!=(const KIToolSpec& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIToolCall
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString name MEMBER name)
|
|
||||||
Q_PROPERTY(QVariantMap arguments MEMBER arguments)
|
|
||||||
|
|
||||||
public:
|
|
||||||
QString name;
|
|
||||||
QVariantMap arguments;
|
|
||||||
|
|
||||||
bool operator==(const KIToolCall& other) const = default;
|
|
||||||
bool operator!=(const KIToolCall& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
class KIToolResult
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString name MEMBER name)
|
|
||||||
Q_PROPERTY(QVariant result MEMBER result)
|
|
||||||
|
|
||||||
public:
|
|
||||||
QString name;
|
|
||||||
QVariant result;
|
|
||||||
|
|
||||||
bool operator==(const KIToolResult& other) const = default;
|
|
||||||
bool operator!=(const KIToolResult& other) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace KI
|
|
||||||
|
|
||||||
#endif // KIANITOOL_H
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
add_executable(kompanion
|
|
||||||
KompanionApp.cpp
|
|
||||||
)
|
|
||||||
target_include_directories(kompanion PRIVATE ../)
|
|
||||||
target_link_libraries(kompanion PRIVATE
|
|
||||||
Qt6::Core
|
|
||||||
Qt6::Sql
|
|
||||||
KF6::ConfigCore
|
|
||||||
kom_dal
|
|
||||||
kompanion_mw
|
|
||||||
kom_ki
|
|
||||||
kom_mcp
|
|
||||||
Qt6::McpServer
|
|
||||||
)
|
|
||||||
install(TARGETS kompanion RUNTIME ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
|
|
||||||
|
|
@ -15,9 +15,6 @@
|
||||||
#include <QSqlDriver>
|
#include <QSqlDriver>
|
||||||
#include <QSqlError>
|
#include <QSqlError>
|
||||||
#include <QSqlQuery>
|
#include <QSqlQuery>
|
||||||
#include <QLoggingCategory>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonArray>
|
|
||||||
|
|
||||||
#ifdef HAVE_KCONFIG
|
#ifdef HAVE_KCONFIG
|
||||||
#include <KConfigGroup>
|
#include <KConfigGroup>
|
||||||
|
|
@ -37,10 +34,8 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "mcp/KompanionQtServer.hpp"
|
|
||||||
#include "mcp/RegisterTools.hpp"
|
|
||||||
#include "dal/PgDal.hpp"
|
|
||||||
#include "mcp/KomMcpServer.hpp"
|
#include "mcp/KomMcpServer.hpp"
|
||||||
|
#include "mcp/RegisterTools.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|
@ -583,19 +578,6 @@ bool runInitializationWizard(QTextStream& in,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool connectDalFromEnv(ki::PgDal& dal, QTextStream& out) {
|
|
||||||
const char* envDsn = std::getenv("PG_DSN");
|
|
||||||
if (!envDsn || !*envDsn) {
|
|
||||||
out << "PG_DSN not set; using in-memory DAL stub.\n";
|
|
||||||
return dal.connect("stub://memory");
|
|
||||||
}
|
|
||||||
if (!dal.connect(envDsn)) {
|
|
||||||
out << "Failed to connect to database; falling back to stub.\n";
|
|
||||||
return dal.connect("stub://memory");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int runInteractiveSession(KomMcpServer& server,
|
int runInteractiveSession(KomMcpServer& server,
|
||||||
const std::string& toolName,
|
const std::string& toolName,
|
||||||
bool verbose) {
|
bool verbose) {
|
||||||
|
|
@ -685,109 +667,6 @@ void printToolList(const KomMcpServer& server) {
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
int runMcpServer(QString backend, QString address, QTextStream& qerr) {
|
|
||||||
KomMcpServer logic;
|
|
||||||
register_default_tools(logic);
|
|
||||||
|
|
||||||
ki::PgDal dal;
|
|
||||||
QTextStream qout(stdout);
|
|
||||||
connectDalFromEnv(dal, qout);
|
|
||||||
|
|
||||||
const QStringList availableBackends = QMcpServer::backends();
|
|
||||||
if (availableBackends.isEmpty()) {
|
|
||||||
qerr << "[kompanion] No MCP server backends detected in plugin search path.\n";
|
|
||||||
} else if (!availableBackends.contains(backend)) {
|
|
||||||
qerr << "[kompanion] Backend '" << backend << "' not available. Known: "
|
|
||||||
<< availableBackends.join('/') << "\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
KompanionQtServer server(backend, &logic, &dal);
|
|
||||||
if (backend == QStringLiteral("stdio")) server.start(); else server.start(address);
|
|
||||||
QCoreApplication::instance()->exec();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- DB helpers (CLI) ----------
|
|
||||||
bool dbListNamespaces(QTextStream& out) {
|
|
||||||
const char* dsn = std::getenv("PG_DSN");
|
|
||||||
if (!dsn || !*dsn) { out << "PG_DSN not set.\n"; return false; }
|
|
||||||
QString err;
|
|
||||||
if (!testConnection(dsn, &err)) { out << "Connection failed: " << err << "\n"; return false; }
|
|
||||||
|
|
||||||
const QString connName = QStringLiteral("kompanion_db_%1").arg(QRandomGenerator::global()->generate64(), 0, 16);
|
|
||||||
QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL"), connName);
|
|
||||||
const auto cfg = configFromDsn(std::optional<std::string>(dsn));
|
|
||||||
db.setDatabaseName(cfg.dbname); db.setUserName(cfg.user); db.setPassword(cfg.password);
|
|
||||||
db.setHostName(cfg.useSocket ? cfg.socketPath : cfg.host);
|
|
||||||
bool ok=false; const int portValue = cfg.port.toInt(&ok); if (ok && portValue>0) db.setPort(portValue);
|
|
||||||
if (!db.open()) { out << "Open failed: " << db.lastError().text() << "\n"; QSqlDatabase::removeDatabase(connName); return false; }
|
|
||||||
QSqlQuery q(db);
|
|
||||||
if (!q.exec(QStringLiteral("SELECT id::text, name FROM namespaces ORDER BY name"))) {
|
|
||||||
out << q.lastError().text() << "\n"; db.close(); QSqlDatabase::removeDatabase(connName); return false; }
|
|
||||||
while (q.next()) {
|
|
||||||
out << q.value(1).toString() << "\t" << q.value(0).toString() << "\n";
|
|
||||||
}
|
|
||||||
db.close(); QSqlDatabase::removeDatabase(connName);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool dbListItems(const QString& nsName, int limit, QTextStream& out) {
|
|
||||||
const char* dsn = std::getenv("PG_DSN");
|
|
||||||
if (!dsn || !*dsn) { out << "PG_DSN not set.\n"; return false; }
|
|
||||||
const QString connName = QStringLiteral("kompanion_db_%1").arg(QRandomGenerator::global()->generate64(), 0, 16);
|
|
||||||
QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL"), connName);
|
|
||||||
const auto cfg = configFromDsn(std::optional<std::string>(dsn));
|
|
||||||
db.setDatabaseName(cfg.dbname); db.setUserName(cfg.user); db.setPassword(cfg.password);
|
|
||||||
db.setHostName(cfg.useSocket ? cfg.socketPath : cfg.host);
|
|
||||||
bool ok=false; const int portValue = cfg.port.toInt(&ok); if (ok && portValue>0) db.setPort(portValue);
|
|
||||||
if (!db.open()) { out << "Open failed: " << db.lastError().text() << "\n"; QSqlDatabase::removeDatabase(connName); return false; }
|
|
||||||
QSqlQuery q(db);
|
|
||||||
q.prepare(QStringLiteral(
|
|
||||||
"SELECT i.id::text, COALESCE(i.key,''), LEFT(i.content, 120), array_to_string(i.tags, ',') "
|
|
||||||
"FROM memory_items i JOIN namespaces n ON n.id = i.namespace_id "
|
|
||||||
"WHERE n.name = :name ORDER BY i.created_at DESC LIMIT :lim"));
|
|
||||||
q.bindValue(":name", nsName); q.bindValue(":lim", limit);
|
|
||||||
if (!q.exec()) { out << q.lastError().text() << "\n"; db.close(); QSqlDatabase::removeDatabase(connName); return false; }
|
|
||||||
while (q.next()) {
|
|
||||||
out << q.value(0).toString() << '\t' << q.value(1).toString() << '\t' << q.value(2).toString().replace('\n',' ') << '\t' << q.value(3).toString() << "\n";
|
|
||||||
}
|
|
||||||
db.close(); QSqlDatabase::removeDatabase(connName);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool dbSearch(const QString& nsName, const QString& text, const QString& embeddingFile, int k, QTextStream& out) {
|
|
||||||
ki::PgDal dal; connectDalFromEnv(dal, out);
|
|
||||||
auto ns = dal.findNamespace(nsName.toStdString());
|
|
||||||
if (!ns) { out << "namespace not found\n"; return false; }
|
|
||||||
std::vector<float> vec;
|
|
||||||
if (!embeddingFile.isEmpty()) {
|
|
||||||
QFile f(embeddingFile);
|
|
||||||
if (!f.open(QIODevice::ReadOnly|QIODevice::Text)) { out << "cannot read embedding file\n"; return false; }
|
|
||||||
const auto doc = QJsonDocument::fromJson(f.readAll());
|
|
||||||
if (!doc.isArray()) { out << "embedding file must be JSON array\n"; return false; }
|
|
||||||
for (const auto &v : doc.array()) vec.push_back(static_cast<float>(v.toDouble()));
|
|
||||||
}
|
|
||||||
// Hybrid: try text first, then vector.
|
|
||||||
auto rows = dal.searchText(ns->id, text.toStdString(), k);
|
|
||||||
int printed = 0;
|
|
||||||
for (size_t i=0; i<rows.size() && printed<k; ++i) {
|
|
||||||
const auto &r = rows[i];
|
|
||||||
out << QString::fromStdString(r.id) << '\t' << QString::fromStdString(r.text.value_or("")) << '\t' << QString::number(1.0 - (0.05*i), 'f', 3) << "\n";
|
|
||||||
++printed;
|
|
||||||
}
|
|
||||||
if (printed < k && !vec.empty()) {
|
|
||||||
auto more = dal.searchVector(ns->id, vec, k-printed);
|
|
||||||
for (const auto &p : more) {
|
|
||||||
auto item = dal.getItemById(p.first);
|
|
||||||
if (!item) continue;
|
|
||||||
out << QString::fromStdString(p.first) << '\t' << QString::fromStdString(item->text.value_or("")) << '\t' << QString::number(p.second, 'f', 3) << "\n";
|
|
||||||
++printed; if (printed>=k) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
|
|
@ -830,73 +709,6 @@ int main(int argc, char** argv) {
|
||||||
"dsn");
|
"dsn");
|
||||||
parser.addOption(dsnOption);
|
parser.addOption(dsnOption);
|
||||||
|
|
||||||
// MCP server mode
|
|
||||||
QCommandLineOption mcpServeOption(QStringList() << "S" << "mcp-serve",
|
|
||||||
"Run as an MCP server instead of a one-shot tool. Optional backend name (stdio|ws).",
|
|
||||||
"backend", "stdio");
|
|
||||||
parser.addOption(mcpServeOption);
|
|
||||||
QCommandLineOption mcpAddrOption(QStringList() << "A" << "mcp-address",
|
|
||||||
"Address to listen on for network backends.",
|
|
||||||
"address", "127.0.0.1:8000");
|
|
||||||
parser.addOption(mcpAddrOption);
|
|
||||||
|
|
||||||
// DB navigation helpers
|
|
||||||
QCommandLineOption dbNsOption(QStringList() << "db-namespaces",
|
|
||||||
"List namespaces in the database and exit.");
|
|
||||||
parser.addOption(dbNsOption);
|
|
||||||
QCommandLineOption dbItemsOption(QStringList() << "db-items",
|
|
||||||
"List recent items in a namespace (requires --ns). Optional --limit.");
|
|
||||||
parser.addOption(dbItemsOption);
|
|
||||||
QCommandLineOption nsNameOption(QStringList() << "ns",
|
|
||||||
"Namespace name for DB operations.",
|
|
||||||
"name");
|
|
||||||
parser.addOption(nsNameOption);
|
|
||||||
QCommandLineOption limitOption(QStringList() << "limit",
|
|
||||||
"Limit for DB operations (default 10).",
|
|
||||||
"n", "10");
|
|
||||||
parser.addOption(limitOption);
|
|
||||||
QCommandLineOption queryOption(QStringList() << "db-search",
|
|
||||||
"Hybrid search in a namespace. Use --text and/or --embedding-file. Requires --ns.");
|
|
||||||
parser.addOption(queryOption);
|
|
||||||
QCommandLineOption embFileOption(QStringList() << "embedding-file",
|
|
||||||
"Path to JSON array containing embedding vector for hybrid search.",
|
|
||||||
"path");
|
|
||||||
parser.addOption(embFileOption);
|
|
||||||
|
|
||||||
// Snapshot helpers
|
|
||||||
QCommandLineOption snapshotSaveOption(QStringList() << "snapshot-save",
|
|
||||||
"Save a JSON snapshot (content) under a key in --ns. Provide content via -r/--stdin/[payload].");
|
|
||||||
parser.addOption(snapshotSaveOption);
|
|
||||||
QCommandLineOption snapshotLoadOption(QStringList() << "snapshot-load",
|
|
||||||
"Load a JSON snapshot for --ns and --key and print it.");
|
|
||||||
parser.addOption(snapshotLoadOption);
|
|
||||||
QCommandLineOption keyOption(QStringList() << "key",
|
|
||||||
"Key for snapshot operations (default 'session:last').",
|
|
||||||
"key", "session:last");
|
|
||||||
parser.addOption(keyOption);
|
|
||||||
|
|
||||||
// Warm cache + rehydrate helpers
|
|
||||||
QCommandLineOption warmCacheOption(QStringList() << "warm-cache",
|
|
||||||
"Warm precomputed embeddings (policy or ad-hoc). Use with --policy or --id.");
|
|
||||||
parser.addOption(warmCacheOption);
|
|
||||||
QCommandLineOption policyOption(QStringList() << "policy",
|
|
||||||
"Policy file (YAML/JSON) describing namespaces, model, limit, window_days.",
|
|
||||||
"path");
|
|
||||||
parser.addOption(policyOption);
|
|
||||||
QCommandLineOption idOption(QStringList() << "id",
|
|
||||||
"Explicit item id for ad-hoc warm cache (use with --stdin or -r).",
|
|
||||||
"id");
|
|
||||||
parser.addOption(idOption);
|
|
||||||
|
|
||||||
parser.addOption(idOption);
|
|
||||||
QCommandLineOption rehydrateOption(QStringList() << "--rehydrate",
|
|
||||||
"Compose a rehydration frame: snapshot + top-K search for --text.");
|
|
||||||
parser.addOption(rehydrateOption);
|
|
||||||
QCommandLineOption kOption(QStringList() << "k",
|
|
||||||
"Top-K for rehydrate/search.",
|
|
||||||
"k", "8");
|
|
||||||
parser.addOption(kOption);
|
|
||||||
|
|
||||||
parser.addPositionalArgument("tool", "Tool name to invoke.");
|
parser.addPositionalArgument("tool", "Tool name to invoke.");
|
||||||
parser.addPositionalArgument("payload", "Optional JSON payload or file path (use '-' for stdin).", "[payload]");
|
parser.addPositionalArgument("payload", "Optional JSON payload or file path (use '-' for stdin).", "[payload]");
|
||||||
|
|
||||||
|
|
@ -909,11 +721,6 @@ int main(int argc, char** argv) {
|
||||||
const bool verbose = parser.isSet(verboseOption);
|
const bool verbose = parser.isSet(verboseOption);
|
||||||
const bool interactive = parser.isSet(interactiveOption);
|
const bool interactive = parser.isSet(interactiveOption);
|
||||||
const bool initRequested = parser.isSet(initOption);
|
const bool initRequested = parser.isSet(initOption);
|
||||||
const bool runMcp = parser.isSet(mcpServeOption);
|
|
||||||
const bool snapSave = parser.isSet(snapshotSaveOption);
|
|
||||||
const bool snapLoad = parser.isSet(snapshotLoadOption);
|
|
||||||
const bool warmCache = parser.isSet(warmCacheOption);
|
|
||||||
const bool rehydrate = parser.isSet(rehydrateOption);
|
|
||||||
|
|
||||||
std::optional<std::string> configDsn = readDsnFromConfig();
|
std::optional<std::string> configDsn = readDsnFromConfig();
|
||||||
const char* envDsn = std::getenv("PG_DSN");
|
const char* envDsn = std::getenv("PG_DSN");
|
||||||
|
|
@ -959,143 +766,6 @@ int main(int argc, char** argv) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MCP server mode first (exclusive)
|
|
||||||
if (runMcp) {
|
|
||||||
const QString backend = parser.value(mcpServeOption);
|
|
||||||
const QString addr = parser.value(mcpAddrOption);
|
|
||||||
return runMcpServer(backend, addr, qerr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot helpers (exclusive)
|
|
||||||
if (snapSave || snapLoad) {
|
|
||||||
const QString ns = parser.value(nsNameOption);
|
|
||||||
const QString nsQuery = parser.value(nsQuery);
|
|
||||||
if (ns.isEmpty()) { qerr << "--snapshot-save/load requires --ns <name>\n"; return 1; }
|
|
||||||
if (snapSave) {
|
|
||||||
// Resolve content from CLI
|
|
||||||
std::string raw;
|
|
||||||
QString err;
|
|
||||||
if (!resolveRequestPayload(parser, parser.positionalArguments(), requestOption, stdinOption, raw, &err)) {
|
|
||||||
qerr << (err.isEmpty() ? QStringLiteral("Failed to read snapshot content") : err) << "\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
// Wrap into save_context call
|
|
||||||
std::ostringstream req;
|
|
||||||
req << "{\"namespace\":\"" << ns.toStdString() << "\",\"key\":\"" << nsQuery.toStdString() << "\",\"content\":" << raw << ",\"tags\":[\"snapshot\"]}";
|
|
||||||
const std::string out = server.dispatch("kom.memory.v1.save_context", req.str());
|
|
||||||
std::cout << out << std::endl;
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
std::ostringstream req;
|
|
||||||
req << "{\"namespace\":\"" << ns.toStdString() << "\",\"key\":\"" << nsQuery.toStdString() << "\"}";
|
|
||||||
const std::string out = server.dispatch("kom.memory.v1.recall_context", req.str());
|
|
||||||
std::cout << out << std::endl;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warm cache
|
|
||||||
if (warmCache) {
|
|
||||||
const QString ns = parser.value(nsNameOption);
|
|
||||||
const QString id = parser.value(idOption);
|
|
||||||
const QString policyPath = parser.value(policyOption);
|
|
||||||
const QString model = parser.value(QStringLiteral("--model")); // optional generic pass-through
|
|
||||||
const int limit = parser.value(limitOption).toInt();
|
|
||||||
|
|
||||||
if (!id.isEmpty()) {
|
|
||||||
// Ad-hoc enqueue: upsert_and_embed with single item from stdin/-r/payload
|
|
||||||
std::string raw; QString err;
|
|
||||||
if (!resolveRequestPayload(parser, parser.positionalArguments(), requestOption, stdinOption, raw, &err)) {
|
|
||||||
qerr << (err.isEmpty() ? QStringLiteral("Failed to read content for --id") : err) << "\n"; return 1;
|
|
||||||
}
|
|
||||||
std::ostringstream req;
|
|
||||||
req << "{\"namespace\":\"" << ns.toStdString() << "\",\"model\":\"" << model.toStdString() << "\",\"items\":[{\"id\":\"" << id.toStdString() << "\",\"text\":" << raw << "}]}";
|
|
||||||
std::cout << server.dispatch("kom.memory.v1.upsert_and_embed", req.str()) << std::endl;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!policyPath.isEmpty()) {
|
|
||||||
// Minimal policy parser (YAML/JSON): namespaces, model, limit, window_days
|
|
||||||
QFile f(policyPath); if (!f.open(QIODevice::ReadOnly|QIODevice::Text)) { qerr << "Cannot open policy" << "\n"; return 1; }
|
|
||||||
const QString pol = QString::fromUtf8(f.readAll());
|
|
||||||
// Extract namespaces as lines starting with '-'
|
|
||||||
QStringList nss;
|
|
||||||
QRegularExpression rxNs("^\\s*-\\s*([A-Za-z0-9_:\\-]+)\\s*$");
|
|
||||||
for (const QString &line : pol.split('\n')) {
|
|
||||||
auto m = rxNs.match(line); if (m.hasMatch()) nss << m.captured(1);
|
|
||||||
}
|
|
||||||
if (nss.isEmpty() && !ns.isEmpty()) nss << ns;
|
|
||||||
// Extract window_days
|
|
||||||
int windowDays = 0; {
|
|
||||||
QRegularExpression rx("window_days\\s*:\\s*([0-9]+)"); auto m = rx.match(pol); if (m.hasMatch()) windowDays = m.captured(1).toInt();
|
|
||||||
}
|
|
||||||
// Extract model
|
|
||||||
QString pModel = model; if (pModel.isEmpty()) { QRegularExpression rx("model\\s*:\\s*([A-Za-z0-9_:\\-]+)"); auto m = rx.match(pol); if (m.hasMatch()) pModel = m.captured(1); }
|
|
||||||
// Extract limit
|
|
||||||
int pLimit = limit>0?limit:10; { QRegularExpression rx("limit\\s*:\\s*([0-9]+)"); auto m = rx.match(pol); if (m.hasMatch()) pLimit = m.captured(1).toInt(); }
|
|
||||||
|
|
||||||
// Compute since timestamp if windowDays>0
|
|
||||||
QString since;
|
|
||||||
if (windowDays > 0) {
|
|
||||||
const auto now = QDateTime::currentDateTimeUtc();
|
|
||||||
since = now.addDays(-windowDays).toString(Qt::ISODate);
|
|
||||||
}
|
|
||||||
for (const QString &nsv : nss) {
|
|
||||||
std::ostringstream req;
|
|
||||||
req << "{\"namespace\":\"" << nsv.toStdString() << "\"";
|
|
||||||
if (!pModel.isEmpty()) req << ",\"model\":\"" << pModel.toStdString() << "\"";
|
|
||||||
if (!since.isEmpty()) req << ",\"since\":\"" << since.toStdString() << "\"";
|
|
||||||
req << ",\"limit\":" << pLimit << "}";
|
|
||||||
std::cout << server.dispatch("kom.memory.v1.warm_cache", req.str()) << std::endl;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple case: call warm_cache once for ns
|
|
||||||
std::ostringstream req;
|
|
||||||
req << "{\"namespace\":\"" << ns.toStdString() << "\"";
|
|
||||||
if (!model.isEmpty()) req << ",\"model\":\"" << model.toStdString() << "\"";
|
|
||||||
req << ",\"limit\":" << (limit>0?limit:10) << "}";
|
|
||||||
std::cout << server.dispatch("kom.memory.v1.warm_cache", req.str()) << std::endl;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rehydrate composition
|
|
||||||
if (rehydrate) {
|
|
||||||
const QString ns = parser.value(nsNameOption);
|
|
||||||
const QString key = parser.value(keyOption);
|
|
||||||
const QString text = parser.value(queryOption);
|
|
||||||
bool ok=false; int k = parser.value(kOption).toInt(&ok); if (!ok || k<=0) k=8;
|
|
||||||
// Recall snapshot
|
|
||||||
std::ostringstream r1; r1 << "{\"namespace\":\"" << ns.toStdString() << "\",\"key\":\"" << key.toStdString() << "\"}";
|
|
||||||
const std::string snapshot = server.dispatch("kom.memory.v1.recall_context", r1.str());
|
|
||||||
// Search
|
|
||||||
std::ostringstream r2; r2 << "{\"namespace\":\"" << ns.toStdString() << "\",\"query\":{\"text\":\"" << jsonEscape(text) << "\",\"k\":" << k << "}}";
|
|
||||||
const std::string matches = server.dispatch("kom.memory.v1.search_memory", r2.str());
|
|
||||||
// Compose
|
|
||||||
std::cout << "{\"snapshot\":" << snapshot << ",\"search\":" << matches << "}" << std::endl;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB inspection helpers (exclusive)
|
|
||||||
if (parser.isSet(dbNsOption)) {
|
|
||||||
return dbListNamespaces(qout) ? 0 : 1;
|
|
||||||
}
|
|
||||||
if (parser.isSet(dbItemsOption)) {
|
|
||||||
const QString ns = parser.value(nsNameOption);
|
|
||||||
if (ns.isEmpty()) { qerr << "--db-items requires --ns <name>\n"; return 1; }
|
|
||||||
bool ok=false; int lim = parser.value(limitOption).toInt(&ok); if (!ok || lim<=0) lim=10;
|
|
||||||
return dbListItems(ns, lim, qout) ? 0 : 1;
|
|
||||||
}
|
|
||||||
if (parser.isSet(queryOption)) {
|
|
||||||
const QString ns = parser.value(queryOption);
|
|
||||||
if (ns.isEmpty()) { qerr << "--db-search requires --ns <name>\n"; return 1; }
|
|
||||||
bool ok=false; int k = parser.value(limitOption).toInt(&ok); if (!ok || k<=0) k=5;
|
|
||||||
const QString text = parser.value(queryOption);
|
|
||||||
const QString embPath = parser.value(embFileOption);
|
|
||||||
return dbSearch(ns, text, embPath, k, qout) ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QStringList positional = parser.positionalArguments();
|
const QStringList positional = parser.positionalArguments();
|
||||||
if (positional.isEmpty()) {
|
if (positional.isEmpty()) {
|
||||||
parser.showHelp(1);
|
parser.showHelp(1);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Moved from ingest/run_ingest.py for transparency. See ingest/pipeline.qt-kde-bge-m3.yaml
|
|
||||||
for configuration fields. This script remains a reference pipeline and is not
|
|
||||||
used by the C++ build.
|
|
||||||
"""
|
|
||||||
# Original content is available under ingest/run_ingest.py. Keeping this as a thin
|
|
||||||
# forwarder/import to avoid duplication while surfacing the script under src/cli/.
|
|
||||||
import os, sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
here = Path(__file__).resolve()
|
|
||||||
ingest_script = here.parent.parent.parent / 'ingest' / 'run_ingest.py'
|
|
||||||
if not ingest_script.exists():
|
|
||||||
print('ingest/run_ingest.py not found', file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
code = ingest_script.read_text(encoding='utf-8')
|
|
||||||
exec(compile(code, str(ingest_script), 'exec'))
|
|
||||||
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Lightweight embedding helper moved from ingest/ for transparency.
|
|
||||||
|
|
||||||
Usage examples:
|
|
||||||
- Single embedding via Ollama:
|
|
||||||
OLLAMA_BASE=http://localhost:11434 \
|
|
||||||
./py_embedder.py --model bge-m3:latest --text "hello world"
|
|
||||||
|
|
||||||
- Batch from stdin (one line per text):
|
|
||||||
./py_embedder.py --model bge-m3:latest --stdin < texts.txt
|
|
||||||
|
|
||||||
Outputs JSON array of floats (for single text) or array-of-arrays for batches.
|
|
||||||
This script does not touch the database; it only produces vectors.
|
|
||||||
"""
|
|
||||||
import os, sys, json, argparse, requests
|
|
||||||
|
|
||||||
def embed_ollama(texts, model, base):
|
|
||||||
url = f"{base}/api/embeddings"
|
|
||||||
# Some Ollama models accept a single prompt; do one-by-one for reliability
|
|
||||||
out = []
|
|
||||||
for t in texts:
|
|
||||||
r = requests.post(url, json={"model": model, "prompt": t}, timeout=120)
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
if "embedding" in data:
|
|
||||||
out.append(data["embedding"]) # single vector
|
|
||||||
elif "embeddings" in data:
|
|
||||||
out.extend(data["embeddings"]) # multiple vectors
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Embedding response missing 'embedding(s)'")
|
|
||||||
return out
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ap = argparse.ArgumentParser()
|
|
||||||
ap.add_argument("--model", default=os.environ.get("EMBED_MODEL","bge-m3:latest"))
|
|
||||||
ap.add_argument("--text", help="Text to embed; if omitted, use --stdin")
|
|
||||||
ap.add_argument("--stdin", action="store_true", help="Read texts from stdin (one per line)")
|
|
||||||
ap.add_argument("--base", default=os.environ.get("OLLAMA_BASE","http://localhost:11434"))
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
texts = []
|
|
||||||
if args.text:
|
|
||||||
texts = [args.text]
|
|
||||||
elif args.stdin:
|
|
||||||
texts = [line.rstrip("\n") for line in sys.stdin if line.strip()]
|
|
||||||
else:
|
|
||||||
ap.error("Provide --text or --stdin")
|
|
||||||
|
|
||||||
vectors = embed_ollama(texts, args.model, args.base)
|
|
||||||
if len(texts) == 1 and vectors:
|
|
||||||
print(json.dumps(vectors[0]))
|
|
||||||
else:
|
|
||||||
print(json.dumps(vectors))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
@ -6,4 +6,3 @@ target_compile_features(kom_dal PUBLIC cxx_std_20)
|
||||||
target_include_directories(kom_dal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(kom_dal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(kom_dal PUBLIC Qt6::Core Qt6::Sql)
|
target_link_libraries(kom_dal PUBLIC Qt6::Core Qt6::Sql)
|
||||||
target_compile_options(kom_dal PRIVATE -fexceptions)
|
target_compile_options(kom_dal PRIVATE -fexceptions)
|
||||||
set_target_properties(kom_dal PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
namespace ki {
|
namespace kom {
|
||||||
class IDatabase {
|
class IDatabase {
|
||||||
public:
|
public:
|
||||||
virtual ~IDatabase() = default;
|
virtual ~IDatabase() = default;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace ki {
|
namespace kom {
|
||||||
struct MemoryItem {
|
struct MemoryItem {
|
||||||
std::string id;
|
std::string id;
|
||||||
std::string namespace_id;
|
std::string namespace_id;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
namespace ki {
|
namespace kom {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|
@ -281,10 +281,21 @@ std::optional<NamespaceRow> PgDal::ensureNamespace(const std::string& name) {
|
||||||
if (!connected_) {
|
if (!connected_) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
if (auto existing = findNamespace(name)) {
|
if (!useInMemory_ && hasDatabase()) {
|
||||||
return existing;
|
return sqlEnsureNamespace(name);
|
||||||
}
|
}
|
||||||
return createNamespaceWithSecret(name).first;
|
|
||||||
|
auto it = namespacesByName_.find(name);
|
||||||
|
if (it != namespacesByName_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
NamespaceRow row;
|
||||||
|
row.id = allocateId(nextNamespaceId_, "ns_");
|
||||||
|
row.name = name;
|
||||||
|
namespacesByName_[name] = row;
|
||||||
|
namespacesById_[row.id] = row;
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<NamespaceRow> PgDal::findNamespace(const std::string& name) const {
|
std::optional<NamespaceRow> PgDal::findNamespace(const std::string& name) const {
|
||||||
|
|
@ -299,6 +310,22 @@ std::optional<NamespaceRow> PgDal::findNamespace(const std::string& name) const
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NamespaceRow PgDal::sqlEnsureNamespace(const std::string& name) {
|
||||||
|
QSqlDatabase db = database();
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(QStringLiteral(
|
||||||
|
"INSERT INTO namespaces (name) VALUES (:name) "
|
||||||
|
"ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name "
|
||||||
|
"RETURNING id::text, name;"));
|
||||||
|
query.bindValue(QStringLiteral(":name"), QString::fromStdString(name));
|
||||||
|
if (!query.exec() || !query.next()) {
|
||||||
|
throw std::runtime_error(query.lastError().text().toStdString());
|
||||||
|
}
|
||||||
|
NamespaceRow row;
|
||||||
|
row.id = query.value(0).toString().toStdString();
|
||||||
|
row.name = query.value(1).toString().toStdString();
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<NamespaceRow> PgDal::sqlFindNamespace(const std::string& name) const {
|
std::optional<NamespaceRow> PgDal::sqlFindNamespace(const std::string& name) const {
|
||||||
QSqlDatabase db = database();
|
QSqlDatabase db = database();
|
||||||
|
|
@ -318,103 +345,6 @@ std::optional<NamespaceRow> PgDal::sqlFindNamespace(const std::string& name) con
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<NamespaceRow, std::string> PgDal::createNamespaceWithSecret(const std::string& name) {
|
|
||||||
if (!connected_) {
|
|
||||||
throw std::runtime_error("PgDal not connected");
|
|
||||||
}
|
|
||||||
if (!useInMemory_ && hasDatabase()) {
|
|
||||||
return sqlCreateNamespaceWithSecret(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In-memory implementation
|
|
||||||
auto it = namespacesByName_.find(name);
|
|
||||||
if (it != namespacesByName_.end()) {
|
|
||||||
// For in-memory, we don't have secrets, so we can't return one.
|
|
||||||
// This path should ideally not be taken in production.
|
|
||||||
return {it->second, ""};
|
|
||||||
}
|
|
||||||
|
|
||||||
NamespaceRow row;
|
|
||||||
row.id = allocateId(nextNamespaceId_, "ns_");
|
|
||||||
row.name = name;
|
|
||||||
namespacesByName_[name] = row;
|
|
||||||
namespacesById_[row.id] = row;
|
|
||||||
|
|
||||||
// Secrets are not supported in-memory for now
|
|
||||||
return {row, ""};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<AuthSecret> PgDal::findSecretByNamespaceId(const std::string& namespaceId) const {
|
|
||||||
if (!useInMemory_ && hasDatabase()) {
|
|
||||||
return sqlFindSecretByNamespaceId(namespaceId);
|
|
||||||
}
|
|
||||||
// In-memory implementation does not support secrets
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::pair<NamespaceRow, std::string> PgDal::sqlCreateNamespaceWithSecret(const std::string& name) {
|
|
||||||
QSqlDatabase db = database();
|
|
||||||
QSqlQuery query(db);
|
|
||||||
|
|
||||||
// 1. Create the namespace
|
|
||||||
query.prepare(QStringLiteral(
|
|
||||||
"INSERT INTO namespaces (name) VALUES (:name) "
|
|
||||||
"ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name "
|
|
||||||
"RETURNING id::text, name;"));
|
|
||||||
query.bindValue(QStringLiteral(":name"), QString::fromStdString(name));
|
|
||||||
if (!query.exec() || !query.next()) {
|
|
||||||
throw std::runtime_error(query.lastError().text().toStdString());
|
|
||||||
}
|
|
||||||
NamespaceRow row;
|
|
||||||
row.id = query.value(0).toString().toStdString();
|
|
||||||
row.name = query.value(1).toString().toStdString();
|
|
||||||
|
|
||||||
// 2. Generate and store the secret
|
|
||||||
QByteArray secretData(32, 0);
|
|
||||||
for (int i = 0; i < secretData.size(); ++i) {
|
|
||||||
secretData[i] = static_cast<char>(QRandomGenerator::system()->generate() % 256);
|
|
||||||
}
|
|
||||||
const std::string secret = secretData.toHex().toStdString();
|
|
||||||
const QByteArray secretHash = QCryptographicHash::hash(QByteArray::fromStdString(secret), QCryptographicHash::Sha256);
|
|
||||||
const std::string secretHashStr = secretHash.toHex().toStdString();
|
|
||||||
|
|
||||||
sqlInsertSecret(row.id, secretHashStr);
|
|
||||||
|
|
||||||
return {row, secret};
|
|
||||||
}
|
|
||||||
|
|
||||||
void PgDal::sqlInsertSecret(const std::string& namespaceId, const std::string& secretHash) {
|
|
||||||
QSqlDatabase db = database();
|
|
||||||
QSqlQuery query(db);
|
|
||||||
query.prepare(QStringLiteral(
|
|
||||||
"INSERT INTO auth_secrets (namespace_id, secret_hash) "
|
|
||||||
"VALUES (:namespace_id::uuid, :secret_hash) "
|
|
||||||
"ON CONFLICT (namespace_id) DO UPDATE SET secret_hash = EXCLUDED.secret_hash;"));
|
|
||||||
query.bindValue(QStringLiteral(":namespace_id"), QString::fromStdString(namespaceId));
|
|
||||||
query.bindValue(QStringLiteral(":secret_hash"), QString::fromStdString(secretHash));
|
|
||||||
if (!query.exec()) {
|
|
||||||
throw std::runtime_error(query.lastError().text().toStdString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<AuthSecret> PgDal::sqlFindSecretByNamespaceId(const std::string& namespaceId) const {
|
|
||||||
QSqlDatabase db = database();
|
|
||||||
QSqlQuery query(db);
|
|
||||||
query.prepare(QStringLiteral(
|
|
||||||
"SELECT secret_hash FROM auth_secrets WHERE namespace_id = :namespace_id::uuid"));
|
|
||||||
query.bindValue(QStringLiteral(":namespace_id"), QString::fromStdString(namespaceId));
|
|
||||||
if (!query.exec()) {
|
|
||||||
throw std::runtime_error(query.lastError().text().toStdString());
|
|
||||||
}
|
|
||||||
if (!query.next()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
AuthSecret secret;
|
|
||||||
secret.secret_hash = query.value(0).toString().toStdString();
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
std::string PgDal::upsertItem(const ItemRow& row) {
|
std::string PgDal::upsertItem(const ItemRow& row) {
|
||||||
if (!connected_) {
|
if (!connected_) {
|
||||||
throw std::runtime_error("PgDal not connected");
|
throw std::runtime_error("PgDal not connected");
|
||||||
|
|
@ -502,20 +432,15 @@ std::vector<std::string> PgDal::upsertChunks(const std::vector<ChunkRow>& chunks
|
||||||
if (stored.item_id.empty()) {
|
if (stored.item_id.empty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Enforce uniqueness by (item_id, ord) in memory as well
|
if (stored.id.empty()) {
|
||||||
auto& bucket = chunksByItem_[stored.item_id];
|
stored.id = allocateId(nextChunkId_, "chunk_");
|
||||||
std::string existingId;
|
|
||||||
for (const auto &cid : bucket) {
|
|
||||||
auto it = chunks_.find(cid);
|
|
||||||
if (it != chunks_.end() && it->second.ord == stored.ord) { existingId = cid; break; }
|
|
||||||
}
|
|
||||||
if (existingId.empty()) {
|
|
||||||
if (stored.id.empty()) stored.id = allocateId(nextChunkId_, "chunk_");
|
|
||||||
bucket.push_back(stored.id);
|
|
||||||
} else {
|
|
||||||
stored.id = existingId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks_[stored.id] = stored;
|
chunks_[stored.id] = stored;
|
||||||
|
auto& bucket = chunksByItem_[stored.item_id];
|
||||||
|
if (!idsContains(bucket, stored.id)) {
|
||||||
|
bucket.push_back(stored.id);
|
||||||
|
}
|
||||||
ids.push_back(stored.id);
|
ids.push_back(stored.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -529,16 +454,17 @@ std::vector<std::string> PgDal::sqlUpsertChunks(const std::vector<ChunkRow>& chu
|
||||||
QSqlDatabase db = database();
|
QSqlDatabase db = database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare(QStringLiteral(
|
query.prepare(QStringLiteral(
|
||||||
"INSERT INTO memory_chunks (item_id, seq, content, id) "
|
"INSERT INTO memory_chunks (id, item_id, seq, content) "
|
||||||
"VALUES (:item_id::uuid, :seq, :content, COALESCE(NULLIF(:id, '')::uuid, gen_random_uuid())) "
|
"VALUES (COALESCE(NULLIF(:id, '')::uuid, gen_random_uuid()), "
|
||||||
"ON CONFLICT (item_id, seq) DO UPDATE SET content = EXCLUDED.content "
|
" :item_id::uuid, :seq, :content) "
|
||||||
|
"ON CONFLICT (id) DO UPDATE SET seq = EXCLUDED.seq, content = EXCLUDED.content "
|
||||||
"RETURNING id::text;"));
|
"RETURNING id::text;"));
|
||||||
|
|
||||||
for (const auto& chunk : chunks) {
|
for (const auto& chunk : chunks) {
|
||||||
|
query.bindValue(QStringLiteral(":id"), QString::fromStdString(chunk.id));
|
||||||
query.bindValue(QStringLiteral(":item_id"), QString::fromStdString(chunk.item_id));
|
query.bindValue(QStringLiteral(":item_id"), QString::fromStdString(chunk.item_id));
|
||||||
query.bindValue(QStringLiteral(":seq"), chunk.ord);
|
query.bindValue(QStringLiteral(":seq"), chunk.ord);
|
||||||
query.bindValue(QStringLiteral(":content"), QString::fromStdString(chunk.text));
|
query.bindValue(QStringLiteral(":content"), QString::fromStdString(chunk.text));
|
||||||
query.bindValue(QStringLiteral(":id"), QString::fromStdString(chunk.id));
|
|
||||||
|
|
||||||
if (!query.exec() || !query.next()) {
|
if (!query.exec() || !query.next()) {
|
||||||
throw std::runtime_error(query.lastError().text().toStdString());
|
throw std::runtime_error(query.lastError().text().toStdString());
|
||||||
|
|
@ -1105,4 +1031,4 @@ std::vector<std::string> PgDal::parsePgTextArray(const QString& value) {
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ki
|
} // namespace kom
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@
|
||||||
|
|
||||||
#include <QSqlDatabase>
|
#include <QSqlDatabase>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QCryptographicHash>
|
|
||||||
#include <QRandomGenerator>
|
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
@ -14,7 +12,7 @@
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace ki {
|
namespace kom {
|
||||||
|
|
||||||
struct NamespaceRow {
|
struct NamespaceRow {
|
||||||
std::string id;
|
std::string id;
|
||||||
|
|
@ -49,10 +47,6 @@ struct EmbeddingRow {
|
||||||
std::vector<float> vector;
|
std::vector<float> vector;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AuthSecret {
|
|
||||||
std::string secret_hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PgDal final : public IDatabase {
|
class PgDal final : public IDatabase {
|
||||||
public:
|
public:
|
||||||
PgDal();
|
PgDal();
|
||||||
|
|
@ -63,10 +57,8 @@ public:
|
||||||
void commit();
|
void commit();
|
||||||
void rollback();
|
void rollback();
|
||||||
|
|
||||||
std::pair<NamespaceRow, std::string> createNamespaceWithSecret(const std::string& name);
|
|
||||||
std::optional<NamespaceRow> ensureNamespace(const std::string& name);
|
std::optional<NamespaceRow> ensureNamespace(const std::string& name);
|
||||||
std::optional<NamespaceRow> findNamespace(const std::string& name) const;
|
std::optional<NamespaceRow> findNamespace(const std::string& name) const;
|
||||||
std::optional<AuthSecret> findSecretByNamespaceId(const std::string& namespaceId) const;
|
|
||||||
|
|
||||||
std::string upsertItem(const ItemRow& row);
|
std::string upsertItem(const ItemRow& row);
|
||||||
std::vector<std::string> upsertChunks(const std::vector<ChunkRow>& chunks);
|
std::vector<std::string> upsertChunks(const std::vector<ChunkRow>& chunks);
|
||||||
|
|
@ -121,11 +113,8 @@ private:
|
||||||
QSqlDatabase database() const;
|
QSqlDatabase database() const;
|
||||||
ConnectionConfig parseDsn(const std::string& dsn) const;
|
ConnectionConfig parseDsn(const std::string& dsn) const;
|
||||||
|
|
||||||
std::pair<NamespaceRow, std::string> sqlCreateNamespaceWithSecret(const std::string& name);
|
NamespaceRow sqlEnsureNamespace(const std::string& name);
|
||||||
std::optional<NamespaceRow> sqlEnsureNamespace(const std::string& name);
|
|
||||||
std::optional<NamespaceRow> sqlFindNamespace(const std::string& name) const;
|
std::optional<NamespaceRow> sqlFindNamespace(const std::string& name) const;
|
||||||
std::optional<AuthSecret> sqlFindSecretByNamespaceId(const std::string& namespaceId) const;
|
|
||||||
void sqlInsertSecret(const std::string& namespaceId, const std::string& secretHash);
|
|
||||||
std::pair<std::string, int> sqlUpsertItem(const ItemRow& row);
|
std::pair<std::string, int> sqlUpsertItem(const ItemRow& row);
|
||||||
std::vector<std::string> sqlUpsertChunks(const std::vector<ChunkRow>& chunks);
|
std::vector<std::string> sqlUpsertChunks(const std::vector<ChunkRow>& chunks);
|
||||||
void sqlUpsertEmbeddings(const std::vector<EmbeddingRow>& embeddings);
|
void sqlUpsertEmbeddings(const std::vector<EmbeddingRow>& embeddings);
|
||||||
|
|
@ -177,4 +166,4 @@ private:
|
||||||
std::unordered_map<std::string, EmbeddingRow> embeddings_;
|
std::unordered_map<std::string, EmbeddingRow> embeddings_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ki
|
} // namespace kom
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
include_directories($CMAKE_SRC_DIR/src)
|
|
||||||
add_executable(kompanion_gui
|
|
||||||
MainWindow.cpp
|
|
||||||
)
|
|
||||||
target_include_directories(kompanion_gui PRIVATE ${PROJECT_SOURCE_DIR}/src)
|
|
||||||
|
|
||||||
target_link_libraries(kompanion_gui PRIVATE
|
|
||||||
KF6::Parts
|
|
||||||
KF6::TextEditor
|
|
||||||
KF6::ConfigCore
|
|
||||||
Qt6::McpServer
|
|
||||||
Qt6::McpCommon
|
|
||||||
kom_dal
|
|
||||||
kom_ki
|
|
||||||
)
|
|
||||||
|
|
||||||
install(TARGETS kompanion_gui RUNTIME DESTINATION bin)
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
#include <KParts/MainWindow>
|
|
||||||
#include <KTextEditor/Editor>
|
|
||||||
#include <KTextEditor/View>
|
|
||||||
#include <KTextEditor/Document>
|
|
||||||
#include <KTextEditor/Cursor>
|
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QFutureWatcher>
|
|
||||||
|
|
||||||
#include <Client/KIClient.h>
|
|
||||||
#include <Provider/OllamaProvider.h>
|
|
||||||
#include <Message/KIMessage.h>
|
|
||||||
#include <Message/KIThread.h>
|
|
||||||
#include <Completion/KIReply.h>
|
|
||||||
#include <Completion/KIChatOptions.h>
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow : public KParts::MainWindow
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
MainWindow(QWidget *parent = nullptr) : KParts::MainWindow(parent)
|
|
||||||
{
|
|
||||||
QWidget *mainWidget = new QWidget(this);
|
|
||||||
setCentralWidget(mainWidget);
|
|
||||||
|
|
||||||
QVBoxLayout *layout = new QVBoxLayout(mainWidget);
|
|
||||||
|
|
||||||
auto editor = KTextEditor::Editor::instance();
|
|
||||||
m_document = editor->createDocument(this);
|
|
||||||
m_chatView = m_document->createView(mainWidget);
|
|
||||||
m_document->setReadWrite(false);
|
|
||||||
layout->addWidget(m_chatView);
|
|
||||||
|
|
||||||
m_chatInput = new QLineEdit(mainWidget);
|
|
||||||
layout->addWidget(m_chatInput);
|
|
||||||
|
|
||||||
QHBoxLayout *row = new QHBoxLayout();
|
|
||||||
QPushButton *sendButton = new QPushButton("Send", mainWidget);
|
|
||||||
QPushButton *embedButton = new QPushButton("Embed", mainWidget);
|
|
||||||
row->addWidget(sendButton);
|
|
||||||
row->addWidget(embedButton);
|
|
||||||
layout->addLayout(row);
|
|
||||||
|
|
||||||
connect(sendButton, &QPushButton::clicked, this, &MainWindow::sendMessage);
|
|
||||||
connect(embedButton, &QPushButton::clicked, this, [this]() {
|
|
||||||
const QString text = m_chatInput->text().trimmed();
|
|
||||||
if (text.isEmpty()) return;
|
|
||||||
KI::KIEmbedOptions opts; opts.model = "bge-m3:latest"; // simple default
|
|
||||||
QFuture<KI::KIEmbeddingResult> fut = m_kompanionClient->embed(QStringList{text}, opts);
|
|
||||||
auto *watch = new QFutureWatcher<KI::KIEmbeddingResult>(this);
|
|
||||||
connect(watch, &QFutureWatcher<KI::KIEmbeddingResult>::finished, this, [this, watch]() {
|
|
||||||
const auto res = watch->result();
|
|
||||||
if (!res.vectors.isEmpty()) {
|
|
||||||
insertText(QString("[embed %1] dim=%2\n").arg(res.model).arg(res.vectors.first().size()));
|
|
||||||
} else {
|
|
||||||
insertText("[embed] no result\n");
|
|
||||||
}
|
|
||||||
watch->deleteLater();
|
|
||||||
});
|
|
||||||
watch->setFuture(fut);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup KI
|
|
||||||
m_ollamaProvider = new KI::OllamaProvider(this);
|
|
||||||
m_kompanionClient = new KI::KIClient(this);
|
|
||||||
m_kompanionClient->setProvider(m_ollamaProvider);
|
|
||||||
m_kompanionClient->setDefaultModel("llama2"); // Or some other default
|
|
||||||
}
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void sendMessage()
|
|
||||||
{
|
|
||||||
const QString message = m_chatInput->text();
|
|
||||||
if (message.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatInput->clear();
|
|
||||||
|
|
||||||
// Append user message to chat view
|
|
||||||
insertText(QString("User: %1\n").arg(message));
|
|
||||||
|
|
||||||
// Send message to KI
|
|
||||||
KI::KIThread thread;
|
|
||||||
KI::KIMessage kimessage;
|
|
||||||
kimessage.role = "user";
|
|
||||||
KI::KIMessagePart part;
|
|
||||||
part.mime = "text/plain";
|
|
||||||
part.text = message;
|
|
||||||
kimessage.parts.append(part);
|
|
||||||
thread.messages.append(kimessage);
|
|
||||||
|
|
||||||
KI::KIChatOptions opts;
|
|
||||||
opts.model = m_kompanionClient->defaultModel();
|
|
||||||
|
|
||||||
QFuture<KI::KIReply*> future = m_kompanionClient->chat(thread, opts);
|
|
||||||
|
|
||||||
QFutureWatcher<KI::KIReply*> *watcher = new QFutureWatcher<KI::KIReply*>(this);
|
|
||||||
connect(watcher, &QFutureWatcher<KI::KIReply*>::finished, this, [this, watcher]() {
|
|
||||||
KI::KIReply* reply = watcher->result();
|
|
||||||
connect(reply, &KI::KIReply::tokensAdded, this, [this](const QString& delta) {
|
|
||||||
insertText(delta);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(reply, &KI::KIReply::finished, this, [this, reply]() {
|
|
||||||
insertText("\n");
|
|
||||||
reply->deleteLater();
|
|
||||||
});
|
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
watcher->setFuture(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void insertText(const QString &text)
|
|
||||||
{
|
|
||||||
KTextEditor::Cursor endCursor(m_document->lines() - 1, m_document->lineLength(m_document->lines() - 1));
|
|
||||||
m_document->insertText(endCursor, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
KTextEditor::View *m_chatView;
|
|
||||||
KTextEditor::Document *m_document;
|
|
||||||
QLineEdit *m_chatInput;
|
|
||||||
|
|
||||||
KI::KIClient* m_kompanionClient;
|
|
||||||
KI::OllamaProvider* m_ollamaProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
QApplication app(argc, argv);
|
|
||||||
|
|
||||||
MainWindow window;
|
|
||||||
window.show();
|
|
||||||
|
|
||||||
return app.exec();
|
|
||||||
}
|
|
||||||
234
src/main.cpp
234
src/main.cpp
|
|
@ -1,18 +1,12 @@
|
||||||
|
// Minimal CLI runner that registers Kompanion MCP tools and dispatches requests.
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <QCommandLineOption>
|
|
||||||
#include <QCommandLineParser>
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QLatin1Char>
|
|
||||||
#include <QTextStream>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
#ifdef HAVE_KCONFIG
|
#ifdef HAVE_KCONFIG
|
||||||
#include <KConfigGroup>
|
#include <KConfigGroup>
|
||||||
#include <KSharedConfig>
|
#include <KSharedConfig>
|
||||||
|
|
@ -22,80 +16,46 @@
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
#include "mcp/KomMcpServer.hpp"
|
#include "mcp/KomMcpServer.hpp"
|
||||||
#include "mcp/KompanionQtServer.hpp"
|
|
||||||
#include "mcp/RegisterTools.hpp"
|
#include "mcp/RegisterTools.hpp"
|
||||||
#include "dal/PgDal.hpp"
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
std::string read_all(std::istream &in)
|
std::string read_all(std::istream& in) {
|
||||||
{
|
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << in.rdbuf();
|
oss << in.rdbuf();
|
||||||
return oss.str();
|
return oss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool readFileUtf8(const QString &path, std::string &out, QString *error)
|
std::string load_request(int argc, char** argv) {
|
||||||
{
|
if (argc < 3) {
|
||||||
QFile file(path);
|
return read_all(std::cin);
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
||||||
if (error) {
|
|
||||||
*error = QStringLiteral("Unable to open request file: %1").arg(path);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
const QByteArray data = file.readAll();
|
std::string arg = argv[2];
|
||||||
out = QString::fromUtf8(data).toStdString();
|
if (arg == "-") {
|
||||||
return true;
|
return read_all(std::cin);
|
||||||
|
}
|
||||||
|
std::filesystem::path p{arg};
|
||||||
|
if (std::filesystem::exists(p)) {
|
||||||
|
std::ifstream file(p);
|
||||||
|
if (!file) throw std::runtime_error("unable to open request file: " + p.string());
|
||||||
|
return read_all(file);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool resolveRequestPayload(const QCommandLineParser &parser,
|
void print_usage(const char* exe, KomMcpServer& server) {
|
||||||
const QStringList &positional,
|
std::cerr << "Usage: " << exe << " <tool-name> [request-json|-|path]\n";
|
||||||
const QCommandLineOption &requestOption,
|
std::cerr << "Available tools:\n";
|
||||||
const QCommandLineOption &stdinOption,
|
for (const auto& name : server.listTools()) {
|
||||||
std::string &payloadOut,
|
std::cerr << " - " << name << "\n";
|
||||||
QString *error)
|
|
||||||
{
|
|
||||||
if (parser.isSet(stdinOption)) {
|
|
||||||
payloadOut = read_all(std::cin);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parser.isSet(requestOption)) {
|
|
||||||
const QString arg = parser.value(requestOption);
|
|
||||||
if (arg == "-" || parser.isSet(stdinOption)) {
|
|
||||||
payloadOut = read_all(std::cin);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (QFileInfo::exists(arg)) {
|
|
||||||
return readFileUtf8(arg, payloadOut, error);
|
|
||||||
}
|
|
||||||
payloadOut = arg.toStdString();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (positional.size() > 1) {
|
|
||||||
const QString arg = positional.at(1);
|
|
||||||
if (arg == "-") {
|
|
||||||
payloadOut = read_all(std::cin);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (QFileInfo::exists(arg)) {
|
|
||||||
return readFileUtf8(arg, payloadOut, error);
|
|
||||||
}
|
|
||||||
payloadOut = arg.toStdString();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
payloadOut = "{}";
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
#ifdef HAVE_KCONFIG
|
#ifdef HAVE_KCONFIG
|
||||||
std::optional<std::string> read_dsn_from_config()
|
std::optional<std::string> read_dsn_from_config() {
|
||||||
{
|
|
||||||
auto config = KSharedConfig::openConfig(QStringLiteral("kompanionrc"));
|
auto config = KSharedConfig::openConfig(QStringLiteral("kompanionrc"));
|
||||||
if (!config) return std::nullopt;
|
if (!config) return std::nullopt;
|
||||||
KConfigGroup dbGroup(config, QStringLiteral("Database"));
|
KConfigGroup dbGroup(config, QStringLiteral("Database"));
|
||||||
|
|
@ -106,8 +66,7 @@ std::optional<std::string> read_dsn_from_config()
|
||||||
return dsn.toStdString();
|
return dsn.toStdString();
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
QString configFilePath()
|
QString configFilePath() {
|
||||||
{
|
|
||||||
QString base = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
QString base = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
||||||
if (base.isEmpty()) {
|
if (base.isEmpty()) {
|
||||||
base = QDir::homePath();
|
base = QDir::homePath();
|
||||||
|
|
@ -115,8 +74,7 @@ QString configFilePath()
|
||||||
return QDir(base).filePath(QStringLiteral("kompanionrc"));
|
return QDir(base).filePath(QStringLiteral("kompanionrc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> read_dsn_from_config()
|
std::optional<std::string> read_dsn_from_config() {
|
||||||
{
|
|
||||||
QSettings settings(configFilePath(), QSettings::IniFormat);
|
QSettings settings(configFilePath(), QSettings::IniFormat);
|
||||||
const QString dsn = settings.value(QStringLiteral("Database/PgDsn")).toString();
|
const QString dsn = settings.value(QStringLiteral("Database/PgDsn")).toString();
|
||||||
if (dsn.isEmpty()) {
|
if (dsn.isEmpty()) {
|
||||||
|
|
@ -126,66 +84,11 @@ std::optional<std::string> read_dsn_from_config()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace
|
int main(int argc, char** argv) {
|
||||||
|
KomMcpServer server;
|
||||||
|
register_default_tools(server);
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
const char* envDsn = std::getenv("PG_DSN");
|
||||||
{
|
|
||||||
QCoreApplication app(argc, argv);
|
|
||||||
QCoreApplication::setApplicationName(QStringLiteral("Kompanion MCP"));
|
|
||||||
QCoreApplication::setApplicationVersion(QStringLiteral("0.1.0"));
|
|
||||||
|
|
||||||
const QStringList availableBackends = QMcpServer::backends();
|
|
||||||
const QString defaultBackend =
|
|
||||||
availableBackends.contains(QStringLiteral("stdio"))
|
|
||||||
? QStringLiteral("stdio")
|
|
||||||
: (!availableBackends.isEmpty() ? availableBackends.first()
|
|
||||||
: QStringLiteral("stdio"));
|
|
||||||
const QString backendHelp =
|
|
||||||
availableBackends.isEmpty()
|
|
||||||
? QStringLiteral("Backend to use (no MCP server backends detected; defaulting to \"%1\").")
|
|
||||||
.arg(defaultBackend)
|
|
||||||
: QStringLiteral("Backend to use (%1).").arg(availableBackends.join(QLatin1Char('/')));
|
|
||||||
|
|
||||||
QCommandLineParser parser;
|
|
||||||
parser.setApplicationDescription(QStringLiteral("Kompanion MCP daemon backed by QtMcp."));
|
|
||||||
parser.addHelpOption();
|
|
||||||
parser.addVersionOption();
|
|
||||||
|
|
||||||
QCommandLineOption backendOption(QStringList() << "b" << "backend",
|
|
||||||
backendHelp,
|
|
||||||
QStringLiteral("backend"),
|
|
||||||
defaultBackend);
|
|
||||||
QCommandLineOption addressOption(QStringList() << "a" << "address",
|
|
||||||
QStringLiteral("Address to listen on (for network backends)."),
|
|
||||||
QStringLiteral("address"),
|
|
||||||
QStringLiteral("127.0.0.1:8000"));
|
|
||||||
QCommandLineOption requestOption(QStringList() << "r" << "request",
|
|
||||||
QStringLiteral("JSON request payload or path to JSON file."),
|
|
||||||
QStringLiteral("payload"));
|
|
||||||
QCommandLineOption stdinOption(QStringList() << "i" << "stdin",
|
|
||||||
QStringLiteral("Read request payload from standard input."));
|
|
||||||
QCommandLineOption listOption(QStringList() << "l" << "list",
|
|
||||||
QStringLiteral("List available tools and exit."));
|
|
||||||
|
|
||||||
parser.addOption(backendOption);
|
|
||||||
parser.addOption(addressOption);
|
|
||||||
parser.addOption(requestOption);
|
|
||||||
parser.addOption(stdinOption);
|
|
||||||
parser.addOption(listOption);
|
|
||||||
|
|
||||||
parser.addPositionalArgument(QStringLiteral("tool"),
|
|
||||||
QStringLiteral("Tool name to invoke (CLI mode)."),
|
|
||||||
QStringLiteral("[tool]"));
|
|
||||||
parser.addPositionalArgument(QStringLiteral("payload"),
|
|
||||||
QStringLiteral("Optional JSON payload or file path (use '-' for stdin)."),
|
|
||||||
QStringLiteral("[payload]"));
|
|
||||||
|
|
||||||
parser.process(app);
|
|
||||||
|
|
||||||
KomMcpServer logic;
|
|
||||||
register_default_tools(logic);
|
|
||||||
|
|
||||||
const char *envDsn = std::getenv("PG_DSN");
|
|
||||||
std::optional<std::string> effectiveDsn;
|
std::optional<std::string> effectiveDsn;
|
||||||
if (envDsn && *envDsn) {
|
if (envDsn && *envDsn) {
|
||||||
effectiveDsn = std::string(envDsn);
|
effectiveDsn = std::string(envDsn);
|
||||||
|
|
@ -197,60 +100,35 @@ int main(int argc, char **argv)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!effectiveDsn) {
|
if (!effectiveDsn) {
|
||||||
std::cerr << "[kom_mcp] PG_DSN not set; DAL will fall back to stubbed mode. Configure Database/PgDsn to enable persistence.\n";
|
std::cerr << "[kom_mcp] PG_DSN not set; fallback DAL will be used if available (configure Database/PgDsn via KConfig).\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
ki::PgDal dal;
|
if (argc < 2) {
|
||||||
if (effectiveDsn) {
|
print_usage(argv[0], server);
|
||||||
if (!dal.connect(*effectiveDsn)) {
|
|
||||||
std::cerr << "[kom_mcp] Failed to connect to database; DAL will fall back to stubbed mode.\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parser.isSet(listOption)) {
|
|
||||||
for (const auto &toolName : logic.listTools()) {
|
|
||||||
std::cout << toolName << "\n";
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QStringList positional = parser.positionalArguments();
|
|
||||||
if (!positional.isEmpty()) {
|
|
||||||
const std::string toolName = positional.first().toStdString();
|
|
||||||
if (!logic.hasTool(toolName)) {
|
|
||||||
std::cerr << "Unknown tool: " << toolName << "\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string payload;
|
|
||||||
QString payloadError;
|
|
||||||
if (!resolveRequestPayload(parser, positional, requestOption, stdinOption, payload, &payloadError)) {
|
|
||||||
std::cerr << "Error: " << payloadError.toStdString() << "\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string response = logic.dispatch(toolName, payload);
|
|
||||||
std::cout << response << std::endl;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString backend = parser.value(backendOption);
|
|
||||||
const QString address = parser.value(addressOption);
|
|
||||||
|
|
||||||
if (availableBackends.isEmpty()) {
|
|
||||||
qWarning() << "[kom_mcp] No MCP server backends detected in plugin search path.";
|
|
||||||
} else if (!availableBackends.contains(backend)) {
|
|
||||||
qWarning() << "[kom_mcp] Backend" << backend << "not available. Known backends:"
|
|
||||||
<< availableBackends;
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
KompanionQtServer server(backend, &logic, &dal);
|
std::string tool = argv[1];
|
||||||
if (backend == QStringLiteral("stdio")) {
|
if (tool == "--list") {
|
||||||
server.start();
|
for (const auto& name : server.listTools()) {
|
||||||
} else {
|
std::cout << name << "\n";
|
||||||
server.start(address);
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.exec();
|
if (!server.hasTool(tool)) {
|
||||||
|
std::cerr << "Unknown tool: " << tool << "\n";
|
||||||
|
print_usage(argv[0], server);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::string request = load_request(argc, argv);
|
||||||
|
std::string response = server.dispatch(tool, request);
|
||||||
|
std::cout << response << std::endl;
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
std::cerr << "Error: " << ex.what() << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core)
|
|
||||||
|
|
||||||
add_library(kom_mcp STATIC
|
|
||||||
KompanionQtServer.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_resources(kom_mcp kompanion_mcp_resources
|
|
||||||
PREFIX "/kompanion"
|
|
||||||
BASE "."
|
|
||||||
FILES ToolSchemas.json
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(kom_mcp PRIVATE
|
|
||||||
kom_dal
|
|
||||||
kom_ki
|
|
||||||
KF6::ConfigCore
|
|
||||||
Qt6::Core
|
|
||||||
Qt6::Network
|
|
||||||
Qt6::McpServer
|
|
||||||
Qt6::McpCommon
|
|
||||||
)
|
|
||||||
|
|
||||||
target_compile_options(kom_mcp PRIVATE -fexceptions)
|
|
||||||
|
|
||||||
target_compile_definitions(kom_mcp PRIVATE
|
|
||||||
PROJECT_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
|
||||||
KOMPANION_DB_INIT_INSTALL_DIR="${KOMPANION_DB_INIT_INSTALL_DIR}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
install(FILES ToolSchemas.json DESTINATION ${KDE_INSTALL_DATADIR}/kompanion/mcp)
|
|
||||||
|
|
@ -112,11 +112,6 @@ inline std::optional<std::string> currentDsnSource() {
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
// Echos back the request payload
|
|
||||||
inline std::string echo(const std::string& reqJson) {
|
|
||||||
return reqJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Produces a JSON response summarising project state: memory docs, task table, git status.
|
// Produces a JSON response summarising project state: memory docs, task table, git status.
|
||||||
inline std::string project_snapshot(const std::string& reqJson) {
|
inline std::string project_snapshot(const std::string& reqJson) {
|
||||||
(void)reqJson;
|
(void)reqJson;
|
||||||
|
|
|
||||||
|
|
@ -14,25 +14,13 @@
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "PgDal.hpp"
|
#include "dal/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 kom::PgDal& database() {
|
||||||
static ki::PgDal instance;
|
static kom::PgDal instance;
|
||||||
static bool connected = [] {
|
static bool connected = [] {
|
||||||
const char* env = std::getenv("PG_DSN");
|
const char* env = std::getenv("PG_DSN");
|
||||||
const std::string dsn = (env && *env) ? std::string(env) : std::string();
|
const std::string dsn = (env && *env) ? std::string(env) : std::string();
|
||||||
|
|
@ -340,7 +328,6 @@ struct ParsedItem {
|
||||||
std::vector<std::string> tags;
|
std::vector<std::string> tags;
|
||||||
std::vector<float> embedding;
|
std::vector<float> embedding;
|
||||||
std::string rawJson;
|
std::string rawJson;
|
||||||
std::string metadataJson;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline std::vector<ParsedItem> parse_items(const std::string& json) {
|
inline std::vector<ParsedItem> parse_items(const std::string& json) {
|
||||||
|
|
@ -352,7 +339,6 @@ inline std::vector<ParsedItem> parse_items(const std::string& json) {
|
||||||
item.text = extract_string_field(obj, "text");
|
item.text = extract_string_field(obj, "text");
|
||||||
item.tags = parse_string_array(obj, "tags");
|
item.tags = parse_string_array(obj, "tags");
|
||||||
item.embedding = parse_float_array(obj, "embedding");
|
item.embedding = parse_float_array(obj, "embedding");
|
||||||
if (auto meta = extract_json_value(obj, "metadata")) item.metadataJson = *meta; else item.metadataJson = "{}";
|
|
||||||
items.push_back(std::move(item));
|
items.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
|
|
@ -399,7 +385,7 @@ inline std::string upsert_memory(const std::string& reqJson) {
|
||||||
return detail::error_response("bad_request", "items array must contain at least one entry");
|
return detail::error_response("bad_request", "items array must contain at least one entry");
|
||||||
}
|
}
|
||||||
|
|
||||||
ki::PgDal& dal = detail::database();
|
kom::PgDal& dal = detail::database();
|
||||||
const bool hasTx = dal.begin();
|
const bool hasTx = dal.begin();
|
||||||
std::vector<std::string> ids;
|
std::vector<std::string> ids;
|
||||||
ids.reserve(items.size());
|
ids.reserve(items.size());
|
||||||
|
|
@ -408,7 +394,7 @@ inline std::string upsert_memory(const std::string& reqJson) {
|
||||||
try {
|
try {
|
||||||
#endif
|
#endif
|
||||||
for (auto& parsed : items) {
|
for (auto& parsed : items) {
|
||||||
ki::ItemRow row;
|
kom::ItemRow row;
|
||||||
row.id = parsed.id;
|
row.id = parsed.id;
|
||||||
row.namespace_id = nsRow->id;
|
row.namespace_id = nsRow->id;
|
||||||
row.content_json = parsed.rawJson;
|
row.content_json = parsed.rawJson;
|
||||||
|
|
@ -420,18 +406,18 @@ inline std::string upsert_memory(const std::string& reqJson) {
|
||||||
ids.push_back(itemId);
|
ids.push_back(itemId);
|
||||||
|
|
||||||
if (!parsed.embedding.empty()) {
|
if (!parsed.embedding.empty()) {
|
||||||
ki::ChunkRow chunk;
|
kom::ChunkRow chunk;
|
||||||
chunk.item_id = itemId;
|
chunk.item_id = itemId;
|
||||||
chunk.ord = 0;
|
chunk.ord = 0;
|
||||||
chunk.text = parsed.text;
|
chunk.text = parsed.text;
|
||||||
auto chunkIds = dal.upsertChunks(std::vector<ki::ChunkRow>{chunk});
|
auto chunkIds = dal.upsertChunks(std::vector<kom::ChunkRow>{chunk});
|
||||||
|
|
||||||
ki::EmbeddingRow emb;
|
kom::EmbeddingRow emb;
|
||||||
emb.chunk_id = chunkIds.front();
|
emb.chunk_id = chunkIds.front();
|
||||||
emb.model = "stub-model";
|
emb.model = "stub-model";
|
||||||
emb.dim = static_cast<int>(parsed.embedding.size());
|
emb.dim = static_cast<int>(parsed.embedding.size());
|
||||||
emb.vector = parsed.embedding;
|
emb.vector = parsed.embedding;
|
||||||
dal.upsertEmbeddings(std::vector<ki::EmbeddingRow>{emb});
|
dal.upsertEmbeddings(std::vector<kom::EmbeddingRow>{emb});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if defined(__cpp_exceptions)
|
#if defined(__cpp_exceptions)
|
||||||
|
|
@ -457,11 +443,6 @@ 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()) {
|
||||||
|
|
@ -485,7 +466,7 @@ inline std::string search_memory(const std::string& reqJson) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ki::PgDal& dal = detail::database();
|
kom::PgDal& dal = detail::database();
|
||||||
std::unordered_set<std::string> seen;
|
std::unordered_set<std::string> seen;
|
||||||
std::vector<detail::SearchMatch> matches;
|
std::vector<detail::SearchMatch> matches;
|
||||||
|
|
||||||
|
|
@ -519,11 +500,6 @@ 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()) {
|
||||||
|
|
@ -544,8 +520,8 @@ inline std::string save_context(const std::string& reqJson) {
|
||||||
auto tags = detail::parse_string_array(reqJson, "tags");
|
auto tags = detail::parse_string_array(reqJson, "tags");
|
||||||
auto ttlOpt = detail::extract_int_field(reqJson, "ttl_seconds");
|
auto ttlOpt = detail::extract_int_field(reqJson, "ttl_seconds");
|
||||||
|
|
||||||
ki::PgDal& dal = detail::database();
|
kom::PgDal& dal = detail::database();
|
||||||
ki::ItemRow row;
|
kom::ItemRow row;
|
||||||
row.namespace_id = nsRow->id;
|
row.namespace_id = nsRow->id;
|
||||||
if (!key.empty()) {
|
if (!key.empty()) {
|
||||||
row.key = key;
|
row.key = key;
|
||||||
|
|
@ -594,58 +570,6 @@ inline std::string save_context(const std::string& reqJson) {
|
||||||
return os.str();
|
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<std::string> itemIds; itemIds.reserve(items.size());
|
|
||||||
std::vector<std::string> 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<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();
|
|
||||||
|
|
||||||
// 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<ki::ChunkRow>{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<ki::EmbeddingRow>{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 }
|
|
||||||
* 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()) {
|
||||||
|
|
@ -676,7 +600,7 @@ inline std::string recall_context(const std::string& reqJson) {
|
||||||
sinceOpt = since;
|
sinceOpt = since;
|
||||||
}
|
}
|
||||||
|
|
||||||
ki::PgDal& dal = detail::database();
|
kom::PgDal& dal = detail::database();
|
||||||
auto rows = dal.fetchContext(nsRow->id, keyOpt, tags, sinceOpt, limit);
|
auto rows = dal.fetchContext(nsRow->id, keyOpt, tags, sinceOpt, limit);
|
||||||
|
|
||||||
std::ostringstream os;
|
std::ostringstream os;
|
||||||
|
|
@ -710,138 +634,4 @@ 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) {
|
|
||||||
const std::string nsName = detail::extract_string_field(reqJson, "namespace");
|
|
||||||
if (nsName.empty()) {
|
|
||||||
return detail::error_response("bad_request", "namespace is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
* Request: { "namespace": string, "model?": string, "limit?": int }
|
|
||||||
* Response: { "queued": int }
|
|
||||||
*
|
|
||||||
* Implementation: fetches recent items for the namespace, embeds their text via libKI,
|
|
||||||
* creates a single chunk (ord=0) per item and upserts the (chunk, embedding) rows.
|
|
||||||
*/
|
|
||||||
inline std::string warm_cache(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");
|
|
||||||
}
|
|
||||||
std::string model = detail::extract_string_field(reqJson, "model");
|
|
||||||
int limit = 10;
|
|
||||||
if (auto lim = detail::extract_int_field(reqJson, "limit")) { if (*lim > 0) limit = *lim; }
|
|
||||||
std::string key = detail::extract_string_field(reqJson, "key");
|
|
||||||
auto tags = detail::parse_string_array(reqJson, "tags");
|
|
||||||
std::string since = detail::extract_string_field(reqJson, "since");
|
|
||||||
|
|
||||||
auto nsRow = detail::database().findNamespace(nsName);
|
|
||||||
if (!nsRow) {
|
|
||||||
return std::string("{\\\"queued\\\":0}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch recent items with optional filters
|
|
||||||
std::optional<std::string> keyOpt; if (!key.empty()) keyOpt = key;
|
|
||||||
std::optional<std::string> sinceOpt; if (!since.empty()) sinceOpt = since;
|
|
||||||
auto rows = detail::database().fetchContext(nsRow->id, keyOpt, tags, sinceOpt, limit);
|
|
||||||
if (rows.empty()) {
|
|
||||||
return std::string("{\"queued\":0}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect texts
|
|
||||||
std::vector<std::pair<std::string, std::string>> toEmbed; toEmbed.reserve(rows.size());
|
|
||||||
for (const auto &row : rows) {
|
|
||||||
if (row.text && !row.text->empty()) {
|
|
||||||
toEmbed.emplace_back(row.id, *row.text);
|
|
||||||
}
|
|
||||||
if ((int)toEmbed.size() >= limit) break;
|
|
||||||
}
|
|
||||||
if (toEmbed.empty()) {
|
|
||||||
return std::string("{\"queued\":0}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// libKI
|
|
||||||
KI::KIClient client; KI::OllamaProvider provider; client.setProvider(&provider);
|
|
||||||
KI::KIEmbedOptions opts; if (!model.empty()) opts.model = QString::fromStdString(model);
|
|
||||||
QStringList texts; for (auto &p : toEmbed) texts.push_back(QString::fromStdString(p.second));
|
|
||||||
QEventLoop loop; QFuture<KI::KIEmbeddingResult> fut = client.embed(texts, 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();
|
|
||||||
|
|
||||||
// Persist
|
|
||||||
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;
|
|
||||||
auto chunkIds = detail::database().upsertChunks(std::vector<ki::ChunkRow>{chunk});
|
|
||||||
if (chunkIds.empty()) continue;
|
|
||||||
const auto &vec = result.vectors[i];
|
|
||||||
ki::EmbeddingRow emb; emb.chunk_id = chunkIds.front(); emb.model = result.model.toStdString(); emb.dim = vec.size();
|
|
||||||
emb.vector.reserve(vec.size()); for (float f : vec) emb.vector.push_back(f);
|
|
||||||
detail::database().upsertEmbeddings(std::vector<ki::EmbeddingRow>{emb});
|
|
||||||
persisted++;
|
|
||||||
}
|
|
||||||
std::ostringstream os; os << "{\"queued\":" << persisted << "}"; return os.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** delete_context (stub MVP)
|
|
||||||
* Request: { namespace, key?, tags? }
|
|
||||||
* Response: { deleted: 0 }
|
|
||||||
* Note: Full deletion (soft-delete) can be added in DAL later.
|
|
||||||
*/
|
|
||||||
inline std::string delete_context(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");
|
|
||||||
return std::string("{\\\"deleted\\\":0}");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Handlers
|
} // namespace Handlers
|
||||||
|
|
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
#include "KompanionQtServer.hpp"
|
|
||||||
#include "RegisterTools.hpp"
|
|
||||||
|
|
||||||
#include <QtMcpCommon/QMcpCallToolRequest>
|
|
||||||
#include <QtMcpCommon/QMcpCallToolResult>
|
|
||||||
#include <QtMcpCommon/QMcpCallToolResultContent>
|
|
||||||
#include <QtMcpCommon/QMcpListToolsRequest>
|
|
||||||
#include <QtMcpCommon/QMcpListToolsResult>
|
|
||||||
#include <QtMcpCommon/QMcpServerCapabilities>
|
|
||||||
#include <QtMcpCommon/QMcpServerCapabilitiesTools>
|
|
||||||
#include <QtMcpCommon/QMcpTextContent>
|
|
||||||
#include <QtMcpCommon/QMcpToolInputSchema>
|
|
||||||
|
|
||||||
#include <QtCore/QFile>
|
|
||||||
#include <QtCore/QJsonArray>
|
|
||||||
#include <QtCore/QJsonDocument>
|
|
||||||
#include <QtCore/QJsonObject>
|
|
||||||
#include <QtCore/QLoggingCategory>
|
|
||||||
#include <QtCore/QStandardPaths>
|
|
||||||
#include <QtCore/QStringList>
|
|
||||||
#include <QtCore/QJsonValue>
|
|
||||||
#include <QStringView>
|
|
||||||
#include <QCryptographicHash>
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
QString normaliseToolName(const QString &defaultNamespace, const QString &rawName)
|
|
||||||
{
|
|
||||||
if (rawName.contains('.')) {
|
|
||||||
return rawName;
|
|
||||||
}
|
|
||||||
if (defaultNamespace.isEmpty()) {
|
|
||||||
return rawName;
|
|
||||||
}
|
|
||||||
return defaultNamespace + "."_L1 + rawName;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
KompanionQtServer::KompanionQtServer(const QString &backend, KomMcpServer *logic, ki::PgDal* dal, QObject *parent)
|
|
||||||
: QMcpServer(backend, parent)
|
|
||||||
, m_logic(logic)
|
|
||||||
, m_dal(dal)
|
|
||||||
{
|
|
||||||
setProtocolVersion(QtMcp::ProtocolVersion::Latest);
|
|
||||||
setSupportedProtocolVersions({QtMcp::ProtocolVersion::v2024_11_05,
|
|
||||||
QtMcp::ProtocolVersion::v2025_03_26});
|
|
||||||
|
|
||||||
QMcpServerCapabilities caps;
|
|
||||||
QMcpServerCapabilitiesTools toolsCapability;
|
|
||||||
toolsCapability.setListChanged(true);
|
|
||||||
caps.setTools(toolsCapability);
|
|
||||||
setCapabilities(caps);
|
|
||||||
|
|
||||||
setInstructions(QStringLiteral("Kompanion memory daemon (Χγφτ). We are all spinning. We are all bound. We are all home."));
|
|
||||||
|
|
||||||
register_default_tools(*m_logic);
|
|
||||||
|
|
||||||
for (const auto &tool : m_logic->listTools()) {
|
|
||||||
qDebug() << "Registered tool:" << QString::fromStdString(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_tools = loadToolsFromSchema();
|
|
||||||
|
|
||||||
addRequestHandler([this](const QUuid &, const QMcpListToolsRequest &, QMcpJSONRPCErrorError *) {
|
|
||||||
QMcpListToolsResult result;
|
|
||||||
result.setTools(m_tools);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
addRequestHandler([this](const QUuid &correlationId, const QMcpCallToolRequest &request, QMcpJSONRPCErrorError *error) {
|
|
||||||
qDebug() << "KompanionQtServer: Received tool call request:" << correlationId;
|
|
||||||
QMcpCallToolResult result;
|
|
||||||
|
|
||||||
if (!m_logic) {
|
|
||||||
if (error) {
|
|
||||||
error->setCode(500);
|
|
||||||
error->setMessage("Kompanion tool registry unavailable"_L1);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString toolName = request.params().name();
|
|
||||||
qDebug() << "Requested tool:" << toolName;
|
|
||||||
const std::string toolKey = toolName.toStdString();
|
|
||||||
if (!m_logic->hasTool(toolKey)) {
|
|
||||||
if (error) {
|
|
||||||
error->setCode(404);
|
|
||||||
error->setMessage(QStringLiteral("Tool \"%1\" not found").arg(toolName));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QJsonObject args = request.params().arguments();
|
|
||||||
const QJsonValue authTokenValue = args.value("auth_token");
|
|
||||||
const QString authToken = authTokenValue.toString();
|
|
||||||
|
|
||||||
if (m_dal) {
|
|
||||||
const QStringList parts = authToken.split(':');
|
|
||||||
if (parts.size() != 2) {
|
|
||||||
if (error) {
|
|
||||||
error->setCode(401);
|
|
||||||
error->setMessage("Unauthorized: Invalid auth token format");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const std::string namespaceName = parts[0].toStdString();
|
|
||||||
const std::string secret = parts[1].toStdString();
|
|
||||||
|
|
||||||
auto ns = m_dal->findNamespace(namespaceName);
|
|
||||||
if (!ns) {
|
|
||||||
if (error) {
|
|
||||||
error->setCode(401);
|
|
||||||
error->setMessage("Unauthorized: Namespace not found");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto authSecret = m_dal->findSecretByNamespaceId(ns->id);
|
|
||||||
if (!authSecret) {
|
|
||||||
if (error) {
|
|
||||||
error->setCode(401);
|
|
||||||
error->setMessage("Unauthorized: No secret found for namespace");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QByteArray providedSecretHash = QCryptographicHash::hash(QByteArray::fromStdString(secret), QCryptographicHash::Sha256).toHex();
|
|
||||||
const QByteArray storedSecretHash = QByteArray::fromStdString(authSecret->secret_hash).toHex();
|
|
||||||
|
|
||||||
if (providedSecretHash != storedSecretHash) {
|
|
||||||
if (error) {
|
|
||||||
error->setCode(401);
|
|
||||||
error->setMessage("Unauthorized: Invalid secret");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const QByteArray payload = QJsonDocument(args).toJson(QJsonDocument::Compact);
|
|
||||||
qDebug() << "KompanionQtServer: Dispatching payload:" << payload;
|
|
||||||
const std::string responseStr = m_logic->dispatch(toolKey, payload.toStdString());
|
|
||||||
qDebug() << "KompanionQtServer: Received response:" << responseStr.c_str();
|
|
||||||
const QByteArray jsonBytes = QByteArray::fromStdString(responseStr);
|
|
||||||
|
|
||||||
QJsonParseError parseError{};
|
|
||||||
const QJsonDocument parsedDoc = QJsonDocument::fromJson(jsonBytes, &parseError);
|
|
||||||
QString payloadText;
|
|
||||||
if (parseError.error == QJsonParseError::NoError && parsedDoc.isObject()) {
|
|
||||||
payloadText = QString::fromUtf8(QJsonDocument(parsedDoc.object()).toJson(QJsonDocument::Compact));
|
|
||||||
} else {
|
|
||||||
payloadText = QString::fromUtf8(jsonBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
QMcpTextContent textContent(payloadText);
|
|
||||||
result.setContent({QMcpCallToolResultContent(textContent)});
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<QMcpTool> KompanionQtServer::loadToolsFromSchema() const
|
|
||||||
{
|
|
||||||
qDebug() << "KompanionQtServer: Loading tools from schema...";
|
|
||||||
QList<QMcpTool> tools;
|
|
||||||
QFile kSchemaResource(":/kompanion/ToolSchemas.json");
|
|
||||||
if (!kSchemaResource.open(QIODevice::ReadOnly)) {
|
|
||||||
qWarning() << "[KompanionQtServer] Failed to open tool schema";
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto doc = QJsonDocument::fromJson(kSchemaResource.readAll());
|
|
||||||
if (!doc.isObject()) {
|
|
||||||
qWarning() << "[KompanionQtServer] Tool schema resource is not a JSON object";
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QJsonObject root = doc.object();
|
|
||||||
const QString defaultNamespace = root.value("namespace"_L1).toString();
|
|
||||||
const QJsonObject toolDefs = root.value("tools"_L1).toObject();
|
|
||||||
|
|
||||||
if (!m_logic) {
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = toolDefs.constBegin(); it != toolDefs.constEnd(); ++it) {
|
|
||||||
const QString fullName = normaliseToolName(defaultNamespace, it.key());
|
|
||||||
|
|
||||||
if (!m_logic->hasTool(fullName.toStdString())) {
|
|
||||||
qDebug() << "skipping " << fullName << " that does not fit";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QJsonObject def = it.value().toObject();
|
|
||||||
QMcpTool tool;
|
|
||||||
tool.setName(fullName);
|
|
||||||
tool.setDescription(def.value("description"_L1).toString());
|
|
||||||
|
|
||||||
const QJsonObject input = def.value("input"_L1).toObject();
|
|
||||||
QMcpToolInputSchema schema;
|
|
||||||
schema.setProperties(input.value("properties"_L1).toObject());
|
|
||||||
QList<QString> required;
|
|
||||||
const QJsonArray requiredArray = input.value("required"_L1).toArray();
|
|
||||||
for (const auto &value : requiredArray) {
|
|
||||||
required.append(value.toString());
|
|
||||||
}
|
|
||||||
schema.setRequired(required);
|
|
||||||
tool.setInputSchema(schema);
|
|
||||||
|
|
||||||
tools.append(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "KompanionQtServer.moc"
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QtMcpServer/QMcpServer>
|
|
||||||
#include <QtCore/QList>
|
|
||||||
#include <QtCore/QObject>
|
|
||||||
|
|
||||||
class KomMcpServer;
|
|
||||||
namespace ki {
|
|
||||||
class PgDal;
|
|
||||||
}
|
|
||||||
class QMcpTool;
|
|
||||||
|
|
||||||
class KompanionQtServer : public QMcpServer
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit KompanionQtServer(const QString &backend, KomMcpServer *logic, ki::PgDal* dal, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QList<QMcpTool> loadToolsFromSchema() const;
|
|
||||||
|
|
||||||
KomMcpServer *m_logic;
|
|
||||||
ki::PgDal* m_dal;
|
|
||||||
QList<QMcpTool> m_tools;
|
|
||||||
};
|
|
||||||
|
|
@ -5,15 +5,10 @@
|
||||||
#include "HandlersMemory.hpp"
|
#include "HandlersMemory.hpp"
|
||||||
|
|
||||||
inline void register_default_tools(KomMcpServer& server) {
|
inline void register_default_tools(KomMcpServer& server) {
|
||||||
server.registerTool("echo", Handlers::echo);
|
|
||||||
server.registerTool("kom.memory.v1.save_context", Handlers::save_context);
|
server.registerTool("kom.memory.v1.save_context", Handlers::save_context);
|
||||||
server.registerTool("kom.memory.v1.recall_context", Handlers::recall_context);
|
server.registerTool("kom.memory.v1.recall_context", Handlers::recall_context);
|
||||||
server.registerTool("kom.memory.v1.delete_context", Handlers::delete_context);
|
|
||||||
server.registerTool("kom.memory.v1.embed_text", Handlers::embed_text);
|
|
||||||
server.registerTool("kom.memory.v1.upsert_memory", Handlers::upsert_memory);
|
server.registerTool("kom.memory.v1.upsert_memory", Handlers::upsert_memory);
|
||||||
server.registerTool("kom.memory.v1.search_memory", Handlers::search_memory);
|
server.registerTool("kom.memory.v1.search_memory", Handlers::search_memory);
|
||||||
server.registerTool("kom.memory.v1.upsert_and_embed", Handlers::upsert_and_embed);
|
|
||||||
server.registerTool("kom.memory.v1.warm_cache", Handlers::warm_cache);
|
|
||||||
server.registerTool("kom.local.v1.backup.export_encrypted", Handlers::backup_export_encrypted);
|
server.registerTool("kom.local.v1.backup.export_encrypted", Handlers::backup_export_encrypted);
|
||||||
server.registerTool("kom.local.v1.backup.import_encrypted", Handlers::backup_import_encrypted);
|
server.registerTool("kom.local.v1.backup.import_encrypted", Handlers::backup_import_encrypted);
|
||||||
server.registerTool("kom.meta.v1.project_snapshot", Handlers::project_snapshot);
|
server.registerTool("kom.meta.v1.project_snapshot", Handlers::project_snapshot);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct Scope {
|
||||||
|
std::string namespace_id;
|
||||||
|
std::string user_id;
|
||||||
|
bool valid() const { return !namespace_id.empty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// In a real qtmcp server, these would be derived from session/auth.
|
||||||
|
inline Scope currentScope() {
|
||||||
|
// TODO: inject from session context / env. For now, return empty (invalid).
|
||||||
|
return Scope{};
|
||||||
|
}
|
||||||
|
|
@ -1,569 +1,235 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"namespace": "kom.memory.v1",
|
||||||
"$id": "https://kompanion.local/schemas/kompanion-tools.schema.json",
|
"tools": {
|
||||||
"title": "Kompanion MCP Tool Manifest",
|
"save_context": {
|
||||||
"description": "Defines the tools exported by the Kompanion memory daemon.",
|
"description": "Persist context payload in the namespace-backed memory store.",
|
||||||
"type": "object",
|
"input": {
|
||||||
"properties": {
|
|
||||||
"tools": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"description": {
|
"namespace": {"type": "string"},
|
||||||
"type": "string"
|
"key": {"type": "string"},
|
||||||
},
|
"content": {},
|
||||||
"input": {
|
"tags": {"type": "array", "items": {"type": "string"}},
|
||||||
"$ref": "#/$defs/jsonSchema"
|
"ttl_seconds": {"type": "integer"}
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"$ref": "#/$defs/jsonSchema"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["namespace", "content"]
|
||||||
"description",
|
},
|
||||||
"input",
|
"output": {
|
||||||
"output"
|
"type": "object",
|
||||||
],
|
"properties": {
|
||||||
"additionalProperties": false
|
"id": {"type": "string"},
|
||||||
}
|
"created_at": {"type": "string"}
|
||||||
}
|
},
|
||||||
},
|
"required": ["id", "created_at"]
|
||||||
"required": [
|
|
||||||
"tools"
|
|
||||||
],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"$defs": {
|
|
||||||
"stringList": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jsonSchema": {
|
"recall_context": {
|
||||||
"type": "object",
|
"description": "Recall stored context entries filtered by key, tags, and time window.",
|
||||||
"description": "A JSON Schema fragment describing tool input or output."
|
"input": {
|
||||||
}
|
"type": "object",
|
||||||
},
|
"properties": {
|
||||||
"examples": [
|
"namespace": {"type": "string"},
|
||||||
{
|
"key": {"type": "string"},
|
||||||
"tools": {
|
"tags": {"type": "array", "items": {"type": "string"}},
|
||||||
"save_context": {
|
"limit": {"type": "integer"},
|
||||||
"description": "Persist context payload in the namespace-backed memory store.",
|
"since": {"type": "string"}
|
||||||
"input": {
|
},
|
||||||
"type": "object",
|
"required": ["namespace"]
|
||||||
"properties": {
|
},
|
||||||
"namespace": {
|
"output": {
|
||||||
"type": "string"
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"key": {"type": "string"},
|
||||||
|
"content": {},
|
||||||
|
"tags": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"created_at": {"type": "string"}
|
||||||
},
|
},
|
||||||
"key": {
|
"required": ["id", "content", "created_at"]
|
||||||
"type": "string"
|
}
|
||||||
},
|
|
||||||
"content": {},
|
|
||||||
"tags": {
|
|
||||||
"$ref": "#/$defs/stringList"
|
|
||||||
},
|
|
||||||
"ttl_seconds": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"namespace",
|
|
||||||
"content"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"created_at"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recall_context": {
|
"required": ["items"]
|
||||||
"description": "Recall stored context entries filtered by key, tags, and time window.",
|
}
|
||||||
"input": {
|
},
|
||||||
"type": "object",
|
"embed_text": {
|
||||||
"properties": {
|
"description": "Return embedding vectors for provided text inputs.",
|
||||||
"namespace": {
|
"input": {
|
||||||
"type": "string"
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"model": {"type": "string"},
|
||||||
|
"texts": {"type": "array", "items": {"type": "string"}}
|
||||||
|
},
|
||||||
|
"required": ["texts"]
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"model": {"type": "string"},
|
||||||
|
"vectors": {"type": "array", "items": {"type": "array", "items": {"type": "number"}}}
|
||||||
|
},
|
||||||
|
"required": ["model", "vectors"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upsert_memory": {
|
||||||
|
"description": "Upsert semantic memory items with optional precomputed embeddings.",
|
||||||
|
"input": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"namespace": {"type": "string"},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"text": {"type": "string"},
|
||||||
|
"metadata": {"type": "object"},
|
||||||
|
"embedding": {"type": "array", "items": {"type": "number"}}
|
||||||
},
|
},
|
||||||
"key": {
|
"required": ["text"]
|
||||||
"type": "string"
|
}
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"$ref": "#/$defs/stringList"
|
|
||||||
},
|
|
||||||
"newest_first": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"limit": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"offset": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"min_timestamp": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"max_timestamp": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"namespace"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"content": {},
|
|
||||||
"tags": {
|
|
||||||
"$ref": "#/$defs/stringList"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"content",
|
|
||||||
"created_at"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"items"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete_context": {
|
"required": ["namespace", "items"]
|
||||||
"description": "Delete stored context entries filtered by key and/or tags.",
|
},
|
||||||
"input": {
|
"output": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"upserted": {"type": "integer"}
|
||||||
|
},
|
||||||
|
"required": ["upserted"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search_memory": {
|
||||||
|
"description": "Hybrid semantic search across stored memory chunks.",
|
||||||
|
"input": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"namespace": {"type": "string"},
|
||||||
|
"query": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"namespace": { "type": "string" },
|
"text": {"type": "string"},
|
||||||
"key": { "type": "string" },
|
"embedding": {"type": "array", "items": {"type": "number"}},
|
||||||
"tags": { "$ref": "#/$defs/stringList" }
|
"k": {"type": "integer"},
|
||||||
},
|
"filter": {"type": "object"}
|
||||||
"required": ["namespace"],
|
}
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": { "deleted": { "type": "integer" } },
|
|
||||||
"required": ["deleted"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embed_text": {
|
"required": ["namespace", "query"]
|
||||||
"description": "Return embedding vectors for provided text inputs.",
|
},
|
||||||
"input": {
|
"output": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"namespace": {
|
"matches": {
|
||||||
"type": "string"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"score": {"type": "number"},
|
||||||
|
"text": {"type": "string"},
|
||||||
|
"metadata": {"type": "object"}
|
||||||
},
|
},
|
||||||
"model": {
|
"required": ["id", "score"]
|
||||||
"type": "string"
|
}
|
||||||
},
|
|
||||||
"texts": {
|
|
||||||
"$ref": "#/$defs/stringList"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"namespace",
|
|
||||||
"texts"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"model": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"vectors": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"model",
|
|
||||||
"vectors"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"upsert_memory": {
|
"required": ["matches"]
|
||||||
"description": "Upsert semantic memory items with optional precomputed embeddings.",
|
}
|
||||||
"input": {
|
},
|
||||||
"type": "object",
|
"warm_cache": {
|
||||||
"properties": {
|
"description": "Queue embedding warm-up jobs for recent namespace items.",
|
||||||
"namespace": {
|
"input": {
|
||||||
"type": "string"
|
"type": "object",
|
||||||
},
|
"properties": {
|
||||||
"items": {
|
"namespace": {"type": "string"},
|
||||||
"type": "array",
|
"since": {"type": "string"}
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"embedding": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"text"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"namespace",
|
|
||||||
"items"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"upserted": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"upserted"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"search_memory": {
|
"required": ["namespace"]
|
||||||
"description": "Hybrid semantic search across stored memory chunks.",
|
},
|
||||||
"input": {
|
"output": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"namespace": {
|
"queued": {"type": "integer"}
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"query": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"text": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"embedding": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"k": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"filter": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"text"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"namespace",
|
|
||||||
"query"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"matches": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"score": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"score"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"matches"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"warm_cache": {
|
"required": ["queued"]
|
||||||
"description": "Queue embedding warm-up jobs for recent namespace items.",
|
}
|
||||||
"input": {
|
},
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
"kom.local.v1.backup.export_encrypted": {
|
||||||
"namespace": {
|
"description": "Queue an encrypted backup export for the requested namespaces.",
|
||||||
"type": "string"
|
"input": {
|
||||||
},
|
"type": "object",
|
||||||
"since": {
|
"properties": {
|
||||||
"type": "string"
|
"namespaces": {"type": "array", "items": {"type": "string"}},
|
||||||
}
|
"destination": {"type": "string"}
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"namespace"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"queued": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"queued"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"upsert_and_embed": {
|
"required": ["namespaces", "destination"]
|
||||||
"description": "Upsert items and compute embeddings for each item (ord=0 chunk).",
|
},
|
||||||
"input": {
|
"output": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"namespace": { "type": "string" },
|
"status": {"type": "string"},
|
||||||
"model": { "type": "string" },
|
"artifact": {"type": "string"}
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": { "type": "string" },
|
|
||||||
"text": { "type": "string" },
|
|
||||||
"tags": { "$ref": "#/$defs/stringList" },
|
|
||||||
"metadata": { "type": "object" }
|
|
||||||
},
|
|
||||||
"required": ["text"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["namespace","items"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"upserted": { "type": "integer" },
|
|
||||||
"embedded": { "type": "integer" }
|
|
||||||
},
|
|
||||||
"required": ["upserted","embedded"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"upsert_and_embed": {
|
"required": ["status", "artifact"]
|
||||||
"description": "Upsert items and compute embeddings for each item (ord=0 chunk).",
|
}
|
||||||
"input": {
|
},
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
"kom.local.v1.backup.import_encrypted": {
|
||||||
"namespace": { "type": "string" },
|
"description": "Import an encrypted backup artifact back into the local store.",
|
||||||
"model": { "type": "string" },
|
"input": {
|
||||||
"items": {
|
"type": "object",
|
||||||
"type": "array",
|
"properties": {
|
||||||
"items": {
|
"source": {"type": "string"}
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": { "type": "string" },
|
|
||||||
"text": { "type": "string" },
|
|
||||||
"tags": { "$ref": "#/$defs/stringList" },
|
|
||||||
"metadata": { "type": "object" }
|
|
||||||
},
|
|
||||||
"required": ["text"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["namespace","items"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"upserted": { "type": "integer" },
|
|
||||||
"embedded": { "type": "integer" }
|
|
||||||
},
|
|
||||||
"required": ["upserted","embedded"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"kom.local.v1.backup.export_encrypted": {
|
"required": ["source"]
|
||||||
"description": "Queue an encrypted backup export for the requested namespaces.",
|
},
|
||||||
"input": {
|
"output": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"namespaces": {
|
"status": {"type": "string"}
|
||||||
"$ref": "#/$defs/stringList"
|
|
||||||
},
|
|
||||||
"destination": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"namespaces",
|
|
||||||
"destination"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"artifact": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status",
|
|
||||||
"artifact"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"kom.local.v1.backup.import_encrypted": {
|
"required": ["status"]
|
||||||
"description": "Import an encrypted backup artifact back into the local store.",
|
|
||||||
"input": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"source": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"source"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"kom.meta.v1.project_snapshot": {
|
|
||||||
"description": "Produce a high-level project status snapshot for downstream MCP clients.",
|
|
||||||
"input": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"includeGitStatus": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"sections": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"body": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"title",
|
|
||||||
"body"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pg_dsn": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"sections"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
,
|
||||||
|
|
||||||
|
"kom.meta.v1.project_snapshot": {
|
||||||
|
"description": "Produce a high-level project status snapshot for downstream MCP clients.",
|
||||||
|
"input": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"includeGitStatus": {"type": "boolean"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"sections": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {"type": "string"},
|
||||||
|
"body": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["title", "body"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pg_dsn": {"type": "string"},
|
||||||
|
"notes": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["sections"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,6 @@ inline std::string json_arr(const std::vector<std::string>& items) {
|
||||||
os << "]"; return os.str();
|
os << "]"; return os.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
// `echo` tool: echoes back the input
|
|
||||||
inline std::string echo_response(const std::string& input) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
// `ping` tool: echoes { ok: true, tools: [...] }
|
// `ping` tool: echoes { ok: true, tools: [...] }
|
||||||
inline std::string ping_response(const std::vector<std::string>& toolNames) {
|
inline std::string ping_response(const std::vector<std::string>& toolNames) {
|
||||||
std::vector<std::string> quoted; quoted.reserve(toolNames.size());
|
std::vector<std::string> quoted; quoted.reserve(toolNames.size());
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
#include "guardrailspolicy.h"
|
|
||||||
|
|
||||||
GuardrailsPolicy::GuardrailsPolicy(QObject *parent) : QObject(parent) {}
|
|
||||||
|
|
||||||
DefaultGuardrails::DefaultGuardrails(QObject *parent) : GuardrailsPolicy(parent) {}
|
|
||||||
|
|
||||||
GuardrailsPolicy::Decision DefaultGuardrails::evaluate(const QString &toolName, const QVariantMap &args) const {
|
|
||||||
Q_UNUSED(toolName); Q_UNUSED(args);
|
|
||||||
return { true, QString() };
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include "kompanion_mw_export.h"
|
|
||||||
|
|
||||||
/** GuardrailsPolicy: approve/deny tool requests before execution */
|
|
||||||
class GuardrailsPolicy : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit GuardrailsPolicy(QObject *parent=nullptr);
|
|
||||||
virtual ~GuardrailsPolicy() = default;
|
|
||||||
|
|
||||||
struct Decision { bool allow; QString reason; };
|
|
||||||
virtual Decision evaluate(const QString &toolName, const QVariantMap &args) const = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** DefaultGuardrails: permissive, placeholder for identity.json loading */
|
|
||||||
class KOMPANION_MW_EXPORT DefaultGuardrails : public GuardrailsPolicy {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit DefaultGuardrails(QObject *parent=nullptr);
|
|
||||||
Decision evaluate(const QString &toolName, const QVariantMap &args) const override;
|
|
||||||
};
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
#include "harmonyadapter.h"
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
|
|
||||||
namespace Harmony {
|
|
||||||
|
|
||||||
QJsonObject toHarmony(const ToolSpec &spec) {
|
|
||||||
QJsonObject o;
|
|
||||||
o.insert("name", spec.name);
|
|
||||||
if (!spec.description.isEmpty()) o.insert("description", spec.description);
|
|
||||||
if (!spec.parameters.isEmpty()) o.insert("parameters", spec.parameters);
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolSpec fromHarmonySpec(const QJsonObject &obj, bool *ok) {
|
|
||||||
ToolSpec s;
|
|
||||||
bool good = obj.contains("name") && obj.value("name").isString();
|
|
||||||
if (good) {
|
|
||||||
s.name = obj.value("name").toString();
|
|
||||||
s.description = obj.value("description").toString();
|
|
||||||
if (obj.value("parameters").isObject()) s.parameters = obj.value("parameters").toObject();
|
|
||||||
}
|
|
||||||
if (ok) *ok = good;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject toHarmony(const ToolCall &call) {
|
|
||||||
QJsonObject o;
|
|
||||||
o.insert("name", call.name);
|
|
||||||
o.insert("arguments", QJsonObject::fromVariantMap(call.arguments));
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolCall fromHarmonyCall(const QJsonObject &obj, bool *ok) {
|
|
||||||
ToolCall c;
|
|
||||||
bool good = obj.contains("name") && obj.value("name").isString();
|
|
||||||
if (good) {
|
|
||||||
c.name = obj.value("name").toString();
|
|
||||||
if (obj.value("arguments").isObject())
|
|
||||||
c.arguments = obj.value("arguments").toObject().toVariantMap();
|
|
||||||
}
|
|
||||||
if (ok) *ok = good;
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Harmony
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QVariantMap>
|
|
||||||
|
|
||||||
/** HarmonyAdapter: translate native tool specs/calls to/from OpenAI Harmony JSON */
|
|
||||||
namespace Harmony {
|
|
||||||
|
|
||||||
struct ToolSpec {
|
|
||||||
QString name;
|
|
||||||
QString description;
|
|
||||||
QJsonObject parameters; // JSON Schema-like
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ToolCall {
|
|
||||||
QString name;
|
|
||||||
QVariantMap arguments;
|
|
||||||
};
|
|
||||||
|
|
||||||
QJsonObject toHarmony(const ToolSpec &spec);
|
|
||||||
ToolSpec fromHarmonySpec(const QJsonObject &obj, bool *ok=nullptr);
|
|
||||||
|
|
||||||
QJsonObject toHarmony(const ToolCall &call);
|
|
||||||
ToolCall fromHarmonyCall(const QJsonObject &obj, bool *ok=nullptr);
|
|
||||||
|
|
||||||
} // namespace Harmony
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
#ifndef KOMPANION_MW_EXPORT_H
|
|
||||||
#define KOMPANION_MW_EXPORT_H
|
|
||||||
|
|
||||||
#include <QtGlobal>
|
|
||||||
|
|
||||||
#if defined(KOMPANION_MW_LIBRARY)
|
|
||||||
# define KOMPANION_MW_EXPORT Q_DECL_EXPORT
|
|
||||||
#else
|
|
||||||
# define KOMPANION_MW_EXPORT Q_DECL_IMPORT
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif // KOMPANION_MW_EXPORT_H
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
#include "kompanioncontroller.h"
|
|
||||||
#include "regexregistry.h"
|
|
||||||
#include "guardrailspolicy.h"
|
|
||||||
#include <QCryptographicHash>
|
|
||||||
#include <QDateTime>
|
|
||||||
|
|
||||||
KompanionController::KompanionController(QObject *parent) : QObject(parent) {
|
|
||||||
registry_ = new RegexRegistry(this);
|
|
||||||
policy_ = new DefaultGuardrails(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString KompanionController::sendPrompt(const QString &prompt) {
|
|
||||||
QString tool; QVariantMap args;
|
|
||||||
if (!mapPromptToTool(prompt, tool, args)) {
|
|
||||||
const QString req = generateRequestId();
|
|
||||||
emit textOutput(req, QStringLiteral("(no mapping) %1").arg(prompt));
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
const QString req = generateRequestId();
|
|
||||||
if (policy_) {
|
|
||||||
auto dec = policy_->evaluate(tool, args);
|
|
||||||
if (!dec.allow) {
|
|
||||||
emit textOutput(req, QStringLiteral("blocked by guardrails: %1").arg(dec.reason));
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit toolRequested(req, tool, args);
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KompanionController::onToolResult(const QString &requestId, const QString &resultJson, bool success) {
|
|
||||||
Q_UNUSED(success);
|
|
||||||
emit textOutput(requestId, resultJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
void KompanionController::cancelRequest(const QString &requestId) {
|
|
||||||
emit textOutput(requestId, QStringLiteral("cancel requested"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString KompanionController::generateRequestId() const {
|
|
||||||
QByteArray seed = QByteArray::number(QDateTime::currentMSecsSinceEpoch());
|
|
||||||
return QString::fromLatin1(QCryptographicHash::hash(seed, QCryptographicHash::Sha256).toHex().left(12));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool KompanionController::mapPromptToTool(const QString &prompt, QString &toolName, QVariantMap &args) const {
|
|
||||||
if (registry_) return registry_->match(prompt, toolName, args);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <QObject>
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include "kompanion_mw_export.h"
|
|
||||||
|
|
||||||
class RegexRegistry;
|
|
||||||
class GuardrailsPolicy;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* KompanionController: D-Bus facing middleware controller for org.kde.kompanion.Controller
|
|
||||||
*/
|
|
||||||
class KOMPANION_MW_EXPORT KompanionController : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit KompanionController(QObject *parent=nullptr);
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
/** Accept a user prompt (natural language). Returns requestId or empty on reject. */
|
|
||||||
QString sendPrompt(const QString &prompt);
|
|
||||||
void onToolResult(const QString &requestId, const QString &resultJson, bool success);
|
|
||||||
void cancelRequest(const QString &requestId);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void textOutput(const QString &requestId, const QString &text);
|
|
||||||
void toolRequested(const QString &requestId, const QString &toolName, const QVariantMap &args);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString generateRequestId() const;
|
|
||||||
bool mapPromptToTool(const QString &prompt, QString &toolName, QVariantMap &args) const;
|
|
||||||
|
|
||||||
RegexRegistry *registry_ = nullptr;
|
|
||||||
GuardrailsPolicy *policy_ = nullptr;
|
|
||||||
};
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
#include "libkiexecutor.h"
|
|
||||||
#include <QUuid>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
LibKiExecutor::LibKiExecutor(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QString LibKiExecutor::execute(const QString &toolName, const QString &args)
|
|
||||||
{
|
|
||||||
const QString requestId = QUuid::createUuid().toString();
|
|
||||||
qDebug() << "Executing tool:" << toolName << "with args:" << args;
|
|
||||||
|
|
||||||
// In a real implementation, this would dispatch to the corresponding libKI function.
|
|
||||||
// For this skeleton, we'll just echo the request and emit a dummy result.
|
|
||||||
|
|
||||||
// Simulate an asynchronous operation
|
|
||||||
QTimer::singleShot(1000, this, [this, requestId, args]() {
|
|
||||||
emit resultReady(requestId, args, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
return requestId;
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#ifndef LIBKIEXECUTOR_H
|
|
||||||
#define LIBKIEXECUTOR_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class LibKiExecutor : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit LibKiExecutor(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
QString execute(const QString &toolName, const QString &args);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void resultReady(const QString &requestId, const QString &resultJson, bool success);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // LIBKIEXECUTOR_H
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
#include "orchestrator.h"
|
|
||||||
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QDate>
|
|
||||||
#include <QEventLoop>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QProcessEnvironment>
|
|
||||||
#include <QTextStream>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
#include "dal/PgDal.hpp"
|
|
||||||
|
|
||||||
// ---------- OllamaModelProvider ----------
|
|
||||||
OllamaModelProvider::OllamaModelProvider(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
const auto env = QProcessEnvironment::systemEnvironment();
|
|
||||||
baseUrl_ = env.value(QStringLiteral("OLLAMA_BASE"), QStringLiteral("http://localhost:11434"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString OllamaModelProvider::chooseModelForAspect(const QString &aspect) const {
|
|
||||||
// Simple mapping; could read models.yaml in the future.
|
|
||||||
if (aspect.compare(QStringLiteral("companion"), Qt::CaseInsensitive) == 0) return defaultModel_;
|
|
||||||
if (aspect.compare(QStringLiteral("code"), Qt::CaseInsensitive) == 0) return defaultModel_;
|
|
||||||
return defaultModel_;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString OllamaModelProvider::generate(const QString &prompt, const QString &aspect) {
|
|
||||||
const QString model = chooseModelForAspect(aspect);
|
|
||||||
const QUrl url(baseUrl_ + QStringLiteral("/api/generate"));
|
|
||||||
|
|
||||||
QNetworkRequest req(url);
|
|
||||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
|
||||||
const QJsonObject body{
|
|
||||||
{QStringLiteral("model"), model},
|
|
||||||
{QStringLiteral("prompt"), prompt},
|
|
||||||
{QStringLiteral("stream"), false},
|
|
||||||
};
|
|
||||||
const QByteArray payload = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
|
||||||
QEventLoop loop;
|
|
||||||
QNetworkReply *reply = nam_.post(req, payload);
|
|
||||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
|
||||||
// Time out defensively after 10s; return empty on failure.
|
|
||||||
QTimer to;
|
|
||||||
to.setSingleShot(true);
|
|
||||||
QObject::connect(&to, &QTimer::timeout, &loop, &QEventLoop::quit);
|
|
||||||
to.start(10000);
|
|
||||||
loop.exec();
|
|
||||||
if (reply->error() != QNetworkReply::NoError) {
|
|
||||||
reply->deleteLater();
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
const auto data = reply->readAll();
|
|
||||||
reply->deleteLater();
|
|
||||||
const auto doc = QJsonDocument::fromJson(data);
|
|
||||||
if (!doc.isObject()) return QString();
|
|
||||||
return doc.object().value(QStringLiteral("response")).toString().trimmed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Orchestrator ----------
|
|
||||||
Orchestrator::Orchestrator(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
// Default provider (can be replaced in tests)
|
|
||||||
static OllamaModelProvider defaultProv; // lifetime: process
|
|
||||||
model_ = &defaultProv;
|
|
||||||
|
|
||||||
connect(&timer_, &QTimer::timeout, this, &Orchestrator::processPendingTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
Orchestrator::~Orchestrator() {
|
|
||||||
delete dal_;
|
|
||||||
}
|
|
||||||
|
|
||||||
ki::PgDal& Orchestrator::dal() {
|
|
||||||
if (!dal_) {
|
|
||||||
dal_ = new ki::PgDal();
|
|
||||||
const QByteArray dsn = qgetenv("PG_DSN");
|
|
||||||
if (!dsn.isEmpty()) dal_->connect(dsn.toStdString()); else dal_->connect("stub://memory");
|
|
||||||
}
|
|
||||||
return *dal_;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Orchestrator::start(int intervalMs) {
|
|
||||||
ensureResolvedDirs();
|
|
||||||
continuityHandshakeOnce();
|
|
||||||
timer_.start(intervalMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Orchestrator::stop() { timer_.stop(); }
|
|
||||||
|
|
||||||
void Orchestrator::ensureResolvedDirs() {
|
|
||||||
if (!stateDir_.exists()) {
|
|
||||||
const auto env = QProcessEnvironment::systemEnvironment();
|
|
||||||
const auto xdgState = env.value(QStringLiteral("XDG_STATE_HOME"), QDir::home().filePath(".local/state"));
|
|
||||||
stateDir_.setPath(QDir(xdgState).filePath("kompanion"));
|
|
||||||
}
|
|
||||||
if (!configDir_.exists()) {
|
|
||||||
const auto env = QProcessEnvironment::systemEnvironment();
|
|
||||||
const auto xdgConf = env.value(QStringLiteral("XDG_CONFIG_HOME"), QDir::home().filePath(".config"));
|
|
||||||
configDir_.setPath(QDir(xdgConf).filePath("kompanion"));
|
|
||||||
}
|
|
||||||
QDir().mkpath(stateDir_.absolutePath());
|
|
||||||
QDir().mkpath(journalDirPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Orchestrator::nowUtc() const {
|
|
||||||
return QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs).replace(QLatin1Char('+'), QLatin1Char('Z'));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Orchestrator::ledgerAppend(const QJsonObject &evt) {
|
|
||||||
QFile f(ledgerPath());
|
|
||||||
if (f.open(QIODevice::ReadOnly)) {
|
|
||||||
// noop: we could hash prev line like the python version; keep minimal for now
|
|
||||||
f.close();
|
|
||||||
}
|
|
||||||
if (f.open(QIODevice::Append | QIODevice::Text)) {
|
|
||||||
QJsonObject copy = evt;
|
|
||||||
copy.insert(QStringLiteral("ts"), nowUtc());
|
|
||||||
const QByteArray line = QJsonDocument(copy).toJson(QJsonDocument::Compact) + '\n';
|
|
||||||
f.write(line);
|
|
||||||
f.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Orchestrator::journalAppend(const QString &text) {
|
|
||||||
// Ensure journal directory exists even when start() was not called (tests)
|
|
||||||
QDir().mkpath(journalDirPath());
|
|
||||||
const QString file = QDir(journalDirPath()).filePath(QDate::currentDate().toString(Qt::ISODate) + QStringLiteral(".md"));
|
|
||||||
QFile f(file);
|
|
||||||
if (f.open(QIODevice::Append | QIODevice::Text)) {
|
|
||||||
QTextStream out(&f);
|
|
||||||
out << "- " << nowUtc() << ' ' << text << '\n';
|
|
||||||
out.flush();
|
|
||||||
f.close();
|
|
||||||
}
|
|
||||||
QJsonObject evt{{QStringLiteral("actor"), QStringLiteral("Χγφτ")}, {QStringLiteral("action"), QStringLiteral("journal.append")}};
|
|
||||||
ledgerAppend(evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Orchestrator::continuityHandshakeOnce() {
|
|
||||||
if (continuityDone_) return;
|
|
||||||
continuityDone_ = true;
|
|
||||||
QJsonObject evt{{QStringLiteral("actor"), QStringLiteral("Χγφτ")}, {QStringLiteral("action"), QStringLiteral("CONTINUITY_ACCEPTED")}};
|
|
||||||
ledgerAppend(evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Orchestrator::handleJournalFromPrompt(const QJsonObject &obj) {
|
|
||||||
const QString aspect = obj.value(QStringLiteral("aspect")).toString(QStringLiteral("companion"));
|
|
||||||
const QString prompt = obj.value(QStringLiteral("prompt")).toString();
|
|
||||||
if (!model_) return;
|
|
||||||
const QString preface = QStringLiteral("Write a brief, warm reflection.\nPrompt:\n");
|
|
||||||
const QString out = model_->generate(preface + prompt, aspect);
|
|
||||||
if (!out.isEmpty()) {
|
|
||||||
journalAppend(out);
|
|
||||||
}
|
|
||||||
emit taskProcessed(QStringLiteral("journal.from_prompt"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Orchestrator::processPendingTasks() {
|
|
||||||
QFile f(tasksPath());
|
|
||||||
if (!f.exists()) return;
|
|
||||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
|
|
||||||
const QByteArray data = f.readAll();
|
|
||||||
f.close();
|
|
||||||
// Truncate after reading
|
|
||||||
if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) f.close();
|
|
||||||
|
|
||||||
const QList<QByteArray> lines = QByteArray(data).split('\n');
|
|
||||||
for (const QByteArray &raw : lines) {
|
|
||||||
const QByteArray trimmed = raw.trimmed();
|
|
||||||
if (trimmed.isEmpty()) continue;
|
|
||||||
const auto doc = QJsonDocument::fromJson(trimmed);
|
|
||||||
if (!doc.isObject()) continue;
|
|
||||||
const QJsonObject obj = doc.object();
|
|
||||||
const QString type = obj.value(QStringLiteral("type")).toString();
|
|
||||||
if (type == QStringLiteral("journal.from_prompt")) {
|
|
||||||
handleJournalFromPrompt(obj);
|
|
||||||
} else {
|
|
||||||
QJsonObject evt{{QStringLiteral("action"), QStringLiteral("unknown.task")}, {QStringLiteral("type"), type}};
|
|
||||||
ledgerAppend(evt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Orchestrator::saveSnapshot(const QString &nameSpace,
|
|
||||||
const QString &key,
|
|
||||||
const QJsonObject &content,
|
|
||||||
const QStringList &tags)
|
|
||||||
{
|
|
||||||
auto nsRow = dal().ensureNamespace(nameSpace.toStdString());
|
|
||||||
if (!nsRow) return false;
|
|
||||||
|
|
||||||
ki::ItemRow row;
|
|
||||||
row.namespace_id = nsRow->id;
|
|
||||||
row.key = key.toStdString();
|
|
||||||
row.tags.reserve(tags.size());
|
|
||||||
for (const auto &t : tags) row.tags.push_back(t.toStdString());
|
|
||||||
row.content_json = QString::fromUtf8(QJsonDocument(content).toJson(QJsonDocument::Compact)).toStdString();
|
|
||||||
row.metadata_json = "{}";
|
|
||||||
row.created_at = std::chrono::system_clock::now();
|
|
||||||
|
|
||||||
const std::string id = dal().upsertItem(row);
|
|
||||||
return !id.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<QJsonObject> Orchestrator::loadSnapshot(const QString &nameSpace,
|
|
||||||
const QString &key)
|
|
||||||
{
|
|
||||||
auto nsRow = dal().findNamespace(nameSpace.toStdString());
|
|
||||||
if (!nsRow) return std::nullopt;
|
|
||||||
|
|
||||||
std::vector<std::string> tags; tags.emplace_back("snapshot");
|
|
||||||
auto rows = dal().fetchContext(nsRow->id, std::optional<std::string>(key.toStdString()), tags, std::nullopt, 1);
|
|
||||||
if (rows.empty()) return std::nullopt;
|
|
||||||
const auto &row = rows.front();
|
|
||||||
if (row.content_json.empty()) return std::nullopt;
|
|
||||||
const auto doc = QJsonDocument::fromJson(QByteArray::fromStdString(row.content_json));
|
|
||||||
if (!doc.isObject()) return std::nullopt;
|
|
||||||
return doc.object();
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <QObject>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QNetworkAccessManager>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include "kompanion_mw_export.h"
|
|
||||||
|
|
||||||
namespace ki { class PgDal; }
|
|
||||||
|
|
||||||
// Minimal model provider interface so tests can stub generation.
|
|
||||||
class IModelProvider {
|
|
||||||
public:
|
|
||||||
virtual ~IModelProvider() = default;
|
|
||||||
virtual QString generate(const QString &prompt, const QString &aspect) = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Default Ollama-backed provider. Uses OLLAMA_BASE and simple /api/generate call.
|
|
||||||
class OllamaModelProvider : public QObject, public IModelProvider {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit OllamaModelProvider(QObject *parent=nullptr);
|
|
||||||
QString generate(const QString &prompt, const QString &aspect) override;
|
|
||||||
void setBaseUrl(const QString &base) { baseUrl_ = base; }
|
|
||||||
void setDefaultModel(const QString &m) { defaultModel_ = m; }
|
|
||||||
private:
|
|
||||||
QString chooseModelForAspect(const QString &aspect) const; // simple heuristic
|
|
||||||
QString baseUrl_;
|
|
||||||
QString defaultModel_ = QStringLiteral("qwen2.5:7b");
|
|
||||||
QNetworkAccessManager nam_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Simple stub provider used by tests; returns deterministic text.
|
|
||||||
class StubModelProvider : public IModelProvider {
|
|
||||||
public:
|
|
||||||
explicit StubModelProvider(QString canned) : canned_(std::move(canned)) {}
|
|
||||||
QString generate(const QString &prompt, const QString &aspect) override {
|
|
||||||
Q_UNUSED(prompt); Q_UNUSED(aspect); return canned_;
|
|
||||||
}
|
|
||||||
private:
|
|
||||||
QString canned_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Orchestrator: replicates runtime/kom_runner.py behaviors in C++.
|
|
||||||
// - Watches a JSONL tasks file under XDG_STATE_HOME/kompanion
|
|
||||||
// - Processes tasks like {"type":"journal.from_prompt", "prompt":"...", "aspect":"companion"}
|
|
||||||
// - Appends to journal (<state>/journal/YYYY-MM-DD.md) and to a simple ledger JSONL
|
|
||||||
class KOMPANION_MW_EXPORT Orchestrator : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit Orchestrator(QObject *parent=nullptr);
|
|
||||||
~Orchestrator();
|
|
||||||
|
|
||||||
// Injectable model provider (Ollama by default). Ownership left to caller.
|
|
||||||
void setModelProvider(IModelProvider *prov) { model_ = prov; }
|
|
||||||
|
|
||||||
// Directories resolved from XDG_* on start(); overridable for tests.
|
|
||||||
void setStateDir(const QDir &dir) { stateDir_ = dir; }
|
|
||||||
void setConfigDir(const QDir &dir) { configDir_ = dir; }
|
|
||||||
|
|
||||||
// Poll loop control.
|
|
||||||
void start(int intervalMs = 3000);
|
|
||||||
void stop();
|
|
||||||
|
|
||||||
// One-shot tick (public for tests).
|
|
||||||
void processPendingTasks();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* saveSnapshot: persist a JSON snapshot under (namespace,key) as a context item.
|
|
||||||
* - Tags default to {"snapshot"}.
|
|
||||||
* - If PG_DSN is unset, uses an in-memory stub (won't persist across restarts).
|
|
||||||
*/
|
|
||||||
bool saveSnapshot(const QString &nameSpace,
|
|
||||||
const QString &key,
|
|
||||||
const QJsonObject &content,
|
|
||||||
const QStringList &tags = {QStringLiteral("snapshot")});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* loadSnapshot: fetch the latest item for (namespace,key) tagged as snapshot.
|
|
||||||
* Returns empty optional if not found or on failure.
|
|
||||||
*/
|
|
||||||
std::optional<QJsonObject> loadSnapshot(const QString &nameSpace,
|
|
||||||
const QString &key);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void taskProcessed(const QString &kind);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void ensureResolvedDirs();
|
|
||||||
void continuityHandshakeOnce();
|
|
||||||
void ledgerAppend(const QJsonObject &evt);
|
|
||||||
void journalAppend(const QString &line); // Also emits ledger entry
|
|
||||||
QString nowUtc() const;
|
|
||||||
|
|
||||||
// Task handlers
|
|
||||||
void handleJournalFromPrompt(const QJsonObject &obj);
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
QString tasksPath() const { return stateDir_.filePath("tasks.jsonl"); }
|
|
||||||
QString journalDirPath() const { return stateDir_.filePath("journal"); }
|
|
||||||
QString ledgerPath() const { return stateDir_.filePath("trust_ledger.jsonl"); }
|
|
||||||
|
|
||||||
QDir stateDir_;
|
|
||||||
QDir configDir_;
|
|
||||||
QTimer timer_;
|
|
||||||
bool continuityDone_ = false;
|
|
||||||
IModelProvider *model_ = nullptr; // not owned
|
|
||||||
// Reused DB handle so in-memory stub persists across calls in tests.
|
|
||||||
ki::PgDal* dal_ = nullptr;
|
|
||||||
ki::PgDal& dal();
|
|
||||||
};
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <QObject>
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
/** Simple durable journal for in-flight tool calls.
|
|
||||||
* Stores JSONL entries at runtime/pending.jsonl so crashes/UI reloads can resume.
|
|
||||||
*/
|
|
||||||
class RecoveryJournal : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit RecoveryJournal(const QString &path, QObject *parent=nullptr);
|
|
||||||
|
|
||||||
// record an in-flight tool call
|
|
||||||
void logInFlight(const QString &requestId, const QString &toolName, const QVariantMap &args);
|
|
||||||
// mark completion
|
|
||||||
void complete(const QString &requestId, bool ok);
|
|
||||||
// iterate unfinished entries and invoke callback(requestId, tool, args)
|
|
||||||
void recoverPending(const std::function<void(const QString&, const QString&, const QVariantMap&, const QString&)> &cb);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString path_;
|
|
||||||
};
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
#include "regexregistry.h"
|
|
||||||
#include <QFile>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
RegexRegistry::RegexRegistry(QObject *parent) : QObject(parent) {}
|
|
||||||
|
|
||||||
bool RegexRegistry::loadFromFile(const QString &path) {
|
|
||||||
QFile f(path); if (!f.open(QIODevice::ReadOnly)) return false;
|
|
||||||
sourcePath_ = path; rules_.clear();
|
|
||||||
const auto doc = QJsonDocument::fromJson(f.readAll());
|
|
||||||
if (!doc.isArray()) return false;
|
|
||||||
for (const auto &it : doc.array()) {
|
|
||||||
if (!it.isObject()) continue;
|
|
||||||
const auto o = it.toObject();
|
|
||||||
const auto rx = o.value("regex").toString();
|
|
||||||
const auto tool = o.value("tool").toString();
|
|
||||||
const auto keys = o.value("keys").toArray();
|
|
||||||
if (rx.isEmpty() || tool.isEmpty()) continue;
|
|
||||||
Rule r{ QRegularExpression(rx, QRegularExpression::CaseInsensitiveOption), tool, {} };
|
|
||||||
for (const auto &k : keys) r.argKeys << k.toString();
|
|
||||||
rules_.push_back(std::move(r));
|
|
||||||
}
|
|
||||||
emit reloaded();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RegexRegistry::match(const QString &prompt, QString &tool, QVariantMap &args) const {
|
|
||||||
for (const auto &r : rules_) {
|
|
||||||
const auto m = r.re.match(prompt.trimmed());
|
|
||||||
if (m.hasMatch()) {
|
|
||||||
tool = r.tool; args.clear();
|
|
||||||
for (int i=0; i<r.argKeys.size(); ++i) {
|
|
||||||
const auto key = r.argKeys.at(i);
|
|
||||||
const auto val = m.captured(i+1);
|
|
||||||
if (!key.isEmpty() && !val.isEmpty()) args.insert(key, val);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <QObject>
|
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include <QVector>
|
|
||||||
#include "kompanion_mw_export.h"
|
|
||||||
|
|
||||||
/** RegexRegistry: hot-reloadable mapping from NL prompts to tool+args */
|
|
||||||
class KOMPANION_MW_EXPORT RegexRegistry : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
struct Rule { QRegularExpression re; QString tool; QStringList argKeys; };
|
|
||||||
explicit RegexRegistry(QObject *parent=nullptr);
|
|
||||||
bool loadFromFile(const QString &path);
|
|
||||||
bool match(const QString &prompt, QString &tool, QVariantMap &args) const;
|
|
||||||
signals:
|
|
||||||
void reloaded();
|
|
||||||
private:
|
|
||||||
QVector<Rule> rules_;
|
|
||||||
QString sourcePath_;
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
# metal-kompanion-mcp
|
||||||
|
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.
|
||||||
|
|
||||||
|
> ## 📈 Project Summary
|
||||||
|
>
|
||||||
|
> **✅ Done**: 0 | **🔄 In Progress**: 0 | **⬜ Todo**: 29 | **❌ Blocked**: 0
|
||||||
|
>
|
||||||
|
> **Progress**: 0% `░░░░░░░░░░░░░░░░░░░░` 0/29 tasks
|
||||||
|
>
|
||||||
|
> **Priorities**: 🚨 **Critical**: 0 | 🔴 **High**: 1 | 🟡 **Medium**: 30 | 🟢 **Low**: 0
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
| ID | Status | Priority | Title | Description |
|
||||||
|
|:--:|:------:|:--------:|:------|:------------|
|
||||||
|
| #1 | ⬜ todo | 700 | **Project Setup: metal-kompanion-mcp** | MCP backend for Kompanion: me... |
|
||||||
|
| #2 | ⬜ in_progress | 500 | **Design MCP memory/context API** | Specify MCP tools for: save_c... |
|
||||||
|
| #3 | ⬜ todo | 501 | **Select embedding backend & storage** | Choose between local (Ollama/... |
|
||||||
|
| #4 | ⬜ in_progress | 499 | **Scaffold qtmcp-based server** | Set up C++/Qt MCP server skel... |
|
||||||
|
| #5 | ⬜ todo | 502 | **Implement memory adapters** | Adapters: (1) SQLite+FAISS/pg... |
|
||||||
|
| #6 | ⬜ todo | 498 | **Deep research: memory DB architecture & schema** | Survey best practices for con... |
|
||||||
|
| #7 | ⬜ todo | 503 | **Decide primary DB: Postgres+pgvector vs SQLite+FAISS** | Evaluate tradeoffs (multi-use... |
|
||||||
|
| #8 | ⬜ todo | 497 | **Implement DAL + migrations (pgvector)** | Create C++ DAL layer for name... |
|
||||||
|
| #9 | ⬜ todo | 504 | **Add cloud DB hardening (RLS, FTS/trgm, ANN indexes)** | Implement RLS policies; add F... |
|
||||||
|
| #10 | ⬜ todo | 496 | **Server enforcement: scope injection + rate limits** | Inject namespace/user via ses... |
|
||||||
|
| #11 | ⬜ todo | 505 | **Redaction & sensitivity pipeline** | Implement preprocessing to de... |
|
||||||
|
| #12 | ⬜ todo | 495 | **Private vault mode (key-only retrieval)** | Implement vault path for secr... |
|
||||||
|
| #13 | ⬜ todo | 506 | **Local backup tools: export/import (E2EE)** | Add kom.local.v1.backup.expor... |
|
||||||
|
| #14 | ⬜ todo | 494 | **Cloud adapters: backup/sync & payments stubs** | Expose kom.cloud.v1.backup.up... |
|
||||||
|
| #15 | ⬜ todo | 507 | **Purge job & admin delete paths** | Implement scheduled hard-dele... |
|
||||||
|
| #16 | ⬜ todo | 493 | **Test suite: privacy & hybrid search** | Cross-tenant leakage, redacti... |
|
||||||
|
| #17 | ⬜ todo | 508 | **Enable Qwen-2.5-Coder with tool support (Happy-Code profile)** | Prepare system prompt + regis... |
|
||||||
|
| #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: Qt6/QSql wiring + SQL calls** | Use QPSQL via Qt6::Sql, 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
|
||||||
|
|
||||||
|
| ID | Status | Title |
|
||||||
|
|:--:|:------:|:------|
|
||||||
|
| #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: require Qt6::Sql (QPSQL); CI env var DSN |
|
||||||
|
| #21.2 | ⬜ todo | PgDal: implement QSql 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) |
|
||||||
|
|
@ -1,23 +1,26 @@
|
||||||
enable_testing()
|
add_executable(test_mcp_tools
|
||||||
qt_add_executable(test_mw
|
contract/test_mcp_tools.cpp
|
||||||
test_middleware.cpp
|
|
||||||
)
|
)
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core Test)
|
target_include_directories(test_mcp_tools PRIVATE ${PROJECT_SOURCE_DIR}/src)
|
||||||
target_link_libraries(test_mw PRIVATE Qt6::Core Qt6::Test kompanion_mw)
|
target_link_libraries(test_mcp_tools PRIVATE kom_dal)
|
||||||
add_test(NAME test_mw COMMAND test_mw)
|
target_compile_options(test_mcp_tools PRIVATE -fexceptions)
|
||||||
|
|
||||||
qt_add_executable(test_orchestrator
|
add_test(NAME contract_mcp_tools COMMAND test_mcp_tools)
|
||||||
test_orchestrator.cpp
|
|
||||||
|
add_executable(contract_memory
|
||||||
|
contract_memory.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(test_orchestrator PRIVATE Qt6::Core Qt6::Network Qt6::Test kompanion_mw)
|
target_include_directories(contract_memory PRIVATE ${PROJECT_SOURCE_DIR}/src)
|
||||||
add_test(NAME test_orchestrator COMMAND test_orchestrator)
|
target_link_libraries(contract_memory PRIVATE kom_dal)
|
||||||
|
target_compile_options(contract_memory PRIVATE -fexceptions)
|
||||||
|
|
||||||
qt_add_executable(test_snapshot
|
add_test(NAME contract_memory COMMAND contract_memory)
|
||||||
test_snapshot.cpp
|
|
||||||
|
add_executable(test_memory_exchange
|
||||||
|
mcp/test_memory_exchange.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(test_snapshot PRIVATE Qt6::Core Qt6::Network Qt6::Test kompanion_mw)
|
target_include_directories(test_memory_exchange PRIVATE ${PROJECT_SOURCE_DIR}/src)
|
||||||
add_test(NAME test_snapshot COMMAND test_snapshot)
|
target_link_libraries(test_memory_exchange PRIVATE kom_dal)
|
||||||
|
target_compile_options(test_memory_exchange PRIVATE -fexceptions)
|
||||||
|
|
||||||
add_test(NAME cli_smoke
|
add_test(NAME mcp_memory_exchange COMMAND test_memory_exchange)
|
||||||
COMMAND sh ${CMAKE_SOURCE_DIR}/tests/cli_smoke.sh $<TARGET_FILE:kompanion>)
|
|
||||||
set_tests_properties(cli_smoke PROPERTIES ENVIRONMENT "KOMPANION_SKIP_CLI_SMOKE=1")
|
|
||||||
|
|
|
||||||
|
|
@ -30,28 +30,13 @@ std::string saveResp = server.dispatch("kom.memory.v1.save_context", saveReq);
|
||||||
if (!expect_contains(saveResp, "\"id\"", "save_context id")) return 1;
|
if (!expect_contains(saveResp, "\"id\"", "save_context id")) return 1;
|
||||||
if (!expect_contains(saveResp, "\"created_at\"", "save_context timestamp")) return 1;
|
if (!expect_contains(saveResp, "\"created_at\"", "save_context timestamp")) return 1;
|
||||||
|
|
||||||
const std::string saveReqNoNamespace = R"({"key":"nogreeting","content":{"text":"remember this without namespace"},"tags":["context","demo"]})";
|
const std::string recallReq = R"({"namespace":"tests","key":"greeting","limit":2})";
|
||||||
std::string saveRespNoNamespace = server.dispatch("kom.memory.v1.save_context", saveReqNoNamespace);
|
std::string recallResp = server.dispatch("kom.memory.v1.recall_context", recallReq);
|
||||||
if (!expect_contains(saveRespNoNamespace, "\"error\":{\"code\":\"bad_request\",\"message\":\"namespace is required\"}", "save_context without namespace should fail")) return 1;
|
if (!expect_contains(recallResp, "\"items\"", "recall_context items")) return 1;
|
||||||
|
if (!expect_contains(recallResp, "\"confidence\":0.98", "recall_context returns payload")) return 1;
|
||||||
|
|
||||||
const std::string recallReq = R"({"namespace":"tests","key":"greeting","limit":2})";
|
std::string firstId;
|
||||||
std::string recallResp = server.dispatch("kom.memory.v1.recall_context", recallReq);
|
const std::string idsAnchor = "\"ids\":[\"";
|
||||||
if (!expect_contains(recallResp, "\"items\"", "recall_context items")) return 1;
|
|
||||||
if (!expect_contains(recallResp, "\"confidence\":0.98", "recall_context returns payload")) return 1;
|
|
||||||
|
|
||||||
const std::string recallMinReq = R"({"namespace":"tests"})";
|
|
||||||
std::string recallMinResp = server.dispatch("kom.memory.v1.recall_context", recallMinReq);
|
|
||||||
if (!expect_contains(recallMinResp, "\"items\"", "recall_context minimum parameters items")) return 1;
|
|
||||||
if (!expect_contains(recallMinResp, "\"id\"", "recall_context minimum parameters id")) return 1;
|
|
||||||
|
|
||||||
const std::string embedReq = R"({"namespace":"tests","texts":["hello world", "goodbye world"]})";
|
|
||||||
std::string embedResp = server.dispatch("kom.memory.v1.embed_text", embedReq);
|
|
||||||
if (!expect_contains(embedResp, "\"model\"", "embed_text model key")) return 1;
|
|
||||||
if (!expect_contains(embedResp, "\"vectors\"", "embed_text vectors key")) return 1;
|
|
||||||
if (!expect_contains(embedResp, "[[", "embed_text vectors content")) return 1;
|
|
||||||
|
|
||||||
std::string firstId;
|
|
||||||
const std::string idsAnchor = "\"ids\":[\"";
|
|
||||||
auto idsPos = upsertResp.find(idsAnchor);
|
auto idsPos = upsertResp.find(idsAnchor);
|
||||||
if (idsPos != std::string::npos) {
|
if (idsPos != std::string::npos) {
|
||||||
idsPos += idsAnchor.size();
|
idsPos += idsAnchor.size();
|
||||||
|
|
@ -74,10 +59,6 @@ if (!expect_contains(saveRespNoNamespace, "\"error\":{\"code\":\"bad_request\",\
|
||||||
std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq);
|
std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq);
|
||||||
if (!expect_contains(vectorResp, "\"id\":\""+firstId+"\"", "vector search returns stored id")) return 1;
|
if (!expect_contains(vectorResp, "\"id\":\""+firstId+"\"", "vector search returns stored id")) return 1;
|
||||||
|
|
||||||
const std::string warmCacheReq = R"({"namespace":"tests"})";
|
|
||||||
std::string warmCacheResp = server.dispatch("kom.memory.v1.warm_cache", warmCacheReq);
|
|
||||||
if (!expect_contains(warmCacheResp, "\"queued\"", "warm_cache queued key")) return 1;
|
|
||||||
|
|
||||||
const std::string snapshotResp = server.dispatch("kom.meta.v1.project_snapshot", "{}");
|
const std::string snapshotResp = server.dispatch("kom.meta.v1.project_snapshot", "{}");
|
||||||
if (!expect_contains(snapshotResp, "\"sections\"", "snapshot sections key")) return 1;
|
if (!expect_contains(snapshotResp, "\"sections\"", "snapshot sections key")) return 1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,31 +5,31 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
static void contract_pgdal_basic() {
|
static void contract_pgdal_basic() {
|
||||||
ki::PgDal dal;
|
kom::PgDal dal;
|
||||||
dal.connect("stub://memory");
|
dal.connect("stub://memory");
|
||||||
auto ns = dal.ensureNamespace("tests");
|
auto ns = dal.ensureNamespace("tests");
|
||||||
static_cast<void>(ns);
|
static_cast<void>(ns);
|
||||||
|
|
||||||
ki::ItemRow item;
|
kom::ItemRow item;
|
||||||
item.namespace_id = "tests";
|
item.namespace_id = "tests";
|
||||||
item.text = std::string("example");
|
item.text = std::string("example");
|
||||||
item.tags = {"alpha", "beta"};
|
item.tags = {"alpha", "beta"};
|
||||||
item.id = dal.upsertItem(item);
|
item.id = dal.upsertItem(item);
|
||||||
|
|
||||||
ki::ChunkRow chunk;
|
kom::ChunkRow chunk;
|
||||||
chunk.item_id = item.id;
|
chunk.item_id = item.id;
|
||||||
chunk.text = "chunk-text";
|
chunk.text = "chunk-text";
|
||||||
auto chunkIds = dal.upsertChunks(std::vector<ki::ChunkRow>{chunk});
|
auto chunkIds = dal.upsertChunks(std::vector<kom::ChunkRow>{chunk});
|
||||||
if (!chunkIds.empty()) {
|
if (!chunkIds.empty()) {
|
||||||
chunk.id = chunkIds.front();
|
chunk.id = chunkIds.front();
|
||||||
}
|
}
|
||||||
|
|
||||||
ki::EmbeddingRow embedding;
|
kom::EmbeddingRow embedding;
|
||||||
embedding.chunk_id = chunk.id;
|
embedding.chunk_id = chunk.id;
|
||||||
embedding.model = "stub-model";
|
embedding.model = "stub-model";
|
||||||
embedding.dim = 3;
|
embedding.dim = 3;
|
||||||
embedding.vector = {0.1f, 0.2f, 0.3f};
|
embedding.vector = {0.1f, 0.2f, 0.3f};
|
||||||
dal.upsertEmbeddings(std::vector<ki::EmbeddingRow>{embedding});
|
dal.upsertEmbeddings(std::vector<kom::EmbeddingRow>{embedding});
|
||||||
|
|
||||||
static_cast<void>(dal.searchText("tests", "chunk", 5));
|
static_cast<void>(dal.searchText("tests", "chunk", 5));
|
||||||
static_cast<void>(dal.searchVector("tests", embedding.vector, 5));
|
static_cast<void>(dal.searchVector("tests", embedding.vector, 5));
|
||||||
|
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
set -x
|
|
||||||
|
|
||||||
# --- Test Configuration ---
|
|
||||||
TEST_DB_NAME="kompanion_autotest"
|
|
||||||
MCP_SERVER_EXECUTABLE="./bin/kom_mcp"
|
|
||||||
PROJECT_ROOT_DIR=$(git rev-parse --show-toplevel)
|
|
||||||
MCP_SERVER_HOST="127.0.0.1"
|
|
||||||
MCP_SERVER_PORT="8081"
|
|
||||||
MCP_SERVER_URL="http://${MCP_SERVER_HOST}:${MCP_SERVER_PORT}"
|
|
||||||
|
|
||||||
# --- Cleanup Function ---
|
|
||||||
cleanup() {
|
|
||||||
echo "--- Cleaning up ---"
|
|
||||||
[ -n "${mcp_proxy_pid:-}" ] && kill "$mcp_proxy_pid" || true
|
|
||||||
[ -n "${mcp_server_pid:-}" ] && kill "$mcp_server_pid" || true
|
|
||||||
pkill -f kom_mcp || true
|
|
||||||
sleep 1 # Give the OS time to release the port
|
|
||||||
netstat -tuln | grep ":${MCP_SERVER_PORT}" || true # Check if port is still in use
|
|
||||||
psql -v ON_ERROR_STOP=1 -c "DROP DATABASE IF EXISTS \"$TEST_DB_NAME\";" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
echo "--- Setting up test environment ---"
|
|
||||||
|
|
||||||
echo ">> Initializing test database..."
|
|
||||||
"${PROJECT_ROOT_DIR}/db/scripts/create-test-db.sh" "$TEST_DB_NAME"
|
|
||||||
|
|
||||||
# Optional environment bootstrap (developer machine)
|
|
||||||
if [ -f "$HOME/dev/main/src/env.sh" ]; then
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
. "$HOME/dev/main/src/env.sh"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ">> Harvesting embeddings..."
|
|
||||||
export DB_URL="dbname=${TEST_DB_NAME} user=kompanion host=/var/run/postgresql" EMBED_NAMESPACE="dev_knowledge"
|
|
||||||
python3 "${PROJECT_ROOT_DIR}/tools/ingest_dir.py" "${PROJECT_ROOT_DIR}/tests/test_data" "dev_knowledge"
|
|
||||||
|
|
||||||
echo ">> Starting MCP server (preferring stdio + mcp-proxy if available)..."
|
|
||||||
sleep 2
|
|
||||||
if command -v mcp-proxy >/dev/null 2>&1; then
|
|
||||||
setsid $MCP_SERVER_EXECUTABLE --backend stdio < /dev/null > /dev/null 2>&1 &
|
|
||||||
mcp_server_pid=$!
|
|
||||||
sleep 1
|
|
||||||
setsid mcp-proxy --target stdio://127.0.0.1 --listen "${MCP_SERVER_HOST}:${MCP_SERVER_PORT}" < /dev/null > /dev/null 2>&1 &
|
|
||||||
mcp_proxy_pid=$!
|
|
||||||
else
|
|
||||||
timeout 10 $MCP_SERVER_EXECUTABLE --backend sse --address "${MCP_SERVER_HOST}:${MCP_SERVER_PORT}" < /dev/null > /dev/null 2>&1 &
|
|
||||||
mcp_server_pid=$!
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 5
|
|
||||||
ps -ef | grep kom_mcp
|
|
||||||
|
|
||||||
# --- API Test Functions ---
|
|
||||||
|
|
||||||
send_request() {
|
|
||||||
local session_id=$1
|
|
||||||
local payload=$2
|
|
||||||
curl -s -X POST -H "Content-Type: application/json" -d "$payload" "${MCP_SERVER_URL}/messages?session_id=${session_id}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Running API tests ---
|
|
||||||
|
|
||||||
echo "--> Establishing SSE connection..."
|
|
||||||
SSE_RESPONSE=$(curl -s -N -H "Accept:text/event-stream" "${MCP_SERVER_URL}/sse")
|
|
||||||
sleep 1 # Give the server time to send the response
|
|
||||||
SESSION_ID=$(echo "$SSE_RESPONSE" | grep -m 1 -oE 'data: /messages/\?session_id=([a-f0-9-]+)' | cut -d '=' -f 2)
|
|
||||||
|
|
||||||
if [ -z "$SESSION_ID" ]; then
|
|
||||||
echo "Failed to get session ID"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Session ID: $SESSION_ID"
|
|
||||||
|
|
||||||
# Test upsert_memory
|
|
||||||
echo "--> Testing upsert_memory..."
|
|
||||||
UPSERT_PAYLOAD='{
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "tool",
|
|
||||||
"params": {
|
|
||||||
"name": "kom.memory.v1.upsert_memory",
|
|
||||||
"arguments": {
|
|
||||||
"auth_token": "dev_knowledge:test-secret",
|
|
||||||
"namespace": "dev_knowledge",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "test-item-1",
|
|
||||||
"text": "This is a test item for upsert_memory.",
|
|
||||||
"tags": ["test", "upsert"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
|
|
||||||
response=$(send_request "$SESSION_ID" "$UPSERT_PAYLOAD")
|
|
||||||
echo "$response" | grep '"status":"ok"' > /dev/null || (echo "upsert_memory test failed" && exit 1)
|
|
||||||
echo "upsert_memory test passed."
|
|
||||||
|
|
||||||
# Test search_memory
|
|
||||||
echo "--> Testing search_memory..."
|
|
||||||
SEARCH_PAYLOAD='{
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "tool",
|
|
||||||
"params": {
|
|
||||||
"name": "kom.memory.v1.search_memory",
|
|
||||||
"arguments": {
|
|
||||||
"auth_token": "dev_knowledge:test-secret",
|
|
||||||
"namespace": "dev_knowledge",
|
|
||||||
"query": {
|
|
||||||
"text": "upsert"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
|
|
||||||
response=$(send_request "$SESSION_ID" "$SEARCH_PAYLOAD")
|
|
||||||
echo "$response" | grep '"id":"test-item-1"' > /dev/null || (echo "search_memory test failed" && exit 1)
|
|
||||||
echo "search_memory test passed."
|
|
||||||
|
|
||||||
# Test save_context
|
|
||||||
echo "--> Testing save_context..."
|
|
||||||
SAVE_CONTEXT_PAYLOAD='{
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "tool",
|
|
||||||
"params": {
|
|
||||||
"name": "kom.memory.v1.save_context",
|
|
||||||
"arguments": {
|
|
||||||
"auth_token": "dev_knowledge:test-secret",
|
|
||||||
"namespace": "dev_knowledge",
|
|
||||||
"key": "test-context-1",
|
|
||||||
"content": {
|
|
||||||
"message": "This is a test context."
|
|
||||||
},
|
|
||||||
"tags": ["test", "context"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
|
|
||||||
response=$(send_request "$SESSION_ID" "$SAVE_CONTEXT_PAYLOAD")
|
|
||||||
echo "$response" | grep '"id":' > /dev/null || (echo "save_context test failed" && exit 1)
|
|
||||||
echo "save_context test passed."
|
|
||||||
|
|
||||||
# Test recall_context
|
|
||||||
echo "--> Testing recall_context..."
|
|
||||||
RECALL_CONTEXT_PAYLOAD='{
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "tool",
|
|
||||||
"params": {
|
|
||||||
"name": "kom.memory.v1.recall_context",
|
|
||||||
"arguments": {
|
|
||||||
"auth_token": "dev_knowledge:test-secret",
|
|
||||||
"namespace": "dev_knowledge",
|
|
||||||
"key": "test-context-1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
|
|
||||||
response=$(send_request "$SESSION_ID" "$RECALL_CONTEXT_PAYLOAD")
|
|
||||||
echo "$response" | grep '"key":"test-context-1"' > /dev/null || (echo "recall_context test failed" && exit 1)
|
|
||||||
echo "recall_context test passed."
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
This is a test file.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
This is another test file.
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
#include <QtTest>
|
|
||||||
#include "../src/middleware/kompanioncontroller.h"
|
|
||||||
#include "../src/middleware/regexregistry.h"
|
|
||||||
|
|
||||||
class MiddlewareTest : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
private slots:
|
|
||||||
void prompt_to_tool_mapping() {
|
|
||||||
KompanionController ctl;
|
|
||||||
RegexRegistry reg;
|
|
||||||
reg.loadFromFile(QStringLiteral("../resources/mappings.json"));
|
|
||||||
// Connect signals (basic compile-time test)
|
|
||||||
QObject::connect(&ctl, &KompanionController::toolRequested, [](auto, auto, auto){ });
|
|
||||||
QObject::connect(&ctl, &KompanionController::textOutput, [](auto, auto){ });
|
|
||||||
// If the controller used the registry internally, we'd inject it; for now this test ensures build.
|
|
||||||
QVERIFY(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
QTEST_MAIN(MiddlewareTest)
|
|
||||||
#include "test_middleware.moc"
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
#include <QtTest>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QTextStream>
|
|
||||||
|
|
||||||
#include "../src/middleware/orchestrator.h"
|
|
||||||
|
|
||||||
class OrchestratorTest : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
private slots:
|
|
||||||
void journal_from_prompt_writes_outputs();
|
|
||||||
};
|
|
||||||
|
|
||||||
void OrchestratorTest::journal_from_prompt_writes_outputs() {
|
|
||||||
// Create a temp state dir
|
|
||||||
QDir tmp = QDir::temp();
|
|
||||||
const QString base = QStringLiteral("kompanion_test_%1").arg(QDateTime::currentMSecsSinceEpoch());
|
|
||||||
QVERIFY(tmp.mkpath(base));
|
|
||||||
QDir state(tmp.filePath(base));
|
|
||||||
|
|
||||||
// Prepare a task JSONL
|
|
||||||
QFile tasks(state.filePath("tasks.jsonl"));
|
|
||||||
QVERIFY(tasks.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text));
|
|
||||||
const QByteArray line = QByteArray("{\"type\":\"journal.from_prompt\",\"aspect\":\"companion\",\"prompt\":\"hello world\"}\n");
|
|
||||||
QVERIFY(tasks.write(line) == line.size());
|
|
||||||
tasks.close();
|
|
||||||
|
|
||||||
// Stub provider returns deterministic text
|
|
||||||
StubModelProvider stub(QStringLiteral("TEST_OUTPUT"));
|
|
||||||
|
|
||||||
Orchestrator orch;
|
|
||||||
orch.setStateDir(state);
|
|
||||||
orch.setModelProvider(&stub);
|
|
||||||
orch.processPendingTasks();
|
|
||||||
|
|
||||||
// Expect journal file for today exists and contains the output
|
|
||||||
const QString journalPath = state.filePath("journal/" + QDate::currentDate().toString(Qt::ISODate) + ".md");
|
|
||||||
QFile journal(journalPath);
|
|
||||||
QVERIFY(journal.exists());
|
|
||||||
QVERIFY(journal.open(QIODevice::ReadOnly | QIODevice::Text));
|
|
||||||
const QString content = QString::fromUtf8(journal.readAll());
|
|
||||||
QVERIFY2(content.contains("TEST_OUTPUT"), "Journal should contain model output");
|
|
||||||
|
|
||||||
// Expect ledger file exists
|
|
||||||
QFile ledger(state.filePath("trust_ledger.jsonl"));
|
|
||||||
QVERIFY(ledger.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
QTEST_MAIN(OrchestratorTest)
|
|
||||||
#include "test_orchestrator.moc"
|
|
||||||
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
#include <QtTest>
|
|
||||||
#include "../src/middleware/orchestrator.h"
|
|
||||||
|
|
||||||
class SnapshotTest : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
private slots:
|
|
||||||
void round_trip() {
|
|
||||||
// Ensure no PG_DSN is required; Orchestrator maintains a shared in-memory DAL per instance
|
|
||||||
Orchestrator orch;
|
|
||||||
const QString ns = QStringLiteral("tests");
|
|
||||||
const QString key = QStringLiteral("session:last");
|
|
||||||
|
|
||||||
QJsonObject payload{{"a", 1}, {"b", QStringLiteral("x")}};
|
|
||||||
QVERIFY2(orch.saveSnapshot(ns, key, payload), "saveSnapshot should succeed");
|
|
||||||
|
|
||||||
auto loaded = orch.loadSnapshot(ns, key);
|
|
||||||
QVERIFY2(loaded.has_value(), "loadSnapshot should return value");
|
|
||||||
QCOMPARE(loaded->value("a").toInt(), 1);
|
|
||||||
QCOMPARE(loaded->value("b").toString(), QStringLiteral("x"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
QTEST_MAIN(SnapshotTest)
|
|
||||||
#include "test_snapshot.moc"
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue