Aller au contenu

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_request fait passer chaque texte utilisateur et assistant par pipeline.anonymize. Le modèle reçoit donc Where 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_request fait passer la réponse par pipeline.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) et examples/pydantic_ai/tools.py (un outil).