@coreidentitylabs/open-graph-memory-mcp 1.0.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/.agents/skills/mcp-builder/LICENSE.txt +202 -0
- package/.agents/skills/mcp-builder/SKILL.md +236 -0
- package/.agents/skills/mcp-builder/reference/evaluation.md +602 -0
- package/.agents/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/.agents/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/.agents/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/.agents/skills/mcp-builder/scripts/connections.py +151 -0
- package/.agents/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/.agents/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/.agents/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/.env.example +26 -0
- package/Implementation Plan.md +358 -0
- package/README.md +187 -0
- package/dist/constants.d.ts +34 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +40 -0
- package/dist/constants.js.map +1 -0
- package/dist/encoding/embedder.d.ts +12 -0
- package/dist/encoding/embedder.d.ts.map +1 -0
- package/dist/encoding/embedder.js +85 -0
- package/dist/encoding/embedder.js.map +1 -0
- package/dist/encoding/pipeline.d.ts +28 -0
- package/dist/encoding/pipeline.d.ts.map +1 -0
- package/dist/encoding/pipeline.js +146 -0
- package/dist/encoding/pipeline.js.map +1 -0
- package/dist/evolution/consolidator.d.ts +12 -0
- package/dist/evolution/consolidator.d.ts.map +1 -0
- package/dist/evolution/consolidator.js +212 -0
- package/dist/evolution/consolidator.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/openai-provider.d.ts +23 -0
- package/dist/llm/openai-provider.d.ts.map +1 -0
- package/dist/llm/openai-provider.js +141 -0
- package/dist/llm/openai-provider.js.map +1 -0
- package/dist/llm/prompts.d.ts +10 -0
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/llm/prompts.js +63 -0
- package/dist/llm/prompts.js.map +1 -0
- package/dist/llm/provider.d.ts +7 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +25 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/resources/context-resource.d.ts +8 -0
- package/dist/resources/context-resource.d.ts.map +1 -0
- package/dist/resources/context-resource.js +51 -0
- package/dist/resources/context-resource.js.map +1 -0
- package/dist/retrieval/search.d.ts +24 -0
- package/dist/retrieval/search.d.ts.map +1 -0
- package/dist/retrieval/search.js +143 -0
- package/dist/retrieval/search.js.map +1 -0
- package/dist/storage/factory.d.ts +10 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/factory.js +35 -0
- package/dist/storage/factory.js.map +1 -0
- package/dist/storage/json-store.d.ts +34 -0
- package/dist/storage/json-store.d.ts.map +1 -0
- package/dist/storage/json-store.js +248 -0
- package/dist/storage/json-store.js.map +1 -0
- package/dist/storage/neo4j-store.d.ts +31 -0
- package/dist/storage/neo4j-store.d.ts.map +1 -0
- package/dist/storage/neo4j-store.js +440 -0
- package/dist/storage/neo4j-store.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +4 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +873 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/implementation_plan.md.resolved.md +322 -0
- package/package.json +43 -0
- package/src/constants.ts +52 -0
- package/src/encoding/embedder.ts +93 -0
- package/src/encoding/pipeline.ts +197 -0
- package/src/evolution/consolidator.ts +281 -0
- package/src/index.ts +67 -0
- package/src/llm/openai-provider.ts +208 -0
- package/src/llm/prompts.ts +66 -0
- package/src/llm/provider.ts +37 -0
- package/src/resources/context-resource.ts +74 -0
- package/src/retrieval/search.ts +203 -0
- package/src/storage/factory.ts +48 -0
- package/src/storage/json-store.ts +325 -0
- package/src/storage/neo4j-store.ts +564 -0
- package/src/tools/memory-tools.ts +1067 -0
- package/src/types.ts +207 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Neo4j Graph Database Storage Backend
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import neo4j, { Driver, Session } from "neo4j-driver";
|
|
6
|
+
import type {
|
|
7
|
+
StorageBackend,
|
|
8
|
+
MemoryNode,
|
|
9
|
+
MemoryEdge,
|
|
10
|
+
ScoredNode,
|
|
11
|
+
Subgraph,
|
|
12
|
+
NodeFilter,
|
|
13
|
+
GraphStats,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
import { cosineSimilarity } from "../encoding/embedder.js";
|
|
16
|
+
|
|
17
|
+
export class Neo4jStore implements StorageBackend {
|
|
18
|
+
private driver: Driver;
|
|
19
|
+
private uri: string;
|
|
20
|
+
private user: string;
|
|
21
|
+
private password: string;
|
|
22
|
+
|
|
23
|
+
constructor(uri: string, user: string, password: string) {
|
|
24
|
+
this.uri = uri;
|
|
25
|
+
this.user = user;
|
|
26
|
+
this.password = password;
|
|
27
|
+
this.driver = neo4j.driver(
|
|
28
|
+
this.uri,
|
|
29
|
+
neo4j.auth.basic(this.user, this.password),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private getSession(): Session {
|
|
34
|
+
return this.driver.session();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async initialize(): Promise<void> {
|
|
38
|
+
const session = this.getSession();
|
|
39
|
+
try {
|
|
40
|
+
// Create constraints and indexes
|
|
41
|
+
await session.run(
|
|
42
|
+
`CREATE CONSTRAINT memory_node_id IF NOT EXISTS FOR (n:MemoryNode) REQUIRE n.id IS UNIQUE`,
|
|
43
|
+
);
|
|
44
|
+
await session.run(
|
|
45
|
+
`CREATE CONSTRAINT memory_edge_id IF NOT EXISTS FOR ()-[r:MEMORY_EDGE]-() REQUIRE r.id IS UNIQUE`,
|
|
46
|
+
);
|
|
47
|
+
await session.run(
|
|
48
|
+
`CREATE INDEX memory_node_name IF NOT EXISTS FOR (n:MemoryNode) ON (n.name)`,
|
|
49
|
+
);
|
|
50
|
+
await session.run(
|
|
51
|
+
`CREATE INDEX memory_node_type IF NOT EXISTS FOR (n:MemoryNode) ON (n.type)`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Verify connection
|
|
55
|
+
const result = await session.run(
|
|
56
|
+
`MATCH (n:MemoryNode) RETURN count(n) AS count`,
|
|
57
|
+
);
|
|
58
|
+
const count = result.records[0]?.get("count")?.toNumber() ?? 0;
|
|
59
|
+
console.error(
|
|
60
|
+
`[open-memory] Connected to Neo4j at ${this.uri}. ${count} existing nodes.`,
|
|
61
|
+
);
|
|
62
|
+
} finally {
|
|
63
|
+
await session.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// -- Node Operations -------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
async addNode(node: MemoryNode): Promise<void> {
|
|
70
|
+
const session = this.getSession();
|
|
71
|
+
try {
|
|
72
|
+
await session.run(
|
|
73
|
+
`CREATE (n:MemoryNode {
|
|
74
|
+
id: $id, name: $name, type: $type, description: $description,
|
|
75
|
+
embedding: $embedding, metadata: $metadata,
|
|
76
|
+
createdAt: $createdAt, updatedAt: $updatedAt,
|
|
77
|
+
validFrom: $validFrom, validUntil: $validUntil,
|
|
78
|
+
source: $source, accessCount: $accessCount,
|
|
79
|
+
lastAccessedAt: $lastAccessedAt
|
|
80
|
+
})`,
|
|
81
|
+
{
|
|
82
|
+
...node,
|
|
83
|
+
embedding: node.embedding ?? [],
|
|
84
|
+
metadata: JSON.stringify(node.metadata),
|
|
85
|
+
validFrom: node.validFrom ?? null,
|
|
86
|
+
validUntil: node.validUntil ?? null,
|
|
87
|
+
source: node.source ?? null,
|
|
88
|
+
lastAccessedAt: node.lastAccessedAt ?? null,
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
} finally {
|
|
92
|
+
await session.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async updateNode(
|
|
97
|
+
id: string,
|
|
98
|
+
updates: Partial<MemoryNode>,
|
|
99
|
+
): Promise<MemoryNode | null> {
|
|
100
|
+
const session = this.getSession();
|
|
101
|
+
try {
|
|
102
|
+
const setClause = buildSetClause(updates);
|
|
103
|
+
if (!setClause) return await this.getNode(id);
|
|
104
|
+
|
|
105
|
+
const result = await session.run(
|
|
106
|
+
`MATCH (n:MemoryNode {id: $id})
|
|
107
|
+
SET ${setClause}, n.updatedAt = $updatedAt
|
|
108
|
+
RETURN n`,
|
|
109
|
+
{
|
|
110
|
+
id,
|
|
111
|
+
updatedAt: new Date().toISOString(),
|
|
112
|
+
...serializeUpdates(updates),
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
if (result.records.length === 0) return null;
|
|
116
|
+
return deserializeNode(result.records[0].get("n").properties);
|
|
117
|
+
} finally {
|
|
118
|
+
await session.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async deleteNode(id: string): Promise<boolean> {
|
|
123
|
+
const session = this.getSession();
|
|
124
|
+
try {
|
|
125
|
+
const result = await session.run(
|
|
126
|
+
`MATCH (n:MemoryNode {id: $id}) DETACH DELETE n RETURN count(n) AS deleted`,
|
|
127
|
+
{ id },
|
|
128
|
+
);
|
|
129
|
+
return (result.records[0]?.get("deleted")?.toNumber() ?? 0) > 0;
|
|
130
|
+
} finally {
|
|
131
|
+
await session.close();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async getNode(id: string): Promise<MemoryNode | null> {
|
|
136
|
+
const session = this.getSession();
|
|
137
|
+
try {
|
|
138
|
+
const result = await session.run(
|
|
139
|
+
`MATCH (n:MemoryNode {id: $id}) RETURN n`,
|
|
140
|
+
{ id },
|
|
141
|
+
);
|
|
142
|
+
if (result.records.length === 0) return null;
|
|
143
|
+
return deserializeNode(result.records[0].get("n").properties);
|
|
144
|
+
} finally {
|
|
145
|
+
await session.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getNodeByName(name: string): Promise<MemoryNode | null> {
|
|
150
|
+
const session = this.getSession();
|
|
151
|
+
try {
|
|
152
|
+
const result = await session.run(
|
|
153
|
+
`MATCH (n:MemoryNode) WHERE toLower(n.name) = toLower($name) RETURN n LIMIT 1`,
|
|
154
|
+
{ name },
|
|
155
|
+
);
|
|
156
|
+
if (result.records.length === 0) return null;
|
|
157
|
+
return deserializeNode(result.records[0].get("n").properties);
|
|
158
|
+
} finally {
|
|
159
|
+
await session.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async findNodes(filter: NodeFilter): Promise<MemoryNode[]> {
|
|
164
|
+
const session = this.getSession();
|
|
165
|
+
try {
|
|
166
|
+
const conditions: string[] = [];
|
|
167
|
+
const params: Record<string, unknown> = {};
|
|
168
|
+
|
|
169
|
+
if (filter.type) {
|
|
170
|
+
conditions.push("n.type = $type");
|
|
171
|
+
params.type = filter.type;
|
|
172
|
+
}
|
|
173
|
+
if (filter.source) {
|
|
174
|
+
conditions.push("n.source = $source");
|
|
175
|
+
params.source = filter.source;
|
|
176
|
+
}
|
|
177
|
+
if (filter.nameContains) {
|
|
178
|
+
conditions.push("toLower(n.name) CONTAINS toLower($nameContains)");
|
|
179
|
+
params.nameContains = filter.nameContains;
|
|
180
|
+
}
|
|
181
|
+
if (filter.createdAfter) {
|
|
182
|
+
conditions.push("n.createdAt >= $createdAfter");
|
|
183
|
+
params.createdAfter = filter.createdAfter;
|
|
184
|
+
}
|
|
185
|
+
if (filter.createdBefore) {
|
|
186
|
+
conditions.push("n.createdAt <= $createdBefore");
|
|
187
|
+
params.createdBefore = filter.createdBefore;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const whereClause =
|
|
191
|
+
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
192
|
+
const result = await session.run(
|
|
193
|
+
`MATCH (n:MemoryNode) ${whereClause} RETURN n ORDER BY n.updatedAt DESC`,
|
|
194
|
+
params,
|
|
195
|
+
);
|
|
196
|
+
return result.records.map((r) => deserializeNode(r.get("n").properties));
|
|
197
|
+
} finally {
|
|
198
|
+
await session.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async getAllNodes(
|
|
203
|
+
limit: number,
|
|
204
|
+
offset: number,
|
|
205
|
+
): Promise<{ nodes: MemoryNode[]; total: number }> {
|
|
206
|
+
const session = this.getSession();
|
|
207
|
+
try {
|
|
208
|
+
const countResult = await session.run(
|
|
209
|
+
`MATCH (n:MemoryNode) RETURN count(n) AS total`,
|
|
210
|
+
);
|
|
211
|
+
const total = countResult.records[0]?.get("total")?.toNumber() ?? 0;
|
|
212
|
+
|
|
213
|
+
const result = await session.run(
|
|
214
|
+
`MATCH (n:MemoryNode) RETURN n ORDER BY n.updatedAt DESC SKIP $offset LIMIT $limit`,
|
|
215
|
+
{ offset: neo4j.int(offset), limit: neo4j.int(limit) },
|
|
216
|
+
);
|
|
217
|
+
const nodes = result.records.map((r) =>
|
|
218
|
+
deserializeNode(r.get("n").properties),
|
|
219
|
+
);
|
|
220
|
+
return { nodes, total };
|
|
221
|
+
} finally {
|
|
222
|
+
await session.close();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async findNodesByEmbedding(
|
|
227
|
+
embedding: number[],
|
|
228
|
+
topK: number,
|
|
229
|
+
): Promise<ScoredNode[]> {
|
|
230
|
+
// For now, fall back to client-side cosine similarity
|
|
231
|
+
// Neo4j vector index can be used in production for better perf
|
|
232
|
+
const session = this.getSession();
|
|
233
|
+
try {
|
|
234
|
+
const result = await session.run(
|
|
235
|
+
`MATCH (n:MemoryNode) WHERE size(n.embedding) > 0 RETURN n`,
|
|
236
|
+
);
|
|
237
|
+
const scored: ScoredNode[] = [];
|
|
238
|
+
for (const record of result.records) {
|
|
239
|
+
const node = deserializeNode(record.get("n").properties);
|
|
240
|
+
if (node.embedding && node.embedding.length > 0) {
|
|
241
|
+
const score = cosineSimilarity(embedding, node.embedding);
|
|
242
|
+
scored.push({ node, score, matchType: "semantic" });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
scored.sort((a, b) => b.score - a.score);
|
|
246
|
+
return scored.slice(0, topK);
|
|
247
|
+
} finally {
|
|
248
|
+
await session.close();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// -- Edge Operations --------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
async addEdge(edge: MemoryEdge): Promise<void> {
|
|
255
|
+
const session = this.getSession();
|
|
256
|
+
try {
|
|
257
|
+
await session.run(
|
|
258
|
+
`MATCH (s:MemoryNode {id: $source})
|
|
259
|
+
MATCH (t:MemoryNode {id: $target})
|
|
260
|
+
CREATE (s)-[r:MEMORY_EDGE {
|
|
261
|
+
id: $id, relation: $relation, description: $description,
|
|
262
|
+
weight: $weight, metadata: $metadata,
|
|
263
|
+
createdAt: $createdAt, updatedAt: $updatedAt
|
|
264
|
+
}]->(t)`,
|
|
265
|
+
{
|
|
266
|
+
...edge,
|
|
267
|
+
metadata: JSON.stringify(edge.metadata),
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
} finally {
|
|
271
|
+
await session.close();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async updateEdge(
|
|
276
|
+
id: string,
|
|
277
|
+
updates: Partial<MemoryEdge>,
|
|
278
|
+
): Promise<MemoryEdge | null> {
|
|
279
|
+
const session = this.getSession();
|
|
280
|
+
try {
|
|
281
|
+
const setClause = buildEdgeSetClause(updates);
|
|
282
|
+
if (!setClause) return await this.getEdge(id);
|
|
283
|
+
|
|
284
|
+
const result = await session.run(
|
|
285
|
+
`MATCH ()-[r:MEMORY_EDGE {id: $id}]-()
|
|
286
|
+
SET ${setClause}, r.updatedAt = $updatedAt
|
|
287
|
+
RETURN r, startNode(r).id AS sourceId, endNode(r).id AS targetId`,
|
|
288
|
+
{
|
|
289
|
+
id,
|
|
290
|
+
updatedAt: new Date().toISOString(),
|
|
291
|
+
...serializeEdgeUpdates(updates),
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
if (result.records.length === 0) return null;
|
|
295
|
+
return deserializeEdge(result.records[0]);
|
|
296
|
+
} finally {
|
|
297
|
+
await session.close();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async deleteEdge(id: string): Promise<boolean> {
|
|
302
|
+
const session = this.getSession();
|
|
303
|
+
try {
|
|
304
|
+
const result = await session.run(
|
|
305
|
+
`MATCH ()-[r:MEMORY_EDGE {id: $id}]-() DELETE r RETURN count(r) AS deleted`,
|
|
306
|
+
{ id },
|
|
307
|
+
);
|
|
308
|
+
return (result.records[0]?.get("deleted")?.toNumber() ?? 0) > 0;
|
|
309
|
+
} finally {
|
|
310
|
+
await session.close();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async getEdge(id: string): Promise<MemoryEdge | null> {
|
|
315
|
+
const session = this.getSession();
|
|
316
|
+
try {
|
|
317
|
+
const result = await session.run(
|
|
318
|
+
`MATCH (s)-[r:MEMORY_EDGE {id: $id}]->(t) RETURN r, s.id AS sourceId, t.id AS targetId`,
|
|
319
|
+
{ id },
|
|
320
|
+
);
|
|
321
|
+
if (result.records.length === 0) return null;
|
|
322
|
+
return deserializeEdge(result.records[0]);
|
|
323
|
+
} finally {
|
|
324
|
+
await session.close();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async getEdgesForNode(
|
|
329
|
+
nodeId: string,
|
|
330
|
+
direction: "in" | "out" | "both" = "both",
|
|
331
|
+
): Promise<MemoryEdge[]> {
|
|
332
|
+
const session = this.getSession();
|
|
333
|
+
try {
|
|
334
|
+
let query: string;
|
|
335
|
+
if (direction === "out") {
|
|
336
|
+
query = `MATCH (s:MemoryNode {id: $nodeId})-[r:MEMORY_EDGE]->(t) RETURN r, s.id AS sourceId, t.id AS targetId`;
|
|
337
|
+
} else if (direction === "in") {
|
|
338
|
+
query = `MATCH (s)-[r:MEMORY_EDGE]->(t:MemoryNode {id: $nodeId}) RETURN r, s.id AS sourceId, t.id AS targetId`;
|
|
339
|
+
} else {
|
|
340
|
+
query = `MATCH (n:MemoryNode {id: $nodeId})-[r:MEMORY_EDGE]-(m) RETURN r, startNode(r).id AS sourceId, endNode(r).id AS targetId`;
|
|
341
|
+
}
|
|
342
|
+
const result = await session.run(query, { nodeId });
|
|
343
|
+
return result.records.map((rec) => deserializeEdge(rec));
|
|
344
|
+
} finally {
|
|
345
|
+
await session.close();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async getEdgesBetween(
|
|
350
|
+
sourceId: string,
|
|
351
|
+
targetId: string,
|
|
352
|
+
): Promise<MemoryEdge[]> {
|
|
353
|
+
const session = this.getSession();
|
|
354
|
+
try {
|
|
355
|
+
const result = await session.run(
|
|
356
|
+
`MATCH (s:MemoryNode)-[r:MEMORY_EDGE]-(t:MemoryNode)
|
|
357
|
+
WHERE (s.id = $sourceId AND t.id = $targetId)
|
|
358
|
+
OR (s.id = $targetId AND t.id = $sourceId)
|
|
359
|
+
RETURN r, startNode(r).id AS sourceId, endNode(r).id AS targetId`,
|
|
360
|
+
{ sourceId, targetId },
|
|
361
|
+
);
|
|
362
|
+
return result.records.map((rec) => deserializeEdge(rec));
|
|
363
|
+
} finally {
|
|
364
|
+
await session.close();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// -- Graph Traversal --------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
async getNeighborhood(nodeId: string, depth: number): Promise<Subgraph> {
|
|
371
|
+
const session = this.getSession();
|
|
372
|
+
try {
|
|
373
|
+
const result = await session.run(
|
|
374
|
+
`MATCH path = (start:MemoryNode {id: $nodeId})-[*0..${depth}]-(neighbor:MemoryNode)
|
|
375
|
+
WITH DISTINCT neighbor, relationships(path) AS rels
|
|
376
|
+
UNWIND rels AS r
|
|
377
|
+
WITH collect(DISTINCT neighbor) AS nodes,
|
|
378
|
+
collect(DISTINCT {rel: r, source: startNode(r).id, target: endNode(r).id}) AS edges
|
|
379
|
+
RETURN nodes, edges`,
|
|
380
|
+
{ nodeId },
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (result.records.length === 0) {
|
|
384
|
+
// Return just the start node if it exists
|
|
385
|
+
const startNode = await this.getNode(nodeId);
|
|
386
|
+
return {
|
|
387
|
+
nodes: startNode ? [startNode] : [],
|
|
388
|
+
edges: [],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const record = result.records[0];
|
|
393
|
+
const rawNodes = record.get("nodes") as unknown[];
|
|
394
|
+
const rawEdges = record.get("edges") as unknown[];
|
|
395
|
+
|
|
396
|
+
const nodes: MemoryNode[] = (rawNodes || []).map((n: unknown) =>
|
|
397
|
+
deserializeNode(
|
|
398
|
+
(n as { properties: Record<string, unknown> }).properties,
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
const edges: MemoryEdge[] = (rawEdges || []).map((e: unknown) => {
|
|
402
|
+
const edge = e as {
|
|
403
|
+
rel: { properties: Record<string, unknown> };
|
|
404
|
+
source: string;
|
|
405
|
+
target: string;
|
|
406
|
+
};
|
|
407
|
+
return {
|
|
408
|
+
id: edge.rel.properties.id as string,
|
|
409
|
+
source: edge.source,
|
|
410
|
+
target: edge.target,
|
|
411
|
+
relation: edge.rel.properties.relation as string,
|
|
412
|
+
description: edge.rel.properties.description as string,
|
|
413
|
+
weight: edge.rel.properties.weight as number,
|
|
414
|
+
metadata: JSON.parse(
|
|
415
|
+
(edge.rel.properties.metadata as string) || "{}",
|
|
416
|
+
),
|
|
417
|
+
createdAt: edge.rel.properties.createdAt as string,
|
|
418
|
+
updatedAt: edge.rel.properties.updatedAt as string,
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return { nodes, edges };
|
|
423
|
+
} finally {
|
|
424
|
+
await session.close();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// -- Stats & Lifecycle ------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
async getStats(): Promise<GraphStats> {
|
|
431
|
+
const session = this.getSession();
|
|
432
|
+
try {
|
|
433
|
+
const nodesResult = await session.run(
|
|
434
|
+
`MATCH (n:MemoryNode) RETURN n.type AS type, count(n) AS count`,
|
|
435
|
+
);
|
|
436
|
+
const nodesByType: Record<string, number> = {};
|
|
437
|
+
let totalNodes = 0;
|
|
438
|
+
for (const record of nodesResult.records) {
|
|
439
|
+
const type = record.get("type") as string;
|
|
440
|
+
const count = (
|
|
441
|
+
record.get("count") as { toNumber(): number }
|
|
442
|
+
).toNumber();
|
|
443
|
+
nodesByType[type] = count;
|
|
444
|
+
totalNodes += count;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const edgesResult = await session.run(
|
|
448
|
+
`MATCH ()-[r:MEMORY_EDGE]-() RETURN count(r) AS total`,
|
|
449
|
+
);
|
|
450
|
+
const totalEdges =
|
|
451
|
+
(
|
|
452
|
+
edgesResult.records[0]?.get("total") as { toNumber(): number }
|
|
453
|
+
)?.toNumber() ?? 0;
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
totalNodes,
|
|
457
|
+
totalEdges,
|
|
458
|
+
nodesByType,
|
|
459
|
+
storageBackend: "neo4j",
|
|
460
|
+
};
|
|
461
|
+
} finally {
|
|
462
|
+
await session.close();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async close(): Promise<void> {
|
|
467
|
+
await this.driver.close();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// =============================================================================
|
|
472
|
+
// Serialization Helpers
|
|
473
|
+
// =============================================================================
|
|
474
|
+
|
|
475
|
+
function deserializeNode(props: Record<string, unknown>): MemoryNode {
|
|
476
|
+
return {
|
|
477
|
+
id: props.id as string,
|
|
478
|
+
name: props.name as string,
|
|
479
|
+
type: props.type as MemoryNode["type"],
|
|
480
|
+
description: props.description as string,
|
|
481
|
+
embedding: (props.embedding as number[]) ?? [],
|
|
482
|
+
metadata:
|
|
483
|
+
typeof props.metadata === "string"
|
|
484
|
+
? JSON.parse(props.metadata)
|
|
485
|
+
: ((props.metadata as Record<string, unknown>) ?? {}),
|
|
486
|
+
createdAt: props.createdAt as string,
|
|
487
|
+
updatedAt: props.updatedAt as string,
|
|
488
|
+
validFrom: (props.validFrom as string) ?? undefined,
|
|
489
|
+
validUntil: (props.validUntil as string) ?? undefined,
|
|
490
|
+
source: (props.source as string) ?? undefined,
|
|
491
|
+
accessCount:
|
|
492
|
+
typeof props.accessCount === "object" && props.accessCount !== null
|
|
493
|
+
? (props.accessCount as { toNumber(): number }).toNumber()
|
|
494
|
+
: ((props.accessCount as number) ?? 0),
|
|
495
|
+
lastAccessedAt: (props.lastAccessedAt as string) ?? undefined,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function deserializeEdge(record: { get(key: string): unknown }): MemoryEdge {
|
|
500
|
+
const rel = record.get("r") as { properties: Record<string, unknown> };
|
|
501
|
+
const props = rel.properties;
|
|
502
|
+
return {
|
|
503
|
+
id: props.id as string,
|
|
504
|
+
source: record.get("sourceId") as string,
|
|
505
|
+
target: record.get("targetId") as string,
|
|
506
|
+
relation: props.relation as string,
|
|
507
|
+
description: props.description as string,
|
|
508
|
+
weight: props.weight as number,
|
|
509
|
+
metadata:
|
|
510
|
+
typeof props.metadata === "string"
|
|
511
|
+
? JSON.parse(props.metadata)
|
|
512
|
+
: ((props.metadata as Record<string, unknown>) ?? {}),
|
|
513
|
+
createdAt: props.createdAt as string,
|
|
514
|
+
updatedAt: props.updatedAt as string,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildSetClause(updates: Partial<MemoryNode>): string {
|
|
519
|
+
const parts: string[] = [];
|
|
520
|
+
if (updates.name !== undefined) parts.push("n.name = $name");
|
|
521
|
+
if (updates.description !== undefined)
|
|
522
|
+
parts.push("n.description = $description");
|
|
523
|
+
if (updates.type !== undefined) parts.push("n.type = $type");
|
|
524
|
+
if (updates.embedding !== undefined) parts.push("n.embedding = $embedding");
|
|
525
|
+
if (updates.metadata !== undefined) parts.push("n.metadata = $metadata");
|
|
526
|
+
if (updates.validFrom !== undefined) parts.push("n.validFrom = $validFrom");
|
|
527
|
+
if (updates.validUntil !== undefined)
|
|
528
|
+
parts.push("n.validUntil = $validUntil");
|
|
529
|
+
if (updates.accessCount !== undefined)
|
|
530
|
+
parts.push("n.accessCount = $accessCount");
|
|
531
|
+
if (updates.lastAccessedAt !== undefined)
|
|
532
|
+
parts.push("n.lastAccessedAt = $lastAccessedAt");
|
|
533
|
+
return parts.join(", ");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function buildEdgeSetClause(updates: Partial<MemoryEdge>): string {
|
|
537
|
+
const parts: string[] = [];
|
|
538
|
+
if (updates.relation !== undefined) parts.push("r.relation = $relation");
|
|
539
|
+
if (updates.description !== undefined)
|
|
540
|
+
parts.push("r.description = $description");
|
|
541
|
+
if (updates.weight !== undefined) parts.push("r.weight = $weight");
|
|
542
|
+
if (updates.metadata !== undefined) parts.push("r.metadata = $metadata");
|
|
543
|
+
return parts.join(", ");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function serializeUpdates(
|
|
547
|
+
updates: Partial<MemoryNode>,
|
|
548
|
+
): Record<string, unknown> {
|
|
549
|
+
const result: Record<string, unknown> = { ...updates };
|
|
550
|
+
if (updates.metadata !== undefined) {
|
|
551
|
+
result.metadata = JSON.stringify(updates.metadata);
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function serializeEdgeUpdates(
|
|
557
|
+
updates: Partial<MemoryEdge>,
|
|
558
|
+
): Record<string, unknown> {
|
|
559
|
+
const result: Record<string, unknown> = { ...updates };
|
|
560
|
+
if (updates.metadata !== undefined) {
|
|
561
|
+
result.metadata = JSON.stringify(updates.metadata);
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|