@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.
- package/LICENSE +21 -0
- package/README.md +718 -0
- package/dist/KnowledgeGraphManager.d.ts +215 -0
- package/dist/KnowledgeGraphManager.js +910 -0
- package/dist/KnowledgeGraphManager.js.map +1 -0
- package/dist/callToolHandler.d.ts +5 -0
- package/dist/callToolHandler.js +26 -0
- package/dist/callToolHandler.js.map +1 -0
- package/dist/cli/neo4j-setup.d.ts +52 -0
- package/dist/cli/neo4j-setup.js +258 -0
- package/dist/cli/neo4j-setup.js.map +1 -0
- package/dist/config/paths.d.ts +13 -0
- package/dist/config/paths.js +41 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/storage.d.ts +35 -0
- package/dist/config/storage.js +52 -0
- package/dist/config/storage.js.map +1 -0
- package/dist/embeddings/DefaultEmbeddingService.d.ts +64 -0
- package/dist/embeddings/DefaultEmbeddingService.js +139 -0
- package/dist/embeddings/DefaultEmbeddingService.js.map +1 -0
- package/dist/embeddings/EmbeddingJobManager.d.ts +212 -0
- package/dist/embeddings/EmbeddingJobManager.js +545 -0
- package/dist/embeddings/EmbeddingJobManager.js.map +1 -0
- package/dist/embeddings/EmbeddingService.d.ts +96 -0
- package/dist/embeddings/EmbeddingService.js +44 -0
- package/dist/embeddings/EmbeddingService.js.map +1 -0
- package/dist/embeddings/EmbeddingServiceFactory.d.ts +72 -0
- package/dist/embeddings/EmbeddingServiceFactory.js +147 -0
- package/dist/embeddings/EmbeddingServiceFactory.js.map +1 -0
- package/dist/embeddings/OpenAIEmbeddingService.d.ts +73 -0
- package/dist/embeddings/OpenAIEmbeddingService.js +195 -0
- package/dist/embeddings/OpenAIEmbeddingService.js.map +1 -0
- package/dist/embeddings/config.d.ts +83 -0
- package/dist/embeddings/config.js +65 -0
- package/dist/embeddings/config.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +220 -0
- package/dist/index.js.map +1 -0
- package/dist/server/handlers/callToolHandler.d.ts +20 -0
- package/dist/server/handlers/callToolHandler.js +505 -0
- package/dist/server/handlers/callToolHandler.js.map +1 -0
- package/dist/server/handlers/listToolsHandler.d.ts +7 -0
- package/dist/server/handlers/listToolsHandler.js +511 -0
- package/dist/server/handlers/listToolsHandler.js.map +1 -0
- package/dist/server/handlers/toolHandlers/addObservations.d.ts +12 -0
- package/dist/server/handlers/toolHandlers/addObservations.js +99 -0
- package/dist/server/handlers/toolHandlers/addObservations.js.map +1 -0
- package/dist/server/handlers/toolHandlers/createEntities.d.ts +12 -0
- package/dist/server/handlers/toolHandlers/createEntities.js +20 -0
- package/dist/server/handlers/toolHandlers/createEntities.js.map +1 -0
- package/dist/server/handlers/toolHandlers/createRelations.d.ts +12 -0
- package/dist/server/handlers/toolHandlers/createRelations.js +20 -0
- package/dist/server/handlers/toolHandlers/createRelations.js.map +1 -0
- package/dist/server/handlers/toolHandlers/deleteEntities.d.ts +12 -0
- package/dist/server/handlers/toolHandlers/deleteEntities.js +20 -0
- package/dist/server/handlers/toolHandlers/deleteEntities.js.map +1 -0
- package/dist/server/handlers/toolHandlers/index.d.ts +8 -0
- package/dist/server/handlers/toolHandlers/index.js +9 -0
- package/dist/server/handlers/toolHandlers/index.js.map +1 -0
- package/dist/server/handlers/toolHandlers/readGraph.d.ts +12 -0
- package/dist/server/handlers/toolHandlers/readGraph.js +20 -0
- package/dist/server/handlers/toolHandlers/readGraph.js.map +1 -0
- package/dist/server/setup.d.ts +8 -0
- package/dist/server/setup.js +48 -0
- package/dist/server/setup.js.map +1 -0
- package/dist/storage/FileStorageProvider.d.ts +125 -0
- package/dist/storage/FileStorageProvider.js +322 -0
- package/dist/storage/FileStorageProvider.js.map +1 -0
- package/dist/storage/SearchResultCache.d.ts +102 -0
- package/dist/storage/SearchResultCache.js +258 -0
- package/dist/storage/SearchResultCache.js.map +1 -0
- package/dist/storage/StorageProvider.d.ts +171 -0
- package/dist/storage/StorageProvider.js +46 -0
- package/dist/storage/StorageProvider.js.map +1 -0
- package/dist/storage/StorageProviderFactory.d.ts +63 -0
- package/dist/storage/StorageProviderFactory.js +113 -0
- package/dist/storage/StorageProviderFactory.js.map +1 -0
- package/dist/storage/VectorStoreFactory.d.ts +43 -0
- package/dist/storage/VectorStoreFactory.js +41 -0
- package/dist/storage/VectorStoreFactory.js.map +1 -0
- package/dist/storage/neo4j/Neo4jConfig.d.ts +37 -0
- package/dist/storage/neo4j/Neo4jConfig.js +13 -0
- package/dist/storage/neo4j/Neo4jConfig.js.map +1 -0
- package/dist/storage/neo4j/Neo4jConnectionManager.d.ts +40 -0
- package/dist/storage/neo4j/Neo4jConnectionManager.js +58 -0
- package/dist/storage/neo4j/Neo4jConnectionManager.js.map +1 -0
- package/dist/storage/neo4j/Neo4jSchemaManager.d.ts +74 -0
- package/dist/storage/neo4j/Neo4jSchemaManager.js +224 -0
- package/dist/storage/neo4j/Neo4jSchemaManager.js.map +1 -0
- package/dist/storage/neo4j/Neo4jStorageProvider.d.ts +225 -0
- package/dist/storage/neo4j/Neo4jStorageProvider.js +1900 -0
- package/dist/storage/neo4j/Neo4jStorageProvider.js.map +1 -0
- package/dist/storage/neo4j/Neo4jVectorStore.d.ts +80 -0
- package/dist/storage/neo4j/Neo4jVectorStore.js +396 -0
- package/dist/storage/neo4j/Neo4jVectorStore.js.map +1 -0
- package/dist/types/entity-embedding.d.ts +156 -0
- package/dist/types/entity-embedding.js +2 -0
- package/dist/types/entity-embedding.js.map +1 -0
- package/dist/types/relation.d.ts +77 -0
- package/dist/types/relation.js +93 -0
- package/dist/types/relation.js.map +1 -0
- package/dist/types/temporalEntity.d.ts +55 -0
- package/dist/types/temporalEntity.js +66 -0
- package/dist/types/temporalEntity.js.map +1 -0
- package/dist/types/temporalRelation.d.ts +60 -0
- package/dist/types/temporalRelation.js +89 -0
- package/dist/types/temporalRelation.js.map +1 -0
- package/dist/types/vector-index.d.ts +48 -0
- package/dist/types/vector-index.js +2 -0
- package/dist/types/vector-index.js.map +1 -0
- package/dist/types/vector-store.d.ts +16 -0
- package/dist/types/vector-store.js +2 -0
- package/dist/types/vector-store.js.map +1 -0
- package/dist/utils/fs.d.ts +2 -0
- package/dist/utils/fs.js +3 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/logger.js.map +1 -0
- 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
|