@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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class BgeTokenizer {
|
|
2
|
+
private readonly vocab;
|
|
3
|
+
private readonly unkToken;
|
|
4
|
+
constructor(tokenizerPath?: string | undefined);
|
|
5
|
+
countTokens(text: string): number;
|
|
6
|
+
encode(text: string): string[];
|
|
7
|
+
private preTokenize;
|
|
8
|
+
private wordPiece;
|
|
9
|
+
private static resolveTokenizerPath;
|
|
10
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export class BgeTokenizer {
|
|
5
|
+
vocab;
|
|
6
|
+
unkToken;
|
|
7
|
+
constructor(tokenizerPath = BgeTokenizer.resolveTokenizerPath()) {
|
|
8
|
+
if (!tokenizerPath)
|
|
9
|
+
throw new Error('BGE tokenizer.json 缺失,无法执行真实 Token 计数');
|
|
10
|
+
const raw = JSON.parse(fs.readFileSync(tokenizerPath, 'utf8'));
|
|
11
|
+
this.vocab = new Set(Object.keys(raw.model?.vocab ?? {}));
|
|
12
|
+
this.unkToken = raw.model?.unk_token ?? '[UNK]';
|
|
13
|
+
if (this.vocab.size === 0)
|
|
14
|
+
throw new Error('BGE tokenizer vocab 为空,无法执行真实 Token 计数');
|
|
15
|
+
}
|
|
16
|
+
countTokens(text) {
|
|
17
|
+
return this.encode(text).length;
|
|
18
|
+
}
|
|
19
|
+
encode(text) {
|
|
20
|
+
const tokens = [];
|
|
21
|
+
for (const token of this.preTokenize(text))
|
|
22
|
+
tokens.push(...this.wordPiece(token));
|
|
23
|
+
return tokens;
|
|
24
|
+
}
|
|
25
|
+
preTokenize(text) {
|
|
26
|
+
const normalized = Array.from(text, char => {
|
|
27
|
+
const code = char.charCodeAt(0);
|
|
28
|
+
return code < 32 || code === 127 ? ' ' : char;
|
|
29
|
+
}).join('')
|
|
30
|
+
.replace(/([\p{Script=Han}])/gu, ' $1 ')
|
|
31
|
+
.normalize('NFKC');
|
|
32
|
+
return normalized.match(/[\p{Script=Han}]|[\p{Letter}\p{Number}_]+|[^\s\p{Letter}\p{Number}_]/gu) ?? [];
|
|
33
|
+
}
|
|
34
|
+
wordPiece(token) {
|
|
35
|
+
if (this.vocab.has(token))
|
|
36
|
+
return [token];
|
|
37
|
+
const pieces = [];
|
|
38
|
+
let start = 0;
|
|
39
|
+
while (start < token.length) {
|
|
40
|
+
let end = token.length;
|
|
41
|
+
let current;
|
|
42
|
+
while (start < end) {
|
|
43
|
+
const piece = `${start > 0 ? '##' : ''}${token.slice(start, end)}`;
|
|
44
|
+
if (this.vocab.has(piece)) {
|
|
45
|
+
current = piece;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
end -= 1;
|
|
49
|
+
}
|
|
50
|
+
if (!current)
|
|
51
|
+
return Array.from(token).map(() => this.unkToken);
|
|
52
|
+
pieces.push(current);
|
|
53
|
+
start = end;
|
|
54
|
+
}
|
|
55
|
+
return pieces;
|
|
56
|
+
}
|
|
57
|
+
static resolveTokenizerPath() {
|
|
58
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
const candidates = [
|
|
60
|
+
process.env.CUSTOMIZE_BGE_TOKENIZER_PATH,
|
|
61
|
+
process.env.KB_BGE_TOKENIZER_PATH,
|
|
62
|
+
path.resolve(process.cwd(), 'packages', 'knowledge', 'models', 'bge-small-zh-v1.5', 'tokenizer.json'),
|
|
63
|
+
path.resolve(process.cwd(), 'models', 'bge-small-zh-v1.5', 'tokenizer.json'),
|
|
64
|
+
path.resolve(currentDir, '..', '..', 'models', 'bge-small-zh-v1.5', 'tokenizer.json'),
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
return candidates.find(candidate => fs.existsSync(candidate));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -14,17 +14,27 @@ export interface ChunkConfig {
|
|
|
14
14
|
headerInjection: boolean;
|
|
15
15
|
}
|
|
16
16
|
export declare class TextChunker {
|
|
17
|
+
private readonly tokenizer;
|
|
17
18
|
chunk(text: string, file: ClassifiedFile, metadata?: Record<string, unknown>): TextChunk[];
|
|
18
19
|
private createCandidates;
|
|
19
20
|
private createTextCandidates;
|
|
20
21
|
private createTableCandidates;
|
|
21
22
|
private createDataCandidates;
|
|
22
23
|
private createCodeCandidates;
|
|
24
|
+
private normalizeCodeLanguage;
|
|
25
|
+
private splitCodeByLanguage;
|
|
26
|
+
private collectIndentSensitiveBlocks;
|
|
27
|
+
private collectBraceBalancedBlocks;
|
|
28
|
+
private splitCodeByStructuralFallback;
|
|
23
29
|
private splitIntoSections;
|
|
24
30
|
private mergeLeadingHeader;
|
|
25
31
|
private recursiveSplit;
|
|
26
32
|
private mergeParts;
|
|
27
33
|
private splitByWindow;
|
|
34
|
+
private splitBySentenceBoundary;
|
|
35
|
+
private isMarkdownTable;
|
|
36
|
+
private extractMarkdownTableRowRange;
|
|
37
|
+
private splitMarkdownTable;
|
|
28
38
|
private enforceCandidateLimit;
|
|
29
39
|
private withHeader;
|
|
30
40
|
private createChunk;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BgeTokenizer } from './bge-tokenizer.js';
|
|
1
2
|
const DEFAULT_CONFIGS = {
|
|
2
3
|
document: { maxChunkSize: 800, overlap: 100, headerInjection: true },
|
|
3
4
|
spreadsheet: { maxChunkSize: 1000, overlap: 120, headerInjection: true },
|
|
@@ -18,7 +19,18 @@ const RECURSIVE_SEPARATORS = [
|
|
|
18
19
|
/(?<=[,、])\s*/u,
|
|
19
20
|
/\s+/u,
|
|
20
21
|
];
|
|
22
|
+
const LANGUAGE_ROUTER = {
|
|
23
|
+
typescript: { delimiters: /\n(?=(?:export\s+)?(?:async\s+)?(?:class|function|interface|type|const|let)\s)/u, blockStart: /\{\s*$/u, indentSensitive: false },
|
|
24
|
+
javascript: { delimiters: /\n(?=(?:export\s+)?(?:async\s+)?(?:class|function|const|let)\s)/u, blockStart: /\{\s*$/u, indentSensitive: false },
|
|
25
|
+
python: { delimiters: /\n(?=(?:class|def)\s)/u, blockStart: /:\s*$/u, indentSensitive: true },
|
|
26
|
+
go: { delimiters: /\n(?=(?:func|type|struct|interface)\s)/u, blockStart: /\{\s*$/u, indentSensitive: false },
|
|
27
|
+
java: { delimiters: /\n(?=(?:public|protected|private|static|final|abstract|\s)*(?:class|interface|enum|(?:\w|<|>|\[|\])+\s+\w+\s*\())/u, blockStart: /\{\s*$/u, indentSensitive: false },
|
|
28
|
+
csharp: { delimiters: /\n(?=(?:public|protected|private|internal|static|sealed|abstract|\s)*(?:class|interface|enum|(?:\w|<|>|\[|\])+\s+\w+\s*\())/u, blockStart: /\{\s*$/u, indentSensitive: false },
|
|
29
|
+
cpp: { delimiters: /\n(?=(?:class|struct|namespace|template)\s|[\w:*&<>]+\s+\w+\s*\()/u, blockStart: /\{\s*$/u, indentSensitive: false },
|
|
30
|
+
c: { delimiters: /\n(?=(?:struct|enum)\s|[\w*]+\s+\w+\s*\()/u, blockStart: /\{\s*$/u, indentSensitive: false },
|
|
31
|
+
};
|
|
21
32
|
export class TextChunker {
|
|
33
|
+
tokenizer = new BgeTokenizer();
|
|
22
34
|
chunk(text, file, metadata = {}) {
|
|
23
35
|
const source = text.trim();
|
|
24
36
|
if (source.length === 0)
|
|
@@ -34,7 +46,7 @@ export class TextChunker {
|
|
|
34
46
|
if (file.category === 'data')
|
|
35
47
|
return this.createDataCandidates(text, config);
|
|
36
48
|
if (file.category === 'code')
|
|
37
|
-
return this.createCodeCandidates(text, config);
|
|
49
|
+
return this.createCodeCandidates(text, file, config);
|
|
38
50
|
return this.createTextCandidates(text, file.category, config);
|
|
39
51
|
}
|
|
40
52
|
createTextCandidates(text, category, config) {
|
|
@@ -62,43 +74,23 @@ export class TextChunker {
|
|
|
62
74
|
return candidates;
|
|
63
75
|
}
|
|
64
76
|
createTableCandidates(text, config) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
break;
|
|
80
|
-
const nextRows = [...rows, nextLine];
|
|
81
|
-
const candidate = [header, `行范围: ${rowStart + 1}-${rowStart + nextRows.length}`, ...nextRows].filter(Boolean).join('\n');
|
|
82
|
-
if (rows.length > 0 && this.estimateTokens(candidate) > config.maxChunkSize)
|
|
83
|
-
break;
|
|
84
|
-
rows.push(nextLine);
|
|
85
|
-
}
|
|
86
|
-
const textChunk = [header, `行范围: ${rowStart + 1}-${rowStart + rows.length}`, ...rows].filter(Boolean).join('\n');
|
|
87
|
-
candidates.push({
|
|
88
|
-
text: textChunk,
|
|
89
|
-
startChar: text.indexOf(rows[0] ?? ''),
|
|
90
|
-
endChar: text.indexOf(rows.at(-1) ?? '') + (rows.at(-1)?.length ?? 0),
|
|
91
|
-
sectionTitle: '表格数据',
|
|
92
|
-
kind: 'table',
|
|
93
|
-
parentId: `table-${parentIndex}`,
|
|
94
|
-
parentIndex,
|
|
95
|
-
childIndex: 0,
|
|
96
|
-
rowRange: `${rowStart + 1}-${rowStart + rows.length}`,
|
|
77
|
+
if (this.isMarkdownTable(text)) {
|
|
78
|
+
return this.splitMarkdownTable(text, config.maxChunkSize).map((part, index) => {
|
|
79
|
+
const startChar = Math.max(0, text.indexOf(part.slice(0, 40)));
|
|
80
|
+
return {
|
|
81
|
+
text: part,
|
|
82
|
+
startChar,
|
|
83
|
+
endChar: startChar + part.length,
|
|
84
|
+
sectionTitle: this.extractSectionTitle(part) ?? '表格数据',
|
|
85
|
+
kind: 'table',
|
|
86
|
+
parentId: `table-${index}`,
|
|
87
|
+
parentIndex: index,
|
|
88
|
+
childIndex: 0,
|
|
89
|
+
rowRange: this.extractMarkdownTableRowRange(part),
|
|
90
|
+
};
|
|
97
91
|
});
|
|
98
|
-
rowStart += Math.max(1, rows.length);
|
|
99
|
-
parentIndex += 1;
|
|
100
92
|
}
|
|
101
|
-
return
|
|
93
|
+
return this.createTextCandidates(text, 'spreadsheet', config);
|
|
102
94
|
}
|
|
103
95
|
createDataCandidates(text, config) {
|
|
104
96
|
const sections = text.split(/\n(?=[\w.[\]-]+[::]\s)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
|
|
@@ -114,19 +106,89 @@ export class TextChunker {
|
|
|
114
106
|
childIndex: 0,
|
|
115
107
|
}));
|
|
116
108
|
}
|
|
117
|
-
createCodeCandidates(text, config) {
|
|
118
|
-
const
|
|
109
|
+
createCodeCandidates(text, file, config) {
|
|
110
|
+
const language = this.normalizeCodeLanguage(file.format);
|
|
111
|
+
const languageConfig = LANGUAGE_ROUTER[language];
|
|
112
|
+
const blocks = languageConfig
|
|
113
|
+
? this.splitCodeByLanguage(text, languageConfig)
|
|
114
|
+
: this.splitCodeByStructuralFallback(text);
|
|
119
115
|
const parts = blocks.length > 1 ? blocks : this.recursiveSplit(text, config.maxChunkSize);
|
|
120
|
-
return this.mergeParts(parts, config.maxChunkSize, config.overlap).map((part, index) =>
|
|
121
|
-
text
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
116
|
+
return this.mergeParts(parts, config.maxChunkSize, config.overlap).map((part, index) => {
|
|
117
|
+
const startChar = Math.max(0, text.indexOf(part.slice(0, 40)));
|
|
118
|
+
return {
|
|
119
|
+
text: part,
|
|
120
|
+
startChar,
|
|
121
|
+
endChar: startChar + part.length,
|
|
122
|
+
sectionTitle: this.extractSectionTitle(part),
|
|
123
|
+
kind: 'code',
|
|
124
|
+
parentId: `code-${language}-${index}`,
|
|
125
|
+
parentIndex: index,
|
|
126
|
+
childIndex: 0,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
normalizeCodeLanguage(format) {
|
|
131
|
+
const aliases = {
|
|
132
|
+
ts: 'typescript', tsx: 'typescript', typescript: 'typescript',
|
|
133
|
+
js: 'javascript', jsx: 'javascript', javascript: 'javascript',
|
|
134
|
+
py: 'python', python: 'python',
|
|
135
|
+
golang: 'go', go: 'go',
|
|
136
|
+
java: 'java', cs: 'csharp', csharp: 'csharp',
|
|
137
|
+
cpp: 'cpp', cxx: 'cpp', cc: 'cpp', hpp: 'cpp',
|
|
138
|
+
c: 'c', h: 'c',
|
|
139
|
+
};
|
|
140
|
+
return aliases[format.toLowerCase()] ?? format.toLowerCase();
|
|
141
|
+
}
|
|
142
|
+
splitCodeByLanguage(text, config) {
|
|
143
|
+
const raw = text.split(config.delimiters).map(part => part.trim()).filter(Boolean);
|
|
144
|
+
if (raw.length <= 1)
|
|
145
|
+
return raw;
|
|
146
|
+
return raw.flatMap(block => config.indentSensitive ? this.collectIndentSensitiveBlocks(block, config) : this.collectBraceBalancedBlocks(block, config));
|
|
147
|
+
}
|
|
148
|
+
collectIndentSensitiveBlocks(block, config) {
|
|
149
|
+
const lines = block.split(/\r?\n/u);
|
|
150
|
+
const result = [];
|
|
151
|
+
let current = [];
|
|
152
|
+
let baseIndent;
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
const indent = line.match(/^\s*/u)?.[0].length ?? 0;
|
|
155
|
+
if (current.length > 0 && baseIndent != null && indent <= baseIndent && config.blockStart.test(current[0] ?? '') && line.trim()) {
|
|
156
|
+
result.push(current.join('\n').trim());
|
|
157
|
+
current = [];
|
|
158
|
+
baseIndent = undefined;
|
|
159
|
+
}
|
|
160
|
+
if (current.length === 0)
|
|
161
|
+
baseIndent = indent;
|
|
162
|
+
current.push(line);
|
|
163
|
+
}
|
|
164
|
+
if (current.length > 0)
|
|
165
|
+
result.push(current.join('\n').trim());
|
|
166
|
+
return result.filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
collectBraceBalancedBlocks(block, _config) {
|
|
169
|
+
const lines = block.split(/\r?\n/u);
|
|
170
|
+
const result = [];
|
|
171
|
+
let current = [];
|
|
172
|
+
let depth = 0;
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
current.push(line);
|
|
175
|
+
depth += (line.match(/\{/gu) ?? []).length;
|
|
176
|
+
depth -= (line.match(/\}/gu) ?? []).length;
|
|
177
|
+
if (current.length > 1 && depth <= 0) {
|
|
178
|
+
result.push(current.join('\n').trim());
|
|
179
|
+
current = [];
|
|
180
|
+
depth = 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (current.length > 0)
|
|
184
|
+
result.push(current.join('\n').trim());
|
|
185
|
+
return result.filter(Boolean);
|
|
186
|
+
}
|
|
187
|
+
splitCodeByStructuralFallback(text) {
|
|
188
|
+
const blocks = this.collectBraceBalancedBlocks(text, { delimiters: /\n/u, blockStart: /\{\s*$/u, indentSensitive: false });
|
|
189
|
+
if (blocks.length > 1)
|
|
190
|
+
return blocks;
|
|
191
|
+
return text.split(/\n(?=\S)/u).map(part => part.trim()).filter(Boolean);
|
|
130
192
|
}
|
|
131
193
|
splitIntoSections(text, category) {
|
|
132
194
|
const pattern = category === 'cad' || category === 'diagram'
|
|
@@ -154,11 +216,13 @@ export class TextChunker {
|
|
|
154
216
|
recursiveSplit(text, maxTokens, separatorIndex = 0) {
|
|
155
217
|
if (this.estimateTokens(text) <= maxTokens)
|
|
156
218
|
return [text.trim()].filter(Boolean);
|
|
219
|
+
if (this.isMarkdownTable(text))
|
|
220
|
+
return this.splitMarkdownTable(text, maxTokens);
|
|
157
221
|
if (separatorIndex >= RECURSIVE_SEPARATORS.length)
|
|
158
|
-
return this.
|
|
222
|
+
return this.splitBySentenceBoundary(text, maxTokens);
|
|
159
223
|
const separator = RECURSIVE_SEPARATORS[separatorIndex];
|
|
160
224
|
if (!separator)
|
|
161
|
-
return this.
|
|
225
|
+
return this.splitBySentenceBoundary(text, maxTokens);
|
|
162
226
|
const parts = text.split(separator).map(part => part.trim()).filter(Boolean);
|
|
163
227
|
if (parts.length <= 1)
|
|
164
228
|
return this.recursiveSplit(text, maxTokens, separatorIndex + 1);
|
|
@@ -183,8 +247,11 @@ export class TextChunker {
|
|
|
183
247
|
return chunks.flatMap(chunk => this.estimateTokens(chunk) > maxTokens ? this.splitByWindow(chunk, maxTokens, overlapTokens) : [chunk]);
|
|
184
248
|
}
|
|
185
249
|
splitByWindow(text, maxTokens, overlapTokens = 0) {
|
|
186
|
-
const
|
|
187
|
-
|
|
250
|
+
const tokens = this.tokenizer.encode(text);
|
|
251
|
+
if (tokens.length <= maxTokens)
|
|
252
|
+
return [text.trim()].filter(Boolean);
|
|
253
|
+
const maxChars = Math.max(200, Math.ceil(text.length * (maxTokens / Math.max(1, tokens.length))));
|
|
254
|
+
const overlapChars = Math.max(0, Math.ceil(text.length * (overlapTokens / Math.max(1, tokens.length))));
|
|
188
255
|
const step = Math.max(1, maxChars - overlapChars);
|
|
189
256
|
const chunks = [];
|
|
190
257
|
for (let start = 0; start < text.length; start += step) {
|
|
@@ -194,6 +261,44 @@ export class TextChunker {
|
|
|
194
261
|
}
|
|
195
262
|
return chunks.filter(Boolean);
|
|
196
263
|
}
|
|
264
|
+
splitBySentenceBoundary(text, maxTokens) {
|
|
265
|
+
const units = text.split(/(?<=[。?!;;.!?])\s+|(?<=[。?!;;.!?])/u).map(part => part.trim()).filter(Boolean);
|
|
266
|
+
if (units.length <= 1)
|
|
267
|
+
return this.splitByWindow(text, maxTokens);
|
|
268
|
+
return this.mergeParts(units, maxTokens, 0);
|
|
269
|
+
}
|
|
270
|
+
isMarkdownTable(text) {
|
|
271
|
+
const lines = text.trim().split(/\r?\n/u);
|
|
272
|
+
return lines.length >= 3 && lines.some(line => /^\s*\|?\s*:?-{3,}:?\s*\|/u.test(line));
|
|
273
|
+
}
|
|
274
|
+
extractMarkdownTableRowRange(text) {
|
|
275
|
+
const lines = text.trim().split(/\r?\n/u).filter(line => /^\s*\|/u.test(line));
|
|
276
|
+
const rowCount = Math.max(0, lines.length - 2);
|
|
277
|
+
return rowCount > 0 ? `1-${rowCount}` : undefined;
|
|
278
|
+
}
|
|
279
|
+
splitMarkdownTable(text, maxTokens) {
|
|
280
|
+
const lines = text.trim().split(/\r?\n/u).filter(Boolean);
|
|
281
|
+
const separatorIndex = lines.findIndex(line => /^\s*\|?\s*:?-{3,}:?\s*\|/u.test(line));
|
|
282
|
+
if (separatorIndex <= 0)
|
|
283
|
+
return this.splitBySentenceBoundary(text, maxTokens);
|
|
284
|
+
const header = lines.slice(0, separatorIndex + 1);
|
|
285
|
+
const rows = lines.slice(separatorIndex + 1);
|
|
286
|
+
const chunks = [];
|
|
287
|
+
let current = [];
|
|
288
|
+
for (const row of rows) {
|
|
289
|
+
const candidate = [...header, ...current, row].join('\n');
|
|
290
|
+
if (current.length > 0 && this.estimateTokens(candidate) > maxTokens) {
|
|
291
|
+
chunks.push([...header, ...current].join('\n'));
|
|
292
|
+
current = [row];
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
current.push(row);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (current.length > 0)
|
|
299
|
+
chunks.push([...header, ...current].join('\n'));
|
|
300
|
+
return chunks.flatMap(chunk => this.estimateTokens(chunk) > maxTokens ? this.splitByWindow(chunk, maxTokens) : [chunk]);
|
|
301
|
+
}
|
|
197
302
|
enforceCandidateLimit(candidates, config) {
|
|
198
303
|
return candidates.flatMap(candidate => {
|
|
199
304
|
if (this.estimateTokens(candidate.text) <= config.maxChunkSize)
|
|
@@ -263,6 +368,6 @@ export class TextChunker {
|
|
|
263
368
|
return text.slice(Math.max(0, text.length - chars));
|
|
264
369
|
}
|
|
265
370
|
estimateTokens(text) {
|
|
266
|
-
return Math.max(1,
|
|
371
|
+
return Math.max(1, this.tokenizer.countTokens(text));
|
|
267
372
|
}
|
|
268
373
|
}
|
|
@@ -76,7 +76,6 @@ export class FileClassifier {
|
|
|
76
76
|
['.html', 'web', 'html'], ['.htm', 'web', 'html'], ['.xhtml', 'web', 'html'], ['.css', 'web', 'stylesheet'], ['.scss', 'web', 'stylesheet'], ['.sass', 'web', 'stylesheet'], ['.less', 'web', 'stylesheet'],
|
|
77
77
|
['.hbs', 'web', 'template'], ['.ejs', 'web', 'template'], ['.pug', 'web', 'template'], ['.j2', 'web', 'template'], ['.jinja2', 'web', 'template'],
|
|
78
78
|
['.drawio', 'diagram', 'drawio'], ['.dio', 'diagram', 'drawio'], ['.vsdx', 'diagram', 'visio'], ['.vdx', 'diagram', 'visio'], ['.puml', 'diagram', 'plantuml'], ['.plantuml', 'diagram', 'plantuml'], ['.mmd', 'diagram', 'mermaid'], ['.mermaid', 'diagram', 'mermaid'], ['.excalidraw', 'diagram', 'excalidraw'],
|
|
79
|
-
['.zip', 'archive', 'zip'], ['.jar', 'archive', 'zip'], ['.war', 'archive', 'zip'], ['.apk', 'archive', 'zip'], ['.tar', 'archive', 'tar'], ['.gz', 'archive', 'other'], ['.tgz', 'archive', 'tar'], ['.bz2', 'archive', 'other'], ['.rar', 'archive', 'other'], ['.7z', 'archive', 'other'],
|
|
80
79
|
];
|
|
81
80
|
return new Map(entries.map(([ext, category, format]) => [ext, [category, format]]));
|
|
82
81
|
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { TextChunk } from '../chunking/text-chunker.js';
|
|
2
2
|
import type { FileCategory, IndexStateRecord } from '../types.js';
|
|
3
|
+
export type KnowledgeJobStatus = 'PENDING' | 'PARSING' | 'CHUNKING' | 'INDEXING' | 'SUCCESS' | 'ERROR';
|
|
4
|
+
export interface KnowledgeIndexJob {
|
|
5
|
+
id: string;
|
|
6
|
+
relativePath: string;
|
|
7
|
+
status: KnowledgeJobStatus;
|
|
8
|
+
percent: number;
|
|
9
|
+
message: string;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
updatedAt: number;
|
|
13
|
+
}
|
|
3
14
|
export interface StoredChunk {
|
|
15
|
+
rowid: number;
|
|
4
16
|
id: string;
|
|
5
17
|
relativePath: string;
|
|
6
18
|
chunkIndex: number;
|
|
@@ -34,6 +46,18 @@ export interface StoredParentChunk {
|
|
|
34
46
|
metadataJson?: string;
|
|
35
47
|
createdAt: number;
|
|
36
48
|
}
|
|
49
|
+
export interface StoredDocumentChunk {
|
|
50
|
+
id: string;
|
|
51
|
+
relativePath: string;
|
|
52
|
+
content: string;
|
|
53
|
+
category: FileCategory;
|
|
54
|
+
format: string;
|
|
55
|
+
collectionName: string;
|
|
56
|
+
parentCount: number;
|
|
57
|
+
chunkCount: number;
|
|
58
|
+
metadataJson?: string;
|
|
59
|
+
createdAt: number;
|
|
60
|
+
}
|
|
37
61
|
export interface FileHashRecord {
|
|
38
62
|
contentHash: string;
|
|
39
63
|
filePath: string;
|
|
@@ -68,6 +92,22 @@ export declare class IndexStateStore {
|
|
|
68
92
|
upsertRecord(record: IndexStateRecord): void;
|
|
69
93
|
updateVerified(relativePath: string, mtime: number): void;
|
|
70
94
|
listRecords(): IndexStateRecord[];
|
|
95
|
+
enqueueIndexJob(job: {
|
|
96
|
+
id: string;
|
|
97
|
+
relativePath: string;
|
|
98
|
+
message?: string;
|
|
99
|
+
}): KnowledgeIndexJob;
|
|
100
|
+
updateIndexJob(id: string, patch: {
|
|
101
|
+
status?: KnowledgeJobStatus;
|
|
102
|
+
percent?: number;
|
|
103
|
+
message?: string;
|
|
104
|
+
errorMessage?: string;
|
|
105
|
+
}): void;
|
|
106
|
+
getIndexJob(id: string): KnowledgeIndexJob | undefined;
|
|
107
|
+
listPendingIndexJobs(limit?: number): KnowledgeIndexJob[];
|
|
108
|
+
countPendingIndexJobs(): number;
|
|
109
|
+
listActiveIndexJobsByPath(relativePath: string): KnowledgeIndexJob[];
|
|
110
|
+
listIndexJobsByPrefix(prefix: string): KnowledgeIndexJob[];
|
|
71
111
|
replaceChunks(relativePath: string, chunks: TextChunk[], file: {
|
|
72
112
|
category: FileCategory;
|
|
73
113
|
format: string;
|
|
@@ -78,9 +118,12 @@ export declare class IndexStateStore {
|
|
|
78
118
|
relativePath?: string;
|
|
79
119
|
limit?: number;
|
|
80
120
|
}): StoredChunk[];
|
|
121
|
+
getChunkByRowid(rowid: number): StoredChunk | undefined;
|
|
122
|
+
getChunksByRowids(rowids: number[]): StoredChunk[];
|
|
81
123
|
getContextChunks(relativePath: string, chunkIndex: number, window?: number): StoredChunk[];
|
|
82
124
|
listParentChunks(relativePath: string): StoredParentChunk[];
|
|
83
125
|
getParentChunk(relativePath: string, parentId: string): StoredParentChunk | undefined;
|
|
126
|
+
getDocumentChunk(relativePath: string): StoredDocumentChunk | undefined;
|
|
84
127
|
getChunksByParent(relativePath: string, parentId: string, limit?: number): StoredChunk[];
|
|
85
128
|
searchChunks(query: string, limit?: number): ChunkSearchResult[];
|
|
86
129
|
private searchChunksFts;
|
|
@@ -127,11 +170,15 @@ export declare class IndexStateStore {
|
|
|
127
170
|
private rowToFileHash;
|
|
128
171
|
private rowToRelationship;
|
|
129
172
|
private rowToParentChunk;
|
|
173
|
+
private rowToDocumentChunk;
|
|
130
174
|
private splitParentGroups;
|
|
175
|
+
private rowToJob;
|
|
131
176
|
private metadataString;
|
|
132
177
|
private rowToChunk;
|
|
133
178
|
private expandSearchTerms;
|
|
134
179
|
private toFtsQuery;
|
|
180
|
+
private mergeKeywordResults;
|
|
181
|
+
private chineseNgrams;
|
|
135
182
|
private bm25ToPositiveScore;
|
|
136
183
|
private scoreChunkDetailed;
|
|
137
184
|
private countOccurrences;
|