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/README.md +125 -161
- package/dist/index.js +197 -145
- package/dist/index.js.map +1 -1
- package/dist/rag-memory.db +0 -0
- package/dist/src/migrations/migrations.d.ts.map +1 -1
- package/dist/src/migrations/migrations.js +35 -0
- package/dist/src/migrations/migrations.js.map +1 -1
- package/dist/src/tools/graph-query-tools.d.ts +2 -0
- package/dist/src/tools/graph-query-tools.d.ts.map +1 -1
- package/dist/src/tools/graph-query-tools.js +88 -0
- package/dist/src/tools/graph-query-tools.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +1 -0
- package/dist/src/tools/tool-registry.d.ts.map +1 -1
- package/package.json +1 -2
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(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
80
|
+
console.error(`✅ Ollama connected (${models.length} models available)`);
|
|
78
81
|
}
|
|
79
82
|
catch (error) {
|
|
80
|
-
console.error('❌ Failed to
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
393
|
-
const queryEmbedding = await this.generateEmbedding(query
|
|
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
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
:
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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(`⚠️
|
|
937
|
-
// Fall through to
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
1595
|
-
const
|
|
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
|
-
//
|
|
1632
|
-
const
|
|
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
|
|
1668
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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) }] };
|