Terminal / agents /unified_loop_llm.py
Baida-A
Initial clean deploy (Reverse Proxy removed)
28a08e7
Raw
History Blame
32.2 kB
"""unified_loop_llm.py — LLMSelectionMixin: selezione LLM, routing e output sanitization.
Estratto da unified_loop.py (P20-TD1 Fase 2).
Contiene:
Block A — LLM selection + routing class attrs:
_SKIP_SMOL_RE, _COMPLEX_APP_RE, _MULTI_FEATURE_RE, _CODE_TASK_RE, _NEEDS_PLAN_RE
_get_llm_for_goal(): CODER vs default LLM selection (S362/S416)
_get_fast_llm(): lazy fast LLM cache (S-FAST)
_get_verifier_llm(): P25-B4 cross-model critic selection
_is_pure_explanation(): conceptual query detection (B5)
_max_tokens_for_goal(): token budget estimation (S373/B13)
Block B — Output sanitization + explanation regex attrs:
_sanitize_agent_output(): staticmethod rimozione monologue interno (S371)
_PURE_EXPLANATION_RE, _EXPL_ACTION_RE, _EXPL_FILE_REF_RE: regex per _is_pure_explanation
Invariante B1: nessun corpo duplicato con unified_loop.py.
MRO Python garantisce self._SKIP_SMOL_RE / self._sanitize_agent_output()
funzionino da qualsiasi metodo di UnifiedAgentLoop.
Importato da: unified_loop.py (solo per ereditarietà LLMSelectionMixin)
"""
from __future__ import annotations
import re
from typing import Any
import logging
_logger = logging.getLogger("agents.unified_loop_llm")
class LLMSelectionMixin:
# ── Block A: LLM selection + routing ─────────────────────────────────────
def _get_llm_for_goal(self, goal: str) -> Any:
"""S362: return CODER-role LLM for code-heavy goals, default otherwise.
GAP-ROUT: route SQL/Reasoning/MMLU to REASONER role (Cerebras 120B).
S416-Fix3: anche app complesse (tok_budget >= 6144) usano CODER (70B)
anche se _CODE_RE non matcha — garantisce qualità su app multi-file."""
g = goal[:500]
_is_code = bool(self._CODE_GOAL_RE.search(g))
_is_reasoning = bool(self._REASONING_GOAL_RE.search(g)) or \
bool(self._SQL_GOAL_RE.search(g)) or \
bool(self._MMLU_GOAL_RE.search(g))
_tok = self._max_tokens_for_goal(goal)
_needs_heavy = _is_code or _is_reasoning or _tok >= 6144
if not _needs_heavy:
return self.llm
if _is_reasoning:
try:
from models.role_router import RoleRouter, Role
return RoleRouter.get_client(Role.REASONER)
except Exception:
pass
if self._coder_llm is None:
try:
from models.role_router import RoleRouter, Role
self._coder_llm = RoleRouter.get_client(Role.CODER)
except Exception:
self._coder_llm = self.llm
return self._coder_llm
def _get_fast_llm(self) -> Any:
"""S-FAST: return Role.FAST client (Groq llama-3.1-8b-instant) per query semplici.
Caricato lazy e cachato in self._fast_llm — zero overhead dopo il primo accesso.
Fallback silenzioso su self.llm se GROQ_API_KEY mancante o RoleRouter non disponibile."""
if self._fast_llm is None:
try:
from models.role_router import RoleRouter, Role
self._fast_llm = RoleRouter.get_client(Role.FAST)
except Exception:
self._fast_llm = self.llm
return self._fast_llm
def _get_verifier_llm(self) -> Any:
"""P25-B4: Cross-model critic — restituisce un provider DIVERSO da self.llm per la verifica.
Elimina il bias di conferma: lo stesso modello che ha generato la risposta
non dovrebbe giudicare se è corretta.
Strategia di selezione:
- Generatore Groq → Verifier Gemini RESEARCHER (ragionamento diverso)
- Generatore Gemini → Verifier Groq CODER (modello diverso)
- Generatore altri → Verifier Gemini RESEARCHER → fallback Groq CODER
- Qualsiasi errore → fallback self.llm (comportamento invariato, zero regressioni)
Cache lazy (self._verifier_llm) — caricato una volta per sessione.
"""
if self._verifier_llm is not None:
return self._verifier_llm
try:
from models.role_router import RoleRouter as _RRv, Role as _Rolev
_gen_prov = getattr(self.llm, 'provider_name', '') or ''
if 'groq' in _gen_prov:
# Generatore Groq → Verifier Gemini (diverso reasoning)
self._verifier_llm = _RRv.get_client(_Rolev.RESEARCHER)
elif 'gemini' in _gen_prov:
# Generatore Gemini → Verifier Groq CODER (70B, diverso modello)
self._verifier_llm = _RRv.get_client(_Rolev.CODER)
elif 'cerebras' in _gen_prov:
# Generatore Cerebras → Verifier Gemini
self._verifier_llm = _RRv.get_client(_Rolev.RESEARCHER)
elif 'nvidia' in _gen_prov:
# Generatore NVIDIA → Verifier Groq CODER (per diversificare)
self._verifier_llm = _RRv.get_client(_Rolev.CODER)
else:
# OpenRouter / SambaNova / altri → Verifier Groq CODER come default cross-model
self._verifier_llm = _RRv.get_client(_Rolev.CODER)
_logger.debug(
"P25-B4 verifier_llm: gen_prov=%s → verifier=%s",
_gen_prov, getattr(self._verifier_llm, 'provider_name', '?'),
)
except Exception as _exc:
_logger.debug("P25-B4 _get_verifier_llm fallback: %s", _exc)
self._verifier_llm = self.llm # fallback: invariato
return self._verifier_llm
def _is_pure_explanation(self, goal: str) -> bool:
"""B5: True se goal è domanda concettuale pura — nessun tool necessario.
4 guard fail-open: len<300 | pattern interrogativo | no action verb | no file ref."""
if len(goal) > 300: return False
if not self._PURE_EXPLANATION_RE.search(goal[:200]): return False
if self._EXPL_ACTION_RE.search(goal[:200]): return False
if self._EXPL_FILE_REF_RE.search(goal[:200]): return False
return True
# S371: _SKIP_SMOL_RE — skippa smolagents per query semplici (notizie, cerca) → direct tools
# S427: ampliato — più query bypassano smolagents → direct tools (più veloce).
_SKIP_SMOL_RE = re.compile(
r'\b(notizie|news|ultime notizie|cerca|ricerca|meteo|tempo|previsioni|'
r'cerca online|cerca su|trova online|guarda su|vai su|'
# S427: meteo/clima ampliato
r'weather|forecast|temperatura|clima|piove|nevica|temporale|umidità|vento|'
# S427: valute/crypto
r'valuta|cambio|tasso|euro|dollaro|bitcoin|ethereum|crypto|yen|sterlina|'
r'exchange rate|currency|'
# S427: knowledge lookup
r'wikipedia|enciclopedia|chi [eè]|chi era|storia di|'
# S427: calcoli diretti
r'calcola|quanto fa|quant[oei]\s+fa|risultato di|computa|'
# S427: data/ora
r'che ora|che giorno|data di oggi|ora attuale|orario|timezone|'
# S427: traduzione
r'traduci|traduzione|translate|translation|'
# EN-DIRECT: English patterns — bypass planner (save 5-15s latency) for simple EN queries
r'calculate|compute|how much is \d|what time is it|current time|today.s date|'
r'what.s the (?:time|date|day|weather)|who (?:is|was|are|were) |what is the (?:weather|capital|population)|'
r'stock price|crypto price|price of (?:bitcoin|ethereum|gold)|'
r'weather in|forecast for|temperature in|'
r'convert \d|how many \w+ in|exchange rate (?:of|for|from)|'
r'latest news (?:about|on)|search (?:for |on )?wikipedia|look up |'
# B7: unit conversion, timezone, date calc, IP — direct tools, skip planner
r'converti\s+\d+\s+\w+\s+(?:in|to)\s+\w+|'
r'quanti\s+giorni\s+(?:tra|fino|mancano)|'
r'che\s+ora\s+[e\xe8]\s+a\s+\w+|what\s+time\s+is\s+it\s+in\s+\w+|'
r'(?:mio\s+ip|my\s+ip|ip\s+address)\s*\??)\b',
re.IGNORECASE,
)
# ── S373: pattern per stima budget token output ────────────────────────
# S427: aggiunti più trigger per app/sistemi complessi + more token budget
_COMPLEX_APP_RE = re.compile(
r'\b(crea|scrivi|implementa|build|create|write|implement|'
r'sviluppa|costruisci|progetta|genera|scaffold|deploy|develop)\b.{0,80}'
r'\b(app|applicazione|application|progetto|project|website|sito|'
r'dashboard|api|backend|frontend|server|service|platform|piattaforma|'
r'sistema|e.?commerce|chatbot|bot|game|gioco|portfolio|blog|crm|cms|'
r'saas|marketplace|admin|panel|landing|cli|tool|library|sdk)\b',
re.IGNORECASE | re.DOTALL,
)
# S427: indicatori di complessità multi-feature ampliati
_MULTI_FEATURE_RE = re.compile(
r'\b(completo|completa|full.?stack|multi.?file|con\s+test|con\s+typescript|'
r'con\s+animazioni?|con\s+filtri?|con\s+localStorag|tipizzato|'
r'multiple\s+components?|più\s+component|separati|con\s+routing|'
r'con\s+auth(?:entication)?|con\s+login|con\s+database|con\s+deploy|'
r'con\s+pagament[io]|con\s+api|con\s+websocket|con\s+i18n|'
r'con\s+dark\s+mode|con\s+responsive|multi.?pagina|multi.?step|'
r'end.?to.?end|production.?ready|scalabile|enterprise|'
r'con\s+docker|con\s+ci.?cd|con\s+testing|con\s+validation)\b',
re.IGNORECASE,
)
# S427: tipi di artefatti codice ampliati per token budget
_CODE_TASK_RE = re.compile(
r'\b(codice|funzione|function|classe|class|componente|component|'
r'modulo|module|script|algoritmo|algorithm|hook|utility|helper|'
r'type|interface|enum|decorator|middleware|service|repository|'
r'controller|handler|resolver|store|reducer|action|mutation|'
r'schema|dto|validator|mapper|factory|builder|'
r'context|provider|consumer|wrapper|composable|mixin)\b',
re.IGNORECASE,
)
# B7: planner richiesto solo per task di progettazione/implementazione complessa
# S427: aggiunti verbi che richiedono pianificazione (patch/debug/ottimizza/ecc.)
_NEEDS_PLAN_RE = re.compile(
# F18: esteso con pattern IT mancanti — installa/nuovo/clona/avvia/esegui/testa
r'\b(crea|implementa|scrivi|refactor|progetta|costruisci|'
r'sistema|architettura|deploy|migra|sviluppa|build|create|'
r'implement|design|architect|'
r'patch|fix|debug|ottimizza|optimize|rinomina|rename|'
r'aggiungi|aggiorna|update|integra|integrate|'
r'scaffold|bootstrap|genera|generate|'
r'ristruttura|restructure|refactorizza|converti|convert|'
r'installa|clona|avvia|esegui|testa|verifica|configura|'
r'install|clone|run|test|verify|setup|configure)\b',
re.IGNORECASE,
)
@classmethod # B13: era @staticmethod — usa class attrs già compilati invece di re.search inline
def _max_tokens_for_goal(cls, goal: str) -> int:
"""S373: stima budget max_tokens in base alla complessità del goal.
Groq llama-3.3-70b supporta 32768 output token, Gemini 2.5-flash 8192.
Default 2048 solo per Q&A semplice; per codice si scala fino a 8192.
B13: usa class attrs _COMPLEX_APP_RE/_MULTI_FEATURE_RE/_CODE_TASK_RE già compilati.
"""
# B6: fast-fix task → risposta atomica breve → 512 token max (-30-60% LLM time)
# Conseguenza: "aggiungi import X" non richiede 4096 tokens — 50-100 bastano.
if cls._FAST_FIX_RE.search(goal[:180]) and len(goal) < 180:
return 512
g = goal[:600]
is_app = bool(cls._COMPLEX_APP_RE.search(g))
is_complex = bool(cls._MULTI_FEATURE_RE.search(g))
is_code = bool(cls._CODE_TASK_RE.search(g))
if is_app and is_complex:
return 8192 # app multi-feature → output completo garantito
if is_app or is_complex:
return 6144 # app semplice o feature complessa
if is_code:
return 4096 # singola funzione/componente
return 2048 # Q&A, spiegazioni, risposte brevi
# ── Block B: Output sanitization + explanation regex attrs ────────────────
@staticmethod
def _sanitize_agent_output(text: str) -> str:
"""S371: Rimuove monologue interno che può leakare da smolagents/LLM.
Patterns rimossi: GOAL:/DONE_WHEN:/OUT_OF_SCOPE: blocks, Proceed.We need to output...,
Thought:/Code:/Observation: lines, raw JSON tool call arrays.
S390: aggiunto stripping <think> blocks (Qwen3/DeepSeek-R1 reasoning leaks).
B12: fast-path — skip tutti i regex se nessun segnale di monologue (risparmia 50-200ms).
"""
if not text:
return text
# S390: strip <think> sempre — Qwen3 e DeepSeek-R1 li iniettano indipendentemente dagli altri segnali
import re as _re # BV-3: hoisted — stdlib, always in sys.modules
if '<think>' in text:
text = _re.sub(r'<think>[\s\S]*?</think>', '', text, flags=_re.IGNORECASE) # blocco chiuso
text = _re.sub(r'<think>[\s\S]*', '', text, flags=_re.IGNORECASE) # blocco aperto (troncato)
text = text.strip()
if not text:
return text
# B12: fast-path exit — ~95% delle risposte normali non contengono questi segnali
_MONOLOGUE_SIGNALS = ('GOAL:', 'DONE_WHEN:', 'OUT_OF_SCOPE:', 'Proceed.', 'Thought:', 'Code:', '[{"action"')
if not any(s in text for s in _MONOLOGUE_SIGNALS):
return text.strip()
# BV-3: _re already imported at function top
# Strip smolagents internal prompt blocks (GOAL/DONE_WHEN/OUT_OF_SCOPE)
text = _re.sub(
r'(?m)^(?:GOAL|DONE_WHEN|OUT_OF_SCOPE):\s*.*(?:\n(?!\n)[^\n]*)*',
'', text
)
# Strip "Proceed. We need to output the tool call now. [{"action":...}]"
text = _re.sub(
r'Proceed\.?\s*We\s+need\s+to\s+output\s+the\s+tool\s+call\s+now\.?\s*\[.*?\]',
'', text, flags=_re.DOTALL
)
# Strip raw JSON tool call arrays at line start
text = _re.sub(r'^\s*\[\s*\{["\']*action["\']*\s*:', '', text, flags=_re.MULTILINE)
# Strip smolagents Thought:/Code:/Observation: line prefixes (when not inside code blocks)
text = _re.sub(r'^(?:Thought|Code|Observation|Action Input):\s*', '', text, flags=_re.MULTILINE)
# Strip [STEP N/M] markers
text = _re.sub(r'\[STEP\s+\d+/\d+\]\s*', '', text)
return text.strip()
# S375: Format directive classifier — speculare al frontend formatClassifier.ts
# Iniettato in _build_messages() per garantire formattazione consistente
# anche sui task tool-heavy gestiti interamente dal backend.
_FORMAT_DIRECTIVE_CODE = (
"FORMATO RISPOSTA OBBLIGATORIO — CODICE:\n"
"• Usa SEMPRE blocchi markdown con linguaggio specificato (```python, ```typescript, ecc.)\n"
"• Struttura: breve spiegazione → blocco codice completo → come usarlo\n"
"• Ogni blocco deve essere autonomo ed eseguibile senza modifiche\n"
"• Aggiungi commenti inline per la logica non ovvia\n"
"• Se multi-file: mostra ogni file in un blocco separato con il nome come titolo\n"
"• Formato titolo file OBBLIGATORIO: ### src/nomefile.tsx (H3 - risparmia spazio verticale su mobile)"
)
_FORMAT_DIRECTIVE_MARKDOWN = (
"FORMATO RISPOSTA OBBLIGATORIO — STRUTTURATO:\n"
"• Usa titoli (##), liste puntate, grassetto per punti chiave\n"
"• Max 3 livelli di gerarchia — non annidare troppo\n"
"• Tabelle markdown per confronti (3+ elementi)\n"
"• Paragrafi brevi (2-3 righe) per leggibilità mobile\n"
"Su mobile, tabelle 3+ colonne: usa lista chiave-valore o ### + punti (scroll orizzontale non usabile su iPhone)"
)
_FORMAT_DIRECTIVE_CONVERSATIONAL = (
"FORMATO RISPOSTA OBBLIGATORIO — CONVERSAZIONALE:\n"
"• Tono diretto e naturale, senza formalismi eccessivi\n"
"• Niente strutture markdown pesanti per domande semplici\n"
"• Rispondi in 1-3 paragrafi se la domanda è semplice\n"
"• Usa grassetto solo per termini chiave critici"
)
_FORMAT_DIRECTIVE_MATH = (
"FORMATO RISPOSTA OBBLIGATORIO — MATEMATICA:\n"
"• Mostra SEMPRE i calcoli passo per passo numerati\n"
"• Usa notazione chiara: P(A|B), E[X], Σ, ecc.\n"
"• Risultato finale in riga separata con grassetto\n"
"• Usa il PUNTO come separatore decimale (non virgola)\n"
"• Esprimi probabilità sia come frazione che come percentuale"
)
# S-FMT-MOBILE: 4 nuove direttive specializzate iPhone (2026-06-12)
_FORMAT_DIRECTIVE_RESEARCH = (
"FORMATO RISPOSTA OBBLIGATORIO - RICERCA:\n"
"- Struttura ogni fonte: Fonte -> sintesi 1-2 righe -> punto chiave\n"
"- Max 4 fonti, priorita qualita su quantita\n"
"- Separazione netta fatti verificati vs interpretazioni\n"
"- Termina con sezione VERDETTO (1 paragrafo, risposta diretta)\n"
"- Nessuna fonte disponibile: dichiaralo esplicitamente"
)
_FORMAT_DIRECTIVE_DEBUG = (
"FORMATO RISPOSTA OBBLIGATORIO - DEBUG:\n"
"- Struttura FISSA 4 sezioni: Errore -> Causa -> Fix (codice) -> Prevenzione\n"
"- Sezione Fix: codice completo pronto da copiare, non frammenti\n"
"- Sezione Causa: spiega PERCHE accade, non solo cosa accade\n"
"- Sezione Prevenzione: max 2 punti concreti\n"
"- Bug multipli: numera ogni set Errore/Causa/Fix/Prevenzione"
)
_FORMAT_DIRECTIVE_MEDIA = (
"FORMATO RISPOSTA OBBLIGATORIO - MEDIA/IMMAGINE:\n"
"- URL immagine come link cliccabile: [Visualizza immagine](URL)\n"
"- URL in riga separata per copy-paste\n"
"- Breve descrizione di cosa raffigura l' immagine generata\n"
"- Se non disponibile: fornisci URL Pollinations come fallback esplicito"
)
_FORMAT_DIRECTIVE_MOBILE_COMPACT = (
"FORMATO RISPOSTA OBBLIGATORIO - MOBILE RAPIDO:\n"
"- Risposta MASSIMO 150 parole, vai dritto al punto\n"
"- Prima riga = verdetto/azione (no preamboli)\n"
"- Usa emoji come icone stato: OK fatto, WARN attenzione, ERR errore, TIP suggerimento\n"
"- Zero spiegazioni non richieste, solo l' essenziale\n"
"- Se servono dettagli: offri follow-up"
)
# GAP-A: narrazione pre-tool — spiega all'utente PERCHÉ l'agente usa quel tool.
# Usato nel _run_subtask (reason field) e come text_chunk prima del gather.
# Zero latency: nessuna chiamata LLM — solo lookup di dizionario.
_TOOL_NARRATION: dict[str, str] = {
"web_search": "🔍 Cerco informazioni aggiornate sul web",
"read_page": "🌐 Leggo la pagina web per estrarre i dati",
"write_file": "✍️ Implemento il codice richiesto",
"read_file": "📂 Leggo il file per analizzare la struttura attuale",
"apply_patch": "🔧 Applico la patch mirata al file",
"run_python": "⚙️ Eseguo il codice Python per verificare il risultato",
"execute_shell": "🖥️ Eseguo il comando shell",
"execute_sql": "🗄️ Eseguo la query SQL",
"database_query": "🗄️ Eseguo la query sul database",
"git_push": "🚀 Invio le modifiche al repository",
"git_commit": "💾 Salvo le modifiche con un commit",
"git_status": "📊 Verifico lo stato del repository",
"git_diff": "🔎 Confronto le modifiche in staging",
"git_clone": "📥 Clono il repository",
"npm_install": "📦 Installo le dipendenze Node.js",
"npm_run": "▶️ Avvio lo script npm",
"pip_install": "📦 Installo i pacchetti Python",
"type_check": "✅ Verifico i tipi TypeScript",
"lint_code": "🔍 Analizzo la qualità del codice",
"get_weather": "🌤️ Recupero le previsioni meteo",
"get_news": "📰 Recupero le ultime notizie",
"recall": "🧠 Consulto la memoria dell'agente",
"list_files": "📁 Elenco i file del progetto",
"directory_tree": "🗂️ Analizzo la struttura del progetto",
"file_search": "🔎 Cerco nel codice del progetto",
"create_project": "🏗️ Creo la struttura del progetto",
"scaffold_project": "🏗️ Genero la struttura del progetto da template",
# Tool planner-only (non in TOOL_REGISTRY) — narrazione pre-gather
"code": "💻 Implemento la soluzione richiesta",
"send_email": "📧 Invio l'email tramite Resend",
"create_pdf": "📄 Genero il documento PDF",
"call_api": "🔗 Chiamo l'API esterna",
"browser_navigate": "🌐 Navigo verso la pagina",
"browser_session_open": "🌐 Apro una sessione browser",
"browser_session_act": "🖱️ Interagisco con la pagina",
"get_image": "🖼️ Recupero l'immagine",
"create_chart": "📊 Creo il grafico",
"diff_text": "🔎 Confronto i testi",
"validate_json": "✅ Valido il JSON",
"calculate": "🔢 Calcolo il risultato matematico",
"image": "🎨 Genero un'immagine con AI",
"generate_image": "🎨 Genero un'immagine con AI",
"delegate_task": "🤝 Delego a un micro-agente specializzato",
"browser_session_open": "🖱️ Apro una sessione browser autonoma",
"browser_session_act": "🖱️ Interagisco con la pagina nel browser",
"web_research": "🔍 Ricerca approfondita multi-fonte sul web",
}
_TOOL_NARRATION_DEFAULT = "⚙️ Eseguo l'operazione"
# GAP-B scaffold preview: albero file mostrato in real-time PRIMA che il tool scriva.
# Sincronizzato con i template in registry.py/_scaffold_project — aggiorna entrambi.
_SCAFFOLD_FILE_TREE: dict[str, list[str]] = {
"react": ["package.json", "index.html", "vite.config.ts", "src/main.tsx", "src/App.tsx", "src/index.css"],
"nextjs": ["package.json", "next.config.mjs", "app/layout.tsx", "app/page.tsx"],
"fastapi": ["main.py", "requirements.txt", "Dockerfile", ".gitignore"],
"flask": ["app.py", "requirements.txt", ".gitignore"],
"django": ["manage.py", "requirements.txt", "config/settings.py", "config/urls.py", "api/views.py", "api/urls.py"],
"express": ["package.json", "src/index.js", ".gitignore"],
}
# S427: ampliato con verbi IT/EN + tecnologie — stesso set di _CODE_RE
_CODE_GOAL_RE = re.compile(
r'\b(scrivi|crea|genera|implementa|refactor|codice|funzione|classe|componente|'
r'script|algoritmo|api|endpoint|hook|store|tipo|interface|migration|query|schema|'
r'write|create|generate|implement|code|function|class|component|backend|frontend|'
r'sistema|correggi|debugga|patch|rinomina|sostituisci|rimpiazza|ottimizza|'
r'rename|replace|remove|delete|fix|debug|optimize|deploy|scaffold|'
r'typescript|javascript|python|react|vue|svelte|angular|next\.?js|nuxt|'
r'fastapi|flask|django|express|nest\.?js|rails|laravel|'
r'service|repository|controller|middleware|utility|helper|'
r'css|scss|html|sql|graphql|dockerfile|prisma|drizzle)\b',
re.IGNORECASE,
)
# S427: ampliato con più concetti matematici IT/EN
_MATH_GOAL_RE = re.compile(
r'\b(calcola|calcolare|probabilit|bayes|integra|derivat|statistic|media|'
r'varianza|percentuale|equazione|formula|risolvi|calculate|probability|'
r'integral|derivative|statistic|mean|variance|equation|solve|'
r'somma|prodotto|divisione|divisore|multiplo|mcd|mcm|modulo|quoziente|'
r'logaritmo|radice|potenza|fattoriale|fibonacci|'
r'trigonometria|seno|coseno|tangente|algebra|geometria|aritmetica|'
r'matrice|determinante|vettore|'
r'sum|product|division|lcm|gcd|remainder|quotient|'
r'logarithm|sqrt|square\s*root|factorial|power|'
r'trigonometry|sine|cosine|tangent|matrix|determinant|vector|'
r'fraction|frazioni|decimali|decimal|percentag)\b',
re.IGNORECASE,
)
# S427: ampliato con più trigger per risposta strutturata markdown
_MARKDOWN_GOAL_RE = re.compile(
r'\b(elenca|confronta|spiega|differenz|vantaggi|svantaggi|guida|tutorial|'
r'passaggi|step|pros?|contro|list|compare|explain|differences?|advantages?|'
r'disadvantages?|guide|steps?|pros?|cons?|'
r'riassumi|riassunto|summarize|summary|overview|panoramica|'
r'tabella|table|sezioni|sections|categorizza|categorie|'
r'elencami|dammi una lista|i migliori|le migliori|i principali|le principali|'
r'tipi di|types\s+of|examples?\s+of|esempi\s+di|'
r'struttura|structure|breakdown|analisi|analysis)\b',
re.IGNORECASE,
)
# S-FMT-MOBILE: regex per direttive specializzate iPhone (2026-06-12)
_RESEARCH_GOAL_RE = re.compile(
r'\b(ricerca|research|trova\s+info|notizie|news|articoli|fonti|'
r'find\s+information|when\s+did|storia\s+di|'
r'ultima\s+notizia|latest\s+on|trending|fact.?check)\b',
re.IGNORECASE,
)
_DEBUG_GOAL_RE = re.compile(
r'\b(errore|error|traceback|exception|stacktrace|stack\s+trace|'
r'debug|debugga|crash|fallisce|non\s+funziona|si\s+rompe|'
r'undefined\s+is\s+not|cannot\s+read|type\s+error|runtime\s+error|'
r'fix\s+(?:the\s+)?(?:bug|error|crash|issue|problem))\b',
re.IGNORECASE,
)
_MEDIA_GOAL_RE = re.compile(
r'\b(genera\s+immagine|generate\s+image|crea\s+immagine|create\s+image|'
r'immagine\s+di|image\s+of|foto\s+di|picture\s+of|'
r'disegna|draw|illustra|illustrate|render|genera\s+foto|'
r'flux|dall.?e|midjourney|stable\s+diffusion|pollinations)\b',
re.IGNORECASE,
)
_MOBILE_COMPACT_RE = re.compile(
r'\b(quick|veloce|breve|dimmi\s+solo|solo\s+il\s+risultato|verdetto|'
r'in\s+breve|tl;?dr|sintesi\s+rapida|recap|riassumi\s+in\s+poche|'
r'risposta\s+breve|brief\s+answer|short\s+answer)\b',
re.IGNORECASE,
)
# GAP-ROUT: routing specializzato per benchmark (SQL, Reasoning, MMLU)
_SQL_GOAL_RE = re.compile(
r'\b(sql|postgresql|cte ricorsiva|recursive cte|with recursive|'
r'window functions?|over\(|partition by|rank\(|row_number\(|'
r'gerarchia|parent_id|manager_id|recursive)\b',
re.IGNORECASE,
)
_REASONING_GOAL_RE = re.compile(
r'\b(reasoning|gsm8k|math|matematica|logica|ragionamento|'
r'ted the t-rex|calcola|calcolare|probabilit|bayes|frazioni|percentuale)\b',
re.IGNORECASE,
)
_MMLU_GOAL_RE = re.compile(
r'\b(mmlu|computer science|informatica|architettura|os|networking|'
r'database|complessità|p vs np|modello osi|acid properties)\b',
re.IGNORECASE,
)
# S-FMT-ORCH: fast-fix detector per bypass ARCHITECT su singola operazione (<180 chars)
# B1: espansa con 10 operazioni atomiche — guardata da len(goal)<180 nel chiamante.
# Conseguenze: skip ARCHITECT (-15s) per operazioni single-step unambiguamente chiare.
# Zero cons: la guard len<180 esclude goal multi-step; il piano sintetico è sufficiente.
_FAST_FIX_RE = re.compile(
r'\b(typos?|rinomina\s+\w|rename\s+\w|'
r'cambia\s+.{1,40}\s+in\s+|change\s+.{1,40}\s+to\s+|'
r'sostituisci\s+.{1,40}\s+con\s+|replace\s+.{1,40}\s+with\s+|'
r'aggiungi\s+commento|add\s+comment|correggi\s+il\s+typo|fix\s+typo|'
r'rimuovi\s+riga|delete\s+line|bump\s+version|update\s+version\s+to|'
# Fix-4.2: import banali non hanno bisogno di 10-15s Architect
r'aggiungi\s+import|add\s+import|manca\s+import|import\s+missing|'
r'install\s+package|installa\s+pacchetto|aggiungi\s+dipendenza|'
# B1-a: parametri / campi / proprietà — operazioni single-step su firme
r'aggiungi\s+(?:un\s+)?(?:parametro|param|argomento|campo|field|propert\w+)\b|'
r'rimuovi\s+(?:il\s+|la\s+|questo\s+|questa\s+)?(?:parametro|param|campo|field|prop|propert\w+)\b|'
# B1-b: tipi / annotazioni — aggiunta tipo/interface senza logica
r'aggiungi\s+(?:il\s+|un\s+)?(?:tipo|type|annotation|return\s+type|tipo\s+di\s+ritorno)\b|'
r'(?:aggiorna|update)\s+(?:il\s+)?(?:tipo|type|interface|schema|signature)\b|'
# B1-c: export — aggiunta/rimozione senza logica
r'aggiungi\s+(?:il\s+|un\s+)?(?:export|export\s+default|default\s+export)\b|'
r'rimuovi\s+(?:il\s+)?(?:export|default\s+export)\b|'
# B1-d: log / debug — aggiungi o rimuovi singola istruzione
r'aggiungi\s+(?:un\s+)?(?:console\.log|print|log|debug)\b|'
r'(?:togli|elimina|cancella|rimuovi)\s+(?:il\s+|i\s+)?(?:console\.log|log\s+di\s+debug|debug\s+log|print)\b|'
# B1-e: formattazione / indentazione — nessuna logica, solo stile
r'correggi\s+(?:l[a\']?\s+)?(?:indentazione|indent|spaziatura|spacing|formatt\w+)\b|'
r'(?:formatta|format)\s+(?:il\s+)?(?:codice|file|questo)\b|'
# B1-f: conversioni semplici — async/arrow/const senza cambiare logica
r'(?:converti|convert)\s+.{1,50}\s+(?:in|to|a)\s+(?:async|arrow\s+function|const|let)\b)\b',
re.IGNORECASE,
)
# B5: pure-explanation bypass — nessun tool, LLM diretto (risparmio -20-30s).
# Fix storia: cos'?[eè] (char class ['è] era sbagliata: matchava 1 char, non seq 'è).
# Fail-open: 4 guard — len<300, pattern, no action verb, no file ref.
_PURE_EXPLANATION_RE = re.compile(
r"^\s*(?:"
r"(?:cos'?[e\xe8]\s+)"
r"|(?:che\s+cos'?[a\xe0]?\s*[e\xe8]\s+)"
r"|(?:spiegami\b)"
r"|(?:dimmi\s+(?:come|cosa|cos|perch[e\xe8]|qual[e\xe8])\b)"
r"|(?:qual[e\xe8]\s+|qual\s+[e\xe8]\s+)(?:la\s+)?(?:differenz[ae]|scopo|significato)"
r"|(?:come\s+funziona\s+(?!il\s+(?:mio|tuo|nostro|codice|progetto|login|sito|sistema|questo)\b))"
r"|(?:cosa\s+(?:fa|significa|vuol\s+dire|rappresenta)\s+)"
r"|(?:a\s+cosa\s+serve\s+)"
r"|(?:perch[e\xe8]\s+(?:si\s+usa|viene\s+usato|[e\xe8]\s+utile|esiste)\s+)"
r"|(?:what\s+(?:is|are|does|means?)\s+(?!my\b|the\s+output\b|this\s+code\b))"
r"|(?:how\s+does\s+(?!my\b|this\b|the\s+code\b|it\s+work\s+in\b))"
r"|(?:explain\s+(?:me\s+)?(?:briefly\s+)?(?:what|how|why|the)\s+)"
r")",
re.IGNORECASE | re.DOTALL,
)
_EXPL_ACTION_RE = re.compile(
r"\b(crea|scrivi|genera|implementa|esegui|correggi|fix|run|create|write|"
r"generate|implement|execute|installa|deploy|avvia|configura|aggiorna|update|"
r"aggiungi|rimuovi|modifica|refactor|build|pubblica|manda|invia|upload|"
r"poi|then|e\s+poi|and\s+then|quindi|dopo|successivamente)\b",
re.IGNORECASE,
)
_EXPL_FILE_REF_RE = re.compile(
r"\b(nel\s+codice|in\s+questo\s+file|nel\s+file|nel\s+progetto|"
r"qui\s+sopra|il\s+codice\s+che|in\s+the\s+code|"
r"in\s+this\s+file|this\s+function|questa\s+funzione)\b",
re.IGNORECASE,
)