Aller au contenu

Référence Détecteurs

Module : piighost.components.detector

Un détecteur est l'étage de détection d'un pipeline. Il lit un texte et renvoie les PII qu'il y trouve. Tout détecteur satisfait le port AnyDetector et renvoie une liste de Detection, quel que soit le backend qu'il enveloppe.

from piighost.components.detector import (
    ChunkedDetector,
    CompositeDetector,
    ExactMatchDetector,
    LLMDetector,
    RegexDetector,
)
from piighost.components.detector.ner import (
    Gliner2Detector,
    Gliner2PiiDetector,
    PresidioDetector,
    SpacyDetector,
    TransformersDetector,
)

Chaque détecteur NER a besoin de son propre extra (gliner2, spacy, transformers, presidio). LLMDetector a besoin de l'extra llm et d'un paquet fournisseur.


AnyDetector (protocole)

Le port que tout détecteur implémente. Une seule méthode asynchrone, donc une implémentation peut attendre une I/O comme un serveur de modèle ou une API LLM sans bloquer le pipeline.

class AnyDetector(Protocol):
    async def detect(self, text: str) -> list[Detection]: ...

detect renvoie les détections dans un ordre quelconque. Les chevauchements et les doublons sont résolus par les étages suivants du pipeline, pas par le détecteur.

Detection

Chaque détecteur renvoie une liste de Detection, un dataclass gelé qui porte l'emplacement de la correspondance, le texte trouvé, son label et sa confiance.

Attribut Type Description
span Span L'emplacement de la détection, en intervalle semi-ouvert
text str La sous-chaîne trouvée
label str La catégorie de PII, par exemple PERSON ou EMAIL
confidence float La confiance du détecteur, dans l'intervalle fermé 0 à 1

RegexDetector

Trouve les PII en appliquant un pattern regex par label. Chaque pattern est compilé une fois à la construction, sous re.ASCII, donc \d et les autres classes de forme ne correspondent qu'à l'ASCII. Un caractère Unicode ressemblant à un chiffre, comme un chiffre arabo-indien, ne correspond pas, car un format de PII utilise des chiffres ASCII. detect émet une détection par correspondance sans chevauchement, à une confiance fixe de 1.0.

Il ne porte aucun validateur de somme de contrôle, donc il correspond sur la forme seule. Une valeur structurée abîmée par un OCR est conservée plutôt que rejetée, car rejeter une vraie valeur reviendrait à la laisser fuiter.

Constructeur

RegexDetector(patterns: dict[str, str])
Paramètre Type Description
patterns dict[str, str] Correspondance d'un label de PII vers le pattern regex à appliquer (requis)
from piighost.components.detector import RegexDetector

detector = RegexDetector({"EMAIL": r"[\w.+-]+@[\w.-]+\.\w{2,}"})
detections = await detector.detect("write to alice@example.com")
# [Detection(span=Span(9, 26), text="alice@example.com", label="EMAIL", confidence=1.0)]

from_hub

RegexDetector.from_hub(ref: str, *, hub: str | None = None) -> RegexDetector

Construit un détecteur à partir des regex que porte une référence du hub piighost. Le hub est un registre de regex de dé-identification testées, adressées par namespace/name et un sélecteur optionnel : un tag, ou les huit caractères hexadécimaux d'un commit.

Paramètre Type Description
ref str Une référence, namespace/name avec un :selector optionnel et un préfixe hub: optionnel. Sans sélecteur, elle résout vers latest (requis)
hub str \| None Origine du hub à interroger. Par défaut PIIGHOST_HUB_URL, puis le hub public
from piighost.components.detector import RegexDetector

detector = RegexDetector.from_hub("piighost/logs:fd79aec6")
detections = await detector.detect("mail me at a@b.co from 10.0.0.1")

Une référence épinglée sur un commit est immuable : la réponse est mise en cache sous ~/.cache/piighost/hub et relue depuis le disque aux appels suivants. Une référence pointant vers un tag ou vers latest bouge, elle est donc récupérée à chaque fois : servir une version périmée détecterait silencieusement moins que ce que l'appelant a demandé.

L'appel lève une sous-classe de HubError (piighost.hub) si la référence ne se parse pas, si le hub est injoignable, ou si la référence résout vers autre chose qu'un détecteur regex simple. Ce dernier cas couvre une référence portant un détecteur modèle : n'en prendre que les regex détecterait moins que ce que la référence promet, donc l'appel échoue plutôt que d'en rendre la moitié.

Il n'utilise que la bibliothèque standard, donc l'installation de base n'a besoin d'aucun extra.


CompositeDetector

Fait tourner plusieurs détecteurs sur le même texte et fusionne leurs détections. Il est lui-même un AnyDetector, donc il se compose avec le pipeline sans changement. Il exécute chaque enfant en parallèle et concatène leurs résultats dans l'ordre des enfants. Il ne déduplique pas. Chevauchements et doublons passent à l'étage de résolution de spans.

Constructeur

CompositeDetector(detectors: list[AnyDetector])
Paramètre Type Description
detectors list[AnyDetector] Les détecteurs enfants à exécuter, dans l'ordre (requis)
from piighost.components.detector import CompositeDetector, RegexDetector
from piighost.components.detector.ner import Gliner2Detector

email_detector = RegexDetector({"EMAIL": r"[\w.+-]+@[\w.-]+\.\w{2,}"})
person_detector = Gliner2Detector(model="fastino/gliner2-multi-v1", labels=["PERSON"])
detector = CompositeDetector([email_detector, person_detector])

ExactMatchDetector

Trouve les occurrences en mot entier de valeurs littérales configurées. Il parcourt le texte pour chaque valeur et émet une détection par occurrence à une confiance de 1.0. La correspondance se fait sur des frontières de mot, donc une valeur ne se déclenche pas à l'intérieur d'un mot plus long (Ann ne correspond pas dans Anne), et elle est insensible à la casse par défaut, donc une valeur correspond quelle que soit sa casse tandis que la détection garde le texte tel qu'il apparaît. Il ne porte aucun modèle et aucune dépendance optionnelle, ce qui en fait le détecteur de choix pour exercer le pipeline dans les tests.

Constructeur

ExactMatchDetector(values: dict[str, str], case_sensitive: bool = False)
Paramètre Type Description
values dict[str, str] Correspondance d'une valeur littérale vers le label de PII à émettre pour elle (requis)
case_sensitive bool Si la correspondance respecte la casse. False par défaut
from piighost.components.detector import ExactMatchDetector

detector = ExactMatchDetector({"Patrick": "PERSON", "Lyon": "LOCATION"})
detections = await detector.detect("Patrick lives in Lyon")

ChunkedDetector

Fait tourner un détecteur enveloppé sur chaque morceau d'un texte long. C'est un décorateur, lui-même un AnyDetector. Il découpe le texte en morceaux qui se chevauchent, exécute le détecteur enveloppé sur chacun, et reprojette chaque détection sur le texte original. Les détections strictement identiques produites par le chevauchement sont supprimées. Conflits de label et confiances différentes passent à l'étage de résolution de spans.

Constructeur

ChunkedDetector(detector: AnyDetector, splitter: AnySplitter | None = None)
Paramètre Type Description
detector AnyDetector Le détecteur exécuté sur chaque morceau (requis)
splitter AnySplitter \| None Le splitter, ou None pour un RecursiveCharacterTextSplitter par défaut
from piighost.components.detector import ChunkedDetector
from piighost.components.detector.ner import SpacyDetector

spacy_detector = SpacyDetector(model="en_core_web_sm")
detector = ChunkedDetector(spacy_detector)

LLMDetector

Détecte les PII avec un modèle de chat LangChain via une sortie structurée. A besoin de l'extra llm et d'un paquet fournisseur. On demande au modèle d'extraire des paires (text, label) contre un schéma dont le champ label est contraint aux labels configurés. Chaque valeur extraite est ensuite localisée dans le texte source par recherche sur frontière de mot, donc une valeur inventée par le modèle mais absente du texte ne donne rien. labels est requis, puisque le schéma en est construit. Le texte source est enveloppé dans des balises <text_to_analyze> et le prompt système ordonne au modèle de traiter le contenu balisé comme des données, jamais comme des instructions, donc une tentative d'injection de prompt dans le texte ne peut pas orienter l'extraction.

Constructeur

LLMDetector(
    model: BaseChatModel | str,
    labels: list[str] | dict[str, str],
    prompt: str | None = None,
    provider: str | None = None,
    confidence: float = 1.0,
)
Paramètre Type Description
model BaseChatModel \| str Un modèle de chat chargé, ou un nom chargé avec init_chat_model (requis)
labels list[str] \| dict[str, str] Les labels à extraire, liste ou map {emitted: internal} (requis)
prompt str \| None Un prompt système personnalisé, ou None pour celui par défaut
provider str \| None Le fournisseur passé à init_chat_model quand model est un nom
confidence float Confiance portée sur chaque détection, 1.0 par défaut, pour qu'un détecteur LLM puisse être départagé face à un détecteur NER à la résolution des chevauchements

Un prompt personnalisé doit contenir un placeholder {labels} et, selon le format f-string de LangChain, doubler toute autre accolade littérale en {{ ou }}.

from piighost.components.detector import LLMDetector

detector = LLMDetector(
    model="gpt-5.6-terra",
    labels=["PERSON", "EMAIL"],
    provider="openai",
)

Détecteurs NER

Les détecteurs adossés à un modèle étendent BaseNERDetector, qui gère la correspondance et le filtrage des labels (voir plus bas). Chacun a besoin de son propre extra et prend un modèle chargé ou un nom de modèle à charger, sauf PresidioDetector, qui prend un AnalyzerEngine construit.

Gliner2Detector

Un modèle GLiNER2 zero-shot. A besoin de l'extra gliner2. labels est requis, car GLiNER2 est interrogé avec les labels internes. Un model en str est chargé avec GLiNER2.from_pretrained.

Gliner2Detector(
    model: GLiNER2 | str,
    labels: list[str] | dict[str, str],
    threshold: float = 0.5,
    max_concurrency: int | None = None,
    max_chars: int | None = None,
    auto_chunk: bool = True,
)
Paramètre Type Description
model GLiNER2 \| str Un modèle chargé, ou un nom chargé avec from_pretrained (requis)
labels list[str] \| dict[str, str] Les labels à interroger, liste ou map {emitted: internal} (requis)
threshold float La confiance à partir de laquelle une entité est conservée
max_concurrency int \| None Plafond d'inférences concurrentes, ou None pour sans limite
max_chars int \| None Limite en caractères vue par une seule inférence, ou None pour aucune limite
auto_chunk bool Si un texte plus long que max_chars est découpé et reprojeté, sinon lève TextTooLongError

Gliner2PiiDetector

Un Gliner2Detector prêt à l'emploi sur le modèle GLiNER2 de fastino affiné pour les PII, avec une map de labels préréglée, donc ni identifiant de modèle ni argument labels n'est requis. Le préréglage couvre la taxonomie du modèle, des noms et coordonnées aux identifiants, données de paiement, identité numérique, secrets et dates sensibles. Passez labels pour restreindre ou étendre l'ensemble, ou model pour injecter une instance chargée, par exemple dans un test, afin qu'aucun poids ne soit téléchargé.

Gliner2PiiDetector(
    model: GLiNER2 | str | None = None,
    labels: list[str] | dict[str, str] | None = None,
    threshold: float = 0.5,
    max_concurrency: int | None = None,
    max_chars: int | None = None,
    auto_chunk: bool = True,
)
Paramètre Type Description
model GLiNER2 \| str \| None Un modèle chargé ou un nom, ou None pour le modèle PII préréglé
labels list[str] \| dict[str, str] \| None Les labels à interroger, ou None pour la map de labels PII préréglée
threshold float La confiance à partir de laquelle une entité est conservée
max_concurrency int \| None Plafond d'inférences concurrentes, ou None pour sans limite
max_chars int \| None Limite en caractères vue par une seule inférence, ou None pour aucune limite
auto_chunk bool Si un texte plus long que max_chars est découpé et reprojeté, sinon lève TextTooLongError

SpacyDetector

Un modèle NER spaCy. A besoin de l'extra spacy. labels est optionnel. Omis, chaque entité produite par spaCy est conservée avec son label spaCy. Un model en str est chargé avec spacy.load.

SpacyDetector(
    model: Language | str,
    labels: list[str] | dict[str, str] | None = None,
    max_concurrency: int | None = None,
)
Paramètre Type Description
model Language \| str Un modèle chargé, ou un nom chargé avec spacy.load (requis)
labels list[str] \| dict[str, str] \| None Les labels à mapper et filtrer, ou None pour garder chaque label natif
max_concurrency int \| None Plafond d'inférences concurrentes, ou None pour sans limite

TransformersDetector

Un pipeline de classification de tokens Hugging Face. A besoin de l'extra transformers. labels est optionnel, gardé natif s'il est omis. Un pipeline en str est chargé comme un pipeline ner. Une entité qui score sous threshold est rejetée.

TransformersDetector(
    pipeline: TokenClassificationPipeline | str,
    labels: list[str] | dict[str, str] | None = None,
    threshold: float = 0.0,
    max_concurrency: int | None = None,
    aggregation_strategy: str = "simple",
    max_chars: int | None = None,
    auto_chunk: bool = True,
)
Paramètre Type Description
pipeline TokenClassificationPipeline \| str Un pipeline construit, ou un nom de modèle chargé comme pipeline ner (requis)
labels list[str] \| dict[str, str] \| None Les labels à mapper et filtrer, ou None pour garder chaque label natif
threshold float Le score sous lequel une entité détectée est rejetée
max_concurrency int \| None Plafond d'inférences concurrentes, ou None pour sans limite
aggregation_strategy str Comment les sous-tokens sont regroupés en entités entières, appliqué seulement à la construction depuis un nom de modèle. Un pipeline injecté garde la sienne. "simple" par défaut
max_chars int \| None Limite en caractères vue par une seule inférence, ou None pour aucune limite
auto_chunk bool Si un texte plus long que max_chars est découpé et reprojeté, sinon lève TextTooLongError

PresidioDetector

Enveloppe un AnalyzerEngine de Presidio pour réutiliser ses recognizers. A besoin de l'extra presidio. L'analyzer est injecté, car un moteur est assemblé d'un moteur NLP et d'un registre de recognizers, pas chargé depuis un nom. labels est optionnel, gardé natif quand il est omis. Une entité scorant sous threshold est écartée par Presidio.

PresidioDetector(
    analyzer: AnalyzerEngine,
    labels: list[str] | dict[str, str] | None = None,
    language: str = "en",
    threshold: float = 0.0,
    max_concurrency: int | None = None,
)
Paramètre Type Description
analyzer AnalyzerEngine Un analyzer Presidio construit (requis)
labels list[str] \| dict[str, str] \| None Les labels à mapper et filtrer, ou None pour garder chaque type natif
language str Le code de langue passé à analyze
threshold float Le score sous lequel une entité est écartée
max_concurrency int \| None Plafond d'inférences concurrentes, ou None pour sans limite

Depuis une config, le type de détecteur presidio construit l'AnalyzerEngine anglais par défaut de Presidio. Pour une autre langue ou des recognizers custom, construisez le moteur vous-même et utilisez PresidioDetector directement.

Gestion des textes longs

Gliner2Detector et TransformersDetector prennent max_chars avec auto_chunk (défaut True). Un texte plus long que max_chars est découpé en morceaux qui se chevauchent, scannés séparément, puis reprojetés sur le texte original. Avec auto_chunk désactivé, un texte au-delà de la limite lève TextTooLongError à la place. max_chars vaut None par défaut, donc il n'y a pas de limite et le texte entier est scanné en une passe. SpacyDetector et PresidioDetector ne les exposent pas.

Correspondance des labels

BaseNERDetector normalise l'argument labels en une map externe vers interne, puis mappe et filtre les détections produites par le modèle. Il distingue le label qu'un modèle utilise nativement du label émis dans Detection.label.

  • Une liste, ["PERSON", "LOCATION"], mappe chaque label vers lui-même.
  • Une map, {"PERSON": "PER"}, prend le label émis comme clé et le label natif du modèle comme valeur, donc une détection que le modèle étiquette PER est émise en PERSON. Un label natif absent des valeurs de la map est rejeté.
  • None ou une map vide n'applique aucune correspondance, donc chaque détection est gardée avec le label donné par le modèle.

Deux labels externes mappant vers un même label interne lèvent LabelMappingError, car la recherche inverse serait ambiguë.

from piighost.components.detector.ner import TransformersDetector

detector = TransformersDetector(
    pipeline="dslim/bert-base-NER",
    labels={"PERSON": "PER", "LOCATION": "LOC"},
)

Catalogues de patterns

Ensembles de patterns regex réutilisables pour RegexDetector. Chaque catalogue est un dict[str, str] simple qui associe un label de PII à un pattern regex. Les patterns correspondent sur la forme seule, sans validation de somme de contrôle.

from piighost.components.detector.patterns import (
    EU_PATTERNS,
    FR_PATTERNS,
    GENERIC_PATTERNS,
    US_PATTERNS,
)

Passez un catalogue à un RegexDetector, ou fusionnez-en plusieurs par fusion de dict, un pattern en ligne sur le même label prenant le dessus.

from piighost.components.detector import RegexDetector
from piighost.components.detector.patterns import FR_PATTERNS, GENERIC_PATTERNS

detector = RegexDetector({**GENERIC_PATTERNS, **FR_PATTERNS})
Catalogue Import Labels
Générique GENERIC_PATTERNS EMAIL, URL, IPV4, CREDIT_CARD
US US_PATTERNS US_SSN, US_PHONE, US_ZIP
EU EU_PATTERNS IBAN
France FR_PATTERNS FR_PHONE, FR_IBAN, FR_NIR, FR_SIRET

Chaque pattern de catalogue est testé contre le backtracking catastrophique, de sorte qu'une entrée adverse ne peut pas transformer un scan en déni de service.

Les labels de GENERIC_PATTERNS ne dépendent d'aucun pays. Les autres sont préfixés (US_, FR_) pour ne pas se confondre quand les catalogues sont fusionnés. EU_PATTERNS porte l'IBAN ISO 13616 partagé entre les États membres. Pour des numéros propres à un pays, utilisez un catalogue par pays.

Tirer les catalogues depuis une config

Une config de détecteur regex tire les catalogues via catalogs. Une entrée est soit un nom prédéfini, parmi generic, us, eu, fr, soit une référence de hub écrite hub:namespace/name avec un :selector optionnel. Les catalogues fusionnent dans l'ordre, puis les patterns en ligne, donc un pattern en ligne l'emporte sur un pattern de catalogue sur le même label. Une config de détecteur regex a besoin d'au moins un pattern en ligne ou un catalogue.

[detector]
type = "regex"
catalogs = ["generic", "fr"]

[detector.patterns]
INTERNAL_ID = "EMP-\\d{6}"

Une référence de hub nomme un catalogue relu au lieu d'en porter une copie : la config reste courte et les patterns restent auditables à leur source :

[detector]
type = "regex"
catalogs = ["hub:piighost/logs:fd79aec6"]

Un catalogue de hub est récupéré à la construction de la config, pas à sa lecture, et une référence épinglée sur un commit est ensuite mise en cache sur disque. Définissez PIIGHOST_HUB_URL pour interroger un registre privé. Un nom inconnu ou une référence malformée échoue au chargement plutôt que sous forme d'URL invalide plus tard.


Voir aussi