server-memory-enhanced 2.0.0 → 2.1.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/dist/index.js CHANGED
@@ -28,6 +28,8 @@ export async function ensureMemoryDirectory() {
28
28
  // Initialize memory directory path (will be set during startup)
29
29
  let MEMORY_DIR_PATH;
30
30
  export { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
31
+ export { JsonlStorageAdapter } from './lib/jsonl-storage-adapter.js';
32
+ export { Neo4jStorageAdapter } from './lib/neo4j-storage-adapter.js';
31
33
  let knowledgeGraphManager;
32
34
  // Zod schemas for enhanced entities and relations
33
35
  const EntitySchemaCompat = EntitySchema;
@@ -0,0 +1,315 @@
1
+ /**
2
+ * JSONL Storage Adapter - implements file-based storage using JSON Lines format
3
+ */
4
+ import { promises as fs } from 'fs';
5
+ import path from 'path';
6
+ // Constants for file naming and types
7
+ const THREAD_FILE_PREFIX = 'thread-';
8
+ const THREAD_FILE_EXTENSION = '.jsonl';
9
+ const ENTITY_TYPE = 'entity';
10
+ const RELATION_TYPE = 'relation';
11
+ const FILE_NOT_FOUND_ERROR = 'ENOENT';
12
+ /**
13
+ * Type guard to check if an error is a FileSystemError
14
+ */
15
+ function isFileSystemError(error) {
16
+ return error instanceof Error && 'code' in error;
17
+ }
18
+ /**
19
+ * Check if a string field is valid (non-empty after trimming)
20
+ */
21
+ function isValidString(value) {
22
+ return typeof value === 'string' && value.trim().length > 0;
23
+ }
24
+ /**
25
+ * JSONL-based storage adapter for the knowledge graph
26
+ * Stores data in thread-specific JSONL files
27
+ *
28
+ * Responsibilities:
29
+ * - File I/O operations for JSONL format
30
+ * - Thread-based data organization
31
+ * - Data serialization/deserialization
32
+ */
33
+ export class JsonlStorageAdapter {
34
+ memoryDirPath;
35
+ constructor(memoryDirPath) {
36
+ this.memoryDirPath = memoryDirPath;
37
+ }
38
+ /**
39
+ * Get the file path for a specific thread
40
+ */
41
+ getThreadFilePath(agentThreadId) {
42
+ return path.join(this.memoryDirPath, `${THREAD_FILE_PREFIX}${agentThreadId}${THREAD_FILE_EXTENSION}`);
43
+ }
44
+ /**
45
+ * Check if an item is a valid entity
46
+ */
47
+ isValidEntity(item) {
48
+ return item.type === ENTITY_TYPE &&
49
+ isValidString(item.name) &&
50
+ isValidString(item.entityType) &&
51
+ Array.isArray(item.observations) &&
52
+ isValidString(item.agentThreadId) &&
53
+ isValidString(item.timestamp) &&
54
+ typeof item.confidence === 'number' &&
55
+ typeof item.importance === 'number';
56
+ }
57
+ /**
58
+ * Check if an item is a valid relation
59
+ */
60
+ isValidRelation(item) {
61
+ return item.type === RELATION_TYPE &&
62
+ isValidString(item.from) &&
63
+ isValidString(item.to) &&
64
+ isValidString(item.relationType) &&
65
+ isValidString(item.agentThreadId) &&
66
+ isValidString(item.timestamp) &&
67
+ typeof item.confidence === 'number' &&
68
+ typeof item.importance === 'number';
69
+ }
70
+ /**
71
+ * Parse and validate a single JSONL line
72
+ */
73
+ parseLine(line, filePath) {
74
+ try {
75
+ return JSON.parse(line);
76
+ }
77
+ catch (parseError) {
78
+ console.warn(`Skipping malformed JSON line in ${filePath} (line length: ${line.length} chars)`);
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Convert JSONL item to Entity
84
+ */
85
+ toEntity(item) {
86
+ return {
87
+ name: item.name,
88
+ entityType: item.entityType,
89
+ observations: item.observations,
90
+ agentThreadId: item.agentThreadId,
91
+ timestamp: item.timestamp,
92
+ confidence: item.confidence,
93
+ importance: item.importance
94
+ };
95
+ }
96
+ /**
97
+ * Convert JSONL item to Relation
98
+ */
99
+ toRelation(item) {
100
+ return {
101
+ from: item.from,
102
+ to: item.to,
103
+ relationType: item.relationType,
104
+ agentThreadId: item.agentThreadId,
105
+ timestamp: item.timestamp,
106
+ confidence: item.confidence,
107
+ importance: item.importance
108
+ };
109
+ }
110
+ /**
111
+ * Process a single JSONL item and add to graph
112
+ */
113
+ processItem(item, graph, filePath) {
114
+ if (!item) {
115
+ return;
116
+ }
117
+ if (this.isValidEntity(item)) {
118
+ graph.entities.push(this.toEntity(item));
119
+ }
120
+ else if (this.isValidRelation(item)) {
121
+ graph.relations.push(this.toRelation(item));
122
+ }
123
+ else if (item.type === ENTITY_TYPE || item.type === RELATION_TYPE) {
124
+ console.warn(`Skipping ${item.type} with missing required fields in ${filePath}`);
125
+ }
126
+ }
127
+ /**
128
+ * Serialize an entity to JSONL format
129
+ */
130
+ serializeEntity(entity) {
131
+ return JSON.stringify({
132
+ type: ENTITY_TYPE,
133
+ name: entity.name,
134
+ entityType: entity.entityType,
135
+ observations: entity.observations,
136
+ agentThreadId: entity.agentThreadId,
137
+ timestamp: entity.timestamp,
138
+ confidence: entity.confidence,
139
+ importance: entity.importance
140
+ });
141
+ }
142
+ /**
143
+ * Serialize a relation to JSONL format
144
+ */
145
+ serializeRelation(relation) {
146
+ return JSON.stringify({
147
+ type: RELATION_TYPE,
148
+ from: relation.from,
149
+ to: relation.to,
150
+ relationType: relation.relationType,
151
+ agentThreadId: relation.agentThreadId,
152
+ timestamp: relation.timestamp,
153
+ confidence: relation.confidence,
154
+ importance: relation.importance
155
+ });
156
+ }
157
+ /**
158
+ * Get or create thread data in the map
159
+ */
160
+ getOrCreateThreadData(threadMap, threadId) {
161
+ let threadData = threadMap.get(threadId);
162
+ if (!threadData) {
163
+ threadData = { entities: [], relations: [] };
164
+ threadMap.set(threadId, threadData);
165
+ }
166
+ return threadData;
167
+ }
168
+ /**
169
+ * Check if error is a file not found error
170
+ */
171
+ isFileNotFoundError(error) {
172
+ return isFileSystemError(error) && error.code === FILE_NOT_FOUND_ERROR;
173
+ }
174
+ /**
175
+ * Extract thread ID from filename
176
+ */
177
+ extractThreadId(fileName) {
178
+ const match = fileName.match(new RegExp(`^${THREAD_FILE_PREFIX}(.+)${THREAD_FILE_EXTENSION}$`));
179
+ return match ? match[1] : null;
180
+ }
181
+ /**
182
+ * Load graph data from a single JSONL file
183
+ */
184
+ async loadGraphFromFile(filePath) {
185
+ try {
186
+ const data = await fs.readFile(filePath, "utf-8");
187
+ const lines = data.split("\n").filter(line => line.trim() !== "");
188
+ const graph = { entities: [], relations: [] };
189
+ for (const line of lines) {
190
+ const item = this.parseLine(line, filePath);
191
+ this.processItem(item, graph, filePath);
192
+ }
193
+ return graph;
194
+ }
195
+ catch (error) {
196
+ if (this.isFileNotFoundError(error)) {
197
+ return { entities: [], relations: [] };
198
+ }
199
+ throw error;
200
+ }
201
+ }
202
+ /**
203
+ * Get all thread file names in the memory directory
204
+ */
205
+ async getThreadFileNames() {
206
+ const files = await fs.readdir(this.memoryDirPath).catch(() => []);
207
+ return files.filter(f => f.startsWith(THREAD_FILE_PREFIX) && f.endsWith(THREAD_FILE_EXTENSION));
208
+ }
209
+ /**
210
+ * Merge multiple graphs into one
211
+ */
212
+ mergeGraphs(graphs) {
213
+ return graphs.reduce((acc, graph) => ({
214
+ entities: [...acc.entities, ...graph.entities],
215
+ relations: [...acc.relations, ...graph.relations]
216
+ }), { entities: [], relations: [] });
217
+ }
218
+ /**
219
+ * Load the complete knowledge graph from all thread files
220
+ */
221
+ async loadGraph() {
222
+ const threadFiles = await this.getThreadFileNames();
223
+ const graphs = await Promise.all(threadFiles.map(f => this.loadGraphFromFile(path.join(this.memoryDirPath, f))));
224
+ return this.mergeGraphs(graphs);
225
+ }
226
+ /**
227
+ * Delete empty thread file if it exists
228
+ */
229
+ async deleteThreadFileIfExists(threadFilePath) {
230
+ try {
231
+ await fs.unlink(threadFilePath);
232
+ }
233
+ catch (error) {
234
+ if (!this.isFileNotFoundError(error)) {
235
+ console.warn(`Failed to delete empty thread file ${threadFilePath}:`, error);
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Serialize thread data to JSONL lines
241
+ */
242
+ serializeThreadData(threadData) {
243
+ return [
244
+ ...threadData.entities.map(e => this.serializeEntity(e)),
245
+ ...threadData.relations.map(r => this.serializeRelation(r))
246
+ ];
247
+ }
248
+ /**
249
+ * Save data for a specific thread
250
+ */
251
+ async saveGraphForThread(agentThreadId, threadData) {
252
+ const threadFilePath = this.getThreadFilePath(agentThreadId);
253
+ const lines = this.serializeThreadData(threadData);
254
+ if (lines.length === 0) {
255
+ await this.deleteThreadFileIfExists(threadFilePath);
256
+ return;
257
+ }
258
+ await fs.writeFile(threadFilePath, lines.join("\n"));
259
+ }
260
+ /**
261
+ * Group graph data by thread ID
262
+ */
263
+ groupByThread(graph) {
264
+ const threadMap = new Map();
265
+ for (const entity of graph.entities) {
266
+ const threadData = this.getOrCreateThreadData(threadMap, entity.agentThreadId);
267
+ threadData.entities.push(entity);
268
+ }
269
+ for (const relation of graph.relations) {
270
+ const threadData = this.getOrCreateThreadData(threadMap, relation.agentThreadId);
271
+ threadData.relations.push(relation);
272
+ }
273
+ return threadMap;
274
+ }
275
+ /**
276
+ * Save all thread data to their respective files
277
+ */
278
+ async saveAllThreads(threadMap) {
279
+ const savePromises = Array.from(threadMap.entries()).map(([threadId, data]) => this.saveGraphForThread(threadId, data));
280
+ await Promise.all(savePromises);
281
+ }
282
+ /**
283
+ * Clean up stale thread files that are no longer in the graph
284
+ */
285
+ async cleanupStaleThreadFiles(activeThreadIds) {
286
+ try {
287
+ const threadFiles = await this.getThreadFileNames();
288
+ const deletePromises = threadFiles.map(async (fileName) => {
289
+ const threadId = this.extractThreadId(fileName);
290
+ if (threadId && !activeThreadIds.has(threadId)) {
291
+ const filePath = path.join(this.memoryDirPath, fileName);
292
+ await this.deleteThreadFileIfExists(filePath);
293
+ }
294
+ });
295
+ await Promise.all(deletePromises);
296
+ }
297
+ catch (error) {
298
+ console.warn('Failed to clean up stale thread files:', error);
299
+ }
300
+ }
301
+ /**
302
+ * Save the complete knowledge graph to thread-specific files
303
+ */
304
+ async saveGraph(graph) {
305
+ const threadMap = this.groupByThread(graph);
306
+ await this.saveAllThreads(threadMap);
307
+ await this.cleanupStaleThreadFiles(new Set(threadMap.keys()));
308
+ }
309
+ /**
310
+ * Initialize the storage adapter (create memory directory if needed)
311
+ */
312
+ async initialize() {
313
+ await fs.mkdir(this.memoryDirPath, { recursive: true });
314
+ }
315
+ }
@@ -1,182 +1,38 @@
1
1
  /**
2
2
  * KnowledgeGraphManager - Main class for managing the knowledge graph
3
3
  */
4
- import { promises as fs } from 'fs';
5
- import path from 'path';
6
4
  import { randomUUID } from 'crypto';
5
+ import { JsonlStorageAdapter } from './jsonl-storage-adapter.js';
7
6
  export class KnowledgeGraphManager {
8
- memoryDirPath;
9
7
  static NEGATION_WORDS = new Set(['not', 'no', 'never', 'neither', 'none', 'doesn\'t', 'don\'t', 'isn\'t', 'aren\'t']);
10
- constructor(memoryDirPath) {
11
- this.memoryDirPath = memoryDirPath;
8
+ storage;
9
+ initializePromise = null;
10
+ constructor(memoryDirPath, storageAdapter) {
11
+ this.storage = storageAdapter || new JsonlStorageAdapter(memoryDirPath);
12
+ // Lazy initialization - will be called on first operation
12
13
  }
13
- getThreadFilePath(agentThreadId) {
14
- return path.join(this.memoryDirPath, `thread-${agentThreadId}.jsonl`);
15
- }
16
- async loadGraphFromFile(filePath) {
17
- try {
18
- const data = await fs.readFile(filePath, "utf-8");
19
- const lines = data.split("\n").filter(line => line.trim() !== "");
20
- return lines.reduce((graph, line) => {
21
- let item;
22
- try {
23
- item = JSON.parse(line);
24
- }
25
- catch (parseError) {
26
- console.warn(`Skipping malformed JSON line in ${filePath} (line length: ${line.length} chars)`);
27
- return graph;
28
- }
29
- if (item.type === "entity") {
30
- // Validate required fields
31
- if (!item.name || !item.entityType || !Array.isArray(item.observations) ||
32
- !item.agentThreadId || !item.timestamp ||
33
- typeof item.confidence !== 'number' || typeof item.importance !== 'number') {
34
- console.warn(`Skipping entity with missing required fields in ${filePath}`);
35
- return graph;
36
- }
37
- graph.entities.push({
38
- name: item.name,
39
- entityType: item.entityType,
40
- observations: item.observations,
41
- agentThreadId: item.agentThreadId,
42
- timestamp: item.timestamp,
43
- confidence: item.confidence,
44
- importance: item.importance
45
- });
46
- }
47
- if (item.type === "relation") {
48
- // Validate required fields
49
- if (!item.from || !item.to || !item.relationType ||
50
- !item.agentThreadId || !item.timestamp ||
51
- typeof item.confidence !== 'number' || typeof item.importance !== 'number') {
52
- console.warn(`Skipping relation with missing required fields in ${filePath}`);
53
- return graph;
54
- }
55
- graph.relations.push({
56
- from: item.from,
57
- to: item.to,
58
- relationType: item.relationType,
59
- agentThreadId: item.agentThreadId,
60
- timestamp: item.timestamp,
61
- confidence: item.confidence,
62
- importance: item.importance
63
- });
64
- }
65
- return graph;
66
- }, { entities: [], relations: [] });
67
- }
68
- catch (error) {
69
- if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
70
- return { entities: [], relations: [] };
71
- }
72
- throw error;
73
- }
74
- }
75
- async loadGraph() {
76
- const files = await fs.readdir(this.memoryDirPath).catch(() => []);
77
- const threadFiles = files.filter(f => f.startsWith('thread-') && f.endsWith('.jsonl'));
78
- const graphs = await Promise.all(threadFiles.map(f => this.loadGraphFromFile(path.join(this.memoryDirPath, f))));
79
- return graphs.reduce((acc, graph) => ({
80
- entities: [...acc.entities, ...graph.entities],
81
- relations: [...acc.relations, ...graph.relations]
82
- }), { entities: [], relations: [] });
83
- }
84
- async saveGraphForThread(agentThreadId, entities, relations) {
85
- const threadFilePath = this.getThreadFilePath(agentThreadId);
86
- const lines = [
87
- ...entities.map(e => JSON.stringify({
88
- type: "entity",
89
- name: e.name,
90
- entityType: e.entityType,
91
- observations: e.observations,
92
- agentThreadId: e.agentThreadId,
93
- timestamp: e.timestamp,
94
- confidence: e.confidence,
95
- importance: e.importance
96
- })),
97
- ...relations.map(r => JSON.stringify({
98
- type: "relation",
99
- from: r.from,
100
- to: r.to,
101
- relationType: r.relationType,
102
- agentThreadId: r.agentThreadId,
103
- timestamp: r.timestamp,
104
- confidence: r.confidence,
105
- importance: r.importance
106
- })),
107
- ];
108
- // Avoid creating or keeping empty files when there is no data for this thread
109
- if (lines.length === 0) {
110
- try {
111
- await fs.unlink(threadFilePath);
112
- }
113
- catch (error) {
114
- // Only ignore ENOENT errors (file doesn't exist)
115
- if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
116
- console.warn(`Failed to delete empty thread file ${threadFilePath}:`, error);
117
- }
118
- }
119
- return;
120
- }
121
- await fs.writeFile(threadFilePath, lines.join("\n"));
122
- }
123
- async saveGraph(graph) {
124
- // Group entities and relations by agentThreadId
125
- const threadMap = new Map();
126
- for (const entity of graph.entities) {
127
- if (!threadMap.has(entity.agentThreadId)) {
128
- threadMap.set(entity.agentThreadId, { entities: [], relations: [] });
129
- }
130
- threadMap.get(entity.agentThreadId).entities.push(entity);
131
- }
132
- for (const relation of graph.relations) {
133
- if (!threadMap.has(relation.agentThreadId)) {
134
- threadMap.set(relation.agentThreadId, { entities: [], relations: [] });
135
- }
136
- threadMap.get(relation.agentThreadId).relations.push(relation);
137
- }
138
- // Save each thread's data to its own file
139
- await Promise.all(Array.from(threadMap.entries()).map(([threadId, data]) => this.saveGraphForThread(threadId, data.entities, data.relations)));
140
- // Clean up stale thread files that no longer have data
141
- try {
142
- const files = await fs.readdir(this.memoryDirPath).catch(() => []);
143
- const threadFiles = files.filter(f => f.startsWith('thread-') && f.endsWith('.jsonl'));
144
- await Promise.all(threadFiles.map(async (fileName) => {
145
- // Extract threadId from filename: thread-{agentThreadId}.jsonl
146
- const match = fileName.match(/^thread-(.+)\.jsonl$/);
147
- if (match) {
148
- const threadId = match[1];
149
- if (!threadMap.has(threadId)) {
150
- const filePath = path.join(this.memoryDirPath, fileName);
151
- try {
152
- await fs.unlink(filePath);
153
- }
154
- catch (error) {
155
- // Only log non-ENOENT errors
156
- if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
157
- console.warn(`Failed to delete stale thread file ${filePath}:`, error);
158
- }
159
- }
160
- }
161
- }
162
- }));
163
- }
164
- catch (error) {
165
- // Best-effort cleanup: log but don't fail the save operation
166
- console.warn('Failed to clean up stale thread files:', error);
14
+ /**
15
+ * Ensure storage is initialized before any operation
16
+ * This is called automatically by all public methods
17
+ */
18
+ async ensureInitialized() {
19
+ if (!this.initializePromise) {
20
+ this.initializePromise = this.storage.initialize();
167
21
  }
22
+ await this.initializePromise;
168
23
  }
169
24
  async createEntities(entities) {
170
- const graph = await this.loadGraph();
25
+ await this.ensureInitialized();
26
+ const graph = await this.storage.loadGraph();
171
27
  // Entity names are globally unique across all threads in the collaborative knowledge graph
172
28
  // This prevents duplicate entities while allowing multiple threads to contribute to the same entity
173
29
  const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
174
30
  graph.entities.push(...newEntities);
175
- await this.saveGraph(graph);
31
+ await this.storage.saveGraph(graph);
176
32
  return newEntities;
177
33
  }
178
34
  async createRelations(relations) {
179
- const graph = await this.loadGraph();
35
+ const graph = await this.storage.loadGraph();
180
36
  // Validate that referenced entities exist
181
37
  const entityNames = new Set(graph.entities.map(e => e.name));
182
38
  const validRelations = relations.filter(r => {
@@ -192,11 +48,11 @@ export class KnowledgeGraphManager {
192
48
  existingRelation.to === r.to &&
193
49
  existingRelation.relationType === r.relationType));
194
50
  graph.relations.push(...newRelations);
195
- await this.saveGraph(graph);
51
+ await this.storage.saveGraph(graph);
196
52
  return newRelations;
197
53
  }
198
54
  async addObservations(observations) {
199
- const graph = await this.loadGraph();
55
+ const graph = await this.storage.loadGraph();
200
56
  const results = observations.map(o => {
201
57
  const entity = graph.entities.find(e => e.name === o.entityName);
202
58
  if (!entity) {
@@ -231,17 +87,17 @@ export class KnowledgeGraphManager {
231
87
  entity.importance = Math.max(entity.importance, o.importance);
232
88
  return { entityName: o.entityName, addedObservations: newObservations };
233
89
  });
234
- await this.saveGraph(graph);
90
+ await this.storage.saveGraph(graph);
235
91
  return results;
236
92
  }
237
93
  async deleteEntities(entityNames) {
238
- const graph = await this.loadGraph();
94
+ const graph = await this.storage.loadGraph();
239
95
  graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
240
96
  graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
241
- await this.saveGraph(graph);
97
+ await this.storage.saveGraph(graph);
242
98
  }
243
99
  async deleteObservations(deletions) {
244
- const graph = await this.loadGraph();
100
+ const graph = await this.storage.loadGraph();
245
101
  deletions.forEach(d => {
246
102
  const entity = graph.entities.find(e => e.name === d.entityName);
247
103
  if (entity) {
@@ -249,22 +105,22 @@ export class KnowledgeGraphManager {
249
105
  entity.observations = entity.observations.filter(o => !d.observations.includes(o.content) && !d.observations.includes(o.id));
250
106
  }
251
107
  });
252
- await this.saveGraph(graph);
108
+ await this.storage.saveGraph(graph);
253
109
  }
254
110
  async deleteRelations(relations) {
255
- const graph = await this.loadGraph();
111
+ const graph = await this.storage.loadGraph();
256
112
  // Delete relations globally across all threads by matching (from, to, relationType)
257
113
  // In a collaborative knowledge graph, deletions affect all threads
258
114
  graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
259
115
  r.to === delRelation.to &&
260
116
  r.relationType === delRelation.relationType));
261
- await this.saveGraph(graph);
117
+ await this.storage.saveGraph(graph);
262
118
  }
263
119
  async readGraph() {
264
- return this.loadGraph();
120
+ return this.storage.loadGraph();
265
121
  }
266
122
  async searchNodes(query) {
267
- const graph = await this.loadGraph();
123
+ const graph = await this.storage.loadGraph();
268
124
  // Filter entities
269
125
  const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) ||
270
126
  e.entityType.toLowerCase().includes(query.toLowerCase()) ||
@@ -280,7 +136,7 @@ export class KnowledgeGraphManager {
280
136
  return filteredGraph;
281
137
  }
282
138
  async openNodes(names) {
283
- const graph = await this.loadGraph();
139
+ const graph = await this.storage.loadGraph();
284
140
  // Filter entities
285
141
  const filteredEntities = graph.entities.filter(e => names.includes(e.name));
286
142
  // Create a Set of filtered entity names for quick lookup
@@ -294,7 +150,7 @@ export class KnowledgeGraphManager {
294
150
  return filteredGraph;
295
151
  }
296
152
  async queryNodes(filters) {
297
- const graph = await this.loadGraph();
153
+ const graph = await this.storage.loadGraph();
298
154
  // If no filters provided, return entire graph
299
155
  if (!filters) {
300
156
  return graph;
@@ -349,7 +205,7 @@ export class KnowledgeGraphManager {
349
205
  }
350
206
  // Enhancement 1: Memory Statistics & Insights
351
207
  async getMemoryStats() {
352
- const graph = await this.loadGraph();
208
+ const graph = await this.storage.loadGraph();
353
209
  // Count entity types
354
210
  const entityTypes = {};
355
211
  graph.entities.forEach(e => {
@@ -392,7 +248,7 @@ export class KnowledgeGraphManager {
392
248
  }
393
249
  // Enhancement 2: Get recent changes
394
250
  async getRecentChanges(since) {
395
- const graph = await this.loadGraph();
251
+ const graph = await this.storage.loadGraph();
396
252
  const sinceDate = new Date(since);
397
253
  // Only return entities and relations that were actually modified since the specified time
398
254
  const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sinceDate);
@@ -405,7 +261,7 @@ export class KnowledgeGraphManager {
405
261
  }
406
262
  // Enhancement 3: Relationship path finding
407
263
  async findRelationPath(from, to, maxDepth = 5) {
408
- const graph = await this.loadGraph();
264
+ const graph = await this.storage.loadGraph();
409
265
  if (from === to) {
410
266
  return { found: true, path: [from], relations: [] };
411
267
  }
@@ -463,7 +319,7 @@ export class KnowledgeGraphManager {
463
319
  }
464
320
  // Enhancement 4: Detect conflicting observations
465
321
  async detectConflicts() {
466
- const graph = await this.loadGraph();
322
+ const graph = await this.storage.loadGraph();
467
323
  const conflicts = [];
468
324
  for (const entity of graph.entities) {
469
325
  const entityConflicts = [];
@@ -504,7 +360,7 @@ export class KnowledgeGraphManager {
504
360
  }
505
361
  // Enhancement 5: Memory pruning
506
362
  async pruneMemory(options) {
507
- const graph = await this.loadGraph();
363
+ const graph = await this.storage.loadGraph();
508
364
  const initialEntityCount = graph.entities.length;
509
365
  const initialRelationCount = graph.relations.length;
510
366
  // Filter entities to remove
@@ -534,7 +390,7 @@ export class KnowledgeGraphManager {
534
390
  const relationsToKeep = graph.relations.filter(r => keptEntityNames.has(r.from) && keptEntityNames.has(r.to));
535
391
  graph.entities = entitiesToKeep;
536
392
  graph.relations = relationsToKeep;
537
- await this.saveGraph(graph);
393
+ await this.storage.saveGraph(graph);
538
394
  return {
539
395
  removedEntities: initialEntityCount - entitiesToKeep.length,
540
396
  removedRelations: initialRelationCount - relationsToKeep.length
@@ -542,7 +398,7 @@ export class KnowledgeGraphManager {
542
398
  }
543
399
  // Enhancement 6: Batch operations
544
400
  async bulkUpdate(updates) {
545
- const graph = await this.loadGraph();
401
+ const graph = await this.storage.loadGraph();
546
402
  let updated = 0;
547
403
  const notFound = [];
548
404
  for (const update of updates) {
@@ -575,12 +431,12 @@ export class KnowledgeGraphManager {
575
431
  entity.timestamp = new Date().toISOString();
576
432
  updated++;
577
433
  }
578
- await this.saveGraph(graph);
434
+ await this.storage.saveGraph(graph);
579
435
  return { updated, notFound };
580
436
  }
581
437
  // Enhancement 7: Flag for review (Human-in-the-Loop)
582
438
  async flagForReview(entityName, reason, reviewer) {
583
- const graph = await this.loadGraph();
439
+ const graph = await this.storage.loadGraph();
584
440
  const entity = graph.entities.find(e => e.name === entityName);
585
441
  if (!entity) {
586
442
  throw new Error(`Entity with name ${entityName} not found`);
@@ -600,17 +456,17 @@ export class KnowledgeGraphManager {
600
456
  };
601
457
  entity.observations.push(flagObservation);
602
458
  entity.timestamp = new Date().toISOString();
603
- await this.saveGraph(graph);
459
+ await this.storage.saveGraph(graph);
604
460
  }
605
461
  }
606
462
  // Enhancement 8: Get entities flagged for review
607
463
  async getFlaggedEntities() {
608
- const graph = await this.loadGraph();
464
+ const graph = await this.storage.loadGraph();
609
465
  return graph.entities.filter(e => e.observations.some(obs => obs.content.includes('[FLAGGED FOR REVIEW:')));
610
466
  }
611
467
  // Enhancement 9: Get context (entities related to a topic/entity)
612
468
  async getContext(entityNames, depth = 1) {
613
- const graph = await this.loadGraph();
469
+ const graph = await this.storage.loadGraph();
614
470
  const contextEntityNames = new Set(entityNames);
615
471
  // Expand to include related entities up to specified depth
616
472
  for (let d = 0; d < depth; d++) {
@@ -635,7 +491,7 @@ export class KnowledgeGraphManager {
635
491
  }
636
492
  // Enhancement 10: List conversations (agent threads)
637
493
  async listConversations() {
638
- const graph = await this.loadGraph();
494
+ const graph = await this.storage.loadGraph();
639
495
  // Group data by agent thread
640
496
  const threadMap = new Map();
641
497
  // Collect entities by thread
@@ -673,7 +529,7 @@ export class KnowledgeGraphManager {
673
529
  }
674
530
  // Analytics: Get analytics for a specific thread (limited to 4 core metrics)
675
531
  async getAnalytics(threadId) {
676
- const graph = await this.loadGraph();
532
+ const graph = await this.storage.loadGraph();
677
533
  // Filter to thread-specific data
678
534
  const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
679
535
  const threadRelations = graph.relations.filter(r => r.agentThreadId === threadId);
@@ -756,7 +612,7 @@ export class KnowledgeGraphManager {
756
612
  }
757
613
  // Observation Versioning: Get full history chain for an observation
758
614
  async getObservationHistory(entityName, observationId) {
759
- const graph = await this.loadGraph();
615
+ const graph = await this.storage.loadGraph();
760
616
  // Find the entity
761
617
  const entity = graph.entities.find(e => e.name === entityName);
762
618
  if (!entity) {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Neo4j Storage Adapter (Skeleton Implementation)
3
+ *
4
+ * This is a skeleton implementation showing how a Neo4j storage adapter could be built.
5
+ * To use this in production, you would need to:
6
+ * 1. Install neo4j-driver: npm install neo4j-driver
7
+ * 2. Implement the actual Cypher queries
8
+ * 3. Add error handling and connection management
9
+ * 4. Add transaction support for atomic operations
10
+ *
11
+ * Example usage:
12
+ * ```typescript
13
+ * import { Neo4jStorageAdapter } from './neo4j-storage-adapter.js';
14
+ * import { KnowledgeGraphManager } from './knowledge-graph-manager.js';
15
+ *
16
+ * const neo4jAdapter = new Neo4jStorageAdapter({
17
+ * uri: 'neo4j://localhost:7687',
18
+ * username: 'neo4j',
19
+ * password: 'password'
20
+ * });
21
+ *
22
+ * await neo4jAdapter.initialize();
23
+ * const manager = new KnowledgeGraphManager('', neo4jAdapter);
24
+ * ```
25
+ */
26
+ /**
27
+ * Neo4j-based storage adapter for the knowledge graph
28
+ * This is a skeleton implementation - requires neo4j-driver package
29
+ */
30
+ export class Neo4jStorageAdapter {
31
+ config;
32
+ // private driver: any; // Would be neo4j.Driver from neo4j-driver package
33
+ constructor(config) {
34
+ this.config = config;
35
+ }
36
+ /**
37
+ * Initialize Neo4j connection
38
+ */
39
+ async initialize() {
40
+ // TODO: Initialize Neo4j driver
41
+ // this.driver = neo4j.driver(
42
+ // this.config.uri,
43
+ // neo4j.auth.basic(this.config.username, this.config.password)
44
+ // );
45
+ // TODO: Verify connectivity
46
+ // await this.driver.verifyConnectivity();
47
+ // TODO: Create constraints and indexes
48
+ // const session = this.driver.session({ database: this.config.database });
49
+ // try {
50
+ // await session.run('CREATE CONSTRAINT IF NOT EXISTS FOR (e:Entity) REQUIRE e.name IS UNIQUE');
51
+ // await session.run('CREATE INDEX IF NOT EXISTS FOR (e:Entity) ON (e.entityType)');
52
+ // await session.run('CREATE INDEX IF NOT EXISTS FOR (e:Entity) ON (e.agentThreadId)');
53
+ // } finally {
54
+ // await session.close();
55
+ // }
56
+ throw new Error('Neo4jStorageAdapter requires neo4j-driver package to be installed and methods to be implemented. See STORAGE.md documentation for setup instructions.');
57
+ }
58
+ /**
59
+ * Load the complete knowledge graph from Neo4j
60
+ */
61
+ async loadGraph() {
62
+ // TODO: Implement Cypher query to load all entities and relations
63
+ // Example Cypher for entities:
64
+ // MATCH (e:Entity)
65
+ // RETURN e.name as name,
66
+ // e.entityType as entityType,
67
+ // e.observations as observations,
68
+ // e.agentThreadId as agentThreadId,
69
+ // e.timestamp as timestamp,
70
+ // e.confidence as confidence,
71
+ // e.importance as importance
72
+ // Example Cypher for relations:
73
+ // MATCH (from:Entity)-[r:RELATES_TO]->(to:Entity)
74
+ // RETURN from.name as from,
75
+ // to.name as to,
76
+ // r.relationType as relationType,
77
+ // r.agentThreadId as agentThreadId,
78
+ // r.timestamp as timestamp,
79
+ // r.confidence as confidence,
80
+ // r.importance as importance
81
+ throw new Error('Neo4jStorageAdapter.loadGraph() is not implemented. This is a skeleton - install neo4j-driver and implement the Cypher queries shown in comments above.');
82
+ }
83
+ /**
84
+ * Save the complete knowledge graph to Neo4j
85
+ */
86
+ async saveGraph(graph) {
87
+ // TODO: Implement transactional save
88
+ // This should:
89
+ // 1. Start a transaction
90
+ // 2. Delete all existing nodes and relationships (or use MERGE for upsert)
91
+ // 3. Create all entities as nodes
92
+ // 4. Create all relations as relationships
93
+ // 5. Commit the transaction
94
+ // Example Cypher for creating entity:
95
+ // MERGE (e:Entity {name: $name})
96
+ // SET e.entityType = $entityType,
97
+ // e.observations = $observations,
98
+ // e.agentThreadId = $agentThreadId,
99
+ // e.timestamp = $timestamp,
100
+ // e.confidence = $confidence,
101
+ // e.importance = $importance
102
+ // Example Cypher for creating relation:
103
+ // MATCH (from:Entity {name: $from})
104
+ // MATCH (to:Entity {name: $to})
105
+ // MERGE (from)-[r:RELATES_TO {relationType: $relationType}]->(to)
106
+ // SET r.agentThreadId = $agentThreadId,
107
+ // r.timestamp = $timestamp,
108
+ // r.confidence = $confidence,
109
+ // r.importance = $importance
110
+ throw new Error('Neo4jStorageAdapter.saveGraph() is not implemented. This is a skeleton - install neo4j-driver and implement the transactional save logic shown in comments above.');
111
+ }
112
+ /**
113
+ * Close Neo4j connection
114
+ */
115
+ async close() {
116
+ // TODO: Close the driver
117
+ // if (this.driver) {
118
+ // await this.driver.close();
119
+ // }
120
+ }
121
+ }
122
+ /**
123
+ * Example of how this adapter could be used:
124
+ *
125
+ * const neo4jAdapter = new Neo4jStorageAdapter({
126
+ * uri: 'neo4j://localhost:7687',
127
+ * username: 'neo4j',
128
+ * password: 'password',
129
+ * database: 'knowledge-graph'
130
+ * });
131
+ *
132
+ * await neo4jAdapter.initialize();
133
+ *
134
+ * const manager = new KnowledgeGraphManager('', neo4jAdapter);
135
+ *
136
+ * // Use the manager as normal - all operations will now use Neo4j
137
+ * await manager.createEntities([...]);
138
+ * const graph = await manager.readGraph();
139
+ *
140
+ * // Clean up when done
141
+ * await neo4jAdapter.close();
142
+ */
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Storage interface abstraction for the Knowledge Graph
3
+ * This allows for different storage backends (JSONL, Neo4j, etc.)
4
+ */
5
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "server-memory-enhanced",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Enhanced MCP server for memory with agent threading, timestamps, and confidence scoring",
5
5
  "license": "MIT",
6
6
  "mcpName": "io.github.modelcontextprotocol/server-memory-enhanced",