@199-bio/engram 0.1.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/.env.example +19 -0
- package/LICENSE +21 -0
- package/LIVING_PLAN.md +180 -0
- package/PLAN.md +514 -0
- package/README.md +304 -0
- package/dist/graph/extractor.d.ts.map +1 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/knowledge-graph.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +473 -0
- package/dist/retrieval/colbert.d.ts.map +1 -0
- package/dist/retrieval/hybrid.d.ts.map +1 -0
- package/dist/retrieval/index.d.ts.map +1 -0
- package/dist/storage/database.d.ts.map +1 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/package.json +62 -0
- package/src/graph/extractor.ts +441 -0
- package/src/graph/index.ts +2 -0
- package/src/graph/knowledge-graph.ts +263 -0
- package/src/index.ts +558 -0
- package/src/retrieval/colbert-bridge.py +222 -0
- package/src/retrieval/colbert.ts +317 -0
- package/src/retrieval/hybrid.ts +218 -0
- package/src/retrieval/index.ts +2 -0
- package/src/storage/database.ts +527 -0
- package/src/storage/index.ts +1 -0
- package/tests/test-interactive.js +218 -0
- package/tests/test-mcp.sh +81 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite database layer for Engram
|
|
3
|
+
* Handles all persistent storage: memories, entities, relations, observations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
|
|
11
|
+
export interface Memory {
|
|
12
|
+
id: string;
|
|
13
|
+
content: string;
|
|
14
|
+
source: string;
|
|
15
|
+
timestamp: Date;
|
|
16
|
+
importance: number;
|
|
17
|
+
access_count: number;
|
|
18
|
+
last_accessed: Date | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Entity {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
type: "person" | "place" | "concept" | "event" | "organization";
|
|
25
|
+
created_at: Date;
|
|
26
|
+
metadata: Record<string, unknown> | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Observation {
|
|
30
|
+
id: string;
|
|
31
|
+
entity_id: string;
|
|
32
|
+
content: string;
|
|
33
|
+
source_memory_id: string | null;
|
|
34
|
+
confidence: number;
|
|
35
|
+
valid_from: Date;
|
|
36
|
+
valid_until: Date | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Relation {
|
|
40
|
+
id: string;
|
|
41
|
+
from_entity: string;
|
|
42
|
+
to_entity: string;
|
|
43
|
+
type: string;
|
|
44
|
+
properties: Record<string, unknown> | null;
|
|
45
|
+
created_at: Date;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class EngramDatabase {
|
|
49
|
+
private db: Database.Database;
|
|
50
|
+
|
|
51
|
+
constructor(dbPath: string) {
|
|
52
|
+
// Ensure directory exists
|
|
53
|
+
const dir = path.dirname(dbPath);
|
|
54
|
+
if (!fs.existsSync(dir)) {
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.db = new Database(dbPath);
|
|
59
|
+
this.db.pragma("journal_mode = WAL"); // Better concurrent access
|
|
60
|
+
this.db.pragma("foreign_keys = ON");
|
|
61
|
+
this.initialize();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private initialize(): void {
|
|
65
|
+
// Memories table
|
|
66
|
+
this.db.exec(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
68
|
+
id TEXT PRIMARY KEY,
|
|
69
|
+
content TEXT NOT NULL,
|
|
70
|
+
source TEXT DEFAULT 'conversation',
|
|
71
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
72
|
+
importance REAL DEFAULT 0.5,
|
|
73
|
+
access_count INTEGER DEFAULT 0,
|
|
74
|
+
last_accessed DATETIME
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_memories_timestamp ON memories(timestamp);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
79
|
+
`);
|
|
80
|
+
|
|
81
|
+
// FTS5 for BM25 search
|
|
82
|
+
this.db.exec(`
|
|
83
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
84
|
+
content,
|
|
85
|
+
content='memories',
|
|
86
|
+
content_rowid='rowid'
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
-- Triggers to keep FTS in sync
|
|
90
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
91
|
+
INSERT INTO memories_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
92
|
+
END;
|
|
93
|
+
|
|
94
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
95
|
+
INSERT INTO memories_fts(memories_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
96
|
+
END;
|
|
97
|
+
|
|
98
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
99
|
+
INSERT INTO memories_fts(memories_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
100
|
+
INSERT INTO memories_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
101
|
+
END;
|
|
102
|
+
`);
|
|
103
|
+
|
|
104
|
+
// Entities table
|
|
105
|
+
this.db.exec(`
|
|
106
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
107
|
+
id TEXT PRIMARY KEY,
|
|
108
|
+
name TEXT NOT NULL,
|
|
109
|
+
type TEXT NOT NULL CHECK(type IN ('person', 'place', 'concept', 'event', 'organization')),
|
|
110
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
111
|
+
metadata JSON
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
116
|
+
`);
|
|
117
|
+
|
|
118
|
+
// Observations table
|
|
119
|
+
this.db.exec(`
|
|
120
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
entity_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
|
|
123
|
+
content TEXT NOT NULL,
|
|
124
|
+
source_memory_id TEXT REFERENCES memories(id) ON DELETE SET NULL,
|
|
125
|
+
confidence REAL DEFAULT 1.0,
|
|
126
|
+
valid_from DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
127
|
+
valid_until DATETIME
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_observations_memory ON observations(source_memory_id);
|
|
132
|
+
`);
|
|
133
|
+
|
|
134
|
+
// Relations table
|
|
135
|
+
this.db.exec(`
|
|
136
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
137
|
+
id TEXT PRIMARY KEY,
|
|
138
|
+
from_entity TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
|
|
139
|
+
to_entity TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
|
|
140
|
+
type TEXT NOT NULL,
|
|
141
|
+
properties JSON,
|
|
142
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(type);
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============ Memory Operations ============
|
|
152
|
+
|
|
153
|
+
createMemory(
|
|
154
|
+
content: string,
|
|
155
|
+
source: string = "conversation",
|
|
156
|
+
importance: number = 0.5
|
|
157
|
+
): Memory {
|
|
158
|
+
const id = randomUUID();
|
|
159
|
+
const stmt = this.db.prepare(`
|
|
160
|
+
INSERT INTO memories (id, content, source, importance)
|
|
161
|
+
VALUES (?, ?, ?, ?)
|
|
162
|
+
`);
|
|
163
|
+
stmt.run(id, content, source, importance);
|
|
164
|
+
return this.getMemory(id)!;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getMemory(id: string): Memory | null {
|
|
168
|
+
const stmt = this.db.prepare("SELECT * FROM memories WHERE id = ?");
|
|
169
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
170
|
+
return row ? this.rowToMemory(row) : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
updateMemory(id: string, updates: Partial<Pick<Memory, "content" | "importance">>): Memory | null {
|
|
174
|
+
const sets: string[] = [];
|
|
175
|
+
const values: unknown[] = [];
|
|
176
|
+
|
|
177
|
+
if (updates.content !== undefined) {
|
|
178
|
+
sets.push("content = ?");
|
|
179
|
+
values.push(updates.content);
|
|
180
|
+
}
|
|
181
|
+
if (updates.importance !== undefined) {
|
|
182
|
+
sets.push("importance = ?");
|
|
183
|
+
values.push(updates.importance);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (sets.length === 0) return this.getMemory(id);
|
|
187
|
+
|
|
188
|
+
values.push(id);
|
|
189
|
+
const stmt = this.db.prepare(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`);
|
|
190
|
+
stmt.run(...values);
|
|
191
|
+
return this.getMemory(id);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
deleteMemory(id: string): boolean {
|
|
195
|
+
const stmt = this.db.prepare("DELETE FROM memories WHERE id = ?");
|
|
196
|
+
const result = stmt.run(id);
|
|
197
|
+
return result.changes > 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
touchMemory(id: string): void {
|
|
201
|
+
const stmt = this.db.prepare(`
|
|
202
|
+
UPDATE memories
|
|
203
|
+
SET access_count = access_count + 1, last_accessed = CURRENT_TIMESTAMP
|
|
204
|
+
WHERE id = ?
|
|
205
|
+
`);
|
|
206
|
+
stmt.run(id);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getAllMemories(limit: number = 1000): Memory[] {
|
|
210
|
+
const stmt = this.db.prepare("SELECT * FROM memories ORDER BY timestamp DESC LIMIT ?");
|
|
211
|
+
const rows = stmt.all(limit) as Record<string, unknown>[];
|
|
212
|
+
return rows.map((row) => this.rowToMemory(row));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============ BM25 Search ============
|
|
216
|
+
|
|
217
|
+
searchBM25(query: string, limit: number = 20): Array<Memory & { score: number }> {
|
|
218
|
+
const stmt = this.db.prepare(`
|
|
219
|
+
SELECT m.*, bm25(memories_fts) as score
|
|
220
|
+
FROM memories_fts fts
|
|
221
|
+
JOIN memories m ON fts.rowid = m.rowid
|
|
222
|
+
WHERE memories_fts MATCH ?
|
|
223
|
+
ORDER BY score
|
|
224
|
+
LIMIT ?
|
|
225
|
+
`);
|
|
226
|
+
|
|
227
|
+
// Escape special FTS5 characters and format query
|
|
228
|
+
const escapedQuery = this.escapeFTS5Query(query);
|
|
229
|
+
const rows = stmt.all(escapedQuery, limit) as Array<Record<string, unknown>>;
|
|
230
|
+
|
|
231
|
+
return rows.map((row) => ({
|
|
232
|
+
...this.rowToMemory(row),
|
|
233
|
+
score: row.score as number,
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private escapeFTS5Query(query: string): string {
|
|
238
|
+
// Simple tokenization - split on whitespace, escape special chars
|
|
239
|
+
const tokens = query
|
|
240
|
+
.replace(/['"()^*:]/g, " ") // Remove FTS5 special chars
|
|
241
|
+
.split(/\s+/)
|
|
242
|
+
.filter((t) => t.length > 0);
|
|
243
|
+
|
|
244
|
+
// Use OR for flexibility
|
|
245
|
+
return tokens.join(" OR ");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============ Entity Operations ============
|
|
249
|
+
|
|
250
|
+
createEntity(
|
|
251
|
+
name: string,
|
|
252
|
+
type: Entity["type"],
|
|
253
|
+
metadata: Record<string, unknown> | null = null
|
|
254
|
+
): Entity {
|
|
255
|
+
const id = randomUUID();
|
|
256
|
+
const stmt = this.db.prepare(`
|
|
257
|
+
INSERT INTO entities (id, name, type, metadata)
|
|
258
|
+
VALUES (?, ?, ?, ?)
|
|
259
|
+
`);
|
|
260
|
+
stmt.run(id, name, type, metadata ? JSON.stringify(metadata) : null);
|
|
261
|
+
return this.getEntity(id)!;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
getEntity(id: string): Entity | null {
|
|
265
|
+
const stmt = this.db.prepare("SELECT * FROM entities WHERE id = ?");
|
|
266
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
267
|
+
return row ? this.rowToEntity(row) : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
findEntityByName(name: string): Entity | null {
|
|
271
|
+
const stmt = this.db.prepare("SELECT * FROM entities WHERE LOWER(name) = LOWER(?)");
|
|
272
|
+
const row = stmt.get(name) as Record<string, unknown> | undefined;
|
|
273
|
+
return row ? this.rowToEntity(row) : null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
searchEntities(query: string, type?: Entity["type"]): Entity[] {
|
|
277
|
+
let sql = "SELECT * FROM entities WHERE name LIKE ?";
|
|
278
|
+
const params: unknown[] = [`%${query}%`];
|
|
279
|
+
|
|
280
|
+
if (type) {
|
|
281
|
+
sql += " AND type = ?";
|
|
282
|
+
params.push(type);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
sql += " LIMIT 50";
|
|
286
|
+
const stmt = this.db.prepare(sql);
|
|
287
|
+
const rows = stmt.all(...params) as Record<string, unknown>[];
|
|
288
|
+
return rows.map((row) => this.rowToEntity(row));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
listEntities(type?: Entity["type"], limit: number = 100): Entity[] {
|
|
292
|
+
let sql = "SELECT * FROM entities";
|
|
293
|
+
const params: unknown[] = [];
|
|
294
|
+
|
|
295
|
+
if (type) {
|
|
296
|
+
sql += " WHERE type = ?";
|
|
297
|
+
params.push(type);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
301
|
+
params.push(limit);
|
|
302
|
+
|
|
303
|
+
const stmt = this.db.prepare(sql);
|
|
304
|
+
const rows = stmt.all(...params) as Record<string, unknown>[];
|
|
305
|
+
return rows.map((row) => this.rowToEntity(row));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
deleteEntity(id: string): boolean {
|
|
309
|
+
const stmt = this.db.prepare("DELETE FROM entities WHERE id = ?");
|
|
310
|
+
const result = stmt.run(id);
|
|
311
|
+
return result.changes > 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============ Observation Operations ============
|
|
315
|
+
|
|
316
|
+
addObservation(
|
|
317
|
+
entityId: string,
|
|
318
|
+
content: string,
|
|
319
|
+
sourceMemoryId: string | null = null,
|
|
320
|
+
confidence: number = 1.0
|
|
321
|
+
): Observation {
|
|
322
|
+
const id = randomUUID();
|
|
323
|
+
const stmt = this.db.prepare(`
|
|
324
|
+
INSERT INTO observations (id, entity_id, content, source_memory_id, confidence)
|
|
325
|
+
VALUES (?, ?, ?, ?, ?)
|
|
326
|
+
`);
|
|
327
|
+
stmt.run(id, entityId, content, sourceMemoryId, confidence);
|
|
328
|
+
return this.getObservation(id)!;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
getObservation(id: string): Observation | null {
|
|
332
|
+
const stmt = this.db.prepare("SELECT * FROM observations WHERE id = ?");
|
|
333
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
334
|
+
return row ? this.rowToObservation(row) : null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
getEntityObservations(entityId: string, includeExpired: boolean = false): Observation[] {
|
|
338
|
+
let sql = "SELECT * FROM observations WHERE entity_id = ?";
|
|
339
|
+
if (!includeExpired) {
|
|
340
|
+
sql += " AND (valid_until IS NULL OR valid_until > CURRENT_TIMESTAMP)";
|
|
341
|
+
}
|
|
342
|
+
sql += " ORDER BY valid_from DESC";
|
|
343
|
+
|
|
344
|
+
const stmt = this.db.prepare(sql);
|
|
345
|
+
const rows = stmt.all(entityId) as Record<string, unknown>[];
|
|
346
|
+
return rows.map((row) => this.rowToObservation(row));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
expireObservation(id: string): void {
|
|
350
|
+
const stmt = this.db.prepare("UPDATE observations SET valid_until = CURRENT_TIMESTAMP WHERE id = ?");
|
|
351
|
+
stmt.run(id);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============ Relation Operations ============
|
|
355
|
+
|
|
356
|
+
createRelation(
|
|
357
|
+
fromEntityId: string,
|
|
358
|
+
toEntityId: string,
|
|
359
|
+
type: string,
|
|
360
|
+
properties: Record<string, unknown> | null = null
|
|
361
|
+
): Relation {
|
|
362
|
+
const id = randomUUID();
|
|
363
|
+
const stmt = this.db.prepare(`
|
|
364
|
+
INSERT INTO relations (id, from_entity, to_entity, type, properties)
|
|
365
|
+
VALUES (?, ?, ?, ?, ?)
|
|
366
|
+
`);
|
|
367
|
+
stmt.run(id, fromEntityId, toEntityId, type, properties ? JSON.stringify(properties) : null);
|
|
368
|
+
return this.getRelation(id)!;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
getRelation(id: string): Relation | null {
|
|
372
|
+
const stmt = this.db.prepare("SELECT * FROM relations WHERE id = ?");
|
|
373
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
374
|
+
return row ? this.rowToRelation(row) : null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
getEntityRelations(entityId: string, direction: "from" | "to" | "both" = "both"): Relation[] {
|
|
378
|
+
let sql: string;
|
|
379
|
+
if (direction === "from") {
|
|
380
|
+
sql = "SELECT * FROM relations WHERE from_entity = ?";
|
|
381
|
+
} else if (direction === "to") {
|
|
382
|
+
sql = "SELECT * FROM relations WHERE to_entity = ?";
|
|
383
|
+
} else {
|
|
384
|
+
sql = "SELECT * FROM relations WHERE from_entity = ? OR to_entity = ?";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const stmt = this.db.prepare(sql);
|
|
388
|
+
const rows = (direction === "both"
|
|
389
|
+
? stmt.all(entityId, entityId)
|
|
390
|
+
: stmt.all(entityId)) as Record<string, unknown>[];
|
|
391
|
+
return rows.map((row) => this.rowToRelation(row));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
findRelation(fromEntityId: string, toEntityId: string, type?: string): Relation | null {
|
|
395
|
+
let sql = "SELECT * FROM relations WHERE from_entity = ? AND to_entity = ?";
|
|
396
|
+
const params: unknown[] = [fromEntityId, toEntityId];
|
|
397
|
+
|
|
398
|
+
if (type) {
|
|
399
|
+
sql += " AND type = ?";
|
|
400
|
+
params.push(type);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const stmt = this.db.prepare(sql);
|
|
404
|
+
const row = stmt.get(...params) as Record<string, unknown> | undefined;
|
|
405
|
+
return row ? this.rowToRelation(row) : null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
deleteRelation(id: string): boolean {
|
|
409
|
+
const stmt = this.db.prepare("DELETE FROM relations WHERE id = ?");
|
|
410
|
+
const result = stmt.run(id);
|
|
411
|
+
return result.changes > 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ============ Graph Traversal ============
|
|
415
|
+
|
|
416
|
+
traverse(
|
|
417
|
+
startEntityId: string,
|
|
418
|
+
depth: number = 2,
|
|
419
|
+
relationTypes?: string[]
|
|
420
|
+
): { entities: Entity[]; relations: Relation[]; observations: Observation[] } {
|
|
421
|
+
const visitedEntities = new Set<string>();
|
|
422
|
+
const allRelations: Relation[] = [];
|
|
423
|
+
const queue: Array<{ entityId: string; currentDepth: number }> = [
|
|
424
|
+
{ entityId: startEntityId, currentDepth: 0 },
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
while (queue.length > 0) {
|
|
428
|
+
const { entityId, currentDepth } = queue.shift()!;
|
|
429
|
+
|
|
430
|
+
if (visitedEntities.has(entityId) || currentDepth > depth) continue;
|
|
431
|
+
visitedEntities.add(entityId);
|
|
432
|
+
|
|
433
|
+
const relations = this.getEntityRelations(entityId);
|
|
434
|
+
for (const rel of relations) {
|
|
435
|
+
if (relationTypes && !relationTypes.includes(rel.type)) continue;
|
|
436
|
+
|
|
437
|
+
allRelations.push(rel);
|
|
438
|
+
const nextEntityId = rel.from_entity === entityId ? rel.to_entity : rel.from_entity;
|
|
439
|
+
|
|
440
|
+
if (!visitedEntities.has(nextEntityId) && currentDepth < depth) {
|
|
441
|
+
queue.push({ entityId: nextEntityId, currentDepth: currentDepth + 1 });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Get all entities and their observations
|
|
447
|
+
const entities: Entity[] = [];
|
|
448
|
+
const observations: Observation[] = [];
|
|
449
|
+
|
|
450
|
+
for (const entityId of visitedEntities) {
|
|
451
|
+
const entity = this.getEntity(entityId);
|
|
452
|
+
if (entity) {
|
|
453
|
+
entities.push(entity);
|
|
454
|
+
observations.push(...this.getEntityObservations(entityId));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return { entities, relations: allRelations, observations };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============ Statistics ============
|
|
462
|
+
|
|
463
|
+
getStats(): {
|
|
464
|
+
memories: number;
|
|
465
|
+
entities: number;
|
|
466
|
+
relations: number;
|
|
467
|
+
observations: number;
|
|
468
|
+
} {
|
|
469
|
+
const memories = (this.db.prepare("SELECT COUNT(*) as count FROM memories").get() as { count: number }).count;
|
|
470
|
+
const entities = (this.db.prepare("SELECT COUNT(*) as count FROM entities").get() as { count: number }).count;
|
|
471
|
+
const relations = (this.db.prepare("SELECT COUNT(*) as count FROM relations").get() as { count: number }).count;
|
|
472
|
+
const observations = (this.db.prepare("SELECT COUNT(*) as count FROM observations").get() as { count: number }).count;
|
|
473
|
+
|
|
474
|
+
return { memories, entities, relations, observations };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ============ Utilities ============
|
|
478
|
+
|
|
479
|
+
close(): void {
|
|
480
|
+
this.db.close();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private rowToMemory(row: Record<string, unknown>): Memory {
|
|
484
|
+
return {
|
|
485
|
+
id: row.id as string,
|
|
486
|
+
content: row.content as string,
|
|
487
|
+
source: row.source as string,
|
|
488
|
+
timestamp: new Date(row.timestamp as string),
|
|
489
|
+
importance: row.importance as number,
|
|
490
|
+
access_count: row.access_count as number,
|
|
491
|
+
last_accessed: row.last_accessed ? new Date(row.last_accessed as string) : null,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private rowToEntity(row: Record<string, unknown>): Entity {
|
|
496
|
+
return {
|
|
497
|
+
id: row.id as string,
|
|
498
|
+
name: row.name as string,
|
|
499
|
+
type: row.type as Entity["type"],
|
|
500
|
+
created_at: new Date(row.created_at as string),
|
|
501
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : null,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private rowToObservation(row: Record<string, unknown>): Observation {
|
|
506
|
+
return {
|
|
507
|
+
id: row.id as string,
|
|
508
|
+
entity_id: row.entity_id as string,
|
|
509
|
+
content: row.content as string,
|
|
510
|
+
source_memory_id: row.source_memory_id as string | null,
|
|
511
|
+
confidence: row.confidence as number,
|
|
512
|
+
valid_from: new Date(row.valid_from as string),
|
|
513
|
+
valid_until: row.valid_until ? new Date(row.valid_until as string) : null,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private rowToRelation(row: Record<string, unknown>): Relation {
|
|
518
|
+
return {
|
|
519
|
+
id: row.id as string,
|
|
520
|
+
from_entity: row.from_entity as string,
|
|
521
|
+
to_entity: row.to_entity as string,
|
|
522
|
+
type: row.type as string,
|
|
523
|
+
properties: row.properties ? JSON.parse(row.properties as string) : null,
|
|
524
|
+
created_at: new Date(row.created_at as string),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./database.js";
|