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
|
@@ -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,48 @@
|
|
|
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
|
+
*/
|
|
9
|
+
export async function detectConflicts(storage) {
|
|
10
|
+
const graph = await storage.loadGraph();
|
|
11
|
+
const conflicts = [];
|
|
12
|
+
for (const entity of graph.entities) {
|
|
13
|
+
const entityConflicts = [];
|
|
14
|
+
for (let i = 0; i < entity.observations.length; i++) {
|
|
15
|
+
for (let j = i + 1; j < entity.observations.length; j++) {
|
|
16
|
+
const obs1Content = entity.observations[i].content.toLowerCase();
|
|
17
|
+
const obs2Content = entity.observations[j].content.toLowerCase();
|
|
18
|
+
// Skip if observations are in the same version chain
|
|
19
|
+
if (entity.observations[i].supersedes === entity.observations[j].id ||
|
|
20
|
+
entity.observations[j].supersedes === entity.observations[i].id ||
|
|
21
|
+
entity.observations[i].superseded_by === entity.observations[j].id ||
|
|
22
|
+
entity.observations[j].superseded_by === entity.observations[i].id) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
// Check for negation patterns
|
|
26
|
+
const obs1HasNegation = hasNegation(obs1Content);
|
|
27
|
+
const obs2HasNegation = hasNegation(obs2Content);
|
|
28
|
+
// If one has negation and they share key words, might be a conflict
|
|
29
|
+
if (obs1HasNegation !== obs2HasNegation) {
|
|
30
|
+
const words1 = obs1Content.split(/\s+/).filter(w => w.length > 3);
|
|
31
|
+
const words2Set = new Set(obs2Content.split(/\s+/).filter(w => w.length > 3));
|
|
32
|
+
const commonWords = words1.filter(w => words2Set.has(w) && !NEGATION_WORDS.has(w));
|
|
33
|
+
if (commonWords.length >= 2) {
|
|
34
|
+
entityConflicts.push({
|
|
35
|
+
obs1: entity.observations[i].content,
|
|
36
|
+
obs2: entity.observations[j].content,
|
|
37
|
+
reason: 'Potential contradiction with negation'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (entityConflicts.length > 0) {
|
|
44
|
+
conflicts.push({ entityName: entity.name, conflicts: entityConflicts });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return conflicts;
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
*/
|
|
8
|
+
export async function getContext(storage, entityNames, depth = 1) {
|
|
9
|
+
const graph = await storage.loadGraph();
|
|
10
|
+
const contextEntityNames = new Set(entityNames);
|
|
11
|
+
// Expand to include related entities up to specified depth
|
|
12
|
+
for (let d = 0; d < depth; d++) {
|
|
13
|
+
const currentEntities = Array.from(contextEntityNames);
|
|
14
|
+
for (const entityName of currentEntities) {
|
|
15
|
+
// Find all relations involving this entity
|
|
16
|
+
const relatedRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
|
|
17
|
+
// Add related entities
|
|
18
|
+
relatedRelations.forEach(r => {
|
|
19
|
+
contextEntityNames.add(r.from);
|
|
20
|
+
contextEntityNames.add(r.to);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Get all entities and relations in context
|
|
25
|
+
const contextEntities = graph.entities.filter(e => contextEntityNames.has(e.name));
|
|
26
|
+
const contextRelations = graph.relations.filter(r => contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
|
|
27
|
+
return {
|
|
28
|
+
entities: contextEntities,
|
|
29
|
+
relations: contextRelations
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory statistics service
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Get comprehensive memory statistics
|
|
6
|
+
*/
|
|
7
|
+
export async function getMemoryStats(storage) {
|
|
8
|
+
const graph = await storage.loadGraph();
|
|
9
|
+
// Count entity types
|
|
10
|
+
const entityTypes = {};
|
|
11
|
+
graph.entities.forEach(e => {
|
|
12
|
+
entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
|
|
13
|
+
});
|
|
14
|
+
// Calculate averages
|
|
15
|
+
const avgConfidence = graph.entities.length > 0
|
|
16
|
+
? graph.entities.reduce((sum, e) => sum + e.confidence, 0) / graph.entities.length
|
|
17
|
+
: 0;
|
|
18
|
+
const avgImportance = graph.entities.length > 0
|
|
19
|
+
? graph.entities.reduce((sum, e) => sum + e.importance, 0) / graph.entities.length
|
|
20
|
+
: 0;
|
|
21
|
+
// Count unique threads
|
|
22
|
+
const threads = new Set([
|
|
23
|
+
...graph.entities.map(e => e.agentThreadId),
|
|
24
|
+
...graph.relations.map(r => r.agentThreadId)
|
|
25
|
+
]);
|
|
26
|
+
// Recent activity (last 7 days, grouped by day)
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
29
|
+
const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
|
|
30
|
+
// Group by day
|
|
31
|
+
const activityByDay = {};
|
|
32
|
+
recentEntities.forEach(e => {
|
|
33
|
+
const day = e.timestamp.substring(0, 10); // YYYY-MM-DD
|
|
34
|
+
activityByDay[day] = (activityByDay[day] || 0) + 1;
|
|
35
|
+
});
|
|
36
|
+
const recentActivity = Object.entries(activityByDay)
|
|
37
|
+
.map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
|
|
38
|
+
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
39
|
+
return {
|
|
40
|
+
entityCount: graph.entities.length,
|
|
41
|
+
relationCount: graph.relations.length,
|
|
42
|
+
threadCount: threads.size,
|
|
43
|
+
entityTypes,
|
|
44
|
+
avgConfidence,
|
|
45
|
+
avgImportance,
|
|
46
|
+
recentActivity
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get recent changes since a specific timestamp
|
|
51
|
+
*/
|
|
52
|
+
export async function getRecentChanges(storage, since) {
|
|
53
|
+
const graph = await storage.loadGraph();
|
|
54
|
+
const sinceDate = new Date(since);
|
|
55
|
+
// Only return entities and relations that were actually modified since the specified time
|
|
56
|
+
const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sinceDate);
|
|
57
|
+
// Only include relations that are recent themselves
|
|
58
|
+
const recentRelations = graph.relations.filter(r => new Date(r.timestamp) >= sinceDate);
|
|
59
|
+
return {
|
|
60
|
+
entities: recentEntities,
|
|
61
|
+
relations: recentRelations
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
*/
|
|
8
|
+
export async function findRelationPath(storage, from, to, maxDepth = 5) {
|
|
9
|
+
const graph = await storage.loadGraph();
|
|
10
|
+
if (from === to) {
|
|
11
|
+
return { found: true, path: [from], relations: [] };
|
|
12
|
+
}
|
|
13
|
+
// Build indexes for efficient relation lookup
|
|
14
|
+
const relationsFrom = new Map();
|
|
15
|
+
const relationsTo = new Map();
|
|
16
|
+
for (const rel of graph.relations) {
|
|
17
|
+
if (!relationsFrom.has(rel.from)) {
|
|
18
|
+
relationsFrom.set(rel.from, []);
|
|
19
|
+
}
|
|
20
|
+
relationsFrom.get(rel.from).push(rel);
|
|
21
|
+
if (!relationsTo.has(rel.to)) {
|
|
22
|
+
relationsTo.set(rel.to, []);
|
|
23
|
+
}
|
|
24
|
+
relationsTo.get(rel.to).push(rel);
|
|
25
|
+
}
|
|
26
|
+
// BFS to find shortest path
|
|
27
|
+
const queue = [
|
|
28
|
+
{ entity: from, path: [from], relations: [] }
|
|
29
|
+
];
|
|
30
|
+
const visited = new Set([from]);
|
|
31
|
+
while (queue.length > 0) {
|
|
32
|
+
const current = queue.shift();
|
|
33
|
+
if (current.path.length > maxDepth) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
|
|
37
|
+
const outgoing = relationsFrom.get(current.entity) || [];
|
|
38
|
+
const incoming = relationsTo.get(current.entity) || [];
|
|
39
|
+
// Check outgoing relations
|
|
40
|
+
for (const rel of outgoing) {
|
|
41
|
+
if (rel.to === to) {
|
|
42
|
+
return {
|
|
43
|
+
found: true,
|
|
44
|
+
path: [...current.path, rel.to],
|
|
45
|
+
relations: [...current.relations, rel]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!visited.has(rel.to)) {
|
|
49
|
+
visited.add(rel.to);
|
|
50
|
+
queue.push({
|
|
51
|
+
entity: rel.to,
|
|
52
|
+
path: [...current.path, rel.to],
|
|
53
|
+
relations: [...current.relations, rel]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Check incoming relations (traverse backwards)
|
|
58
|
+
for (const rel of incoming) {
|
|
59
|
+
if (rel.from === to) {
|
|
60
|
+
return {
|
|
61
|
+
found: true,
|
|
62
|
+
path: [...current.path, rel.from],
|
|
63
|
+
relations: [...current.relations, rel]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (!visited.has(rel.from)) {
|
|
67
|
+
visited.add(rel.from);
|
|
68
|
+
queue.push({
|
|
69
|
+
entity: rel.from,
|
|
70
|
+
path: [...current.path, rel.from],
|
|
71
|
+
relations: [...current.relations, rel]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { found: false, path: [], relations: [] };
|
|
77
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collaboration features (flagging, review)
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Flag an entity for review
|
|
7
|
+
*/
|
|
8
|
+
export async function flagForReview(storage, entityName, reason, reviewer) {
|
|
9
|
+
const graph = await storage.loadGraph();
|
|
10
|
+
const entity = graph.entities.find(e => e.name === entityName);
|
|
11
|
+
if (!entity) {
|
|
12
|
+
throw new Error(`Entity with name ${entityName} not found`);
|
|
13
|
+
}
|
|
14
|
+
// Add a special observation to mark for review
|
|
15
|
+
const flagContent = `[FLAGGED FOR REVIEW: ${reason}${reviewer ? ` - Reviewer: ${reviewer}` : ''}]`;
|
|
16
|
+
// Check if this flag already exists (by content)
|
|
17
|
+
if (!entity.observations.some(o => o.content === flagContent)) {
|
|
18
|
+
const flagObservation = {
|
|
19
|
+
id: `obs_${randomUUID()}`,
|
|
20
|
+
content: flagContent,
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
version: 1,
|
|
23
|
+
agentThreadId: entity.agentThreadId,
|
|
24
|
+
confidence: 1.0, // Flag observations have full confidence
|
|
25
|
+
importance: 1.0 // Flag observations are highly important
|
|
26
|
+
};
|
|
27
|
+
entity.observations.push(flagObservation);
|
|
28
|
+
entity.timestamp = new Date().toISOString();
|
|
29
|
+
await storage.saveGraph(graph);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get all entities flagged for review
|
|
34
|
+
*/
|
|
35
|
+
export async function getFlaggedEntities(storage) {
|
|
36
|
+
const graph = await storage.loadGraph();
|
|
37
|
+
return graph.entities.filter(e => e.observations.some(obs => obs.content.includes('[FLAGGED FOR REVIEW:')));
|
|
38
|
+
}
|