"""Costruzione del verificatore LLM (giudice a valle). Estratta dal `ServiceRegistry` perché è una factory autonoma: dipende solo dalle `Settings` e da un hook `on_before` (fornito dal registry per liberare la VRAM prima di caricare l'LLM). Ritorna `None`, con degradazione controllata, quando l'ML è disabilitato o il modello-giudice non è disponibile. """ from __future__ import annotations from collections.abc import Callable from config.runtime.settings import Settings from core.support.logging_utils import get_logger logger = get_logger("registry") def build_verifier( settings: Settings, on_before: Callable[[], None], approver: str | None = None, simulate: bool = False, ): """Costruisce il verificatore LLM (giudice a valle) o None se indisponibile. Il modello-giudice è scelto tramite la chiave `approver` (default da Settings, fallback al predefinito del registry): un plugin in `layers/llm/llm_approver/` fornisce client (llama.cpp locale) e prompt. Richiede ENABLE_ML come il layer LLM. `on_before` è l'hook (eseguito una sola volta prima della prima chiamata al modello) che libera la VRAM dei modelli di detection prima di caricare l'LLM. `simulate=True` (debug): il verificatore costruisce i prompt per ogni span ma NON chiama l'LLM (nessun load del modello, nessuna valutazione). Serve a ispezionare in debug i prompt che verrebbero inviati. """ if not settings.enable_ml: logger.info("Verifica LLM richiesta ma ENABLE_ML=False: ignorata.") return None try: from layers.llm.backends.base import LlmGenerationParams from layers.llm.llm_approver import provider as approver_provider from layers.llm.verifier import LlmSpanVerifier key = approver or settings.approver plugin = approver_provider.resolve_approver(key) logger.info( "[giudice] modello selezionato: '%s' (%s)", plugin.spec.key, plugin.spec.repo, ) # Batch moderato: dare al modello il contesto di una lista lo fa # "ingaggiare" meglio (a entità singola tende a rispondere []), ma # piccolo per limitare troncamenti/omissioni dei modelli più deboli. verifier_batch = 8 # Token di output PROPORZIONATI al batch: ogni oggetto JSON pesa ~32 # token, quindi bastano ~32/entità + margine. Tetto basso = niente # generazioni runaway = molto più veloce (prima era 768, lentissimo). # La repetition_penalty effettiva è quella dello spec del giudice # (tarata per modello), applicata in BaseApprover.build_client. params = LlmGenerationParams( max_new_tokens=48 * verifier_batch, # ~384 per 8 entità temperature=settings.llm_temperature, timeout=settings.llm_timeout, repetition_penalty=settings.verifier_repetition_penalty, ) client = plugin.build_client(settings, params) prompt = plugin.prompt() except Exception as exc: # noqa: BLE001 - degradazione controllata logger.warning("Verificatore LLM non disponibile: %s", exc) return None # Hook eseguito UNA volta prima della prima chiamata al giudice: libera # dalla VRAM TUTTI gli altri modelli (sempre, non solo su cuda: su CPU è # un no-op innocuo che comunque libera RAM e cache engine). return LlmSpanVerifier( client, weight=settings.verifier_weight, batch_size=verifier_batch, on_before=on_before, prompt=prompt, simulate=simulate, )