agentfootprint 8.9.0 → 8.10.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/AGENTS.md +2 -0
- package/CLAUDE.md +4 -0
- package/bin/agentfootprint-index.mjs +192 -0
- package/dist/doors/rag.js +66 -0
- package/dist/doors/rag.js.map +1 -0
- package/dist/esm/doors/rag.d.ts +49 -0
- package/dist/esm/doors/rag.js +50 -0
- package/dist/esm/doors/rag.js.map +1 -0
- package/dist/esm/rag/hash.d.ts +2 -0
- package/dist/esm/rag/hash.js +22 -0
- package/dist/esm/rag/hash.js.map +1 -0
- package/dist/esm/rag/index.d.ts +17 -0
- package/dist/esm/rag/index.js +17 -0
- package/dist/esm/rag/index.js.map +1 -0
- package/dist/esm/rag/indexCorpus.d.ts +102 -0
- package/dist/esm/rag/indexCorpus.js +339 -0
- package/dist/esm/rag/indexCorpus.js.map +1 -0
- package/dist/esm/rag/indexFolder.d.ts +40 -0
- package/dist/esm/rag/indexFolder.js +42 -0
- package/dist/esm/rag/indexFolder.js.map +1 -0
- package/dist/esm/rag/loadDocuments.d.ts +34 -0
- package/dist/esm/rag/loadDocuments.js +160 -0
- package/dist/esm/rag/loadDocuments.js.map +1 -0
- package/dist/esm/rag/loaders/html.d.ts +42 -0
- package/dist/esm/rag/loaders/html.js +48 -0
- package/dist/esm/rag/loaders/html.js.map +1 -0
- package/dist/esm/rag/loaders/index.d.ts +30 -0
- package/dist/esm/rag/loaders/index.js +24 -0
- package/dist/esm/rag/loaders/index.js.map +1 -0
- package/dist/esm/rag/loaders/markdown.d.ts +27 -0
- package/dist/esm/rag/loaders/markdown.js +12 -0
- package/dist/esm/rag/loaders/markdown.js.map +1 -0
- package/dist/esm/rag/loaders/mock.d.ts +28 -0
- package/dist/esm/rag/loaders/mock.js +14 -0
- package/dist/esm/rag/loaders/mock.js.map +1 -0
- package/dist/esm/rag/loaders/pdf.d.ts +39 -0
- package/dist/esm/rag/loaders/pdf.js +90 -0
- package/dist/esm/rag/loaders/pdf.js.map +1 -0
- package/dist/esm/rag/loaders/text.d.ts +23 -0
- package/dist/esm/rag/loaders/text.js +21 -0
- package/dist/esm/rag/loaders/text.js.map +1 -0
- package/dist/esm/rag/splitDocuments.d.ts +21 -0
- package/dist/esm/rag/splitDocuments.js +65 -0
- package/dist/esm/rag/splitDocuments.js.map +1 -0
- package/dist/esm/rag/splitters/byHeading.d.ts +36 -0
- package/dist/esm/rag/splitters/byHeading.js +66 -0
- package/dist/esm/rag/splitters/byHeading.js.map +1 -0
- package/dist/esm/rag/splitters/byParagraph.d.ts +24 -0
- package/dist/esm/rag/splitters/byParagraph.js +16 -0
- package/dist/esm/rag/splitters/byParagraph.js.map +1 -0
- package/dist/esm/rag/splitters/constants.d.ts +19 -0
- package/dist/esm/rag/splitters/constants.js +20 -0
- package/dist/esm/rag/splitters/constants.js.map +1 -0
- package/dist/esm/rag/splitters/fixedWithOverlap.d.ts +31 -0
- package/dist/esm/rag/splitters/fixedWithOverlap.js +34 -0
- package/dist/esm/rag/splitters/fixedWithOverlap.js.map +1 -0
- package/dist/esm/rag/splitters/index.d.ts +50 -0
- package/dist/esm/rag/splitters/index.js +51 -0
- package/dist/esm/rag/splitters/index.js.map +1 -0
- package/dist/esm/rag/splitters/shared.d.ts +87 -0
- package/dist/esm/rag/splitters/shared.js +188 -0
- package/dist/esm/rag/splitters/shared.js.map +1 -0
- package/dist/esm/rag/splitters/wholeDocument.d.ts +16 -0
- package/dist/esm/rag/splitters/wholeDocument.js +10 -0
- package/dist/esm/rag/splitters/wholeDocument.js.map +1 -0
- package/dist/esm/rag/types.d.ts +200 -0
- package/dist/esm/rag/types.js +20 -0
- package/dist/esm/rag/types.js.map +1 -0
- package/dist/rag/hash.js +26 -0
- package/dist/rag/hash.js.map +1 -0
- package/dist/rag/index.js +40 -0
- package/dist/rag/index.js.map +1 -0
- package/dist/rag/indexCorpus.js +344 -0
- package/dist/rag/indexCorpus.js.map +1 -0
- package/dist/rag/indexFolder.js +46 -0
- package/dist/rag/indexFolder.js.map +1 -0
- package/dist/rag/loadDocuments.js +164 -0
- package/dist/rag/loadDocuments.js.map +1 -0
- package/dist/rag/loaders/html.js +53 -0
- package/dist/rag/loaders/html.js.map +1 -0
- package/dist/rag/loaders/index.js +33 -0
- package/dist/rag/loaders/index.js.map +1 -0
- package/dist/rag/loaders/markdown.js +16 -0
- package/dist/rag/loaders/markdown.js.map +1 -0
- package/dist/rag/loaders/mock.js +18 -0
- package/dist/rag/loaders/mock.js.map +1 -0
- package/dist/rag/loaders/pdf.js +118 -0
- package/dist/rag/loaders/pdf.js.map +1 -0
- package/dist/rag/loaders/text.js +26 -0
- package/dist/rag/loaders/text.js.map +1 -0
- package/dist/rag/splitDocuments.js +69 -0
- package/dist/rag/splitDocuments.js.map +1 -0
- package/dist/rag/splitters/byHeading.js +70 -0
- package/dist/rag/splitters/byHeading.js.map +1 -0
- package/dist/rag/splitters/byParagraph.js +20 -0
- package/dist/rag/splitters/byParagraph.js.map +1 -0
- package/dist/rag/splitters/constants.js +23 -0
- package/dist/rag/splitters/constants.js.map +1 -0
- package/dist/rag/splitters/fixedWithOverlap.js +38 -0
- package/dist/rag/splitters/fixedWithOverlap.js.map +1 -0
- package/dist/rag/splitters/index.js +60 -0
- package/dist/rag/splitters/index.js.map +1 -0
- package/dist/rag/splitters/shared.js +199 -0
- package/dist/rag/splitters/shared.js.map +1 -0
- package/dist/rag/splitters/wholeDocument.js +14 -0
- package/dist/rag/splitters/wholeDocument.js.map +1 -0
- package/dist/rag/types.js +21 -0
- package/dist/rag/types.js.map +1 -0
- package/dist/types/doors/rag.d.ts +50 -0
- package/dist/types/doors/rag.d.ts.map +1 -0
- package/dist/types/rag/hash.d.ts +3 -0
- package/dist/types/rag/hash.d.ts.map +1 -0
- package/dist/types/rag/index.d.ts +18 -0
- package/dist/types/rag/index.d.ts.map +1 -0
- package/dist/types/rag/indexCorpus.d.ts +103 -0
- package/dist/types/rag/indexCorpus.d.ts.map +1 -0
- package/dist/types/rag/indexFolder.d.ts +41 -0
- package/dist/types/rag/indexFolder.d.ts.map +1 -0
- package/dist/types/rag/loadDocuments.d.ts +35 -0
- package/dist/types/rag/loadDocuments.d.ts.map +1 -0
- package/dist/types/rag/loaders/html.d.ts +43 -0
- package/dist/types/rag/loaders/html.d.ts.map +1 -0
- package/dist/types/rag/loaders/index.d.ts +31 -0
- package/dist/types/rag/loaders/index.d.ts.map +1 -0
- package/dist/types/rag/loaders/markdown.d.ts +28 -0
- package/dist/types/rag/loaders/markdown.d.ts.map +1 -0
- package/dist/types/rag/loaders/mock.d.ts +29 -0
- package/dist/types/rag/loaders/mock.d.ts.map +1 -0
- package/dist/types/rag/loaders/pdf.d.ts +40 -0
- package/dist/types/rag/loaders/pdf.d.ts.map +1 -0
- package/dist/types/rag/loaders/text.d.ts +24 -0
- package/dist/types/rag/loaders/text.d.ts.map +1 -0
- package/dist/types/rag/splitDocuments.d.ts +22 -0
- package/dist/types/rag/splitDocuments.d.ts.map +1 -0
- package/dist/types/rag/splitters/byHeading.d.ts +37 -0
- package/dist/types/rag/splitters/byHeading.d.ts.map +1 -0
- package/dist/types/rag/splitters/byParagraph.d.ts +25 -0
- package/dist/types/rag/splitters/byParagraph.d.ts.map +1 -0
- package/dist/types/rag/splitters/constants.d.ts +20 -0
- package/dist/types/rag/splitters/constants.d.ts.map +1 -0
- package/dist/types/rag/splitters/fixedWithOverlap.d.ts +32 -0
- package/dist/types/rag/splitters/fixedWithOverlap.d.ts.map +1 -0
- package/dist/types/rag/splitters/index.d.ts +51 -0
- package/dist/types/rag/splitters/index.d.ts.map +1 -0
- package/dist/types/rag/splitters/shared.d.ts +88 -0
- package/dist/types/rag/splitters/shared.d.ts.map +1 -0
- package/dist/types/rag/splitters/wholeDocument.d.ts +17 -0
- package/dist/types/rag/splitters/wholeDocument.d.ts.map +1 -0
- package/dist/types/rag/types.d.ts +201 -0
- package/dist/types/rag/types.d.ts.map +1 -0
- package/package.json +23 -4
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* htmlLoader — HTML with the tags taken out, without a parser.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Adapter behind `DocumentLoader`.
|
|
5
|
+
* Role: rag/ layer. Zero dependency.
|
|
6
|
+
* Emits: N/A.
|
|
7
|
+
*
|
|
8
|
+
* ── What this is, and what it is not ────────────────────────────────────────
|
|
9
|
+
* This is a **tag stripper**, not an HTML parser, and the distinction is worth
|
|
10
|
+
* being blunt about because the failure mode is silent. It removes `<script>`
|
|
11
|
+
* and `<style>` bodies, replaces tags with whitespace, and decodes the handful
|
|
12
|
+
* of entities that appear in real prose. That is enough for documentation
|
|
13
|
+
* pages, exported articles, and saved knowledge-base entries — the things
|
|
14
|
+
* people actually put in a corpus.
|
|
15
|
+
*
|
|
16
|
+
* It is NOT enough for a modern application page. A single-page app's HTML is
|
|
17
|
+
* mostly scaffolding, and what you get back will be navigation labels and
|
|
18
|
+
* button text. If your corpus is that, run a real extractor (Readability,
|
|
19
|
+
* Mercury, your CMS's own export) and feed the result in as text — the
|
|
20
|
+
* `{ text, uri }` arm of `DocumentSource` exists for exactly that.
|
|
21
|
+
*
|
|
22
|
+
* A parser was considered and rejected for v1: every option is a dependency
|
|
23
|
+
* measured in megabytes, and the difference it buys on the documents people
|
|
24
|
+
* index is small. If that stops being true, the fix is another
|
|
25
|
+
* `DocumentLoader` passed ahead of this one — no change here.
|
|
26
|
+
*
|
|
27
|
+
* ── Offsets ─────────────────────────────────────────────────────────────────
|
|
28
|
+
* Tags are replaced with whitespace of the SAME LENGTH rather than deleted, so
|
|
29
|
+
* offsets into the extracted text line up with offsets into the original file.
|
|
30
|
+
* A chunk's `charStart` therefore points at the same place in both, and the
|
|
31
|
+
* text that gets stored is the extracted text, which is what the offsets index.
|
|
32
|
+
*/
|
|
33
|
+
import type { DocumentLoader } from '../types.js';
|
|
34
|
+
/**
|
|
35
|
+
* Strip markup to text.
|
|
36
|
+
*
|
|
37
|
+
* Exported because the splitter tests and the security tests both need to
|
|
38
|
+
* assert on it directly, and because a consumer writing their own loader for a
|
|
39
|
+
* markup-ish format may reasonably want the same pass.
|
|
40
|
+
*/
|
|
41
|
+
export declare function stripTags(html: string): string;
|
|
42
|
+
export declare function htmlLoader(): DocumentLoader;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { decodeText } from './text.js';
|
|
2
|
+
/** Blank a matched region, preserving newlines so line structure survives. */
|
|
3
|
+
function blank(match) {
|
|
4
|
+
return match.replace(/[^\n]/g, ' ');
|
|
5
|
+
}
|
|
6
|
+
const ENTITIES = {
|
|
7
|
+
'&': '&',
|
|
8
|
+
'<': '<',
|
|
9
|
+
'>': '>',
|
|
10
|
+
'"': '"',
|
|
11
|
+
''': "'",
|
|
12
|
+
''': "'",
|
|
13
|
+
' ': ' ',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Strip markup to text.
|
|
17
|
+
*
|
|
18
|
+
* Exported because the splitter tests and the security tests both need to
|
|
19
|
+
* assert on it directly, and because a consumer writing their own loader for a
|
|
20
|
+
* markup-ish format may reasonably want the same pass.
|
|
21
|
+
*/
|
|
22
|
+
export function stripTags(html) {
|
|
23
|
+
let out = html;
|
|
24
|
+
// Script and style CONTENT is not prose. Blanked wholesale, bodies included.
|
|
25
|
+
out = out.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, blank);
|
|
26
|
+
out = out.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, blank);
|
|
27
|
+
out = out.replace(/<!--[\s\S]*?-->/g, blank);
|
|
28
|
+
// Every remaining tag becomes whitespace of equal length — see the header.
|
|
29
|
+
out = out.replace(/<[^>]*>/g, blank);
|
|
30
|
+
// Entities are decoded LAST and only for the fixed set above. A general
|
|
31
|
+
// numeric decode would let `<script>` reappear as a tag in text that
|
|
32
|
+
// has already been stripped.
|
|
33
|
+
for (const [entity, char] of Object.entries(ENTITIES)) {
|
|
34
|
+
out = out.split(entity).join(char.padEnd(entity.length, ' '));
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
export function htmlLoader() {
|
|
39
|
+
return {
|
|
40
|
+
name: 'html',
|
|
41
|
+
extensions: ['.html', '.htm', '.xhtml'],
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
43
|
+
async load(input) {
|
|
44
|
+
return { text: stripTags(decodeText(input.bytes)) };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../../../src/rag/loaders/html.ts"],"names":[],"mappings":"AAiCA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,8EAA8E;AAC9E,SAAS,KAAK,CAAC,KAAa;IAC1B,OAAO,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,QAAQ,GAAqC;IACjD,OAAO,EAAE,GAAG;IACZ,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,GAAG;IACX,QAAQ,EAAE,GAAG;IACb,OAAO,EAAE,GAAG;IACZ,QAAQ,EAAE,GAAG;IACb,QAAQ,EAAE,GAAG;CACd,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,6EAA6E;IAC7E,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,wCAAwC,EAAE,KAAK,CAAC,CAAC;IACnE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,sCAAsC,EAAE,KAAK,CAAC,CAAC;IACjE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;IAC7C,2EAA2E;IAC3E,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACrC,wEAAwE;IACxE,6EAA6E;IAC7E,6BAA6B;IAC7B,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtD,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC;QACvC,4DAA4D;QAC5D,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACtD,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rag/loaders — one adapter per format, each doing exactly one job: turn
|
|
3
|
+
* bytes into text plus the positions that text came from.
|
|
4
|
+
*
|
|
5
|
+
* Pattern: Adapter (one per format) behind the `DocumentLoader` port.
|
|
6
|
+
* Role: rag/ layer, outermost ring. Three of the five need no dependency
|
|
7
|
+
* at all; only PDF does, and it lazy-loads a single optional peer so
|
|
8
|
+
* importing this barrel costs nothing for a consumer indexing
|
|
9
|
+
* Markdown.
|
|
10
|
+
* Emits: N/A — a loader reads a file and returns text.
|
|
11
|
+
*
|
|
12
|
+
* `DEFAULT_LOADERS` is the routing table `loadDocuments` uses when you pass
|
|
13
|
+
* none. A loader you pass yourself is consulted FIRST, so overriding one is
|
|
14
|
+
* putting yours in front rather than editing a registry.
|
|
15
|
+
*/
|
|
16
|
+
import type { DocumentLoader } from '../types.js';
|
|
17
|
+
export { textLoader } from './text.js';
|
|
18
|
+
export { markdownLoader } from './markdown.js';
|
|
19
|
+
export { htmlLoader } from './html.js';
|
|
20
|
+
export { pdfLoader, MissingPdfSupportError, type PdfLoaderOptions } from './pdf.js';
|
|
21
|
+
export { mockLoader, type MockLoaderOptions } from './mock.js';
|
|
22
|
+
/**
|
|
23
|
+
* The loaders `loadDocuments` uses when the caller names none, in routing
|
|
24
|
+
* order.
|
|
25
|
+
*
|
|
26
|
+
* `pdfLoader()` is in the list, but constructing it costs nothing — the
|
|
27
|
+
* optional peer is loaded on the first PDF it is actually asked to read, so a
|
|
28
|
+
* corpus of Markdown never touches it and never needs it installed.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_LOADERS: readonly DocumentLoader[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { textLoader } from './text.js';
|
|
2
|
+
import { markdownLoader } from './markdown.js';
|
|
3
|
+
import { htmlLoader } from './html.js';
|
|
4
|
+
import { pdfLoader } from './pdf.js';
|
|
5
|
+
export { textLoader } from './text.js';
|
|
6
|
+
export { markdownLoader } from './markdown.js';
|
|
7
|
+
export { htmlLoader } from './html.js';
|
|
8
|
+
export { pdfLoader, MissingPdfSupportError } from './pdf.js';
|
|
9
|
+
export { mockLoader } from './mock.js';
|
|
10
|
+
/**
|
|
11
|
+
* The loaders `loadDocuments` uses when the caller names none, in routing
|
|
12
|
+
* order.
|
|
13
|
+
*
|
|
14
|
+
* `pdfLoader()` is in the list, but constructing it costs nothing — the
|
|
15
|
+
* optional peer is loaded on the first PDF it is actually asked to read, so a
|
|
16
|
+
* corpus of Markdown never touches it and never needs it installed.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_LOADERS = Object.freeze([
|
|
19
|
+
markdownLoader(),
|
|
20
|
+
textLoader(),
|
|
21
|
+
htmlLoader(),
|
|
22
|
+
pdfLoader(),
|
|
23
|
+
]);
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/rag/loaders/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAyB,MAAM,UAAU,CAAC;AACpF,OAAO,EAAE,UAAU,EAA0B,MAAM,WAAW,CAAC;AAE/D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAA8B,MAAM,CAAC,MAAM,CAAC;IACtE,cAAc,EAAE;IAChB,UAAU,EAAE;IACZ,UAAU,EAAE;IACZ,SAAS,EAAE;CACZ,CAAC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdownLoader — Markdown, kept as Markdown.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Adapter behind `DocumentLoader`.
|
|
5
|
+
* Role: rag/ layer. Zero dependency, and it deliberately does NOT parse.
|
|
6
|
+
* Emits: N/A.
|
|
7
|
+
*
|
|
8
|
+
* ── Why the markup stays ────────────────────────────────────────────────────
|
|
9
|
+
* The obvious thing to do here is strip the syntax and hand over prose. It is
|
|
10
|
+
* the wrong thing, twice over:
|
|
11
|
+
*
|
|
12
|
+
* 1. **The headings are the document's own structure.** `byHeading` cuts on
|
|
13
|
+
* `#` lines, which is the one splitting rule in this library that is not a
|
|
14
|
+
* heuristic — the author already told us where the sections are. Stripping
|
|
15
|
+
* `#` throws that away and leaves us guessing at boundaries we were handed.
|
|
16
|
+
* 2. **Stripping shifts every offset.** `text.slice(charStart, charEnd)` has to
|
|
17
|
+
* equal the chunk, against the text that was stored. A loader that rewrites
|
|
18
|
+
* text and then reports offsets into the original is reporting fiction.
|
|
19
|
+
*
|
|
20
|
+
* Models read Markdown perfectly well; a table that survives as a table is
|
|
21
|
+
* more useful in a prompt than the same table flattened into a word salad.
|
|
22
|
+
*
|
|
23
|
+
* What it DOES do is the same normalisation `textLoader` does, for the same
|
|
24
|
+
* reason: line endings, before any offset exists.
|
|
25
|
+
*/
|
|
26
|
+
import type { DocumentLoader } from '../types.js';
|
|
27
|
+
export declare function markdownLoader(): DocumentLoader;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { decodeText } from './text.js';
|
|
2
|
+
export function markdownLoader() {
|
|
3
|
+
return {
|
|
4
|
+
name: 'markdown',
|
|
5
|
+
extensions: ['.md', '.markdown', '.mdx'],
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
7
|
+
async load(input) {
|
|
8
|
+
return { text: decodeText(input.bytes) };
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../../../src/rag/loaders/markdown.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,UAAU,cAAc;IAC5B,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,UAAU,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC;QACxC,4DAA4D;QAC5D,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mockLoader — a loader with no filesystem and no format, for tests.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Adapter behind `DocumentLoader`.
|
|
5
|
+
* Role: rag/ layer. The first rung of the mock-first ladder for indexing:
|
|
6
|
+
* prove the pipeline runs — discover, split, plan, embed, store,
|
|
7
|
+
* report — before a single real file or a single real embedding is
|
|
8
|
+
* involved.
|
|
9
|
+
* Emits: N/A.
|
|
10
|
+
*
|
|
11
|
+
* It claims whatever extensions you give it (default: everything a test is
|
|
12
|
+
* likely to name), and returns the text you handed it. Unlike the other
|
|
13
|
+
* loaders it never touches bytes at all, which is exactly what makes it
|
|
14
|
+
* useful: a test can assert on chunking and planning without a temp directory.
|
|
15
|
+
*/
|
|
16
|
+
import type { DocumentLoader } from '../types.js';
|
|
17
|
+
export interface MockLoaderOptions {
|
|
18
|
+
/** Extensions to claim. Default: a broad set, so routing is never the thing under test. */
|
|
19
|
+
readonly extensions?: readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* Return text for a uri, instead of decoding the bytes. Use for a fixture
|
|
22
|
+
* corpus that exists only in the test file.
|
|
23
|
+
*/
|
|
24
|
+
readonly textFor?: (uri: string) => string | undefined;
|
|
25
|
+
/** Pretend this uri is paginated — for exercising page provenance without a PDF. */
|
|
26
|
+
readonly pagesFor?: (uri: string) => readonly string[] | undefined;
|
|
27
|
+
}
|
|
28
|
+
export declare function mockLoader(options?: MockLoaderOptions): DocumentLoader;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { decodeText } from './text.js';
|
|
2
|
+
export function mockLoader(options = {}) {
|
|
3
|
+
return {
|
|
4
|
+
name: 'mock',
|
|
5
|
+
extensions: options.extensions ?? ['.md', '.txt', '.html', '.pdf', '.mock'],
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
7
|
+
async load(input) {
|
|
8
|
+
const pages = options.pagesFor?.(input.uri);
|
|
9
|
+
const text = options.textFor?.(input.uri) ?? decodeText(input.bytes);
|
|
10
|
+
return { text: pages ? pages.join('\n\n') : text, ...(pages && { pages }) };
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=mock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock.js","sourceRoot":"","sources":["../../../../src/rag/loaders/mock.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAcvC,MAAM,UAAU,UAAU,CAAC,UAA6B,EAAE;IACxD,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC;QAC3E,4DAA4D;QAC5D,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC5C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QAC9E,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DocumentLoader } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The slice of `unpdf` this loader uses — declared structurally, so a stub, a
|
|
4
|
+
* pinned fork, or a future version satisfies it without this package taking a
|
|
5
|
+
* hard type dependency on an optional peer.
|
|
6
|
+
*/
|
|
7
|
+
export interface UnpdfBackend {
|
|
8
|
+
getDocumentProxy(data: Uint8Array): Promise<unknown>;
|
|
9
|
+
extractText(doc: unknown, options?: {
|
|
10
|
+
mergePages?: boolean;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
totalPages: number;
|
|
13
|
+
text: string | string[];
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export interface PdfLoaderOptions {
|
|
17
|
+
/**
|
|
18
|
+
* An ALREADY-IMPORTED `unpdf`. Supply this and the lazy load never happens —
|
|
19
|
+
* which is what makes the loader work in a BUNDLED app, where a bare
|
|
20
|
+
* specifier reaches the runtime unresolved:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import * as unpdf from 'unpdf';
|
|
24
|
+
* pdfLoader({ backend: unpdf });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The same mechanism the embedders and the store adapters use: the library
|
|
28
|
+
* states the surface it needs, the host owns the construction.
|
|
29
|
+
*/
|
|
30
|
+
readonly backend?: UnpdfBackend;
|
|
31
|
+
}
|
|
32
|
+
/** Raised when a PDF is met and `unpdf` is not installed. */
|
|
33
|
+
export declare class MissingPdfSupportError extends Error {
|
|
34
|
+
readonly code: "ERR_MISSING_PDF_SUPPORT";
|
|
35
|
+
/** The file that could not be read. */
|
|
36
|
+
readonly uri: string;
|
|
37
|
+
constructor(uri: string);
|
|
38
|
+
}
|
|
39
|
+
export declare function pdfLoader(options?: PdfLoaderOptions): DocumentLoader;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pdfLoader — PDF text extraction, page by page.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Adapter behind `DocumentLoader`, over a lazily-loaded optional peer.
|
|
5
|
+
* Role: rag/ layer. The ONE loader that needs a dependency, and the reason
|
|
6
|
+
* it is optional rather than bundled.
|
|
7
|
+
* Emits: N/A.
|
|
8
|
+
*
|
|
9
|
+
* ── Why `unpdf`, measured rather than assumed ───────────────────────────────
|
|
10
|
+
* PDF text extraction cannot be done without a real library. The candidates
|
|
11
|
+
* were installed and measured:
|
|
12
|
+
*
|
|
13
|
+
* | package | installed size | packages | deps | verdict |
|
|
14
|
+
* |---|---|---|---|---|
|
|
15
|
+
* | **`unpdf`** | **2.5 MB** | **1** | **none** | chosen |
|
|
16
|
+
* | `pdf-parse@2` | 86 MB | 3 | `@napi-rs/canvas` (**native**) | a native binary, to read text |
|
|
17
|
+
* | `pdf-parse@1` | 34 MB | 4 | debug, node-ensure | unmaintained since 2018 |
|
|
18
|
+
* | `pdfjs-dist@6` | 62 MB | 2 | none | 25× the size for the same engine |
|
|
19
|
+
*
|
|
20
|
+
* `unpdf` is a serverless build of the same pdf.js engine the heavyweight
|
|
21
|
+
* options wrap, with dual CJS/ESM exports and zero transitive dependencies.
|
|
22
|
+
* A real 2-page PDF extracts in ~95 ms.
|
|
23
|
+
*
|
|
24
|
+
* ── Pages are the point ─────────────────────────────────────────────────────
|
|
25
|
+
* It returns text PER PAGE, and that is why the page number in a citation is a
|
|
26
|
+
* fact rather than a guess. `<source id="handbook.pdf#7" page="2">` can be
|
|
27
|
+
* checked by opening page 2. A loader that flattened the document first could
|
|
28
|
+
* only ever say "somewhere in this PDF".
|
|
29
|
+
*
|
|
30
|
+
* ── A known warning on Node 22 ──────────────────────────────────────────────
|
|
31
|
+
* pdf.js v5 calls `Math.sumPrecise`, which arrives in V8 later than Node 22.
|
|
32
|
+
* The extraction is correct; the console shows `Warning: TypeError:
|
|
33
|
+
* Math.sumPrecise is not a function` per page. It is upstream and harmless,
|
|
34
|
+
* and it is documented here so it does not read as data loss.
|
|
35
|
+
*/
|
|
36
|
+
import { lazyRequire } from '../../lib/lazyRequire.js';
|
|
37
|
+
/** Raised when a PDF is met and `unpdf` is not installed. */
|
|
38
|
+
export class MissingPdfSupportError extends Error {
|
|
39
|
+
code = 'ERR_MISSING_PDF_SUPPORT';
|
|
40
|
+
/** The file that could not be read. */
|
|
41
|
+
uri;
|
|
42
|
+
constructor(uri) {
|
|
43
|
+
super(`Reading '${uri}' requires the \`unpdf\` peer dependency.\n` +
|
|
44
|
+
' Install: npm install unpdf\n' +
|
|
45
|
+
' Or pass `backend` to pdfLoader() if your bundler resolves it statically.');
|
|
46
|
+
this.name = 'MissingPdfSupportError';
|
|
47
|
+
this.uri = uri;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function pdfLoader(options = {}) {
|
|
51
|
+
let backend;
|
|
52
|
+
const getBackend = (uri) => {
|
|
53
|
+
if (options.backend)
|
|
54
|
+
return Promise.resolve(options.backend);
|
|
55
|
+
return (backend ??= (async () => {
|
|
56
|
+
// `unpdf` is ESM-only in its import condition, so a dynamic import is
|
|
57
|
+
// tried first and the CJS require is the fallback — one of the two works
|
|
58
|
+
// on every runtime this package supports.
|
|
59
|
+
try {
|
|
60
|
+
const spec = 'unpdf';
|
|
61
|
+
return (await import(spec));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
try {
|
|
65
|
+
return lazyRequire('unpdf');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new MissingPdfSupportError(uri);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})());
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
name: 'pdf',
|
|
75
|
+
extensions: ['.pdf'],
|
|
76
|
+
async load(input) {
|
|
77
|
+
const unpdf = await getBackend(input.uri);
|
|
78
|
+
const doc = await unpdf.getDocumentProxy(input.bytes);
|
|
79
|
+
const extracted = await unpdf.extractText(doc, { mergePages: false });
|
|
80
|
+
const pages = Array.isArray(extracted.text)
|
|
81
|
+
? extracted.text.map((p) => String(p))
|
|
82
|
+
: [String(extracted.text)];
|
|
83
|
+
// Pages are joined with a blank line so `byParagraph` sees a boundary at
|
|
84
|
+
// every page break — and so the offsets a splitter computes index THIS
|
|
85
|
+
// string, which is the one that gets stored.
|
|
86
|
+
return { text: pages.join('\n\n'), pages };
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=pdf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../../../src/rag/loaders/pdf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAiCvD,6DAA6D;AAC7D,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,IAAI,GAAG,yBAAkC,CAAC;IACnD,uCAAuC;IAC9B,GAAG,CAAS;IAErB,YAAY,GAAW;QACrB,KAAK,CACH,YAAY,GAAG,6CAA6C;YAC1D,iCAAiC;YACjC,4EAA4E,CAC/E,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;CACF;AAED,MAAM,UAAU,SAAS,CAAC,UAA4B,EAAE;IACtD,IAAI,OAA0C,CAAC;IAE/C,MAAM,UAAU,GAAG,CAAC,GAAW,EAAyB,EAAE;QACxD,IAAI,OAAO,CAAC,OAAO;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7D,OAAO,CAAC,OAAO,KAAK,CAAC,KAAK,IAA2B,EAAE;YACrD,sEAAsE;YACtE,yEAAyE;YACzE,0CAA0C;YAC1C,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,OAAO,CAAC;gBACrB,OAAO,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAA4B,CAAC;YACzD,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC;oBACH,OAAO,WAAW,CAAe,OAAO,CAAC,CAAC;gBAC5C,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;gBACxC,CAAC;YACH,CAAC;QACH,CAAC,CAAC,EAAE,CAAC,CAAC;IACR,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,KAAK;QACX,UAAU,EAAE,CAAC,MAAM,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC1C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACtD,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC;gBACzC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBACtC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7B,yEAAyE;YACzE,uEAAuE;YACvE,6CAA6C;YAC7C,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* textLoader — plain text, decoded and left alone.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Adapter behind `DocumentLoader`.
|
|
5
|
+
* Role: rag/ layer. The simplest possible loader, and the one that defines
|
|
6
|
+
* what "leaving positions alone" means for the others: it decodes
|
|
7
|
+
* UTF-8, normalises line endings, and does nothing else. No trimming,
|
|
8
|
+
* no collapsing of blank lines, no smart quotes — every one of those
|
|
9
|
+
* would shift every offset after it, and an offset that does not
|
|
10
|
+
* index the original is worse than no offset at all.
|
|
11
|
+
* Emits: N/A.
|
|
12
|
+
*
|
|
13
|
+
* Line-ending normalisation is the one exception, and it is a deliberate one:
|
|
14
|
+
* `\r\n` → `\n` happens BEFORE any offset is computed, so offsets index the
|
|
15
|
+
* normalised text, which is also the text that is stored and shown. The
|
|
16
|
+
* alternative — keeping `\r` — makes every paragraph boundary two characters
|
|
17
|
+
* on Windows and one everywhere else, so the same document chunks differently
|
|
18
|
+
* depending on who checked it out.
|
|
19
|
+
*/
|
|
20
|
+
import type { DocumentLoader } from '../types.js';
|
|
21
|
+
/** Decode UTF-8 and normalise line endings. Shared by every text-shaped loader. */
|
|
22
|
+
export declare function decodeText(bytes: Uint8Array): string;
|
|
23
|
+
export declare function textLoader(): DocumentLoader;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Decode UTF-8 and normalise line endings. Shared by every text-shaped loader. */
|
|
2
|
+
export function decodeText(bytes) {
|
|
3
|
+
// `fatal: false` — a corpus is other people's files, and one bad byte in a
|
|
4
|
+
// 200-page document should degrade that character, not lose the document.
|
|
5
|
+
const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
|
|
6
|
+
// Strip a UTF-8 BOM: it is invisible, it is not content, and left in place it
|
|
7
|
+
// becomes the first character of the first chunk of the document.
|
|
8
|
+
const withoutBom = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
9
|
+
return withoutBom.replace(/\r\n/g, '\n');
|
|
10
|
+
}
|
|
11
|
+
export function textLoader() {
|
|
12
|
+
return {
|
|
13
|
+
name: 'text',
|
|
14
|
+
extensions: ['.txt', '.text', '.log', '.csv', '.tsv', '.json', '.yaml', '.yml'],
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
16
|
+
async load(input) {
|
|
17
|
+
return { text: decodeText(input.bytes) };
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../../../src/rag/loaders/text.ts"],"names":[],"mappings":"AAqBA,mFAAmF;AACnF,MAAM,UAAU,UAAU,CAAC,KAAiB;IAC1C,2EAA2E;IAC3E,0EAA0E;IAC1E,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACtE,8EAA8E;IAC9E,kEAAkE;IAClE,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxE,OAAO,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC;QAC/E,4DAA4D;QAC5D,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Chunk, LoadedDocument, Splitter } from './types.js';
|
|
2
|
+
export interface SplitDocumentsOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Verify each piece against its own offsets and throw on a mismatch.
|
|
5
|
+
* Default true. There is no good reason to turn it off except a splitter
|
|
6
|
+
* that deliberately rewrites text, in which case its citations cannot be
|
|
7
|
+
* checked against the source and you should know that.
|
|
8
|
+
*/
|
|
9
|
+
readonly verifyOffsets?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Cut documents into chunks.
|
|
13
|
+
*
|
|
14
|
+
* @throws when a splitter returns a piece whose offsets do not match its text.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const chunks = splitDocuments(documents, byHeading());
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function splitDocuments(documents: readonly LoadedDocument[], splitter: Splitter, options?: SplitDocumentsOptions): readonly Chunk[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* splitDocuments — documents become chunks that know where they came from.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: application of the `Splitter` strategy, plus identity and hashing.
|
|
5
|
+
* Role: rag/ layer. Step two of the pipeline.
|
|
6
|
+
* Emits: N/A — `indexCorpus` wraps this in a stage and records what it did.
|
|
7
|
+
*
|
|
8
|
+
* The splitter decides WHERE to cut; this decides what a cut is CALLED and how
|
|
9
|
+
* a later run recognises it:
|
|
10
|
+
*
|
|
11
|
+
* - **id** is `'<docUri>#<index>'` — stable across runs for an unchanged
|
|
12
|
+
* document, human-readable, and the string the model is asked to cite.
|
|
13
|
+
* - **contentHash** is sha-256 of the chunk text. Two runs over an unchanged
|
|
14
|
+
* document produce identical hashes, which is what makes "skip this one"
|
|
15
|
+
* a fact rather than a guess about mtimes.
|
|
16
|
+
*
|
|
17
|
+
* The invariant every splitter must hold — `doc.text.slice(charStart, charEnd)
|
|
18
|
+
* === chunk.text` — is CHECKED here rather than trusted, because a custom
|
|
19
|
+
* splitter is a supported thing to write and a silently wrong offset produces
|
|
20
|
+
* citations that point at the wrong words.
|
|
21
|
+
*/
|
|
22
|
+
import { sha256 } from './hash.js';
|
|
23
|
+
/**
|
|
24
|
+
* Cut documents into chunks.
|
|
25
|
+
*
|
|
26
|
+
* @throws when a splitter returns a piece whose offsets do not match its text.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const chunks = splitDocuments(documents, byHeading());
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function splitDocuments(documents, splitter, options = {}) {
|
|
34
|
+
const verify = options.verifyOffsets ?? true;
|
|
35
|
+
const chunks = [];
|
|
36
|
+
for (const doc of documents) {
|
|
37
|
+
const pieces = splitter.split(doc);
|
|
38
|
+
let index = 0;
|
|
39
|
+
for (const piece of pieces) {
|
|
40
|
+
if (verify && doc.text.slice(piece.charStart, piece.charEnd) !== piece.text) {
|
|
41
|
+
throw new Error(`splitter '${splitter.name}' produced a chunk of '${doc.uri}' whose offsets do not ` +
|
|
42
|
+
`match its text (chars ${piece.charStart}–${piece.charEnd}). A chunk that cannot be ` +
|
|
43
|
+
`located in its own document produces citations that point at the wrong words, so ` +
|
|
44
|
+
`this refuses rather than indexing it. Splitters must slice the document rather ` +
|
|
45
|
+
`than rebuild the text.`);
|
|
46
|
+
}
|
|
47
|
+
if (piece.text.length === 0)
|
|
48
|
+
continue;
|
|
49
|
+
chunks.push({
|
|
50
|
+
id: `${doc.uri}#${index}`,
|
|
51
|
+
docUri: doc.uri,
|
|
52
|
+
index,
|
|
53
|
+
text: piece.text,
|
|
54
|
+
charStart: piece.charStart,
|
|
55
|
+
charEnd: piece.charEnd,
|
|
56
|
+
...(piece.page !== undefined && { page: piece.page }),
|
|
57
|
+
...(piece.heading !== undefined && { heading: piece.heading }),
|
|
58
|
+
contentHash: sha256(piece.text),
|
|
59
|
+
});
|
|
60
|
+
index += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return chunks;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=splitDocuments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splitDocuments.js","sourceRoot":"","sources":["../../../src/rag/splitDocuments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAanC;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,SAAoC,EACpC,QAAkB,EAClB,UAAiC,EAAE;IAEnC,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,IAAI,IAAI,CAAC;IAC7C,MAAM,MAAM,GAAY,EAAE,CAAC;IAE3B,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC;gBAC5E,MAAM,IAAI,KAAK,CACb,aAAa,QAAQ,CAAC,IAAI,0BAA0B,GAAG,CAAC,GAAG,yBAAyB;oBAClF,yBAAyB,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,OAAO,4BAA4B;oBACrF,mFAAmF;oBACnF,iFAAiF;oBACjF,wBAAwB,CAC3B,CAAC;YACJ,CAAC;YACD,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACtC,MAAM,CAAC,IAAI,CAAC;gBACV,EAAE,EAAE,GAAG,GAAG,CAAC,GAAG,IAAI,KAAK,EAAE;gBACzB,MAAM,EAAE,GAAG,CAAC,GAAG;gBACf,KAAK;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;gBACrD,GAAG,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;gBAC9D,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;aAChC,CAAC,CAAC;YACH,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* byHeading — cut where the author already said a section starts.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Strategy (one of `Splitter`).
|
|
5
|
+
* Role: rag/ layer. The default for Markdown, and the only splitter in this
|
|
6
|
+
* library that is **not a heuristic**.
|
|
7
|
+
* Emits: N/A.
|
|
8
|
+
*
|
|
9
|
+
* Every other strategy infers boundaries from typography. This one reads them:
|
|
10
|
+
* a Markdown `#` line is the document telling you where a section begins, and
|
|
11
|
+
* ignoring that in favour of a character count is throwing away the best
|
|
12
|
+
* information in the file.
|
|
13
|
+
*
|
|
14
|
+
* Each chunk carries its heading, which flows through to the citation the
|
|
15
|
+
* model sees — `<source id="refunds.md#3" heading="Refund timing">` — so a
|
|
16
|
+
* reader can find the passage by name rather than by counting characters.
|
|
17
|
+
*
|
|
18
|
+
* A section longer than `maxChars` is packed by paragraph within the section
|
|
19
|
+
* and, failing that, hard-cut; every piece keeps the heading it came from.
|
|
20
|
+
* Text before the first heading (a preamble, a title block) is its own
|
|
21
|
+
* heading-less section rather than being attached to a heading it precedes.
|
|
22
|
+
*/
|
|
23
|
+
import type { Splitter } from '../types.js';
|
|
24
|
+
export interface ByHeadingOptions {
|
|
25
|
+
/** Target chunk size in characters. Default 1000. Sections longer than this are packed within. */
|
|
26
|
+
readonly maxChars?: number;
|
|
27
|
+
/** Backward overlap. Default 150. */
|
|
28
|
+
readonly overlapChars?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Deepest heading level that starts a new section (1 = `#` only, 6 = all).
|
|
31
|
+
* Default 6 — every heading is a boundary. Lower it when a document uses
|
|
32
|
+
* `####` for emphasis rather than structure.
|
|
33
|
+
*/
|
|
34
|
+
readonly maxLevel?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function byHeading(options?: ByHeadingOptions): Splitter;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { DEFAULT_MAX_CHARS, DEFAULT_OVERLAP_CHARS } from './constants.js';
|
|
2
|
+
import { applyOverlap, foldRunts, hardCut, packSpans, paragraphSpans, toPieces, } from './shared.js';
|
|
3
|
+
/** ATX headings only (`# Title`). Setext (`Title\n=====`) is rare in docs corpora. */
|
|
4
|
+
const HEADING = /^(#{1,6})[ \t]+(.+?)[ \t]*$/gm;
|
|
5
|
+
export function byHeading(options = {}) {
|
|
6
|
+
const maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
|
|
7
|
+
const overlapChars = options.overlapChars ?? DEFAULT_OVERLAP_CHARS;
|
|
8
|
+
const maxLevel = options.maxLevel ?? 6;
|
|
9
|
+
return {
|
|
10
|
+
name: 'byHeading',
|
|
11
|
+
split(doc) {
|
|
12
|
+
const text = doc.text;
|
|
13
|
+
// Where each section starts, and what it is called.
|
|
14
|
+
const starts = [];
|
|
15
|
+
HEADING.lastIndex = 0;
|
|
16
|
+
let match;
|
|
17
|
+
while ((match = HEADING.exec(text)) !== null) {
|
|
18
|
+
if ((match[1] ?? '').length > maxLevel)
|
|
19
|
+
continue;
|
|
20
|
+
starts.push({ offset: match.index, heading: match[2] });
|
|
21
|
+
}
|
|
22
|
+
// A document with no headings at all is a paragraph document; say so by
|
|
23
|
+
// doing the paragraph thing rather than returning one giant chunk.
|
|
24
|
+
if (starts.length === 0) {
|
|
25
|
+
const packed = packSpans(paragraphSpans(text), maxChars);
|
|
26
|
+
const cut = packed.flatMap((s) => s.end - s.start > maxChars ? hardCut(s, maxChars, overlapChars) : [s]);
|
|
27
|
+
return toPieces(doc, applyOverlap(foldRunts(cut), overlapChars));
|
|
28
|
+
}
|
|
29
|
+
const sections = [];
|
|
30
|
+
// Preamble before the first heading — its own section, unnamed.
|
|
31
|
+
const firstStart = starts[0]?.offset ?? 0;
|
|
32
|
+
if (firstStart > 0)
|
|
33
|
+
sections.push({ start: 0, end: firstStart });
|
|
34
|
+
for (let i = 0; i < starts.length; i++) {
|
|
35
|
+
const start = starts[i]?.offset ?? 0;
|
|
36
|
+
const end = starts[i + 1]?.offset ?? text.length;
|
|
37
|
+
const heading = starts[i]?.heading;
|
|
38
|
+
sections.push({ start, end, ...(heading !== undefined && { heading }) });
|
|
39
|
+
}
|
|
40
|
+
// Sections that fit stay whole; the rest are packed by paragraph WITHIN
|
|
41
|
+
// the section, so a boundary never merges two headings' content.
|
|
42
|
+
const spans = [];
|
|
43
|
+
for (const section of sections) {
|
|
44
|
+
if (section.end - section.start <= maxChars) {
|
|
45
|
+
spans.push(section);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const inner = paragraphSpans(text.slice(section.start, section.end)).map((s) => ({
|
|
49
|
+
start: section.start + s.start,
|
|
50
|
+
end: section.start + s.end,
|
|
51
|
+
...(section.heading !== undefined && { heading: section.heading }),
|
|
52
|
+
}));
|
|
53
|
+
for (const packedSpan of packSpans(inner, maxChars)) {
|
|
54
|
+
if (packedSpan.end - packedSpan.start > maxChars) {
|
|
55
|
+
spans.push(...hardCut(packedSpan, maxChars, overlapChars));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
spans.push(packedSpan);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return toPieces(doc, applyOverlap(foldRunts(spans), overlapChars));
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=byHeading.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byHeading.js","sourceRoot":"","sources":["../../../../src/rag/splitters/byHeading.ts"],"names":[],"mappings":"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"}
|