Spaces:
Sleeping
Sleeping
File size: 7,040 Bytes
641b62c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | // ─── 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;
}
|