mirror of
https://github.com/zooai/brain.git
synced 2026-10-30 14:50:48 +00:00
Closed every remaining π‘ / π΅ row in PARITY.md. Each algorithm FortΓ©mi ships now has a real implementation in @hanzo/bot-memory v2.0.0 with matching ports in hanzo-memory (Python), bot-go (Go), and hanzoai/mcp (Rust). Cross-runtime tests: 121 TS + 53 Python + 58 Go + 38 Rust = 270 passing. Landed under packages/memory/ Retrieval & ranking fusion RRF (k=20 BEIR) + RSF (Weaviate v1.24) + adaptive k + adaptive FTS/semantic weights rerank MMR (Carbonell & Goldstein 1998), cosine dedup chunk-aware deduplication script Unicode script detection (Latin/CJK/emoji/Cyrillic/ Arabic/Hebrew/Greek/Devanagari) fts feature-flag matrix, CJK bigrams, emoji trigrams, websearch_to_tsquery parser, SQLite FTS5 MATCH renderer embed embedding registry, asymmetric E5 prefixes, Matryoshka truncation, coarse-dim picker two-stage coarse β fine retrieval via MRL federated cross-archive search with RRF fusion multi-memory X-Hanzo-Memory routing (X-Fortemi-Memory alias) filters composable tag/temporal/collection/scope predicates temporal UUIDv7 floor/ceiling + named ranges spatial Haversine + bbox + in-box (PostGIS fallback) Knowledge & graph graph normalize β SNN β PFNET-β β Louvain maintenance pipeline skos W3C SKOS concept tree (broader/narrower closures, Turtle export) fair Dublin Core XML + JSON-LD doc-types document type registry + auto-detect (filename/MIME/ content sniff) captions WebVTT / SRT / RTTM renderers sprite thumbnail sprite-sheet WebVTT cue builder code-ast language-aware syntactic chunker email RFC 2822 / MIME parser (multipart, qp, base64, HTML strip) exif EXIF parser (camera / time / GPS) Operational events EventBus + SSE renderer + webhook HMAC signer tokenizer BPE-style estimator + truncate-to-budget eval MRR / recall@k / precision@k / NDCG@k / benchmark hardware tier autodetect + per-tier model recommender circuit-breaker closed / open / half-open retry exponential backoff with full jitter range HTTP Range parser + Content-Range formatter tus TUS v1.0.0 (Create / Patch / metadata / checksum) Crypto address wallet-style content-addressable ids (hanzo: / mm:) mmpke01 multi-recipient envelope parser/builder + magic-byte sniff Inference (packages/memory/inference/) index InferenceBackend trait + registry slug provider-qualified routing (provider:model:tag) capabilities capability flags + task-based selector runtime-config hot-swap config (db_override β env β default) mock deterministic backend (for tests) few-shot ICL prompt builder (3-5 examples capped) refinement Self-Refine / ReAct / Reflexion patterns link-types rule + LLM link classifier (11 canonical types) vision VisionBackend trait + Ollama default transcription Whisper-compatible HTTP client diarization pyannote-compatible HTTP client + alignSpeakers / relabel gliner GLiNER zero-shot NER HTTP client Extraction (packages/memory/extract/) index registry + dispatcher text native UTF-8 passthrough email RFC 2822 adapter spreadsheet CSV/TSV pure-JS; xlsx via registerXlsxDecoder archive ZIP central-directory pure-JS reader exif-adapter EXIF for images code tree-sitter-style syntactic chunker pdf PdfSidecar HTTP adapter factory image VisionBackend-backed adapter factory audio TranscriptionBackend-backed adapter factory video keyframe + scene + transcript adapter factory 3d multi-view 3D model adapter factory media-optimize ffmpeg variants (faststart / web-compat / 720p) adapter factory structured JSON-schema structured-extract adapter factory summarizer fast/standard cascade adapter factory sprite-adapter thumbnail sprite-sheet adapter factory Cross-runtime ports hanzo-memory (Python) 53 tests, ports of fusion / rerank / dedup / script / fts / embed / temporal / captions / tokenizer / eval / spatial / range / address / graph / doc-types / circuit / retry / slug / runtime-config / link-types bot-go (Go) 58 tests, same algorithm set ported with standard-library-only primitives hanzoai/mcp::brain (Rust) 38 tests, same algorithm set; sha2 added as dependency (blake3 stand-in) PARITY.md is now 100% green (β in-monorepo + π’ sibling-repo). No π΅ placeholders remain.
165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
/**
|
|
* Pure-JS RFC 2822 / 5322 email message parser.
|
|
*
|
|
* Extracts headers, the primary text/plain body, and attachment metadata.
|
|
* For full multipart binary extraction, hand the message to
|
|
* `hanzo-tools-fs` β this module is the brain-side parser that powers
|
|
* search and link extraction.
|
|
*/
|
|
|
|
export interface EmailHeader {
|
|
name: string;
|
|
value: string;
|
|
}
|
|
|
|
export interface EmailAttachment {
|
|
filename?: string;
|
|
mimeType: string;
|
|
size: number;
|
|
/** Raw body slice β caller can persist. */
|
|
body: Uint8Array;
|
|
}
|
|
|
|
export interface ParsedEmail {
|
|
headers: EmailHeader[];
|
|
from?: string;
|
|
to: string[];
|
|
cc: string[];
|
|
bcc: string[];
|
|
subject?: string;
|
|
date?: string;
|
|
messageId?: string;
|
|
/** Decoded text/plain body (first part if multipart). */
|
|
textBody: string;
|
|
/** HTML body if present. */
|
|
htmlBody?: string;
|
|
attachments: EmailAttachment[];
|
|
}
|
|
|
|
export function parseEmail(raw: string | Uint8Array): ParsedEmail {
|
|
const text = typeof raw === "string" ? raw : new TextDecoder("utf-8").decode(raw);
|
|
const split = splitHeadersAndBody(text);
|
|
const headers = parseHeaders(split.headers);
|
|
|
|
const get = (name: string): string | undefined => {
|
|
const h = headers.find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
return h?.value;
|
|
};
|
|
|
|
const out: ParsedEmail = {
|
|
headers,
|
|
from: get("From"),
|
|
to: parseAddresses(get("To")),
|
|
cc: parseAddresses(get("Cc")),
|
|
bcc: parseAddresses(get("Bcc")),
|
|
subject: get("Subject"),
|
|
date: get("Date"),
|
|
messageId: get("Message-ID"),
|
|
textBody: "",
|
|
attachments: [],
|
|
};
|
|
|
|
const contentType = get("Content-Type") ?? "text/plain";
|
|
if (contentType.toLowerCase().startsWith("multipart/")) {
|
|
const boundary = contentType.match(/boundary="?([^"]+)"?/i)?.[1];
|
|
if (boundary) parseMultipart(split.body, boundary, out);
|
|
else out.textBody = split.body;
|
|
} else if (contentType.toLowerCase().startsWith("text/html")) {
|
|
out.htmlBody = split.body;
|
|
out.textBody = stripHtml(split.body);
|
|
} else {
|
|
out.textBody = split.body;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function splitHeadersAndBody(text: string): { headers: string; body: string } {
|
|
const idx = text.indexOf("\r\n\r\n");
|
|
if (idx !== -1) return { headers: text.slice(0, idx), body: text.slice(idx + 4) };
|
|
const idx2 = text.indexOf("\n\n");
|
|
if (idx2 !== -1) return { headers: text.slice(0, idx2), body: text.slice(idx2 + 2) };
|
|
return { headers: text, body: "" };
|
|
}
|
|
|
|
function parseHeaders(text: string): EmailHeader[] {
|
|
const out: EmailHeader[] = [];
|
|
const lines = text.split(/\r?\n/);
|
|
let cur: EmailHeader | null = null;
|
|
for (const line of lines) {
|
|
if (/^[\s\t]/.test(line) && cur) {
|
|
cur.value += " " + line.trim();
|
|
continue;
|
|
}
|
|
const m = line.match(/^([!-9;-~]+):\s*(.*)$/);
|
|
if (m) {
|
|
if (cur) out.push(cur);
|
|
cur = { name: m[1], value: m[2] };
|
|
}
|
|
}
|
|
if (cur) out.push(cur);
|
|
return out;
|
|
}
|
|
|
|
function parseAddresses(value: string | undefined): string[] {
|
|
if (!value) return [];
|
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
}
|
|
|
|
function parseMultipart(body: string, boundary: string, out: ParsedEmail): void {
|
|
const sep = `--${boundary}`;
|
|
const end = `--${boundary}--`;
|
|
const parts: string[] = [];
|
|
let i = body.indexOf(sep);
|
|
while (i !== -1) {
|
|
const next = body.indexOf(sep, i + sep.length);
|
|
const finish = body.indexOf(end, i + sep.length);
|
|
const stop = finish !== -1 && (next === -1 || finish < next) ? finish : next;
|
|
if (stop === -1) break;
|
|
parts.push(body.slice(i + sep.length, stop).replace(/^\r?\n/, ""));
|
|
if (stop === finish) break;
|
|
i = next;
|
|
}
|
|
for (const p of parts) {
|
|
const split = splitHeadersAndBody(p);
|
|
const partHeaders = parseHeaders(split.headers);
|
|
const ct = (partHeaders.find((h) => h.name.toLowerCase() === "content-type")?.value ?? "text/plain").toLowerCase();
|
|
const cd = partHeaders.find((h) => h.name.toLowerCase() === "content-disposition")?.value ?? "";
|
|
if (cd.toLowerCase().startsWith("attachment")) {
|
|
const filename = cd.match(/filename="?([^";]+)"?/i)?.[1];
|
|
const enc = partHeaders.find((h) => h.name.toLowerCase() === "content-transfer-encoding")?.value?.toLowerCase();
|
|
const decoded = decodeBody(split.body, enc);
|
|
out.attachments.push({
|
|
filename,
|
|
mimeType: ct.split(";")[0],
|
|
size: decoded.length,
|
|
body: decoded,
|
|
});
|
|
} else if (ct.startsWith("text/html") && !out.htmlBody) {
|
|
out.htmlBody = split.body;
|
|
if (!out.textBody) out.textBody = stripHtml(split.body);
|
|
} else if (ct.startsWith("text/plain") && !out.textBody) {
|
|
out.textBody = split.body;
|
|
}
|
|
}
|
|
}
|
|
|
|
function decodeBody(body: string, enc?: string): Uint8Array {
|
|
if (enc === "base64") {
|
|
return Uint8Array.from(Buffer.from(body.replace(/\s+/g, ""), "base64"));
|
|
}
|
|
if (enc === "quoted-printable") {
|
|
return new TextEncoder().encode(decodeQuotedPrintable(body));
|
|
}
|
|
return new TextEncoder().encode(body);
|
|
}
|
|
|
|
function decodeQuotedPrintable(s: string): string {
|
|
return s
|
|
.replace(/=\r?\n/g, "")
|
|
.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
}
|
|
|
|
function stripHtml(html: string): string {
|
|
return html.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
}
|