@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,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Graph Manager
|
|
3
|
+
* High-level operations for the entity-relation-observation graph
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
EngramDatabase,
|
|
8
|
+
Entity,
|
|
9
|
+
Observation,
|
|
10
|
+
Relation,
|
|
11
|
+
} from "../storage/database.js";
|
|
12
|
+
import { entityExtractor, ExtractedEntity } from "./extractor.js";
|
|
13
|
+
|
|
14
|
+
export interface EntityWithDetails extends Entity {
|
|
15
|
+
observations: Observation[];
|
|
16
|
+
relationsFrom: Array<Relation & { targetEntity: Entity }>;
|
|
17
|
+
relationsTo: Array<Relation & { sourceEntity: Entity }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GraphTraversal {
|
|
21
|
+
rootEntity: Entity;
|
|
22
|
+
entities: Entity[];
|
|
23
|
+
relations: Relation[];
|
|
24
|
+
observations: Observation[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class KnowledgeGraph {
|
|
28
|
+
constructor(private db: EngramDatabase) {}
|
|
29
|
+
|
|
30
|
+
// ============ Entity Operations ============
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get or create an entity by name
|
|
34
|
+
*/
|
|
35
|
+
getOrCreateEntity(
|
|
36
|
+
name: string,
|
|
37
|
+
type: Entity["type"] = "person"
|
|
38
|
+
): Entity {
|
|
39
|
+
const existing = this.db.findEntityByName(name);
|
|
40
|
+
if (existing) return existing;
|
|
41
|
+
return this.db.createEntity(name, type);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get entity with all its observations and relations
|
|
46
|
+
*/
|
|
47
|
+
getEntityDetails(nameOrId: string): EntityWithDetails | null {
|
|
48
|
+
// Try to find by name first, then by ID
|
|
49
|
+
let entity = this.db.findEntityByName(nameOrId);
|
|
50
|
+
if (!entity) {
|
|
51
|
+
entity = this.db.getEntity(nameOrId);
|
|
52
|
+
}
|
|
53
|
+
if (!entity) return null;
|
|
54
|
+
|
|
55
|
+
const observations = this.db.getEntityObservations(entity.id);
|
|
56
|
+
const relations = this.db.getEntityRelations(entity.id, "both");
|
|
57
|
+
|
|
58
|
+
const relationsFrom: Array<Relation & { targetEntity: Entity }> = [];
|
|
59
|
+
const relationsTo: Array<Relation & { sourceEntity: Entity }> = [];
|
|
60
|
+
|
|
61
|
+
for (const rel of relations) {
|
|
62
|
+
if (rel.from_entity === entity.id) {
|
|
63
|
+
const target = this.db.getEntity(rel.to_entity);
|
|
64
|
+
if (target) {
|
|
65
|
+
relationsFrom.push({ ...rel, targetEntity: target });
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
const source = this.db.getEntity(rel.from_entity);
|
|
69
|
+
if (source) {
|
|
70
|
+
relationsTo.push({ ...rel, sourceEntity: source });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
...entity,
|
|
77
|
+
observations,
|
|
78
|
+
relationsFrom,
|
|
79
|
+
relationsTo,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Find entities matching a query
|
|
85
|
+
*/
|
|
86
|
+
findEntities(query: string, type?: Entity["type"]): Entity[] {
|
|
87
|
+
return this.db.searchEntities(query, type);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* List all entities of a type
|
|
92
|
+
*/
|
|
93
|
+
listEntities(type?: Entity["type"], limit: number = 100): Entity[] {
|
|
94
|
+
return this.db.listEntities(type, limit);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============ Observation Operations ============
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Add an observation to an entity
|
|
101
|
+
*/
|
|
102
|
+
addObservation(
|
|
103
|
+
entityNameOrId: string,
|
|
104
|
+
content: string,
|
|
105
|
+
sourceMemoryId?: string,
|
|
106
|
+
confidence: number = 1.0
|
|
107
|
+
): Observation {
|
|
108
|
+
const entity = this.db.findEntityByName(entityNameOrId) ||
|
|
109
|
+
this.db.getEntity(entityNameOrId);
|
|
110
|
+
|
|
111
|
+
if (!entity) {
|
|
112
|
+
throw new Error(`Entity not found: ${entityNameOrId}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return this.db.addObservation(entity.id, content, sourceMemoryId || null, confidence);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get all observations for an entity
|
|
120
|
+
*/
|
|
121
|
+
getObservations(entityNameOrId: string): Observation[] {
|
|
122
|
+
const entity = this.db.findEntityByName(entityNameOrId) ||
|
|
123
|
+
this.db.getEntity(entityNameOrId);
|
|
124
|
+
|
|
125
|
+
if (!entity) return [];
|
|
126
|
+
|
|
127
|
+
return this.db.getEntityObservations(entity.id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============ Relation Operations ============
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create or get a relation between two entities
|
|
134
|
+
*/
|
|
135
|
+
relate(
|
|
136
|
+
fromNameOrId: string,
|
|
137
|
+
toNameOrId: string,
|
|
138
|
+
type: string,
|
|
139
|
+
properties?: Record<string, unknown>
|
|
140
|
+
): Relation {
|
|
141
|
+
const fromEntity = this.db.findEntityByName(fromNameOrId) ||
|
|
142
|
+
this.db.getEntity(fromNameOrId);
|
|
143
|
+
const toEntity = this.db.findEntityByName(toNameOrId) ||
|
|
144
|
+
this.db.getEntity(toNameOrId);
|
|
145
|
+
|
|
146
|
+
if (!fromEntity) throw new Error(`Entity not found: ${fromNameOrId}`);
|
|
147
|
+
if (!toEntity) throw new Error(`Entity not found: ${toNameOrId}`);
|
|
148
|
+
|
|
149
|
+
// Check if relation already exists
|
|
150
|
+
const existing = this.db.findRelation(fromEntity.id, toEntity.id, type);
|
|
151
|
+
if (existing) return existing;
|
|
152
|
+
|
|
153
|
+
return this.db.createRelation(fromEntity.id, toEntity.id, type, properties || null);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get relations for an entity
|
|
158
|
+
*/
|
|
159
|
+
getRelations(
|
|
160
|
+
entityNameOrId: string,
|
|
161
|
+
direction: "from" | "to" | "both" = "both"
|
|
162
|
+
): Relation[] {
|
|
163
|
+
const entity = this.db.findEntityByName(entityNameOrId) ||
|
|
164
|
+
this.db.getEntity(entityNameOrId);
|
|
165
|
+
|
|
166
|
+
if (!entity) return [];
|
|
167
|
+
|
|
168
|
+
return this.db.getEntityRelations(entity.id, direction);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============ Graph Traversal ============
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Traverse the graph from an entity
|
|
175
|
+
*/
|
|
176
|
+
traverse(
|
|
177
|
+
entityNameOrId: string,
|
|
178
|
+
depth: number = 2,
|
|
179
|
+
relationTypes?: string[]
|
|
180
|
+
): GraphTraversal | null {
|
|
181
|
+
const entity = this.db.findEntityByName(entityNameOrId) ||
|
|
182
|
+
this.db.getEntity(entityNameOrId);
|
|
183
|
+
|
|
184
|
+
if (!entity) return null;
|
|
185
|
+
|
|
186
|
+
const result = this.db.traverse(entity.id, depth, relationTypes);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
rootEntity: entity,
|
|
190
|
+
...result,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ============ Auto-extraction ============
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Extract entities from text and add them to the graph
|
|
198
|
+
*/
|
|
199
|
+
extractAndStore(
|
|
200
|
+
text: string,
|
|
201
|
+
sourceMemoryId?: string
|
|
202
|
+
): { entities: Entity[]; observations: Observation[] } {
|
|
203
|
+
const extracted = entityExtractor.extractAll(text);
|
|
204
|
+
const entities: Entity[] = [];
|
|
205
|
+
const observations: Observation[] = [];
|
|
206
|
+
|
|
207
|
+
for (const ext of extracted) {
|
|
208
|
+
// Only store high-confidence extractions
|
|
209
|
+
if (ext.confidence < 0.5) continue;
|
|
210
|
+
|
|
211
|
+
// Get or create entity
|
|
212
|
+
const entity = this.getOrCreateEntity(ext.name, ext.type);
|
|
213
|
+
entities.push(entity);
|
|
214
|
+
|
|
215
|
+
// Create observation linking this entity to the memory content
|
|
216
|
+
const obs = this.db.addObservation(
|
|
217
|
+
entity.id,
|
|
218
|
+
text,
|
|
219
|
+
sourceMemoryId || null,
|
|
220
|
+
ext.confidence
|
|
221
|
+
);
|
|
222
|
+
observations.push(obs);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Extract and create relationships
|
|
226
|
+
const relationships = entityExtractor.extractRelationships(text);
|
|
227
|
+
for (const rel of relationships) {
|
|
228
|
+
try {
|
|
229
|
+
// Ensure both entities exist
|
|
230
|
+
const fromEntity = this.getOrCreateEntity(rel.subject, "person");
|
|
231
|
+
const toEntity = this.getOrCreateEntity(rel.object, "person");
|
|
232
|
+
|
|
233
|
+
// Create the relation
|
|
234
|
+
this.db.createRelation(fromEntity.id, toEntity.id, rel.relation);
|
|
235
|
+
} catch {
|
|
236
|
+
// Silently ignore relation creation failures
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { entities, observations };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Find memories related to an entity by traversing the graph
|
|
245
|
+
*/
|
|
246
|
+
findRelatedMemoryIds(
|
|
247
|
+
entityNameOrId: string,
|
|
248
|
+
depth: number = 2
|
|
249
|
+
): string[] {
|
|
250
|
+
const traversal = this.traverse(entityNameOrId, depth);
|
|
251
|
+
if (!traversal) return [];
|
|
252
|
+
|
|
253
|
+
const memoryIds = new Set<string>();
|
|
254
|
+
|
|
255
|
+
for (const obs of traversal.observations) {
|
|
256
|
+
if (obs.source_memory_id) {
|
|
257
|
+
memoryIds.add(obs.source_memory_id);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return Array.from(memoryIds);
|
|
262
|
+
}
|
|
263
|
+
}
|