@lotargo/memory_plugin 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -120
- package/mcp-server/admin/server.js +228 -228
- package/mcp-server/admin/snapshot.js +303 -303
- package/mcp-server/benchmarks/fetch_real_corpus.js +351 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +170 -0
- package/mcp-server/benchmarks/stress_ingestion.js +195 -193
- package/mcp-server/benchmarks/test_dual_layer.js +140 -140
- package/mcp-server/cli.js +293 -7
- package/mcp-server/config/config_manager.js +11 -7
- package/mcp-server/db/database.js +43 -43
- package/mcp-server/graph/graph_extractor.js +72 -72
- package/mcp-server/graph/knowledge_linker.js +102 -102
- package/mcp-server/index.js +454 -454
- package/mcp-server/ingest/chunker.js +337 -337
- package/mcp-server/ingest/exporter.js +80 -80
- package/mcp-server/ingest/normalizer.js +104 -104
- package/mcp-server/ingest/pipeline.js +22 -7
- package/mcp-server/ingest/sentence_segmenter.js +74 -74
- package/mcp-server/memory.js +72 -72
- package/mcp-server/ml/gpu_monitor.js +166 -0
- package/mcp-server/ml/model_manager.js +321 -17
- package/mcp-server/preinstall.js +44 -44
- package/mcp-server/retrieval/retriever.js +10 -5
- package/mcp-server/setup.js +148 -148
- package/mcp-server/storage/blob_store.js +62 -62
- package/opencode-plugin/index.js +244 -244
- package/package.json +58 -54
- package/skills/using-memory/SKILL.md +122 -122
|
@@ -1,337 +1,337 @@
|
|
|
1
|
-
import { splitSentencesMultilingual } from "./sentence_segmenter.js";
|
|
2
|
-
|
|
3
|
-
export function estimateTokens(text) {
|
|
4
|
-
if (!text) return 0;
|
|
5
|
-
return Math.ceil(text.length / 4);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// 1. BIG LEVEL: Heading & Section Hierarchy Parser
|
|
9
|
-
export function parseSections(markdown, docTitle = "Document") {
|
|
10
|
-
const lines = markdown.split("\n");
|
|
11
|
-
const sections = [];
|
|
12
|
-
const headerStack = [];
|
|
13
|
-
|
|
14
|
-
let currentHeading = docTitle;
|
|
15
|
-
let currentLines = [];
|
|
16
|
-
|
|
17
|
-
function pushCurrentSection() {
|
|
18
|
-
const content = currentLines.join("\n").trim();
|
|
19
|
-
if (content.length > 0) {
|
|
20
|
-
const breadcrumbs = [docTitle, ...headerStack.map((h) => h.text)].join(" > ");
|
|
21
|
-
sections.push({
|
|
22
|
-
heading: currentHeading,
|
|
23
|
-
breadcrumbs,
|
|
24
|
-
content,
|
|
25
|
-
token_count: estimateTokens(content),
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
currentLines = [];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
for (const line of lines) {
|
|
32
|
-
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
33
|
-
|
|
34
|
-
if (headingMatch) {
|
|
35
|
-
pushCurrentSection();
|
|
36
|
-
|
|
37
|
-
const level = headingMatch[1].length;
|
|
38
|
-
const text = headingMatch[2].trim();
|
|
39
|
-
|
|
40
|
-
while (headerStack.length > 0 && headerStack[headerStack.length - 1].level >= level) {
|
|
41
|
-
headerStack.pop();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
headerStack.push({ level, text });
|
|
45
|
-
currentHeading = text;
|
|
46
|
-
} else {
|
|
47
|
-
currentLines.push(line);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
pushCurrentSection();
|
|
52
|
-
|
|
53
|
-
if (sections.length === 0) {
|
|
54
|
-
sections.push({
|
|
55
|
-
heading: docTitle,
|
|
56
|
-
breadcrumbs: docTitle,
|
|
57
|
-
content: markdown.trim(),
|
|
58
|
-
token_count: estimateTokens(markdown),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return sections;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 2. MEDIUM LEVEL: Logical Paragraphs & Structural Block Extractor
|
|
66
|
-
export function extractMediumBlocks(section, sectionId, docId) {
|
|
67
|
-
const content = section.content;
|
|
68
|
-
if (!content || content.trim().length === 0) return [];
|
|
69
|
-
|
|
70
|
-
const lines = content.split("\n");
|
|
71
|
-
const mediumBlocks = [];
|
|
72
|
-
let blockIndex = 0;
|
|
73
|
-
|
|
74
|
-
let currentLines = [];
|
|
75
|
-
let currentBlockType = "paragraph"; // 'paragraph', 'code', 'table', 'list', 'blockquote'
|
|
76
|
-
|
|
77
|
-
function pushCurrentBlock() {
|
|
78
|
-
const blockContent = currentLines.join("\n").trim();
|
|
79
|
-
if (blockContent.length > 0) {
|
|
80
|
-
mediumBlocks.push({
|
|
81
|
-
id: `${sectionId}_m${blockIndex++}`,
|
|
82
|
-
section_id: sectionId,
|
|
83
|
-
doc_id: docId,
|
|
84
|
-
breadcrumbs: section.breadcrumbs,
|
|
85
|
-
content: blockContent,
|
|
86
|
-
block_type: currentBlockType,
|
|
87
|
-
token_count: estimateTokens(blockContent),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
currentLines = [];
|
|
91
|
-
currentBlockType = "paragraph";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let inFencedCode = false;
|
|
95
|
-
let codeFenceMarker = "";
|
|
96
|
-
|
|
97
|
-
for (let i = 0; i < lines.length; i++) {
|
|
98
|
-
const line = lines[i];
|
|
99
|
-
|
|
100
|
-
// Check code fence
|
|
101
|
-
const fenceMatch = line.match(/^(\s*)(```|~~~)/);
|
|
102
|
-
if (fenceMatch) {
|
|
103
|
-
if (!inFencedCode) {
|
|
104
|
-
if (currentLines.length > 0) pushCurrentBlock();
|
|
105
|
-
inFencedCode = true;
|
|
106
|
-
codeFenceMarker = fenceMatch[2];
|
|
107
|
-
currentBlockType = "code";
|
|
108
|
-
currentLines.push(line);
|
|
109
|
-
} else {
|
|
110
|
-
currentLines.push(line);
|
|
111
|
-
if (line.includes(codeFenceMarker)) {
|
|
112
|
-
inFencedCode = false;
|
|
113
|
-
pushCurrentBlock();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (inFencedCode) {
|
|
120
|
-
currentLines.push(line);
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Check table line
|
|
125
|
-
const isTableLine = /^\s*\|.*\|\s*$/.test(line);
|
|
126
|
-
if (isTableLine) {
|
|
127
|
-
if (currentBlockType !== "table" && currentLines.length > 0) {
|
|
128
|
-
pushCurrentBlock();
|
|
129
|
-
}
|
|
130
|
-
currentBlockType = "table";
|
|
131
|
-
currentLines.push(line);
|
|
132
|
-
continue;
|
|
133
|
-
} else if (currentBlockType === "table") {
|
|
134
|
-
pushCurrentBlock();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check list item line
|
|
138
|
-
const isListLine = /^\s*([*+-]|\d+\.)\s+/.test(line);
|
|
139
|
-
if (isListLine) {
|
|
140
|
-
if (currentBlockType !== "list" && currentBlockType !== "paragraph" && currentLines.length > 0) {
|
|
141
|
-
pushCurrentBlock();
|
|
142
|
-
}
|
|
143
|
-
currentBlockType = "list";
|
|
144
|
-
currentLines.push(line);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Check empty line
|
|
149
|
-
if (line.trim().length === 0) {
|
|
150
|
-
if (currentLines.length > 0) {
|
|
151
|
-
pushCurrentBlock();
|
|
152
|
-
}
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
currentLines.push(line);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
pushCurrentBlock();
|
|
160
|
-
return mediumBlocks;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 3. SMALL LEVEL: Sentence & Smart AST/Table Chunk Extractor
|
|
164
|
-
export function createSmallChunks(mediumBlock, sectionId, docId) {
|
|
165
|
-
const content = mediumBlock.content;
|
|
166
|
-
const tokenCount = mediumBlock.token_count || estimateTokens(content);
|
|
167
|
-
const smallChunks = [];
|
|
168
|
-
let smallIdx = 0;
|
|
169
|
-
|
|
170
|
-
function makeChunk(chunkText, extraMeta = {}) {
|
|
171
|
-
if (!chunkText || chunkText.trim().length === 0) return;
|
|
172
|
-
smallChunks.push({
|
|
173
|
-
id: `${mediumBlock.id}_s${smallIdx++}`,
|
|
174
|
-
medium_id: mediumBlock.id,
|
|
175
|
-
section_id: sectionId,
|
|
176
|
-
doc_id: docId,
|
|
177
|
-
content: chunkText.trim(),
|
|
178
|
-
breadcrumbs: mediumBlock.breadcrumbs,
|
|
179
|
-
token_count: estimateTokens(chunkText),
|
|
180
|
-
...extraMeta,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// RULE FOR TABLES
|
|
185
|
-
if (mediumBlock.block_type === "table") {
|
|
186
|
-
if (tokenCount <= 350) {
|
|
187
|
-
makeChunk(content);
|
|
188
|
-
return smallChunks;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const lines = content.split("\n");
|
|
192
|
-
const headerLines = [];
|
|
193
|
-
const dataLines = [];
|
|
194
|
-
|
|
195
|
-
for (const l of lines) {
|
|
196
|
-
if (headerLines.length < 2 && (l.includes("|---") || l.includes("|:--") || headerLines.length === 0)) {
|
|
197
|
-
headerLines.push(l);
|
|
198
|
-
} else {
|
|
199
|
-
dataLines.push(l);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const headerStr = headerLines.join("\n");
|
|
204
|
-
const chunkSize = 8;
|
|
205
|
-
for (let i = 0; i < dataLines.length; i += chunkSize) {
|
|
206
|
-
const rowBatch = dataLines.slice(i, i + chunkSize);
|
|
207
|
-
const tableChunkText = `${headerStr}\n${rowBatch.join("\n")}`;
|
|
208
|
-
makeChunk(tableChunkText);
|
|
209
|
-
}
|
|
210
|
-
return smallChunks;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// RULE FOR CODE BLOCKS
|
|
214
|
-
if (mediumBlock.block_type === "code") {
|
|
215
|
-
if (tokenCount <= 350) {
|
|
216
|
-
makeChunk(content);
|
|
217
|
-
return smallChunks;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const codeLines = content.split("\n");
|
|
221
|
-
const firstLine = codeLines[0] || "";
|
|
222
|
-
const lastLine = codeLines[codeLines.length - 1] || "";
|
|
223
|
-
const isFenced = firstLine.startsWith("```") || firstLine.startsWith("~~~");
|
|
224
|
-
const fenceHeader = isFenced ? firstLine : "";
|
|
225
|
-
const fenceFooter = isFenced && (lastLine.startsWith("```") || lastLine.startsWith("~~~")) ? lastLine : "";
|
|
226
|
-
|
|
227
|
-
const bodyLines = isFenced ? codeLines.slice(1, -1) : codeLines;
|
|
228
|
-
|
|
229
|
-
const astBlocks = [];
|
|
230
|
-
let currentAstBlock = [];
|
|
231
|
-
|
|
232
|
-
for (const line of bodyLines) {
|
|
233
|
-
const isBoundary = /^\s*(?:export\s+|async\s+)?(?:function|class|def|pub\s+fn|fn|struct|interface|enum)\s+/.test(line);
|
|
234
|
-
if (isBoundary && currentAstBlock.length > 0) {
|
|
235
|
-
astBlocks.push(currentAstBlock.join("\n"));
|
|
236
|
-
currentAstBlock = [];
|
|
237
|
-
}
|
|
238
|
-
currentAstBlock.push(line);
|
|
239
|
-
}
|
|
240
|
-
if (currentAstBlock.length > 0) {
|
|
241
|
-
astBlocks.push(currentAstBlock.join("\n"));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
for (const block of astBlocks) {
|
|
245
|
-
const fullChunk = fenceHeader ? `${fenceHeader}\n${block}\n${fenceFooter}` : block;
|
|
246
|
-
makeChunk(fullChunk);
|
|
247
|
-
}
|
|
248
|
-
return smallChunks;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// STANDARD SENTENCE SEGMENTATION WITH SAFE SENTENCE-WINDOWING (100-180 TOKENS, 1 SENTENCE OVERLAP)
|
|
252
|
-
const sentences = splitSentencesMultilingual(content);
|
|
253
|
-
if (sentences.length === 0 || tokenCount <= 180) {
|
|
254
|
-
makeChunk(content);
|
|
255
|
-
return smallChunks;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const TARGET_WINDOW_TOKENS = 150;
|
|
259
|
-
let currentWindow = [];
|
|
260
|
-
let currentTokens = 0;
|
|
261
|
-
|
|
262
|
-
for (let i = 0; i < sentences.length; i++) {
|
|
263
|
-
const sentence = sentences[i];
|
|
264
|
-
const sTokens = estimateTokens(sentence);
|
|
265
|
-
|
|
266
|
-
if (currentTokens + sTokens > TARGET_WINDOW_TOKENS && currentWindow.length > 0) {
|
|
267
|
-
makeChunk(currentWindow.join(" "));
|
|
268
|
-
|
|
269
|
-
// Safe Overlap: Keep the last sentence of the previous window if available
|
|
270
|
-
const lastSentence = currentWindow[currentWindow.length - 1];
|
|
271
|
-
currentWindow = [lastSentence, sentence];
|
|
272
|
-
currentTokens = estimateTokens(lastSentence) + sTokens;
|
|
273
|
-
} else {
|
|
274
|
-
currentWindow.push(sentence);
|
|
275
|
-
currentTokens += sTokens;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (currentWindow.length > 0) {
|
|
280
|
-
makeChunk(currentWindow.join(" "));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return smallChunks;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function createMicroChunks(section, sectionId, docId) {
|
|
287
|
-
const mediumBlocks = extractMediumBlocks(section, sectionId, docId);
|
|
288
|
-
const microChunks = [];
|
|
289
|
-
for (const med of mediumBlocks) {
|
|
290
|
-
const smalls = createSmallChunks(med, sectionId, docId);
|
|
291
|
-
microChunks.push(...smalls);
|
|
292
|
-
}
|
|
293
|
-
return microChunks;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// 4. TRIPLE HIERARCHY BUILDER: Big -> Medium -> Small Reference Tree
|
|
297
|
-
export function buildTripleHierarchy(markdown, docId, docTitle = "Document") {
|
|
298
|
-
const sectionsData = parseSections(markdown, docTitle);
|
|
299
|
-
const sections = [];
|
|
300
|
-
const mediumChunks = [];
|
|
301
|
-
const microChunks = [];
|
|
302
|
-
const tocTree = [];
|
|
303
|
-
|
|
304
|
-
sectionsData.forEach((sec, idx) => {
|
|
305
|
-
const sectionId = `${docId}_s${idx}`;
|
|
306
|
-
const sectionObj = {
|
|
307
|
-
id: sectionId,
|
|
308
|
-
doc_id: docId,
|
|
309
|
-
heading: sec.heading,
|
|
310
|
-
breadcrumbs: sec.breadcrumbs,
|
|
311
|
-
content: sec.content,
|
|
312
|
-
token_count: sec.token_count,
|
|
313
|
-
};
|
|
314
|
-
sections.push(sectionObj);
|
|
315
|
-
|
|
316
|
-
tocTree.push({
|
|
317
|
-
section_id: sectionId,
|
|
318
|
-
heading: sec.heading,
|
|
319
|
-
breadcrumbs: sec.breadcrumbs,
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const mediums = extractMediumBlocks(sec, sectionId, docId);
|
|
323
|
-
mediumChunks.push(...mediums);
|
|
324
|
-
|
|
325
|
-
for (const med of mediums) {
|
|
326
|
-
const smalls = createSmallChunks(med, sectionId, docId);
|
|
327
|
-
microChunks.push(...smalls);
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
toc: JSON.stringify(tocTree),
|
|
333
|
-
sections,
|
|
334
|
-
mediumChunks,
|
|
335
|
-
microChunks,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
1
|
+
import { splitSentencesMultilingual } from "./sentence_segmenter.js";
|
|
2
|
+
|
|
3
|
+
export function estimateTokens(text) {
|
|
4
|
+
if (!text) return 0;
|
|
5
|
+
return Math.ceil(text.length / 4);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// 1. BIG LEVEL: Heading & Section Hierarchy Parser
|
|
9
|
+
export function parseSections(markdown, docTitle = "Document") {
|
|
10
|
+
const lines = markdown.split("\n");
|
|
11
|
+
const sections = [];
|
|
12
|
+
const headerStack = [];
|
|
13
|
+
|
|
14
|
+
let currentHeading = docTitle;
|
|
15
|
+
let currentLines = [];
|
|
16
|
+
|
|
17
|
+
function pushCurrentSection() {
|
|
18
|
+
const content = currentLines.join("\n").trim();
|
|
19
|
+
if (content.length > 0) {
|
|
20
|
+
const breadcrumbs = [docTitle, ...headerStack.map((h) => h.text)].join(" > ");
|
|
21
|
+
sections.push({
|
|
22
|
+
heading: currentHeading,
|
|
23
|
+
breadcrumbs,
|
|
24
|
+
content,
|
|
25
|
+
token_count: estimateTokens(content),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
currentLines = [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
33
|
+
|
|
34
|
+
if (headingMatch) {
|
|
35
|
+
pushCurrentSection();
|
|
36
|
+
|
|
37
|
+
const level = headingMatch[1].length;
|
|
38
|
+
const text = headingMatch[2].trim();
|
|
39
|
+
|
|
40
|
+
while (headerStack.length > 0 && headerStack[headerStack.length - 1].level >= level) {
|
|
41
|
+
headerStack.pop();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
headerStack.push({ level, text });
|
|
45
|
+
currentHeading = text;
|
|
46
|
+
} else {
|
|
47
|
+
currentLines.push(line);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pushCurrentSection();
|
|
52
|
+
|
|
53
|
+
if (sections.length === 0) {
|
|
54
|
+
sections.push({
|
|
55
|
+
heading: docTitle,
|
|
56
|
+
breadcrumbs: docTitle,
|
|
57
|
+
content: markdown.trim(),
|
|
58
|
+
token_count: estimateTokens(markdown),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return sections;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. MEDIUM LEVEL: Logical Paragraphs & Structural Block Extractor
|
|
66
|
+
export function extractMediumBlocks(section, sectionId, docId) {
|
|
67
|
+
const content = section.content;
|
|
68
|
+
if (!content || content.trim().length === 0) return [];
|
|
69
|
+
|
|
70
|
+
const lines = content.split("\n");
|
|
71
|
+
const mediumBlocks = [];
|
|
72
|
+
let blockIndex = 0;
|
|
73
|
+
|
|
74
|
+
let currentLines = [];
|
|
75
|
+
let currentBlockType = "paragraph"; // 'paragraph', 'code', 'table', 'list', 'blockquote'
|
|
76
|
+
|
|
77
|
+
function pushCurrentBlock() {
|
|
78
|
+
const blockContent = currentLines.join("\n").trim();
|
|
79
|
+
if (blockContent.length > 0) {
|
|
80
|
+
mediumBlocks.push({
|
|
81
|
+
id: `${sectionId}_m${blockIndex++}`,
|
|
82
|
+
section_id: sectionId,
|
|
83
|
+
doc_id: docId,
|
|
84
|
+
breadcrumbs: section.breadcrumbs,
|
|
85
|
+
content: blockContent,
|
|
86
|
+
block_type: currentBlockType,
|
|
87
|
+
token_count: estimateTokens(blockContent),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
currentLines = [];
|
|
91
|
+
currentBlockType = "paragraph";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let inFencedCode = false;
|
|
95
|
+
let codeFenceMarker = "";
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
const line = lines[i];
|
|
99
|
+
|
|
100
|
+
// Check code fence
|
|
101
|
+
const fenceMatch = line.match(/^(\s*)(```|~~~)/);
|
|
102
|
+
if (fenceMatch) {
|
|
103
|
+
if (!inFencedCode) {
|
|
104
|
+
if (currentLines.length > 0) pushCurrentBlock();
|
|
105
|
+
inFencedCode = true;
|
|
106
|
+
codeFenceMarker = fenceMatch[2];
|
|
107
|
+
currentBlockType = "code";
|
|
108
|
+
currentLines.push(line);
|
|
109
|
+
} else {
|
|
110
|
+
currentLines.push(line);
|
|
111
|
+
if (line.includes(codeFenceMarker)) {
|
|
112
|
+
inFencedCode = false;
|
|
113
|
+
pushCurrentBlock();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (inFencedCode) {
|
|
120
|
+
currentLines.push(line);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check table line
|
|
125
|
+
const isTableLine = /^\s*\|.*\|\s*$/.test(line);
|
|
126
|
+
if (isTableLine) {
|
|
127
|
+
if (currentBlockType !== "table" && currentLines.length > 0) {
|
|
128
|
+
pushCurrentBlock();
|
|
129
|
+
}
|
|
130
|
+
currentBlockType = "table";
|
|
131
|
+
currentLines.push(line);
|
|
132
|
+
continue;
|
|
133
|
+
} else if (currentBlockType === "table") {
|
|
134
|
+
pushCurrentBlock();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check list item line
|
|
138
|
+
const isListLine = /^\s*([*+-]|\d+\.)\s+/.test(line);
|
|
139
|
+
if (isListLine) {
|
|
140
|
+
if (currentBlockType !== "list" && currentBlockType !== "paragraph" && currentLines.length > 0) {
|
|
141
|
+
pushCurrentBlock();
|
|
142
|
+
}
|
|
143
|
+
currentBlockType = "list";
|
|
144
|
+
currentLines.push(line);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check empty line
|
|
149
|
+
if (line.trim().length === 0) {
|
|
150
|
+
if (currentLines.length > 0) {
|
|
151
|
+
pushCurrentBlock();
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
currentLines.push(line);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
pushCurrentBlock();
|
|
160
|
+
return mediumBlocks;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 3. SMALL LEVEL: Sentence & Smart AST/Table Chunk Extractor
|
|
164
|
+
export function createSmallChunks(mediumBlock, sectionId, docId) {
|
|
165
|
+
const content = mediumBlock.content;
|
|
166
|
+
const tokenCount = mediumBlock.token_count || estimateTokens(content);
|
|
167
|
+
const smallChunks = [];
|
|
168
|
+
let smallIdx = 0;
|
|
169
|
+
|
|
170
|
+
function makeChunk(chunkText, extraMeta = {}) {
|
|
171
|
+
if (!chunkText || chunkText.trim().length === 0) return;
|
|
172
|
+
smallChunks.push({
|
|
173
|
+
id: `${mediumBlock.id}_s${smallIdx++}`,
|
|
174
|
+
medium_id: mediumBlock.id,
|
|
175
|
+
section_id: sectionId,
|
|
176
|
+
doc_id: docId,
|
|
177
|
+
content: chunkText.trim(),
|
|
178
|
+
breadcrumbs: mediumBlock.breadcrumbs,
|
|
179
|
+
token_count: estimateTokens(chunkText),
|
|
180
|
+
...extraMeta,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// RULE FOR TABLES
|
|
185
|
+
if (mediumBlock.block_type === "table") {
|
|
186
|
+
if (tokenCount <= 350) {
|
|
187
|
+
makeChunk(content);
|
|
188
|
+
return smallChunks;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const lines = content.split("\n");
|
|
192
|
+
const headerLines = [];
|
|
193
|
+
const dataLines = [];
|
|
194
|
+
|
|
195
|
+
for (const l of lines) {
|
|
196
|
+
if (headerLines.length < 2 && (l.includes("|---") || l.includes("|:--") || headerLines.length === 0)) {
|
|
197
|
+
headerLines.push(l);
|
|
198
|
+
} else {
|
|
199
|
+
dataLines.push(l);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const headerStr = headerLines.join("\n");
|
|
204
|
+
const chunkSize = 8;
|
|
205
|
+
for (let i = 0; i < dataLines.length; i += chunkSize) {
|
|
206
|
+
const rowBatch = dataLines.slice(i, i + chunkSize);
|
|
207
|
+
const tableChunkText = `${headerStr}\n${rowBatch.join("\n")}`;
|
|
208
|
+
makeChunk(tableChunkText);
|
|
209
|
+
}
|
|
210
|
+
return smallChunks;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// RULE FOR CODE BLOCKS
|
|
214
|
+
if (mediumBlock.block_type === "code") {
|
|
215
|
+
if (tokenCount <= 350) {
|
|
216
|
+
makeChunk(content);
|
|
217
|
+
return smallChunks;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const codeLines = content.split("\n");
|
|
221
|
+
const firstLine = codeLines[0] || "";
|
|
222
|
+
const lastLine = codeLines[codeLines.length - 1] || "";
|
|
223
|
+
const isFenced = firstLine.startsWith("```") || firstLine.startsWith("~~~");
|
|
224
|
+
const fenceHeader = isFenced ? firstLine : "";
|
|
225
|
+
const fenceFooter = isFenced && (lastLine.startsWith("```") || lastLine.startsWith("~~~")) ? lastLine : "";
|
|
226
|
+
|
|
227
|
+
const bodyLines = isFenced ? codeLines.slice(1, -1) : codeLines;
|
|
228
|
+
|
|
229
|
+
const astBlocks = [];
|
|
230
|
+
let currentAstBlock = [];
|
|
231
|
+
|
|
232
|
+
for (const line of bodyLines) {
|
|
233
|
+
const isBoundary = /^\s*(?:export\s+|async\s+)?(?:function|class|def|pub\s+fn|fn|struct|interface|enum)\s+/.test(line);
|
|
234
|
+
if (isBoundary && currentAstBlock.length > 0) {
|
|
235
|
+
astBlocks.push(currentAstBlock.join("\n"));
|
|
236
|
+
currentAstBlock = [];
|
|
237
|
+
}
|
|
238
|
+
currentAstBlock.push(line);
|
|
239
|
+
}
|
|
240
|
+
if (currentAstBlock.length > 0) {
|
|
241
|
+
astBlocks.push(currentAstBlock.join("\n"));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const block of astBlocks) {
|
|
245
|
+
const fullChunk = fenceHeader ? `${fenceHeader}\n${block}\n${fenceFooter}` : block;
|
|
246
|
+
makeChunk(fullChunk);
|
|
247
|
+
}
|
|
248
|
+
return smallChunks;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// STANDARD SENTENCE SEGMENTATION WITH SAFE SENTENCE-WINDOWING (100-180 TOKENS, 1 SENTENCE OVERLAP)
|
|
252
|
+
const sentences = splitSentencesMultilingual(content);
|
|
253
|
+
if (sentences.length === 0 || tokenCount <= 180) {
|
|
254
|
+
makeChunk(content);
|
|
255
|
+
return smallChunks;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const TARGET_WINDOW_TOKENS = 150;
|
|
259
|
+
let currentWindow = [];
|
|
260
|
+
let currentTokens = 0;
|
|
261
|
+
|
|
262
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
263
|
+
const sentence = sentences[i];
|
|
264
|
+
const sTokens = estimateTokens(sentence);
|
|
265
|
+
|
|
266
|
+
if (currentTokens + sTokens > TARGET_WINDOW_TOKENS && currentWindow.length > 0) {
|
|
267
|
+
makeChunk(currentWindow.join(" "));
|
|
268
|
+
|
|
269
|
+
// Safe Overlap: Keep the last sentence of the previous window if available
|
|
270
|
+
const lastSentence = currentWindow[currentWindow.length - 1];
|
|
271
|
+
currentWindow = [lastSentence, sentence];
|
|
272
|
+
currentTokens = estimateTokens(lastSentence) + sTokens;
|
|
273
|
+
} else {
|
|
274
|
+
currentWindow.push(sentence);
|
|
275
|
+
currentTokens += sTokens;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (currentWindow.length > 0) {
|
|
280
|
+
makeChunk(currentWindow.join(" "));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return smallChunks;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function createMicroChunks(section, sectionId, docId) {
|
|
287
|
+
const mediumBlocks = extractMediumBlocks(section, sectionId, docId);
|
|
288
|
+
const microChunks = [];
|
|
289
|
+
for (const med of mediumBlocks) {
|
|
290
|
+
const smalls = createSmallChunks(med, sectionId, docId);
|
|
291
|
+
microChunks.push(...smalls);
|
|
292
|
+
}
|
|
293
|
+
return microChunks;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 4. TRIPLE HIERARCHY BUILDER: Big -> Medium -> Small Reference Tree
|
|
297
|
+
export function buildTripleHierarchy(markdown, docId, docTitle = "Document") {
|
|
298
|
+
const sectionsData = parseSections(markdown, docTitle);
|
|
299
|
+
const sections = [];
|
|
300
|
+
const mediumChunks = [];
|
|
301
|
+
const microChunks = [];
|
|
302
|
+
const tocTree = [];
|
|
303
|
+
|
|
304
|
+
sectionsData.forEach((sec, idx) => {
|
|
305
|
+
const sectionId = `${docId}_s${idx}`;
|
|
306
|
+
const sectionObj = {
|
|
307
|
+
id: sectionId,
|
|
308
|
+
doc_id: docId,
|
|
309
|
+
heading: sec.heading,
|
|
310
|
+
breadcrumbs: sec.breadcrumbs,
|
|
311
|
+
content: sec.content,
|
|
312
|
+
token_count: sec.token_count,
|
|
313
|
+
};
|
|
314
|
+
sections.push(sectionObj);
|
|
315
|
+
|
|
316
|
+
tocTree.push({
|
|
317
|
+
section_id: sectionId,
|
|
318
|
+
heading: sec.heading,
|
|
319
|
+
breadcrumbs: sec.breadcrumbs,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const mediums = extractMediumBlocks(sec, sectionId, docId);
|
|
323
|
+
mediumChunks.push(...mediums);
|
|
324
|
+
|
|
325
|
+
for (const med of mediums) {
|
|
326
|
+
const smalls = createSmallChunks(med, sectionId, docId);
|
|
327
|
+
microChunks.push(...smalls);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
toc: JSON.stringify(tocTree),
|
|
333
|
+
sections,
|
|
334
|
+
mediumChunks,
|
|
335
|
+
microChunks,
|
|
336
|
+
};
|
|
337
|
+
}
|