rag-memory-epf-mcp 1.6.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/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
@@ -300,11 +316,50 @@ class RAGKnowledgeGraphManager {
300
316
  const sourceId = `entity_${relation.from.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
301
317
  const targetId = `entity_${relation.to.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
302
318
  this.db.prepare(`
303
- DELETE FROM relationships
319
+ DELETE FROM relationships
304
320
  WHERE source_entity = ? AND target_entity = ? AND relationType = ?
305
321
  `).run(sourceId, targetId, relation.relationType);
306
322
  }
307
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
+ }
308
363
  async readGraph() {
309
364
  if (!this.db)
310
365
  throw new Error('Database not initialized');
@@ -330,7 +385,7 @@ class RAGKnowledgeGraphManager {
330
385
  }));
331
386
  return { entities, relations };
332
387
  }
333
- async searchNodes(query, limit = 10) {
388
+ async searchNodes(query, limit = 10, since, until) {
334
389
  if (!this.db)
335
390
  throw new Error('Database not initialized');
336
391
  console.error(`🔍 Semantic entity search: "${query}"`);
@@ -353,11 +408,25 @@ class RAGKnowledgeGraphManager {
353
408
  AND k = ?
354
409
  ORDER BY ee.distance
355
410
  `).all(Buffer.from(queryEmbedding.buffer), limit);
356
- 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) {
357
426
  console.error(`â„šī¸ No semantic matches found for "${query}"`);
358
427
  return { entities: [], relations: [] };
359
428
  }
360
- const entities = entityResults.map(result => ({
429
+ const entities = filteredResults.map(result => ({
361
430
  name: result.name,
362
431
  entityType: result.entityType,
363
432
  observations: JSON.parse(result.observations),
@@ -591,11 +660,11 @@ class RAGKnowledgeGraphManager {
591
660
  SELECT id FROM entities
592
661
  `).all();
593
662
  let embeddedCount = 0;
594
- for (const entity of entities) {
595
- const success = await this.embedEntity(entity.id);
596
- if (success) {
597
- embeddedCount++;
598
- }
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;
599
668
  }
600
669
  console.error(`✅ Entity embeddings completed: ${embeddedCount}/${entities.length} entities embedded`);
601
670
  return {
@@ -678,7 +747,7 @@ class RAGKnowledgeGraphManager {
678
747
  for (const chunk of chunks) {
679
748
  // Generate embedding
680
749
  const embedding = await this.generateEmbedding(chunk.text);
681
- const rowid = Number(chunk.rowid);
750
+ const rowid = safeRowid(chunk.rowid);
682
751
  try {
683
752
  // Delete existing embedding if any
684
753
  this.db.exec(`DELETE FROM chunks WHERE rowid = ${rowid}`);
@@ -722,7 +791,7 @@ class RAGKnowledgeGraphManager {
722
791
  // Delete vectors and associations
723
792
  for (const chunk of existingChunks) {
724
793
  // Delete vector embeddings (vec0 needs literal integer, not parameterized)
725
- this.db.exec(`DELETE FROM chunks WHERE rowid = ${Number(chunk.rowid)}`);
794
+ this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(chunk.rowid)}`);
726
795
  deletedVectors++;
727
796
  // Delete chunk-entity associations
728
797
  const associations = this.db.prepare(`
@@ -1099,7 +1168,7 @@ class RAGKnowledgeGraphManager {
1099
1168
  // Store in vector table
1100
1169
  try {
1101
1170
  // First, delete any existing embedding for this rowid
1102
- this.db.exec(`DELETE FROM chunks WHERE rowid = ${rowid}`);
1171
+ this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(rowid)}`);
1103
1172
  // Insert new embedding with explicit rowid to match chunk_metadata
1104
1173
  // Use parameterized only for embedding blob, rowid as literal integer
1105
1174
  this.db.prepare(`
@@ -1275,7 +1344,7 @@ class RAGKnowledgeGraphManager {
1275
1344
  `).run(chunk.rowid);
1276
1345
  deletedAssociations += associations.changes;
1277
1346
  // Delete vector embeddings (vec0 needs literal integer, not parameterized)
1278
- this.db.exec(`DELETE FROM chunks WHERE rowid = ${Number(chunk.rowid)}`);
1347
+ this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(chunk.rowid)}`);
1279
1348
  deletedVectors++;
1280
1349
  }
1281
1350
  // Delete chunk metadata
@@ -1405,6 +1474,117 @@ class RAGKnowledgeGraphManager {
1405
1474
  console.error(`✅ Found ${documents.length} documents`);
1406
1475
  return { documents };
1407
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
+ }
1408
1588
  async hybridSearch(query, limit = 5, useGraph = true) {
1409
1589
  if (!this.db)
1410
1590
  throw new Error('Database not initialized');
@@ -1463,8 +1643,89 @@ class RAGKnowledgeGraphManager {
1463
1643
  }
1464
1644
  }
1465
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
+ }
1466
1727
  if (vectorResults.length === 0) {
1467
- console.error(`â„šī¸ No vector matches found for "${query}"`);
1728
+ console.error(`â„šī¸ No vector or FTS5 matches found for "${query}"`);
1468
1729
  return [];
1469
1730
  }
1470
1731
  // Get entity information for graph enhancement via vector similarity
@@ -1617,7 +1878,8 @@ class RAGKnowledgeGraphManager {
1617
1878
  const { summary, keyHighlight, relevanceScore } = await this.generateContentSummary(result.text, queryEmbedding, chunkEntities, result.chunk_type === 'relationship' ? 1 : 2 // Shorter summary for relationships
1618
1879
  );
1619
1880
  const vectorSimilarity = Math.max(0, 1 - result.distance / 2);
1620
- 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;
1621
1883
  // Determine document title and source ID
1622
1884
  let documentTitle;
1623
1885
  let sourceId;
@@ -1647,6 +1909,7 @@ class RAGKnowledgeGraphManager {
1647
1909
  entities: chunkEntities,
1648
1910
  vector_similarity: vectorSimilarity,
1649
1911
  graph_boost: useGraph ? graphBoost : undefined,
1912
+ fts_boost: ftsBoost > 0 ? ftsBoost : undefined,
1650
1913
  full_context_available: true,
1651
1914
  chunk_type: result.chunk_type,
1652
1915
  source_id: sourceId
@@ -1856,10 +2119,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1856
2119
  case "deleteRelations":
1857
2120
  await ragKgManager.deleteRelations(validatedArgs.relations);
1858
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) }] };
1859
2124
  case "readGraph":
1860
2125
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.readGraph(), null, 2) }] };
1861
2126
  case "searchNodes":
1862
- 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) }] };
1863
2128
  case "openNodes":
1864
2129
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.openNodes(validatedArgs.names), null, 2) }] };
1865
2130
  // New RAG tools
@@ -1888,6 +2153,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1888
2153
  // NEW: Entity embedding tools
1889
2154
  case "embedAllEntities":
1890
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) }] };
1891
2161
  // NEW: Migration tools
1892
2162
  case "getMigrationStatus":
1893
2163
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getMigrationStatus(), null, 2) }] };