@mastra/rag 0.0.0-commonjs-20250227130920

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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +23 -0
  2. package/CHANGELOG.md +1151 -0
  3. package/LICENSE +44 -0
  4. package/README.md +26 -0
  5. package/dist/_tsup-dts-rollup.d.cts +620 -0
  6. package/dist/_tsup-dts-rollup.d.ts +620 -0
  7. package/dist/index.cjs +2524 -0
  8. package/dist/index.d.cts +22 -0
  9. package/dist/index.d.ts +22 -0
  10. package/dist/index.js +2505 -0
  11. package/docker-compose.yaml +22 -0
  12. package/eslint.config.js +6 -0
  13. package/package.json +72 -0
  14. package/src/document/document.test.ts +985 -0
  15. package/src/document/document.ts +281 -0
  16. package/src/document/index.ts +2 -0
  17. package/src/document/transformers/character.ts +279 -0
  18. package/src/document/transformers/html.ts +346 -0
  19. package/src/document/transformers/json.ts +265 -0
  20. package/src/document/transformers/latex.ts +19 -0
  21. package/src/document/transformers/markdown.ts +244 -0
  22. package/src/document/transformers/text.ts +134 -0
  23. package/src/document/transformers/token.ts +148 -0
  24. package/src/document/transformers/transformer.ts +5 -0
  25. package/src/document/types.ts +103 -0
  26. package/src/graph-rag/index.test.ts +235 -0
  27. package/src/graph-rag/index.ts +306 -0
  28. package/src/index.ts +6 -0
  29. package/src/rerank/index.test.ts +150 -0
  30. package/src/rerank/index.ts +154 -0
  31. package/src/tools/document-chunker.ts +30 -0
  32. package/src/tools/graph-rag.ts +117 -0
  33. package/src/tools/index.ts +3 -0
  34. package/src/tools/vector-query.ts +90 -0
  35. package/src/utils/default-settings.ts +33 -0
  36. package/src/utils/index.ts +2 -0
  37. package/src/utils/vector-prompts.ts +729 -0
  38. package/src/utils/vector-search.ts +41 -0
  39. package/tsconfig.json +5 -0
  40. package/vitest.config.ts +11 -0
@@ -0,0 +1,985 @@
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
+ import { embedMany } from 'ai';
3
+ import { describe, it, expect } from 'vitest';
4
+
5
+ import { MDocument } from './document';
6
+ import { Language } from './types';
7
+
8
+ const sampleMarkdown = `
9
+ # Complete Guide to Modern Web Development
10
+ ## Introduction
11
+ Welcome to our comprehensive guide on modern web development. This resource covers essential concepts, best practices, and tools that every developer should know in 2024.
12
+
13
+ ### Who This Guide Is For
14
+ - Beginning developers looking to establish a solid foundation
15
+ - Intermediate developers wanting to modernize their skillset
16
+ - Senior developers seeking a refresher on current best practices
17
+ `;
18
+
19
+ const openai = createOpenAI({
20
+ apiKey: process.env.OPENAI_API_KEY,
21
+ });
22
+
23
+ describe('MDocument', () => {
24
+ describe('basics', () => {
25
+ let chunks: MDocument['chunks'];
26
+ let doc: MDocument;
27
+ it('initialization', () => {
28
+ const doc = new MDocument({ docs: [{ text: 'test' }], type: 'text' });
29
+ expect(doc.getDocs()).toHaveLength(1);
30
+ expect(doc.getText()?.[0]).toBe('test');
31
+ });
32
+
33
+ it('initialization with array', () => {
34
+ doc = new MDocument({ docs: [{ text: 'test' }, { text: 'test2' }], type: 'text' });
35
+ expect(doc.getDocs()).toHaveLength(2);
36
+ expect(doc.getDocs()[0]?.text).toBe('test');
37
+ expect(doc.getDocs()[1]?.text).toBe('test2');
38
+ });
39
+
40
+ it('chunk - metadata title', async () => {
41
+ const doc = MDocument.fromMarkdown(sampleMarkdown);
42
+
43
+ chunks = await doc.chunk({
44
+ size: 1500,
45
+ overlap: 0,
46
+ separator: `\n`,
47
+ extract: {
48
+ keywords: true,
49
+ },
50
+ });
51
+
52
+ expect(doc.getMetadata()?.[0]).toBeTruthy();
53
+ expect(chunks).toBeInstanceOf(Array);
54
+ }, 15000);
55
+
56
+ it('embed - create embedding from chunk', async () => {
57
+ const embeddings = await embedMany({
58
+ values: chunks.map(chunk => chunk.text),
59
+ model: openai.embedding('text-embedding-3-small'),
60
+ });
61
+
62
+ console.log(embeddings);
63
+ expect(embeddings).toBeDefined();
64
+ });
65
+ });
66
+
67
+ describe('chunkCharacter', () => {
68
+ it('should split text on simple separator', async () => {
69
+ const text = 'Hello world\n\nHow are you\n\nI am fine';
70
+
71
+ const doc = MDocument.fromText(text, { meta: 'data' });
72
+
73
+ await doc.chunk({
74
+ strategy: 'character',
75
+ separator: '\n\n',
76
+ isSeparatorRegex: false,
77
+ size: 50,
78
+ overlap: 5,
79
+ });
80
+
81
+ const chunks = doc.getDocs();
82
+
83
+ expect(chunks).toHaveLength(3);
84
+ expect(chunks?.[0]?.text).toBe('Hello world');
85
+ expect(chunks?.[1]?.text).toBe('How are you');
86
+ expect(chunks?.[2]?.text).toBe('I am fine');
87
+ });
88
+
89
+ it('should handle regex separator', async () => {
90
+ const text = 'Hello world\n\nHow are you';
91
+
92
+ const doc = MDocument.fromText(text, { meta: 'data' });
93
+
94
+ await doc.chunk({
95
+ strategy: 'character',
96
+ separator: '\\s+',
97
+ isSeparatorRegex: true,
98
+ size: 50,
99
+ overlap: 5,
100
+ });
101
+
102
+ expect(doc.getText().join(' ')).toBe('Hello world How are you');
103
+ });
104
+
105
+ it('should keep separator when specified', async () => {
106
+ const text = 'Hello\n\nWorld';
107
+
108
+ const doc = MDocument.fromText(text, { meta: 'data' });
109
+
110
+ await doc.chunk({
111
+ strategy: 'character',
112
+ separator: '\n\n',
113
+ isSeparatorRegex: false,
114
+ size: 50,
115
+ overlap: 5,
116
+ keepSeparator: 'end',
117
+ });
118
+ const chunks = doc.getText();
119
+
120
+ expect(chunks[0]).toBe('Hello\n\n');
121
+ expect(chunks[1]).toBe('World');
122
+ });
123
+
124
+ describe('separator handling', () => {
125
+ it('should keep separator at end when specified', async () => {
126
+ const text = 'Hello\n\nWorld';
127
+
128
+ const doc = MDocument.fromText(text, { meta: 'data' });
129
+
130
+ await doc.chunk({
131
+ strategy: 'character',
132
+ separator: '\n\n',
133
+ isSeparatorRegex: false,
134
+ size: 50,
135
+ overlap: 5,
136
+ keepSeparator: 'end',
137
+ });
138
+
139
+ const chunks = doc.getText();
140
+
141
+ expect(chunks).toHaveLength(2);
142
+ expect(chunks[0]).toBe('Hello\n\n');
143
+ expect(chunks[1]).toBe('World');
144
+ });
145
+
146
+ it('should keep separator at start when specified', async () => {
147
+ const text = 'Hello\n\nWorld\n\nTest';
148
+
149
+ const doc = MDocument.fromText(text, { meta: 'data' });
150
+
151
+ await doc.chunk({
152
+ strategy: 'character',
153
+ separator: '\n\n',
154
+ isSeparatorRegex: false,
155
+ size: 50,
156
+ overlap: 5,
157
+ keepSeparator: 'start',
158
+ });
159
+
160
+ const chunks = doc.getText();
161
+
162
+ expect(chunks).toHaveLength(3);
163
+ expect(chunks[0]).toBe('Hello');
164
+ expect(chunks[1]).toBe('\n\nWorld');
165
+ expect(chunks[2]).toBe('\n\nTest');
166
+ });
167
+
168
+ it('should handle multiple consecutive separators', async () => {
169
+ const text = 'Hello\n\n\n\nWorld';
170
+
171
+ const doc = MDocument.fromText(text, { meta: 'data' });
172
+
173
+ await doc.chunk({
174
+ strategy: 'character',
175
+ separator: '\n\n',
176
+ isSeparatorRegex: false,
177
+ size: 50,
178
+ overlap: 5,
179
+ keepSeparator: 'end',
180
+ });
181
+
182
+ const chunks = doc.getText();
183
+
184
+ expect(chunks.length).toBeGreaterThan(0);
185
+ expect(chunks.join('')).toBe(text);
186
+ });
187
+
188
+ it('should handle text ending with separator', async () => {
189
+ const text = 'Hello\n\nWorld\n\n';
190
+
191
+ const doc = MDocument.fromText(text, { meta: 'data' });
192
+
193
+ await doc.chunk({
194
+ strategy: 'character',
195
+ separator: '\n\n',
196
+ isSeparatorRegex: false,
197
+ size: 50,
198
+ overlap: 5,
199
+ keepSeparator: 'end',
200
+ });
201
+
202
+ const chunks = doc.getText();
203
+
204
+ expect(chunks.length).toBeGreaterThan(0);
205
+ expect(chunks.join('')).toBe(text);
206
+ });
207
+
208
+ it('should handle text starting with separator', async () => {
209
+ const text = '\n\nHello\n\nWorld';
210
+
211
+ const doc = MDocument.fromText(text, { meta: 'data' });
212
+
213
+ await doc.chunk({
214
+ strategy: 'character',
215
+ separator: '\n\n',
216
+ isSeparatorRegex: false,
217
+ size: 50,
218
+ overlap: 5,
219
+ keepSeparator: 'start',
220
+ });
221
+
222
+ const chunks = doc.getText();
223
+
224
+ expect(chunks.length).toBeGreaterThan(0);
225
+ expect(chunks.join('')).toBe(text);
226
+ });
227
+ });
228
+ });
229
+
230
+ describe('chunkRecursive', () => {
231
+ it('chunkRecursive', async () => {
232
+ const text =
233
+ 'Hello world.\n\nThis is a test of the recursive splitting system.\nIt should handle multiple lines and different separators appropriately.';
234
+
235
+ const doc = MDocument.fromText(text, { meta: 'data' });
236
+
237
+ await doc.chunk({
238
+ strategy: 'recursive',
239
+ separators: ['\n\n', '\n', ' ', ''],
240
+ isSeparatorRegex: false,
241
+ size: 50,
242
+ overlap: 5,
243
+ });
244
+
245
+ expect(doc.getDocs()?.length).toBeGreaterThan(1);
246
+
247
+ doc.getText()?.forEach(t => {
248
+ expect(t.length).toBeLessThanOrEqual(50);
249
+ });
250
+ });
251
+
252
+ it('chunkRecursive - language options', async () => {
253
+ const tsCode = `
254
+ interface User {
255
+ name: string;
256
+ age: number;
257
+ }
258
+
259
+ function greet(user: User) {
260
+ console.log(\`Hello \${user.name}\`);
261
+ }
262
+ `;
263
+
264
+ const doc = MDocument.fromText(tsCode, { meta: 'data' });
265
+
266
+ await doc.chunk({
267
+ size: 50,
268
+ overlap: 5,
269
+ language: Language.TS,
270
+ });
271
+
272
+ expect(doc.getDocs().length).toBeGreaterThan(1);
273
+ expect(doc.getText().some(chunk => chunk.includes('interface'))).toBe(true);
274
+ expect(doc.getText().some(chunk => chunk.includes('function'))).toBe(true);
275
+ });
276
+
277
+ it('should throw error for unsupported language', async () => {
278
+ const doc = MDocument.fromText('tsCode', { meta: 'data' });
279
+
280
+ await expect(
281
+ doc.chunk({
282
+ size: 50,
283
+ overlap: 5,
284
+ language: 'invalid-language' as any,
285
+ }),
286
+ ).rejects.toThrow();
287
+ });
288
+
289
+ it('should maintain context with overlap', async () => {
290
+ const text = 'This is a test.\nIt has multiple lines.\nEach line should be handled properly.';
291
+ const doc = MDocument.fromText(text, { meta: 'data' });
292
+
293
+ await doc.chunk();
294
+
295
+ for (let i = 1; i < doc.getDocs().length; i++) {
296
+ const prevChunk = doc.getDocs()[i - 1]?.text;
297
+ const currentChunk = doc.getDocs()?.[i]?.text;
298
+
299
+ const hasOverlap = prevChunk?.split(' ').some(word => currentChunk?.includes(word));
300
+
301
+ expect(hasOverlap).toBe(true);
302
+ }
303
+ });
304
+ });
305
+
306
+ describe('chunkHTML', () => {
307
+ it('should split HTML with headers correctly', async () => {
308
+ const html = `
309
+ <html>
310
+ <body>
311
+ <h1>Main Title</h1>
312
+ <p>Main content.</p>
313
+ <h2>Section 1</h2>
314
+ <p>Section 1 content.</p>
315
+ <h3>Subsection 1.1</h3>
316
+ <p>Subsection content.</p>
317
+ </body>
318
+ </html>
319
+ `;
320
+
321
+ const doc = MDocument.fromHTML(html, { meta: 'data' });
322
+
323
+ await doc.chunk({
324
+ strategy: 'html',
325
+ headers: [
326
+ ['h1', 'Header 1'],
327
+ ['h2', 'Header 2'],
328
+ ['h3', 'Header 3'],
329
+ ],
330
+ });
331
+
332
+ const docs = doc.getDocs();
333
+ expect(docs.length).toBeGreaterThan(1);
334
+ expect(docs?.[0]?.metadata?.['Header 1']).toBe('Main Title');
335
+ expect(docs?.[1]?.metadata?.['Header 2']).toBe('Section 1');
336
+ });
337
+
338
+ it('should handle nested content', async () => {
339
+ const html = `
340
+ <html>
341
+ <body>
342
+ <h1>Title</h1>
343
+ <div>
344
+ <p>Nested content.</p>
345
+ <div>
346
+ <p>Deeply nested content.</p>
347
+ </div>
348
+ </div>
349
+ </body>
350
+ </html>
351
+ `;
352
+
353
+ const doc = MDocument.fromHTML(html, { meta: 'data' });
354
+
355
+ await doc.chunk({
356
+ strategy: 'html',
357
+ headers: [
358
+ ['h1', 'Header 1'],
359
+ ['h2', 'Header 2'],
360
+ ['h3', 'Header 3'],
361
+ ],
362
+ });
363
+
364
+ const docs = doc.getDocs();
365
+ const mainSection = docs.find(doc => doc.metadata?.['Header 1'] === 'Title');
366
+ expect(mainSection?.text).toContain('Nested content');
367
+ expect(mainSection?.text).toContain('Deeply nested content');
368
+ });
369
+
370
+ it('should respect returnEachElement option', async () => {
371
+ const html = `
372
+ <html>
373
+ <body>
374
+ <h1>Title</h1>
375
+ <p>Paragraph 1</p>
376
+ <h1>Title</h1>
377
+ <p>Paragraph 2</p>
378
+ <h1>Title</h1>
379
+ <p>Paragraph 3</p>
380
+ </body>
381
+ </html>
382
+ `;
383
+
384
+ const doc = MDocument.fromHTML(html, { meta: 'data' });
385
+
386
+ await doc.chunk({
387
+ strategy: 'html',
388
+
389
+ returnEachLine: true,
390
+ headers: [
391
+ ['h1', 'Header 1'],
392
+ ['h2', 'Header 2'],
393
+ ['h3', 'Header 3'],
394
+ ],
395
+ });
396
+
397
+ const docs = doc.getDocs();
398
+
399
+ expect(docs.length).toBeGreaterThan(2);
400
+ docs.forEach(doc => {
401
+ expect(doc.metadata?.['Header 1']).toBe('Title');
402
+ });
403
+ });
404
+
405
+ it('should split HTML into sections', async () => {
406
+ const html = `
407
+ <html>
408
+ <body>
409
+ <h1>Document Title</h1>
410
+ <p>Introduction text.</p>
411
+ <h2>First Section</h2>
412
+ <p>First section content.</p>
413
+ <h2>Second Section</h2>
414
+ <p>Second section content.</p>
415
+ </body>
416
+ </html>
417
+ `;
418
+
419
+ const doc = MDocument.fromHTML(html, { meta: 'data' });
420
+
421
+ await doc.chunk({
422
+ strategy: 'html',
423
+ sections: [
424
+ ['h1', 'Header 1'],
425
+ ['h2', 'Header 2'],
426
+ ],
427
+ });
428
+ const docs = doc.getDocs();
429
+
430
+ expect(docs.length).toBe(3);
431
+ expect(docs?.[0]?.metadata?.['Header 1']).toBe('Document Title');
432
+ expect(docs?.[1]?.metadata?.['Header 2']).toBe('First Section');
433
+ });
434
+
435
+ it('should properly merge metadata', async () => {
436
+ const doc = new MDocument({
437
+ docs: [
438
+ {
439
+ text: `
440
+ <h1>Title 1</h1>
441
+ <p>Content 1</p>
442
+ `,
443
+ metadata: { source: 'doc1' },
444
+ },
445
+ {
446
+ text: `
447
+ <h1>Title 2</h1>
448
+ <p>Content 2</p>
449
+ `,
450
+ metadata: { source: 'doc2' },
451
+ },
452
+ ],
453
+ type: 'html',
454
+ });
455
+
456
+ await doc.chunk({
457
+ strategy: 'html',
458
+ sections: [
459
+ ['h1', 'Header 1'],
460
+ ['h2', 'Header 2'],
461
+ ],
462
+ });
463
+
464
+ doc.getDocs().forEach(doc => {
465
+ expect(doc?.metadata).toHaveProperty('source');
466
+ expect(doc?.metadata).toHaveProperty('Header 1');
467
+ });
468
+ });
469
+
470
+ it('should handle empty or invalid HTML', async () => {
471
+ const emptyHtml = '';
472
+ const invalidHtml = '<unclosed>test';
473
+ const noHeadersHtml = '<div>test</div>';
474
+
475
+ const doc1 = MDocument.fromHTML(emptyHtml, { meta: 'data' });
476
+ const doc2 = MDocument.fromHTML(invalidHtml, { meta: 'data' });
477
+ const doc3 = MDocument.fromHTML(noHeadersHtml, { meta: 'data' });
478
+
479
+ await doc1.chunk({
480
+ strategy: 'html',
481
+ headers: [
482
+ ['h1', 'Header 1'],
483
+ ['h2', 'Header 2'],
484
+ ],
485
+ });
486
+
487
+ await doc2.chunk({
488
+ strategy: 'html',
489
+ headers: [
490
+ ['h1', 'Header 1'],
491
+ ['h2', 'Header 2'],
492
+ ],
493
+ });
494
+
495
+ await doc3.chunk({
496
+ strategy: 'html',
497
+ headers: [
498
+ ['h1', 'Header 1'],
499
+ ['h2', 'Header 2'],
500
+ ],
501
+ });
502
+
503
+ expect(doc1.getDocs()).toHaveLength(0);
504
+ expect(doc2.getDocs()).toHaveLength(0);
505
+ expect(doc3.getDocs()).toHaveLength(0);
506
+ });
507
+
508
+ it('should handle complex nested header hierarchies', async () => {
509
+ const html = `
510
+ <html>
511
+ <body>
512
+ <h1>Main Title</h1>
513
+ <p>Main content</p>
514
+ <h2>Section 1</h2>
515
+ <p>Section 1 content</p>
516
+ <h3>Subsection 1.1</h3>
517
+ <p>Subsection 1.1 content</p>
518
+ <h2>Section 2</h2>
519
+ <h3>Subsection 2.1</h3>
520
+ <p>Subsection 2.1 content</p>
521
+ </body>
522
+ </html>
523
+ `;
524
+
525
+ const doc = MDocument.fromHTML(html, { meta: 'data' });
526
+ await doc.chunk({
527
+ strategy: 'html',
528
+ headers: [
529
+ ['h1', 'Header 1'],
530
+ ['h2', 'Header 2'],
531
+ ['h3', 'Header 3'],
532
+ ],
533
+ });
534
+
535
+ const docs = doc.getDocs();
536
+ expect(docs.length).toBeGreaterThan(3);
537
+ expect(docs.some(d => d.metadata?.['Header 1'] === 'Main Title')).toBe(true);
538
+ expect(docs.some(d => d.metadata?.['Header 2'] === 'Section 1')).toBe(true);
539
+ expect(docs.some(d => d.metadata?.['Header 3'] === 'Subsection 1.1')).toBe(true);
540
+ });
541
+
542
+ it('should handle headers with mixed content and special characters', async () => {
543
+ const html = `
544
+ <html>
545
+ <body>
546
+ <h1>Title with <strong>bold</strong> &amp; <em>emphasis</em></h1>
547
+ <p>Content 1</p>
548
+ <h2>Section with &lt;tags&gt; &amp; symbols</h2>
549
+ <p>Content 2</p>
550
+ </body>
551
+ </html>
552
+ `;
553
+
554
+ const doc = MDocument.fromHTML(html, { meta: 'data' });
555
+ await doc.chunk({
556
+ strategy: 'html',
557
+ headers: [
558
+ ['h1', 'Header 1'],
559
+ ['h2', 'Header 2'],
560
+ ],
561
+ });
562
+
563
+ const docs = doc.getDocs();
564
+ expect(docs.length).toBeGreaterThan(1);
565
+ expect(docs[0]?.metadata?.['Header 1']).toContain('bold');
566
+ expect(docs[0]?.metadata?.['Header 1']).toContain('&');
567
+ expect(docs[0]?.metadata?.['Header 1']).toContain('emphasis');
568
+ expect(docs[1]?.metadata?.['Header 2']).toContain('<tags>');
569
+ });
570
+
571
+ it('should handle headers with no content or whitespace content', async () => {
572
+ const html = `
573
+ <html>
574
+ <body>
575
+ <h1>Empty Section</h1>
576
+ <h2>Whitespace Section</h2>
577
+
578
+ <h2>Valid Section</h2>
579
+ <p>Content</p>
580
+ </body>
581
+ </html>
582
+ `;
583
+
584
+ const doc = MDocument.fromHTML(html, { meta: 'data' });
585
+ await doc.chunk({
586
+ strategy: 'html',
587
+ headers: [
588
+ ['h1', 'Header 1'],
589
+ ['h2', 'Header 2'],
590
+ ],
591
+ });
592
+
593
+ const docs = doc.getDocs();
594
+ expect(docs.some(d => d.metadata?.['Header 1'] === 'Empty Section')).toBe(true);
595
+ expect(docs.some(d => d.metadata?.['Header 2'] === 'Valid Section')).toBe(true);
596
+ expect(docs.find(d => d.metadata?.['Header 2'] === 'Valid Section')?.text).toContain('Content');
597
+ });
598
+
599
+ it('should generate correct XPaths for deeply nested elements', async () => {
600
+ const html = `
601
+ <html>
602
+ <body>
603
+ <div class="container">
604
+ <section id="main">
605
+ <div>
606
+ <h1>Deeply Nested Title</h1>
607
+ <p>Content</p>
608
+ </div>
609
+ <div>
610
+ <h1>Second Title</h1>
611
+ <p>More Content</p>
612
+ </div>
613
+ </section>
614
+ </div>
615
+ </body>
616
+ </html>
617
+ `;
618
+
619
+ const doc = MDocument.fromHTML(html, { meta: 'data' });
620
+ await doc.chunk({
621
+ strategy: 'html',
622
+ headers: [['h1', 'Header 1']],
623
+ });
624
+
625
+ const docs = doc.getDocs();
626
+ expect(docs).toHaveLength(2);
627
+
628
+ // First h1
629
+ expect(docs[0]?.metadata?.['Header 1']).toBe('Deeply Nested Title');
630
+ const xpath1 = docs[0]?.metadata?.xpath as string;
631
+ expect(xpath1).toBeDefined();
632
+ expect(xpath1).toMatch(/^\/html\[1\]\/body\[1\]\/div\[1\]\/section\[1\]\/div\[1\]\/h1\[1\]$/);
633
+
634
+ // Second h1
635
+ expect(docs[1]?.metadata?.['Header 1']).toBe('Second Title');
636
+ const xpath2 = docs[1]?.metadata?.xpath as string;
637
+ expect(xpath2).toBeDefined();
638
+ expect(xpath2).toMatch(/^\/html\[1\]\/body\[1\]\/div\[1\]\/section\[1\]\/div\[2\]\/h1\[1\]$/);
639
+ });
640
+ });
641
+
642
+ describe('chunkJson', () => {
643
+ describe('Unicode handling', () => {
644
+ it('should handle Unicode characters correctly', async () => {
645
+ const input = {
646
+ key1: '你好',
647
+ key2: '世界',
648
+ };
649
+
650
+ const doc = MDocument.fromJSON(JSON.stringify(input), { meta: 'data' });
651
+
652
+ await doc.chunk({
653
+ strategy: 'json',
654
+ maxSize: 50,
655
+ minSize: 50,
656
+ ensureAscii: true,
657
+ });
658
+
659
+ expect(doc.getText().some(chunk => chunk.includes('\\u'))).toBe(true);
660
+
661
+ const combined = doc
662
+ .getText()
663
+ .map(chunk => {
664
+ const c = JSON.parse(chunk);
665
+ const retVal: Record<string, string> = {};
666
+ Object.entries(c).forEach(([key, value]) => {
667
+ retVal[key] = JSON.parse(`"${value as string}"`);
668
+ });
669
+
670
+ return retVal;
671
+ })
672
+ .reduce((acc, curr) => ({ ...acc, ...curr }), {});
673
+
674
+ expect(combined?.key1?.charCodeAt(0)).toBe('你'.charCodeAt(0));
675
+ expect(combined?.key1?.charCodeAt(1)).toBe('好'.charCodeAt(0));
676
+ expect(combined?.key2?.charCodeAt(0)).toBe('世'.charCodeAt(0));
677
+ expect(combined?.key2?.charCodeAt(1)).toBe('界'.charCodeAt(0));
678
+
679
+ expect(combined?.key1).toBe('你好');
680
+ expect(combined?.key2).toBe('世界');
681
+ });
682
+
683
+ it('should handle non-ASCII without escaping when ensureAscii is false', async () => {
684
+ const input = {
685
+ key1: '你好',
686
+ key2: '世界',
687
+ };
688
+
689
+ const doc = MDocument.fromJSON(JSON.stringify(input), { meta: 'data' });
690
+
691
+ await doc.chunk({
692
+ strategy: 'json',
693
+ maxSize: 50,
694
+ ensureAscii: false,
695
+ });
696
+
697
+ expect(doc.getText().some(chunk => chunk.includes('你好'))).toBe(true);
698
+
699
+ const combined = doc
700
+ .getText()
701
+ .map(chunk => JSON.parse(chunk))
702
+ .reduce((acc, curr) => ({ ...acc, ...curr }), {});
703
+
704
+ expect(combined.key1).toBe('你好');
705
+ expect(combined.key2).toBe('世界');
706
+ });
707
+ });
708
+ });
709
+
710
+ describe('chunkToken', () => {
711
+ it('should handle different encodings', async () => {
712
+ const text = 'This is a test text for different encodings.';
713
+ const doc = MDocument.fromText(text, { meta: 'data' });
714
+
715
+ await doc.chunk({
716
+ strategy: 'token',
717
+ encodingName: 'cl100k_base',
718
+ size: 10,
719
+ overlap: 2,
720
+ });
721
+
722
+ const chunks = doc.getText();
723
+
724
+ expect(chunks.length).toBeGreaterThan(0);
725
+ expect(chunks.join(' ').trim()).toBe(text);
726
+ });
727
+
728
+ it('should handle special tokens correctly', async () => {
729
+ const text = 'Test text <|endoftext|> more text';
730
+
731
+ const doc = MDocument.fromText(text, { meta: 'data' });
732
+
733
+ await doc.chunk({
734
+ strategy: 'token',
735
+ encodingName: 'gpt2',
736
+ size: 10,
737
+ disallowedSpecial: new Set(),
738
+ allowedSpecial: new Set(['<|endoftext|>']),
739
+ overlap: 2,
740
+ });
741
+
742
+ const chunks = doc.getText();
743
+
744
+ expect(chunks.join(' ').includes('<|endoftext|>')).toBe(true);
745
+ });
746
+
747
+ it('should strip whitespace when configured', async () => {
748
+ const text = ' This has whitespace ';
749
+
750
+ const doc = MDocument.fromText(text, { meta: 'data' });
751
+
752
+ await doc.chunk({
753
+ strategy: 'token',
754
+ encodingName: 'gpt2',
755
+ size: 10,
756
+ disallowedSpecial: new Set(),
757
+ allowedSpecial: new Set(['<|endoftext|>']),
758
+ overlap: 2,
759
+ });
760
+
761
+ const chunks = doc.getText();
762
+
763
+ chunks.forEach(chunk => {
764
+ expect(chunk).not.toMatch(/^\s+|\s+$/);
765
+ });
766
+ });
767
+
768
+ describe('Error cases', () => {
769
+ it('should throw error for invalid chunk size and overlap', async () => {
770
+ const text = ' This has whitespace ';
771
+ const doc = MDocument.fromText(text, { meta: 'data' });
772
+
773
+ await expect(
774
+ doc.chunk({
775
+ strategy: 'token',
776
+ size: 100,
777
+ overlap: 150, // overlap larger than chunk size
778
+ }),
779
+ ).rejects.toThrow();
780
+ });
781
+
782
+ it('should handle invalid encoding name', async () => {
783
+ const text = ' This has whitespace ';
784
+ const doc = MDocument.fromText(text, { meta: 'data' });
785
+
786
+ await expect(
787
+ doc.chunk({
788
+ strategy: 'token',
789
+ encodingName: 'invalid-encoding' as any,
790
+ size: 100,
791
+ overlap: 150, // overlap larger than chunk size
792
+ }),
793
+ ).rejects.toThrow();
794
+ });
795
+ });
796
+ });
797
+
798
+ describe('chunkMarkdown', () => {
799
+ it('should split markdown text correctly', async () => {
800
+ const text = `# Header 1
801
+
802
+ This is some text under header 1.
803
+
804
+ ## Header 2
805
+
806
+ This is some text under header 2.
807
+
808
+ ### Header 3
809
+
810
+ - List item 1
811
+ - List item 2`;
812
+
813
+ const doc = MDocument.fromMarkdown(text, { meta: 'data' });
814
+
815
+ await doc.chunk({
816
+ strategy: 'markdown',
817
+ size: 100,
818
+ overlap: 10,
819
+ });
820
+
821
+ const chunks = doc.getText();
822
+ expect(chunks.length).toBeGreaterThan(1);
823
+ expect(chunks[0]).toContain('# Header 1');
824
+ });
825
+
826
+ it('should handle code blocks', async () => {
827
+ const text = `# Code Example
828
+
829
+ \`\`\`javascript
830
+ function hello() {
831
+ console.log('Hello, World!');
832
+ }
833
+ \`\`\`
834
+
835
+ Regular text after code block.`;
836
+
837
+ const doc = MDocument.fromMarkdown(text, { meta: 'data' });
838
+
839
+ await doc.chunk({
840
+ strategy: 'markdown',
841
+ size: 100,
842
+ overlap: 10,
843
+ });
844
+
845
+ const chunks = doc.getText();
846
+ expect(chunks.some(chunk => chunk.includes('```javascript'))).toBe(true);
847
+ });
848
+ });
849
+
850
+ describe('MarkdownHeader', () => {
851
+ it('should split on headers and preserve metadata', async () => {
852
+ const text = `# Main Title
853
+
854
+ Some content here.
855
+
856
+ ## Section 1
857
+
858
+ Section 1 content.
859
+
860
+ ### Subsection 1.1
861
+
862
+ Subsection content.
863
+
864
+ ## Section 2
865
+
866
+ Final content.`;
867
+
868
+ const doc = MDocument.fromMarkdown(text);
869
+
870
+ await doc.chunk({
871
+ strategy: 'markdown',
872
+ headers: [
873
+ ['#', 'Header 1'],
874
+ ['##', 'Header 2'],
875
+ ['###', 'Header 3'],
876
+ ],
877
+ });
878
+
879
+ const docs = doc.getDocs();
880
+
881
+ expect(docs.length).toBeGreaterThan(1);
882
+ expect(docs?.[0]?.metadata?.['Header 1']).toBe('Main Title');
883
+
884
+ const section1 = docs.find(doc => doc?.metadata?.['Header 2'] === 'Section 1');
885
+ expect(section1).toBeDefined();
886
+ expect(section1?.text).toContain('Section 1 content');
887
+ });
888
+
889
+ it('should handle nested headers correctly', async () => {
890
+ const text = `# Top Level
891
+
892
+ ## Section A
893
+ Content A
894
+
895
+ ### Subsection A1
896
+ Content A1
897
+
898
+ ## Section B
899
+ Content B`;
900
+
901
+ const doc = MDocument.fromMarkdown(text, { meta: 'data' });
902
+
903
+ await doc.chunk({
904
+ strategy: 'markdown',
905
+ headers: [
906
+ ['#', 'Header 1'],
907
+ ['##', 'Header 2'],
908
+ ['###', 'Header 3'],
909
+ ],
910
+ });
911
+
912
+ const subsectionDoc = doc.getDocs().find(doc => doc?.metadata?.['Header 3'] === 'Subsection A1');
913
+ expect(subsectionDoc).toBeDefined();
914
+ expect(subsectionDoc?.metadata?.['Header 1']).toBe('Top Level');
915
+ expect(subsectionDoc?.metadata?.['Header 2']).toBe('Section A');
916
+ });
917
+
918
+ it('should handle code blocks without splitting them', async () => {
919
+ const text = `# Code Section
920
+
921
+ \`\`\`python
922
+ def hello():
923
+ print("Hello World")
924
+ \`\`\`
925
+
926
+ ## Next Section`;
927
+
928
+ const doc = MDocument.fromMarkdown(text, { meta: 'data' });
929
+
930
+ await doc.chunk({
931
+ strategy: 'markdown',
932
+ headers: [
933
+ ['#', 'Header 1'],
934
+ ['##', 'Header 2'],
935
+ ['###', 'Header 3'],
936
+ ],
937
+ });
938
+
939
+ const codeDoc = doc.getDocs().find(doc => doc?.text?.includes('```python'));
940
+ expect(codeDoc?.text).toContain('print("Hello World")');
941
+ });
942
+
943
+ it('should respect returnEachLine option', async () => {
944
+ const text = `# Title
945
+
946
+ Line 1
947
+ Line 2
948
+ Line 3`;
949
+
950
+ const doc = MDocument.fromMarkdown(text, { meta: 'data' });
951
+
952
+ await doc.chunk({
953
+ strategy: 'markdown',
954
+ headers: [['#', 'Header 1']],
955
+ returnEachLine: true,
956
+ });
957
+
958
+ expect(doc.getDocs().length).toBe(4); // Title + 3 lines
959
+ doc
960
+ .getDocs()
961
+ .slice(1)
962
+ .forEach(doc => {
963
+ expect(doc.metadata?.['Header 1']).toBe('Title');
964
+ });
965
+ });
966
+
967
+ it('should handle stripHeaders option', async () => {
968
+ const text = `# Title
969
+
970
+ Content`;
971
+
972
+ const doc = MDocument.fromMarkdown(text, { meta: 'data' });
973
+
974
+ await doc.chunk({
975
+ strategy: 'markdown',
976
+ headers: [['#', 'Header 1']],
977
+ returnEachLine: false,
978
+ stripHeaders: false,
979
+ });
980
+
981
+ const docs = doc.getDocs();
982
+ expect(docs?.[0]?.text).toContain('# Title');
983
+ });
984
+ });
985
+ });