@voltx/rag 0.2.0 → 0.3.0
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 +109 -0
- package/dist/index.d.mts +275 -14
- package/dist/index.d.ts +275 -14
- package/dist/index.js +488 -34
- package/dist/index.mjs +478 -35
- package/package.json +15 -14
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -21,20 +21,29 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CharacterSplitter: () => CharacterSplitter,
|
|
24
|
+
JSONLoader: () => JSONLoader,
|
|
25
|
+
MDocument: () => MDocument,
|
|
26
|
+
MarkdownLoader: () => MarkdownLoader,
|
|
27
|
+
MarkdownSplitter: () => MarkdownSplitter,
|
|
24
28
|
RAGPipeline: () => RAGPipeline,
|
|
29
|
+
RecursiveTextSplitter: () => RecursiveTextSplitter,
|
|
30
|
+
TextLoader: () => TextLoader,
|
|
25
31
|
VERSION: () => VERSION,
|
|
32
|
+
WebLoader: () => WebLoader,
|
|
33
|
+
cosineSimilarity: () => cosineSimilarity,
|
|
34
|
+
createEmbedder: () => createEmbedder,
|
|
26
35
|
createRAGPipeline: () => createRAGPipeline
|
|
27
36
|
});
|
|
28
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// src/splitters.ts
|
|
29
40
|
var CharacterSplitter = class {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
chunkSize;
|
|
42
|
+
overlap;
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.chunkSize = options.chunkSize ?? 1e3;
|
|
45
|
+
this.overlap = options.overlap ?? 200;
|
|
33
46
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Find the best split point near `pos` by looking for sentence/paragraph
|
|
36
|
-
* boundaries first, then word boundaries, falling back to exact position.
|
|
37
|
-
*/
|
|
38
47
|
findBreakPoint(text, pos) {
|
|
39
48
|
if (pos >= text.length) return text.length;
|
|
40
49
|
const searchStart = Math.max(0, pos - Math.floor(this.chunkSize * 0.2));
|
|
@@ -48,9 +57,7 @@ var CharacterSplitter = class {
|
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
for (let i = pos; i >= searchStart; i--) {
|
|
51
|
-
if (/\s/.test(text[i]))
|
|
52
|
-
return i + 1;
|
|
53
|
-
}
|
|
60
|
+
if (/\s/.test(text[i])) return i + 1;
|
|
54
61
|
}
|
|
55
62
|
return pos;
|
|
56
63
|
}
|
|
@@ -61,56 +68,503 @@ var CharacterSplitter = class {
|
|
|
61
68
|
while (start < text.length) {
|
|
62
69
|
const rawEnd = Math.min(start + this.chunkSize, text.length);
|
|
63
70
|
const end = rawEnd >= text.length ? rawEnd : this.findBreakPoint(text, rawEnd);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
const content = text.slice(start, end).trim();
|
|
72
|
+
if (content.length > 0) {
|
|
73
|
+
chunks.push({
|
|
74
|
+
id: `chunk-${index++}`,
|
|
75
|
+
content,
|
|
76
|
+
metadata: { start, end, splitter: "character" }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
69
79
|
const step = end - start - this.overlap;
|
|
70
80
|
start += step > 0 ? step : end - start;
|
|
71
81
|
}
|
|
82
|
+
return chunks;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var RecursiveTextSplitter = class {
|
|
86
|
+
chunkSize;
|
|
87
|
+
overlap;
|
|
88
|
+
separators;
|
|
89
|
+
constructor(options = {}) {
|
|
90
|
+
this.chunkSize = options.chunkSize ?? 1e3;
|
|
91
|
+
this.overlap = options.overlap ?? 200;
|
|
92
|
+
this.separators = options.separators ?? ["\n\n", "\n", ". ", " ", ""];
|
|
93
|
+
}
|
|
94
|
+
split(text) {
|
|
95
|
+
const rawChunks = this.splitText(text, this.separators);
|
|
96
|
+
const merged = this.mergeWithOverlap(rawChunks);
|
|
97
|
+
return merged.map((content, i) => ({
|
|
98
|
+
id: `chunk-${i}`,
|
|
99
|
+
content,
|
|
100
|
+
metadata: { index: i, splitter: "recursive" }
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Recursively split text. Try the first separator; if any resulting piece
|
|
105
|
+
* is still too large, recurse with the next separator in the list.
|
|
106
|
+
*/
|
|
107
|
+
splitText(text, separators) {
|
|
108
|
+
const results = [];
|
|
109
|
+
let bestSep = "";
|
|
110
|
+
let bestIdx = 0;
|
|
111
|
+
for (let i = 0; i < separators.length; i++) {
|
|
112
|
+
const sep = separators[i];
|
|
113
|
+
if (sep === "") {
|
|
114
|
+
bestSep = sep;
|
|
115
|
+
bestIdx = i;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (text.includes(sep)) {
|
|
119
|
+
bestSep = sep;
|
|
120
|
+
bestIdx = i;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const pieces = bestSep === "" ? [...text] : text.split(bestSep);
|
|
125
|
+
const remainingSeps = separators.slice(bestIdx + 1);
|
|
126
|
+
let current = "";
|
|
127
|
+
for (const piece of pieces) {
|
|
128
|
+
const candidate = current ? current + bestSep + piece : piece;
|
|
129
|
+
if (candidate.length <= this.chunkSize) {
|
|
130
|
+
current = candidate;
|
|
131
|
+
} else {
|
|
132
|
+
if (current.trim()) {
|
|
133
|
+
results.push(current.trim());
|
|
134
|
+
}
|
|
135
|
+
if (piece.length > this.chunkSize && remainingSeps.length > 0) {
|
|
136
|
+
const subChunks = this.splitText(piece, remainingSeps);
|
|
137
|
+
results.push(...subChunks);
|
|
138
|
+
current = "";
|
|
139
|
+
} else {
|
|
140
|
+
current = piece;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (current.trim()) {
|
|
145
|
+
results.push(current.trim());
|
|
146
|
+
}
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Merge chunks with overlap to maintain context between adjacent chunks.
|
|
151
|
+
*/
|
|
152
|
+
mergeWithOverlap(chunks) {
|
|
153
|
+
if (this.overlap <= 0 || chunks.length <= 1) return chunks;
|
|
154
|
+
const result = [];
|
|
155
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
156
|
+
if (i === 0) {
|
|
157
|
+
result.push(chunks[i]);
|
|
158
|
+
} else {
|
|
159
|
+
const prev = chunks[i - 1];
|
|
160
|
+
const overlapText = prev.slice(-this.overlap);
|
|
161
|
+
const spaceIdx = overlapText.indexOf(" ");
|
|
162
|
+
const cleanOverlap = spaceIdx >= 0 ? overlapText.slice(spaceIdx + 1) : overlapText;
|
|
163
|
+
result.push((cleanOverlap + " " + chunks[i]).trim());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var MarkdownSplitter = class {
|
|
170
|
+
chunkSize;
|
|
171
|
+
overlap;
|
|
172
|
+
includeHeaders;
|
|
173
|
+
constructor(options = {}) {
|
|
174
|
+
this.chunkSize = options.chunkSize ?? 1500;
|
|
175
|
+
this.overlap = options.overlap ?? 100;
|
|
176
|
+
this.includeHeaders = options.includeHeaders ?? true;
|
|
177
|
+
}
|
|
178
|
+
split(text) {
|
|
179
|
+
const sections = this.splitByHeadings(text);
|
|
180
|
+
const chunks = [];
|
|
181
|
+
let index = 0;
|
|
182
|
+
const fallback = new RecursiveTextSplitter({
|
|
183
|
+
chunkSize: this.chunkSize,
|
|
184
|
+
overlap: this.overlap
|
|
185
|
+
});
|
|
186
|
+
for (const section of sections) {
|
|
187
|
+
if (section.content.length <= this.chunkSize) {
|
|
188
|
+
chunks.push({
|
|
189
|
+
id: `chunk-${index++}`,
|
|
190
|
+
content: section.content.trim(),
|
|
191
|
+
metadata: {
|
|
192
|
+
...section.headers,
|
|
193
|
+
splitter: "markdown"
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
const subChunks = fallback.split(section.content);
|
|
198
|
+
for (const sub of subChunks) {
|
|
199
|
+
chunks.push({
|
|
200
|
+
id: `chunk-${index++}`,
|
|
201
|
+
content: sub.content.trim(),
|
|
202
|
+
metadata: {
|
|
203
|
+
...section.headers,
|
|
204
|
+
...sub.metadata,
|
|
205
|
+
splitter: "markdown"
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
72
211
|
return chunks.filter((c) => c.content.length > 0);
|
|
73
212
|
}
|
|
213
|
+
splitByHeadings(text) {
|
|
214
|
+
const lines = text.split("\n");
|
|
215
|
+
const sections = [];
|
|
216
|
+
const headerStack = {};
|
|
217
|
+
let currentContent = "";
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
220
|
+
if (headerMatch) {
|
|
221
|
+
if (currentContent.trim()) {
|
|
222
|
+
sections.push({
|
|
223
|
+
content: this.includeHeaders ? this.buildHeaderPrefix(headerStack) + currentContent.trim() : currentContent.trim(),
|
|
224
|
+
headers: { ...headerStack }
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const level = headerMatch[1].length;
|
|
228
|
+
const title = headerMatch[2].trim();
|
|
229
|
+
for (let i = level; i <= 6; i++) {
|
|
230
|
+
delete headerStack[`h${i}`];
|
|
231
|
+
}
|
|
232
|
+
headerStack[`h${level}`] = title;
|
|
233
|
+
currentContent = "";
|
|
234
|
+
} else {
|
|
235
|
+
currentContent += line + "\n";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (currentContent.trim()) {
|
|
239
|
+
sections.push({
|
|
240
|
+
content: this.includeHeaders ? this.buildHeaderPrefix(headerStack) + currentContent.trim() : currentContent.trim(),
|
|
241
|
+
headers: { ...headerStack }
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return sections;
|
|
245
|
+
}
|
|
246
|
+
buildHeaderPrefix(headers) {
|
|
247
|
+
const parts = [];
|
|
248
|
+
for (let i = 1; i <= 6; i++) {
|
|
249
|
+
const h = headers[`h${i}`];
|
|
250
|
+
if (h) parts.push(h);
|
|
251
|
+
}
|
|
252
|
+
return parts.length > 0 ? parts.join(" > ") + "\n\n" : "";
|
|
253
|
+
}
|
|
74
254
|
};
|
|
255
|
+
|
|
256
|
+
// src/loaders.ts
|
|
257
|
+
var import_promises = require("fs/promises");
|
|
258
|
+
var import_node_fs = require("fs");
|
|
259
|
+
var TextLoader = class {
|
|
260
|
+
name = "text";
|
|
261
|
+
async load(source) {
|
|
262
|
+
if ((0, import_node_fs.existsSync)(source)) {
|
|
263
|
+
return (0, import_promises.readFile)(source, "utf-8");
|
|
264
|
+
}
|
|
265
|
+
return source;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
var MarkdownLoader = class {
|
|
269
|
+
name = "markdown";
|
|
270
|
+
async load(source) {
|
|
271
|
+
let text;
|
|
272
|
+
if ((0, import_node_fs.existsSync)(source)) {
|
|
273
|
+
text = await (0, import_promises.readFile)(source, "utf-8");
|
|
274
|
+
} else {
|
|
275
|
+
text = source;
|
|
276
|
+
}
|
|
277
|
+
const frontMatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
278
|
+
return text.replace(frontMatterRegex, "").trim();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
var JSONLoader = class {
|
|
282
|
+
name = "json";
|
|
283
|
+
textKeys;
|
|
284
|
+
separator;
|
|
285
|
+
constructor(options = {}) {
|
|
286
|
+
this.textKeys = options.textKeys ?? ["content", "text", "body", "description"];
|
|
287
|
+
this.separator = options.separator ?? "\n\n";
|
|
288
|
+
}
|
|
289
|
+
async load(source) {
|
|
290
|
+
let raw;
|
|
291
|
+
if ((0, import_node_fs.existsSync)(source)) {
|
|
292
|
+
raw = await (0, import_promises.readFile)(source, "utf-8");
|
|
293
|
+
} else {
|
|
294
|
+
raw = source;
|
|
295
|
+
}
|
|
296
|
+
const data = JSON.parse(raw);
|
|
297
|
+
const texts = this.extractTexts(data);
|
|
298
|
+
return texts.join(this.separator);
|
|
299
|
+
}
|
|
300
|
+
extractTexts(data) {
|
|
301
|
+
const results = [];
|
|
302
|
+
if (Array.isArray(data)) {
|
|
303
|
+
for (const item of data) {
|
|
304
|
+
results.push(...this.extractTexts(item));
|
|
305
|
+
}
|
|
306
|
+
} else if (data !== null && typeof data === "object") {
|
|
307
|
+
const obj = data;
|
|
308
|
+
for (const key of this.textKeys) {
|
|
309
|
+
if (key in obj && typeof obj[key] === "string") {
|
|
310
|
+
results.push(obj[key]);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (results.length === 0) {
|
|
314
|
+
for (const value of Object.values(obj)) {
|
|
315
|
+
if (typeof value === "string" && value.length > 20) {
|
|
316
|
+
results.push(value);
|
|
317
|
+
} else if (typeof value === "object" && value !== null) {
|
|
318
|
+
results.push(...this.extractTexts(value));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else if (typeof data === "string") {
|
|
323
|
+
results.push(data);
|
|
324
|
+
}
|
|
325
|
+
return results;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
var WebLoader = class {
|
|
329
|
+
name = "web";
|
|
330
|
+
async load(source) {
|
|
331
|
+
const response = await fetch(source);
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
throw new Error(`[voltx/rag] WebLoader failed to fetch ${source}: ${response.status}`);
|
|
334
|
+
}
|
|
335
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
336
|
+
const text = await response.text();
|
|
337
|
+
if (contentType.includes("text/html")) {
|
|
338
|
+
return this.stripHTML(text);
|
|
339
|
+
}
|
|
340
|
+
return text;
|
|
341
|
+
}
|
|
342
|
+
stripHTML(html) {
|
|
343
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// src/embedder.ts
|
|
348
|
+
var import_ai = require("@voltx/ai");
|
|
349
|
+
function createEmbedder(config) {
|
|
350
|
+
const { model } = config;
|
|
351
|
+
return {
|
|
352
|
+
name: `voltx-embedder:${model}`,
|
|
353
|
+
async embed(text) {
|
|
354
|
+
const result = await (0, import_ai.embed)({ model, value: text });
|
|
355
|
+
return result.embedding;
|
|
356
|
+
},
|
|
357
|
+
async embedBatch(texts) {
|
|
358
|
+
if (texts.length === 0) return [];
|
|
359
|
+
if (texts.length === 1) {
|
|
360
|
+
const result2 = await (0, import_ai.embed)({ model, value: texts[0] });
|
|
361
|
+
return [result2.embedding];
|
|
362
|
+
}
|
|
363
|
+
const result = await (0, import_ai.embedMany)({ model, values: texts });
|
|
364
|
+
return result.embeddings;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/document.ts
|
|
370
|
+
var MDocument = class _MDocument {
|
|
371
|
+
content;
|
|
372
|
+
format;
|
|
373
|
+
chunks = null;
|
|
374
|
+
constructor(content, format) {
|
|
375
|
+
this.content = content;
|
|
376
|
+
this.format = format;
|
|
377
|
+
}
|
|
378
|
+
/** Create from plain text */
|
|
379
|
+
static fromText(content) {
|
|
380
|
+
return new _MDocument(content, "text");
|
|
381
|
+
}
|
|
382
|
+
/** Create from markdown */
|
|
383
|
+
static fromMarkdown(content) {
|
|
384
|
+
return new _MDocument(content, "markdown");
|
|
385
|
+
}
|
|
386
|
+
/** Create from JSON string */
|
|
387
|
+
static fromJSON(content) {
|
|
388
|
+
JSON.parse(content);
|
|
389
|
+
return new _MDocument(content, "json");
|
|
390
|
+
}
|
|
391
|
+
/** Create from HTML (strips tags) */
|
|
392
|
+
static fromHTML(html) {
|
|
393
|
+
const text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
394
|
+
return new _MDocument(text, "html");
|
|
395
|
+
}
|
|
396
|
+
/** Get the raw content */
|
|
397
|
+
getContent() {
|
|
398
|
+
return this.content;
|
|
399
|
+
}
|
|
400
|
+
/** Get the document format */
|
|
401
|
+
getFormat() {
|
|
402
|
+
return this.format;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Split the document into chunks using the specified strategy.
|
|
406
|
+
* Returns the chunks and caches them for subsequent embed() calls.
|
|
407
|
+
*/
|
|
408
|
+
chunk(options = {}) {
|
|
409
|
+
const strategy = options.strategy ?? this.defaultStrategy();
|
|
410
|
+
const splitter = this.createSplitter(strategy, options);
|
|
411
|
+
this.chunks = splitter.split(this.content);
|
|
412
|
+
return this.chunks;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Embed the chunks using the provided embedder.
|
|
416
|
+
* Must call chunk() first.
|
|
417
|
+
*/
|
|
418
|
+
async embed(embedder) {
|
|
419
|
+
if (!this.chunks) {
|
|
420
|
+
throw new Error("[voltx/rag] Call chunk() before embed()");
|
|
421
|
+
}
|
|
422
|
+
const texts = this.chunks.map((c) => c.content);
|
|
423
|
+
const embeddings = await embedder.embedBatch(texts);
|
|
424
|
+
for (let i = 0; i < this.chunks.length; i++) {
|
|
425
|
+
this.chunks[i].embedding = embeddings[i];
|
|
426
|
+
}
|
|
427
|
+
return this.chunks;
|
|
428
|
+
}
|
|
429
|
+
/** Get cached chunks (null if chunk() hasn't been called) */
|
|
430
|
+
getChunks() {
|
|
431
|
+
return this.chunks;
|
|
432
|
+
}
|
|
433
|
+
defaultStrategy() {
|
|
434
|
+
if (this.format === "markdown") return "markdown";
|
|
435
|
+
return "recursive";
|
|
436
|
+
}
|
|
437
|
+
createSplitter(strategy, options) {
|
|
438
|
+
switch (strategy) {
|
|
439
|
+
case "markdown":
|
|
440
|
+
return new MarkdownSplitter({
|
|
441
|
+
chunkSize: options.chunkSize,
|
|
442
|
+
overlap: options.overlap,
|
|
443
|
+
includeHeaders: options.includeHeaders
|
|
444
|
+
});
|
|
445
|
+
case "character":
|
|
446
|
+
return new CharacterSplitter({
|
|
447
|
+
chunkSize: options.chunkSize,
|
|
448
|
+
overlap: options.overlap
|
|
449
|
+
});
|
|
450
|
+
case "recursive":
|
|
451
|
+
default:
|
|
452
|
+
return new RecursiveTextSplitter({
|
|
453
|
+
chunkSize: options.chunkSize,
|
|
454
|
+
overlap: options.overlap,
|
|
455
|
+
separators: options.separators
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/utils.ts
|
|
462
|
+
function cosineSimilarity(a, b) {
|
|
463
|
+
if (a.length !== b.length) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`[voltx/rag] Vector dimension mismatch: ${a.length} vs ${b.length}`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
let dotProduct = 0;
|
|
469
|
+
let normA = 0;
|
|
470
|
+
let normB = 0;
|
|
471
|
+
for (let i = 0; i < a.length; i++) {
|
|
472
|
+
dotProduct += a[i] * b[i];
|
|
473
|
+
normA += a[i] * a[i];
|
|
474
|
+
normB += b[i] * b[i];
|
|
475
|
+
}
|
|
476
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
477
|
+
if (magnitude === 0) return 0;
|
|
478
|
+
return dotProduct / magnitude;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/index.ts
|
|
75
482
|
var RAGPipeline = class {
|
|
76
|
-
|
|
483
|
+
loader;
|
|
484
|
+
splitter;
|
|
485
|
+
embedder;
|
|
486
|
+
vectorStore;
|
|
77
487
|
constructor(config) {
|
|
78
|
-
this.
|
|
488
|
+
this.loader = config.loader;
|
|
489
|
+
this.splitter = config.splitter ?? new RecursiveTextSplitter();
|
|
490
|
+
this.embedder = config.embedder;
|
|
491
|
+
this.vectorStore = config.vectorStore;
|
|
79
492
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Ingest a document: load → split → embed (batch) → store in vector DB.
|
|
495
|
+
*
|
|
496
|
+
* @param source - File path, URL, or raw text (depends on loader)
|
|
497
|
+
* @param idPrefix - Optional prefix for chunk IDs (default: "doc")
|
|
498
|
+
* @returns Number of chunks ingested and their IDs
|
|
499
|
+
*/
|
|
500
|
+
async ingest(source, idPrefix = "doc") {
|
|
501
|
+
const text = this.loader ? await this.loader.load(source) : source;
|
|
502
|
+
const chunks = this.splitter.split(text);
|
|
85
503
|
const texts = chunks.map((c) => c.content);
|
|
86
|
-
const embeddings = await embedder.embedBatch(texts);
|
|
504
|
+
const embeddings = await this.embedder.embedBatch(texts);
|
|
87
505
|
const docs = chunks.map((chunk, i) => ({
|
|
88
|
-
id: chunk.id
|
|
506
|
+
id: `${idPrefix}-${chunk.id}`,
|
|
89
507
|
content: chunk.content,
|
|
90
508
|
embedding: embeddings[i],
|
|
91
509
|
metadata: chunk.metadata
|
|
92
510
|
}));
|
|
93
|
-
await vectorStore.upsert(docs);
|
|
94
|
-
return docs.length;
|
|
95
|
-
}
|
|
96
|
-
/** Query: embed question → search vector store → return sources */
|
|
97
|
-
async query(question, topK = 5) {
|
|
98
|
-
const { embedder, vectorStore } = this.config;
|
|
99
|
-
const embedding = await embedder.embed(question);
|
|
100
|
-
const results = await vectorStore.search(embedding, topK);
|
|
511
|
+
await this.vectorStore.upsert(docs);
|
|
101
512
|
return {
|
|
102
|
-
|
|
513
|
+
chunks: docs.length,
|
|
514
|
+
ids: docs.map((d) => d.id)
|
|
103
515
|
};
|
|
104
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Query: embed question → search vector store → return ranked sources.
|
|
519
|
+
*
|
|
520
|
+
* @param question - The user's question
|
|
521
|
+
* @param options - Query options (topK, minScore)
|
|
522
|
+
*/
|
|
523
|
+
async query(question, options = {}) {
|
|
524
|
+
const { topK = 5, minScore = 0 } = options;
|
|
525
|
+
const queryEmbedding = await this.embedder.embed(question);
|
|
526
|
+
const results = await this.vectorStore.search(queryEmbedding, topK);
|
|
527
|
+
const filtered = minScore > 0 ? results.filter((r) => r.score >= minScore) : results;
|
|
528
|
+
return {
|
|
529
|
+
sources: filtered.map((r) => r.document),
|
|
530
|
+
queryEmbedding
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Convenience: query + format sources into a context string for LLM prompts.
|
|
535
|
+
*/
|
|
536
|
+
async getContext(question, options = {}) {
|
|
537
|
+
const { sources } = await this.query(question, options);
|
|
538
|
+
if (sources.length === 0) {
|
|
539
|
+
return "No relevant context found.";
|
|
540
|
+
}
|
|
541
|
+
return sources.map((s, i) => `[Source ${i + 1}]
|
|
542
|
+
${s.content}`).join("\n\n---\n\n");
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Delete documents from the vector store by IDs.
|
|
546
|
+
*/
|
|
547
|
+
async delete(ids) {
|
|
548
|
+
await this.vectorStore.delete(ids);
|
|
549
|
+
}
|
|
105
550
|
};
|
|
106
551
|
function createRAGPipeline(config) {
|
|
107
552
|
return new RAGPipeline(config);
|
|
108
553
|
}
|
|
109
|
-
var VERSION = "0.
|
|
554
|
+
var VERSION = "0.3.0";
|
|
110
555
|
// Annotate the CommonJS export names for ESM import in node:
|
|
111
556
|
0 && (module.exports = {
|
|
112
557
|
CharacterSplitter,
|
|
558
|
+
JSONLoader,
|
|
559
|
+
MDocument,
|
|
560
|
+
MarkdownLoader,
|
|
561
|
+
MarkdownSplitter,
|
|
113
562
|
RAGPipeline,
|
|
563
|
+
RecursiveTextSplitter,
|
|
564
|
+
TextLoader,
|
|
114
565
|
VERSION,
|
|
566
|
+
WebLoader,
|
|
567
|
+
cosineSimilarity,
|
|
568
|
+
createEmbedder,
|
|
115
569
|
createRAGPipeline
|
|
116
570
|
});
|