Architecture¶
PIIGhost est organise en couches distinctes : un anonymiseur stateless au coeur, encapsule dans un pipeline avec cache et resolution d'entites, etendu par un pipeline conversationnel avec memoire, adapte au monde LangChain via un middleware.
Vue d'ensemble¶
---
title: "architecture en couches de piighost"
---
flowchart TB
classDef hook fill:#BBDEFB,stroke:#1565C0,color:#000
classDef layer fill:#90CAF9,stroke:#1565C0,color:#000
classDef core fill:#A5D6A7,stroke:#2E7D32,color:#000
classDef protocol fill:#FFF9C4,stroke:#F9A825,color:#000
classDef ext fill:#E1BEE7,stroke:#6A1B9A,color:#000
subgraph MW ["PIIAnonymizationMiddleware : couche LangChain"]
direction LR
HBEF["abefore_model"]:::hook
HAFT["aafter_model"]:::hook
HTOOL["awrap_tool_call"]:::hook
end
subgraph THREAD ["ThreadAnonymizationPipeline : mémoire & ops string"]
direction LR
MEM["ConversationMemory"]:::layer
DEANO_ENT["deanonymize_with_ent"]:::layer
ANON_ENT["anonymize_with_ent"]:::layer
end
subgraph PIPE ["AnonymizationPipeline : cache & orchestration"]
direction LR
DETECT_API["detect_entities"]:::core
ANON_API["anonymize"]:::core
DEANON_API["deanonymize"]:::core
end
subgraph PROTO ["Protocoles composants : pipeline 5 étapes"]
direction LR
P_DETECT["AnyDetector"]:::protocol
P_SPANS["AnySpanConflictResolver"]:::protocol
P_LINK["AnyEntityLinker"]:::protocol
P_ENT["AnyEntityConflictResolver"]:::protocol
P_ANON["AnyAnonymizer"]:::protocol
P_DETECT --> P_SPANS --> P_LINK --> P_ENT --> P_ANON
end
CACHE[("aiocache")]:::ext
LLM(["Fournisseur LLM"]):::ext
TOOLS(["Outils de l'agent"]):::ext
HBEF --> MEM
HAFT --> DEANO_ENT
HTOOL --> ANON_ENT
HTOOL --> DEANO_ENT
MEM --> ANON_API
DEANO_ENT --> DEANON_API
ANON_ENT --> ANON_API
ANON_API --> P_DETECT
DETECT_API --> P_DETECT
ANON_API <--> CACHE
DEANON_API <--> CACHE
MW <--> LLM
MW <--> TOOLS
Architecture en couches : du protocole au middleware LangChain.
Pipeline 5 etapes¶
Tout est remplaçable
Chaque étape se trouve derrière un protocole. Voir Étendre PIIGhost pour brancher votre propre détecteur, linker, résolveur ou factory.
Le coeur de PIIGhost est AnonymizationPipeline qui orchestre 5 etapes, chacune implementee par un protocole swappable.
---
title: "piighost AnonymizationPipeline.anonymize() flow"
---
flowchart LR
classDef stage fill:#90CAF9,stroke:#1565C0,color:#000
classDef protocol fill:#FFF9C4,stroke:#F9A825,color:#000
classDef data fill:#A5D6A7,stroke:#2E7D32,color:#000
INPUT(["`**Texte source**
_'Patrick habite a Paris.
Patrick aime Paris.'_`"]):::data
DETECT["`**1. Detect**
_AnyDetector_`"]:::stage
RESOLVE_SPANS["`**2. Resolve Spans**
_AnySpanConflictResolver_`"]:::stage
LINK["`**3. Link Entities**
_AnyEntityLinker_`"]:::stage
RESOLVE_ENTITIES["`**4. Resolve Entities**
_AnyEntityConflictResolver_`"]:::stage
ANONYMIZE["`**5. Anonymize**
_AnyAnonymizer_`"]:::stage
OUTPUT(["`**Sortie**
_'<<PERSON:1>> habite a <<LOCATION:1>>.
<<PERSON:1>> aime <<LOCATION:1>>.'_`"]):::data
INPUT --> DETECT
DETECT -- "list[Detection]" --> RESOLVE_SPANS
RESOLVE_SPANS -- "dedupliquees" --> LINK
LINK -- "list[Entity]" --> RESOLVE_ENTITIES
RESOLVE_ENTITIES -- "fusionnees" --> ANONYMIZE
ANONYMIZE --> OUTPUT
P_DETECT["`GlinerDetector
_(ou RegexDetector, ExactMatchDetector, CompositeDetector…)_`"]:::protocol
P_RESOLVE_SPANS["`ConfidenceSpanConflictResolver
_(plus haute confiance gagne)_`"]:::protocol
P_LINK["`ExactEntityLinker
_(regex word-boundary)_`"]:::protocol
P_RESOLVE_ENTITIES["`MergeEntityConflictResolver
_(fusion union-find)_`"]:::protocol
P_ANONYMIZE["`Anonymizer + LabelCounterPlaceholderFactory
_(tags <<LABEL:N>>)_`"]:::protocol
P_DETECT -. "implemente" .-> DETECT
P_RESOLVE_SPANS -. "implemente" .-> RESOLVE_SPANS
P_LINK -. "implemente" .-> LINK
P_RESOLVE_ENTITIES -. "implemente" .-> RESOLVE_ENTITIES
P_ANONYMIZE -. "implemente" .-> ANONYMIZE
Etape 1 Detect¶
AnyDetector execute la detection async sur le texte source et retourne une liste d'objets Detection (text, label, position, confidence).
Les implementations fournies incluent ExactMatchDetector (regex word-boundary), RegexDetector (patterns), Gliner2Detector (NER), et CompositeDetector (chaine plusieurs detecteurs).
Exemple :
Texte : "Patrick habite a Paris."
Détections :
- PERSON "Patrick" [0:7] confidence=0.95
- LOCATION "Paris" [17:22] confidence=0.92
A ce stade, on a une liste brute de detections. Pas encore d'anonymisation ni de gestion de doublons : juste « voici ce qui ressemble a des PII et ou elles sont ».
Etape 2 Resolve Spans¶
Le probleme. Quand on chaine plusieurs detecteurs sur le meme texte, ils peuvent revendiquer le meme morceau avec des labels differents. Sans arbitrage, le remplacement final tape deux fois sur la meme position et casse le texte.
AnySpanConflictResolver gere les detections qui se chevauchent (totalement ou partiellement) en gardant celle avec la plus haute confiance.
Exemple :
Texte : "Patrick travaille chez Orange depuis 2015."
Détections en entrée :
- PERSON "Patrick" [0:7] confidence=0.95 (NER A)
- ORG "Patrick" [0:7] confidence=0.60 (NER B, confond avec un nom d'entreprise)
Après ConfidenceSpanConflictResolver :
- PERSON "Patrick" [0:7] confidence=0.95
Etape 3 Link Entities¶
Le probleme. Le NER rate des occurrences. Il trouve Patrick Dupont dans la phrase 1, mais rate Patrick tout seul dans la phrase 3. Si on s'arrete a la detection brute, Patrick reste en clair dans le texte anonymise.
AnyEntityLinker etend et groupe les detections en objets Entity. ExactEntityLinker cherche toutes les occurrences de chaque texte detecte par recherche word-boundary, puis les groupe par texte normalise.
Exemple :
Texte : "Patrick Dupont habite à Paris. Patrick adore Paris."
Détections brutes du NER :
- PERSON "Patrick Dupont" (phrase 1)
- LOCATION "Paris" (phrase 1)
# "Patrick" et "Paris" de la phrase 2 ont été ratés par le NER
Après ExactEntityLinker :
- Entity(label=PERSON, detections=["Patrick Dupont", "Patrick"])
- Entity(label=LOCATION, detections=["Paris", "Paris"])
Le matching est strict sur la chaine. Pour rattraper les variantes de casse ou les fautes (patrick, Patriick), il faut un linker fuzzy custom (voir Etendre PIIGhost).
Etape 4 Resolve Entities¶
Le probleme. Apres le linker, deux entites distinctes peuvent referer a la meme personne (le NER detecte Patrick Dupont, un dictionnaire metier detecte Patrick tout seul). Sans fusion, Patrick Dupont devient <<PERSON:1>> et Patrick devient <<PERSON:2>>, et le LLM pense qu'il s'agit de deux personnes differentes.
AnyEntityConflictResolver fusionne ces entites. MergeEntityConflictResolver utilise un algorithme union-find pour fusionner les entites partageant des detections communes (matching strict). FuzzyEntityConflictResolver fusionne les entites avec un texte canonique similaire via similarite Jaro-Winkler (plus tolerant, faux positifs plus eleves).
Exemple :
Avant fusion :
- Entity(label=PERSON, detections=["Patrick Dupont"])
- Entity(label=PERSON, detections=["Patrick"])
# Les deux entités partagent une détection sur la chaîne "Patrick"
Après MergeEntityConflictResolver :
- Entity(label=PERSON, detections=["Patrick Dupont", "Patrick"])
Etape 5 Anonymize¶
AnyAnonymizer utilise un AnyPlaceholderFactory pour generer un placeholder unique par entite, puis remplace les spans dans le texte de droite a gauche (pour ne pas decaler les positions des spans suivants).
Exemple :
Entrée : "Patrick Dupont habite à Paris. Patrick adore Paris."
Après Anonymizer + LabelCounterPlaceholderFactory :
"<<PERSON:1>> habite à <<LOCATION:1>>. <<PERSON:1>> adore <<LOCATION:1>>."
Le format <<LABEL:N>> par defaut a quatre proprietes utiles : il est unique comme token, le LLM voit immediatement de quel type de PII il s'agit, il n'est pas ambigu dans du texte normal, et il distingue plusieurs personnes entre elles (contrairement a <<PERSON>> tout court). Pour les autres formats disponibles (hash, faker, mask), voir Placeholder Factories.
Recapitulatif des composants¶
| Etape | Protocole | Defaut | Passe-plat | Quand l'utiliser |
|---|---|---|---|---|
| 1. Detect | AnyDetector |
Gliner2Detector |
— (toujours requis) | Detection NER, regex, exact match, ou composite. Voir Detecteurs. |
| 2. Resolve Spans | AnySpanConflictResolver |
ConfidenceSpanConflictResolver |
DisabledSpanConflictResolver |
Garde la detection la plus confiante en cas de chevauchement. Desactiver si vos detections sont deja propres. |
| 3. Link Entities | AnyEntityLinker |
ExactEntityLinker |
DisabledEntityLinker |
Rattrape les occurrences ratees par le detecteur via word-boundary. Desactiver si vous voulez vous limiter aux detections brutes. |
| 4. Resolve Entities | AnyEntityConflictResolver |
MergeEntityConflictResolver |
DisabledEntityConflictResolver |
Fusionne les entites distinctes referant a la meme PII. FuzzyEntityConflictResolver (Jaro-Winkler) tolere les fautes mais augmente le risque de faux positifs. |
| 5. Anonymize | AnyAnonymizer (+ AnyPlaceholderFactory) |
Anonymizer + LabelCounterPlaceholderFactory |
— (toujours requis) | Le choix du PlaceholderFactory pilote le format de sortie. Voir Placeholder Factories. |
Chaque variante Disabled* est un passe-plat strict (entree = sortie). Utile en test, ou pour brancher un pipeline minimal qui se contente d'un detecteur deja parfait. Voir Etendre PIIGhost pour brancher votre propre implementation.
Flux middleware LangChain¶
Le PIIAnonymizationMiddleware intercepte le cycle de l'agent a 3 points cles.
---
title: "piighost PIIAnonymizationMiddleware dans la boucle agent"
---
sequenceDiagram
participant U as Utilisateur
participant M as Middleware
participant L as LLM
participant T as Outil
U->>M: "Envoie un email a Patrick a Paris"
M->>M: abefore_model()<br/>NER detect + anonymise
M->>L: "Envoie un email a <<PERSON:1>> a <<LOCATION:1>>"
L->>M: tool_call(send_email, to=<<PERSON:1>>)
M->>M: awrap_tool_call()<br/>desanonymise les args
M->>T: send_email(to="Patrick")
T->>M: "Email envoye a Patrick"
M->>M: awrap_tool_call()<br/>reanonymise le resultat
M->>L: "Email envoye a <<PERSON:1>>"
L->>M: "C'est fait ! Email envoye a <<PERSON:1>>."
M->>M: aafter_model()<br/>desanonymise pour l'utilisateur
M->>U: "C'est fait ! Email envoye a Patrick."
abefore_model¶
Avant chaque appel LLM : execute pipeline.anonymize() sur tous les messages. Detection NER complete sur HumanMessage, reanonymisation sur AIMessage / ToolMessage.
aafter_model¶
Apres chaque reponse LLM : desanonymise tous les messages. Essaie d'abord pipeline.deanonymize() (cache), puis pipeline.deanonymize_with_ent() (entites) en cas de CacheMissError.
awrap_tool_call¶
Enveloppe chaque appel d'outil :
- Desanonymise les arguments
stravant l'execution → l'outil recoit les vraies valeurs - Execute l'outil
- Reanonymise la reponse de l'outil → le LLM ne voit pas de vraies donnees
Couche conversation ThreadAnonymizationPipeline¶
ThreadAnonymizationPipeline étend AnonymizationPipeline avec :
| Mecanisme | Description |
|---|---|
ConversationMemory |
Accumule les entites entre les messages, dedupliquees par (text.lower(), label) |
deanonymize_with_ent() |
Remplacement de chaine : tokens → valeurs originales (plus long d'abord) |
anonymize_with_ent() |
Remplacement de chaine : valeurs originales → tokens (plus long d'abord) |
Cycle de vie d'une PII¶
Du point de vue d'une PII donnée, voici les états qu'elle traverse entre sa détection initiale et son affichage à l'utilisateur final, et les transitions possibles (premier passage, cache hit, désanonymisation).
flowchart TB
classDef state fill:#90CAF9,stroke:#1565C0,color:#000
classDef cache fill:#FFF9C4,stroke:#F9A825,color:#000
classDef terminal fill:#E1BEE7,stroke:#6A1B9A,color:#000
START([Texte brut]):::terminal
DET[Détectée]:::state
VAL[Validée]:::state
LINK[Groupée en Entity]:::state
MERGE[Consolidée]:::state
ANON[Anonymisée]:::state
CACHE[("En cache
_thread_id scope_")]:::cache
REST[Restaurée]:::state
END([Texte restauré]):::terminal
START -->|AnyDetector NER / regex| DET
DET -->|Resolve Spans| VAL
VAL -->|Link Entities| LINK
LINK -->|Resolve Entities| MERGE
MERGE -->|placeholder factory| ANON
ANON -->|store SHA-256 key| CACHE
CACHE -.->|cache hit même thread| ANON
ANON -->|deanonymize| REST
REST --> END
Cycle de vie d'une PII au fil du pipeline et du cache de conversation.
La mémoire (ConversationMemory) partage le mapping d'une entité sur toute la conversation identifiée par un thread_id. Un second message contenant la même PII saute directement à l'état Anonymisée via le cache, sans repasser par le détecteur NER.
Modeles de donnees¶
Tous les modeles sont des dataclasses gelees (immutables, thread-safe) :
| Modele | Champs cles |
|---|---|
Detection |
text, label, position: Span, confidence |
Entity |
detections: tuple[Detection, ...], label (propriete) |
Span |
start_pos, end_pos, overlaps() |
Injection de dependances¶
Chaque etape utilise un protocole (typage structurel Python) comme point d'injection :
AnonymizationPipeline(
detector=GlinerDetector(...), # AnyDetector
span_resolver=ConfidenceSpanConflictResolver(), # AnySpanConflictResolver
entity_linker=ExactEntityLinker(), # AnyEntityLinker
entity_resolver=MergeEntityConflictResolver(), # AnyEntityConflictResolver
anonymizer=Anonymizer(LabelCounterPlaceholderFactory()), # AnyAnonymizer
)
Pour remplacer un composant, il suffit de fournir un objet implementant le protocole correspondant. Voir Etendre PIIGhost.