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:
parent
6fd938d4f3
commit
720d445f1f
|
|
@ -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"] }
|
||||||
|
]
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue