@engram-mem/graph 0.1.1
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/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +20 -0
- package/dist/config.js.map +1 -0
- package/dist/context-extractors.d.ts +15 -0
- package/dist/context-extractors.d.ts.map +1 -0
- package/dist/context-extractors.js +206 -0
- package/dist/context-extractors.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/neural-graph.d.ts +151 -0
- package/dist/neural-graph.d.ts.map +1 -0
- package/dist/neural-graph.js +893 -0
- package/dist/neural-graph.js.map +1 -0
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +23 -0
- package/dist/schema.js.map +1 -0
- package/dist/spreading-activation.d.ts +9 -0
- package/dist/spreading-activation.d.ts.map +1 -0
- package/dist/spreading-activation.js +108 -0
- package/dist/spreading-activation.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
import neo4j from 'neo4j-driver';
|
|
2
|
+
import { ALL_SCHEMA_STATEMENTS } from './schema.js';
|
|
3
|
+
import { SpreadingActivation } from './spreading-activation.js';
|
|
4
|
+
import { extractPersons, classifyEmotion } from './context-extractors.js';
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Neo4j Integer Conversion
|
|
7
|
+
// ============================================================================
|
|
8
|
+
function toNativeProperties(props) {
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const [key, value] of Object.entries(props)) {
|
|
11
|
+
if (neo4j.isInt(value)) {
|
|
12
|
+
result[key] = value.toNumber();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
result[key] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// ID Generation Helpers
|
|
22
|
+
// ============================================================================
|
|
23
|
+
function normalizeForId(name) {
|
|
24
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/(^_|_$)/g, '');
|
|
25
|
+
}
|
|
26
|
+
function getYearWeek(date) {
|
|
27
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
28
|
+
const dayNum = d.getUTCDay() || 7;
|
|
29
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
30
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
31
|
+
const weekNum = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
32
|
+
return `${d.getUTCFullYear()}-W${String(weekNum).padStart(2, '0')}`;
|
|
33
|
+
}
|
|
34
|
+
function getDayOfWeek(date) {
|
|
35
|
+
const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
36
|
+
return days[date.getDay()];
|
|
37
|
+
}
|
|
38
|
+
function getTimeOfDay(date) {
|
|
39
|
+
const hour = date.getHours();
|
|
40
|
+
if (hour >= 6 && hour < 12)
|
|
41
|
+
return 'morning';
|
|
42
|
+
if (hour >= 12 && hour < 17)
|
|
43
|
+
return 'afternoon';
|
|
44
|
+
if (hour >= 17 && hour < 21)
|
|
45
|
+
return 'evening';
|
|
46
|
+
return 'night';
|
|
47
|
+
}
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// NeuralGraph
|
|
50
|
+
// ============================================================================
|
|
51
|
+
export class NeuralGraph {
|
|
52
|
+
driver;
|
|
53
|
+
constructor(config) {
|
|
54
|
+
this.driver = neo4j.driver(config.neo4jUri, neo4j.auth.basic(config.neo4jUser, config.neo4jPassword));
|
|
55
|
+
}
|
|
56
|
+
// --------------------------------------------------------------------------
|
|
57
|
+
// Lifecycle
|
|
58
|
+
// --------------------------------------------------------------------------
|
|
59
|
+
async initialize() {
|
|
60
|
+
await this.driver.verifyConnectivity();
|
|
61
|
+
const session = this.driver.session();
|
|
62
|
+
try {
|
|
63
|
+
for (const statement of ALL_SCHEMA_STATEMENTS) {
|
|
64
|
+
await session.run(statement);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await session.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async dispose() {
|
|
72
|
+
await this.driver.close();
|
|
73
|
+
}
|
|
74
|
+
// --------------------------------------------------------------------------
|
|
75
|
+
// Node Operations (all use MERGE for idempotency)
|
|
76
|
+
// --------------------------------------------------------------------------
|
|
77
|
+
async addMemoryNode(input) {
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
const session = this.driver.session();
|
|
80
|
+
try {
|
|
81
|
+
await session.executeWrite(async (tx) => {
|
|
82
|
+
await tx.run(`MERGE (m:Memory {id: $id})
|
|
83
|
+
ON CREATE SET
|
|
84
|
+
m.memoryType = $memoryType,
|
|
85
|
+
m.label = $label,
|
|
86
|
+
m.projectId = $projectId,
|
|
87
|
+
m.createdAt = $now,
|
|
88
|
+
m.lastAccessed = $now,
|
|
89
|
+
m.activationCount = 0
|
|
90
|
+
ON MATCH SET
|
|
91
|
+
m.lastAccessed = $now,
|
|
92
|
+
m.activationCount = m.activationCount + 1`, {
|
|
93
|
+
id: input.id,
|
|
94
|
+
memoryType: input.memoryType,
|
|
95
|
+
label: input.label.slice(0, 100),
|
|
96
|
+
projectId: input.projectId ?? null,
|
|
97
|
+
now,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return input.id;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await session.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async addPersonNode(input) {
|
|
107
|
+
const id = `person:${normalizeForId(input.name)}`;
|
|
108
|
+
const now = new Date().toISOString();
|
|
109
|
+
const aliases = JSON.stringify(input.aliases ?? []);
|
|
110
|
+
const session = this.driver.session();
|
|
111
|
+
try {
|
|
112
|
+
await session.executeWrite(async (tx) => {
|
|
113
|
+
await tx.run(`MERGE (p:Person {id: $id})
|
|
114
|
+
ON CREATE SET
|
|
115
|
+
p.name = $name,
|
|
116
|
+
p.aliases = $aliases,
|
|
117
|
+
p.firstSeen = $now,
|
|
118
|
+
p.lastSeen = $now,
|
|
119
|
+
p.createdAt = $now,
|
|
120
|
+
p.lastAccessed = $now,
|
|
121
|
+
p.activationCount = 0
|
|
122
|
+
ON MATCH SET
|
|
123
|
+
p.lastSeen = $now,
|
|
124
|
+
p.lastAccessed = $now,
|
|
125
|
+
p.activationCount = p.activationCount + 1`, { id, name: input.name, aliases, now });
|
|
126
|
+
});
|
|
127
|
+
return id;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await session.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async addTopicNode(input) {
|
|
134
|
+
const id = `topic:${normalizeForId(input.name)}`;
|
|
135
|
+
const now = new Date().toISOString();
|
|
136
|
+
const session = this.driver.session();
|
|
137
|
+
try {
|
|
138
|
+
await session.executeWrite(async (tx) => {
|
|
139
|
+
await tx.run(`MERGE (t:Topic {id: $id})
|
|
140
|
+
ON CREATE SET
|
|
141
|
+
t.name = $name,
|
|
142
|
+
t.description = $description,
|
|
143
|
+
t.createdAt = $now,
|
|
144
|
+
t.lastAccessed = $now,
|
|
145
|
+
t.activationCount = 0
|
|
146
|
+
ON MATCH SET
|
|
147
|
+
t.lastAccessed = $now,
|
|
148
|
+
t.activationCount = t.activationCount + 1`, { id, name: input.name, description: input.description ?? null, now });
|
|
149
|
+
});
|
|
150
|
+
return id;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await session.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async addEntityNode(input) {
|
|
157
|
+
const id = `entity:${normalizeForId(input.name)}`;
|
|
158
|
+
const now = new Date().toISOString();
|
|
159
|
+
const session = this.driver.session();
|
|
160
|
+
try {
|
|
161
|
+
await session.executeWrite(async (tx) => {
|
|
162
|
+
await tx.run(`MERGE (e:Entity {id: $id})
|
|
163
|
+
ON CREATE SET
|
|
164
|
+
e.name = $name,
|
|
165
|
+
e.entityType = $entityType,
|
|
166
|
+
e.createdAt = $now,
|
|
167
|
+
e.lastAccessed = $now,
|
|
168
|
+
e.activationCount = 0
|
|
169
|
+
ON MATCH SET
|
|
170
|
+
e.lastAccessed = $now,
|
|
171
|
+
e.activationCount = e.activationCount + 1`, { id, name: input.name, entityType: input.entityType, now });
|
|
172
|
+
});
|
|
173
|
+
return id;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
await session.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async addEmotionNode(input) {
|
|
180
|
+
const scoped = input.sessionScoped !== false;
|
|
181
|
+
const id = scoped
|
|
182
|
+
? `emotion:${input.sessionId}:${input.label}`
|
|
183
|
+
: `emotion:global:${input.label}`;
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const session = this.driver.session();
|
|
186
|
+
try {
|
|
187
|
+
await session.executeWrite(async (tx) => {
|
|
188
|
+
await tx.run(`MERGE (e:Emotion {id: $id})
|
|
189
|
+
ON CREATE SET
|
|
190
|
+
e.label = $label,
|
|
191
|
+
e.intensity = $intensity,
|
|
192
|
+
e.sessionScoped = $sessionScoped,
|
|
193
|
+
e.sessionId = $sessionId,
|
|
194
|
+
e.createdAt = $now,
|
|
195
|
+
e.lastAccessed = $now,
|
|
196
|
+
e.activationCount = 0
|
|
197
|
+
ON MATCH SET
|
|
198
|
+
e.intensity = CASE WHEN $intensity > e.intensity THEN $intensity ELSE e.intensity END,
|
|
199
|
+
e.lastAccessed = $now,
|
|
200
|
+
e.activationCount = e.activationCount + 1`, {
|
|
201
|
+
id,
|
|
202
|
+
label: input.label,
|
|
203
|
+
intensity: input.intensity,
|
|
204
|
+
sessionScoped: scoped,
|
|
205
|
+
sessionId: scoped ? input.sessionId : null,
|
|
206
|
+
now,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
return id;
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await session.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async addIntentNode(input) {
|
|
216
|
+
const scoped = input.sessionScoped !== false;
|
|
217
|
+
const id = scoped
|
|
218
|
+
? `intent:${input.sessionId}:${input.intentType}`
|
|
219
|
+
: `intent:global:${input.intentType}`;
|
|
220
|
+
const now = new Date().toISOString();
|
|
221
|
+
const session = this.driver.session();
|
|
222
|
+
try {
|
|
223
|
+
await session.executeWrite(async (tx) => {
|
|
224
|
+
await tx.run(`MERGE (i:Intent {id: $id})
|
|
225
|
+
ON CREATE SET
|
|
226
|
+
i.intentType = $intentType,
|
|
227
|
+
i.sessionScoped = $sessionScoped,
|
|
228
|
+
i.sessionId = $sessionId,
|
|
229
|
+
i.createdAt = $now,
|
|
230
|
+
i.lastAccessed = $now,
|
|
231
|
+
i.activationCount = 0
|
|
232
|
+
ON MATCH SET
|
|
233
|
+
i.lastAccessed = $now,
|
|
234
|
+
i.activationCount = i.activationCount + 1`, {
|
|
235
|
+
id,
|
|
236
|
+
intentType: input.intentType,
|
|
237
|
+
sessionScoped: scoped,
|
|
238
|
+
sessionId: scoped ? input.sessionId : null,
|
|
239
|
+
now,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
return id;
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await session.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async addSessionNode(input) {
|
|
249
|
+
const now = new Date().toISOString();
|
|
250
|
+
const session = this.driver.session();
|
|
251
|
+
try {
|
|
252
|
+
await session.executeWrite(async (tx) => {
|
|
253
|
+
await tx.run(`MERGE (s:Session {sessionId: $sessionId})
|
|
254
|
+
ON CREATE SET
|
|
255
|
+
s.id = $sessionId,
|
|
256
|
+
s.startTime = $startTime,
|
|
257
|
+
s.endTime = $endTime,
|
|
258
|
+
s.createdAt = $now,
|
|
259
|
+
s.lastAccessed = $now,
|
|
260
|
+
s.activationCount = 0
|
|
261
|
+
ON MATCH SET
|
|
262
|
+
s.endTime = COALESCE($endTime, s.endTime),
|
|
263
|
+
s.lastAccessed = $now,
|
|
264
|
+
s.activationCount = s.activationCount + 1`, {
|
|
265
|
+
sessionId: input.sessionId,
|
|
266
|
+
startTime: input.startTime,
|
|
267
|
+
endTime: input.endTime ?? null,
|
|
268
|
+
now,
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
return input.sessionId;
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
await session.close();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async addTimeContextNode(input) {
|
|
278
|
+
const yearWeek = getYearWeek(input.timestamp);
|
|
279
|
+
const dayOfWeek = getDayOfWeek(input.timestamp);
|
|
280
|
+
const timeOfDay = getTimeOfDay(input.timestamp);
|
|
281
|
+
const id = `time:${yearWeek}:${dayOfWeek}:${timeOfDay}`;
|
|
282
|
+
const now = new Date().toISOString();
|
|
283
|
+
const session = this.driver.session();
|
|
284
|
+
try {
|
|
285
|
+
await session.executeWrite(async (tx) => {
|
|
286
|
+
await tx.run(`MERGE (t:TimeContext {id: $id})
|
|
287
|
+
ON CREATE SET
|
|
288
|
+
t.yearWeek = $yearWeek,
|
|
289
|
+
t.dayOfWeek = $dayOfWeek,
|
|
290
|
+
t.timeOfDay = $timeOfDay,
|
|
291
|
+
t.createdAt = $now,
|
|
292
|
+
t.lastAccessed = $now,
|
|
293
|
+
t.activationCount = 0
|
|
294
|
+
ON MATCH SET
|
|
295
|
+
t.lastAccessed = $now,
|
|
296
|
+
t.activationCount = t.activationCount + 1`, { id, yearWeek, dayOfWeek, timeOfDay, now });
|
|
297
|
+
});
|
|
298
|
+
return id;
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
await session.close();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// --------------------------------------------------------------------------
|
|
305
|
+
// Edge Operations
|
|
306
|
+
// --------------------------------------------------------------------------
|
|
307
|
+
async addEdge(sourceId, targetId, type, weight) {
|
|
308
|
+
const now = new Date().toISOString();
|
|
309
|
+
const session = this.driver.session();
|
|
310
|
+
try {
|
|
311
|
+
await session.executeWrite(async (tx) => {
|
|
312
|
+
await tx.run(`MATCH (source) WHERE source.id = $sourceId
|
|
313
|
+
MATCH (target) WHERE target.id = $targetId
|
|
314
|
+
MERGE (source)-[r:${type}]->(target)
|
|
315
|
+
ON CREATE SET
|
|
316
|
+
r.weight = $weight,
|
|
317
|
+
r.createdAt = $now,
|
|
318
|
+
r.lastTraversed = $now,
|
|
319
|
+
r.traversalCount = 1
|
|
320
|
+
ON MATCH SET
|
|
321
|
+
r.weight = CASE WHEN $weight > r.weight THEN $weight ELSE r.weight END,
|
|
322
|
+
r.lastTraversed = $now,
|
|
323
|
+
r.traversalCount = r.traversalCount + 1`, { sourceId, targetId, weight, now });
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
await session.close();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// --------------------------------------------------------------------------
|
|
331
|
+
// Query Operations
|
|
332
|
+
// --------------------------------------------------------------------------
|
|
333
|
+
async getNode(id) {
|
|
334
|
+
const session = this.driver.session();
|
|
335
|
+
try {
|
|
336
|
+
const result = await session.executeRead(async (tx) => {
|
|
337
|
+
return tx.run(`MATCH (n) WHERE n.id = $id
|
|
338
|
+
RETURN n, labels(n)[0] AS label`, { id });
|
|
339
|
+
});
|
|
340
|
+
if (result.records.length === 0)
|
|
341
|
+
return null;
|
|
342
|
+
const record = result.records[0];
|
|
343
|
+
const node = record.get('n');
|
|
344
|
+
return {
|
|
345
|
+
id,
|
|
346
|
+
label: record.get('label'),
|
|
347
|
+
properties: toNativeProperties(node.properties),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
await session.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async getNeighbors(id, opts) {
|
|
355
|
+
const direction = opts?.direction ?? 'both';
|
|
356
|
+
const limit = opts?.limit ?? 50;
|
|
357
|
+
let pattern;
|
|
358
|
+
if (direction === 'out') {
|
|
359
|
+
pattern = opts?.edgeType
|
|
360
|
+
? `(source)-[r:${opts.edgeType}]->(neighbor)`
|
|
361
|
+
: '(source)-[r]->(neighbor)';
|
|
362
|
+
}
|
|
363
|
+
else if (direction === 'in') {
|
|
364
|
+
pattern = opts?.edgeType
|
|
365
|
+
? `(source)<-[r:${opts.edgeType}]-(neighbor)`
|
|
366
|
+
: '(source)<-[r]-(neighbor)';
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
pattern = opts?.edgeType
|
|
370
|
+
? `(source)-[r:${opts.edgeType}]-(neighbor)`
|
|
371
|
+
: '(source)-[r]-(neighbor)';
|
|
372
|
+
}
|
|
373
|
+
const session = this.driver.session();
|
|
374
|
+
try {
|
|
375
|
+
const result = await session.executeRead(async (tx) => {
|
|
376
|
+
return tx.run(`MATCH (source) WHERE source.id = $id
|
|
377
|
+
MATCH ${pattern}
|
|
378
|
+
RETURN neighbor, labels(neighbor)[0] AS label, r.weight AS weight
|
|
379
|
+
ORDER BY r.weight DESC
|
|
380
|
+
LIMIT $limit`, { id, limit: neo4j.int(limit) });
|
|
381
|
+
});
|
|
382
|
+
return result.records.map(record => {
|
|
383
|
+
const weight = record.get('weight');
|
|
384
|
+
return {
|
|
385
|
+
id: record.get('neighbor').properties.id,
|
|
386
|
+
label: record.get('label'),
|
|
387
|
+
properties: toNativeProperties(record.get('neighbor').properties),
|
|
388
|
+
edgeWeight: neo4j.isInt(weight) ? weight.toNumber() : weight ?? 0,
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
await session.close();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// --------------------------------------------------------------------------
|
|
397
|
+
// Bulk Operations
|
|
398
|
+
// --------------------------------------------------------------------------
|
|
399
|
+
async decomposeEpisode(input) {
|
|
400
|
+
const now = new Date().toISOString();
|
|
401
|
+
const yearWeek = getYearWeek(input.timestamp);
|
|
402
|
+
const dayOfWeek = getDayOfWeek(input.timestamp);
|
|
403
|
+
const timeOfDay = getTimeOfDay(input.timestamp);
|
|
404
|
+
const timeContextId = `time:${yearWeek}:${dayOfWeek}:${timeOfDay}`;
|
|
405
|
+
const session = this.driver.session();
|
|
406
|
+
try {
|
|
407
|
+
await session.executeWrite(async (tx) => {
|
|
408
|
+
// 1. Memory node
|
|
409
|
+
await tx.run(`MERGE (m:Memory {id: $id})
|
|
410
|
+
ON CREATE SET
|
|
411
|
+
m.memoryType = $memoryType,
|
|
412
|
+
m.label = $label,
|
|
413
|
+
m.projectId = $projectId,
|
|
414
|
+
m.createdAt = $now,
|
|
415
|
+
m.lastAccessed = $now,
|
|
416
|
+
m.activationCount = 0
|
|
417
|
+
ON MATCH SET
|
|
418
|
+
m.lastAccessed = $now,
|
|
419
|
+
m.activationCount = m.activationCount + 1`, {
|
|
420
|
+
id: input.episodeId,
|
|
421
|
+
memoryType: input.memoryType,
|
|
422
|
+
label: input.label.slice(0, 100),
|
|
423
|
+
projectId: input.projectId ?? null,
|
|
424
|
+
now,
|
|
425
|
+
});
|
|
426
|
+
// 2. Session node + OCCURRED_IN edge
|
|
427
|
+
await tx.run(`MERGE (s:Session {sessionId: $sessionId})
|
|
428
|
+
ON CREATE SET
|
|
429
|
+
s.id = $sessionId,
|
|
430
|
+
s.startTime = $now,
|
|
431
|
+
s.createdAt = $now,
|
|
432
|
+
s.lastAccessed = $now,
|
|
433
|
+
s.activationCount = 0
|
|
434
|
+
ON MATCH SET
|
|
435
|
+
s.lastAccessed = $now
|
|
436
|
+
WITH s
|
|
437
|
+
MATCH (m:Memory {id: $memoryId})
|
|
438
|
+
MERGE (m)-[r:OCCURRED_IN]->(s)
|
|
439
|
+
ON CREATE SET
|
|
440
|
+
r.weight = 1.0,
|
|
441
|
+
r.createdAt = $now,
|
|
442
|
+
r.lastTraversed = $now,
|
|
443
|
+
r.traversalCount = 1`, { sessionId: input.sessionId, memoryId: input.episodeId, now });
|
|
444
|
+
// 3. TimeContext node + OCCURRED_AT edge
|
|
445
|
+
await tx.run(`MERGE (t:TimeContext {id: $timeContextId})
|
|
446
|
+
ON CREATE SET
|
|
447
|
+
t.yearWeek = $yearWeek,
|
|
448
|
+
t.dayOfWeek = $dayOfWeek,
|
|
449
|
+
t.timeOfDay = $timeOfDay,
|
|
450
|
+
t.createdAt = $now,
|
|
451
|
+
t.lastAccessed = $now,
|
|
452
|
+
t.activationCount = 0
|
|
453
|
+
ON MATCH SET
|
|
454
|
+
t.lastAccessed = $now
|
|
455
|
+
WITH t
|
|
456
|
+
MATCH (m:Memory {id: $memoryId})
|
|
457
|
+
MERGE (m)-[r:OCCURRED_AT]->(t)
|
|
458
|
+
ON CREATE SET
|
|
459
|
+
r.weight = 0.5,
|
|
460
|
+
r.createdAt = $now,
|
|
461
|
+
r.lastTraversed = $now,
|
|
462
|
+
r.traversalCount = 1`, { timeContextId, yearWeek, dayOfWeek, timeOfDay, memoryId: input.episodeId, now });
|
|
463
|
+
// 4. Person nodes + SPOKE edges
|
|
464
|
+
for (const personName of input.persons) {
|
|
465
|
+
const personId = `person:${normalizeForId(personName)}`;
|
|
466
|
+
await tx.run(`MERGE (p:Person {id: $personId})
|
|
467
|
+
ON CREATE SET
|
|
468
|
+
p.name = $name,
|
|
469
|
+
p.aliases = '[]',
|
|
470
|
+
p.firstSeen = $now,
|
|
471
|
+
p.lastSeen = $now,
|
|
472
|
+
p.createdAt = $now,
|
|
473
|
+
p.lastAccessed = $now,
|
|
474
|
+
p.activationCount = 0
|
|
475
|
+
ON MATCH SET
|
|
476
|
+
p.lastSeen = $now,
|
|
477
|
+
p.lastAccessed = $now
|
|
478
|
+
WITH p
|
|
479
|
+
MATCH (m:Memory {id: $memoryId})
|
|
480
|
+
MERGE (m)-[r:SPOKE]->(p)
|
|
481
|
+
ON CREATE SET
|
|
482
|
+
r.weight = 0.7,
|
|
483
|
+
r.createdAt = $now,
|
|
484
|
+
r.lastTraversed = $now,
|
|
485
|
+
r.traversalCount = 1
|
|
486
|
+
ON MATCH SET
|
|
487
|
+
r.lastTraversed = $now,
|
|
488
|
+
r.traversalCount = r.traversalCount + 1`, { personId, name: personName, memoryId: input.episodeId, now });
|
|
489
|
+
}
|
|
490
|
+
// 5. Entity nodes + CONTEXTUAL edges
|
|
491
|
+
for (const entity of input.entities) {
|
|
492
|
+
const entityId = `entity:${normalizeForId(entity.name)}`;
|
|
493
|
+
await tx.run(`MERGE (e:Entity {id: $entityId})
|
|
494
|
+
ON CREATE SET
|
|
495
|
+
e.name = $name,
|
|
496
|
+
e.entityType = $entityType,
|
|
497
|
+
e.createdAt = $now,
|
|
498
|
+
e.lastAccessed = $now,
|
|
499
|
+
e.activationCount = 0
|
|
500
|
+
ON MATCH SET
|
|
501
|
+
e.lastAccessed = $now
|
|
502
|
+
WITH e
|
|
503
|
+
MATCH (m:Memory {id: $memoryId})
|
|
504
|
+
MERGE (m)-[r:CONTEXTUAL]->(e)
|
|
505
|
+
ON CREATE SET
|
|
506
|
+
r.weight = 0.6,
|
|
507
|
+
r.createdAt = $now,
|
|
508
|
+
r.lastTraversed = $now,
|
|
509
|
+
r.traversalCount = 1
|
|
510
|
+
ON MATCH SET
|
|
511
|
+
r.lastTraversed = $now,
|
|
512
|
+
r.traversalCount = r.traversalCount + 1`, { entityId, name: entity.name, entityType: entity.entityType, memoryId: input.episodeId, now });
|
|
513
|
+
}
|
|
514
|
+
// 6. Emotion node + EMOTIONAL edge
|
|
515
|
+
if (input.emotion) {
|
|
516
|
+
const emotionId = `emotion:${input.sessionId}:${input.emotion.label}`;
|
|
517
|
+
await tx.run(`MERGE (e:Emotion {id: $emotionId})
|
|
518
|
+
ON CREATE SET
|
|
519
|
+
e.label = $label,
|
|
520
|
+
e.intensity = $intensity,
|
|
521
|
+
e.sessionScoped = true,
|
|
522
|
+
e.sessionId = $sessionId,
|
|
523
|
+
e.createdAt = $now,
|
|
524
|
+
e.lastAccessed = $now,
|
|
525
|
+
e.activationCount = 0
|
|
526
|
+
ON MATCH SET
|
|
527
|
+
e.intensity = CASE WHEN $intensity > e.intensity THEN $intensity ELSE e.intensity END,
|
|
528
|
+
e.lastAccessed = $now
|
|
529
|
+
WITH e
|
|
530
|
+
MATCH (m:Memory {id: $memoryId})
|
|
531
|
+
MERGE (m)-[r:EMOTIONAL]->(e)
|
|
532
|
+
ON CREATE SET
|
|
533
|
+
r.weight = $intensity,
|
|
534
|
+
r.createdAt = $now,
|
|
535
|
+
r.lastTraversed = $now,
|
|
536
|
+
r.traversalCount = 1`, {
|
|
537
|
+
emotionId,
|
|
538
|
+
label: input.emotion.label,
|
|
539
|
+
intensity: input.emotion.intensity,
|
|
540
|
+
sessionId: input.sessionId,
|
|
541
|
+
memoryId: input.episodeId,
|
|
542
|
+
now,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
// 7. Intent node + INTENTIONAL edge
|
|
546
|
+
if (input.intent) {
|
|
547
|
+
const intentId = `intent:${input.sessionId}:${input.intent}`;
|
|
548
|
+
await tx.run(`MERGE (i:Intent {id: $intentId})
|
|
549
|
+
ON CREATE SET
|
|
550
|
+
i.intentType = $intentType,
|
|
551
|
+
i.sessionScoped = true,
|
|
552
|
+
i.sessionId = $sessionId,
|
|
553
|
+
i.createdAt = $now,
|
|
554
|
+
i.lastAccessed = $now,
|
|
555
|
+
i.activationCount = 0
|
|
556
|
+
ON MATCH SET
|
|
557
|
+
i.lastAccessed = $now
|
|
558
|
+
WITH i
|
|
559
|
+
MATCH (m:Memory {id: $memoryId})
|
|
560
|
+
MERGE (m)-[r:INTENTIONAL]->(i)
|
|
561
|
+
ON CREATE SET
|
|
562
|
+
r.weight = 0.5,
|
|
563
|
+
r.createdAt = $now,
|
|
564
|
+
r.lastTraversed = $now,
|
|
565
|
+
r.traversalCount = 1`, {
|
|
566
|
+
intentId,
|
|
567
|
+
intentType: input.intent,
|
|
568
|
+
sessionId: input.sessionId,
|
|
569
|
+
memoryId: input.episodeId,
|
|
570
|
+
now,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
await session.close();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// --------------------------------------------------------------------------
|
|
580
|
+
// Schema Management
|
|
581
|
+
// --------------------------------------------------------------------------
|
|
582
|
+
async ensureSchema() {
|
|
583
|
+
const session = this.driver.session();
|
|
584
|
+
try {
|
|
585
|
+
for (const statement of ALL_SCHEMA_STATEMENTS) {
|
|
586
|
+
await session.run(statement);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
await session.close();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// --------------------------------------------------------------------------
|
|
594
|
+
// Health
|
|
595
|
+
// --------------------------------------------------------------------------
|
|
596
|
+
async ping() {
|
|
597
|
+
try {
|
|
598
|
+
await this.driver.verifyConnectivity();
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// --------------------------------------------------------------------------
|
|
606
|
+
// Stats
|
|
607
|
+
// --------------------------------------------------------------------------
|
|
608
|
+
async stats() {
|
|
609
|
+
const session = this.driver.session();
|
|
610
|
+
try {
|
|
611
|
+
const nodeCountResult = await session.run(`MATCH (n)
|
|
612
|
+
UNWIND labels(n) AS label
|
|
613
|
+
RETURN label, count(n) AS count
|
|
614
|
+
ORDER BY label`);
|
|
615
|
+
const relCountResult = await session.run(`MATCH ()-[r]->()
|
|
616
|
+
RETURN type(r) AS type, count(r) AS count
|
|
617
|
+
ORDER BY type`);
|
|
618
|
+
const nodes = {};
|
|
619
|
+
for (const record of nodeCountResult.records) {
|
|
620
|
+
const label = record.get('label');
|
|
621
|
+
const count = record.get('count').toNumber();
|
|
622
|
+
nodes[label] = count;
|
|
623
|
+
}
|
|
624
|
+
const relationships = {};
|
|
625
|
+
for (const record of relCountResult.records) {
|
|
626
|
+
const type = record.get('type');
|
|
627
|
+
const count = record.get('count').toNumber();
|
|
628
|
+
relationships[type] = count;
|
|
629
|
+
}
|
|
630
|
+
const totalNodes = Object.values(nodes).reduce((a, b) => a + b, 0);
|
|
631
|
+
const totalRels = Object.values(relationships).reduce((a, b) => a + b, 0);
|
|
632
|
+
return { nodes, relationships, total: { nodes: totalNodes, relationships: totalRels } };
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
await session.close();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// --------------------------------------------------------------------------
|
|
639
|
+
// Cleanup (for testing)
|
|
640
|
+
// --------------------------------------------------------------------------
|
|
641
|
+
async clearAll() {
|
|
642
|
+
const session = this.driver.session();
|
|
643
|
+
try {
|
|
644
|
+
let deleted = 1;
|
|
645
|
+
while (deleted > 0) {
|
|
646
|
+
const result = await session.run(`MATCH (n) WITH n LIMIT 10000
|
|
647
|
+
DETACH DELETE n
|
|
648
|
+
RETURN count(*) AS deleted`);
|
|
649
|
+
deleted = result.records[0]?.get('deleted')?.toNumber() ?? 0;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
await session.close();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
// --------------------------------------------------------------------------
|
|
657
|
+
// Wave 2 Facade: connectivity, entity lookup, spreading activation,
|
|
658
|
+
// simplified ingestion, edge strengthening. These wrap lower-level methods
|
|
659
|
+
// with the shape Wave 2's retrieval pipeline expects.
|
|
660
|
+
// --------------------------------------------------------------------------
|
|
661
|
+
/**
|
|
662
|
+
* Availability check — alias for ping(). Returns true iff Neo4j is reachable
|
|
663
|
+
* and responsive. Does NOT throw.
|
|
664
|
+
*/
|
|
665
|
+
async isAvailable() {
|
|
666
|
+
return this.ping();
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Look up Person, Entity, and Topic nodes by name. Case-insensitive match
|
|
670
|
+
* on Entity and Topic; exact match on Person.name.
|
|
671
|
+
*
|
|
672
|
+
* Used by Wave 2's entity-based seed injection: extract names from the
|
|
673
|
+
* query, pass them here, feed the resulting node IDs into spreading
|
|
674
|
+
* activation as independent graph seeds.
|
|
675
|
+
*/
|
|
676
|
+
async lookupEntityNodes(names) {
|
|
677
|
+
if (names.length === 0)
|
|
678
|
+
return [];
|
|
679
|
+
const session = this.driver.session();
|
|
680
|
+
try {
|
|
681
|
+
const result = await session.executeRead(async (tx) => {
|
|
682
|
+
return tx.run(`UNWIND $names AS name
|
|
683
|
+
MATCH (n)
|
|
684
|
+
WHERE (n:Person AND n.name = name)
|
|
685
|
+
OR (n:Entity AND toLower(n.name) = toLower(name))
|
|
686
|
+
OR (n:Topic AND toLower(n.name) = toLower(name))
|
|
687
|
+
RETURN DISTINCT n.id AS nodeId,
|
|
688
|
+
labels(n)[0] AS nodeType,
|
|
689
|
+
n.name AS name`, { names });
|
|
690
|
+
});
|
|
691
|
+
return result.records.map((record) => ({
|
|
692
|
+
nodeId: record.get('nodeId'),
|
|
693
|
+
nodeType: record.get('nodeType'),
|
|
694
|
+
name: record.get('name'),
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
// Non-fatal: caller treats an empty result the same as "no entities"
|
|
699
|
+
return [];
|
|
700
|
+
}
|
|
701
|
+
finally {
|
|
702
|
+
await session.close();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Wave 2 spreading activation facade. Wraps the standalone
|
|
707
|
+
* SpreadingActivation class with the option shape Wave 2 uses.
|
|
708
|
+
*
|
|
709
|
+
* Note: seedActivations is accepted for forward compatibility but the
|
|
710
|
+
* underlying Cypher query uses uniform activation (1.0) per seed.
|
|
711
|
+
* Per-seed initial weights are a Wave 3 enhancement.
|
|
712
|
+
*/
|
|
713
|
+
async spreadActivation(opts) {
|
|
714
|
+
const sa = new SpreadingActivation(this.driver);
|
|
715
|
+
const params = {
|
|
716
|
+
maxHops: opts.maxHops ?? 2,
|
|
717
|
+
decayPerHop: opts.decay ?? 0.6,
|
|
718
|
+
minActivation: opts.threshold ?? 0.05,
|
|
719
|
+
maxNodes: opts.budget ?? 100,
|
|
720
|
+
edgeTypeFilter: (opts.edgeFilter ?? []),
|
|
721
|
+
};
|
|
722
|
+
const results = await sa.activate(opts.seedNodeIds, params);
|
|
723
|
+
return results.map((r) => ({
|
|
724
|
+
nodeId: r.nodeId,
|
|
725
|
+
nodeType: r.nodeType,
|
|
726
|
+
activation: r.activation,
|
|
727
|
+
depth: r.hops,
|
|
728
|
+
properties: r.properties,
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Strengthen a set of directed edge pairs (sourceId, targetId).
|
|
733
|
+
* For each pair, increments weight by 0.02 (capped at 1.0), bumps
|
|
734
|
+
* traversalCount, and updates lastTraversed.
|
|
735
|
+
*
|
|
736
|
+
* This is the reconsolidation analog: edges we actually traversed
|
|
737
|
+
* during recall get slightly stronger, making future retrieval faster.
|
|
738
|
+
*/
|
|
739
|
+
async strengthenTraversedEdges(pairs) {
|
|
740
|
+
if (pairs.length === 0)
|
|
741
|
+
return;
|
|
742
|
+
const now = new Date().toISOString();
|
|
743
|
+
const session = this.driver.session();
|
|
744
|
+
try {
|
|
745
|
+
await session.executeWrite(async (tx) => {
|
|
746
|
+
await tx.run(`UNWIND $pairs AS pair
|
|
747
|
+
MATCH (a)-[r]->(b)
|
|
748
|
+
WHERE a.id = pair[0] AND b.id = pair[1]
|
|
749
|
+
SET r.weight = CASE
|
|
750
|
+
WHEN r.weight + 0.02 > 1.0 THEN 1.0
|
|
751
|
+
ELSE r.weight + 0.02
|
|
752
|
+
END,
|
|
753
|
+
r.lastTraversed = $now,
|
|
754
|
+
r.traversalCount = coalesce(r.traversalCount, 0) + 1`, { pairs, now });
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
await session.close();
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Simplified ingestion path for Wave 2. Takes a core-shaped episode,
|
|
763
|
+
* extracts persons/emotion internally (or uses LLM-extracted entities
|
|
764
|
+
* when provided), and delegates to decomposeEpisode().
|
|
765
|
+
*
|
|
766
|
+
* Also creates the TEMPORAL edge from previousEpisodeId when provided.
|
|
767
|
+
*/
|
|
768
|
+
async ingestEpisode(input) {
|
|
769
|
+
const timestamp = input.createdAt instanceof Date
|
|
770
|
+
? input.createdAt
|
|
771
|
+
: new Date(input.createdAt);
|
|
772
|
+
const emotionResult = classifyEmotion(input.content);
|
|
773
|
+
const emotion = emotionResult.label !== 'neutral'
|
|
774
|
+
? { label: emotionResult.label, intensity: emotionResult.intensity }
|
|
775
|
+
: null;
|
|
776
|
+
const label = input.content.slice(0, 100);
|
|
777
|
+
// Decide the entity source: LLM-extracted (precise) or regex fallback.
|
|
778
|
+
let persons;
|
|
779
|
+
let typedEntities;
|
|
780
|
+
if (input.llmEntities && input.llmEntities.length > 0) {
|
|
781
|
+
// LLM path: use structured typed entities. Higher precision; no
|
|
782
|
+
// regex extraction runs.
|
|
783
|
+
persons = input.llmEntities
|
|
784
|
+
.filter((e) => e.type === 'person')
|
|
785
|
+
.map((e) => e.name);
|
|
786
|
+
typedEntities = input.llmEntities
|
|
787
|
+
.filter((e) => e.type !== 'person')
|
|
788
|
+
.map((e) => {
|
|
789
|
+
// Map LLM types to Wave 1's EntityNodeInput.entityType enum.
|
|
790
|
+
// 'org' collapses to 'concept' because Wave 1 has no 'org' type.
|
|
791
|
+
const entityType = e.type === 'tech' ? 'tech'
|
|
792
|
+
: e.type === 'project' ? 'project'
|
|
793
|
+
: 'concept';
|
|
794
|
+
return { name: e.name, entityType };
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
// Regex fallback path: the old heuristic extractors.
|
|
799
|
+
persons = extractPersons(input.content).map((p) => p.name);
|
|
800
|
+
typedEntities = input.entities.map((name) => ({
|
|
801
|
+
name,
|
|
802
|
+
entityType: 'tech',
|
|
803
|
+
}));
|
|
804
|
+
}
|
|
805
|
+
await this.decomposeEpisode({
|
|
806
|
+
episodeId: input.id,
|
|
807
|
+
memoryType: 'episode',
|
|
808
|
+
label,
|
|
809
|
+
sessionId: input.sessionId,
|
|
810
|
+
timestamp,
|
|
811
|
+
persons,
|
|
812
|
+
entities: typedEntities,
|
|
813
|
+
emotion,
|
|
814
|
+
intent: null,
|
|
815
|
+
});
|
|
816
|
+
// PROJECT edge: tag this memory with its project scope.
|
|
817
|
+
// Uses MERGE on Project.id = 'project:<name>' so project nodes are
|
|
818
|
+
// singletons across the whole graph — all memories from the same
|
|
819
|
+
// project share the node, which means Wave 2 spreading activation
|
|
820
|
+
// from a Project seed naturally pulls in all project memories.
|
|
821
|
+
if (input.project && input.project !== 'global') {
|
|
822
|
+
const projectId = `project:${normalizeForId(input.project)}`;
|
|
823
|
+
const now = new Date().toISOString();
|
|
824
|
+
const session = this.driver.session();
|
|
825
|
+
try {
|
|
826
|
+
await session.executeWrite(async (tx) => {
|
|
827
|
+
await tx.run(`MERGE (p:Project {id: $projectId})
|
|
828
|
+
ON CREATE SET
|
|
829
|
+
p.name = $name,
|
|
830
|
+
p.createdAt = $now,
|
|
831
|
+
p.lastAccessed = $now,
|
|
832
|
+
p.activationCount = 0
|
|
833
|
+
ON MATCH SET
|
|
834
|
+
p.lastAccessed = $now
|
|
835
|
+
WITH p
|
|
836
|
+
MATCH (m:Memory {id: $memoryId})
|
|
837
|
+
MERGE (m)-[r:PROJECT]->(p)
|
|
838
|
+
ON CREATE SET
|
|
839
|
+
r.weight = 1.0,
|
|
840
|
+
r.createdAt = $now,
|
|
841
|
+
r.lastTraversed = $now,
|
|
842
|
+
r.traversalCount = 1
|
|
843
|
+
ON MATCH SET
|
|
844
|
+
r.lastTraversed = $now,
|
|
845
|
+
r.traversalCount = r.traversalCount + 1`, { projectId, name: input.project, memoryId: input.id, now });
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
finally {
|
|
849
|
+
await session.close();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
// TEMPORAL edge from previous episode in the same session.
|
|
853
|
+
//
|
|
854
|
+
// Race condition fix: Memory.ingest() calls ingestEpisode in a
|
|
855
|
+
// fire-and-forget manner, which means episode N+1's decomposition can
|
|
856
|
+
// start before episode N's Memory node finishes being written. The
|
|
857
|
+
// previous implementation used MATCH (prev:Memory {id: ...}) which
|
|
858
|
+
// silently produced no edge when prev didn't exist yet, dropping ~18%
|
|
859
|
+
// of TEMPORAL edges in practice.
|
|
860
|
+
//
|
|
861
|
+
// The fix uses MERGE for BOTH endpoints. If the previous Memory node
|
|
862
|
+
// doesn't exist yet, we create a stub with just the id; its properties
|
|
863
|
+
// will be filled in by ON MATCH when its own decomposeEpisode arrives.
|
|
864
|
+
// This is safe because decomposeEpisode also uses MERGE on Memory {id},
|
|
865
|
+
// so a stub will be populated rather than duplicated.
|
|
866
|
+
if (input.previousEpisodeId) {
|
|
867
|
+
const session = this.driver.session();
|
|
868
|
+
try {
|
|
869
|
+
await session.executeWrite(async (tx) => {
|
|
870
|
+
await tx.run(`MERGE (prev:Memory {id: $prevId})
|
|
871
|
+
MERGE (curr:Memory {id: $currId})
|
|
872
|
+
MERGE (prev)-[r:TEMPORAL]->(curr)
|
|
873
|
+
ON CREATE SET
|
|
874
|
+
r.weight = 0.8,
|
|
875
|
+
r.createdAt = $now,
|
|
876
|
+
r.lastTraversed = $now,
|
|
877
|
+
r.traversalCount = 1
|
|
878
|
+
ON MATCH SET
|
|
879
|
+
r.lastTraversed = $now,
|
|
880
|
+
r.traversalCount = r.traversalCount + 1`, {
|
|
881
|
+
prevId: input.previousEpisodeId,
|
|
882
|
+
currId: input.id,
|
|
883
|
+
now: new Date().toISOString(),
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
finally {
|
|
888
|
+
await session.close();
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
//# sourceMappingURL=neural-graph.js.map
|