C'est quoi Pydantic Logfire ?

Découvrez comment Logfire vous montre ce que votre agent IA a vraiment fait.

Publié le par Gabriel Trouvé (mis à jour le )

48 minutes

Pour commencer, je vous invite à aller lire notre article C'est quoi un agent IA ?. Dans ce dernier, nous avons vu qu'un agent décide lui-même du chemin à suivre : les outils, les arguments, quand s'arrêter, etc., ce qui peut devenir compliqué à déboguer.

Dans cet article, nous allons nous attaquer à Logfire, un outil qui permet de voir étape par étape ce que fait votre agent.

Pourquoi un agent IA est-il difficile à déboguer ?

De manière classique, dans un programme, une entrée suit le même chemin dans le code. Avec un agent, ce n'est plus le cas, car, pour une question identique, le modèle peut appeler différents outils.

Voici deux termes que vous croiserez souvent :

  • span : une unité de travail avec un début et une durée, par exemple un appel au modèle ou l'exécution d'un outil

  • trace : l'ensemble des spans d'une exécution

C'est ce que Logfire va suivre pour nous.

Qu'est-ce que Pydantic Logfire ?

Logfire est une plateforme développée par l'équipe de Pydantic et PydanticAI. Vous connaissez peut-être OpenTelemetry, technologie sur laquelle s'appuie Logfire. Il s'agit d'un standard ouvert qui permet de collecter des traces, logs et métriques.

Logfire est composée de deux parties :

  • un SDK que l'on installe dans son projet Python

  • une plateforme web pour consulter les données

Le SDK est open source (licence MIT). Par contre, la plateforme est fermée, bien qu'il soit possible de l'héberger soi-même avec une licence entreprise.

Logfire propose un plan Personal qui est gratuit, incluant 10 millions de logs, ce qui est largement suffisant pour tester et pour de petits projets.

Concernant le stockage des données, deux régions sont proposées : USA et Europe.

Attention

Le choix de la région se fait à l'inscription !

Choix de la région

Choix de la région

Vous pouvez vous créer un compte : vous en aurez besoin pour la suite.

Comment installer Logfire ?

Pas de surprise pour les habitués, créons un projet avec uv :

uv init --no-package note-de-frais
cd note-de-frais
uv add logfire "pydantic-ai-slim[mistral]" httpx python-dotenv
SHELL

Attention

Pourquoi installer httpx à la main ? Actuellement, le modèle Mistral de PydanticAI importe httpx, alors que le SDK de mistralai est passé à httpx2. Sans ça, vous aurez une erreur ModuleNotFoundError: No module named 'httpx'.

J'ai signalé le problème dans une issue, une pull request est ouverte, alors peut-être qu'un correctif est sorti au moment où vous lisez ces lignes.

PydanticAI a besoin de la clé d'API Mistral dans la variable MISTRAL_API_KEY. Nous allons la stocker dans un fichier .env à la racine du projet.

# .env
MISTRAL_API_KEY=votre-clé
SHELL

Nous allons maintenant nous connecter à Logfire depuis le projet :

uv run logfire --region eu auth
SHELL

Votre navigateur s'ouvre, vous n'avez plus qu'à autoriser, et les identifiants sont sauvegardés dans ~/.logfire/default.toml.

Ensuite, depuis la racine du dossier, vous pouvez créer un projet :

uv run logfire projects new note-de-frais
SHELL

Il y a maintenant un dossier .logfire qui contient le jeton d'écriture du projet.

Vérifions que tout fonctionne. Dans le fichier main.py, remplacez le contenu par :

import logfire

# Connexion au projet Logfire (identifiants de `logfire auth`)
logfire.configure()

# {nom} est remplacé par la valeur de l'attribut nom
logfire.info("Bonjour {nom} !", nom="Patrick")
PYTHON

Puis lancez :

uv run main.py
SHELL

Le message apparaît dans le Live view de votre projet.

Live view

Live view

Comment tracer un agent PydanticAI avec Logfire ?

Maintenant, on passe aux choses sérieuses : Patrick. Il a besoin d'un assistant comptable, alors on va lui en coder un dans main.py :

import logfire
from dotenv import load_dotenv
from pydantic_ai import Agent

# On charge la clé d'API Mistral depuis le fichier .env
load_dotenv()

# 1. On connecte le SDK à notre projet Logfire
logfire.configure()
# 2. On enregistre chaque exécution de nos agents Pydantic AI
logfire.instrument_pydantic_ai()

NOTES_DE_FRAIS = {"sebastien": 230.50, "patrick": 87.20}
PLAFOND = 150.0

agent = Agent(
    "mistral:mistral-small-latest",
    name="assistant_comptable",
    instructions="Tu es l'assistant de la comptabilité. Réponds en une phrase.",
)


@agent.tool_plain
def consulter_note_de_frais(prenom: str) -> float:
    """Renvoie le montant de la note de frais d'un employé.

    Args:
        prenom: Prénom de l'employé.
    """
    return NOTES_DE_FRAIS.get(prenom.lower(), 0.0)


@agent.tool_plain
def verifier_plafond(montant: float) -> bool:
    """Indique si un montant dépasse le plafond de remboursement.

    Args:
        montant: Montant en euros.
    """
    return montant > PLAFOND


resultat = agent.run_sync("La note de frais de Sébastien dépasse-t-elle le plafond ?")
print(resultat.output)
PYTHON
  • load_dotenv() charge la clé d'API se trouvant dans le .env

  • logfire.configure() se connecte au projet

  • logfire.instrument_pydantic_ai() enregistre les exécutions de nos agents

  • pour la partie PydanticAI, n'hésitez pas à aller voir notre article sur le sujet. Nous n'appelons jamais nous-mêmes les outils décorés de @agent.tool_plain, c'est l'agent qui décide quand s'en servir

Les deux lignes de Logfire suffisent ! Maintenant, lançons main.py :

uv run main.py

# Résultat : 
Logfire project URL: https://logfire-eu.pydantic.dev/gabrieltrouve5/note-de-frais
15:44:59.663 assistant_comptable run
15:44:59.665   chat mistral-small-latest
15:45:00.833   running tool: consulter_note_de_frais
15:45:00.833   running tool: verifier_plafond
15:45:00.837   chat mistral-small-latest
La note de frais de Sébastien est de **0,00 €**, elle ne dépasse donc pas le plafond.
SHELL

Problème : Sébastien a une note de 230,50 €, au-dessus des 150 €. L'agent se trompe et aucune erreur n'est levée.

Comment trouver une erreur dans la trace d'un agent IA ?

Regardez le Live view de Logfire et cliquez sur la trace assistant_comptable run. Dépliez consulter_note_de_frais pour identifier le coupable :

consulter_note_de_frais

consulter_note_de_frais

Trouvé ! Le modèle a envoyé le prénom avec l'accent : Sébastien. Et comme "Sébastien".lower() donne sébastien, une clé absente du dictionnaire, la méthode get() renvoie la valeur par défaut 0.0.

Si je déplie l'appel à verifier_plafond :

verifier_plafond

verifier_plafond

verifier_plafond reçoit 0 et répond false, puis l'agent annonce une note à 0,00 €. On va corriger ça !

Corriger l'outil avec ModelRetry

Nous allons signaler le problème au modèle avec ModelRetry :

from pydantic_ai import Agent, ModelRetry


@agent.tool_plain
def consulter_note_de_frais(prenom: str) -> float:
    """Renvoie le montant de la note de frais d'un employé.

    Args:
        prenom: Prénom de l'employé, sans accent.
    """
    cle = prenom.lower()
    if cle not in NOTES_DE_FRAIS:
        # On renvoie l'erreur au modèle pour qu'il corrige son appel
        raise ModelRetry(f"Aucun employé nommé {prenom}. Réessaie sans accent.")
    return NOTES_DE_FRAIS[cle]
PYTHON

Nous avons modifié consulter_note_de_frais en utilisant ModelRetry. Regardez les logs :

consulter_note_de_frais avec ModelRetry

consulter_note_de_frais avec ModelRetry

Après le premier appel, Pydantic ajoute Fix the errors and try again., puis retente sans l'accent… avec succès !

Ajouter ses spans

Le décorateur @logfire.instrument() permet de tracer ses propres fonctions. En effet, Logfire ne se contente pas de ce que fait l'agent !

import logfire
from dotenv import load_dotenv
from pydantic_ai import Agent, ModelRetry

load_dotenv()
logfire.configure()
logfire.instrument_pydantic_ai()

NOTES_DE_FRAIS = {"sebastien": 230.50, "patrick": 87.20}
PLAFOND = 150.0

agent = Agent(
    "mistral:mistral-small-latest",
    name="assistant_comptable",
    instructions="Tu es l'assistant de la comptabilité. Réponds en une phrase.",
)


@agent.tool_plain
def consulter_note_de_frais(prenom: str) -> float:
    """Renvoie le montant de la note de frais d'un employé.

    Args:
        prenom: Prénom de l'employé, sans accent.
    """
    cle = prenom.lower()
    if cle not in NOTES_DE_FRAIS:
        # On renvoie l'erreur au modèle pour qu'il corrige son appel
        raise ModelRetry(f"Aucun employé nommé {prenom}. Réessaie sans accent.")
    return NOTES_DE_FRAIS[cle]


@agent.tool_plain
def verifier_plafond(montant: float) -> bool:
    """Indique si un montant dépasse le plafond de remboursement.

    Args:
        montant: Montant en euros.
    """
    return montant > PLAFOND


# Notre propre span, qui englobera l'exécution de l'agent
@logfire.instrument("Contrôle de la note de {prenom}")
def controler_note(prenom: str) -> str:
    resultat = agent.run_sync(
        f"La note de frais de {prenom} dépasse-t-elle le plafond ?"
    )
    return resultat.output


print(controler_note("Sébastien"))
PYTHON

Dès qu'il y a un appel à controler_note(), le décorateur crée un span, et l'exécution de l'agent est imbriquée dans notre span :

11:37:27.609 Contrôle de la note de Sébastien
11:37:27.624   assistant_comptable run
11:37:27.626     chat mistral-small-latest
11:37:28.340     running tool: consulter_note_de_frais
11:37:28.341     chat mistral-small-latest
11:37:28.691     running tool: verifier_plafond
11:37:28.694     chat mistral-small-latest
SHELL

Créer vos propres spans permet de donner des noms « métier » à la trace, ce qui permet d'englober tout ce que la fonction fait autour de l'agent.

Comment éviter d'envoyer des données sensibles à Logfire ?

Vous vous en doutez, tout ce que Logfire enregistre quitte votre ordinateur.

Le masquage automatique

Le SDK masque automatiquement les attributs dont la clé ressemble à une donnée sensible : secret, api_key, password, etc.

Faisons un test ! Je crée un fichier masquage.py :

import logfire

logfire.configure()

with logfire.span(
    "Connexion de {utilisateur}",
    utilisateur="patrick",
    password="azerty",
    secret="azerty",
    api_key="azerty"
):
    pass
PYTHON

Résultat :

Arguments:
{
  'api_key': "[Scrubbed due to 'api_key']",
  'password': "[Scrubbed due to 'password']",
  'secret': "[Scrubbed due to 'secret']",
  'utilisateur': 'patrick'
}
JSON

On remarque bien que les valeurs sensibles sont masquées automatiquement. Si vous le souhaitez, vous pouvez ajouter vos propres motifs, car mot_de_passe partirait en clair.

import logfire

# On ajoute nos propres motifs, en plus de ceux par défaut
logfire.configure(
    scrubbing=logfire.ScrubbingOptions(extra_patterns=["mot[._ -]?de[._ -]?passe"])
)

with logfire.span(
    "Connexion de {utilisateur}",
    utilisateur="patrick",
    mot_de_passe="azerty",
):
    pass
PYTHON

Résultat :

Arguments:
{
  'mot_de_passe': "[Scrubbed due to 'mot_de_passe']",
  'utilisateur': 'patrick'
}
JSON

Et pour les prompts ?

Vos échanges avec le modèle ne sont pas masqués : un modèle peut alors écrire une donnée sensible. Vous pouvez couper l'envoi de ce contenu :

logfire.instrument_pydantic_ai(include_content=False)
PYTHON

Le problème ici est que nous ne voyons plus les prompts, réponses, arguments et résultats.

Peut-on tracer toute une application avec Logfire ?

Le SDK est capable de gérer plus de 40 bibliothèques comme Django ou encore FastAPI. Et tout ça en une ligne.

Faisons le test avec FastAPI. Mais il faut installer fastapi, uvicorn et l'extra fastapi de logfire.

uv add fastapi uvicorn "logfire[fastapi]"
SHELL

Maintenant, notre fichier main.py :

import logfire
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic_ai import Agent, ModelRetry

load_dotenv()
logfire.configure()
logfire.instrument_pydantic_ai()

NOTES_DE_FRAIS = {"sebastien": 230.50, "patrick": 87.20}
PLAFOND = 150.0

agent = Agent(
    "mistral:mistral-small-latest",
    name="assistant_comptable",
    instructions="Tu es l'assistant de la comptabilité. Réponds en une phrase.",
)


@agent.tool_plain
def consulter_note_de_frais(prenom: str) -> float:
    """Renvoie le montant de la note de frais d'un employé.

    Args:
        prenom: Prénom de l'employé, sans accent.
    """
    cle = prenom.lower()
    if cle not in NOTES_DE_FRAIS:
        # On renvoie l'erreur au modèle pour qu'il corrige son appel
        raise ModelRetry(f"Aucun employé nommé {prenom}. Réessaie sans accent.")
    return NOTES_DE_FRAIS[cle]


@agent.tool_plain
def verifier_plafond(montant: float) -> bool:
    """Indique si un montant dépasse le plafond de remboursement.

    Args:
        montant: Montant en euros.
    """
    return montant > PLAFOND


# Notre propre span, qui englobera l'exécution de l'agent
@logfire.instrument("Contrôle de la note de {prenom}")
def controler_note(prenom: str) -> str:
    resultat = agent.run_sync(
        f"La note de frais de {prenom} dépasse-t-elle le plafond ?"
    )
    return resultat.output


app = FastAPI()
# Chaque requête HTTP devient une trace
logfire.instrument_fastapi(app)


@app.get("/notes/{prenom}")
def verifier_note(prenom: str) -> dict[str, str]:
    return {"reponse": controler_note(prenom)}
PYTHON

Lancez le serveur, puis, depuis un autre terminal, appelez la route :

# Lancement du serveur
uv run uvicorn main:app --reload
# Appel de la route
curl http://127.0.0.1:8000/notes/Sebastien
SHELL

Résultat :

INFO:     Started server process [18035]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
14:51:47.398 GET /notes/Sebastien
14:51:47.406   Contrôle de la note de Sebastien
14:51:47.409     assistant_comptable run
14:51:47.410       chat mistral-small-latest
14:51:48.495       running tool: consulter_note_de_frais
14:51:48.496       chat mistral-small-latest
14:51:49.064       running tool: verifier_plafond
14:51:49.065       chat mistral-small-latest
INFO:     127.0.0.1:51398 - "GET /notes/Sebastien HTTP/1.1" 200 OK
SHELL

Vous remarquez que la requête, la partie métier et l'agent se retrouvent dans une même trace.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.