n8n-nodes-engram 0.2.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 +661 -0
- package/README.md +334 -0
- package/dist/community/CommunityDetector.d.ts +11 -0
- package/dist/community/CommunityDetector.js +126 -0
- package/dist/community/CommunityDetector.js.map +1 -0
- package/dist/community/CommunitySummarizer.d.ts +8 -0
- package/dist/community/CommunitySummarizer.js +56 -0
- package/dist/community/CommunitySummarizer.js.map +1 -0
- package/dist/community/index.d.ts +2 -0
- package/dist/community/index.js +8 -0
- package/dist/community/index.js.map +1 -0
- package/dist/credentials/EngramExtractionApi.credentials.d.ts +8 -0
- package/dist/credentials/EngramExtractionApi.credentials.js +41 -0
- package/dist/credentials/EngramExtractionApi.credentials.js.map +1 -0
- package/dist/credentials/EngramNeo4jApi.credentials.d.ts +8 -0
- package/dist/credentials/EngramNeo4jApi.credentials.js +59 -0
- package/dist/credentials/EngramNeo4jApi.credentials.js.map +1 -0
- package/dist/descriptions.d.ts +4 -0
- package/dist/descriptions.js +41 -0
- package/dist/descriptions.js.map +1 -0
- package/dist/embeddings/EmbeddingService.d.ts +24 -0
- package/dist/embeddings/EmbeddingService.js +64 -0
- package/dist/embeddings/EmbeddingService.js.map +1 -0
- package/dist/embeddings/cosine.d.ts +1 -0
- package/dist/embeddings/cosine.js +21 -0
- package/dist/embeddings/cosine.js.map +1 -0
- package/dist/embeddings/index.d.ts +2 -0
- package/dist/embeddings/index.js +8 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/extraction/ContradictionDetector.d.ts +11 -0
- package/dist/extraction/ContradictionDetector.js +35 -0
- package/dist/extraction/ContradictionDetector.js.map +1 -0
- package/dist/extraction/EntityDeduplicator.d.ts +17 -0
- package/dist/extraction/EntityDeduplicator.js +39 -0
- package/dist/extraction/EntityDeduplicator.js.map +1 -0
- package/dist/extraction/EntityExtractor.d.ts +11 -0
- package/dist/extraction/EntityExtractor.js +33 -0
- package/dist/extraction/EntityExtractor.js.map +1 -0
- package/dist/extraction/ExtractionPipeline.d.ts +24 -0
- package/dist/extraction/ExtractionPipeline.js +126 -0
- package/dist/extraction/ExtractionPipeline.js.map +1 -0
- package/dist/extraction/LlmClient.d.ts +36 -0
- package/dist/extraction/LlmClient.js +73 -0
- package/dist/extraction/LlmClient.js.map +1 -0
- package/dist/extraction/RelationshipExtractor.d.ts +12 -0
- package/dist/extraction/RelationshipExtractor.js +38 -0
- package/dist/extraction/RelationshipExtractor.js.map +1 -0
- package/dist/extraction/index.d.ts +6 -0
- package/dist/extraction/index.js +16 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/extraction/prompts.d.ts +16 -0
- package/dist/extraction/prompts.js +101 -0
- package/dist/extraction/prompts.js.map +1 -0
- package/dist/memory/EngramChatMemory.d.ts +42 -0
- package/dist/memory/EngramChatMemory.js +162 -0
- package/dist/memory/EngramChatMemory.js.map +1 -0
- package/dist/memory/EngramChatMessageHistory.d.ts +22 -0
- package/dist/memory/EngramChatMessageHistory.js +72 -0
- package/dist/memory/EngramChatMessageHistory.js.map +1 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +8 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/nodes/EngramAdmin/EngramAdmin.node.d.ts +5 -0
- package/dist/nodes/EngramAdmin/EngramAdmin.node.js +798 -0
- package/dist/nodes/EngramAdmin/EngramAdmin.node.js.map +1 -0
- package/dist/nodes/EngramAdmin/engram-admin.png +0 -0
- package/dist/nodes/EngramExplorer/EngramExplorer.node.d.ts +5 -0
- package/dist/nodes/EngramExplorer/EngramExplorer.node.js +932 -0
- package/dist/nodes/EngramExplorer/EngramExplorer.node.js.map +1 -0
- package/dist/nodes/EngramExplorer/engram-explorer.png +0 -0
- package/dist/nodes/EngramMemory/EngramMemory.node.d.ts +10 -0
- package/dist/nodes/EngramMemory/EngramMemory.node.js +462 -0
- package/dist/nodes/EngramMemory/EngramMemory.node.js.map +1 -0
- package/dist/nodes/EngramMemory/engram.png +0 -0
- package/dist/nodes/EngramTrigger/EngramTrigger.node.d.ts +5 -0
- package/dist/nodes/EngramTrigger/EngramTrigger.node.js +146 -0
- package/dist/nodes/EngramTrigger/EngramTrigger.node.js.map +1 -0
- package/dist/nodes/EngramTrigger/engram-trigger.png +0 -0
- package/dist/schemas/Community.schema.d.ts +656 -0
- package/dist/schemas/Community.schema.js +26 -0
- package/dist/schemas/Community.schema.js.map +1 -0
- package/dist/schemas/EntityEdge.schema.d.ts +86 -0
- package/dist/schemas/EntityEdge.schema.js +34 -0
- package/dist/schemas/EntityEdge.schema.js.map +1 -0
- package/dist/schemas/EntityNode.schema.d.ts +56 -0
- package/dist/schemas/EntityNode.schema.js +24 -0
- package/dist/schemas/EntityNode.schema.js.map +1 -0
- package/dist/schemas/EpisodicNode.schema.d.ts +53 -0
- package/dist/schemas/EpisodicNode.schema.js +23 -0
- package/dist/schemas/EpisodicNode.schema.js.map +1 -0
- package/dist/schemas/GraphData.schema.d.ts +220 -0
- package/dist/schemas/GraphData.schema.js +25 -0
- package/dist/schemas/GraphData.schema.js.map +1 -0
- package/dist/schemas/index.d.ts +5 -0
- package/dist/schemas/index.js +20 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/search/HybridSearchEngine.d.ts +31 -0
- package/dist/search/HybridSearchEngine.js +140 -0
- package/dist/search/HybridSearchEngine.js.map +1 -0
- package/dist/search/MinisearchProvider.d.ts +20 -0
- package/dist/search/MinisearchProvider.js +77 -0
- package/dist/search/MinisearchProvider.js.map +1 -0
- package/dist/search/TextSearchProvider.d.ts +20 -0
- package/dist/search/TextSearchProvider.js +3 -0
- package/dist/search/TextSearchProvider.js.map +1 -0
- package/dist/search/index.d.ts +3 -0
- package/dist/search/index.js +8 -0
- package/dist/search/index.js.map +1 -0
- package/dist/storage/GraphologyStorage.d.ts +42 -0
- package/dist/storage/GraphologyStorage.js +665 -0
- package/dist/storage/GraphologyStorage.js.map +1 -0
- package/dist/storage/IGraphStorage.d.ts +64 -0
- package/dist/storage/IGraphStorage.js +3 -0
- package/dist/storage/IGraphStorage.js.map +1 -0
- package/dist/storage/Neo4jStorage.d.ts +45 -0
- package/dist/storage/Neo4jStorage.js +949 -0
- package/dist/storage/Neo4jStorage.js.map +1 -0
- package/dist/storage/StorageFactory.d.ts +14 -0
- package/dist/storage/StorageFactory.js +26 -0
- package/dist/storage/StorageFactory.js.map +1 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.js +8 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/traversal/EpisodeTraverser.d.ts +10 -0
- package/dist/traversal/EpisodeTraverser.js +46 -0
- package/dist/traversal/EpisodeTraverser.js.map +1 -0
- package/dist/traversal/GraphTraverser.d.ts +24 -0
- package/dist/traversal/GraphTraverser.js +89 -0
- package/dist/traversal/GraphTraverser.js.map +1 -0
- package/dist/traversal/index.d.ts +2 -0
- package/dist/traversal/index.js +8 -0
- package/dist/traversal/index.js.map +1 -0
- package/dist/utils/descriptions.d.ts +6 -0
- package/dist/utils/descriptions.js +95 -0
- package/dist/utils/descriptions.js.map +1 -0
- package/dist/utils/helpers.d.ts +23 -0
- package/dist/utils/helpers.js +146 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/logWrapper.d.ts +26 -0
- package/dist/utils/logWrapper.js +300 -0
- package/dist/utils/logWrapper.js.map +1 -0
- package/dist/utils/sharedFields.d.ts +6 -0
- package/dist/utils/sharedFields.js +121 -0
- package/dist/utils/sharedFields.js.map +1 -0
- package/dist/utils/temporal.d.ts +22 -0
- package/dist/utils/temporal.js +44 -0
- package/dist/utils/temporal.js.map +1 -0
- package/dist/utils/tracing.d.ts +7 -0
- package/dist/utils/tracing.js +20 -0
- package/dist/utils/tracing.js.map +1 -0
- package/dist/utils/uuid.d.ts +2 -0
- package/dist/utils/uuid.js +13 -0
- package/dist/utils/uuid.js.map +1 -0
- package/package.json +108 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Neo4jStorage = void 0;
|
|
7
|
+
const neo4j_driver_1 = __importDefault(require("neo4j-driver"));
|
|
8
|
+
const uuid_1 = require("../utils/uuid");
|
|
9
|
+
const temporal_1 = require("../utils/temporal");
|
|
10
|
+
const cosine_1 = require("../embeddings/cosine");
|
|
11
|
+
class Neo4jStorage {
|
|
12
|
+
constructor(uri, username, password, database) {
|
|
13
|
+
this.initialized = false;
|
|
14
|
+
this.vectorIndexCreated = false;
|
|
15
|
+
const resolvedUri = uri.replace('://localhost:', '://127.0.0.1:');
|
|
16
|
+
this.driver = neo4j_driver_1.default.driver(resolvedUri, neo4j_driver_1.default.auth.basic(username, password));
|
|
17
|
+
this.database = database !== null && database !== void 0 ? database : 'neo4j';
|
|
18
|
+
}
|
|
19
|
+
async initialize() {
|
|
20
|
+
if (this.initialized)
|
|
21
|
+
return;
|
|
22
|
+
await this.driver.verifyConnectivity();
|
|
23
|
+
const session = this.getSession();
|
|
24
|
+
try {
|
|
25
|
+
await session.executeWrite(async (tx) => {
|
|
26
|
+
await tx.run('CREATE INDEX IF NOT EXISTS FOR (e:Entity) ON (e.uuid)');
|
|
27
|
+
await tx.run('CREATE INDEX IF NOT EXISTS FOR (e:Entity) ON (e.group_id)');
|
|
28
|
+
await tx.run('CREATE INDEX IF NOT EXISTS FOR (e:Entity) ON (e.name)');
|
|
29
|
+
await tx.run('CREATE INDEX IF NOT EXISTS FOR (ep:Episode) ON (ep.uuid)');
|
|
30
|
+
await tx.run('CREATE INDEX IF NOT EXISTS FOR (ep:Episode) ON (ep.group_id)');
|
|
31
|
+
await tx.run('CREATE FULLTEXT INDEX entitySearch IF NOT EXISTS FOR (e:Entity) ON EACH [e.name, e.summary, e.entity_type]');
|
|
32
|
+
await tx.run('CREATE FULLTEXT INDEX edgeSearch IF NOT EXISTS FOR ()-[r:RELATES_TO]-() ON EACH [r.name, r.fact]');
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await session.close();
|
|
37
|
+
}
|
|
38
|
+
this.initialized = true;
|
|
39
|
+
}
|
|
40
|
+
async close() {
|
|
41
|
+
await this.driver.close();
|
|
42
|
+
}
|
|
43
|
+
getSession() {
|
|
44
|
+
return this.driver.session({ database: this.database });
|
|
45
|
+
}
|
|
46
|
+
async addEntity(input) {
|
|
47
|
+
var _a, _b, _c, _d;
|
|
48
|
+
const now = (0, temporal_1.nowIso)();
|
|
49
|
+
const entity = {
|
|
50
|
+
uuid: (0, uuid_1.generateUuid)(),
|
|
51
|
+
name: input.name,
|
|
52
|
+
group_id: input.group_id,
|
|
53
|
+
summary: (_a = input.summary) !== null && _a !== void 0 ? _a : '',
|
|
54
|
+
entity_type: (_b = input.entity_type) !== null && _b !== void 0 ? _b : 'unknown',
|
|
55
|
+
name_embedding: (_c = input.name_embedding) !== null && _c !== void 0 ? _c : null,
|
|
56
|
+
attributes: (_d = input.attributes) !== null && _d !== void 0 ? _d : {},
|
|
57
|
+
created_at: now,
|
|
58
|
+
updated_at: now,
|
|
59
|
+
};
|
|
60
|
+
if (entity.name_embedding && entity.name_embedding.length > 0) {
|
|
61
|
+
await this.ensureVectorIndex(entity.name_embedding.length);
|
|
62
|
+
}
|
|
63
|
+
const session = this.getSession();
|
|
64
|
+
try {
|
|
65
|
+
await session.executeWrite(async (tx) => {
|
|
66
|
+
await tx.run(`CREATE (e:Entity {
|
|
67
|
+
uuid: $uuid,
|
|
68
|
+
name: $name,
|
|
69
|
+
group_id: $group_id,
|
|
70
|
+
summary: $summary,
|
|
71
|
+
entity_type: $entity_type,
|
|
72
|
+
name_embedding: $name_embedding,
|
|
73
|
+
attributes: $attributes,
|
|
74
|
+
created_at: $created_at,
|
|
75
|
+
updated_at: $updated_at
|
|
76
|
+
})`, {
|
|
77
|
+
...entity,
|
|
78
|
+
attributes: JSON.stringify(entity.attributes),
|
|
79
|
+
name_embedding: entity.name_embedding,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await session.close();
|
|
85
|
+
}
|
|
86
|
+
return entity;
|
|
87
|
+
}
|
|
88
|
+
async getEntity(uuid) {
|
|
89
|
+
const session = this.getSession();
|
|
90
|
+
try {
|
|
91
|
+
const result = await session.executeRead(async (tx) => {
|
|
92
|
+
return tx.run('MATCH (e:Entity {uuid: $uuid}) RETURN e', { uuid });
|
|
93
|
+
});
|
|
94
|
+
if (result.records.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
return this.recordToEntity(result.records[0].get('e').properties);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await session.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async getEntityByName(name, groupId) {
|
|
103
|
+
const session = this.getSession();
|
|
104
|
+
try {
|
|
105
|
+
const result = await session.executeRead(async (tx) => {
|
|
106
|
+
return tx.run('MATCH (e:Entity {group_id: $groupId}) WHERE toLower(e.name) = toLower($name) RETURN e LIMIT 1', { name, groupId });
|
|
107
|
+
});
|
|
108
|
+
if (result.records.length === 0)
|
|
109
|
+
return null;
|
|
110
|
+
return this.recordToEntity(result.records[0].get('e').properties);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await session.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async updateEntity(uuid, updates) {
|
|
117
|
+
const session = this.getSession();
|
|
118
|
+
try {
|
|
119
|
+
const setClauses = ['e.updated_at = $updated_at'];
|
|
120
|
+
const params = {
|
|
121
|
+
uuid,
|
|
122
|
+
updated_at: (0, temporal_1.nowIso)(),
|
|
123
|
+
};
|
|
124
|
+
if (updates.name !== undefined) {
|
|
125
|
+
setClauses.push('e.name = $name');
|
|
126
|
+
params.name = updates.name;
|
|
127
|
+
}
|
|
128
|
+
if (updates.summary !== undefined) {
|
|
129
|
+
setClauses.push('e.summary = $summary');
|
|
130
|
+
params.summary = updates.summary;
|
|
131
|
+
}
|
|
132
|
+
if (updates.entity_type !== undefined) {
|
|
133
|
+
setClauses.push('e.entity_type = $entity_type');
|
|
134
|
+
params.entity_type = updates.entity_type;
|
|
135
|
+
}
|
|
136
|
+
if (updates.attributes !== undefined) {
|
|
137
|
+
setClauses.push('e.attributes = $attributes');
|
|
138
|
+
params.attributes = JSON.stringify(updates.attributes);
|
|
139
|
+
}
|
|
140
|
+
if (updates.name_embedding !== undefined) {
|
|
141
|
+
setClauses.push('e.name_embedding = $name_embedding');
|
|
142
|
+
params.name_embedding = updates.name_embedding;
|
|
143
|
+
if (updates.name_embedding && updates.name_embedding.length > 0) {
|
|
144
|
+
await this.ensureVectorIndex(updates.name_embedding.length);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const result = await session.executeWrite(async (tx) => {
|
|
148
|
+
return tx.run(`MATCH (e:Entity {uuid: $uuid}) SET ${setClauses.join(', ')} RETURN e`, params);
|
|
149
|
+
});
|
|
150
|
+
if (result.records.length === 0) {
|
|
151
|
+
throw new Error(`Entity not found: ${uuid}`);
|
|
152
|
+
}
|
|
153
|
+
return this.recordToEntity(result.records[0].get('e').properties);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await session.close();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async deleteEntity(uuid) {
|
|
160
|
+
const session = this.getSession();
|
|
161
|
+
try {
|
|
162
|
+
await session.executeWrite(async (tx) => {
|
|
163
|
+
await tx.run('MATCH (e:Entity {uuid: $uuid}) DETACH DELETE e', { uuid });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await session.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async listEntities(groupId, options) {
|
|
171
|
+
const session = this.getSession();
|
|
172
|
+
try {
|
|
173
|
+
let query = 'MATCH (e:Entity {group_id: $groupId})';
|
|
174
|
+
const params = { groupId };
|
|
175
|
+
if (options === null || options === void 0 ? void 0 : options.entity_type) {
|
|
176
|
+
query += ' WHERE e.entity_type = $entity_type';
|
|
177
|
+
params.entity_type = options.entity_type;
|
|
178
|
+
}
|
|
179
|
+
query += ' RETURN e ORDER BY e.created_at DESC';
|
|
180
|
+
if (options === null || options === void 0 ? void 0 : options.offset) {
|
|
181
|
+
query += ' SKIP $offset';
|
|
182
|
+
params.offset = neo4j_driver_1.default.int(options.offset);
|
|
183
|
+
}
|
|
184
|
+
if (options === null || options === void 0 ? void 0 : options.limit) {
|
|
185
|
+
query += ' LIMIT $limit';
|
|
186
|
+
params.limit = neo4j_driver_1.default.int(options.limit);
|
|
187
|
+
}
|
|
188
|
+
const result = await session.executeRead(async (tx) => {
|
|
189
|
+
return tx.run(query, params);
|
|
190
|
+
});
|
|
191
|
+
return result.records.map((r) => this.recordToEntity(r.get('e').properties));
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await session.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async addEdge(input) {
|
|
198
|
+
var _a, _b, _c, _d, _e, _f;
|
|
199
|
+
const now = (0, temporal_1.nowIso)();
|
|
200
|
+
const edge = {
|
|
201
|
+
uuid: (0, uuid_1.generateUuid)(),
|
|
202
|
+
group_id: input.group_id,
|
|
203
|
+
source_node_uuid: input.source_node_uuid,
|
|
204
|
+
target_node_uuid: input.target_node_uuid,
|
|
205
|
+
name: input.name,
|
|
206
|
+
fact: input.fact,
|
|
207
|
+
fact_embedding: (_a = input.fact_embedding) !== null && _a !== void 0 ? _a : null,
|
|
208
|
+
episodes: (_b = input.episodes) !== null && _b !== void 0 ? _b : [],
|
|
209
|
+
valid_at: (_c = input.valid_at) !== null && _c !== void 0 ? _c : null,
|
|
210
|
+
invalid_at: (_d = input.invalid_at) !== null && _d !== void 0 ? _d : null,
|
|
211
|
+
expired_at: (_e = input.expired_at) !== null && _e !== void 0 ? _e : null,
|
|
212
|
+
attributes: (_f = input.attributes) !== null && _f !== void 0 ? _f : {},
|
|
213
|
+
created_at: now,
|
|
214
|
+
updated_at: now,
|
|
215
|
+
};
|
|
216
|
+
if (edge.fact_embedding && edge.fact_embedding.length > 0) {
|
|
217
|
+
await this.ensureVectorIndex(edge.fact_embedding.length);
|
|
218
|
+
}
|
|
219
|
+
const session = this.getSession();
|
|
220
|
+
try {
|
|
221
|
+
const result = await session.executeWrite(async (tx) => {
|
|
222
|
+
return tx.run(`MATCH (source:Entity {uuid: $source_node_uuid})
|
|
223
|
+
MATCH (target:Entity {uuid: $target_node_uuid})
|
|
224
|
+
CREATE (source)-[r:RELATES_TO {
|
|
225
|
+
uuid: $uuid,
|
|
226
|
+
group_id: $group_id,
|
|
227
|
+
source_node_uuid: $source_node_uuid,
|
|
228
|
+
target_node_uuid: $target_node_uuid,
|
|
229
|
+
name: $name,
|
|
230
|
+
fact: $fact,
|
|
231
|
+
fact_embedding: $fact_embedding,
|
|
232
|
+
episodes: $episodes,
|
|
233
|
+
valid_at: $valid_at,
|
|
234
|
+
invalid_at: $invalid_at,
|
|
235
|
+
expired_at: $expired_at,
|
|
236
|
+
attributes: $attributes,
|
|
237
|
+
created_at: $created_at,
|
|
238
|
+
updated_at: $updated_at
|
|
239
|
+
}]->(target)
|
|
240
|
+
RETURN r`, {
|
|
241
|
+
...edge,
|
|
242
|
+
attributes: JSON.stringify(edge.attributes),
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
if (result.records.length === 0) {
|
|
246
|
+
throw new Error(`Cannot create edge: source entity (${input.source_node_uuid}) or target entity (${input.target_node_uuid}) not found`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await session.close();
|
|
251
|
+
}
|
|
252
|
+
return edge;
|
|
253
|
+
}
|
|
254
|
+
async getEdge(uuid) {
|
|
255
|
+
const session = this.getSession();
|
|
256
|
+
try {
|
|
257
|
+
const result = await session.executeRead(async (tx) => {
|
|
258
|
+
return tx.run('MATCH ()-[r:RELATES_TO {uuid: $uuid}]->() RETURN r', { uuid });
|
|
259
|
+
});
|
|
260
|
+
if (result.records.length === 0)
|
|
261
|
+
return null;
|
|
262
|
+
return this.recordToEdge(result.records[0].get('r').properties);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
await session.close();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async getEdgesBetween(sourceUuid, targetUuid) {
|
|
269
|
+
const session = this.getSession();
|
|
270
|
+
try {
|
|
271
|
+
const result = await session.executeRead(async (tx) => {
|
|
272
|
+
return tx.run(`MATCH (s:Entity {uuid: $sourceUuid})-[r:RELATES_TO]->(t:Entity {uuid: $targetUuid})
|
|
273
|
+
RETURN r
|
|
274
|
+
UNION
|
|
275
|
+
MATCH (s:Entity {uuid: $targetUuid})-[r:RELATES_TO]->(t:Entity {uuid: $sourceUuid})
|
|
276
|
+
RETURN r`, { sourceUuid, targetUuid });
|
|
277
|
+
});
|
|
278
|
+
return result.records.map((rec) => this.recordToEdge(rec.get('r').properties));
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
await session.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async getEdgesForEntity(entityUuid) {
|
|
285
|
+
const session = this.getSession();
|
|
286
|
+
try {
|
|
287
|
+
const result = await session.executeRead(async (tx) => {
|
|
288
|
+
return tx.run(`MATCH (e:Entity {uuid: $entityUuid})-[r:RELATES_TO]->()
|
|
289
|
+
RETURN r
|
|
290
|
+
UNION
|
|
291
|
+
MATCH (e:Entity {uuid: $entityUuid})<-[r:RELATES_TO]-()
|
|
292
|
+
RETURN r`, { entityUuid });
|
|
293
|
+
});
|
|
294
|
+
return result.records.map((rec) => this.recordToEdge(rec.get('r').properties));
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
await session.close();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async updateEdge(uuid, updates) {
|
|
301
|
+
const session = this.getSession();
|
|
302
|
+
try {
|
|
303
|
+
const setClauses = ['r.updated_at = $updated_at'];
|
|
304
|
+
const params = {
|
|
305
|
+
uuid,
|
|
306
|
+
updated_at: (0, temporal_1.nowIso)(),
|
|
307
|
+
};
|
|
308
|
+
if (updates.name !== undefined) {
|
|
309
|
+
setClauses.push('r.name = $name');
|
|
310
|
+
params.name = updates.name;
|
|
311
|
+
}
|
|
312
|
+
if (updates.fact !== undefined) {
|
|
313
|
+
setClauses.push('r.fact = $fact');
|
|
314
|
+
params.fact = updates.fact;
|
|
315
|
+
}
|
|
316
|
+
if (updates.valid_at !== undefined) {
|
|
317
|
+
setClauses.push('r.valid_at = $valid_at');
|
|
318
|
+
params.valid_at = updates.valid_at;
|
|
319
|
+
}
|
|
320
|
+
if (updates.invalid_at !== undefined) {
|
|
321
|
+
setClauses.push('r.invalid_at = $invalid_at');
|
|
322
|
+
params.invalid_at = updates.invalid_at;
|
|
323
|
+
}
|
|
324
|
+
if (updates.expired_at !== undefined) {
|
|
325
|
+
setClauses.push('r.expired_at = $expired_at');
|
|
326
|
+
params.expired_at = updates.expired_at;
|
|
327
|
+
}
|
|
328
|
+
if (updates.episodes !== undefined) {
|
|
329
|
+
setClauses.push('r.episodes = $episodes');
|
|
330
|
+
params.episodes = updates.episodes;
|
|
331
|
+
}
|
|
332
|
+
if (updates.attributes !== undefined) {
|
|
333
|
+
setClauses.push('r.attributes = $attributes');
|
|
334
|
+
params.attributes = JSON.stringify(updates.attributes);
|
|
335
|
+
}
|
|
336
|
+
if (updates.fact_embedding !== undefined) {
|
|
337
|
+
setClauses.push('r.fact_embedding = $fact_embedding');
|
|
338
|
+
params.fact_embedding = updates.fact_embedding;
|
|
339
|
+
if (updates.fact_embedding && updates.fact_embedding.length > 0) {
|
|
340
|
+
await this.ensureVectorIndex(updates.fact_embedding.length);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const result = await session.executeWrite(async (tx) => {
|
|
344
|
+
return tx.run(`MATCH ()-[r:RELATES_TO {uuid: $uuid}]->()
|
|
345
|
+
SET ${setClauses.join(', ')}
|
|
346
|
+
RETURN r`, params);
|
|
347
|
+
});
|
|
348
|
+
if (result.records.length === 0) {
|
|
349
|
+
throw new Error(`Edge not found: ${uuid}`);
|
|
350
|
+
}
|
|
351
|
+
return this.recordToEdge(result.records[0].get('r').properties);
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
await session.close();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async deleteEdge(uuid) {
|
|
358
|
+
const session = this.getSession();
|
|
359
|
+
try {
|
|
360
|
+
await session.executeWrite(async (tx) => {
|
|
361
|
+
await tx.run('MATCH ()-[r:RELATES_TO {uuid: $uuid}]->() DELETE r', { uuid });
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
await session.close();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async addEpisode(input) {
|
|
369
|
+
var _a, _b;
|
|
370
|
+
const episode = {
|
|
371
|
+
uuid: (0, uuid_1.generateUuid)(),
|
|
372
|
+
group_id: input.group_id,
|
|
373
|
+
content: input.content,
|
|
374
|
+
role: input.role,
|
|
375
|
+
source_type: (_a = input.source_type) !== null && _a !== void 0 ? _a : 'message',
|
|
376
|
+
reference_time: input.reference_time,
|
|
377
|
+
previous_episode_uuid: (_b = input.previous_episode_uuid) !== null && _b !== void 0 ? _b : null,
|
|
378
|
+
created_at: (0, temporal_1.nowIso)(),
|
|
379
|
+
};
|
|
380
|
+
const session = this.getSession();
|
|
381
|
+
try {
|
|
382
|
+
await session.executeWrite(async (tx) => {
|
|
383
|
+
await tx.run(`CREATE (ep:Episode {
|
|
384
|
+
uuid: $uuid,
|
|
385
|
+
group_id: $group_id,
|
|
386
|
+
content: $content,
|
|
387
|
+
role: $role,
|
|
388
|
+
source_type: $source_type,
|
|
389
|
+
reference_time: $reference_time,
|
|
390
|
+
previous_episode_uuid: $previous_episode_uuid,
|
|
391
|
+
created_at: $created_at
|
|
392
|
+
})`, episode);
|
|
393
|
+
if (episode.previous_episode_uuid) {
|
|
394
|
+
await tx.run(`MATCH (prev:Episode {uuid: $prevUuid})
|
|
395
|
+
MATCH (curr:Episode {uuid: $currUuid})
|
|
396
|
+
CREATE (prev)-[:NEXT_EPISODE]->(curr)`, {
|
|
397
|
+
prevUuid: episode.previous_episode_uuid,
|
|
398
|
+
currUuid: episode.uuid,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
await session.close();
|
|
405
|
+
}
|
|
406
|
+
return episode;
|
|
407
|
+
}
|
|
408
|
+
async getEpisode(uuid) {
|
|
409
|
+
const session = this.getSession();
|
|
410
|
+
try {
|
|
411
|
+
const result = await session.executeRead(async (tx) => {
|
|
412
|
+
return tx.run('MATCH (ep:Episode {uuid: $uuid}) RETURN ep', { uuid });
|
|
413
|
+
});
|
|
414
|
+
if (result.records.length === 0)
|
|
415
|
+
return null;
|
|
416
|
+
return this.recordToEpisode(result.records[0].get('ep').properties);
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
await session.close();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async getRecentEpisodes(groupId, limit) {
|
|
423
|
+
const session = this.getSession();
|
|
424
|
+
try {
|
|
425
|
+
const result = await session.executeRead(async (tx) => {
|
|
426
|
+
return tx.run(`MATCH (ep:Episode {group_id: $groupId})
|
|
427
|
+
RETURN ep
|
|
428
|
+
ORDER BY ep.created_at DESC
|
|
429
|
+
LIMIT $limit`, { groupId, limit: neo4j_driver_1.default.int(limit) });
|
|
430
|
+
});
|
|
431
|
+
const episodes = result.records.map((r) => this.recordToEpisode(r.get('ep').properties));
|
|
432
|
+
return episodes.reverse();
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
await session.close();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async getEpisodeCount(groupId) {
|
|
439
|
+
var _a, _b;
|
|
440
|
+
const session = this.getSession();
|
|
441
|
+
try {
|
|
442
|
+
const result = await session.executeRead(async (tx) => {
|
|
443
|
+
return tx.run('MATCH (ep:Episode {group_id: $groupId}) RETURN count(ep) as cnt', {
|
|
444
|
+
groupId,
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
return (_b = (_a = result.records[0]) === null || _a === void 0 ? void 0 : _a.get('cnt').toNumber()) !== null && _b !== void 0 ? _b : 0;
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
await session.close();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
sanitizeLuceneQuery(query) {
|
|
454
|
+
const sanitized = query.replace(/[+\-&|!(){}[\]^"~*?:\\/]/g, (ch) => `\\${ch}`);
|
|
455
|
+
return sanitized.trim();
|
|
456
|
+
}
|
|
457
|
+
async searchEntities(query, groupId, options) {
|
|
458
|
+
var _a;
|
|
459
|
+
const safeQuery = this.sanitizeLuceneQuery(query);
|
|
460
|
+
if (!safeQuery)
|
|
461
|
+
return [];
|
|
462
|
+
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 10;
|
|
463
|
+
const session = this.getSession();
|
|
464
|
+
try {
|
|
465
|
+
let cypher = `CALL db.index.fulltext.queryNodes('entitySearch', $query)
|
|
466
|
+
YIELD node, score
|
|
467
|
+
WHERE node.group_id = $groupId`;
|
|
468
|
+
const params = { query: safeQuery, groupId };
|
|
469
|
+
if (options === null || options === void 0 ? void 0 : options.entity_type) {
|
|
470
|
+
cypher += ' AND node.entity_type = $entityType';
|
|
471
|
+
params.entityType = options.entity_type;
|
|
472
|
+
}
|
|
473
|
+
cypher += ' RETURN node, score ORDER BY score DESC LIMIT $limit';
|
|
474
|
+
params.limit = neo4j_driver_1.default.int(limit);
|
|
475
|
+
const result = await session.executeRead(async (tx) => {
|
|
476
|
+
return tx.run(cypher, params);
|
|
477
|
+
});
|
|
478
|
+
const maxScore = result.records.length > 0 ? result.records[0].get('score') : 1;
|
|
479
|
+
return result.records
|
|
480
|
+
.filter((r) => {
|
|
481
|
+
var _a;
|
|
482
|
+
const score = r.get('score') / (maxScore || 1);
|
|
483
|
+
return score >= ((_a = options === null || options === void 0 ? void 0 : options.min_score) !== null && _a !== void 0 ? _a : 0);
|
|
484
|
+
})
|
|
485
|
+
.map((r) => ({
|
|
486
|
+
entity: this.recordToEntity(r.get('node').properties),
|
|
487
|
+
score: r.get('score') / (maxScore || 1),
|
|
488
|
+
}));
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
await session.close();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async searchEdges(query, groupId, options) {
|
|
495
|
+
var _a, _b;
|
|
496
|
+
const safeQuery = this.sanitizeLuceneQuery(query);
|
|
497
|
+
if (!safeQuery)
|
|
498
|
+
return [];
|
|
499
|
+
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 10;
|
|
500
|
+
const includeExpired = (_b = options === null || options === void 0 ? void 0 : options.include_expired) !== null && _b !== void 0 ? _b : false;
|
|
501
|
+
const session = this.getSession();
|
|
502
|
+
try {
|
|
503
|
+
let cypher = `CALL db.index.fulltext.queryRelationships('edgeSearch', $query)
|
|
504
|
+
YIELD relationship, score
|
|
505
|
+
WHERE relationship.group_id = $groupId`;
|
|
506
|
+
if (!includeExpired) {
|
|
507
|
+
cypher += ' AND relationship.expired_at IS NULL';
|
|
508
|
+
}
|
|
509
|
+
cypher += ` WITH relationship, score
|
|
510
|
+
ORDER BY score DESC LIMIT $limit
|
|
511
|
+
MATCH (source:Entity {uuid: relationship.source_node_uuid})
|
|
512
|
+
MATCH (target:Entity {uuid: relationship.target_node_uuid})
|
|
513
|
+
RETURN relationship, source, target, score`;
|
|
514
|
+
const params = {
|
|
515
|
+
query: safeQuery,
|
|
516
|
+
groupId,
|
|
517
|
+
limit: neo4j_driver_1.default.int(limit),
|
|
518
|
+
};
|
|
519
|
+
const result = await session.executeRead(async (tx) => {
|
|
520
|
+
return tx.run(cypher, params);
|
|
521
|
+
});
|
|
522
|
+
const maxScore = result.records.length > 0 ? result.records[0].get('score') : 1;
|
|
523
|
+
return result.records
|
|
524
|
+
.filter((r) => {
|
|
525
|
+
var _a;
|
|
526
|
+
const score = r.get('score') / (maxScore || 1);
|
|
527
|
+
return score >= ((_a = options === null || options === void 0 ? void 0 : options.min_score) !== null && _a !== void 0 ? _a : 0);
|
|
528
|
+
})
|
|
529
|
+
.map((r) => ({
|
|
530
|
+
edge: this.recordToEdge(r.get('relationship').properties),
|
|
531
|
+
sourceEntity: this.recordToEntity(r.get('source').properties),
|
|
532
|
+
targetEntity: this.recordToEntity(r.get('target').properties),
|
|
533
|
+
score: r.get('score') / (maxScore || 1),
|
|
534
|
+
}));
|
|
535
|
+
}
|
|
536
|
+
finally {
|
|
537
|
+
await session.close();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async ensureVectorIndex(dimensions) {
|
|
541
|
+
if (this.vectorIndexCreated)
|
|
542
|
+
return;
|
|
543
|
+
const session = this.getSession();
|
|
544
|
+
try {
|
|
545
|
+
await session.executeWrite(async (tx) => {
|
|
546
|
+
await tx.run(`CREATE VECTOR INDEX entityNameEmbedding IF NOT EXISTS
|
|
547
|
+
FOR (e:Entity)
|
|
548
|
+
ON (e.name_embedding)
|
|
549
|
+
OPTIONS {indexConfig: {
|
|
550
|
+
\`vector.dimensions\`: $dimensions,
|
|
551
|
+
\`vector.similarity_function\`: 'cosine'
|
|
552
|
+
}}`, { dimensions: neo4j_driver_1.default.int(dimensions) });
|
|
553
|
+
});
|
|
554
|
+
this.vectorIndexCreated = true;
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
console.warn('Engram: Could not create vector indexes (Neo4j may not support them):', error.message);
|
|
558
|
+
}
|
|
559
|
+
finally {
|
|
560
|
+
await session.close();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async searchEntitiesByVector(vector, groupId, options) {
|
|
564
|
+
var _a, _b;
|
|
565
|
+
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 10;
|
|
566
|
+
const minScore = (_b = options === null || options === void 0 ? void 0 : options.min_score) !== null && _b !== void 0 ? _b : 0;
|
|
567
|
+
const session = this.getSession();
|
|
568
|
+
try {
|
|
569
|
+
const result = await session.executeRead(async (tx) => {
|
|
570
|
+
return tx.run(`CALL db.index.vector.queryNodes('entityNameEmbedding', $topK, $vector)
|
|
571
|
+
YIELD node, score
|
|
572
|
+
WHERE node.group_id = $groupId AND score >= $minScore
|
|
573
|
+
RETURN node, score
|
|
574
|
+
ORDER BY score DESC
|
|
575
|
+
LIMIT $limit`, {
|
|
576
|
+
vector,
|
|
577
|
+
groupId,
|
|
578
|
+
topK: neo4j_driver_1.default.int(limit * 2),
|
|
579
|
+
minScore,
|
|
580
|
+
limit: neo4j_driver_1.default.int(limit),
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
return result.records.map((r) => ({
|
|
584
|
+
entity: this.recordToEntity(r.get('node').properties),
|
|
585
|
+
score: r.get('score'),
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
return this.bruteForceEntityVectorSearch(vector, groupId, options);
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
await session.close();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async searchEdgesByVector(vector, groupId, options) {
|
|
596
|
+
var _a, _b;
|
|
597
|
+
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 10;
|
|
598
|
+
const minScore = (_b = options === null || options === void 0 ? void 0 : options.min_score) !== null && _b !== void 0 ? _b : 0;
|
|
599
|
+
const session = this.getSession();
|
|
600
|
+
try {
|
|
601
|
+
const result = await session.executeRead(async (tx) => {
|
|
602
|
+
return tx.run(`MATCH (source:Entity)-[r:RELATES_TO]->(target:Entity)
|
|
603
|
+
WHERE r.group_id = $groupId
|
|
604
|
+
AND r.fact_embedding IS NOT NULL
|
|
605
|
+
AND r.expired_at IS NULL
|
|
606
|
+
RETURN r, source, target`, { groupId });
|
|
607
|
+
});
|
|
608
|
+
const scored = [];
|
|
609
|
+
for (const rec of result.records) {
|
|
610
|
+
const edgeProps = rec.get('r').properties;
|
|
611
|
+
const embedding = edgeProps.fact_embedding;
|
|
612
|
+
if (!embedding || embedding.length !== vector.length)
|
|
613
|
+
continue;
|
|
614
|
+
try {
|
|
615
|
+
const score = (0, cosine_1.cosineSimilarity)(vector, embedding);
|
|
616
|
+
if (score < minScore)
|
|
617
|
+
continue;
|
|
618
|
+
scored.push({
|
|
619
|
+
edge: this.recordToEdge(edgeProps),
|
|
620
|
+
sourceEntity: this.recordToEntity(rec.get('source').properties),
|
|
621
|
+
targetEntity: this.recordToEntity(rec.get('target').properties),
|
|
622
|
+
score,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
scored.sort((a, b) => b.score - a.score);
|
|
629
|
+
return scored.slice(0, limit);
|
|
630
|
+
}
|
|
631
|
+
finally {
|
|
632
|
+
await session.close();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async bruteForceEntityVectorSearch(vector, groupId, options) {
|
|
636
|
+
var _a, _b;
|
|
637
|
+
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 10;
|
|
638
|
+
const minScore = (_b = options === null || options === void 0 ? void 0 : options.min_score) !== null && _b !== void 0 ? _b : 0;
|
|
639
|
+
const session = this.getSession();
|
|
640
|
+
try {
|
|
641
|
+
const result = await session.executeRead(async (tx) => {
|
|
642
|
+
return tx.run(`MATCH (e:Entity {group_id: $groupId})
|
|
643
|
+
WHERE e.name_embedding IS NOT NULL
|
|
644
|
+
RETURN e`, { groupId });
|
|
645
|
+
});
|
|
646
|
+
const scored = [];
|
|
647
|
+
for (const rec of result.records) {
|
|
648
|
+
const entity = this.recordToEntity(rec.get('e').properties);
|
|
649
|
+
if (!entity.name_embedding || entity.name_embedding.length !== vector.length)
|
|
650
|
+
continue;
|
|
651
|
+
try {
|
|
652
|
+
const score = (0, cosine_1.cosineSimilarity)(vector, entity.name_embedding);
|
|
653
|
+
if (score >= minScore) {
|
|
654
|
+
scored.push({ entity, score });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
scored.sort((a, b) => b.score - a.score);
|
|
661
|
+
return scored.slice(0, limit);
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
await session.close();
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async clearGroup(groupId) {
|
|
668
|
+
const session = this.getSession();
|
|
669
|
+
try {
|
|
670
|
+
await session.executeWrite(async (tx) => {
|
|
671
|
+
await tx.run(`MATCH ()-[r:RELATES_TO {group_id: $groupId}]->()
|
|
672
|
+
DELETE r`, { groupId });
|
|
673
|
+
await tx.run(`MATCH (n)
|
|
674
|
+
WHERE n.group_id = $groupId AND (n:Entity OR n:Episode)
|
|
675
|
+
DETACH DELETE n`, { groupId });
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
await session.close();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async clearAll() {
|
|
683
|
+
const session = this.getSession();
|
|
684
|
+
try {
|
|
685
|
+
await session.executeWrite(async (tx) => {
|
|
686
|
+
await tx.run('MATCH (n) WHERE n:Entity OR n:Episode DETACH DELETE n');
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
await session.close();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async exportGraph(groupId) {
|
|
694
|
+
const entities = [];
|
|
695
|
+
const edges = [];
|
|
696
|
+
const episodes = [];
|
|
697
|
+
const session = this.getSession();
|
|
698
|
+
try {
|
|
699
|
+
const entityQuery = groupId
|
|
700
|
+
? 'MATCH (e:Entity {group_id: $groupId}) RETURN e'
|
|
701
|
+
: 'MATCH (e:Entity) RETURN e';
|
|
702
|
+
const entityResult = await session.executeRead(async (tx) => {
|
|
703
|
+
return tx.run(entityQuery, groupId ? { groupId } : {});
|
|
704
|
+
});
|
|
705
|
+
for (const r of entityResult.records) {
|
|
706
|
+
entities.push(this.recordToEntity(r.get('e').properties));
|
|
707
|
+
}
|
|
708
|
+
const edgeQuery = groupId
|
|
709
|
+
? 'MATCH ()-[r:RELATES_TO {group_id: $groupId}]->() RETURN r'
|
|
710
|
+
: 'MATCH ()-[r:RELATES_TO]->() RETURN r';
|
|
711
|
+
const edgeResult = await session.executeRead(async (tx) => {
|
|
712
|
+
return tx.run(edgeQuery, groupId ? { groupId } : {});
|
|
713
|
+
});
|
|
714
|
+
for (const r of edgeResult.records) {
|
|
715
|
+
edges.push(this.recordToEdge(r.get('r').properties));
|
|
716
|
+
}
|
|
717
|
+
const episodeQuery = groupId
|
|
718
|
+
? 'MATCH (ep:Episode {group_id: $groupId}) RETURN ep'
|
|
719
|
+
: 'MATCH (ep:Episode) RETURN ep';
|
|
720
|
+
const episodeResult = await session.executeRead(async (tx) => {
|
|
721
|
+
return tx.run(episodeQuery, groupId ? { groupId } : {});
|
|
722
|
+
});
|
|
723
|
+
for (const r of episodeResult.records) {
|
|
724
|
+
episodes.push(this.recordToEpisode(r.get('ep').properties));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
finally {
|
|
728
|
+
await session.close();
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
version: '1.0',
|
|
732
|
+
exported_at: (0, temporal_1.nowIso)(),
|
|
733
|
+
group_id: groupId,
|
|
734
|
+
entities,
|
|
735
|
+
edges,
|
|
736
|
+
episodes,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
async importGraph(data) {
|
|
740
|
+
const session = this.getSession();
|
|
741
|
+
try {
|
|
742
|
+
await session.executeWrite(async (tx) => {
|
|
743
|
+
for (const entity of data.entities) {
|
|
744
|
+
await tx.run(`MERGE (e:Entity {uuid: $uuid})
|
|
745
|
+
SET e += {
|
|
746
|
+
name: $name,
|
|
747
|
+
group_id: $group_id,
|
|
748
|
+
summary: $summary,
|
|
749
|
+
entity_type: $entity_type,
|
|
750
|
+
name_embedding: $name_embedding,
|
|
751
|
+
attributes: $attributes,
|
|
752
|
+
created_at: $created_at,
|
|
753
|
+
updated_at: $updated_at
|
|
754
|
+
}`, {
|
|
755
|
+
...entity,
|
|
756
|
+
attributes: JSON.stringify(entity.attributes),
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
for (const episode of data.episodes) {
|
|
760
|
+
await tx.run(`MERGE (ep:Episode {uuid: $uuid})
|
|
761
|
+
SET ep += {
|
|
762
|
+
group_id: $group_id,
|
|
763
|
+
content: $content,
|
|
764
|
+
role: $role,
|
|
765
|
+
source_type: $source_type,
|
|
766
|
+
reference_time: $reference_time,
|
|
767
|
+
previous_episode_uuid: $previous_episode_uuid,
|
|
768
|
+
created_at: $created_at
|
|
769
|
+
}`, episode);
|
|
770
|
+
}
|
|
771
|
+
for (const edge of data.edges) {
|
|
772
|
+
await tx.run(`MATCH (source:Entity {uuid: $source_node_uuid})
|
|
773
|
+
MATCH (target:Entity {uuid: $target_node_uuid})
|
|
774
|
+
MERGE (source)-[r:RELATES_TO {uuid: $uuid}]->(target)
|
|
775
|
+
SET r += {
|
|
776
|
+
group_id: $group_id,
|
|
777
|
+
source_node_uuid: $source_node_uuid,
|
|
778
|
+
target_node_uuid: $target_node_uuid,
|
|
779
|
+
name: $name,
|
|
780
|
+
fact: $fact,
|
|
781
|
+
fact_embedding: $fact_embedding,
|
|
782
|
+
episodes: $episodes,
|
|
783
|
+
valid_at: $valid_at,
|
|
784
|
+
invalid_at: $invalid_at,
|
|
785
|
+
expired_at: $expired_at,
|
|
786
|
+
attributes: $attributes,
|
|
787
|
+
created_at: $created_at,
|
|
788
|
+
updated_at: $updated_at
|
|
789
|
+
}`, {
|
|
790
|
+
...edge,
|
|
791
|
+
attributes: JSON.stringify(edge.attributes),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
for (const episode of data.episodes) {
|
|
795
|
+
if (episode.previous_episode_uuid) {
|
|
796
|
+
await tx.run(`MATCH (prev:Episode {uuid: $prevUuid})
|
|
797
|
+
MATCH (curr:Episode {uuid: $currUuid})
|
|
798
|
+
MERGE (prev)-[:NEXT_EPISODE]->(curr)`, {
|
|
799
|
+
prevUuid: episode.previous_episode_uuid,
|
|
800
|
+
currUuid: episode.uuid,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
await session.close();
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async getStats(groupId) {
|
|
811
|
+
var _a, _b, _c;
|
|
812
|
+
const session = this.getSession();
|
|
813
|
+
try {
|
|
814
|
+
const where = groupId ? ' WHERE n.group_id = $groupId' : '';
|
|
815
|
+
const params = groupId ? { groupId } : {};
|
|
816
|
+
const result = await session.executeRead(async (tx) => {
|
|
817
|
+
return tx.run(`MATCH (n)
|
|
818
|
+
${where}
|
|
819
|
+
WITH n
|
|
820
|
+
WHERE n:Entity OR n:Episode
|
|
821
|
+
RETURN
|
|
822
|
+
sum(CASE WHEN n:Entity THEN 1 ELSE 0 END) as entityCount,
|
|
823
|
+
sum(CASE WHEN n:Episode THEN 1 ELSE 0 END) as episodeCount,
|
|
824
|
+
collect(DISTINCT n.group_id) as groupIds,
|
|
825
|
+
collect(CASE WHEN n:Entity THEN n.entity_type ELSE null END) as entityTypes,
|
|
826
|
+
min(CASE WHEN n:Episode THEN n.created_at ELSE null END) as oldestEpisode,
|
|
827
|
+
max(CASE WHEN n:Episode THEN n.created_at ELSE null END) as newestEpisode`, params);
|
|
828
|
+
});
|
|
829
|
+
const edgeResult = await session.executeRead(async (tx) => {
|
|
830
|
+
const edgeWhere = groupId ? 'WHERE r.group_id = $groupId' : '';
|
|
831
|
+
return tx.run(`MATCH ()-[r:RELATES_TO]->() ${edgeWhere} RETURN count(r) as edgeCount`, params);
|
|
832
|
+
});
|
|
833
|
+
const record = result.records[0];
|
|
834
|
+
const entityTypesList = record.get('entityTypes').filter(Boolean);
|
|
835
|
+
const entityTypes = {};
|
|
836
|
+
for (const t of entityTypesList) {
|
|
837
|
+
entityTypes[t] = ((_a = entityTypes[t]) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
entity_count: record.get('entityCount').toNumber(),
|
|
841
|
+
edge_count: edgeResult.records[0].get('edgeCount').toNumber(),
|
|
842
|
+
episode_count: record.get('episodeCount').toNumber(),
|
|
843
|
+
group_ids: record.get('groupIds'),
|
|
844
|
+
entity_types: entityTypes,
|
|
845
|
+
oldest_episode: (_b = record.get('oldestEpisode')) !== null && _b !== void 0 ? _b : null,
|
|
846
|
+
newest_episode: (_c = record.get('newestEpisode')) !== null && _c !== void 0 ? _c : null,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
finally {
|
|
850
|
+
await session.close();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async applyRetention(groupId, policy) {
|
|
854
|
+
var _a, _b, _c, _d, _e, _f;
|
|
855
|
+
if (policy.type === 'forever')
|
|
856
|
+
return 0;
|
|
857
|
+
const session = this.getSession();
|
|
858
|
+
try {
|
|
859
|
+
if (policy.type === 'days' && policy.value) {
|
|
860
|
+
const cutoffDate = new Date(Date.now() - policy.value * 24 * 60 * 60 * 1000).toISOString();
|
|
861
|
+
const result = await session.executeWrite(async (tx) => {
|
|
862
|
+
return tx.run(`MATCH (ep:Episode {group_id: $groupId})
|
|
863
|
+
WHERE ep.created_at < $cutoffDate
|
|
864
|
+
DETACH DELETE ep
|
|
865
|
+
RETURN count(*) as removed`, { groupId, cutoffDate });
|
|
866
|
+
});
|
|
867
|
+
return (_c = (_b = (_a = result.records[0]) === null || _a === void 0 ? void 0 : _a.get('removed')) === null || _b === void 0 ? void 0 : _b.toNumber()) !== null && _c !== void 0 ? _c : 0;
|
|
868
|
+
}
|
|
869
|
+
if (policy.type === 'max_episodes' && policy.value) {
|
|
870
|
+
const result = await session.executeWrite(async (tx) => {
|
|
871
|
+
return tx.run(`MATCH (ep:Episode {group_id: $groupId})
|
|
872
|
+
WITH ep ORDER BY ep.created_at DESC
|
|
873
|
+
SKIP $maxEpisodes
|
|
874
|
+
WITH collect(ep) as toDelete, count(ep) as cnt
|
|
875
|
+
UNWIND toDelete as ep
|
|
876
|
+
DETACH DELETE ep
|
|
877
|
+
RETURN cnt as removed`, { groupId, maxEpisodes: neo4j_driver_1.default.int(policy.value) });
|
|
878
|
+
});
|
|
879
|
+
return (_f = (_e = (_d = result.records[0]) === null || _d === void 0 ? void 0 : _d.get('removed')) === null || _e === void 0 ? void 0 : _e.toNumber()) !== null && _f !== void 0 ? _f : 0;
|
|
880
|
+
}
|
|
881
|
+
return 0;
|
|
882
|
+
}
|
|
883
|
+
finally {
|
|
884
|
+
await session.close();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
recordToEntity(props) {
|
|
888
|
+
var _a, _b, _c;
|
|
889
|
+
return {
|
|
890
|
+
uuid: props.uuid,
|
|
891
|
+
name: props.name,
|
|
892
|
+
group_id: props.group_id,
|
|
893
|
+
summary: (_a = props.summary) !== null && _a !== void 0 ? _a : '',
|
|
894
|
+
entity_type: (_b = props.entity_type) !== null && _b !== void 0 ? _b : 'unknown',
|
|
895
|
+
name_embedding: (_c = props.name_embedding) !== null && _c !== void 0 ? _c : null,
|
|
896
|
+
attributes: this.parseJsonField(props.attributes, {}),
|
|
897
|
+
created_at: props.created_at,
|
|
898
|
+
updated_at: props.updated_at,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
recordToEdge(props) {
|
|
902
|
+
var _a, _b, _c, _d, _e;
|
|
903
|
+
return {
|
|
904
|
+
uuid: props.uuid,
|
|
905
|
+
group_id: props.group_id,
|
|
906
|
+
source_node_uuid: props.source_node_uuid,
|
|
907
|
+
target_node_uuid: props.target_node_uuid,
|
|
908
|
+
name: props.name,
|
|
909
|
+
fact: props.fact,
|
|
910
|
+
fact_embedding: (_a = props.fact_embedding) !== null && _a !== void 0 ? _a : null,
|
|
911
|
+
episodes: (_b = props.episodes) !== null && _b !== void 0 ? _b : [],
|
|
912
|
+
valid_at: (_c = props.valid_at) !== null && _c !== void 0 ? _c : null,
|
|
913
|
+
invalid_at: (_d = props.invalid_at) !== null && _d !== void 0 ? _d : null,
|
|
914
|
+
expired_at: (_e = props.expired_at) !== null && _e !== void 0 ? _e : null,
|
|
915
|
+
attributes: this.parseJsonField(props.attributes, {}),
|
|
916
|
+
created_at: props.created_at,
|
|
917
|
+
updated_at: props.updated_at,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
recordToEpisode(props) {
|
|
921
|
+
var _a, _b;
|
|
922
|
+
return {
|
|
923
|
+
uuid: props.uuid,
|
|
924
|
+
group_id: props.group_id,
|
|
925
|
+
content: props.content,
|
|
926
|
+
role: props.role,
|
|
927
|
+
source_type: (_a = props.source_type) !== null && _a !== void 0 ? _a : 'message',
|
|
928
|
+
reference_time: props.reference_time,
|
|
929
|
+
previous_episode_uuid: (_b = props.previous_episode_uuid) !== null && _b !== void 0 ? _b : null,
|
|
930
|
+
created_at: props.created_at,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
parseJsonField(value, fallback) {
|
|
934
|
+
if (typeof value === 'string') {
|
|
935
|
+
try {
|
|
936
|
+
return JSON.parse(value);
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
return fallback;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (typeof value === 'object' && value !== null) {
|
|
943
|
+
return value;
|
|
944
|
+
}
|
|
945
|
+
return fallback;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
exports.Neo4jStorage = Neo4jStorage;
|
|
949
|
+
//# sourceMappingURL=Neo4jStorage.js.map
|