"""Astrazioni del sottosistema di rilevamento (SOLID). Speculare al sottosistema di rendering PDF: ogni *metodologia di rilevamento* e' una strategia isolata che dichiara quali riconoscitori attivare. Le strategie sono registrate in un `DetectionRegistry` ed escludibili da configurazione: aggiungerne/toglierne una non tocca il codice esistente (Open/Closed) ne' il livello web (Dependency Inversion). - Single Responsibility: ogni strategia vive in un proprio file e sa solo comporre il proprio insieme di riconoscitori. - Liskov: tutte rispettano `recognizers(provider) -> list[Recognizer]`. - Interface Segregation: la strategia espone solo `info` e `recognizers`; il `RecognizerProvider` espone una factory lazy per ogni famiglia di tecniche. - Dependency Inversion: le strategie dipendono dal Protocol `RecognizerProvider`, non dai modelli pesanti concreti. """ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Sequence from dataclasses import dataclass from enum import Enum from typing import Protocol, runtime_checkable from core.contracts import Recognizer class DetectionTechnique(str, Enum): """Famiglia tecnologica di una metodologia di rilevamento.""" DETERMINISTIC = "deterministic" # regex + checksum, nessun modello STATISTICAL_NER = "statistical_ner" # token classification (transformer) SEMANTIC_ZEROSHOT = "zero_shot" # natural language inference GENERATIVE_LLM = "generative_llm" # LLM istruito che estrae JSON EMBEDDING = "embedding" # similarita' del coseno su embedding HYBRID = "hybrid" # fusione multi-tecnica @dataclass(frozen=True, slots=True) class StrategyInfo: """Metadati di una strategia, usati anche dalla UI per i controlli.""" key: str label: str technique: DetectionTechnique description: str requires_ml: bool # True se senza modelli degrada al solo backbone models: tuple[str, ...] = () # id dei modelli usati (informativo per la UI) default: bool = False @runtime_checkable class RecognizerProvider(Protocol): """Sorgente lazy di riconoscitori per famiglia di tecnica. Ogni metodo costruisce (una sola volta, con caching a carico dell'implementazione) i riconoscitori della propria famiglia. In assenza dei modelli o con ML disabilitato ritorna una sequenza vuota: le strategie degradano in modo controllato sul backbone a regole. """ def rules(self) -> Sequence[Recognizer]: ... def ner(self) -> Sequence[Recognizer]: ... def ner_comuni(self) -> Sequence[Recognizer]: ... def zero_shot(self) -> Sequence[Recognizer]: ... def baseline(self) -> Sequence[Recognizer]: ... def llm(self) -> Sequence[Recognizer]: ... def embedding(self) -> Sequence[Recognizer]: ... # Costruzione generica per chiave di layer (supporta layer aggiunti a runtime). def family(self, key: str) -> Sequence[Recognizer]: ... class DetectionStrategy(ABC): """Interfaccia comune a tutte le metodologie di rilevamento.""" #: Metadati statici della strategia. info: StrategyInfo @abstractmethod def recognizers(self, provider: RecognizerProvider) -> list[Recognizer]: """Compone l'insieme di riconoscitori della metodologia.""" def make_strategy(info: StrategyInfo, *provider_methods: str) -> DetectionStrategy: """Crea una strategia che compone i riconoscitori dei metodi indicati. `provider_methods` sono i nomi dei metodi del `RecognizerProvider` da concatenare in ordine (es. "rules", "ner"). Elimina la duplicazione delle classi-strategia quasi identiche: aggiungere una metodologia diventa una riga dichiarativa (Open/Closed) invece di un nuovo file. """ class _ComposedStrategy(DetectionStrategy): def recognizers(self, provider: RecognizerProvider) -> list[Recognizer]: recs: list[Recognizer] = [] for method in provider_methods: recs.extend(getattr(provider, method)()) return recs strategy = _ComposedStrategy() strategy.info = info return strategy class DetectionRegistry: """Collezione ordinata di metodologie abilitate (Open/Closed).""" def __init__(self) -> None: self._strategies: dict[str, DetectionStrategy] = {} def register(self, strategy: DetectionStrategy) -> DetectionRegistry: if strategy.info.key in self._strategies: raise ValueError(f"Strategia gia' registrata: {strategy.info.key}") self._strategies[strategy.info.key] = strategy return self def has(self, key: str) -> bool: return key in self._strategies def get(self, key: str) -> DetectionStrategy | None: return self._strategies.get(key) def infos(self) -> list[StrategyInfo]: return [s.info for s in self._strategies.values()] def default_key(self) -> str: for s in self._strategies.values(): if s.info.default: return s.info.key # fallback: la prima registrata return next(iter(self._strategies)) def __len__(self) -> int: return len(self._strategies)