# Pipeline di Rilevamento — Documento Tecnico File di riferimento: `core/pipeline.py`, `core/span_resolver.py`, `core/pipeline_config.py` --- ## 1. Flusso Generale ```plantuml @startuml pipeline-generale skinparam backgroundColor #FAFAFA skinparam defaultFontName Arial skinparam ArrowColor #374151 skinparam ActivityBorderColor #6366f1 skinparam ActivityBackgroundColor #EEF2FF skinparam ActivityDiamondBackgroundColor #FEF3C7 skinparam ActivityDiamondBorderColor #D97706 title Pipeline di Rilevamento — detect() start :Testo in ingresso; if (len(text) > 1500 char?) then (sì) :Chunking\n1500 char, overlap 200; else (no) :Chunk unico; endif fork :Step0\nPresidio built-in IT+EN\n(regex, no ML); fork again :L0 + L1\nanalyzer_full.analyze()\n(Regex appalti + NER transformer); fork again :L2\nGLiNER.analyze()\n(zero-shot appalti); end fork :Filtro per min_score\n(default 0.85); :Split L0/L1\n_is_regex_result()\n→ regex_results / ner_results; :Riclassificazione NER\n(NUMERO_DOCUMENTO → CIG/CUP/REA\nse pattern fullmatch); :Cross-layer Agreement Boost\n(Jaccard ≥ 0.80 su ≥ 2 livelli → +0.15 score); :resolve_overlapping_spans()\n[L0, L1, L2, Step0]\npriorità 0 > 1 > 2 > 3; :Post-boost regex\n(fullmatch POST_BOOST_PATTERNS → +0.30 score); :Lista finale RecognizerResult\ncon source_priority e metadata; stop @enduml ``` --- ## 2. Ordine di Esecuzione vs Ordine di Priorità ```plantuml @startuml esecuzione-vs-priorita skinparam backgroundColor #FAFAFA skinparam defaultFontName Arial skinparam SequenceArrowColor #374151 skinparam SequenceLifeLineBorderColor #9CA3AF skinparam SequenceGroupBorderColor #6366f1 skinparam SequenceGroupHeaderFontColor #6366f1 skinparam SequenceParticipantBorderColor #6366f1 skinparam SequenceParticipantBackgroundColor #EEF2FF title Esecuzione: Step0 per primo / Priorità: Step0 ultima participant "detect()" as D participant "Step0\n(prior. 3)" as S0 #FFD6D6 participant "L0 Regex\n(prior. 0)" as L0 #D1FAE5 participant "L1 NER\n(prior. 1)" as L1 #DBEAFE participant "L2 GLiNER\n(prior. 2)" as L2 #FEF3C7 participant "SpanResolver" as SR note over S0 : Eseguito per PRIMO\npuro regex, no ML D -> S0 : _run_step0_chunked() S0 --> D : step0_results\n(score bassi se no context) D -> L0 : analyzer_full.analyze() D -> L1 : (stesso risultato, split) L0 --> D : regex_results L1 --> D : ner_results D -> L2 : _run_gliner_chunked() L2 --> D : gliner_results D -> D : filtro min_score\nper tutti i layer group Agreement Boost (tutti i layer insieme) D -> D : _cross_layer_boost(\n [L0, L1, L2, Step0]\n) end note over SR : Priorità (vince il minore):\nL0=0, L1=1, L2=2, Step0=3 D -> SR : resolve_overlapping_spans(\n [L0, L1, L2, Step0]\n) SR -> SR : Itera nell'ordine 0→3\nAggiunge a "accepted"\nse no overlap già accettato SR --> D : Entità finali\n(Step0 scartato se\ncoperto da L0/L1/L2) D -> D : _post_boost_check() D --> D : lista RecognizerResult @enduml ``` --- ## 3. Chunking del Testo ```plantuml @startuml chunking skinparam backgroundColor #FAFAFA skinparam defaultFontName Arial skinparam RectangleBorderColor #6366f1 skinparam RectangleBackgroundColor #EEF2FF title _chunk_text() — chunk_size=1500, overlap=200 rectangle "Testo originale (N caratteri)" as T { rectangle "Chunk 0\n[0 … 1500)" as C0 #D1FAE5 rectangle "Chunk 1\n[1300 … 2800)" as C1 #DBEAFE rectangle "Chunk 2\n[2600 … 4100)" as C2 #FEF3C7 rectangle "…" as CN } note bottom of C0 : step = chunk_size - overlap\n= 1500 - 200 = 1300 note bottom of C1 : overlap 200 char\ncon chunk precedente note right of CN : Ogni chunk viene\nanalizzato da tutti i layer.\nGli offset vengono\nriportati al testo originale. @enduml ``` --- ## 4. Risoluzione degli Span Sovrapposti ```plantuml @startuml span-resolver skinparam backgroundColor #FAFAFA skinparam defaultFontName Arial title resolve_overlapping_spans() — logica di priorità |L0 Regex (prior. 0)| start :Ordina per -score, -lunghezza; :Itera span; if (overlap con\nqualcosa in "accepted"?) then (no) :Aggiunge a accepted\nsource_priority = 0; else (sì) :SCARTA; endif stop |L1 NER (prior. 1)| start :Ordina per -score, -lunghezza; :Itera span; if (overlap con\nqualcosa in "accepted"?) then (no) :Aggiunge a accepted\nsource_priority = 1; else (sì) :SCARTA; endif stop |L2 GLiNER (prior. 2)| start :Itera span; if (overlap con\nqualcosa in "accepted"?) then (no) :Aggiunge a accepted\nsource_priority = 2; else (sì) :SCARTA; endif stop |Step0 (prior. 3 — minima)| start :Itera span; if (overlap con\nL0 / L1 / L2\ngià accettati?) then (no) :Aggiunge a accepted\nsource_priority = 3; else (sì) :SCARTA\n(L0/L1/L2 vincono sempre); endif stop @enduml ``` --- ## 5. Cross-Layer Agreement Boost ```plantuml @startuml agreement-boost skinparam backgroundColor #FAFAFA skinparam defaultFontName Arial skinparam ActivityBorderColor #6366f1 skinparam ActivityBackgroundColor #EEF2FF skinparam ActivityDiamondBackgroundColor #FEF3C7 skinparam ActivityDiamondBorderColor #D97706 title _cross_layer_boost() — Jaccard ≥ 0.80 start :Appiattisci tutti i layer\nin lista [(layer_idx, result)]; repeat :Prendi result R dal layer i; :Cerca tutti gli altri result O\nda layer j ≠ i; :Calcola Jaccard(R, O)\n= |R∩O| / |R∪O|; if (Jaccard ≥ 0.80?) then (sì) :Aggiungi j al set "agreeing"; endif if (|agreeing| ≥ 2\n(min_agreement_layers)?) then (sì) :score = min(1.0,\nscore + 0.15); :Salva metadata:\ncross_layer_agreement = |agreeing|; endif repeat while (altri result?) is (sì) ->no; :Restituisce i layer\ncon score aggiornati; stop @enduml ``` --- ## 6. Post-Boost Regex ```plantuml @startuml post-boost skinparam backgroundColor #FAFAFA skinparam defaultFontName Arial skinparam ActivityBorderColor #6366f1 skinparam ActivityBackgroundColor #EEF2FF skinparam ActivityDiamondBackgroundColor #FEF3C7 title _post_boost_check() — applicato DOPO span resolution start :Riceve lista finale\ndi RecognizerResult; repeat :Prendi entità R; :Cerca pattern in\nPOST_BOOST_PATTERNS[R.entity_type]; if (pattern esiste?) then (sì) if (fullmatch(text[R.start:R.end])?) then (sì) :score = min(1.0, score + 0.30); :Salva metadata:\npost_boost = True; else (no) :score invariato; endif else (no) :score invariato; endif repeat while (altra entità?) is (sì) ->no; :Restituisce lista\ncon score finali; stop note right Entità con post_boost=True sono indicate con "+" nel badge del Tab Evidenziato. Boost: +0.30 (max 1.0) end note @enduml ``` --- ## 7. Struttura dei Dati ```plantuml @startuml struttura-dati skinparam backgroundColor #FAFAFA skinparam defaultFontName Arial skinparam ClassBorderColor #6366f1 skinparam ClassBackgroundColor #EEF2FF skinparam ClassHeaderBackgroundColor #6366f1 skinparam ClassHeaderFontColor #FFFFFF title RecognizerResult — metadati aggiunti dal pipeline class RecognizerResult { + entity_type : str + start : int + end : int + score : float + analysis_explanation + recognition_metadata : dict } class "recognition_metadata" { + source_priority : int ..valori possibili.. 0 → L0 Regex appalti 1 → L1 NER transformer 2 → L2 GLiNER 3 → Step0 Presidio built-in -- + cross_layer_agreement : int ..se ≥ 2 layer concordano.. -- + post_boost : bool ..se fullmatch POST_BOOST_PATTERNS.. } RecognizerResult --> "recognition_metadata" class PipelineConfig <> { + post_boost_delta : float = 0.30 + post_boost_max : float = 1.0 + agreement_delta : float = 0.15 + agreement_max : float = 1.0 + min_agreement_layers : int = 2 + chunk_size : int = 1500 + chunk_overlap : int = 200 + jaccard_threshold : float = 0.80 + gliner_threshold : float = 0.65 } @enduml ``` --- ## 8. Tabella Riepilogativa dei Layer | | **Step0** | **L0 Regex** | **L1 NER** | **L2 GLiNER** | |---|---|---|---|---| | **Priorità** | 3 (minima) | 0 (massima) | 1 | 2 | | **Esecuzione** | 1ª (prima di tutto) | 2ª (con L1) | 2ª (con L0) | 3ª | | **Tecnologia** | Regex puro, no ML | PatternRecognizer Presidio | Transformer HuggingFace | Zero-shot NER | | **Modello** | Presidio built-in | *(nessuno)* | `Italian_NER_XXL_v2` | `GLiNER_PII_ITA` | | **Scope** | IT+EN universale | Appalti italiani | Anagrafico-legale IT | Appalti zero-shot | | **N. entità** | 31 recognizer | 20 pattern | ~60 tipi | 14 label | | **Vince su** | Nessuno | L1, L2, Step0 | L2, Step0 | Step0 | | **Perde contro** | L0, L1, L2 | *(nessuno)* | L0 | L0, L1 | | **Score tipico** | 0.05–1.0 | 0.45–0.92 | 0.40–1.0 | 0.65–1.0 | | **Context boost** | No (no NLP artifacts) | Sì (parole chiave) | Sì (via SpaCy) | Sì (threshold) | --- ## 9. Evoluzione dello Score di una Singola Entità ```plantuml @startuml score-evolution skinparam backgroundColor #FAFAFA skinparam defaultFontName Arial title Esempio: IBAN "IT60X0542811101000000123456" |Step0| :IbanRecognizer.analyze()\nscore = 0.50 (base); |L1 NER| :Italian_NER_XXL_v2\nIBAN → IBAN_CODE\nscore = 0.72; |Filtro min_score| :Step0: 0.50 ≥ 0.85? NO → SCARTATO L1: 0.72 ≥ 0.85? NO → SCARTATO; note right Solo le entità che superano il min_score partecipano al boosting e alla risoluzione. end note |Post-boost| :IBAN_CODE fullmatch pattern ^IT\d{2}[A-Z]\d{10}[A-Z0-9]{12}$ → score += 0.30 → 0.72 + 0.30 = 1.00; |Risultato finale| :IBAN_CODE, score=1.00 source_priority=1 (NER) post_boost=True; @enduml ``` --- *File sorgente: `core/pipeline.py` — funzioni `detect()`, `_cross_layer_boost()`, `_post_boost_check()`, `_chunk_text()`, `_run_step0_chunked()`.* *Parametri: `core/pipeline_config.py` → `PipelineConfig`.*