@henrychong-ai/mcp-neo4j-knowledge-graph 1.0.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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +718 -0
  3. package/dist/KnowledgeGraphManager.d.ts +215 -0
  4. package/dist/KnowledgeGraphManager.js +910 -0
  5. package/dist/KnowledgeGraphManager.js.map +1 -0
  6. package/dist/callToolHandler.d.ts +5 -0
  7. package/dist/callToolHandler.js +26 -0
  8. package/dist/callToolHandler.js.map +1 -0
  9. package/dist/cli/neo4j-setup.d.ts +52 -0
  10. package/dist/cli/neo4j-setup.js +258 -0
  11. package/dist/cli/neo4j-setup.js.map +1 -0
  12. package/dist/config/paths.d.ts +13 -0
  13. package/dist/config/paths.js +41 -0
  14. package/dist/config/paths.js.map +1 -0
  15. package/dist/config/storage.d.ts +35 -0
  16. package/dist/config/storage.js +52 -0
  17. package/dist/config/storage.js.map +1 -0
  18. package/dist/embeddings/DefaultEmbeddingService.d.ts +64 -0
  19. package/dist/embeddings/DefaultEmbeddingService.js +139 -0
  20. package/dist/embeddings/DefaultEmbeddingService.js.map +1 -0
  21. package/dist/embeddings/EmbeddingJobManager.d.ts +212 -0
  22. package/dist/embeddings/EmbeddingJobManager.js +545 -0
  23. package/dist/embeddings/EmbeddingJobManager.js.map +1 -0
  24. package/dist/embeddings/EmbeddingService.d.ts +96 -0
  25. package/dist/embeddings/EmbeddingService.js +44 -0
  26. package/dist/embeddings/EmbeddingService.js.map +1 -0
  27. package/dist/embeddings/EmbeddingServiceFactory.d.ts +72 -0
  28. package/dist/embeddings/EmbeddingServiceFactory.js +147 -0
  29. package/dist/embeddings/EmbeddingServiceFactory.js.map +1 -0
  30. package/dist/embeddings/OpenAIEmbeddingService.d.ts +73 -0
  31. package/dist/embeddings/OpenAIEmbeddingService.js +195 -0
  32. package/dist/embeddings/OpenAIEmbeddingService.js.map +1 -0
  33. package/dist/embeddings/config.d.ts +83 -0
  34. package/dist/embeddings/config.js +65 -0
  35. package/dist/embeddings/config.js.map +1 -0
  36. package/dist/index.d.ts +4 -0
  37. package/dist/index.js +220 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/server/handlers/callToolHandler.d.ts +20 -0
  40. package/dist/server/handlers/callToolHandler.js +505 -0
  41. package/dist/server/handlers/callToolHandler.js.map +1 -0
  42. package/dist/server/handlers/listToolsHandler.d.ts +7 -0
  43. package/dist/server/handlers/listToolsHandler.js +511 -0
  44. package/dist/server/handlers/listToolsHandler.js.map +1 -0
  45. package/dist/server/handlers/toolHandlers/addObservations.d.ts +12 -0
  46. package/dist/server/handlers/toolHandlers/addObservations.js +99 -0
  47. package/dist/server/handlers/toolHandlers/addObservations.js.map +1 -0
  48. package/dist/server/handlers/toolHandlers/createEntities.d.ts +12 -0
  49. package/dist/server/handlers/toolHandlers/createEntities.js +20 -0
  50. package/dist/server/handlers/toolHandlers/createEntities.js.map +1 -0
  51. package/dist/server/handlers/toolHandlers/createRelations.d.ts +12 -0
  52. package/dist/server/handlers/toolHandlers/createRelations.js +20 -0
  53. package/dist/server/handlers/toolHandlers/createRelations.js.map +1 -0
  54. package/dist/server/handlers/toolHandlers/deleteEntities.d.ts +12 -0
  55. package/dist/server/handlers/toolHandlers/deleteEntities.js +20 -0
  56. package/dist/server/handlers/toolHandlers/deleteEntities.js.map +1 -0
  57. package/dist/server/handlers/toolHandlers/index.d.ts +8 -0
  58. package/dist/server/handlers/toolHandlers/index.js +9 -0
  59. package/dist/server/handlers/toolHandlers/index.js.map +1 -0
  60. package/dist/server/handlers/toolHandlers/readGraph.d.ts +12 -0
  61. package/dist/server/handlers/toolHandlers/readGraph.js +20 -0
  62. package/dist/server/handlers/toolHandlers/readGraph.js.map +1 -0
  63. package/dist/server/setup.d.ts +8 -0
  64. package/dist/server/setup.js +48 -0
  65. package/dist/server/setup.js.map +1 -0
  66. package/dist/storage/FileStorageProvider.d.ts +125 -0
  67. package/dist/storage/FileStorageProvider.js +322 -0
  68. package/dist/storage/FileStorageProvider.js.map +1 -0
  69. package/dist/storage/SearchResultCache.d.ts +102 -0
  70. package/dist/storage/SearchResultCache.js +258 -0
  71. package/dist/storage/SearchResultCache.js.map +1 -0
  72. package/dist/storage/StorageProvider.d.ts +171 -0
  73. package/dist/storage/StorageProvider.js +46 -0
  74. package/dist/storage/StorageProvider.js.map +1 -0
  75. package/dist/storage/StorageProviderFactory.d.ts +63 -0
  76. package/dist/storage/StorageProviderFactory.js +113 -0
  77. package/dist/storage/StorageProviderFactory.js.map +1 -0
  78. package/dist/storage/VectorStoreFactory.d.ts +43 -0
  79. package/dist/storage/VectorStoreFactory.js +41 -0
  80. package/dist/storage/VectorStoreFactory.js.map +1 -0
  81. package/dist/storage/neo4j/Neo4jConfig.d.ts +37 -0
  82. package/dist/storage/neo4j/Neo4jConfig.js +13 -0
  83. package/dist/storage/neo4j/Neo4jConfig.js.map +1 -0
  84. package/dist/storage/neo4j/Neo4jConnectionManager.d.ts +40 -0
  85. package/dist/storage/neo4j/Neo4jConnectionManager.js +58 -0
  86. package/dist/storage/neo4j/Neo4jConnectionManager.js.map +1 -0
  87. package/dist/storage/neo4j/Neo4jSchemaManager.d.ts +74 -0
  88. package/dist/storage/neo4j/Neo4jSchemaManager.js +224 -0
  89. package/dist/storage/neo4j/Neo4jSchemaManager.js.map +1 -0
  90. package/dist/storage/neo4j/Neo4jStorageProvider.d.ts +225 -0
  91. package/dist/storage/neo4j/Neo4jStorageProvider.js +1900 -0
  92. package/dist/storage/neo4j/Neo4jStorageProvider.js.map +1 -0
  93. package/dist/storage/neo4j/Neo4jVectorStore.d.ts +80 -0
  94. package/dist/storage/neo4j/Neo4jVectorStore.js +396 -0
  95. package/dist/storage/neo4j/Neo4jVectorStore.js.map +1 -0
  96. package/dist/types/entity-embedding.d.ts +156 -0
  97. package/dist/types/entity-embedding.js +2 -0
  98. package/dist/types/entity-embedding.js.map +1 -0
  99. package/dist/types/relation.d.ts +77 -0
  100. package/dist/types/relation.js +93 -0
  101. package/dist/types/relation.js.map +1 -0
  102. package/dist/types/temporalEntity.d.ts +55 -0
  103. package/dist/types/temporalEntity.js +66 -0
  104. package/dist/types/temporalEntity.js.map +1 -0
  105. package/dist/types/temporalRelation.d.ts +60 -0
  106. package/dist/types/temporalRelation.js +89 -0
  107. package/dist/types/temporalRelation.js.map +1 -0
  108. package/dist/types/vector-index.d.ts +48 -0
  109. package/dist/types/vector-index.js +2 -0
  110. package/dist/types/vector-index.js.map +1 -0
  111. package/dist/types/vector-store.d.ts +16 -0
  112. package/dist/types/vector-store.js +2 -0
  113. package/dist/types/vector-store.js.map +1 -0
  114. package/dist/utils/fs.d.ts +2 -0
  115. package/dist/utils/fs.js +3 -0
  116. package/dist/utils/fs.js.map +1 -0
  117. package/dist/utils/logger.d.ts +10 -0
  118. package/dist/utils/logger.js +35 -0
  119. package/dist/utils/logger.js.map +1 -0
  120. package/package.json +85 -0
@@ -0,0 +1,1900 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { Neo4jConnectionManager } from './Neo4jConnectionManager.js';
3
+ import { DEFAULT_NEO4J_CONFIG } from './Neo4jConfig.js';
4
+ import { Neo4jSchemaManager } from './Neo4jSchemaManager.js';
5
+ import { logger } from '../../utils/logger.js';
6
+ import neo4j from 'neo4j-driver';
7
+ import { Neo4jVectorStore } from './Neo4jVectorStore.js';
8
+ import { EmbeddingServiceFactory } from '../../embeddings/EmbeddingServiceFactory.js';
9
+ /**
10
+ * A storage provider that uses Neo4j to store the knowledge graph
11
+ */
12
+ export class Neo4jStorageProvider {
13
+ /**
14
+ * Create a new Neo4jStorageProvider
15
+ * @param options Configuration options
16
+ */
17
+ constructor(options) {
18
+ this.embeddingService = null;
19
+ // Set up configuration
20
+ this.config = {
21
+ ...DEFAULT_NEO4J_CONFIG,
22
+ ...(options?.config || {}),
23
+ };
24
+ // Configure decay settings
25
+ this.decayConfig = {
26
+ enabled: options?.decayConfig?.enabled ?? true,
27
+ halfLifeDays: options?.decayConfig?.halfLifeDays ?? 30,
28
+ minConfidence: options?.decayConfig?.minConfidence ?? 0.1,
29
+ };
30
+ // Set up connection manager
31
+ this.connectionManager = options?.connectionManager || new Neo4jConnectionManager(this.config);
32
+ // Set up schema manager
33
+ this.schemaManager = new Neo4jSchemaManager(this.connectionManager, this.config, false);
34
+ // Set up vector store
35
+ this.vectorStore = new Neo4jVectorStore({
36
+ connectionManager: this.connectionManager,
37
+ indexName: this.config.vectorIndexName,
38
+ dimensions: 1536,
39
+ similarityFunction: 'cosine',
40
+ entityNodeLabel: 'Entity',
41
+ });
42
+ logger.debug('Neo4jStorageProvider: Initializing embedding service');
43
+ try {
44
+ // Set up embedding service
45
+ this.embeddingService = EmbeddingServiceFactory.createFromEnvironment();
46
+ logger.debug('Neo4jStorageProvider: Embedding service initialized successfully', {
47
+ provider: this.embeddingService.getProviderInfo().provider,
48
+ model: this.embeddingService.getProviderInfo().model,
49
+ dimensions: this.embeddingService.getProviderInfo().dimensions,
50
+ });
51
+ }
52
+ catch (error) {
53
+ logger.error('Neo4jStorageProvider: Failed to initialize embedding service', error);
54
+ }
55
+ // Initialize the schema and vector store
56
+ this.initializeSchema().catch((err) => {
57
+ logger.error('Failed to initialize Neo4j schema', err);
58
+ });
59
+ }
60
+ /**
61
+ * Get the connection manager (primarily for testing)
62
+ */
63
+ getConnectionManager() {
64
+ return this.connectionManager;
65
+ }
66
+ /**
67
+ * Initialize Neo4j schema
68
+ */
69
+ async initializeSchema() {
70
+ try {
71
+ await this.schemaManager.initializeSchema(false);
72
+ logger.info('Neo4j schema initialized successfully');
73
+ // Initialize vector store after schema is ready
74
+ try {
75
+ await this.vectorStore.initialize();
76
+ logger.info('Neo4j vector store initialized successfully');
77
+ }
78
+ catch (vectorError) {
79
+ logger.error('Failed to initialize Neo4j vector store', vectorError);
80
+ // Continue even if vector store initialization fails
81
+ }
82
+ }
83
+ catch (schemaError) {
84
+ logger.error('Failed to initialize Neo4j schema', schemaError);
85
+ throw schemaError;
86
+ }
87
+ }
88
+ /**
89
+ * Close Neo4j connections
90
+ */
91
+ async close() {
92
+ try {
93
+ await this.connectionManager.close();
94
+ logger.debug('Neo4j connections closed');
95
+ }
96
+ catch (error) {
97
+ logger.error('Error closing Neo4j connections', error);
98
+ }
99
+ }
100
+ /**
101
+ * Convert a Neo4j node to an entity object
102
+ * @param node Neo4j node properties
103
+ * @returns Entity object
104
+ */
105
+ nodeToEntity(node) {
106
+ const observations = typeof node.observations === 'string' ? JSON.parse(node.observations) : [];
107
+ return {
108
+ name: node.name,
109
+ entityType: node.entityType,
110
+ observations,
111
+ id: node.id,
112
+ version: node.version,
113
+ createdAt: node.createdAt,
114
+ updatedAt: node.updatedAt,
115
+ validFrom: node.validFrom,
116
+ validTo: node.validTo,
117
+ changedBy: node.changedBy,
118
+ };
119
+ }
120
+ /**
121
+ * Parse a Neo4j relationship into a relation object
122
+ * @param rel Relationship properties
123
+ * @param fromNode From node name
124
+ * @param toNode To node name
125
+ * @returns Relation object
126
+ */
127
+ /**
128
+ * Parse a Neo4j relationship into a relation object
129
+ * @param rel Relationship properties
130
+ * @param fromNode From node name
131
+ * @param toNode To node name
132
+ * @returns Relation object
133
+ */
134
+ relationshipToRelation(rel, fromNode, toNode) {
135
+ // Extract timestamps from the Neo4j relation for metadata
136
+ const now = Date.now();
137
+ const createdAt = rel.createdAt || now;
138
+ const updatedAt = rel.updatedAt || now;
139
+ // Create metadata with required fields
140
+ const metadata = {
141
+ createdAt,
142
+ updatedAt,
143
+ };
144
+ // Try to merge any additional metadata from the relation
145
+ if (typeof rel.metadata === 'string' && rel.metadata) {
146
+ try {
147
+ const parsedMetadata = JSON.parse(rel.metadata);
148
+ Object.assign(metadata, parsedMetadata);
149
+ }
150
+ catch {
151
+ logger.warn(`Failed to parse metadata for relation from ${fromNode} to ${toNode}`);
152
+ }
153
+ }
154
+ // Create a standard Relation object with proper type handling
155
+ return {
156
+ from: fromNode,
157
+ to: toNode,
158
+ relationType: rel.relationType,
159
+ // Convert null to undefined for compatibility with Relation interface
160
+ strength: rel.strength === null ? undefined : rel.strength,
161
+ confidence: rel.confidence === null ? undefined : rel.confidence,
162
+ metadata,
163
+ };
164
+ }
165
+ /**
166
+ * Load the complete knowledge graph from Neo4j
167
+ */
168
+ async loadGraph() {
169
+ try {
170
+ const startTime = Date.now();
171
+ // Load entities query
172
+ const entityQuery = `
173
+ MATCH (e:Entity)
174
+ WHERE e.validTo IS NULL
175
+ RETURN e
176
+ `;
177
+ // Execute query to get all current entities
178
+ const entityResult = await this.connectionManager.executeQuery(entityQuery, {});
179
+ // Process entity results
180
+ const entities = entityResult.records.map((record) => {
181
+ const node = record.get('e').properties;
182
+ return this.nodeToEntity(node);
183
+ });
184
+ // Load relations query
185
+ const relationQuery = `
186
+ MATCH (from:Entity)-[r:RELATES_TO]->(to:Entity)
187
+ WHERE r.validTo IS NULL
188
+ RETURN from.name AS fromName, to.name AS toName, r
189
+ `;
190
+ // Execute query to get all current relations
191
+ const relationResult = await this.connectionManager.executeQuery(relationQuery, {});
192
+ // Process relation results
193
+ const relations = relationResult.records.map((record) => {
194
+ const fromName = record.get('fromName');
195
+ const toName = record.get('toName');
196
+ const rel = record.get('r').properties;
197
+ return this.relationshipToRelation(rel, fromName, toName);
198
+ });
199
+ const timeTaken = Date.now() - startTime;
200
+ // Return the complete graph
201
+ return {
202
+ entities,
203
+ relations,
204
+ total: entities.length,
205
+ timeTaken,
206
+ };
207
+ }
208
+ catch (error) {
209
+ logger.error('Error loading graph from Neo4j', error);
210
+ throw error;
211
+ }
212
+ }
213
+ /**
214
+ * Save a complete knowledge graph to Neo4j (warning: this will overwrite existing data)
215
+ * @param graph The knowledge graph to save
216
+ */
217
+ async saveGraph(graph) {
218
+ try {
219
+ // Start a new session
220
+ const session = await this.connectionManager.getSession();
221
+ try {
222
+ // Begin transaction
223
+ const txc = session.beginTransaction();
224
+ try {
225
+ // Delete all existing data
226
+ await txc.run('MATCH (n) DETACH DELETE n', {});
227
+ // Process entities
228
+ for (const entity of graph.entities) {
229
+ const extendedEntity = entity;
230
+ const params = {
231
+ id: extendedEntity.id || uuidv4(),
232
+ name: entity.name,
233
+ entityType: entity.entityType,
234
+ observations: JSON.stringify(entity.observations || []),
235
+ version: extendedEntity.version || 1,
236
+ createdAt: extendedEntity.createdAt || Date.now(),
237
+ updatedAt: extendedEntity.updatedAt || Date.now(),
238
+ validFrom: extendedEntity.validFrom || Date.now(),
239
+ validTo: extendedEntity.validTo || null,
240
+ changedBy: extendedEntity.changedBy || null,
241
+ };
242
+ // Create entity
243
+ await txc.run(`
244
+ CREATE (e:Entity {
245
+ id: $id,
246
+ name: $name,
247
+ entityType: $entityType,
248
+ observations: $observations,
249
+ version: $version,
250
+ createdAt: $createdAt,
251
+ updatedAt: $updatedAt,
252
+ validFrom: $validFrom,
253
+ validTo: $validTo,
254
+ changedBy: $changedBy
255
+ })
256
+ `, params);
257
+ }
258
+ // Process relations
259
+ for (const relation of graph.relations) {
260
+ const extendedRelation = relation;
261
+ const params = {
262
+ id: extendedRelation.id || uuidv4(),
263
+ fromName: relation.from,
264
+ toName: relation.to,
265
+ relationType: relation.relationType,
266
+ strength: relation.strength || null,
267
+ confidence: relation.confidence || null,
268
+ metadata: relation.metadata ? JSON.stringify(relation.metadata) : null,
269
+ version: extendedRelation.version || 1,
270
+ createdAt: extendedRelation.createdAt || Date.now(),
271
+ updatedAt: extendedRelation.updatedAt || Date.now(),
272
+ validFrom: extendedRelation.validFrom || Date.now(),
273
+ validTo: extendedRelation.validTo || null,
274
+ changedBy: extendedRelation.changedBy || null,
275
+ };
276
+ // Create relation
277
+ await txc.run(`
278
+ MATCH (from:Entity {name: $fromName})
279
+ MATCH (to:Entity {name: $toName})
280
+ CREATE (from)-[r:RELATES_TO {
281
+ id: $id,
282
+ relationType: $relationType,
283
+ strength: $strength,
284
+ confidence: $confidence,
285
+ metadata: $metadata,
286
+ version: $version,
287
+ createdAt: $createdAt,
288
+ updatedAt: $updatedAt,
289
+ validFrom: $validFrom,
290
+ validTo: $validTo,
291
+ changedBy: $changedBy
292
+ }]->(to)
293
+ `, params);
294
+ }
295
+ // Commit transaction
296
+ await txc.commit();
297
+ logger.info(`Saved graph with ${graph.entities.length} entities and ${graph.relations.length} relations to Neo4j`);
298
+ }
299
+ catch (error) {
300
+ // Rollback on error
301
+ await txc.rollback();
302
+ throw error;
303
+ }
304
+ }
305
+ finally {
306
+ // Close session
307
+ await session.close();
308
+ }
309
+ }
310
+ catch (error) {
311
+ logger.error('Error saving graph to Neo4j', error);
312
+ throw error;
313
+ }
314
+ }
315
+ /**
316
+ * Search for nodes in the graph that match the query
317
+ * @param query The search query string
318
+ * @param options Optional search parameters
319
+ */
320
+ async searchNodes(query, options = {}) {
321
+ try {
322
+ const startTime = Date.now();
323
+ // Prepare search parameters
324
+ const rawLimit = options.limit || 10;
325
+ const parameters = {
326
+ query: `(?i).*${query}.*`, // Case-insensitive regex pattern
327
+ limit: neo4j.int(Math.floor(rawLimit)),
328
+ };
329
+ // Add entity type filter if provided
330
+ let entityTypeFilter = '';
331
+ if (options.entityTypes && options.entityTypes.length > 0) {
332
+ entityTypeFilter = 'AND e.entityType IN $entityTypes';
333
+ parameters.entityTypes = options.entityTypes;
334
+ }
335
+ // Build the search query
336
+ const searchQuery = `
337
+ MATCH (e:Entity)
338
+ WHERE (e.name =~ $query OR e.entityType =~ $query OR e.observations =~ $query)
339
+ ${entityTypeFilter}
340
+ AND e.validTo IS NULL
341
+ RETURN e
342
+ LIMIT $limit
343
+ `;
344
+ // Execute the search
345
+ const result = await this.connectionManager.executeQuery(searchQuery, parameters);
346
+ // Process entity results
347
+ const entities = result.records.map((record) => {
348
+ const node = record.get('e').properties;
349
+ return this.nodeToEntity(node);
350
+ });
351
+ // Get relations between found entities
352
+ const entityNames = entities.map((e) => e.name);
353
+ if (entityNames.length > 0) {
354
+ const relationsQuery = `
355
+ MATCH (from:Entity)-[r:RELATES_TO]->(to:Entity)
356
+ WHERE from.name IN $entityNames
357
+ AND to.name IN $entityNames
358
+ AND r.validTo IS NULL
359
+ RETURN from.name AS fromName, to.name AS toName, r
360
+ `;
361
+ const relationsResult = await this.connectionManager.executeQuery(relationsQuery, {
362
+ entityNames,
363
+ });
364
+ // Process relation results
365
+ const relations = relationsResult.records.map((record) => {
366
+ const fromName = record.get('fromName');
367
+ const toName = record.get('toName');
368
+ const rel = record.get('r').properties;
369
+ return this.relationshipToRelation(rel, fromName, toName);
370
+ });
371
+ const timeTaken = Date.now() - startTime;
372
+ // Return the search results as a graph
373
+ return {
374
+ entities,
375
+ relations,
376
+ total: entities.length,
377
+ timeTaken,
378
+ };
379
+ }
380
+ const timeTaken = Date.now() - startTime;
381
+ // Return just the entities if no relations
382
+ return {
383
+ entities,
384
+ relations: [],
385
+ total: entities.length,
386
+ timeTaken,
387
+ };
388
+ }
389
+ catch (error) {
390
+ logger.error('Error searching nodes in Neo4j', error);
391
+ throw error;
392
+ }
393
+ }
394
+ /**
395
+ * Open specific nodes by their exact names
396
+ * @param names Array of node names to open
397
+ */
398
+ async openNodes(names) {
399
+ try {
400
+ const startTime = Date.now();
401
+ if (!names || names.length === 0) {
402
+ return { entities: [], relations: [] };
403
+ }
404
+ // Query for entities by name
405
+ const entityQuery = `
406
+ MATCH (e:Entity)
407
+ WHERE e.name IN $names
408
+ AND e.validTo IS NULL
409
+ RETURN e
410
+ `;
411
+ // Execute query to get entities
412
+ const entityResult = await this.connectionManager.executeQuery(entityQuery, { names });
413
+ // Process entity results
414
+ const entities = entityResult.records.map((record) => {
415
+ const node = record.get('e').properties;
416
+ return this.nodeToEntity(node);
417
+ });
418
+ // Get relations between the specified entities
419
+ const relationsQuery = `
420
+ MATCH (from:Entity)-[r:RELATES_TO]->(to:Entity)
421
+ WHERE from.name IN $names
422
+ AND to.name IN $names
423
+ AND r.validTo IS NULL
424
+ RETURN from.name AS fromName, to.name AS toName, r
425
+ `;
426
+ // Execute query to get relations
427
+ const relationsResult = await this.connectionManager.executeQuery(relationsQuery, { names });
428
+ // Process relation results
429
+ const relations = relationsResult.records.map((record) => {
430
+ const fromName = record.get('fromName');
431
+ const toName = record.get('toName');
432
+ const rel = record.get('r').properties;
433
+ return this.relationshipToRelation(rel, fromName, toName);
434
+ });
435
+ const timeTaken = Date.now() - startTime;
436
+ // Return the entities and their relations
437
+ return {
438
+ entities,
439
+ relations,
440
+ total: entities.length,
441
+ timeTaken,
442
+ };
443
+ }
444
+ catch (error) {
445
+ logger.error('Error opening nodes in Neo4j', error);
446
+ throw error;
447
+ }
448
+ }
449
+ /**
450
+ * Create new entities in the knowledge graph
451
+ * @param entities Array of entities to create
452
+ */
453
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
454
+ async createEntities(entities) {
455
+ try {
456
+ if (!entities || entities.length === 0) {
457
+ return [];
458
+ }
459
+ const session = await this.connectionManager.getSession();
460
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
461
+ const createdEntities = [];
462
+ try {
463
+ // Begin transaction
464
+ const txc = session.beginTransaction();
465
+ try {
466
+ for (const entity of entities) {
467
+ // Generate temporal and identity metadata
468
+ const now = Date.now();
469
+ const entityId = uuidv4();
470
+ // Add debug log for embedding generation attempts
471
+ logger.debug(`Neo4jStorageProvider: Processing embeddings for entity "${entity.name}"`, {
472
+ entityType: entity.entityType,
473
+ hasEmbeddingService: !!this.embeddingService,
474
+ });
475
+ // Generate embedding if embedding service is available
476
+ let embedding = null;
477
+ if (this.embeddingService) {
478
+ try {
479
+ // Prepare text for embedding
480
+ const text = Array.isArray(entity.observations)
481
+ ? entity.observations.join('\n')
482
+ : '';
483
+ // Generate embedding using the instance's embedding service
484
+ embedding = await this.embeddingService.generateEmbedding(text);
485
+ logger.info(`Generated embedding for entity: ${entity.name}`);
486
+ }
487
+ catch (error) {
488
+ logger.error(`Failed to generate embedding for entity: ${entity.name}`, error);
489
+ // Continue without embedding if generation fails
490
+ }
491
+ }
492
+ else {
493
+ logger.warn(`Neo4jStorageProvider: Skipping embedding for entity "${entity.name}" - No embedding service available`);
494
+ }
495
+ // Create entity with parameters
496
+ const params = {
497
+ id: entityId,
498
+ name: entity.name,
499
+ entityType: entity.entityType,
500
+ observations: JSON.stringify(entity.observations || []),
501
+ version: 1,
502
+ createdAt: entity.createdAt || now,
503
+ updatedAt: entity.updatedAt || now,
504
+ validFrom: entity.validFrom || now,
505
+ validTo: null,
506
+ changedBy: entity.changedBy || null,
507
+ embedding: embedding, // Add embedding directly to entity
508
+ };
509
+ // Create entity query
510
+ const createQuery = `
511
+ CREATE (e:Entity {
512
+ id: $id,
513
+ name: $name,
514
+ entityType: $entityType,
515
+ observations: $observations,
516
+ version: $version,
517
+ createdAt: $createdAt,
518
+ updatedAt: $updatedAt,
519
+ validFrom: $validFrom,
520
+ validTo: $validTo,
521
+ changedBy: $changedBy,
522
+ embedding: $embedding
523
+ })
524
+ RETURN e
525
+ `;
526
+ // Execute query
527
+ const result = await txc.run(createQuery, params);
528
+ // Get created entity from result
529
+ if (result.records.length > 0) {
530
+ const node = result.records[0].get('e').properties;
531
+ const createdEntity = this.nodeToEntity(node);
532
+ createdEntities.push(createdEntity);
533
+ logger.info(`Created entity with embedding: ${entity.name}`);
534
+ }
535
+ }
536
+ // Commit transaction
537
+ await txc.commit();
538
+ return createdEntities;
539
+ }
540
+ catch (error) {
541
+ // Rollback on error
542
+ await txc.rollback();
543
+ throw error;
544
+ }
545
+ }
546
+ finally {
547
+ // Close session
548
+ await session.close();
549
+ }
550
+ }
551
+ catch (error) {
552
+ logger.error('Error creating entities in Neo4j', error);
553
+ throw error;
554
+ }
555
+ }
556
+ /**
557
+ * Create new relations between entities
558
+ * @param relations Array of relations to create
559
+ */
560
+ async createRelations(relations) {
561
+ try {
562
+ if (!relations || relations.length === 0) {
563
+ return [];
564
+ }
565
+ const session = await this.connectionManager.getSession();
566
+ const createdRelations = [];
567
+ try {
568
+ // Begin transaction
569
+ const txc = session.beginTransaction();
570
+ try {
571
+ for (const relation of relations) {
572
+ // Generate temporal and identity metadata
573
+ const now = Date.now();
574
+ const relationId = uuidv4();
575
+ // Check if entities exist
576
+ const checkQuery = `
577
+ MATCH (from:Entity {name: $fromName})
578
+ MATCH (to:Entity {name: $toName})
579
+ RETURN from, to
580
+ `;
581
+ const checkResult = await txc.run(checkQuery, {
582
+ fromName: relation.from,
583
+ toName: relation.to,
584
+ });
585
+ // If either entity doesn't exist, skip this relation
586
+ if (checkResult.records.length === 0) {
587
+ logger.warn(`Skipping relation creation: One or both entities not found (${relation.from} -> ${relation.to})`);
588
+ continue;
589
+ }
590
+ // Create relation with parameters
591
+ const extendedRelation = relation;
592
+ const params = {
593
+ id: relationId,
594
+ fromName: relation.from,
595
+ toName: relation.to,
596
+ relationType: relation.relationType,
597
+ strength: relation.strength || null,
598
+ confidence: relation.confidence || null,
599
+ metadata: relation.metadata ? JSON.stringify(relation.metadata) : null,
600
+ version: 1,
601
+ createdAt: extendedRelation.createdAt || now,
602
+ updatedAt: extendedRelation.updatedAt || now,
603
+ validFrom: extendedRelation.validFrom || now,
604
+ validTo: null,
605
+ changedBy: extendedRelation.changedBy || null,
606
+ };
607
+ // Create relation query
608
+ const createQuery = `
609
+ MATCH (from:Entity {name: $fromName})
610
+ MATCH (to:Entity {name: $toName})
611
+ CREATE (from)-[r:RELATES_TO {
612
+ id: $id,
613
+ relationType: $relationType,
614
+ strength: $strength,
615
+ confidence: $confidence,
616
+ metadata: $metadata,
617
+ version: $version,
618
+ createdAt: $createdAt,
619
+ updatedAt: $updatedAt,
620
+ validFrom: $validFrom,
621
+ validTo: $validTo,
622
+ changedBy: $changedBy
623
+ }]->(to)
624
+ RETURN r, from, to
625
+ `;
626
+ // Execute query
627
+ const result = await txc.run(createQuery, params);
628
+ // Get created relation from result
629
+ if (result.records.length > 0) {
630
+ const record = result.records[0];
631
+ const rel = record.get('r').properties;
632
+ const fromNode = record.get('from').properties;
633
+ const toNode = record.get('to').properties;
634
+ const createdRelation = this.relationshipToRelation(rel, fromNode.name, toNode.name);
635
+ createdRelations.push(createdRelation);
636
+ }
637
+ }
638
+ // Commit transaction
639
+ await txc.commit();
640
+ return createdRelations;
641
+ }
642
+ catch (error) {
643
+ // Rollback on error
644
+ await txc.rollback();
645
+ throw error;
646
+ }
647
+ }
648
+ finally {
649
+ // Close session
650
+ await session.close();
651
+ }
652
+ }
653
+ catch (error) {
654
+ logger.error('Error creating relations in Neo4j', error);
655
+ throw error;
656
+ }
657
+ }
658
+ /**
659
+ * Add observations to entities
660
+ * @param observations Array of objects with entity name and observation contents
661
+ */
662
+ async addObservations(observations) {
663
+ try {
664
+ if (!observations || observations.length === 0) {
665
+ return [];
666
+ }
667
+ const session = await this.connectionManager.getSession();
668
+ const results = [];
669
+ try {
670
+ // Begin transaction
671
+ const txc = session.beginTransaction();
672
+ try {
673
+ for (const obs of observations) {
674
+ if (!obs.entityName || !obs.contents || obs.contents.length === 0) {
675
+ continue;
676
+ }
677
+ // Step 1: Get the current entity and its relationships
678
+ const getQuery = `
679
+ MATCH (e:Entity {name: $name})
680
+ WHERE e.validTo IS NULL
681
+ OPTIONAL MATCH (e)-[r:RELATES_TO]->(to:Entity)
682
+ WHERE r.validTo IS NULL
683
+ OPTIONAL MATCH (from:Entity)-[r2:RELATES_TO]->(e)
684
+ WHERE r2.validTo IS NULL
685
+ RETURN e, collect(DISTINCT {rel: r, to: to}) as outgoing,
686
+ collect(DISTINCT {rel: r2, from: from}) as incoming
687
+ `;
688
+ const getResult = await txc.run(getQuery, { name: obs.entityName });
689
+ if (getResult.records.length === 0) {
690
+ logger.warn(`Entity not found: ${obs.entityName}`);
691
+ continue;
692
+ }
693
+ // Get entity properties
694
+ const currentNode = getResult.records[0].get('e').properties;
695
+ const currentObservations = JSON.parse(currentNode.observations || '[]');
696
+ const outgoingRels = getResult.records[0].get('outgoing');
697
+ const incomingRels = getResult.records[0].get('incoming');
698
+ // Step 2: Create a new version of the entity with updated observations
699
+ const now = Date.now();
700
+ const newVersion = (currentNode.version || 0) + 1;
701
+ const newEntityId = uuidv4();
702
+ // Filter out duplicates
703
+ const newObservations = obs.contents.filter((content) => !currentObservations.includes(content));
704
+ // Skip if no new observations
705
+ if (newObservations.length === 0) {
706
+ results.push({
707
+ entityName: obs.entityName,
708
+ addedObservations: [],
709
+ });
710
+ continue;
711
+ }
712
+ // Combine observations
713
+ const allObservations = [...currentObservations, ...newObservations];
714
+ // Step 3: Mark the old entity and its relationships as invalid
715
+ const invalidateQuery = `
716
+ MATCH (e:Entity {id: $id})
717
+ SET e.validTo = $now
718
+ WITH e
719
+ OPTIONAL MATCH (e)-[r:RELATES_TO]->()
720
+ WHERE r.validTo IS NULL
721
+ SET r.validTo = $now
722
+ WITH e
723
+ OPTIONAL MATCH ()-[r2:RELATES_TO]->(e)
724
+ WHERE r2.validTo IS NULL
725
+ SET r2.validTo = $now
726
+ `;
727
+ await txc.run(invalidateQuery, {
728
+ id: currentNode.id,
729
+ now,
730
+ });
731
+ // Step 4: Create the new version
732
+ const createQuery = `
733
+ CREATE (e:Entity {
734
+ id: $id,
735
+ name: $name,
736
+ entityType: $entityType,
737
+ observations: $observations,
738
+ version: $version,
739
+ createdAt: $createdAt,
740
+ updatedAt: $now,
741
+ validFrom: $now,
742
+ validTo: null,
743
+ changedBy: $changedBy
744
+ })
745
+ RETURN e
746
+ `;
747
+ const createParams = {
748
+ id: newEntityId,
749
+ name: currentNode.name,
750
+ entityType: currentNode.entityType,
751
+ observations: JSON.stringify(allObservations),
752
+ version: newVersion,
753
+ createdAt: currentNode.createdAt,
754
+ now,
755
+ changedBy: null,
756
+ };
757
+ await txc.run(createQuery, createParams);
758
+ // Step 5: Recreate relationships for the new version
759
+ for (const outRel of outgoingRels) {
760
+ if (!outRel.rel || !outRel.to)
761
+ continue;
762
+ const relProps = outRel.rel.properties;
763
+ const newRelId = uuidv4();
764
+ const createOutRelQuery = `
765
+ MATCH (from:Entity {id: $fromId})
766
+ MATCH (to:Entity {id: $toId})
767
+ CREATE (from)-[r:RELATES_TO {
768
+ id: $id,
769
+ relationType: $relationType,
770
+ strength: $strength,
771
+ confidence: $confidence,
772
+ metadata: $metadata,
773
+ version: $version,
774
+ createdAt: $createdAt,
775
+ updatedAt: $now,
776
+ validFrom: $now,
777
+ validTo: null,
778
+ changedBy: $changedBy
779
+ }]->(to)
780
+ `;
781
+ await txc.run(createOutRelQuery, {
782
+ fromId: newEntityId,
783
+ toId: outRel.to.properties.id,
784
+ id: newRelId,
785
+ relationType: relProps.relationType,
786
+ strength: relProps.strength !== undefined ? relProps.strength : 0.9,
787
+ confidence: relProps.confidence !== undefined ? relProps.confidence : 0.95,
788
+ metadata: relProps.metadata || null,
789
+ version: relProps.version || 1,
790
+ createdAt: relProps.createdAt || Date.now(),
791
+ now,
792
+ changedBy: null,
793
+ });
794
+ }
795
+ for (const inRel of incomingRels) {
796
+ if (!inRel.rel || !inRel.from)
797
+ continue;
798
+ const relProps = inRel.rel.properties;
799
+ const newRelId = uuidv4();
800
+ const createInRelQuery = `
801
+ MATCH (from:Entity {id: $fromId})
802
+ MATCH (to:Entity {id: $toId})
803
+ CREATE (from)-[r:RELATES_TO {
804
+ id: $id,
805
+ relationType: $relationType,
806
+ strength: $strength,
807
+ confidence: $confidence,
808
+ metadata: $metadata,
809
+ version: $version,
810
+ createdAt: $createdAt,
811
+ updatedAt: $now,
812
+ validFrom: $now,
813
+ validTo: null,
814
+ changedBy: $changedBy
815
+ }]->(to)
816
+ `;
817
+ await txc.run(createInRelQuery, {
818
+ fromId: inRel.from.properties.id,
819
+ toId: newEntityId,
820
+ id: newRelId,
821
+ relationType: relProps.relationType,
822
+ strength: relProps.strength !== undefined ? relProps.strength : 0.9,
823
+ confidence: relProps.confidence !== undefined ? relProps.confidence : 0.95,
824
+ metadata: relProps.metadata || null,
825
+ version: relProps.version || 1,
826
+ createdAt: relProps.createdAt || Date.now(),
827
+ now,
828
+ changedBy: null,
829
+ });
830
+ }
831
+ // Step 6: Add result to return array
832
+ results.push({
833
+ entityName: obs.entityName,
834
+ addedObservations: newObservations,
835
+ });
836
+ }
837
+ // Commit transaction
838
+ await txc.commit();
839
+ return results;
840
+ }
841
+ catch (error) {
842
+ // Rollback on error
843
+ await txc.rollback();
844
+ throw error;
845
+ }
846
+ }
847
+ finally {
848
+ // Close session
849
+ await session.close();
850
+ }
851
+ }
852
+ catch (error) {
853
+ logger.error('Error adding observations in Neo4j', error);
854
+ throw error;
855
+ }
856
+ }
857
+ /**
858
+ * Delete entities and their relations
859
+ * @param entityNames Array of entity names to delete
860
+ */
861
+ async deleteEntities(entityNames) {
862
+ try {
863
+ if (!entityNames || entityNames.length === 0) {
864
+ return;
865
+ }
866
+ const session = await this.connectionManager.getSession();
867
+ try {
868
+ // Begin transaction
869
+ const txc = session.beginTransaction();
870
+ try {
871
+ // Delete entities and their relations
872
+ const deleteQuery = `
873
+ MATCH (e:Entity)
874
+ WHERE e.name IN $names
875
+ DETACH DELETE e
876
+ `;
877
+ await txc.run(deleteQuery, { names: entityNames });
878
+ // Commit transaction
879
+ await txc.commit();
880
+ }
881
+ catch (error) {
882
+ // Rollback on error
883
+ await txc.rollback();
884
+ throw error;
885
+ }
886
+ }
887
+ finally {
888
+ // Close session
889
+ await session.close();
890
+ }
891
+ }
892
+ catch (error) {
893
+ logger.error('Error deleting entities in Neo4j', error);
894
+ throw error;
895
+ }
896
+ }
897
+ /**
898
+ * Delete observations from entities
899
+ * @param deletions Array of objects with entity name and observations to delete
900
+ */
901
+ async deleteObservations(deletions) {
902
+ try {
903
+ if (!deletions || deletions.length === 0) {
904
+ return;
905
+ }
906
+ const session = await this.connectionManager.getSession();
907
+ try {
908
+ // Begin transaction
909
+ const txc = session.beginTransaction();
910
+ try {
911
+ for (const deletion of deletions) {
912
+ if (!deletion.entityName ||
913
+ !deletion.observations ||
914
+ deletion.observations.length === 0) {
915
+ continue;
916
+ }
917
+ // Step 1: Get the current entity
918
+ const getQuery = `
919
+ MATCH (e:Entity {name: $name})
920
+ WHERE e.validTo IS NULL
921
+ RETURN e
922
+ `;
923
+ const getResult = await txc.run(getQuery, { name: deletion.entityName });
924
+ if (getResult.records.length === 0) {
925
+ logger.warn(`Entity not found: ${deletion.entityName}`);
926
+ continue;
927
+ }
928
+ // Get entity properties
929
+ const currentNode = getResult.records[0].get('e').properties;
930
+ const currentObservations = JSON.parse(currentNode.observations || '[]');
931
+ // Step 2: Remove the observations
932
+ const updatedObservations = currentObservations.filter((obs) => !deletion.observations.includes(obs));
933
+ // Step 3: Create a new version of the entity with updated observations
934
+ const now = Date.now();
935
+ const newVersion = (currentNode.version || 0) + 1;
936
+ const newEntityId = uuidv4();
937
+ // Step 4: Mark the old entity as invalid
938
+ const invalidateQuery = `
939
+ MATCH (e:Entity {id: $id})
940
+ SET e.validTo = $now
941
+ `;
942
+ await txc.run(invalidateQuery, {
943
+ id: currentNode.id,
944
+ now,
945
+ });
946
+ // Step 5: Create the new version
947
+ const createQuery = `
948
+ CREATE (e:Entity {
949
+ id: $id,
950
+ name: $name,
951
+ entityType: $entityType,
952
+ observations: $observations,
953
+ version: $version,
954
+ createdAt: $createdAt,
955
+ updatedAt: $now,
956
+ validFrom: $now,
957
+ validTo: null,
958
+ changedBy: $changedBy
959
+ })
960
+ RETURN e
961
+ `;
962
+ const createParams = {
963
+ id: newEntityId,
964
+ name: currentNode.name,
965
+ entityType: currentNode.entityType,
966
+ observations: JSON.stringify(updatedObservations),
967
+ version: newVersion,
968
+ createdAt: currentNode.createdAt,
969
+ now,
970
+ changedBy: null,
971
+ };
972
+ await txc.run(createQuery, createParams);
973
+ }
974
+ // Commit transaction
975
+ await txc.commit();
976
+ }
977
+ catch (error) {
978
+ // Rollback on error
979
+ await txc.rollback();
980
+ throw error;
981
+ }
982
+ }
983
+ finally {
984
+ // Close session
985
+ await session.close();
986
+ }
987
+ }
988
+ catch (error) {
989
+ logger.error('Error deleting observations in Neo4j', error);
990
+ throw error;
991
+ }
992
+ }
993
+ /**
994
+ * Delete relations from the graph
995
+ * @param relations Array of relations to delete
996
+ */
997
+ async deleteRelations(relations) {
998
+ try {
999
+ if (!relations || relations.length === 0) {
1000
+ return;
1001
+ }
1002
+ const session = await this.connectionManager.getSession();
1003
+ try {
1004
+ // Begin transaction
1005
+ const txc = session.beginTransaction();
1006
+ try {
1007
+ for (const relation of relations) {
1008
+ // Delete relation query
1009
+ const deleteQuery = `
1010
+ MATCH (from:Entity {name: $fromName})-[r:RELATES_TO]->(to:Entity {name: $toName})
1011
+ WHERE r.relationType = $relationType
1012
+ DELETE r
1013
+ `;
1014
+ await txc.run(deleteQuery, {
1015
+ fromName: relation.from,
1016
+ toName: relation.to,
1017
+ relationType: relation.relationType,
1018
+ });
1019
+ }
1020
+ // Commit transaction
1021
+ await txc.commit();
1022
+ }
1023
+ catch (error) {
1024
+ // Rollback on error
1025
+ await txc.rollback();
1026
+ throw error;
1027
+ }
1028
+ }
1029
+ finally {
1030
+ // Close session
1031
+ await session.close();
1032
+ }
1033
+ }
1034
+ catch (error) {
1035
+ logger.error('Error deleting relations in Neo4j', error);
1036
+ throw error;
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Get an entity by name
1041
+ * @param entityName Name of the entity to retrieve
1042
+ */
1043
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1044
+ async getEntity(entityName) {
1045
+ try {
1046
+ // Query for entity by name
1047
+ const query = `
1048
+ MATCH (e:Entity {name: $name})
1049
+ WHERE e.validTo IS NULL
1050
+ RETURN e
1051
+ `;
1052
+ // Execute query
1053
+ const result = await this.connectionManager.executeQuery(query, { name: entityName });
1054
+ // Return null if no entity found
1055
+ if (result.records.length === 0) {
1056
+ return null;
1057
+ }
1058
+ // Convert node to entity
1059
+ const node = result.records[0].get('e').properties;
1060
+ return this.nodeToEntity(node);
1061
+ }
1062
+ catch (error) {
1063
+ logger.error(`Error retrieving entity ${entityName} from Neo4j`, error);
1064
+ throw error;
1065
+ }
1066
+ }
1067
+ /**
1068
+ * Get a specific relation by its source, target, and type
1069
+ * @param from Source entity name
1070
+ * @param to Target entity name
1071
+ * @param type Relation type
1072
+ */
1073
+ async getRelation(from, to, type) {
1074
+ try {
1075
+ // Query for relation
1076
+ const query = `
1077
+ MATCH (from:Entity {name: $fromName})-[r:RELATES_TO]->(to:Entity {name: $toName})
1078
+ WHERE r.relationType = $relationType
1079
+ AND r.validTo IS NULL
1080
+ RETURN r, from, to
1081
+ `;
1082
+ // Execute query
1083
+ const result = await this.connectionManager.executeQuery(query, {
1084
+ fromName: from,
1085
+ toName: to,
1086
+ relationType: type,
1087
+ });
1088
+ // Return null if no relation found
1089
+ if (result.records.length === 0) {
1090
+ return null;
1091
+ }
1092
+ // Convert relationship to relation
1093
+ const record = result.records[0];
1094
+ const rel = record.get('r').properties;
1095
+ const fromNode = record.get('from').properties;
1096
+ const toNode = record.get('to').properties;
1097
+ return this.relationshipToRelation(rel, fromNode.name, toNode.name);
1098
+ }
1099
+ catch (error) {
1100
+ logger.error(`Error retrieving relation from Neo4j`, error);
1101
+ throw error;
1102
+ }
1103
+ }
1104
+ /**
1105
+ * Update an existing relation with new properties
1106
+ * @param relation The relation with updated properties
1107
+ */
1108
+ async updateRelation(relation) {
1109
+ try {
1110
+ const session = await this.connectionManager.getSession();
1111
+ try {
1112
+ // Begin transaction
1113
+ const txc = session.beginTransaction();
1114
+ try {
1115
+ // Step 1: Get the current relation
1116
+ const getQuery = `
1117
+ MATCH (from:Entity {name: $fromName})-[r:RELATES_TO]->(to:Entity {name: $toName})
1118
+ WHERE r.relationType = $relationType
1119
+ AND r.validTo IS NULL
1120
+ RETURN r
1121
+ `;
1122
+ const getResult = await txc.run(getQuery, {
1123
+ fromName: relation.from,
1124
+ toName: relation.to,
1125
+ relationType: relation.relationType,
1126
+ });
1127
+ if (getResult.records.length === 0) {
1128
+ throw new Error(`Relation not found: ${relation.from} -> ${relation.to} (${relation.relationType})`);
1129
+ }
1130
+ // Get relation properties
1131
+ const currentRel = getResult.records[0].get('r').properties;
1132
+ // Step 2: Update the relation with temporal versioning
1133
+ const now = Date.now();
1134
+ const newVersion = (currentRel.version || 0) + 1;
1135
+ const newRelationId = uuidv4();
1136
+ // Step 3: Mark the old relation as invalid
1137
+ const invalidateQuery = `
1138
+ MATCH (from:Entity {name: $fromName})-[r:RELATES_TO {id: $id}]->(to:Entity {name: $toName})
1139
+ SET r.validTo = $now
1140
+ `;
1141
+ await txc.run(invalidateQuery, {
1142
+ fromName: relation.from,
1143
+ toName: relation.to,
1144
+ id: currentRel.id,
1145
+ now,
1146
+ });
1147
+ // Step 4: Create the new version of the relation
1148
+ const createQuery = `
1149
+ MATCH (from:Entity {name: $fromName})
1150
+ MATCH (to:Entity {name: $toName})
1151
+ CREATE (from)-[r:RELATES_TO {
1152
+ id: $id,
1153
+ relationType: $relationType,
1154
+ strength: $strength,
1155
+ confidence: $confidence,
1156
+ metadata: $metadata,
1157
+ version: $version,
1158
+ createdAt: $createdAt,
1159
+ updatedAt: $now,
1160
+ validFrom: $now,
1161
+ validTo: null,
1162
+ changedBy: $changedBy
1163
+ }]->(to)
1164
+ `;
1165
+ const extendedRelation = relation;
1166
+ const createParams = {
1167
+ id: newRelationId,
1168
+ fromName: relation.from,
1169
+ toName: relation.to,
1170
+ relationType: relation.relationType,
1171
+ strength: relation.strength !== undefined ? relation.strength : currentRel.strength,
1172
+ confidence: relation.confidence !== undefined ? relation.confidence : currentRel.confidence,
1173
+ metadata: relation.metadata ? JSON.stringify(relation.metadata) : currentRel.metadata,
1174
+ version: newVersion,
1175
+ createdAt: currentRel.createdAt,
1176
+ now,
1177
+ changedBy: extendedRelation.changedBy || null,
1178
+ };
1179
+ await txc.run(createQuery, createParams);
1180
+ // Commit transaction
1181
+ await txc.commit();
1182
+ }
1183
+ catch (error) {
1184
+ // Rollback on error
1185
+ await txc.rollback();
1186
+ throw error;
1187
+ }
1188
+ }
1189
+ finally {
1190
+ // Close session
1191
+ await session.close();
1192
+ }
1193
+ }
1194
+ catch (error) {
1195
+ logger.error('Error updating relation in Neo4j', error);
1196
+ throw error;
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Get the history of all versions of an entity
1201
+ * @param entityName The name of the entity to retrieve history for
1202
+ */
1203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1204
+ async getEntityHistory(entityName) {
1205
+ try {
1206
+ // Query for entity history
1207
+ const query = `
1208
+ MATCH (e:Entity {name: $name})
1209
+ RETURN e
1210
+ ORDER BY e.validFrom ASC
1211
+ `;
1212
+ // Execute query
1213
+ const result = await this.connectionManager.executeQuery(query, { name: entityName });
1214
+ // Return empty array if no history found
1215
+ if (result.records.length === 0) {
1216
+ return [];
1217
+ }
1218
+ // Convert nodes to entities
1219
+ return result.records.map((record) => {
1220
+ const node = record.get('e').properties;
1221
+ return this.nodeToEntity(node);
1222
+ });
1223
+ }
1224
+ catch (error) {
1225
+ logger.error(`Error retrieving history for entity ${entityName} from Neo4j`, error);
1226
+ throw error;
1227
+ }
1228
+ }
1229
+ /**
1230
+ * Get the history of all versions of a relation
1231
+ * @param from Source entity name
1232
+ * @param to Target entity name
1233
+ * @param relationType Type of the relation
1234
+ */
1235
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1236
+ async getRelationHistory(from, to, relationType) {
1237
+ try {
1238
+ // Query for relation history
1239
+ const query = `
1240
+ MATCH (from:Entity {name: $fromName})-[r:RELATES_TO]->(to:Entity {name: $toName})
1241
+ WHERE r.relationType = $relationType
1242
+ RETURN r, from, to
1243
+ ORDER BY r.validFrom ASC
1244
+ `;
1245
+ // Execute query
1246
+ const result = await this.connectionManager.executeQuery(query, {
1247
+ fromName: from,
1248
+ toName: to,
1249
+ relationType,
1250
+ });
1251
+ // Return empty array if no history found
1252
+ if (result.records.length === 0) {
1253
+ return [];
1254
+ }
1255
+ // Convert relationships to relations
1256
+ return result.records.map((record) => {
1257
+ const rel = record.get('r').properties;
1258
+ const fromNode = record.get('from').properties;
1259
+ const toNode = record.get('to').properties;
1260
+ return this.relationshipToRelation(rel, fromNode.name, toNode.name);
1261
+ });
1262
+ }
1263
+ catch (error) {
1264
+ logger.error(`Error retrieving relation history from Neo4j`, error);
1265
+ throw error;
1266
+ }
1267
+ }
1268
+ /**
1269
+ * Get the state of the knowledge graph at a specific point in time
1270
+ * @param timestamp The timestamp to get the graph state at
1271
+ */
1272
+ async getGraphAtTime(timestamp) {
1273
+ try {
1274
+ const startTime = Date.now();
1275
+ // Query for entities valid at timestamp
1276
+ const entityQuery = `
1277
+ MATCH (e:Entity)
1278
+ WHERE e.validFrom <= $timestamp
1279
+ AND (e.validTo IS NULL OR e.validTo > $timestamp)
1280
+ RETURN e
1281
+ `;
1282
+ // Execute entity query
1283
+ const entityResult = await this.connectionManager.executeQuery(entityQuery, { timestamp });
1284
+ // Convert nodes to entities
1285
+ const entities = entityResult.records.map((record) => {
1286
+ const node = record.get('e').properties;
1287
+ return this.nodeToEntity(node);
1288
+ });
1289
+ // Query for relations valid at timestamp
1290
+ const relationQuery = `
1291
+ MATCH (from:Entity)-[r:RELATES_TO]->(to:Entity)
1292
+ WHERE r.validFrom <= $timestamp
1293
+ AND (r.validTo IS NULL OR r.validTo > $timestamp)
1294
+ RETURN r, from.name AS fromName, to.name AS toName
1295
+ `;
1296
+ // Execute relation query
1297
+ const relationResult = await this.connectionManager.executeQuery(relationQuery, {
1298
+ timestamp,
1299
+ });
1300
+ // Convert relationships to relations
1301
+ const relations = relationResult.records.map((record) => {
1302
+ const rel = record.get('r').properties;
1303
+ const fromName = record.get('fromName');
1304
+ const toName = record.get('toName');
1305
+ return this.relationshipToRelation(rel, fromName, toName);
1306
+ });
1307
+ const timeTaken = Date.now() - startTime;
1308
+ // Return the graph state at the timestamp
1309
+ return {
1310
+ entities,
1311
+ relations,
1312
+ total: entities.length,
1313
+ timeTaken,
1314
+ };
1315
+ }
1316
+ catch (error) {
1317
+ logger.error(`Error retrieving graph state at timestamp ${timestamp} from Neo4j`, error);
1318
+ throw error;
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Get the current knowledge graph with confidence decay applied to relations
1323
+ * based on their age and the configured decay settings
1324
+ */
1325
+ async getDecayedGraph() {
1326
+ try {
1327
+ // If decay is not enabled, just return the regular graph
1328
+ if (!this.decayConfig.enabled) {
1329
+ return this.loadGraph();
1330
+ }
1331
+ const startTime = Date.now();
1332
+ // Load entities
1333
+ const entityQuery = `
1334
+ MATCH (e:Entity)
1335
+ WHERE e.validTo IS NULL
1336
+ RETURN e
1337
+ `;
1338
+ const entityResult = await this.connectionManager.executeQuery(entityQuery, {});
1339
+ const entities = entityResult.records.map((record) => {
1340
+ const node = record.get('e').properties;
1341
+ return this.nodeToEntity(node);
1342
+ });
1343
+ // Calculate decay factor
1344
+ const halfLifeMs = this.decayConfig.halfLifeDays * 24 * 60 * 60 * 1000;
1345
+ const decayFactor = Math.log(0.5) / halfLifeMs;
1346
+ // Load relations and apply decay
1347
+ const relationQuery = `
1348
+ MATCH (from:Entity)-[r:RELATES_TO]->(to:Entity)
1349
+ WHERE r.validTo IS NULL
1350
+ RETURN r, from.name AS fromName, to.name AS toName
1351
+ `;
1352
+ const relationResult = await this.connectionManager.executeQuery(relationQuery, {});
1353
+ const relations = relationResult.records.map((record) => {
1354
+ const rel = record.get('r').properties;
1355
+ const fromName = record.get('fromName');
1356
+ const toName = record.get('toName');
1357
+ // Create base relation
1358
+ const relation = this.relationshipToRelation(rel, fromName, toName);
1359
+ // Apply decay if confidence is present
1360
+ if (relation.confidence !== null && relation.confidence !== undefined) {
1361
+ const extendedRelation = relation;
1362
+ const ageDiff = startTime - (extendedRelation.validFrom || extendedRelation.createdAt || startTime);
1363
+ let decayedConfidence = relation.confidence * Math.exp(decayFactor * ageDiff);
1364
+ // Don't let confidence decay below minimum
1365
+ if (decayedConfidence < this.decayConfig.minConfidence) {
1366
+ decayedConfidence = this.decayConfig.minConfidence;
1367
+ }
1368
+ relation.confidence = decayedConfidence;
1369
+ }
1370
+ return relation;
1371
+ });
1372
+ const timeTaken = Date.now() - startTime;
1373
+ // Return the graph with decayed confidence values
1374
+ return {
1375
+ entities,
1376
+ relations,
1377
+ total: entities.length,
1378
+ timeTaken,
1379
+ diagnostics: {
1380
+ decay_info: {
1381
+ enabled: this.decayConfig.enabled,
1382
+ halfLifeDays: this.decayConfig.halfLifeDays,
1383
+ minConfidence: this.decayConfig.minConfidence,
1384
+ decayFactor,
1385
+ },
1386
+ },
1387
+ };
1388
+ }
1389
+ catch (error) {
1390
+ logger.error('Error getting decayed graph from Neo4j', error);
1391
+ throw error;
1392
+ }
1393
+ }
1394
+ /**
1395
+ * Store or update the embedding vector for an entity
1396
+ * @param entityName The name of the entity to update
1397
+ * @param embedding The embedding data to store
1398
+ */
1399
+ async updateEntityEmbedding(entityName, embedding) {
1400
+ try {
1401
+ // Verify that the entity exists
1402
+ const entity = await this.getEntity(entityName);
1403
+ if (!entity) {
1404
+ throw new Error(`Entity ${entityName} not found`);
1405
+ }
1406
+ const session = await this.connectionManager.getSession();
1407
+ try {
1408
+ // Begin transaction
1409
+ const txc = session.beginTransaction();
1410
+ try {
1411
+ // Update the entity with the embedding
1412
+ const updateQuery = `
1413
+ MATCH (e:Entity {name: $name})
1414
+ WHERE e.validTo IS NULL
1415
+ SET e.embedding = $embedding,
1416
+ e.updatedAt = $now
1417
+ RETURN e
1418
+ `;
1419
+ await txc.run(updateQuery, {
1420
+ name: entityName,
1421
+ embedding: embedding.vector,
1422
+ now: Date.now(),
1423
+ });
1424
+ // Commit transaction
1425
+ await txc.commit();
1426
+ }
1427
+ catch (error) {
1428
+ // Rollback on error
1429
+ await txc.rollback();
1430
+ throw error;
1431
+ }
1432
+ }
1433
+ finally {
1434
+ // Close session
1435
+ await session.close();
1436
+ }
1437
+ }
1438
+ catch (error) {
1439
+ logger.error(`Error updating embedding for entity ${entityName} in Neo4j`, error);
1440
+ throw error;
1441
+ }
1442
+ }
1443
+ /**
1444
+ * Get the embedding vector for an entity
1445
+ * @param entityName The name of the entity
1446
+ * @returns Promise resolving to the EntityEmbedding or null if not found
1447
+ */
1448
+ async getEntityEmbedding(entityName) {
1449
+ try {
1450
+ // Verify that the entity exists
1451
+ const entity = await this.getEntity(entityName);
1452
+ if (!entity) {
1453
+ logger.debug(`Entity not found when retrieving embedding: ${entityName}`);
1454
+ return null;
1455
+ }
1456
+ const session = await this.connectionManager.getSession();
1457
+ try {
1458
+ // Query to get the entity with its embedding
1459
+ const query = `
1460
+ MATCH (e:Entity {name: $name})
1461
+ WHERE e.validTo IS NULL
1462
+ RETURN e.embedding AS embedding
1463
+ `;
1464
+ const result = await session.run(query, { name: entityName });
1465
+ if (result.records.length === 0 || !result.records[0].get('embedding')) {
1466
+ logger.debug(`No embedding found for entity: ${entityName}`);
1467
+ return null;
1468
+ }
1469
+ const embeddingVector = result.records[0].get('embedding');
1470
+ // Return the embedding in the expected format
1471
+ return {
1472
+ vector: embeddingVector,
1473
+ model: 'unknown', // We don't store the model info in Neo4j
1474
+ lastUpdated: entity.updatedAt || Date.now(),
1475
+ };
1476
+ }
1477
+ finally {
1478
+ await session.close();
1479
+ }
1480
+ }
1481
+ catch (error) {
1482
+ logger.error(`Error retrieving embedding for entity ${entityName} from Neo4j`, error);
1483
+ return null;
1484
+ }
1485
+ }
1486
+ /**
1487
+ * Find entities similar to a query vector
1488
+ * @param queryVector The vector to compare against
1489
+ * @param limit Maximum number of results to return
1490
+ */
1491
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1492
+ async findSimilarEntities(queryVector, limit = 10) {
1493
+ try {
1494
+ // Direct vector search implementation using the approach proven to work in our test script
1495
+ logger.debug(`Neo4jStorageProvider: Using direct vector search with ${limit} limit`);
1496
+ const session = await this.connectionManager.getSession();
1497
+ try {
1498
+ const result = await session.run(`
1499
+ CALL db.index.vector.queryNodes(
1500
+ 'entity_embeddings',
1501
+ $limit,
1502
+ $embedding
1503
+ )
1504
+ YIELD node, score
1505
+ RETURN node.name AS name, node.entityType AS entityType, score
1506
+ ORDER BY score DESC
1507
+ `, {
1508
+ limit: neo4j.int(Math.floor(limit)),
1509
+ embedding: queryVector,
1510
+ });
1511
+ const foundResults = result.records.length;
1512
+ logger.debug(`Neo4jStorageProvider: Direct vector search found ${foundResults} results`);
1513
+ if (foundResults > 0) {
1514
+ // Convert to entity objects
1515
+ const entityPromises = result.records.map(async (record) => {
1516
+ const entityName = record.get('name');
1517
+ const score = record.get('score');
1518
+ const entity = await this.getEntity(entityName);
1519
+ if (entity) {
1520
+ return {
1521
+ ...entity,
1522
+ score,
1523
+ };
1524
+ }
1525
+ return null;
1526
+ });
1527
+ const entities = (await Promise.all(entityPromises)).filter(Boolean);
1528
+ // Return only valid entities
1529
+ return entities.filter((entity) => entity && entity.validTo === null).slice(0, limit);
1530
+ }
1531
+ logger.debug('Neo4jStorageProvider: No results from vector search');
1532
+ return [];
1533
+ }
1534
+ finally {
1535
+ await session.close();
1536
+ }
1537
+ }
1538
+ catch (error) {
1539
+ logger.error('Error finding similar entities in Neo4j', error);
1540
+ return [];
1541
+ }
1542
+ }
1543
+ /**
1544
+ * Search for entities using semantic search
1545
+ * @param query The search query text
1546
+ * @param options Search options including semantic search parameters
1547
+ */
1548
+ async semanticSearch(query, options = {}) {
1549
+ try {
1550
+ // Create diagnostics object for debugging
1551
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1552
+ const diagnostics = {
1553
+ query,
1554
+ startTime: Date.now(),
1555
+ stepsTaken: [],
1556
+ };
1557
+ // Log start of semantic search
1558
+ diagnostics.stepsTaken.push({
1559
+ step: 'start',
1560
+ timestamp: Date.now(),
1561
+ options: {
1562
+ query,
1563
+ hybridSearch: options.hybridSearch,
1564
+ hasQueryVector: !!options.queryVector,
1565
+ limit: options.limit,
1566
+ entityTypes: options.entityTypes,
1567
+ minSimilarity: options.minSimilarity,
1568
+ },
1569
+ });
1570
+ // Enhanced logging for semantic search
1571
+ logger.debug('Neo4jStorageProvider: Starting semantic search', {
1572
+ query,
1573
+ hybridSearch: options.hybridSearch,
1574
+ hasQueryVector: !!options.queryVector,
1575
+ limit: options.limit,
1576
+ entityTypes: options.entityTypes,
1577
+ });
1578
+ // Ensure vector store is initialized
1579
+ if (!this.vectorStore['initialized']) {
1580
+ logger.info('Neo4jStorageProvider: Vector store not initialized, initializing now');
1581
+ diagnostics.stepsTaken.push({
1582
+ step: 'vectorStoreInitialization',
1583
+ timestamp: Date.now(),
1584
+ status: 'started',
1585
+ });
1586
+ try {
1587
+ await this.vectorStore.initialize();
1588
+ logger.info('Neo4jStorageProvider: Vector store initialized successfully for semantic search');
1589
+ diagnostics.stepsTaken.push({
1590
+ step: 'vectorStoreInitialization',
1591
+ timestamp: Date.now(),
1592
+ status: 'success',
1593
+ });
1594
+ }
1595
+ catch (initError) {
1596
+ logger.error('Neo4jStorageProvider: Failed to initialize vector store for semantic search', initError);
1597
+ diagnostics.stepsTaken.push({
1598
+ step: 'vectorStoreInitialization',
1599
+ timestamp: Date.now(),
1600
+ status: 'error',
1601
+ error: initError instanceof Error ? initError.message : String(initError),
1602
+ });
1603
+ // We'll continue but might fail if the vector operations are called
1604
+ }
1605
+ }
1606
+ // If no embedding service, log a warning
1607
+ if (!this.embeddingService) {
1608
+ logger.warn('Neo4jStorageProvider: No embedding service available for semantic search');
1609
+ diagnostics.stepsTaken.push({
1610
+ step: 'embeddingServiceCheck',
1611
+ timestamp: Date.now(),
1612
+ status: 'unavailable',
1613
+ });
1614
+ }
1615
+ else {
1616
+ diagnostics.stepsTaken.push({
1617
+ step: 'embeddingServiceCheck',
1618
+ timestamp: Date.now(),
1619
+ status: 'available',
1620
+ model: this.embeddingService.getProviderInfo().model,
1621
+ dimensions: this.embeddingService.getProviderInfo().dimensions,
1622
+ });
1623
+ }
1624
+ // Generate query vector if not provided and embedding service is available
1625
+ if (!options.queryVector && this.embeddingService) {
1626
+ try {
1627
+ logger.debug('Neo4jStorageProvider: Generating query vector for semantic search');
1628
+ diagnostics.stepsTaken.push({
1629
+ step: 'generateQueryEmbedding',
1630
+ timestamp: Date.now(),
1631
+ status: 'started',
1632
+ });
1633
+ options.queryVector = await this.embeddingService.generateEmbedding(query);
1634
+ diagnostics.stepsTaken.push({
1635
+ step: 'generateQueryEmbedding',
1636
+ timestamp: Date.now(),
1637
+ status: 'success',
1638
+ vectorLength: options.queryVector.length,
1639
+ sampleValues: options.queryVector.slice(0, 3),
1640
+ });
1641
+ logger.debug('Neo4jStorageProvider: Query vector generated successfully', {
1642
+ vectorLength: options.queryVector.length,
1643
+ });
1644
+ }
1645
+ catch (embedError) {
1646
+ diagnostics.stepsTaken.push({
1647
+ step: 'generateQueryEmbedding',
1648
+ timestamp: Date.now(),
1649
+ status: 'error',
1650
+ error: embedError instanceof Error ? embedError.message : String(embedError),
1651
+ });
1652
+ logger.error('Neo4jStorageProvider: Failed to generate query vector for semantic search', embedError);
1653
+ }
1654
+ }
1655
+ else if (options.queryVector) {
1656
+ diagnostics.stepsTaken.push({
1657
+ step: 'searchMethod',
1658
+ timestamp: Date.now(),
1659
+ method: 'vectorOnly',
1660
+ });
1661
+ const searchLimit = Math.floor(options.limit || 10);
1662
+ const minSimilarity = options.minSimilarity || 0.6;
1663
+ diagnostics.stepsTaken.push({
1664
+ step: 'vectorSearch',
1665
+ timestamp: Date.now(),
1666
+ status: 'started',
1667
+ limit: searchLimit,
1668
+ minSimilarity,
1669
+ });
1670
+ // DIRECT VECTOR SEARCH IMPLEMENTATION
1671
+ // Instead of using findSimilarEntities - which isn't working in the MCP context
1672
+ // we'll directly use the working technique from our test script
1673
+ try {
1674
+ const session = await this.connectionManager.getSession();
1675
+ try {
1676
+ const vectorResult = await session.run(`
1677
+ CALL db.index.vector.queryNodes(
1678
+ 'entity_embeddings',
1679
+ $limit,
1680
+ $embedding
1681
+ )
1682
+ YIELD node, score
1683
+ WHERE score >= $minScore
1684
+ RETURN node.name AS name, node.entityType AS entityType, score
1685
+ ORDER BY score DESC
1686
+ `, {
1687
+ limit: neo4j.int(searchLimit),
1688
+ embedding: options.queryVector,
1689
+ minScore: minSimilarity,
1690
+ });
1691
+ const foundResults = vectorResult.records.length;
1692
+ logger.debug(`Neo4jStorageProvider: Direct vector search found ${foundResults} results`);
1693
+ if (foundResults > 0) {
1694
+ // Convert to EntityData objects
1695
+ const entityPromises = vectorResult.records.map(async (record) => {
1696
+ const entityName = record.get('name');
1697
+ return this.getEntity(entityName);
1698
+ });
1699
+ const entities = (await Promise.all(entityPromises)).filter(Boolean);
1700
+ diagnostics.stepsTaken.push({
1701
+ step: 'vectorSearch',
1702
+ timestamp: Date.now(),
1703
+ status: 'completed',
1704
+ resultsCount: entities.length,
1705
+ });
1706
+ // If no entities found after filtering, return empty result
1707
+ if (entities.length === 0) {
1708
+ diagnostics.endTime = Date.now();
1709
+ diagnostics.totalTimeTaken = diagnostics.endTime - diagnostics.startTime;
1710
+ // Only include diagnostics if DEBUG is enabled
1711
+ const result = { entities: [], relations: [] };
1712
+ if (process.env.DEBUG === 'true') {
1713
+ result.diagnostics = diagnostics;
1714
+ }
1715
+ return result;
1716
+ }
1717
+ // Get related relations
1718
+ const entityNames = entities.map((e) => e.name);
1719
+ const finalGraph = await this.openNodes(entityNames);
1720
+ diagnostics.endTime = Date.now();
1721
+ diagnostics.totalTimeTaken = diagnostics.endTime - diagnostics.startTime;
1722
+ // Only include diagnostics if DEBUG is enabled
1723
+ if (process.env.DEBUG === 'true') {
1724
+ return {
1725
+ ...finalGraph,
1726
+ diagnostics,
1727
+ };
1728
+ }
1729
+ return finalGraph;
1730
+ }
1731
+ else {
1732
+ // No results from vector search
1733
+ diagnostics.stepsTaken.push({
1734
+ step: 'vectorSearch',
1735
+ timestamp: Date.now(),
1736
+ status: 'completed',
1737
+ resultsCount: 0,
1738
+ });
1739
+ diagnostics.endTime = Date.now();
1740
+ diagnostics.totalTimeTaken = diagnostics.endTime - diagnostics.startTime;
1741
+ // Only include diagnostics if DEBUG is enabled
1742
+ const result = { entities: [], relations: [] };
1743
+ if (process.env.DEBUG === 'true') {
1744
+ result.diagnostics = diagnostics;
1745
+ }
1746
+ return result;
1747
+ }
1748
+ }
1749
+ catch (error) {
1750
+ logger.error(`Neo4jStorageProvider: Direct vector search error: ${error instanceof Error ? error.message : String(error)}`);
1751
+ diagnostics.stepsTaken.push({
1752
+ step: 'vectorSearch',
1753
+ timestamp: Date.now(),
1754
+ status: 'error',
1755
+ error: error instanceof Error ? error.message : String(error),
1756
+ });
1757
+ }
1758
+ finally {
1759
+ await session.close();
1760
+ }
1761
+ }
1762
+ catch (error) {
1763
+ logger.error(`Neo4jStorageProvider: Direct vector search session error: ${error instanceof Error ? error.message : String(error)}`);
1764
+ }
1765
+ // If we get here, the direct approach failed, fall back to original implementation
1766
+ const results = await this.findSimilarEntities(options.queryVector, searchLimit * 2 // findSimilarEntities will handle neo4j.int conversion
1767
+ );
1768
+ // Filter by min similarity and entity types
1769
+ const filteredResults = results
1770
+ .filter((result) => result.score >= minSimilarity)
1771
+ .filter((result) => {
1772
+ if (!options.entityTypes || options.entityTypes.length === 0) {
1773
+ return true;
1774
+ }
1775
+ return options.entityTypes.includes(result.entityType);
1776
+ })
1777
+ .slice(0, searchLimit);
1778
+ diagnostics.stepsTaken.push({
1779
+ step: 'filterResults',
1780
+ timestamp: Date.now(),
1781
+ status: 'completed',
1782
+ filteredResultsCount: filteredResults.length,
1783
+ });
1784
+ // If no results, return empty graph
1785
+ if (filteredResults.length === 0) {
1786
+ diagnostics.stepsTaken.push({
1787
+ step: 'finalResult',
1788
+ timestamp: Date.now(),
1789
+ status: 'empty',
1790
+ });
1791
+ diagnostics.endTime = Date.now();
1792
+ diagnostics.totalTimeTaken = diagnostics.endTime - diagnostics.startTime;
1793
+ // Only include diagnostics if DEBUG is enabled
1794
+ const result = { entities: [], relations: [] };
1795
+ if (process.env.DEBUG === 'true') {
1796
+ result.diagnostics = diagnostics;
1797
+ }
1798
+ return result;
1799
+ }
1800
+ // Get the entities and relations
1801
+ const entityNames = filteredResults.map((r) => r.name);
1802
+ diagnostics.stepsTaken.push({
1803
+ step: 'openNodes',
1804
+ timestamp: Date.now(),
1805
+ status: 'started',
1806
+ entityNames,
1807
+ });
1808
+ const finalGraph = await this.openNodes(entityNames);
1809
+ diagnostics.stepsTaken.push({
1810
+ step: 'openNodes',
1811
+ timestamp: Date.now(),
1812
+ status: 'completed',
1813
+ entitiesCount: finalGraph.entities.length,
1814
+ relationsCount: finalGraph.relations.length,
1815
+ });
1816
+ diagnostics.endTime = Date.now();
1817
+ diagnostics.totalTimeTaken = diagnostics.endTime - diagnostics.startTime;
1818
+ // Only include diagnostics if DEBUG is enabled
1819
+ if (process.env.DEBUG === 'true') {
1820
+ return {
1821
+ ...finalGraph,
1822
+ diagnostics,
1823
+ };
1824
+ }
1825
+ return finalGraph;
1826
+ }
1827
+ // If no query vector provided, fall back to text search
1828
+ diagnostics.stepsTaken.push({
1829
+ step: 'searchMethod',
1830
+ timestamp: Date.now(),
1831
+ method: 'textOnly',
1832
+ reason: 'No query vector available',
1833
+ });
1834
+ const textSearchLimit = Math.floor(options.limit || 10);
1835
+ diagnostics.stepsTaken.push({
1836
+ step: 'textSearch',
1837
+ timestamp: Date.now(),
1838
+ status: 'started',
1839
+ limit: textSearchLimit,
1840
+ });
1841
+ const textResults = await this.searchNodes(query, { ...options, limit: textSearchLimit });
1842
+ diagnostics.stepsTaken.push({
1843
+ step: 'textSearch',
1844
+ timestamp: Date.now(),
1845
+ status: 'completed',
1846
+ resultsCount: textResults.entities.length,
1847
+ timeTaken: textResults.timeTaken,
1848
+ });
1849
+ diagnostics.endTime = Date.now();
1850
+ diagnostics.totalTimeTaken = diagnostics.endTime - diagnostics.startTime;
1851
+ // Only include diagnostics if DEBUG is enabled
1852
+ if (process.env.DEBUG === 'true') {
1853
+ return {
1854
+ ...textResults,
1855
+ diagnostics,
1856
+ };
1857
+ }
1858
+ return textResults;
1859
+ }
1860
+ catch (error) {
1861
+ logger.error('Error performing semantic search in Neo4j', error);
1862
+ throw error;
1863
+ }
1864
+ }
1865
+ /**
1866
+ * Direct diagnostic method to check Neo4j vector embeddings
1867
+ * Bypasses all abstractions to query the database directly
1868
+ */
1869
+ async diagnoseVectorSearch() {
1870
+ try {
1871
+ // First, make sure vector store is initialized
1872
+ if (!this.vectorStore['initialized']) {
1873
+ try {
1874
+ await this.vectorStore.initialize();
1875
+ }
1876
+ catch {
1877
+ // Continue even if initialization fails
1878
+ }
1879
+ }
1880
+ // Check if we can access the diagnostic method
1881
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1882
+ if (typeof this.vectorStore.diagnosticGetEntityEmbeddings === 'function') {
1883
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1884
+ return await this.vectorStore.diagnosticGetEntityEmbeddings();
1885
+ }
1886
+ else {
1887
+ return {
1888
+ error: 'Diagnostic method not available',
1889
+ vectorStoreType: this.vectorStore.constructor.name,
1890
+ };
1891
+ }
1892
+ }
1893
+ catch (error) {
1894
+ return {
1895
+ error: error instanceof Error ? error.message : String(error),
1896
+ };
1897
+ }
1898
+ }
1899
+ }
1900
+ //# sourceMappingURL=Neo4jStorageProvider.js.map