Spaces:
Sleeping
Sleeping
| // ─── normalizeLlmOutput.ts ──────────────────────────────────────────────────── | |
| // Fase 5 — primo stadio della pipeline: normalize → validate → sanitize → render | |
| // | |
| // Regola: ogni output LLM passa da qui prima di qualsiasi altro trattamento. | |
| // Browser-safe, zero deps, iPhone compatible. | |
| /** Risultato della normalizzazione */ | |
| export interface NormalizeResult { | |
| text: string; | |
| changed: boolean; | |
| repairs: string[]; | |
| } | |
| /** | |
| * Normalizza l'output grezzo del modello. | |
| * Ordine operazioni (non modificare — l'ordine è semanticamente rilevante): | |
| * 0. BOM UTF-8 — DEVE essere primo: trim() nei passi successivi lo rimuoverebbe | |
| * silenziosamente senza loggarlo come repair (S226 fix) | |
| * 0a. <think> blocks chiusi — safety net S188+S189 | |
| * 0b. <think> blocks aperti/incompleti | |
| * 1. Sequenze escape ANSI — DEVE essere prima del passo 2: \x1b (0x1B) è incluso | |
| * nel range di controllo e verrebbe rimosso prima (S226 fix) | |
| * 2. Null-byte e caratteri di controllo non stampabili (dopo ANSI, non tocca più \x1b) | |
| * 3. Newline multipli eccessivi (>2 consecutivi → 2) | |
| * 4. Spazi bianchi trailing per riga | |
| * 5. Trim globale | |
| */ | |
| export function normalizeLlmOutput(raw: string): NormalizeResult { | |
| if (typeof raw !== "string") { | |
| return { text: "", changed: true, repairs: ["input non stringa → stringa vuota"] }; | |
| } | |
| const repairs: string[] = []; | |
| let t = raw; | |
| // 0. BOM UTF-8 — DEVE essere prima di qualsiasi trim() (S226) | |
| // trim() in step 0a/0b rimuoverebbe \uFEFF silenziosamente senza repair log. | |
| if (t.startsWith("\uFEFF")) { t = t.slice(1); repairs.push("rimosso BOM UTF-8"); } | |
| // 0a. <think>...</think> blocks chiusi — S188 safety net | |
| const _c0a = t.replace(/<think>[\s\S]*?<\/think>/gi, "").trim(); | |
| if (_c0a !== t) { repairs.push("rimossi blocchi <think> chiusi"); t = _c0a; } | |
| // 0b. <think> aperto non chiuso (finish_reason=length) — S189 | |
| const _c0b = t.replace(/<think>[\s\S]*/gi, "").trim(); | |
| if (_c0b !== t) { repairs.push("rimosso blocco <think> incompleto"); t = _c0b; } | |
| // 1. Sequenze ANSI escape (\e[...m oppure \x1b[...m) — S226: salito prima del passo 2 | |
| // \x1b (0x1B = 27) è nel range [\x0E-\x1F] del passo 2 — deve essere strippato prima. | |
| const cleaned1 = t.replace(/\x1b\[[0-9;]*[mGKHF]/g, ""); | |
| if (cleaned1 !== t) { repairs.push("rimossi escape ANSI"); t = cleaned1; } | |
| // 2. Null bytes e control chars (eccetto \t \n \r) — dopo ANSI: \x1b già rimosso | |
| const cleaned2 = t.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); | |
| if (cleaned2 !== t) { repairs.push("rimossi caratteri di controllo"); t = cleaned2; } | |
| // 2.5 Lone UTF-16 surrogate codepoints — Groq occasionally emits them in emoji/special chars. | |
| // They crash JSON.stringify / IndexedDB storage and corrupt Dexie state if unhandled. | |
| // Must run BEFORE any slice/chunk operations (slice mid-surrogate creates new lone surrogates). | |
| const cleaned2_5 = t.replace(/[\uD800-\uDFFF]/g, ""); | |
| if (cleaned2_5 !== t) { repairs.push("rimossi surrogate UTF-16 isolati"); t = cleaned2_5; } | |
| // 3. Newline eccessivi (più di 2 consecutivi → 2) | |
| const cleaned3 = t.replace(/\n{3,}/g, "\n\n"); | |
| if (cleaned3 !== t) { repairs.push("collassati newline eccessivi"); t = cleaned3; } | |
| // 4. Spazi trailing per riga | |
| const cleaned4 = t.replace(/[ \t]+$/gm, ""); | |
| if (cleaned4 !== t) { repairs.push("rimossi spazi trailing"); t = cleaned4; } | |
| // 5. Trim globale | |
| const cleaned5 = t.trim(); | |
| if (cleaned5 !== t) { t = cleaned5; } | |
| // 6. AbortController normalization in code blocks (S241-C2) | |
| // Rinomina `ctrl` → `controller` nei blocchi di codice TS/JS generati dall'AI. | |
| // Opera SOLO dentro ``` code block — non nel testo libero. | |
| const cleaned6 = t.replace( | |
| /([ `]{3}(?:typescript|javascript|ts|js)[^\n]*\n)([\s\S]*?)([ `]{3})/g, | |
| (_match: string, open: string, code: string, close: string): string => { | |
| const fixed = code | |
| .replace(/\bconst\s+ctrl\s*=\s*new\s+AbortController/g, "const controller = new AbortController") | |
| .replace(/\blet\s+ctrl\s*=\s*new\s+AbortController/g, "let controller = new AbortController") | |
| .replace(/\bvar\s+ctrl\s*=\s*new\s+AbortController/g, "var controller = new AbortController") | |
| .replace(/\bctrl\.abort\b/g, "controller.abort") | |
| .replace(/\bctrl\.signal\b/g, "controller.signal"); | |
| return open + fixed + close; | |
| } | |
| ); | |
| if (cleaned6 !== t) { repairs.push("normalizzato AbortController (ctrl→controller)"); t = cleaned6; } | |
| // 7. S371: Strip internal monologue patterns leaking from smolagents/backend | |
| // Rimosso: GOAL:/DONE_WHEN:/OUT_OF_SCOPE: blocks, Proceed.We need to output..., | |
| // Thought:/Code:/Observation: line prefixes, raw JSON tool arrays. | |
| // Safari-safe regex: no lookbehind, no named groups. | |
| const _s7a = t.replace(/(?:^|\n)(?:GOAL|DONE_WHEN|OUT_OF_SCOPE):[\s\S]*?(?=\n\n|$)/gm, ""); | |
| if (_s7a !== t) { repairs.push("rimossi blocchi GOAL/DONE_WHEN/OUT_OF_SCOPE"); t = _s7a; } | |
| const _s7b = t.replace(/Proceed\.?\s*We\s+need\s+to\s+output\s+the\s+tool\s+call\s+now\.?\s*\[[\s\S]*?\]/g, ""); | |
| if (_s7b !== t) { repairs.push("rimosso Proceed.We need to output..."); t = _s7b; } | |
| // S375: gm flag — rimuove TUTTE le occorrenze di JSON tool array (non solo la prima) | |
| // Aggiunto supporto sia per "action" con virgolette singole che doppie | |
| const _s7c = t.replace(/^\s*\[\s*\{["']action["']:/gm, ""); | |
| if (_s7c !== t) { repairs.push("rimossi JSON tool array raw (tutte le occorrenze)"); t = _s7c; } | |
| const _s7d = t.replace(/^(?:Thought|Code|Observation|Action Input):\s*/gm, ""); | |
| if (_s7d !== t) { repairs.push("rimossi prefissi smolagents Thought/Code/Observation"); t = _s7d; } | |
| const _s7e = t.replace(/\[STEP\s+\d+\/\d+\]\s*/g, ""); | |
| if (_s7e !== t) { repairs.push("rimossi [STEP N/M] markers"); t = _s7e; } | |
| // S375-F: strip Action: {"type":...} JSON leaks (smolagents structured output) | |
| // Formato alternativo usato da alcuni adapter smolagents → non mostrare all'utente | |
| const _s7f = t.replace(/^\s*Action:\s*\{["']type["'][\s\S]*?\}\s*$/gm, ""); | |
| if (_s7f !== t) { repairs.push("rimosso Action:{type:...} JSON strutturato"); t = _s7f; } | |
| // S375-G: collassa newline eccessivi creati dalle strip precedenti (step 7a-7f) | |
| const _s7g = t.replace(/\n{3,}/g, "\n\n").trim(); | |
| if (_s7g !== t) { repairs.push("collassati newline post-strip step-7"); t = _s7g; } | |
| return { text: t, changed: repairs.length > 0, repairs }; | |
| } | |
| /** Versione semplificata: ritorna solo la stringa normalizzata */ | |
| export function normalize(raw: string): string { | |
| return normalizeLlmOutput(raw).text; | |
| } | |
| /** | |
| * Alias per compatibilità con i caller esistenti che importano normalizeLlmText. | |
| * @deprecated Usa normalizeLlmOutput() o normalize() invece. | |
| */ | |
| export function normalizeLlmText(raw: string): string { | |
| return normalizeLlmOutput(raw).text; | |
| } | |