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 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 newEntities = [];
113
- const stmt = this.db.prepare(`
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 observations = JSON.stringify(entity.observations || []);
120
- const metadata = JSON.stringify({});
121
- const result = stmt.run(entityId, entity.name, entity.entityType, observations, metadata);
122
- if (result.changes > 0) {
123
- newEntities.push(entity);
124
- // Generate embedding for the new entity
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 newEntities;
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 newObservations = obs.contents.filter(content => !currentObservations.includes(content));
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
- if (entityResults.length === 0) {
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 = entityResults.map(result => ({
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
- for (const entity of entities) {
563
- const success = await this.embedEntity(entity.id);
564
- if (success) {
565
- embeddedCount++;
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 = Number(chunk.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 = ${Number(chunk.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 = ${Number(chunk.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 finalScore = Math.max(vectorSimilarity, relevanceScore) + graphBoost;
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) }] };