server-memory-enhanced 2.3.1 → 2.3.2
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/lib/analysis/analytics-service.js +111 -0
- package/dist/lib/analysis/conflict-detector.js +48 -0
- package/dist/lib/analysis/context-builder.js +31 -0
- package/dist/lib/analysis/memory-stats.js +63 -0
- package/dist/lib/analysis/path-finder.js +77 -0
- package/dist/lib/collaboration/conversation-service.js +43 -0
- package/dist/lib/collaboration/flag-manager.js +38 -0
- package/dist/lib/knowledge-graph-manager.js +92 -822
- 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 +68 -0
- package/dist/lib/queries/graph-reader.js +9 -0
- package/dist/lib/queries/search-service.js +99 -0
- 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 +54 -0
- package/package.json +1 -1
|
@@ -1,48 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* KnowledgeGraphManager - Main class for managing the knowledge graph
|
|
3
|
+
* Acts as a facade to coordinate operations across different services
|
|
3
4
|
*/
|
|
4
|
-
import { randomUUID } from 'crypto';
|
|
5
5
|
import { JsonlStorageAdapter } from './jsonl-storage-adapter.js';
|
|
6
|
+
// Import CRUD operations
|
|
7
|
+
import * as EntityOps from './operations/entity-operations.js';
|
|
8
|
+
import * as RelationOps from './operations/relation-operations.js';
|
|
9
|
+
import * as ObservationOps from './operations/observation-operations.js';
|
|
10
|
+
// Import query operations
|
|
11
|
+
import * as GraphReader from './queries/graph-reader.js';
|
|
12
|
+
import * as SearchService from './queries/search-service.js';
|
|
13
|
+
import * as EntityQueries from './queries/entity-queries.js';
|
|
14
|
+
// Import analysis services
|
|
15
|
+
import * as MemoryStats from './analysis/memory-stats.js';
|
|
16
|
+
import * as PathFinder from './analysis/path-finder.js';
|
|
17
|
+
import * as ConflictDetector from './analysis/conflict-detector.js';
|
|
18
|
+
import * as ContextBuilder from './analysis/context-builder.js';
|
|
19
|
+
import * as AnalyticsService from './analysis/analytics-service.js';
|
|
20
|
+
// Import maintenance services
|
|
21
|
+
import * as MemoryPruner from './maintenance/memory-pruner.js';
|
|
22
|
+
import * as BulkUpdater from './maintenance/bulk-updater.js';
|
|
23
|
+
// Import versioning services
|
|
24
|
+
import * as ObservationHistory from './versioning/observation-history.js';
|
|
25
|
+
// Import collaboration services
|
|
26
|
+
import * as FlagManager from './collaboration/flag-manager.js';
|
|
27
|
+
import * as ConversationService from './collaboration/conversation-service.js';
|
|
6
28
|
export class KnowledgeGraphManager {
|
|
7
|
-
static NEGATION_WORDS = new Set(['not', 'no', 'never', 'neither', 'none', 'doesn\'t', 'don\'t', 'isn\'t', 'aren\'t']);
|
|
8
29
|
storage;
|
|
9
30
|
initializePromise = null;
|
|
10
31
|
constructor(memoryDirPath, storageAdapter) {
|
|
11
32
|
this.storage = storageAdapter || new JsonlStorageAdapter(memoryDirPath);
|
|
12
33
|
// Lazy initialization - will be called on first operation
|
|
13
34
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Check if content contains any negation words (using word boundary matching)
|
|
16
|
-
* Handles punctuation and contractions, avoids creating intermediate Set for performance
|
|
17
|
-
*/
|
|
18
|
-
hasNegation(content) {
|
|
19
|
-
// Extract words using word boundary regex, preserving contractions (include apostrophes)
|
|
20
|
-
const lowerContent = content.toLowerCase();
|
|
21
|
-
const wordMatches = lowerContent.match(/\b[\w']+\b/g);
|
|
22
|
-
if (!wordMatches) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
// Check each word against negation words without creating intermediate Set
|
|
26
|
-
for (const word of wordMatches) {
|
|
27
|
-
if (KnowledgeGraphManager.NEGATION_WORDS.has(word)) {
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Create a composite key for relation deduplication.
|
|
35
|
-
*
|
|
36
|
-
* We explicitly normalize the components to primitive strings to ensure
|
|
37
|
-
* stable serialization and to document the assumption that `from`, `to`,
|
|
38
|
-
* and `relationType` are simple string identifiers.
|
|
39
|
-
*/
|
|
40
|
-
createRelationKey(relation) {
|
|
41
|
-
const from = String(relation.from);
|
|
42
|
-
const to = String(relation.to);
|
|
43
|
-
const relationType = String(relation.relationType);
|
|
44
|
-
return JSON.stringify([from, to, relationType]);
|
|
45
|
-
}
|
|
46
35
|
/**
|
|
47
36
|
* Ensure storage is initialized before any operation
|
|
48
37
|
* This is called automatically by all public methods
|
|
@@ -53,838 +42,119 @@ export class KnowledgeGraphManager {
|
|
|
53
42
|
}
|
|
54
43
|
await this.initializePromise;
|
|
55
44
|
}
|
|
56
|
-
|
|
57
|
-
* Find an entity by name in the knowledge graph.
|
|
58
|
-
* @param graph - The knowledge graph to search
|
|
59
|
-
* @param entityName - Name of the entity to find
|
|
60
|
-
* @returns The found entity
|
|
61
|
-
* @throws Error if entity not found
|
|
62
|
-
*/
|
|
63
|
-
findEntity(graph, entityName) {
|
|
64
|
-
const entity = graph.entities.find(e => e.name === entityName);
|
|
65
|
-
if (!entity) {
|
|
66
|
-
throw new Error(`Entity '${entityName}' not found`);
|
|
67
|
-
}
|
|
68
|
-
return entity;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Find an observation by ID within an entity.
|
|
72
|
-
* @param entity - The entity containing the observation
|
|
73
|
-
* @param observationId - ID of the observation to find
|
|
74
|
-
* @returns The found observation
|
|
75
|
-
* @throws Error if observation not found
|
|
76
|
-
*/
|
|
77
|
-
findObservation(entity, observationId) {
|
|
78
|
-
const observation = entity.observations.find(o => o.id === observationId);
|
|
79
|
-
if (!observation) {
|
|
80
|
-
throw new Error(`Observation '${observationId}' not found in entity '${entity.name}'`);
|
|
81
|
-
}
|
|
82
|
-
return observation;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Validate that an observation can be updated (not already superseded).
|
|
86
|
-
* @param observation - The observation to validate
|
|
87
|
-
* @throws Error if observation has already been superseded
|
|
88
|
-
*/
|
|
89
|
-
validateObservationNotSuperseded(observation) {
|
|
90
|
-
if (observation.superseded_by) {
|
|
91
|
-
throw new Error(`Observation '${observation.id}' has already been superseded by '${observation.superseded_by}'. Update the latest version instead.`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Resolve confidence value using inheritance chain: params > observation > entity.
|
|
96
|
-
* @param providedValue - Value provided in parameters (optional)
|
|
97
|
-
* @param observationValue - Value from observation (optional)
|
|
98
|
-
* @param entityValue - Value from entity (fallback)
|
|
99
|
-
* @returns Resolved confidence value
|
|
100
|
-
*/
|
|
101
|
-
resolveInheritedValue(providedValue, observationValue, entityValue) {
|
|
102
|
-
return providedValue ?? observationValue ?? entityValue;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Create a new observation version from an existing observation.
|
|
106
|
-
* @param oldObs - The observation being updated
|
|
107
|
-
* @param entity - The entity containing the observation
|
|
108
|
-
* @param params - Update parameters
|
|
109
|
-
* @returns New observation with incremented version
|
|
110
|
-
*/
|
|
111
|
-
createObservationVersion(oldObs, entity, params) {
|
|
112
|
-
return {
|
|
113
|
-
id: `obs_${randomUUID()}`,
|
|
114
|
-
content: params.newContent,
|
|
115
|
-
timestamp: params.timestamp,
|
|
116
|
-
version: oldObs.version + 1,
|
|
117
|
-
supersedes: oldObs.id,
|
|
118
|
-
agentThreadId: params.agentThreadId,
|
|
119
|
-
confidence: this.resolveInheritedValue(params.confidence, oldObs.confidence, entity.confidence),
|
|
120
|
-
importance: this.resolveInheritedValue(params.importance, oldObs.importance, entity.importance)
|
|
121
|
-
};
|
|
122
|
-
}
|
|
45
|
+
// Entity Operations
|
|
123
46
|
async createEntities(entities) {
|
|
124
47
|
await this.ensureInitialized();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
graph.entities.push(...newEntities);
|
|
131
|
-
await this.storage.saveGraph(graph);
|
|
132
|
-
return newEntities;
|
|
48
|
+
return EntityOps.createEntities(this.storage, entities);
|
|
49
|
+
}
|
|
50
|
+
async deleteEntities(entityNames) {
|
|
51
|
+
await this.ensureInitialized();
|
|
52
|
+
return EntityOps.deleteEntities(this.storage, entityNames);
|
|
133
53
|
}
|
|
54
|
+
// Relation Operations
|
|
134
55
|
async createRelations(relations) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const entityNames = new Set(graph.entities.map(e => e.name));
|
|
138
|
-
const validRelations = relations.filter(r => {
|
|
139
|
-
if (!entityNames.has(r.from) || !entityNames.has(r.to)) {
|
|
140
|
-
console.warn(`Skipping relation ${r.from} -> ${r.to}: one or both entities do not exist`);
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
return true;
|
|
144
|
-
});
|
|
145
|
-
// Relations are globally unique by (from, to, relationType) across all threads
|
|
146
|
-
// This enables multiple threads to collaboratively build the knowledge graph
|
|
147
|
-
const existingRelationKeys = new Set(graph.relations.map(r => this.createRelationKey(r)));
|
|
148
|
-
// Create composite keys once per valid relation to avoid duplicate serialization
|
|
149
|
-
const validRelationsWithKeys = validRelations.map(r => ({
|
|
150
|
-
relation: r,
|
|
151
|
-
key: this.createRelationKey(r)
|
|
152
|
-
}));
|
|
153
|
-
const newRelations = validRelationsWithKeys
|
|
154
|
-
.filter(item => !existingRelationKeys.has(item.key))
|
|
155
|
-
.map(item => item.relation);
|
|
156
|
-
graph.relations.push(...newRelations);
|
|
157
|
-
await this.storage.saveGraph(graph);
|
|
158
|
-
return newRelations;
|
|
56
|
+
await this.ensureInitialized();
|
|
57
|
+
return RelationOps.createRelations(this.storage, relations);
|
|
159
58
|
}
|
|
160
|
-
async
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const entity = graph.entities.find(e => e.name === o.entityName);
|
|
164
|
-
if (!entity) {
|
|
165
|
-
throw new Error(`Entity with name ${o.entityName} not found`);
|
|
166
|
-
}
|
|
167
|
-
// Check for existing observations with same content to create version chain
|
|
168
|
-
const newObservations = [];
|
|
169
|
-
// Build a Set of existing observation contents for efficient lookup (single-pass)
|
|
170
|
-
const existingContents = entity.observations.reduce((set, obs) => {
|
|
171
|
-
if (!obs.superseded_by) {
|
|
172
|
-
set.add(obs.content);
|
|
173
|
-
}
|
|
174
|
-
return set;
|
|
175
|
-
}, new Set());
|
|
176
|
-
for (const content of o.contents) {
|
|
177
|
-
// Check if observation with this content already exists (latest version)
|
|
178
|
-
if (existingContents.has(content)) {
|
|
179
|
-
// Don't add duplicate - observation with this content already exists
|
|
180
|
-
// Versioning is for UPDATES to content, not for re-asserting the same content
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
// Create brand new observation
|
|
184
|
-
const newObs = {
|
|
185
|
-
id: `obs_${randomUUID()}`,
|
|
186
|
-
content: content,
|
|
187
|
-
timestamp: o.timestamp,
|
|
188
|
-
version: 1,
|
|
189
|
-
agentThreadId: o.agentThreadId,
|
|
190
|
-
confidence: o.confidence,
|
|
191
|
-
importance: o.importance
|
|
192
|
-
};
|
|
193
|
-
entity.observations.push(newObs);
|
|
194
|
-
newObservations.push(newObs);
|
|
195
|
-
}
|
|
196
|
-
// Update entity metadata
|
|
197
|
-
entity.timestamp = o.timestamp;
|
|
198
|
-
entity.confidence = Math.max(entity.confidence, o.confidence);
|
|
199
|
-
entity.importance = Math.max(entity.importance, o.importance);
|
|
200
|
-
return { entityName: o.entityName, addedObservations: newObservations };
|
|
201
|
-
});
|
|
202
|
-
await this.storage.saveGraph(graph);
|
|
203
|
-
return results;
|
|
59
|
+
async deleteRelations(relations) {
|
|
60
|
+
await this.ensureInitialized();
|
|
61
|
+
return RelationOps.deleteRelations(this.storage, relations);
|
|
204
62
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
graph.relations = graph.relations.filter(r => !namesToDelete.has(r.from) && !namesToDelete.has(r.to));
|
|
210
|
-
await this.storage.saveGraph(graph);
|
|
63
|
+
// Observation Operations
|
|
64
|
+
async addObservations(observations) {
|
|
65
|
+
await this.ensureInitialized();
|
|
66
|
+
return ObservationOps.addObservations(this.storage, observations);
|
|
211
67
|
}
|
|
212
68
|
async deleteObservations(deletions) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const entity = graph.entities.find(e => e.name === d.entityName);
|
|
216
|
-
if (entity) {
|
|
217
|
-
// Delete observations by content (for backward compatibility) or by ID
|
|
218
|
-
entity.observations = entity.observations.filter(o => !d.observations.includes(o.content) && !d.observations.includes(o.id));
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
await this.storage.saveGraph(graph);
|
|
69
|
+
await this.ensureInitialized();
|
|
70
|
+
return ObservationOps.deleteObservations(this.storage, deletions);
|
|
222
71
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Update an existing observation by creating a new version with updated content.
|
|
225
|
-
* This maintains the version history through the supersedes/superseded_by chain.
|
|
226
|
-
*
|
|
227
|
-
* @param params - Update parameters
|
|
228
|
-
* @param params.entityName - Name of the entity containing the observation
|
|
229
|
-
* @param params.observationId - ID of the observation to update
|
|
230
|
-
* @param params.newContent - New content for the observation
|
|
231
|
-
* @param params.agentThreadId - Agent thread ID making this update
|
|
232
|
-
* @param params.timestamp - ISO 8601 timestamp of the update
|
|
233
|
-
* @param params.confidence - Optional confidence score (0-1), inherits from old observation if not provided
|
|
234
|
-
* @param params.importance - Optional importance score (0-1), inherits from old observation if not provided
|
|
235
|
-
* @returns The newly created observation with incremented version number
|
|
236
|
-
* @throws Error if entity not found
|
|
237
|
-
* @throws Error if observation not found
|
|
238
|
-
* @throws Error if observation has already been superseded (must update latest version)
|
|
239
|
-
*/
|
|
240
72
|
async updateObservation(params) {
|
|
241
73
|
await this.ensureInitialized();
|
|
242
|
-
|
|
243
|
-
// Find and validate the entity and observation
|
|
244
|
-
const entity = this.findEntity(graph, params.entityName);
|
|
245
|
-
const oldObs = this.findObservation(entity, params.observationId);
|
|
246
|
-
this.validateObservationNotSuperseded(oldObs);
|
|
247
|
-
// Create new version with inheritance chain
|
|
248
|
-
const newObs = this.createObservationVersion(oldObs, entity, params);
|
|
249
|
-
// Link old observation to new one
|
|
250
|
-
oldObs.superseded_by = newObs.id;
|
|
251
|
-
// Add new observation to entity
|
|
252
|
-
entity.observations.push(newObs);
|
|
253
|
-
// Update entity timestamp
|
|
254
|
-
entity.timestamp = params.timestamp;
|
|
255
|
-
await this.storage.saveGraph(graph);
|
|
256
|
-
return newObs;
|
|
257
|
-
}
|
|
258
|
-
async deleteRelations(relations) {
|
|
259
|
-
const graph = await this.storage.loadGraph();
|
|
260
|
-
// Delete relations globally across all threads by matching (from, to, relationType)
|
|
261
|
-
// In a collaborative knowledge graph, deletions affect all threads
|
|
262
|
-
graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
|
|
263
|
-
r.to === delRelation.to &&
|
|
264
|
-
r.relationType === delRelation.relationType));
|
|
265
|
-
await this.storage.saveGraph(graph);
|
|
74
|
+
return ObservationOps.updateObservation(this.storage, params);
|
|
266
75
|
}
|
|
76
|
+
// Graph Reading Operations
|
|
267
77
|
async readGraph() {
|
|
268
|
-
|
|
78
|
+
await this.ensureInitialized();
|
|
79
|
+
return GraphReader.readGraph(this.storage);
|
|
269
80
|
}
|
|
81
|
+
// Search Operations
|
|
270
82
|
async searchNodes(query) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
274
|
-
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
|
|
275
|
-
e.observations.some(o => o.content?.toLowerCase().includes(query.toLowerCase())));
|
|
276
|
-
// Create a Set of filtered entity names for quick lookup
|
|
277
|
-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
278
|
-
// Filter relations to only include those between filtered entities
|
|
279
|
-
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
280
|
-
const filteredGraph = {
|
|
281
|
-
entities: filteredEntities,
|
|
282
|
-
relations: filteredRelations,
|
|
283
|
-
};
|
|
284
|
-
return filteredGraph;
|
|
83
|
+
await this.ensureInitialized();
|
|
84
|
+
return SearchService.searchNodes(this.storage, query);
|
|
285
85
|
}
|
|
286
86
|
async openNodes(names) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const filteredEntities = graph.entities.filter(e => names.includes(e.name));
|
|
290
|
-
// Create a Set of filtered entity names for quick lookup
|
|
291
|
-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
292
|
-
// Filter relations to only include those between filtered entities
|
|
293
|
-
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
294
|
-
const filteredGraph = {
|
|
295
|
-
entities: filteredEntities,
|
|
296
|
-
relations: filteredRelations,
|
|
297
|
-
};
|
|
298
|
-
return filteredGraph;
|
|
87
|
+
await this.ensureInitialized();
|
|
88
|
+
return SearchService.openNodes(this.storage, names);
|
|
299
89
|
}
|
|
300
90
|
async queryNodes(filters) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!filters) {
|
|
304
|
-
return graph;
|
|
305
|
-
}
|
|
306
|
-
// Apply filters to entities
|
|
307
|
-
const filteredEntities = graph.entities.filter(e => {
|
|
308
|
-
// Timestamp range filter
|
|
309
|
-
if (filters.timestampStart && e.timestamp < filters.timestampStart)
|
|
310
|
-
return false;
|
|
311
|
-
if (filters.timestampEnd && e.timestamp > filters.timestampEnd)
|
|
312
|
-
return false;
|
|
313
|
-
// Confidence range filter
|
|
314
|
-
if (filters.confidenceMin !== undefined && e.confidence < filters.confidenceMin)
|
|
315
|
-
return false;
|
|
316
|
-
if (filters.confidenceMax !== undefined && e.confidence > filters.confidenceMax)
|
|
317
|
-
return false;
|
|
318
|
-
// Importance range filter
|
|
319
|
-
if (filters.importanceMin !== undefined && e.importance < filters.importanceMin)
|
|
320
|
-
return false;
|
|
321
|
-
if (filters.importanceMax !== undefined && e.importance > filters.importanceMax)
|
|
322
|
-
return false;
|
|
323
|
-
return true;
|
|
324
|
-
});
|
|
325
|
-
// Create a Set of filtered entity names for quick lookup
|
|
326
|
-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
327
|
-
// Apply filters to relations (and ensure they connect filtered entities)
|
|
328
|
-
const filteredRelations = graph.relations.filter(r => {
|
|
329
|
-
// Must connect filtered entities
|
|
330
|
-
if (!filteredEntityNames.has(r.from) || !filteredEntityNames.has(r.to))
|
|
331
|
-
return false;
|
|
332
|
-
// Timestamp range filter
|
|
333
|
-
if (filters.timestampStart && r.timestamp < filters.timestampStart)
|
|
334
|
-
return false;
|
|
335
|
-
if (filters.timestampEnd && r.timestamp > filters.timestampEnd)
|
|
336
|
-
return false;
|
|
337
|
-
// Confidence range filter
|
|
338
|
-
if (filters.confidenceMin !== undefined && r.confidence < filters.confidenceMin)
|
|
339
|
-
return false;
|
|
340
|
-
if (filters.confidenceMax !== undefined && r.confidence > filters.confidenceMax)
|
|
341
|
-
return false;
|
|
342
|
-
// Importance range filter
|
|
343
|
-
if (filters.importanceMin !== undefined && r.importance < filters.importanceMin)
|
|
344
|
-
return false;
|
|
345
|
-
if (filters.importanceMax !== undefined && r.importance > filters.importanceMax)
|
|
346
|
-
return false;
|
|
347
|
-
return true;
|
|
348
|
-
});
|
|
349
|
-
return {
|
|
350
|
-
entities: filteredEntities,
|
|
351
|
-
relations: filteredRelations,
|
|
352
|
-
};
|
|
91
|
+
await this.ensureInitialized();
|
|
92
|
+
return SearchService.queryNodes(this.storage, filters);
|
|
353
93
|
}
|
|
354
|
-
|
|
355
|
-
* Get names of all entities that can be referenced in relations.
|
|
356
|
-
* @returns Set of entity names that exist in the graph.
|
|
357
|
-
*
|
|
358
|
-
* Note: Returns ALL entities globally because entity names are globally unique across
|
|
359
|
-
* all threads in the collaborative knowledge graph (by design - see createEntities).
|
|
360
|
-
* This enables any thread to reference any existing entity, supporting incremental
|
|
361
|
-
* building and cross-thread collaboration. Thread-specific filtering is not needed
|
|
362
|
-
* since entity names cannot conflict across threads.
|
|
363
|
-
*/
|
|
94
|
+
// Entity Query Operations
|
|
364
95
|
async getAllEntityNames() {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
// Return all entities in the graph that can be referenced
|
|
368
|
-
// This allows incremental building: entities from previous save_memory calls
|
|
369
|
-
// can be referenced in new calls, enabling cross-save entity relations
|
|
370
|
-
for (const entity of graph.entities) {
|
|
371
|
-
entityNames.add(entity.name);
|
|
372
|
-
}
|
|
373
|
-
return entityNames;
|
|
96
|
+
await this.ensureInitialized();
|
|
97
|
+
return EntityQueries.getAllEntityNames(this.storage);
|
|
374
98
|
}
|
|
375
|
-
/**
|
|
376
|
-
* @deprecated Use {@link getAllEntityNames} instead.
|
|
377
|
-
*
|
|
378
|
-
* This method is kept for backward compatibility. It accepts a threadId parameter
|
|
379
|
-
* for API consistency but does not use it for filtering; it returns the same
|
|
380
|
-
* global set of entity names as {@link getAllEntityNames}.
|
|
381
|
-
*
|
|
382
|
-
* @param threadId The thread ID (accepted but not used)
|
|
383
|
-
* @returns Set of entity names that exist in the graph
|
|
384
|
-
*/
|
|
385
99
|
async getEntityNamesInThread(threadId) {
|
|
386
|
-
|
|
100
|
+
await this.ensureInitialized();
|
|
101
|
+
return EntityQueries.getEntityNamesInThread(this.storage, threadId);
|
|
387
102
|
}
|
|
388
|
-
/**
|
|
389
|
-
* List entities with optional filtering by type and name pattern
|
|
390
|
-
* @param threadId Optional thread ID to filter by. If not provided, returns entities from all threads.
|
|
391
|
-
* @param entityType Optional entity type filter (exact match)
|
|
392
|
-
* @param namePattern Optional name pattern filter (case-insensitive substring match)
|
|
393
|
-
* @returns Array of entities with name and entityType
|
|
394
|
-
*/
|
|
395
103
|
async listEntities(threadId, entityType, namePattern) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
// Filter by thread ID if specified (otherwise returns all threads)
|
|
399
|
-
if (threadId) {
|
|
400
|
-
filteredEntities = filteredEntities.filter(e => e.agentThreadId === threadId);
|
|
401
|
-
}
|
|
402
|
-
// Filter by entity type if specified
|
|
403
|
-
if (entityType) {
|
|
404
|
-
filteredEntities = filteredEntities.filter(e => e.entityType === entityType);
|
|
405
|
-
}
|
|
406
|
-
// Filter by name pattern if specified (case-insensitive)
|
|
407
|
-
if (namePattern) {
|
|
408
|
-
const pattern = namePattern.toLowerCase();
|
|
409
|
-
filteredEntities = filteredEntities.filter(e => e.name.toLowerCase().includes(pattern));
|
|
410
|
-
}
|
|
411
|
-
// Return simplified list with just name and entityType
|
|
412
|
-
return filteredEntities.map(e => ({
|
|
413
|
-
name: e.name,
|
|
414
|
-
entityType: e.entityType
|
|
415
|
-
}));
|
|
104
|
+
await this.ensureInitialized();
|
|
105
|
+
return EntityQueries.listEntities(this.storage, threadId, entityType, namePattern);
|
|
416
106
|
}
|
|
417
|
-
//
|
|
107
|
+
// Memory Statistics & Insights
|
|
418
108
|
async getMemoryStats() {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const entityTypes = {};
|
|
422
|
-
graph.entities.forEach(e => {
|
|
423
|
-
entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
|
|
424
|
-
});
|
|
425
|
-
// Calculate averages
|
|
426
|
-
const avgConfidence = graph.entities.length > 0
|
|
427
|
-
? graph.entities.reduce((sum, e) => sum + e.confidence, 0) / graph.entities.length
|
|
428
|
-
: 0;
|
|
429
|
-
const avgImportance = graph.entities.length > 0
|
|
430
|
-
? graph.entities.reduce((sum, e) => sum + e.importance, 0) / graph.entities.length
|
|
431
|
-
: 0;
|
|
432
|
-
// Count unique threads
|
|
433
|
-
const threads = new Set([
|
|
434
|
-
...graph.entities.map(e => e.agentThreadId),
|
|
435
|
-
...graph.relations.map(r => r.agentThreadId)
|
|
436
|
-
]);
|
|
437
|
-
// Recent activity (last 7 days, grouped by day)
|
|
438
|
-
const now = new Date();
|
|
439
|
-
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
440
|
-
const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
|
|
441
|
-
// Group by day
|
|
442
|
-
const activityByDay = {};
|
|
443
|
-
recentEntities.forEach(e => {
|
|
444
|
-
const day = e.timestamp.substring(0, 10); // YYYY-MM-DD
|
|
445
|
-
activityByDay[day] = (activityByDay[day] || 0) + 1;
|
|
446
|
-
});
|
|
447
|
-
const recentActivity = Object.entries(activityByDay)
|
|
448
|
-
.map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
|
|
449
|
-
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
450
|
-
return {
|
|
451
|
-
entityCount: graph.entities.length,
|
|
452
|
-
relationCount: graph.relations.length,
|
|
453
|
-
threadCount: threads.size,
|
|
454
|
-
entityTypes,
|
|
455
|
-
avgConfidence,
|
|
456
|
-
avgImportance,
|
|
457
|
-
recentActivity
|
|
458
|
-
};
|
|
109
|
+
await this.ensureInitialized();
|
|
110
|
+
return MemoryStats.getMemoryStats(this.storage);
|
|
459
111
|
}
|
|
460
|
-
// Enhancement 2: Get recent changes
|
|
461
112
|
async getRecentChanges(since) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// Only return entities and relations that were actually modified since the specified time
|
|
465
|
-
const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sinceDate);
|
|
466
|
-
// Only include relations that are recent themselves
|
|
467
|
-
const recentRelations = graph.relations.filter(r => new Date(r.timestamp) >= sinceDate);
|
|
468
|
-
return {
|
|
469
|
-
entities: recentEntities,
|
|
470
|
-
relations: recentRelations
|
|
471
|
-
};
|
|
113
|
+
await this.ensureInitialized();
|
|
114
|
+
return MemoryStats.getRecentChanges(this.storage, since);
|
|
472
115
|
}
|
|
473
|
-
//
|
|
116
|
+
// Analysis Operations
|
|
474
117
|
async findRelationPath(from, to, maxDepth = 5) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return { found: true, path: [from], relations: [] };
|
|
478
|
-
}
|
|
479
|
-
// Build indexes for efficient relation lookup
|
|
480
|
-
const relationsFrom = new Map();
|
|
481
|
-
const relationsTo = new Map();
|
|
482
|
-
for (const rel of graph.relations) {
|
|
483
|
-
if (!relationsFrom.has(rel.from)) {
|
|
484
|
-
relationsFrom.set(rel.from, []);
|
|
485
|
-
}
|
|
486
|
-
relationsFrom.get(rel.from).push(rel);
|
|
487
|
-
if (!relationsTo.has(rel.to)) {
|
|
488
|
-
relationsTo.set(rel.to, []);
|
|
489
|
-
}
|
|
490
|
-
relationsTo.get(rel.to).push(rel);
|
|
491
|
-
}
|
|
492
|
-
// BFS to find shortest path
|
|
493
|
-
const queue = [
|
|
494
|
-
{ entity: from, path: [from], relations: [] }
|
|
495
|
-
];
|
|
496
|
-
const visited = new Set([from]);
|
|
497
|
-
while (queue.length > 0) {
|
|
498
|
-
const current = queue.shift();
|
|
499
|
-
if (current.path.length > maxDepth) {
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
// Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
|
|
503
|
-
const outgoing = relationsFrom.get(current.entity) || [];
|
|
504
|
-
const incoming = relationsTo.get(current.entity) || [];
|
|
505
|
-
// Check outgoing relations
|
|
506
|
-
for (const rel of outgoing) {
|
|
507
|
-
if (rel.to === to) {
|
|
508
|
-
return {
|
|
509
|
-
found: true,
|
|
510
|
-
path: [...current.path, rel.to],
|
|
511
|
-
relations: [...current.relations, rel]
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
if (!visited.has(rel.to)) {
|
|
515
|
-
visited.add(rel.to);
|
|
516
|
-
queue.push({
|
|
517
|
-
entity: rel.to,
|
|
518
|
-
path: [...current.path, rel.to],
|
|
519
|
-
relations: [...current.relations, rel]
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
// Check incoming relations (traverse backwards)
|
|
524
|
-
for (const rel of incoming) {
|
|
525
|
-
if (rel.from === to) {
|
|
526
|
-
return {
|
|
527
|
-
found: true,
|
|
528
|
-
path: [...current.path, rel.from],
|
|
529
|
-
relations: [...current.relations, rel]
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
if (!visited.has(rel.from)) {
|
|
533
|
-
visited.add(rel.from);
|
|
534
|
-
queue.push({
|
|
535
|
-
entity: rel.from,
|
|
536
|
-
path: [...current.path, rel.from],
|
|
537
|
-
relations: [...current.relations, rel]
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return { found: false, path: [], relations: [] };
|
|
118
|
+
await this.ensureInitialized();
|
|
119
|
+
return PathFinder.findRelationPath(this.storage, from, to, maxDepth);
|
|
543
120
|
}
|
|
544
|
-
// Enhancement 4: Detect conflicting observations
|
|
545
121
|
async detectConflicts() {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
entity.observations[j].supersedes === entity.observations[i].id ||
|
|
557
|
-
entity.observations[i].superseded_by === entity.observations[j].id ||
|
|
558
|
-
entity.observations[j].superseded_by === entity.observations[i].id) {
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
// Check for negation patterns
|
|
562
|
-
const obs1HasNegation = this.hasNegation(obs1Content);
|
|
563
|
-
const obs2HasNegation = this.hasNegation(obs2Content);
|
|
564
|
-
// If one has negation and they share key words, might be a conflict
|
|
565
|
-
if (obs1HasNegation !== obs2HasNegation) {
|
|
566
|
-
const words1 = obs1Content.split(/\s+/).filter(w => w.length > 3);
|
|
567
|
-
const words2Set = new Set(obs2Content.split(/\s+/).filter(w => w.length > 3));
|
|
568
|
-
const commonWords = words1.filter(w => words2Set.has(w) && !KnowledgeGraphManager.NEGATION_WORDS.has(w));
|
|
569
|
-
if (commonWords.length >= 2) {
|
|
570
|
-
entityConflicts.push({
|
|
571
|
-
obs1: entity.observations[i].content,
|
|
572
|
-
obs2: entity.observations[j].content,
|
|
573
|
-
reason: 'Potential contradiction with negation'
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
if (entityConflicts.length > 0) {
|
|
580
|
-
conflicts.push({ entityName: entity.name, conflicts: entityConflicts });
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
return conflicts;
|
|
122
|
+
await this.ensureInitialized();
|
|
123
|
+
return ConflictDetector.detectConflicts(this.storage);
|
|
124
|
+
}
|
|
125
|
+
async getContext(entityNames, depth = 1) {
|
|
126
|
+
await this.ensureInitialized();
|
|
127
|
+
return ContextBuilder.getContext(this.storage, entityNames, depth);
|
|
128
|
+
}
|
|
129
|
+
async getAnalytics(threadId) {
|
|
130
|
+
await this.ensureInitialized();
|
|
131
|
+
return AnalyticsService.getAnalytics(this.storage, threadId);
|
|
584
132
|
}
|
|
585
|
-
//
|
|
133
|
+
// Memory Maintenance
|
|
586
134
|
async pruneMemory(options) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const initialRelationCount = graph.relations.length;
|
|
590
|
-
// Filter entities to remove
|
|
591
|
-
let entitiesToKeep = graph.entities;
|
|
592
|
-
if (options.olderThan) {
|
|
593
|
-
const cutoffDate = new Date(options.olderThan);
|
|
594
|
-
entitiesToKeep = entitiesToKeep.filter(e => new Date(e.timestamp) >= cutoffDate);
|
|
595
|
-
}
|
|
596
|
-
if (options.importanceLessThan !== undefined) {
|
|
597
|
-
entitiesToKeep = entitiesToKeep.filter(e => e.importance >= options.importanceLessThan);
|
|
598
|
-
}
|
|
599
|
-
// Ensure we keep minimum entities
|
|
600
|
-
// If keepMinEntities is set and we need more entities, take from the already-filtered set
|
|
601
|
-
// sorted by importance and recency
|
|
602
|
-
if (options.keepMinEntities && entitiesToKeep.length < options.keepMinEntities) {
|
|
603
|
-
// Sort the filtered entities by importance and timestamp, keep the most important and recent
|
|
604
|
-
const sorted = [...entitiesToKeep].sort((a, b) => {
|
|
605
|
-
if (a.importance !== b.importance)
|
|
606
|
-
return b.importance - a.importance;
|
|
607
|
-
return b.timestamp.localeCompare(a.timestamp);
|
|
608
|
-
});
|
|
609
|
-
// If we still don't have enough, we keep what we have
|
|
610
|
-
entitiesToKeep = sorted.slice(0, Math.min(options.keepMinEntities, sorted.length));
|
|
611
|
-
}
|
|
612
|
-
const keptEntityNames = new Set(entitiesToKeep.map(e => e.name));
|
|
613
|
-
// Remove relations that reference removed entities
|
|
614
|
-
const relationsToKeep = graph.relations.filter(r => keptEntityNames.has(r.from) && keptEntityNames.has(r.to));
|
|
615
|
-
graph.entities = entitiesToKeep;
|
|
616
|
-
graph.relations = relationsToKeep;
|
|
617
|
-
await this.storage.saveGraph(graph);
|
|
618
|
-
return {
|
|
619
|
-
removedEntities: initialEntityCount - entitiesToKeep.length,
|
|
620
|
-
removedRelations: initialRelationCount - relationsToKeep.length
|
|
621
|
-
};
|
|
135
|
+
await this.ensureInitialized();
|
|
136
|
+
return MemoryPruner.pruneMemory(this.storage, options);
|
|
622
137
|
}
|
|
623
|
-
// Enhancement 6: Batch operations
|
|
624
138
|
async bulkUpdate(updates) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const notFound = [];
|
|
628
|
-
for (const update of updates) {
|
|
629
|
-
const entity = graph.entities.find(e => e.name === update.entityName);
|
|
630
|
-
if (!entity) {
|
|
631
|
-
notFound.push(update.entityName);
|
|
632
|
-
continue;
|
|
633
|
-
}
|
|
634
|
-
if (update.confidence !== undefined) {
|
|
635
|
-
entity.confidence = update.confidence;
|
|
636
|
-
}
|
|
637
|
-
if (update.importance !== undefined) {
|
|
638
|
-
entity.importance = update.importance;
|
|
639
|
-
}
|
|
640
|
-
if (update.addObservations) {
|
|
641
|
-
// Filter out observations that already exist (by content)
|
|
642
|
-
const newObsContents = update.addObservations.filter(obsContent => !entity.observations.some(o => o.content === obsContent));
|
|
643
|
-
// Create Observation objects for new observations
|
|
644
|
-
const newObservations = newObsContents.map(content => ({
|
|
645
|
-
id: `obs_${randomUUID()}`,
|
|
646
|
-
content: content,
|
|
647
|
-
timestamp: new Date().toISOString(),
|
|
648
|
-
version: 1,
|
|
649
|
-
agentThreadId: entity.agentThreadId, // Use entity's thread ID
|
|
650
|
-
confidence: update.confidence ?? entity.confidence,
|
|
651
|
-
importance: update.importance ?? entity.importance
|
|
652
|
-
}));
|
|
653
|
-
entity.observations.push(...newObservations);
|
|
654
|
-
}
|
|
655
|
-
entity.timestamp = new Date().toISOString();
|
|
656
|
-
updated++;
|
|
657
|
-
}
|
|
658
|
-
await this.storage.saveGraph(graph);
|
|
659
|
-
return { updated, notFound };
|
|
139
|
+
await this.ensureInitialized();
|
|
140
|
+
return BulkUpdater.bulkUpdate(this.storage, updates);
|
|
660
141
|
}
|
|
661
|
-
//
|
|
142
|
+
// Collaboration Features
|
|
662
143
|
async flagForReview(entityName, reason, reviewer) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (!entity) {
|
|
666
|
-
throw new Error(`Entity with name ${entityName} not found`);
|
|
667
|
-
}
|
|
668
|
-
// Add a special observation to mark for review
|
|
669
|
-
const flagContent = `[FLAGGED FOR REVIEW: ${reason}${reviewer ? ` - Reviewer: ${reviewer}` : ''}]`;
|
|
670
|
-
// Check if this flag already exists (by content)
|
|
671
|
-
if (!entity.observations.some(o => o.content === flagContent)) {
|
|
672
|
-
const flagObservation = {
|
|
673
|
-
id: `obs_${randomUUID()}`,
|
|
674
|
-
content: flagContent,
|
|
675
|
-
timestamp: new Date().toISOString(),
|
|
676
|
-
version: 1,
|
|
677
|
-
agentThreadId: entity.agentThreadId,
|
|
678
|
-
confidence: 1.0, // Flag observations have full confidence
|
|
679
|
-
importance: 1.0 // Flag observations are highly important
|
|
680
|
-
};
|
|
681
|
-
entity.observations.push(flagObservation);
|
|
682
|
-
entity.timestamp = new Date().toISOString();
|
|
683
|
-
await this.storage.saveGraph(graph);
|
|
684
|
-
}
|
|
144
|
+
await this.ensureInitialized();
|
|
145
|
+
return FlagManager.flagForReview(this.storage, entityName, reason, reviewer);
|
|
685
146
|
}
|
|
686
|
-
// Enhancement 8: Get entities flagged for review
|
|
687
147
|
async getFlaggedEntities() {
|
|
688
|
-
|
|
689
|
-
return
|
|
690
|
-
}
|
|
691
|
-
// Enhancement 9: Get context (entities related to a topic/entity)
|
|
692
|
-
async getContext(entityNames, depth = 1) {
|
|
693
|
-
const graph = await this.storage.loadGraph();
|
|
694
|
-
const contextEntityNames = new Set(entityNames);
|
|
695
|
-
// Expand to include related entities up to specified depth
|
|
696
|
-
for (let d = 0; d < depth; d++) {
|
|
697
|
-
const currentEntities = Array.from(contextEntityNames);
|
|
698
|
-
for (const entityName of currentEntities) {
|
|
699
|
-
// Find all relations involving this entity
|
|
700
|
-
const relatedRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
|
|
701
|
-
// Add related entities
|
|
702
|
-
relatedRelations.forEach(r => {
|
|
703
|
-
contextEntityNames.add(r.from);
|
|
704
|
-
contextEntityNames.add(r.to);
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
// Get all entities and relations in context
|
|
709
|
-
const contextEntities = graph.entities.filter(e => contextEntityNames.has(e.name));
|
|
710
|
-
const contextRelations = graph.relations.filter(r => contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
|
|
711
|
-
return {
|
|
712
|
-
entities: contextEntities,
|
|
713
|
-
relations: contextRelations
|
|
714
|
-
};
|
|
148
|
+
await this.ensureInitialized();
|
|
149
|
+
return FlagManager.getFlaggedEntities(this.storage);
|
|
715
150
|
}
|
|
716
|
-
// Enhancement 10: List conversations (agent threads)
|
|
717
151
|
async listConversations() {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const threadMap = new Map();
|
|
721
|
-
// Collect entities by thread
|
|
722
|
-
for (const entity of graph.entities) {
|
|
723
|
-
if (!threadMap.has(entity.agentThreadId)) {
|
|
724
|
-
threadMap.set(entity.agentThreadId, { entities: [], relations: [], timestamps: [] });
|
|
725
|
-
}
|
|
726
|
-
const threadData = threadMap.get(entity.agentThreadId);
|
|
727
|
-
threadData.entities.push(entity);
|
|
728
|
-
threadData.timestamps.push(entity.timestamp);
|
|
729
|
-
}
|
|
730
|
-
// Collect relations by thread
|
|
731
|
-
for (const relation of graph.relations) {
|
|
732
|
-
if (!threadMap.has(relation.agentThreadId)) {
|
|
733
|
-
threadMap.set(relation.agentThreadId, { entities: [], relations: [], timestamps: [] });
|
|
734
|
-
}
|
|
735
|
-
const threadData = threadMap.get(relation.agentThreadId);
|
|
736
|
-
threadData.relations.push(relation);
|
|
737
|
-
threadData.timestamps.push(relation.timestamp);
|
|
738
|
-
}
|
|
739
|
-
// Build conversation summaries
|
|
740
|
-
const conversations = Array.from(threadMap.entries()).map(([agentThreadId, data]) => {
|
|
741
|
-
const timestamps = data.timestamps.sort((a, b) => a.localeCompare(b));
|
|
742
|
-
return {
|
|
743
|
-
agentThreadId,
|
|
744
|
-
entityCount: data.entities.length,
|
|
745
|
-
relationCount: data.relations.length,
|
|
746
|
-
firstCreated: timestamps[0] || '',
|
|
747
|
-
lastUpdated: timestamps[timestamps.length - 1] || ''
|
|
748
|
-
};
|
|
749
|
-
});
|
|
750
|
-
// Sort by last updated (most recent first)
|
|
751
|
-
conversations.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
|
|
752
|
-
return { conversations };
|
|
753
|
-
}
|
|
754
|
-
// Analytics: Get analytics for a specific thread (limited to 4 core metrics)
|
|
755
|
-
async getAnalytics(threadId) {
|
|
756
|
-
const graph = await this.storage.loadGraph();
|
|
757
|
-
// Filter to thread-specific data
|
|
758
|
-
const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
|
|
759
|
-
const threadRelations = graph.relations.filter(r => r.agentThreadId === threadId);
|
|
760
|
-
// 1. Recent changes (last 10, sorted by timestamp)
|
|
761
|
-
const recent_changes = threadEntities
|
|
762
|
-
.map(e => ({
|
|
763
|
-
entityName: e.name,
|
|
764
|
-
entityType: e.entityType,
|
|
765
|
-
lastModified: e.timestamp,
|
|
766
|
-
changeType: 'created' // Simplified: all are 'created' for now
|
|
767
|
-
}))
|
|
768
|
-
.sort((a, b) => b.lastModified.localeCompare(a.lastModified))
|
|
769
|
-
.slice(0, 10);
|
|
770
|
-
// 2. Top by importance (top 10)
|
|
771
|
-
const top_important = threadEntities
|
|
772
|
-
.map(e => ({
|
|
773
|
-
entityName: e.name,
|
|
774
|
-
entityType: e.entityType,
|
|
775
|
-
importance: e.importance,
|
|
776
|
-
observationCount: e.observations.length
|
|
777
|
-
}))
|
|
778
|
-
.sort((a, b) => b.importance - a.importance)
|
|
779
|
-
.slice(0, 10);
|
|
780
|
-
// 3. Most connected entities (top 10 by relation count)
|
|
781
|
-
const entityRelationCounts = new Map();
|
|
782
|
-
for (const entity of threadEntities) {
|
|
783
|
-
entityRelationCounts.set(entity.name, new Set());
|
|
784
|
-
}
|
|
785
|
-
for (const relation of threadRelations) {
|
|
786
|
-
if (entityRelationCounts.has(relation.from)) {
|
|
787
|
-
entityRelationCounts.get(relation.from).add(relation.to);
|
|
788
|
-
}
|
|
789
|
-
if (entityRelationCounts.has(relation.to)) {
|
|
790
|
-
entityRelationCounts.get(relation.to).add(relation.from);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
const most_connected = Array.from(entityRelationCounts.entries())
|
|
794
|
-
.map(([entityName, connectedSet]) => {
|
|
795
|
-
const entity = threadEntities.find(e => e.name === entityName);
|
|
796
|
-
return {
|
|
797
|
-
entityName,
|
|
798
|
-
entityType: entity.entityType,
|
|
799
|
-
relationCount: connectedSet.size,
|
|
800
|
-
connectedTo: Array.from(connectedSet)
|
|
801
|
-
};
|
|
802
|
-
})
|
|
803
|
-
.sort((a, b) => b.relationCount - a.relationCount)
|
|
804
|
-
.slice(0, 10);
|
|
805
|
-
// 4. Orphaned entities (entities with no relations or broken relations)
|
|
806
|
-
const orphaned_entities = [];
|
|
807
|
-
const allEntityNames = new Set(threadEntities.map(e => e.name));
|
|
808
|
-
for (const entity of threadEntities) {
|
|
809
|
-
const relationCount = entityRelationCounts.get(entity.name)?.size || 0;
|
|
810
|
-
if (relationCount === 0) {
|
|
811
|
-
orphaned_entities.push({
|
|
812
|
-
entityName: entity.name,
|
|
813
|
-
entityType: entity.entityType,
|
|
814
|
-
reason: 'no_relations'
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
else {
|
|
818
|
-
// Check for broken relations (pointing to non-existent entities)
|
|
819
|
-
const entityRelations = threadRelations.filter(r => r.from === entity.name || r.to === entity.name);
|
|
820
|
-
const hasBrokenRelation = entityRelations.some(r => !allEntityNames.has(r.from) || !allEntityNames.has(r.to));
|
|
821
|
-
if (hasBrokenRelation) {
|
|
822
|
-
orphaned_entities.push({
|
|
823
|
-
entityName: entity.name,
|
|
824
|
-
entityType: entity.entityType,
|
|
825
|
-
reason: 'broken_relation'
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
return {
|
|
831
|
-
recent_changes,
|
|
832
|
-
top_important,
|
|
833
|
-
most_connected,
|
|
834
|
-
orphaned_entities
|
|
835
|
-
};
|
|
152
|
+
await this.ensureInitialized();
|
|
153
|
+
return ConversationService.listConversations(this.storage);
|
|
836
154
|
}
|
|
837
|
-
// Observation Versioning
|
|
155
|
+
// Observation Versioning
|
|
838
156
|
async getObservationHistory(entityName, observationId) {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const entity = graph.entities.find(e => e.name === entityName);
|
|
842
|
-
if (!entity) {
|
|
843
|
-
throw new Error(`Entity '${entityName}' not found`);
|
|
844
|
-
}
|
|
845
|
-
// Find the starting observation
|
|
846
|
-
const startObs = entity.observations.find(o => o.id === observationId);
|
|
847
|
-
if (!startObs) {
|
|
848
|
-
throw new Error(`Observation '${observationId}' not found in entity '${entityName}'`);
|
|
849
|
-
}
|
|
850
|
-
// Build the version chain
|
|
851
|
-
const history = [];
|
|
852
|
-
// Trace backwards to find all predecessors
|
|
853
|
-
let currentObs = startObs;
|
|
854
|
-
const visited = new Set();
|
|
855
|
-
while (currentObs) {
|
|
856
|
-
if (visited.has(currentObs.id)) {
|
|
857
|
-
// Circular reference protection
|
|
858
|
-
break;
|
|
859
|
-
}
|
|
860
|
-
visited.add(currentObs.id);
|
|
861
|
-
history.unshift(currentObs); // Add to beginning for chronological order
|
|
862
|
-
// Find predecessor
|
|
863
|
-
if (currentObs.supersedes) {
|
|
864
|
-
currentObs = entity.observations.find(o => o.id === currentObs.supersedes);
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
break;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
// Trace forwards to find all successors
|
|
871
|
-
let forwardObs = startObs;
|
|
872
|
-
visited.clear();
|
|
873
|
-
while (forwardObs.superseded_by) {
|
|
874
|
-
if (visited.has(forwardObs.superseded_by)) {
|
|
875
|
-
// Circular reference protection
|
|
876
|
-
break;
|
|
877
|
-
}
|
|
878
|
-
const successor = entity.observations.find(o => o.id === forwardObs.superseded_by);
|
|
879
|
-
if (successor) {
|
|
880
|
-
visited.add(successor.id);
|
|
881
|
-
history.push(successor);
|
|
882
|
-
forwardObs = successor;
|
|
883
|
-
}
|
|
884
|
-
else {
|
|
885
|
-
break;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
return history;
|
|
157
|
+
await this.ensureInitialized();
|
|
158
|
+
return ObservationHistory.getObservationHistory(this.storage, entityName, observationId);
|
|
889
159
|
}
|
|
890
160
|
}
|