@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.
@@ -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
+ }