@customize-agent/knowledge 4.0.1 → 4.0.2
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/chunking/bge-tokenizer.d.ts +10 -0
- package/dist/chunking/bge-tokenizer.js +68 -0
- package/dist/chunking/text-chunker.d.ts +10 -0
- package/dist/chunking/text-chunker.js +158 -53
- package/dist/classification/classifier.js +0 -1
- package/dist/core/index-state-store.d.ts +47 -0
- package/dist/core/index-state-store.js +184 -50
- package/dist/core/knowledge-base-manager.d.ts +24 -2
- package/dist/core/knowledge-base-manager.js +195 -51
- package/dist/core/multi-project-manager.d.ts +2 -0
- package/dist/core/multi-project-manager.js +13 -2
- package/dist/embedding/embedding-provider.d.ts +22 -0
- package/dist/embedding/embedding-provider.js +116 -2
- package/dist/extraction/content-extractor.d.ts +30 -2
- package/dist/extraction/content-extractor.js +514 -115
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/search/federation-search.d.ts +1 -0
- package/dist/search/federation-search.js +1 -0
- package/dist/vector/hnsw-vector-store.d.ts +20 -0
- package/dist/vector/hnsw-vector-store.js +107 -0
- package/dist/vector/types.d.ts +2 -0
- package/dist/vector/vector-indexer.d.ts +2 -0
- package/dist/vector/vector-indexer.js +13 -1
- package/models/bge-small-zh-v1.5/config.json +31 -0
- package/models/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
- package/models/bge-small-zh-v1.5/special_tokens_map.json +7 -0
- package/models/bge-small-zh-v1.5/tokenizer.json +21278 -0
- package/models/bge-small-zh-v1.5/tokenizer_config.json +15 -0
- package/package.json +11 -4
- package/scripts/install-hnsw.cjs +47 -0
- package/dist/vector/sqlite-vec-store.d.ts +0 -38
- package/dist/vector/sqlite-vec-store.js +0 -203
|
@@ -10,7 +10,7 @@ import { createEmbeddingProviderFromEnvironment } from '../embedding/embedding-p
|
|
|
10
10
|
import { ContentExtractor } from '../extraction/content-extractor.js';
|
|
11
11
|
import { FederationSearch } from '../search/federation-search.js';
|
|
12
12
|
import { CollectionManager } from '../vector/collection-manager.js';
|
|
13
|
-
import {
|
|
13
|
+
import { HNSWVectorStore } from '../vector/hnsw-vector-store.js';
|
|
14
14
|
import { VectorIndexer } from '../vector/vector-indexer.js';
|
|
15
15
|
import { ChangeTracker } from './change-tracker.js';
|
|
16
16
|
import { KnowledgeFileScanner } from './file-scanner.js';
|
|
@@ -22,7 +22,7 @@ export class KnowledgeBaseManager {
|
|
|
22
22
|
projectId;
|
|
23
23
|
kbPath;
|
|
24
24
|
store;
|
|
25
|
-
|
|
25
|
+
vectorRoot;
|
|
26
26
|
classifier = new FileClassifier();
|
|
27
27
|
scanner = new KnowledgeFileScanner();
|
|
28
28
|
collections = new CollectionManager();
|
|
@@ -61,7 +61,7 @@ export class KnowledgeBaseManager {
|
|
|
61
61
|
dbPath = path.join(storageRoot, 'projects', options.projectId, 'kb.db');
|
|
62
62
|
}
|
|
63
63
|
this.store = new IndexStateStore(dbPath);
|
|
64
|
-
this.
|
|
64
|
+
this.vectorRoot = path.join(path.dirname(dbPath), 'hnsw');
|
|
65
65
|
}
|
|
66
66
|
initialize() {
|
|
67
67
|
if (this.scope === 'project' && this.projectRoot) {
|
|
@@ -76,6 +76,22 @@ export class KnowledgeBaseManager {
|
|
|
76
76
|
fs.mkdirSync(this.kbPath, { recursive: true });
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
async forceReindexAll(options = {}) {
|
|
80
|
+
this.initialize();
|
|
81
|
+
const records = this.store.listRecords();
|
|
82
|
+
for (const record of records) {
|
|
83
|
+
await this.deleteVectorFile(record.collectionName, record.relativePath);
|
|
84
|
+
this.store.deleteRecord(record.relativePath);
|
|
85
|
+
}
|
|
86
|
+
return this.incrementalIndex(options);
|
|
87
|
+
}
|
|
88
|
+
async consumePendingIndexJobs(options = {}) {
|
|
89
|
+
this.initialize();
|
|
90
|
+
const jobs = this.store.listPendingIndexJobs(options.limit ?? 50);
|
|
91
|
+
if (jobs.length === 0)
|
|
92
|
+
return this.incrementalIndex(options);
|
|
93
|
+
return this.incrementalIndex({ ...options, onlyRelativePaths: jobs.map(job => job.relativePath) });
|
|
94
|
+
}
|
|
79
95
|
async incrementalIndex(options = {}) {
|
|
80
96
|
this.initialize();
|
|
81
97
|
const previousOnProgress = this.onProgress;
|
|
@@ -88,9 +104,22 @@ export class KnowledgeBaseManager {
|
|
|
88
104
|
const diskFiles = await this.scanner.scan(this.kbPath, [...kbIgnore, ...configIgnore]);
|
|
89
105
|
const tracker = new ChangeTracker(this.store);
|
|
90
106
|
const diff = await tracker.computeDiff(diskFiles, this.classifier, this.kbPath);
|
|
107
|
+
const onlyRelativePaths = options.onlyRelativePaths ? new Set(options.onlyRelativePaths) : undefined;
|
|
108
|
+
if (onlyRelativePaths) {
|
|
109
|
+
diff.newFiles = diff.newFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
110
|
+
diff.modifiedFiles = diff.modifiedFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
111
|
+
diff.deletedFiles = diff.deletedFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
112
|
+
diff.hasChanges = diff.newFiles.length + diff.modifiedFiles.length + diff.deletedFiles.length > 0;
|
|
113
|
+
for (const relativePath of onlyRelativePaths) {
|
|
114
|
+
const exists = diff.newFiles.some(file => file.relativePath === relativePath) || diff.modifiedFiles.some(file => file.relativePath === relativePath) || diff.deletedFiles.some(file => file.relativePath === relativePath);
|
|
115
|
+
if (!exists)
|
|
116
|
+
this.updateJobsForFile(relativePath, 'ERROR', 100, '待索引文件不存在或未发生变化', '待索引文件不存在或未发生变化');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
91
119
|
for (const deleted of diff.deletedFiles) {
|
|
92
120
|
await this.deleteVectorFile(deleted.collectionName, deleted.relativePath);
|
|
93
121
|
this.store.deleteRecord(deleted.relativePath);
|
|
122
|
+
this.updateJobsForFile(deleted.relativePath, 'SUCCESS', 100, '文件已删除,索引记录和向量已清理');
|
|
94
123
|
}
|
|
95
124
|
const now = Date.now();
|
|
96
125
|
const indexedBefore = [...this.store.loadActiveRecords().values()];
|
|
@@ -102,12 +131,14 @@ export class KnowledgeBaseManager {
|
|
|
102
131
|
? this.collections.getCollectionName('global', file.category)
|
|
103
132
|
: this.collections.getCollectionName('project', file.category, this.projectId);
|
|
104
133
|
const basePercent = filesToIndex.length === 0 ? 40 : 20 + Math.round((index / filesToIndex.length) * 45);
|
|
134
|
+
this.updateJobsForFile(file.relativePath, 'PARSING', basePercent, `正在解析 ${file.relativePath}`);
|
|
105
135
|
this.reportProgress({ stage: 'parsing', percent: basePercent, message: `正在解析 ${file.relativePath}`, filePath: file.relativePath });
|
|
106
136
|
const extraction = await this.extractor.extract(file);
|
|
107
137
|
extraction.metadata.textLength = extraction.text.length;
|
|
108
138
|
if (!this.hasUsableContent(extraction.text, extraction.metadata)) {
|
|
109
139
|
const reason = extraction.warnings[0] ?? '未解析出可用于模型的正文内容,已跳过向量化';
|
|
110
140
|
diff.skippedFiles.push({ file, reason });
|
|
141
|
+
this.updateJobsForFile(file.relativePath, 'ERROR', 100, reason, reason);
|
|
111
142
|
this.store.upsertRecord({
|
|
112
143
|
relativePath: file.relativePath,
|
|
113
144
|
category: file.category,
|
|
@@ -129,6 +160,7 @@ export class KnowledgeBaseManager {
|
|
|
129
160
|
const normalizedDuplicate = !duplicate && normalizedHash
|
|
130
161
|
? this.store.findNormalizedDuplicate(normalizedHash, file.relativePath)
|
|
131
162
|
: undefined;
|
|
163
|
+
this.updateJobsForFile(file.relativePath, 'CHUNKING', Math.min(80, basePercent + 10), `正在切片 ${file.relativePath}`);
|
|
132
164
|
this.reportProgress({ stage: 'chunking', percent: Math.min(80, basePercent + 10), message: `正在切片 ${file.relativePath}`, filePath: file.relativePath });
|
|
133
165
|
const chunks = this.chunker.chunk(extraction.text, file, extraction.metadata);
|
|
134
166
|
this.reportProgress({ stage: 'chunking', percent: Math.min(84, basePercent + 14), message: `切片完成:${chunks.length} 块`, filePath: file.relativePath, chunkCount: chunks.length });
|
|
@@ -212,20 +244,31 @@ export class KnowledgeBaseManager {
|
|
|
212
244
|
format: file.format,
|
|
213
245
|
collectionName,
|
|
214
246
|
});
|
|
247
|
+
this.updateJobsForFile(file.relativePath, options.vectorMode === 'defer' ? 'SUCCESS' : 'INDEXING', options.vectorMode === 'defer' ? 100 : 85, options.vectorMode === 'defer' ? '解析和切片已完成' : '等待向量入库');
|
|
215
248
|
}
|
|
216
249
|
const stats = this.getStats();
|
|
217
250
|
this.store.setMetadata('last_incremental_index_at', String(now));
|
|
218
251
|
this.store.setMetadata('total_chunks', String(stats.chunkCount));
|
|
219
252
|
this.store.setMetadata('total_files_indexed', String(stats.fileCount));
|
|
253
|
+
const hasIndexChanges = diff.newFiles.length + diff.modifiedFiles.length + diff.deletedFiles.length > 0;
|
|
220
254
|
if (options.vectorMode === 'defer') {
|
|
255
|
+
if (hasIndexChanges)
|
|
256
|
+
this.store.setMetadata('vector_index_status', 'pending');
|
|
221
257
|
this.reportProgress({ stage: 'vectorizing', percent: 85, message: '解析和切片已完成,向量入库转入后台/稍后执行', chunkCount: stats.chunkCount, vectorStatus: this.getVectorStatus() });
|
|
222
|
-
void this.ensureVectorIndexFresh(stats.chunkCount, diff.newFiles.length + diff.modifiedFiles.length + diff.deletedFiles.length > 0).catch(() => undefined);
|
|
223
258
|
}
|
|
224
259
|
else {
|
|
225
|
-
await this.ensureVectorIndexFresh(stats.chunkCount,
|
|
260
|
+
await this.ensureVectorIndexFresh(stats.chunkCount, hasIndexChanges);
|
|
226
261
|
}
|
|
227
262
|
this.lastSkippedFiles = diff.skippedFiles;
|
|
228
263
|
const vectorStatus = this.getVectorStatus();
|
|
264
|
+
if (options.vectorMode !== 'defer') {
|
|
265
|
+
for (const file of filesToIndex) {
|
|
266
|
+
if (vectorStatus.status === 'error')
|
|
267
|
+
this.updateJobsForFile(file.relativePath, 'ERROR', 100, 'HNSWLib 向量入库失败', vectorStatus.error);
|
|
268
|
+
else
|
|
269
|
+
this.updateJobsForFile(file.relativePath, 'SUCCESS', 100, '解析、切片和向量索引完成');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
229
272
|
const vectorDeferred = options.vectorMode === 'defer';
|
|
230
273
|
this.reportProgress({
|
|
231
274
|
stage: vectorDeferred || vectorStatus.status !== 'error' ? 'done' : 'error',
|
|
@@ -233,7 +276,7 @@ export class KnowledgeBaseManager {
|
|
|
233
276
|
message: vectorDeferred
|
|
234
277
|
? '解析、切片和 SQLite 入库已完成,向量入库后台执行'
|
|
235
278
|
: vectorStatus.status === 'error'
|
|
236
|
-
? '解析和切片已完成,
|
|
279
|
+
? '解析和切片已完成,HNSWLib 向量待入库'
|
|
237
280
|
: '知识库索引完成',
|
|
238
281
|
chunkCount: stats.chunkCount,
|
|
239
282
|
vectorStatus,
|
|
@@ -264,8 +307,10 @@ export class KnowledgeBaseManager {
|
|
|
264
307
|
}
|
|
265
308
|
const parentChunks = item.parentId ? this.store.getChunksByParent(item.filePath, item.parentId, 6) : [];
|
|
266
309
|
const chunks = parentChunks.length > 0 ? parentChunks : this.store.getContextChunks(item.filePath, chunkIndex, 1);
|
|
267
|
-
if (chunks.length === 0)
|
|
268
|
-
|
|
310
|
+
if (chunks.length === 0) {
|
|
311
|
+
const document = this.store.getDocumentChunk(item.filePath);
|
|
312
|
+
return document ? { ...item, content: document.content, sectionTitle: item.sectionTitle ?? 'Document Parent' } : item;
|
|
313
|
+
}
|
|
269
314
|
return {
|
|
270
315
|
...item,
|
|
271
316
|
content: chunks.map(chunk => chunk.content).join('\n\n---\n\n'),
|
|
@@ -279,15 +324,19 @@ export class KnowledgeBaseManager {
|
|
|
279
324
|
const start = Date.now();
|
|
280
325
|
const weights = this.retrievalWeights(options.weights);
|
|
281
326
|
const rewrittenQueries = await this.rewriteQueries(query);
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
327
|
+
const rankedLists = [];
|
|
328
|
+
for (const [queryIndex, rewritten] of rewrittenQueries.entries()) {
|
|
329
|
+
rankedLists.push({ source: 'keyword', items: this.keywordSearchItems(rewritten, limit * 3), queryIndex });
|
|
330
|
+
if (queryIndex < 3) {
|
|
331
|
+
try {
|
|
332
|
+
rankedLists.push({ source: 'vector', items: (await this.semanticSearch(rewritten, { ...options, limit: limit * 3 })).results, queryIndex });
|
|
333
|
+
}
|
|
334
|
+
catch { /* vector search is optional in hybrid search */ }
|
|
287
335
|
}
|
|
288
|
-
catch { /* vector search is optional in hybrid search */ }
|
|
289
336
|
}
|
|
290
|
-
const
|
|
337
|
+
const keywordItems = rankedLists.filter(list => list.source === 'keyword').flatMap(list => list.items);
|
|
338
|
+
const vectorItems = rankedLists.filter(list => list.source === 'vector').flatMap(list => list.items);
|
|
339
|
+
const merged = this.mergeContexts(this.mergeHybridRankedLists(rankedLists, limit * 4, weights).map(item => this.expandContext(item)), limit * 2);
|
|
291
340
|
const useLLMRerank = !!this.llmProvider;
|
|
292
341
|
const preReranked = useLLMRerank ? merged : this.heuristicRerank(query, merged);
|
|
293
342
|
const reranked = useLLMRerank ? (await this.llmRerank(query, preReranked)) : preReranked;
|
|
@@ -310,7 +359,7 @@ export class KnowledgeBaseManager {
|
|
|
310
359
|
const queryEmbedding = await this.embeddingProvider.embedQuery(query);
|
|
311
360
|
const search = new FederationSearch(this.vectorStores);
|
|
312
361
|
try {
|
|
313
|
-
|
|
362
|
+
const result = await search.search({
|
|
314
363
|
query,
|
|
315
364
|
queryEmbedding,
|
|
316
365
|
topK: options.limit ?? 10,
|
|
@@ -319,6 +368,7 @@ export class KnowledgeBaseManager {
|
|
|
319
368
|
collections: options.collections,
|
|
320
369
|
filters: options.filters,
|
|
321
370
|
});
|
|
371
|
+
return { ...result, results: this.hydrateVectorResultsFromSqlite(result.results) };
|
|
322
372
|
}
|
|
323
373
|
catch {
|
|
324
374
|
return { results: [], scopesSearched: this.scope === 'global' ? ['global'] : ['project'], queryTimeMs: 0 };
|
|
@@ -372,22 +422,25 @@ export class KnowledgeBaseManager {
|
|
|
372
422
|
async uploadFile(fileName, content, targetRelativePath, onProgress, options = {}) {
|
|
373
423
|
return this.uploadFiles([{ fileName, content, targetRelativePath }], onProgress, options);
|
|
374
424
|
}
|
|
375
|
-
async
|
|
425
|
+
async stageUploadedFiles(files, operationId = `upload-${Date.now()}`) {
|
|
376
426
|
this.initialize();
|
|
377
|
-
const
|
|
378
|
-
for (
|
|
427
|
+
const jobs = [];
|
|
428
|
+
for (let index = 0; index < files.length; index++) {
|
|
429
|
+
const file = files[index];
|
|
379
430
|
const relativePath = this.getUploadRelativePath(file.fileName, file.targetRelativePath);
|
|
380
431
|
const targetPath = this.resolveKbRelativePath(relativePath);
|
|
381
432
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
382
433
|
fs.writeFileSync(targetPath, file.content);
|
|
383
|
-
uploadedPaths.push(relativePath);
|
|
384
|
-
}
|
|
385
|
-
for (const relativePath of uploadedPaths) {
|
|
386
434
|
const record = this.store.listRecords().find(item => item.relativePath === relativePath);
|
|
387
435
|
if (record)
|
|
388
436
|
await this.deleteVectorFile(record.collectionName, relativePath);
|
|
389
437
|
this.store.deleteRecord(relativePath);
|
|
438
|
+
jobs.push(this.store.enqueueIndexJob({ id: `${operationId}-${index}`, relativePath, message: '文件已落盘,等待后台解析' }));
|
|
390
439
|
}
|
|
440
|
+
return jobs;
|
|
441
|
+
}
|
|
442
|
+
async uploadFiles(files, onProgress, options = {}) {
|
|
443
|
+
await this.stageUploadedFiles(files);
|
|
391
444
|
return this.incrementalIndex({ onProgress, vectorMode: options.vectorMode });
|
|
392
445
|
}
|
|
393
446
|
listFailedFiles() {
|
|
@@ -398,12 +451,17 @@ export class KnowledgeBaseManager {
|
|
|
398
451
|
const targetPath = this.resolveKbRelativePath(normalized);
|
|
399
452
|
if (fs.existsSync(targetPath))
|
|
400
453
|
fs.unlinkSync(targetPath);
|
|
401
|
-
// 同步删除
|
|
454
|
+
// 同步删除 HNSWLib 向量数据,避免孤儿向量污染搜索结果
|
|
402
455
|
const record = this.store.listRecords().find(r => r.relativePath === normalized);
|
|
403
456
|
if (record) {
|
|
404
457
|
await this.deleteVectorFile(record.collectionName, normalized);
|
|
405
458
|
}
|
|
406
459
|
this.store.deleteRecord(normalized);
|
|
460
|
+
const stats = this.getStats();
|
|
461
|
+
this.store.setMetadata('total_chunks', String(stats.chunkCount));
|
|
462
|
+
this.store.setMetadata('total_files_indexed', String(stats.fileCount));
|
|
463
|
+
this.store.setMetadata('vector_indexed_chunks', String(stats.chunkCount));
|
|
464
|
+
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
407
465
|
}
|
|
408
466
|
tagFile(relativePath, tags) {
|
|
409
467
|
this.store.setTags(this.normalizeRelativePath(relativePath), tags);
|
|
@@ -425,18 +483,31 @@ export class KnowledgeBaseManager {
|
|
|
425
483
|
}
|
|
426
484
|
async indexVectors(options = {}) {
|
|
427
485
|
const chunks = this.store.listChunks(options);
|
|
428
|
-
|
|
486
|
+
const collectionNames = new Set(chunks.map(chunk => chunk.collectionName));
|
|
487
|
+
for (const collectionName of collectionNames)
|
|
429
488
|
this.ensureVectorStore(collectionName);
|
|
430
|
-
|
|
489
|
+
if (!options.relativePath) {
|
|
490
|
+
for (const collectionName of collectionNames)
|
|
491
|
+
await this.vectorStores.get(collectionName)?.clearCollection?.();
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
for (const collectionName of collectionNames)
|
|
495
|
+
await this.vectorStores.get(collectionName)?.deleteByFilePath(options.relativePath);
|
|
496
|
+
}
|
|
497
|
+
this.reportProgress({ stage: 'vectorizing', percent: 85, message: `正在写入 HNSWLib 向量库,共 ${chunks.length} 个切片`, chunkCount: chunks.length });
|
|
431
498
|
const indexer = new VectorIndexer(this.embeddingProvider, this.vectorStores);
|
|
432
499
|
try {
|
|
433
500
|
const results = await indexer.indexChunks(chunks);
|
|
434
|
-
|
|
435
|
-
|
|
501
|
+
const actualModel = results[0]?.embeddingModel ?? this.embeddingProvider.model;
|
|
502
|
+
const actualDimension = results[0]?.embeddingDimension ?? this.embeddingProvider.dimensions;
|
|
503
|
+
this.store.setMetadata('embedding_model', actualModel);
|
|
504
|
+
this.store.setMetadata('embedding_dimension', String(actualDimension));
|
|
436
505
|
this.store.setMetadata('vector_indexed_chunks', String(chunks.length));
|
|
437
506
|
this.store.setMetadata('vector_index_status', 'ready');
|
|
438
507
|
this.store.setMetadata('vector_index_error', '');
|
|
439
508
|
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
509
|
+
if (options.relativePath)
|
|
510
|
+
this.updateJobsForFile(options.relativePath, 'SUCCESS', 100, '解析、切片和向量索引完成');
|
|
440
511
|
return results;
|
|
441
512
|
}
|
|
442
513
|
catch (error) {
|
|
@@ -444,7 +515,9 @@ export class KnowledgeBaseManager {
|
|
|
444
515
|
this.store.setMetadata('vector_index_status', 'error');
|
|
445
516
|
this.store.setMetadata('vector_index_error', message);
|
|
446
517
|
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
447
|
-
this.reportProgress({ stage: 'error', percent: 85, message: '
|
|
518
|
+
this.reportProgress({ stage: 'error', percent: 85, message: 'HNSWLib 向量入库失败', chunkCount: chunks.length, vectorStatus: this.getVectorStatus() });
|
|
519
|
+
if (options.relativePath)
|
|
520
|
+
this.updateJobsForFile(options.relativePath, 'ERROR', 100, 'HNSWLib 向量入库失败', message);
|
|
448
521
|
return [];
|
|
449
522
|
}
|
|
450
523
|
}
|
|
@@ -462,13 +535,23 @@ export class KnowledgeBaseManager {
|
|
|
462
535
|
lastIndexedAt: stats.lastIndexedAt,
|
|
463
536
|
};
|
|
464
537
|
}
|
|
538
|
+
listIndexJobsByPrefix(prefix) {
|
|
539
|
+
return this.store.listIndexJobsByPrefix(prefix);
|
|
540
|
+
}
|
|
541
|
+
countPendingIndexJobs() {
|
|
542
|
+
return this.store.countPendingIndexJobs();
|
|
543
|
+
}
|
|
544
|
+
failPendingIndexJobs(message) {
|
|
545
|
+
for (const job of this.store.listPendingIndexJobs(1_000))
|
|
546
|
+
this.store.updateIndexJob(job.id, { status: 'ERROR', percent: 100, message, errorMessage: message });
|
|
547
|
+
}
|
|
465
548
|
getVectorStatus() {
|
|
466
549
|
return {
|
|
467
550
|
status: this.store.getMetadata('vector_index_status') ?? 'pending',
|
|
468
551
|
error: this.store.getMetadata('vector_index_error') || undefined,
|
|
469
552
|
indexedChunks: Number(this.store.getMetadata('vector_indexed_chunks') ?? 0),
|
|
470
553
|
lastIndexedAt: Number(this.store.getMetadata('last_vector_index_at') ?? 0),
|
|
471
|
-
backend: `SQLite +
|
|
554
|
+
backend: `SQLite + HNSWLib (${this.vectorRoot})`,
|
|
472
555
|
};
|
|
473
556
|
}
|
|
474
557
|
async rewriteQueries(query) {
|
|
@@ -599,10 +682,22 @@ ${resultsText}
|
|
|
599
682
|
return this.heuristicRerank(query, items);
|
|
600
683
|
}
|
|
601
684
|
}
|
|
685
|
+
hydrateVectorResultsFromSqlite(items) {
|
|
686
|
+
return items.map(item => {
|
|
687
|
+
if (!item.rowid)
|
|
688
|
+
return item;
|
|
689
|
+
const chunk = this.store.getChunkByRowid(item.rowid);
|
|
690
|
+
if (!chunk)
|
|
691
|
+
return item;
|
|
692
|
+
const hydrated = this.toFederatedItem({ ...chunk, score: item.score, scoreDetails: item.scoreDetails }, 'vector');
|
|
693
|
+
return { ...hydrated, score: item.score, scoreDetails: item.scoreDetails };
|
|
694
|
+
});
|
|
695
|
+
}
|
|
602
696
|
toFederatedItem(result, source) {
|
|
603
697
|
const metadata = this.parseMetadata(result.metadataJson);
|
|
604
698
|
return {
|
|
605
699
|
id: result.id,
|
|
700
|
+
rowid: result.rowid,
|
|
606
701
|
content: result.content,
|
|
607
702
|
filePath: result.relativePath,
|
|
608
703
|
scope: this.scope === 'global' ? 'global' : 'project',
|
|
@@ -618,32 +713,70 @@ ${resultsText}
|
|
|
618
713
|
facets: this.metadataFacets(metadata),
|
|
619
714
|
};
|
|
620
715
|
}
|
|
621
|
-
|
|
716
|
+
mergeHybridRankedLists(lists, limit, weights = this.retrievalWeights()) {
|
|
717
|
+
const k = Number(process.env.KB_RETRIEVAL_RRF_K ?? 60);
|
|
718
|
+
const byKey = new Map();
|
|
719
|
+
for (const list of lists) {
|
|
720
|
+
const sourceWeight = list.source === 'vector' ? (weights.vector ?? 0.9) : (weights.keyword ?? 1);
|
|
721
|
+
const rewriteWeight = list.queryIndex === 0 ? 1 : (weights.rewrite ?? 0.72);
|
|
722
|
+
list.items.forEach((item, index) => {
|
|
723
|
+
const key = this.contextKey(item);
|
|
724
|
+
const rankScore = sourceWeight * rewriteWeight / (k + index + 1);
|
|
725
|
+
const existing = byKey.get(key);
|
|
726
|
+
if (!existing) {
|
|
727
|
+
byKey.set(key, {
|
|
728
|
+
...item,
|
|
729
|
+
score: rankScore,
|
|
730
|
+
source: list.source,
|
|
731
|
+
scoreDetails: { ...item.scoreDetails, hybridScore: rankScore },
|
|
732
|
+
seenSources: new Set([list.source]),
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
existing.score += rankScore;
|
|
737
|
+
existing.seenSources?.add(list.source);
|
|
738
|
+
existing.source = existing.seenSources && existing.seenSources.size > 1 ? 'hybrid' : existing.source;
|
|
739
|
+
existing.scoreDetails = { ...existing.scoreDetails, ...item.scoreDetails, hybridScore: existing.score };
|
|
740
|
+
if (item.score > (existing.scoreDetails?.keywordScore ?? existing.scoreDetails?.vectorScore ?? 0)) {
|
|
741
|
+
existing.content = item.content;
|
|
742
|
+
existing.sectionTitle = item.sectionTitle ?? existing.sectionTitle;
|
|
743
|
+
existing.chunkIndex = item.chunkIndex ?? existing.chunkIndex;
|
|
744
|
+
existing.parentId = item.parentId ?? existing.parentId;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
const hybridBonus = weights.hybridBonus ?? 0.35;
|
|
749
|
+
return [...byKey.values()].map(item => {
|
|
750
|
+
const bonus = item.seenSources && item.seenSources.size > 1 ? item.score * hybridBonus : 0;
|
|
751
|
+
const { seenSources: _seenSources, ...result } = item;
|
|
752
|
+
return { ...result, score: result.score + bonus, scoreDetails: { ...result.scoreDetails, hybridScore: result.score + bonus } };
|
|
753
|
+
}).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
754
|
+
}
|
|
755
|
+
mergeContexts(items, limit) {
|
|
622
756
|
const byKey = new Map();
|
|
623
757
|
for (const item of items) {
|
|
624
|
-
const key =
|
|
625
|
-
const sourceWeight = item.source === 'vector' ? (weights.vector ?? 0.9) : item.source === 'keyword' ? (weights.keyword ?? 1) : 1.1;
|
|
626
|
-
const weighted = { ...item, score: item.score * sourceWeight, scoreDetails: { ...item.scoreDetails, hybridScore: item.score * sourceWeight } };
|
|
758
|
+
const key = this.contextKey(item);
|
|
627
759
|
const existing = byKey.get(key);
|
|
628
760
|
if (!existing) {
|
|
629
|
-
byKey.set(key,
|
|
630
|
-
|
|
631
|
-
else {
|
|
632
|
-
const score = Math.max(existing.score, weighted.score) + Math.min(existing.score, weighted.score) * (weights.hybridBonus ?? 0.35);
|
|
633
|
-
byKey.set(key, {
|
|
634
|
-
...existing,
|
|
635
|
-
score,
|
|
636
|
-
source: existing.source === weighted.source ? existing.source : 'hybrid',
|
|
637
|
-
scoreDetails: {
|
|
638
|
-
...existing.scoreDetails,
|
|
639
|
-
...weighted.scoreDetails,
|
|
640
|
-
hybridScore: score,
|
|
641
|
-
},
|
|
642
|
-
});
|
|
761
|
+
byKey.set(key, item);
|
|
762
|
+
continue;
|
|
643
763
|
}
|
|
764
|
+
const score = Math.max(existing.score, item.score);
|
|
765
|
+
byKey.set(key, {
|
|
766
|
+
...existing,
|
|
767
|
+
score,
|
|
768
|
+
source: existing.source === item.source ? existing.source : 'hybrid',
|
|
769
|
+
content: existing.content.length >= item.content.length ? existing.content : item.content,
|
|
770
|
+
scoreDetails: { ...existing.scoreDetails, ...item.scoreDetails, hybridScore: score },
|
|
771
|
+
});
|
|
644
772
|
}
|
|
645
773
|
return [...byKey.values()].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
646
774
|
}
|
|
775
|
+
contextKey(item) {
|
|
776
|
+
if (item.rowid)
|
|
777
|
+
return `${item.scope}:rowid:${item.rowid}`;
|
|
778
|
+
return `${item.scope}:${item.filePath}:${item.parentId ?? item.chunkIndex ?? item.id}`;
|
|
779
|
+
}
|
|
647
780
|
parseChunkIndex(id) {
|
|
648
781
|
const match = /#(\d+)$/u.exec(id);
|
|
649
782
|
return match ? Number(match[1]) : 0;
|
|
@@ -688,10 +821,16 @@ ${resultsText}
|
|
|
688
821
|
reportProgress(progress) {
|
|
689
822
|
this.onProgress?.(progress);
|
|
690
823
|
}
|
|
824
|
+
updateJobsForFile(relativePath, status, percent, message, errorMessage) {
|
|
825
|
+
for (const job of this.store.listActiveIndexJobsByPath(relativePath)) {
|
|
826
|
+
this.store.updateIndexJob(job.id, { status, percent, message, errorMessage });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
691
829
|
ensureVectorStore(collectionName) {
|
|
692
830
|
if (this.vectorStores.has(collectionName))
|
|
693
831
|
return;
|
|
694
|
-
|
|
832
|
+
const safeName = collectionName.replace(/[^a-zA-Z0-9_.-]/gu, '_');
|
|
833
|
+
this.vectorStores.set(collectionName, new HNSWVectorStore(collectionName, path.join(this.vectorRoot, `${safeName}.hnsw`), this.embeddingProvider.dimensions));
|
|
695
834
|
}
|
|
696
835
|
async deleteVectorFile(collectionName, relativePath) {
|
|
697
836
|
this.ensureVectorStore(collectionName);
|
|
@@ -706,10 +845,15 @@ ${resultsText}
|
|
|
706
845
|
async ensureVectorIndexFresh(chunkCount, force = false) {
|
|
707
846
|
if (chunkCount === 0)
|
|
708
847
|
return;
|
|
848
|
+
for (const record of this.store.listRecords())
|
|
849
|
+
this.ensureVectorStore(record.collectionName);
|
|
850
|
+
if ([...this.vectorStores.values()].some(store => store.needsRebuild?.())) {
|
|
851
|
+
await this.indexVectors();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
709
854
|
const indexedChunks = Number(this.store.getMetadata('vector_indexed_chunks') ?? 0);
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
if (!force && indexedChunks === chunkCount && model === this.embeddingProvider.model && dimension === String(this.embeddingProvider.dimensions))
|
|
855
|
+
const status = this.store.getMetadata('vector_index_status');
|
|
856
|
+
if (!force && indexedChunks === chunkCount && status === 'ready')
|
|
713
857
|
return;
|
|
714
858
|
await this.indexVectors();
|
|
715
859
|
}
|
|
@@ -8,6 +8,7 @@ export declare class MultiProjectManager {
|
|
|
8
8
|
private readonly registry;
|
|
9
9
|
private readonly configManager;
|
|
10
10
|
private readonly projects;
|
|
11
|
+
private readonly lastSearchIndexCheck;
|
|
11
12
|
private globalKB?;
|
|
12
13
|
constructor(storageRoot?: string, llmProvider?: LLMSearchProvider);
|
|
13
14
|
getProject(projectRoot: string): Promise<KnowledgeBaseManager>;
|
|
@@ -28,6 +29,7 @@ export declare class MultiProjectManager {
|
|
|
28
29
|
forgetProject(projectId: string): Promise<void>;
|
|
29
30
|
closeProject(projectId: string): Promise<void>;
|
|
30
31
|
shutdown(): Promise<void>;
|
|
32
|
+
private ensureFreshForSearch;
|
|
31
33
|
private mergeDebug;
|
|
32
34
|
private updateRegistry;
|
|
33
35
|
}
|
|
@@ -12,6 +12,7 @@ export class MultiProjectManager {
|
|
|
12
12
|
registry;
|
|
13
13
|
configManager;
|
|
14
14
|
projects = new Map();
|
|
15
|
+
lastSearchIndexCheck = new Map();
|
|
15
16
|
globalKB;
|
|
16
17
|
constructor(storageRoot = path.join(os.homedir(), USER_DATA_DIR), llmProvider) {
|
|
17
18
|
this.storageRoot = storageRoot;
|
|
@@ -55,7 +56,7 @@ export class MultiProjectManager {
|
|
|
55
56
|
const limit = options.limit ?? 10;
|
|
56
57
|
const scope = options.scope ?? 'all';
|
|
57
58
|
const project = await this.getProject(projectRoot);
|
|
58
|
-
await
|
|
59
|
+
await this.ensureFreshForSearch(projectRoot, project);
|
|
59
60
|
if (scope === 'project')
|
|
60
61
|
return project.hybridSearch(query, { limit, weights: options.weights });
|
|
61
62
|
const projectResults = scope === 'all'
|
|
@@ -76,7 +77,7 @@ export class MultiProjectManager {
|
|
|
76
77
|
async semanticSearch(projectRoot, query, options = {}) {
|
|
77
78
|
const scope = options.scope ?? 'all';
|
|
78
79
|
const project = await this.getProject(projectRoot);
|
|
79
|
-
await
|
|
80
|
+
await this.ensureFreshForSearch(projectRoot, project);
|
|
80
81
|
if (scope === 'project') {
|
|
81
82
|
return project.semanticSearch(query, options);
|
|
82
83
|
}
|
|
@@ -141,6 +142,16 @@ export class MultiProjectManager {
|
|
|
141
142
|
this.globalKB = undefined;
|
|
142
143
|
this.registry.close();
|
|
143
144
|
}
|
|
145
|
+
async ensureFreshForSearch(projectRoot, project) {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
const key = path.resolve(projectRoot);
|
|
148
|
+
const ttlMs = Number(process.env.KB_SEARCH_INDEX_TTL_MS ?? 30000);
|
|
149
|
+
const last = this.lastSearchIndexCheck.get(key) ?? 0;
|
|
150
|
+
if (now - last < ttlMs)
|
|
151
|
+
return;
|
|
152
|
+
this.lastSearchIndexCheck.set(key, now);
|
|
153
|
+
await project.incrementalIndex({ vectorMode: 'defer' });
|
|
154
|
+
}
|
|
144
155
|
mergeDebug(projectDebug, globalDebug) {
|
|
145
156
|
if (!projectDebug && !globalDebug)
|
|
146
157
|
return undefined;
|
|
@@ -30,4 +30,26 @@ export declare class OpenAICompatibleEmbeddingProvider implements EmbeddingProvi
|
|
|
30
30
|
embedQuery(text: string): Promise<number[]>;
|
|
31
31
|
private embed;
|
|
32
32
|
}
|
|
33
|
+
export interface LocalTransformersEmbeddingOptions {
|
|
34
|
+
model?: string;
|
|
35
|
+
dimensions?: number;
|
|
36
|
+
modelPath?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare class LocalTransformersEmbeddingProvider implements EmbeddingProvider {
|
|
39
|
+
readonly model: string;
|
|
40
|
+
readonly dimensions: number;
|
|
41
|
+
private readonly modelPath?;
|
|
42
|
+
private static pipelines;
|
|
43
|
+
constructor(options?: LocalTransformersEmbeddingOptions);
|
|
44
|
+
embedDocuments(texts: string[]): Promise<number[][]>;
|
|
45
|
+
embedQuery(text: string): Promise<number[]>;
|
|
46
|
+
private embed;
|
|
47
|
+
private getPipeline;
|
|
48
|
+
private createPipeline;
|
|
49
|
+
private parseVectors;
|
|
50
|
+
private isTensorLike;
|
|
51
|
+
private splitFlatVectors;
|
|
52
|
+
private resizeVector;
|
|
53
|
+
private normalize;
|
|
54
|
+
}
|
|
33
55
|
export declare function createEmbeddingProviderFromEnvironment(): EmbeddingProvider;
|