rag-memory-epf-mcp 1.5.0 → 1.7.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/README.md +6 -0
- package/dist/index.js +328 -26
- package/dist/index.js.map +1 -1
- package/dist/src/migrations/migrations.d.ts.map +1 -1
- package/dist/src/migrations/migrations.js +105 -0
- package/dist/src/migrations/migrations.js.map +1 -1
- package/dist/src/tools/graph-query-tools.d.ts +4 -0
- package/dist/src/tools/graph-query-tools.d.ts.map +1 -1
- package/dist/src/tools/graph-query-tools.js +156 -1
- package/dist/src/tools/graph-query-tools.js.map +1 -1
- package/dist/src/tools/knowledge-graph-tools.d.ts +2 -0
- package/dist/src/tools/knowledge-graph-tools.d.ts.map +1 -1
- package/dist/src/tools/knowledge-graph-tools.js +62 -0
- package/dist/src/tools/knowledge-graph-tools.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +3 -0
- package/dist/src/tools/tool-registry.d.ts.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -130,6 +130,12 @@ Your entities, relationships, documents, and chunk text are preserved. Only vect
|
|
|
130
130
|
|
|
131
131
|
## Changelog
|
|
132
132
|
|
|
133
|
+
### v1.6.0
|
|
134
|
+
|
|
135
|
+
- **Entity upsert** — `createEntities` now merges new observations into existing entities instead of silently ignoring duplicates. Entity type is also updated if a more specific type is provided.
|
|
136
|
+
- **Automatic observation timestamps** — all new observations are prefixed with `[YYYY-MM-DD]` for staleness tracking. Existing dated observations are preserved as-is.
|
|
137
|
+
- **Dedup by content** — date prefixes are stripped when comparing observations to prevent duplicate entries with different dates.
|
|
138
|
+
|
|
133
139
|
### v1.5.0
|
|
134
140
|
|
|
135
141
|
- **Improved auto entity linking** — chunk-level precision instead of linking to all chunks
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,14 @@ const DB_FILE_PATH = process.env.DB_FILE_PATH
|
|
|
24
24
|
? process.env.DB_FILE_PATH
|
|
25
25
|
: path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.DB_FILE_PATH)
|
|
26
26
|
: defaultDbPath;
|
|
27
|
+
// Safe rowid for vec0 virtual tables (require literal integer, not parameterized)
|
|
28
|
+
function safeRowid(value) {
|
|
29
|
+
const n = Number(value);
|
|
30
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
31
|
+
throw new Error(`Invalid rowid: ${value}`);
|
|
32
|
+
}
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
27
35
|
// Enhanced RAG-enabled Knowledge Graph Manager
|
|
28
36
|
class RAGKnowledgeGraphManager {
|
|
29
37
|
db = null;
|
|
@@ -36,6 +44,14 @@ class RAGKnowledgeGraphManager {
|
|
|
36
44
|
this.db = new Database(DB_FILE_PATH);
|
|
37
45
|
// Load sqlite-vec extension
|
|
38
46
|
sqliteVec.load(this.db);
|
|
47
|
+
// SQLite performance & safety optimizations
|
|
48
|
+
this.db.pragma('journal_mode = WAL');
|
|
49
|
+
this.db.pragma('synchronous = NORMAL');
|
|
50
|
+
this.db.pragma('busy_timeout = 5000');
|
|
51
|
+
this.db.pragma('cache_size = -32000');
|
|
52
|
+
this.db.pragma('temp_store = MEMORY');
|
|
53
|
+
this.db.pragma('mmap_size = 268435456');
|
|
54
|
+
this.db.pragma('foreign_keys = ON');
|
|
39
55
|
// Initialize tiktoken
|
|
40
56
|
this.encoding = get_encoding("cl100k_base");
|
|
41
57
|
// Initialize embedding model
|
|
@@ -106,27 +122,56 @@ class RAGKnowledgeGraphManager {
|
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
124
|
// === ORIGINAL MCP FUNCTIONALITY ===
|
|
125
|
+
_timestampObservation(obs) {
|
|
126
|
+
// If observation already has a date prefix like [2026-03-21], skip
|
|
127
|
+
if (/^\[\d{4}-\d{2}-\d{2}\]/.test(obs))
|
|
128
|
+
return obs;
|
|
129
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
130
|
+
return `[${today}] ${obs}`;
|
|
131
|
+
}
|
|
109
132
|
async createEntities(entities) {
|
|
110
133
|
if (!this.db)
|
|
111
134
|
throw new Error('Database not initialized');
|
|
112
|
-
const
|
|
113
|
-
const
|
|
135
|
+
const result = [];
|
|
136
|
+
const insertStmt = this.db.prepare(`
|
|
114
137
|
INSERT OR IGNORE INTO entities (id, name, entityType, observations, metadata)
|
|
115
138
|
VALUES (?, ?, ?, ?, ?)
|
|
116
139
|
`);
|
|
117
140
|
for (const entity of entities) {
|
|
118
141
|
const entityId = `entity_${entity.name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
const timestamped = (entity.observations || []).map(o => this._timestampObservation(o));
|
|
143
|
+
// Try insert first
|
|
144
|
+
const insertResult = insertStmt.run(entityId, entity.name, entity.entityType, JSON.stringify(timestamped), '{}');
|
|
145
|
+
if (insertResult.changes > 0) {
|
|
146
|
+
// New entity created
|
|
147
|
+
result.push({ ...entity, observations: timestamped });
|
|
125
148
|
console.error(`🔮 Generating embedding for new entity: ${entity.name}`);
|
|
126
149
|
await this.embedEntity(entityId);
|
|
127
150
|
}
|
|
151
|
+
else {
|
|
152
|
+
// Entity already exists — upsert: merge observations and update entityType
|
|
153
|
+
const existing = this.db.prepare(`SELECT observations, entityType FROM entities WHERE id = ?`)
|
|
154
|
+
.get(entityId);
|
|
155
|
+
if (existing) {
|
|
156
|
+
const currentObs = JSON.parse(existing.observations);
|
|
157
|
+
// Strip date prefix for dedup comparison
|
|
158
|
+
const stripDate = (s) => s.replace(/^\[\d{4}-\d{2}-\d{2}\]\s*/, '');
|
|
159
|
+
const currentBare = new Set(currentObs.map(stripDate));
|
|
160
|
+
const newObs = timestamped.filter(o => !currentBare.has(stripDate(o)));
|
|
161
|
+
const needsTypeUpdate = entity.entityType && entity.entityType !== 'CONCEPT' && entity.entityType !== existing.entityType;
|
|
162
|
+
if (newObs.length > 0 || needsTypeUpdate) {
|
|
163
|
+
const mergedObs = [...currentObs, ...newObs];
|
|
164
|
+
const updatedType = needsTypeUpdate ? entity.entityType : existing.entityType;
|
|
165
|
+
this.db.prepare(`UPDATE entities SET observations = ?, entityType = ? WHERE id = ?`)
|
|
166
|
+
.run(JSON.stringify(mergedObs), updatedType, entityId);
|
|
167
|
+
console.error(`♻️ Upserted entity: ${entity.name} (+${newObs.length} obs${needsTypeUpdate ? ', type→' + updatedType : ''})`);
|
|
168
|
+
await this.embedEntity(entityId);
|
|
169
|
+
result.push({ ...entity, observations: mergedObs });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
128
173
|
}
|
|
129
|
-
return
|
|
174
|
+
return result;
|
|
130
175
|
}
|
|
131
176
|
async createRelations(relations) {
|
|
132
177
|
if (!this.db)
|
|
@@ -167,7 +212,10 @@ class RAGKnowledgeGraphManager {
|
|
|
167
212
|
throw new Error(`Entity with name ${obs.entityName} not found`);
|
|
168
213
|
}
|
|
169
214
|
const currentObservations = JSON.parse(entity.observations);
|
|
170
|
-
const
|
|
215
|
+
const stripDate = (s) => s.replace(/^\[\d{4}-\d{2}-\d{2}\]\s*/, '');
|
|
216
|
+
const currentBare = new Set(currentObservations.map(stripDate));
|
|
217
|
+
const timestamped = obs.contents.map(c => this._timestampObservation(c));
|
|
218
|
+
const newObservations = timestamped.filter(c => !currentBare.has(stripDate(c)));
|
|
171
219
|
if (newObservations.length > 0) {
|
|
172
220
|
const updatedObservations = [...currentObservations, ...newObservations];
|
|
173
221
|
this.db.prepare(`
|
|
@@ -268,11 +316,50 @@ class RAGKnowledgeGraphManager {
|
|
|
268
316
|
const sourceId = `entity_${relation.from.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
|
|
269
317
|
const targetId = `entity_${relation.to.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
|
|
270
318
|
this.db.prepare(`
|
|
271
|
-
DELETE FROM relationships
|
|
319
|
+
DELETE FROM relationships
|
|
272
320
|
WHERE source_entity = ? AND target_entity = ? AND relationType = ?
|
|
273
321
|
`).run(sourceId, targetId, relation.relationType);
|
|
274
322
|
}
|
|
275
323
|
}
|
|
324
|
+
async updateRelations(updates) {
|
|
325
|
+
if (!this.db)
|
|
326
|
+
throw new Error('Database not initialized');
|
|
327
|
+
let updated = 0;
|
|
328
|
+
let notFound = 0;
|
|
329
|
+
for (const update of updates) {
|
|
330
|
+
const sourceId = `entity_${update.from.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
|
|
331
|
+
const targetId = `entity_${update.to.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
|
|
332
|
+
const relationId = `rel_${sourceId}_${update.relationType}_${targetId}`.toLowerCase();
|
|
333
|
+
// Check if relation exists
|
|
334
|
+
const existing = this.db.prepare(`
|
|
335
|
+
SELECT id FROM relationships WHERE id = ?
|
|
336
|
+
`).get(relationId);
|
|
337
|
+
if (!existing) {
|
|
338
|
+
notFound++;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
// Build dynamic update
|
|
342
|
+
const setClauses = [];
|
|
343
|
+
const values = [];
|
|
344
|
+
if (update.confidence !== undefined) {
|
|
345
|
+
setClauses.push('confidence = ?');
|
|
346
|
+
values.push(update.confidence);
|
|
347
|
+
}
|
|
348
|
+
if (update.metadata !== undefined) {
|
|
349
|
+
setClauses.push('metadata = ?');
|
|
350
|
+
values.push(JSON.stringify(update.metadata));
|
|
351
|
+
}
|
|
352
|
+
if (setClauses.length === 0) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
values.push(relationId);
|
|
356
|
+
this.db.prepare(`
|
|
357
|
+
UPDATE relationships SET ${setClauses.join(', ')} WHERE id = ?
|
|
358
|
+
`).run(...values);
|
|
359
|
+
updated++;
|
|
360
|
+
}
|
|
361
|
+
return { updated, notFound };
|
|
362
|
+
}
|
|
276
363
|
async readGraph() {
|
|
277
364
|
if (!this.db)
|
|
278
365
|
throw new Error('Database not initialized');
|
|
@@ -298,7 +385,7 @@ class RAGKnowledgeGraphManager {
|
|
|
298
385
|
}));
|
|
299
386
|
return { entities, relations };
|
|
300
387
|
}
|
|
301
|
-
async searchNodes(query, limit = 10) {
|
|
388
|
+
async searchNodes(query, limit = 10, since, until) {
|
|
302
389
|
if (!this.db)
|
|
303
390
|
throw new Error('Database not initialized');
|
|
304
391
|
console.error(`🔍 Semantic entity search: "${query}"`);
|
|
@@ -321,11 +408,25 @@ class RAGKnowledgeGraphManager {
|
|
|
321
408
|
AND k = ?
|
|
322
409
|
ORDER BY ee.distance
|
|
323
410
|
`).all(Buffer.from(queryEmbedding.buffer), limit);
|
|
324
|
-
|
|
411
|
+
// Filter by temporal range if specified
|
|
412
|
+
let filteredResults = entityResults;
|
|
413
|
+
if (since || until) {
|
|
414
|
+
filteredResults = entityResults.filter(r => {
|
|
415
|
+
const entity = this.db.prepare('SELECT created_at FROM entities WHERE id = ?').get(r.entity_id);
|
|
416
|
+
if (!entity)
|
|
417
|
+
return false;
|
|
418
|
+
if (since && entity.created_at < since)
|
|
419
|
+
return false;
|
|
420
|
+
if (until && entity.created_at > until)
|
|
421
|
+
return false;
|
|
422
|
+
return true;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (filteredResults.length === 0) {
|
|
325
426
|
console.error(`ℹ️ No semantic matches found for "${query}"`);
|
|
326
427
|
return { entities: [], relations: [] };
|
|
327
428
|
}
|
|
328
|
-
const entities =
|
|
429
|
+
const entities = filteredResults.map(result => ({
|
|
329
430
|
name: result.name,
|
|
330
431
|
entityType: result.entityType,
|
|
331
432
|
observations: JSON.parse(result.observations),
|
|
@@ -559,11 +660,11 @@ class RAGKnowledgeGraphManager {
|
|
|
559
660
|
SELECT id FROM entities
|
|
560
661
|
`).all();
|
|
561
662
|
let embeddedCount = 0;
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
663
|
+
const batchSize = 32;
|
|
664
|
+
for (let i = 0; i < entities.length; i += batchSize) {
|
|
665
|
+
const batch = entities.slice(i, i + batchSize);
|
|
666
|
+
const results = await Promise.all(batch.map(e => this.embedEntity(e.id)));
|
|
667
|
+
embeddedCount += results.filter(Boolean).length;
|
|
567
668
|
}
|
|
568
669
|
console.error(`✅ Entity embeddings completed: ${embeddedCount}/${entities.length} entities embedded`);
|
|
569
670
|
return {
|
|
@@ -646,7 +747,7 @@ class RAGKnowledgeGraphManager {
|
|
|
646
747
|
for (const chunk of chunks) {
|
|
647
748
|
// Generate embedding
|
|
648
749
|
const embedding = await this.generateEmbedding(chunk.text);
|
|
649
|
-
const rowid =
|
|
750
|
+
const rowid = safeRowid(chunk.rowid);
|
|
650
751
|
try {
|
|
651
752
|
// Delete existing embedding if any
|
|
652
753
|
this.db.exec(`DELETE FROM chunks WHERE rowid = ${rowid}`);
|
|
@@ -690,7 +791,7 @@ class RAGKnowledgeGraphManager {
|
|
|
690
791
|
// Delete vectors and associations
|
|
691
792
|
for (const chunk of existingChunks) {
|
|
692
793
|
// Delete vector embeddings (vec0 needs literal integer, not parameterized)
|
|
693
|
-
this.db.exec(`DELETE FROM chunks WHERE rowid = ${
|
|
794
|
+
this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(chunk.rowid)}`);
|
|
694
795
|
deletedVectors++;
|
|
695
796
|
// Delete chunk-entity associations
|
|
696
797
|
const associations = this.db.prepare(`
|
|
@@ -1067,7 +1168,7 @@ class RAGKnowledgeGraphManager {
|
|
|
1067
1168
|
// Store in vector table
|
|
1068
1169
|
try {
|
|
1069
1170
|
// First, delete any existing embedding for this rowid
|
|
1070
|
-
this.db.exec(`DELETE FROM chunks WHERE rowid = ${rowid}`);
|
|
1171
|
+
this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(rowid)}`);
|
|
1071
1172
|
// Insert new embedding with explicit rowid to match chunk_metadata
|
|
1072
1173
|
// Use parameterized only for embedding blob, rowid as literal integer
|
|
1073
1174
|
this.db.prepare(`
|
|
@@ -1243,7 +1344,7 @@ class RAGKnowledgeGraphManager {
|
|
|
1243
1344
|
`).run(chunk.rowid);
|
|
1244
1345
|
deletedAssociations += associations.changes;
|
|
1245
1346
|
// Delete vector embeddings (vec0 needs literal integer, not parameterized)
|
|
1246
|
-
this.db.exec(`DELETE FROM chunks WHERE rowid = ${
|
|
1347
|
+
this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(chunk.rowid)}`);
|
|
1247
1348
|
deletedVectors++;
|
|
1248
1349
|
}
|
|
1249
1350
|
// Delete chunk metadata
|
|
@@ -1373,6 +1474,117 @@ class RAGKnowledgeGraphManager {
|
|
|
1373
1474
|
console.error(`✅ Found ${documents.length} documents`);
|
|
1374
1475
|
return { documents };
|
|
1375
1476
|
}
|
|
1477
|
+
async exportGraph() {
|
|
1478
|
+
if (!this.db)
|
|
1479
|
+
throw new Error('Database not initialized');
|
|
1480
|
+
console.error('📦 Exporting knowledge graph...');
|
|
1481
|
+
const entities = this.db.prepare(`
|
|
1482
|
+
SELECT id, name, entityType, observations, metadata, created_at FROM entities
|
|
1483
|
+
`).all().map((row) => ({
|
|
1484
|
+
id: row.id,
|
|
1485
|
+
name: row.name,
|
|
1486
|
+
entityType: row.entityType,
|
|
1487
|
+
observations: JSON.parse(row.observations),
|
|
1488
|
+
metadata: JSON.parse(row.metadata || '{}'),
|
|
1489
|
+
created_at: row.created_at
|
|
1490
|
+
}));
|
|
1491
|
+
const relations = this.db.prepare(`
|
|
1492
|
+
SELECT id, source_entity, target_entity, relationType, confidence, metadata, created_at FROM relationships
|
|
1493
|
+
`).all().map((row) => ({
|
|
1494
|
+
id: row.id,
|
|
1495
|
+
source_entity: row.source_entity,
|
|
1496
|
+
target_entity: row.target_entity,
|
|
1497
|
+
relationType: row.relationType,
|
|
1498
|
+
confidence: row.confidence,
|
|
1499
|
+
metadata: JSON.parse(row.metadata || '{}'),
|
|
1500
|
+
created_at: row.created_at
|
|
1501
|
+
}));
|
|
1502
|
+
const documents = this.db.prepare(`
|
|
1503
|
+
SELECT id, content, metadata, created_at FROM documents
|
|
1504
|
+
`).all().map((row) => ({
|
|
1505
|
+
id: row.id,
|
|
1506
|
+
content: row.content,
|
|
1507
|
+
metadata: JSON.parse(row.metadata || '{}'),
|
|
1508
|
+
created_at: row.created_at
|
|
1509
|
+
}));
|
|
1510
|
+
console.error(`✅ Export completed: ${entities.length} entities, ${relations.length} relations, ${documents.length} documents`);
|
|
1511
|
+
return {
|
|
1512
|
+
entities,
|
|
1513
|
+
relations,
|
|
1514
|
+
documents,
|
|
1515
|
+
metadata: {
|
|
1516
|
+
exportedAt: new Date().toISOString(),
|
|
1517
|
+
version: '1.0.0',
|
|
1518
|
+
entityCount: entities.length,
|
|
1519
|
+
relationCount: relations.length,
|
|
1520
|
+
documentCount: documents.length
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
async importGraph(data, options = { merge: true }) {
|
|
1525
|
+
if (!this.db)
|
|
1526
|
+
throw new Error('Database not initialized');
|
|
1527
|
+
console.error(`📥 Importing knowledge graph (merge: ${options.merge !== false})...`);
|
|
1528
|
+
const imported = { entities: 0, relations: 0, documents: 0 };
|
|
1529
|
+
const skipped = { entities: 0, relations: 0, documents: 0 };
|
|
1530
|
+
// If merge=false, clear existing data first
|
|
1531
|
+
if (options.merge === false) {
|
|
1532
|
+
this.db.exec(`DELETE FROM relationships`);
|
|
1533
|
+
this.db.exec(`DELETE FROM entities`);
|
|
1534
|
+
this.db.exec(`DELETE FROM documents`);
|
|
1535
|
+
console.error('🗑️ Cleared existing data for full import');
|
|
1536
|
+
}
|
|
1537
|
+
// Import entities using INSERT OR IGNORE
|
|
1538
|
+
if (data.entities && Array.isArray(data.entities)) {
|
|
1539
|
+
const stmt = this.db.prepare(`
|
|
1540
|
+
INSERT OR IGNORE INTO entities (id, name, entityType, observations, metadata, created_at)
|
|
1541
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1542
|
+
`);
|
|
1543
|
+
for (const entity of data.entities) {
|
|
1544
|
+
const result = stmt.run(entity.id, entity.name, entity.entityType || 'CONCEPT', JSON.stringify(entity.observations || []), JSON.stringify(entity.metadata || {}), entity.created_at || new Date().toISOString());
|
|
1545
|
+
if (result.changes > 0) {
|
|
1546
|
+
imported.entities++;
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
skipped.entities++;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
// Import relations using INSERT OR IGNORE
|
|
1554
|
+
if (data.relations && Array.isArray(data.relations)) {
|
|
1555
|
+
const stmt = this.db.prepare(`
|
|
1556
|
+
INSERT OR IGNORE INTO relationships (id, source_entity, target_entity, relationType, confidence, metadata, created_at)
|
|
1557
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1558
|
+
`);
|
|
1559
|
+
for (const relation of data.relations) {
|
|
1560
|
+
const result = stmt.run(relation.id, relation.source_entity, relation.target_entity, relation.relationType, relation.confidence ?? 1.0, JSON.stringify(relation.metadata || {}), relation.created_at || new Date().toISOString());
|
|
1561
|
+
if (result.changes > 0) {
|
|
1562
|
+
imported.relations++;
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
skipped.relations++;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
// Import documents using INSERT OR REPLACE
|
|
1570
|
+
if (data.documents && Array.isArray(data.documents)) {
|
|
1571
|
+
const stmt = this.db.prepare(`
|
|
1572
|
+
INSERT OR REPLACE INTO documents (id, content, metadata, created_at)
|
|
1573
|
+
VALUES (?, ?, ?, ?)
|
|
1574
|
+
`);
|
|
1575
|
+
for (const doc of data.documents) {
|
|
1576
|
+
const result = stmt.run(doc.id, doc.content, JSON.stringify(doc.metadata || {}), doc.created_at || new Date().toISOString());
|
|
1577
|
+
if (result.changes > 0) {
|
|
1578
|
+
imported.documents++;
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
skipped.documents++;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
console.error(`✅ Import completed: ${imported.entities} entities, ${imported.relations} relations, ${imported.documents} documents imported`);
|
|
1586
|
+
return { imported, skipped };
|
|
1587
|
+
}
|
|
1376
1588
|
async hybridSearch(query, limit = 5, useGraph = true) {
|
|
1377
1589
|
if (!this.db)
|
|
1378
1590
|
throw new Error('Database not initialized');
|
|
@@ -1431,8 +1643,89 @@ class RAGKnowledgeGraphManager {
|
|
|
1431
1643
|
}
|
|
1432
1644
|
}
|
|
1433
1645
|
const vectorResults = Array.from(resultMap.values()).sort((a, b) => a.distance - b.distance);
|
|
1646
|
+
// FTS5 full-text search as additional signal (Reciprocal Rank Fusion)
|
|
1647
|
+
const ftsBoostMap = new Map();
|
|
1648
|
+
try {
|
|
1649
|
+
const ftsSearchQuery = (q) => {
|
|
1650
|
+
// Escape FTS5 special characters and build a query with OR between terms
|
|
1651
|
+
const sanitized = q.replace(/["\*\(\)\-]/g, ' ').trim();
|
|
1652
|
+
if (!sanitized)
|
|
1653
|
+
return [];
|
|
1654
|
+
const terms = sanitized.split(/\s+/).filter(t => t.length > 0);
|
|
1655
|
+
if (terms.length === 0)
|
|
1656
|
+
return [];
|
|
1657
|
+
const ftsExpr = terms.map(t => `"${t}"`).join(' OR ');
|
|
1658
|
+
return this.db.prepare(`
|
|
1659
|
+
SELECT cm.rowid, cm.chunk_id, bm25(chunks_fts) as fts_score
|
|
1660
|
+
FROM chunks_fts
|
|
1661
|
+
JOIN chunk_metadata cm ON chunks_fts.rowid = cm.rowid
|
|
1662
|
+
WHERE chunks_fts MATCH ?
|
|
1663
|
+
ORDER BY bm25(chunks_fts)
|
|
1664
|
+
LIMIT ?
|
|
1665
|
+
`).all(ftsExpr, limit * 3);
|
|
1666
|
+
};
|
|
1667
|
+
const ftsOriginal = ftsSearchQuery(query);
|
|
1668
|
+
const ftsTranslated = translatedQuery ? ftsSearchQuery(translatedQuery) : [];
|
|
1669
|
+
// Merge FTS5 results, keeping best score per chunk_id
|
|
1670
|
+
const ftsResultMap = new Map();
|
|
1671
|
+
let rank = 1;
|
|
1672
|
+
for (const r of ftsOriginal) {
|
|
1673
|
+
ftsResultMap.set(r.chunk_id, { chunk_id: r.chunk_id, fts_score: r.fts_score, rank });
|
|
1674
|
+
rank++;
|
|
1675
|
+
}
|
|
1676
|
+
for (const r of ftsTranslated) {
|
|
1677
|
+
if (!ftsResultMap.has(r.chunk_id)) {
|
|
1678
|
+
ftsResultMap.set(r.chunk_id, { chunk_id: r.chunk_id, fts_score: r.fts_score, rank });
|
|
1679
|
+
rank++;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
// Build vector rank map for RRF
|
|
1683
|
+
const vectorRankMap = new Map();
|
|
1684
|
+
vectorResults.forEach((r, idx) => vectorRankMap.set(r.chunk_id, idx + 1));
|
|
1685
|
+
// Calculate RRF-based FTS5 boost (k=60)
|
|
1686
|
+
const k = 60;
|
|
1687
|
+
for (const [chunkId, ftsResult] of ftsResultMap) {
|
|
1688
|
+
const ftsComponent = 1 / (k + ftsResult.rank);
|
|
1689
|
+
ftsBoostMap.set(chunkId, ftsComponent);
|
|
1690
|
+
}
|
|
1691
|
+
// Add FTS5-only results to the vector result pool
|
|
1692
|
+
for (const [chunkId] of ftsResultMap) {
|
|
1693
|
+
if (!resultMap.has(chunkId)) {
|
|
1694
|
+
const chunkRow = this.db.prepare(`
|
|
1695
|
+
SELECT
|
|
1696
|
+
cm.rowid,
|
|
1697
|
+
cm.chunk_id,
|
|
1698
|
+
cm.chunk_type,
|
|
1699
|
+
cm.document_id,
|
|
1700
|
+
cm.entity_id,
|
|
1701
|
+
cm.relationship_id,
|
|
1702
|
+
cm.chunk_index,
|
|
1703
|
+
cm.text,
|
|
1704
|
+
cm.start_pos,
|
|
1705
|
+
cm.end_pos,
|
|
1706
|
+
COALESCE(cm.metadata, '{}') as chunk_metadata,
|
|
1707
|
+
COALESCE(d.metadata, '{}') as doc_metadata
|
|
1708
|
+
FROM chunk_metadata cm
|
|
1709
|
+
LEFT JOIN documents d ON cm.document_id = d.id
|
|
1710
|
+
WHERE cm.chunk_id = ?
|
|
1711
|
+
`).get(chunkId);
|
|
1712
|
+
if (chunkRow) {
|
|
1713
|
+
vectorResults.push({
|
|
1714
|
+
...chunkRow,
|
|
1715
|
+
distance: 2.0
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
const ftsCount = ftsResultMap.size;
|
|
1721
|
+
const ftsOnlyCount = [...ftsResultMap.keys()].filter(id => !vectorRankMap.has(id)).length;
|
|
1722
|
+
console.error(`📝 FTS5 search: ${ftsCount} matches (${ftsOnlyCount} FTS5-only), ${ftsBoostMap.size} boosted`);
|
|
1723
|
+
}
|
|
1724
|
+
catch (ftsError) {
|
|
1725
|
+
console.error(`⚠️ FTS5 search unavailable (graceful degradation):`, ftsError instanceof Error ? ftsError.message : ftsError);
|
|
1726
|
+
}
|
|
1434
1727
|
if (vectorResults.length === 0) {
|
|
1435
|
-
console.error(`ℹ️ No vector matches found for "${query}"`);
|
|
1728
|
+
console.error(`ℹ️ No vector or FTS5 matches found for "${query}"`);
|
|
1436
1729
|
return [];
|
|
1437
1730
|
}
|
|
1438
1731
|
// Get entity information for graph enhancement via vector similarity
|
|
@@ -1585,7 +1878,8 @@ class RAGKnowledgeGraphManager {
|
|
|
1585
1878
|
const { summary, keyHighlight, relevanceScore } = await this.generateContentSummary(result.text, queryEmbedding, chunkEntities, result.chunk_type === 'relationship' ? 1 : 2 // Shorter summary for relationships
|
|
1586
1879
|
);
|
|
1587
1880
|
const vectorSimilarity = Math.max(0, 1 - result.distance / 2);
|
|
1588
|
-
const
|
|
1881
|
+
const ftsBoost = ftsBoostMap.get(result.chunk_id) || 0;
|
|
1882
|
+
const finalScore = Math.max(vectorSimilarity, relevanceScore) + graphBoost + ftsBoost;
|
|
1589
1883
|
// Determine document title and source ID
|
|
1590
1884
|
let documentTitle;
|
|
1591
1885
|
let sourceId;
|
|
@@ -1615,6 +1909,7 @@ class RAGKnowledgeGraphManager {
|
|
|
1615
1909
|
entities: chunkEntities,
|
|
1616
1910
|
vector_similarity: vectorSimilarity,
|
|
1617
1911
|
graph_boost: useGraph ? graphBoost : undefined,
|
|
1912
|
+
fts_boost: ftsBoost > 0 ? ftsBoost : undefined,
|
|
1618
1913
|
full_context_available: true,
|
|
1619
1914
|
chunk_type: result.chunk_type,
|
|
1620
1915
|
source_id: sourceId
|
|
@@ -1824,10 +2119,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1824
2119
|
case "deleteRelations":
|
|
1825
2120
|
await ragKgManager.deleteRelations(validatedArgs.relations);
|
|
1826
2121
|
return { content: [{ type: "text", text: "Relations deleted successfully" }] };
|
|
2122
|
+
case "updateRelations":
|
|
2123
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.updateRelations(validatedArgs.updates), null, 2) }] };
|
|
1827
2124
|
case "readGraph":
|
|
1828
2125
|
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.readGraph(), null, 2) }] };
|
|
1829
2126
|
case "searchNodes":
|
|
1830
|
-
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.searchNodes(validatedArgs.query, validatedArgs.limit || 10), null, 2) }] };
|
|
2127
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.searchNodes(validatedArgs.query, validatedArgs.limit || 10, validatedArgs.since, validatedArgs.until), null, 2) }] };
|
|
1831
2128
|
case "openNodes":
|
|
1832
2129
|
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.openNodes(validatedArgs.names), null, 2) }] };
|
|
1833
2130
|
// New RAG tools
|
|
@@ -1856,6 +2153,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1856
2153
|
// NEW: Entity embedding tools
|
|
1857
2154
|
case "embedAllEntities":
|
|
1858
2155
|
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.embedAllEntities(), null, 2) }] };
|
|
2156
|
+
// NEW: Export/Import tools
|
|
2157
|
+
case "exportGraph":
|
|
2158
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.exportGraph(), null, 2) }] };
|
|
2159
|
+
case "importGraph":
|
|
2160
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.importGraph(validatedArgs.data, { merge: validatedArgs.merge !== false }), null, 2) }] };
|
|
1859
2161
|
// NEW: Migration tools
|
|
1860
2162
|
case "getMigrationStatus":
|
|
1861
2163
|
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getMigrationStatus(), null, 2) }] };
|