simple-dynamsoft-mcp 6.3.0 → 7.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/.env.example +35 -9
- package/README.md +156 -497
- package/package.json +13 -7
- package/scripts/prebuild-rag-index.mjs +1 -1
- package/scripts/run-gemini-tests.mjs +1 -1
- package/scripts/sync-submodules.mjs +1 -1
- package/scripts/verify-doc-resources.mjs +79 -0
- package/src/data/bootstrap.js +475 -0
- package/src/data/download-utils.js +99 -0
- package/src/data/hydration-mode.js +15 -0
- package/src/data/hydration-policy.js +39 -0
- package/src/data/repo-map.js +149 -0
- package/src/{data-root.js → data/root.js} +1 -1
- package/src/{submodule-sync.js → data/submodule-sync.js} +1 -1
- package/src/index.js +49 -1499
- package/src/observability/logging.js +51 -0
- package/src/rag/config.js +96 -0
- package/src/rag/index.js +266 -0
- package/src/rag/lexical-provider.js +170 -0
- package/src/rag/logger.js +46 -0
- package/src/rag/profile-config.js +48 -0
- package/src/rag/providers.js +585 -0
- package/src/rag/search-utils.js +166 -0
- package/src/rag/vector-cache.js +323 -0
- package/src/server/create-server.js +168 -0
- package/src/server/helpers/server-helpers.js +33 -0
- package/src/{resource-index → server/resource-index}/paths.js +2 -2
- package/src/{resource-index → server/resource-index}/samples.js +9 -1
- package/src/{resource-index.js → server/resource-index.js} +158 -93
- package/src/server/resources/register-resources.js +56 -0
- package/src/server/runtime-config.js +66 -0
- package/src/server/tools/register-index-tools.js +130 -0
- package/src/server/tools/register-project-tools.js +305 -0
- package/src/server/tools/register-quickstart-tools.js +572 -0
- package/src/server/tools/register-sample-tools.js +333 -0
- package/src/server/tools/register-version-tools.js +136 -0
- package/src/server/transports/http.js +84 -0
- package/src/server/transports/stdio.js +12 -0
- package/src/data-bootstrap.js +0 -255
- package/src/rag.js +0 -1203
- /package/src/{gemini-retry.js → rag/gemini-retry.js} +0 -0
- /package/src/{normalizers.js → server/normalizers.js} +0 -0
- /package/src/{resource-index → server/resource-index}/builders.js +0 -0
- /package/src/{resource-index → server/resource-index}/config.js +0 -0
- /package/src/{resource-index → server/resource-index}/docs-loader.js +0 -0
- /package/src/{resource-index → server/resource-index}/uri.js +0 -0
- /package/src/{resource-index → server/resource-index}/version-policy.js +0 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
sleepMs,
|
|
6
|
+
parseRetryAfterMs,
|
|
7
|
+
normalizeGeminiRetryConfig,
|
|
8
|
+
isRateLimitGeminiStatus,
|
|
9
|
+
GeminiHttpError,
|
|
10
|
+
executeWithGeminiRetry
|
|
11
|
+
} from "./gemini-retry.js";
|
|
12
|
+
|
|
13
|
+
function ensureDirectory(path) {
|
|
14
|
+
if (!existsSync(path)) {
|
|
15
|
+
mkdirSync(path, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveProviderChain(ragConfig) {
|
|
20
|
+
let primary = ragConfig.provider;
|
|
21
|
+
if (primary === "auto") {
|
|
22
|
+
primary = ragConfig.geminiApiKey ? "gemini" : "local";
|
|
23
|
+
}
|
|
24
|
+
const chain = [primary];
|
|
25
|
+
if (ragConfig.fallback && ragConfig.fallback !== "none" && ragConfig.fallback !== primary) {
|
|
26
|
+
chain.push(ragConfig.fallback);
|
|
27
|
+
}
|
|
28
|
+
return Array.from(new Set(chain));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function embedTextsWithProgress(
|
|
32
|
+
texts,
|
|
33
|
+
embedder,
|
|
34
|
+
batchSize = 1,
|
|
35
|
+
{
|
|
36
|
+
offset = 0,
|
|
37
|
+
total = texts.length,
|
|
38
|
+
onChunk = null,
|
|
39
|
+
providerName = "",
|
|
40
|
+
logRag,
|
|
41
|
+
isRateLimitError
|
|
42
|
+
} = {}
|
|
43
|
+
) {
|
|
44
|
+
const results = [];
|
|
45
|
+
const normalizedBatchSize = Math.max(1, batchSize);
|
|
46
|
+
let completed = offset;
|
|
47
|
+
let currentBatchSize = normalizedBatchSize;
|
|
48
|
+
let rateLimitFailures = 0;
|
|
49
|
+
let batchDowngrades = 0;
|
|
50
|
+
let singleFallbackBatches = 0;
|
|
51
|
+
|
|
52
|
+
const reportChunk = async (vectors, mode, sourceBatchSize) => {
|
|
53
|
+
if (!Array.isArray(vectors) || vectors.length === 0) return;
|
|
54
|
+
completed += vectors.length;
|
|
55
|
+
if (onChunk) {
|
|
56
|
+
await onChunk({
|
|
57
|
+
vectors,
|
|
58
|
+
mode,
|
|
59
|
+
sourceBatchSize,
|
|
60
|
+
completed,
|
|
61
|
+
total
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (embedder.embedBatch && normalizedBatchSize > 1) {
|
|
67
|
+
let index = 0;
|
|
68
|
+
while (index < texts.length) {
|
|
69
|
+
const batch = texts.slice(index, index + currentBatchSize);
|
|
70
|
+
try {
|
|
71
|
+
const vectors = await embedder.embedBatch(batch);
|
|
72
|
+
if (!Array.isArray(vectors) || vectors.length !== batch.length) {
|
|
73
|
+
throw new Error(`Gemini batch response size mismatch expected=${batch.length} actual=${vectors?.length || 0}`);
|
|
74
|
+
}
|
|
75
|
+
results.push(...vectors);
|
|
76
|
+
index += batch.length;
|
|
77
|
+
rateLimitFailures = 0;
|
|
78
|
+
await reportChunk(vectors, "batch", batch.length);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (isRateLimitError(error)) {
|
|
81
|
+
rateLimitFailures += 1;
|
|
82
|
+
const nextBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
|
|
83
|
+
if (nextBatchSize < currentBatchSize) {
|
|
84
|
+
batchDowngrades += 1;
|
|
85
|
+
logRag(
|
|
86
|
+
`gemini batch downgrade provider=${providerName || "unknown"} from=${currentBatchSize} to=${nextBatchSize} ` +
|
|
87
|
+
`rate_limit_failures=${rateLimitFailures}`
|
|
88
|
+
);
|
|
89
|
+
currentBatchSize = nextBatchSize;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
singleFallbackBatches += 1;
|
|
95
|
+
logRag(
|
|
96
|
+
`batch embedding fallback provider=${providerName || "unknown"} batch_size=${batch.length} reason=${error.message}`
|
|
97
|
+
);
|
|
98
|
+
for (const text of batch) {
|
|
99
|
+
const vector = await embedder.embed(text);
|
|
100
|
+
results.push(vector);
|
|
101
|
+
await reportChunk([vector], "single_fallback", 1);
|
|
102
|
+
}
|
|
103
|
+
index += batch.length;
|
|
104
|
+
rateLimitFailures = 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
vectors: results,
|
|
110
|
+
stats: {
|
|
111
|
+
batchDowngrades,
|
|
112
|
+
singleFallbackBatches,
|
|
113
|
+
finalBatchSize: currentBatchSize
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const text of texts) {
|
|
119
|
+
const vector = await embedder.embed(text);
|
|
120
|
+
results.push(vector);
|
|
121
|
+
await reportChunk([vector], "single", 1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
vectors: results,
|
|
126
|
+
stats: {
|
|
127
|
+
batchDowngrades,
|
|
128
|
+
singleFallbackBatches,
|
|
129
|
+
finalBatchSize: 1
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createProviderOrchestrator({
|
|
135
|
+
pkgVersion,
|
|
136
|
+
ragConfig,
|
|
137
|
+
ragLogState,
|
|
138
|
+
logRag,
|
|
139
|
+
resourceIndex,
|
|
140
|
+
resourceIndexByUri,
|
|
141
|
+
createLexicalProvider,
|
|
142
|
+
getRagSignatureData,
|
|
143
|
+
utils,
|
|
144
|
+
vectorCache
|
|
145
|
+
}) {
|
|
146
|
+
let fuseSearch = utils.createFuseSearch(resourceIndex);
|
|
147
|
+
const providerCache = new Map();
|
|
148
|
+
let localEmbedderPromise = null;
|
|
149
|
+
let geminiEmbedderPromise = null;
|
|
150
|
+
|
|
151
|
+
async function getLocalEmbedder() {
|
|
152
|
+
if (localEmbedderPromise) return localEmbedderPromise;
|
|
153
|
+
localEmbedderPromise = (async () => {
|
|
154
|
+
const { pipeline, env } = await import("@xenova/transformers");
|
|
155
|
+
ensureDirectory(ragConfig.modelCacheDir);
|
|
156
|
+
if (!ragLogState.localEmbedderInit) {
|
|
157
|
+
ragLogState.localEmbedderInit = true;
|
|
158
|
+
logRag(
|
|
159
|
+
`init local embedder model=${ragConfig.localModel} quantized=${ragConfig.localQuantized} model_cache_dir=${ragConfig.modelCacheDir}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
env.cacheDir = ragConfig.modelCacheDir;
|
|
163
|
+
env.allowLocalModels = true;
|
|
164
|
+
const extractor = await pipeline("feature-extraction", ragConfig.localModel, {
|
|
165
|
+
quantized: ragConfig.localQuantized
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
embed: async (text) => {
|
|
169
|
+
const output = await extractor(text, { pooling: "mean", normalize: true });
|
|
170
|
+
return Array.from(output.data);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
})();
|
|
174
|
+
return localEmbedderPromise;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function getGeminiEmbedder() {
|
|
178
|
+
if (!ragConfig.geminiApiKey) {
|
|
179
|
+
throw new Error("GEMINI_API_KEY is required for gemini embeddings.");
|
|
180
|
+
}
|
|
181
|
+
if (geminiEmbedderPromise) return geminiEmbedderPromise;
|
|
182
|
+
const retryConfig = normalizeGeminiRetryConfig({
|
|
183
|
+
maxAttempts: ragConfig.geminiRetryMaxAttempts,
|
|
184
|
+
baseDelayMs: ragConfig.geminiRetryBaseDelayMs,
|
|
185
|
+
maxDelayMs: ragConfig.geminiRetryMaxDelayMs,
|
|
186
|
+
requestThrottleMs: ragConfig.geminiRequestThrottleMs
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
geminiEmbedderPromise = Promise.resolve((() => {
|
|
190
|
+
const metrics = {
|
|
191
|
+
requests: 0,
|
|
192
|
+
retries: 0,
|
|
193
|
+
retryDelayMs: 0,
|
|
194
|
+
throttleEvents: 0,
|
|
195
|
+
throttleDelayMs: 0,
|
|
196
|
+
rateLimitRetries: 0
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let nextAllowedAt = 0;
|
|
200
|
+
|
|
201
|
+
const throttleRequest = async (operation) => {
|
|
202
|
+
if (retryConfig.requestThrottleMs <= 0) return;
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
const waitMs = Math.max(0, nextAllowedAt - now);
|
|
205
|
+
if (waitMs > 0) {
|
|
206
|
+
metrics.throttleEvents += 1;
|
|
207
|
+
metrics.throttleDelayMs += waitMs;
|
|
208
|
+
logRag(`gemini throttle op=${operation} wait_ms=${waitMs}`);
|
|
209
|
+
await sleepMs(waitMs);
|
|
210
|
+
}
|
|
211
|
+
nextAllowedAt = Date.now() + retryConfig.requestThrottleMs;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const requestJson = async (operation, endpoint, body) => executeWithGeminiRetry({
|
|
215
|
+
operation,
|
|
216
|
+
retryConfig,
|
|
217
|
+
logger: (message) => logRag(message),
|
|
218
|
+
onRetry: ({ delayMs, rateLimited }) => {
|
|
219
|
+
metrics.retries += 1;
|
|
220
|
+
metrics.retryDelayMs += delayMs;
|
|
221
|
+
if (rateLimited) {
|
|
222
|
+
metrics.rateLimitRetries += 1;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
requestFn: async () => {
|
|
226
|
+
await throttleRequest(operation);
|
|
227
|
+
metrics.requests += 1;
|
|
228
|
+
const response = await fetch(
|
|
229
|
+
`${ragConfig.geminiBaseUrl}/v1beta/${endpoint}?key=${ragConfig.geminiApiKey}`,
|
|
230
|
+
{
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: { "Content-Type": "application/json" },
|
|
233
|
+
body: JSON.stringify(body)
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
const detail = await response.text();
|
|
238
|
+
throw new GeminiHttpError(`Gemini ${operation} failed (${response.status}): ${detail}`, {
|
|
239
|
+
status: response.status,
|
|
240
|
+
detail,
|
|
241
|
+
retryAfterMs: parseRetryAfterMs(response.headers.get("retry-after"))
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return response.json();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
embed: async (text) => {
|
|
250
|
+
const payload = await requestJson(
|
|
251
|
+
"embedContent",
|
|
252
|
+
`${ragConfig.geminiModel}:embedContent`,
|
|
253
|
+
{
|
|
254
|
+
content: {
|
|
255
|
+
parts: [{ text }]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
const embedding = payload.embedding?.values || payload.embedding || payload.embeddings?.[0]?.values;
|
|
260
|
+
if (!embedding) {
|
|
261
|
+
throw new Error("Gemini embedding response missing embedding values.");
|
|
262
|
+
}
|
|
263
|
+
return embedding;
|
|
264
|
+
},
|
|
265
|
+
embedBatch: async (texts) => {
|
|
266
|
+
const payload = await requestJson(
|
|
267
|
+
"batchEmbedContents",
|
|
268
|
+
`${ragConfig.geminiModel}:batchEmbedContents`,
|
|
269
|
+
{
|
|
270
|
+
requests: texts.map((text) => ({
|
|
271
|
+
model: ragConfig.geminiModel,
|
|
272
|
+
content: {
|
|
273
|
+
parts: [{ text }]
|
|
274
|
+
}
|
|
275
|
+
}))
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
const embeddings = payload.embeddings || payload.responses;
|
|
279
|
+
if (!Array.isArray(embeddings)) {
|
|
280
|
+
throw new Error("Gemini batch response missing embeddings.");
|
|
281
|
+
}
|
|
282
|
+
return embeddings.map((item) => item.values || item.embedding?.values || item.embedding);
|
|
283
|
+
},
|
|
284
|
+
getMetrics: () => ({ ...metrics }),
|
|
285
|
+
resetMetrics: () => {
|
|
286
|
+
metrics.requests = 0;
|
|
287
|
+
metrics.retries = 0;
|
|
288
|
+
metrics.retryDelayMs = 0;
|
|
289
|
+
metrics.throttleEvents = 0;
|
|
290
|
+
metrics.throttleDelayMs = 0;
|
|
291
|
+
metrics.rateLimitRetries = 0;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
})());
|
|
295
|
+
return geminiEmbedderPromise;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function createVectorProvider({ name, model, embedder, batchSize }) {
|
|
299
|
+
const signature = utils.buildIndexSignature({
|
|
300
|
+
pkgVersion,
|
|
301
|
+
signatureData: getRagSignatureData(),
|
|
302
|
+
ragConfig
|
|
303
|
+
});
|
|
304
|
+
const cacheMeta = {
|
|
305
|
+
provider: name,
|
|
306
|
+
model,
|
|
307
|
+
signature
|
|
308
|
+
};
|
|
309
|
+
const cacheKey = createHash("sha256").update(JSON.stringify(cacheMeta)).digest("hex");
|
|
310
|
+
const cacheFile = join(ragConfig.cacheDir, vectorCache.makeCacheFileName(name, model, cacheKey));
|
|
311
|
+
const checkpointFile = join(ragConfig.cacheDir, vectorCache.makeCheckpointFileName(name, model, cacheKey));
|
|
312
|
+
const expectedCacheState = {
|
|
313
|
+
cacheKey,
|
|
314
|
+
signature,
|
|
315
|
+
provider: name,
|
|
316
|
+
model
|
|
317
|
+
};
|
|
318
|
+
logRag(
|
|
319
|
+
`provider=${name} cache_file=${cacheFile} rebuild=${ragConfig.rebuild} cache_key=${cacheKey.slice(0, 12)}`
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
let indexPromise = null;
|
|
323
|
+
const loadIndex = async () => {
|
|
324
|
+
if (indexPromise) return indexPromise;
|
|
325
|
+
indexPromise = (async () => {
|
|
326
|
+
if (!ragConfig.rebuild) {
|
|
327
|
+
let cacheState = vectorCache.loadVectorIndexCache(cacheFile, expectedCacheState);
|
|
328
|
+
if (cacheState.hit) {
|
|
329
|
+
const cached = cacheState.payload;
|
|
330
|
+
logRag(
|
|
331
|
+
`cache hit provider=${name} file=${cacheFile} items=${cached.items.length} vectors=${cached.vectors.length}`
|
|
332
|
+
);
|
|
333
|
+
return {
|
|
334
|
+
items: cached.items,
|
|
335
|
+
vectors: cached.vectors
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
logRag(`cache miss provider=${name} file=${cacheFile} reason=${cacheState.reason}`);
|
|
339
|
+
|
|
340
|
+
const downloadResult = await vectorCache.maybeDownloadPrebuiltVectorIndex({
|
|
341
|
+
provider: name,
|
|
342
|
+
model,
|
|
343
|
+
cacheKey,
|
|
344
|
+
signature,
|
|
345
|
+
cacheFile
|
|
346
|
+
});
|
|
347
|
+
if (downloadResult.downloaded) {
|
|
348
|
+
cacheState = vectorCache.loadVectorIndexCache(cacheFile, expectedCacheState);
|
|
349
|
+
if (cacheState.hit) {
|
|
350
|
+
const cached = cacheState.payload;
|
|
351
|
+
logRag(
|
|
352
|
+
`cache hit provider=${name} file=${cacheFile} source=prebuilt_download items=${cached.items.length} vectors=${cached.vectors.length}`
|
|
353
|
+
);
|
|
354
|
+
return {
|
|
355
|
+
items: cached.items,
|
|
356
|
+
vectors: cached.vectors
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
logRag(`cache miss provider=${name} file=${cacheFile} source=prebuilt_download reason=${cacheState.reason}`);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
logRag(`cache bypass provider=${name} file=${cacheFile} reason=rebuild_true`);
|
|
363
|
+
vectorCache.clearVectorIndexCheckpoint(checkpointFile);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const items = utils.buildEmbeddingItems(resourceIndex, ragConfig);
|
|
367
|
+
const texts = items.map((item) => item.text);
|
|
368
|
+
const indexedItems = items.map((item) => ({ id: item.id, uri: item.uri }));
|
|
369
|
+
let normalized = [];
|
|
370
|
+
let resumeFrom = 0;
|
|
371
|
+
if (!ragConfig.rebuild) {
|
|
372
|
+
const checkpointState = vectorCache.loadVectorIndexCheckpoint(checkpointFile, cacheKey, indexedItems);
|
|
373
|
+
if (checkpointState.hit) {
|
|
374
|
+
normalized = checkpointState.payload.vectors;
|
|
375
|
+
resumeFrom = normalized.length;
|
|
376
|
+
logRag(
|
|
377
|
+
`checkpoint resume provider=${name} file=${checkpointFile} completed=${resumeFrom}/${texts.length}`
|
|
378
|
+
);
|
|
379
|
+
} else if (checkpointState.reason !== "missing") {
|
|
380
|
+
logRag(`checkpoint ignored provider=${name} file=${checkpointFile} reason=${checkpointState.reason}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (name === "gemini" && embedder.resetMetrics) {
|
|
385
|
+
embedder.resetMetrics();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const checkpointIntervalMs = 5000;
|
|
389
|
+
let lastCheckpointAt = 0;
|
|
390
|
+
const persistCheckpoint = (force = false) => {
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
if (!force && now - lastCheckpointAt < checkpointIntervalMs) return;
|
|
393
|
+
const payload = {
|
|
394
|
+
cacheKey,
|
|
395
|
+
meta: cacheMeta,
|
|
396
|
+
items: indexedItems,
|
|
397
|
+
vectors: normalized,
|
|
398
|
+
completed: normalized.length,
|
|
399
|
+
total: texts.length,
|
|
400
|
+
updatedAt: new Date().toISOString()
|
|
401
|
+
};
|
|
402
|
+
vectorCache.saveVectorIndexCheckpoint(checkpointFile, payload);
|
|
403
|
+
lastCheckpointAt = now;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
if (resumeFrom < texts.length) {
|
|
407
|
+
logRag(
|
|
408
|
+
`building index provider=${name} embed_items=${texts.length} remaining=${texts.length - resumeFrom} batch_size=${batchSize}`
|
|
409
|
+
);
|
|
410
|
+
try {
|
|
411
|
+
const embeddingResult = await embedTextsWithProgress(
|
|
412
|
+
texts.slice(resumeFrom),
|
|
413
|
+
embedder,
|
|
414
|
+
batchSize,
|
|
415
|
+
{
|
|
416
|
+
offset: resumeFrom,
|
|
417
|
+
total: texts.length,
|
|
418
|
+
providerName: name,
|
|
419
|
+
logRag,
|
|
420
|
+
isRateLimitError: (error) => utils.isRateLimitError(error, isRateLimitGeminiStatus),
|
|
421
|
+
onChunk: ({ vectors, completed, total }) => {
|
|
422
|
+
normalized.push(...vectors.map(utils.normalizeVector));
|
|
423
|
+
persistCheckpoint(completed >= total);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (name === "gemini") {
|
|
429
|
+
const metrics = embedder.getMetrics ? embedder.getMetrics() : {};
|
|
430
|
+
logRag(
|
|
431
|
+
`gemini build metrics provider=${name} requests=${metrics.requests || 0} retries=${metrics.retries || 0} ` +
|
|
432
|
+
`retry_delay_ms=${metrics.retryDelayMs || 0} throttle_events=${metrics.throttleEvents || 0} ` +
|
|
433
|
+
`throttle_delay_ms=${metrics.throttleDelayMs || 0} rate_limit_retries=${metrics.rateLimitRetries || 0} ` +
|
|
434
|
+
`batch_downgrades=${embeddingResult.stats.batchDowngrades} single_fallback_batches=${embeddingResult.stats.singleFallbackBatches} ` +
|
|
435
|
+
`final_batch_size=${embeddingResult.stats.finalBatchSize}`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
} catch (error) {
|
|
439
|
+
persistCheckpoint(true);
|
|
440
|
+
if (name === "gemini") {
|
|
441
|
+
const metrics = embedder.getMetrics ? embedder.getMetrics() : {};
|
|
442
|
+
logRag(
|
|
443
|
+
`gemini build failed provider=${name} requests=${metrics.requests || 0} retries=${metrics.retries || 0} ` +
|
|
444
|
+
`retry_delay_ms=${metrics.retryDelayMs || 0} throttle_events=${metrics.throttleEvents || 0} ` +
|
|
445
|
+
`throttle_delay_ms=${metrics.throttleDelayMs || 0} rate_limit_retries=${metrics.rateLimitRetries || 0} ` +
|
|
446
|
+
`checkpoint_completed=${normalized.length}/${texts.length} error=${error.message}`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
logRag(`checkpoint already complete provider=${name} completed=${resumeFrom}/${texts.length}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const payload = {
|
|
456
|
+
cacheKey,
|
|
457
|
+
meta: cacheMeta,
|
|
458
|
+
items: indexedItems,
|
|
459
|
+
vectors: normalized
|
|
460
|
+
};
|
|
461
|
+
vectorCache.saveVectorIndexCache(cacheFile, payload);
|
|
462
|
+
vectorCache.clearVectorIndexCheckpoint(checkpointFile);
|
|
463
|
+
logRag(`cache saved provider=${name} file=${cacheFile} items=${payload.items.length} vectors=${payload.vectors.length}`);
|
|
464
|
+
return {
|
|
465
|
+
items: payload.items,
|
|
466
|
+
vectors: payload.vectors
|
|
467
|
+
};
|
|
468
|
+
})();
|
|
469
|
+
return indexPromise;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
name,
|
|
474
|
+
search: async (query, filters, limit) => {
|
|
475
|
+
const prepared = utils.truncateText(utils.normalizeText(query), ragConfig.maxTextChars);
|
|
476
|
+
if (!prepared) return [];
|
|
477
|
+
const index = await loadIndex();
|
|
478
|
+
const queryVector = utils.normalizeVector(await embedder.embed(prepared));
|
|
479
|
+
const bestByUri = new Map();
|
|
480
|
+
|
|
481
|
+
for (let i = 0; i < index.vectors.length; i++) {
|
|
482
|
+
const score = utils.dotProduct(queryVector, index.vectors[i]);
|
|
483
|
+
if (ragConfig.minScore && score < ragConfig.minScore) continue;
|
|
484
|
+
const item = index.items[i];
|
|
485
|
+
const entry = resourceIndexByUri.get(item.uri);
|
|
486
|
+
if (!entry || !utils.entryMatchesScope(entry, filters)) continue;
|
|
487
|
+
const existing = bestByUri.get(item.uri);
|
|
488
|
+
if (!existing || score > existing.score) {
|
|
489
|
+
bestByUri.set(item.uri, { entry, score });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const results = Array.from(bestByUri.values())
|
|
494
|
+
.sort((a, b) => b.score - a.score)
|
|
495
|
+
.map((item) => utils.attachScore(item.entry, item.score));
|
|
496
|
+
|
|
497
|
+
if (limit) return results.slice(0, limit);
|
|
498
|
+
return results;
|
|
499
|
+
},
|
|
500
|
+
warm: async () => {
|
|
501
|
+
await loadIndex();
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function createFuseProvider() {
|
|
507
|
+
return {
|
|
508
|
+
name: "fuse",
|
|
509
|
+
search: async (query, filters, limit) => {
|
|
510
|
+
const results = [];
|
|
511
|
+
for (const result of fuseSearch.search(query)) {
|
|
512
|
+
const entry = result.item;
|
|
513
|
+
if (!utils.entryMatchesScope(entry, filters)) continue;
|
|
514
|
+
const score = Number.isFinite(result.score) ? Math.max(0, 1 - result.score) : undefined;
|
|
515
|
+
results.push(utils.attachScore(entry, score));
|
|
516
|
+
}
|
|
517
|
+
if (limit) return results.slice(0, limit);
|
|
518
|
+
return results;
|
|
519
|
+
},
|
|
520
|
+
warm: async () => {}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function refreshProviders() {
|
|
525
|
+
fuseSearch = utils.createFuseSearch(resourceIndex);
|
|
526
|
+
providerCache.clear();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function loadSearchProvider(name) {
|
|
530
|
+
if (providerCache.has(name)) return providerCache.get(name);
|
|
531
|
+
let providerPromise;
|
|
532
|
+
if (name === "fuse") {
|
|
533
|
+
providerPromise = Promise.resolve(createFuseProvider());
|
|
534
|
+
} else if (name === "lexical") {
|
|
535
|
+
providerPromise = Promise.resolve(createLexicalProvider({
|
|
536
|
+
entries: resourceIndex,
|
|
537
|
+
entryMatchesScope: utils.entryMatchesScope,
|
|
538
|
+
attachScore: utils.attachScore
|
|
539
|
+
}));
|
|
540
|
+
} else if (name === "local") {
|
|
541
|
+
providerPromise = (async () => {
|
|
542
|
+
const embedder = await getLocalEmbedder();
|
|
543
|
+
return createVectorProvider({
|
|
544
|
+
name: "local",
|
|
545
|
+
model: ragConfig.localModel,
|
|
546
|
+
embedder,
|
|
547
|
+
batchSize: 1
|
|
548
|
+
});
|
|
549
|
+
})();
|
|
550
|
+
} else if (name === "gemini") {
|
|
551
|
+
providerPromise = (async () => {
|
|
552
|
+
const embedder = await getGeminiEmbedder();
|
|
553
|
+
return createVectorProvider({
|
|
554
|
+
name: "gemini",
|
|
555
|
+
model: ragConfig.geminiModel,
|
|
556
|
+
embedder,
|
|
557
|
+
batchSize: Math.max(1, ragConfig.geminiBatchSize)
|
|
558
|
+
});
|
|
559
|
+
})();
|
|
560
|
+
} else {
|
|
561
|
+
providerPromise = Promise.reject(new Error(`Unknown search provider: ${name}`));
|
|
562
|
+
}
|
|
563
|
+
if (!ragLogState.providerReady.has(name)) {
|
|
564
|
+
ragLogState.providerReady.add(name);
|
|
565
|
+
logRag("provider_ready", {
|
|
566
|
+
profile: ragConfig.profile,
|
|
567
|
+
provider: name,
|
|
568
|
+
fallback: ragConfig.fallback
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
providerCache.set(name, providerPromise);
|
|
572
|
+
return providerPromise;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
resolveProviderChain: () => resolveProviderChain(ragConfig),
|
|
577
|
+
loadSearchProvider,
|
|
578
|
+
refreshProviders
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export {
|
|
583
|
+
createProviderOrchestrator,
|
|
584
|
+
resolveProviderChain
|
|
585
|
+
};
|