@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,346 @@
1
+ import { Document } from 'llamaindex';
2
+ import { parse } from 'node-html-better-parser';
3
+
4
+ import { RecursiveCharacterTransformer } from './character';
5
+
6
+ interface ElementType {
7
+ url: string;
8
+ xpath: string;
9
+ content: string;
10
+ metadata: Record<string, string>;
11
+ }
12
+
13
+ export class HTMLHeaderTransformer {
14
+ private headersToSplitOn: [string, string][];
15
+ private returnEachElement: boolean;
16
+
17
+ constructor(headersToSplitOn: [string, string][], returnEachElement: boolean = false) {
18
+ this.returnEachElement = returnEachElement;
19
+ this.headersToSplitOn = [...headersToSplitOn].sort();
20
+ }
21
+
22
+ splitText({ text }: { text: string }): Document[] {
23
+ const root = parse(text);
24
+
25
+ const headerFilter = this.headersToSplitOn.map(([header]) => header);
26
+ const headerMapping = Object.fromEntries(this.headersToSplitOn);
27
+
28
+ const elements: ElementType[] = [];
29
+ const headers = root.querySelectorAll(headerFilter.join(','));
30
+
31
+ headers.forEach(header => {
32
+ let content = '';
33
+ const parentNode = header.parentNode;
34
+
35
+ if (parentNode && parentNode.childNodes) {
36
+ let foundHeader = false;
37
+ for (const node of parentNode.childNodes) {
38
+ // Start collecting content after we find our header
39
+ if (node === header) {
40
+ foundHeader = true;
41
+ continue;
42
+ }
43
+
44
+ // If we found our header and hit another header, stop
45
+ // @ts-expect-error - node.tagName is not defined on type Node
46
+ if (foundHeader && node.tagName && headerFilter.includes(node.tagName.toLowerCase())) {
47
+ break;
48
+ }
49
+
50
+ // Collect content between headers
51
+ if (foundHeader) {
52
+ content += this.getTextContent(node) + ' ';
53
+ }
54
+ }
55
+ }
56
+
57
+ elements.push({
58
+ url: text,
59
+ xpath: this.getXPath(header),
60
+ content: content.trim(),
61
+ metadata: {
62
+ [headerMapping?.[header.tagName.toLowerCase()]!]: header.text || '',
63
+ },
64
+ });
65
+ });
66
+
67
+ return this.returnEachElement
68
+ ? elements.map(
69
+ el =>
70
+ new Document({
71
+ text: el.content,
72
+ metadata: { ...el.metadata, xpath: el.xpath },
73
+ }),
74
+ )
75
+ : this.aggregateElementsToChunks(elements);
76
+ }
77
+
78
+ private getXPath(element: any): string {
79
+ if (!element) return '';
80
+
81
+ const parts: string[] = [];
82
+ let current = element;
83
+
84
+ while (current && current.tagName) {
85
+ let index = 1;
86
+ const parent = current.parentNode;
87
+
88
+ if (parent && parent.childNodes) {
89
+ // Count preceding siblings with same tag
90
+ for (const sibling of parent.childNodes) {
91
+ if (sibling === current) break;
92
+ if (sibling.tagName === current.tagName) {
93
+ index++;
94
+ }
95
+ }
96
+ }
97
+
98
+ parts.unshift(`${current.tagName.toLowerCase()}[${index}]`);
99
+ current = current.parentNode;
100
+ }
101
+
102
+ return '/' + parts.join('/');
103
+ }
104
+
105
+ private getTextContent(element: any): string {
106
+ if (!element) return '';
107
+
108
+ // For text nodes, return their content
109
+ if (!element.tagName) {
110
+ return element.text || '';
111
+ }
112
+
113
+ // For element nodes, combine their text with children's text
114
+ let content = element.text || '';
115
+
116
+ if (element.childNodes) {
117
+ for (const child of element.childNodes) {
118
+ const childText = this.getTextContent(child);
119
+ if (childText) {
120
+ content += ' ' + childText;
121
+ }
122
+ }
123
+ }
124
+
125
+ return content.trim();
126
+ }
127
+
128
+ private aggregateElementsToChunks(elements: ElementType[]): Document[] {
129
+ const aggregatedChunks: ElementType[] = [];
130
+
131
+ for (const element of elements) {
132
+ if (
133
+ aggregatedChunks.length > 0 &&
134
+ JSON.stringify(aggregatedChunks[aggregatedChunks.length - 1]!.metadata) === JSON.stringify(element.metadata)
135
+ ) {
136
+ // If the last element has the same metadata, append content
137
+ aggregatedChunks[aggregatedChunks.length - 1]!.content += ' \n' + element.content;
138
+ } else {
139
+ // Otherwise, add as new element
140
+ aggregatedChunks.push({ ...element });
141
+ }
142
+ }
143
+
144
+ return aggregatedChunks.map(
145
+ chunk =>
146
+ new Document({
147
+ text: chunk.content,
148
+ metadata: { ...chunk.metadata, xpath: chunk.xpath },
149
+ }),
150
+ );
151
+ }
152
+
153
+ createDocuments(texts: string[], metadatas?: Record<string, any>[]): Document[] {
154
+ const _metadatas = metadatas || Array(texts.length).fill({});
155
+ const documents: Document[] = [];
156
+
157
+ for (let i = 0; i < texts.length; i++) {
158
+ const chunks = this.splitText({ text: texts[i]! });
159
+ for (const chunk of chunks) {
160
+ const metadata = { ...(_metadatas[i] || {}) };
161
+ const chunkMetadata = chunk.metadata;
162
+
163
+ if (chunkMetadata) {
164
+ for (const [key, value] of Object.entries(chunkMetadata || {})) {
165
+ if (value === '#TITLE#') {
166
+ chunkMetadata[key] = metadata['Title'];
167
+ }
168
+ }
169
+ }
170
+
171
+ documents.push(
172
+ new Document({
173
+ text: chunk.text!,
174
+ metadata: { ...metadata, ...chunkMetadata },
175
+ }),
176
+ );
177
+ }
178
+ }
179
+
180
+ return documents;
181
+ }
182
+
183
+ transformDocuments(documents: Document[]): Document[] {
184
+ const texts: string[] = [];
185
+ const metadatas: Record<string, any>[] = [];
186
+
187
+ for (const doc of documents) {
188
+ texts.push(doc.text);
189
+ metadatas.push(doc.metadata);
190
+ }
191
+
192
+ return this.createDocuments(texts, metadatas);
193
+ }
194
+ }
195
+
196
+ export class HTMLSectionTransformer {
197
+ private headersToSplitOn: Record<string, string>;
198
+ private options: Record<string, any>;
199
+
200
+ constructor(headersToSplitOn: [string, string][], options: Record<string, any> = {}) {
201
+ this.headersToSplitOn = Object.fromEntries(headersToSplitOn.map(([tag, name]) => [tag.toLowerCase(), name]));
202
+ this.options = options;
203
+ }
204
+
205
+ splitText(text: string): Document[] {
206
+ const sections = this.splitHtmlByHeaders(text);
207
+
208
+ return sections.map(
209
+ section =>
210
+ new Document({
211
+ text: section.content,
212
+ metadata: {
213
+ [this.headersToSplitOn[section.tagName.toLowerCase()]!]: section.header,
214
+ xpath: section.xpath,
215
+ },
216
+ }),
217
+ );
218
+ }
219
+
220
+ private getXPath(element: any): string {
221
+ const parts: string[] = [];
222
+ let current = element;
223
+
224
+ while (current && current.nodeType === 1) {
225
+ let index = 1;
226
+ let sibling = current.previousSibling;
227
+
228
+ while (sibling) {
229
+ if (sibling.nodeType === 1 && sibling.tagName === current.tagName) {
230
+ index++;
231
+ }
232
+ sibling = sibling.previousSibling;
233
+ }
234
+
235
+ if (current.tagName) {
236
+ parts.unshift(`${current.tagName.toLowerCase()}[${index}]`);
237
+ }
238
+ current = current.parentNode;
239
+ }
240
+
241
+ return '/' + parts.join('/');
242
+ }
243
+
244
+ private splitHtmlByHeaders(htmlDoc: string): Array<{
245
+ header: string;
246
+ content: string;
247
+ tagName: string;
248
+ xpath: string;
249
+ }> {
250
+ const sections: Array<{
251
+ header: string;
252
+ content: string;
253
+ tagName: string;
254
+ xpath: string;
255
+ }> = [];
256
+
257
+ const root = parse(htmlDoc);
258
+ const headers = Object.keys(this.headersToSplitOn);
259
+ const headerElements = root.querySelectorAll(headers.join(','));
260
+
261
+ headerElements.forEach((headerElement, index) => {
262
+ const header = headerElement.text?.trim() || '';
263
+ const tagName = headerElement.tagName;
264
+ const xpath = this.getXPath(headerElement);
265
+ let content = '';
266
+
267
+ // @ts-expect-error - nextElementSibling is not defined on type Element
268
+ let currentElement = headerElement.nextElementSibling;
269
+ const nextHeader = headerElements[index + 1];
270
+
271
+ while (currentElement && (!nextHeader || currentElement !== nextHeader)) {
272
+ if (currentElement.text) {
273
+ content += currentElement.text.trim() + ' ';
274
+ }
275
+ currentElement = currentElement.nextElementSibling;
276
+ }
277
+
278
+ content = content.trim();
279
+ sections.push({
280
+ header,
281
+ content,
282
+ tagName,
283
+ xpath,
284
+ });
285
+ });
286
+
287
+ return sections;
288
+ }
289
+
290
+ async splitDocuments(documents: Document[]): Promise<Document[]> {
291
+ const texts: string[] = [];
292
+ const metadatas: Record<string, any>[] = [];
293
+
294
+ for (const doc of documents) {
295
+ texts.push(doc.text);
296
+ metadatas.push(doc.metadata);
297
+ }
298
+ const results = await this.createDocuments(texts, metadatas);
299
+ const textSplitter = new RecursiveCharacterTransformer({ options: this.options });
300
+
301
+ return textSplitter.splitDocuments(results);
302
+ }
303
+
304
+ createDocuments(texts: string[], metadatas?: Record<string, any>[]): Document[] {
305
+ const _metadatas = metadatas || Array(texts.length).fill({});
306
+ const documents: Document[] = [];
307
+
308
+ for (let i = 0; i < texts.length; i++) {
309
+ const chunks = this.splitText(texts[i]!);
310
+ for (const chunk of chunks) {
311
+ const metadata = { ...(_metadatas[i] || {}) };
312
+
313
+ const chunkMetadata = chunk.metadata;
314
+
315
+ if (chunkMetadata) {
316
+ for (const [key, value] of Object.entries(chunkMetadata || {})) {
317
+ if (value === '#TITLE#') {
318
+ chunkMetadata[key] = metadata['Title'];
319
+ }
320
+ }
321
+ }
322
+
323
+ documents.push(
324
+ new Document({
325
+ text: chunk.text!,
326
+ metadata: { ...metadata, ...chunkMetadata },
327
+ }),
328
+ );
329
+ }
330
+ }
331
+
332
+ return documents;
333
+ }
334
+
335
+ transformDocuments(documents: Document[]): Document[] {
336
+ const texts: string[] = [];
337
+ const metadatas: Record<string, any>[] = [];
338
+
339
+ for (const doc of documents) {
340
+ texts.push(doc.text);
341
+ metadatas.push(doc.metadata);
342
+ }
343
+
344
+ return this.createDocuments(texts, metadatas);
345
+ }
346
+ }
@@ -0,0 +1,265 @@
1
+ import { Document } from 'llamaindex';
2
+
3
+ export class RecursiveJsonTransformer {
4
+ private maxSize: number;
5
+ private minSize: number;
6
+
7
+ constructor({ maxSize = 2000, minSize }: { maxSize: number; minSize?: number }) {
8
+ this.maxSize = maxSize;
9
+ this.minSize = minSize ?? Math.max(maxSize - 200, 50);
10
+ }
11
+
12
+ private static jsonSize(data: Record<string, any>): number {
13
+ const seen = new WeakSet();
14
+
15
+ function getStringifiableData(obj: any): any {
16
+ if (obj === null || typeof obj !== 'object') {
17
+ return obj;
18
+ }
19
+
20
+ if (seen.has(obj)) {
21
+ return '[Circular]';
22
+ }
23
+
24
+ seen.add(obj);
25
+
26
+ if (Array.isArray(obj)) {
27
+ const safeArray = [];
28
+ for (const item of obj) {
29
+ safeArray.push(getStringifiableData(item));
30
+ }
31
+ return safeArray;
32
+ }
33
+
34
+ const safeObj: Record<string, any> = {};
35
+ for (const key in obj) {
36
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
37
+ safeObj[key] = getStringifiableData(obj[key]);
38
+ }
39
+ }
40
+ return safeObj;
41
+ }
42
+
43
+ const stringifiable = getStringifiableData(data);
44
+ return JSON.stringify(stringifiable).length;
45
+ }
46
+
47
+ /**
48
+ * Transform JSON data while handling circular references
49
+ */
50
+ public transform(data: Record<string, any>): Record<string, any> {
51
+ const size = RecursiveJsonTransformer.jsonSize(data);
52
+
53
+ const seen = new WeakSet();
54
+
55
+ function createSafeCopy(obj: any): any {
56
+ if (obj === null || typeof obj !== 'object') {
57
+ return obj;
58
+ }
59
+
60
+ if (seen.has(obj)) {
61
+ return '[Circular]';
62
+ }
63
+
64
+ seen.add(obj);
65
+
66
+ if (Array.isArray(obj)) {
67
+ return obj.map(item => createSafeCopy(item));
68
+ }
69
+
70
+ const copy: Record<string, any> = {};
71
+ for (const key in obj) {
72
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
73
+ copy[key] = createSafeCopy(obj[key]);
74
+ }
75
+ }
76
+ return copy;
77
+ }
78
+
79
+ return {
80
+ size,
81
+ data: createSafeCopy(data),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Set a value in a nested dictionary based on the given path
87
+ */
88
+ private static setNestedDict(d: Record<string, any>, path: string[], value: any): void {
89
+ let current = d;
90
+ for (const key of path.slice(0, -1)) {
91
+ current[key] = current[key] || {};
92
+ current = current[key];
93
+ }
94
+ current[path[path.length - 1]!] = value;
95
+ }
96
+
97
+ /**
98
+ * Convert lists in the JSON structure to dictionaries with index-based keys
99
+ */
100
+ private listToDictPreprocessing(data: any): any {
101
+ if (data && typeof data === 'object') {
102
+ if (Array.isArray(data)) {
103
+ return Object.fromEntries(data.map((item, index) => [String(index), this.listToDictPreprocessing(item)]));
104
+ }
105
+ return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, this.listToDictPreprocessing(v)]));
106
+ }
107
+ return data;
108
+ }
109
+
110
+ /**
111
+ * Split json into maximum size dictionaries while preserving structure
112
+ */
113
+ private jsonSplit({
114
+ data,
115
+ currentPath = [],
116
+ chunks = [{}],
117
+ }: {
118
+ data: Record<string, any>;
119
+ currentPath?: string[];
120
+ chunks?: Record<string, any>[];
121
+ }): Record<string, any>[] {
122
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
123
+ for (const [key, value] of Object.entries(data)) {
124
+ const newPath = [...currentPath, key];
125
+ const chunkSize = RecursiveJsonTransformer.jsonSize(chunks[chunks.length - 1] || {});
126
+ const size = RecursiveJsonTransformer.jsonSize({ [key]: value });
127
+ const remaining = this.maxSize - chunkSize;
128
+
129
+ if (size < remaining) {
130
+ // Add item to current chunk
131
+ RecursiveJsonTransformer.setNestedDict(chunks[chunks.length - 1] || {}, newPath, value);
132
+ } else {
133
+ if (chunkSize >= this.minSize) {
134
+ // Chunk is big enough, start a new chunk
135
+ chunks.push({});
136
+ }
137
+ // Iterate
138
+ this.jsonSplit({
139
+ data: typeof value === 'object' ? value : { [key]: value },
140
+ currentPath: newPath,
141
+ chunks,
142
+ });
143
+ }
144
+ }
145
+ } else {
146
+ RecursiveJsonTransformer.setNestedDict(chunks[chunks.length - 1] || {}, currentPath, data);
147
+ }
148
+ return chunks;
149
+ }
150
+
151
+ /**
152
+ * Splits JSON into a list of JSON chunks
153
+ */
154
+ splitJson({
155
+ jsonData,
156
+ convertLists = false,
157
+ }: {
158
+ jsonData: Record<string, any>;
159
+ convertLists?: boolean;
160
+ }): Record<string, any>[] {
161
+ const processedData = convertLists ? this.listToDictPreprocessing(jsonData) : jsonData;
162
+
163
+ const chunks = this.jsonSplit({ data: processedData });
164
+
165
+ if (Object.keys(chunks[chunks.length - 1] || {}).length === 0) {
166
+ chunks.pop();
167
+ }
168
+
169
+ return chunks;
170
+ }
171
+
172
+ private escapeNonAscii(obj: any): any {
173
+ if (typeof obj === 'string') {
174
+ return obj.replace(/[\u0080-\uffff]/g, char => {
175
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
176
+ });
177
+ }
178
+
179
+ if (typeof obj === 'object' && obj !== null) {
180
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, this.escapeNonAscii(value)]));
181
+ }
182
+
183
+ return obj;
184
+ }
185
+ /**
186
+ * Splits JSON into a list of JSON formatted strings
187
+ */
188
+ splitText({
189
+ jsonData,
190
+ convertLists = false,
191
+ ensureAscii = true,
192
+ }: {
193
+ jsonData: Record<string, any>;
194
+ convertLists?: boolean;
195
+ ensureAscii?: boolean;
196
+ }): string[] {
197
+ const chunks = this.splitJson({ jsonData, convertLists });
198
+
199
+ if (ensureAscii) {
200
+ const escapedChunks = chunks.map(chunk => this.escapeNonAscii(chunk));
201
+ return escapedChunks.map(chunk => JSON.stringify(chunk));
202
+ }
203
+
204
+ return chunks.map(chunk => JSON.stringify(chunk));
205
+ }
206
+
207
+ /**
208
+ * Create documents from a list of json objects
209
+ */
210
+ createDocuments({
211
+ texts,
212
+ convertLists = false,
213
+ ensureAscii = true,
214
+ metadatas,
215
+ }: {
216
+ texts: string[];
217
+ convertLists?: boolean;
218
+ ensureAscii?: boolean;
219
+ metadatas?: Record<string, any>[];
220
+ }): Document[] {
221
+ const _metadatas = metadatas || Array(texts.length).fill({});
222
+ const documents: Document[] = [];
223
+
224
+ texts.forEach((text, i) => {
225
+ const chunks = this.splitText({ jsonData: JSON.parse(text), convertLists, ensureAscii });
226
+ chunks.forEach(chunk => {
227
+ const metadata = { ...(_metadatas[i] || {}) };
228
+ documents.push(
229
+ new Document({
230
+ text: chunk,
231
+ metadata,
232
+ }),
233
+ );
234
+ });
235
+ });
236
+
237
+ return documents;
238
+ }
239
+
240
+ transformDocuments({
241
+ ensureAscii,
242
+ documents,
243
+ convertLists,
244
+ }: {
245
+ ensureAscii?: boolean;
246
+ convertLists?: boolean;
247
+ documents: Document[];
248
+ }): Document[] {
249
+ const texts: string[] = [];
250
+ const metadatas: Record<string, any>[] = [];
251
+
252
+ for (const doc of documents) {
253
+ texts.push(doc.text);
254
+ metadatas.push(doc.metadata);
255
+ }
256
+
257
+ return this.createDocuments({
258
+ texts,
259
+ metadatas,
260
+
261
+ ensureAscii,
262
+ convertLists,
263
+ });
264
+ }
265
+ }
@@ -0,0 +1,19 @@
1
+ import { Language } from '../types';
2
+
3
+ import { RecursiveCharacterTransformer } from './character';
4
+
5
+ export class LatexTransformer extends RecursiveCharacterTransformer {
6
+ constructor(
7
+ options: {
8
+ size?: number;
9
+ overlap?: number;
10
+ lengthFunction?: (text: string) => number;
11
+ keepSeparator?: boolean | 'start' | 'end';
12
+ addStartIndex?: boolean;
13
+ stripWhitespace?: boolean;
14
+ } = {},
15
+ ) {
16
+ const separators = RecursiveCharacterTransformer.getSeparatorsForLanguage(Language.LATEX);
17
+ super({ separators, isSeparatorRegex: true, options });
18
+ }
19
+ }