@danielsimonjr/memory-mcp 0.7.1 → 0.41.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/__tests__/edge-cases/edge-cases.test.js +406 -0
- package/dist/__tests__/file-path.test.js +5 -5
- package/dist/__tests__/integration/workflows.test.js +449 -0
- package/dist/__tests__/knowledge-graph.test.js +8 -3
- package/dist/__tests__/performance/benchmarks.test.js +411 -0
- package/dist/__tests__/unit/core/EntityManager.test.js +334 -0
- package/dist/__tests__/unit/core/GraphStorage.test.js +205 -0
- package/dist/__tests__/unit/core/RelationManager.test.js +274 -0
- package/dist/__tests__/unit/features/CompressionManager.test.js +350 -0
- package/dist/__tests__/unit/search/BasicSearch.test.js +311 -0
- package/dist/__tests__/unit/search/BooleanSearch.test.js +432 -0
- package/dist/__tests__/unit/search/FuzzySearch.test.js +448 -0
- package/dist/__tests__/unit/search/RankedSearch.test.js +379 -0
- package/dist/__tests__/unit/utils/levenshtein.test.js +77 -0
- package/dist/core/EntityManager.js +554 -0
- package/dist/core/GraphStorage.js +172 -0
- package/dist/core/KnowledgeGraphManager.js +400 -0
- package/dist/core/ObservationManager.js +129 -0
- package/dist/core/RelationManager.js +186 -0
- package/dist/core/TransactionManager.js +389 -0
- package/dist/core/index.js +9 -0
- package/dist/features/AnalyticsManager.js +222 -0
- package/dist/features/ArchiveManager.js +74 -0
- package/dist/features/BackupManager.js +311 -0
- package/dist/features/CompressionManager.js +310 -0
- package/dist/features/ExportManager.js +305 -0
- package/dist/features/HierarchyManager.js +219 -0
- package/dist/features/ImportExportManager.js +50 -0
- package/dist/features/ImportManager.js +328 -0
- package/dist/features/TagManager.js +210 -0
- package/dist/features/index.js +12 -0
- package/dist/index.js +13 -997
- package/dist/memory.jsonl +225 -0
- package/dist/search/BasicSearch.js +161 -0
- package/dist/search/BooleanSearch.js +304 -0
- package/dist/search/FuzzySearch.js +115 -0
- package/dist/search/RankedSearch.js +206 -0
- package/dist/search/SavedSearchManager.js +145 -0
- package/dist/search/SearchManager.js +305 -0
- package/dist/search/SearchSuggestions.js +57 -0
- package/dist/search/TFIDFIndexManager.js +217 -0
- package/dist/search/index.js +10 -0
- package/dist/server/MCPServer.js +889 -0
- package/dist/types/analytics.types.js +6 -0
- package/dist/types/entity.types.js +7 -0
- package/dist/types/import-export.types.js +7 -0
- package/dist/types/index.js +12 -0
- package/dist/types/search.types.js +7 -0
- package/dist/types/tag.types.js +6 -0
- package/dist/utils/constants.js +127 -0
- package/dist/utils/dateUtils.js +89 -0
- package/dist/utils/errors.js +121 -0
- package/dist/utils/index.js +13 -0
- package/dist/utils/levenshtein.js +62 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/pathUtils.js +115 -0
- package/dist/utils/schemas.js +184 -0
- package/dist/utils/searchCache.js +209 -0
- package/dist/utils/tfidf.js +90 -0
- package/dist/utils/validationUtils.js +109 -0
- package/package.json +50 -48
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hierarchy Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages entity parent-child relationships and hierarchical operations.
|
|
5
|
+
*
|
|
6
|
+
* @module features/HierarchyManager
|
|
7
|
+
*/
|
|
8
|
+
import { EntityNotFoundError, CycleDetectedError } from '../utils/errors.js';
|
|
9
|
+
/**
|
|
10
|
+
* Manages hierarchical entity relationships.
|
|
11
|
+
*/
|
|
12
|
+
export class HierarchyManager {
|
|
13
|
+
storage;
|
|
14
|
+
constructor(storage) {
|
|
15
|
+
this.storage = storage;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set the parent of an entity.
|
|
19
|
+
*
|
|
20
|
+
* Validates:
|
|
21
|
+
* - Entity and parent exist
|
|
22
|
+
* - Setting parent won't create a cycle
|
|
23
|
+
*
|
|
24
|
+
* @param entityName - Entity to set parent for
|
|
25
|
+
* @param parentName - Parent entity name (null to remove parent)
|
|
26
|
+
* @returns Updated entity
|
|
27
|
+
* @throws {EntityNotFoundError} If entity or parent not found
|
|
28
|
+
* @throws {CycleDetectedError} If setting parent would create a cycle
|
|
29
|
+
*/
|
|
30
|
+
async setEntityParent(entityName, parentName) {
|
|
31
|
+
const graph = await this.storage.loadGraph();
|
|
32
|
+
const entity = graph.entities.find(e => e.name === entityName);
|
|
33
|
+
if (!entity) {
|
|
34
|
+
throw new EntityNotFoundError(entityName);
|
|
35
|
+
}
|
|
36
|
+
// If setting a parent, validate it exists and doesn't create a cycle
|
|
37
|
+
if (parentName !== null) {
|
|
38
|
+
const parent = graph.entities.find(e => e.name === parentName);
|
|
39
|
+
if (!parent) {
|
|
40
|
+
throw new EntityNotFoundError(parentName);
|
|
41
|
+
}
|
|
42
|
+
// Check for cycles
|
|
43
|
+
if (this.wouldCreateCycle(graph, entityName, parentName)) {
|
|
44
|
+
throw new CycleDetectedError(entityName, parentName);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
entity.parentId = parentName || undefined;
|
|
48
|
+
entity.lastModified = new Date().toISOString();
|
|
49
|
+
await this.storage.saveGraph(graph);
|
|
50
|
+
return entity;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if setting a parent would create a cycle in the hierarchy.
|
|
54
|
+
*
|
|
55
|
+
* @param graph - Knowledge graph
|
|
56
|
+
* @param entityName - Entity to set parent for
|
|
57
|
+
* @param parentName - Proposed parent
|
|
58
|
+
* @returns True if cycle would be created
|
|
59
|
+
*/
|
|
60
|
+
wouldCreateCycle(graph, entityName, parentName) {
|
|
61
|
+
const visited = new Set();
|
|
62
|
+
let current = parentName;
|
|
63
|
+
while (current) {
|
|
64
|
+
if (visited.has(current)) {
|
|
65
|
+
return true; // Cycle detected in existing hierarchy
|
|
66
|
+
}
|
|
67
|
+
if (current === entityName) {
|
|
68
|
+
return true; // Would create a cycle
|
|
69
|
+
}
|
|
70
|
+
visited.add(current);
|
|
71
|
+
const currentEntity = graph.entities.find(e => e.name === current);
|
|
72
|
+
current = currentEntity?.parentId;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the immediate children of an entity.
|
|
78
|
+
*
|
|
79
|
+
* @param entityName - Parent entity name
|
|
80
|
+
* @returns Array of child entities
|
|
81
|
+
* @throws {EntityNotFoundError} If entity not found
|
|
82
|
+
*/
|
|
83
|
+
async getChildren(entityName) {
|
|
84
|
+
const graph = await this.storage.loadGraph();
|
|
85
|
+
// Verify entity exists
|
|
86
|
+
if (!graph.entities.find(e => e.name === entityName)) {
|
|
87
|
+
throw new EntityNotFoundError(entityName);
|
|
88
|
+
}
|
|
89
|
+
return graph.entities.filter(e => e.parentId === entityName);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the parent of an entity.
|
|
93
|
+
*
|
|
94
|
+
* @param entityName - Entity name
|
|
95
|
+
* @returns Parent entity or null if no parent
|
|
96
|
+
* @throws {EntityNotFoundError} If entity not found
|
|
97
|
+
*/
|
|
98
|
+
async getParent(entityName) {
|
|
99
|
+
const graph = await this.storage.loadGraph();
|
|
100
|
+
const entity = graph.entities.find(e => e.name === entityName);
|
|
101
|
+
if (!entity) {
|
|
102
|
+
throw new EntityNotFoundError(entityName);
|
|
103
|
+
}
|
|
104
|
+
if (!entity.parentId) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const parent = graph.entities.find(e => e.name === entity.parentId);
|
|
108
|
+
return parent || null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get all ancestors of an entity (parent, grandparent, etc.).
|
|
112
|
+
*
|
|
113
|
+
* @param entityName - Entity name
|
|
114
|
+
* @returns Array of ancestor entities (ordered from immediate parent to root)
|
|
115
|
+
* @throws {EntityNotFoundError} If entity not found
|
|
116
|
+
*/
|
|
117
|
+
async getAncestors(entityName) {
|
|
118
|
+
const graph = await this.storage.loadGraph();
|
|
119
|
+
const ancestors = [];
|
|
120
|
+
let current = graph.entities.find(e => e.name === entityName);
|
|
121
|
+
if (!current) {
|
|
122
|
+
throw new EntityNotFoundError(entityName);
|
|
123
|
+
}
|
|
124
|
+
while (current.parentId) {
|
|
125
|
+
const parent = graph.entities.find(e => e.name === current.parentId);
|
|
126
|
+
if (!parent)
|
|
127
|
+
break;
|
|
128
|
+
ancestors.push(parent);
|
|
129
|
+
current = parent;
|
|
130
|
+
}
|
|
131
|
+
return ancestors;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get all descendants of an entity (children, grandchildren, etc.).
|
|
135
|
+
*
|
|
136
|
+
* Uses breadth-first traversal.
|
|
137
|
+
*
|
|
138
|
+
* @param entityName - Entity name
|
|
139
|
+
* @returns Array of descendant entities
|
|
140
|
+
* @throws {EntityNotFoundError} If entity not found
|
|
141
|
+
*/
|
|
142
|
+
async getDescendants(entityName) {
|
|
143
|
+
const graph = await this.storage.loadGraph();
|
|
144
|
+
// Verify entity exists
|
|
145
|
+
if (!graph.entities.find(e => e.name === entityName)) {
|
|
146
|
+
throw new EntityNotFoundError(entityName);
|
|
147
|
+
}
|
|
148
|
+
const descendants = [];
|
|
149
|
+
const toProcess = [entityName];
|
|
150
|
+
while (toProcess.length > 0) {
|
|
151
|
+
const current = toProcess.shift();
|
|
152
|
+
const children = graph.entities.filter(e => e.parentId === current);
|
|
153
|
+
for (const child of children) {
|
|
154
|
+
descendants.push(child);
|
|
155
|
+
toProcess.push(child.name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return descendants;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get the entire subtree rooted at an entity (entity + all descendants).
|
|
162
|
+
*
|
|
163
|
+
* Includes relations between entities in the subtree.
|
|
164
|
+
*
|
|
165
|
+
* @param entityName - Root entity name
|
|
166
|
+
* @returns Knowledge graph containing subtree
|
|
167
|
+
* @throws {EntityNotFoundError} If entity not found
|
|
168
|
+
*/
|
|
169
|
+
async getSubtree(entityName) {
|
|
170
|
+
const graph = await this.storage.loadGraph();
|
|
171
|
+
const entity = graph.entities.find(e => e.name === entityName);
|
|
172
|
+
if (!entity) {
|
|
173
|
+
throw new EntityNotFoundError(entityName);
|
|
174
|
+
}
|
|
175
|
+
const descendants = await this.getDescendants(entityName);
|
|
176
|
+
const subtreeEntities = [entity, ...descendants];
|
|
177
|
+
const subtreeEntityNames = new Set(subtreeEntities.map(e => e.name));
|
|
178
|
+
// Include relations between entities in the subtree
|
|
179
|
+
const subtreeRelations = graph.relations.filter(r => subtreeEntityNames.has(r.from) && subtreeEntityNames.has(r.to));
|
|
180
|
+
return {
|
|
181
|
+
entities: subtreeEntities,
|
|
182
|
+
relations: subtreeRelations,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get root entities (entities with no parent).
|
|
187
|
+
*
|
|
188
|
+
* @returns Array of root entities
|
|
189
|
+
*/
|
|
190
|
+
async getRootEntities() {
|
|
191
|
+
const graph = await this.storage.loadGraph();
|
|
192
|
+
return graph.entities.filter(e => !e.parentId);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get the depth of an entity in the hierarchy.
|
|
196
|
+
*
|
|
197
|
+
* Root entities have depth 0, their children depth 1, etc.
|
|
198
|
+
*
|
|
199
|
+
* @param entityName - Entity name
|
|
200
|
+
* @returns Depth (number of ancestors)
|
|
201
|
+
* @throws Error if entity not found
|
|
202
|
+
*/
|
|
203
|
+
async getEntityDepth(entityName) {
|
|
204
|
+
const ancestors = await this.getAncestors(entityName);
|
|
205
|
+
return ancestors.length;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Move an entity to a new parent (maintaining its descendants).
|
|
209
|
+
*
|
|
210
|
+
* Alias for setEntityParent.
|
|
211
|
+
*
|
|
212
|
+
* @param entityName - Entity to move
|
|
213
|
+
* @param newParentName - New parent name (null to make root)
|
|
214
|
+
* @returns Updated entity
|
|
215
|
+
*/
|
|
216
|
+
async moveEntity(entityName, newParentName) {
|
|
217
|
+
return await this.setEntityParent(entityName, newParentName);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import/Export Manager
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates import and export operations with optional filtering.
|
|
5
|
+
*
|
|
6
|
+
* @module features/ImportExportManager
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Orchestrates import and export operations.
|
|
10
|
+
*/
|
|
11
|
+
export class ImportExportManager {
|
|
12
|
+
basicSearch;
|
|
13
|
+
exportManager;
|
|
14
|
+
importManager;
|
|
15
|
+
constructor(exportManager, importManager, basicSearch) {
|
|
16
|
+
this.basicSearch = basicSearch;
|
|
17
|
+
this.exportManager = exportManager;
|
|
18
|
+
this.importManager = importManager;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Export graph to specified format with optional filtering.
|
|
22
|
+
*
|
|
23
|
+
* @param format - Export format
|
|
24
|
+
* @param filter - Optional export filter
|
|
25
|
+
* @returns Formatted export string
|
|
26
|
+
*/
|
|
27
|
+
async exportGraph(format, filter) {
|
|
28
|
+
let graph;
|
|
29
|
+
if (filter) {
|
|
30
|
+
graph = await this.basicSearch.searchByDateRange(filter.startDate, filter.endDate, filter.entityType, filter.tags);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// Get full graph via basicSearch's storage
|
|
34
|
+
graph = await this.basicSearch.storage.loadGraph();
|
|
35
|
+
}
|
|
36
|
+
return this.exportManager.exportGraph(graph, format);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Import graph from formatted data.
|
|
40
|
+
*
|
|
41
|
+
* @param format - Import format
|
|
42
|
+
* @param data - Import data string
|
|
43
|
+
* @param mergeStrategy - How to handle conflicts
|
|
44
|
+
* @param dryRun - If true, preview changes without applying
|
|
45
|
+
* @returns Import result with statistics
|
|
46
|
+
*/
|
|
47
|
+
async importGraph(format, data, mergeStrategy, dryRun) {
|
|
48
|
+
return this.importManager.importGraph(format, data, mergeStrategy, dryRun);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import Manager
|
|
3
|
+
*
|
|
4
|
+
* Imports knowledge graphs from various formats (JSON, CSV, GraphML) with merge strategies.
|
|
5
|
+
*
|
|
6
|
+
* @module features/ImportManager
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Manages knowledge graph imports from multiple formats.
|
|
10
|
+
*/
|
|
11
|
+
export class ImportManager {
|
|
12
|
+
storage;
|
|
13
|
+
constructor(storage) {
|
|
14
|
+
this.storage = storage;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Import graph from formatted data.
|
|
18
|
+
*
|
|
19
|
+
* @param format - Import format
|
|
20
|
+
* @param data - Import data string
|
|
21
|
+
* @param mergeStrategy - How to handle conflicts
|
|
22
|
+
* @param dryRun - If true, preview changes without applying
|
|
23
|
+
* @returns Import result with statistics
|
|
24
|
+
*/
|
|
25
|
+
async importGraph(format, data, mergeStrategy = 'skip', dryRun = false) {
|
|
26
|
+
let importedGraph;
|
|
27
|
+
try {
|
|
28
|
+
switch (format) {
|
|
29
|
+
case 'json':
|
|
30
|
+
importedGraph = this.parseJsonImport(data);
|
|
31
|
+
break;
|
|
32
|
+
case 'csv':
|
|
33
|
+
importedGraph = this.parseCsvImport(data);
|
|
34
|
+
break;
|
|
35
|
+
case 'graphml':
|
|
36
|
+
importedGraph = this.parseGraphMLImport(data);
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(`Unsupported import format: ${format}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
entitiesAdded: 0,
|
|
45
|
+
entitiesSkipped: 0,
|
|
46
|
+
entitiesUpdated: 0,
|
|
47
|
+
relationsAdded: 0,
|
|
48
|
+
relationsSkipped: 0,
|
|
49
|
+
errors: [`Failed to parse ${format} data: ${error instanceof Error ? error.message : String(error)}`],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return await this.mergeImportedGraph(importedGraph, mergeStrategy, dryRun);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse JSON format.
|
|
56
|
+
*/
|
|
57
|
+
parseJsonImport(data) {
|
|
58
|
+
const parsed = JSON.parse(data);
|
|
59
|
+
if (!parsed.entities || !Array.isArray(parsed.entities)) {
|
|
60
|
+
throw new Error('Invalid JSON: missing or invalid entities array');
|
|
61
|
+
}
|
|
62
|
+
if (!parsed.relations || !Array.isArray(parsed.relations)) {
|
|
63
|
+
throw new Error('Invalid JSON: missing or invalid relations array');
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
entities: parsed.entities,
|
|
67
|
+
relations: parsed.relations,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parse CSV format.
|
|
72
|
+
*
|
|
73
|
+
* Expects: # ENTITIES section with header, then # RELATIONS section with header
|
|
74
|
+
*/
|
|
75
|
+
parseCsvImport(data) {
|
|
76
|
+
const lines = data
|
|
77
|
+
.split('\n')
|
|
78
|
+
.map(line => line.trim())
|
|
79
|
+
.filter(line => line);
|
|
80
|
+
const entities = [];
|
|
81
|
+
const relations = [];
|
|
82
|
+
let section = null;
|
|
83
|
+
let headerParsed = false;
|
|
84
|
+
const parseCsvLine = (line) => {
|
|
85
|
+
const fields = [];
|
|
86
|
+
let current = '';
|
|
87
|
+
let inQuotes = false;
|
|
88
|
+
for (let i = 0; i < line.length; i++) {
|
|
89
|
+
const char = line[i];
|
|
90
|
+
if (char === '"') {
|
|
91
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
92
|
+
current += '"';
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
inQuotes = !inQuotes;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (char === ',' && !inQuotes) {
|
|
100
|
+
fields.push(current);
|
|
101
|
+
current = '';
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
current += char;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
fields.push(current);
|
|
108
|
+
return fields;
|
|
109
|
+
};
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
if (line.startsWith('# ENTITIES')) {
|
|
112
|
+
section = 'entities';
|
|
113
|
+
headerParsed = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
else if (line.startsWith('# RELATIONS')) {
|
|
117
|
+
section = 'relations';
|
|
118
|
+
headerParsed = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (line.startsWith('#'))
|
|
122
|
+
continue;
|
|
123
|
+
if (section === 'entities') {
|
|
124
|
+
if (!headerParsed) {
|
|
125
|
+
headerParsed = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const fields = parseCsvLine(line);
|
|
129
|
+
if (fields.length >= 2) {
|
|
130
|
+
const entity = {
|
|
131
|
+
name: fields[0],
|
|
132
|
+
entityType: fields[1],
|
|
133
|
+
observations: fields[2]
|
|
134
|
+
? fields[2]
|
|
135
|
+
.split(';')
|
|
136
|
+
.map(s => s.trim())
|
|
137
|
+
.filter(s => s)
|
|
138
|
+
: [],
|
|
139
|
+
createdAt: fields[3] || undefined,
|
|
140
|
+
lastModified: fields[4] || undefined,
|
|
141
|
+
tags: fields[5]
|
|
142
|
+
? fields[5]
|
|
143
|
+
.split(';')
|
|
144
|
+
.map(s => s.trim().toLowerCase())
|
|
145
|
+
.filter(s => s)
|
|
146
|
+
: undefined,
|
|
147
|
+
importance: fields[6] ? parseFloat(fields[6]) : undefined,
|
|
148
|
+
};
|
|
149
|
+
entities.push(entity);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (section === 'relations') {
|
|
153
|
+
if (!headerParsed) {
|
|
154
|
+
headerParsed = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const fields = parseCsvLine(line);
|
|
158
|
+
if (fields.length >= 3) {
|
|
159
|
+
const relation = {
|
|
160
|
+
from: fields[0],
|
|
161
|
+
to: fields[1],
|
|
162
|
+
relationType: fields[2],
|
|
163
|
+
createdAt: fields[3] || undefined,
|
|
164
|
+
lastModified: fields[4] || undefined,
|
|
165
|
+
};
|
|
166
|
+
relations.push(relation);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { entities, relations };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Parse GraphML format.
|
|
174
|
+
*
|
|
175
|
+
* Note: Simplified regex-based parser for basic GraphML structure.
|
|
176
|
+
*/
|
|
177
|
+
parseGraphMLImport(data) {
|
|
178
|
+
const entities = [];
|
|
179
|
+
const relations = [];
|
|
180
|
+
// Extract nodes
|
|
181
|
+
const nodeRegex = /<node\s+id="([^"]+)"[^>]*>([\s\S]*?)<\/node>/g;
|
|
182
|
+
let nodeMatch;
|
|
183
|
+
while ((nodeMatch = nodeRegex.exec(data)) !== null) {
|
|
184
|
+
const nodeId = nodeMatch[1];
|
|
185
|
+
const nodeContent = nodeMatch[2];
|
|
186
|
+
const getDataValue = (key) => {
|
|
187
|
+
const dataRegex = new RegExp(`<data\\s+key="${key}">([^<]*)<\/data>`);
|
|
188
|
+
const match = dataRegex.exec(nodeContent);
|
|
189
|
+
return match ? match[1] : undefined;
|
|
190
|
+
};
|
|
191
|
+
const entity = {
|
|
192
|
+
name: nodeId,
|
|
193
|
+
entityType: getDataValue('d0') || getDataValue('entityType') || 'unknown',
|
|
194
|
+
observations: (getDataValue('d1') || getDataValue('observations') || '')
|
|
195
|
+
.split(';')
|
|
196
|
+
.map(s => s.trim())
|
|
197
|
+
.filter(s => s),
|
|
198
|
+
createdAt: getDataValue('d2') || getDataValue('createdAt'),
|
|
199
|
+
lastModified: getDataValue('d3') || getDataValue('lastModified'),
|
|
200
|
+
tags: (getDataValue('d4') || getDataValue('tags') || '')
|
|
201
|
+
.split(';')
|
|
202
|
+
.map(s => s.trim().toLowerCase())
|
|
203
|
+
.filter(s => s),
|
|
204
|
+
importance: getDataValue('d5') || getDataValue('importance') ? parseFloat(getDataValue('d5') || getDataValue('importance') || '0') : undefined,
|
|
205
|
+
};
|
|
206
|
+
entities.push(entity);
|
|
207
|
+
}
|
|
208
|
+
// Extract edges
|
|
209
|
+
const edgeRegex = /<edge\s+[^>]*source="([^"]+)"\s+target="([^"]+)"[^>]*>([\s\S]*?)<\/edge>/g;
|
|
210
|
+
let edgeMatch;
|
|
211
|
+
while ((edgeMatch = edgeRegex.exec(data)) !== null) {
|
|
212
|
+
const source = edgeMatch[1];
|
|
213
|
+
const target = edgeMatch[2];
|
|
214
|
+
const edgeContent = edgeMatch[3];
|
|
215
|
+
const getDataValue = (key) => {
|
|
216
|
+
const dataRegex = new RegExp(`<data\\s+key="${key}">([^<]*)<\/data>`);
|
|
217
|
+
const match = dataRegex.exec(edgeContent);
|
|
218
|
+
return match ? match[1] : undefined;
|
|
219
|
+
};
|
|
220
|
+
const relation = {
|
|
221
|
+
from: source,
|
|
222
|
+
to: target,
|
|
223
|
+
relationType: getDataValue('e0') || getDataValue('relationType') || 'related_to',
|
|
224
|
+
createdAt: getDataValue('e1') || getDataValue('createdAt'),
|
|
225
|
+
lastModified: getDataValue('e2') || getDataValue('lastModified'),
|
|
226
|
+
};
|
|
227
|
+
relations.push(relation);
|
|
228
|
+
}
|
|
229
|
+
return { entities, relations };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Merge imported graph with existing graph.
|
|
233
|
+
*/
|
|
234
|
+
async mergeImportedGraph(importedGraph, mergeStrategy, dryRun) {
|
|
235
|
+
const existingGraph = await this.storage.loadGraph();
|
|
236
|
+
const result = {
|
|
237
|
+
entitiesAdded: 0,
|
|
238
|
+
entitiesSkipped: 0,
|
|
239
|
+
entitiesUpdated: 0,
|
|
240
|
+
relationsAdded: 0,
|
|
241
|
+
relationsSkipped: 0,
|
|
242
|
+
errors: [],
|
|
243
|
+
};
|
|
244
|
+
const existingEntitiesMap = new Map();
|
|
245
|
+
for (const entity of existingGraph.entities) {
|
|
246
|
+
existingEntitiesMap.set(entity.name, entity);
|
|
247
|
+
}
|
|
248
|
+
const existingRelationsSet = new Set();
|
|
249
|
+
for (const relation of existingGraph.relations) {
|
|
250
|
+
existingRelationsSet.add(`${relation.from}|${relation.to}|${relation.relationType}`);
|
|
251
|
+
}
|
|
252
|
+
// Process entities
|
|
253
|
+
for (const importedEntity of importedGraph.entities) {
|
|
254
|
+
const existing = existingEntitiesMap.get(importedEntity.name);
|
|
255
|
+
if (!existing) {
|
|
256
|
+
result.entitiesAdded++;
|
|
257
|
+
if (!dryRun) {
|
|
258
|
+
existingGraph.entities.push(importedEntity);
|
|
259
|
+
existingEntitiesMap.set(importedEntity.name, importedEntity);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
switch (mergeStrategy) {
|
|
264
|
+
case 'replace':
|
|
265
|
+
result.entitiesUpdated++;
|
|
266
|
+
if (!dryRun) {
|
|
267
|
+
Object.assign(existing, importedEntity);
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
case 'skip':
|
|
271
|
+
result.entitiesSkipped++;
|
|
272
|
+
break;
|
|
273
|
+
case 'merge':
|
|
274
|
+
result.entitiesUpdated++;
|
|
275
|
+
if (!dryRun) {
|
|
276
|
+
existing.observations = [
|
|
277
|
+
...new Set([...existing.observations, ...importedEntity.observations]),
|
|
278
|
+
];
|
|
279
|
+
if (importedEntity.tags) {
|
|
280
|
+
existing.tags = existing.tags || [];
|
|
281
|
+
existing.tags = [...new Set([...existing.tags, ...importedEntity.tags])];
|
|
282
|
+
}
|
|
283
|
+
if (importedEntity.importance !== undefined) {
|
|
284
|
+
existing.importance = importedEntity.importance;
|
|
285
|
+
}
|
|
286
|
+
existing.lastModified = new Date().toISOString();
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
case 'fail':
|
|
290
|
+
result.errors.push(`Entity "${importedEntity.name}" already exists`);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Process relations
|
|
296
|
+
for (const importedRelation of importedGraph.relations) {
|
|
297
|
+
const relationKey = `${importedRelation.from}|${importedRelation.to}|${importedRelation.relationType}`;
|
|
298
|
+
if (!existingEntitiesMap.has(importedRelation.from)) {
|
|
299
|
+
result.errors.push(`Relation source entity "${importedRelation.from}" does not exist`);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!existingEntitiesMap.has(importedRelation.to)) {
|
|
303
|
+
result.errors.push(`Relation target entity "${importedRelation.to}" does not exist`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (!existingRelationsSet.has(relationKey)) {
|
|
307
|
+
result.relationsAdded++;
|
|
308
|
+
if (!dryRun) {
|
|
309
|
+
existingGraph.relations.push(importedRelation);
|
|
310
|
+
existingRelationsSet.add(relationKey);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
if (mergeStrategy === 'fail') {
|
|
315
|
+
result.errors.push(`Relation "${relationKey}" already exists`);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
result.relationsSkipped++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Save if not dry run and no blocking errors
|
|
323
|
+
if (!dryRun && (mergeStrategy !== 'fail' || result.errors.length === 0)) {
|
|
324
|
+
await this.storage.saveGraph(existingGraph);
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
}
|