cozo-memory 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +356 -5
- package/dist/adaptive-retrieval.js +520 -0
- package/dist/db-inspect.js +25 -0
- package/dist/dynamic-fusion.js +602 -0
- package/dist/hybrid-search.js +4 -4
- package/dist/index.js +699 -23
- package/dist/inference-engine.js +104 -76
- package/dist/logical-edges-service.js +316 -0
- package/dist/multi-hop-vector-pivot.js +390 -0
- package/dist/temporal-embedding-service.js +313 -0
- package/dist/test-adaptive-integration.js +84 -0
- package/dist/test-adaptive-retrieval.js +135 -0
- package/dist/test-compaction.js +91 -0
- package/dist/test-dynamic-fusion.js +231 -0
- package/dist/test-fact-lifecycle.js +82 -0
- package/dist/test-logical-edges.js +282 -0
- package/dist/test-manual-compact.js +95 -0
- package/dist/test-multi-hop-vector-pivot-v2.js +239 -0
- package/dist/test-multi-hop-vector-pivot.js +240 -0
- package/dist/test-temporal-embeddings.js +123 -0
- package/dist/test-validity-retract.js +45 -0
- package/dist/test-validity-rm.js +49 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,6 +16,8 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
16
16
|
const pdf_mjs_1 = require("pdfjs-dist/legacy/build/pdf.mjs");
|
|
17
17
|
const hybrid_search_1 = require("./hybrid-search");
|
|
18
18
|
const inference_engine_1 = require("./inference-engine");
|
|
19
|
+
const dynamic_fusion_1 = require("./dynamic-fusion");
|
|
20
|
+
const adaptive_retrieval_1 = require("./adaptive-retrieval");
|
|
19
21
|
exports.DB_PATH = path_1.default.resolve(__dirname, "..", "memory_db.cozo");
|
|
20
22
|
const DB_ENGINE = process.env.DB_ENGINE || "sqlite"; // "sqlite" or "rocksdb"
|
|
21
23
|
exports.USER_ENTITY_ID = "global_user_profile";
|
|
@@ -26,8 +28,11 @@ class MemoryServer {
|
|
|
26
28
|
mcp;
|
|
27
29
|
embeddingService;
|
|
28
30
|
hybridSearch;
|
|
31
|
+
dynamicFusion;
|
|
32
|
+
adaptiveRetrieval;
|
|
29
33
|
inferenceEngine;
|
|
30
34
|
initPromise;
|
|
35
|
+
compactionLocks = new Set();
|
|
31
36
|
// Metrics tracking
|
|
32
37
|
metrics = {
|
|
33
38
|
operations: {
|
|
@@ -75,6 +80,8 @@ class MemoryServer {
|
|
|
75
80
|
console.error(`[DB] Using backend: ${DB_ENGINE}, path: ${fullDbPath}`);
|
|
76
81
|
this.embeddingService = new embedding_service_1.EmbeddingService();
|
|
77
82
|
this.hybridSearch = new hybrid_search_1.HybridSearch(this.db, this.embeddingService);
|
|
83
|
+
this.dynamicFusion = new dynamic_fusion_1.DynamicFusionSearch(this.db, this.embeddingService);
|
|
84
|
+
this.adaptiveRetrieval = new adaptive_retrieval_1.AdaptiveGraphRetrieval(this.db, this.embeddingService);
|
|
78
85
|
this.inferenceEngine = new inference_engine_1.InferenceEngine(this.db, this.embeddingService);
|
|
79
86
|
this.mcp = new fastmcp_1.FastMCP({
|
|
80
87
|
name: "cozo-memory-server",
|
|
@@ -1422,13 +1429,13 @@ class MemoryServer {
|
|
|
1422
1429
|
if (session_id) {
|
|
1423
1430
|
try {
|
|
1424
1431
|
await this.db.run(`
|
|
1425
|
-
?[
|
|
1426
|
-
*session_state{
|
|
1427
|
-
|
|
1432
|
+
?[session_id, last_active, status, metadata] :=
|
|
1433
|
+
*session_state{session_id, metadata},
|
|
1434
|
+
session_id = $session_id,
|
|
1428
1435
|
last_active = $now,
|
|
1429
1436
|
status = "active"
|
|
1430
|
-
:update session_state {
|
|
1431
|
-
`, {
|
|
1437
|
+
:update session_state {session_id => last_active, status, metadata}
|
|
1438
|
+
`, { session_id: session_id, now: Math.floor(now / 1000000) });
|
|
1432
1439
|
}
|
|
1433
1440
|
catch (e) {
|
|
1434
1441
|
console.warn(`[AddObservation] Session activity update failed for ${session_id}:`, e.message);
|
|
@@ -1437,6 +1444,12 @@ class MemoryServer {
|
|
|
1437
1444
|
// Optional: Automatic inference after new observation (in background)
|
|
1438
1445
|
const suggestionsRaw = await this.inferenceEngine.inferRelations(entityId);
|
|
1439
1446
|
const suggestions = await this.formatInferredRelationsForContext(suggestionsRaw);
|
|
1447
|
+
// Automatic Compaction (Threshold-based)
|
|
1448
|
+
// Skip for compaction-generated observations to prevent recursive loop
|
|
1449
|
+
const metadataKind = args.metadata?.kind;
|
|
1450
|
+
if (metadataKind !== "compaction" && metadataKind !== "session_summary") {
|
|
1451
|
+
this.compactEntity({ entity_id: entityId }).catch(e => console.error(`[AutoCompact] Error for ${entityId}:`, e));
|
|
1452
|
+
}
|
|
1440
1453
|
const created_at_iso = new Date(Math.floor(now / 1000)).toISOString();
|
|
1441
1454
|
return {
|
|
1442
1455
|
id,
|
|
@@ -1472,6 +1485,13 @@ class MemoryServer {
|
|
|
1472
1485
|
}
|
|
1473
1486
|
async stopSession(args) {
|
|
1474
1487
|
await this.initPromise;
|
|
1488
|
+
// Automatic Session Compaction
|
|
1489
|
+
try {
|
|
1490
|
+
await this.compactSession({ session_id: args.id });
|
|
1491
|
+
}
|
|
1492
|
+
catch (e) {
|
|
1493
|
+
console.warn(`[AutoCompact] Session compaction failed for ${args.id}:`, e.message);
|
|
1494
|
+
}
|
|
1475
1495
|
await this.db.run(`
|
|
1476
1496
|
?[session_id, last_active, status, metadata] :=
|
|
1477
1497
|
*session_state{session_id, last_active, metadata},
|
|
@@ -1796,9 +1816,9 @@ ids[id] <- $ids
|
|
|
1796
1816
|
else {
|
|
1797
1817
|
// Select top 5 entities with the most observations
|
|
1798
1818
|
const res = await this.db.run(`
|
|
1799
|
-
?[
|
|
1800
|
-
*entity{id, name, type, @ "NOW"},
|
|
1801
|
-
*observation{entity_id: id, @ "NOW"}
|
|
1819
|
+
?[eid, ename, etype, count(oid)] :=
|
|
1820
|
+
*entity{id: eid, name: ename, type: etype, @ "NOW"},
|
|
1821
|
+
*observation{entity_id: eid, id: oid, @ "NOW"}
|
|
1802
1822
|
`);
|
|
1803
1823
|
entitiesToReflect = res.rows
|
|
1804
1824
|
.map((r) => ({ id: String(r[0]), name: String(r[1]), type: String(r[2]), count: Number(r[3]) }))
|
|
@@ -1859,8 +1879,8 @@ If no special patterns are recognizable, answer with "No new insights".`;
|
|
|
1859
1879
|
for (const candidate of candidates) {
|
|
1860
1880
|
// Check if relationship already exists
|
|
1861
1881
|
const existing = await this.db.run(`
|
|
1862
|
-
?[count(
|
|
1863
|
-
|
|
1882
|
+
?[count(fid)] := *relationship{from_id: fid, to_id: tid, relation_type: rel, @ "NOW"},
|
|
1883
|
+
fid = $from, tid = $to, rel = $rel
|
|
1864
1884
|
`, { from: candidate.from_id, to: candidate.to_id, rel: candidate.relation_type });
|
|
1865
1885
|
if (Number(existing.rows[0][0]) > 0)
|
|
1866
1886
|
continue;
|
|
@@ -2191,6 +2211,132 @@ ids[id] <- $ids
|
|
|
2191
2211
|
return { error: error.message || "Error during ingest" };
|
|
2192
2212
|
}
|
|
2193
2213
|
}
|
|
2214
|
+
async compactSession(args) {
|
|
2215
|
+
await this.initPromise;
|
|
2216
|
+
const model = args.model || "demyagent-4b-i1:Q6_K";
|
|
2217
|
+
try {
|
|
2218
|
+
// 1. Get all observations from this session
|
|
2219
|
+
const obsRes = await this.db.run('?[text, ts] := *observation{session_id: $sid, text, created_at, @ "NOW"}, ts = to_int(created_at) :order ts', {
|
|
2220
|
+
sid: args.session_id
|
|
2221
|
+
});
|
|
2222
|
+
if (obsRes.rows.length < 3) {
|
|
2223
|
+
return { status: "skipped", reason: "Too few observations for session compaction" };
|
|
2224
|
+
}
|
|
2225
|
+
const observations = obsRes.rows.map((r) => r[0]);
|
|
2226
|
+
const systemPrompt = `You are a memory compaction module. Summarize the following session observations into exactly 2-3 concise bullet points.
|
|
2227
|
+
Focus only on core facts, preferences, or important events. Skip conversational filler.
|
|
2228
|
+
Format:
|
|
2229
|
+
- Bullet Point 1
|
|
2230
|
+
- Bullet Point 2
|
|
2231
|
+
(- Bullet Point 3)`;
|
|
2232
|
+
const userPrompt = `Session ID: ${args.session_id}\n\nObservations:\n${observations.join("\n")}`;
|
|
2233
|
+
const response = await this.callOllama(model, systemPrompt, userPrompt);
|
|
2234
|
+
if (!response)
|
|
2235
|
+
throw new Error("Ollama summary failed");
|
|
2236
|
+
// 2. Add as preference/long-term info to user profile
|
|
2237
|
+
await this.addObservation({
|
|
2238
|
+
entity_id: exports.USER_ENTITY_ID,
|
|
2239
|
+
text: `Session Summary (${args.session_id}): ${response}`,
|
|
2240
|
+
metadata: { kind: "session_summary", session_id: args.session_id, model },
|
|
2241
|
+
deduplicate: true
|
|
2242
|
+
});
|
|
2243
|
+
return { status: "session_compacted", session_id: args.session_id, summary: response };
|
|
2244
|
+
}
|
|
2245
|
+
catch (error) {
|
|
2246
|
+
console.error(`[CompactSession] Error:`, error);
|
|
2247
|
+
return { error: error.message };
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
async compactEntity(args) {
|
|
2251
|
+
if (this.compactionLocks.has(args.entity_id))
|
|
2252
|
+
return { status: "already_compacting" };
|
|
2253
|
+
this.compactionLocks.add(args.entity_id);
|
|
2254
|
+
await this.initPromise;
|
|
2255
|
+
const threshold = args.threshold || 20;
|
|
2256
|
+
const model = args.model || "demyagent-4b-i1:Q6_K";
|
|
2257
|
+
try {
|
|
2258
|
+
// 1. Check count of active observations
|
|
2259
|
+
const countRes = await this.db.run('?[count(oid)] := *observation{entity_id: $eid, id: oid, @ "NOW"}', { eid: args.entity_id });
|
|
2260
|
+
const currentCount = Number(countRes.rows[0][0]);
|
|
2261
|
+
if (currentCount <= threshold) {
|
|
2262
|
+
return { entity_id: args.entity_id, status: "ok", count: currentCount };
|
|
2263
|
+
}
|
|
2264
|
+
console.error(`[CompactEntity] Compacting ${args.entity_id} (Count: ${currentCount})`);
|
|
2265
|
+
// 2. Get older observations (limit to currentCount - threshold/2 to keep some recent context)
|
|
2266
|
+
const toKeep = Math.floor(threshold / 2);
|
|
2267
|
+
const toCompact = currentCount - toKeep;
|
|
2268
|
+
const obsRes = await this.db.run(`
|
|
2269
|
+
?[oid, txt, ts] := *observation{entity_id: $eid, id: oid, text: txt, created_at, @ "NOW"}, ts = to_int(created_at)
|
|
2270
|
+
:order ts
|
|
2271
|
+
:limit $limit
|
|
2272
|
+
`, { eid: args.entity_id, limit: toCompact });
|
|
2273
|
+
if (obsRes.rows.length === 0)
|
|
2274
|
+
return { entity_id: args.entity_id, status: "no_older_obs_found" };
|
|
2275
|
+
const idsToInvalidate = obsRes.rows.map((r) => String(r[0]));
|
|
2276
|
+
const textsToCompact = obsRes.rows.map((r) => r[1]);
|
|
2277
|
+
// 3. Check for existing ExecutiveSummary
|
|
2278
|
+
const existingSummaryRes = await this.db.run('?[sid, stxt] := *observation{entity_id: $eid, text: stxt, id: sid, @ "NOW"}, regex_matches(stxt, "(?i).*(Executive\\s*Summary|ExecutiveSummary).*") :limit 1', { eid: args.entity_id });
|
|
2279
|
+
let summaryText = "";
|
|
2280
|
+
let oldSummaryId = "";
|
|
2281
|
+
if (existingSummaryRes.rows.length > 0) {
|
|
2282
|
+
oldSummaryId = String(existingSummaryRes.rows[0][0]);
|
|
2283
|
+
summaryText = String(existingSummaryRes.rows[0][1]).replace(/Executive\s*Summary:\s*/i, "").replace(/ExecutiveSummary:\s*/i, "");
|
|
2284
|
+
console.error(`[CompactEntity] Found existing summary to merge: ${oldSummaryId}`);
|
|
2285
|
+
}
|
|
2286
|
+
// 4. Summarize / Merge
|
|
2287
|
+
const systemPrompt = `You are a memory compaction module. You are maintaining an "Executive Summary" for an entity.
|
|
2288
|
+
Merge the NEW observations into the EXISTING summary (if present).
|
|
2289
|
+
The summary should be dense, data-rich, and capture all relevant historical facts.
|
|
2290
|
+
Keep it structured and concise (max 5-8 sentences).
|
|
2291
|
+
Format MUST start with "ExecutiveSummary: " followed by the consolidated content.`;
|
|
2292
|
+
const userPrompt = `Entity ID: ${args.entity_id}\nExisting Summary: ${summaryText || "None"}\n\nNew Info to integrate:\n${textsToCompact.join("\n")}`;
|
|
2293
|
+
console.error(`[CompactEntity] Calling Ollama for ${idsToInvalidate.length} observations...`);
|
|
2294
|
+
const newSummaryText = await this.callOllama(model, systemPrompt, userPrompt);
|
|
2295
|
+
if (!newSummaryText)
|
|
2296
|
+
throw new Error("Ollama compaction failed");
|
|
2297
|
+
// 5. Atomic Update
|
|
2298
|
+
// a) Invalidate old observations
|
|
2299
|
+
for (const oid of idsToInvalidate) {
|
|
2300
|
+
await this.invalidateObservation({ observation_id: oid });
|
|
2301
|
+
}
|
|
2302
|
+
// b) Invalidate old summary if exists
|
|
2303
|
+
if (oldSummaryId) {
|
|
2304
|
+
await this.invalidateObservation({ observation_id: oldSummaryId });
|
|
2305
|
+
}
|
|
2306
|
+
// c) Add new summary
|
|
2307
|
+
await this.addObservation({
|
|
2308
|
+
entity_id: args.entity_id,
|
|
2309
|
+
text: newSummaryText,
|
|
2310
|
+
metadata: { kind: "compaction", model, compacted_count: idsToInvalidate.length }
|
|
2311
|
+
});
|
|
2312
|
+
return { status: "entity_compacted", entity_id: args.entity_id, observations_compacted: idsToInvalidate.length };
|
|
2313
|
+
}
|
|
2314
|
+
catch (error) {
|
|
2315
|
+
console.error(`[CompactEntity] Error for ${args.entity_id}:`, error);
|
|
2316
|
+
return { error: error.message };
|
|
2317
|
+
}
|
|
2318
|
+
finally {
|
|
2319
|
+
this.compactionLocks.delete(args.entity_id);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
async callOllama(model, system, user) {
|
|
2323
|
+
try {
|
|
2324
|
+
const ollamaMod = await import("ollama");
|
|
2325
|
+
const ollamaClient = ollamaMod?.default ?? ollamaMod;
|
|
2326
|
+
const response = await ollamaClient.chat({
|
|
2327
|
+
model,
|
|
2328
|
+
messages: [
|
|
2329
|
+
{ role: "system", content: system },
|
|
2330
|
+
{ role: "user", content: user },
|
|
2331
|
+
],
|
|
2332
|
+
});
|
|
2333
|
+
return response?.message?.content?.trim?.() ?? null;
|
|
2334
|
+
}
|
|
2335
|
+
catch (e) {
|
|
2336
|
+
console.error(`[OllamaCall] Error:`, e);
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2194
2340
|
async deleteEntity(args) {
|
|
2195
2341
|
const startTime = Date.now();
|
|
2196
2342
|
try {
|
|
@@ -2203,9 +2349,9 @@ ids[id] <- $ids
|
|
|
2203
2349
|
}
|
|
2204
2350
|
console.error(`[Delete] Entity found: ${entityRes.rows[0][0]}`);
|
|
2205
2351
|
// 2. Count related data before deletion
|
|
2206
|
-
const obsCount = await this.db.run('?[count(
|
|
2207
|
-
const relOutCount = await this.db.run('?[count(
|
|
2208
|
-
const relInCount = await this.db.run('?[count(
|
|
2352
|
+
const obsCount = await this.db.run('?[count(oid)] := *observation{id: oid, entity_id: $id, @ "NOW"}', { id: args.entity_id });
|
|
2353
|
+
const relOutCount = await this.db.run('?[count(fid)] := *relationship{from_id: fid, to_id, @ "NOW"}, from_id = $id', { id: args.entity_id });
|
|
2354
|
+
const relInCount = await this.db.run('?[count(tid)] := *relationship{from_id, to_id: tid, @ "NOW"}, to_id = $id', { id: args.entity_id });
|
|
2209
2355
|
console.error(`[Delete] Related data: ${obsCount.rows[0][0]} observations, ${relOutCount.rows[0][0]} outgoing relations, ${relInCount.rows[0][0]} incoming relations`);
|
|
2210
2356
|
// 3. Delete all related data in a transaction (block)
|
|
2211
2357
|
console.error(`[Delete] Executing deletion transaction...`);
|
|
@@ -2243,6 +2389,280 @@ ids[id] <- $ids
|
|
|
2243
2389
|
return { error: "Deletion failed", message: error.message };
|
|
2244
2390
|
}
|
|
2245
2391
|
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Memory Defragmentation
|
|
2394
|
+
* Reorganizes memory structure by:
|
|
2395
|
+
* 1. Detecting and merging duplicate/near-duplicate observations using LSH MinHash
|
|
2396
|
+
* 2. Connecting fragmented knowledge islands (small connected components) to main graph
|
|
2397
|
+
* 3. Removing orphaned entities (no observations or relations)
|
|
2398
|
+
*
|
|
2399
|
+
* Inspired by Letta MemFS defragmentation
|
|
2400
|
+
*/
|
|
2401
|
+
async defragMemory(args) {
|
|
2402
|
+
await this.initPromise;
|
|
2403
|
+
const startTime = Date.now();
|
|
2404
|
+
try {
|
|
2405
|
+
console.error(`[Defrag] Starting memory defragmentation (confirm=${args.confirm})`);
|
|
2406
|
+
const similarity_threshold = args.similarity_threshold || 0.95; // High threshold for near-duplicates
|
|
2407
|
+
const min_island_size = args.min_island_size || 3; // Islands with <= 3 nodes
|
|
2408
|
+
const stats = {
|
|
2409
|
+
duplicates_found: 0,
|
|
2410
|
+
duplicates_merged: 0,
|
|
2411
|
+
islands_found: 0,
|
|
2412
|
+
islands_connected: 0,
|
|
2413
|
+
orphans_found: 0,
|
|
2414
|
+
orphans_removed: 0,
|
|
2415
|
+
};
|
|
2416
|
+
// Step 1: Find duplicate observations using semantic similarity
|
|
2417
|
+
console.error(`[Defrag] Step 1: Detecting duplicate observations (threshold=${similarity_threshold})`);
|
|
2418
|
+
const allObs = await this.db.run(`
|
|
2419
|
+
?[id, entity_id, text, embedding] := *observation{id, entity_id, text, embedding, @ "NOW"}
|
|
2420
|
+
`);
|
|
2421
|
+
const duplicatePairs = [];
|
|
2422
|
+
// Compare embeddings for similarity (using existing embeddings)
|
|
2423
|
+
for (let i = 0; i < allObs.rows.length; i++) {
|
|
2424
|
+
for (let j = i + 1; j < allObs.rows.length; j++) {
|
|
2425
|
+
const emb1 = allObs.rows[i][3];
|
|
2426
|
+
const emb2 = allObs.rows[j][3];
|
|
2427
|
+
if (!emb1 || !emb2 || emb1.length === 0 || emb2.length === 0)
|
|
2428
|
+
continue;
|
|
2429
|
+
// Cosine similarity
|
|
2430
|
+
const similarity = this.cosineSimilarity(emb1, emb2);
|
|
2431
|
+
if (similarity >= similarity_threshold) {
|
|
2432
|
+
duplicatePairs.push([
|
|
2433
|
+
String(allObs.rows[i][0]), // id1
|
|
2434
|
+
String(allObs.rows[j][0]), // id2
|
|
2435
|
+
similarity
|
|
2436
|
+
]);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
stats.duplicates_found = duplicatePairs.length;
|
|
2441
|
+
console.error(`[Defrag] Found ${stats.duplicates_found} duplicate observation pairs`);
|
|
2442
|
+
// Step 2: Find fragmented knowledge islands (small connected components)
|
|
2443
|
+
console.error(`[Defrag] Step 2: Detecting fragmented knowledge islands`);
|
|
2444
|
+
const components = await this.recomputeConnectedComponents();
|
|
2445
|
+
const smallIslands = Object.entries(components.components || {})
|
|
2446
|
+
.filter(([_, entities]) => entities.length > 0 && entities.length <= min_island_size)
|
|
2447
|
+
.map(([compId, entities]) => ({ compId, entities: entities, size: entities.length }));
|
|
2448
|
+
stats.islands_found = smallIslands.length;
|
|
2449
|
+
console.error(`[Defrag] Found ${stats.islands_found} small knowledge islands (size <= ${min_island_size})`);
|
|
2450
|
+
// Step 3: Find orphaned entities (no observations, no relations)
|
|
2451
|
+
console.error(`[Defrag] Step 3: Detecting orphaned entities`);
|
|
2452
|
+
// Simplified approach: Get all entities, then filter in JavaScript
|
|
2453
|
+
const allEntities = await this.db.run(`
|
|
2454
|
+
?[id, name, type] := *entity{id, name, type, @ "NOW"}, id != "global_user_profile"
|
|
2455
|
+
`);
|
|
2456
|
+
const orphanedEntities = { rows: [] };
|
|
2457
|
+
for (const row of allEntities.rows) {
|
|
2458
|
+
const entityId = String(row[0]);
|
|
2459
|
+
// Check if entity has observations
|
|
2460
|
+
const obsCheck = await this.db.run(`
|
|
2461
|
+
?[count(id)] := *observation{id, entity_id, @ "NOW"}, entity_id = $eid
|
|
2462
|
+
`, { eid: entityId });
|
|
2463
|
+
const hasObs = Number(obsCheck.rows[0]?.[0] || 0) > 0;
|
|
2464
|
+
if (hasObs)
|
|
2465
|
+
continue;
|
|
2466
|
+
// Check if entity has relationships
|
|
2467
|
+
const relCheck = await this.db.run(`
|
|
2468
|
+
out[count(from_id)] := *relationship{from_id, @ "NOW"}, from_id = $eid
|
|
2469
|
+
in[count(to_id)] := *relationship{to_id, @ "NOW"}, to_id = $eid
|
|
2470
|
+
?[total] := out[out_count], in[in_count], total = out_count + in_count
|
|
2471
|
+
`, { eid: entityId });
|
|
2472
|
+
const hasRel = Number(relCheck.rows[0]?.[0] || 0) > 0;
|
|
2473
|
+
if (hasRel)
|
|
2474
|
+
continue;
|
|
2475
|
+
// This entity is orphaned
|
|
2476
|
+
orphanedEntities.rows.push(row);
|
|
2477
|
+
}
|
|
2478
|
+
stats.orphans_found = orphanedEntities.rows.length;
|
|
2479
|
+
console.error(`[Defrag] Found ${stats.orphans_found} orphaned entities`);
|
|
2480
|
+
// If not confirmed, return dry-run results
|
|
2481
|
+
if (!args.confirm) {
|
|
2482
|
+
return {
|
|
2483
|
+
status: "dry_run",
|
|
2484
|
+
message: "Defragmentation analysis complete. Set confirm=true to execute.",
|
|
2485
|
+
statistics: stats,
|
|
2486
|
+
preview: {
|
|
2487
|
+
duplicate_samples: duplicatePairs.slice(0, 5).map(([id1, id2, sim]) => ({
|
|
2488
|
+
observation_id_1: id1,
|
|
2489
|
+
observation_id_2: id2,
|
|
2490
|
+
similarity: sim
|
|
2491
|
+
})),
|
|
2492
|
+
island_samples: smallIslands.slice(0, 5),
|
|
2493
|
+
orphan_samples: orphanedEntities.rows.slice(0, 5).map((r) => ({
|
|
2494
|
+
id: String(r[0]),
|
|
2495
|
+
name: String(r[1]),
|
|
2496
|
+
type: String(r[2])
|
|
2497
|
+
}))
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
// Execute defragmentation
|
|
2502
|
+
console.error(`[Defrag] Executing defragmentation...`);
|
|
2503
|
+
// Merge duplicates: Keep first, delete second
|
|
2504
|
+
for (const [id1, id2, similarity] of duplicatePairs) {
|
|
2505
|
+
try {
|
|
2506
|
+
// Delete the duplicate observation
|
|
2507
|
+
await this.db.run(`
|
|
2508
|
+
?[id, created_at] := *observation{id, created_at}, id = $id2
|
|
2509
|
+
:rm observation {id, created_at}
|
|
2510
|
+
`, { id2 });
|
|
2511
|
+
stats.duplicates_merged++;
|
|
2512
|
+
}
|
|
2513
|
+
catch (e) {
|
|
2514
|
+
console.error(`[Defrag] Failed to merge duplicate ${id2}:`, e.message);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
// Connect islands: Create semantic relations to main graph
|
|
2518
|
+
for (const island of smallIslands) {
|
|
2519
|
+
try {
|
|
2520
|
+
// Find the most central entity in the island
|
|
2521
|
+
const islandEntityIds = island.entities.map((e) => e.id);
|
|
2522
|
+
// Find semantically similar entities in the main graph (not in this island)
|
|
2523
|
+
for (const entityId of islandEntityIds) {
|
|
2524
|
+
const entityData = await this.db.run(`
|
|
2525
|
+
?[embedding] := *entity{id: $id, embedding, @ "NOW"}
|
|
2526
|
+
`, { id: entityId });
|
|
2527
|
+
if (entityData.rows.length === 0)
|
|
2528
|
+
continue;
|
|
2529
|
+
const embedding = entityData.rows[0][0];
|
|
2530
|
+
if (!embedding || embedding.length === 0)
|
|
2531
|
+
continue;
|
|
2532
|
+
// Find similar entity in main graph
|
|
2533
|
+
const similarEntities = await this.db.run(`
|
|
2534
|
+
~entity:semantic { id: target_id | query: $emb, k: 1, bind_distance: dist }
|
|
2535
|
+
?[target_id, dist] := ~entity:semantic { id: target_id | query: $emb, k: 1, bind_distance: dist },
|
|
2536
|
+
target_id != $id
|
|
2537
|
+
`, { emb: embedding, id: entityId });
|
|
2538
|
+
if (similarEntities.rows.length > 0) {
|
|
2539
|
+
const targetId = String(similarEntities.rows[0][0]);
|
|
2540
|
+
const distance = Number(similarEntities.rows[0][1]);
|
|
2541
|
+
const similarity = 1 - distance;
|
|
2542
|
+
// Create bridge relation
|
|
2543
|
+
if (similarity >= 0.7) {
|
|
2544
|
+
await this.createRelation({
|
|
2545
|
+
from_id: entityId,
|
|
2546
|
+
to_id: targetId,
|
|
2547
|
+
relation_type: "semantically_related",
|
|
2548
|
+
strength: similarity,
|
|
2549
|
+
metadata: { created_by: "defrag", reason: "island_connection" }
|
|
2550
|
+
});
|
|
2551
|
+
stats.islands_connected++;
|
|
2552
|
+
break; // One connection per island is enough
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
catch (e) {
|
|
2558
|
+
console.error(`[Defrag] Failed to connect island ${island.compId}:`, e.message);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
// Remove orphaned entities
|
|
2562
|
+
for (const row of orphanedEntities.rows) {
|
|
2563
|
+
try {
|
|
2564
|
+
const entityId = String(row[0]);
|
|
2565
|
+
await this.deleteEntity({ entity_id: entityId });
|
|
2566
|
+
stats.orphans_removed++;
|
|
2567
|
+
}
|
|
2568
|
+
catch (e) {
|
|
2569
|
+
console.error(`[Defrag] Failed to remove orphan ${row[0]}:`, e.message);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
this.trackOperation('defrag', startTime);
|
|
2573
|
+
return {
|
|
2574
|
+
status: "completed",
|
|
2575
|
+
message: `Defragmentation complete: ${stats.duplicates_merged} duplicates merged, ${stats.islands_connected} islands connected, ${stats.orphans_removed} orphans removed`,
|
|
2576
|
+
statistics: stats,
|
|
2577
|
+
duration_ms: Date.now() - startTime
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
catch (error) {
|
|
2581
|
+
console.error("[Defrag] Error during defragmentation:", error);
|
|
2582
|
+
this.trackError('defrag');
|
|
2583
|
+
return { error: "Defragmentation failed", message: error.message };
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
cosineSimilarity(a, b) {
|
|
2587
|
+
if (a.length !== b.length)
|
|
2588
|
+
return 0;
|
|
2589
|
+
let dotProduct = 0;
|
|
2590
|
+
let normA = 0;
|
|
2591
|
+
let normB = 0;
|
|
2592
|
+
for (let i = 0; i < a.length; i++) {
|
|
2593
|
+
dotProduct += a[i] * b[i];
|
|
2594
|
+
normA += a[i] * a[i];
|
|
2595
|
+
normB += b[i] * b[i];
|
|
2596
|
+
}
|
|
2597
|
+
if (normA === 0 || normB === 0)
|
|
2598
|
+
return 0;
|
|
2599
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
2600
|
+
}
|
|
2601
|
+
async invalidateObservation(args) {
|
|
2602
|
+
await this.initPromise;
|
|
2603
|
+
const startTime = Date.now();
|
|
2604
|
+
try {
|
|
2605
|
+
const now = Date.now() * 1000;
|
|
2606
|
+
// Invalidate by putting a retraction (assertive=false) at current time
|
|
2607
|
+
const existing = await this.db.run(`?[oid, eid, sid, tid, txt, emb, meta] := *observation{id: oid, entity_id: eid, session_id: sid, task_id: tid, text: txt, embedding: emb, metadata: meta, @ "NOW"}, oid = $id`, { id: args.observation_id });
|
|
2608
|
+
if (existing.rows.length === 0) {
|
|
2609
|
+
return { error: "Observation not found or already invalidated" };
|
|
2610
|
+
}
|
|
2611
|
+
const row = existing.rows[0];
|
|
2612
|
+
const params = {
|
|
2613
|
+
id: row[0],
|
|
2614
|
+
v: [now, false],
|
|
2615
|
+
entity_id: row[1],
|
|
2616
|
+
session_id: row[2],
|
|
2617
|
+
task_id: row[3],
|
|
2618
|
+
text: row[4],
|
|
2619
|
+
embedding: row[5],
|
|
2620
|
+
metadata: row[6]
|
|
2621
|
+
};
|
|
2622
|
+
await this.db.run(`
|
|
2623
|
+
?[id, created_at, entity_id, session_id, task_id, text, embedding, metadata] <- [[$id, $v, $entity_id, $session_id, $task_id, $text, $embedding, $metadata]]
|
|
2624
|
+
:put observation {id, created_at => entity_id, session_id, task_id, text, embedding, metadata}
|
|
2625
|
+
`, params);
|
|
2626
|
+
this.trackOperation('invalidate_observation', startTime);
|
|
2627
|
+
return { status: "Observation invalidated", id: args.observation_id, invalidated_at: now };
|
|
2628
|
+
}
|
|
2629
|
+
catch (error) {
|
|
2630
|
+
console.error("[InvalidateObs] Error:", error);
|
|
2631
|
+
this.trackError('invalidate_observation');
|
|
2632
|
+
return { error: "Invalidation failed", message: error.message };
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
async invalidateRelationship(args) {
|
|
2636
|
+
await this.initPromise;
|
|
2637
|
+
const startTime = Date.now();
|
|
2638
|
+
try {
|
|
2639
|
+
const now = Date.now() * 1000;
|
|
2640
|
+
const existing = await this.db.run(`?[f, t, type, strength, metadata] := *relationship{from_id: f, to_id: t, relation_type: type, strength, metadata, @ "NOW"}, f = $f, t = $t, type = $type`, { f: args.from_id, t: args.to_id, type: args.relation_type });
|
|
2641
|
+
if (existing.rows.length === 0) {
|
|
2642
|
+
return { error: "Relationship not found or already invalidated" };
|
|
2643
|
+
}
|
|
2644
|
+
const row = existing.rows[0];
|
|
2645
|
+
const params = {
|
|
2646
|
+
f: row[0],
|
|
2647
|
+
t: row[1],
|
|
2648
|
+
type: row[2],
|
|
2649
|
+
v: [now, false],
|
|
2650
|
+
strength: row[3],
|
|
2651
|
+
metadata: row[4]
|
|
2652
|
+
};
|
|
2653
|
+
await this.db.run(`
|
|
2654
|
+
?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[$f, $t, $type, $v, $strength, $metadata]]
|
|
2655
|
+
:put relationship {from_id, to_id, relation_type, created_at => strength, metadata}
|
|
2656
|
+
`, params);
|
|
2657
|
+
this.trackOperation('invalidate_relationship', startTime);
|
|
2658
|
+
return { status: "Relationship invalidated", from_id: args.from_id, to_id: args.to_id, relation_type: args.relation_type, invalidated_at: now };
|
|
2659
|
+
}
|
|
2660
|
+
catch (error) {
|
|
2661
|
+
console.error("[InvalidateRel] Error:", error);
|
|
2662
|
+
this.trackError('invalidate_relationship');
|
|
2663
|
+
return { error: "Invalidation failed", message: error.message };
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2246
2666
|
async runTransaction(args) {
|
|
2247
2667
|
await this.initPromise;
|
|
2248
2668
|
try {
|
|
@@ -2444,6 +2864,46 @@ ids[id] <- $ids
|
|
|
2444
2864
|
results.push({ action: "delete_entity", id: entity_id });
|
|
2445
2865
|
break;
|
|
2446
2866
|
}
|
|
2867
|
+
case "invalidate_observation": {
|
|
2868
|
+
const { observation_id } = params;
|
|
2869
|
+
if (!observation_id) {
|
|
2870
|
+
return { error: `Missing observation_id for invalidate_observation in operation ${i}` };
|
|
2871
|
+
}
|
|
2872
|
+
const now = Date.now() * 1000;
|
|
2873
|
+
allParams[`inv_obs_id${suffix}`] = observation_id;
|
|
2874
|
+
allParams[`inv_obs_v${suffix}`] = [now, false];
|
|
2875
|
+
statements.push(`
|
|
2876
|
+
{
|
|
2877
|
+
?[oid, cat, eid, sid, tid, txt, emb, meta] :=
|
|
2878
|
+
*observation{id: oid, entity_id: eid, session_id: sid, task_id: tid, text: txt, embedding: emb, metadata: meta, @ "NOW"},
|
|
2879
|
+
oid = $inv_obs_id${suffix}, cat = $inv_obs_v${suffix}
|
|
2880
|
+
:put observation {id: oid, created_at: cat => entity_id: eid, session_id: sid, task_id: tid, text: txt, embedding: emb, metadata: meta}
|
|
2881
|
+
}
|
|
2882
|
+
`);
|
|
2883
|
+
results.push({ action: "invalidate_observation", id: observation_id });
|
|
2884
|
+
break;
|
|
2885
|
+
}
|
|
2886
|
+
case "invalidate_relation": {
|
|
2887
|
+
const { from_id, to_id, relation_type } = params;
|
|
2888
|
+
if (!from_id || !to_id || !relation_type) {
|
|
2889
|
+
return { error: `Missing from_id, to_id, or relation_type for invalidate_relation in operation ${i}` };
|
|
2890
|
+
}
|
|
2891
|
+
const now = Date.now() * 1000;
|
|
2892
|
+
allParams[`inv_rel_f${suffix}`] = from_id;
|
|
2893
|
+
allParams[`inv_rel_t${suffix}`] = to_id;
|
|
2894
|
+
allParams[`inv_rel_type${suffix}`] = relation_type;
|
|
2895
|
+
allParams[`inv_rel_v${suffix}`] = [now, false];
|
|
2896
|
+
statements.push(`
|
|
2897
|
+
{
|
|
2898
|
+
?[f, t, type, cat, str, meta] :=
|
|
2899
|
+
*relationship{from_id: f, to_id: t, relation_type: type, strength: str, metadata: meta, @ "NOW"},
|
|
2900
|
+
f = $inv_rel_f${suffix}, t = $inv_rel_t${suffix}, type = $inv_rel_type${suffix}, cat = $inv_rel_v${suffix}
|
|
2901
|
+
:put relationship {from_id: f, to_id: t, relation_type: type, created_at: cat => strength: str, metadata: meta}
|
|
2902
|
+
}
|
|
2903
|
+
`);
|
|
2904
|
+
results.push({ action: "invalidate_relation", from_id, to_id, relation_type });
|
|
2905
|
+
break;
|
|
2906
|
+
}
|
|
2447
2907
|
default:
|
|
2448
2908
|
return { error: `Unknown operation: ${op.action}` };
|
|
2449
2909
|
}
|
|
@@ -2623,6 +3083,16 @@ ids[id] <- $ids
|
|
|
2623
3083
|
action: zod_1.z.literal("delete_entity"),
|
|
2624
3084
|
entity_id: zod_1.z.string().describe("ID of the entity to delete"),
|
|
2625
3085
|
}).passthrough(),
|
|
3086
|
+
zod_1.z.object({
|
|
3087
|
+
action: zod_1.z.literal("invalidate_observation"),
|
|
3088
|
+
observation_id: zod_1.z.string().describe("ID of the observation to invalidate"),
|
|
3089
|
+
}).passthrough(),
|
|
3090
|
+
zod_1.z.object({
|
|
3091
|
+
action: zod_1.z.literal("invalidate_relation"),
|
|
3092
|
+
from_id: zod_1.z.string().describe("Source entity ID"),
|
|
3093
|
+
to_id: zod_1.z.string().describe("Target entity ID"),
|
|
3094
|
+
relation_type: zod_1.z.string().describe("Type of the relationship"),
|
|
3095
|
+
}).passthrough(),
|
|
2626
3096
|
zod_1.z.object({
|
|
2627
3097
|
action: zod_1.z.literal("add_observation"),
|
|
2628
3098
|
entity_id: zod_1.z.string().optional().describe("ID of the entity"),
|
|
@@ -2746,7 +3216,7 @@ ids[id] <- $ids
|
|
|
2746
3216
|
]);
|
|
2747
3217
|
const MutateMemoryParameters = zod_1.z.object({
|
|
2748
3218
|
action: zod_1.z
|
|
2749
|
-
.enum(["create_entity", "update_entity", "delete_entity", "add_observation", "create_relation", "run_transaction", "add_inference_rule", "ingest_file", "start_session", "stop_session", "start_task", "stop_task"])
|
|
3219
|
+
.enum(["create_entity", "update_entity", "delete_entity", "add_observation", "create_relation", "run_transaction", "add_inference_rule", "ingest_file", "start_session", "stop_session", "start_task", "stop_task", "invalidate_observation", "invalidate_relation"])
|
|
2750
3220
|
.describe("Action (determines which fields are required)"),
|
|
2751
3221
|
name: zod_1.z.string().optional().describe("For create_entity (required) or add_inference_rule (required)"),
|
|
2752
3222
|
type: zod_1.z.string().optional().describe("For create_entity (required)"),
|
|
@@ -2768,6 +3238,7 @@ ids[id] <- $ids
|
|
|
2768
3238
|
relation_type: zod_1.z.string().optional().describe("For create_relation (required)"),
|
|
2769
3239
|
strength: zod_1.z.number().min(0).max(1).optional().describe("Optional for create_relation"),
|
|
2770
3240
|
metadata: MetadataSchema.optional().describe("Optional for create_entity/update_entity/add_observation/create_relation/ingest_file"),
|
|
3241
|
+
observation_id: zod_1.z.string().optional().describe("For invalidate_observation (required)"),
|
|
2771
3242
|
operations: zod_1.z.array(zod_1.z.object({
|
|
2772
3243
|
action: zod_1.z.enum(["create_entity", "add_observation", "create_relation", "delete_entity"]),
|
|
2773
3244
|
params: zod_1.z.any().describe("Parameters for the operation as an object")
|
|
@@ -2798,6 +3269,8 @@ Supported actions:
|
|
|
2798
3269
|
- 'stop_session': Closes a session. Params: { id: string }.
|
|
2799
3270
|
- 'start_task': Initializes a new task within a session. Params: { name: string, session_id?: string, metadata?: object }.
|
|
2800
3271
|
- 'stop_task': Marks a task as completed. Params: { id: string }.
|
|
3272
|
+
- 'invalidate_observation': Invalidates (soft-deletes) an observation at the current time. Params: { observation_id: string }.
|
|
3273
|
+
- 'invalidate_relation': Invalidates (soft-deletes) a relationship at the current time. Params: { from_id: string, to_id: string, relation_type: string }.
|
|
2801
3274
|
|
|
2802
3275
|
Validation: Invalid syntax or missing columns in inference rules will result in errors.`,
|
|
2803
3276
|
parameters: MutateMemoryParameters,
|
|
@@ -2825,6 +3298,10 @@ Validation: Invalid syntax or missing columns in inference rules will result in
|
|
|
2825
3298
|
return JSON.stringify(await this.createRelation(rest));
|
|
2826
3299
|
if (action === "run_transaction")
|
|
2827
3300
|
return JSON.stringify(await this.runTransaction(rest));
|
|
3301
|
+
if (action === "invalidate_observation")
|
|
3302
|
+
return JSON.stringify(await this.invalidateObservation(rest));
|
|
3303
|
+
if (action === "invalidate_relation")
|
|
3304
|
+
return JSON.stringify(await this.invalidateRelationship(rest));
|
|
2828
3305
|
if (action === "delete_entity")
|
|
2829
3306
|
return JSON.stringify(await this.deleteEntity({ entity_id: rest.entity_id }));
|
|
2830
3307
|
if (action === "add_inference_rule")
|
|
@@ -2921,13 +3398,56 @@ Validation: Invalid syntax or missing columns in inference rules will result in
|
|
|
2921
3398
|
session_id: zod_1.z.string().optional().describe("Prioritize results from this session"),
|
|
2922
3399
|
task_id: zod_1.z.string().optional().describe("Prioritize results from this task"),
|
|
2923
3400
|
}),
|
|
3401
|
+
zod_1.z.object({
|
|
3402
|
+
action: zod_1.z.literal("dynamic_fusion"),
|
|
3403
|
+
query: zod_1.z.string().describe("Search query"),
|
|
3404
|
+
config: zod_1.z.object({
|
|
3405
|
+
vector: zod_1.z.object({
|
|
3406
|
+
enabled: zod_1.z.boolean().optional().default(true),
|
|
3407
|
+
weight: zod_1.z.number().min(0).max(1).optional().default(0.4),
|
|
3408
|
+
topK: zod_1.z.number().optional().default(20),
|
|
3409
|
+
efSearch: zod_1.z.number().optional().default(100),
|
|
3410
|
+
}).optional(),
|
|
3411
|
+
sparse: zod_1.z.object({
|
|
3412
|
+
enabled: zod_1.z.boolean().optional().default(true),
|
|
3413
|
+
weight: zod_1.z.number().min(0).max(1).optional().default(0.3),
|
|
3414
|
+
topK: zod_1.z.number().optional().default(20),
|
|
3415
|
+
minScore: zod_1.z.number().optional().default(0.1),
|
|
3416
|
+
}).optional(),
|
|
3417
|
+
fts: zod_1.z.object({
|
|
3418
|
+
enabled: zod_1.z.boolean().optional().default(true),
|
|
3419
|
+
weight: zod_1.z.number().min(0).max(1).optional().default(0.2),
|
|
3420
|
+
topK: zod_1.z.number().optional().default(20),
|
|
3421
|
+
fuzzy: zod_1.z.boolean().optional().default(true),
|
|
3422
|
+
}).optional(),
|
|
3423
|
+
graph: zod_1.z.object({
|
|
3424
|
+
enabled: zod_1.z.boolean().optional().default(true),
|
|
3425
|
+
weight: zod_1.z.number().min(0).max(1).optional().default(0.1),
|
|
3426
|
+
maxDepth: zod_1.z.number().min(1).max(3).optional().default(2),
|
|
3427
|
+
maxResults: zod_1.z.number().optional().default(20),
|
|
3428
|
+
relationTypes: zod_1.z.array(zod_1.z.string()).optional(),
|
|
3429
|
+
}).optional(),
|
|
3430
|
+
fusion: zod_1.z.object({
|
|
3431
|
+
strategy: zod_1.z.enum(['rrf', 'weighted_sum', 'max', 'adaptive']).optional().default('rrf'),
|
|
3432
|
+
rrfK: zod_1.z.number().optional().default(60),
|
|
3433
|
+
minScore: zod_1.z.number().optional().default(0.0),
|
|
3434
|
+
deduplication: zod_1.z.boolean().optional().default(true),
|
|
3435
|
+
}).optional(),
|
|
3436
|
+
}).optional().describe("Dynamic fusion configuration (all paths optional)"),
|
|
3437
|
+
limit: zod_1.z.number().optional().default(10).describe("Maximum number of results to return"),
|
|
3438
|
+
}),
|
|
3439
|
+
zod_1.z.object({
|
|
3440
|
+
action: zod_1.z.literal("adaptive_retrieval"),
|
|
3441
|
+
query: zod_1.z.string().describe("Search query for adaptive retrieval"),
|
|
3442
|
+
limit: zod_1.z.number().optional().default(10).describe("Maximum number of results"),
|
|
3443
|
+
}),
|
|
2924
3444
|
]);
|
|
2925
3445
|
const QueryMemoryParameters = zod_1.z.object({
|
|
2926
3446
|
action: zod_1.z
|
|
2927
|
-
.enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking", "agentic_search"])
|
|
3447
|
+
.enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking", "agentic_search", "dynamic_fusion", "adaptive_retrieval"])
|
|
2928
3448
|
.describe("Action (determines which fields are required)"),
|
|
2929
|
-
query: zod_1.z.string().optional().describe("Required for search/advancedSearch/context/graph_rag/graph_walking/agentic_search"),
|
|
2930
|
-
limit: zod_1.z.number().optional().describe("Only for search/advancedSearch/graph_rag/graph_walking"),
|
|
3449
|
+
query: zod_1.z.string().optional().describe("Required for search/advancedSearch/context/graph_rag/graph_walking/agentic_search/dynamic_fusion/adaptive_retrieval"),
|
|
3450
|
+
limit: zod_1.z.number().optional().describe("Only for search/advancedSearch/graph_rag/graph_walking/dynamic_fusion/adaptive_retrieval"),
|
|
2931
3451
|
session_id: zod_1.z.string().optional().describe("Optional session ID for context boosting"),
|
|
2932
3452
|
task_id: zod_1.z.string().optional().describe("Optional task ID for context boosting"),
|
|
2933
3453
|
filters: zod_1.z.any().optional().describe("Only for advancedSearch"),
|
|
@@ -2943,6 +3463,7 @@ Validation: Invalid syntax or missing columns in inference rules will result in
|
|
|
2943
3463
|
max_depth: zod_1.z.number().optional().describe("Only for graph_rag/graph_walking: Maximum expansion depth"),
|
|
2944
3464
|
start_entity_id: zod_1.z.string().optional().describe("Only for graph_walking: Start entity"),
|
|
2945
3465
|
rerank: zod_1.z.boolean().optional().describe("Only for search/advancedSearch/agentic_search: Enable Cross-Encoder reranking"),
|
|
3466
|
+
config: zod_1.z.any().optional().describe("Only for dynamic_fusion: Fusion configuration object"),
|
|
2946
3467
|
});
|
|
2947
3468
|
this.mcp.addTool({
|
|
2948
3469
|
name: "query_memory",
|
|
@@ -2958,8 +3479,10 @@ Supported actions:
|
|
|
2958
3479
|
- 'graph_rag': Graph-based reasoning (Hybrid RAG). Finds semantic vector seeds first, then expands via graph traversals. Ideal for multi-hop reasoning. Params: { query: string, max_depth?: number, limit?: number }.
|
|
2959
3480
|
- 'graph_walking': Recursive semantic graph search. Starts at vector seeds or an entity and follows relationships to other semantically relevant entities. Params: { query: string, start_entity_id?: string, max_depth?: number, limit?: number }.
|
|
2960
3481
|
- 'agentic_search': Auto-Routing Search. Uses local LLM to analyze intent and routes the query automatically to the best strategy (Vector, Graph, or Community Summaries). Params: { query: string, limit?: number }.
|
|
3482
|
+
- 'adaptive_retrieval': GraphRAG-R1 inspired adaptive retrieval with Progressive Retrieval Attenuation (PRA) and Cost-Aware F1 (CAF) scoring. Automatically selects optimal strategy based on query complexity and historical performance. Params: { query: string, limit?: number }.
|
|
3483
|
+
- 'dynamic_fusion': Advanced multi-path fusion search combining Vector (HNSW), Sparse (keyword), FTS (full-text), and Graph traversal with configurable weights and strategies. Params: { query: string, config?: { vector?, sparse?, fts?, graph?, fusion? }, limit?: number }. Each path can be enabled/disabled and weighted independently. Fusion strategies: 'rrf' (Reciprocal Rank Fusion), 'weighted_sum', 'max', 'adaptive'. Returns results with path contribution details and performance stats.
|
|
2961
3484
|
|
|
2962
|
-
Notes: '
|
|
3485
|
+
Notes: 'adaptive_retrieval' learns from usage and optimizes over time. 'dynamic_fusion' provides the most control and transparency over retrieval paths. 'agentic_search' is the most adaptive. 'context' is ideal for exploratory questions. 'search' and 'advancedSearch' are better for targeted fact retrieval.`,
|
|
2963
3486
|
parameters: QueryMemoryParameters,
|
|
2964
3487
|
execute: async (args) => {
|
|
2965
3488
|
await this.initPromise;
|
|
@@ -3130,6 +3653,101 @@ Notes: 'agentic_search' is the most powerful and adaptable, 'context' is ideal f
|
|
|
3130
3653
|
});
|
|
3131
3654
|
return JSON.stringify(results);
|
|
3132
3655
|
}
|
|
3656
|
+
if (input.action === "dynamic_fusion") {
|
|
3657
|
+
const startTime = Date.now();
|
|
3658
|
+
if (!input.query || input.query.trim().length === 0) {
|
|
3659
|
+
return JSON.stringify({ error: "Search query must not be empty." });
|
|
3660
|
+
}
|
|
3661
|
+
try {
|
|
3662
|
+
console.log('[query_memory] Dynamic Fusion search:', {
|
|
3663
|
+
query: input.query,
|
|
3664
|
+
config: input.config ? 'custom' : 'default',
|
|
3665
|
+
limit: input.limit
|
|
3666
|
+
});
|
|
3667
|
+
// Execute dynamic fusion search
|
|
3668
|
+
const { results, stats } = await this.dynamicFusion.search(input.query, input.config || {});
|
|
3669
|
+
// Apply limit
|
|
3670
|
+
const limitedResults = results.slice(0, input.limit || 10);
|
|
3671
|
+
const response = {
|
|
3672
|
+
results: limitedResults,
|
|
3673
|
+
stats: {
|
|
3674
|
+
...stats,
|
|
3675
|
+
totalTime: Date.now() - startTime,
|
|
3676
|
+
resultsReturned: limitedResults.length,
|
|
3677
|
+
resultsTotal: results.length
|
|
3678
|
+
},
|
|
3679
|
+
metadata: {
|
|
3680
|
+
query: input.query,
|
|
3681
|
+
fusionStrategy: input.config?.fusion?.strategy || 'rrf',
|
|
3682
|
+
enabledPaths: {
|
|
3683
|
+
vector: input.config?.vector?.enabled !== false,
|
|
3684
|
+
sparse: input.config?.sparse?.enabled !== false,
|
|
3685
|
+
fts: input.config?.fts?.enabled !== false,
|
|
3686
|
+
graph: input.config?.graph?.enabled !== false
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
console.log('[query_memory] Dynamic Fusion completed:', {
|
|
3691
|
+
resultsReturned: limitedResults.length,
|
|
3692
|
+
totalTime: response.stats.totalTime,
|
|
3693
|
+
pathContributions: stats.pathContributions
|
|
3694
|
+
});
|
|
3695
|
+
return JSON.stringify(response);
|
|
3696
|
+
}
|
|
3697
|
+
catch (error) {
|
|
3698
|
+
console.error('[query_memory] Dynamic Fusion error:', error);
|
|
3699
|
+
return JSON.stringify({
|
|
3700
|
+
error: "Dynamic fusion search failed",
|
|
3701
|
+
details: error.message
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
if (input.action === "adaptive_retrieval") {
|
|
3706
|
+
const startTime = Date.now();
|
|
3707
|
+
if (!input.query || input.query.trim().length === 0) {
|
|
3708
|
+
return JSON.stringify({ error: "Search query must not be empty." });
|
|
3709
|
+
}
|
|
3710
|
+
try {
|
|
3711
|
+
console.log('[query_memory] Adaptive Retrieval search:', {
|
|
3712
|
+
query: input.query,
|
|
3713
|
+
limit: input.limit
|
|
3714
|
+
});
|
|
3715
|
+
// Execute adaptive retrieval
|
|
3716
|
+
const result = await this.adaptiveRetrieval.retrieve(input.query, input.limit || 10);
|
|
3717
|
+
const response = {
|
|
3718
|
+
results: result.results,
|
|
3719
|
+
metadata: {
|
|
3720
|
+
query: input.query,
|
|
3721
|
+
strategy: result.strategy,
|
|
3722
|
+
retrievalCount: result.retrievalCount,
|
|
3723
|
+
latency: result.latency,
|
|
3724
|
+
cafScore: result.cafScore,
|
|
3725
|
+
totalTime: Date.now() - startTime
|
|
3726
|
+
},
|
|
3727
|
+
performance: {
|
|
3728
|
+
strategyUsed: result.strategy,
|
|
3729
|
+
retrievalCalls: result.retrievalCount,
|
|
3730
|
+
costAwareF1: result.cafScore,
|
|
3731
|
+
latencyMs: result.latency
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
3734
|
+
console.log('[query_memory] Adaptive Retrieval completed:', {
|
|
3735
|
+
strategy: result.strategy,
|
|
3736
|
+
resultsReturned: result.results.length,
|
|
3737
|
+
retrievalCount: result.retrievalCount,
|
|
3738
|
+
cafScore: result.cafScore?.toFixed(3),
|
|
3739
|
+
totalTime: response.metadata.totalTime
|
|
3740
|
+
});
|
|
3741
|
+
return JSON.stringify(response);
|
|
3742
|
+
}
|
|
3743
|
+
catch (error) {
|
|
3744
|
+
console.error('[query_memory] Adaptive Retrieval error:', error);
|
|
3745
|
+
return JSON.stringify({
|
|
3746
|
+
error: "Adaptive retrieval search failed",
|
|
3747
|
+
details: error.message
|
|
3748
|
+
});
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3133
3751
|
if (input.action === "graph_rag") {
|
|
3134
3752
|
if (!input.query || input.query.trim().length === 0) {
|
|
3135
3753
|
return JSON.stringify({ error: "Search query must not be empty." });
|
|
@@ -3603,6 +4221,12 @@ Supported actions:
|
|
|
3603
4221
|
min_entity_degree: zod_1.z.number().min(0).max(100).optional().default(2),
|
|
3604
4222
|
model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
|
|
3605
4223
|
}),
|
|
4224
|
+
zod_1.z.object({
|
|
4225
|
+
action: zod_1.z.literal("defrag"),
|
|
4226
|
+
confirm: zod_1.z.boolean().describe("Must be true to confirm defragmentation"),
|
|
4227
|
+
similarity_threshold: zod_1.z.number().min(0.8).max(1.0).optional().default(0.95).describe("Similarity threshold for duplicate detection (0.8-1.0)"),
|
|
4228
|
+
min_island_size: zod_1.z.number().min(1).max(10).optional().default(3).describe("Maximum size of knowledge islands to connect"),
|
|
4229
|
+
}),
|
|
3606
4230
|
zod_1.z.object({
|
|
3607
4231
|
action: zod_1.z.literal("reflect"),
|
|
3608
4232
|
entity_id: zod_1.z.string().optional().describe("Optional entity ID for targeted reflection"),
|
|
@@ -3618,10 +4242,16 @@ Supported actions:
|
|
|
3618
4242
|
model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
|
|
3619
4243
|
min_community_size: zod_1.z.number().min(2).max(100).optional().default(3),
|
|
3620
4244
|
}),
|
|
4245
|
+
zod_1.z.object({
|
|
4246
|
+
action: zod_1.z.literal("compact"),
|
|
4247
|
+
session_id: zod_1.z.string().optional().describe("Session ID to compact"),
|
|
4248
|
+
entity_id: zod_1.z.string().optional().describe("Entity ID to compact"),
|
|
4249
|
+
model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
|
|
4250
|
+
}),
|
|
3621
4251
|
]);
|
|
3622
4252
|
const ManageSystemParameters = zod_1.z.object({
|
|
3623
4253
|
action: zod_1.z
|
|
3624
|
-
.enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory", "summarize_communities"])
|
|
4254
|
+
.enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "defrag", "reflect", "clear_memory", "summarize_communities", "compact"])
|
|
3625
4255
|
.describe("Action (determines which fields are required)"),
|
|
3626
4256
|
format: zod_1.z.enum(["json", "markdown", "obsidian"]).optional().describe("Export format (for export_memory)"),
|
|
3627
4257
|
includeMetadata: zod_1.z.boolean().optional().describe("Include metadata (for export_memory)"),
|
|
@@ -3636,10 +4266,12 @@ Supported actions:
|
|
|
3636
4266
|
snapshot_id_a: zod_1.z.string().optional().describe("Required for snapshot_diff"),
|
|
3637
4267
|
snapshot_id_b: zod_1.z.string().optional().describe("Required for snapshot_diff"),
|
|
3638
4268
|
metadata: MetadataSchema.optional().describe("Optional for snapshot_create"),
|
|
3639
|
-
confirm: zod_1.z.boolean().optional().describe("Required for cleanup/clear_memory and must be true"),
|
|
4269
|
+
confirm: zod_1.z.boolean().optional().describe("Required for cleanup/defrag/clear_memory and must be true"),
|
|
3640
4270
|
older_than_days: zod_1.z.number().optional().describe("Optional for cleanup"),
|
|
3641
4271
|
max_observations: zod_1.z.number().optional().describe("Optional for cleanup"),
|
|
3642
4272
|
min_entity_degree: zod_1.z.number().optional().describe("Optional for cleanup"),
|
|
4273
|
+
similarity_threshold: zod_1.z.number().optional().describe("Optional for defrag (0.8-1.0, default 0.95)"),
|
|
4274
|
+
min_island_size: zod_1.z.number().optional().describe("Optional for defrag (1-10, default 3)"),
|
|
3643
4275
|
model: zod_1.z.string().optional().describe("Optional for cleanup/reflect/summarize_communities"),
|
|
3644
4276
|
entity_id: zod_1.z.string().optional().describe("Optional for reflect"),
|
|
3645
4277
|
min_community_size: zod_1.z.number().optional().describe("Optional for summarize_communities"),
|
|
@@ -3666,6 +4298,9 @@ Supported actions:
|
|
|
3666
4298
|
- 'cleanup': Janitor service for consolidation. Params: { confirm: boolean, older_than_days?: number, max_observations?: number, min_entity_degree?: number, model?: string }.
|
|
3667
4299
|
* With confirm=false: Dry-Run (shows candidates).
|
|
3668
4300
|
* With confirm=true: Merges old/isolated fragments using LLM (Executive Summary) and removes noise.
|
|
4301
|
+
- 'defrag': Memory defragmentation. Reorganizes memory structure by detecting/merging duplicates, connecting fragmented knowledge islands, and removing orphaned entities. Params: { confirm: boolean, similarity_threshold?: number (0.8-1.0, default 0.95), min_island_size?: number (1-10, default 3) }.
|
|
4302
|
+
* With confirm=false: Dry-Run (shows analysis and candidates).
|
|
4303
|
+
* With confirm=true: Executes defragmentation and returns statistics.
|
|
3669
4304
|
- 'reflect': Reflection service. Analyzes memory for contradictions and insights. Params: { entity_id?: string, model?: string }.
|
|
3670
4305
|
- 'clear_memory': Resets the entire database. Params: { confirm: boolean (must be true) }.
|
|
3671
4306
|
- 'summarize_communities': Hierarchical GraphRAG. Generates summaries for entity clusters. Params: { model?: string, min_community_size?: number }.`,
|
|
@@ -3764,9 +4399,9 @@ Supported actions:
|
|
|
3764
4399
|
if (input.action === "snapshot_create") {
|
|
3765
4400
|
try {
|
|
3766
4401
|
// Optimization: Sequential execution and count aggregation instead of full fetch
|
|
3767
|
-
const entityResult = await this.db.run('?[count(
|
|
3768
|
-
const obsResult = await this.db.run('?[count(
|
|
3769
|
-
const relResult = await this.db.run('?[count(
|
|
4402
|
+
const entityResult = await this.db.run('?[count(eid)] := *entity{id: eid, @ "NOW"}');
|
|
4403
|
+
const obsResult = await this.db.run('?[count(oid)] := *observation{id: oid, @ "NOW"}');
|
|
4404
|
+
const relResult = await this.db.run('?[count(fid)] := *relationship{from_id: fid, to_id, @ "NOW"}');
|
|
3770
4405
|
const snapshot_id = (0, uuid_1.v4)();
|
|
3771
4406
|
const counts = {
|
|
3772
4407
|
entities: Number(entityResult.rows[0]?.[0] || 0),
|
|
@@ -3856,6 +4491,19 @@ Supported actions:
|
|
|
3856
4491
|
return JSON.stringify({ error: error.message || "Error during cleanup" });
|
|
3857
4492
|
}
|
|
3858
4493
|
}
|
|
4494
|
+
if (input.action === "defrag") {
|
|
4495
|
+
try {
|
|
4496
|
+
const result = await this.defragMemory({
|
|
4497
|
+
confirm: Boolean(input.confirm),
|
|
4498
|
+
similarity_threshold: input.similarity_threshold,
|
|
4499
|
+
min_island_size: input.min_island_size,
|
|
4500
|
+
});
|
|
4501
|
+
return JSON.stringify(result);
|
|
4502
|
+
}
|
|
4503
|
+
catch (error) {
|
|
4504
|
+
return JSON.stringify({ error: error.message || "Error during defragmentation" });
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
3859
4507
|
if (input.action === "reflect") {
|
|
3860
4508
|
try {
|
|
3861
4509
|
const result = await this.reflectMemory({
|
|
@@ -3897,6 +4545,34 @@ Supported actions:
|
|
|
3897
4545
|
return JSON.stringify({ error: error.message || "Error clearing memory" });
|
|
3898
4546
|
}
|
|
3899
4547
|
}
|
|
4548
|
+
if (input.action === "compact") {
|
|
4549
|
+
try {
|
|
4550
|
+
if (input.session_id) {
|
|
4551
|
+
return JSON.stringify(await this.compactSession({ session_id: input.session_id, model: input.model }));
|
|
4552
|
+
}
|
|
4553
|
+
else if (input.entity_id) {
|
|
4554
|
+
return JSON.stringify(await this.compactEntity({ entity_id: input.entity_id, model: input.model }));
|
|
4555
|
+
}
|
|
4556
|
+
else {
|
|
4557
|
+
// Compact all entities that exceed threshold
|
|
4558
|
+
const res = await this.db.run(`
|
|
4559
|
+
?[eid, count(oid)] := *observation{entity_id: eid, id: oid, @ "NOW"}
|
|
4560
|
+
`);
|
|
4561
|
+
const threshold = 20;
|
|
4562
|
+
const entitiesToCompact = res.rows
|
|
4563
|
+
.filter((r) => Number(r[1]) > threshold)
|
|
4564
|
+
.map((r) => String(r[0]));
|
|
4565
|
+
const results = [];
|
|
4566
|
+
for (const eid of entitiesToCompact) {
|
|
4567
|
+
results.push(await this.compactEntity({ entity_id: eid, model: input.model }));
|
|
4568
|
+
}
|
|
4569
|
+
return JSON.stringify({ status: "global_compaction_completed", entities_processed: results.length, results });
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
catch (error) {
|
|
4573
|
+
return JSON.stringify({ error: error.message || "Error during compaction" });
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
3900
4576
|
return JSON.stringify({ error: "Unknown action" });
|
|
3901
4577
|
},
|
|
3902
4578
|
});
|