cozo-memory 1.0.0
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/LICENSE +201 -0
- package/README.md +533 -0
- package/dist/api_bridge.js +266 -0
- package/dist/benchmark-gpu-cpu.js +188 -0
- package/dist/benchmark-heavy.js +230 -0
- package/dist/benchmark.js +160 -0
- package/dist/clear-cache.js +29 -0
- package/dist/db-service.js +228 -0
- package/dist/download-model.js +48 -0
- package/dist/embedding-service.js +249 -0
- package/dist/full-system-test.js +45 -0
- package/dist/hybrid-search.js +337 -0
- package/dist/index.js +3106 -0
- package/dist/inference-engine.js +348 -0
- package/dist/memory-service.js +215 -0
- package/dist/test-advanced-filters.js +64 -0
- package/dist/test-advanced-search.js +82 -0
- package/dist/test-advanced-time.js +47 -0
- package/dist/test-embedding.js +22 -0
- package/dist/test-filter-expr.js +84 -0
- package/dist/test-fts.js +58 -0
- package/dist/test-functions.js +25 -0
- package/dist/test-gpu-check.js +16 -0
- package/dist/test-graph-algs-final.js +76 -0
- package/dist/test-graph-filters.js +88 -0
- package/dist/test-graph-rag.js +124 -0
- package/dist/test-graph-walking.js +138 -0
- package/dist/test-index.js +35 -0
- package/dist/test-int-filter.js +48 -0
- package/dist/test-integration.js +69 -0
- package/dist/test-lower.js +35 -0
- package/dist/test-lsh.js +67 -0
- package/dist/test-mcp-tool.js +40 -0
- package/dist/test-pagerank.js +31 -0
- package/dist/test-semantic-walk.js +145 -0
- package/dist/test-time-filter.js +66 -0
- package/dist/test-time-functions.js +38 -0
- package/dist/test-triggers.js +60 -0
- package/dist/test-ts-ort.js +48 -0
- package/dist/test-validity-access.js +35 -0
- package/dist/test-validity-body.js +42 -0
- package/dist/test-validity-decomp.js +37 -0
- package/dist/test-validity-extraction.js +45 -0
- package/dist/test-validity-json.js +35 -0
- package/dist/test-validity.js +38 -0
- package/dist/types.js +3 -0
- package/dist/verify-gpu.js +30 -0
- package/dist/verify_transaction_tool.js +46 -0
- package/package.json +75 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MemoryServer = exports.USER_ENTITY_TYPE = exports.USER_ENTITY_NAME = exports.USER_ENTITY_ID = exports.DB_PATH = void 0;
|
|
7
|
+
const embedding_service_1 = require("./embedding-service");
|
|
8
|
+
const fastmcp_1 = require("fastmcp");
|
|
9
|
+
const cozo_node_1 = require("cozo-node");
|
|
10
|
+
const zod_1 = require("zod");
|
|
11
|
+
const uuid_1 = require("uuid");
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const hybrid_search_1 = require("./hybrid-search");
|
|
14
|
+
const inference_engine_1 = require("./inference-engine");
|
|
15
|
+
exports.DB_PATH = path_1.default.resolve(__dirname, "..", "memory_db.cozo");
|
|
16
|
+
const DB_ENGINE = process.env.DB_ENGINE || "sqlite"; // "sqlite" or "rocksdb"
|
|
17
|
+
const EMBEDDING_MODEL = "Xenova/bge-m3";
|
|
18
|
+
const EMBEDDING_DIM = 1024;
|
|
19
|
+
exports.USER_ENTITY_ID = "global_user_profile";
|
|
20
|
+
exports.USER_ENTITY_NAME = "The User";
|
|
21
|
+
exports.USER_ENTITY_TYPE = "User";
|
|
22
|
+
class MemoryServer {
|
|
23
|
+
db;
|
|
24
|
+
mcp;
|
|
25
|
+
embeddingService;
|
|
26
|
+
hybridSearch;
|
|
27
|
+
inferenceEngine;
|
|
28
|
+
initPromise;
|
|
29
|
+
constructor(dbPath = exports.DB_PATH) {
|
|
30
|
+
const fullDbPath = DB_ENGINE === "sqlite" ? dbPath + ".db" : dbPath;
|
|
31
|
+
this.db = new cozo_node_1.CozoDb(DB_ENGINE, fullDbPath);
|
|
32
|
+
console.error(`[DB] Using backend: ${DB_ENGINE}, path: ${fullDbPath}`);
|
|
33
|
+
this.embeddingService = new embedding_service_1.EmbeddingService();
|
|
34
|
+
this.hybridSearch = new hybrid_search_1.HybridSearch(this.db, this.embeddingService);
|
|
35
|
+
this.inferenceEngine = new inference_engine_1.InferenceEngine(this.db, this.embeddingService);
|
|
36
|
+
this.mcp = new fastmcp_1.FastMCP({
|
|
37
|
+
name: "cozo-memory-server",
|
|
38
|
+
version: "1.0.0",
|
|
39
|
+
});
|
|
40
|
+
this.initPromise = (async () => {
|
|
41
|
+
await this.setupSchema();
|
|
42
|
+
console.error("[Server] Schema setup fully completed.");
|
|
43
|
+
})();
|
|
44
|
+
this.registerTools();
|
|
45
|
+
}
|
|
46
|
+
async janitorCleanup(args) {
|
|
47
|
+
await this.initPromise;
|
|
48
|
+
const olderThanDays = Math.max(1, Math.floor(args.older_than_days ?? 30));
|
|
49
|
+
const maxObservations = Math.max(1, Math.floor(args.max_observations ?? 20));
|
|
50
|
+
const minEntityDegree = Math.max(0, Math.floor(args.min_entity_degree ?? 2));
|
|
51
|
+
const model = args.model ?? "demyagent-4b-i1:Q6_K";
|
|
52
|
+
const before = (Date.now() - olderThanDays * 24 * 60 * 60 * 1000) * 1000;
|
|
53
|
+
const fetchLimit = Math.max(maxObservations * 5, maxObservations);
|
|
54
|
+
const candidatesRes = await this.db.run(`
|
|
55
|
+
?[obs_id, entity_id, text, metadata, ts] :=
|
|
56
|
+
*observation{id: obs_id, entity_id, text, metadata, created_at, @ "NOW"},
|
|
57
|
+
ts = to_int(created_at),
|
|
58
|
+
ts < $before
|
|
59
|
+
:limit $limit
|
|
60
|
+
`, { before, limit: fetchLimit });
|
|
61
|
+
const candidates = candidatesRes.rows.map((r) => ({
|
|
62
|
+
obs_id: r[0],
|
|
63
|
+
entity_id: r[1],
|
|
64
|
+
text: r[2],
|
|
65
|
+
metadata: r[3],
|
|
66
|
+
ts: Number(r[4]),
|
|
67
|
+
}));
|
|
68
|
+
const degreeByEntity = new Map();
|
|
69
|
+
const filtered = [];
|
|
70
|
+
for (const c of candidates) {
|
|
71
|
+
let degree = degreeByEntity.get(c.entity_id);
|
|
72
|
+
if (degree === undefined) {
|
|
73
|
+
const [outRes, inRes] = await Promise.all([
|
|
74
|
+
this.db.run('?[to_id] := *relationship{from_id: $id, to_id, @ "NOW"}', { id: c.entity_id }),
|
|
75
|
+
this.db.run('?[from_id] := *relationship{from_id, to_id: $id, @ "NOW"}', { id: c.entity_id }),
|
|
76
|
+
]);
|
|
77
|
+
const computedDegree = outRes.rows.length + inRes.rows.length;
|
|
78
|
+
degreeByEntity.set(c.entity_id, computedDegree);
|
|
79
|
+
degree = computedDegree;
|
|
80
|
+
}
|
|
81
|
+
if ((degree ?? 0) < minEntityDegree)
|
|
82
|
+
filtered.push(c);
|
|
83
|
+
if (filtered.length >= maxObservations)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
filtered.sort((a, b) => a.ts - b.ts);
|
|
87
|
+
const picked = filtered.slice(0, maxObservations);
|
|
88
|
+
const byEntity = new Map();
|
|
89
|
+
for (const p of picked) {
|
|
90
|
+
const arr = byEntity.get(p.entity_id) ?? [];
|
|
91
|
+
arr.push({ obs_id: p.obs_id, text: p.text, metadata: p.metadata, ts: p.ts });
|
|
92
|
+
byEntity.set(p.entity_id, arr);
|
|
93
|
+
}
|
|
94
|
+
// Always perform cache cleanup, regardless of observation candidates
|
|
95
|
+
const cutoff = Math.floor((Date.now() - olderThanDays * 24 * 3600 * 1000) / 1000);
|
|
96
|
+
console.error(`[Janitor] Cleaning cache (older than ${new Date(cutoff * 1000).toISOString()}, ts=${cutoff})...`);
|
|
97
|
+
let cacheDeletedCount = 0;
|
|
98
|
+
try {
|
|
99
|
+
// First count what we want to delete
|
|
100
|
+
const toDeleteRes = await this.db.run(`?[query_hash] := *search_cache{query_hash, created_at}, created_at < $cutoff`, { cutoff });
|
|
101
|
+
const toDeleteHashes = toDeleteRes.rows.map((r) => [r[0]]);
|
|
102
|
+
if (toDeleteHashes.length > 0) {
|
|
103
|
+
console.error(`[Janitor] Deleting ${toDeleteHashes.length} cache entries...`);
|
|
104
|
+
// We use :delete with the hashes (as a list of lists)
|
|
105
|
+
const deleteRes = await this.db.run(`
|
|
106
|
+
?[query_hash] <- $hashes
|
|
107
|
+
:delete search_cache {query_hash}
|
|
108
|
+
`, { hashes: toDeleteHashes });
|
|
109
|
+
console.error(`[Janitor] :delete result:`, JSON.stringify(deleteRes));
|
|
110
|
+
cacheDeletedCount = toDeleteHashes.length;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.error(`[Janitor] No obsolete cache entries found.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
console.error(`[Janitor] Cache cleanup error:`, e.message);
|
|
118
|
+
}
|
|
119
|
+
if (picked.length === 0) {
|
|
120
|
+
return args.confirm
|
|
121
|
+
? {
|
|
122
|
+
status: "no_op",
|
|
123
|
+
criteria: { older_than_days: olderThanDays, max_observations: maxObservations, min_entity_degree: minEntityDegree },
|
|
124
|
+
cache_deleted: cacheDeletedCount
|
|
125
|
+
}
|
|
126
|
+
: {
|
|
127
|
+
status: "dry_run",
|
|
128
|
+
criteria: { older_than_days: olderThanDays, max_observations: maxObservations, min_entity_degree: minEntityDegree },
|
|
129
|
+
candidates: [],
|
|
130
|
+
cache_deleted: 0
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (!args.confirm) {
|
|
134
|
+
return {
|
|
135
|
+
status: "dry_run",
|
|
136
|
+
criteria: { older_than_days: olderThanDays, max_observations: maxObservations, min_entity_degree: minEntityDegree },
|
|
137
|
+
candidates: Array.from(byEntity.entries()).map(([entity_id, obs]) => ({
|
|
138
|
+
entity_id,
|
|
139
|
+
observation_ids: obs.map((o) => o.obs_id),
|
|
140
|
+
count: obs.length,
|
|
141
|
+
})),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const summaryEntity = await this.createEntity({
|
|
145
|
+
name: `Janitor Summary ${new Date().toISOString()}`,
|
|
146
|
+
type: "Summary",
|
|
147
|
+
metadata: {
|
|
148
|
+
model,
|
|
149
|
+
older_than_days: olderThanDays,
|
|
150
|
+
max_observations: maxObservations,
|
|
151
|
+
min_entity_degree: minEntityDegree,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const summary_entity_id = summaryEntity?.id;
|
|
155
|
+
if (!summary_entity_id)
|
|
156
|
+
return { error: "Could not create summary entity" };
|
|
157
|
+
const results = [];
|
|
158
|
+
for (const [entity_id, obs] of byEntity.entries()) {
|
|
159
|
+
const entityInfo = await this.db.run('?[name, type] := *entity{id: $id, name, type, @ "NOW"}', { id: entity_id });
|
|
160
|
+
const entityName = entityInfo.rows.length > 0 ? entityInfo.rows[0][0] : entity_id;
|
|
161
|
+
const entityType = entityInfo.rows.length > 0 ? entityInfo.rows[0][1] : "Unknown";
|
|
162
|
+
const levels = obs
|
|
163
|
+
.map((o) => {
|
|
164
|
+
const level = o.metadata?.janitor?.level;
|
|
165
|
+
const n = typeof level === "number" ? level : Number(level);
|
|
166
|
+
return Number.isFinite(n) ? n : null;
|
|
167
|
+
})
|
|
168
|
+
.filter((n) => typeof n === "number");
|
|
169
|
+
const nextLevel = (levels.length > 0 ? Math.max(...levels) : 0) + 1;
|
|
170
|
+
const minTs = obs.reduce((m, o) => Math.min(m, o.ts), Number.POSITIVE_INFINITY);
|
|
171
|
+
const maxTs = obs.reduce((m, o) => Math.max(m, o.ts), Number.NEGATIVE_INFINITY);
|
|
172
|
+
const systemPrompt = "Here are older fragments (or previous summaries) from your memory. Summarize them into a single, permanent Executive Summary. Respond only with the Executive Summary.";
|
|
173
|
+
const userPrompt = `Entity: ${entityName} (${entityType})\nLevel: ${nextLevel}\n\nFragments:\n` + obs.map((o) => `- ${o.text}`).join("\n");
|
|
174
|
+
let summaryText;
|
|
175
|
+
try {
|
|
176
|
+
const ollamaMod = await import("ollama");
|
|
177
|
+
const ollamaClient = ollamaMod?.default ?? ollamaMod;
|
|
178
|
+
const response = await ollamaClient.chat({
|
|
179
|
+
model,
|
|
180
|
+
messages: [
|
|
181
|
+
{ role: "system", content: systemPrompt },
|
|
182
|
+
{ role: "user", content: userPrompt },
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
summaryText = response?.message?.content?.trim?.() ?? "";
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
console.warn(`[Janitor] Ollama error for ${entityName}: ${e.message}. Using fallback concatenation.`);
|
|
189
|
+
summaryText = "Summary (Fallback): " + obs.map(o => o.text).join("; ");
|
|
190
|
+
}
|
|
191
|
+
if (!summaryText || summaryText.trim() === "" || summaryText.trim().toUpperCase() === "DELETE") {
|
|
192
|
+
summaryText = "Summary (Fallback): " + obs.map((o) => o.text).join("; ");
|
|
193
|
+
}
|
|
194
|
+
let executiveSummaryEntityId = null;
|
|
195
|
+
let executiveSummaryObservationId = null;
|
|
196
|
+
if (summaryText && summaryText.trim() !== "") {
|
|
197
|
+
const nowIso = new Date().toISOString();
|
|
198
|
+
const execEntity = await this.createEntity({
|
|
199
|
+
name: `${entityName} — Executive Summary L${nextLevel} (${nowIso.slice(0, 10)})`,
|
|
200
|
+
type: "ExecutiveSummary",
|
|
201
|
+
metadata: {
|
|
202
|
+
janitor: {
|
|
203
|
+
kind: "executive_summary",
|
|
204
|
+
level: nextLevel,
|
|
205
|
+
model,
|
|
206
|
+
summarized_at: nowIso,
|
|
207
|
+
source_entity_id: entity_id,
|
|
208
|
+
source_entity_name: entityName,
|
|
209
|
+
source_entity_type: entityType,
|
|
210
|
+
source_observation_ids: obs.map((o) => o.obs_id),
|
|
211
|
+
source_time_range: { min_ts: minTs, max_ts: maxTs },
|
|
212
|
+
older_than_days: olderThanDays,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
executiveSummaryEntityId = execEntity?.id ?? null;
|
|
217
|
+
if (executiveSummaryEntityId) {
|
|
218
|
+
await this.createRelation({
|
|
219
|
+
from_id: executiveSummaryEntityId,
|
|
220
|
+
to_id: entity_id,
|
|
221
|
+
relation_type: "summary_of",
|
|
222
|
+
strength: 1,
|
|
223
|
+
metadata: {
|
|
224
|
+
janitor: {
|
|
225
|
+
kind: "executive_summary",
|
|
226
|
+
level: nextLevel,
|
|
227
|
+
model,
|
|
228
|
+
summarized_at: nowIso,
|
|
229
|
+
source_observation_ids: obs.map((o) => o.obs_id),
|
|
230
|
+
source_time_range: { min_ts: minTs, max_ts: maxTs },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
const added = await this.addObservation({
|
|
235
|
+
entity_id: executiveSummaryEntityId,
|
|
236
|
+
text: summaryText,
|
|
237
|
+
metadata: {
|
|
238
|
+
janitor: {
|
|
239
|
+
kind: "executive_summary",
|
|
240
|
+
level: nextLevel,
|
|
241
|
+
source_entity_id: entity_id,
|
|
242
|
+
source_observation_ids: obs.map((o) => o.obs_id),
|
|
243
|
+
model,
|
|
244
|
+
summarized_at: nowIso,
|
|
245
|
+
source_time_range: { min_ts: minTs, max_ts: maxTs },
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
executiveSummaryObservationId = added?.id ?? null;
|
|
250
|
+
await this.createRelation({
|
|
251
|
+
from_id: summary_entity_id,
|
|
252
|
+
to_id: executiveSummaryEntityId,
|
|
253
|
+
relation_type: "generated",
|
|
254
|
+
strength: 1,
|
|
255
|
+
metadata: { source_entity_id: entity_id },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const o of obs) {
|
|
260
|
+
await this.db.run(`{ ?[id, created_at] := *observation{id, created_at}, id = $id :rm observation {id, created_at} }`, { id: o.obs_id });
|
|
261
|
+
}
|
|
262
|
+
await this.createRelation({
|
|
263
|
+
from_id: summary_entity_id,
|
|
264
|
+
to_id: entity_id,
|
|
265
|
+
relation_type: "summarizes",
|
|
266
|
+
strength: 1,
|
|
267
|
+
metadata: {
|
|
268
|
+
deleted_observation_ids: obs.map((o) => o.obs_id),
|
|
269
|
+
executive_summary_entity_id: executiveSummaryEntityId,
|
|
270
|
+
executive_summary_observation_id: executiveSummaryObservationId,
|
|
271
|
+
level: nextLevel,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
results.push({
|
|
275
|
+
entity_id,
|
|
276
|
+
entity_name: entityName,
|
|
277
|
+
status: executiveSummaryEntityId ? "consolidated" : "deleted_only",
|
|
278
|
+
deleted_observation_ids: obs.map((o) => o.obs_id),
|
|
279
|
+
executive_summary_entity_id: executiveSummaryEntityId,
|
|
280
|
+
executive_summary_observation_id: executiveSummaryObservationId,
|
|
281
|
+
level: nextLevel,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
status: "completed",
|
|
286
|
+
summary_entity_id,
|
|
287
|
+
processed_entities: results.length,
|
|
288
|
+
deleted_observations: picked.length,
|
|
289
|
+
cache_deleted: cacheDeletedCount,
|
|
290
|
+
results,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async advancedSearch(args) {
|
|
294
|
+
await this.initPromise;
|
|
295
|
+
return this.hybridSearch.advancedSearch(args);
|
|
296
|
+
}
|
|
297
|
+
async recomputeCommunities() {
|
|
298
|
+
await this.initPromise;
|
|
299
|
+
// Check if there are any edges before running LabelPropagation
|
|
300
|
+
// LabelPropagation in CozoDB currently panics on empty input relations
|
|
301
|
+
const edgeCheckRes = await this.db.run(`?[from_id] := *relationship{from_id, @ "NOW"} :limit 1`);
|
|
302
|
+
const hasEdges = edgeCheckRes.rows.length > 0;
|
|
303
|
+
if (!hasEdges) {
|
|
304
|
+
console.error("[Communities] No relationships found, skipping LabelPropagation.");
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
const query = `
|
|
308
|
+
edges[f, t, s] := *relationship{from_id: f, to_id: t, strength: s, @ "NOW"}
|
|
309
|
+
temp_communities[community_id, entity_id] <~ LabelPropagation(edges[f, t, s])
|
|
310
|
+
?[entity_id, community_id] := temp_communities[community_id, entity_id]
|
|
311
|
+
`;
|
|
312
|
+
const result = await this.db.run(query);
|
|
313
|
+
for (const row of result.rows) {
|
|
314
|
+
const entity_id = String(row[0]);
|
|
315
|
+
const community_id = String(row[1]);
|
|
316
|
+
await this.db.run(`?[entity_id, community_id] <- [[$entity_id, $community_id]]
|
|
317
|
+
:put entity_community {entity_id => community_id}`, { entity_id, community_id });
|
|
318
|
+
}
|
|
319
|
+
return result.rows.map((r) => ({ entity_id: String(r[0]), community_id: String(r[1]) }));
|
|
320
|
+
}
|
|
321
|
+
async recomputeBetweennessCentrality() {
|
|
322
|
+
await this.initPromise;
|
|
323
|
+
const edgeCheckRes = await this.db.run(`?[from_id] := *relationship{from_id, @ "NOW"} :limit 1`);
|
|
324
|
+
if (edgeCheckRes.rows.length === 0) {
|
|
325
|
+
console.error("[Betweenness] No relationships found, skipping Betweenness Centrality.");
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
const query = `
|
|
329
|
+
edges[f, t] := *relationship{from_id: f, to_id: t, @ "NOW"}
|
|
330
|
+
temp_betweenness[entity_id, centrality] <~ BetweennessCentrality(edges[f, t])
|
|
331
|
+
?[entity_id, centrality] := temp_betweenness[entity_id, centrality]
|
|
332
|
+
`.trim();
|
|
333
|
+
try {
|
|
334
|
+
const result = await this.db.run(query);
|
|
335
|
+
console.error(`[Betweenness] ${result.rows.length} entities calculated.`);
|
|
336
|
+
return result.rows.map((r) => ({ entity_id: String(r[0]), centrality: Number(r[1]) }));
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
console.error("[Betweenness] Error during calculation:", e.message);
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async recomputeConnectedComponents() {
|
|
344
|
+
await this.initPromise;
|
|
345
|
+
const edgeCheckRes = await this.db.run(`?[from_id] := *relationship{from_id, @ "NOW"} :limit 1`);
|
|
346
|
+
if (edgeCheckRes.rows.length === 0) {
|
|
347
|
+
console.error("[ConnectedComponents] No relationships found.");
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
const query = `
|
|
351
|
+
edges[f, t] := *relationship{from_id: f, to_id: t, @ "NOW"}
|
|
352
|
+
temp_components[entity_id, component_id] <~ ConnectedComponents(edges[f, t])
|
|
353
|
+
?[entity_id, component_id] := temp_components[entity_id, component_id]
|
|
354
|
+
`.trim();
|
|
355
|
+
try {
|
|
356
|
+
const result = await this.db.run(query);
|
|
357
|
+
return result.rows.map((r) => ({ entity_id: String(r[0]), component_id: String(r[1]) }));
|
|
358
|
+
}
|
|
359
|
+
catch (e) {
|
|
360
|
+
console.error("[ConnectedComponents] Calculation error:", e.message);
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async computeShortestPath(args) {
|
|
365
|
+
await this.initPromise;
|
|
366
|
+
const query = `
|
|
367
|
+
edges[f, t, s] := *relationship{from_id: f, to_id: t, strength: s, @ "NOW"}
|
|
368
|
+
start_n[ns] <- [[$start]]
|
|
369
|
+
end_n[ng] <- [[$end]]
|
|
370
|
+
?[s_node, g_node, dist, path] <~ ShortestPathDijkstra(edges[f, t, s], start_n[ns], end_n[ng])
|
|
371
|
+
`;
|
|
372
|
+
try {
|
|
373
|
+
const result = await this.db.run(query.replace(/^\s+/gm, '').trim(), { start: args.start_entity, end: args.end_entity });
|
|
374
|
+
if (result.rows.length === 0)
|
|
375
|
+
return null;
|
|
376
|
+
return {
|
|
377
|
+
start: result.rows[0][0],
|
|
378
|
+
goal: result.rows[0][1],
|
|
379
|
+
distance: result.rows[0][2],
|
|
380
|
+
path: result.rows[0][3]
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
console.error("[ShortestPath] Calculation error:", e.message);
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async recomputeHITS() {
|
|
389
|
+
await this.initPromise;
|
|
390
|
+
const edgeCheckRes = await this.db.run(`?[from_id] := *relationship{from_id, @ "NOW"} :limit 1`);
|
|
391
|
+
if (edgeCheckRes.rows.length === 0)
|
|
392
|
+
return [];
|
|
393
|
+
const query = `
|
|
394
|
+
edges[f, t] := *relationship{from_id: f, to_id: t, @ "NOW"}
|
|
395
|
+
nodes[n] := edges[n, _]
|
|
396
|
+
nodes[n] := edges[_, n]
|
|
397
|
+
initial_auth[n, v] := nodes[n], v = 1.0
|
|
398
|
+
initial_hub[n, v] := nodes[n], v = 1.0
|
|
399
|
+
auth1[v, sum(h)] := edges[u, v], initial_hub[u, h]
|
|
400
|
+
hub1[u, sum(a)] := edges[u, v], auth1[v, a]
|
|
401
|
+
?[entity_id, auth_score, hub_score] := auth1[entity_id, auth_score], hub1[entity_id, hub_score]
|
|
402
|
+
`;
|
|
403
|
+
try {
|
|
404
|
+
const result = await this.db.run(query.replace(/^\s+/gm, '').trim());
|
|
405
|
+
return result.rows.map((r) => ({ entity_id: String(r[0]), authority: Number(r[1]), hub: Number(r[2]) }));
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
console.error("[HITS] Calculation error:", e.message);
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async recomputePageRank() {
|
|
413
|
+
await this.initPromise;
|
|
414
|
+
const edgeCheckRes = await this.db.run(`?[from_id] := *relationship{from_id, @ "NOW"} :limit 1`);
|
|
415
|
+
if (edgeCheckRes.rows.length === 0) {
|
|
416
|
+
console.error("[PageRank] No relationships found, skipping PageRank.");
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
const query = `
|
|
420
|
+
edges[f, t, s] := *relationship{from_id: f, to_id: t, strength: s, @ "NOW"}
|
|
421
|
+
temp_rank[entity_id, rank] <~ PageRank(edges[f, t, s])
|
|
422
|
+
?[entity_id, rank] := temp_rank[entity_id, rank]
|
|
423
|
+
`.trim();
|
|
424
|
+
try {
|
|
425
|
+
const result = await this.db.run(query);
|
|
426
|
+
// Save results
|
|
427
|
+
for (const row of result.rows) {
|
|
428
|
+
const entity_id = String(row[0]);
|
|
429
|
+
const pagerank = Float64Array.from([row[1]])[0]; // Ensure it is a float
|
|
430
|
+
await this.db.run(`?[entity_id, pagerank] <- [[$entity_id, $pagerank]]
|
|
431
|
+
:put entity_rank {entity_id => pagerank}`, { entity_id, pagerank });
|
|
432
|
+
}
|
|
433
|
+
console.error(`[PageRank] ${result.rows.length} entities ranked.`);
|
|
434
|
+
return result.rows.map((r) => ({ entity_id: String(r[0]), pagerank: Number(r[1]) }));
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
console.error("[PageRank] Calculation error:", e.message);
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async setupSchema() {
|
|
442
|
+
try {
|
|
443
|
+
console.error("[Schema] Initializing schema...");
|
|
444
|
+
const existingRelations = await this.db.run("::relations");
|
|
445
|
+
const relations = existingRelations.rows.map((r) => r[0]);
|
|
446
|
+
// Entity Table
|
|
447
|
+
if (!relations.includes("entity")) {
|
|
448
|
+
try {
|
|
449
|
+
await this.db.run(`{:create entity {id: String, created_at: Validity => name: String, type: String, embedding: <F32; ${EMBEDDING_DIM}>, name_embedding: <F32; ${EMBEDDING_DIM}>, metadata: Json}}`);
|
|
450
|
+
console.error("[Schema] Entity table created.");
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
console.error("[Schema] Entity table error:", e.message);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
const timeTravelReady = await this.isTimeTravelReady("entity");
|
|
458
|
+
// Check if name_embedding exists (v1.7 Multi-Vector Support)
|
|
459
|
+
const columnsRes = await this.db.run(`::columns entity`);
|
|
460
|
+
const columns = columnsRes.rows.map((r) => r[0]);
|
|
461
|
+
if (!columns.includes("name_embedding") || !timeTravelReady) {
|
|
462
|
+
// Drop indices before migration
|
|
463
|
+
try {
|
|
464
|
+
await this.db.run("::hnsw drop entity:semantic");
|
|
465
|
+
}
|
|
466
|
+
catch (e) { }
|
|
467
|
+
try {
|
|
468
|
+
await this.db.run("::hnsw drop entity:name_semantic");
|
|
469
|
+
}
|
|
470
|
+
catch (e) { }
|
|
471
|
+
try {
|
|
472
|
+
await this.db.run("::fts drop entity:fts");
|
|
473
|
+
}
|
|
474
|
+
catch (e) { }
|
|
475
|
+
const typesToDrop = ['person', 'project', 'task', 'note'];
|
|
476
|
+
for (const type of typesToDrop) {
|
|
477
|
+
try {
|
|
478
|
+
await this.db.run(`::hnsw drop entity:semantic_${type}`);
|
|
479
|
+
}
|
|
480
|
+
catch (e) { }
|
|
481
|
+
}
|
|
482
|
+
if (!columns.includes("name_embedding")) {
|
|
483
|
+
await this.db.run(`
|
|
484
|
+
?[id, created_at, name, type, embedding, name_embedding, metadata] :=
|
|
485
|
+
*entity{id, name, type, embedding, metadata, created_at: created_at_raw},
|
|
486
|
+
created_at = [created_at_raw, true],
|
|
487
|
+
name_embedding = embedding
|
|
488
|
+
:replace entity {id: String, created_at: Validity => name: String, type: String, embedding: <F32; ${EMBEDDING_DIM}>, name_embedding: <F32; ${EMBEDDING_DIM}>, metadata: Json}
|
|
489
|
+
`);
|
|
490
|
+
console.error("[Schema] Entity Tabelle migriert (Multi-Vector Support).");
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
await this.db.run(`
|
|
494
|
+
?[id, created_at, name, type, embedding, name_embedding, metadata] :=
|
|
495
|
+
*entity{id, name, type, embedding, name_embedding, metadata, created_at: created_at_raw},
|
|
496
|
+
created_at = [created_at_raw, true]
|
|
497
|
+
:replace entity {id: String, created_at: Validity => name: String, type: String, embedding: <F32; ${EMBEDDING_DIM}>, name_embedding: <F32; ${EMBEDDING_DIM}>, metadata: Json}
|
|
498
|
+
`);
|
|
499
|
+
console.error("[Schema] Entity table migrated (Validity).");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
await this.db.run(`{::hnsw create entity:semantic {dim: ${EMBEDDING_DIM}, m: 16, dtype: F32, fields: [embedding], distance: Cosine, ef_construction: 200}}`);
|
|
505
|
+
console.error("[Schema] Entity HNSW index created.");
|
|
506
|
+
}
|
|
507
|
+
catch (e) {
|
|
508
|
+
// We mostly ignore index errors, as ::hnsw create has no simple check
|
|
509
|
+
if (!e.message.includes("already exists") && !e.message.includes("unexpected input")) {
|
|
510
|
+
console.error("[Schema] Entity index notice:", e.message);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
await this.db.run(`{::hnsw create entity:name_semantic {dim: ${EMBEDDING_DIM}, m: 16, dtype: F32, fields: [name_embedding], distance: Cosine, ef_construction: 200}}`);
|
|
515
|
+
console.error("[Schema] Entity Name-HNSW index created.");
|
|
516
|
+
}
|
|
517
|
+
catch (e) {
|
|
518
|
+
if (!e.message.includes("already exists") && !e.message.includes("unexpected input")) {
|
|
519
|
+
console.error("[Schema] Entity name-index notice:", e.message);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// Filtered HNSW indices for common types (v1.7)
|
|
523
|
+
const commonTypes = ['Person', 'Project', 'Task', 'Note'];
|
|
524
|
+
for (const type of commonTypes) {
|
|
525
|
+
try {
|
|
526
|
+
await this.db.run(`{::hnsw create entity:semantic_${type.toLowerCase()} {dim: ${EMBEDDING_DIM}, m: 16, dtype: F32, fields: [embedding], distance: Cosine, ef_construction: 200, filter: type == '${type}'}}`);
|
|
527
|
+
console.error(`[Schema] Entity HNSW index for ${type} created.`);
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
if (!e.message.includes("already exists") && !e.message.includes("unexpected input")) {
|
|
531
|
+
console.error(`[Schema] Entity index (${type}) notice:`, e.message);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Observation Table
|
|
536
|
+
if (!relations.includes("observation")) {
|
|
537
|
+
try {
|
|
538
|
+
await this.db.run(`{:create observation {id: String, created_at: Validity => entity_id: String, text: String, embedding: <F32; ${EMBEDDING_DIM}>, metadata: Json}}`);
|
|
539
|
+
console.error("[Schema] Observation table created.");
|
|
540
|
+
}
|
|
541
|
+
catch (e) {
|
|
542
|
+
console.error("[Schema] Observation table error:", e.message);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
const timeTravelReady = await this.isTimeTravelReady("observation");
|
|
547
|
+
if (!timeTravelReady) {
|
|
548
|
+
// Drop indices before migration
|
|
549
|
+
try {
|
|
550
|
+
await this.db.run("::hnsw drop observation:semantic");
|
|
551
|
+
}
|
|
552
|
+
catch (e) { }
|
|
553
|
+
try {
|
|
554
|
+
await this.db.run("::fts drop observation:fts");
|
|
555
|
+
}
|
|
556
|
+
catch (e) { }
|
|
557
|
+
try {
|
|
558
|
+
await this.db.run("::lsh drop observation:lsh");
|
|
559
|
+
}
|
|
560
|
+
catch (e) { }
|
|
561
|
+
await this.db.run(`
|
|
562
|
+
?[id, created_at, entity_id, text, embedding, metadata] :=
|
|
563
|
+
*observation{id, entity_id, text, embedding, metadata, created_at: created_at_raw},
|
|
564
|
+
created_at = [created_at_raw, true]
|
|
565
|
+
:replace observation {id: String, created_at: Validity => entity_id: String, text: String, embedding: <F32; ${EMBEDDING_DIM}>, metadata: Json}
|
|
566
|
+
`);
|
|
567
|
+
console.error("[Schema] Observation table migrated (Validity).");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
await this.db.run(`{::hnsw create observation:semantic {dim: ${EMBEDDING_DIM}, m: 16, dtype: F32, fields: [embedding], distance: Cosine, ef_construction: 200}}`);
|
|
572
|
+
console.error("[Schema] Observation HNSW index created.");
|
|
573
|
+
}
|
|
574
|
+
catch (e) {
|
|
575
|
+
if (!e.message.includes("already exists") && !e.message.includes("unexpected input")) {
|
|
576
|
+
console.error("[Schema] Observation index notice:", e.message);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// FTS Indices (v0.7 Feature)
|
|
580
|
+
try {
|
|
581
|
+
await this.db.run(`
|
|
582
|
+
::fts create entity:fts {
|
|
583
|
+
extractor: name,
|
|
584
|
+
tokenizer: Simple,
|
|
585
|
+
filters: [Lowercase, Stemmer('english'), Stopwords('en')]
|
|
586
|
+
}
|
|
587
|
+
`);
|
|
588
|
+
console.error("[Schema] Entity FTS index created.");
|
|
589
|
+
}
|
|
590
|
+
catch (e) {
|
|
591
|
+
if (!e.message.includes("already exists")) {
|
|
592
|
+
console.error("[Schema] Entity FTS error:", e.message);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
await this.db.run(`
|
|
597
|
+
::fts create observation:fts {
|
|
598
|
+
extractor: text,
|
|
599
|
+
tokenizer: Simple,
|
|
600
|
+
filters: [Lowercase, Stemmer('english'), Stopwords('en')]
|
|
601
|
+
}
|
|
602
|
+
`);
|
|
603
|
+
console.error("[Schema] Observation FTS index created.");
|
|
604
|
+
}
|
|
605
|
+
catch (e) {
|
|
606
|
+
if (!e.message.includes("already exists")) {
|
|
607
|
+
console.error("[Schema] Observation FTS error:", e.message);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// LSH Index (v0.7 Feature)
|
|
611
|
+
try {
|
|
612
|
+
await this.db.run(`
|
|
613
|
+
::lsh create observation:lsh {
|
|
614
|
+
extractor: text,
|
|
615
|
+
tokenizer: Simple,
|
|
616
|
+
n_gram: 3,
|
|
617
|
+
n_perm: 200,
|
|
618
|
+
target_threshold: 0.5
|
|
619
|
+
}
|
|
620
|
+
`);
|
|
621
|
+
console.error("[Schema] Observation LSH index created.");
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
if (!e.message.includes("already exists")) {
|
|
625
|
+
console.error("[Schema] Observation LSH error:", e.message);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Semantic Cache Table (v0.8+)
|
|
629
|
+
if (!relations.includes("search_cache")) {
|
|
630
|
+
try {
|
|
631
|
+
await this.db.run(`{:create search_cache {query_hash: String => query_text: String, results: Json, options: Json, embedding: <F32; ${EMBEDDING_DIM}>, created_at: Int}}`);
|
|
632
|
+
console.error("[Schema] Search Cache table created.");
|
|
633
|
+
await this.db.run(`{::hnsw create search_cache:semantic {dim: ${EMBEDDING_DIM}, m: 16, dtype: F32, fields: [embedding], distance: Cosine, ef_construction: 200}}`);
|
|
634
|
+
console.error("[Schema] Search Cache HNSW index created.");
|
|
635
|
+
}
|
|
636
|
+
catch (e) {
|
|
637
|
+
console.error("[Schema] Search Cache setup error:", e.message);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Relationship Table
|
|
641
|
+
if (!relations.includes("relationship")) {
|
|
642
|
+
try {
|
|
643
|
+
await this.db.run('{:create relationship {from_id: String, to_id: String, relation_type: String, created_at: Validity => strength: Float, metadata: Json}}');
|
|
644
|
+
console.error("[Schema] Relationship table created.");
|
|
645
|
+
}
|
|
646
|
+
catch (e) {
|
|
647
|
+
console.error("[Schema] Relationship table error:", e.message);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
const timeTravelReady = await this.isTimeTravelReady("relationship");
|
|
652
|
+
if (!timeTravelReady) {
|
|
653
|
+
// No indices to drop for relationship usually, but let's check
|
|
654
|
+
await this.db.run(`
|
|
655
|
+
?[from_id, to_id, relation_type, created_at, strength, metadata] :=
|
|
656
|
+
*relationship{from_id, to_id, relation_type, strength, metadata, created_at: created_at_raw},
|
|
657
|
+
created_at = [created_at_raw, true]
|
|
658
|
+
:replace relationship {from_id: String, to_id: String, relation_type: String, created_at: Validity => strength: Float, metadata: Json}
|
|
659
|
+
`);
|
|
660
|
+
console.error("[Schema] Relationship table migrated (Validity).");
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Entity Community Table
|
|
664
|
+
if (!relations.includes("entity_community")) {
|
|
665
|
+
try {
|
|
666
|
+
await this.db.run('{:create entity_community {entity_id: String => community_id: String}}');
|
|
667
|
+
console.error("[Schema] Entity Community table created.");
|
|
668
|
+
}
|
|
669
|
+
catch (e) {
|
|
670
|
+
console.error("[Schema] Entity Community table error:", e.message);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
try {
|
|
675
|
+
await this.db.run(`
|
|
676
|
+
?[entity_id, community_id] :=
|
|
677
|
+
*entity_community{entity_id, community_id}
|
|
678
|
+
:replace entity_community {entity_id: String => community_id: String}
|
|
679
|
+
`);
|
|
680
|
+
console.error("[Schema] Entity Community table migrated (Key-Value).");
|
|
681
|
+
}
|
|
682
|
+
catch (e) {
|
|
683
|
+
console.error("[Schema] Entity Community migration notice:", e.message);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Entity Rank Table (PageRank Scores)
|
|
687
|
+
if (!relations.includes("entity_rank")) {
|
|
688
|
+
try {
|
|
689
|
+
await this.db.run('{:create entity_rank {entity_id: String => pagerank: Float}}');
|
|
690
|
+
console.error("[Schema] Entity Rank table created.");
|
|
691
|
+
}
|
|
692
|
+
catch (e) {
|
|
693
|
+
console.error("[Schema] Entity Rank table error:", e.message);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Memory Snapshot Table
|
|
697
|
+
if (!relations.includes("memory_snapshot")) {
|
|
698
|
+
try {
|
|
699
|
+
await this.db.run('{:create memory_snapshot {snapshot_id => entity_count: Int, observation_count: Int, relation_count: Int, metadata: Json, created_at: Int}}');
|
|
700
|
+
console.error("[Schema] Snapshot table created.");
|
|
701
|
+
}
|
|
702
|
+
catch (e) {
|
|
703
|
+
console.error("[Schema] Snapshot table error:", e.message);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (!relations.includes("inference_rule")) {
|
|
707
|
+
try {
|
|
708
|
+
await this.db.run('{:create inference_rule {id: String => name: String, datalog: String, created_at: Int}}');
|
|
709
|
+
console.error("[Schema] Inference Rule table created.");
|
|
710
|
+
}
|
|
711
|
+
catch (e) {
|
|
712
|
+
console.error("[Schema] Inference Rule table error:", e.message);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
// Migration: Check if created_at exists
|
|
717
|
+
try {
|
|
718
|
+
const cols = await this.db.run('::columns inference_rule');
|
|
719
|
+
const hasCreatedAt = cols.rows.some((r) => r[0] === 'created_at');
|
|
720
|
+
if (!hasCreatedAt) {
|
|
721
|
+
console.error("[Schema] Migration: Adding created_at to inference_rule...");
|
|
722
|
+
await this.db.run(`
|
|
723
|
+
?[id, name, datalog, created_at] := *inference_rule{id, name, datalog}, created_at = 0
|
|
724
|
+
:replace inference_rule {id: String => name: String, datalog: String, created_at: Int}
|
|
725
|
+
`);
|
|
726
|
+
console.error("[Schema] Migration: inference_rule successfully updated.");
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
catch (e) {
|
|
730
|
+
console.error("[Schema] Inference Rule migration error:", e.message);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// Triggers for Data Integrity (v0.5+)
|
|
734
|
+
// Triggers disabled for now due to syntax issues with current CozoDB version
|
|
735
|
+
/*
|
|
736
|
+
try {
|
|
737
|
+
await this.db.run(`
|
|
738
|
+
::set_triggers relationship on put {
|
|
739
|
+
?[from_id] := _new{from_id, to_id}, from_id == to_id :assert empty
|
|
740
|
+
}
|
|
741
|
+
`);
|
|
742
|
+
console.error("[Schema] Trigger 'check_no_self_loops' created.");
|
|
743
|
+
} catch (e: any) {
|
|
744
|
+
// Fallback for environment where ::set_triggers might have slightly different behavior
|
|
745
|
+
try {
|
|
746
|
+
await this.db.run(`
|
|
747
|
+
::set_triggers relationship {
|
|
748
|
+
"check_no_self_loops": {
|
|
749
|
+
"on": "put",
|
|
750
|
+
"query": "?[from_id] := _new{from_id, to_id}, from_id == to_id :assert empty"
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
`);
|
|
754
|
+
console.error("[Schema] Trigger 'check_no_self_loops' created (Fallback).");
|
|
755
|
+
} catch (e2: any) {
|
|
756
|
+
console.error("[Schema] Relationship Trigger error:", e.message);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
*/
|
|
760
|
+
// Triggers disabled for now due to syntax issues with current CozoDB version
|
|
761
|
+
/*
|
|
762
|
+
try {
|
|
763
|
+
// This trigger prevents an entity from being marked as 'active' and 'discontinued' at the same time
|
|
764
|
+
// if this information is explicitly in the metadata.
|
|
765
|
+
await this.db.run(`
|
|
766
|
+
::set_triggers entity on put {
|
|
767
|
+
?[id] := _new{id, metadata},
|
|
768
|
+
(get(metadata, 'status') == 'aktiv' || get(metadata, 'status') == 'active'),
|
|
769
|
+
(get(metadata, 'archived') == true || get(metadata, 'status') == 'eingestellt')
|
|
770
|
+
:assert empty
|
|
771
|
+
}
|
|
772
|
+
`);
|
|
773
|
+
console.error("[Schema] Trigger 'check_metadata_conflict' created.");
|
|
774
|
+
} catch (e: any) {
|
|
775
|
+
try {
|
|
776
|
+
await this.db.run(`
|
|
777
|
+
::set_triggers entity {
|
|
778
|
+
"check_metadata_conflict": {
|
|
779
|
+
"on": "put",
|
|
780
|
+
"query": "?[id] := _new{id, metadata}, (get(metadata, 'status') == 'aktiv' || get(metadata, 'status') == 'active'), (get(metadata, 'archived') == true || get(metadata, 'status') == 'eingestellt') :assert empty"
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
`);
|
|
784
|
+
console.error("[Schema] Trigger 'check_metadata_conflict' created (Fallback).");
|
|
785
|
+
} catch (e2: any) {
|
|
786
|
+
console.error("[Schema] Entity Metadata Trigger error:", e.message);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
*/
|
|
790
|
+
// User Profile Initialization
|
|
791
|
+
await this.initUserProfile();
|
|
792
|
+
console.error("CozoDB Schema Setup completed.");
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
console.error("Unexpected error during schema setup:", error);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async isTimeTravelReady(relationName) {
|
|
799
|
+
try {
|
|
800
|
+
const keyField = relationName === "relationship" ? "from_id" : "id";
|
|
801
|
+
await this.db.run(`?[k] := *${relationName}{${keyField}: k, @ "NOW"} :limit 1`);
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
async graph_walking(args) {
|
|
809
|
+
try {
|
|
810
|
+
const queryEmbedding = await this.embeddingService.embed(args.query);
|
|
811
|
+
const limit = args.limit || 5;
|
|
812
|
+
const maxDepth = args.max_depth || 3;
|
|
813
|
+
let seedQuery;
|
|
814
|
+
let params = {
|
|
815
|
+
query_vector: queryEmbedding,
|
|
816
|
+
limit: limit,
|
|
817
|
+
max_depth: maxDepth,
|
|
818
|
+
topk: 100, // Increased for graph walking
|
|
819
|
+
ef_search: 100
|
|
820
|
+
};
|
|
821
|
+
if (args.start_entity_id) {
|
|
822
|
+
params.start_id = args.start_entity_id;
|
|
823
|
+
seedQuery = `seeds[id, score] := id = $start_id, score = 1.0`;
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
seedQuery = `seeds[id, score] := ~entity:semantic{id | query: vec($query_vector), k: $topk, ef: $ef_search, bind_distance: dist}, score = 1.0 - dist`;
|
|
827
|
+
}
|
|
828
|
+
const datalog = `
|
|
829
|
+
rank_val[id, r] := *entity_rank{entity_id: id, pagerank: r}
|
|
830
|
+
rank_val[id, r] := *entity{id, @ "NOW"}, not *entity_rank{entity_id: id}, r = 0.0
|
|
831
|
+
|
|
832
|
+
${seedQuery}
|
|
833
|
+
|
|
834
|
+
path[start_id, current_id, d, path_score] := seeds[start_id, s], current_id = start_id, d = 0, path_score = s
|
|
835
|
+
|
|
836
|
+
path[start_id, next_id, d_new, path_score_new] :=
|
|
837
|
+
path[start_id, current_id, d, path_score],
|
|
838
|
+
*relationship{from_id: current_id, to_id: next_id, @ "NOW"},
|
|
839
|
+
d < $max_depth,
|
|
840
|
+
d_new = d + 1,
|
|
841
|
+
~entity:semantic{id: next_id | query: vec($query_vector), k: $topk, ef: $ef_search, bind_distance: dist},
|
|
842
|
+
sim = 1.0 - dist,
|
|
843
|
+
sim > 0.5,
|
|
844
|
+
path_score_new = path_score * sim * (1.0 - 0.1 * d_new)
|
|
845
|
+
|
|
846
|
+
path[start_id, next_id, d_new, path_score_new] :=
|
|
847
|
+
path[start_id, current_id, d, path_score],
|
|
848
|
+
*relationship{to_id: current_id, from_id: next_id, @ "NOW"},
|
|
849
|
+
d < $max_depth,
|
|
850
|
+
d_new = d + 1,
|
|
851
|
+
~entity:semantic{id: next_id | query: vec($query_vector), k: $topk, ef: $ef_search, bind_distance: dist},
|
|
852
|
+
sim = 1.0 - dist,
|
|
853
|
+
sim > 0.5,
|
|
854
|
+
path_score_new = path_score * sim * (1.0 - 0.1 * d_new)
|
|
855
|
+
|
|
856
|
+
result_entities[id, max_score] := path[_, id, _, s], max_score = max(s)
|
|
857
|
+
|
|
858
|
+
?[id, name, type, score, pr] :=
|
|
859
|
+
result_entities[id, s],
|
|
860
|
+
*entity{id, name, type, @ "NOW"},
|
|
861
|
+
rank_val[id, pr],
|
|
862
|
+
score = s * (1.0 + pr)
|
|
863
|
+
|
|
864
|
+
?[id, name, type, score, pr] :=
|
|
865
|
+
result_entities[id, s],
|
|
866
|
+
*entity{id, name, type, @ "NOW"},
|
|
867
|
+
not rank_val[id, _],
|
|
868
|
+
pr = 0.0,
|
|
869
|
+
score = s
|
|
870
|
+
|
|
871
|
+
:sort -score
|
|
872
|
+
:limit $limit
|
|
873
|
+
`;
|
|
874
|
+
const res = await this.db.run(datalog, params);
|
|
875
|
+
return res.rows.map((r) => ({
|
|
876
|
+
id: r[0],
|
|
877
|
+
name: r[1],
|
|
878
|
+
type: r[2],
|
|
879
|
+
score: r[3],
|
|
880
|
+
pagerank: r[4]
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
catch (error) {
|
|
884
|
+
console.error("Error in graph_walking:", error);
|
|
885
|
+
return { error: error.message };
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
resolveValiditySpec(as_of) {
|
|
889
|
+
if (!as_of || as_of === "NOW")
|
|
890
|
+
return '"NOW"';
|
|
891
|
+
// Check if it's a numeric timestamp
|
|
892
|
+
if (/^\d+$/.test(as_of)) {
|
|
893
|
+
let ts = parseInt(as_of, 10);
|
|
894
|
+
// If it looks like microseconds (16 digits), convert to millis for Date
|
|
895
|
+
if (as_of.length >= 15) {
|
|
896
|
+
ts = Math.floor(ts / 1000);
|
|
897
|
+
}
|
|
898
|
+
return `'${new Date(ts).toISOString()}'`;
|
|
899
|
+
}
|
|
900
|
+
const parsed = Date.parse(as_of);
|
|
901
|
+
if (!Number.isFinite(parsed))
|
|
902
|
+
return null;
|
|
903
|
+
// Format as string 'YYYY-MM-DDTHH:mm:ss.sssZ' for CozoDB
|
|
904
|
+
return `'${new Date(parsed).toISOString()}'`;
|
|
905
|
+
}
|
|
906
|
+
async formatInferredRelationsForContext(relations) {
|
|
907
|
+
const uniqueIds = Array.from(new Set(relations.flatMap((r) => [r.from_id, r.to_id]).filter(Boolean)));
|
|
908
|
+
const nameById = new Map();
|
|
909
|
+
await Promise.all(uniqueIds.map(async (id) => {
|
|
910
|
+
try {
|
|
911
|
+
const res = await this.db.run('?[name, type] := *entity{id: $id, name, type, @ "NOW"} :limit 1', { id });
|
|
912
|
+
if (res.rows.length > 0)
|
|
913
|
+
nameById.set(id, { name: String(res.rows[0][0]), type: String(res.rows[0][1]) });
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
}
|
|
917
|
+
}));
|
|
918
|
+
return relations.map((r) => {
|
|
919
|
+
const fromMeta = nameById.get(r.from_id);
|
|
920
|
+
const toMeta = nameById.get(r.to_id);
|
|
921
|
+
const fromName = fromMeta?.name ?? r.from_id;
|
|
922
|
+
const toName = toMeta?.name ?? r.to_id;
|
|
923
|
+
const edgePart = r.relation_type === "expert_in" ? `Expertise for ${toName}` : `${r.relation_type} -> ${toName}`;
|
|
924
|
+
return {
|
|
925
|
+
...r,
|
|
926
|
+
is_inferred: true,
|
|
927
|
+
from_name: fromMeta?.name,
|
|
928
|
+
from_type: fromMeta?.type,
|
|
929
|
+
to_name: toMeta?.name,
|
|
930
|
+
to_type: toMeta?.type,
|
|
931
|
+
uncertainty_hint: `Presumably ${fromName} (${edgePart}), because ${r.reason}`,
|
|
932
|
+
};
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
async createEntity(args) {
|
|
936
|
+
try {
|
|
937
|
+
if (!args.name || args.name.trim() === "") {
|
|
938
|
+
return { error: "Entity name must not be empty" };
|
|
939
|
+
}
|
|
940
|
+
if (!args.type || args.type.trim() === "") {
|
|
941
|
+
return { error: "Entity type must not be empty" };
|
|
942
|
+
}
|
|
943
|
+
// Check for existing entity with same name (case-insensitive)
|
|
944
|
+
const existingId = await this.findEntityIdByName(args.name);
|
|
945
|
+
if (existingId) {
|
|
946
|
+
return { id: existingId, name: args.name, type: args.type, status: "Entity already exists (Name-Match)" };
|
|
947
|
+
}
|
|
948
|
+
// Conflict Detection (Application-Level Fallback for Triggers)
|
|
949
|
+
if (args.metadata) {
|
|
950
|
+
const status = args.metadata.status || "";
|
|
951
|
+
const isArchived = args.metadata.archived === true;
|
|
952
|
+
const isAktiv = status === "aktiv" || status === "active";
|
|
953
|
+
const isEingestellt = status === "eingestellt" || isArchived;
|
|
954
|
+
if (isAktiv && isEingestellt) {
|
|
955
|
+
throw new Error(`Conflict detected: Entity '${args.name}' cannot be 'active' and 'discontinued' at the same time.`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
const id = (0, uuid_1.v4)();
|
|
959
|
+
return this.createEntityWithId(id, args.name, args.type, args.metadata);
|
|
960
|
+
}
|
|
961
|
+
catch (error) {
|
|
962
|
+
console.error("Error in create_entity:", error);
|
|
963
|
+
if (error.display) {
|
|
964
|
+
console.error("CozoDB Error Details:", error.display);
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
error: "Internal error creating entity",
|
|
968
|
+
message: error.message || String(error),
|
|
969
|
+
details: error.stack,
|
|
970
|
+
cozo_display: error.display
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
async createEntityWithId(id, name, type, metadata) {
|
|
975
|
+
const embedding = await this.embeddingService.embed(`${name} ${type}`);
|
|
976
|
+
const name_embedding = await this.embeddingService.embed(name);
|
|
977
|
+
console.error(`[Debug] Embeddings created for ${name}.`);
|
|
978
|
+
// Use direct vector binding for performance and to avoid long string issues
|
|
979
|
+
const now = Date.now() * 1000;
|
|
980
|
+
await this.db.run(`
|
|
981
|
+
?[id, created_at, name, type, embedding, name_embedding, metadata] <- [
|
|
982
|
+
[$id, [${now}, true], $name, $type, $embedding, $name_embedding, $metadata]
|
|
983
|
+
] :insert entity {id, created_at => name, type, embedding, name_embedding, metadata}
|
|
984
|
+
`, { id, name, type, embedding, name_embedding, metadata: metadata || {} });
|
|
985
|
+
return { id, name, type, status: "Entity created" };
|
|
986
|
+
}
|
|
987
|
+
async initUserProfile() {
|
|
988
|
+
try {
|
|
989
|
+
const res = await this.db.run('?[id] := *entity{id, @ "NOW"}, id = $id', { id: exports.USER_ENTITY_ID });
|
|
990
|
+
if (res.rows.length === 0) {
|
|
991
|
+
console.error("[User] Initializing global user profile...");
|
|
992
|
+
await this.createEntityWithId(exports.USER_ENTITY_ID, exports.USER_ENTITY_NAME, exports.USER_ENTITY_TYPE, { is_global_user: true });
|
|
993
|
+
await this.addObservation({
|
|
994
|
+
entity_id: exports.USER_ENTITY_ID,
|
|
995
|
+
text: "This is the global user profile for preferences and work styles.",
|
|
996
|
+
metadata: { kind: "system_init" }
|
|
997
|
+
});
|
|
998
|
+
console.error("[User] Global user profile created.");
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
catch (e) {
|
|
1002
|
+
console.error("[User] Error initializing user profile:", e.message);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
async updateEntity(args) {
|
|
1006
|
+
try {
|
|
1007
|
+
const current = await this.db.run('?[name, type, metadata] := *entity{id: $id, name, type, metadata, @ "NOW"}', { id: args.id });
|
|
1008
|
+
if (current.rows.length === 0)
|
|
1009
|
+
return { error: "Entity not found" };
|
|
1010
|
+
const name = args.name ?? current.rows[0][0];
|
|
1011
|
+
const type = args.type ?? current.rows[0][1];
|
|
1012
|
+
// Conflict Detection (Application-Level Fallback for Triggers)
|
|
1013
|
+
const mergedMetadata = { ...(current.rows[0][2] || {}), ...(args.metadata || {}) };
|
|
1014
|
+
const status = mergedMetadata.status || "";
|
|
1015
|
+
const isArchived = mergedMetadata.archived === true;
|
|
1016
|
+
const isAktiv = status === "aktiv" || status === "active";
|
|
1017
|
+
const isEingestellt = status === "eingestellt" || isArchived;
|
|
1018
|
+
if (isAktiv && isEingestellt) {
|
|
1019
|
+
throw new Error(`Conflict detected: Entity '${name}' cannot be 'active' and 'discontinued' at the same time.`);
|
|
1020
|
+
}
|
|
1021
|
+
// Check if the new name already exists for a DIFFERENT entity
|
|
1022
|
+
if (args.name && args.name !== current.rows[0][0]) {
|
|
1023
|
+
const existingId = await this.findEntityIdByName(args.name);
|
|
1024
|
+
if (existingId && existingId !== args.id) {
|
|
1025
|
+
return { error: `Name '${args.name}' is already used by another entity (${existingId}).` };
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
const embedding = await this.embeddingService.embed(`${name} ${type}`);
|
|
1029
|
+
const name_embedding = await this.embeddingService.embed(name);
|
|
1030
|
+
const now = Date.now() * 1000;
|
|
1031
|
+
// Using v0.7 :update and ++ for metadata merge (v1.7 Multi-Vector)
|
|
1032
|
+
await this.db.run(`
|
|
1033
|
+
?[id, created_at, name, type, embedding, name_embedding, metadata] :=
|
|
1034
|
+
*entity{id, created_at, metadata: old_meta, @ "NOW"},
|
|
1035
|
+
id = $id,
|
|
1036
|
+
name = $name,
|
|
1037
|
+
type = $type,
|
|
1038
|
+
embedding = $embedding,
|
|
1039
|
+
name_embedding = $name_embedding,
|
|
1040
|
+
metadata = old_meta ++ $new_meta
|
|
1041
|
+
:update entity {id, created_at, name, type, embedding, name_embedding, metadata}
|
|
1042
|
+
`, {
|
|
1043
|
+
id: args.id,
|
|
1044
|
+
name,
|
|
1045
|
+
type,
|
|
1046
|
+
embedding,
|
|
1047
|
+
name_embedding,
|
|
1048
|
+
new_meta: args.metadata || {}
|
|
1049
|
+
});
|
|
1050
|
+
return { status: "Entity updated", id: args.id };
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
console.error("Error in update_entity:", error);
|
|
1054
|
+
return {
|
|
1055
|
+
error: "Internal error updating entity",
|
|
1056
|
+
message: error.message || String(error),
|
|
1057
|
+
details: error.stack,
|
|
1058
|
+
cozo_display: error.display
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async addObservation(args) {
|
|
1063
|
+
try {
|
|
1064
|
+
if (!args.text || args.text.trim() === "") {
|
|
1065
|
+
return { error: "Observation text must not be empty" };
|
|
1066
|
+
}
|
|
1067
|
+
const deduplicate = args.deduplicate ?? true;
|
|
1068
|
+
let entityId;
|
|
1069
|
+
if (args.entity_id) {
|
|
1070
|
+
const entityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: args.entity_id });
|
|
1071
|
+
if (entityRes.rows.length === 0) {
|
|
1072
|
+
return { error: `Entity with ID '${args.entity_id}' not found` };
|
|
1073
|
+
}
|
|
1074
|
+
entityId = args.entity_id;
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
const entityName = (args.entity_name ?? "").trim();
|
|
1078
|
+
if (!entityName)
|
|
1079
|
+
return { error: "For ingest, 'entity_id' or 'entity_name' is mandatory to assign data." };
|
|
1080
|
+
const existing = await this.findEntityIdByName(entityName);
|
|
1081
|
+
if (existing) {
|
|
1082
|
+
entityId = existing;
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
const created = await this.createEntity({
|
|
1086
|
+
name: entityName,
|
|
1087
|
+
type: (args.entity_type ?? "Unknown").trim() || "Unknown",
|
|
1088
|
+
metadata: {},
|
|
1089
|
+
});
|
|
1090
|
+
if (created?.error)
|
|
1091
|
+
return created;
|
|
1092
|
+
entityId = created.id;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const id = (0, uuid_1.v4)();
|
|
1096
|
+
const embedding = await this.embeddingService.embed(args.text);
|
|
1097
|
+
// Check for duplicates (using v0.7 features)
|
|
1098
|
+
if (deduplicate) {
|
|
1099
|
+
try {
|
|
1100
|
+
// 1. Exact match check
|
|
1101
|
+
const exact = await this.db.run('?[existing_id, existing_text] := *observation{entity_id: $entity, id: existing_id, text: existing_text, @ "NOW"}, existing_text == $text :limit 1', { entity: entityId, text: args.text });
|
|
1102
|
+
if (exact.rows.length > 0) {
|
|
1103
|
+
const [existingId, existingText] = exact.rows[0];
|
|
1104
|
+
return {
|
|
1105
|
+
status: 'duplicate_detected',
|
|
1106
|
+
existing_observation_id: existingId,
|
|
1107
|
+
similarity: 1.0,
|
|
1108
|
+
suggestion: 'Exact same observation already exists.',
|
|
1109
|
+
text: existingText
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
// 2. Near-duplicate check via LSH (v0.7)
|
|
1113
|
+
// Note: bind_distance is not supported in LSH, using k: 1 instead
|
|
1114
|
+
const nearDup = await this.db.run(`
|
|
1115
|
+
?[existing_id, existing_text] :=
|
|
1116
|
+
~observation:lsh {id: existing_id, text: existing_text | query: $text, k: 1},
|
|
1117
|
+
*observation {id: existing_id, entity_id: $entity, @ "NOW"}
|
|
1118
|
+
:limit 1
|
|
1119
|
+
`, { text: args.text, entity: entityId });
|
|
1120
|
+
if (nearDup.rows.length > 0) {
|
|
1121
|
+
const [existingId, existingText] = nearDup.rows[0];
|
|
1122
|
+
return {
|
|
1123
|
+
status: 'duplicate_detected',
|
|
1124
|
+
existing_observation_id: existingId,
|
|
1125
|
+
similarity: 0.9, // Estimate, as LSH only returns hits within threshold
|
|
1126
|
+
suggestion: 'Very similar observation found (LSH Match).',
|
|
1127
|
+
text: existingText
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch (e) {
|
|
1132
|
+
console.warn("[AddObservation] Duplicate check via LSH failed:", e.message);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const now = Date.now() * 1000;
|
|
1136
|
+
await this.db.run(`
|
|
1137
|
+
?[id, created_at, entity_id, text, embedding, metadata] <- [
|
|
1138
|
+
[$id, [${now}, true], $entity_id, $text, $embedding, $metadata]
|
|
1139
|
+
] :insert observation {id, created_at => entity_id, text, embedding, metadata}
|
|
1140
|
+
`, { id, entity_id: entityId, text: args.text, embedding, metadata: args.metadata || {} });
|
|
1141
|
+
// Optional: Automatic inference after new observation (in background)
|
|
1142
|
+
const suggestionsRaw = await this.inferenceEngine.inferRelations(entityId);
|
|
1143
|
+
const suggestions = await this.formatInferredRelationsForContext(suggestionsRaw);
|
|
1144
|
+
return {
|
|
1145
|
+
id,
|
|
1146
|
+
entity_id: entityId,
|
|
1147
|
+
status: "Observation saved",
|
|
1148
|
+
inferred_suggestions: suggestions
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
catch (error) {
|
|
1152
|
+
return { error: error.message || "Unknown error" };
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async createRelation(args) {
|
|
1156
|
+
if (args.from_id === args.to_id) {
|
|
1157
|
+
return { error: "Self-references in relationships are not allowed" };
|
|
1158
|
+
}
|
|
1159
|
+
// Check if both entities exist
|
|
1160
|
+
const [fromEntity, toEntity] = await Promise.all([
|
|
1161
|
+
this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: args.from_id }),
|
|
1162
|
+
this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: args.to_id })
|
|
1163
|
+
]);
|
|
1164
|
+
if (fromEntity.rows.length === 0) {
|
|
1165
|
+
return { error: `Source entity with ID '${args.from_id}' not found` };
|
|
1166
|
+
}
|
|
1167
|
+
if (toEntity.rows.length === 0) {
|
|
1168
|
+
return { error: `Target entity with ID '${args.to_id}' not found` };
|
|
1169
|
+
}
|
|
1170
|
+
const now = Date.now() * 1000;
|
|
1171
|
+
await this.db.run(`?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[$from_id, $to_id, $relation_type, [${now}, true], $strength, $metadata]] :insert relationship {from_id, to_id, relation_type, created_at => strength, metadata}`, {
|
|
1172
|
+
from_id: args.from_id,
|
|
1173
|
+
to_id: args.to_id,
|
|
1174
|
+
relation_type: args.relation_type,
|
|
1175
|
+
strength: args.strength ?? 1.0,
|
|
1176
|
+
metadata: args.metadata || {}
|
|
1177
|
+
});
|
|
1178
|
+
return { status: "Relationship created" };
|
|
1179
|
+
}
|
|
1180
|
+
async exploreGraph(args) {
|
|
1181
|
+
await this.initPromise;
|
|
1182
|
+
// Check if start entity exists
|
|
1183
|
+
const startRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: args.start_entity });
|
|
1184
|
+
if (startRes.rows.length === 0) {
|
|
1185
|
+
throw new Error(`Start entity with ID '${args.start_entity}' not found`);
|
|
1186
|
+
}
|
|
1187
|
+
const start = args.start_entity;
|
|
1188
|
+
const end = args.end_entity;
|
|
1189
|
+
const maxHops = Math.min(5, Math.max(1, Math.floor(args.max_hops ?? 3)));
|
|
1190
|
+
const relationTypes = (args.relation_types ?? []).map(String).filter((t) => t.trim().length > 0);
|
|
1191
|
+
const getEdges = async (fromIds) => {
|
|
1192
|
+
if (fromIds.length === 0)
|
|
1193
|
+
return [];
|
|
1194
|
+
const frontier = fromIds.map((id) => [id]);
|
|
1195
|
+
const params = { frontier };
|
|
1196
|
+
let query = `
|
|
1197
|
+
frontier[from_id] <- $frontier
|
|
1198
|
+
`;
|
|
1199
|
+
if (relationTypes.length > 0) {
|
|
1200
|
+
query += `
|
|
1201
|
+
allowed[rel_type] <- $allowed
|
|
1202
|
+
?[from_id, to_id, rel_type] :=
|
|
1203
|
+
frontier[from_id],
|
|
1204
|
+
*relationship{from_id, to_id, relation_type: rel_type, @ "NOW"},
|
|
1205
|
+
allowed[rel_type]
|
|
1206
|
+
`.trim();
|
|
1207
|
+
params.allowed = relationTypes.map((t) => [t]);
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
query += `
|
|
1211
|
+
?[from_id, to_id, rel_type] :=
|
|
1212
|
+
frontier[from_id],
|
|
1213
|
+
*relationship{from_id, to_id, relation_type: rel_type, @ "NOW"}
|
|
1214
|
+
`.trim();
|
|
1215
|
+
}
|
|
1216
|
+
const res = await this.db.run(query.trim(), params);
|
|
1217
|
+
return (res.rows || []).map((r) => ({ from: String(r[0]), to: String(r[1]), rel: String(r[2]) }));
|
|
1218
|
+
};
|
|
1219
|
+
if (end) {
|
|
1220
|
+
if (start === end) {
|
|
1221
|
+
return { start_entity: start, end_entity: end, path: [start], path_length: 1 };
|
|
1222
|
+
}
|
|
1223
|
+
const visited = new Set([start]);
|
|
1224
|
+
const parent = new Map();
|
|
1225
|
+
let frontier = [start];
|
|
1226
|
+
let found = false;
|
|
1227
|
+
for (let depth = 0; depth < maxHops; depth++) {
|
|
1228
|
+
const edges = await getEdges(frontier);
|
|
1229
|
+
const next = [];
|
|
1230
|
+
for (const e of edges) {
|
|
1231
|
+
if (visited.has(e.to))
|
|
1232
|
+
continue;
|
|
1233
|
+
visited.add(e.to);
|
|
1234
|
+
parent.set(e.to, e.from);
|
|
1235
|
+
if (e.to === end) {
|
|
1236
|
+
found = true;
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1239
|
+
next.push(e.to);
|
|
1240
|
+
}
|
|
1241
|
+
if (found)
|
|
1242
|
+
break;
|
|
1243
|
+
frontier = next;
|
|
1244
|
+
if (frontier.length === 0)
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
if (!found) {
|
|
1248
|
+
return { start_entity: start, end_entity: end, path: [], path_length: 0 };
|
|
1249
|
+
}
|
|
1250
|
+
const path = [];
|
|
1251
|
+
let cur = end;
|
|
1252
|
+
while (cur) {
|
|
1253
|
+
path.push(cur);
|
|
1254
|
+
if (cur === start)
|
|
1255
|
+
break;
|
|
1256
|
+
cur = parent.get(cur);
|
|
1257
|
+
}
|
|
1258
|
+
path.reverse();
|
|
1259
|
+
const hops = Math.max(0, path.length - 1);
|
|
1260
|
+
if (hops > maxHops) {
|
|
1261
|
+
return { start_entity: start, end_entity: end, path: [], path_length: 0 };
|
|
1262
|
+
}
|
|
1263
|
+
return { start_entity: start, end_entity: end, path, path_length: path.length };
|
|
1264
|
+
}
|
|
1265
|
+
const distance = new Map();
|
|
1266
|
+
distance.set(start, 0);
|
|
1267
|
+
let frontier = [start];
|
|
1268
|
+
for (let depth = 0; depth < maxHops; depth++) {
|
|
1269
|
+
const edges = await getEdges(frontier);
|
|
1270
|
+
const next = [];
|
|
1271
|
+
for (const e of edges) {
|
|
1272
|
+
if (distance.has(e.to))
|
|
1273
|
+
continue;
|
|
1274
|
+
distance.set(e.to, depth + 1);
|
|
1275
|
+
next.push(e.to);
|
|
1276
|
+
}
|
|
1277
|
+
frontier = next;
|
|
1278
|
+
if (frontier.length === 0)
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
const targetIds = Array.from(distance.entries())
|
|
1282
|
+
.filter(([id, d]) => id !== start && d > 0)
|
|
1283
|
+
.sort((a, b) => a[1] - b[1])
|
|
1284
|
+
.map(([id]) => id);
|
|
1285
|
+
if (targetIds.length === 0) {
|
|
1286
|
+
return { start_entity: start, results: [] };
|
|
1287
|
+
}
|
|
1288
|
+
const ids = targetIds.map((id) => [id]);
|
|
1289
|
+
const entityRes = await this.db.run(`
|
|
1290
|
+
ids[id] <- $ids
|
|
1291
|
+
?[id, name, type] := ids[id], *entity{id, name, type, @ "NOW"}
|
|
1292
|
+
`.trim(), { ids });
|
|
1293
|
+
const metaById = new Map();
|
|
1294
|
+
(entityRes.rows || []).forEach((r) => metaById.set(String(r[0]), { name: String(r[1]), type: String(r[2]) }));
|
|
1295
|
+
const results = targetIds
|
|
1296
|
+
.map((id) => {
|
|
1297
|
+
const meta = metaById.get(id);
|
|
1298
|
+
if (!meta) {
|
|
1299
|
+
// Fallback for missing entities
|
|
1300
|
+
return { entity_id: id, name: "Unknown (Missing Entity)", type: "Unknown", hops: distance.get(id) ?? null };
|
|
1301
|
+
}
|
|
1302
|
+
return { entity_id: id, name: meta.name, type: meta.type, hops: distance.get(id) ?? null };
|
|
1303
|
+
})
|
|
1304
|
+
.filter((r) => r !== undefined);
|
|
1305
|
+
return { start_entity: start, results };
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Tracks the temporal evolution of relationships of an entity (Time-Travel Analysis).
|
|
1309
|
+
* Returns a list of events (ASSERTED/RETRACTED) over time.
|
|
1310
|
+
* Optional filters for target entity and time range.
|
|
1311
|
+
*/
|
|
1312
|
+
async getRelationEvolution(args) {
|
|
1313
|
+
await this.initPromise;
|
|
1314
|
+
const fromId = args.from_id;
|
|
1315
|
+
const toId = args.to_id;
|
|
1316
|
+
const since = args.since ? args.since * 1000 : undefined; // Cozo uses microseconds
|
|
1317
|
+
const until = args.until ? args.until * 1000 : undefined;
|
|
1318
|
+
// 1. Query all historical states for this relationship(s)
|
|
1319
|
+
let query = `
|
|
1320
|
+
?[from_id, to_id, relation_type, strength, metadata, created_at] :=
|
|
1321
|
+
*relationship{from_id, to_id, relation_type, strength, metadata, created_at},
|
|
1322
|
+
from_id = $from_id
|
|
1323
|
+
`;
|
|
1324
|
+
const params = { from_id: fromId };
|
|
1325
|
+
if (toId) {
|
|
1326
|
+
query += `, to_id = $to_id`;
|
|
1327
|
+
params.to_id = toId;
|
|
1328
|
+
}
|
|
1329
|
+
const res = await this.db.run(query, params);
|
|
1330
|
+
// 2. Resolve names of involved entities
|
|
1331
|
+
const uniqueIds = new Set();
|
|
1332
|
+
uniqueIds.add(fromId);
|
|
1333
|
+
(res.rows || []).forEach((r) => uniqueIds.add(String(r[1])));
|
|
1334
|
+
const nameRes = await this.db.run(`
|
|
1335
|
+
ids[id] <- $ids
|
|
1336
|
+
?[id, name] := ids[id], *entity{id, name, @ "NOW"}
|
|
1337
|
+
`, { ids: Array.from(uniqueIds).map(id => [id]) });
|
|
1338
|
+
const nameById = new Map();
|
|
1339
|
+
(nameRes.rows || []).forEach((r) => nameById.set(String(r[0]), String(r[1])));
|
|
1340
|
+
// 3. Process and filter events
|
|
1341
|
+
let events = (res.rows || []).map((r) => {
|
|
1342
|
+
const validity = r[5]; // [timestamp, is_asserted]
|
|
1343
|
+
const timestamp = Number(validity[0]);
|
|
1344
|
+
const isAsserted = Boolean(validity[1]);
|
|
1345
|
+
return {
|
|
1346
|
+
timestamp,
|
|
1347
|
+
date: new Date(Math.floor(timestamp / 1000)).toISOString(),
|
|
1348
|
+
operation: isAsserted ? "ASSERTED" : "RETRACTED",
|
|
1349
|
+
from_id: String(r[0]),
|
|
1350
|
+
from_name: nameById.get(String(r[0])) || String(r[0]),
|
|
1351
|
+
to_id: String(r[1]),
|
|
1352
|
+
to_name: nameById.get(String(r[1])) || String(r[1]),
|
|
1353
|
+
relation_type: String(r[2]),
|
|
1354
|
+
strength: Number(r[3]),
|
|
1355
|
+
metadata: r[4]
|
|
1356
|
+
};
|
|
1357
|
+
});
|
|
1358
|
+
// Apply time range filter
|
|
1359
|
+
if (since !== undefined) {
|
|
1360
|
+
events = events.filter((e) => e.timestamp >= since);
|
|
1361
|
+
}
|
|
1362
|
+
if (until !== undefined) {
|
|
1363
|
+
events = events.filter((e) => e.timestamp <= until);
|
|
1364
|
+
}
|
|
1365
|
+
// Sort by time (ascending)
|
|
1366
|
+
events.sort((a, b) => a.timestamp - b.timestamp);
|
|
1367
|
+
// 4. Create diff summary
|
|
1368
|
+
const diff = {
|
|
1369
|
+
added: [],
|
|
1370
|
+
removed: [],
|
|
1371
|
+
modified: []
|
|
1372
|
+
};
|
|
1373
|
+
// Simple logic: We look at the events in the selected time period
|
|
1374
|
+
// For a real "diff" analysis between two points in time, one would have to compare the state @ start and @ end.
|
|
1375
|
+
// Here we provide the changes in the time period for now.
|
|
1376
|
+
events.forEach((e) => {
|
|
1377
|
+
if (e.operation === "ASSERTED") {
|
|
1378
|
+
diff.added.push(e);
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
diff.removed.push(e);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
return {
|
|
1385
|
+
from_id: fromId,
|
|
1386
|
+
from_name: nameById.get(fromId) || fromId,
|
|
1387
|
+
to_id: toId,
|
|
1388
|
+
to_name: toId ? (nameById.get(toId) || toId) : undefined,
|
|
1389
|
+
time_range: {
|
|
1390
|
+
since: args.since,
|
|
1391
|
+
until: args.until
|
|
1392
|
+
},
|
|
1393
|
+
event_count: events.length,
|
|
1394
|
+
timeline: events,
|
|
1395
|
+
summary: diff
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
async reflectMemory(args) {
|
|
1399
|
+
await this.initPromise;
|
|
1400
|
+
const model = args.model ?? "demyagent-4b-i1:Q6_K";
|
|
1401
|
+
const targetEntityId = args.entity_id;
|
|
1402
|
+
let entitiesToReflect = [];
|
|
1403
|
+
if (targetEntityId) {
|
|
1404
|
+
const res = await this.db.run('?[id, name, type] := *entity{id, name, type, @ "NOW"}, id = $id', { id: targetEntityId });
|
|
1405
|
+
if (res.rows.length > 0) {
|
|
1406
|
+
entitiesToReflect.push({ id: String(res.rows[0][0]), name: String(res.rows[0][1]), type: String(res.rows[0][2]) });
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
// Select top 5 entities with the most observations
|
|
1411
|
+
const res = await this.db.run(`
|
|
1412
|
+
?[id, name, type, count(id)] :=
|
|
1413
|
+
*entity{id, name, type, @ "NOW"},
|
|
1414
|
+
*observation{entity_id: id, @ "NOW"}
|
|
1415
|
+
`);
|
|
1416
|
+
entitiesToReflect = res.rows
|
|
1417
|
+
.map((r) => ({ id: String(r[0]), name: String(r[1]), type: String(r[2]), count: Number(r[3]) }))
|
|
1418
|
+
.sort((a, b) => b.count - a.count)
|
|
1419
|
+
.slice(0, 5);
|
|
1420
|
+
}
|
|
1421
|
+
const results = [];
|
|
1422
|
+
for (const entity of entitiesToReflect) {
|
|
1423
|
+
const obsRes = await this.db.run('?[text, ts] := *observation{entity_id: $id, text, created_at, @ "NOW"}, ts = to_int(created_at) :order ts', {
|
|
1424
|
+
id: entity.id,
|
|
1425
|
+
});
|
|
1426
|
+
if (obsRes.rows.length < 2) {
|
|
1427
|
+
results.push({ entity_id: entity.id, status: "skipped", reason: "Too few observations for reflection" });
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const observations = obsRes.rows.map((r) => `- [${new Date(Number(r[1]) / 1000).toISOString()}] ${r[0]}`);
|
|
1431
|
+
const systemPrompt = `You are an analytical memory module. Analyze the following observations about an entity.
|
|
1432
|
+
Look for contradictions, temporal developments, behavioral patterns, or deeper insights.
|
|
1433
|
+
Formulate a concise reflection (max. 3-4 sentences) that helps the user understand the current state or evolution.
|
|
1434
|
+
If there are contradictory statements, name them explicitly.
|
|
1435
|
+
If no special patterns are recognizable, answer with "No new insights".`;
|
|
1436
|
+
const userPrompt = `Entity: ${entity.name} (${entity.type})\n\nObservations:\n${observations.join("\n")}`;
|
|
1437
|
+
let reflectionText;
|
|
1438
|
+
try {
|
|
1439
|
+
const ollamaMod = await import("ollama");
|
|
1440
|
+
const ollamaClient = ollamaMod?.default ?? ollamaMod;
|
|
1441
|
+
const response = await ollamaClient.chat({
|
|
1442
|
+
model,
|
|
1443
|
+
messages: [
|
|
1444
|
+
{ role: "system", content: systemPrompt },
|
|
1445
|
+
{ role: "user", content: userPrompt },
|
|
1446
|
+
],
|
|
1447
|
+
});
|
|
1448
|
+
reflectionText = response?.message?.content?.trim?.() ?? "";
|
|
1449
|
+
}
|
|
1450
|
+
catch (e) {
|
|
1451
|
+
console.error(`[Reflect] Ollama error for ${entity.name}:`, e);
|
|
1452
|
+
reflectionText = "";
|
|
1453
|
+
}
|
|
1454
|
+
if (reflectionText && reflectionText !== "No new insights" && !reflectionText.includes("No new insights")) {
|
|
1455
|
+
await this.addObservation({
|
|
1456
|
+
entity_id: entity.id,
|
|
1457
|
+
text: `Reflexive insight: ${reflectionText}`,
|
|
1458
|
+
metadata: { kind: "reflection", model, generated_at: Date.now() },
|
|
1459
|
+
});
|
|
1460
|
+
results.push({ entity_id: entity.id, status: "reflected", insight: reflectionText });
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
results.push({ entity_id: entity.id, status: "no_insight_found" });
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return { status: "completed", results };
|
|
1467
|
+
}
|
|
1468
|
+
async detectStatusConflicts(entityIds) {
|
|
1469
|
+
await this.initPromise;
|
|
1470
|
+
const ids = Array.from(new Set((entityIds ?? []).map(String).filter((x) => x.trim().length > 0))).slice(0, 50);
|
|
1471
|
+
if (ids.length === 0)
|
|
1472
|
+
return [];
|
|
1473
|
+
const list = ids.map((id) => [id]);
|
|
1474
|
+
const activeRe = "(?i).*(\\bactive\\b|\\brunning\\b|\\bongoing\\b|in\\s+operation|continues|continued|not\\s+discontinued).*";
|
|
1475
|
+
const inactiveRe = "(?i).*(discontinued|cancelled|stopped|shut\\s+down|closed|shutdown|deprecated|archived|terminated|abandoned).*";
|
|
1476
|
+
const latestByRegex = async (re) => {
|
|
1477
|
+
const res = await this.db.run(`
|
|
1478
|
+
ids[id] <- $ids
|
|
1479
|
+
?[entity_id, ts, text] :=
|
|
1480
|
+
ids[entity_id],
|
|
1481
|
+
*observation{entity_id, text, created_at, @ "NOW"},
|
|
1482
|
+
ts = to_int(created_at),
|
|
1483
|
+
regex_matches(text, $re)
|
|
1484
|
+
`.trim(), { ids: list, re });
|
|
1485
|
+
const out = new Map();
|
|
1486
|
+
for (const row of res.rows) {
|
|
1487
|
+
const entityId = String(row[0]);
|
|
1488
|
+
const createdAt = Number(row[1]);
|
|
1489
|
+
const text = String(row[2]);
|
|
1490
|
+
const existing = out.get(entityId);
|
|
1491
|
+
if (!existing || createdAt > existing.created_at) {
|
|
1492
|
+
out.set(entityId, { created_at: createdAt, text });
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return out;
|
|
1496
|
+
};
|
|
1497
|
+
const [activeLatest, inactiveLatest] = await Promise.all([latestByRegex(activeRe), latestByRegex(inactiveRe)]);
|
|
1498
|
+
const toYear = (micros) => new Date(Math.floor(micros / 1000)).getUTCFullYear();
|
|
1499
|
+
const conflictIds = ids.filter((id) => {
|
|
1500
|
+
const active = activeLatest.get(id);
|
|
1501
|
+
const inactive = inactiveLatest.get(id);
|
|
1502
|
+
if (!active || !inactive)
|
|
1503
|
+
return false;
|
|
1504
|
+
const ay = toYear(active.created_at);
|
|
1505
|
+
const iy = toYear(inactive.created_at);
|
|
1506
|
+
// A conflict only exists if both pieces of information are from the same time period (year).
|
|
1507
|
+
// Different years indicate a status change (e.g. discontinued in 2024, active again in 2025).
|
|
1508
|
+
// This matches the proposal for temporal consistency.
|
|
1509
|
+
return ay === iy;
|
|
1510
|
+
});
|
|
1511
|
+
if (conflictIds.length === 0)
|
|
1512
|
+
return [];
|
|
1513
|
+
const metaRes = await this.db.run(`
|
|
1514
|
+
ids[id] <- $ids
|
|
1515
|
+
?[id, name, type] := ids[id], *entity{id, name, type, @ "NOW"}
|
|
1516
|
+
`.trim(), { ids: conflictIds.map((id) => [id]) });
|
|
1517
|
+
const metaById = new Map();
|
|
1518
|
+
for (const row of metaRes.rows)
|
|
1519
|
+
metaById.set(String(row[0]), { name: String(row[1]), type: String(row[2]) });
|
|
1520
|
+
return conflictIds
|
|
1521
|
+
.map((id) => {
|
|
1522
|
+
const meta = metaById.get(id);
|
|
1523
|
+
const active = activeLatest.get(id);
|
|
1524
|
+
const inactive = inactiveLatest.get(id);
|
|
1525
|
+
if (!meta || !active || !inactive)
|
|
1526
|
+
return undefined;
|
|
1527
|
+
const ay = toYear(active.created_at);
|
|
1528
|
+
const iy = toYear(inactive.created_at);
|
|
1529
|
+
const years = ay === iy ? String(ay) : `${Math.min(ay, iy)} vs. ${Math.max(ay, iy)}`;
|
|
1530
|
+
return {
|
|
1531
|
+
entity_id: id,
|
|
1532
|
+
entity_name: meta.name,
|
|
1533
|
+
entity_type: meta.type,
|
|
1534
|
+
kind: "status",
|
|
1535
|
+
summary: `Conflict: Contradictory info on status of ${meta.name} in same period (${years}).`,
|
|
1536
|
+
evidence: {
|
|
1537
|
+
active: { created_at: active.created_at, year: ay, text: active.text },
|
|
1538
|
+
inactive: { created_at: inactive.created_at, year: iy, text: inactive.text },
|
|
1539
|
+
},
|
|
1540
|
+
};
|
|
1541
|
+
})
|
|
1542
|
+
.filter((x) => x !== undefined);
|
|
1543
|
+
}
|
|
1544
|
+
async addInferenceRule(args) {
|
|
1545
|
+
await this.initPromise;
|
|
1546
|
+
try {
|
|
1547
|
+
if (!args.name || args.name.trim() === "") {
|
|
1548
|
+
return { error: "Rule name must not be empty" };
|
|
1549
|
+
}
|
|
1550
|
+
if (!args.datalog || args.datalog.trim() === "") {
|
|
1551
|
+
return { error: "Datalog must not be empty" };
|
|
1552
|
+
}
|
|
1553
|
+
// Check if a rule with this name already exists
|
|
1554
|
+
const existingRule = await this.db.run('?[id] := *inference_rule{name: $name, id}', { name: args.name });
|
|
1555
|
+
if (existingRule.rows.length > 0) {
|
|
1556
|
+
return { error: `An inference rule with the name '${args.name}' already exists.` };
|
|
1557
|
+
}
|
|
1558
|
+
// Validate Datalog code
|
|
1559
|
+
try {
|
|
1560
|
+
const validationRes = await this.db.run(args.datalog, { id: "validation-test" });
|
|
1561
|
+
const expectedHeaders = ["from_id", "to_id", "relation_type", "confidence", "reason"];
|
|
1562
|
+
const actualHeaders = validationRes.headers;
|
|
1563
|
+
const missingHeaders = expectedHeaders.filter(h => !actualHeaders.includes(h));
|
|
1564
|
+
if (missingHeaders.length > 0) {
|
|
1565
|
+
return { error: `Invalid Datalog result set. Missing columns: ${missingHeaders.join(", ")}. Expected: ${expectedHeaders.join(", ")}` };
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
catch (validationError) {
|
|
1569
|
+
return { error: `Datalog syntax error: ${validationError.message}` };
|
|
1570
|
+
}
|
|
1571
|
+
const id = (0, uuid_1.v4)();
|
|
1572
|
+
const now = Date.now();
|
|
1573
|
+
await this.db.run("?[id, name, datalog, created_at] <- [[$id, $name, $datalog, $now]] :put inference_rule {id => name, datalog, created_at}", { id, name: args.name, datalog: args.datalog, now });
|
|
1574
|
+
return { id, name: args.name, status: "Rule saved" };
|
|
1575
|
+
}
|
|
1576
|
+
catch (error) {
|
|
1577
|
+
return { error: error.message || "Error saving rule" };
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async findEntityIdByName(name) {
|
|
1581
|
+
await this.initPromise;
|
|
1582
|
+
const lowerName = name.toLowerCase();
|
|
1583
|
+
const res = await this.db.run(`
|
|
1584
|
+
?[id, ts] :=
|
|
1585
|
+
*entity{id, name, created_at, @ "NOW"},
|
|
1586
|
+
lowercase(name) == $lower_name,
|
|
1587
|
+
ts = to_int(created_at)
|
|
1588
|
+
:order -ts
|
|
1589
|
+
:limit 1
|
|
1590
|
+
`, { lower_name: lowerName });
|
|
1591
|
+
if (res.rows.length === 0)
|
|
1592
|
+
return null;
|
|
1593
|
+
return String(res.rows[0][0]);
|
|
1594
|
+
}
|
|
1595
|
+
async ingestFile(args) {
|
|
1596
|
+
await this.initPromise;
|
|
1597
|
+
try {
|
|
1598
|
+
const content = (args.content ?? "").trim();
|
|
1599
|
+
if (!content)
|
|
1600
|
+
return { error: "Content must not be empty" };
|
|
1601
|
+
let entityId = undefined;
|
|
1602
|
+
let createdEntity = false;
|
|
1603
|
+
if (args.entity_id) {
|
|
1604
|
+
const entityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: args.entity_id });
|
|
1605
|
+
if (entityRes.rows.length === 0)
|
|
1606
|
+
return { error: `Entity with ID '${args.entity_id}' not found` };
|
|
1607
|
+
entityId = args.entity_id;
|
|
1608
|
+
}
|
|
1609
|
+
else {
|
|
1610
|
+
const entityName = (args.entity_name ?? "").trim();
|
|
1611
|
+
if (!entityName)
|
|
1612
|
+
return { error: "For ingest, 'entity_id' or 'entity_name' is mandatory to assign data." };
|
|
1613
|
+
const existing = await this.findEntityIdByName(entityName);
|
|
1614
|
+
if (existing) {
|
|
1615
|
+
entityId = existing;
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
const created = await this.createEntity({
|
|
1619
|
+
name: entityName,
|
|
1620
|
+
type: (args.entity_type ?? "Document").trim() || "Document",
|
|
1621
|
+
metadata: args.metadata || {},
|
|
1622
|
+
});
|
|
1623
|
+
if (created?.error)
|
|
1624
|
+
return created;
|
|
1625
|
+
entityId = created.id;
|
|
1626
|
+
createdEntity = true;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (!entityId)
|
|
1630
|
+
return { error: "Entity could not be determined" };
|
|
1631
|
+
const maxObs = Math.min(200, Math.max(1, Math.floor(args.max_observations ?? 50)));
|
|
1632
|
+
const deduplicate = args.deduplicate ?? true;
|
|
1633
|
+
const chunking = args.chunking ?? "none";
|
|
1634
|
+
const observations = [];
|
|
1635
|
+
if (args.format === "markdown") {
|
|
1636
|
+
if (chunking === "paragraphs") {
|
|
1637
|
+
const parts = content
|
|
1638
|
+
.split(/\r?\n\s*\r?\n+/g)
|
|
1639
|
+
.map((p) => p.trim())
|
|
1640
|
+
.filter((p) => p.length > 0);
|
|
1641
|
+
for (const p of parts.slice(0, maxObs))
|
|
1642
|
+
observations.push({ text: p });
|
|
1643
|
+
}
|
|
1644
|
+
else {
|
|
1645
|
+
observations.push({ text: content });
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
let parsed;
|
|
1650
|
+
try {
|
|
1651
|
+
parsed = JSON.parse(content);
|
|
1652
|
+
}
|
|
1653
|
+
catch {
|
|
1654
|
+
return { error: "JSON could not be parsed" };
|
|
1655
|
+
}
|
|
1656
|
+
const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.observations) ? parsed.observations : null;
|
|
1657
|
+
if (!items)
|
|
1658
|
+
return { error: "JSON expects Array or { observations: [...] }" };
|
|
1659
|
+
for (const item of items.slice(0, maxObs)) {
|
|
1660
|
+
if (typeof item === "string") {
|
|
1661
|
+
const t = item.trim();
|
|
1662
|
+
if (t)
|
|
1663
|
+
observations.push({ text: t });
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (item && typeof item === "object") {
|
|
1667
|
+
const t = String(item.text ?? "").trim();
|
|
1668
|
+
if (t)
|
|
1669
|
+
observations.push({ text: t, metadata: item.metadata });
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (observations.length === 0)
|
|
1674
|
+
return { error: "No observations extracted" };
|
|
1675
|
+
const insertedIds = [];
|
|
1676
|
+
let skippedDuplicates = 0;
|
|
1677
|
+
for (const o of observations) {
|
|
1678
|
+
const text = (o.text ?? "").trim();
|
|
1679
|
+
if (!text)
|
|
1680
|
+
continue;
|
|
1681
|
+
const res = await this.addObservation({
|
|
1682
|
+
entity_id: entityId,
|
|
1683
|
+
text,
|
|
1684
|
+
metadata: { ...(args.observation_metadata || {}), ...(o.metadata || {}) },
|
|
1685
|
+
deduplicate
|
|
1686
|
+
});
|
|
1687
|
+
if (res.status === 'duplicate_detected') {
|
|
1688
|
+
skippedDuplicates += 1;
|
|
1689
|
+
}
|
|
1690
|
+
else if (res.id) {
|
|
1691
|
+
insertedIds.push(res.id);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
return {
|
|
1695
|
+
status: "ingested",
|
|
1696
|
+
entity_id: entityId,
|
|
1697
|
+
created_entity: createdEntity,
|
|
1698
|
+
observations_requested: observations.length,
|
|
1699
|
+
observations_added: insertedIds.length,
|
|
1700
|
+
observations_skipped_duplicates: skippedDuplicates,
|
|
1701
|
+
observation_ids: insertedIds,
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
catch (error) {
|
|
1705
|
+
return { error: error.message || "Error during ingest" };
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
async deleteEntity(args) {
|
|
1709
|
+
try {
|
|
1710
|
+
// 1. Check if entity exists
|
|
1711
|
+
const entityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: args.entity_id });
|
|
1712
|
+
if (entityRes.rows.length === 0) {
|
|
1713
|
+
return { error: `Entity with ID '${args.entity_id}' not found` };
|
|
1714
|
+
}
|
|
1715
|
+
// 2. Delete all related data in a transaction (block)
|
|
1716
|
+
await this.db.run(`
|
|
1717
|
+
{ ?[id, created_at] := *observation{id, entity_id, created_at}, entity_id = $target_id :rm observation {id, created_at} }
|
|
1718
|
+
{ ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, from_id = $target_id :rm relationship {from_id, to_id, relation_type, created_at} }
|
|
1719
|
+
{ ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, to_id = $target_id :rm relationship {from_id, to_id, relation_type, created_at} }
|
|
1720
|
+
{ ?[id, created_at] := *entity{id, created_at}, id = $target_id :rm entity {id, created_at} }
|
|
1721
|
+
`, { target_id: args.entity_id });
|
|
1722
|
+
return { status: "Entity and all related data deleted" };
|
|
1723
|
+
}
|
|
1724
|
+
catch (error) {
|
|
1725
|
+
console.error("Error during deletion:", error);
|
|
1726
|
+
return { error: "Deletion failed", message: error.message };
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
async runTransaction(args) {
|
|
1730
|
+
await this.initPromise;
|
|
1731
|
+
try {
|
|
1732
|
+
if (!args.operations || args.operations.length === 0) {
|
|
1733
|
+
return { error: "Keine Operationen angegeben" };
|
|
1734
|
+
}
|
|
1735
|
+
const statements = [];
|
|
1736
|
+
const allParams = {};
|
|
1737
|
+
const results = [];
|
|
1738
|
+
const createdEntityIds = new Map(); // name -> id map for transaction-local lookups
|
|
1739
|
+
for (let i = 0; i < args.operations.length; i++) {
|
|
1740
|
+
const op = args.operations[i];
|
|
1741
|
+
const suffix = `_${i}`;
|
|
1742
|
+
let params = op.params;
|
|
1743
|
+
if (typeof params === 'string') {
|
|
1744
|
+
try {
|
|
1745
|
+
params = JSON.parse(params);
|
|
1746
|
+
}
|
|
1747
|
+
catch (e) {
|
|
1748
|
+
return { error: `Invalid JSON parameters (Parse Error) in operation ${i}` };
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
if (!params || typeof params !== 'object') {
|
|
1752
|
+
return { error: `Invalid parameter structure (not an object) in operation ${i}` };
|
|
1753
|
+
}
|
|
1754
|
+
switch (op.action) {
|
|
1755
|
+
case "create_entity": {
|
|
1756
|
+
const { name, type, metadata } = params;
|
|
1757
|
+
const id = params.id || (0, uuid_1.v4)();
|
|
1758
|
+
const embedding = await this.embeddingService.embed(`${name || "unknown"} ${type || "unknown"}`);
|
|
1759
|
+
const name_embedding = await this.embeddingService.embed(name || "unknown");
|
|
1760
|
+
const now = Date.now() * 1000;
|
|
1761
|
+
if (name) {
|
|
1762
|
+
createdEntityIds.set(name, id);
|
|
1763
|
+
}
|
|
1764
|
+
allParams[`id${suffix}`] = id;
|
|
1765
|
+
allParams[`name${suffix}`] = name || "unknown";
|
|
1766
|
+
allParams[`type${suffix}`] = type || "unknown";
|
|
1767
|
+
allParams[`embedding${suffix}`] = embedding;
|
|
1768
|
+
allParams[`name_embedding${suffix}`] = name_embedding;
|
|
1769
|
+
allParams[`metadata${suffix}`] = metadata || {};
|
|
1770
|
+
statements.push(`
|
|
1771
|
+
{
|
|
1772
|
+
?[id, created_at, name, type, embedding, name_embedding, metadata] <- [
|
|
1773
|
+
[$id${suffix}, [${now}, true], $name${suffix}, $type${suffix}, $embedding${suffix}, $name_embedding${suffix}, $metadata${suffix}]
|
|
1774
|
+
] :insert entity {id, created_at => name, type, embedding, name_embedding, metadata}
|
|
1775
|
+
}
|
|
1776
|
+
`);
|
|
1777
|
+
results.push({ action: "create_entity", id, name });
|
|
1778
|
+
break;
|
|
1779
|
+
}
|
|
1780
|
+
case "add_observation": {
|
|
1781
|
+
let { entity_id, entity_name, entity_type, text, metadata } = params;
|
|
1782
|
+
// Resolve entity_id if not provided
|
|
1783
|
+
if (!entity_id) {
|
|
1784
|
+
if (entity_name) {
|
|
1785
|
+
// 1. Check if created in this transaction
|
|
1786
|
+
if (createdEntityIds.has(entity_name)) {
|
|
1787
|
+
entity_id = createdEntityIds.get(entity_name);
|
|
1788
|
+
}
|
|
1789
|
+
else {
|
|
1790
|
+
// 2. Lookup in DB
|
|
1791
|
+
const existing = await this.findEntityIdByName(entity_name);
|
|
1792
|
+
if (existing) {
|
|
1793
|
+
entity_id = existing;
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
// 3. Auto-create entity
|
|
1797
|
+
const newId = (0, uuid_1.v4)();
|
|
1798
|
+
const newName = entity_name;
|
|
1799
|
+
const newType = entity_type || "Unknown";
|
|
1800
|
+
const newEmbedding = await this.embeddingService.embed(`${newName} ${newType}`);
|
|
1801
|
+
const newNameEmbedding = await this.embeddingService.embed(newName);
|
|
1802
|
+
const newNow = Date.now() * 1000;
|
|
1803
|
+
const createSuffix = `${suffix}_autocreate`;
|
|
1804
|
+
allParams[`id${createSuffix}`] = newId;
|
|
1805
|
+
allParams[`name${createSuffix}`] = newName;
|
|
1806
|
+
allParams[`type${createSuffix}`] = newType;
|
|
1807
|
+
allParams[`embedding${createSuffix}`] = newEmbedding;
|
|
1808
|
+
allParams[`name_embedding${createSuffix}`] = newNameEmbedding;
|
|
1809
|
+
allParams[`metadata${createSuffix}`] = {}; // Default metadata
|
|
1810
|
+
statements.push(`
|
|
1811
|
+
{
|
|
1812
|
+
?[id, created_at, name, type, embedding, name_embedding, metadata] <- [
|
|
1813
|
+
[$id${createSuffix}, [${newNow}, true], $name${createSuffix}, $type${createSuffix}, $embedding${createSuffix}, $name_embedding${createSuffix}, $metadata${createSuffix}]
|
|
1814
|
+
] :insert entity {id, created_at => name, type, embedding, name_embedding, metadata}
|
|
1815
|
+
}
|
|
1816
|
+
`);
|
|
1817
|
+
createdEntityIds.set(newName, newId);
|
|
1818
|
+
entity_id = newId;
|
|
1819
|
+
results.push({ action: "create_entity (auto)", id: newId, name: newName });
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
else {
|
|
1824
|
+
return { error: `entity_id or entity_name is required for add_observation in operation ${i}` };
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
const id = params.id || (0, uuid_1.v4)();
|
|
1828
|
+
const embedding = await this.embeddingService.embed(text || "");
|
|
1829
|
+
const now = Date.now() * 1000;
|
|
1830
|
+
allParams[`obs_id${suffix}`] = id;
|
|
1831
|
+
allParams[`obs_entity_id${suffix}`] = entity_id;
|
|
1832
|
+
allParams[`obs_text${suffix}`] = text || "";
|
|
1833
|
+
allParams[`obs_embedding${suffix}`] = embedding;
|
|
1834
|
+
allParams[`obs_metadata${suffix}`] = metadata || {};
|
|
1835
|
+
statements.push(`
|
|
1836
|
+
{
|
|
1837
|
+
?[id, created_at, entity_id, text, embedding, metadata] <- [
|
|
1838
|
+
[$obs_id${suffix}, [${now}, true], $obs_entity_id${suffix}, $obs_text${suffix}, $obs_embedding${suffix}, $obs_metadata${suffix}]
|
|
1839
|
+
] :insert observation {id, created_at => entity_id, text, embedding, metadata}
|
|
1840
|
+
}
|
|
1841
|
+
`);
|
|
1842
|
+
results.push({ action: "add_observation", id, entity_id });
|
|
1843
|
+
break;
|
|
1844
|
+
}
|
|
1845
|
+
case "create_relation": {
|
|
1846
|
+
let { from_id, to_id, relation_type, strength, metadata } = params;
|
|
1847
|
+
// 1. Check if IDs are actually names of entities created in this transaction
|
|
1848
|
+
if (createdEntityIds.has(from_id)) {
|
|
1849
|
+
from_id = createdEntityIds.get(from_id);
|
|
1850
|
+
}
|
|
1851
|
+
else if (!(0, uuid_1.validate)(from_id)) {
|
|
1852
|
+
// 2. Try to resolve from DB if not UUID (and not found in local transaction)
|
|
1853
|
+
const existingId = await this.findEntityIdByName(from_id);
|
|
1854
|
+
if (existingId) {
|
|
1855
|
+
from_id = existingId;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
if (createdEntityIds.has(to_id)) {
|
|
1859
|
+
to_id = createdEntityIds.get(to_id);
|
|
1860
|
+
}
|
|
1861
|
+
else if (!(0, uuid_1.validate)(to_id)) {
|
|
1862
|
+
const existingId = await this.findEntityIdByName(to_id);
|
|
1863
|
+
if (existingId) {
|
|
1864
|
+
to_id = existingId;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
const now = Date.now() * 1000;
|
|
1868
|
+
allParams[`rel_from${suffix}`] = from_id;
|
|
1869
|
+
allParams[`rel_to${suffix}`] = to_id;
|
|
1870
|
+
allParams[`rel_type${suffix}`] = relation_type;
|
|
1871
|
+
allParams[`rel_strength${suffix}`] = strength ?? 1.0;
|
|
1872
|
+
allParams[`rel_metadata${suffix}`] = metadata || {};
|
|
1873
|
+
statements.push(`
|
|
1874
|
+
{
|
|
1875
|
+
?[from_id, to_id, relation_type, created_at, strength, metadata] <- [
|
|
1876
|
+
[$rel_from${suffix}, $rel_to${suffix}, $rel_type${suffix}, [${now}, true], $rel_strength${suffix}, $rel_metadata${suffix}]
|
|
1877
|
+
] :insert relationship {from_id, to_id, relation_type, created_at => strength, metadata}
|
|
1878
|
+
}
|
|
1879
|
+
`);
|
|
1880
|
+
results.push({ action: "create_relation", from_id, to_id, relation_type });
|
|
1881
|
+
break;
|
|
1882
|
+
}
|
|
1883
|
+
case "delete_entity": {
|
|
1884
|
+
const { entity_id } = params;
|
|
1885
|
+
if (!entity_id) {
|
|
1886
|
+
return { error: `Missing entity_id for delete_entity in operation ${i}` };
|
|
1887
|
+
}
|
|
1888
|
+
allParams[`target_id${suffix}`] = entity_id;
|
|
1889
|
+
// Delete observations
|
|
1890
|
+
statements.push(`
|
|
1891
|
+
{ ?[id, created_at] := *observation{id, entity_id, created_at}, entity_id = $target_id${suffix} :rm observation {id, created_at} }
|
|
1892
|
+
`);
|
|
1893
|
+
// Delete outgoing relationships
|
|
1894
|
+
statements.push(`
|
|
1895
|
+
{ ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, from_id = $target_id${suffix} :rm relationship {from_id, to_id, relation_type, created_at} }
|
|
1896
|
+
`);
|
|
1897
|
+
// Delete incoming relationships
|
|
1898
|
+
statements.push(`
|
|
1899
|
+
{ ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, to_id = $target_id${suffix} :rm relationship {from_id, to_id, relation_type, created_at} }
|
|
1900
|
+
`);
|
|
1901
|
+
// Delete entity itself
|
|
1902
|
+
statements.push(`
|
|
1903
|
+
{ ?[id, created_at] := *entity{id, created_at}, id = $target_id${suffix} :rm entity {id, created_at} }
|
|
1904
|
+
`);
|
|
1905
|
+
results.push({ action: "delete_entity", id: entity_id });
|
|
1906
|
+
break;
|
|
1907
|
+
}
|
|
1908
|
+
default:
|
|
1909
|
+
return { error: `Unknown operation: ${op.action}` };
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
const transactionQuery = statements.join("\n");
|
|
1913
|
+
console.error(`[Transaction] Executing ${statements.length} operations atomically...`);
|
|
1914
|
+
await this.db.run(transactionQuery, allParams);
|
|
1915
|
+
return {
|
|
1916
|
+
status: "success",
|
|
1917
|
+
message: `${statements.length} operations executed atomically`,
|
|
1918
|
+
results
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
catch (error) {
|
|
1922
|
+
console.error("Error in runTransaction:", error);
|
|
1923
|
+
return {
|
|
1924
|
+
error: "Transaction failed",
|
|
1925
|
+
message: error.message,
|
|
1926
|
+
cozo_display: error.display
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
async findBridgeEntities() {
|
|
1931
|
+
await this.initPromise;
|
|
1932
|
+
// Get all entities that belong to a community
|
|
1933
|
+
const communityRes = await this.db.run(`
|
|
1934
|
+
?[entity_id, community_id] := *entity_community{entity_id, community_id}
|
|
1935
|
+
`);
|
|
1936
|
+
const entityToCommunity = new Map();
|
|
1937
|
+
for (const row of communityRes.rows) {
|
|
1938
|
+
entityToCommunity.set(String(row[0]), String(row[1]));
|
|
1939
|
+
}
|
|
1940
|
+
// Get all relationships
|
|
1941
|
+
const relRes = await this.db.run(`
|
|
1942
|
+
?[from_id, to_id] := *relationship{from_id, to_id, @ "NOW"}
|
|
1943
|
+
`);
|
|
1944
|
+
const entityBridges = new Map(); // entity_id -> Set of community_ids it connects to
|
|
1945
|
+
for (const row of relRes.rows) {
|
|
1946
|
+
const fromId = String(row[0]);
|
|
1947
|
+
const toId = String(row[1]);
|
|
1948
|
+
const fromComm = entityToCommunity.get(fromId);
|
|
1949
|
+
const toComm = entityToCommunity.get(toId);
|
|
1950
|
+
if (fromComm && toComm && fromComm !== toComm) {
|
|
1951
|
+
// fromId is a bridge candidate because it connects to toComm (different community)
|
|
1952
|
+
let commsFrom = entityBridges.get(fromId);
|
|
1953
|
+
if (!commsFrom) {
|
|
1954
|
+
commsFrom = new Set();
|
|
1955
|
+
commsFrom.add(fromComm); // Add its own community
|
|
1956
|
+
entityBridges.set(fromId, commsFrom);
|
|
1957
|
+
}
|
|
1958
|
+
commsFrom.add(toComm);
|
|
1959
|
+
// toId is a bridge candidate because it connects to fromComm
|
|
1960
|
+
let commsTo = entityBridges.get(toId);
|
|
1961
|
+
if (!commsTo) {
|
|
1962
|
+
commsTo = new Set();
|
|
1963
|
+
commsTo.add(toComm); // Add its own community
|
|
1964
|
+
entityBridges.set(toId, commsTo);
|
|
1965
|
+
}
|
|
1966
|
+
commsTo.add(fromComm);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
const bridges = [];
|
|
1970
|
+
for (const [entityId, comms] of entityBridges.entries()) {
|
|
1971
|
+
if (comms.size > 1) {
|
|
1972
|
+
// Fetch entity name and type for better display
|
|
1973
|
+
const entityInfo = await this.db.run('?[name, type] := *entity{id: $id, name, type, @ "NOW"}', { id: entityId });
|
|
1974
|
+
const name = entityInfo.rows.length > 0 ? entityInfo.rows[0][0] : entityId;
|
|
1975
|
+
const type = entityInfo.rows.length > 0 ? entityInfo.rows[0][1] : "Unknown";
|
|
1976
|
+
bridges.push({
|
|
1977
|
+
entity_id: entityId,
|
|
1978
|
+
name,
|
|
1979
|
+
type,
|
|
1980
|
+
connected_communities: Array.from(comms),
|
|
1981
|
+
community_count: comms.size
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
// Sort by number of communities connected (descending)
|
|
1986
|
+
bridges.sort((a, b) => b.community_count - a.community_count);
|
|
1987
|
+
return bridges;
|
|
1988
|
+
}
|
|
1989
|
+
async graphRag(args) {
|
|
1990
|
+
await this.initPromise;
|
|
1991
|
+
return this.hybridSearch.graphRag(args);
|
|
1992
|
+
}
|
|
1993
|
+
registerTools() {
|
|
1994
|
+
const MetadataSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.any());
|
|
1995
|
+
const MutateMemorySchema = zod_1.z.discriminatedUnion("action", [
|
|
1996
|
+
zod_1.z.object({
|
|
1997
|
+
action: zod_1.z.literal("create_entity"),
|
|
1998
|
+
name: zod_1.z.string().describe("Name of the entity"),
|
|
1999
|
+
type: zod_1.z.string().describe("Type of the entity"),
|
|
2000
|
+
metadata: MetadataSchema.optional().describe("Additional metadata"),
|
|
2001
|
+
}).passthrough(),
|
|
2002
|
+
zod_1.z.object({
|
|
2003
|
+
action: zod_1.z.literal("update_entity"),
|
|
2004
|
+
id: zod_1.z.string().describe("ID of the entity to update"),
|
|
2005
|
+
name: zod_1.z.string().min(1).optional().describe("New name"),
|
|
2006
|
+
type: zod_1.z.string().min(1).optional().describe("New type"),
|
|
2007
|
+
metadata: MetadataSchema.optional().describe("New metadata"),
|
|
2008
|
+
}).passthrough(),
|
|
2009
|
+
zod_1.z.object({
|
|
2010
|
+
action: zod_1.z.literal("delete_entity"),
|
|
2011
|
+
entity_id: zod_1.z.string().describe("ID of the entity to delete"),
|
|
2012
|
+
}).passthrough(),
|
|
2013
|
+
zod_1.z.object({
|
|
2014
|
+
action: zod_1.z.literal("add_observation"),
|
|
2015
|
+
entity_id: zod_1.z.string().optional().describe("ID of the entity"),
|
|
2016
|
+
entity_name: zod_1.z.string().optional().describe("Name of the entity (will be created if not exists)"),
|
|
2017
|
+
entity_type: zod_1.z.string().optional().default("Unknown").describe("Type of the entity (only when creating)"),
|
|
2018
|
+
text: zod_1.z.string().describe("The fact or observation"),
|
|
2019
|
+
metadata: MetadataSchema.optional().describe("Additional metadata"),
|
|
2020
|
+
deduplicate: zod_1.z.boolean().optional().default(true).describe("Skip exact duplicates"),
|
|
2021
|
+
}).passthrough().refine((v) => Boolean(v.entity_id) || Boolean(v.entity_name), {
|
|
2022
|
+
message: "entity_id or entity_name is required",
|
|
2023
|
+
path: ["entity_id"],
|
|
2024
|
+
}),
|
|
2025
|
+
zod_1.z.object({
|
|
2026
|
+
action: zod_1.z.literal("create_relation"),
|
|
2027
|
+
from_id: zod_1.z.string().describe("Source entity ID"),
|
|
2028
|
+
to_id: zod_1.z.string().describe("Target entity ID"),
|
|
2029
|
+
relation_type: zod_1.z.string().nonempty().describe("Type of the relationship"),
|
|
2030
|
+
strength: zod_1.z.number().min(0).max(1).optional().default(1.0).describe("Strength of the relationship"),
|
|
2031
|
+
metadata: MetadataSchema.optional().describe("Additional metadata"),
|
|
2032
|
+
}).passthrough(),
|
|
2033
|
+
zod_1.z.object({
|
|
2034
|
+
action: zod_1.z.literal("run_transaction"),
|
|
2035
|
+
operations: zod_1.z.array(zod_1.z.discriminatedUnion("action", [
|
|
2036
|
+
zod_1.z.object({
|
|
2037
|
+
action: zod_1.z.literal("create_entity"),
|
|
2038
|
+
params: zod_1.z.union([
|
|
2039
|
+
zod_1.z.object({
|
|
2040
|
+
name: zod_1.z.string().describe("Name of the entity"),
|
|
2041
|
+
type: zod_1.z.string().describe("Type of the entity"),
|
|
2042
|
+
metadata: MetadataSchema.optional().describe("Additional metadata"),
|
|
2043
|
+
}).passthrough(),
|
|
2044
|
+
zod_1.z.string().describe("JSON string of parameters")
|
|
2045
|
+
])
|
|
2046
|
+
}),
|
|
2047
|
+
zod_1.z.object({
|
|
2048
|
+
action: zod_1.z.literal("delete_entity"),
|
|
2049
|
+
params: zod_1.z.union([
|
|
2050
|
+
zod_1.z.object({
|
|
2051
|
+
entity_id: zod_1.z.string().describe("ID of the entity to delete"),
|
|
2052
|
+
}).passthrough(),
|
|
2053
|
+
zod_1.z.string().describe("JSON string of parameters")
|
|
2054
|
+
])
|
|
2055
|
+
}),
|
|
2056
|
+
zod_1.z.object({
|
|
2057
|
+
action: zod_1.z.literal("add_observation"),
|
|
2058
|
+
params: zod_1.z.union([
|
|
2059
|
+
zod_1.z.object({
|
|
2060
|
+
entity_id: zod_1.z.string().optional().describe("ID of the entity"),
|
|
2061
|
+
entity_name: zod_1.z.string().optional().describe("Name of the entity (will be created if not exists)"),
|
|
2062
|
+
entity_type: zod_1.z.string().optional().default("Unknown").describe("Type of the entity (only when creating)"),
|
|
2063
|
+
text: zod_1.z.string().describe("The fact or observation"),
|
|
2064
|
+
metadata: MetadataSchema.optional().describe("Additional metadata"),
|
|
2065
|
+
}).passthrough().refine((v) => Boolean(v.entity_id) || Boolean(v.entity_name), {
|
|
2066
|
+
message: "entity_id or entity_name is required",
|
|
2067
|
+
path: ["entity_id"],
|
|
2068
|
+
}),
|
|
2069
|
+
zod_1.z.string().describe("JSON string of parameters")
|
|
2070
|
+
])
|
|
2071
|
+
}),
|
|
2072
|
+
zod_1.z.object({
|
|
2073
|
+
action: zod_1.z.literal("create_relation"),
|
|
2074
|
+
params: zod_1.z.union([
|
|
2075
|
+
zod_1.z.object({
|
|
2076
|
+
from_id: zod_1.z.string().describe("Source entity ID"),
|
|
2077
|
+
to_id: zod_1.z.string().describe("Target entity ID"),
|
|
2078
|
+
relation_type: zod_1.z.string().nonempty().describe("Type of the relationship"),
|
|
2079
|
+
strength: zod_1.z.number().min(0).max(1).optional().default(1.0).describe("Strength of the relationship"),
|
|
2080
|
+
metadata: MetadataSchema.optional().describe("Additional metadata"),
|
|
2081
|
+
}).passthrough(),
|
|
2082
|
+
zod_1.z.string().describe("JSON string of parameters")
|
|
2083
|
+
])
|
|
2084
|
+
}),
|
|
2085
|
+
])).describe("List of operations to be executed atomically")
|
|
2086
|
+
}).passthrough(),
|
|
2087
|
+
zod_1.z.object({
|
|
2088
|
+
action: zod_1.z.literal("add_inference_rule"),
|
|
2089
|
+
name: zod_1.z.string().describe("Name of the rule"),
|
|
2090
|
+
datalog: zod_1.z.string().describe("CozoDB Datalog Query"),
|
|
2091
|
+
}),
|
|
2092
|
+
zod_1.z.object({
|
|
2093
|
+
action: zod_1.z.literal("ingest_file"),
|
|
2094
|
+
entity_id: zod_1.z.string().optional().describe("ID of the target entity"),
|
|
2095
|
+
entity_name: zod_1.z.string().optional().describe("Name of the target entity (will be created if not exists)"),
|
|
2096
|
+
entity_type: zod_1.z.string().optional().default("Document").describe("Type of the target entity (only when creating)"),
|
|
2097
|
+
format: zod_1.z.enum(["markdown", "json"]).describe("Input format"),
|
|
2098
|
+
chunking: zod_1.z.enum(["none", "paragraphs"]).optional().default("none").describe("Chunking for Markdown"),
|
|
2099
|
+
content: zod_1.z.string().describe("File content (or LLM summary)"),
|
|
2100
|
+
metadata: MetadataSchema.optional().describe("Metadata for entity creation"),
|
|
2101
|
+
observation_metadata: MetadataSchema.optional().describe("Metadata applied to all observations"),
|
|
2102
|
+
deduplicate: zod_1.z.boolean().optional().default(true).describe("Skip exact duplicates"),
|
|
2103
|
+
max_observations: zod_1.z.number().min(1).max(200).optional().default(50).describe("Maximum number of observations"),
|
|
2104
|
+
}).refine((v) => Boolean(v.entity_id) || Boolean(v.entity_name), {
|
|
2105
|
+
message: "entity_id or entity_name is required for ingest_file",
|
|
2106
|
+
path: ["entity_id"],
|
|
2107
|
+
}),
|
|
2108
|
+
]);
|
|
2109
|
+
const MutateMemoryParameters = zod_1.z.object({
|
|
2110
|
+
action: zod_1.z
|
|
2111
|
+
.enum(["create_entity", "update_entity", "delete_entity", "add_observation", "create_relation", "run_transaction", "add_inference_rule", "ingest_file"])
|
|
2112
|
+
.describe("Action (determines which fields are required)"),
|
|
2113
|
+
name: zod_1.z.string().optional().describe("For create_entity (required) or add_inference_rule (required)"),
|
|
2114
|
+
type: zod_1.z.string().optional().describe("For create_entity (required)"),
|
|
2115
|
+
id: zod_1.z.string().optional().describe("For update_entity (required)"),
|
|
2116
|
+
entity_id: zod_1.z.string().optional().describe("For delete_entity (required); alternative to entity_name for add_observation/ingest_file"),
|
|
2117
|
+
entity_name: zod_1.z.string().optional().describe("For add_observation/ingest_file as alternative to entity_id"),
|
|
2118
|
+
entity_type: zod_1.z.string().optional().describe("Only when entity_name is used and entity is created new"),
|
|
2119
|
+
text: zod_1.z.string().optional().describe("For add_observation (required)"),
|
|
2120
|
+
datalog: zod_1.z.string().optional().describe("For add_inference_rule (required)"),
|
|
2121
|
+
format: zod_1.z.enum(["markdown", "json"]).optional().describe("For ingest_file (required)"),
|
|
2122
|
+
chunking: zod_1.z.enum(["none", "paragraphs"]).optional().describe("Optional for ingest_file (for markdown)"),
|
|
2123
|
+
content: zod_1.z.string().optional().describe("For ingest_file (required)"),
|
|
2124
|
+
observation_metadata: MetadataSchema.optional().describe("Optional for ingest_file"),
|
|
2125
|
+
deduplicate: zod_1.z.boolean().optional().describe("Optional for ingest_file and add_observation"),
|
|
2126
|
+
max_observations: zod_1.z.number().optional().describe("Optional for ingest_file"),
|
|
2127
|
+
from_id: zod_1.z.string().optional().describe("For create_relation (required)"),
|
|
2128
|
+
to_id: zod_1.z.string().optional().describe("For create_relation (required)"),
|
|
2129
|
+
relation_type: zod_1.z.string().optional().describe("For create_relation (required)"),
|
|
2130
|
+
strength: zod_1.z.number().min(0).max(1).optional().describe("Optional for create_relation"),
|
|
2131
|
+
metadata: MetadataSchema.optional().describe("Optional for create_entity/update_entity/add_observation/create_relation/ingest_file"),
|
|
2132
|
+
operations: zod_1.z.array(zod_1.z.object({
|
|
2133
|
+
action: zod_1.z.enum(["create_entity", "add_observation", "create_relation", "delete_entity"]),
|
|
2134
|
+
params: zod_1.z.any().describe("Parameters for the operation as an object")
|
|
2135
|
+
})).optional().describe("For run_transaction: List of operations to be executed atomically"),
|
|
2136
|
+
});
|
|
2137
|
+
this.mcp.addTool({
|
|
2138
|
+
name: "mutate_memory",
|
|
2139
|
+
description: `Write access to memory. Select operation via 'action'.
|
|
2140
|
+
Supported actions:
|
|
2141
|
+
- 'create_entity': Creates a new entity. Params: { name: string, type: string, metadata?: object }
|
|
2142
|
+
- 'update_entity': Updates an existing entity. Params: { id: string, name?: string, type?: string, metadata?: object }
|
|
2143
|
+
- 'delete_entity': Deletes an entity and its observations. Params: { entity_id: string }
|
|
2144
|
+
- 'add_observation': Stores a fact. Params: { entity_id?: string, entity_name?: string, entity_type?: string, text: string, metadata?: object, deduplicate?: boolean }. Automatic deduplication active (can be disabled).
|
|
2145
|
+
NOTE: Use special 'entity_id': 'global_user_profile' to store persistent user preferences (likes, work style, dislikes). These are prioritized in searches.
|
|
2146
|
+
- 'create_relation': Creates a connection between entities. Params: { from_id: string, to_id: string, relation_type: string, strength?: number (0-1), metadata?: object }. No self-references allowed.
|
|
2147
|
+
- 'run_transaction': Executes multiple operations atomically in one transaction. Params: { operations: Array<{ action: "create_entity"|"add_observation"|"create_relation", params: object }> }. Ideal for complex, related changes.
|
|
2148
|
+
- 'add_inference_rule': Adds a custom Datalog inference rule. Params: { name: string, datalog: string }.
|
|
2149
|
+
IMPORTANT: The Datalog result set MUST return exactly 5 columns: [from_id, to_id, relation_type, confidence, reason].
|
|
2150
|
+
Use '$id' as placeholder for the start entity.
|
|
2151
|
+
Available tables:
|
|
2152
|
+
- *entity{id, name, type, metadata, @ "NOW"}
|
|
2153
|
+
- *relationship{from_id, to_id, relation_type, strength, metadata, @ "NOW"}
|
|
2154
|
+
- *observation{id, entity_id, text, metadata, @ "NOW"}
|
|
2155
|
+
Example (Manager Transitivity):
|
|
2156
|
+
'?[from_id, to_id, relation_type, confidence, reason] := *relationship{from_id: $id, to_id: mid, relation_type: "manager_of", @ "NOW"}, *relationship{from_id: mid, to_id: target, relation_type: "manager_of", @ "NOW"}, from_id = $id, to_id = target, relation_type = "ober_manager_von", confidence = 0.6, reason = "Transitive Manager Path"'
|
|
2157
|
+
- 'ingest_file': Bulk import of documents (Markdown/JSON). Supports chunking (paragraphs) and automatic entity creation. Params: { entity_id | entity_name (required), format, content, ... }. Ideal for quickly populating memory from existing notes.
|
|
2158
|
+
|
|
2159
|
+
Validation: Invalid syntax or missing columns in inference rules will result in errors.`,
|
|
2160
|
+
parameters: MutateMemoryParameters,
|
|
2161
|
+
execute: async (args) => {
|
|
2162
|
+
await this.initPromise;
|
|
2163
|
+
console.error(`[mutate_memory] Call with:`, JSON.stringify(args, null, 2));
|
|
2164
|
+
// Zod discriminatedUnion is strict. We try to parse it more flexibly.
|
|
2165
|
+
const parsed = MutateMemorySchema.safeParse(args);
|
|
2166
|
+
if (!parsed.success) {
|
|
2167
|
+
console.error(`[mutate_memory] Validation error:`, JSON.stringify(parsed.error.issues, null, 2));
|
|
2168
|
+
return JSON.stringify({
|
|
2169
|
+
error: "Invalid input for action: " + args.action,
|
|
2170
|
+
issues: parsed.error.issues,
|
|
2171
|
+
received: args
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
const { action, ...rest } = parsed.data;
|
|
2175
|
+
if (action === "create_entity")
|
|
2176
|
+
return JSON.stringify(await this.createEntity(rest));
|
|
2177
|
+
if (action === "update_entity")
|
|
2178
|
+
return JSON.stringify(await this.updateEntity(rest));
|
|
2179
|
+
if (action === "add_observation")
|
|
2180
|
+
return JSON.stringify(await this.addObservation(rest));
|
|
2181
|
+
if (action === "create_relation")
|
|
2182
|
+
return JSON.stringify(await this.createRelation(rest));
|
|
2183
|
+
if (action === "run_transaction")
|
|
2184
|
+
return JSON.stringify(await this.runTransaction(rest));
|
|
2185
|
+
if (action === "delete_entity")
|
|
2186
|
+
return JSON.stringify(await this.deleteEntity({ entity_id: rest.entity_id }));
|
|
2187
|
+
if (action === "add_inference_rule")
|
|
2188
|
+
return JSON.stringify(await this.addInferenceRule(rest));
|
|
2189
|
+
if (action === "ingest_file")
|
|
2190
|
+
return JSON.stringify(await this.ingestFile(rest));
|
|
2191
|
+
return JSON.stringify({ error: "Unknown action" });
|
|
2192
|
+
},
|
|
2193
|
+
});
|
|
2194
|
+
const QueryMemorySchema = zod_1.z.discriminatedUnion("action", [
|
|
2195
|
+
zod_1.z.object({
|
|
2196
|
+
action: zod_1.z.literal("search"),
|
|
2197
|
+
query: zod_1.z.string().describe("Search query"),
|
|
2198
|
+
limit: zod_1.z.number().optional().default(10).describe("Maximum number of results"),
|
|
2199
|
+
entity_types: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by entity types"),
|
|
2200
|
+
include_entities: zod_1.z.boolean().optional().default(true).describe("Include entities in search"),
|
|
2201
|
+
include_observations: zod_1.z.boolean().optional().default(true).describe("Include observations in search"),
|
|
2202
|
+
}),
|
|
2203
|
+
zod_1.z.object({
|
|
2204
|
+
action: zod_1.z.literal("advancedSearch"),
|
|
2205
|
+
query: zod_1.z.string().describe("Search query"),
|
|
2206
|
+
limit: zod_1.z.number().optional().default(10).describe("Maximum number of results"),
|
|
2207
|
+
filters: zod_1.z.object({
|
|
2208
|
+
entityTypes: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by entity types"),
|
|
2209
|
+
metadata: zod_1.z.union([
|
|
2210
|
+
zod_1.z.record(zod_1.z.string(), zod_1.z.any()),
|
|
2211
|
+
zod_1.z.string().transform((str, ctx) => {
|
|
2212
|
+
try {
|
|
2213
|
+
return JSON.parse(str);
|
|
2214
|
+
}
|
|
2215
|
+
catch (e) {
|
|
2216
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: "Invalid JSON string for metadata" });
|
|
2217
|
+
return zod_1.z.NEVER;
|
|
2218
|
+
}
|
|
2219
|
+
})
|
|
2220
|
+
]).optional().describe("Filter by metadata (exact match)"),
|
|
2221
|
+
}).optional().describe("Filters for the search"),
|
|
2222
|
+
graphConstraints: zod_1.z.object({
|
|
2223
|
+
requiredRelations: zod_1.z.array(zod_1.z.string()).optional().describe("Only entities with these relationship types"),
|
|
2224
|
+
targetEntityIds: zod_1.z.array(zod_1.z.string()).optional().describe("Only entities connected to these target IDs"),
|
|
2225
|
+
}).optional().describe("Graph constraints"),
|
|
2226
|
+
vectorParams: zod_1.z.object({
|
|
2227
|
+
efSearch: zod_1.z.number().optional().describe("HNSW search precision"),
|
|
2228
|
+
}).optional().describe("Vector parameters"),
|
|
2229
|
+
}),
|
|
2230
|
+
zod_1.z.object({
|
|
2231
|
+
action: zod_1.z.literal("context"),
|
|
2232
|
+
query: zod_1.z.string().describe("Context query"),
|
|
2233
|
+
context_window: zod_1.z.number().min(1).max(50).optional().default(20).describe("Number of context items"),
|
|
2234
|
+
time_range_hours: zod_1.z.number().optional().describe("Time window in hours"),
|
|
2235
|
+
}),
|
|
2236
|
+
zod_1.z.object({
|
|
2237
|
+
action: zod_1.z.literal("entity_details"),
|
|
2238
|
+
entity_id: zod_1.z.string().describe("ID of the entity"),
|
|
2239
|
+
as_of: zod_1.z.string().optional().describe("Timestamp for historical query (ISO string or 'NOW')"),
|
|
2240
|
+
}),
|
|
2241
|
+
zod_1.z.object({
|
|
2242
|
+
action: zod_1.z.literal("history"),
|
|
2243
|
+
entity_id: zod_1.z.string().describe("ID of the entity"),
|
|
2244
|
+
}),
|
|
2245
|
+
zod_1.z.object({
|
|
2246
|
+
action: zod_1.z.literal("graph_rag"),
|
|
2247
|
+
query: zod_1.z.string().describe("Search query for initial vector seeds"),
|
|
2248
|
+
max_depth: zod_1.z.number().min(1).max(3).optional().default(2).describe("Maximum depth of graph expansion (Default: 2)"),
|
|
2249
|
+
limit: zod_1.z.number().optional().default(10).describe("Number of initial vector seeds"),
|
|
2250
|
+
}),
|
|
2251
|
+
zod_1.z.object({
|
|
2252
|
+
action: zod_1.z.literal("graph_walking"),
|
|
2253
|
+
query: zod_1.z.string().describe("Search query for relevance check"),
|
|
2254
|
+
start_entity_id: zod_1.z.string().optional().describe("Optional start entity (otherwise searched via vector)"),
|
|
2255
|
+
max_depth: zod_1.z.number().min(1).max(5).optional().default(3).describe("Maximum walking depth"),
|
|
2256
|
+
limit: zod_1.z.number().optional().default(5).describe("Number of results"),
|
|
2257
|
+
}),
|
|
2258
|
+
]);
|
|
2259
|
+
const QueryMemoryParameters = zod_1.z.object({
|
|
2260
|
+
action: zod_1.z
|
|
2261
|
+
.enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking"])
|
|
2262
|
+
.describe("Action (determines which fields are required)"),
|
|
2263
|
+
query: zod_1.z.string().optional().describe("Required for search/advancedSearch/context/graph_rag/graph_walking"),
|
|
2264
|
+
limit: zod_1.z.number().optional().describe("Only for search/advancedSearch/graph_rag/graph_walking"),
|
|
2265
|
+
filters: zod_1.z.any().optional().describe("Only for advancedSearch"),
|
|
2266
|
+
graphConstraints: zod_1.z.any().optional().describe("Only for advancedSearch"),
|
|
2267
|
+
vectorOptions: zod_1.z.any().optional().describe("Only for advancedSearch"),
|
|
2268
|
+
entity_types: zod_1.z.array(zod_1.z.string()).optional().describe("Only for search"),
|
|
2269
|
+
include_entities: zod_1.z.boolean().optional().describe("Only for search"),
|
|
2270
|
+
include_observations: zod_1.z.boolean().optional().describe("Only for search"),
|
|
2271
|
+
context_window: zod_1.z.number().optional().describe("Only for context"),
|
|
2272
|
+
time_range_hours: zod_1.z.number().optional().describe("Only for context"),
|
|
2273
|
+
entity_id: zod_1.z.string().optional().describe("Required for entity_details/history"),
|
|
2274
|
+
as_of: zod_1.z.string().optional().describe("Only for entity_details: ISO string or 'NOW'"),
|
|
2275
|
+
max_depth: zod_1.z.number().optional().describe("Only for graph_rag/graph_walking: Maximum expansion depth"),
|
|
2276
|
+
start_entity_id: zod_1.z.string().optional().describe("Only for graph_walking: Start entity"),
|
|
2277
|
+
});
|
|
2278
|
+
this.mcp.addTool({
|
|
2279
|
+
name: "query_memory",
|
|
2280
|
+
description: `Read access to memory. Select operation via 'action'.
|
|
2281
|
+
Supported actions:
|
|
2282
|
+
- 'search': Hybrid search (Vector + Keyword + Graph). Params: { query: string, limit?: number, entity_types?: string[], include_entities?: boolean, include_observations?: boolean }.
|
|
2283
|
+
NOTE: Results from user profile ('global_user_profile') are automatically boosted and prioritized.
|
|
2284
|
+
- 'advancedSearch': Advanced search with metadata filters and graph constraints. Params: { query: string, limit?: number, filters?: { entityTypes?: string[], metadata?: object }, graphConstraints?: { requiredRelations?: string[], targetEntityIds?: string[] }, vectorOptions?: { topk?: number, efSearch?: number } }.
|
|
2285
|
+
- 'context': Retrieves comprehensive context. Params: { query: string, context_window?: number, time_range_hours?: number }. Returns entities, observations, graph relationships, and implicit inference suggestions.
|
|
2286
|
+
NOTE: User profile is automatically included in context if relevant to enable personalization.
|
|
2287
|
+
- 'entity_details': Detailed view of an entity. Params: { entity_id: string, as_of?: string ('ISO-String' or 'NOW') }.
|
|
2288
|
+
- 'history': Retrieve historical evolution of an entity. Params: { entity_id: string }.
|
|
2289
|
+
- '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 }.
|
|
2290
|
+
- '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 }.
|
|
2291
|
+
|
|
2292
|
+
Notes: 'context' is ideal for exploratory questions. 'search' and 'advancedSearch' are better for targeted fact retrieval.`,
|
|
2293
|
+
parameters: QueryMemoryParameters,
|
|
2294
|
+
execute: async (args) => {
|
|
2295
|
+
await this.initPromise;
|
|
2296
|
+
const parsed = QueryMemorySchema.safeParse(args);
|
|
2297
|
+
if (!parsed.success)
|
|
2298
|
+
return JSON.stringify({ error: "Invalid input", issues: parsed.error.issues });
|
|
2299
|
+
const input = parsed.data;
|
|
2300
|
+
if (input.action === "search") {
|
|
2301
|
+
if (!input.query || input.query.trim().length === 0) {
|
|
2302
|
+
return JSON.stringify({ error: "Search query must not be empty." });
|
|
2303
|
+
}
|
|
2304
|
+
const results = await this.hybridSearch.advancedSearch({
|
|
2305
|
+
query: input.query,
|
|
2306
|
+
limit: input.limit,
|
|
2307
|
+
entityTypes: input.entity_types,
|
|
2308
|
+
includeEntities: input.include_entities,
|
|
2309
|
+
includeObservations: input.include_observations,
|
|
2310
|
+
});
|
|
2311
|
+
const conflictEntityIds = Array.from(new Set(results
|
|
2312
|
+
.map((r) => (r.name ? r.id : r.entity_id))
|
|
2313
|
+
.filter((x) => typeof x === "string" && x.length > 0)));
|
|
2314
|
+
const conflicts = await this.detectStatusConflicts(conflictEntityIds);
|
|
2315
|
+
const conflictById = new Map(conflicts.map((c) => [c.entity_id, c]));
|
|
2316
|
+
return JSON.stringify(results.map((result) => ({
|
|
2317
|
+
id: result.id,
|
|
2318
|
+
name: result.name,
|
|
2319
|
+
type: result.type,
|
|
2320
|
+
text: result.text,
|
|
2321
|
+
score: result.score,
|
|
2322
|
+
source: result.source,
|
|
2323
|
+
entity_id: result.entity_id,
|
|
2324
|
+
created_at: result.created_at,
|
|
2325
|
+
updated_at: result.updated_at,
|
|
2326
|
+
metadata: result.metadata,
|
|
2327
|
+
explanation: result.explanation,
|
|
2328
|
+
conflict_flag: conflictById.get(result.name ? result.id : result.entity_id) ?? undefined,
|
|
2329
|
+
})));
|
|
2330
|
+
}
|
|
2331
|
+
if (input.action === "advancedSearch") {
|
|
2332
|
+
if (!input.query || input.query.trim().length === 0) {
|
|
2333
|
+
return JSON.stringify({ error: "Search query must not be empty." });
|
|
2334
|
+
}
|
|
2335
|
+
const results = await this.hybridSearch.advancedSearch({
|
|
2336
|
+
query: input.query,
|
|
2337
|
+
limit: input.limit,
|
|
2338
|
+
filters: input.filters,
|
|
2339
|
+
graphConstraints: input.graphConstraints,
|
|
2340
|
+
vectorParams: input.vectorParams,
|
|
2341
|
+
});
|
|
2342
|
+
const conflictEntityIds = Array.from(new Set(results
|
|
2343
|
+
.map((r) => (r.name ? r.id : r.entity_id))
|
|
2344
|
+
.filter((x) => typeof x === "string" && x.length > 0)));
|
|
2345
|
+
const conflicts = await this.detectStatusConflicts(conflictEntityIds);
|
|
2346
|
+
const conflictById = new Map(conflicts.map((c) => [c.entity_id, c]));
|
|
2347
|
+
return JSON.stringify(results.map((result) => ({
|
|
2348
|
+
id: result.id,
|
|
2349
|
+
name: result.name,
|
|
2350
|
+
type: result.type,
|
|
2351
|
+
text: result.text,
|
|
2352
|
+
score: result.score,
|
|
2353
|
+
source: result.source,
|
|
2354
|
+
entity_id: result.entity_id,
|
|
2355
|
+
created_at: result.created_at,
|
|
2356
|
+
updated_at: result.updated_at,
|
|
2357
|
+
explanation: result.explanation,
|
|
2358
|
+
conflict_flag: conflictById.get(result.name ? result.id : result.entity_id) ?? undefined,
|
|
2359
|
+
})));
|
|
2360
|
+
}
|
|
2361
|
+
if (input.action === "context") {
|
|
2362
|
+
if (!input.query || input.query.trim().length === 0) {
|
|
2363
|
+
return JSON.stringify({ error: "Search query must not be empty." });
|
|
2364
|
+
}
|
|
2365
|
+
const searchResults = await this.hybridSearch.advancedSearch({
|
|
2366
|
+
query: input.query,
|
|
2367
|
+
limit: input.context_window,
|
|
2368
|
+
includeEntities: true,
|
|
2369
|
+
includeObservations: true,
|
|
2370
|
+
timeRangeHours: input.time_range_hours,
|
|
2371
|
+
});
|
|
2372
|
+
const entities = searchResults.filter((r) => r.name);
|
|
2373
|
+
const observations = searchResults.filter((r) => r.text);
|
|
2374
|
+
const conflictEntityIds = Array.from(new Set(searchResults
|
|
2375
|
+
.map((r) => (r.name ? r.id : r.entity_id))
|
|
2376
|
+
.filter((x) => typeof x === "string" && x.length > 0)));
|
|
2377
|
+
const conflicts = await this.detectStatusConflicts(conflictEntityIds);
|
|
2378
|
+
const conflictById = new Map(conflicts.map((c) => [c.entity_id, c]));
|
|
2379
|
+
const graphExpansion = [];
|
|
2380
|
+
for (const entity of entities) {
|
|
2381
|
+
try {
|
|
2382
|
+
const connections = await this.db.run(`
|
|
2383
|
+
?[target_name, rel_type, target_id] :=
|
|
2384
|
+
*relationship{from_id: $id, to_id: target_id, relation_type: rel_type, @ "NOW"},
|
|
2385
|
+
*entity{id: target_id, name: target_name, @ "NOW"}
|
|
2386
|
+
|
|
2387
|
+
?[target_name, rel_type, target_id] :=
|
|
2388
|
+
*relationship{from_id: target_id, to_id: $id, relation_type: rel_type, @ "NOW"},
|
|
2389
|
+
*entity{id: target_id, name: target_name, @ "NOW"}
|
|
2390
|
+
`, { id: entity.id });
|
|
2391
|
+
if (connections.rows.length > 0) {
|
|
2392
|
+
graphExpansion.push({
|
|
2393
|
+
entity: entity.name,
|
|
2394
|
+
entity_id: entity.id,
|
|
2395
|
+
connections: connections.rows.map((c) => ({
|
|
2396
|
+
target_name: c[0],
|
|
2397
|
+
relation_type: c[1],
|
|
2398
|
+
target_id: c[2],
|
|
2399
|
+
})),
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
catch (e) {
|
|
2404
|
+
console.error(`Error during graph expansion for ${entity.name}:`, e);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
const inferred_relations = [];
|
|
2408
|
+
for (const entity of entities) {
|
|
2409
|
+
try {
|
|
2410
|
+
const inferred = await this.inferenceEngine.inferImplicitRelations(entity.id);
|
|
2411
|
+
if (Array.isArray(inferred) && inferred.length > 0)
|
|
2412
|
+
inferred_relations.push(...inferred);
|
|
2413
|
+
}
|
|
2414
|
+
catch (e) {
|
|
2415
|
+
console.error(`Error during implicit inference for ${entity.name}:`, e);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
const enriched_inferred_relations = await this.formatInferredRelationsForContext(inferred_relations);
|
|
2419
|
+
const context = {
|
|
2420
|
+
query: input.query,
|
|
2421
|
+
timestamp: new Date().toISOString(),
|
|
2422
|
+
entities: entities.map((r) => ({
|
|
2423
|
+
id: r.id,
|
|
2424
|
+
name: r.name,
|
|
2425
|
+
type: r.type,
|
|
2426
|
+
relevance_score: r.score,
|
|
2427
|
+
source: r.source,
|
|
2428
|
+
uncertainty_hint: r.source === "inference" && typeof r.explanation === 'object' ? r.explanation?.details : undefined,
|
|
2429
|
+
conflict_flag: conflictById.get(r.id) ?? undefined,
|
|
2430
|
+
})),
|
|
2431
|
+
observations: observations.map((r) => ({
|
|
2432
|
+
id: r.id,
|
|
2433
|
+
text: r.text,
|
|
2434
|
+
entity_id: r.entity_id,
|
|
2435
|
+
relevance_score: r.score,
|
|
2436
|
+
source: r.source,
|
|
2437
|
+
conflict_flag: conflictById.get(r.entity_id) ?? undefined,
|
|
2438
|
+
})),
|
|
2439
|
+
graph_context: graphExpansion,
|
|
2440
|
+
inferred_relations: enriched_inferred_relations,
|
|
2441
|
+
conflict_flags: conflicts,
|
|
2442
|
+
total_results: searchResults.length,
|
|
2443
|
+
};
|
|
2444
|
+
return JSON.stringify(context);
|
|
2445
|
+
}
|
|
2446
|
+
if (input.action === "graph_rag") {
|
|
2447
|
+
if (!input.query || input.query.trim().length === 0) {
|
|
2448
|
+
return JSON.stringify({ error: "Search query must not be empty." });
|
|
2449
|
+
}
|
|
2450
|
+
const results = await this.hybridSearch.graphRag({
|
|
2451
|
+
query: input.query,
|
|
2452
|
+
limit: input.limit,
|
|
2453
|
+
graphConstraints: {
|
|
2454
|
+
maxDepth: input.max_depth
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2457
|
+
return JSON.stringify(results);
|
|
2458
|
+
}
|
|
2459
|
+
if (input.action === "graph_walking") {
|
|
2460
|
+
if (!input.query || input.query.trim().length === 0) {
|
|
2461
|
+
return JSON.stringify({ error: "Search query must not be empty." });
|
|
2462
|
+
}
|
|
2463
|
+
const results = await this.graph_walking({
|
|
2464
|
+
query: input.query,
|
|
2465
|
+
start_entity_id: input.start_entity_id,
|
|
2466
|
+
max_depth: input.max_depth,
|
|
2467
|
+
limit: input.limit
|
|
2468
|
+
});
|
|
2469
|
+
return JSON.stringify(results);
|
|
2470
|
+
}
|
|
2471
|
+
if (input.action === "entity_details") {
|
|
2472
|
+
const validitySpec = this.resolveValiditySpec(input.as_of);
|
|
2473
|
+
if (!validitySpec)
|
|
2474
|
+
return JSON.stringify({ error: "Invalid as_of format" });
|
|
2475
|
+
let entityRes, obsRes, relOutRes;
|
|
2476
|
+
if (validitySpec === '"NOW"') {
|
|
2477
|
+
entityRes = await this.db.run(`?[name, type, metadata] := *entity {id: $id, name, type, metadata, @ "NOW"}`, { id: input.entity_id });
|
|
2478
|
+
if (entityRes.rows.length === 0)
|
|
2479
|
+
return JSON.stringify({ error: "Entity not found" });
|
|
2480
|
+
obsRes = await this.db.run(`?[text, metadata] := *observation {entity_id: $id, text, metadata, @ "NOW"}`, { id: input.entity_id });
|
|
2481
|
+
relOutRes = await this.db.run(`?[target_id, type, target_name] := *relationship {from_id: $id, to_id: target_id, relation_type: type, @ "NOW"}, *entity {id: target_id, name: target_name, @ "NOW"}`, { id: input.entity_id });
|
|
2482
|
+
}
|
|
2483
|
+
else {
|
|
2484
|
+
// Use standard CozoDB @ operator
|
|
2485
|
+
entityRes = await this.db.run(`?[name, type, metadata] := *entity {id: $id, name, type, metadata, @ ${validitySpec}}`, { id: input.entity_id });
|
|
2486
|
+
if (entityRes.rows.length === 0)
|
|
2487
|
+
return JSON.stringify({ error: "Entity not found" });
|
|
2488
|
+
obsRes = await this.db.run(`?[text, metadata] := *observation {entity_id: $id, text, metadata, @ ${validitySpec}}`, { id: input.entity_id });
|
|
2489
|
+
relOutRes = await this.db.run(`?[target_id, type, target_name] := *relationship {from_id: $id, to_id: target_id, relation_type: type, @ ${validitySpec}}, *entity {id: target_id, name: target_name, @ ${validitySpec}}`, { id: input.entity_id });
|
|
2490
|
+
}
|
|
2491
|
+
return JSON.stringify({
|
|
2492
|
+
entity: { name: entityRes.rows[0][0], type: entityRes.rows[0][1], metadata: entityRes.rows[0][2] },
|
|
2493
|
+
observations: obsRes.rows.map((r) => ({ text: r[0], metadata: r[1] })),
|
|
2494
|
+
relations: relOutRes.rows.map((r) => ({ target_id: r[0], type: r[1], target_name: r[2] })),
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
if (input.action === "history") {
|
|
2498
|
+
const entityRes = await this.db.run(`?[ts, asserted, name, type, metadata] := *entity{id: $id, name, type, metadata, created_at}, ts = to_int(created_at), asserted = to_bool(created_at) :order ts`, { id: input.entity_id });
|
|
2499
|
+
if (entityRes.rows.length === 0)
|
|
2500
|
+
return JSON.stringify({ error: "Entity not found" });
|
|
2501
|
+
const obsRes = await this.db.run(`?[ts, asserted, id, text, metadata] := *observation{id, entity_id: $id, text, metadata, created_at}, ts = to_int(created_at), asserted = to_bool(created_at) :order ts`, { id: input.entity_id });
|
|
2502
|
+
const relOutRes = await this.db.run(`?[ts, asserted, target_id, type, strength, metadata] := *relationship{from_id: $id, to_id: target_id, relation_type: type, strength, metadata, created_at}, ts = to_int(created_at), asserted = to_bool(created_at) :order ts`, { id: input.entity_id });
|
|
2503
|
+
const relInRes = await this.db.run(`?[ts, asserted, source_id, type, strength, metadata] := *relationship{from_id: source_id, to_id: $id, relation_type: type, strength, metadata, created_at}, ts = to_int(created_at), asserted = to_bool(created_at) :order ts`, { id: input.entity_id });
|
|
2504
|
+
return JSON.stringify({
|
|
2505
|
+
entity_history: entityRes.rows.map((r) => ({
|
|
2506
|
+
timestamp: r[0],
|
|
2507
|
+
asserted: r[1],
|
|
2508
|
+
name: r[2],
|
|
2509
|
+
type: r[3],
|
|
2510
|
+
metadata: r[4],
|
|
2511
|
+
})),
|
|
2512
|
+
observation_history: obsRes.rows.map((r) => ({
|
|
2513
|
+
timestamp: r[0],
|
|
2514
|
+
asserted: r[1],
|
|
2515
|
+
id: r[2],
|
|
2516
|
+
text: r[3],
|
|
2517
|
+
metadata: r[4],
|
|
2518
|
+
})),
|
|
2519
|
+
relation_history: {
|
|
2520
|
+
outgoing: relOutRes.rows.map((r) => ({
|
|
2521
|
+
timestamp: r[0],
|
|
2522
|
+
asserted: r[1],
|
|
2523
|
+
target_id: r[2],
|
|
2524
|
+
type: r[3],
|
|
2525
|
+
strength: r[4],
|
|
2526
|
+
metadata: r[5],
|
|
2527
|
+
})),
|
|
2528
|
+
incoming: relInRes.rows.map((r) => ({
|
|
2529
|
+
timestamp: r[0],
|
|
2530
|
+
asserted: r[1],
|
|
2531
|
+
source_id: r[2],
|
|
2532
|
+
type: r[3],
|
|
2533
|
+
strength: r[4],
|
|
2534
|
+
metadata: r[5],
|
|
2535
|
+
})),
|
|
2536
|
+
},
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
return JSON.stringify({ error: "Unknown action" });
|
|
2540
|
+
},
|
|
2541
|
+
});
|
|
2542
|
+
const AnalyzeGraphSchema = zod_1.z.discriminatedUnion("action", [
|
|
2543
|
+
zod_1.z.object({
|
|
2544
|
+
action: zod_1.z.literal("explore"),
|
|
2545
|
+
start_entity: zod_1.z.string().describe("Start entity ID"),
|
|
2546
|
+
end_entity: zod_1.z.string().optional().describe("Target entity ID"),
|
|
2547
|
+
max_hops: zod_1.z.number().min(1).max(5).optional().default(3).describe("Maximum number of hops"),
|
|
2548
|
+
relation_types: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by relationship types"),
|
|
2549
|
+
}),
|
|
2550
|
+
zod_1.z.object({
|
|
2551
|
+
action: zod_1.z.literal("communities"),
|
|
2552
|
+
}),
|
|
2553
|
+
zod_1.z.object({
|
|
2554
|
+
action: zod_1.z.literal("pagerank"),
|
|
2555
|
+
}),
|
|
2556
|
+
zod_1.z.object({
|
|
2557
|
+
action: zod_1.z.literal("betweenness"),
|
|
2558
|
+
}),
|
|
2559
|
+
zod_1.z.object({
|
|
2560
|
+
action: zod_1.z.literal("hits"),
|
|
2561
|
+
}),
|
|
2562
|
+
zod_1.z.object({
|
|
2563
|
+
action: zod_1.z.literal("connected_components"),
|
|
2564
|
+
}),
|
|
2565
|
+
zod_1.z.object({
|
|
2566
|
+
action: zod_1.z.literal("shortest_path"),
|
|
2567
|
+
start_entity: zod_1.z.string().describe("Start entity ID"),
|
|
2568
|
+
end_entity: zod_1.z.string().describe("Target entity ID"),
|
|
2569
|
+
}),
|
|
2570
|
+
zod_1.z.object({
|
|
2571
|
+
action: zod_1.z.literal("bridge_discovery"),
|
|
2572
|
+
}),
|
|
2573
|
+
zod_1.z.object({
|
|
2574
|
+
action: zod_1.z.literal("infer_relations"),
|
|
2575
|
+
entity_id: zod_1.z.string().describe("ID of the entity"),
|
|
2576
|
+
}),
|
|
2577
|
+
zod_1.z.object({
|
|
2578
|
+
action: zod_1.z.literal("get_relation_evolution"),
|
|
2579
|
+
from_id: zod_1.z.string().describe("ID of the source entity"),
|
|
2580
|
+
to_id: zod_1.z.string().optional().describe("Optional ID of the target entity (if omitted, evolution of all outgoing relationships of the source entity is shown)"),
|
|
2581
|
+
}),
|
|
2582
|
+
zod_1.z.object({
|
|
2583
|
+
action: zod_1.z.literal("semantic_walk"),
|
|
2584
|
+
start_entity: zod_1.z.string().describe("Start entity ID"),
|
|
2585
|
+
max_depth: zod_1.z.number().optional().default(3).describe("Maximum depth (Default: 3)"),
|
|
2586
|
+
min_similarity: zod_1.z.number().optional().default(0.7).describe("Minimum similarity (0.0-1.0, Default: 0.7)"),
|
|
2587
|
+
}),
|
|
2588
|
+
zod_1.z.object({
|
|
2589
|
+
action: zod_1.z.literal("hnsw_clusters"),
|
|
2590
|
+
}),
|
|
2591
|
+
]);
|
|
2592
|
+
const AnalyzeGraphParameters = zod_1.z.object({
|
|
2593
|
+
action: zod_1.z
|
|
2594
|
+
.enum(["explore", "communities", "pagerank", "betweenness", "hits", "connected_components", "shortest_path", "bridge_discovery", "infer_relations", "get_relation_evolution", "semantic_walk", "hnsw_clusters"])
|
|
2595
|
+
.describe("Action (determines which fields are required)"),
|
|
2596
|
+
start_entity: zod_1.z.string().optional().describe("Required for explore/shortest_path/semantic_walk (Start entity ID)"),
|
|
2597
|
+
end_entity: zod_1.z.string().optional().describe("Optional for explore / required for shortest_path"),
|
|
2598
|
+
max_hops: zod_1.z.number().optional().describe("Optional for explore"),
|
|
2599
|
+
relation_types: zod_1.z.array(zod_1.z.string()).optional().describe("Optional for explore"),
|
|
2600
|
+
entity_id: zod_1.z.string().optional().describe("Required for infer_relations"),
|
|
2601
|
+
from_id: zod_1.z.string().optional().describe("Required for get_relation_evolution"),
|
|
2602
|
+
to_id: zod_1.z.string().optional().describe("Optional for get_relation_evolution"),
|
|
2603
|
+
max_depth: zod_1.z.number().optional().describe("Optional for semantic_walk"),
|
|
2604
|
+
min_similarity: zod_1.z.number().optional().describe("Optional for semantic_walk"),
|
|
2605
|
+
});
|
|
2606
|
+
this.mcp.addTool({
|
|
2607
|
+
name: "analyze_graph",
|
|
2608
|
+
description: `Graph analysis and advanced retrieval strategies. Select operation via 'action'.
|
|
2609
|
+
Supported actions:
|
|
2610
|
+
- 'explore': Navigates through the graph. Params: { start_entity: string, end_entity?: string, max_hops?: number, relation_types?: string[] }.
|
|
2611
|
+
* Without end_entity: Returns the neighborhood (up to 5 hops).
|
|
2612
|
+
* With end_entity: Finds the shortest path (BFS).
|
|
2613
|
+
- 'communities': Recomputes entity groups (communities) using Label Propagation.
|
|
2614
|
+
- 'pagerank': Calculates the importance of entities (Top 10).
|
|
2615
|
+
- 'betweenness': Finds central bridge entities (Betweenness Centrality).
|
|
2616
|
+
- 'hits': Identifies Hubs and Authorities.
|
|
2617
|
+
- 'connected_components': Identifies isolated subgraphs.
|
|
2618
|
+
- 'shortest_path': Calculates the shortest path between two entities (Dijkstra). Params: { start_entity: string, end_entity: string }.
|
|
2619
|
+
- 'bridge_discovery': Identifies bridge entities between communities.
|
|
2620
|
+
- 'infer_relations': Starts the inference engine for an entity. Params: { entity_id: string }.
|
|
2621
|
+
- 'get_relation_evolution': Tracks the temporal evolution of relationships. Params: { from_id: string, to_id?: string }.
|
|
2622
|
+
- 'semantic_walk': Performs a recursive "Graph Walk" that follows both explicit relationships and semantic similarity. Params: { start_entity: string, max_depth?: number, min_similarity?: number }.
|
|
2623
|
+
- 'hnsw_clusters': Analyzes clusters directly on the HNSW graph (Layer 0). Extremely fast as no vector calculations are needed.`,
|
|
2624
|
+
parameters: AnalyzeGraphParameters,
|
|
2625
|
+
execute: async (args) => {
|
|
2626
|
+
await this.initPromise;
|
|
2627
|
+
const parsed = AnalyzeGraphSchema.safeParse(args);
|
|
2628
|
+
if (!parsed.success)
|
|
2629
|
+
return JSON.stringify({ error: "Invalid input", issues: parsed.error.issues });
|
|
2630
|
+
const input = parsed.data;
|
|
2631
|
+
if (input.action === "infer_relations") {
|
|
2632
|
+
try {
|
|
2633
|
+
// Check if entity exists
|
|
2634
|
+
const entityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: input.entity_id });
|
|
2635
|
+
if (entityRes.rows.length === 0) {
|
|
2636
|
+
return JSON.stringify({ error: `Entity with ID '${input.entity_id}' not found` });
|
|
2637
|
+
}
|
|
2638
|
+
const suggestions = await this.inferenceEngine.inferRelations(input.entity_id);
|
|
2639
|
+
return JSON.stringify({ entity_id: input.entity_id, suggestions });
|
|
2640
|
+
}
|
|
2641
|
+
catch (error) {
|
|
2642
|
+
return JSON.stringify({ error: error.message || "Error during inference" });
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
if (input.action === "bridge_discovery") {
|
|
2646
|
+
try {
|
|
2647
|
+
const bridges = await this.findBridgeEntities();
|
|
2648
|
+
return JSON.stringify({ bridge_count: bridges.length, bridges });
|
|
2649
|
+
}
|
|
2650
|
+
catch (error) {
|
|
2651
|
+
console.error("Bridge Discovery Error:", error);
|
|
2652
|
+
return JSON.stringify({ error: error.message || "Error during bridge discovery" });
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
if (input.action === "communities") {
|
|
2656
|
+
try {
|
|
2657
|
+
const mapping = await this.recomputeCommunities();
|
|
2658
|
+
const entitiesRes = await this.db.run('?[id, name, type] := *entity{id, name, type, @ "NOW"}');
|
|
2659
|
+
const entityMap = new Map();
|
|
2660
|
+
entitiesRes.rows.forEach((r) => entityMap.set(r[0], { name: r[1], type: r[2] }));
|
|
2661
|
+
const communities = {};
|
|
2662
|
+
mapping.forEach((r) => {
|
|
2663
|
+
const communityId = String(r.community_id);
|
|
2664
|
+
const entityId = r.entity_id;
|
|
2665
|
+
const info = entityMap.get(entityId) || { name: "Unknown", type: "Unknown" };
|
|
2666
|
+
if (!communities[communityId])
|
|
2667
|
+
communities[communityId] = [];
|
|
2668
|
+
communities[communityId].push({ id: entityId, name: info.name, type: info.type });
|
|
2669
|
+
});
|
|
2670
|
+
return JSON.stringify({ community_count: Object.keys(communities).length, communities });
|
|
2671
|
+
}
|
|
2672
|
+
catch (error) {
|
|
2673
|
+
console.error("Community Detection Error:", error);
|
|
2674
|
+
return JSON.stringify({ error: error.message || "Error during community detection" });
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
if (input.action === "pagerank") {
|
|
2678
|
+
try {
|
|
2679
|
+
const results = await this.recomputePageRank();
|
|
2680
|
+
return JSON.stringify({
|
|
2681
|
+
status: "completed",
|
|
2682
|
+
entity_count: results.length,
|
|
2683
|
+
top_ranks: results.sort((a, b) => b.pagerank - a.pagerank).slice(0, 10)
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
catch (error) {
|
|
2687
|
+
console.error("PageRank Error:", error);
|
|
2688
|
+
return JSON.stringify({ error: error.message || "Error during PageRank calculation" });
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
if (input.action === "betweenness") {
|
|
2692
|
+
try {
|
|
2693
|
+
const results = await this.recomputeBetweennessCentrality();
|
|
2694
|
+
return JSON.stringify({
|
|
2695
|
+
status: "completed",
|
|
2696
|
+
entity_count: results.length,
|
|
2697
|
+
top_centrality: results.sort((a, b) => b.centrality - a.centrality).slice(0, 10)
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
catch (error) {
|
|
2701
|
+
return JSON.stringify({ error: error.message || "Error during Betweenness Centrality" });
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
if (input.action === "hits") {
|
|
2705
|
+
try {
|
|
2706
|
+
const results = await this.recomputeHITS();
|
|
2707
|
+
return JSON.stringify({
|
|
2708
|
+
status: "completed",
|
|
2709
|
+
entity_count: results.length,
|
|
2710
|
+
top_hubs: results.sort((a, b) => b.hub - a.hub).slice(0, 5),
|
|
2711
|
+
top_authorities: results.sort((a, b) => b.authority - a.authority).slice(0, 5)
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
catch (error) {
|
|
2715
|
+
return JSON.stringify({ error: error.message || "Error during HITS calculation" });
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (input.action === "connected_components") {
|
|
2719
|
+
try {
|
|
2720
|
+
const results = await this.recomputeConnectedComponents();
|
|
2721
|
+
const groups = {};
|
|
2722
|
+
results.forEach((r) => {
|
|
2723
|
+
if (!groups[r.component_id])
|
|
2724
|
+
groups[r.component_id] = [];
|
|
2725
|
+
groups[r.component_id].push(r.entity_id);
|
|
2726
|
+
});
|
|
2727
|
+
return JSON.stringify({
|
|
2728
|
+
component_count: Object.keys(groups).length,
|
|
2729
|
+
components: groups
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
catch (error) {
|
|
2733
|
+
return JSON.stringify({ error: error.message || "Error during Connected Components" });
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
if (input.action === "shortest_path") {
|
|
2737
|
+
try {
|
|
2738
|
+
const result = await this.computeShortestPath({
|
|
2739
|
+
start_entity: input.start_entity,
|
|
2740
|
+
end_entity: input.end_entity
|
|
2741
|
+
});
|
|
2742
|
+
if (!result)
|
|
2743
|
+
return JSON.stringify({ error: "No path found" });
|
|
2744
|
+
return JSON.stringify(result);
|
|
2745
|
+
}
|
|
2746
|
+
catch (error) {
|
|
2747
|
+
return JSON.stringify({ error: error.message || "Error during Shortest Path" });
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
if (input.action === "explore") {
|
|
2751
|
+
try {
|
|
2752
|
+
const result = await this.exploreGraph({
|
|
2753
|
+
start_entity: input.start_entity,
|
|
2754
|
+
end_entity: input.end_entity,
|
|
2755
|
+
max_hops: input.max_hops,
|
|
2756
|
+
relation_types: input.relation_types,
|
|
2757
|
+
});
|
|
2758
|
+
return JSON.stringify(result);
|
|
2759
|
+
}
|
|
2760
|
+
catch (error) {
|
|
2761
|
+
console.error("Error during Graph Traversal:", error);
|
|
2762
|
+
return JSON.stringify({ error: "Graph Traversal failed", details: error.message });
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (input.action === "get_relation_evolution") {
|
|
2766
|
+
try {
|
|
2767
|
+
const fromId = input.from_id;
|
|
2768
|
+
const toId = input.to_id;
|
|
2769
|
+
// Fetch entity names for better output
|
|
2770
|
+
const fromEntityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: fromId });
|
|
2771
|
+
if (fromEntityRes.rows.length === 0) {
|
|
2772
|
+
return JSON.stringify({ error: `Source entity with ID '${fromId}' not found` });
|
|
2773
|
+
}
|
|
2774
|
+
const fromName = fromEntityRes.rows[0][0];
|
|
2775
|
+
let query = "";
|
|
2776
|
+
let params = { from: fromId };
|
|
2777
|
+
if (toId) {
|
|
2778
|
+
const toEntityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: toId });
|
|
2779
|
+
if (toEntityRes.rows.length === 0) {
|
|
2780
|
+
return JSON.stringify({ error: `Target entity with ID '${toId}' not found` });
|
|
2781
|
+
}
|
|
2782
|
+
params.to = toId;
|
|
2783
|
+
query = `
|
|
2784
|
+
?[ts, asserted, target_name, rel_type, strength, metadata] :=
|
|
2785
|
+
*relationship{from_id: $from, to_id: $to, relation_type: rel_type, strength, metadata, created_at},
|
|
2786
|
+
*entity{id: $to, name: target_name, @ "NOW"},
|
|
2787
|
+
ts = to_int(created_at),
|
|
2788
|
+
asserted = to_bool(created_at)
|
|
2789
|
+
:order ts
|
|
2790
|
+
`;
|
|
2791
|
+
}
|
|
2792
|
+
else {
|
|
2793
|
+
query = `
|
|
2794
|
+
?[ts, asserted, target_name, rel_type, strength, metadata] :=
|
|
2795
|
+
*relationship{from_id: $from, to_id: target_id, relation_type: rel_type, strength, metadata, created_at},
|
|
2796
|
+
*entity{id: target_id, name: target_name, @ "NOW"},
|
|
2797
|
+
ts = to_int(created_at),
|
|
2798
|
+
asserted = to_bool(created_at)
|
|
2799
|
+
:order ts
|
|
2800
|
+
`;
|
|
2801
|
+
}
|
|
2802
|
+
const res = await this.db.run(query, params);
|
|
2803
|
+
const history = res.rows.map((r) => ({
|
|
2804
|
+
timestamp: r[0],
|
|
2805
|
+
iso_date: new Date(r[0] / 1000).toISOString(), // Cozo uses microseconds for Validity
|
|
2806
|
+
action: r[1] ? "ASSERTED" : "RETRACTED",
|
|
2807
|
+
target_name: r[2],
|
|
2808
|
+
relation_type: r[3],
|
|
2809
|
+
strength: r[4],
|
|
2810
|
+
metadata: r[5],
|
|
2811
|
+
}));
|
|
2812
|
+
// Group by relationship (target + type) to show transitions
|
|
2813
|
+
const evolution = {};
|
|
2814
|
+
history.forEach((h) => {
|
|
2815
|
+
const key = `${h.target_name}:${h.relation_type}`;
|
|
2816
|
+
if (!evolution[key])
|
|
2817
|
+
evolution[key] = [];
|
|
2818
|
+
evolution[key].push(h);
|
|
2819
|
+
});
|
|
2820
|
+
return JSON.stringify({
|
|
2821
|
+
from_name: fromName,
|
|
2822
|
+
from_id: fromId,
|
|
2823
|
+
timeline: history,
|
|
2824
|
+
grouped_evolution: evolution,
|
|
2825
|
+
description: "Shows the temporal evolution of relationships. 'ASSERTED' means the relationship was created or updated, 'RETRACTED' means it was ended/deleted."
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
catch (error) {
|
|
2829
|
+
console.error("Error during Relation Evolution:", error);
|
|
2830
|
+
return JSON.stringify({ error: error.message || "Error during Relation Evolution" });
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
if (input.action === "semantic_walk") {
|
|
2834
|
+
try {
|
|
2835
|
+
const startEntityId = input.start_entity;
|
|
2836
|
+
const maxDepth = input.max_depth || 3;
|
|
2837
|
+
const minSimilarity = input.min_similarity || 0.7;
|
|
2838
|
+
// Check if start entity exists
|
|
2839
|
+
const entityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: startEntityId });
|
|
2840
|
+
if (entityRes.rows.length === 0) {
|
|
2841
|
+
return JSON.stringify({ error: `Start entity with ID '${startEntityId}' not found` });
|
|
2842
|
+
}
|
|
2843
|
+
const startName = entityRes.rows[0][0];
|
|
2844
|
+
console.error(`[SemanticWalk] Starting walk for '${startName}' (${startEntityId}) - Depth: ${maxDepth}, MinSim: ${minSimilarity}`);
|
|
2845
|
+
const results = await this.inferenceEngine.semanticGraphWalk(startEntityId, maxDepth, minSimilarity);
|
|
2846
|
+
// Enrich results with names
|
|
2847
|
+
const enrichedResults = await Promise.all(results.map(async (r) => {
|
|
2848
|
+
const nameRes = await this.db.run('?[name, type] := *entity{id: $id, name, type, @ "NOW"}', { id: r.entity_id });
|
|
2849
|
+
const name = nameRes.rows.length > 0 ? nameRes.rows[0][0] : "Unknown";
|
|
2850
|
+
const type = nameRes.rows.length > 0 ? nameRes.rows[0][1] : "Unknown";
|
|
2851
|
+
return {
|
|
2852
|
+
...r,
|
|
2853
|
+
entity_name: name,
|
|
2854
|
+
entity_type: type
|
|
2855
|
+
};
|
|
2856
|
+
}));
|
|
2857
|
+
return JSON.stringify({
|
|
2858
|
+
start_entity: { id: startEntityId, name: startName },
|
|
2859
|
+
parameters: { max_depth: maxDepth, min_similarity: minSimilarity },
|
|
2860
|
+
found_entities: enrichedResults.length,
|
|
2861
|
+
results: enrichedResults
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
catch (error) {
|
|
2865
|
+
console.error("Error during Semantic Walk:", error);
|
|
2866
|
+
return JSON.stringify({ error: error.message || "Error during Semantic Walk" });
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
if (input.action === "hnsw_clusters") {
|
|
2870
|
+
try {
|
|
2871
|
+
const clusters = await this.inferenceEngine.analyzeHnswClusters();
|
|
2872
|
+
return JSON.stringify({ cluster_count: clusters.length, clusters });
|
|
2873
|
+
}
|
|
2874
|
+
catch (error) {
|
|
2875
|
+
return JSON.stringify({ error: error.message || "Error during HNSW Cluster Analysis" });
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
return JSON.stringify({ error: "Unknown action" });
|
|
2879
|
+
},
|
|
2880
|
+
});
|
|
2881
|
+
const ManageSystemSchema = zod_1.z.discriminatedUnion("action", [
|
|
2882
|
+
zod_1.z.object({ action: zod_1.z.literal("health") }),
|
|
2883
|
+
zod_1.z.object({
|
|
2884
|
+
action: zod_1.z.literal("snapshot_create"),
|
|
2885
|
+
metadata: MetadataSchema.optional().describe("Additional metadata for the snapshot"),
|
|
2886
|
+
}),
|
|
2887
|
+
zod_1.z.object({ action: zod_1.z.literal("snapshot_list") }),
|
|
2888
|
+
zod_1.z.object({
|
|
2889
|
+
action: zod_1.z.literal("snapshot_diff"),
|
|
2890
|
+
snapshot_id_a: zod_1.z.string().describe("First snapshot"),
|
|
2891
|
+
snapshot_id_b: zod_1.z.string().describe("Second snapshot"),
|
|
2892
|
+
}),
|
|
2893
|
+
zod_1.z.object({
|
|
2894
|
+
action: zod_1.z.literal("cleanup"),
|
|
2895
|
+
confirm: zod_1.z.boolean().describe("Must be true to confirm cleanup"),
|
|
2896
|
+
older_than_days: zod_1.z.number().min(1).max(3650).optional().default(30),
|
|
2897
|
+
max_observations: zod_1.z.number().min(1).max(200).optional().default(20),
|
|
2898
|
+
min_entity_degree: zod_1.z.number().min(0).max(100).optional().default(2),
|
|
2899
|
+
model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
|
|
2900
|
+
}),
|
|
2901
|
+
zod_1.z.object({
|
|
2902
|
+
action: zod_1.z.literal("reflect"),
|
|
2903
|
+
entity_id: zod_1.z.string().optional().describe("Optional entity ID for targeted reflection"),
|
|
2904
|
+
model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
|
|
2905
|
+
}),
|
|
2906
|
+
zod_1.z.object({
|
|
2907
|
+
action: zod_1.z.literal("clear_memory"),
|
|
2908
|
+
confirm: zod_1.z.boolean().describe("Must be true to confirm deletion"),
|
|
2909
|
+
}),
|
|
2910
|
+
]);
|
|
2911
|
+
const ManageSystemParameters = zod_1.z.object({
|
|
2912
|
+
action: zod_1.z
|
|
2913
|
+
.enum(["health", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory"])
|
|
2914
|
+
.describe("Action (determines which fields are required)"),
|
|
2915
|
+
snapshot_id_a: zod_1.z.string().optional().describe("Required for snapshot_diff"),
|
|
2916
|
+
snapshot_id_b: zod_1.z.string().optional().describe("Required for snapshot_diff"),
|
|
2917
|
+
metadata: MetadataSchema.optional().describe("Optional for snapshot_create"),
|
|
2918
|
+
confirm: zod_1.z.boolean().optional().describe("Required for cleanup/clear_memory and must be true"),
|
|
2919
|
+
older_than_days: zod_1.z.number().optional().describe("Optional for cleanup"),
|
|
2920
|
+
max_observations: zod_1.z.number().optional().describe("Optional for cleanup"),
|
|
2921
|
+
min_entity_degree: zod_1.z.number().optional().describe("Optional for cleanup"),
|
|
2922
|
+
model: zod_1.z.string().optional().describe("Optional for cleanup/reflect"),
|
|
2923
|
+
entity_id: zod_1.z.string().optional().describe("Optional for reflect"),
|
|
2924
|
+
});
|
|
2925
|
+
this.mcp.addTool({
|
|
2926
|
+
name: "manage_system",
|
|
2927
|
+
description: `System maintenance and memory management. Select operation via 'action'.
|
|
2928
|
+
Supported actions:
|
|
2929
|
+
- 'health': Status check. Returns DB counts and embedding cache statistics.
|
|
2930
|
+
- 'snapshot_create': Creates a backup point. Params: { metadata?: object }.
|
|
2931
|
+
- 'snapshot_list': Lists all available snapshots.
|
|
2932
|
+
- 'snapshot_diff': Compares two snapshots. Params: { snapshot_id_a: string, snapshot_id_b: string }.
|
|
2933
|
+
- 'cleanup': Janitor service for consolidation. Params: { confirm: boolean, older_than_days?: number, max_observations?: number, min_entity_degree?: number, model?: string }.
|
|
2934
|
+
* With confirm=false: Dry-Run (shows candidates).
|
|
2935
|
+
* With confirm=true: Merges old/isolated fragments using LLM (Executive Summary) and removes noise.
|
|
2936
|
+
- 'reflect': Reflection service. Analyzes memory for contradictions and insights. Params: { entity_id?: string, model?: string }.
|
|
2937
|
+
- 'clear_memory': Resets the entire database. Params: { confirm: boolean (must be true) }.`,
|
|
2938
|
+
parameters: ManageSystemParameters,
|
|
2939
|
+
execute: async (args) => {
|
|
2940
|
+
await this.initPromise;
|
|
2941
|
+
const parsed = ManageSystemSchema.safeParse(args);
|
|
2942
|
+
if (!parsed.success)
|
|
2943
|
+
return JSON.stringify({ error: "Invalid input", issues: parsed.error.issues });
|
|
2944
|
+
const input = parsed.data;
|
|
2945
|
+
if (input.action === "health") {
|
|
2946
|
+
try {
|
|
2947
|
+
const [entityResult, obsResult, relResult] = await Promise.all([
|
|
2948
|
+
this.db.run('?[id] := *entity{id, @ "NOW"}'),
|
|
2949
|
+
this.db.run('?[id] := *observation{id, @ "NOW"}'),
|
|
2950
|
+
this.db.run('?[from_id, to_id] := *relationship{from_id, to_id, @ "NOW"}'),
|
|
2951
|
+
]);
|
|
2952
|
+
return JSON.stringify({
|
|
2953
|
+
status: "healthy",
|
|
2954
|
+
database: {
|
|
2955
|
+
entities: entityResult.rows.length,
|
|
2956
|
+
observations: obsResult.rows.length,
|
|
2957
|
+
relations: relResult.rows.length,
|
|
2958
|
+
},
|
|
2959
|
+
performance: { embedding_cache: this.embeddingService.getCacheStats() },
|
|
2960
|
+
timestamp: new Date().toISOString(),
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
catch (error) {
|
|
2964
|
+
return JSON.stringify({ status: "error", error: error.message, timestamp: new Date().toISOString() });
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
if (input.action === "snapshot_create") {
|
|
2968
|
+
try {
|
|
2969
|
+
// Optimization: Sequential execution and count aggregation instead of full fetch
|
|
2970
|
+
const entityResult = await this.db.run('?[count(id)] := *entity{id, @ "NOW"}');
|
|
2971
|
+
const obsResult = await this.db.run('?[count(id)] := *observation{id, @ "NOW"}');
|
|
2972
|
+
const relResult = await this.db.run('?[count(from_id)] := *relationship{from_id, to_id, @ "NOW"}');
|
|
2973
|
+
const snapshot_id = (0, uuid_1.v4)();
|
|
2974
|
+
const counts = {
|
|
2975
|
+
entities: Number(entityResult.rows[0]?.[0] || 0),
|
|
2976
|
+
observations: Number(obsResult.rows[0]?.[0] || 0),
|
|
2977
|
+
relations: Number(relResult.rows[0]?.[0] || 0),
|
|
2978
|
+
};
|
|
2979
|
+
const now = Date.now();
|
|
2980
|
+
await this.db.run("?[snapshot_id, entity_count, observation_count, relation_count, metadata, created_at] <- [[$id, $e, $o, $r, $meta, $now]]:put memory_snapshot {snapshot_id => entity_count, observation_count, relation_count, metadata, created_at}", {
|
|
2981
|
+
id: snapshot_id,
|
|
2982
|
+
e: counts.entities,
|
|
2983
|
+
o: counts.observations,
|
|
2984
|
+
r: counts.relations,
|
|
2985
|
+
meta: input.metadata || {},
|
|
2986
|
+
now,
|
|
2987
|
+
});
|
|
2988
|
+
return JSON.stringify({ snapshot_id, ...counts, status: "Snapshot created" });
|
|
2989
|
+
}
|
|
2990
|
+
catch (error) {
|
|
2991
|
+
return JSON.stringify({ error: error.message || "Error creating snapshot" });
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
if (input.action === "snapshot_list") {
|
|
2995
|
+
try {
|
|
2996
|
+
const result = await this.db.run("?[id, e, o, r, meta, created_at] := *memory_snapshot{snapshot_id: id, entity_count: e, observation_count: o, relation_count: r, metadata: meta, created_at: created_at}");
|
|
2997
|
+
return JSON.stringify(result.rows.map((r) => ({
|
|
2998
|
+
snapshot_id: r[0],
|
|
2999
|
+
entity_count: r[1],
|
|
3000
|
+
observation_count: r[2],
|
|
3001
|
+
relation_count: r[3],
|
|
3002
|
+
metadata: r[4],
|
|
3003
|
+
created_at: r[5],
|
|
3004
|
+
})));
|
|
3005
|
+
}
|
|
3006
|
+
catch (error) {
|
|
3007
|
+
return JSON.stringify({ error: error.message || "Error listing snapshots" });
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
if (input.action === "snapshot_diff") {
|
|
3011
|
+
try {
|
|
3012
|
+
const [aRes, bRes] = await Promise.all([
|
|
3013
|
+
this.db.run("?[id, e, o, r, meta, created_at] := *memory_snapshot{snapshot_id: id, entity_count: e, observation_count: o, relation_count: r, metadata: meta, created_at: created_at}, id = $id :limit 1", { id: input.snapshot_id_a }),
|
|
3014
|
+
this.db.run("?[id, e, o, r, meta, created_at] := *memory_snapshot{snapshot_id: id, entity_count: e, observation_count: o, relation_count: r, metadata: meta, created_at: created_at}, id = $id :limit 1", { id: input.snapshot_id_b }),
|
|
3015
|
+
]);
|
|
3016
|
+
if (aRes.rows.length === 0 || bRes.rows.length === 0) {
|
|
3017
|
+
return JSON.stringify({
|
|
3018
|
+
error: "Snapshot not found",
|
|
3019
|
+
missing: {
|
|
3020
|
+
snapshot_id_a: aRes.rows.length === 0 ? input.snapshot_id_a : undefined,
|
|
3021
|
+
snapshot_id_b: bRes.rows.length === 0 ? input.snapshot_id_b : undefined,
|
|
3022
|
+
},
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
const a = aRes.rows[0];
|
|
3026
|
+
const b = bRes.rows[0];
|
|
3027
|
+
const aCounts = { entities: a[1], observations: a[2], relations: a[3] };
|
|
3028
|
+
const bCounts = { entities: b[1], observations: b[2], relations: b[3] };
|
|
3029
|
+
const aCreated = Number(a[5]);
|
|
3030
|
+
const bCreated = Number(b[5]);
|
|
3031
|
+
return JSON.stringify({
|
|
3032
|
+
snapshot_id_a: a[0],
|
|
3033
|
+
snapshot_id_b: b[0],
|
|
3034
|
+
created_at: { a: aCreated, b: bCreated, delta_ms: bCreated - aCreated },
|
|
3035
|
+
counts: {
|
|
3036
|
+
entities: { a: aCounts.entities, b: bCounts.entities, delta: bCounts.entities - aCounts.entities },
|
|
3037
|
+
observations: { a: aCounts.observations, b: bCounts.observations, delta: bCounts.observations - aCounts.observations },
|
|
3038
|
+
relations: { a: aCounts.relations, b: bCounts.relations, delta: bCounts.relations - aCounts.relations },
|
|
3039
|
+
},
|
|
3040
|
+
metadata: { a: a[4], b: b[4] },
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
catch (error) {
|
|
3044
|
+
return JSON.stringify({ error: error.message || "Error during snapshot diff" });
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
if (input.action === "cleanup") {
|
|
3048
|
+
try {
|
|
3049
|
+
const result = await this.janitorCleanup({
|
|
3050
|
+
confirm: Boolean(input.confirm),
|
|
3051
|
+
older_than_days: input.older_than_days,
|
|
3052
|
+
max_observations: input.max_observations,
|
|
3053
|
+
min_entity_degree: input.min_entity_degree,
|
|
3054
|
+
model: input.model,
|
|
3055
|
+
});
|
|
3056
|
+
return JSON.stringify(result);
|
|
3057
|
+
}
|
|
3058
|
+
catch (error) {
|
|
3059
|
+
return JSON.stringify({ error: error.message || "Error during cleanup" });
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
if (input.action === "reflect") {
|
|
3063
|
+
try {
|
|
3064
|
+
const result = await this.reflectMemory({
|
|
3065
|
+
entity_id: input.entity_id,
|
|
3066
|
+
model: input.model,
|
|
3067
|
+
});
|
|
3068
|
+
return JSON.stringify(result);
|
|
3069
|
+
}
|
|
3070
|
+
catch (error) {
|
|
3071
|
+
return JSON.stringify({ error: error.message || "Error during reflection" });
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
if (input.action === "clear_memory") {
|
|
3075
|
+
if (!input.confirm) {
|
|
3076
|
+
return JSON.stringify({ error: "Deletion not confirmed. Set 'confirm' to true." });
|
|
3077
|
+
}
|
|
3078
|
+
try {
|
|
3079
|
+
await this.db.run(`
|
|
3080
|
+
{ ?[id, created_at] := *observation{id, created_at} :rm observation {id, created_at} }
|
|
3081
|
+
{ ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at} :rm relationship {from_id, to_id, relation_type, created_at} }
|
|
3082
|
+
{ ?[id, created_at] := *entity{id, created_at} :rm entity {id, created_at} }
|
|
3083
|
+
`);
|
|
3084
|
+
return JSON.stringify({ status: "Memory completely cleared" });
|
|
3085
|
+
}
|
|
3086
|
+
catch (error) {
|
|
3087
|
+
return JSON.stringify({ error: error.message || "Error clearing memory" });
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
return JSON.stringify({ error: "Unknown action" });
|
|
3091
|
+
},
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
async start() {
|
|
3095
|
+
await this.mcp.start({ transportType: "stdio" });
|
|
3096
|
+
console.error("Cozo Memory MCP Server running on stdio");
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
exports.MemoryServer = MemoryServer;
|
|
3100
|
+
if (require.main === module) {
|
|
3101
|
+
const server = new MemoryServer();
|
|
3102
|
+
server.start().catch((err) => {
|
|
3103
|
+
console.error("Server could not be started:", err);
|
|
3104
|
+
process.exit(1);
|
|
3105
|
+
});
|
|
3106
|
+
}
|