@mrxkun/mcfast-mcp 4.0.0 → 4.0.1
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 +101 -1
- package/package.json +3 -1
- package/src/index.js +191 -1
- package/src/memory/index.js +14 -0
- package/src/memory/memory-engine.js +530 -0
- package/src/memory/stores/database.js +104 -0
- package/src/memory/utils/chunker.js +94 -0
- package/src/memory/utils/daily-logs.js +263 -0
- package/src/memory/utils/dashboard-client.js +141 -0
- package/src/memory/utils/embedder.js +217 -0
- package/src/memory/utils/enhanced-embedder.js +717 -0
- package/src/memory/utils/indexer.js +118 -0
- package/src/memory/utils/simple-embedder.js +234 -0
- package/src/memory/utils/smart-router.js +344 -0
- package/src/memory/utils/sync-engine.js +373 -0
- package/src/memory/utils/ultra-embedder.js +1448 -0
- package/src/memory/watchers/file-watcher.js +61 -0
- package/src/tools/memory_get.js +235 -0
- package/src/tools/memory_search.js +296 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Indexer
|
|
3
|
+
* Tổng hợp việc parse file, extract facts, chunk và embed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parse } from '../../strategies/tree-sitter/index.js';
|
|
7
|
+
import { detectLanguage } from '../../strategies/syntax-validator.js';
|
|
8
|
+
import { Chunker } from './chunker.js';
|
|
9
|
+
import { Embedder } from './embedder.js';
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
|
|
12
|
+
export class CodeIndexer {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.chunker = new Chunker(options.chunker);
|
|
15
|
+
this.embedder = new Embedder(options.embedder);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async indexFile(filePath, content) {
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
|
|
21
|
+
const language = detectLanguage(filePath);
|
|
22
|
+
const contentHash = crypto.createHash('md5').update(content).digest('hex');
|
|
23
|
+
|
|
24
|
+
const ast = await this.parseAST(content, filePath, language);
|
|
25
|
+
const facts = ast ? await this.extractFacts(ast, filePath, language) : [];
|
|
26
|
+
const chunks = this.chunker.chunk(content, { filePath, language });
|
|
27
|
+
const embeddings = await this.generateEmbeddings(chunks, language);
|
|
28
|
+
|
|
29
|
+
const duration = Date.now() - startTime;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
file: {
|
|
33
|
+
id: this.generateFileId(filePath),
|
|
34
|
+
path: filePath,
|
|
35
|
+
content_hash: contentHash,
|
|
36
|
+
last_modified: Date.now(),
|
|
37
|
+
language,
|
|
38
|
+
line_count: content.split('\n').length
|
|
39
|
+
},
|
|
40
|
+
facts,
|
|
41
|
+
chunks,
|
|
42
|
+
embeddings,
|
|
43
|
+
duration
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async parseAST(content, filePath, language) {
|
|
48
|
+
try {
|
|
49
|
+
return await parse(content, filePath);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.warn(`[Indexer] Failed to parse ${filePath}:`, error.message);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async extractFacts(ast, filePath, language) {
|
|
57
|
+
const facts = [];
|
|
58
|
+
const fileId = this.generateFileId(filePath);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const { QUERIES } = await import('../../strategies/tree-sitter/queries.js');
|
|
62
|
+
const queries = QUERIES[language];
|
|
63
|
+
|
|
64
|
+
if (queries?.definitions) {
|
|
65
|
+
const captures = queries.definitions.captures(ast.rootNode);
|
|
66
|
+
|
|
67
|
+
for (const capture of captures) {
|
|
68
|
+
if (capture.name === 'name') {
|
|
69
|
+
facts.push({
|
|
70
|
+
id: this.generateFactId(fileId, capture.node.text, 'function'),
|
|
71
|
+
file_id: fileId,
|
|
72
|
+
type: 'function',
|
|
73
|
+
name: capture.node.text,
|
|
74
|
+
line_start: capture.node.startPosition.row + 1,
|
|
75
|
+
line_end: capture.node.endPosition.row + 1,
|
|
76
|
+
signature: '',
|
|
77
|
+
exported: false,
|
|
78
|
+
confidence: 1.0
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn(`[Indexer] Failed to extract facts:`, error.message);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return facts;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async generateEmbeddings(chunks, language = 'javascript') {
|
|
91
|
+
const embeddings = [];
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
for (const chunk of chunks) {
|
|
95
|
+
const vector = await this.embedder.embedCode(chunk.content, language);
|
|
96
|
+
embeddings.push({
|
|
97
|
+
chunk_id: chunk.id,
|
|
98
|
+
embedding: Buffer.from(new Float32Array(vector).buffer),
|
|
99
|
+
model: 'simple-embedder'
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('[Indexer] Failed to generate embeddings:', error.message);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return embeddings;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
generateFileId(filePath) {
|
|
110
|
+
return crypto.createHash('md5').update(filePath).digest('hex').substring(0, 16);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
generateFactId(fileId, name, type) {
|
|
114
|
+
return crypto.createHash('md5').update(`${fileId}:${name}:${type}`).digest('hex').substring(0, 16);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default CodeIndexer;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Embedding Generator
|
|
3
|
+
* KHÔNG dùng LLM - Chỉ dùng thuật toán truyền thống
|
|
4
|
+
* Methods: TF-IDF, Bag-of-Words, Code-specific features
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class SimpleEmbedder {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.dimension = options.dimension || 512;
|
|
10
|
+
this.vocabulary = new Map(); // word -> index
|
|
11
|
+
this.idf = new Map(); // word -> idf score
|
|
12
|
+
this.documentCount = 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TF-IDF Embedding
|
|
17
|
+
* Phương pháp truyền thống dùng trong search engines
|
|
18
|
+
*/
|
|
19
|
+
embed(text) {
|
|
20
|
+
const vector = new Array(this.dimension).fill(0);
|
|
21
|
+
|
|
22
|
+
// Tokenize code
|
|
23
|
+
const tokens = this.tokenize(text);
|
|
24
|
+
|
|
25
|
+
// Calculate TF (Term Frequency)
|
|
26
|
+
const tf = new Map();
|
|
27
|
+
tokens.forEach(token => {
|
|
28
|
+
tf.set(token, (tf.get(token) || 0) + 1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Normalize TF
|
|
32
|
+
const maxTf = Math.max(...tf.values());
|
|
33
|
+
tf.forEach((count, token) => {
|
|
34
|
+
tf.set(token, count / maxTf);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Create embedding vector
|
|
38
|
+
tf.forEach((freq, token) => {
|
|
39
|
+
const index = this.getTokenIndex(token);
|
|
40
|
+
const idf = this.idf.get(token) || 1;
|
|
41
|
+
vector[index % this.dimension] = freq * idf;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Normalize vector
|
|
45
|
+
return this.normalize(vector);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Code-specific embedding
|
|
50
|
+
* Tập trung vào đặc trưng của code
|
|
51
|
+
*/
|
|
52
|
+
embedCode(code, language = 'javascript') {
|
|
53
|
+
const vector = new Array(this.dimension).fill(0);
|
|
54
|
+
|
|
55
|
+
// 1. Tokenize và tách từ khóa
|
|
56
|
+
const tokens = this.tokenize(code);
|
|
57
|
+
|
|
58
|
+
// 2. Trích xuất đặc trưng code
|
|
59
|
+
const features = this.extractCodeFeatures(code, language);
|
|
60
|
+
|
|
61
|
+
// 3. Kết hợp features vào vector
|
|
62
|
+
let index = 0;
|
|
63
|
+
|
|
64
|
+
// Function names (vị trí 0-100)
|
|
65
|
+
features.functions.forEach((func, i) => {
|
|
66
|
+
const hash = this.hashString(func) % 100;
|
|
67
|
+
vector[hash] = 1.0;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Variable names (vị trí 100-200)
|
|
71
|
+
features.variables.forEach((variable, i) => {
|
|
72
|
+
const hash = 100 + (this.hashString(variable) % 100);
|
|
73
|
+
vector[hash] = 0.8;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Class names (vị trí 200-300)
|
|
77
|
+
features.classes.forEach((cls, i) => {
|
|
78
|
+
const hash = 200 + (this.hashString(cls) % 100);
|
|
79
|
+
vector[hash] = 1.0;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Keywords (vị trí 300-400)
|
|
83
|
+
const keywords = this.getKeywords(tokens, language);
|
|
84
|
+
keywords.forEach((keyword, i) => {
|
|
85
|
+
const hash = 300 + (this.hashString(keyword) % 100);
|
|
86
|
+
vector[hash] = 0.6;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Import/Export patterns (vị trí 400-500)
|
|
90
|
+
features.imports.forEach((imp, i) => {
|
|
91
|
+
const hash = 400 + (this.hashString(imp) % 100);
|
|
92
|
+
vector[hash] = 0.9;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Code structure (vị trí 500-512)
|
|
96
|
+
vector[500] = features.hasAsync ? 1.0 : 0;
|
|
97
|
+
vector[501] = features.hasClass ? 1.0 : 0;
|
|
98
|
+
vector[502] = features.hasExport ? 1.0 : 0;
|
|
99
|
+
vector[503] = features.lineCount / 1000; // Normalized
|
|
100
|
+
|
|
101
|
+
return this.normalize(vector);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Tokenize text/code
|
|
106
|
+
*/
|
|
107
|
+
tokenize(text) {
|
|
108
|
+
// Tách từ theo camelCase, snake_case, và ký tự đặc biệt
|
|
109
|
+
return text
|
|
110
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase → camel Case
|
|
111
|
+
.replace(/_/g, ' ') // snake_case → snake case
|
|
112
|
+
.toLowerCase()
|
|
113
|
+
.match(/[a-z]+/g) || [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Trích xuất đặc trưng từ code
|
|
118
|
+
*/
|
|
119
|
+
extractCodeFeatures(code, language) {
|
|
120
|
+
const features = {
|
|
121
|
+
functions: [],
|
|
122
|
+
variables: [],
|
|
123
|
+
classes: [],
|
|
124
|
+
imports: [],
|
|
125
|
+
hasAsync: /\basync\b/.test(code),
|
|
126
|
+
hasClass: /\bclass\b/.test(code),
|
|
127
|
+
hasExport: /\bexport\b/.test(code),
|
|
128
|
+
lineCount: code.split('\n').length
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Extract function names
|
|
132
|
+
const funcMatches = code.matchAll(/(?:function|const|let|var)\s+(\w+)\s*[=\(]/g);
|
|
133
|
+
for (const match of funcMatches) {
|
|
134
|
+
features.functions.push(match[1]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Extract arrow functions
|
|
138
|
+
const arrowMatches = code.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*\(?.*\)?\s*=>/g);
|
|
139
|
+
for (const match of arrowMatches) {
|
|
140
|
+
features.functions.push(match[1]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extract class names
|
|
144
|
+
const classMatches = code.matchAll(/class\s+(\w+)/g);
|
|
145
|
+
for (const match of classMatches) {
|
|
146
|
+
features.classes.push(match[1]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Extract imports
|
|
150
|
+
const importMatches = code.matchAll(/import\s+(?:{?\s*([^{}]+)\s*}?\s+from\s+)?['"]([^'"]+)['"]/g);
|
|
151
|
+
for (const match of importMatches) {
|
|
152
|
+
if (match[1]) {
|
|
153
|
+
features.imports.push(...match[1].split(',').map(s => s.trim()));
|
|
154
|
+
}
|
|
155
|
+
features.imports.push(match[2]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Extract variable names (simple heuristic)
|
|
159
|
+
const varMatches = code.matchAll(/(?:const|let|var)\s+(\w+)/g);
|
|
160
|
+
for (const match of varMatches) {
|
|
161
|
+
if (!features.functions.includes(match[1])) {
|
|
162
|
+
features.variables.push(match[1]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return features;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Lấy keywords quan trọng
|
|
171
|
+
*/
|
|
172
|
+
getKeywords(tokens, language) {
|
|
173
|
+
const codeKeywords = {
|
|
174
|
+
javascript: ['function', 'class', 'const', 'let', 'var', 'async', 'await', 'return', 'if', 'for', 'while', 'import', 'export'],
|
|
175
|
+
typescript: ['interface', 'type', 'enum', 'namespace', 'extends', 'implements', 'function', 'class', 'const', 'let', 'var', 'async', 'await'],
|
|
176
|
+
python: ['def', 'class', 'import', 'from', 'return', 'if', 'for', 'while'],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const keywords = codeKeywords[language] || codeKeywords.javascript;
|
|
180
|
+
return tokens.filter(token => keywords.includes(token));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Hash string thành số
|
|
185
|
+
*/
|
|
186
|
+
hashString(str) {
|
|
187
|
+
let hash = 0;
|
|
188
|
+
for (let i = 0; i < str.length; i++) {
|
|
189
|
+
const char = str.charCodeAt(i);
|
|
190
|
+
hash = ((hash << 5) - hash) + char;
|
|
191
|
+
hash = hash & hash;
|
|
192
|
+
}
|
|
193
|
+
return Math.abs(hash);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Lấy index cho token trong vocabulary
|
|
198
|
+
*/
|
|
199
|
+
getTokenIndex(token) {
|
|
200
|
+
if (!this.vocabulary.has(token)) {
|
|
201
|
+
this.vocabulary.set(token, this.vocabulary.size);
|
|
202
|
+
}
|
|
203
|
+
return this.vocabulary.get(token);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Normalize vector (L2 normalization)
|
|
208
|
+
*/
|
|
209
|
+
normalize(vector) {
|
|
210
|
+
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
211
|
+
if (magnitude === 0) return vector;
|
|
212
|
+
return vector.map(val => val / magnitude);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Tính cosine similarity giữa 2 embeddings
|
|
217
|
+
*/
|
|
218
|
+
cosineSimilarity(embedding1, embedding2) {
|
|
219
|
+
let dotProduct = 0;
|
|
220
|
+
for (let i = 0; i < embedding1.length; i++) {
|
|
221
|
+
dotProduct += embedding1[i] * embedding2[i];
|
|
222
|
+
}
|
|
223
|
+
return dotProduct; // Vectors đã normalize nên chỉ cần dot product
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Batch embeddings
|
|
228
|
+
*/
|
|
229
|
+
embedBatch(texts) {
|
|
230
|
+
return texts.map(text => this.embedCode(text));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export default SimpleEmbedder;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Router - Intelligent Local vs Mercury Decision Engine
|
|
3
|
+
*
|
|
4
|
+
* Tự động quyết định khi nào dùng local (90%) vs Mercury (95%)
|
|
5
|
+
* dựa trên: query complexity, cost optimization, và user preferences
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class SmartRouter {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.dashboardClient = options.dashboardClient || null;
|
|
11
|
+
this.localAccuracy = 0.90;
|
|
12
|
+
this.mercuryAccuracy = 0.95;
|
|
13
|
+
this.costPerMercuryCall = 0.01; // $0.01 per call
|
|
14
|
+
|
|
15
|
+
// User configuration từ Dashboard
|
|
16
|
+
this.config = {
|
|
17
|
+
enableMercury: false,
|
|
18
|
+
smartRoutingMode: 'auto', // 'always_local', 'auto', 'always_mercury'
|
|
19
|
+
rerankThreshold: 0.7,
|
|
20
|
+
monthlyBudget: 20,
|
|
21
|
+
currentUsage: 0
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Query analysis patterns
|
|
25
|
+
this.complexityPatterns = {
|
|
26
|
+
high: [
|
|
27
|
+
/\b(find|search|locate)\b.*\b(all|every|each)\b/i,
|
|
28
|
+
/\b(compare|difference|versus|vs)\b/i,
|
|
29
|
+
/\b(relationship|connection|link|reference)\b/i,
|
|
30
|
+
/\b(impact|affect|change|modify)\b.*\b(multiple|many|several)\b/i,
|
|
31
|
+
/\b(best|optimal|recommend|suggest)\b/i,
|
|
32
|
+
/\b(why|how|explain|understand)\b/i,
|
|
33
|
+
/\b(similar|related|like)\b/i,
|
|
34
|
+
/\b(dependencies|imports|uses)\b/i,
|
|
35
|
+
/\b(refactor|restructure|reorganize)\b/i,
|
|
36
|
+
/\b(natural language|plain text|sentence)\b/i
|
|
37
|
+
],
|
|
38
|
+
medium: [
|
|
39
|
+
/\b(update|change|modify|edit)\b/i,
|
|
40
|
+
/\b(fix|correct|repair|solve)\b/i,
|
|
41
|
+
/\b(add|create|new|insert)\b/i,
|
|
42
|
+
/\b(remove|delete|clear)\b/i,
|
|
43
|
+
/\b(validate|check|verify|test)\b/i,
|
|
44
|
+
/\b(handle|process|manage)\b/i
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Historical performance tracking
|
|
49
|
+
this.history = {
|
|
50
|
+
localSuccess: 0,
|
|
51
|
+
localTotal: 0,
|
|
52
|
+
mercurySuccess: 0,
|
|
53
|
+
mercuryTotal: 0,
|
|
54
|
+
queryTypes: new Map()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load configuration từ Dashboard
|
|
60
|
+
*/
|
|
61
|
+
async loadConfig() {
|
|
62
|
+
if (!this.dashboardClient) return;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const config = await this.dashboardClient.getSearchConfig();
|
|
66
|
+
this.config = {
|
|
67
|
+
enableMercury: config.enable_mercury_rerank ?? false,
|
|
68
|
+
smartRoutingMode: config.smart_routing_mode ?? 'auto',
|
|
69
|
+
rerankThreshold: config.rerank_threshold ?? 0.7,
|
|
70
|
+
monthlyBudget: config.monthly_budget ?? 20,
|
|
71
|
+
currentUsage: config.usage_this_month ?? 0
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn('[SmartRouter] Failed to load config:', error.message);
|
|
75
|
+
// Fallback: use default config
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Main decision method - chọn local hay Mercury
|
|
81
|
+
*/
|
|
82
|
+
async shouldUseMercury(query, localResults = null) {
|
|
83
|
+
// 1. Kiểm tra cơ bản
|
|
84
|
+
if (!this.config.enableMercury) {
|
|
85
|
+
return { useMercury: false, reason: 'Mercury disabled in settings' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Check mode
|
|
89
|
+
if (this.config.smartRoutingMode === 'always_local') {
|
|
90
|
+
return { useMercury: false, reason: 'Mode: always_local' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.config.smartRoutingMode === 'always_mercury') {
|
|
94
|
+
const withinBudget = await this.checkBudget();
|
|
95
|
+
if (withinBudget) {
|
|
96
|
+
return { useMercury: true, reason: 'Mode: always_mercury' };
|
|
97
|
+
}
|
|
98
|
+
return { useMercury: false, reason: 'Budget exceeded, fallback to local' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. Auto mode - smart decision
|
|
102
|
+
return this.makeSmartDecision(query, localResults);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Smart decision logic
|
|
107
|
+
*/
|
|
108
|
+
async makeSmartDecision(query, localResults) {
|
|
109
|
+
const scores = {
|
|
110
|
+
queryComplexity: this.analyzeQueryComplexity(query),
|
|
111
|
+
confidenceGap: this.calculateConfidenceGap(localResults),
|
|
112
|
+
budgetStatus: await this.checkBudget() ? 1 : 0,
|
|
113
|
+
historicalSuccess: this.getHistoricalSuccess(query),
|
|
114
|
+
urgency: this.analyzeUrgency(query)
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Weighted scoring
|
|
118
|
+
const totalScore =
|
|
119
|
+
(scores.queryComplexity * 0.30) +
|
|
120
|
+
(scores.confidenceGap * 0.25) +
|
|
121
|
+
(scores.budgetStatus * 0.20) +
|
|
122
|
+
(scores.historicalSuccess * 0.15) +
|
|
123
|
+
(scores.urgency * 0.10);
|
|
124
|
+
|
|
125
|
+
// Decision
|
|
126
|
+
const useMercury = totalScore >= this.config.rerankThreshold;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
useMercury,
|
|
130
|
+
reason: useMercury ?
|
|
131
|
+
`Complex query detected (score: ${totalScore.toFixed(2)})` :
|
|
132
|
+
`Simple query, local sufficient (score: ${totalScore.toFixed(2)})`,
|
|
133
|
+
scores,
|
|
134
|
+
confidence: totalScore
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Analyze query complexity (0-1)
|
|
140
|
+
*/
|
|
141
|
+
analyzeQueryComplexity(query) {
|
|
142
|
+
const queryLower = query.toLowerCase();
|
|
143
|
+
let complexity = 0.5; // Base
|
|
144
|
+
|
|
145
|
+
// Check high complexity patterns
|
|
146
|
+
for (const pattern of this.complexityPatterns.high) {
|
|
147
|
+
if (pattern.test(queryLower)) {
|
|
148
|
+
complexity += 0.15;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check medium complexity patterns
|
|
153
|
+
for (const pattern of this.complexityPatterns.medium) {
|
|
154
|
+
if (pattern.test(queryLower)) {
|
|
155
|
+
complexity += 0.05;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Query length factor
|
|
160
|
+
const wordCount = query.split(/\s+/).length;
|
|
161
|
+
if (wordCount > 10) complexity += 0.1;
|
|
162
|
+
if (wordCount > 20) complexity += 0.1;
|
|
163
|
+
|
|
164
|
+
// Multiple operations
|
|
165
|
+
const operations = ['create', 'update', 'delete', 'find', 'validate', 'handle'];
|
|
166
|
+
const opCount = operations.filter(op => queryLower.includes(op)).length;
|
|
167
|
+
complexity += opCount * 0.05;
|
|
168
|
+
|
|
169
|
+
return Math.min(1, complexity);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Calculate confidence gap from local results
|
|
174
|
+
*/
|
|
175
|
+
calculateConfidenceGap(localResults) {
|
|
176
|
+
if (!localResults || localResults.length === 0) {
|
|
177
|
+
return 1.0; // No results = need Mercury
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check top result confidence
|
|
181
|
+
const topScore = localResults[0].score || localResults[0].similarity || 0;
|
|
182
|
+
|
|
183
|
+
// Check score distribution
|
|
184
|
+
if (localResults.length >= 2) {
|
|
185
|
+
const secondScore = localResults[1].score || localResults[1].similarity || 0;
|
|
186
|
+
const gap = topScore - secondScore;
|
|
187
|
+
|
|
188
|
+
// If gap is small, need Mercury to differentiate
|
|
189
|
+
if (gap < 0.1) return 0.8;
|
|
190
|
+
if (gap < 0.2) return 0.5;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Low confidence in top result
|
|
194
|
+
if (topScore < 0.6) return 0.7;
|
|
195
|
+
if (topScore < 0.8) return 0.4;
|
|
196
|
+
|
|
197
|
+
return 0.2; // High confidence, no need for Mercury
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if within budget
|
|
202
|
+
*/
|
|
203
|
+
async checkBudget() {
|
|
204
|
+
if (this.config.monthlyBudget === 0) return true; // Unlimited
|
|
205
|
+
|
|
206
|
+
const projectedCost = (this.config.currentUsage + 1) * this.costPerMercuryCall;
|
|
207
|
+
const remaining = this.config.monthlyBudget - projectedCost;
|
|
208
|
+
|
|
209
|
+
// Use Mercury if we have 20% budget remaining
|
|
210
|
+
return remaining > (this.config.monthlyBudget * 0.2);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get historical success rate for similar queries
|
|
215
|
+
*/
|
|
216
|
+
getHistoricalSuccess(query) {
|
|
217
|
+
// Extract query type
|
|
218
|
+
const queryType = this.extractQueryType(query);
|
|
219
|
+
const history = this.history.queryTypes.get(queryType);
|
|
220
|
+
|
|
221
|
+
if (!history || history.total < 5) {
|
|
222
|
+
return 0.5; // Neutral if not enough data
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const successRate = history.success / history.total;
|
|
226
|
+
|
|
227
|
+
// If local has high success rate, prefer local
|
|
228
|
+
if (successRate > 0.8) return 0.2;
|
|
229
|
+
if (successRate > 0.6) return 0.4;
|
|
230
|
+
|
|
231
|
+
// Low success rate, prefer Mercury
|
|
232
|
+
return 0.8;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Analyze urgency (0-1)
|
|
237
|
+
*/
|
|
238
|
+
analyzeUrgency(query) {
|
|
239
|
+
const queryLower = query.toLowerCase();
|
|
240
|
+
|
|
241
|
+
// Urgent keywords
|
|
242
|
+
const urgentWords = ['fix', 'bug', 'error', 'crash', 'urgent', 'critical', 'asap'];
|
|
243
|
+
if (urgentWords.some(w => queryLower.includes(w))) {
|
|
244
|
+
return 0.9; // Use best accuracy (Mercury)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Exploration keywords
|
|
248
|
+
const exploreWords = ['explore', 'understand', 'learn', 'research', 'investigate'];
|
|
249
|
+
if (exploreWords.some(w => queryLower.includes(w))) {
|
|
250
|
+
return 0.3; // Can use local, not urgent
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return 0.5;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extract query type for history tracking
|
|
258
|
+
*/
|
|
259
|
+
extractQueryType(query) {
|
|
260
|
+
const queryLower = query.toLowerCase();
|
|
261
|
+
|
|
262
|
+
if (/\b(find|search|locate|get)\b/.test(queryLower)) return 'search';
|
|
263
|
+
if (/\b(update|change|modify|edit)\b/.test(queryLower)) return 'update';
|
|
264
|
+
if (/\b(create|add|new|insert)\b/.test(queryLower)) return 'create';
|
|
265
|
+
if (/\b(delete|remove|clear)\b/.test(queryLower)) return 'delete';
|
|
266
|
+
if (/\b(fix|correct|repair)\b/.test(queryLower)) return 'fix';
|
|
267
|
+
if (/\b(validate|check|verify|test)\b/.test(queryLower)) return 'validate';
|
|
268
|
+
if (/\b(refactor|restructure)\b/.test(queryLower)) return 'refactor';
|
|
269
|
+
|
|
270
|
+
return 'general';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Record result for learning
|
|
275
|
+
*/
|
|
276
|
+
recordResult(query, usedMercury, success) {
|
|
277
|
+
const queryType = this.extractQueryType(query);
|
|
278
|
+
|
|
279
|
+
// Update query type history
|
|
280
|
+
if (!this.history.queryTypes.has(queryType)) {
|
|
281
|
+
this.history.queryTypes.set(queryType, { success: 0, total: 0 });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const history = this.history.queryTypes.get(queryType);
|
|
285
|
+
history.total++;
|
|
286
|
+
if (success) history.success++;
|
|
287
|
+
|
|
288
|
+
// Update global stats
|
|
289
|
+
if (usedMercury) {
|
|
290
|
+
this.history.mercuryTotal++;
|
|
291
|
+
if (success) this.history.mercurySuccess++;
|
|
292
|
+
} else {
|
|
293
|
+
this.history.localTotal++;
|
|
294
|
+
if (success) this.history.localSuccess++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Log usage to Dashboard
|
|
300
|
+
*/
|
|
301
|
+
async logUsage(query, usedMercury, duration) {
|
|
302
|
+
if (!this.dashboardClient) return;
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await this.dashboardClient.logSearchUsage({
|
|
306
|
+
query,
|
|
307
|
+
usedMercury,
|
|
308
|
+
duration,
|
|
309
|
+
timestamp: Date.now()
|
|
310
|
+
});
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.warn('[SmartRouter] Failed to log usage:', error.message);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get stats
|
|
318
|
+
*/
|
|
319
|
+
getStats() {
|
|
320
|
+
const localRate = this.history.localTotal > 0 ?
|
|
321
|
+
this.history.localSuccess / this.history.localTotal : 0;
|
|
322
|
+
const mercuryRate = this.history.mercuryTotal > 0 ?
|
|
323
|
+
this.history.mercurySuccess / this.history.mercuryTotal : 0;
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
config: this.config,
|
|
327
|
+
history: {
|
|
328
|
+
local: {
|
|
329
|
+
total: this.history.localTotal,
|
|
330
|
+
success: this.history.localSuccess,
|
|
331
|
+
rate: localRate
|
|
332
|
+
},
|
|
333
|
+
mercury: {
|
|
334
|
+
total: this.history.mercuryTotal,
|
|
335
|
+
success: this.history.mercurySuccess,
|
|
336
|
+
rate: mercuryRate
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
estimatedMonthlyCost: this.history.mercuryTotal * this.costPerMercuryCall
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export default SmartRouter;
|