Intégration Pydantic AI
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. Elle couvre aussi la frontière des outils, c'est-à-dire les appels d'outils et leurs résultats. 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.
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 conversation 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(
model="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 conversation, puis renvoie une capability Pydantic AI. Enregistrez-la avec capabilities=[...]. Chaque identifiant de conversation a ses propres 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_id="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())La réponse est restaurée pour l'affichage. Sa formulation dépend du modèle, par exemple :
I have no address for Patrick.Qui voit quoi
GLiNER2 repère Patrick comme PERSON dans le message entrant. À partir de là, la capability remplace dans un sens avant l'appel au modèle, et dans l'autre sens après :
before_model_requestfait passer chaque texte utilisateur et assistant parpipeline.anonymize. Le modèle reçoit doncWhere does <<PERSON:1>> live?. Ce hook 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 réapparaît jamais en clair 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 texte qui a encore la forme d'un jeton 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. C'est le défaut, qui bloque plutôt que de laisser passer. KEEP le laisse. DROP le retire.
from piighost.integrations.langchain import InventedPlaceholderStrategy
hooks = pii_hooks(
pipeline,
thread_id="thread-42",
invented_strategy=InventedPlaceholderStrategy.DROP,
)Appels d'outils
pii_hooks traite aussi la frontière des outils. tool_strategy la pilote, avec 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 donc, et non <<PERSON:1>>. Le résultat texte de l'outil est dé-identifié à nouveau avant que le modèle ne le lise, et 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_id="thread-42", tool_strategy=ToolCallStrategy.FULL)Valeurs de l'assistant
Toute valeur n'est pas une donnée confidentielle de l'utilisateur. Le modèle introduit parfois lui-même une valeur tirée de sa connaissance du monde. La dé-identifier cacherait cette valeur au modèle au tour suivant, sans rien protéger côté utilisateur. assistant_strategy décide du sort d'une valeur introduite par l'assistant, avec encore le même enum que le middleware. Sous PRESERVE, le défaut, la valeur reste en clair. Le modèle garde donc sa propre connaissance, et seule une valeur utilisateur connue est dé-identifiée. ANONYMIZE la dé-identifie quand même. IGNORE saute entièrement les messages de l'assistant, et le détecteur ne tourne donc pas sur eux.
from piighost.integrations.langchain import EntityCreateByAssistantStrategy
hooks = pii_hooks(
pipeline,
thread_id="thread-42",
assistant_strategy=EntityCreateByAssistantStrategy.ANONYMIZE,
)Voir aussi
- 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).