agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
90 lines • 4.25 kB
JavaScript
/**
* pdfLoader — PDF text extraction, page by page.
*
* Pattern: Adapter behind `DocumentLoader`, over a lazily-loaded optional peer.
* Role: rag/ layer. The ONE loader that needs a dependency, and the reason
* it is optional rather than bundled.
* Emits: N/A.
*
* ── Why `unpdf`, measured rather than assumed ───────────────────────────────
* PDF text extraction cannot be done without a real library. The candidates
* were installed and measured:
*
* | package | installed size | packages | deps | verdict |
* |---|---|---|---|---|
* | **`unpdf`** | **2.5 MB** | **1** | **none** | chosen |
* | `pdf-parse@2` | 86 MB | 3 | `@napi-rs/canvas` (**native**) | a native binary, to read text |
* | `pdf-parse@1` | 34 MB | 4 | debug, node-ensure | unmaintained since 2018 |
* | `pdfjs-dist@6` | 62 MB | 2 | none | 25× the size for the same engine |
*
* `unpdf` is a serverless build of the same pdf.js engine the heavyweight
* options wrap, with dual CJS/ESM exports and zero transitive dependencies.
* A real 2-page PDF extracts in ~95 ms.
*
* ── Pages are the point ─────────────────────────────────────────────────────
* It returns text PER PAGE, and that is why the page number in a citation is a
* fact rather than a guess. `<source id="handbook.pdf#7" page="2">` can be
* checked by opening page 2. A loader that flattened the document first could
* only ever say "somewhere in this PDF".
*
* ── A known warning on Node 22 ──────────────────────────────────────────────
* pdf.js v5 calls `Math.sumPrecise`, which arrives in V8 later than Node 22.
* The extraction is correct; the console shows `Warning: TypeError:
* Math.sumPrecise is not a function` per page. It is upstream and harmless,
* and it is documented here so it does not read as data loss.
*/
import { lazyRequire } from '../../lib/lazyRequire.js';
/** Raised when a PDF is met and `unpdf` is not installed. */
export class MissingPdfSupportError extends Error {
code = 'ERR_MISSING_PDF_SUPPORT';
/** The file that could not be read. */
uri;
constructor(uri) {
super(`Reading '${uri}' requires the \`unpdf\` peer dependency.\n` +
' Install: npm install unpdf\n' +
' Or pass `backend` to pdfLoader() if your bundler resolves it statically.');
this.name = 'MissingPdfSupportError';
this.uri = uri;
}
}
export function pdfLoader(options = {}) {
let backend;
const getBackend = (uri) => {
if (options.backend)
return Promise.resolve(options.backend);
return (backend ??= (async () => {
// `unpdf` is ESM-only in its import condition, so a dynamic import is
// tried first and the CJS require is the fallback — one of the two works
// on every runtime this package supports.
try {
const spec = 'unpdf';
return (await import(spec));
}
catch {
try {
return lazyRequire('unpdf');
}
catch {
throw new MissingPdfSupportError(uri);
}
}
})());
};
return {
name: 'pdf',
extensions: ['.pdf'],
async load(input) {
const unpdf = await getBackend(input.uri);
const doc = await unpdf.getDocumentProxy(input.bytes);
const extracted = await unpdf.extractText(doc, { mergePages: false });
const pages = Array.isArray(extracted.text)
? extracted.text.map((p) => String(p))
: [String(extracted.text)];
// Pages are joined with a blank line so `byParagraph` sees a boundary at
// every page break — and so the offsets a splitter computes index THIS
// string, which is the one that gets stored.
return { text: pages.join('\n\n'), pages };
},
};
}
//# sourceMappingURL=pdf.js.map