diff --git a/resources/mappings.json b/resources/mappings.json new file mode 100644 index 0000000..df6b4a7 --- /dev/null +++ b/resources/mappings.json @@ -0,0 +1,8 @@ +[ + { "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"] } +] diff --git a/src/cli/KompanionApp.cpp b/src/cli/KompanionApp.cpp index 49f6dcd..858642c 100644 --- a/src/cli/KompanionApp.cpp +++ b/src/cli/KompanionApp.cpp @@ -879,6 +879,30 @@ int main(int argc, char** argv) { "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); + QCommandLineOption rehydrateOption(QStringList() << "--rehydrate", + "Compose a rehydration frame: snapshot + top-K search for --text."); + parser.addOption(rehydrateOption); + QCommandLineOption textQOption(QStringList() << "--text", + "Text query for rehydrate/search.", + "q"); + parser.addOption(textQOption); + QCommandLineOption kOption(QStringList() << "-k", + "Top-K for rehydrate/search.", + "k", "8"); + parser.addOption(kOption); + parser.addPositionalArgument("tool", "Tool name to invoke."); parser.addPositionalArgument("payload", "Optional JSON payload or file path (use '-' for stdin).", "[payload]"); @@ -894,6 +918,8 @@ int main(int argc, char** argv) { 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 configDsn = readDsnFromConfig(); const char* envDsn = std::getenv("PG_DSN"); @@ -974,6 +1000,89 @@ int main(int argc, char** argv) { } } + // 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(textQOption); + 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\":\"" << detail::json_escape(text.toStdString()) << "\",\"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; diff --git a/src/mcp/HandlersMemory.hpp b/src/mcp/HandlersMemory.hpp index 6a01453..8ff7bea 100644 --- a/src/mcp/HandlersMemory.hpp +++ b/src/mcp/HandlersMemory.hpp @@ -713,8 +713,8 @@ inline std::string embed_text(const std::string& reqJson) { } os << ']'; } - os << "]}"; - return os.str(); + os << "]}"; + return os.str(); } /** @@ -733,15 +733,19 @@ inline std::string warm_cache(const std::string& reqJson) { 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}"); + return std::string("{\\\"queued\\\":0}"); } - // Fetch recent items - std::vector tags; // empty - auto rows = detail::database().fetchContext(nsRow->id, std::nullopt, tags, std::nullopt, limit); + // Fetch recent items with optional filters + std::optional keyOpt; if (!key.empty()) keyOpt = key; + std::optional 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}"); } @@ -779,7 +783,18 @@ inline std::string warm_cache(const std::string& reqJson) { detail::database().upsertEmbeddings(std::vector{emb}); persisted++; } - std::ostringstream os; os << "{\"queued\":" << persisted << "}"; return os.str(); + 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 diff --git a/src/mcp/RegisterTools.hpp b/src/mcp/RegisterTools.hpp index 820b768..404388d 100644 --- a/src/mcp/RegisterTools.hpp +++ b/src/mcp/RegisterTools.hpp @@ -8,6 +8,7 @@ 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.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.search_memory", Handlers::search_memory); diff --git a/src/mcp/ToolSchemas.json b/src/mcp/ToolSchemas.json index 4010b3b..660b955 100644 --- a/src/mcp/ToolSchemas.json +++ b/src/mcp/ToolSchemas.json @@ -162,6 +162,25 @@ "additionalProperties": false } }, + "delete_context": { + "description": "Delete stored context entries filtered by key and/or tags.", + "input": { + "type": "object", + "properties": { + "namespace": { "type": "string" }, + "key": { "type": "string" }, + "tags": { "$ref": "#/$defs/stringList" } + }, + "required": ["namespace"], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { "deleted": { "type": "integer" } }, + "required": ["deleted"], + "additionalProperties": false + } + }, "embed_text": { "description": "Return embedding vectors for provided text inputs.", "input": { @@ -400,6 +419,41 @@ "additionalProperties": false } }, + "upsert_and_embed": { + "description": "Upsert items and compute embeddings for each item (ord=0 chunk).", + "input": { + "type": "object", + "properties": { + "namespace": { "type": "string" }, + "model": { "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 + } + }, "kom.local.v1.backup.export_encrypted": { "description": "Queue an encrypted backup export for the requested namespaces.", "input": {