server-memory-enhanced 2.3.1 → 3.0.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 +45 -64
- package/dist/lib/analysis/analytics-service.js +111 -0
- package/dist/lib/analysis/conflict-detector.js +50 -0
- package/dist/lib/analysis/context-builder.js +34 -0
- package/dist/lib/analysis/memory-stats.js +67 -0
- package/dist/lib/analysis/path-finder.js +78 -0
- package/dist/lib/collaboration/conversation-service.js +43 -0
- package/dist/lib/collaboration/flag-manager.js +40 -0
- package/dist/lib/knowledge-graph-manager.js +102 -832
- package/dist/lib/maintenance/bulk-updater.js +44 -0
- package/dist/lib/maintenance/memory-pruner.js +48 -0
- package/dist/lib/operations/entity-operations.js +27 -0
- package/dist/lib/operations/observation-operations.js +105 -0
- package/dist/lib/operations/relation-operations.js +45 -0
- package/dist/lib/queries/entity-queries.js +70 -0
- package/dist/lib/queries/graph-reader.js +20 -0
- package/dist/lib/queries/search-service.js +113 -0
- package/dist/lib/schemas.js +61 -6
- package/dist/lib/utils/entity-finder.js +31 -0
- package/dist/lib/utils/negation-detector.js +23 -0
- package/dist/lib/utils/observation-validator.js +43 -0
- package/dist/lib/utils/relation-key.js +16 -0
- package/dist/lib/validation/entity-type-validator.js +32 -0
- package/dist/lib/validation/observation-validator.js +61 -0
- package/dist/lib/validation/quality-scorer.js +27 -0
- package/dist/lib/validation/relation-validator.js +36 -0
- package/dist/lib/validation/request-validator.js +91 -0
- package/dist/lib/validation.js +8 -200
- package/dist/lib/versioning/observation-history.js +60 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { promises as fs } from 'fs';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
|
|
9
|
-
import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema, UpdateObservationInputSchema, UpdateObservationOutputSchema } from './lib/schemas.js';
|
|
9
|
+
import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema, UpdateObservationInputSchema, UpdateObservationOutputSchema, ReadGraphInputSchema, SearchNodesInputSchema, OpenNodesInputSchema, QueryNodesInputSchema, GetMemoryStatsInputSchema, GetRecentChangesInputSchema, FindRelationPathInputSchema, DetectConflictsInputSchema, GetFlaggedEntitiesInputSchema, GetContextInputSchema } from './lib/schemas.js';
|
|
10
10
|
import { handleSaveMemory } from './lib/save-memory-handler.js';
|
|
11
11
|
import { validateSaveMemoryRequest } from './lib/validation.js';
|
|
12
12
|
import { JsonlStorageAdapter } from './lib/jsonl-storage-adapter.js';
|
|
@@ -314,14 +314,14 @@ server.registerTool("delete_relations", {
|
|
|
314
314
|
// Register read_graph tool
|
|
315
315
|
server.registerTool("read_graph", {
|
|
316
316
|
title: "Read Graph",
|
|
317
|
-
description: "Read the
|
|
318
|
-
inputSchema:
|
|
317
|
+
description: "Read the knowledge graph for a specific thread (thread isolation enforced)",
|
|
318
|
+
inputSchema: ReadGraphInputSchema,
|
|
319
319
|
outputSchema: {
|
|
320
320
|
entities: z.array(EntitySchemaCompat),
|
|
321
321
|
relations: z.array(RelationSchemaCompat)
|
|
322
322
|
}
|
|
323
|
-
}, async () => {
|
|
324
|
-
const graph = await knowledgeGraphManager.readGraph();
|
|
323
|
+
}, async (input) => {
|
|
324
|
+
const graph = await knowledgeGraphManager.readGraph(input.threadId);
|
|
325
325
|
return {
|
|
326
326
|
content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
|
|
327
327
|
structuredContent: { ...graph }
|
|
@@ -330,16 +330,14 @@ server.registerTool("read_graph", {
|
|
|
330
330
|
// Register search_nodes tool
|
|
331
331
|
server.registerTool("search_nodes", {
|
|
332
332
|
title: "Search Nodes",
|
|
333
|
-
description: "Search for nodes in the knowledge graph based on a query",
|
|
334
|
-
inputSchema:
|
|
335
|
-
query: z.string().describe("The search query to match against entity names, types, and observation content")
|
|
336
|
-
},
|
|
333
|
+
description: "Search for nodes in the knowledge graph based on a query (thread isolation enforced)",
|
|
334
|
+
inputSchema: SearchNodesInputSchema,
|
|
337
335
|
outputSchema: {
|
|
338
336
|
entities: z.array(EntitySchemaCompat),
|
|
339
337
|
relations: z.array(RelationSchemaCompat)
|
|
340
338
|
}
|
|
341
|
-
}, async (
|
|
342
|
-
const graph = await knowledgeGraphManager.searchNodes(query);
|
|
339
|
+
}, async (input) => {
|
|
340
|
+
const graph = await knowledgeGraphManager.searchNodes(input.threadId, input.query);
|
|
343
341
|
return {
|
|
344
342
|
content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
|
|
345
343
|
structuredContent: { ...graph }
|
|
@@ -348,16 +346,14 @@ server.registerTool("search_nodes", {
|
|
|
348
346
|
// Register open_nodes tool
|
|
349
347
|
server.registerTool("open_nodes", {
|
|
350
348
|
title: "Open Nodes",
|
|
351
|
-
description: "Open specific nodes in the knowledge graph by their names",
|
|
352
|
-
inputSchema:
|
|
353
|
-
names: z.array(z.string()).describe("An array of entity names to retrieve")
|
|
354
|
-
},
|
|
349
|
+
description: "Open specific nodes in the knowledge graph by their names (thread isolation enforced)",
|
|
350
|
+
inputSchema: OpenNodesInputSchema,
|
|
355
351
|
outputSchema: {
|
|
356
352
|
entities: z.array(EntitySchemaCompat),
|
|
357
353
|
relations: z.array(RelationSchemaCompat)
|
|
358
354
|
}
|
|
359
|
-
}, async (
|
|
360
|
-
const graph = await knowledgeGraphManager.openNodes(names);
|
|
355
|
+
}, async (input) => {
|
|
356
|
+
const graph = await knowledgeGraphManager.openNodes(input.threadId, input.names);
|
|
361
357
|
return {
|
|
362
358
|
content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
|
|
363
359
|
structuredContent: { ...graph }
|
|
@@ -366,21 +362,15 @@ server.registerTool("open_nodes", {
|
|
|
366
362
|
// Register query_nodes tool for advanced filtering
|
|
367
363
|
server.registerTool("query_nodes", {
|
|
368
364
|
title: "Query Nodes",
|
|
369
|
-
description: "Query nodes and relations in the knowledge graph with advanced filtering by timestamp, confidence, and importance ranges",
|
|
370
|
-
inputSchema:
|
|
371
|
-
timestampStart: z.string().optional().describe("ISO 8601 timestamp - filter for items created on or after this time"),
|
|
372
|
-
timestampEnd: z.string().optional().describe("ISO 8601 timestamp - filter for items created on or before this time"),
|
|
373
|
-
confidenceMin: z.number().min(0).max(1).optional().describe("Minimum confidence value (0-1)"),
|
|
374
|
-
confidenceMax: z.number().min(0).max(1).optional().describe("Maximum confidence value (0-1)"),
|
|
375
|
-
importanceMin: z.number().min(0).max(1).optional().describe("Minimum importance value (0-1)"),
|
|
376
|
-
importanceMax: z.number().min(0).max(1).optional().describe("Maximum importance value (0-1)")
|
|
377
|
-
},
|
|
365
|
+
description: "Query nodes and relations in the knowledge graph with advanced filtering by timestamp, confidence, and importance ranges (thread isolation enforced)",
|
|
366
|
+
inputSchema: QueryNodesInputSchema,
|
|
378
367
|
outputSchema: {
|
|
379
368
|
entities: z.array(EntitySchemaCompat),
|
|
380
369
|
relations: z.array(RelationSchemaCompat)
|
|
381
370
|
}
|
|
382
|
-
}, async (
|
|
383
|
-
const
|
|
371
|
+
}, async (input) => {
|
|
372
|
+
const { threadId, ...filters } = input;
|
|
373
|
+
const graph = await knowledgeGraphManager.queryNodes(threadId, Object.keys(filters).length > 0 ? filters : undefined);
|
|
384
374
|
return {
|
|
385
375
|
content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
|
|
386
376
|
structuredContent: { ...graph }
|
|
@@ -389,7 +379,7 @@ server.registerTool("query_nodes", {
|
|
|
389
379
|
// Register list_entities tool for simple entity lookup
|
|
390
380
|
server.registerTool("list_entities", {
|
|
391
381
|
title: "List Entities",
|
|
392
|
-
description: "List entities with optional filtering by entity type and name pattern. Returns a simple list of entity names and types for quick discovery.",
|
|
382
|
+
description: "List entities with optional filtering by entity type and name pattern. Returns a simple list of entity names and types for quick discovery. Thread isolation enforced - only shows entities from the specified thread.",
|
|
393
383
|
inputSchema: ListEntitiesInputSchema,
|
|
394
384
|
outputSchema: ListEntitiesOutputSchema
|
|
395
385
|
}, async (input) => {
|
|
@@ -522,8 +512,8 @@ server.registerTool("validate_memory", {
|
|
|
522
512
|
// Register get_memory_stats tool
|
|
523
513
|
server.registerTool("get_memory_stats", {
|
|
524
514
|
title: "Get Memory Statistics",
|
|
525
|
-
description: "Get comprehensive statistics about the knowledge graph including entity counts,
|
|
526
|
-
inputSchema:
|
|
515
|
+
description: "Get comprehensive statistics about the knowledge graph for a specific thread including entity counts, activity, and confidence/importance metrics (thread isolation enforced)",
|
|
516
|
+
inputSchema: GetMemoryStatsInputSchema,
|
|
527
517
|
outputSchema: {
|
|
528
518
|
entityCount: z.number(),
|
|
529
519
|
relationCount: z.number(),
|
|
@@ -536,8 +526,8 @@ server.registerTool("get_memory_stats", {
|
|
|
536
526
|
entityCount: z.number()
|
|
537
527
|
}))
|
|
538
528
|
}
|
|
539
|
-
}, async () => {
|
|
540
|
-
const stats = await knowledgeGraphManager.getMemoryStats();
|
|
529
|
+
}, async (input) => {
|
|
530
|
+
const stats = await knowledgeGraphManager.getMemoryStats(input.threadId);
|
|
541
531
|
return {
|
|
542
532
|
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
|
|
543
533
|
structuredContent: stats
|
|
@@ -546,16 +536,14 @@ server.registerTool("get_memory_stats", {
|
|
|
546
536
|
// Register get_recent_changes tool
|
|
547
537
|
server.registerTool("get_recent_changes", {
|
|
548
538
|
title: "Get Recent Changes",
|
|
549
|
-
description: "Retrieve entities and relations that were created or modified since a specific timestamp",
|
|
550
|
-
inputSchema:
|
|
551
|
-
since: z.string().describe("ISO 8601 timestamp - return changes since this time")
|
|
552
|
-
},
|
|
539
|
+
description: "Retrieve entities and relations that were created or modified since a specific timestamp (thread isolation enforced)",
|
|
540
|
+
inputSchema: GetRecentChangesInputSchema,
|
|
553
541
|
outputSchema: {
|
|
554
542
|
entities: z.array(EntitySchemaCompat),
|
|
555
543
|
relations: z.array(RelationSchemaCompat)
|
|
556
544
|
}
|
|
557
|
-
}, async (
|
|
558
|
-
const changes = await knowledgeGraphManager.getRecentChanges(since);
|
|
545
|
+
}, async (input) => {
|
|
546
|
+
const changes = await knowledgeGraphManager.getRecentChanges(input.threadId, input.since);
|
|
559
547
|
return {
|
|
560
548
|
content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
|
|
561
549
|
structuredContent: { ...changes }
|
|
@@ -564,19 +552,15 @@ server.registerTool("get_recent_changes", {
|
|
|
564
552
|
// Register find_relation_path tool
|
|
565
553
|
server.registerTool("find_relation_path", {
|
|
566
554
|
title: "Find Relationship Path",
|
|
567
|
-
description: "Find a path of relationships connecting two entities in the knowledge graph",
|
|
568
|
-
inputSchema:
|
|
569
|
-
from: z.string().describe("Starting entity name"),
|
|
570
|
-
to: z.string().describe("Target entity name"),
|
|
571
|
-
maxDepth: z.number().optional().default(5).describe("Maximum path depth to search (default: 5)")
|
|
572
|
-
},
|
|
555
|
+
description: "Find a path of relationships connecting two entities in the knowledge graph (thread isolation enforced)",
|
|
556
|
+
inputSchema: FindRelationPathInputSchema,
|
|
573
557
|
outputSchema: {
|
|
574
558
|
found: z.boolean(),
|
|
575
559
|
path: z.array(z.string()),
|
|
576
560
|
relations: z.array(RelationSchemaCompat)
|
|
577
561
|
}
|
|
578
|
-
}, async (
|
|
579
|
-
const result = await knowledgeGraphManager.findRelationPath(from, to, maxDepth || 5);
|
|
562
|
+
}, async (input) => {
|
|
563
|
+
const result = await knowledgeGraphManager.findRelationPath(input.threadId, input.from, input.to, input.maxDepth || 5);
|
|
580
564
|
return {
|
|
581
565
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
582
566
|
structuredContent: result
|
|
@@ -585,8 +569,8 @@ server.registerTool("find_relation_path", {
|
|
|
585
569
|
// Register detect_conflicts tool
|
|
586
570
|
server.registerTool("detect_conflicts", {
|
|
587
571
|
title: "Detect Conflicts",
|
|
588
|
-
description: "Detect potentially conflicting observations within entities using pattern matching and negation detection",
|
|
589
|
-
inputSchema:
|
|
572
|
+
description: "Detect potentially conflicting observations within entities using pattern matching and negation detection (thread isolation enforced)",
|
|
573
|
+
inputSchema: DetectConflictsInputSchema,
|
|
590
574
|
outputSchema: {
|
|
591
575
|
conflicts: z.array(z.object({
|
|
592
576
|
entityName: z.string(),
|
|
@@ -597,8 +581,8 @@ server.registerTool("detect_conflicts", {
|
|
|
597
581
|
}))
|
|
598
582
|
}))
|
|
599
583
|
}
|
|
600
|
-
}, async () => {
|
|
601
|
-
const conflicts = await knowledgeGraphManager.detectConflicts();
|
|
584
|
+
}, async (input) => {
|
|
585
|
+
const conflicts = await knowledgeGraphManager.detectConflicts(input.threadId);
|
|
602
586
|
return {
|
|
603
587
|
content: [{ type: "text", text: JSON.stringify({ conflicts }, null, 2) }],
|
|
604
588
|
structuredContent: { conflicts }
|
|
@@ -670,13 +654,13 @@ server.registerTool("flag_for_review", {
|
|
|
670
654
|
// Register get_flagged_entities tool
|
|
671
655
|
server.registerTool("get_flagged_entities", {
|
|
672
656
|
title: "Get Flagged Entities",
|
|
673
|
-
description: "Retrieve all entities that have been flagged for human review",
|
|
674
|
-
inputSchema:
|
|
657
|
+
description: "Retrieve all entities that have been flagged for human review (thread isolation enforced)",
|
|
658
|
+
inputSchema: GetFlaggedEntitiesInputSchema,
|
|
675
659
|
outputSchema: {
|
|
676
660
|
entities: z.array(EntitySchemaCompat)
|
|
677
661
|
}
|
|
678
|
-
}, async () => {
|
|
679
|
-
const entities = await knowledgeGraphManager.getFlaggedEntities();
|
|
662
|
+
}, async (input) => {
|
|
663
|
+
const entities = await knowledgeGraphManager.getFlaggedEntities(input.threadId);
|
|
680
664
|
return {
|
|
681
665
|
content: [{ type: "text", text: JSON.stringify({ entities }, null, 2) }],
|
|
682
666
|
structuredContent: { entities }
|
|
@@ -685,17 +669,14 @@ server.registerTool("get_flagged_entities", {
|
|
|
685
669
|
// Register get_context tool
|
|
686
670
|
server.registerTool("get_context", {
|
|
687
671
|
title: "Get Context",
|
|
688
|
-
description: "Retrieve entities and relations related to specified entities up to a certain depth, useful for understanding context around specific topics",
|
|
689
|
-
inputSchema:
|
|
690
|
-
entityNames: z.array(z.string()).describe("Names of entities to get context for"),
|
|
691
|
-
depth: z.number().optional().default(1).describe("How many relationship hops to include (default: 1)")
|
|
692
|
-
},
|
|
672
|
+
description: "Retrieve entities and relations related to specified entities up to a certain depth, useful for understanding context around specific topics (thread isolation enforced)",
|
|
673
|
+
inputSchema: GetContextInputSchema,
|
|
693
674
|
outputSchema: {
|
|
694
675
|
entities: z.array(EntitySchemaCompat),
|
|
695
676
|
relations: z.array(RelationSchemaCompat)
|
|
696
677
|
}
|
|
697
|
-
}, async (
|
|
698
|
-
const context = await knowledgeGraphManager.getContext(entityNames, depth || 1);
|
|
678
|
+
}, async (input) => {
|
|
679
|
+
const context = await knowledgeGraphManager.getContext(input.threadId, input.entityNames, input.depth || 1);
|
|
699
680
|
return {
|
|
700
681
|
content: [{ type: "text", text: JSON.stringify(context, null, 2) }],
|
|
701
682
|
structuredContent: { ...context }
|
|
@@ -738,11 +719,11 @@ server.registerTool("get_analytics", {
|
|
|
738
719
|
// Register get_observation_history tool
|
|
739
720
|
server.registerTool("get_observation_history", {
|
|
740
721
|
title: "Get Observation History",
|
|
741
|
-
description: "Retrieve the full version chain for a specific observation, showing how it evolved over time",
|
|
722
|
+
description: "Retrieve the full version chain for a specific observation, showing how it evolved over time (thread isolation enforced)",
|
|
742
723
|
inputSchema: GetObservationHistoryInputSchema,
|
|
743
724
|
outputSchema: GetObservationHistoryOutputSchema
|
|
744
725
|
}, async (input) => {
|
|
745
|
-
const result = await knowledgeGraphManager.getObservationHistory(input.entityName, input.observationId);
|
|
726
|
+
const result = await knowledgeGraphManager.getObservationHistory(input.threadId, input.entityName, input.observationId);
|
|
746
727
|
return {
|
|
747
728
|
content: [{ type: "text", text: JSON.stringify({ history: result }, null, 2) }],
|
|
748
729
|
structuredContent: { history: result }
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics service for thread-specific metrics
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Calculate recent changes for a thread
|
|
6
|
+
*/
|
|
7
|
+
function calculateRecentChanges(threadEntities) {
|
|
8
|
+
return threadEntities
|
|
9
|
+
.map(e => ({
|
|
10
|
+
entityName: e.name,
|
|
11
|
+
entityType: e.entityType,
|
|
12
|
+
lastModified: e.timestamp,
|
|
13
|
+
changeType: 'created' // Simplified: all are 'created' for now
|
|
14
|
+
}))
|
|
15
|
+
.sort((a, b) => b.lastModified.localeCompare(a.lastModified))
|
|
16
|
+
.slice(0, 10);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Calculate top important entities for a thread
|
|
20
|
+
*/
|
|
21
|
+
function calculateTopImportant(threadEntities) {
|
|
22
|
+
return threadEntities
|
|
23
|
+
.map(e => ({
|
|
24
|
+
entityName: e.name,
|
|
25
|
+
entityType: e.entityType,
|
|
26
|
+
importance: e.importance,
|
|
27
|
+
observationCount: e.observations.length
|
|
28
|
+
}))
|
|
29
|
+
.sort((a, b) => b.importance - a.importance)
|
|
30
|
+
.slice(0, 10);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Calculate most connected entities for a thread
|
|
34
|
+
*/
|
|
35
|
+
function calculateMostConnected(threadEntities, entityRelationCounts) {
|
|
36
|
+
return Array.from(entityRelationCounts.entries())
|
|
37
|
+
.map(([entityName, connectedSet]) => {
|
|
38
|
+
const entity = threadEntities.find(e => e.name === entityName);
|
|
39
|
+
return {
|
|
40
|
+
entityName,
|
|
41
|
+
entityType: entity.entityType,
|
|
42
|
+
relationCount: connectedSet.size,
|
|
43
|
+
connectedTo: Array.from(connectedSet)
|
|
44
|
+
};
|
|
45
|
+
})
|
|
46
|
+
.sort((a, b) => b.relationCount - a.relationCount)
|
|
47
|
+
.slice(0, 10);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Calculate orphaned entities for a thread
|
|
51
|
+
*/
|
|
52
|
+
function calculateOrphanedEntities(threadEntities, threadRelations, entityRelationCounts) {
|
|
53
|
+
const orphaned_entities = [];
|
|
54
|
+
const allEntityNames = new Set(threadEntities.map(e => e.name));
|
|
55
|
+
for (const entity of threadEntities) {
|
|
56
|
+
const relationCount = entityRelationCounts.get(entity.name)?.size || 0;
|
|
57
|
+
if (relationCount === 0) {
|
|
58
|
+
orphaned_entities.push({
|
|
59
|
+
entityName: entity.name,
|
|
60
|
+
entityType: entity.entityType,
|
|
61
|
+
reason: 'no_relations'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Check for broken relations (pointing to non-existent entities)
|
|
66
|
+
const entityRelations = threadRelations.filter(r => r.from === entity.name || r.to === entity.name);
|
|
67
|
+
const hasBrokenRelation = entityRelations.some(r => !allEntityNames.has(r.from) || !allEntityNames.has(r.to));
|
|
68
|
+
if (hasBrokenRelation) {
|
|
69
|
+
orphaned_entities.push({
|
|
70
|
+
entityName: entity.name,
|
|
71
|
+
entityType: entity.entityType,
|
|
72
|
+
reason: 'broken_relation'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return orphaned_entities;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get analytics for a specific thread (limited to 4 core metrics)
|
|
81
|
+
*/
|
|
82
|
+
export async function getAnalytics(storage, threadId) {
|
|
83
|
+
const graph = await storage.loadGraph();
|
|
84
|
+
// Filter to thread-specific data
|
|
85
|
+
const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
|
|
86
|
+
const threadRelations = graph.relations.filter(r => r.agentThreadId === threadId);
|
|
87
|
+
// Calculate all metrics
|
|
88
|
+
const recent_changes = calculateRecentChanges(threadEntities);
|
|
89
|
+
const top_important = calculateTopImportant(threadEntities);
|
|
90
|
+
// Build the relation counts map once for both most_connected and orphaned_entities
|
|
91
|
+
const entityRelationCounts = new Map();
|
|
92
|
+
for (const entity of threadEntities) {
|
|
93
|
+
entityRelationCounts.set(entity.name, new Set());
|
|
94
|
+
}
|
|
95
|
+
for (const relation of threadRelations) {
|
|
96
|
+
if (entityRelationCounts.has(relation.from)) {
|
|
97
|
+
entityRelationCounts.get(relation.from).add(relation.to);
|
|
98
|
+
}
|
|
99
|
+
if (entityRelationCounts.has(relation.to)) {
|
|
100
|
+
entityRelationCounts.get(relation.to).add(relation.from);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const most_connected = calculateMostConnected(threadEntities, entityRelationCounts);
|
|
104
|
+
const orphaned_entities = calculateOrphanedEntities(threadEntities, threadRelations, entityRelationCounts);
|
|
105
|
+
return {
|
|
106
|
+
recent_changes,
|
|
107
|
+
top_important,
|
|
108
|
+
most_connected,
|
|
109
|
+
orphaned_entities
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict detection service
|
|
3
|
+
*/
|
|
4
|
+
import { hasNegation, NEGATION_WORDS } from '../utils/negation-detector.js';
|
|
5
|
+
/**
|
|
6
|
+
* Detect conflicting observations within entities
|
|
7
|
+
* Identifies potential contradictions by checking for negation patterns
|
|
8
|
+
* Filtered by threadId for thread isolation
|
|
9
|
+
*/
|
|
10
|
+
export async function detectConflicts(storage, threadId) {
|
|
11
|
+
const graph = await storage.loadGraph();
|
|
12
|
+
const conflicts = [];
|
|
13
|
+
// Only analyze entities in the specified thread
|
|
14
|
+
for (const entity of graph.entities.filter(e => e.agentThreadId === threadId)) {
|
|
15
|
+
const entityConflicts = [];
|
|
16
|
+
for (let i = 0; i < entity.observations.length; i++) {
|
|
17
|
+
for (let j = i + 1; j < entity.observations.length; j++) {
|
|
18
|
+
const obs1Content = entity.observations[i].content.toLowerCase();
|
|
19
|
+
const obs2Content = entity.observations[j].content.toLowerCase();
|
|
20
|
+
// Skip if observations are in the same version chain
|
|
21
|
+
if (entity.observations[i].supersedes === entity.observations[j].id ||
|
|
22
|
+
entity.observations[j].supersedes === entity.observations[i].id ||
|
|
23
|
+
entity.observations[i].superseded_by === entity.observations[j].id ||
|
|
24
|
+
entity.observations[j].superseded_by === entity.observations[i].id) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
// Check for negation patterns
|
|
28
|
+
const obs1HasNegation = hasNegation(obs1Content);
|
|
29
|
+
const obs2HasNegation = hasNegation(obs2Content);
|
|
30
|
+
// If one has negation and they share key words, might be a conflict
|
|
31
|
+
if (obs1HasNegation !== obs2HasNegation) {
|
|
32
|
+
const words1 = obs1Content.split(/\s+/).filter(w => w.length > 3);
|
|
33
|
+
const words2Set = new Set(obs2Content.split(/\s+/).filter(w => w.length > 3));
|
|
34
|
+
const commonWords = words1.filter(w => words2Set.has(w) && !NEGATION_WORDS.has(w));
|
|
35
|
+
if (commonWords.length >= 2) {
|
|
36
|
+
entityConflicts.push({
|
|
37
|
+
obs1: entity.observations[i].content,
|
|
38
|
+
obs2: entity.observations[j].content,
|
|
39
|
+
reason: 'Potential contradiction with negation'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (entityConflicts.length > 0) {
|
|
46
|
+
conflicts.push({ entityName: entity.name, conflicts: entityConflicts });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return conflicts;
|
|
50
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context builder service
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Get context (entities related to specified entities up to a certain depth)
|
|
6
|
+
* Expands to include related entities up to specified depth
|
|
7
|
+
* Filtered by threadId for thread isolation
|
|
8
|
+
*/
|
|
9
|
+
export async function getContext(storage, threadId, entityNames, depth = 1) {
|
|
10
|
+
const graph = await storage.loadGraph();
|
|
11
|
+
const contextEntityNames = new Set(entityNames);
|
|
12
|
+
// Expand to include related entities up to specified depth - only within this thread
|
|
13
|
+
for (let d = 0; d < depth; d++) {
|
|
14
|
+
const currentEntities = Array.from(contextEntityNames);
|
|
15
|
+
for (const entityName of currentEntities) {
|
|
16
|
+
// Find all relations involving this entity - only from this thread
|
|
17
|
+
const relatedRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
|
|
18
|
+
(r.from === entityName || r.to === entityName));
|
|
19
|
+
// Add related entities
|
|
20
|
+
relatedRelations.forEach(r => {
|
|
21
|
+
contextEntityNames.add(r.from);
|
|
22
|
+
contextEntityNames.add(r.to);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Get all entities and relations in context - only from this thread
|
|
27
|
+
const contextEntities = graph.entities.filter(e => e.agentThreadId === threadId && contextEntityNames.has(e.name));
|
|
28
|
+
const contextRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
|
|
29
|
+
contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
|
|
30
|
+
return {
|
|
31
|
+
entities: contextEntities,
|
|
32
|
+
relations: contextRelations
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory statistics service
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Get comprehensive memory statistics for a specific thread
|
|
6
|
+
* Filtered by threadId for thread isolation
|
|
7
|
+
*/
|
|
8
|
+
export async function getMemoryStats(storage, threadId) {
|
|
9
|
+
const graph = await storage.loadGraph();
|
|
10
|
+
// Filter entities and relations by threadId
|
|
11
|
+
const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
|
|
12
|
+
const threadRelations = graph.relations.filter(r => r.agentThreadId === threadId);
|
|
13
|
+
// Count entity types
|
|
14
|
+
const entityTypes = {};
|
|
15
|
+
threadEntities.forEach(e => {
|
|
16
|
+
entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
|
|
17
|
+
});
|
|
18
|
+
// Calculate averages
|
|
19
|
+
const avgConfidence = threadEntities.length > 0
|
|
20
|
+
? threadEntities.reduce((sum, e) => sum + e.confidence, 0) / threadEntities.length
|
|
21
|
+
: 0;
|
|
22
|
+
const avgImportance = threadEntities.length > 0
|
|
23
|
+
? threadEntities.reduce((sum, e) => sum + e.importance, 0) / threadEntities.length
|
|
24
|
+
: 0;
|
|
25
|
+
// Count unique threads in the system (across all entities in the graph)
|
|
26
|
+
const threads = new Set(graph.entities
|
|
27
|
+
.map(e => e.agentThreadId)
|
|
28
|
+
.filter((id) => !!id));
|
|
29
|
+
// Recent activity (last 7 days, grouped by day) - only for this thread
|
|
30
|
+
const now = new Date();
|
|
31
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
32
|
+
const recentEntities = threadEntities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
|
|
33
|
+
// Group by day
|
|
34
|
+
const activityByDay = {};
|
|
35
|
+
recentEntities.forEach(e => {
|
|
36
|
+
const day = e.timestamp.substring(0, 10); // YYYY-MM-DD
|
|
37
|
+
activityByDay[day] = (activityByDay[day] || 0) + 1;
|
|
38
|
+
});
|
|
39
|
+
const recentActivity = Object.entries(activityByDay)
|
|
40
|
+
.map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
|
|
41
|
+
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
42
|
+
return {
|
|
43
|
+
entityCount: threadEntities.length,
|
|
44
|
+
relationCount: threadRelations.length,
|
|
45
|
+
threadCount: threads.size,
|
|
46
|
+
entityTypes,
|
|
47
|
+
avgConfidence,
|
|
48
|
+
avgImportance,
|
|
49
|
+
recentActivity
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get recent changes since a specific timestamp for a specific thread
|
|
54
|
+
* Filtered by threadId for thread isolation
|
|
55
|
+
*/
|
|
56
|
+
export async function getRecentChanges(storage, threadId, since) {
|
|
57
|
+
const graph = await storage.loadGraph();
|
|
58
|
+
const sinceDate = new Date(since);
|
|
59
|
+
// Only return entities and relations from this thread that were modified since the specified time
|
|
60
|
+
const recentEntities = graph.entities.filter(e => e.agentThreadId === threadId && new Date(e.timestamp) >= sinceDate);
|
|
61
|
+
// Only include relations from this thread that are recent themselves
|
|
62
|
+
const recentRelations = graph.relations.filter(r => r.agentThreadId === threadId && new Date(r.timestamp) >= sinceDate);
|
|
63
|
+
return {
|
|
64
|
+
entities: recentEntities,
|
|
65
|
+
relations: recentRelations
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path finding service for the knowledge graph
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Find the shortest path between two entities in the knowledge graph
|
|
6
|
+
* Uses BFS algorithm with bidirectional search
|
|
7
|
+
* Filtered by threadId for thread isolation
|
|
8
|
+
*/
|
|
9
|
+
export async function findRelationPath(storage, threadId, from, to, maxDepth = 5) {
|
|
10
|
+
const graph = await storage.loadGraph();
|
|
11
|
+
if (from === to) {
|
|
12
|
+
return { found: true, path: [from], relations: [] };
|
|
13
|
+
}
|
|
14
|
+
// Build indexes for efficient relation lookup - only for relations in this thread
|
|
15
|
+
const relationsFrom = new Map();
|
|
16
|
+
const relationsTo = new Map();
|
|
17
|
+
for (const rel of graph.relations.filter(r => r.agentThreadId === threadId)) {
|
|
18
|
+
if (!relationsFrom.has(rel.from)) {
|
|
19
|
+
relationsFrom.set(rel.from, []);
|
|
20
|
+
}
|
|
21
|
+
relationsFrom.get(rel.from).push(rel);
|
|
22
|
+
if (!relationsTo.has(rel.to)) {
|
|
23
|
+
relationsTo.set(rel.to, []);
|
|
24
|
+
}
|
|
25
|
+
relationsTo.get(rel.to).push(rel);
|
|
26
|
+
}
|
|
27
|
+
// BFS to find shortest path
|
|
28
|
+
const queue = [
|
|
29
|
+
{ entity: from, path: [from], relations: [] }
|
|
30
|
+
];
|
|
31
|
+
const visited = new Set([from]);
|
|
32
|
+
while (queue.length > 0) {
|
|
33
|
+
const current = queue.shift();
|
|
34
|
+
if (current.path.length > maxDepth) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
|
|
38
|
+
const outgoing = relationsFrom.get(current.entity) || [];
|
|
39
|
+
const incoming = relationsTo.get(current.entity) || [];
|
|
40
|
+
// Check outgoing relations
|
|
41
|
+
for (const rel of outgoing) {
|
|
42
|
+
if (rel.to === to) {
|
|
43
|
+
return {
|
|
44
|
+
found: true,
|
|
45
|
+
path: [...current.path, rel.to],
|
|
46
|
+
relations: [...current.relations, rel]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!visited.has(rel.to)) {
|
|
50
|
+
visited.add(rel.to);
|
|
51
|
+
queue.push({
|
|
52
|
+
entity: rel.to,
|
|
53
|
+
path: [...current.path, rel.to],
|
|
54
|
+
relations: [...current.relations, rel]
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Check incoming relations (traverse backwards)
|
|
59
|
+
for (const rel of incoming) {
|
|
60
|
+
if (rel.from === to) {
|
|
61
|
+
return {
|
|
62
|
+
found: true,
|
|
63
|
+
path: [...current.path, rel.from],
|
|
64
|
+
relations: [...current.relations, rel]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (!visited.has(rel.from)) {
|
|
68
|
+
visited.add(rel.from);
|
|
69
|
+
queue.push({
|
|
70
|
+
entity: rel.from,
|
|
71
|
+
path: [...current.path, rel.from],
|
|
72
|
+
relations: [...current.relations, rel]
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { found: false, path: [], relations: [] };
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation service (agent threads)
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* List all conversations (agent threads) with summary information
|
|
6
|
+
*/
|
|
7
|
+
export async function listConversations(storage) {
|
|
8
|
+
const graph = await storage.loadGraph();
|
|
9
|
+
// Group data by agent thread
|
|
10
|
+
const threadMap = new Map();
|
|
11
|
+
// Collect entities by thread
|
|
12
|
+
for (const entity of graph.entities) {
|
|
13
|
+
if (!threadMap.has(entity.agentThreadId)) {
|
|
14
|
+
threadMap.set(entity.agentThreadId, { entities: [], relations: [], timestamps: [] });
|
|
15
|
+
}
|
|
16
|
+
const threadData = threadMap.get(entity.agentThreadId);
|
|
17
|
+
threadData.entities.push(entity);
|
|
18
|
+
threadData.timestamps.push(entity.timestamp);
|
|
19
|
+
}
|
|
20
|
+
// Collect relations by thread
|
|
21
|
+
for (const relation of graph.relations) {
|
|
22
|
+
if (!threadMap.has(relation.agentThreadId)) {
|
|
23
|
+
threadMap.set(relation.agentThreadId, { entities: [], relations: [], timestamps: [] });
|
|
24
|
+
}
|
|
25
|
+
const threadData = threadMap.get(relation.agentThreadId);
|
|
26
|
+
threadData.relations.push(relation);
|
|
27
|
+
threadData.timestamps.push(relation.timestamp);
|
|
28
|
+
}
|
|
29
|
+
// Build conversation summaries
|
|
30
|
+
const conversations = Array.from(threadMap.entries()).map(([agentThreadId, data]) => {
|
|
31
|
+
const timestamps = data.timestamps.sort((a, b) => a.localeCompare(b));
|
|
32
|
+
return {
|
|
33
|
+
agentThreadId,
|
|
34
|
+
entityCount: data.entities.length,
|
|
35
|
+
relationCount: data.relations.length,
|
|
36
|
+
firstCreated: timestamps[0] || '',
|
|
37
|
+
lastUpdated: timestamps[timestamps.length - 1] || ''
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
// Sort by last updated (most recent first)
|
|
41
|
+
conversations.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
|
|
42
|
+
return { conversations };
|
|
43
|
+
}
|