@levalicious/server-memory 0.0.11 → 0.0.13
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/binding.gyp +16 -0
- package/dist/index.js +4 -0
- package/dist/scripts/migrate-jsonl.js +169 -0
- package/dist/scripts/verify-migration.js +39 -0
- package/dist/server.js +711 -404
- package/dist/src/graphfile.js +560 -0
- package/dist/src/memoryfile.js +134 -0
- package/dist/src/pagerank.js +78 -0
- package/dist/src/stringtable.js +373 -0
- package/dist/tests/concurrency.test.js +189 -0
- package/dist/tests/memory-server.test.js +135 -24
- package/native/binding.c +340 -0
- package/native/memoryfile.c +343 -0
- package/native/memoryfile.h +82 -0
- package/package.json +8 -4
package/dist/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
import { promises as fs } from 'fs';
|
|
5
4
|
import { randomBytes } from 'crypto';
|
|
6
5
|
import path from 'path';
|
|
7
6
|
import { fileURLToPath } from 'url';
|
|
8
|
-
import
|
|
7
|
+
import { GraphFile, DIR_FORWARD, DIR_BACKWARD } from './src/graphfile.js';
|
|
8
|
+
import { StringTable } from './src/stringtable.js';
|
|
9
|
+
import { structuralSample } from './src/pagerank.js';
|
|
9
10
|
// Define memory file path using environment variable with fallback
|
|
10
11
|
const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.json');
|
|
11
12
|
// If MEMORY_FILE_PATH is just a filename, put it in the same directory as the script
|
|
@@ -17,17 +18,43 @@ const DEFAULT_MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH
|
|
|
17
18
|
/**
|
|
18
19
|
* Sort entities by the specified field and direction.
|
|
19
20
|
* Returns a new array (does not mutate input).
|
|
20
|
-
*
|
|
21
|
+
* Defaults to 'llmrank' when sortBy is undefined.
|
|
22
|
+
*
|
|
23
|
+
* For 'pagerank' sort: uses structural rank (desc by default).
|
|
24
|
+
* For 'llmrank' sort: uses walker rank, falls back to structural rank on tie, then random.
|
|
25
|
+
* Both rank sorts require rankMaps parameter.
|
|
21
26
|
*/
|
|
22
|
-
function sortEntities(entities, sortBy, sortDir) {
|
|
23
|
-
if (!sortBy)
|
|
24
|
-
return entities; // No sorting - preserve current behavior
|
|
27
|
+
function sortEntities(entities, sortBy = "llmrank", sortDir, rankMaps) {
|
|
25
28
|
const dir = sortDir ?? (sortBy === "name" ? "asc" : "desc");
|
|
26
29
|
const mult = dir === "asc" ? 1 : -1;
|
|
27
30
|
return [...entities].sort((a, b) => {
|
|
28
31
|
if (sortBy === "name") {
|
|
29
32
|
return mult * a.name.localeCompare(b.name);
|
|
30
33
|
}
|
|
34
|
+
if (sortBy === "pagerank") {
|
|
35
|
+
const aRank = rankMaps?.structural.get(a.name) ?? 0;
|
|
36
|
+
const bRank = rankMaps?.structural.get(b.name) ?? 0;
|
|
37
|
+
const diff = aRank - bRank;
|
|
38
|
+
if (diff !== 0)
|
|
39
|
+
return mult * diff;
|
|
40
|
+
return Math.random() - 0.5; // random tiebreak
|
|
41
|
+
}
|
|
42
|
+
if (sortBy === "llmrank") {
|
|
43
|
+
// Primary: walker rank
|
|
44
|
+
const aWalker = rankMaps?.walker.get(a.name) ?? 0;
|
|
45
|
+
const bWalker = rankMaps?.walker.get(b.name) ?? 0;
|
|
46
|
+
const walkerDiff = aWalker - bWalker;
|
|
47
|
+
if (walkerDiff !== 0)
|
|
48
|
+
return mult * walkerDiff;
|
|
49
|
+
// Fallback: structural rank
|
|
50
|
+
const aStruct = rankMaps?.structural.get(a.name) ?? 0;
|
|
51
|
+
const bStruct = rankMaps?.structural.get(b.name) ?? 0;
|
|
52
|
+
const structDiff = aStruct - bStruct;
|
|
53
|
+
if (structDiff !== 0)
|
|
54
|
+
return mult * structDiff;
|
|
55
|
+
// Final: random tiebreak
|
|
56
|
+
return Math.random() - 0.5;
|
|
57
|
+
}
|
|
31
58
|
// For timestamps, treat undefined as 0 (oldest)
|
|
32
59
|
const aVal = a[sortBy] ?? 0;
|
|
33
60
|
const bVal = b[sortBy] ?? 0;
|
|
@@ -36,17 +63,36 @@ function sortEntities(entities, sortBy, sortDir) {
|
|
|
36
63
|
}
|
|
37
64
|
/**
|
|
38
65
|
* Sort neighbors by the specified field and direction.
|
|
39
|
-
*
|
|
66
|
+
* Defaults to 'llmrank' when sortBy is undefined.
|
|
40
67
|
*/
|
|
41
|
-
function sortNeighbors(neighbors, sortBy, sortDir) {
|
|
42
|
-
if (!sortBy)
|
|
43
|
-
return neighbors;
|
|
68
|
+
function sortNeighbors(neighbors, sortBy = "llmrank", sortDir, rankMaps) {
|
|
44
69
|
const dir = sortDir ?? (sortBy === "name" ? "asc" : "desc");
|
|
45
70
|
const mult = dir === "asc" ? 1 : -1;
|
|
46
71
|
return [...neighbors].sort((a, b) => {
|
|
47
72
|
if (sortBy === "name") {
|
|
48
73
|
return mult * a.name.localeCompare(b.name);
|
|
49
74
|
}
|
|
75
|
+
if (sortBy === "pagerank") {
|
|
76
|
+
const aRank = rankMaps?.structural.get(a.name) ?? 0;
|
|
77
|
+
const bRank = rankMaps?.structural.get(b.name) ?? 0;
|
|
78
|
+
const diff = aRank - bRank;
|
|
79
|
+
if (diff !== 0)
|
|
80
|
+
return mult * diff;
|
|
81
|
+
return Math.random() - 0.5;
|
|
82
|
+
}
|
|
83
|
+
if (sortBy === "llmrank") {
|
|
84
|
+
const aWalker = rankMaps?.walker.get(a.name) ?? 0;
|
|
85
|
+
const bWalker = rankMaps?.walker.get(b.name) ?? 0;
|
|
86
|
+
const walkerDiff = aWalker - bWalker;
|
|
87
|
+
if (walkerDiff !== 0)
|
|
88
|
+
return mult * walkerDiff;
|
|
89
|
+
const aStruct = rankMaps?.structural.get(a.name) ?? 0;
|
|
90
|
+
const bStruct = rankMaps?.structural.get(b.name) ?? 0;
|
|
91
|
+
const structDiff = aStruct - bStruct;
|
|
92
|
+
if (structDiff !== 0)
|
|
93
|
+
return mult * structDiff;
|
|
94
|
+
return Math.random() - 0.5;
|
|
95
|
+
}
|
|
50
96
|
const aVal = a[sortBy] ?? 0;
|
|
51
97
|
const bVal = b[sortBy] ?? 0;
|
|
52
98
|
return mult * (aVal - bVal);
|
|
@@ -131,428 +177,676 @@ function paginateGraph(graph, entityCursor = 0, relationCursor = 0) {
|
|
|
131
177
|
}
|
|
132
178
|
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
|
|
133
179
|
export class KnowledgeGraphManager {
|
|
134
|
-
|
|
180
|
+
gf;
|
|
181
|
+
st;
|
|
182
|
+
/** In-memory name→offset index, rebuilt from node log on open */
|
|
183
|
+
nameIndex;
|
|
184
|
+
/** Generation counter from last rebuildNameIndex — avoids redundant rebuilds */
|
|
185
|
+
cachedEntityCount = -1;
|
|
135
186
|
constructor(memoryFilePath = DEFAULT_MEMORY_FILE_PATH) {
|
|
136
|
-
|
|
187
|
+
// Derive binary file paths from the base path
|
|
188
|
+
const dir = path.dirname(memoryFilePath);
|
|
189
|
+
const base = path.basename(memoryFilePath, path.extname(memoryFilePath));
|
|
190
|
+
const graphPath = path.join(dir, `${base}.graph`);
|
|
191
|
+
const strPath = path.join(dir, `${base}.strings`);
|
|
192
|
+
this.st = new StringTable(strPath);
|
|
193
|
+
this.gf = new GraphFile(graphPath, this.st);
|
|
194
|
+
this.nameIndex = new Map();
|
|
195
|
+
this.rebuildNameIndex();
|
|
196
|
+
// Run initial structural sampling if graph is non-empty
|
|
197
|
+
if (this.nameIndex.size > 0) {
|
|
198
|
+
structuralSample(this.gf, 1, 0.85);
|
|
199
|
+
this.gf.sync();
|
|
200
|
+
}
|
|
137
201
|
}
|
|
138
|
-
|
|
139
|
-
|
|
202
|
+
// --- Locking helpers ---
|
|
203
|
+
/**
|
|
204
|
+
* Acquire a shared (read) lock, refresh mappings (in case another process
|
|
205
|
+
* grew the files), rebuild the name index if the entity count changed,
|
|
206
|
+
* run the callback, then release the lock.
|
|
207
|
+
*/
|
|
208
|
+
withReadLock(fn) {
|
|
209
|
+
this.gf.lockShared();
|
|
140
210
|
try {
|
|
141
|
-
|
|
211
|
+
this.gf.refresh();
|
|
212
|
+
this.st.refresh();
|
|
213
|
+
this.maybeRebuildNameIndex();
|
|
214
|
+
return fn();
|
|
142
215
|
}
|
|
143
|
-
|
|
144
|
-
|
|
216
|
+
finally {
|
|
217
|
+
this.gf.unlock();
|
|
145
218
|
}
|
|
146
|
-
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Acquire an exclusive (write) lock, refresh mappings, rebuild name index,
|
|
222
|
+
* run the callback, sync both files, then release the lock.
|
|
223
|
+
*/
|
|
224
|
+
withWriteLock(fn) {
|
|
225
|
+
this.gf.lockExclusive();
|
|
147
226
|
try {
|
|
148
|
-
|
|
227
|
+
this.gf.refresh();
|
|
228
|
+
this.st.refresh();
|
|
229
|
+
this.maybeRebuildNameIndex();
|
|
230
|
+
const result = fn();
|
|
231
|
+
this.gf.sync();
|
|
232
|
+
this.st.sync();
|
|
233
|
+
// Update cached count after write
|
|
234
|
+
this.cachedEntityCount = this.gf.getEntityCount();
|
|
235
|
+
return result;
|
|
149
236
|
}
|
|
150
237
|
finally {
|
|
151
|
-
|
|
238
|
+
this.gf.unlock();
|
|
152
239
|
}
|
|
153
240
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
241
|
+
/** Rebuild the in-memory name→offset index from the node log */
|
|
242
|
+
rebuildNameIndex() {
|
|
243
|
+
this.nameIndex.clear();
|
|
244
|
+
const offsets = this.gf.getAllEntityOffsets();
|
|
245
|
+
for (const offset of offsets) {
|
|
246
|
+
const rec = this.gf.readEntity(offset);
|
|
247
|
+
const name = this.st.get(BigInt(rec.nameId));
|
|
248
|
+
this.nameIndex.set(name, offset);
|
|
249
|
+
}
|
|
250
|
+
this.cachedEntityCount = this.gf.getEntityCount();
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Rebuild nameIndex only if the entity count changed (another process wrote).
|
|
254
|
+
* This is cheap to check (single u32 read) and avoids a full scan on every lock.
|
|
255
|
+
*/
|
|
256
|
+
maybeRebuildNameIndex() {
|
|
257
|
+
const currentCount = this.gf.getEntityCount();
|
|
258
|
+
if (currentCount !== this.cachedEntityCount) {
|
|
259
|
+
this.rebuildNameIndex();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Build rank maps from the binary store for pagerank/llmrank sorting.
|
|
263
|
+
* NOTE: Must be called inside a lock (read or write). */
|
|
264
|
+
getRankMapsUnlocked() {
|
|
265
|
+
const structural = new Map();
|
|
266
|
+
const walker = new Map();
|
|
267
|
+
const structTotal = this.gf.getStructuralTotal();
|
|
268
|
+
const walkerTotal = this.gf.getWalkerTotal();
|
|
269
|
+
for (const [name, offset] of this.nameIndex) {
|
|
270
|
+
const rec = this.gf.readEntity(offset);
|
|
271
|
+
structural.set(name, structTotal > 0n ? Number(rec.structuralVisits) / Number(structTotal) : 0);
|
|
272
|
+
walker.set(name, walkerTotal > 0n ? Number(rec.walkerVisits) / Number(walkerTotal) : 0);
|
|
166
273
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
274
|
+
return { structural, walker };
|
|
275
|
+
}
|
|
276
|
+
/** Build rank maps (acquires read lock). */
|
|
277
|
+
getRankMaps() {
|
|
278
|
+
return this.withReadLock(() => this.getRankMapsUnlocked());
|
|
279
|
+
}
|
|
280
|
+
/** Increment walker visit count for a list of entity names */
|
|
281
|
+
recordWalkerVisits(names) {
|
|
282
|
+
this.withWriteLock(() => {
|
|
283
|
+
for (const name of names) {
|
|
284
|
+
const offset = this.nameIndex.get(name);
|
|
285
|
+
if (offset !== undefined) {
|
|
286
|
+
this.gf.incrementWalkerVisit(offset);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/** Re-run structural sampling (call after graph mutations) */
|
|
292
|
+
resample() {
|
|
293
|
+
this.withWriteLock(() => {
|
|
294
|
+
if (this.nameIndex.size > 0) {
|
|
295
|
+
structuralSample(this.gf, 1, 0.85);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/** Convert an EntityRecord to the public Entity interface */
|
|
300
|
+
recordToEntity(rec) {
|
|
301
|
+
const name = this.st.get(BigInt(rec.nameId));
|
|
302
|
+
const entityType = this.st.get(BigInt(rec.typeId));
|
|
303
|
+
const observations = [];
|
|
304
|
+
if (rec.obs0Id !== 0)
|
|
305
|
+
observations.push(this.st.get(BigInt(rec.obs0Id)));
|
|
306
|
+
if (rec.obs1Id !== 0)
|
|
307
|
+
observations.push(this.st.get(BigInt(rec.obs1Id)));
|
|
308
|
+
const entity = { name, entityType, observations };
|
|
309
|
+
const mtime = Number(rec.mtime);
|
|
310
|
+
const obsMtime = Number(rec.obsMtime);
|
|
311
|
+
if (mtime > 0)
|
|
312
|
+
entity.mtime = mtime;
|
|
313
|
+
if (obsMtime > 0)
|
|
314
|
+
entity.obsMtime = obsMtime;
|
|
315
|
+
return entity;
|
|
316
|
+
}
|
|
317
|
+
/** Get all entities as Entity objects (preserves node log order = insertion order) */
|
|
318
|
+
getAllEntities() {
|
|
319
|
+
const offsets = this.gf.getAllEntityOffsets();
|
|
320
|
+
return offsets.map(o => this.recordToEntity(this.gf.readEntity(o)));
|
|
321
|
+
}
|
|
322
|
+
/** Get all relations by scanning adjacency lists (forward edges only to avoid duplication) */
|
|
323
|
+
getAllRelations() {
|
|
324
|
+
const relations = [];
|
|
325
|
+
const offsets = this.gf.getAllEntityOffsets();
|
|
326
|
+
for (const offset of offsets) {
|
|
327
|
+
const rec = this.gf.readEntity(offset);
|
|
328
|
+
const fromName = this.st.get(BigInt(rec.nameId));
|
|
329
|
+
const edges = this.gf.getEdges(offset);
|
|
330
|
+
for (const edge of edges) {
|
|
331
|
+
if (edge.direction !== DIR_FORWARD)
|
|
332
|
+
continue;
|
|
333
|
+
const targetRec = this.gf.readEntity(edge.targetOffset);
|
|
334
|
+
const toName = this.st.get(BigInt(targetRec.nameId));
|
|
335
|
+
const relationType = this.st.get(BigInt(edge.relTypeId));
|
|
336
|
+
const r = { from: fromName, to: toName, relationType };
|
|
337
|
+
const mtime = Number(edge.mtime);
|
|
338
|
+
if (mtime > 0)
|
|
339
|
+
r.mtime = mtime;
|
|
340
|
+
relations.push(r);
|
|
170
341
|
}
|
|
171
|
-
throw error;
|
|
172
342
|
}
|
|
343
|
+
return relations;
|
|
173
344
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
await fs.writeFile(this.memoryFilePath, content);
|
|
345
|
+
/** Load the full graph (entities + relations) */
|
|
346
|
+
loadGraph() {
|
|
347
|
+
return {
|
|
348
|
+
entities: this.getAllEntities(),
|
|
349
|
+
relations: this.getAllRelations(),
|
|
350
|
+
};
|
|
181
351
|
}
|
|
182
352
|
async createEntities(entities) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
353
|
+
// Validate observation limits (can do outside lock)
|
|
354
|
+
for (const entity of entities) {
|
|
355
|
+
if (entity.observations.length > 2) {
|
|
356
|
+
throw new Error(`Entity "${entity.name}" has ${entity.observations.length} observations. Maximum allowed is 2.`);
|
|
357
|
+
}
|
|
358
|
+
for (const obs of entity.observations) {
|
|
359
|
+
if (obs.length > 140) {
|
|
360
|
+
throw new Error(`Observation in entity "${entity.name}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
|
|
189
361
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return this.withWriteLock(() => {
|
|
365
|
+
const now = BigInt(Date.now());
|
|
366
|
+
const newEntities = [];
|
|
367
|
+
for (const e of entities) {
|
|
368
|
+
const existingOffset = this.nameIndex.get(e.name);
|
|
369
|
+
if (existingOffset !== undefined) {
|
|
370
|
+
const existing = this.recordToEntity(this.gf.readEntity(existingOffset));
|
|
371
|
+
const sameType = existing.entityType === e.entityType;
|
|
372
|
+
const sameObs = existing.observations.length === e.observations.length &&
|
|
373
|
+
existing.observations.every((o, i) => o === e.observations[i]);
|
|
374
|
+
if (sameType && sameObs)
|
|
375
|
+
continue;
|
|
376
|
+
throw new Error(`Entity "${e.name}" already exists with different data (type: "${existing.entityType}" vs "${e.entityType}", observations: ${existing.observations.length} vs ${e.observations.length})`);
|
|
377
|
+
}
|
|
378
|
+
const obsMtime = e.observations.length > 0 ? now : 0n;
|
|
379
|
+
const rec = this.gf.createEntity(e.name, e.entityType, now, obsMtime);
|
|
380
|
+
for (const obs of e.observations) {
|
|
381
|
+
this.gf.addObservation(rec.offset, obs, now);
|
|
194
382
|
}
|
|
383
|
+
if (e.observations.length > 0) {
|
|
384
|
+
const updated = this.gf.readEntity(rec.offset);
|
|
385
|
+
updated.mtime = now;
|
|
386
|
+
updated.obsMtime = now;
|
|
387
|
+
this.gf.updateEntity(updated);
|
|
388
|
+
}
|
|
389
|
+
this.nameIndex.set(e.name, rec.offset);
|
|
390
|
+
const newEntity = {
|
|
391
|
+
...e,
|
|
392
|
+
mtime: Number(now),
|
|
393
|
+
obsMtime: e.observations.length > 0 ? Number(now) : undefined,
|
|
394
|
+
};
|
|
395
|
+
newEntities.push(newEntity);
|
|
195
396
|
}
|
|
196
|
-
const now = Date.now();
|
|
197
|
-
const newEntities = entities
|
|
198
|
-
.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name))
|
|
199
|
-
.map(e => ({ ...e, mtime: now, obsMtime: e.observations.length > 0 ? now : undefined }));
|
|
200
|
-
graph.entities.push(...newEntities);
|
|
201
|
-
await this.saveGraph(graph);
|
|
202
397
|
return newEntities;
|
|
203
398
|
});
|
|
204
399
|
}
|
|
205
400
|
async createRelations(relations) {
|
|
206
|
-
return this.
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
401
|
+
return this.withWriteLock(() => {
|
|
402
|
+
const now = BigInt(Date.now());
|
|
403
|
+
const newRelations = [];
|
|
404
|
+
const fromOffsets = new Set();
|
|
405
|
+
for (const r of relations) {
|
|
406
|
+
const fromOffset = this.nameIndex.get(r.from);
|
|
407
|
+
const toOffset = this.nameIndex.get(r.to);
|
|
408
|
+
if (fromOffset === undefined || toOffset === undefined)
|
|
409
|
+
continue;
|
|
410
|
+
const existingEdges = this.gf.getEdges(fromOffset);
|
|
411
|
+
const relTypeId = Number(this.st.find(r.relationType) ?? -1n);
|
|
412
|
+
const isDuplicate = existingEdges.some(e => e.direction === DIR_FORWARD &&
|
|
413
|
+
e.targetOffset === toOffset &&
|
|
414
|
+
e.relTypeId === relTypeId);
|
|
415
|
+
if (isDuplicate)
|
|
416
|
+
continue;
|
|
417
|
+
const rTypeId = Number(this.st.intern(r.relationType));
|
|
418
|
+
const forwardEntry = {
|
|
419
|
+
targetOffset: toOffset,
|
|
420
|
+
direction: DIR_FORWARD,
|
|
421
|
+
relTypeId: rTypeId,
|
|
422
|
+
mtime: now,
|
|
423
|
+
};
|
|
424
|
+
this.gf.addEdge(fromOffset, forwardEntry);
|
|
425
|
+
const rTypeId2 = Number(this.st.intern(r.relationType));
|
|
426
|
+
const backwardEntry = {
|
|
427
|
+
targetOffset: fromOffset,
|
|
428
|
+
direction: DIR_BACKWARD,
|
|
429
|
+
relTypeId: rTypeId2,
|
|
430
|
+
mtime: now,
|
|
431
|
+
};
|
|
432
|
+
this.gf.addEdge(toOffset, backwardEntry);
|
|
433
|
+
fromOffsets.add(fromOffset);
|
|
434
|
+
newRelations.push({ ...r, mtime: Number(now) });
|
|
435
|
+
}
|
|
436
|
+
for (const offset of fromOffsets) {
|
|
437
|
+
const rec = this.gf.readEntity(offset);
|
|
438
|
+
rec.mtime = now;
|
|
439
|
+
this.gf.updateEntity(rec);
|
|
440
|
+
}
|
|
223
441
|
return newRelations;
|
|
224
442
|
});
|
|
225
443
|
}
|
|
226
444
|
async addObservations(observations) {
|
|
227
|
-
return this.
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
445
|
+
return this.withWriteLock(() => {
|
|
446
|
+
const results = [];
|
|
447
|
+
for (const o of observations) {
|
|
448
|
+
const offset = this.nameIndex.get(o.entityName);
|
|
449
|
+
if (offset === undefined) {
|
|
232
450
|
throw new Error(`Entity with name ${o.entityName} not found`);
|
|
233
451
|
}
|
|
234
|
-
// Validate observation character limits
|
|
235
452
|
for (const obs of o.contents) {
|
|
236
453
|
if (obs.length > 140) {
|
|
237
454
|
throw new Error(`Observation for "${o.entityName}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
|
|
238
455
|
}
|
|
239
456
|
}
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
457
|
+
const rec = this.gf.readEntity(offset);
|
|
458
|
+
const existingObs = [];
|
|
459
|
+
if (rec.obs0Id !== 0)
|
|
460
|
+
existingObs.push(this.st.get(BigInt(rec.obs0Id)));
|
|
461
|
+
if (rec.obs1Id !== 0)
|
|
462
|
+
existingObs.push(this.st.get(BigInt(rec.obs1Id)));
|
|
463
|
+
const newObservations = o.contents.filter(content => !existingObs.includes(content));
|
|
464
|
+
if (existingObs.length + newObservations.length > 2) {
|
|
465
|
+
throw new Error(`Adding ${newObservations.length} observations to "${o.entityName}" would exceed limit of 2 (currently has ${existingObs.length}).`);
|
|
244
466
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
entity.mtime = now;
|
|
249
|
-
entity.obsMtime = now;
|
|
467
|
+
const now = BigInt(Date.now());
|
|
468
|
+
for (const obs of newObservations) {
|
|
469
|
+
this.gf.addObservation(offset, obs, now);
|
|
250
470
|
}
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
await this.saveGraph(graph);
|
|
471
|
+
results.push({ entityName: o.entityName, addedObservations: newObservations });
|
|
472
|
+
}
|
|
254
473
|
return results;
|
|
255
474
|
});
|
|
256
475
|
}
|
|
257
476
|
async deleteEntities(entityNames) {
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
477
|
+
this.withWriteLock(() => {
|
|
478
|
+
for (const name of entityNames) {
|
|
479
|
+
const offset = this.nameIndex.get(name);
|
|
480
|
+
if (offset === undefined)
|
|
481
|
+
continue;
|
|
482
|
+
const edges = this.gf.getEdges(offset);
|
|
483
|
+
for (const edge of edges) {
|
|
484
|
+
const reverseDir = edge.direction === DIR_FORWARD ? DIR_BACKWARD : DIR_FORWARD;
|
|
485
|
+
this.gf.removeEdge(edge.targetOffset, offset, edge.relTypeId, reverseDir);
|
|
486
|
+
this.st.release(BigInt(edge.relTypeId));
|
|
487
|
+
}
|
|
488
|
+
this.gf.deleteEntity(offset);
|
|
489
|
+
this.nameIndex.delete(name);
|
|
490
|
+
}
|
|
263
491
|
});
|
|
264
492
|
}
|
|
265
493
|
async deleteObservations(deletions) {
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (entity.observations.length !== originalLen) {
|
|
275
|
-
entity.mtime = now;
|
|
276
|
-
entity.obsMtime = now;
|
|
277
|
-
}
|
|
494
|
+
this.withWriteLock(() => {
|
|
495
|
+
const now = BigInt(Date.now());
|
|
496
|
+
for (const d of deletions) {
|
|
497
|
+
const offset = this.nameIndex.get(d.entityName);
|
|
498
|
+
if (offset === undefined)
|
|
499
|
+
continue;
|
|
500
|
+
for (const obs of d.observations) {
|
|
501
|
+
this.gf.removeObservation(offset, obs, now);
|
|
278
502
|
}
|
|
279
|
-
}
|
|
280
|
-
await this.saveGraph(graph);
|
|
503
|
+
}
|
|
281
504
|
});
|
|
282
505
|
}
|
|
283
506
|
async deleteRelations(relations) {
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
507
|
+
this.withWriteLock(() => {
|
|
508
|
+
for (const r of relations) {
|
|
509
|
+
const fromOffset = this.nameIndex.get(r.from);
|
|
510
|
+
const toOffset = this.nameIndex.get(r.to);
|
|
511
|
+
if (fromOffset === undefined || toOffset === undefined)
|
|
512
|
+
continue;
|
|
513
|
+
const relTypeId = this.st.find(r.relationType);
|
|
514
|
+
if (relTypeId === null)
|
|
515
|
+
continue;
|
|
516
|
+
const removedForward = this.gf.removeEdge(fromOffset, toOffset, Number(relTypeId), DIR_FORWARD);
|
|
517
|
+
if (removedForward)
|
|
518
|
+
this.st.release(relTypeId);
|
|
519
|
+
const removedBackward = this.gf.removeEdge(toOffset, fromOffset, Number(relTypeId), DIR_BACKWARD);
|
|
520
|
+
if (removedBackward)
|
|
521
|
+
this.st.release(relTypeId);
|
|
522
|
+
}
|
|
290
523
|
});
|
|
291
524
|
}
|
|
292
525
|
// Regex-based search function
|
|
293
|
-
async searchNodes(query, sortBy, sortDir) {
|
|
294
|
-
const graph = await this.loadGraph();
|
|
526
|
+
async searchNodes(query, sortBy, sortDir, direction = 'forward') {
|
|
295
527
|
let regex;
|
|
296
528
|
try {
|
|
297
|
-
regex = new RegExp(query, 'i');
|
|
529
|
+
regex = new RegExp(query, 'i');
|
|
298
530
|
}
|
|
299
531
|
catch (e) {
|
|
300
532
|
throw new Error(`Invalid regex pattern: ${query}`);
|
|
301
533
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
regex.test(e.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
async openNodesFiltered(names) {
|
|
317
|
-
const graph = await this.loadGraph();
|
|
318
|
-
// Filter entities
|
|
319
|
-
const filteredEntities = graph.entities.filter(e => names.includes(e.name));
|
|
320
|
-
// Create a Set of filtered entity names for quick lookup
|
|
321
|
-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
322
|
-
// Filter relations to only include those between filtered entities
|
|
323
|
-
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
324
|
-
const filteredGraph = {
|
|
325
|
-
entities: filteredEntities,
|
|
326
|
-
relations: filteredRelations,
|
|
327
|
-
};
|
|
328
|
-
return filteredGraph;
|
|
329
|
-
}
|
|
330
|
-
async openNodes(names) {
|
|
331
|
-
const graph = await this.loadGraph();
|
|
332
|
-
// Filter entities
|
|
333
|
-
const filteredEntities = graph.entities.filter(e => names.includes(e.name));
|
|
334
|
-
// Create a Set of filtered entity names for quick lookup
|
|
335
|
-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
336
|
-
// Filter relations to only include those between filtered entities
|
|
337
|
-
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from));
|
|
338
|
-
const filteredGraph = {
|
|
339
|
-
entities: filteredEntities,
|
|
340
|
-
relations: filteredRelations,
|
|
341
|
-
};
|
|
342
|
-
return filteredGraph;
|
|
343
|
-
}
|
|
344
|
-
async getNeighbors(entityName, depth = 1, sortBy, sortDir) {
|
|
345
|
-
const graph = await this.loadGraph();
|
|
346
|
-
const visited = new Set();
|
|
347
|
-
const neighborNames = new Set();
|
|
348
|
-
const traverse = (currentName, currentDepth) => {
|
|
349
|
-
if (currentDepth > depth || visited.has(currentName))
|
|
350
|
-
return;
|
|
351
|
-
visited.add(currentName);
|
|
352
|
-
// Find all relations involving this entity
|
|
353
|
-
const connectedRelations = graph.relations.filter(r => r.from === currentName || r.to === currentName);
|
|
354
|
-
// Collect neighbor names
|
|
355
|
-
connectedRelations.forEach(r => {
|
|
356
|
-
const neighborName = r.from === currentName ? r.to : r.from;
|
|
357
|
-
neighborNames.add(neighborName);
|
|
534
|
+
return this.withReadLock(() => {
|
|
535
|
+
const allEntities = this.getAllEntities();
|
|
536
|
+
const filteredEntities = allEntities.filter(e => regex.test(e.name) ||
|
|
537
|
+
regex.test(e.entityType) ||
|
|
538
|
+
e.observations.some(o => regex.test(o)));
|
|
539
|
+
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
540
|
+
const allRelations = this.getAllRelations();
|
|
541
|
+
const filteredRelations = allRelations.filter(r => {
|
|
542
|
+
if (direction === 'forward')
|
|
543
|
+
return filteredEntityNames.has(r.from);
|
|
544
|
+
if (direction === 'backward')
|
|
545
|
+
return filteredEntityNames.has(r.to);
|
|
546
|
+
return filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to);
|
|
358
547
|
});
|
|
359
|
-
|
|
360
|
-
// Traverse to connected entities
|
|
361
|
-
connectedRelations.forEach(r => {
|
|
362
|
-
const nextEntity = r.from === currentName ? r.to : r.from;
|
|
363
|
-
traverse(nextEntity, currentDepth + 1);
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
traverse(entityName, 0);
|
|
368
|
-
// Remove the starting entity from neighbors (it's not its own neighbor)
|
|
369
|
-
neighborNames.delete(entityName);
|
|
370
|
-
// Build neighbor objects with timestamps
|
|
371
|
-
const entityMap = new Map(graph.entities.map(e => [e.name, e]));
|
|
372
|
-
const neighbors = Array.from(neighborNames).map(name => {
|
|
373
|
-
const entity = entityMap.get(name);
|
|
548
|
+
const rankMaps = this.getRankMapsUnlocked();
|
|
374
549
|
return {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
obsMtime: entity?.obsMtime,
|
|
550
|
+
entities: sortEntities(filteredEntities, sortBy, sortDir, rankMaps),
|
|
551
|
+
relations: filteredRelations,
|
|
378
552
|
};
|
|
379
553
|
});
|
|
380
|
-
return sortNeighbors(neighbors, sortBy, sortDir);
|
|
381
554
|
}
|
|
382
|
-
async
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
visited.add(current);
|
|
391
|
-
const outgoingRelations = graph.relations.filter(r => r.from === current);
|
|
392
|
-
for (const relation of outgoingRelations) {
|
|
393
|
-
const result = dfs(relation.to, target, [...path, relation], depth + 1);
|
|
394
|
-
if (result)
|
|
395
|
-
return result;
|
|
555
|
+
async openNodes(names, direction = 'forward') {
|
|
556
|
+
return this.withReadLock(() => {
|
|
557
|
+
const filteredEntities = [];
|
|
558
|
+
for (const name of names) {
|
|
559
|
+
const offset = this.nameIndex.get(name);
|
|
560
|
+
if (offset === undefined)
|
|
561
|
+
continue;
|
|
562
|
+
filteredEntities.push(this.recordToEntity(this.gf.readEntity(offset)));
|
|
396
563
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
564
|
+
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
565
|
+
const filteredRelations = [];
|
|
566
|
+
for (const name of filteredEntityNames) {
|
|
567
|
+
const offset = this.nameIndex.get(name);
|
|
568
|
+
const edges = this.gf.getEdges(offset);
|
|
569
|
+
for (const edge of edges) {
|
|
570
|
+
if (edge.direction !== DIR_FORWARD && edge.direction !== DIR_BACKWARD)
|
|
571
|
+
continue;
|
|
572
|
+
const targetRec = this.gf.readEntity(edge.targetOffset);
|
|
573
|
+
const targetName = this.st.get(BigInt(targetRec.nameId));
|
|
574
|
+
const relationType = this.st.get(BigInt(edge.relTypeId));
|
|
575
|
+
const mtime = Number(edge.mtime);
|
|
576
|
+
if (edge.direction === DIR_FORWARD) {
|
|
577
|
+
if (direction === 'backward')
|
|
578
|
+
continue;
|
|
579
|
+
const r = { from: name, to: targetName, relationType };
|
|
580
|
+
if (mtime > 0)
|
|
581
|
+
r.mtime = mtime;
|
|
582
|
+
filteredRelations.push(r);
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
if (direction === 'forward')
|
|
586
|
+
continue;
|
|
587
|
+
if (direction === 'any' && !filteredEntityNames.has(targetName))
|
|
588
|
+
continue;
|
|
589
|
+
const r = { from: targetName, to: name, relationType };
|
|
590
|
+
if (mtime > 0)
|
|
591
|
+
r.mtime = mtime;
|
|
592
|
+
filteredRelations.push(r);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return { entities: filteredEntities, relations: filteredRelations };
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
async getNeighbors(entityName, depth = 1, sortBy, sortDir, direction = 'forward') {
|
|
600
|
+
return this.withReadLock(() => {
|
|
601
|
+
const startOffset = this.nameIndex.get(entityName);
|
|
602
|
+
if (startOffset === undefined)
|
|
603
|
+
return [];
|
|
604
|
+
const visited = new Set();
|
|
605
|
+
const neighborNames = new Set();
|
|
606
|
+
const traverse = (currentName, currentDepth) => {
|
|
607
|
+
if (currentDepth > depth || visited.has(currentName))
|
|
608
|
+
return;
|
|
609
|
+
visited.add(currentName);
|
|
610
|
+
const offset = this.nameIndex.get(currentName);
|
|
611
|
+
if (offset === undefined)
|
|
612
|
+
return;
|
|
613
|
+
const edges = this.gf.getEdges(offset);
|
|
614
|
+
for (const edge of edges) {
|
|
615
|
+
if (direction === 'forward' && edge.direction !== DIR_FORWARD)
|
|
616
|
+
continue;
|
|
617
|
+
if (direction === 'backward' && edge.direction !== DIR_BACKWARD)
|
|
618
|
+
continue;
|
|
619
|
+
const targetRec = this.gf.readEntity(edge.targetOffset);
|
|
620
|
+
const neighborName = this.st.get(BigInt(targetRec.nameId));
|
|
621
|
+
neighborNames.add(neighborName);
|
|
622
|
+
if (currentDepth < depth) {
|
|
623
|
+
traverse(neighborName, currentDepth + 1);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
traverse(entityName, 0);
|
|
628
|
+
neighborNames.delete(entityName);
|
|
629
|
+
const neighbors = Array.from(neighborNames).map(name => {
|
|
630
|
+
const offset = this.nameIndex.get(name);
|
|
631
|
+
if (!offset)
|
|
632
|
+
return { name };
|
|
633
|
+
const rec = this.gf.readEntity(offset);
|
|
634
|
+
const mtime = Number(rec.mtime);
|
|
635
|
+
const obsMtime = Number(rec.obsMtime);
|
|
636
|
+
const n = { name };
|
|
637
|
+
if (mtime > 0)
|
|
638
|
+
n.mtime = mtime;
|
|
639
|
+
if (obsMtime > 0)
|
|
640
|
+
n.obsMtime = obsMtime;
|
|
641
|
+
return n;
|
|
642
|
+
});
|
|
643
|
+
const rankMaps = this.getRankMapsUnlocked();
|
|
644
|
+
return sortNeighbors(neighbors, sortBy, sortDir, rankMaps);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async findPath(fromEntity, toEntity, maxDepth = 5, direction = 'forward') {
|
|
648
|
+
return this.withReadLock(() => {
|
|
649
|
+
const visited = new Set();
|
|
650
|
+
const dfs = (current, target, pathSoFar, depth) => {
|
|
651
|
+
if (depth > maxDepth || visited.has(current))
|
|
652
|
+
return null;
|
|
653
|
+
if (current === target)
|
|
654
|
+
return pathSoFar;
|
|
655
|
+
visited.add(current);
|
|
656
|
+
const offset = this.nameIndex.get(current);
|
|
657
|
+
if (offset === undefined) {
|
|
658
|
+
visited.delete(current);
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
const edges = this.gf.getEdges(offset);
|
|
662
|
+
for (const edge of edges) {
|
|
663
|
+
if (direction === 'forward' && edge.direction !== DIR_FORWARD)
|
|
664
|
+
continue;
|
|
665
|
+
if (direction === 'backward' && edge.direction !== DIR_BACKWARD)
|
|
666
|
+
continue;
|
|
667
|
+
const targetRec = this.gf.readEntity(edge.targetOffset);
|
|
668
|
+
const nextName = this.st.get(BigInt(targetRec.nameId));
|
|
669
|
+
const relationType = this.st.get(BigInt(edge.relTypeId));
|
|
670
|
+
const mtime = Number(edge.mtime);
|
|
671
|
+
let rel;
|
|
672
|
+
if (edge.direction === DIR_FORWARD) {
|
|
673
|
+
rel = { from: current, to: nextName, relationType };
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
rel = { from: nextName, to: current, relationType };
|
|
677
|
+
}
|
|
678
|
+
if (mtime > 0)
|
|
679
|
+
rel.mtime = mtime;
|
|
680
|
+
const result = dfs(nextName, target, [...pathSoFar, rel], depth + 1);
|
|
681
|
+
if (result)
|
|
682
|
+
return result;
|
|
683
|
+
}
|
|
684
|
+
visited.delete(current);
|
|
685
|
+
return null;
|
|
686
|
+
};
|
|
687
|
+
return dfs(fromEntity, toEntity, [], 0) || [];
|
|
688
|
+
});
|
|
401
689
|
}
|
|
402
690
|
async getEntitiesByType(entityType, sortBy, sortDir) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
691
|
+
return this.withReadLock(() => {
|
|
692
|
+
const filtered = this.getAllEntities().filter(e => e.entityType === entityType);
|
|
693
|
+
const rankMaps = this.getRankMapsUnlocked();
|
|
694
|
+
return sortEntities(filtered, sortBy, sortDir, rankMaps);
|
|
695
|
+
});
|
|
406
696
|
}
|
|
407
697
|
async getEntityTypes() {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
698
|
+
return this.withReadLock(() => {
|
|
699
|
+
const types = new Set(this.getAllEntities().map(e => e.entityType));
|
|
700
|
+
return Array.from(types).sort();
|
|
701
|
+
});
|
|
411
702
|
}
|
|
412
703
|
async getRelationTypes() {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
704
|
+
return this.withReadLock(() => {
|
|
705
|
+
const types = new Set(this.getAllRelations().map(r => r.relationType));
|
|
706
|
+
return Array.from(types).sort();
|
|
707
|
+
});
|
|
416
708
|
}
|
|
417
709
|
async getStats() {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
710
|
+
return this.withReadLock(() => {
|
|
711
|
+
const entities = this.getAllEntities();
|
|
712
|
+
const relations = this.getAllRelations();
|
|
713
|
+
const entityTypes = new Set(entities.map(e => e.entityType));
|
|
714
|
+
const relationTypes = new Set(relations.map(r => r.relationType));
|
|
715
|
+
return {
|
|
716
|
+
entityCount: entities.length,
|
|
717
|
+
relationCount: relations.length,
|
|
718
|
+
entityTypes: entityTypes.size,
|
|
719
|
+
relationTypes: relationTypes.size,
|
|
720
|
+
};
|
|
721
|
+
});
|
|
427
722
|
}
|
|
428
723
|
async getOrphanedEntities(strict = false, sortBy, sortDir) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
724
|
+
return this.withReadLock(() => {
|
|
725
|
+
const entities = this.getAllEntities();
|
|
726
|
+
if (!strict) {
|
|
727
|
+
const connectedEntityNames = new Set();
|
|
728
|
+
const relations = this.getAllRelations();
|
|
729
|
+
relations.forEach(r => {
|
|
730
|
+
connectedEntityNames.add(r.from);
|
|
731
|
+
connectedEntityNames.add(r.to);
|
|
732
|
+
});
|
|
733
|
+
const orphans = entities.filter(e => !connectedEntityNames.has(e.name));
|
|
734
|
+
const rankMaps = this.getRankMapsUnlocked();
|
|
735
|
+
return sortEntities(orphans, sortBy, sortDir, rankMaps);
|
|
736
|
+
}
|
|
737
|
+
const neighbors = new Map();
|
|
738
|
+
entities.forEach(e => neighbors.set(e.name, new Set()));
|
|
739
|
+
const relations = this.getAllRelations();
|
|
740
|
+
relations.forEach(r => {
|
|
741
|
+
neighbors.get(r.from)?.add(r.to);
|
|
742
|
+
neighbors.get(r.to)?.add(r.from);
|
|
436
743
|
});
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const queue = ['Self'];
|
|
451
|
-
while (queue.length > 0) {
|
|
452
|
-
const current = queue.shift();
|
|
453
|
-
if (connectedToSelf.has(current))
|
|
454
|
-
continue;
|
|
455
|
-
connectedToSelf.add(current);
|
|
456
|
-
const currentNeighbors = neighbors.get(current);
|
|
457
|
-
if (currentNeighbors) {
|
|
458
|
-
for (const neighbor of currentNeighbors) {
|
|
459
|
-
if (!connectedToSelf.has(neighbor)) {
|
|
460
|
-
queue.push(neighbor);
|
|
744
|
+
const connectedToSelf = new Set();
|
|
745
|
+
const queue = ['Self'];
|
|
746
|
+
while (queue.length > 0) {
|
|
747
|
+
const current = queue.shift();
|
|
748
|
+
if (connectedToSelf.has(current))
|
|
749
|
+
continue;
|
|
750
|
+
connectedToSelf.add(current);
|
|
751
|
+
const currentNeighbors = neighbors.get(current);
|
|
752
|
+
if (currentNeighbors) {
|
|
753
|
+
for (const neighbor of currentNeighbors) {
|
|
754
|
+
if (!connectedToSelf.has(neighbor)) {
|
|
755
|
+
queue.push(neighbor);
|
|
756
|
+
}
|
|
461
757
|
}
|
|
462
758
|
}
|
|
463
759
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
760
|
+
const orphans = entities.filter(e => !connectedToSelf.has(e.name));
|
|
761
|
+
const rankMaps = this.getRankMapsUnlocked();
|
|
762
|
+
return sortEntities(orphans, sortBy, sortDir, rankMaps);
|
|
763
|
+
});
|
|
468
764
|
}
|
|
469
765
|
async validateGraph() {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
// Check for observation limit violations
|
|
484
|
-
graph.entities.forEach(e => {
|
|
485
|
-
const oversizedObservations = [];
|
|
486
|
-
e.observations.forEach((obs, idx) => {
|
|
487
|
-
if (obs.length > 140) {
|
|
488
|
-
oversizedObservations.push(idx);
|
|
489
|
-
}
|
|
766
|
+
return this.withReadLock(() => {
|
|
767
|
+
const entities = this.getAllEntities();
|
|
768
|
+
const relations = this.getAllRelations();
|
|
769
|
+
const entityNames = new Set(entities.map(e => e.name));
|
|
770
|
+
const missingEntities = new Set();
|
|
771
|
+
const observationViolations = [];
|
|
772
|
+
relations.forEach(r => {
|
|
773
|
+
if (!entityNames.has(r.from))
|
|
774
|
+
missingEntities.add(r.from);
|
|
775
|
+
if (!entityNames.has(r.to))
|
|
776
|
+
missingEntities.add(r.to);
|
|
490
777
|
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
778
|
+
entities.forEach(e => {
|
|
779
|
+
const oversizedObservations = [];
|
|
780
|
+
e.observations.forEach((obs, idx) => {
|
|
781
|
+
if (obs.length > 140)
|
|
782
|
+
oversizedObservations.push(idx);
|
|
496
783
|
});
|
|
497
|
-
|
|
784
|
+
if (e.observations.length > 2 || oversizedObservations.length > 0) {
|
|
785
|
+
observationViolations.push({
|
|
786
|
+
entity: e.name,
|
|
787
|
+
count: e.observations.length,
|
|
788
|
+
oversizedObservations,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
return { missingEntities: Array.from(missingEntities), observationViolations };
|
|
498
793
|
});
|
|
499
|
-
return {
|
|
500
|
-
missingEntities: Array.from(missingEntities),
|
|
501
|
-
observationViolations
|
|
502
|
-
};
|
|
503
794
|
}
|
|
504
|
-
async randomWalk(start, depth = 3, seed) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
throw new Error(`Start entity not found: ${start}`);
|
|
510
|
-
}
|
|
511
|
-
// Create seeded RNG if seed provided, otherwise use crypto.randomBytes
|
|
512
|
-
let rngState = seed ? this.hashSeed(seed) : null;
|
|
513
|
-
const random = () => {
|
|
514
|
-
if (rngState !== null) {
|
|
515
|
-
// Simple seeded PRNG (xorshift32)
|
|
516
|
-
rngState ^= rngState << 13;
|
|
517
|
-
rngState ^= rngState >>> 17;
|
|
518
|
-
rngState ^= rngState << 5;
|
|
519
|
-
return (rngState >>> 0) / 0xFFFFFFFF;
|
|
795
|
+
async randomWalk(start, depth = 3, seed, direction = 'forward') {
|
|
796
|
+
return this.withReadLock(() => {
|
|
797
|
+
const startOffset = this.nameIndex.get(start);
|
|
798
|
+
if (!startOffset) {
|
|
799
|
+
throw new Error(`Start entity not found: ${start}`);
|
|
520
800
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
801
|
+
// Create seeded RNG if seed provided
|
|
802
|
+
let rngState = seed ? this.hashSeed(seed) : null;
|
|
803
|
+
const random = () => {
|
|
804
|
+
if (rngState !== null) {
|
|
805
|
+
rngState ^= rngState << 13;
|
|
806
|
+
rngState ^= rngState >>> 17;
|
|
807
|
+
rngState ^= rngState << 5;
|
|
808
|
+
return (rngState >>> 0) / 0xFFFFFFFF;
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
return randomBytes(4).readUInt32BE() / 0xFFFFFFFF;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
const pathNames = [start];
|
|
815
|
+
let current = start;
|
|
816
|
+
for (let i = 0; i < depth; i++) {
|
|
817
|
+
const offset = this.nameIndex.get(current);
|
|
818
|
+
if (!offset)
|
|
819
|
+
break;
|
|
820
|
+
const edges = this.gf.getEdges(offset);
|
|
821
|
+
const validNeighbors = new Set();
|
|
822
|
+
for (const edge of edges) {
|
|
823
|
+
if (direction === 'forward' && edge.direction !== DIR_FORWARD)
|
|
824
|
+
continue;
|
|
825
|
+
if (direction === 'backward' && edge.direction !== DIR_BACKWARD)
|
|
826
|
+
continue;
|
|
827
|
+
const targetRec = this.gf.readEntity(edge.targetOffset);
|
|
828
|
+
const neighborName = this.st.get(BigInt(targetRec.nameId));
|
|
829
|
+
if (neighborName !== current)
|
|
830
|
+
validNeighbors.add(neighborName);
|
|
831
|
+
}
|
|
832
|
+
const neighborArr = Array.from(validNeighbors).filter(n => this.nameIndex.has(n));
|
|
833
|
+
if (neighborArr.length === 0)
|
|
834
|
+
break;
|
|
835
|
+
const idx = Math.floor(random() * neighborArr.length);
|
|
836
|
+
current = neighborArr[idx];
|
|
837
|
+
pathNames.push(current);
|
|
535
838
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (validNeighbors.length === 0)
|
|
539
|
-
break; // Dead end
|
|
540
|
-
// Pick random neighbor (uniform over entities)
|
|
541
|
-
const idx = Math.floor(random() * validNeighbors.length);
|
|
542
|
-
current = validNeighbors[idx];
|
|
543
|
-
path.push(current);
|
|
544
|
-
}
|
|
545
|
-
return { entity: current, path };
|
|
839
|
+
return { entity: current, path: pathNames };
|
|
840
|
+
});
|
|
546
841
|
}
|
|
547
842
|
hashSeed(seed) {
|
|
548
|
-
// Simple string hash to 32-bit integer
|
|
549
843
|
let hash = 0;
|
|
550
844
|
for (let i = 0; i < seed.length; i++) {
|
|
551
845
|
const char = seed.charCodeAt(i);
|
|
552
846
|
hash = ((hash << 5) - hash) + char;
|
|
553
|
-
hash = hash & hash;
|
|
847
|
+
hash = hash & hash;
|
|
554
848
|
}
|
|
555
|
-
return hash || 1;
|
|
849
|
+
return hash || 1;
|
|
556
850
|
}
|
|
557
851
|
decodeTimestamp(timestamp, relative = false) {
|
|
558
852
|
const ts = timestamp ?? Date.now();
|
|
@@ -593,43 +887,54 @@ export class KnowledgeGraphManager {
|
|
|
593
887
|
return result;
|
|
594
888
|
}
|
|
595
889
|
async addThought(observations, previousCtxId) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
890
|
+
// Validate observations (can do outside lock)
|
|
891
|
+
if (observations.length > 2) {
|
|
892
|
+
throw new Error(`Thought has ${observations.length} observations. Maximum allowed is 2.`);
|
|
893
|
+
}
|
|
894
|
+
for (const obs of observations) {
|
|
895
|
+
if (obs.length > 140) {
|
|
896
|
+
throw new Error(`Observation exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
|
|
601
897
|
}
|
|
898
|
+
}
|
|
899
|
+
return this.withWriteLock(() => {
|
|
900
|
+
const now = BigInt(Date.now());
|
|
901
|
+
const ctxId = randomBytes(12).toString('hex');
|
|
902
|
+
const obsMtime = observations.length > 0 ? now : 0n;
|
|
903
|
+
const rec = this.gf.createEntity(ctxId, 'Thought', now, obsMtime);
|
|
602
904
|
for (const obs of observations) {
|
|
603
|
-
|
|
604
|
-
throw new Error(`Observation exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
|
|
605
|
-
}
|
|
905
|
+
this.gf.addObservation(rec.offset, obs, now);
|
|
606
906
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
observations,
|
|
615
|
-
mtime: now,
|
|
616
|
-
obsMtime: observations.length > 0 ? now : undefined,
|
|
617
|
-
};
|
|
618
|
-
graph.entities.push(thoughtEntity);
|
|
619
|
-
// Link to previous thought if it exists
|
|
907
|
+
if (observations.length > 0) {
|
|
908
|
+
const updated = this.gf.readEntity(rec.offset);
|
|
909
|
+
updated.mtime = now;
|
|
910
|
+
updated.obsMtime = now;
|
|
911
|
+
this.gf.updateEntity(updated);
|
|
912
|
+
}
|
|
913
|
+
this.nameIndex.set(ctxId, rec.offset);
|
|
620
914
|
if (previousCtxId) {
|
|
621
|
-
const
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
915
|
+
const prevOffset = this.nameIndex.get(previousCtxId);
|
|
916
|
+
if (prevOffset !== undefined) {
|
|
917
|
+
const prevRec = this.gf.readEntity(prevOffset);
|
|
918
|
+
prevRec.mtime = now;
|
|
919
|
+
this.gf.updateEntity(prevRec);
|
|
920
|
+
const followsTypeId = Number(this.st.intern('follows'));
|
|
921
|
+
this.gf.addEdge(prevOffset, { targetOffset: rec.offset, direction: DIR_FORWARD, relTypeId: followsTypeId, mtime: now });
|
|
922
|
+
const followsTypeId2 = Number(this.st.intern('follows'));
|
|
923
|
+
this.gf.addEdge(rec.offset, { targetOffset: prevOffset, direction: DIR_BACKWARD, relTypeId: followsTypeId2, mtime: now });
|
|
924
|
+
const precededByTypeId = Number(this.st.intern('preceded_by'));
|
|
925
|
+
this.gf.addEdge(rec.offset, { targetOffset: prevOffset, direction: DIR_FORWARD, relTypeId: precededByTypeId, mtime: now });
|
|
926
|
+
const precededByTypeId2 = Number(this.st.intern('preceded_by'));
|
|
927
|
+
this.gf.addEdge(prevOffset, { targetOffset: rec.offset, direction: DIR_BACKWARD, relTypeId: precededByTypeId2, mtime: now });
|
|
627
928
|
}
|
|
628
929
|
}
|
|
629
|
-
await this.saveGraph(graph);
|
|
630
930
|
return { ctxId };
|
|
631
931
|
});
|
|
632
932
|
}
|
|
933
|
+
/** Close the underlying binary store files */
|
|
934
|
+
close() {
|
|
935
|
+
this.gf.close();
|
|
936
|
+
this.st.close();
|
|
937
|
+
}
|
|
633
938
|
}
|
|
634
939
|
/**
|
|
635
940
|
* Creates a configured MCP server instance with all tools registered.
|
|
@@ -645,12 +950,16 @@ export function createServer(memoryFilePath) {
|
|
|
645
950
|
sizes: ["any"]
|
|
646
951
|
}
|
|
647
952
|
],
|
|
648
|
-
version: "0.0.
|
|
953
|
+
version: "0.0.13",
|
|
649
954
|
}, {
|
|
650
955
|
capabilities: {
|
|
651
956
|
tools: {},
|
|
652
957
|
},
|
|
653
958
|
});
|
|
959
|
+
// Close binary store on server close
|
|
960
|
+
server.onclose = () => {
|
|
961
|
+
knowledgeGraphManager.close();
|
|
962
|
+
};
|
|
654
963
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
655
964
|
return {
|
|
656
965
|
tools: [
|
|
@@ -798,7 +1107,8 @@ export function createServer(memoryFilePath) {
|
|
|
798
1107
|
type: "object",
|
|
799
1108
|
properties: {
|
|
800
1109
|
query: { type: "string", description: "Regex pattern to match against entity names, types, and observations." },
|
|
801
|
-
|
|
1110
|
+
direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction filter for returned relations. Default: forward" },
|
|
1111
|
+
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name", "pagerank", "llmrank"], description: "Sort field for entities. Omit for insertion order." },
|
|
802
1112
|
sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
|
|
803
1113
|
entityCursor: { type: "number", description: "Cursor for entity pagination (from previous response's nextCursor)" },
|
|
804
1114
|
relationCursor: { type: "number", description: "Cursor for relation pagination" },
|
|
@@ -806,23 +1116,6 @@ export function createServer(memoryFilePath) {
|
|
|
806
1116
|
required: ["query"],
|
|
807
1117
|
},
|
|
808
1118
|
},
|
|
809
|
-
{
|
|
810
|
-
name: "open_nodes_filtered",
|
|
811
|
-
description: "Open specific nodes in the knowledge graph by their names, filtering relations to only those between the opened nodes. Results are paginated (max 512 chars).",
|
|
812
|
-
inputSchema: {
|
|
813
|
-
type: "object",
|
|
814
|
-
properties: {
|
|
815
|
-
names: {
|
|
816
|
-
type: "array",
|
|
817
|
-
items: { type: "string" },
|
|
818
|
-
description: "An array of entity names to retrieve",
|
|
819
|
-
},
|
|
820
|
-
entityCursor: { type: "number", description: "Cursor for entity pagination" },
|
|
821
|
-
relationCursor: { type: "number", description: "Cursor for relation pagination" },
|
|
822
|
-
},
|
|
823
|
-
required: ["names"],
|
|
824
|
-
},
|
|
825
|
-
},
|
|
826
1119
|
{
|
|
827
1120
|
name: "open_nodes",
|
|
828
1121
|
description: "Open specific nodes in the knowledge graph by their names. Results are paginated (max 512 chars).",
|
|
@@ -834,6 +1127,7 @@ export function createServer(memoryFilePath) {
|
|
|
834
1127
|
items: { type: "string" },
|
|
835
1128
|
description: "An array of entity names to retrieve",
|
|
836
1129
|
},
|
|
1130
|
+
direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction filter for returned relations. Default: forward" },
|
|
837
1131
|
entityCursor: { type: "number", description: "Cursor for entity pagination" },
|
|
838
1132
|
relationCursor: { type: "number", description: "Cursor for relation pagination" },
|
|
839
1133
|
},
|
|
@@ -848,7 +1142,8 @@ export function createServer(memoryFilePath) {
|
|
|
848
1142
|
properties: {
|
|
849
1143
|
entityName: { type: "string", description: "The name of the entity to find neighbors for" },
|
|
850
1144
|
depth: { type: "number", description: "Maximum depth to traverse (default: 1)", default: 1 },
|
|
851
|
-
|
|
1145
|
+
direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction to follow. Default: forward" },
|
|
1146
|
+
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name", "pagerank", "llmrank"], description: "Sort field for neighbors. Omit for arbitrary order." },
|
|
852
1147
|
sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
|
|
853
1148
|
cursor: { type: "number", description: "Cursor for pagination" },
|
|
854
1149
|
},
|
|
@@ -864,6 +1159,7 @@ export function createServer(memoryFilePath) {
|
|
|
864
1159
|
fromEntity: { type: "string", description: "The name of the starting entity" },
|
|
865
1160
|
toEntity: { type: "string", description: "The name of the target entity" },
|
|
866
1161
|
maxDepth: { type: "number", description: "Maximum depth to search (default: 5)", default: 5 },
|
|
1162
|
+
direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction to follow. Default: forward" },
|
|
867
1163
|
cursor: { type: "number", description: "Cursor for pagination" },
|
|
868
1164
|
},
|
|
869
1165
|
required: ["fromEntity", "toEntity"],
|
|
@@ -876,7 +1172,7 @@ export function createServer(memoryFilePath) {
|
|
|
876
1172
|
type: "object",
|
|
877
1173
|
properties: {
|
|
878
1174
|
entityType: { type: "string", description: "The type of entities to retrieve" },
|
|
879
|
-
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for entities. Omit for insertion order." },
|
|
1175
|
+
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name", "pagerank", "llmrank"], description: "Sort field for entities. Omit for insertion order." },
|
|
880
1176
|
sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
|
|
881
1177
|
cursor: { type: "number", description: "Cursor for pagination" },
|
|
882
1178
|
},
|
|
@@ -914,7 +1210,7 @@ export function createServer(memoryFilePath) {
|
|
|
914
1210
|
type: "object",
|
|
915
1211
|
properties: {
|
|
916
1212
|
strict: { type: "boolean", description: "If true, returns entities not connected to 'Self' (directly or indirectly). Default: false" },
|
|
917
|
-
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for entities. Omit for insertion order." },
|
|
1213
|
+
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name", "pagerank", "llmrank"], description: "Sort field for entities. Omit for insertion order." },
|
|
918
1214
|
sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
|
|
919
1215
|
cursor: { type: "number", description: "Cursor for pagination" },
|
|
920
1216
|
},
|
|
@@ -948,6 +1244,7 @@ export function createServer(memoryFilePath) {
|
|
|
948
1244
|
start: { type: "string", description: "Name of the entity to start the walk from." },
|
|
949
1245
|
depth: { type: "number", description: "Number of steps to take. Default: 3" },
|
|
950
1246
|
seed: { type: "string", description: "Optional seed for reproducible walks." },
|
|
1247
|
+
direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction to follow. Default: forward" },
|
|
951
1248
|
},
|
|
952
1249
|
required: ["start"],
|
|
953
1250
|
},
|
|
@@ -983,39 +1280,49 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
|
|
|
983
1280
|
throw new Error(`No arguments provided for tool: ${name}`);
|
|
984
1281
|
}
|
|
985
1282
|
switch (name) {
|
|
986
|
-
case "create_entities":
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
1283
|
+
case "create_entities": {
|
|
1284
|
+
const result = await knowledgeGraphManager.createEntities(args.entities);
|
|
1285
|
+
knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
|
|
1286
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1287
|
+
}
|
|
1288
|
+
case "create_relations": {
|
|
1289
|
+
const result = await knowledgeGraphManager.createRelations(args.relations);
|
|
1290
|
+
knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
|
|
1291
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1292
|
+
}
|
|
990
1293
|
case "add_observations":
|
|
991
1294
|
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations), null, 2) }] };
|
|
992
1295
|
case "delete_entities":
|
|
993
1296
|
await knowledgeGraphManager.deleteEntities(args.entityNames);
|
|
1297
|
+
knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
|
|
994
1298
|
return { content: [{ type: "text", text: "Entities deleted successfully" }] };
|
|
995
1299
|
case "delete_observations":
|
|
996
1300
|
await knowledgeGraphManager.deleteObservations(args.deletions);
|
|
997
1301
|
return { content: [{ type: "text", text: "Observations deleted successfully" }] };
|
|
998
1302
|
case "delete_relations":
|
|
999
1303
|
await knowledgeGraphManager.deleteRelations(args.relations);
|
|
1304
|
+
knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
|
|
1000
1305
|
return { content: [{ type: "text", text: "Relations deleted successfully" }] };
|
|
1001
1306
|
case "search_nodes": {
|
|
1002
|
-
const graph = await knowledgeGraphManager.searchNodes(args.query, args.sortBy, args.sortDir);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
case "open_nodes_filtered": {
|
|
1006
|
-
const graph = await knowledgeGraphManager.openNodesFiltered(args.names);
|
|
1307
|
+
const graph = await knowledgeGraphManager.searchNodes(args.query, args.sortBy, args.sortDir, args.direction ?? 'forward');
|
|
1308
|
+
// Record walker visits for entities that will be returned to the LLM
|
|
1309
|
+
knowledgeGraphManager.recordWalkerVisits(graph.entities.map(e => e.name));
|
|
1007
1310
|
return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
|
|
1008
1311
|
}
|
|
1009
1312
|
case "open_nodes": {
|
|
1010
|
-
const graph = await knowledgeGraphManager.openNodes(args.names);
|
|
1313
|
+
const graph = await knowledgeGraphManager.openNodes(args.names, args.direction ?? 'forward');
|
|
1314
|
+
// Record walker visits for opened nodes
|
|
1315
|
+
knowledgeGraphManager.recordWalkerVisits(graph.entities.map(e => e.name));
|
|
1011
1316
|
return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
|
|
1012
1317
|
}
|
|
1013
1318
|
case "get_neighbors": {
|
|
1014
|
-
const neighbors = await knowledgeGraphManager.getNeighbors(args.entityName, args.depth ?? 1, args.sortBy, args.sortDir);
|
|
1319
|
+
const neighbors = await knowledgeGraphManager.getNeighbors(args.entityName, args.depth ?? 1, args.sortBy, args.sortDir, args.direction ?? 'forward');
|
|
1320
|
+
// Record walker visits for returned neighbors
|
|
1321
|
+
knowledgeGraphManager.recordWalkerVisits(neighbors.map(n => n.name));
|
|
1015
1322
|
return { content: [{ type: "text", text: JSON.stringify(paginateItems(neighbors, args.cursor ?? 0)) }] };
|
|
1016
1323
|
}
|
|
1017
1324
|
case "find_path": {
|
|
1018
|
-
const path = await knowledgeGraphManager.findPath(args.fromEntity, args.toEntity, args.maxDepth);
|
|
1325
|
+
const path = await knowledgeGraphManager.findPath(args.fromEntity, args.toEntity, args.maxDepth, args.direction ?? 'forward');
|
|
1019
1326
|
return { content: [{ type: "text", text: JSON.stringify(paginateItems(path, args.cursor ?? 0)) }] };
|
|
1020
1327
|
}
|
|
1021
1328
|
case "get_entities_by_type": {
|
|
@@ -1037,7 +1344,7 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
|
|
|
1037
1344
|
case "decode_timestamp":
|
|
1038
1345
|
return { content: [{ type: "text", text: JSON.stringify(knowledgeGraphManager.decodeTimestamp(args.timestamp, args.relative ?? false)) }] };
|
|
1039
1346
|
case "random_walk": {
|
|
1040
|
-
const result = await knowledgeGraphManager.randomWalk(args.start, args.depth ?? 3, args.seed);
|
|
1347
|
+
const result = await knowledgeGraphManager.randomWalk(args.start, args.depth ?? 3, args.seed, args.direction ?? 'forward');
|
|
1041
1348
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1042
1349
|
}
|
|
1043
1350
|
case "sequentialthinking": {
|