@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.
@@ -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
+ }