@customize-agent/knowledge 4.0.33 → 4.0.34
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.
|
@@ -130,11 +130,21 @@ export declare class IndexStateStore {
|
|
|
130
130
|
format: string;
|
|
131
131
|
collectionName: string;
|
|
132
132
|
}): void;
|
|
133
|
+
private chunkQueryParts;
|
|
133
134
|
listChunks(options?: {
|
|
134
135
|
collectionName?: string;
|
|
135
136
|
relativePath?: string;
|
|
136
137
|
limit?: number;
|
|
137
138
|
}): StoredChunk[];
|
|
139
|
+
countChunks(options?: {
|
|
140
|
+
collectionName?: string;
|
|
141
|
+
relativePath?: string;
|
|
142
|
+
}): number;
|
|
143
|
+
listChunksByContentBudget(options?: {
|
|
144
|
+
collectionName?: string;
|
|
145
|
+
relativePath?: string;
|
|
146
|
+
maxContentChars?: number;
|
|
147
|
+
}): StoredChunk[];
|
|
138
148
|
getChunkByRowid(rowid: number): StoredChunk | undefined;
|
|
139
149
|
getChunksByRowids(rowids: number[]): StoredChunk[];
|
|
140
150
|
getContextChunks(relativePath: string, chunkIndex: number, window?: number): StoredChunk[];
|
|
@@ -152,6 +162,7 @@ export declare class IndexStateStore {
|
|
|
152
162
|
filePaths?: string[];
|
|
153
163
|
}): ChunkSearchResult[];
|
|
154
164
|
private filePathFilterClause;
|
|
165
|
+
private resolveChunkSearchLimit;
|
|
155
166
|
private searchChunksFts;
|
|
156
167
|
private searchChunksLike;
|
|
157
168
|
findExactDuplicate(contentHash: string, excludePath?: string): FileHashRecord | undefined;
|
|
@@ -182,7 +182,7 @@ export class IndexStateStore {
|
|
|
182
182
|
});
|
|
183
183
|
transaction();
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
chunkQueryParts(options = {}) {
|
|
186
186
|
const conditions = [];
|
|
187
187
|
const params = [];
|
|
188
188
|
if (options.collectionName) {
|
|
@@ -194,17 +194,47 @@ export class IndexStateStore {
|
|
|
194
194
|
params.push(options.relativePath);
|
|
195
195
|
}
|
|
196
196
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
197
|
+
return { where, params };
|
|
198
|
+
}
|
|
199
|
+
listChunks(options = {}) {
|
|
200
|
+
const { where, params } = this.chunkQueryParts(options);
|
|
197
201
|
const limit = options.limit ? 'LIMIT ?' : '';
|
|
198
|
-
|
|
199
|
-
params.push(options.limit);
|
|
202
|
+
const queryParams = options.limit ? [...params, options.limit] : params;
|
|
200
203
|
const rows = this.db.prepare(`
|
|
201
204
|
SELECT rowid, * FROM kb_chunks
|
|
202
205
|
${where}
|
|
203
206
|
ORDER BY relative_path, chunk_index
|
|
204
207
|
${limit}
|
|
205
|
-
`).all(...
|
|
208
|
+
`).all(...queryParams);
|
|
206
209
|
return rows.map(row => this.rowToChunk(row, 0));
|
|
207
210
|
}
|
|
211
|
+
countChunks(options = {}) {
|
|
212
|
+
const { where, params } = this.chunkQueryParts(options);
|
|
213
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS count FROM kb_chunks ${where}`).get(...params);
|
|
214
|
+
return Number(row?.count || 0);
|
|
215
|
+
}
|
|
216
|
+
listChunksByContentBudget(options = {}) {
|
|
217
|
+
const maxContentChars = Number(options.maxContentChars);
|
|
218
|
+
if (!Number.isFinite(maxContentChars) || maxContentChars <= 0)
|
|
219
|
+
return this.listChunks(options);
|
|
220
|
+
const { where, params } = this.chunkQueryParts(options);
|
|
221
|
+
const rows = this.db.prepare(`
|
|
222
|
+
SELECT rowid, * FROM kb_chunks
|
|
223
|
+
${where}
|
|
224
|
+
ORDER BY relative_path, chunk_index
|
|
225
|
+
`).iterate(...params);
|
|
226
|
+
const chunks = [];
|
|
227
|
+
let usedChars = 0;
|
|
228
|
+
for (const row of rows) {
|
|
229
|
+
const chunk = this.rowToChunk(row, 0);
|
|
230
|
+
const length = chunk.content?.length || 0;
|
|
231
|
+
if (chunks.length > 0 && usedChars + length > maxContentChars)
|
|
232
|
+
break;
|
|
233
|
+
chunks.push(chunk);
|
|
234
|
+
usedChars += length;
|
|
235
|
+
}
|
|
236
|
+
return chunks;
|
|
237
|
+
}
|
|
208
238
|
getChunkByRowid(rowid) {
|
|
209
239
|
const row = this.db.prepare('SELECT rowid, * FROM kb_chunks WHERE rowid = ?').get(rowid);
|
|
210
240
|
return row ? this.rowToChunk(row, 0) : undefined;
|
|
@@ -264,20 +294,28 @@ export class IndexStateStore {
|
|
|
264
294
|
* @param limit 返回结果数量上限
|
|
265
295
|
* @returns 搜索结果列表(按相关性得分排序)
|
|
266
296
|
*/
|
|
267
|
-
searchChunks(query, limit
|
|
297
|
+
searchChunks(query, limit, filters = {}) {
|
|
268
298
|
const terms = this.expandSearchTerms(query);
|
|
269
299
|
if (terms.length === 0)
|
|
270
300
|
return [];
|
|
271
301
|
const filePaths = [...new Set((filters.filePaths ?? []).filter(Boolean))];
|
|
302
|
+
const effectiveLimit = this.resolveChunkSearchLimit(limit, filePaths);
|
|
272
303
|
const results = [
|
|
273
|
-
...(this.ftsEnabled ? this.searchChunksFts(terms,
|
|
274
|
-
...this.searchChunksLike(terms,
|
|
304
|
+
...(this.ftsEnabled ? this.searchChunksFts(terms, effectiveLimit, filePaths) : []),
|
|
305
|
+
...this.searchChunksLike(terms, effectiveLimit, filePaths),
|
|
275
306
|
];
|
|
276
|
-
return this.mergeKeywordResults(results,
|
|
307
|
+
return this.mergeKeywordResults(results, effectiveLimit);
|
|
277
308
|
}
|
|
278
309
|
filePathFilterClause(filePaths, column = 'relative_path') {
|
|
279
310
|
return filePaths.length > 0 ? ` AND ${column} IN (${filePaths.map(() => '?').join(', ')})` : '';
|
|
280
311
|
}
|
|
312
|
+
resolveChunkSearchLimit(limit, filePaths) {
|
|
313
|
+
if (Number.isFinite(limit) && limit > 0)
|
|
314
|
+
return Math.ceil(limit);
|
|
315
|
+
const row = this.db.prepare(`SELECT COUNT(*) as count FROM kb_chunks WHERE 1 = 1${this.filePathFilterClause(filePaths)}`)
|
|
316
|
+
.get(...filePaths);
|
|
317
|
+
return Math.max(1, Math.ceil(Number(row?.count) || 0));
|
|
318
|
+
}
|
|
281
319
|
searchChunksFts(terms, limit, filePaths) {
|
|
282
320
|
try {
|
|
283
321
|
const matchQuery = this.toFtsQuery(terms);
|
|
@@ -66,6 +66,8 @@ export declare class KnowledgeBaseManager {
|
|
|
66
66
|
}): Promise<DiffResult>;
|
|
67
67
|
search(query: string, limit?: number, filters?: SearchFilters): ChunkSearchResult[];
|
|
68
68
|
keywordSearchItems(query: string, limit?: number, filters?: SearchFilters): FederatedSearchItem[];
|
|
69
|
+
private resolveSearchLimit;
|
|
70
|
+
private searchCorpusSize;
|
|
69
71
|
expandContext(item: FederatedSearchItem): FederatedSearchItem;
|
|
70
72
|
hybridSearch(query: string, options?: {
|
|
71
73
|
limit?: number;
|
|
@@ -81,11 +83,14 @@ export declare class KnowledgeBaseManager {
|
|
|
81
83
|
}): Promise<FederatedResult>;
|
|
82
84
|
listRelationships(filePath?: string): FileRelationship[];
|
|
83
85
|
listFiles(): IndexStateRecord[];
|
|
84
|
-
getFileDetail(relativePath: string
|
|
86
|
+
getFileDetail(relativePath: string, options?: {
|
|
87
|
+
maxChunkContentChars?: number;
|
|
88
|
+
}): {
|
|
85
89
|
file: IndexStateRecord;
|
|
86
90
|
absolutePath: string;
|
|
87
91
|
directory: string;
|
|
88
92
|
chunks: import("./index-state-store.js").StoredChunk[];
|
|
93
|
+
totalChunkCount: number;
|
|
89
94
|
parents: import("./index-state-store.js").StoredParentChunk[];
|
|
90
95
|
relationships: FileRelationship[];
|
|
91
96
|
tags: {
|
|
@@ -326,11 +326,24 @@ export class KnowledgeBaseManager {
|
|
|
326
326
|
this.onProgress = previousOnProgress;
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
|
-
search(query, limit
|
|
330
|
-
return this.store.searchChunks(query, limit, { filePaths: filters?.filePaths ?? (filters?.filePath ? [filters.filePath] : undefined) });
|
|
329
|
+
search(query, limit, filters) {
|
|
330
|
+
return this.store.searchChunks(query, this.resolveSearchLimit(limit, filters), { filePaths: filters?.filePaths ?? (filters?.filePath ? [filters.filePath] : undefined) });
|
|
331
331
|
}
|
|
332
|
-
keywordSearchItems(query, limit
|
|
333
|
-
return this.store.searchChunks(query, limit, { filePaths: filters?.filePaths ?? (filters?.filePath ? [filters.filePath] : undefined) }).map(result => this.toFederatedItem(result, 'keyword'));
|
|
332
|
+
keywordSearchItems(query, limit, filters) {
|
|
333
|
+
return this.store.searchChunks(query, this.resolveSearchLimit(limit, filters), { filePaths: filters?.filePaths ?? (filters?.filePath ? [filters.filePath] : undefined) }).map(result => this.toFederatedItem(result, 'keyword'));
|
|
334
|
+
}
|
|
335
|
+
resolveSearchLimit(limit, filters) {
|
|
336
|
+
if (Number.isFinite(limit) && limit > 0)
|
|
337
|
+
return Math.ceil(limit);
|
|
338
|
+
return this.searchCorpusSize(filters);
|
|
339
|
+
}
|
|
340
|
+
searchCorpusSize(filters) {
|
|
341
|
+
const paths = filters?.filePaths ?? (filters?.filePath ? [filters.filePath] : undefined);
|
|
342
|
+
const pathSet = new Set(paths?.filter(Boolean));
|
|
343
|
+
const total = this.store.listRecords()
|
|
344
|
+
.filter(record => pathSet.size === 0 || pathSet.has(record.relativePath))
|
|
345
|
+
.reduce((sum, record) => sum + Math.max(0, Math.ceil(Number(record.chunkCount) || 0)), 0);
|
|
346
|
+
return Math.max(1, total);
|
|
334
347
|
}
|
|
335
348
|
expandContext(item) {
|
|
336
349
|
const chunkIndex = item.chunkIndex ?? this.parseChunkIndex(item.id);
|
|
@@ -361,7 +374,9 @@ export class KnowledgeBaseManager {
|
|
|
361
374
|
};
|
|
362
375
|
}
|
|
363
376
|
async hybridSearch(query, options = {}) {
|
|
364
|
-
const
|
|
377
|
+
const requestedLimit = Number.isFinite(options.limit) && options.limit > 0 ? Math.ceil(options.limit) : undefined;
|
|
378
|
+
const corpusSize = this.searchCorpusSize(options.filters);
|
|
379
|
+
const effectiveLimit = requestedLimit ?? corpusSize;
|
|
365
380
|
const start = Date.now();
|
|
366
381
|
const weights = this.retrievalWeights(options.weights);
|
|
367
382
|
const rewrittenQueries = options.generationMode ? [query.trim()].filter(Boolean) : await this.rewriteQueries(query);
|
|
@@ -370,10 +385,12 @@ export class KnowledgeBaseManager {
|
|
|
370
385
|
const vectorMultiplier = options.generationMode ? 3 : 6;
|
|
371
386
|
const vectorQueryLimit = options.generationMode ? 1 : 3;
|
|
372
387
|
for (const [queryIndex, rewritten] of rewrittenQueries.entries()) {
|
|
373
|
-
rankedLists.push({ source: 'keyword', items: this.keywordSearchItems(rewritten,
|
|
388
|
+
rankedLists.push({ source: 'keyword', items: this.keywordSearchItems(rewritten, effectiveLimit * keywordMultiplier, options.filters), queryIndex });
|
|
374
389
|
if (queryIndex < vectorQueryLimit) {
|
|
375
390
|
try {
|
|
376
|
-
|
|
391
|
+
const vectorLimit = effectiveLimit * vectorMultiplier;
|
|
392
|
+
const keywordLimit = effectiveLimit * keywordMultiplier;
|
|
393
|
+
rankedLists.push({ source: 'vector', items: (await this.semanticSearch(rewritten, { ...options, limit: vectorLimit })).results.slice(0, keywordLimit), queryIndex });
|
|
377
394
|
}
|
|
378
395
|
catch { /* 向量搜索在混合搜索中是可选的 */ }
|
|
379
396
|
}
|
|
@@ -381,12 +398,14 @@ export class KnowledgeBaseManager {
|
|
|
381
398
|
const keywordItems = rankedLists.filter(list => list.source === 'keyword').flatMap(list => list.items);
|
|
382
399
|
const vectorItems = rankedLists.filter(list => list.source === 'vector').flatMap(list => list.items);
|
|
383
400
|
// 1. 先进行初筛合并,合并相同的子块并计算混合初始分(不获取大片段,保留子块自身用于精确打分)
|
|
384
|
-
const
|
|
401
|
+
const mergeLimit = effectiveLimit * 4;
|
|
402
|
+
const mergedChildChunks = this.mergeContexts(this.mergeHybridRankedLists(rankedLists, mergeLimit, weights), mergeLimit);
|
|
385
403
|
// 2. 对这些子块进行交叉编码器重排(Cross-Encoder Rerank)
|
|
386
404
|
let reranked = mergedChildChunks;
|
|
387
405
|
let rerankerName = 'local-heuristic-fallback';
|
|
388
406
|
if (mergedChildChunks.length > 0) {
|
|
389
|
-
const
|
|
407
|
+
const rerankLimit = requestedLimit ? Math.min(30, mergeLimit) : mergedChildChunks.length;
|
|
408
|
+
const candidates = mergedChildChunks.slice(0, rerankLimit);
|
|
390
409
|
// 这里使用的是子块自身内容,通常在 500 tokens 左右,不仅相关性判断最准,而且不会超出 Reranker 的 max_length
|
|
391
410
|
const textsToRerank = candidates.map(item => `${item.titlePath ?? item.sectionTitle ?? ''}\n${item.content}`);
|
|
392
411
|
try {
|
|
@@ -409,8 +428,8 @@ export class KnowledgeBaseManager {
|
|
|
409
428
|
reranked = this.heuristicRerank(query, mergedChildChunks);
|
|
410
429
|
}
|
|
411
430
|
}
|
|
412
|
-
// 3.
|
|
413
|
-
const finalExpandedResults = this.mergeExpandedContexts(reranked.map(item => this.expandContext(item)),
|
|
431
|
+
// 3. 拿到精确打分后的结果,此时再进行 expandContext 向上追溯到完整的父块大片段
|
|
432
|
+
const finalExpandedResults = this.mergeExpandedContexts(reranked.map(item => this.expandContext(item)), effectiveLimit);
|
|
414
433
|
return {
|
|
415
434
|
results: finalExpandedResults,
|
|
416
435
|
scopesSearched: this.scope === 'global' ? ['global'] : ['project'],
|
|
@@ -433,7 +452,7 @@ export class KnowledgeBaseManager {
|
|
|
433
452
|
const result = await search.search({
|
|
434
453
|
query,
|
|
435
454
|
queryEmbedding,
|
|
436
|
-
topK: options.limit
|
|
455
|
+
topK: this.resolveSearchLimit(options.limit, options.filters),
|
|
437
456
|
scope: this.scope,
|
|
438
457
|
projectId: this.projectId,
|
|
439
458
|
collections: options.collections,
|
|
@@ -451,17 +470,21 @@ export class KnowledgeBaseManager {
|
|
|
451
470
|
listFiles() {
|
|
452
471
|
return this.store.listRecords();
|
|
453
472
|
}
|
|
454
|
-
getFileDetail(relativePath) {
|
|
473
|
+
getFileDetail(relativePath, options = {}) {
|
|
455
474
|
const normalized = this.normalizeRelativePath(relativePath);
|
|
456
475
|
const file = this.store.listRecords().find(record => record.relativePath === normalized);
|
|
457
476
|
if (!file)
|
|
458
477
|
return undefined;
|
|
459
478
|
const absolutePath = this.resolveKbRelativePath(normalized);
|
|
479
|
+
const chunks = options.maxChunkContentChars
|
|
480
|
+
? this.store.listChunksByContentBudget({ relativePath: normalized, maxContentChars: options.maxChunkContentChars })
|
|
481
|
+
: this.store.listChunks({ relativePath: normalized });
|
|
460
482
|
return {
|
|
461
483
|
file,
|
|
462
484
|
absolutePath,
|
|
463
485
|
directory: path.dirname(absolutePath),
|
|
464
|
-
chunks
|
|
486
|
+
chunks,
|
|
487
|
+
totalChunkCount: this.store.countChunks({ relativePath: normalized }),
|
|
465
488
|
parents: this.store.listParentChunks(normalized),
|
|
466
489
|
relationships: this.store.listRelationships(normalized),
|
|
467
490
|
tags: this.store.listTags(normalized),
|
|
@@ -59,7 +59,7 @@ export class MultiProjectManager {
|
|
|
59
59
|
return this.registry.list();
|
|
60
60
|
}
|
|
61
61
|
async search(projectRoot, query, options = {}) {
|
|
62
|
-
const limit = options.limit
|
|
62
|
+
const limit = Number.isFinite(options.limit) && options.limit > 0 ? Math.ceil(options.limit) : undefined;
|
|
63
63
|
const scope = options.scope ?? 'project';
|
|
64
64
|
const project = await this.getProject(projectRoot);
|
|
65
65
|
if (scope === 'project')
|
|
@@ -73,7 +73,8 @@ export class MultiProjectManager {
|
|
|
73
73
|
}
|
|
74
74
|
const global = await this.getGlobalKB();
|
|
75
75
|
const globalResults = await global.hybridSearch(query, { limit, filters: options.filters, weights: options.weights, generationMode: options.generationMode });
|
|
76
|
-
const
|
|
76
|
+
const mergeLimit = limit ?? (projectResults.results.length + globalResults.results.length);
|
|
77
|
+
const merged = new FederationSearch().merge([...projectResults.results, ...globalResults.results], mergeLimit, 'all');
|
|
77
78
|
return {
|
|
78
79
|
...merged,
|
|
79
80
|
debug: this.mergeDebug(projectResults.debug, globalResults.debug),
|
|
@@ -94,7 +95,8 @@ export class MultiProjectManager {
|
|
|
94
95
|
}
|
|
95
96
|
const global = await this.getGlobalKB();
|
|
96
97
|
const globalResults = await global.semanticSearch(query, options);
|
|
97
|
-
|
|
98
|
+
const mergeLimit = Number.isFinite(options.limit) && options.limit > 0 ? Math.ceil(options.limit) : projectResults.results.length + globalResults.results.length;
|
|
99
|
+
return new FederationSearch().merge([...projectResults.results, ...globalResults.results], mergeLimit, 'all');
|
|
98
100
|
}
|
|
99
101
|
async findCrossProjectDuplicates() {
|
|
100
102
|
const projects = this.registry.list();
|