@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
92 lines • 3.74 kB
JavaScript
/** Thrown when no extractor is registered for a content type — ingestion treats it as "skip, don't index". */
export class UnsupportedContentTypeError extends Error {
contentType;
constructor(contentType) {
super(`No text extractor registered for content type "${contentType}"`);
this.contentType = contentType;
this.name = 'UnsupportedContentTypeError';
}
}
/**
* A {@link TextExtractor} that dispatches by content type. Register exact types (`application/json`) or
* a whole family (`text/*`); an exact match always wins over a family. Unregistered types throw
* {@link UnsupportedContentTypeError}. This is the extension point for binary formats:
* `.register('application/pdf', pdfFn)` — the PDF/DOCX library stays entirely on the host side.
*/
export class MimeTextExtractor {
exact = new Map();
families = [];
register(contentType, fn) {
if (contentType.endsWith('/*')) {
// "text/*" → match anything starting "text/"
this.families.push({ prefix: contentType.slice(0, -1), fn });
}
else {
this.exact.set(contentType.toLowerCase(), fn);
}
return this;
}
async extract(bytes, contentType) {
const normalized = normalizeContentType(contentType);
const fn = this.resolve(normalized);
if (fn === undefined) {
throw new UnsupportedContentTypeError(contentType);
}
return fn(bytes, normalized);
}
/** Whether a content type would resolve to an extractor (without reading bytes). */
supports(contentType) {
return this.resolve(normalizeContentType(contentType)) !== undefined;
}
resolve(contentType) {
const exact = this.exact.get(contentType);
if (exact !== undefined) {
return exact;
}
for (const family of this.families) {
if (contentType.startsWith(family.prefix)) {
return family.fn;
}
}
return undefined;
}
}
/** Strip any `; charset=…` parameter off a content type and lower-case it. */
export function normalizeContentType(contentType) {
return (contentType.split(';')[0] ?? contentType).trim().toLowerCase();
}
/** Decode bytes as UTF-8 — the extractor for `text/*` and JSON. */
export function decodeUtf8(bytes) {
return bytes.toString('utf8');
}
/** Decode UTF-8 then strip HTML: drop `<script>`/`<style>` bodies and tags, decode common entities. */
export function extractHtmlText(bytes) {
return bytes
.toString('utf8')
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/ /gi, ' ')
.replace(/&/gi, '&')
.replace(/</gi, '<')
.replace(/>/gi, '>')
.replace(/"/gi, '"')
.replace(/�*39;|'/gi, "'")
.replace(/\s+/g, ' ')
.trim();
}
/**
* The default extractor: `text/plain`, `text/csv`, `text/markdown` (and the whole `text/*` family) plus
* `application/json` decode as UTF-8, `text/html` is tag-stripped, and everything else — PDF, DOCX,
* images — throws {@link UnsupportedContentTypeError} (so binary formats are skipped rather than indexed
* as garbage). Extend it for those: `.register('application/pdf', myPdfExtractor)`.
*/
export function defaultTextExtractor() {
return new MimeTextExtractor()
.register('text/html', extractHtmlText)
.register('text/plain', decodeUtf8)
.register('text/csv', decodeUtf8)
.register('text/markdown', decodeUtf8)
.register('text/*', decodeUtf8)
.register('application/json', decodeUtf8);
}
//# sourceMappingURL=text-extractor.js.map