@framers/agentos 0.1.32 → 0.1.34

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.
Files changed (102) hide show
  1. package/README.md +5 -2
  2. package/dist/api/AgentOS.d.ts +62 -1
  3. package/dist/api/AgentOS.d.ts.map +1 -1
  4. package/dist/api/AgentOS.js +177 -2
  5. package/dist/api/AgentOS.js.map +1 -1
  6. package/dist/api/AgentOSOrchestrator.d.ts +187 -0
  7. package/dist/api/AgentOSOrchestrator.d.ts.map +1 -1
  8. package/dist/api/AgentOSOrchestrator.js +709 -16
  9. package/dist/api/AgentOSOrchestrator.js.map +1 -1
  10. package/dist/cognitive_substrate/GMI.d.ts.map +1 -1
  11. package/dist/cognitive_substrate/GMI.js +36 -1
  12. package/dist/cognitive_substrate/GMI.js.map +1 -1
  13. package/dist/cognitive_substrate/IGMI.d.ts +21 -0
  14. package/dist/cognitive_substrate/IGMI.d.ts.map +1 -1
  15. package/dist/cognitive_substrate/IGMI.js.map +1 -1
  16. package/dist/config/AgentOSConfig.d.ts.map +1 -1
  17. package/dist/config/AgentOSConfig.js +17 -0
  18. package/dist/config/AgentOSConfig.js.map +1 -1
  19. package/dist/config/VectorStoreConfiguration.d.ts +2 -1
  20. package/dist/config/VectorStoreConfiguration.d.ts.map +1 -1
  21. package/dist/config/VectorStoreConfiguration.js.map +1 -1
  22. package/dist/core/knowledge/Neo4jKnowledgeGraph.d.ts +89 -0
  23. package/dist/core/knowledge/Neo4jKnowledgeGraph.d.ts.map +1 -0
  24. package/dist/core/knowledge/Neo4jKnowledgeGraph.js +683 -0
  25. package/dist/core/knowledge/Neo4jKnowledgeGraph.js.map +1 -0
  26. package/dist/core/llm/providers/implementations/OllamaProvider.d.ts +14 -1
  27. package/dist/core/llm/providers/implementations/OllamaProvider.d.ts.map +1 -1
  28. package/dist/core/llm/providers/implementations/OllamaProvider.js +142 -37
  29. package/dist/core/llm/providers/implementations/OllamaProvider.js.map +1 -1
  30. package/dist/core/llm/providers/implementations/OpenAIProvider.js +3 -3
  31. package/dist/core/llm/providers/implementations/OpenAIProvider.js.map +1 -1
  32. package/dist/core/observability/otel.d.ts +2 -0
  33. package/dist/core/observability/otel.d.ts.map +1 -1
  34. package/dist/core/observability/otel.js +14 -0
  35. package/dist/core/observability/otel.js.map +1 -1
  36. package/dist/core/orchestration/SqlTaskOutcomeTelemetryStore.d.ts +30 -0
  37. package/dist/core/orchestration/SqlTaskOutcomeTelemetryStore.d.ts.map +1 -0
  38. package/dist/core/orchestration/SqlTaskOutcomeTelemetryStore.js +123 -0
  39. package/dist/core/orchestration/SqlTaskOutcomeTelemetryStore.js.map +1 -0
  40. package/dist/core/orchestration/TurnPlanner.d.ts +89 -0
  41. package/dist/core/orchestration/TurnPlanner.d.ts.map +1 -0
  42. package/dist/core/orchestration/TurnPlanner.js +242 -0
  43. package/dist/core/orchestration/TurnPlanner.js.map +1 -0
  44. package/dist/discovery/CapabilityDiscoveryEngine.js +4 -4
  45. package/dist/discovery/CapabilityDiscoveryEngine.js.map +1 -1
  46. package/dist/discovery/CapabilityGraph.d.ts +2 -2
  47. package/dist/discovery/CapabilityGraph.d.ts.map +1 -1
  48. package/dist/discovery/CapabilityGraph.js +46 -17
  49. package/dist/discovery/CapabilityGraph.js.map +1 -1
  50. package/dist/discovery/Neo4jCapabilityGraph.d.ts +58 -0
  51. package/dist/discovery/Neo4jCapabilityGraph.d.ts.map +1 -0
  52. package/dist/discovery/Neo4jCapabilityGraph.js +226 -0
  53. package/dist/discovery/Neo4jCapabilityGraph.js.map +1 -0
  54. package/dist/discovery/index.d.ts +1 -0
  55. package/dist/discovery/index.d.ts.map +1 -1
  56. package/dist/discovery/index.js +1 -0
  57. package/dist/discovery/index.js.map +1 -1
  58. package/dist/discovery/types.d.ts +1 -1
  59. package/dist/discovery/types.d.ts.map +1 -1
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +2 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/neo4j/Neo4jConnectionManager.d.ts +59 -0
  65. package/dist/neo4j/Neo4jConnectionManager.d.ts.map +1 -0
  66. package/dist/neo4j/Neo4jConnectionManager.js +115 -0
  67. package/dist/neo4j/Neo4jConnectionManager.js.map +1 -0
  68. package/dist/neo4j/Neo4jCypherRunner.d.ts +39 -0
  69. package/dist/neo4j/Neo4jCypherRunner.d.ts.map +1 -0
  70. package/dist/neo4j/Neo4jCypherRunner.js +74 -0
  71. package/dist/neo4j/Neo4jCypherRunner.js.map +1 -0
  72. package/dist/neo4j/index.d.ts +12 -0
  73. package/dist/neo4j/index.d.ts.map +1 -0
  74. package/dist/neo4j/index.js +11 -0
  75. package/dist/neo4j/index.js.map +1 -0
  76. package/dist/neo4j/types.d.ts +27 -0
  77. package/dist/neo4j/types.d.ts.map +1 -0
  78. package/dist/neo4j/types.js +6 -0
  79. package/dist/neo4j/types.js.map +1 -0
  80. package/dist/rag/VectorStoreManager.d.ts.map +1 -1
  81. package/dist/rag/VectorStoreManager.js +6 -7
  82. package/dist/rag/VectorStoreManager.js.map +1 -1
  83. package/dist/rag/graphrag/GraphRAGEngine.d.ts.map +1 -1
  84. package/dist/rag/graphrag/GraphRAGEngine.js +42 -10
  85. package/dist/rag/graphrag/GraphRAGEngine.js.map +1 -1
  86. package/dist/rag/graphrag/Neo4jGraphRAGEngine.d.ts +95 -0
  87. package/dist/rag/graphrag/Neo4jGraphRAGEngine.d.ts.map +1 -0
  88. package/dist/rag/graphrag/Neo4jGraphRAGEngine.js +748 -0
  89. package/dist/rag/graphrag/Neo4jGraphRAGEngine.js.map +1 -0
  90. package/dist/rag/graphrag/index.d.ts +1 -0
  91. package/dist/rag/graphrag/index.d.ts.map +1 -1
  92. package/dist/rag/graphrag/index.js +1 -0
  93. package/dist/rag/graphrag/index.js.map +1 -1
  94. package/dist/rag/implementations/vector_stores/Neo4jVectorStore.d.ts +55 -0
  95. package/dist/rag/implementations/vector_stores/Neo4jVectorStore.d.ts.map +1 -0
  96. package/dist/rag/implementations/vector_stores/Neo4jVectorStore.js +369 -0
  97. package/dist/rag/implementations/vector_stores/Neo4jVectorStore.js.map +1 -0
  98. package/dist/rag/implementations/vector_stores/index.d.ts +1 -0
  99. package/dist/rag/implementations/vector_stores/index.d.ts.map +1 -1
  100. package/dist/rag/implementations/vector_stores/index.js +2 -0
  101. package/dist/rag/implementations/vector_stores/index.js.map +1 -1
  102. package/package.json +5 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Neo4jKnowledgeGraph.d.ts","sourceRoot":"","sources":["../../../src/core/knowledge/Neo4jKnowledgeGraph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,683 @@
1
+ /**
2
+ * @fileoverview Neo4j-backed Knowledge Graph implementation.
3
+ *
4
+ * Implements `IKnowledgeGraph` using Neo4j for persistent entity/relation/memory
5
+ * storage with native Cypher traversal, shortest path, and vector index-based
6
+ * semantic search.
7
+ *
8
+ * Features:
9
+ * - Persistent entity/relation storage via Neo4j
10
+ * - Native BFS traversal and shortest path via Cypher variable-length paths
11
+ * - Vector indexes on KnowledgeEntity.embedding and EpisodicMemory.embedding
12
+ * - Dynamic relationship types via APOC merge.relationship
13
+ * - Memory decay via Cypher-based exponential formula
14
+ * - Shared Neo4jConnectionManager for connection pooling
15
+ *
16
+ * @module @framers/agentos/core/knowledge/Neo4jKnowledgeGraph
17
+ * @see ./IKnowledgeGraph.ts for the interface definition.
18
+ */
19
+ import { Neo4jCypherRunner } from '../../neo4j/Neo4jCypherRunner.js';
20
+ // ============================================================================
21
+ // Constants
22
+ // ============================================================================
23
+ const ENTITY_LABEL = 'KnowledgeEntity';
24
+ const MEMORY_LABEL = 'EpisodicMemory';
25
+ const ENTITY_VEC_INDEX = 'knowledge_entity_embeddings';
26
+ const MEMORY_VEC_INDEX = 'episodic_memory_embeddings';
27
+ const DEFAULT_EMBEDDING_DIM = 1536;
28
+ // Map interface relation types to Neo4j relationship type strings
29
+ function relTypeToNeo4j(type) {
30
+ return type.toUpperCase();
31
+ }
32
+ function neo4jToRelType(type) {
33
+ return type.toLowerCase();
34
+ }
35
+ function generateId() {
36
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
37
+ }
38
+ function nowIso() {
39
+ return new Date().toISOString();
40
+ }
41
+ // ============================================================================
42
+ // Implementation
43
+ // ============================================================================
44
+ export class Neo4jKnowledgeGraph {
45
+ constructor(config) {
46
+ this.config = config;
47
+ this.embeddingDimension = config.embeddingDimension ?? DEFAULT_EMBEDDING_DIM;
48
+ this.memoryDecayRate = config.memoryDecayRate ?? 0.01;
49
+ this.minImportanceThreshold = config.minImportanceThreshold ?? 0.05;
50
+ }
51
+ async initialize() {
52
+ this.cypher = new Neo4jCypherRunner(this.config.connectionManager);
53
+ // Create constraints for fast lookups
54
+ await this.cypher.writeVoid(`CREATE CONSTRAINT ke_unique IF NOT EXISTS FOR (n:${ENTITY_LABEL}) REQUIRE n.entityId IS UNIQUE`);
55
+ await this.cypher.writeVoid(`CREATE CONSTRAINT em_unique IF NOT EXISTS FOR (n:${MEMORY_LABEL}) REQUIRE n.memoryId IS UNIQUE`);
56
+ // Create vector indexes for semantic search
57
+ await this.cypher.writeVoid(`CREATE VECTOR INDEX ${ENTITY_VEC_INDEX} IF NOT EXISTS
58
+ FOR (n:${ENTITY_LABEL}) ON (n.embedding)
59
+ OPTIONS { indexConfig: {
60
+ \`vector.dimensions\`: toInteger($dim),
61
+ \`vector.similarity_function\`: 'cosine'
62
+ }}`, { dim: this.embeddingDimension });
63
+ await this.cypher.writeVoid(`CREATE VECTOR INDEX ${MEMORY_VEC_INDEX} IF NOT EXISTS
64
+ FOR (n:${MEMORY_LABEL}) ON (n.embedding)
65
+ OPTIONS { indexConfig: {
66
+ \`vector.dimensions\`: toInteger($dim),
67
+ \`vector.similarity_function\`: 'cosine'
68
+ }}`, { dim: this.embeddingDimension });
69
+ }
70
+ // ============ Entity Operations ============
71
+ async upsertEntity(entity) {
72
+ const id = entity.id ?? generateId();
73
+ const now = nowIso();
74
+ const results = await this.cypher.write(`MERGE (e:${ENTITY_LABEL} { entityId: $id })
75
+ ON CREATE SET
76
+ e.type = $type,
77
+ e.label = $label,
78
+ e.properties_json = $properties_json,
79
+ e.embedding = $embedding,
80
+ e.confidence = $confidence,
81
+ e.source_json = $source_json,
82
+ e.ownerId = $ownerId,
83
+ e.tags = $tags,
84
+ e.metadata_json = $metadata_json,
85
+ e.createdAt = $now,
86
+ e.updatedAt = $now
87
+ ON MATCH SET
88
+ e.type = $type,
89
+ e.label = $label,
90
+ e.properties_json = $properties_json,
91
+ e.embedding = CASE WHEN $embedding IS NOT NULL THEN $embedding ELSE e.embedding END,
92
+ e.confidence = $confidence,
93
+ e.source_json = $source_json,
94
+ e.ownerId = $ownerId,
95
+ e.tags = $tags,
96
+ e.metadata_json = $metadata_json,
97
+ e.updatedAt = $now
98
+ RETURN e`, {
99
+ id,
100
+ type: entity.type,
101
+ label: entity.label,
102
+ properties_json: JSON.stringify(entity.properties),
103
+ embedding: entity.embedding ?? null,
104
+ confidence: entity.confidence,
105
+ source_json: JSON.stringify(entity.source),
106
+ ownerId: entity.ownerId ?? null,
107
+ tags: entity.tags ?? [],
108
+ metadata_json: entity.metadata ? JSON.stringify(entity.metadata) : null,
109
+ now,
110
+ });
111
+ return this.nodeToEntity(results[0]?.e, id, now);
112
+ }
113
+ async getEntity(id) {
114
+ const results = await this.cypher.read(`MATCH (e:${ENTITY_LABEL} { entityId: $id }) RETURN e`, { id });
115
+ if (results.length === 0)
116
+ return undefined;
117
+ return this.nodeToEntity(results[0].e);
118
+ }
119
+ async queryEntities(options) {
120
+ const conditions = [];
121
+ const params = {};
122
+ if (options?.entityTypes?.length) {
123
+ conditions.push('e.type IN $entityTypes');
124
+ params.entityTypes = options.entityTypes;
125
+ }
126
+ if (options?.ownerId) {
127
+ conditions.push('e.ownerId = $ownerId');
128
+ params.ownerId = options.ownerId;
129
+ }
130
+ if (options?.tags?.length) {
131
+ conditions.push('ANY(tag IN $tags WHERE tag IN e.tags)');
132
+ params.tags = options.tags;
133
+ }
134
+ if (options?.minConfidence !== undefined) {
135
+ conditions.push('e.confidence >= $minConfidence');
136
+ params.minConfidence = options.minConfidence;
137
+ }
138
+ if (options?.timeRange?.from) {
139
+ conditions.push('e.createdAt >= $from');
140
+ params.from = options.timeRange.from;
141
+ }
142
+ if (options?.timeRange?.to) {
143
+ conditions.push('e.createdAt <= $to');
144
+ params.to = options.timeRange.to;
145
+ }
146
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
147
+ const limit = options?.limit ?? 100;
148
+ const offset = options?.offset ?? 0;
149
+ const results = await this.cypher.read(`MATCH (e:${ENTITY_LABEL}) ${where}
150
+ RETURN e
151
+ ORDER BY e.updatedAt DESC
152
+ SKIP $offset LIMIT $limit`, { ...params, offset, limit });
153
+ return results.map((r) => this.nodeToEntity(r.e));
154
+ }
155
+ async deleteEntity(id) {
156
+ const results = await this.cypher.write(`MATCH (e:${ENTITY_LABEL} { entityId: $id })
157
+ DETACH DELETE e
158
+ RETURN 1 AS deleted`, { id });
159
+ return results.length > 0;
160
+ }
161
+ // ============ Relation Operations ============
162
+ async upsertRelation(relation) {
163
+ const id = relation.id ?? generateId();
164
+ const now = nowIso();
165
+ const relType = relTypeToNeo4j(relation.type);
166
+ // Use dynamic relationship type via a workaround:
167
+ // We store all relations as :KNOWLEDGE_REL with a relType property,
168
+ // because APOC may not be available. If APOC is available, use dynamic types.
169
+ const results = await this.cypher.write(`MATCH (src:${ENTITY_LABEL} { entityId: $sourceId })
170
+ MATCH (tgt:${ENTITY_LABEL} { entityId: $targetId })
171
+ MERGE (src)-[r:KNOWLEDGE_REL { relationId: $id }]->(tgt)
172
+ ON CREATE SET
173
+ r.relType = $relType,
174
+ r.label = $label,
175
+ r.properties_json = $props_json,
176
+ r.weight = $weight,
177
+ r.bidirectional = $bidirectional,
178
+ r.confidence = $confidence,
179
+ r.source_json = $source_json,
180
+ r.validFrom = $validFrom,
181
+ r.validTo = $validTo,
182
+ r.createdAt = $now
183
+ ON MATCH SET
184
+ r.relType = $relType,
185
+ r.label = $label,
186
+ r.properties_json = $props_json,
187
+ r.weight = $weight,
188
+ r.bidirectional = $bidirectional,
189
+ r.confidence = $confidence,
190
+ r.source_json = $source_json,
191
+ r.validFrom = $validFrom,
192
+ r.validTo = $validTo
193
+ RETURN r`, {
194
+ id,
195
+ sourceId: relation.sourceId,
196
+ targetId: relation.targetId,
197
+ relType,
198
+ label: relation.label,
199
+ props_json: relation.properties ? JSON.stringify(relation.properties) : null,
200
+ weight: relation.weight,
201
+ bidirectional: relation.bidirectional,
202
+ confidence: relation.confidence,
203
+ source_json: JSON.stringify(relation.source),
204
+ validFrom: relation.validFrom ?? null,
205
+ validTo: relation.validTo ?? null,
206
+ now,
207
+ });
208
+ return this.relToKnowledgeRelation(results[0]?.r, id, relation.sourceId, relation.targetId, now);
209
+ }
210
+ async getRelations(entityId, options) {
211
+ const direction = options?.direction ?? 'both';
212
+ const types = options?.types?.map(relTypeToNeo4j);
213
+ let cypher;
214
+ const params = { entityId };
215
+ if (types?.length) {
216
+ params.types = types;
217
+ }
218
+ const typeFilter = types?.length ? 'AND r.relType IN $types' : '';
219
+ if (direction === 'outgoing') {
220
+ cypher = `MATCH (e:${ENTITY_LABEL} { entityId: $entityId })-[r:KNOWLEDGE_REL]->(t:${ENTITY_LABEL})
221
+ WHERE true ${typeFilter}
222
+ RETURN r, e.entityId AS srcId, t.entityId AS tgtId`;
223
+ }
224
+ else if (direction === 'incoming') {
225
+ cypher = `MATCH (s:${ENTITY_LABEL})-[r:KNOWLEDGE_REL]->(e:${ENTITY_LABEL} { entityId: $entityId })
226
+ WHERE true ${typeFilter}
227
+ RETURN r, s.entityId AS srcId, e.entityId AS tgtId`;
228
+ }
229
+ else {
230
+ cypher = `MATCH (e:${ENTITY_LABEL} { entityId: $entityId })-[r:KNOWLEDGE_REL]-(other:${ENTITY_LABEL})
231
+ WHERE true ${typeFilter}
232
+ RETURN r,
233
+ CASE WHEN startNode(r) = e THEN e.entityId ELSE other.entityId END AS srcId,
234
+ CASE WHEN endNode(r) = e THEN e.entityId ELSE other.entityId END AS tgtId`;
235
+ }
236
+ const results = await this.cypher.read(cypher, params);
237
+ return results.map((row) => this.relToKnowledgeRelation(row.r, undefined, row.srcId, row.tgtId));
238
+ }
239
+ async deleteRelation(id) {
240
+ const results = await this.cypher.write(`MATCH ()-[r:KNOWLEDGE_REL { relationId: $id }]->()
241
+ DELETE r
242
+ RETURN 1 AS deleted`, { id });
243
+ return results.length > 0;
244
+ }
245
+ // ============ Episodic Memory Operations ============
246
+ async recordMemory(memory) {
247
+ const id = generateId();
248
+ const now = nowIso();
249
+ await this.cypher.writeVoid(`CREATE (m:${MEMORY_LABEL} {
250
+ memoryId: $id,
251
+ type: $type,
252
+ summary: $summary,
253
+ description: $description,
254
+ participants: $participants,
255
+ valence: $valence,
256
+ importance: $importance,
257
+ embedding: $embedding,
258
+ occurredAt: $occurredAt,
259
+ durationMs: $durationMs,
260
+ outcome: $outcome,
261
+ insights_json: $insights_json,
262
+ context_json: $context_json,
263
+ entityIds: $entityIds,
264
+ createdAt: $now,
265
+ accessCount: 0,
266
+ lastAccessedAt: $now
267
+ })`, {
268
+ id,
269
+ type: memory.type,
270
+ summary: memory.summary,
271
+ description: memory.description ?? null,
272
+ participants: memory.participants,
273
+ valence: memory.valence ?? null,
274
+ importance: memory.importance,
275
+ embedding: memory.embedding ?? null,
276
+ occurredAt: memory.occurredAt,
277
+ durationMs: memory.durationMs ?? null,
278
+ outcome: memory.outcome ?? null,
279
+ insights_json: memory.insights ? JSON.stringify(memory.insights) : null,
280
+ context_json: memory.context ? JSON.stringify(memory.context) : null,
281
+ entityIds: memory.entityIds,
282
+ now,
283
+ });
284
+ // Link to entities
285
+ if (memory.entityIds.length > 0) {
286
+ await this.cypher.writeVoid(`MATCH (m:${MEMORY_LABEL} { memoryId: $id })
287
+ UNWIND $entityIds AS eid
288
+ MATCH (e:${ENTITY_LABEL} { entityId: eid })
289
+ MERGE (m)-[:REFERS_TO]->(e)`, { id, entityIds: memory.entityIds });
290
+ }
291
+ return {
292
+ id,
293
+ ...memory,
294
+ createdAt: now,
295
+ accessCount: 0,
296
+ lastAccessedAt: now,
297
+ };
298
+ }
299
+ async getMemory(id) {
300
+ const results = await this.cypher.read(`MATCH (m:${MEMORY_LABEL} { memoryId: $id }) RETURN m`, { id });
301
+ if (results.length === 0)
302
+ return undefined;
303
+ // Update access count
304
+ await this.cypher.writeVoid(`MATCH (m:${MEMORY_LABEL} { memoryId: $id })
305
+ SET m.accessCount = m.accessCount + 1, m.lastAccessedAt = $now`, { id, now: nowIso() });
306
+ return this.nodeToMemory(results[0].m);
307
+ }
308
+ async queryMemories(options) {
309
+ const conditions = [];
310
+ const params = {};
311
+ if (options?.types?.length) {
312
+ conditions.push('m.type IN $types');
313
+ params.types = options.types;
314
+ }
315
+ if (options?.participants?.length) {
316
+ conditions.push('ANY(p IN $participants WHERE p IN m.participants)');
317
+ params.participants = options.participants;
318
+ }
319
+ if (options?.minImportance !== undefined) {
320
+ conditions.push('m.importance >= $minImportance');
321
+ params.minImportance = options.minImportance;
322
+ }
323
+ if (options?.timeRange?.from) {
324
+ conditions.push('m.occurredAt >= $from');
325
+ params.from = options.timeRange.from;
326
+ }
327
+ if (options?.timeRange?.to) {
328
+ conditions.push('m.occurredAt <= $to');
329
+ params.to = options.timeRange.to;
330
+ }
331
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
332
+ const limit = options?.limit ?? 50;
333
+ const results = await this.cypher.read(`MATCH (m:${MEMORY_LABEL}) ${where}
334
+ RETURN m ORDER BY m.importance DESC, m.occurredAt DESC LIMIT $limit`, { ...params, limit });
335
+ return results.map((r) => this.nodeToMemory(r.m));
336
+ }
337
+ async recallMemories(query, topK) {
338
+ // This requires embedding the query first — for now, do text-based recall
339
+ // When embedding manager is wired, this will use vector search
340
+ const k = topK ?? 5;
341
+ const results = await this.cypher.read(`MATCH (m:${MEMORY_LABEL})
342
+ WHERE m.summary CONTAINS $query OR m.description CONTAINS $query
343
+ RETURN m ORDER BY m.importance DESC LIMIT $k`, { query, k });
344
+ // Update access counts
345
+ const ids = results.map((r) => r.m.properties?.memoryId ?? r.m.memoryId);
346
+ if (ids.length > 0) {
347
+ await this.cypher.writeVoid(`MATCH (m:${MEMORY_LABEL}) WHERE m.memoryId IN $ids
348
+ SET m.accessCount = m.accessCount + 1, m.lastAccessedAt = $now`, { ids, now: nowIso() });
349
+ }
350
+ return results.map((r) => this.nodeToMemory(r.m));
351
+ }
352
+ // ============ Graph Traversal ============
353
+ async traverse(startEntityId, options) {
354
+ const maxDepth = options?.maxDepth ?? 3;
355
+ const maxNodes = options?.maxNodes ?? 100;
356
+ const minWeight = options?.minWeight ?? 0;
357
+ const direction = options?.direction ?? 'both';
358
+ const relTypes = options?.relationTypes?.map(relTypeToNeo4j);
359
+ // Get root entity
360
+ const rootResults = await this.cypher.read(`MATCH (e:${ENTITY_LABEL} { entityId: $id }) RETURN e`, { id: startEntityId });
361
+ if (rootResults.length === 0) {
362
+ throw new Error(`Entity not found: ${startEntityId}`);
363
+ }
364
+ const root = this.nodeToEntity(rootResults[0].e);
365
+ // Build direction pattern
366
+ let pattern;
367
+ if (direction === 'outgoing')
368
+ pattern = '(start)-[r:KNOWLEDGE_REL*1..maxD]->(neighbor)';
369
+ else if (direction === 'incoming')
370
+ pattern = '(start)<-[r:KNOWLEDGE_REL*1..maxD]-(neighbor)';
371
+ else
372
+ pattern = '(start)-[r:KNOWLEDGE_REL*1..maxD]-(neighbor)';
373
+ // Replace maxD placeholder
374
+ pattern = pattern.replace('maxD', String(maxDepth));
375
+ const typeFilter = relTypes?.length
376
+ ? 'AND ALL(rel IN relationships(path) WHERE rel.relType IN $relTypes)'
377
+ : '';
378
+ const weightFilter = minWeight > 0
379
+ ? 'AND ALL(rel IN relationships(path) WHERE rel.weight >= $minWeight)'
380
+ : '';
381
+ const results = await this.cypher.read(`MATCH (start:${ENTITY_LABEL} { entityId: $startId })
382
+ MATCH path = ${pattern}
383
+ WHERE neighbor <> start
384
+ ${typeFilter}
385
+ ${weightFilter}
386
+ WITH neighbor, min(length(path)) AS depth, relationships(path) AS rels
387
+ RETURN neighbor, depth, rels
388
+ ORDER BY depth ASC
389
+ LIMIT $maxNodes`, {
390
+ startId: startEntityId,
391
+ relTypes: relTypes ?? [],
392
+ minWeight,
393
+ maxNodes,
394
+ });
395
+ // Organize by depth levels
396
+ const levelMap = new Map();
397
+ for (const row of results) {
398
+ const depth = typeof row.depth === 'object' ? Number(row.depth.low ?? row.depth) : Number(row.depth);
399
+ if (!levelMap.has(depth)) {
400
+ levelMap.set(depth, { entities: [], relations: [] });
401
+ }
402
+ const level = levelMap.get(depth);
403
+ level.entities.push(this.nodeToEntity(row.neighbor));
404
+ }
405
+ const levels = Array.from(levelMap.entries())
406
+ .sort(([a], [b]) => a - b)
407
+ .map(([depth, data]) => ({ depth, ...data }));
408
+ return {
409
+ root,
410
+ levels,
411
+ totalEntities: results.length,
412
+ totalRelations: results.reduce((sum, r) => sum + (r.rels?.length ?? 0), 0),
413
+ };
414
+ }
415
+ async findPath(sourceId, targetId, maxDepth) {
416
+ const max = maxDepth ?? 10;
417
+ const results = await this.cypher.read(`MATCH (src:${ENTITY_LABEL} { entityId: $sourceId }),
418
+ (tgt:${ENTITY_LABEL} { entityId: $targetId })
419
+ MATCH path = shortestPath((src)-[:KNOWLEDGE_REL*1..${max}]-(tgt))
420
+ RETURN [n IN nodes(path) | n] AS pathNodes,
421
+ [r IN relationships(path) | r] AS pathRels`, { sourceId, targetId });
422
+ if (results.length === 0)
423
+ return null;
424
+ const { pathNodes, pathRels } = results[0];
425
+ const result = [];
426
+ for (let i = 0; i < pathNodes.length; i++) {
427
+ const entry = {
428
+ entity: this.nodeToEntity(pathNodes[i]),
429
+ };
430
+ if (i < pathRels.length) {
431
+ entry.relation = this.relPropsToKnowledgeRelation(pathRels[i]);
432
+ }
433
+ result.push(entry);
434
+ }
435
+ return result;
436
+ }
437
+ async getNeighborhood(entityId, depth) {
438
+ const d = depth ?? 1;
439
+ const results = await this.cypher.read(`MATCH (e:${ENTITY_LABEL} { entityId: $entityId })-[r:KNOWLEDGE_REL*1..${d}]-(n:${ENTITY_LABEL})
440
+ WHERE n <> e
441
+ UNWIND r AS rel
442
+ WITH DISTINCT n, rel
443
+ RETURN n, rel`, { entityId });
444
+ const entityMap = new Map();
445
+ const relations = [];
446
+ for (const row of results) {
447
+ const entity = this.nodeToEntity(row.n);
448
+ entityMap.set(entity.id, entity);
449
+ if (row.r) {
450
+ relations.push(this.relPropsToKnowledgeRelation(row.r));
451
+ }
452
+ }
453
+ return {
454
+ entities: Array.from(entityMap.values()),
455
+ relations,
456
+ };
457
+ }
458
+ // ============ Semantic Search ============
459
+ async semanticSearch(options) {
460
+ // Without embedding manager here, we fall back to text-based search.
461
+ // When the caller provides query embeddings via the full stack, vector search is used.
462
+ const topK = options.topK ?? 10;
463
+ const minSim = options.minSimilarity ?? 0;
464
+ const results = [];
465
+ const scope = options.scope ?? 'all';
466
+ if (scope === 'entities' || scope === 'all') {
467
+ const conditions = [];
468
+ const params = { query: options.query, limit: topK };
469
+ if (options.entityTypes?.length) {
470
+ conditions.push('e.type IN $entityTypes');
471
+ params.entityTypes = options.entityTypes;
472
+ }
473
+ if (options.ownerId) {
474
+ conditions.push('e.ownerId = $ownerId');
475
+ params.ownerId = options.ownerId;
476
+ }
477
+ const where = conditions.length > 0 ? `AND ${conditions.join(' AND ')}` : '';
478
+ const entityResults = await this.cypher.read(`MATCH (e:${ENTITY_LABEL})
479
+ WHERE (e.label CONTAINS $query OR e.properties_json CONTAINS $query) ${where}
480
+ RETURN e LIMIT $limit`, params);
481
+ for (const row of entityResults) {
482
+ results.push({
483
+ item: this.nodeToEntity(row.e),
484
+ type: 'entity',
485
+ similarity: 0.5, // Placeholder — real score from vector search
486
+ });
487
+ }
488
+ }
489
+ if (scope === 'memories' || scope === 'all') {
490
+ const memResults = await this.cypher.read(`MATCH (m:${MEMORY_LABEL})
491
+ WHERE m.summary CONTAINS $query OR m.description CONTAINS $query
492
+ RETURN m LIMIT $limit`, { query: options.query, limit: topK });
493
+ for (const row of memResults) {
494
+ results.push({
495
+ item: this.nodeToMemory(row.m),
496
+ type: 'memory',
497
+ similarity: 0.5,
498
+ });
499
+ }
500
+ }
501
+ return results
502
+ .filter((r) => r.similarity >= minSim)
503
+ .sort((a, b) => b.similarity - a.similarity)
504
+ .slice(0, topK);
505
+ }
506
+ // ============ Knowledge Extraction ============
507
+ async extractFromText(text, _options) {
508
+ // Extraction requires LLM — this is a placeholder.
509
+ // The actual extraction pipeline lives in the orchestration layer.
510
+ return { entities: [], relations: [] };
511
+ }
512
+ // ============ Maintenance ============
513
+ async mergeEntities(entityIds, primaryId) {
514
+ // Redirect all relations from secondary entities to primary
515
+ const secondaryIds = entityIds.filter((id) => id !== primaryId);
516
+ for (const secId of secondaryIds) {
517
+ // Outgoing relations: sec -> target becomes primary -> target
518
+ await this.cypher.writeVoid(`MATCH (sec:${ENTITY_LABEL} { entityId: $secId })-[r:KNOWLEDGE_REL]->(tgt:${ENTITY_LABEL})
519
+ MATCH (primary:${ENTITY_LABEL} { entityId: $primaryId })
520
+ WHERE NOT (primary)-[:KNOWLEDGE_REL]->(tgt)
521
+ CREATE (primary)-[r2:KNOWLEDGE_REL]->(tgt)
522
+ SET r2 = properties(r)
523
+ DELETE r`, { secId, primaryId });
524
+ // Incoming relations: source -> sec becomes source -> primary
525
+ await this.cypher.writeVoid(`MATCH (src:${ENTITY_LABEL})-[r:KNOWLEDGE_REL]->(sec:${ENTITY_LABEL} { entityId: $secId })
526
+ MATCH (primary:${ENTITY_LABEL} { entityId: $primaryId })
527
+ WHERE NOT (src)-[:KNOWLEDGE_REL]->(primary)
528
+ CREATE (src)-[r2:KNOWLEDGE_REL]->(primary)
529
+ SET r2 = properties(r)
530
+ DELETE r`, { secId, primaryId });
531
+ // Memory links: memory -> sec becomes memory -> primary
532
+ await this.cypher.writeVoid(`MATCH (m:${MEMORY_LABEL})-[r:REFERS_TO]->(sec:${ENTITY_LABEL} { entityId: $secId })
533
+ MATCH (primary:${ENTITY_LABEL} { entityId: $primaryId })
534
+ MERGE (m)-[:REFERS_TO]->(primary)
535
+ DELETE r`, { secId, primaryId });
536
+ // Delete secondary entity
537
+ await this.cypher.writeVoid(`MATCH (sec:${ENTITY_LABEL} { entityId: $secId }) DETACH DELETE sec`, { secId });
538
+ }
539
+ const entity = await this.getEntity(primaryId);
540
+ if (!entity)
541
+ throw new Error(`Primary entity not found: ${primaryId}`);
542
+ return entity;
543
+ }
544
+ async decayMemories(decayFactor) {
545
+ const factor = decayFactor ?? this.memoryDecayRate;
546
+ const threshold = this.minImportanceThreshold;
547
+ const results = await this.cypher.write(`MATCH (m:${MEMORY_LABEL})
548
+ WHERE m.importance > $threshold
549
+ WITH m,
550
+ duration.between(datetime(m.lastAccessedAt), datetime()).days AS ageDays
551
+ SET m.importance = m.importance * (1.0 - $factor) + log(toFloat(m.accessCount + 1)) * 0.1
552
+ WITH m WHERE m.importance <= $threshold
553
+ DETACH DELETE m
554
+ RETURN count(m) AS decayedCount`, { factor, threshold });
555
+ return Number(results[0]?.decayedCount ?? 0);
556
+ }
557
+ async getStats() {
558
+ const results = await this.cypher.read(`MATCH (e:${ENTITY_LABEL})
559
+ WITH count(e) AS totalEntities,
560
+ avg(e.confidence) AS avgConfidence,
561
+ min(e.createdAt) AS oldest,
562
+ max(e.createdAt) AS newest
563
+ OPTIONAL MATCH ()-[r:KNOWLEDGE_REL]->()
564
+ WITH totalEntities, avgConfidence, oldest, newest, count(r) AS totalRelations
565
+ OPTIONAL MATCH (m:${MEMORY_LABEL})
566
+ RETURN totalEntities, totalRelations, count(m) AS totalMemories,
567
+ avgConfidence, oldest, newest`);
568
+ const row = results[0] ?? {};
569
+ // Entity type counts
570
+ const typeCounts = await this.cypher.read(`MATCH (e:${ENTITY_LABEL}) RETURN e.type AS type, count(e) AS count`);
571
+ const entitiesByType = {};
572
+ for (const tc of typeCounts) {
573
+ entitiesByType[tc.type] = Number(tc.count);
574
+ }
575
+ // Relation type counts
576
+ const relCounts = await this.cypher.read(`MATCH ()-[r:KNOWLEDGE_REL]->() RETURN r.relType AS type, count(r) AS count`);
577
+ const relationsByType = {};
578
+ for (const rc of relCounts) {
579
+ relationsByType[neo4jToRelType(rc.type)] = Number(rc.count);
580
+ }
581
+ return {
582
+ totalEntities: Number(row.totalEntities ?? 0),
583
+ totalRelations: Number(row.totalRelations ?? 0),
584
+ totalMemories: Number(row.totalMemories ?? 0),
585
+ entitiesByType: entitiesByType,
586
+ relationsByType: relationsByType,
587
+ avgConfidence: Number(row.avgConfidence ?? 0),
588
+ oldestEntry: row.oldest ?? '',
589
+ newestEntry: row.newest ?? '',
590
+ };
591
+ }
592
+ async clear() {
593
+ await this.cypher.writeVoid(`MATCH (n:${ENTITY_LABEL}) DETACH DELETE n`);
594
+ await this.cypher.writeVoid(`MATCH (n:${MEMORY_LABEL}) DETACH DELETE n`);
595
+ }
596
+ // ============ Private Helpers ============
597
+ nodeToEntity(node, fallbackId, fallbackNow) {
598
+ const props = node?.properties ?? node ?? {};
599
+ return {
600
+ id: props.entityId ?? fallbackId ?? '',
601
+ type: props.type ?? 'custom',
602
+ label: props.label ?? '',
603
+ properties: this.safeParseJson(props.properties_json, {}),
604
+ embedding: props.embedding ?? undefined,
605
+ confidence: Number(props.confidence ?? 0),
606
+ source: this.safeParseJson(props.source_json, { type: 'system', timestamp: '' }),
607
+ createdAt: props.createdAt ?? fallbackNow ?? '',
608
+ updatedAt: props.updatedAt ?? fallbackNow ?? '',
609
+ ownerId: props.ownerId ?? undefined,
610
+ tags: props.tags ?? [],
611
+ metadata: props.metadata_json ? this.safeParseJson(props.metadata_json, undefined) : undefined,
612
+ };
613
+ }
614
+ nodeToMemory(node) {
615
+ const props = node?.properties ?? node ?? {};
616
+ return {
617
+ id: props.memoryId ?? '',
618
+ type: props.type ?? 'interaction',
619
+ summary: props.summary ?? '',
620
+ description: props.description ?? undefined,
621
+ participants: props.participants ?? [],
622
+ valence: props.valence ?? undefined,
623
+ importance: Number(props.importance ?? 0),
624
+ entityIds: props.entityIds ?? [],
625
+ embedding: props.embedding ?? undefined,
626
+ occurredAt: props.occurredAt ?? '',
627
+ durationMs: props.durationMs ? Number(props.durationMs) : undefined,
628
+ outcome: props.outcome ?? undefined,
629
+ insights: props.insights_json ? this.safeParseJson(props.insights_json, []) : undefined,
630
+ context: props.context_json ? this.safeParseJson(props.context_json, undefined) : undefined,
631
+ createdAt: props.createdAt ?? '',
632
+ accessCount: Number(props.accessCount ?? 0),
633
+ lastAccessedAt: props.lastAccessedAt ?? '',
634
+ };
635
+ }
636
+ relToKnowledgeRelation(rel, fallbackId, fallbackSrcId, fallbackTgtId, fallbackNow) {
637
+ const props = rel?.properties ?? rel ?? {};
638
+ return {
639
+ id: props.relationId ?? fallbackId ?? '',
640
+ sourceId: fallbackSrcId ?? '',
641
+ targetId: fallbackTgtId ?? '',
642
+ type: neo4jToRelType(props.relType ?? 'RELATED_TO'),
643
+ label: props.label ?? '',
644
+ properties: props.properties_json ? this.safeParseJson(props.properties_json, {}) : undefined,
645
+ weight: Number(props.weight ?? 0),
646
+ bidirectional: Boolean(props.bidirectional),
647
+ confidence: Number(props.confidence ?? 0),
648
+ source: this.safeParseJson(props.source_json, { type: 'system', timestamp: '' }),
649
+ createdAt: props.createdAt ?? fallbackNow ?? '',
650
+ validFrom: props.validFrom ?? undefined,
651
+ validTo: props.validTo ?? undefined,
652
+ };
653
+ }
654
+ relPropsToKnowledgeRelation(rel) {
655
+ const props = rel?.properties ?? rel ?? {};
656
+ return {
657
+ id: props.relationId ?? '',
658
+ sourceId: '',
659
+ targetId: '',
660
+ type: neo4jToRelType(props.relType ?? 'RELATED_TO'),
661
+ label: props.label ?? '',
662
+ properties: props.properties_json ? this.safeParseJson(props.properties_json, {}) : undefined,
663
+ weight: Number(props.weight ?? 0),
664
+ bidirectional: Boolean(props.bidirectional),
665
+ confidence: Number(props.confidence ?? 0),
666
+ source: this.safeParseJson(props.source_json, { type: 'system', timestamp: '' }),
667
+ createdAt: props.createdAt ?? '',
668
+ validFrom: props.validFrom ?? undefined,
669
+ validTo: props.validTo ?? undefined,
670
+ };
671
+ }
672
+ safeParseJson(json, fallback) {
673
+ if (!json)
674
+ return fallback;
675
+ try {
676
+ return JSON.parse(json);
677
+ }
678
+ catch {
679
+ return fallback;
680
+ }
681
+ }
682
+ }
683
+ //# sourceMappingURL=Neo4jKnowledgeGraph.js.map