rag-memory-epf-mcp 1.8.0 → 2.0.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
@@ -7,16 +7,11 @@ import * as sqliteVec from 'sqlite-vec';
7
7
  import { get_encoding } from 'tiktoken';
8
8
  import path from 'path';
9
9
  import { fileURLToPath } from 'url';
10
- import { pipeline, env } from '@huggingface/transformers';
11
10
  // Import our new structured tool system
12
11
  import { getAllMCPTools, validateToolArgs, getSystemInfo } from './src/tools/tool-registry.js';
13
12
  // Import migration system
14
13
  import { MigrationManager } from './src/migrations/migration-manager.js';
15
14
  import { migrations } from './src/migrations/migrations.js';
16
- // Configure Hugging Face transformers for better compatibility
17
- if (env.backends?.onnx?.wasm) {
18
- env.backends.onnx.wasm.wasmPaths = './node_modules/@huggingface/transformers/dist/';
19
- }
20
15
  // Define database file path using environment variable with fallback
21
16
  const defaultDbPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'rag-memory.db');
22
17
  const DB_FILE_PATH = process.env.DB_FILE_PATH
@@ -24,6 +19,9 @@ const DB_FILE_PATH = process.env.DB_FILE_PATH
24
19
  ? process.env.DB_FILE_PATH
25
20
  : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.DB_FILE_PATH)
26
21
  : defaultDbPath;
22
+ const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL || 'qwen3-embedding:8b';
23
+ const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
24
+ const EMBEDDING_DIM = 4096;
27
25
  // Safe rowid for vec0 virtual tables (require literal integer, not parameterized)
28
26
  function safeRowid(value) {
29
27
  const n = Number(value);
@@ -36,8 +34,9 @@ function safeRowid(value) {
36
34
  class RAGKnowledgeGraphManager {
37
35
  db = null;
38
36
  encoding = null;
39
- embeddingModel = null;
40
37
  modelInitialized = false;
38
+ embeddingCache = new Map();
39
+ EMBEDDING_CACHE_MAX = 500;
41
40
  async initialize() {
42
41
  console.error('🚀 Initializing RAG Knowledge Graph MCP Server...');
43
42
  // Initialize database
@@ -65,20 +64,24 @@ class RAGKnowledgeGraphManager {
65
64
  }
66
65
  async initializeEmbeddingModel() {
67
66
  try {
68
- console.error('🤖 Loading embedding model: Qwen3-Embedding-0.6B (1024-dim, 100+ languages)...');
69
- // Configure environment to allow remote model downloads
70
- env.allowRemoteModels = true;
71
- env.allowLocalModels = true;
72
- this.embeddingModel = await pipeline('feature-extraction', 'onnx-community/Qwen3-Embedding-0.6B-ONNX', {
73
- revision: 'main',
74
- dtype: 'fp16',
75
- });
67
+ console.error(`🤖 Connecting to Ollama: ${OLLAMA_URL} (model: ${EMBEDDING_MODEL}, ${EMBEDDING_DIM}-dim)...`);
68
+ const response = await fetch(`${OLLAMA_URL}/api/tags`);
69
+ if (!response.ok) {
70
+ throw new Error(`Ollama not responding: ${response.status}`);
71
+ }
72
+ const data = await response.json();
73
+ const models = data.models || [];
74
+ const modelFound = models.some((m) => m.name.startsWith(EMBEDDING_MODEL.split(':')[0]));
75
+ if (!modelFound) {
76
+ console.error(`⚠️ Model ${EMBEDDING_MODEL} not found in Ollama. Run: ollama pull ${EMBEDDING_MODEL}`);
77
+ console.error(`📋 Available models: ${models.map((m) => m.name).join(', ') || 'none'}`);
78
+ }
76
79
  this.modelInitialized = true;
77
- console.error('✅ Qwen3-Embedding-0.6B model loaded successfully');
80
+ console.error(`✅ Ollama connected (${models.length} models available)`);
78
81
  }
79
82
  catch (error) {
80
- console.error('❌ Failed to load embedding model:', error);
81
- console.error('📋 Falling back to simple embedding generation');
83
+ console.error('❌ Failed to connect to Ollama:', error instanceof Error ? error.message : error);
84
+ console.error('📋 Falling back to simple embedding generation. Start Ollama: ollama serve');
82
85
  this.modelInitialized = false;
83
86
  }
84
87
  }
@@ -111,11 +114,8 @@ class RAGKnowledgeGraphManager {
111
114
  this.encoding.free();
112
115
  this.encoding = null;
113
116
  }
114
- if (this.embeddingModel) {
115
- // Clean up the embedding model if it has cleanup methods
116
- this.embeddingModel = null;
117
- this.modelInitialized = false;
118
- }
117
+ this.modelInitialized = false;
118
+ this.embeddingCache.clear();
119
119
  if (this.db) {
120
120
  this.db.close();
121
121
  this.db = null;
@@ -385,12 +385,121 @@ class RAGKnowledgeGraphManager {
385
385
  }));
386
386
  return { entities, relations };
387
387
  }
388
+ async getNeighbors(entityNames, depth = 1, relationType) {
389
+ if (!this.db)
390
+ throw new Error('Database not initialized');
391
+ // Cap depth at 5 to prevent runaway queries
392
+ const effectiveDepth = Math.min(Math.max(depth, 1), 5);
393
+ // Convert entity names to IDs
394
+ const seedIds = entityNames.map(name => `entity_${name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`);
395
+ // Build dynamic placeholders for the seed IDs
396
+ const seedPlaceholders = seedIds.map(() => '?').join(',');
397
+ // Build the recursive CTE query
398
+ const relationFilter = relationType
399
+ ? `AND r.relationType = ?`
400
+ : '';
401
+ const cteQuery = `
402
+ WITH RECURSIVE traversal(entity_id, depth, path) AS (
403
+ -- Base case: seed entities
404
+ SELECT id, 0, id FROM entities WHERE id IN (${seedPlaceholders})
405
+ UNION ALL
406
+ -- Recursive: follow relationships up to max depth
407
+ SELECT
408
+ CASE WHEN r.source_entity = t.entity_id THEN r.target_entity ELSE r.source_entity END,
409
+ t.depth + 1,
410
+ t.path || ',' || CASE WHEN r.source_entity = t.entity_id THEN r.target_entity ELSE r.source_entity END
411
+ FROM traversal t
412
+ JOIN relationships r ON (r.source_entity = t.entity_id OR r.target_entity = t.entity_id)
413
+ WHERE t.depth < ?
414
+ ${relationFilter}
415
+ -- Cycle detection: don't revisit entities already in path
416
+ AND instr(t.path, CASE WHEN r.source_entity = t.entity_id THEN r.target_entity ELSE r.source_entity END) = 0
417
+ )
418
+ SELECT DISTINCT entity_id, MIN(depth) as min_depth, path
419
+ FROM traversal
420
+ GROUP BY entity_id
421
+ `;
422
+ // Build parameters
423
+ const params = [...seedIds, effectiveDepth];
424
+ if (relationType) {
425
+ params.push(relationType);
426
+ }
427
+ const traversalResults = this.db.prepare(cteQuery).all(...params);
428
+ if (traversalResults.length === 0) {
429
+ return { entities: [], relations: [], paths: [] };
430
+ }
431
+ // Collect all discovered entity IDs
432
+ const discoveredIds = traversalResults.map(r => r.entity_id);
433
+ const idPlaceholders = discoveredIds.map(() => '?').join(',');
434
+ // Fetch entity details
435
+ const entityRows = this.db.prepare(`
436
+ SELECT id, name, entityType, observations FROM entities WHERE id IN (${idPlaceholders})
437
+ `).all(...discoveredIds);
438
+ // Build id-to-depth and id-to-name maps
439
+ const idToDepth = new Map();
440
+ for (const r of traversalResults) {
441
+ idToDepth.set(r.entity_id, r.min_depth);
442
+ }
443
+ const idToName = new Map();
444
+ for (const row of entityRows) {
445
+ idToName.set(row.id, row.name);
446
+ }
447
+ const entities = entityRows.map(row => ({
448
+ name: row.name,
449
+ entityType: row.entityType,
450
+ observations: JSON.parse(row.observations),
451
+ depth: idToDepth.get(row.id) ?? 0,
452
+ }));
453
+ // Fetch relations between all discovered entities
454
+ let relQuery = `
455
+ SELECT
456
+ r.source_entity,
457
+ r.target_entity,
458
+ e1.name as from_name,
459
+ e2.name as to_name,
460
+ r.relationType
461
+ FROM relationships r
462
+ JOIN entities e1 ON r.source_entity = e1.id
463
+ JOIN entities e2 ON r.target_entity = e2.id
464
+ WHERE r.source_entity IN (${idPlaceholders})
465
+ AND r.target_entity IN (${idPlaceholders})
466
+ `;
467
+ const relParams = [...discoveredIds, ...discoveredIds];
468
+ if (relationType) {
469
+ relQuery += ` AND r.relationType = ?`;
470
+ relParams.push(relationType);
471
+ }
472
+ const relationRows = this.db.prepare(relQuery).all(...relParams);
473
+ const relations = relationRows.map(row => ({
474
+ from: row.from_name,
475
+ to: row.to_name,
476
+ relationType: row.relationType,
477
+ depth: Math.max(idToDepth.get(row.source_entity) ?? 0, idToDepth.get(row.target_entity) ?? 0),
478
+ }));
479
+ // Build shortest paths from seed entities to all discovered entities
480
+ const paths = [];
481
+ for (const result of traversalResults) {
482
+ if (result.min_depth === 0)
483
+ continue; // Skip seed entities themselves
484
+ const pathIds = result.path.split(',');
485
+ const pathNames = pathIds.map(id => idToName.get(id) || id).filter(Boolean);
486
+ if (pathNames.length >= 2) {
487
+ paths.push({
488
+ from: pathNames[0],
489
+ to: pathNames[pathNames.length - 1],
490
+ path: pathNames,
491
+ });
492
+ }
493
+ }
494
+ console.error(`✅ getNeighbors: Found ${entities.length} entities, ${relations.length} relations, ${paths.length} paths (depth=${effectiveDepth})`);
495
+ return { entities, relations, paths };
496
+ }
388
497
  async searchNodes(query, limit = 10, since, until) {
389
498
  if (!this.db)
390
499
  throw new Error('Database not initialized');
391
500
  console.error(`🔍 Semantic entity search: "${query}"`);
392
- // Generate query embedding (with instruction prefix for Qwen3)
393
- const queryEmbedding = await this.generateEmbedding(query, 1024, true);
501
+ // Generate query embedding
502
+ const queryEmbedding = await this.generateEmbedding(query);
394
503
  // Perform vector similarity search on entities
395
504
  const entityResults = this.db.prepare(`
396
505
  SELECT
@@ -809,55 +918,6 @@ class RAGKnowledgeGraphManager {
809
918
  console.error(` └─ Deleted ${metadata.changes} chunk metadata records`);
810
919
  }
811
920
  }
812
- // Simple configurable term extraction (replacing hardcoded patterns)
813
- // Cross-lingual query translation using entity DB + domain dictionary
814
- translateQueryCrossLingual(query) {
815
- if (!this.db)
816
- return null;
817
- // Domain-specific Korean→English dictionary
818
- const domainDict = {
819
- '할랄': 'halal', '인증': 'certification', '상호인정': 'mutual recognition',
820
- '인정': 'accreditation', '인증기관': 'certification body',
821
- '표준': 'standard', '감사': 'audit', '심사': 'audit review',
822
- '도축': 'slaughter', '식품': 'food', '화장품': 'cosmetics',
823
- '수출': 'export', '수입': 'import', '무역': 'trade',
824
- '방문': 'visit', '협력': 'cooperation', '협약': 'agreement',
825
- '제안': 'proposal', '회의': 'meeting', '이메일': 'email',
826
- '보고서': 'report', '전략': 'strategy', '사업': 'business',
827
- '정부': 'government', '지원': 'support', '바우처': 'voucher',
828
- '창업': 'startup', '대학': 'university',
829
- };
830
- // Build entity Korean→English mapping from observations
831
- try {
832
- const entities = this.db.prepare(`
833
- SELECT name, observations FROM entities
834
- `).all();
835
- for (const entity of entities) {
836
- try {
837
- const obs = JSON.parse(entity.observations);
838
- for (const o of obs) {
839
- const match = o.match(/한국어명:\s*(.+)/);
840
- if (match) {
841
- const koreanName = match[1].trim();
842
- domainDict[koreanName] = entity.name;
843
- }
844
- }
845
- }
846
- catch { }
847
- }
848
- }
849
- catch { }
850
- // Translate Korean terms in query
851
- let translated = query;
852
- // Sort by length descending to match longer terms first
853
- const sortedTerms = Object.entries(domainDict).sort((a, b) => b[0].length - a[0].length);
854
- for (const [ko, en] of sortedTerms) {
855
- translated = translated.replace(new RegExp(ko, 'g'), en);
856
- }
857
- // Remove remaining Korean characters and clean up
858
- translated = translated.replace(/[\uAC00-\uD7A3]+/g, ' ').replace(/\s+/g, ' ').trim();
859
- return translated.length > 2 ? translated : null;
860
- }
861
921
  extractTermsFromText(text, options = {}) {
862
922
  const { minLength = 3, includeCapitalized = true, customPatterns = [] } = options;
863
923
  const terms = new Set();
@@ -917,33 +977,58 @@ class RAGKnowledgeGraphManager {
917
977
  }
918
978
  return chunks;
919
979
  }
920
- // Generate embeddings using sentence transformers
921
- // isQuery: true for search queries (adds instruction prefix), false for documents/entities
922
- async generateEmbedding(text, dimensions = 1024, isQuery = false) {
923
- if (this.modelInitialized && this.embeddingModel) {
980
+ // Generate embeddings using Ollama API
981
+ async generateEmbedding(text) {
982
+ // Check cache first
983
+ const cacheKey = text.length > 100 ? text.substring(0, 100) : text;
984
+ const cached = this.embeddingCache.get(cacheKey);
985
+ if (cached)
986
+ return cached;
987
+ if (this.modelInitialized) {
924
988
  try {
925
- // Qwen3: add instruction prefix for queries (task-specific instruction improves accuracy)
926
- const inputText = isQuery
927
- ? `Instruct: Given a search query, retrieve relevant entities and documents from a knowledge graph about halal certification, accreditation, and standards\nQuery: ${text}`
928
- : text;
929
- // Use the real sentence transformer model (Qwen3: last_token pooling)
930
- const result = await this.embeddingModel(inputText, { pooling: 'last_token', normalize: true });
931
- // Extract the embedding array and convert to Float32Array
932
- const embedding = result.data;
933
- return new Float32Array(embedding.slice(0, dimensions));
989
+ const response = await fetch(`${OLLAMA_URL}/api/embed`, {
990
+ method: 'POST',
991
+ headers: { 'Content-Type': 'application/json' },
992
+ body: JSON.stringify({
993
+ model: EMBEDDING_MODEL,
994
+ input: text
995
+ })
996
+ });
997
+ if (!response.ok) {
998
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
999
+ }
1000
+ const data = await response.json();
1001
+ const modelResult = new Float32Array(data.embeddings[0]);
1002
+ // Cache the result (LRU: evict oldest if full)
1003
+ if (this.embeddingCache.size >= this.EMBEDDING_CACHE_MAX) {
1004
+ const firstKey = this.embeddingCache.keys().next().value;
1005
+ if (firstKey)
1006
+ this.embeddingCache.delete(firstKey);
1007
+ }
1008
+ this.embeddingCache.set(cacheKey, modelResult);
1009
+ return modelResult;
934
1010
  }
935
1011
  catch (error) {
936
- console.error(`⚠️ Embedding model failed for text "${text.slice(0, 50)}...":`, error instanceof Error ? error.message : error);
937
- // Fall through to enhanced general implementation
1012
+ console.error(`⚠️ Ollama embedding failed for text "${text.slice(0, 50)}...":`, error instanceof Error ? error.message : error);
1013
+ // Fall through to fallback implementation
938
1014
  }
939
1015
  }
940
- // Enhanced general-purpose semantic embedding
1016
+ // Enhanced general-purpose semantic embedding (fallback)
1017
+ const dimensions = EMBEDDING_DIM;
941
1018
  const embedding = new Array(dimensions).fill(0);
942
1019
  // Normalize and tokenize text (preserve Unicode letters including Korean, Arabic, etc.)
943
1020
  const normalizedText = text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, ' ').replace(/\s+/g, ' ').trim();
944
1021
  const words = normalizedText.split(' ').filter(word => word.length > 1);
945
1022
  if (words.length === 0) {
946
- return new Float32Array(embedding);
1023
+ const emptyResult = new Float32Array(embedding);
1024
+ // Cache the result (LRU: evict oldest if full)
1025
+ if (this.embeddingCache.size >= this.EMBEDDING_CACHE_MAX) {
1026
+ const firstKey = this.embeddingCache.keys().next().value;
1027
+ if (firstKey)
1028
+ this.embeddingCache.delete(firstKey);
1029
+ }
1030
+ this.embeddingCache.set(cacheKey, emptyResult);
1031
+ return emptyResult;
947
1032
  }
948
1033
  // Enhanced word importance calculation
949
1034
  const wordFreq = new Map();
@@ -1076,7 +1161,15 @@ class RAGKnowledgeGraphManager {
1076
1161
  // L2 normalization for cosine similarity
1077
1162
  const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
1078
1163
  const normalizedEmbedding = magnitude > 0 ? embedding.map(val => val / magnitude) : embedding;
1079
- return new Float32Array(normalizedEmbedding);
1164
+ const fallbackResult = new Float32Array(normalizedEmbedding);
1165
+ // Cache the result (LRU: evict oldest if full)
1166
+ if (this.embeddingCache.size >= this.EMBEDDING_CACHE_MAX) {
1167
+ const firstKey = this.embeddingCache.keys().next().value;
1168
+ if (firstKey)
1169
+ this.embeddingCache.delete(firstKey);
1170
+ }
1171
+ this.embeddingCache.set(cacheKey, fallbackResult);
1172
+ return fallbackResult;
1080
1173
  }
1081
1174
  // Calculate position-based importance weight
1082
1175
  calculatePositionWeight(position, totalWords) {
@@ -1123,7 +1216,7 @@ class RAGKnowledgeGraphManager {
1123
1216
  if (!document) {
1124
1217
  throw new Error(`Document with ID ${documentId} not found`);
1125
1218
  }
1126
- const { maxTokens = 200, overlap = 20 } = options;
1219
+ const { maxTokens = 800, overlap = 160 } = options;
1127
1220
  console.error(`🔪 Chunking document: ${documentId} (maxTokens: ${maxTokens}, overlap: ${overlap})`);
1128
1221
  // Clean up existing chunks
1129
1222
  await this.cleanupDocument(documentId);
@@ -1591,18 +1684,8 @@ class RAGKnowledgeGraphManager {
1591
1684
  if (!this.encoding)
1592
1685
  throw new Error('Tokenizer not initialized');
1593
1686
  console.error(`🔍 Enhanced hybrid search: "${query}"`);
1594
- // Detect Korean in query and build cross-lingual translation
1595
- const hasKorean = /[\uAC00-\uD7A3]/.test(query);
1596
- let translatedQuery = null;
1597
- if (hasKorean) {
1598
- translatedQuery = this.translateQueryCrossLingual(query);
1599
- if (translatedQuery) {
1600
- console.error(`🌐 Cross-lingual: "${query}" → "${translatedQuery}"`);
1601
- }
1602
- }
1603
- // Generate query embedding(s) (with instruction prefix for Qwen3)
1604
- const queryEmbedding = await this.generateEmbedding(query, 1024, true);
1605
- const translatedEmbedding = translatedQuery ? await this.generateEmbedding(translatedQuery, 1024, true) : null;
1687
+ // Generate query embedding
1688
+ const queryEmbedding = await this.generateEmbedding(query);
1606
1689
  // Vector search helper
1607
1690
  const searchChunks = (embedding, k) => {
1608
1691
  return this.db.prepare(`
@@ -1628,21 +1711,8 @@ class RAGKnowledgeGraphManager {
1628
1711
  ORDER BY c.distance
1629
1712
  `).all(Buffer.from(embedding.buffer), k);
1630
1713
  };
1631
- // Dual search: original + translated (if available)
1632
- const originalResults = searchChunks(queryEmbedding, limit * 3);
1633
- const translatedResults = translatedEmbedding ? searchChunks(translatedEmbedding, limit * 3) : [];
1634
- // Merge and deduplicate by chunk_id, keeping best distance
1635
- const resultMap = new Map();
1636
- for (const r of originalResults) {
1637
- resultMap.set(r.chunk_id, r);
1638
- }
1639
- for (const r of translatedResults) {
1640
- const existing = resultMap.get(r.chunk_id);
1641
- if (!existing || r.distance < existing.distance) {
1642
- resultMap.set(r.chunk_id, r);
1643
- }
1644
- }
1645
- const vectorResults = Array.from(resultMap.values()).sort((a, b) => a.distance - b.distance);
1714
+ // Vector search
1715
+ const vectorResults = searchChunks(queryEmbedding, limit * 3);
1646
1716
  // FTS5 full-text search as additional signal (Reciprocal Rank Fusion)
1647
1717
  const ftsBoostMap = new Map();
1648
1718
  try {
@@ -1664,21 +1734,14 @@ class RAGKnowledgeGraphManager {
1664
1734
  LIMIT ?
1665
1735
  `).all(ftsExpr, limit * 3);
1666
1736
  };
1667
- const ftsOriginal = ftsSearchQuery(query);
1668
- const ftsTranslated = translatedQuery ? ftsSearchQuery(translatedQuery) : [];
1669
- // Merge FTS5 results, keeping best score per chunk_id
1737
+ const ftsResults = ftsSearchQuery(query);
1738
+ // Build FTS5 boost map
1670
1739
  const ftsResultMap = new Map();
1671
1740
  let rank = 1;
1672
- for (const r of ftsOriginal) {
1741
+ for (const r of ftsResults) {
1673
1742
  ftsResultMap.set(r.chunk_id, { chunk_id: r.chunk_id, fts_score: r.fts_score, rank });
1674
1743
  rank++;
1675
1744
  }
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
1745
  // Build vector rank map for RRF
1683
1746
  const vectorRankMap = new Map();
1684
1747
  vectorResults.forEach((r, idx) => vectorRankMap.set(r.chunk_id, idx + 1));
@@ -1690,7 +1753,7 @@ class RAGKnowledgeGraphManager {
1690
1753
  }
1691
1754
  // Add FTS5-only results to the vector result pool
1692
1755
  for (const [chunkId] of ftsResultMap) {
1693
- if (!resultMap.has(chunkId)) {
1756
+ if (!vectorResults.some(r => r.chunk_id === chunkId)) {
1694
1757
  const chunkRow = this.db.prepare(`
1695
1758
  SELECT
1696
1759
  cm.rowid,
@@ -1732,7 +1795,7 @@ class RAGKnowledgeGraphManager {
1732
1795
  let connectedEntities = new Set();
1733
1796
  let queryMatchedEntities = new Set();
1734
1797
  if (useGraph) {
1735
- // Vector search: find entities semantically similar to the query (dual search)
1798
+ // Vector search: find entities semantically similar to the query
1736
1799
  try {
1737
1800
  const searchEntities = (embedding) => {
1738
1801
  return this.db.prepare(`
@@ -1748,20 +1811,7 @@ class RAGKnowledgeGraphManager {
1748
1811
  ORDER BY ee.distance
1749
1812
  `).all(Buffer.from(embedding.buffer));
1750
1813
  };
1751
- // Merge original + translated entity results
1752
- const entityMap = new Map();
1753
- for (const e of searchEntities(queryEmbedding)) {
1754
- entityMap.set(e.entity_id, e);
1755
- }
1756
- if (translatedEmbedding) {
1757
- for (const e of searchEntities(translatedEmbedding)) {
1758
- const existing = entityMap.get(e.entity_id);
1759
- if (!existing || e.distance < existing.distance) {
1760
- entityMap.set(e.entity_id, e);
1761
- }
1762
- }
1763
- }
1764
- const similarEntities = Array.from(entityMap.values()).sort((a, b) => a.distance - b.distance);
1814
+ const similarEntities = searchEntities(queryEmbedding);
1765
1815
  for (const entity of similarEntities) {
1766
1816
  const similarity = Math.max(0, 1 - entity.distance / 2);
1767
1817
  if (similarity > 0.5) {
@@ -2127,6 +2177,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2127
2177
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.searchNodes(validatedArgs.query, validatedArgs.limit || 10, validatedArgs.since, validatedArgs.until), null, 2) }] };
2128
2178
  case "openNodes":
2129
2179
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.openNodes(validatedArgs.names), null, 2) }] };
2180
+ case "getNeighbors":
2181
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getNeighbors(validatedArgs.entityNames, validatedArgs.depth || 1, validatedArgs.relationType), null, 2) }] };
2130
2182
  // New RAG tools
2131
2183
  case "storeDocument":
2132
2184
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.storeDocument(validatedArgs.id, validatedArgs.content, validatedArgs.metadata || {}), null, 2) }] };