Spaces:
Paused
Paused
OpenItaLaw Builder
feat: adaptive prompts, ISTAT/EUR-Lex clients, metadata normalizer, accountability dashboard
2642dc6 | """ | |
| metadata_normalizer.py — Runtime metadata normalization for OpenItaLaw. | |
| Normalizes the heterogeneous field names in doc_metadata.jsonl into canonical | |
| fields at FAISS load time, adds battle_ground detection, pre-republican flags, | |
| and hierarchy_level. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import re | |
| from typing import Any | |
| log = logging.getLogger(__name__) | |
| # --------------------------------------------------------------------------- | |
| # Battle-ground keyword mapping (12 constitutional battlegrounds) | |
| # --------------------------------------------------------------------------- | |
| BATTLE_GROUND_KEYWORDS: dict[str, list[str]] = { | |
| "LAVORO": [ | |
| "lavoro", "lavorat", "impiego", "occupazione", "disoccupazione", | |
| "salario", "retribuzione", "ccnl", "sindac", "sciopero", | |
| "precari", "contratto a termine", "jobs act", "somministrazione", | |
| "rider", "gig economy", "infortuni", "sicurezza lavoro", | |
| ], | |
| "SALUTE": [ | |
| "salute", "sanitari", "ssn", "ospedale", "medic", "farmac", | |
| "vaccin", "pandemia", "covid", "liste d'attesa", "lea", | |
| "pronto soccorso", "asl", "azienda sanitaria", | |
| ], | |
| "ISTRUZIONE": [ | |
| "istruzione", "scuola", "scolastic", "universit", "docent", | |
| "insegnament", "studenti", "neet", "dispersione", "formazione", | |
| "buona scuola", "diritto allo studio", "borse di studio", | |
| ], | |
| "UGUAGLIANZA": [ | |
| "uguaglianza", "discriminazione", "pari opportunit", "disuguaglianza", | |
| "gini", "povertà", "reddito di cittadinanza", "inclusione", | |
| "genere", "parità", "disabilit", "immigra", "integrazione", | |
| ], | |
| "GIUSTIZIA": [ | |
| "giustizia", "processo", "magistrat", "giudice", "tribunale", | |
| "tar", "corte", "avvocat", "gratuito patrocinio", "detenuti", | |
| "carcere", "prescrizione", "procedura penale", "procedura civile", | |
| ], | |
| "AMBIENTE": [ | |
| "ambiente", "ecologi", "inquinamento", "rifiuti", "clima", | |
| "energie rinnovabili", "biodiversità", "paesaggio", "acqua", | |
| "sostenibilit", "emissioni", "condono edilizio", "abusivismo", | |
| ], | |
| "WELFARE": [ | |
| "welfare", "pensione", "previdenz", "assistenz", "inps", | |
| "assegno", "maternità", "disabilità", "non autosufficien", | |
| "reddito", "invalidità", "indennità", | |
| ], | |
| "DEMOCRAZIA": [ | |
| "democrazia", "elettoral", "referendum", "voto", "elezioni", | |
| "parlamento", "camera", "senato", "astensionismo", "sovranità", | |
| "partecipazione", "petizione", "iniziativa legislativa", | |
| ], | |
| "FISCO": [ | |
| "fisco", "tribut", "imposta", "tasse", "iva", "irpef", "ires", | |
| "evasione", "flat tax", "progressiv", "agenzia delle entrate", | |
| "contribuent", "dichiarazione dei redditi", | |
| ], | |
| "TRASPARENZA_PA": [ | |
| "trasparenza", "anticorruzione", "anac", "appalti", "corruzione", | |
| "foia", "accesso civico", "accesso agli atti", "whistleblow", | |
| "pubblica amministrazione", "conflitto di interessi", | |
| ], | |
| "EUROPA": [ | |
| "europa", "ue", "unione europea", "direttiva", "regolamento ue", | |
| "corte di giustizia", "cedu", "recepimento", "infrazione", | |
| "trattato", "commissione europea", "parlamento europeo", | |
| ], | |
| "DIGITALE": [ | |
| "digitale", "internet", "privacy", "gdpr", "dato personale", | |
| "intelligenza artificiale", "cybersicurezza", "spid", "cie", | |
| "pa digitale", "connettività", "banda larga", | |
| ], | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Canonical field mappings | |
| # --------------------------------------------------------------------------- | |
| _VALIDITY_ALIASES = { | |
| "validity_status", "vigenza", "stato_vigore", "status_vigenza", | |
| "abrogazione", "vigente", | |
| } | |
| _TITLE_ALIASES = { | |
| "title", "titolo", "source_title", "nome", "denominazione", | |
| } | |
| _DATE_ALIASES = { | |
| "pub_date", "data_pubbl", "enactment_date", "data", | |
| "data_pubblicazione", "data_entrata_vigore", | |
| } | |
| _URN_ALIASES = { | |
| "urn", "id", "url", "uri", "identifier", "normattiva_url", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Normalization logic | |
| # --------------------------------------------------------------------------- | |
| _PRE_REPUBLICAN_YEAR = 1948 | |
| _RE_YEAR = re.compile(r"\b(1[0-9]{3}|20[0-9]{2})\b") | |
| def _extract_year(date_str: str) -> int | None: | |
| """Extract 4-digit year from various date formats.""" | |
| if not date_str: | |
| return None | |
| m = _RE_YEAR.search(str(date_str)) | |
| return int(m.group(1)) if m else None | |
| def _normalize_validity(doc: dict) -> str: | |
| """Resolve validity_status from whichever alias field is present.""" | |
| for alias in _VALIDITY_ALIASES: | |
| val = doc.get(alias) | |
| if val and isinstance(val, str) and val.strip(): | |
| raw = val.strip().lower() | |
| if any(k in raw for k in ("abrogat", "non vigente", "non in vigore")): | |
| return "abrogato" | |
| if any(k in raw for k in ("vigente", "in_corso", "in vigore")): | |
| return "in_corso" | |
| return raw | |
| return "sconosciuto" | |
| def _resolve_first(doc: dict, aliases: set[str]) -> str: | |
| """Return the first non-empty string value for any of the given keys.""" | |
| for key in aliases: | |
| val = doc.get(key) | |
| if val and isinstance(val, str) and val.strip(): | |
| return val.strip() | |
| return "" | |
| def _detect_battle_grounds(doc: dict) -> list[str]: | |
| """Detect which constitutional battle grounds a document relates to.""" | |
| text = " ".join( | |
| str(doc.get(k, "")) | |
| for k in ("title", "titolo", "source_title", "urn", "text", "body") | |
| ).lower() | |
| if not text.strip(): | |
| return [] | |
| grounds = [] | |
| for ground, keywords in BATTLE_GROUND_KEYWORDS.items(): | |
| if any(kw in text for kw in keywords): | |
| grounds.append(ground) | |
| return grounds | |
| _HIERARCHY = [ | |
| ("costituzione", 1), | |
| ("legge.costituzionale", 2), | |
| ("trattato", 2), | |
| ("regolamento.ue", 3), ("regolamento.ce", 3), ("regolamento.cee", 3), | |
| ("direttiva.ue", 3), ("direttiva.ce", 3), ("direttiva.cee", 3), | |
| ("legge", 4), | |
| ("decreto.legge", 4), | |
| ("decreto.legislativo", 4), | |
| ("decreto.del.presidente.della.repubblica", 5), | |
| ("decreto.del.presidente.del.consiglio", 5), | |
| ("decreto.ministeriale", 6), | |
| ("regio.decreto", 4), | |
| ("regolamento", 7), | |
| ("circolare", 8), | |
| ] | |
| def _infer_hierarchy_level(urn: str, title: str = "") -> int: | |
| """Return numeric hierarchy level (1=highest) from URN or title.""" | |
| combined = f"{urn} {title}".lower() | |
| for pattern, level in _HIERARCHY: | |
| if pattern in combined or pattern.replace(".", " ") in combined: | |
| return level | |
| return 9 | |
| def normalize_metadata_entry(doc: dict) -> dict: | |
| """Normalize a single metadata entry in-place and return it. | |
| Adds canonical fields without removing originals so downstream | |
| code that references old field names still works. | |
| """ | |
| # Canonical URN | |
| doc["urn"] = _resolve_first(doc, _URN_ALIASES) or doc.get("urn", "") | |
| # Canonical title | |
| doc["title"] = _resolve_first(doc, _TITLE_ALIASES) or doc.get("title", "") | |
| # Canonical date | |
| doc["pub_date"] = _resolve_first(doc, _DATE_ALIASES) or doc.get("pub_date", "") | |
| # Validity status | |
| doc["validity_status"] = _normalize_validity(doc) | |
| # Year and era flags | |
| year = _extract_year(doc["pub_date"]) | |
| doc["enactment_year"] = year | |
| doc["is_pre_republican"] = bool(year and year < _PRE_REPUBLICAN_YEAR) | |
| # Hierarchy level | |
| doc["hierarchy_level"] = _infer_hierarchy_level(doc["urn"], doc["title"]) | |
| # Battle grounds | |
| doc["battle_grounds"] = _detect_battle_grounds(doc) | |
| return doc | |
| def normalize_all_metadata(metadata: list[dict]) -> list[dict]: | |
| """Normalize entire metadata list in-place. Called once at FAISS load time.""" | |
| count = 0 | |
| for doc in metadata: | |
| normalize_metadata_entry(doc) | |
| count += 1 | |
| log.info("Normalized %d metadata entries", count) | |
| return metadata | |