Faire tourner un agent Pydantic AI derrière PIIGhost¶
Vous voulez un agent Pydantic AI où le modèle ne voit jamais que des jetons, jamais les vrais noms de la conversation, et où une valeur garde le même jeton d'un tour à l'autre. Cette page assemble cet agent de bout en bout avec un détecteur GLiNER2, un ThreadAnonymizationPipeline, et pii_hooks, la capability qui dé-identifie autour du modèle.
La capability couvre les messages, le prompt utilisateur et les réponses du modèle lui-même, et la frontière des outils aussi. Sous la stratégie par défaut, un outil reçoit les vraies valeurs pendant que le modèle continue de travailler sur des jetons.
Prérequis
piighost installé avec les extras pydantic-ai et gliner2, pip install piighost[pydantic-ai,gliner2], plus une clé OpenAI dans OPENAI_API_KEY. La première exécution télécharge les poids de GLiNER2, environ 500 Mo.
1. Construire le pipeline sur un détecteur GLiNER2¶
Gliner2Detector enrobe un modèle GLiNER2. Passez l'identifiant du modèle sous forme de chaîne et il se charge à la construction. Passez labels pour lui indiquer quels types d'entités interroger. Seul le détecteur est requis, car le pipeline de thread fournit par défaut son linker, son anonymiseur, et un stockage de conversation en mémoire. L'anonymiseur par défaut émet le jeton délimité <<PERSON:1>> que pii_hooks sait retrouver.
from piighost.components.detector.ner import Gliner2Detector
from piighost.pipeline import ThreadAnonymizationPipeline
detector = Gliner2Detector(
"fastino/gliner2-multi-v1",
labels=["PERSON", "LOCATION"],
threshold=0.5,
)
pipeline = ThreadAnonymizationPipeline(detector)
2. Attacher la capability à l'agent¶
pii_hooks prend le pipeline et un identifiant de thread, puis renvoie une capability Pydantic AI. Enregistrez-la avec capabilities=[...]. L'identifiant de thread cadre les jetons, une valeur garde donc un seul jeton pour toute la conversation. C'est une chaîne fixe ici. Passez un appelable sur le contexte d'exécution, par exemple lambda ctx: ctx.deps.thread_id, pour le lire à chaque exécution.
from pydantic_ai import Agent
from piighost.integrations.pydantic_ai import pii_hooks
hooks = pii_hooks(pipeline, "thread-42")
agent = Agent("openai:gpt-5.6-terra", capabilities=[hooks])
3. Lancer un tour¶
La capability dé-identifie le prompt avant que le modèle ne le lise et restaure la réponse pour l'affichage, le modèle travaille donc sur <<PERSON:1>> pendant que vous lisez Patrick.
import asyncio
async def main() -> None:
result = await agent.run("Where does Patrick live?")
print(result.output)
asyncio.run(main())
Qui voit quoi¶
GLiNER2 repère Patrick comme PERSON dans le message entrant. À partir de là, la capability substitue un sens de chaque côté de l'appel au modèle :
before_model_requestfait passer chaque texte utilisateur et assistant parpipeline.anonymize, le modèle reçoit doncWhere does <<PERSON:1>> live?. Elle réécrit aussi les textes de l'assistant, une valeur restaurée pour l'affichage à un tour précédent est donc dé-identifiée à nouveau avant l'appel suivant et ne refuit jamais dans l'historique.after_model_requestfait passer la réponse parpipeline.deanonymize, vous lisez donc la vraie valeur.
Le thread_id garde <<PERSON:1>> lié à Patrick à chaque tour.
Les jetons que le modèle invente¶
Après la restauration, chaque jeton émis est revenu à sa valeur, un jeton qui suit encore la grammaire a donc été inventé par le modèle, par hallucination ou par injection de prompt. pii_hooks prend un invented_strategy qui décide de ce qui se passe alors. RAISE le refuse, le défaut fail-closed. KEEP le laisse. DROP le retire.
from piighost.integrations.langchain import InventedPlaceholderStrategy
hooks = pii_hooks(
pipeline,
"thread-42",
invented_strategy=InventedPlaceholderStrategy.DROP,
)
Appels d'outils¶
pii_hooks traite aussi la frontière des outils, pilotée par tool_strategy, le même enum que le middleware LangChain. Sous FULL, le défaut, les arguments d'un appel d'outil sont restaurés avant l'exécution, un outil qui a besoin de Patrick le reçoit et non <<PERSON:1>>, et le résultat texte de l'outil est ré-dé-identifié avant que le modèle ne le lise, le modèle continue donc de voir des jetons. INPUT ne restaure que les arguments, OUTPUT ne dé-identifie à nouveau que le résultat, et PASSTHROUGH ne touche à rien.
from piighost.integrations.langchain import ToolCallStrategy
hooks = pii_hooks(pipeline, "thread-42", tool_strategy=ToolCallStrategy.FULL)
Valeurs de l'assistant¶
Toute valeur n'est pas une PII utilisateur. Quand le modèle introduit lui-même une valeur tirée de sa connaissance du monde, la tokeniser la lui cacherait au tour suivant sans rien protéger côté utilisateur. assistant_strategy décide du sort d'une valeur introduite par l'assistant, encore le même enum que le middleware. Sous PRESERVE, le défaut, elle reste en clair, le modèle garde donc sa propre connaissance et seule une PII utilisateur connue est tokenisée. ANONYMIZE la tokenise quand même, et IGNORE saute entièrement les messages de l'assistant, économisant le détecteur.
from piighost.integrations.langchain import EntityCreateByAssistantStrategy
hooks = pii_hooks(
pipeline,
"thread-42",
assistant_strategy=EntityCreateByAssistantStrategy.ANONYMIZE,
)
Et ensuite¶
- Pour comparer avec le middleware d'agent LangChain, voyez l'intégration LangChain.
- Pour remplacer GLiNER2 par spaCy, un pack de regex, ou votre propre détecteur, voyez Étendre PIIGhost.
- Les scripts exécutables sont dans
examples/pydantic_ai/base.py(messages) etexamples/pydantic_ai/tools.py(un outil).