cli: add --warm-cache (policy/ad-hoc) and --rehydrate; mcp: warm_cache accepts since/tags/key; add delete_context (stub) and upsert_and_embed wiring; resources: basic NL regex mappings

This commit is contained in:
Χγφτ Kompanion 2025-10-19 11:56:14 +02:00
parent 6fd938d4f3
commit 720d445f1f
5 changed files with 194 additions and 7 deletions

8
resources/mappings.json Normal file
View File

@ -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"] }
]

View File

@ -879,6 +879,30 @@ int main(int argc, char** argv) {
"key", "session:last"); "key", "session:last");
parser.addOption(keyOption); 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("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]");
@ -894,6 +918,8 @@ int main(int argc, char** argv) {
const bool runMcp = parser.isSet(mcpServeOption); const bool runMcp = parser.isSet(mcpServeOption);
const bool snapSave = parser.isSet(snapshotSaveOption); const bool snapSave = parser.isSet(snapshotSaveOption);
const bool snapLoad = parser.isSet(snapshotLoadOption); 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");
@ -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) // DB inspection helpers (exclusive)
if (parser.isSet(dbNsOption)) { if (parser.isSet(dbNsOption)) {
return dbListNamespaces(qout) ? 0 : 1; return dbListNamespaces(qout) ? 0 : 1;

View File

@ -713,8 +713,8 @@ inline std::string embed_text(const std::string& reqJson) {
} }
os << ']'; os << ']';
} }
os << "]}"; os << "]}";
return os.str(); 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"); std::string model = detail::extract_string_field(reqJson, "model");
int limit = 10; int limit = 10;
if (auto lim = detail::extract_int_field(reqJson, "limit")) { if (*lim > 0) limit = *lim; } 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); auto nsRow = detail::database().findNamespace(nsName);
if (!nsRow) { if (!nsRow) {
return std::string("{\"queued\":0}"); return std::string("{\\\"queued\\\":0}");
} }
// Fetch recent items // Fetch recent items with optional filters
std::vector<std::string> tags; // empty std::optional<std::string> keyOpt; if (!key.empty()) keyOpt = key;
auto rows = detail::database().fetchContext(nsRow->id, std::nullopt, tags, std::nullopt, limit); std::optional<std::string> sinceOpt; if (!since.empty()) sinceOpt = since;
auto rows = detail::database().fetchContext(nsRow->id, keyOpt, tags, sinceOpt, limit);
if (rows.empty()) { if (rows.empty()) {
return std::string("{\"queued\":0}"); return std::string("{\"queued\":0}");
} }
@ -779,7 +783,18 @@ inline std::string warm_cache(const std::string& reqJson) {
detail::database().upsertEmbeddings(std::vector<ki::EmbeddingRow>{emb}); detail::database().upsertEmbeddings(std::vector<ki::EmbeddingRow>{emb});
persisted++; 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 } // namespace Handlers

View File

@ -8,6 +8,7 @@ inline void register_default_tools(KomMcpServer& server) {
server.registerTool("echo", Handlers::echo); 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.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);

View File

@ -162,6 +162,25 @@
"additionalProperties": false "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": { "embed_text": {
"description": "Return embedding vectors for provided text inputs.", "description": "Return embedding vectors for provided text inputs.",
"input": { "input": {
@ -400,6 +419,41 @@
"additionalProperties": false "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": { "kom.local.v1.backup.export_encrypted": {
"description": "Queue an encrypted backup export for the requested namespaces.", "description": "Queue an encrypted backup export for the requested namespaces.",
"input": { "input": {