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