server-memory-enhanced 0.2.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 +83 -659
- package/dist/lib/constants.js +55 -0
- package/dist/lib/jsonl-storage-adapter.js +315 -0
- package/dist/lib/knowledge-graph-manager.js +666 -0
- package/dist/lib/neo4j-storage-adapter.js +142 -0
- package/dist/lib/relation-inverter.js +59 -0
- package/dist/lib/save-memory-handler.js +100 -0
- package/dist/lib/schemas.js +102 -0
- package/dist/lib/storage-interface.js +5 -0
- package/dist/lib/types.js +4 -0
- package/dist/lib/validation.js +151 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ import { z } from "zod";
|
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
+
import { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
|
|
9
|
+
import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema } from './lib/schemas.js';
|
|
10
|
+
import { handleSaveMemory } from './lib/save-memory-handler.js';
|
|
8
11
|
// Define memory directory path using environment variable with fallback
|
|
9
12
|
export const defaultMemoryDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory-data');
|
|
10
13
|
export async function ensureMemoryDirectory() {
|
|
@@ -24,661 +27,56 @@ export async function ensureMemoryDirectory() {
|
|
|
24
27
|
}
|
|
25
28
|
// Initialize memory directory path (will be set during startup)
|
|
26
29
|
let MEMORY_DIR_PATH;
|
|
27
|
-
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
static NEGATION_WORDS = new Set(['not', 'no', 'never', 'neither', 'none', 'doesn\'t', 'don\'t', 'isn\'t', 'aren\'t']);
|
|
31
|
-
constructor(memoryDirPath) {
|
|
32
|
-
this.memoryDirPath = memoryDirPath;
|
|
33
|
-
}
|
|
34
|
-
getThreadFilePath(agentThreadId) {
|
|
35
|
-
return path.join(this.memoryDirPath, `thread-${agentThreadId}.jsonl`);
|
|
36
|
-
}
|
|
37
|
-
async loadGraphFromFile(filePath) {
|
|
38
|
-
try {
|
|
39
|
-
const data = await fs.readFile(filePath, "utf-8");
|
|
40
|
-
const lines = data.split("\n").filter(line => line.trim() !== "");
|
|
41
|
-
return lines.reduce((graph, line) => {
|
|
42
|
-
let item;
|
|
43
|
-
try {
|
|
44
|
-
item = JSON.parse(line);
|
|
45
|
-
}
|
|
46
|
-
catch (parseError) {
|
|
47
|
-
console.warn(`Skipping malformed JSON line in ${filePath} (line length: ${line.length} chars)`);
|
|
48
|
-
return graph;
|
|
49
|
-
}
|
|
50
|
-
if (item.type === "entity") {
|
|
51
|
-
// Validate required fields
|
|
52
|
-
if (!item.name || !item.entityType || !Array.isArray(item.observations) ||
|
|
53
|
-
!item.agentThreadId || !item.timestamp ||
|
|
54
|
-
typeof item.confidence !== 'number' || typeof item.importance !== 'number') {
|
|
55
|
-
console.warn(`Skipping entity with missing required fields in ${filePath}`);
|
|
56
|
-
return graph;
|
|
57
|
-
}
|
|
58
|
-
graph.entities.push({
|
|
59
|
-
name: item.name,
|
|
60
|
-
entityType: item.entityType,
|
|
61
|
-
observations: item.observations,
|
|
62
|
-
agentThreadId: item.agentThreadId,
|
|
63
|
-
timestamp: item.timestamp,
|
|
64
|
-
confidence: item.confidence,
|
|
65
|
-
importance: item.importance
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
if (item.type === "relation") {
|
|
69
|
-
// Validate required fields
|
|
70
|
-
if (!item.from || !item.to || !item.relationType ||
|
|
71
|
-
!item.agentThreadId || !item.timestamp ||
|
|
72
|
-
typeof item.confidence !== 'number' || typeof item.importance !== 'number') {
|
|
73
|
-
console.warn(`Skipping relation with missing required fields in ${filePath}`);
|
|
74
|
-
return graph;
|
|
75
|
-
}
|
|
76
|
-
graph.relations.push({
|
|
77
|
-
from: item.from,
|
|
78
|
-
to: item.to,
|
|
79
|
-
relationType: item.relationType,
|
|
80
|
-
agentThreadId: item.agentThreadId,
|
|
81
|
-
timestamp: item.timestamp,
|
|
82
|
-
confidence: item.confidence,
|
|
83
|
-
importance: item.importance
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
return graph;
|
|
87
|
-
}, { entities: [], relations: [] });
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
|
|
91
|
-
return { entities: [], relations: [] };
|
|
92
|
-
}
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async loadGraph() {
|
|
97
|
-
const files = await fs.readdir(this.memoryDirPath).catch(() => []);
|
|
98
|
-
const threadFiles = files.filter(f => f.startsWith('thread-') && f.endsWith('.jsonl'));
|
|
99
|
-
const graphs = await Promise.all(threadFiles.map(f => this.loadGraphFromFile(path.join(this.memoryDirPath, f))));
|
|
100
|
-
return graphs.reduce((acc, graph) => ({
|
|
101
|
-
entities: [...acc.entities, ...graph.entities],
|
|
102
|
-
relations: [...acc.relations, ...graph.relations]
|
|
103
|
-
}), { entities: [], relations: [] });
|
|
104
|
-
}
|
|
105
|
-
async saveGraphForThread(agentThreadId, entities, relations) {
|
|
106
|
-
const threadFilePath = this.getThreadFilePath(agentThreadId);
|
|
107
|
-
const lines = [
|
|
108
|
-
...entities.map(e => JSON.stringify({
|
|
109
|
-
type: "entity",
|
|
110
|
-
name: e.name,
|
|
111
|
-
entityType: e.entityType,
|
|
112
|
-
observations: e.observations,
|
|
113
|
-
agentThreadId: e.agentThreadId,
|
|
114
|
-
timestamp: e.timestamp,
|
|
115
|
-
confidence: e.confidence,
|
|
116
|
-
importance: e.importance
|
|
117
|
-
})),
|
|
118
|
-
...relations.map(r => JSON.stringify({
|
|
119
|
-
type: "relation",
|
|
120
|
-
from: r.from,
|
|
121
|
-
to: r.to,
|
|
122
|
-
relationType: r.relationType,
|
|
123
|
-
agentThreadId: r.agentThreadId,
|
|
124
|
-
timestamp: r.timestamp,
|
|
125
|
-
confidence: r.confidence,
|
|
126
|
-
importance: r.importance
|
|
127
|
-
})),
|
|
128
|
-
];
|
|
129
|
-
// Avoid creating or keeping empty files when there is no data for this thread
|
|
130
|
-
if (lines.length === 0) {
|
|
131
|
-
try {
|
|
132
|
-
await fs.unlink(threadFilePath);
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
// Only ignore ENOENT errors (file doesn't exist)
|
|
136
|
-
if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
|
|
137
|
-
console.warn(`Failed to delete empty thread file ${threadFilePath}:`, error);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
await fs.writeFile(threadFilePath, lines.join("\n"));
|
|
143
|
-
}
|
|
144
|
-
async saveGraph(graph) {
|
|
145
|
-
// Group entities and relations by agentThreadId
|
|
146
|
-
const threadMap = new Map();
|
|
147
|
-
for (const entity of graph.entities) {
|
|
148
|
-
if (!threadMap.has(entity.agentThreadId)) {
|
|
149
|
-
threadMap.set(entity.agentThreadId, { entities: [], relations: [] });
|
|
150
|
-
}
|
|
151
|
-
threadMap.get(entity.agentThreadId).entities.push(entity);
|
|
152
|
-
}
|
|
153
|
-
for (const relation of graph.relations) {
|
|
154
|
-
if (!threadMap.has(relation.agentThreadId)) {
|
|
155
|
-
threadMap.set(relation.agentThreadId, { entities: [], relations: [] });
|
|
156
|
-
}
|
|
157
|
-
threadMap.get(relation.agentThreadId).relations.push(relation);
|
|
158
|
-
}
|
|
159
|
-
// Save each thread's data to its own file
|
|
160
|
-
await Promise.all(Array.from(threadMap.entries()).map(([threadId, data]) => this.saveGraphForThread(threadId, data.entities, data.relations)));
|
|
161
|
-
// Clean up stale thread files that no longer have data
|
|
162
|
-
try {
|
|
163
|
-
const files = await fs.readdir(this.memoryDirPath).catch(() => []);
|
|
164
|
-
const threadFiles = files.filter(f => f.startsWith('thread-') && f.endsWith('.jsonl'));
|
|
165
|
-
await Promise.all(threadFiles.map(async (fileName) => {
|
|
166
|
-
// Extract threadId from filename: thread-{agentThreadId}.jsonl
|
|
167
|
-
const match = fileName.match(/^thread-(.+)\.jsonl$/);
|
|
168
|
-
if (match) {
|
|
169
|
-
const threadId = match[1];
|
|
170
|
-
if (!threadMap.has(threadId)) {
|
|
171
|
-
const filePath = path.join(this.memoryDirPath, fileName);
|
|
172
|
-
try {
|
|
173
|
-
await fs.unlink(filePath);
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
// Only log non-ENOENT errors
|
|
177
|
-
if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
|
|
178
|
-
console.warn(`Failed to delete stale thread file ${filePath}:`, error);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}));
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
// Best-effort cleanup: log but don't fail the save operation
|
|
187
|
-
console.warn('Failed to clean up stale thread files:', error);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
async createEntities(entities) {
|
|
191
|
-
const graph = await this.loadGraph();
|
|
192
|
-
// Entity names are globally unique across all threads in the collaborative knowledge graph
|
|
193
|
-
// This prevents duplicate entities while allowing multiple threads to contribute to the same entity
|
|
194
|
-
const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
|
|
195
|
-
graph.entities.push(...newEntities);
|
|
196
|
-
await this.saveGraph(graph);
|
|
197
|
-
return newEntities;
|
|
198
|
-
}
|
|
199
|
-
async createRelations(relations) {
|
|
200
|
-
const graph = await this.loadGraph();
|
|
201
|
-
// Validate that referenced entities exist
|
|
202
|
-
const entityNames = new Set(graph.entities.map(e => e.name));
|
|
203
|
-
const validRelations = relations.filter(r => {
|
|
204
|
-
if (!entityNames.has(r.from) || !entityNames.has(r.to)) {
|
|
205
|
-
console.warn(`Skipping relation ${r.from} -> ${r.to}: one or both entities do not exist`);
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
return true;
|
|
209
|
-
});
|
|
210
|
-
// Relations are globally unique by (from, to, relationType) across all threads
|
|
211
|
-
// This enables multiple threads to collaboratively build the knowledge graph
|
|
212
|
-
const newRelations = validRelations.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
|
|
213
|
-
existingRelation.to === r.to &&
|
|
214
|
-
existingRelation.relationType === r.relationType));
|
|
215
|
-
graph.relations.push(...newRelations);
|
|
216
|
-
await this.saveGraph(graph);
|
|
217
|
-
return newRelations;
|
|
218
|
-
}
|
|
219
|
-
async addObservations(observations) {
|
|
220
|
-
const graph = await this.loadGraph();
|
|
221
|
-
const results = observations.map(o => {
|
|
222
|
-
const entity = graph.entities.find(e => e.name === o.entityName);
|
|
223
|
-
if (!entity) {
|
|
224
|
-
throw new Error(`Entity with name ${o.entityName} not found`);
|
|
225
|
-
}
|
|
226
|
-
const newObservations = o.contents.filter(content => !entity.observations.includes(content));
|
|
227
|
-
entity.observations.push(...newObservations);
|
|
228
|
-
// Update metadata based on this operation, but keep original agentThreadId
|
|
229
|
-
// to maintain thread file consistency and avoid orphaned data
|
|
230
|
-
entity.timestamp = o.timestamp;
|
|
231
|
-
entity.confidence = o.confidence;
|
|
232
|
-
entity.importance = o.importance;
|
|
233
|
-
return { entityName: o.entityName, addedObservations: newObservations };
|
|
234
|
-
});
|
|
235
|
-
await this.saveGraph(graph);
|
|
236
|
-
return results;
|
|
237
|
-
}
|
|
238
|
-
async deleteEntities(entityNames) {
|
|
239
|
-
const graph = await this.loadGraph();
|
|
240
|
-
graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
|
|
241
|
-
graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
|
|
242
|
-
await this.saveGraph(graph);
|
|
243
|
-
}
|
|
244
|
-
async deleteObservations(deletions) {
|
|
245
|
-
const graph = await this.loadGraph();
|
|
246
|
-
deletions.forEach(d => {
|
|
247
|
-
const entity = graph.entities.find(e => e.name === d.entityName);
|
|
248
|
-
if (entity) {
|
|
249
|
-
entity.observations = entity.observations.filter(o => !d.observations.includes(o));
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
await this.saveGraph(graph);
|
|
253
|
-
}
|
|
254
|
-
async deleteRelations(relations) {
|
|
255
|
-
const graph = await this.loadGraph();
|
|
256
|
-
// Delete relations globally across all threads by matching (from, to, relationType)
|
|
257
|
-
// In a collaborative knowledge graph, deletions affect all threads
|
|
258
|
-
graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
|
|
259
|
-
r.to === delRelation.to &&
|
|
260
|
-
r.relationType === delRelation.relationType));
|
|
261
|
-
await this.saveGraph(graph);
|
|
262
|
-
}
|
|
263
|
-
async readGraph() {
|
|
264
|
-
return this.loadGraph();
|
|
265
|
-
}
|
|
266
|
-
async searchNodes(query) {
|
|
267
|
-
const graph = await this.loadGraph();
|
|
268
|
-
// Filter entities
|
|
269
|
-
const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
270
|
-
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
|
|
271
|
-
e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())));
|
|
272
|
-
// Create a Set of filtered entity names for quick lookup
|
|
273
|
-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
274
|
-
// Filter relations to only include those between filtered entities
|
|
275
|
-
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
276
|
-
const filteredGraph = {
|
|
277
|
-
entities: filteredEntities,
|
|
278
|
-
relations: filteredRelations,
|
|
279
|
-
};
|
|
280
|
-
return filteredGraph;
|
|
281
|
-
}
|
|
282
|
-
async openNodes(names) {
|
|
283
|
-
const graph = await this.loadGraph();
|
|
284
|
-
// Filter entities
|
|
285
|
-
const filteredEntities = graph.entities.filter(e => names.includes(e.name));
|
|
286
|
-
// Create a Set of filtered entity names for quick lookup
|
|
287
|
-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
288
|
-
// Filter relations to only include those between filtered entities
|
|
289
|
-
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
290
|
-
const filteredGraph = {
|
|
291
|
-
entities: filteredEntities,
|
|
292
|
-
relations: filteredRelations,
|
|
293
|
-
};
|
|
294
|
-
return filteredGraph;
|
|
295
|
-
}
|
|
296
|
-
async queryNodes(filters) {
|
|
297
|
-
const graph = await this.loadGraph();
|
|
298
|
-
// If no filters provided, return entire graph
|
|
299
|
-
if (!filters) {
|
|
300
|
-
return graph;
|
|
301
|
-
}
|
|
302
|
-
// Apply filters to entities
|
|
303
|
-
const filteredEntities = graph.entities.filter(e => {
|
|
304
|
-
// Timestamp range filter
|
|
305
|
-
if (filters.timestampStart && e.timestamp < filters.timestampStart)
|
|
306
|
-
return false;
|
|
307
|
-
if (filters.timestampEnd && e.timestamp > filters.timestampEnd)
|
|
308
|
-
return false;
|
|
309
|
-
// Confidence range filter
|
|
310
|
-
if (filters.confidenceMin !== undefined && e.confidence < filters.confidenceMin)
|
|
311
|
-
return false;
|
|
312
|
-
if (filters.confidenceMax !== undefined && e.confidence > filters.confidenceMax)
|
|
313
|
-
return false;
|
|
314
|
-
// Importance range filter
|
|
315
|
-
if (filters.importanceMin !== undefined && e.importance < filters.importanceMin)
|
|
316
|
-
return false;
|
|
317
|
-
if (filters.importanceMax !== undefined && e.importance > filters.importanceMax)
|
|
318
|
-
return false;
|
|
319
|
-
return true;
|
|
320
|
-
});
|
|
321
|
-
// Create a Set of filtered entity names for quick lookup
|
|
322
|
-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
323
|
-
// Apply filters to relations (and ensure they connect filtered entities)
|
|
324
|
-
const filteredRelations = graph.relations.filter(r => {
|
|
325
|
-
// Must connect filtered entities
|
|
326
|
-
if (!filteredEntityNames.has(r.from) || !filteredEntityNames.has(r.to))
|
|
327
|
-
return false;
|
|
328
|
-
// Timestamp range filter
|
|
329
|
-
if (filters.timestampStart && r.timestamp < filters.timestampStart)
|
|
330
|
-
return false;
|
|
331
|
-
if (filters.timestampEnd && r.timestamp > filters.timestampEnd)
|
|
332
|
-
return false;
|
|
333
|
-
// Confidence range filter
|
|
334
|
-
if (filters.confidenceMin !== undefined && r.confidence < filters.confidenceMin)
|
|
335
|
-
return false;
|
|
336
|
-
if (filters.confidenceMax !== undefined && r.confidence > filters.confidenceMax)
|
|
337
|
-
return false;
|
|
338
|
-
// Importance range filter
|
|
339
|
-
if (filters.importanceMin !== undefined && r.importance < filters.importanceMin)
|
|
340
|
-
return false;
|
|
341
|
-
if (filters.importanceMax !== undefined && r.importance > filters.importanceMax)
|
|
342
|
-
return false;
|
|
343
|
-
return true;
|
|
344
|
-
});
|
|
345
|
-
return {
|
|
346
|
-
entities: filteredEntities,
|
|
347
|
-
relations: filteredRelations,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
// Enhancement 1: Memory Statistics & Insights
|
|
351
|
-
async getMemoryStats() {
|
|
352
|
-
const graph = await this.loadGraph();
|
|
353
|
-
// Count entity types
|
|
354
|
-
const entityTypes = {};
|
|
355
|
-
graph.entities.forEach(e => {
|
|
356
|
-
entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
|
|
357
|
-
});
|
|
358
|
-
// Calculate averages
|
|
359
|
-
const avgConfidence = graph.entities.length > 0
|
|
360
|
-
? graph.entities.reduce((sum, e) => sum + e.confidence, 0) / graph.entities.length
|
|
361
|
-
: 0;
|
|
362
|
-
const avgImportance = graph.entities.length > 0
|
|
363
|
-
? graph.entities.reduce((sum, e) => sum + e.importance, 0) / graph.entities.length
|
|
364
|
-
: 0;
|
|
365
|
-
// Count unique threads
|
|
366
|
-
const threads = new Set([
|
|
367
|
-
...graph.entities.map(e => e.agentThreadId),
|
|
368
|
-
...graph.relations.map(r => r.agentThreadId)
|
|
369
|
-
]);
|
|
370
|
-
// Recent activity (last 7 days, grouped by day)
|
|
371
|
-
const now = new Date();
|
|
372
|
-
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
373
|
-
const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
|
|
374
|
-
// Group by day
|
|
375
|
-
const activityByDay = {};
|
|
376
|
-
recentEntities.forEach(e => {
|
|
377
|
-
const day = e.timestamp.substring(0, 10); // YYYY-MM-DD
|
|
378
|
-
activityByDay[day] = (activityByDay[day] || 0) + 1;
|
|
379
|
-
});
|
|
380
|
-
const recentActivity = Object.entries(activityByDay)
|
|
381
|
-
.map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
|
|
382
|
-
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
383
|
-
return {
|
|
384
|
-
entityCount: graph.entities.length,
|
|
385
|
-
relationCount: graph.relations.length,
|
|
386
|
-
threadCount: threads.size,
|
|
387
|
-
entityTypes,
|
|
388
|
-
avgConfidence,
|
|
389
|
-
avgImportance,
|
|
390
|
-
recentActivity
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
// Enhancement 2: Get recent changes
|
|
394
|
-
async getRecentChanges(since) {
|
|
395
|
-
const graph = await this.loadGraph();
|
|
396
|
-
const sinceDate = new Date(since);
|
|
397
|
-
// Only return entities and relations that were actually modified since the specified time
|
|
398
|
-
const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sinceDate);
|
|
399
|
-
const recentEntityNames = new Set(recentEntities.map(e => e.name));
|
|
400
|
-
// Only include relations that are recent themselves
|
|
401
|
-
const recentRelations = graph.relations.filter(r => new Date(r.timestamp) >= sinceDate);
|
|
402
|
-
return {
|
|
403
|
-
entities: recentEntities,
|
|
404
|
-
relations: recentRelations
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
// Enhancement 3: Relationship path finding
|
|
408
|
-
async findRelationPath(from, to, maxDepth = 5) {
|
|
409
|
-
const graph = await this.loadGraph();
|
|
410
|
-
if (from === to) {
|
|
411
|
-
return { found: true, path: [from], relations: [] };
|
|
412
|
-
}
|
|
413
|
-
// BFS to find shortest path
|
|
414
|
-
const queue = [
|
|
415
|
-
{ entity: from, path: [from], relations: [] }
|
|
416
|
-
];
|
|
417
|
-
const visited = new Set([from]);
|
|
418
|
-
while (queue.length > 0) {
|
|
419
|
-
const current = queue.shift();
|
|
420
|
-
if (current.path.length > maxDepth) {
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
// Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
|
|
424
|
-
const outgoing = graph.relations.filter(r => r.from === current.entity);
|
|
425
|
-
const incoming = graph.relations.filter(r => r.to === current.entity);
|
|
426
|
-
// Check outgoing relations
|
|
427
|
-
for (const rel of outgoing) {
|
|
428
|
-
if (rel.to === to) {
|
|
429
|
-
return {
|
|
430
|
-
found: true,
|
|
431
|
-
path: [...current.path, rel.to],
|
|
432
|
-
relations: [...current.relations, rel]
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
if (!visited.has(rel.to)) {
|
|
436
|
-
visited.add(rel.to);
|
|
437
|
-
queue.push({
|
|
438
|
-
entity: rel.to,
|
|
439
|
-
path: [...current.path, rel.to],
|
|
440
|
-
relations: [...current.relations, rel]
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// Check incoming relations (traverse backwards)
|
|
445
|
-
for (const rel of incoming) {
|
|
446
|
-
if (rel.from === to) {
|
|
447
|
-
return {
|
|
448
|
-
found: true,
|
|
449
|
-
path: [...current.path, rel.from],
|
|
450
|
-
relations: [...current.relations, rel]
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
if (!visited.has(rel.from)) {
|
|
454
|
-
visited.add(rel.from);
|
|
455
|
-
queue.push({
|
|
456
|
-
entity: rel.from,
|
|
457
|
-
path: [...current.path, rel.from],
|
|
458
|
-
relations: [...current.relations, rel]
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
return { found: false, path: [], relations: [] };
|
|
464
|
-
}
|
|
465
|
-
// Enhancement 4: Detect conflicting observations
|
|
466
|
-
async detectConflicts() {
|
|
467
|
-
const graph = await this.loadGraph();
|
|
468
|
-
const conflicts = [];
|
|
469
|
-
for (const entity of graph.entities) {
|
|
470
|
-
const entityConflicts = [];
|
|
471
|
-
for (let i = 0; i < entity.observations.length; i++) {
|
|
472
|
-
for (let j = i + 1; j < entity.observations.length; j++) {
|
|
473
|
-
const obs1 = entity.observations[i].toLowerCase();
|
|
474
|
-
const obs2 = entity.observations[j].toLowerCase();
|
|
475
|
-
// Check for negation patterns
|
|
476
|
-
const obs1HasNegation = Array.from(KnowledgeGraphManager.NEGATION_WORDS).some(word => obs1.includes(word));
|
|
477
|
-
const obs2HasNegation = Array.from(KnowledgeGraphManager.NEGATION_WORDS).some(word => obs2.includes(word));
|
|
478
|
-
// If one has negation and they share key words, might be a conflict
|
|
479
|
-
if (obs1HasNegation !== obs2HasNegation) {
|
|
480
|
-
const words1 = obs1.split(/\s+/).filter(w => w.length > 3);
|
|
481
|
-
const words2Set = new Set(obs2.split(/\s+/).filter(w => w.length > 3));
|
|
482
|
-
const commonWords = words1.filter(w => words2Set.has(w) && !KnowledgeGraphManager.NEGATION_WORDS.has(w));
|
|
483
|
-
if (commonWords.length >= 2) {
|
|
484
|
-
entityConflicts.push({
|
|
485
|
-
obs1: entity.observations[i],
|
|
486
|
-
obs2: entity.observations[j],
|
|
487
|
-
reason: 'Potential contradiction with negation'
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
if (entityConflicts.length > 0) {
|
|
494
|
-
conflicts.push({ entityName: entity.name, conflicts: entityConflicts });
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return conflicts;
|
|
498
|
-
}
|
|
499
|
-
// Enhancement 5: Memory pruning
|
|
500
|
-
async pruneMemory(options) {
|
|
501
|
-
const graph = await this.loadGraph();
|
|
502
|
-
const initialEntityCount = graph.entities.length;
|
|
503
|
-
const initialRelationCount = graph.relations.length;
|
|
504
|
-
// Filter entities to remove
|
|
505
|
-
let entitiesToKeep = graph.entities;
|
|
506
|
-
if (options.olderThan) {
|
|
507
|
-
const cutoffDate = new Date(options.olderThan);
|
|
508
|
-
entitiesToKeep = entitiesToKeep.filter(e => new Date(e.timestamp) >= cutoffDate);
|
|
509
|
-
}
|
|
510
|
-
if (options.importanceLessThan !== undefined) {
|
|
511
|
-
entitiesToKeep = entitiesToKeep.filter(e => e.importance >= options.importanceLessThan);
|
|
512
|
-
}
|
|
513
|
-
// Ensure we keep minimum entities
|
|
514
|
-
// If keepMinEntities is set and we need more entities, take from the already-filtered set
|
|
515
|
-
// sorted by importance and recency
|
|
516
|
-
if (options.keepMinEntities && entitiesToKeep.length < options.keepMinEntities) {
|
|
517
|
-
// Sort the filtered entities by importance and timestamp, keep the most important and recent
|
|
518
|
-
const sorted = [...entitiesToKeep].sort((a, b) => {
|
|
519
|
-
if (a.importance !== b.importance)
|
|
520
|
-
return b.importance - a.importance;
|
|
521
|
-
return b.timestamp.localeCompare(a.timestamp);
|
|
522
|
-
});
|
|
523
|
-
// If we still don't have enough, we keep what we have
|
|
524
|
-
entitiesToKeep = sorted.slice(0, Math.min(options.keepMinEntities, sorted.length));
|
|
525
|
-
}
|
|
526
|
-
const keptEntityNames = new Set(entitiesToKeep.map(e => e.name));
|
|
527
|
-
// Remove relations that reference removed entities
|
|
528
|
-
const relationsToKeep = graph.relations.filter(r => keptEntityNames.has(r.from) && keptEntityNames.has(r.to));
|
|
529
|
-
graph.entities = entitiesToKeep;
|
|
530
|
-
graph.relations = relationsToKeep;
|
|
531
|
-
await this.saveGraph(graph);
|
|
532
|
-
return {
|
|
533
|
-
removedEntities: initialEntityCount - entitiesToKeep.length,
|
|
534
|
-
removedRelations: initialRelationCount - relationsToKeep.length
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
// Enhancement 6: Batch operations
|
|
538
|
-
async bulkUpdate(updates) {
|
|
539
|
-
const graph = await this.loadGraph();
|
|
540
|
-
let updated = 0;
|
|
541
|
-
const notFound = [];
|
|
542
|
-
for (const update of updates) {
|
|
543
|
-
const entity = graph.entities.find(e => e.name === update.entityName);
|
|
544
|
-
if (!entity) {
|
|
545
|
-
notFound.push(update.entityName);
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
if (update.confidence !== undefined) {
|
|
549
|
-
entity.confidence = update.confidence;
|
|
550
|
-
}
|
|
551
|
-
if (update.importance !== undefined) {
|
|
552
|
-
entity.importance = update.importance;
|
|
553
|
-
}
|
|
554
|
-
if (update.addObservations) {
|
|
555
|
-
const newObs = update.addObservations.filter(obs => !entity.observations.includes(obs));
|
|
556
|
-
entity.observations.push(...newObs);
|
|
557
|
-
}
|
|
558
|
-
entity.timestamp = new Date().toISOString();
|
|
559
|
-
updated++;
|
|
560
|
-
}
|
|
561
|
-
await this.saveGraph(graph);
|
|
562
|
-
return { updated, notFound };
|
|
563
|
-
}
|
|
564
|
-
// Enhancement 7: Flag for review (Human-in-the-Loop)
|
|
565
|
-
async flagForReview(entityName, reason, reviewer) {
|
|
566
|
-
const graph = await this.loadGraph();
|
|
567
|
-
const entity = graph.entities.find(e => e.name === entityName);
|
|
568
|
-
if (!entity) {
|
|
569
|
-
throw new Error(`Entity with name ${entityName} not found`);
|
|
570
|
-
}
|
|
571
|
-
// Add a special observation to mark for review
|
|
572
|
-
const flagObservation = `[FLAGGED FOR REVIEW: ${reason}${reviewer ? ` - Reviewer: ${reviewer}` : ''}]`;
|
|
573
|
-
if (!entity.observations.includes(flagObservation)) {
|
|
574
|
-
entity.observations.push(flagObservation);
|
|
575
|
-
entity.timestamp = new Date().toISOString();
|
|
576
|
-
await this.saveGraph(graph);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
// Enhancement 8: Get entities flagged for review
|
|
580
|
-
async getFlaggedEntities() {
|
|
581
|
-
const graph = await this.loadGraph();
|
|
582
|
-
return graph.entities.filter(e => e.observations.some(obs => obs.includes('[FLAGGED FOR REVIEW:')));
|
|
583
|
-
}
|
|
584
|
-
// Enhancement 9: Get context (entities related to a topic/entity)
|
|
585
|
-
async getContext(entityNames, depth = 1) {
|
|
586
|
-
const graph = await this.loadGraph();
|
|
587
|
-
const contextEntityNames = new Set(entityNames);
|
|
588
|
-
// Expand to include related entities up to specified depth
|
|
589
|
-
for (let d = 0; d < depth; d++) {
|
|
590
|
-
const currentEntities = Array.from(contextEntityNames);
|
|
591
|
-
for (const entityName of currentEntities) {
|
|
592
|
-
// Find all relations involving this entity
|
|
593
|
-
const relatedRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
|
|
594
|
-
// Add related entities
|
|
595
|
-
relatedRelations.forEach(r => {
|
|
596
|
-
contextEntityNames.add(r.from);
|
|
597
|
-
contextEntityNames.add(r.to);
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
// Get all entities and relations in context
|
|
602
|
-
const contextEntities = graph.entities.filter(e => contextEntityNames.has(e.name));
|
|
603
|
-
const contextRelations = graph.relations.filter(r => contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
|
|
604
|
-
return {
|
|
605
|
-
entities: contextEntities,
|
|
606
|
-
relations: contextRelations
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
// Enhancement 10: List conversations (agent threads)
|
|
610
|
-
async listConversations() {
|
|
611
|
-
const graph = await this.loadGraph();
|
|
612
|
-
// Group data by agent thread
|
|
613
|
-
const threadMap = new Map();
|
|
614
|
-
// Collect entities by thread
|
|
615
|
-
for (const entity of graph.entities) {
|
|
616
|
-
if (!threadMap.has(entity.agentThreadId)) {
|
|
617
|
-
threadMap.set(entity.agentThreadId, { entities: [], relations: [], timestamps: [] });
|
|
618
|
-
}
|
|
619
|
-
const threadData = threadMap.get(entity.agentThreadId);
|
|
620
|
-
threadData.entities.push(entity);
|
|
621
|
-
threadData.timestamps.push(entity.timestamp);
|
|
622
|
-
}
|
|
623
|
-
// Collect relations by thread
|
|
624
|
-
for (const relation of graph.relations) {
|
|
625
|
-
if (!threadMap.has(relation.agentThreadId)) {
|
|
626
|
-
threadMap.set(relation.agentThreadId, { entities: [], relations: [], timestamps: [] });
|
|
627
|
-
}
|
|
628
|
-
const threadData = threadMap.get(relation.agentThreadId);
|
|
629
|
-
threadData.relations.push(relation);
|
|
630
|
-
threadData.timestamps.push(relation.timestamp);
|
|
631
|
-
}
|
|
632
|
-
// Build conversation summaries
|
|
633
|
-
const conversations = Array.from(threadMap.entries()).map(([agentThreadId, data]) => {
|
|
634
|
-
const timestamps = data.timestamps.sort((a, b) => a.localeCompare(b));
|
|
635
|
-
return {
|
|
636
|
-
agentThreadId,
|
|
637
|
-
entityCount: data.entities.length,
|
|
638
|
-
relationCount: data.relations.length,
|
|
639
|
-
firstCreated: timestamps[0] || '',
|
|
640
|
-
lastUpdated: timestamps[timestamps.length - 1] || ''
|
|
641
|
-
};
|
|
642
|
-
});
|
|
643
|
-
// Sort by last updated (most recent first)
|
|
644
|
-
conversations.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
|
|
645
|
-
return { conversations };
|
|
646
|
-
}
|
|
647
|
-
}
|
|
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';
|
|
648
33
|
let knowledgeGraphManager;
|
|
649
34
|
// Zod schemas for enhanced entities and relations
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
entityType: z.string().describe("The type of the entity"),
|
|
653
|
-
observations: z.array(z.string()).describe("An array of observation contents associated with the entity"),
|
|
654
|
-
agentThreadId: z.string().describe("The agent thread ID that created this entity"),
|
|
655
|
-
timestamp: z.string().describe("ISO 8601 timestamp of when the entity was created"),
|
|
656
|
-
confidence: z.number().min(0).max(1).describe("Confidence coefficient from 0 to 1"),
|
|
657
|
-
importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
|
|
658
|
-
});
|
|
659
|
-
const RelationSchema = z.object({
|
|
660
|
-
from: z.string().describe("The name of the entity where the relation starts"),
|
|
661
|
-
to: z.string().describe("The name of the entity where the relation ends"),
|
|
662
|
-
relationType: z.string().describe("The type of the relation"),
|
|
663
|
-
agentThreadId: z.string().describe("The agent thread ID that created this relation"),
|
|
664
|
-
timestamp: z.string().describe("ISO 8601 timestamp of when the relation was created"),
|
|
665
|
-
confidence: z.number().min(0).max(1).describe("Confidence coefficient from 0 to 1"),
|
|
666
|
-
importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
|
|
667
|
-
});
|
|
35
|
+
const EntitySchemaCompat = EntitySchema;
|
|
36
|
+
const RelationSchemaCompat = RelationSchema;
|
|
668
37
|
// The server instance and tools exposed to Claude
|
|
669
38
|
const server = new McpServer({
|
|
670
39
|
name: "memory-enhanced-server",
|
|
671
40
|
version: "0.2.0",
|
|
672
41
|
});
|
|
673
|
-
// Register
|
|
42
|
+
// Register NEW save_memory tool (Section 1 of spec - Unified Tool)
|
|
43
|
+
server.registerTool("save_memory", {
|
|
44
|
+
title: "Save Memory",
|
|
45
|
+
description: "Save entities and their relations to memory graph atomically. RULES: 1) Each observation max 150 chars (atomic facts only). 2) Each entity MUST have at least 1 relation. This is the recommended way to create entities and relations.",
|
|
46
|
+
inputSchema: SaveMemoryInputSchema,
|
|
47
|
+
outputSchema: SaveMemoryOutputSchema
|
|
48
|
+
}, async (input) => {
|
|
49
|
+
const result = await handleSaveMemory(input, (entities) => knowledgeGraphManager.createEntities(entities), (relations) => knowledgeGraphManager.createRelations(relations));
|
|
50
|
+
if (result.success) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `✓ Successfully saved ${result.created.entities} entities and ${result.created.relations} relations.\n` +
|
|
55
|
+
`Quality score: ${(result.quality_score * 100).toFixed(1)}%\n` +
|
|
56
|
+
(result.warnings.length > 0 ? `\nWarnings:\n${result.warnings.join('\n')}` : '')
|
|
57
|
+
}],
|
|
58
|
+
structuredContent: result
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
return {
|
|
63
|
+
content: [{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: `✗ Validation failed:\n${result.validation_errors?.join('\n')}`
|
|
66
|
+
}],
|
|
67
|
+
structuredContent: result,
|
|
68
|
+
isError: true
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
674
72
|
server.registerTool("create_entities", {
|
|
675
73
|
title: "Create Entities",
|
|
676
74
|
description: "Create multiple new entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance)",
|
|
677
75
|
inputSchema: {
|
|
678
|
-
entities: z.array(
|
|
76
|
+
entities: z.array(EntitySchemaCompat)
|
|
679
77
|
},
|
|
680
78
|
outputSchema: {
|
|
681
|
-
entities: z.array(
|
|
79
|
+
entities: z.array(EntitySchemaCompat)
|
|
682
80
|
}
|
|
683
81
|
}, async ({ entities }) => {
|
|
684
82
|
const result = await knowledgeGraphManager.createEntities(entities);
|
|
@@ -692,10 +90,10 @@ server.registerTool("create_relations", {
|
|
|
692
90
|
title: "Create Relations",
|
|
693
91
|
description: "Create multiple new relations between entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance). Relations should be in active voice",
|
|
694
92
|
inputSchema: {
|
|
695
|
-
relations: z.array(
|
|
93
|
+
relations: z.array(RelationSchemaCompat)
|
|
696
94
|
},
|
|
697
95
|
outputSchema: {
|
|
698
|
-
relations: z.array(
|
|
96
|
+
relations: z.array(RelationSchemaCompat)
|
|
699
97
|
}
|
|
700
98
|
}, async ({ relations }) => {
|
|
701
99
|
const result = await knowledgeGraphManager.createRelations(relations);
|
|
@@ -775,7 +173,7 @@ server.registerTool("delete_relations", {
|
|
|
775
173
|
title: "Delete Relations",
|
|
776
174
|
description: "Delete multiple relations from the knowledge graph",
|
|
777
175
|
inputSchema: {
|
|
778
|
-
relations: z.array(
|
|
176
|
+
relations: z.array(RelationSchemaCompat).describe("An array of relations to delete")
|
|
779
177
|
},
|
|
780
178
|
outputSchema: {
|
|
781
179
|
success: z.boolean(),
|
|
@@ -794,8 +192,8 @@ server.registerTool("read_graph", {
|
|
|
794
192
|
description: "Read the entire knowledge graph",
|
|
795
193
|
inputSchema: {},
|
|
796
194
|
outputSchema: {
|
|
797
|
-
entities: z.array(
|
|
798
|
-
relations: z.array(
|
|
195
|
+
entities: z.array(EntitySchemaCompat),
|
|
196
|
+
relations: z.array(RelationSchemaCompat)
|
|
799
197
|
}
|
|
800
198
|
}, async () => {
|
|
801
199
|
const graph = await knowledgeGraphManager.readGraph();
|
|
@@ -812,8 +210,8 @@ server.registerTool("search_nodes", {
|
|
|
812
210
|
query: z.string().describe("The search query to match against entity names, types, and observation content")
|
|
813
211
|
},
|
|
814
212
|
outputSchema: {
|
|
815
|
-
entities: z.array(
|
|
816
|
-
relations: z.array(
|
|
213
|
+
entities: z.array(EntitySchemaCompat),
|
|
214
|
+
relations: z.array(RelationSchemaCompat)
|
|
817
215
|
}
|
|
818
216
|
}, async ({ query }) => {
|
|
819
217
|
const graph = await knowledgeGraphManager.searchNodes(query);
|
|
@@ -830,8 +228,8 @@ server.registerTool("open_nodes", {
|
|
|
830
228
|
names: z.array(z.string()).describe("An array of entity names to retrieve")
|
|
831
229
|
},
|
|
832
230
|
outputSchema: {
|
|
833
|
-
entities: z.array(
|
|
834
|
-
relations: z.array(
|
|
231
|
+
entities: z.array(EntitySchemaCompat),
|
|
232
|
+
relations: z.array(RelationSchemaCompat)
|
|
835
233
|
}
|
|
836
234
|
}, async ({ names }) => {
|
|
837
235
|
const graph = await knowledgeGraphManager.openNodes(names);
|
|
@@ -853,8 +251,8 @@ server.registerTool("query_nodes", {
|
|
|
853
251
|
importanceMax: z.number().min(0).max(1).optional().describe("Maximum importance value (0-1)")
|
|
854
252
|
},
|
|
855
253
|
outputSchema: {
|
|
856
|
-
entities: z.array(
|
|
857
|
-
relations: z.array(
|
|
254
|
+
entities: z.array(EntitySchemaCompat),
|
|
255
|
+
relations: z.array(RelationSchemaCompat)
|
|
858
256
|
}
|
|
859
257
|
}, async (filters) => {
|
|
860
258
|
const graph = await knowledgeGraphManager.queryNodes(filters);
|
|
@@ -895,8 +293,8 @@ server.registerTool("get_recent_changes", {
|
|
|
895
293
|
since: z.string().describe("ISO 8601 timestamp - return changes since this time")
|
|
896
294
|
},
|
|
897
295
|
outputSchema: {
|
|
898
|
-
entities: z.array(
|
|
899
|
-
relations: z.array(
|
|
296
|
+
entities: z.array(EntitySchemaCompat),
|
|
297
|
+
relations: z.array(RelationSchemaCompat)
|
|
900
298
|
}
|
|
901
299
|
}, async ({ since }) => {
|
|
902
300
|
const changes = await knowledgeGraphManager.getRecentChanges(since);
|
|
@@ -917,7 +315,7 @@ server.registerTool("find_relation_path", {
|
|
|
917
315
|
outputSchema: {
|
|
918
316
|
found: z.boolean(),
|
|
919
317
|
path: z.array(z.string()),
|
|
920
|
-
relations: z.array(
|
|
318
|
+
relations: z.array(RelationSchemaCompat)
|
|
921
319
|
}
|
|
922
320
|
}, async ({ from, to, maxDepth }) => {
|
|
923
321
|
const result = await knowledgeGraphManager.findRelationPath(from, to, maxDepth || 5);
|
|
@@ -1017,7 +415,7 @@ server.registerTool("get_flagged_entities", {
|
|
|
1017
415
|
description: "Retrieve all entities that have been flagged for human review",
|
|
1018
416
|
inputSchema: {},
|
|
1019
417
|
outputSchema: {
|
|
1020
|
-
entities: z.array(
|
|
418
|
+
entities: z.array(EntitySchemaCompat)
|
|
1021
419
|
}
|
|
1022
420
|
}, async () => {
|
|
1023
421
|
const entities = await knowledgeGraphManager.getFlaggedEntities();
|
|
@@ -1035,8 +433,8 @@ server.registerTool("get_context", {
|
|
|
1035
433
|
depth: z.number().optional().default(1).describe("How many relationship hops to include (default: 1)")
|
|
1036
434
|
},
|
|
1037
435
|
outputSchema: {
|
|
1038
|
-
entities: z.array(
|
|
1039
|
-
relations: z.array(
|
|
436
|
+
entities: z.array(EntitySchemaCompat),
|
|
437
|
+
relations: z.array(RelationSchemaCompat)
|
|
1040
438
|
}
|
|
1041
439
|
}, async ({ entityNames, depth }) => {
|
|
1042
440
|
const context = await knowledgeGraphManager.getContext(entityNames, depth || 1);
|
|
@@ -1066,6 +464,32 @@ server.registerTool("list_conversations", {
|
|
|
1066
464
|
structuredContent: result
|
|
1067
465
|
};
|
|
1068
466
|
});
|
|
467
|
+
// Register get_analytics tool
|
|
468
|
+
server.registerTool("get_analytics", {
|
|
469
|
+
title: "Get Analytics",
|
|
470
|
+
description: "Get analytics for a specific thread with 4 core metrics: recent changes, top important entities, most connected entities, and orphaned entities",
|
|
471
|
+
inputSchema: GetAnalyticsInputSchema,
|
|
472
|
+
outputSchema: GetAnalyticsOutputSchema
|
|
473
|
+
}, async (input) => {
|
|
474
|
+
const result = await knowledgeGraphManager.getAnalytics(input.threadId);
|
|
475
|
+
return {
|
|
476
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
477
|
+
structuredContent: result
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
// Register get_observation_history tool
|
|
481
|
+
server.registerTool("get_observation_history", {
|
|
482
|
+
title: "Get Observation History",
|
|
483
|
+
description: "Retrieve the full version chain for a specific observation, showing how it evolved over time",
|
|
484
|
+
inputSchema: GetObservationHistoryInputSchema,
|
|
485
|
+
outputSchema: GetObservationHistoryOutputSchema
|
|
486
|
+
}, async (input) => {
|
|
487
|
+
const result = await knowledgeGraphManager.getObservationHistory(input.entityName, input.observationId);
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: "text", text: JSON.stringify({ history: result }, null, 2) }],
|
|
490
|
+
structuredContent: { history: result }
|
|
491
|
+
};
|
|
492
|
+
});
|
|
1069
493
|
async function main() {
|
|
1070
494
|
// Initialize memory directory path
|
|
1071
495
|
MEMORY_DIR_PATH = await ensureMemoryDirectory();
|