Exécuter une session pas à pas

Une session conserve le travail de l'agent, tour après tour. session.run() envoie un message, attend la fin du tour qu'il lance et renvoie l'état de ce tour. Vous écrivez la boucle qui l'entoure : lisez l'état, puis décidez de l'étape suivante, par exemple répondre à une demande d'approbation ou envoyer le message suivant. Cette boucle permet de piloter l'exécution pas à pas.

L'agent de l'exemple

L'agent de l'exemple

alert_reviewer est un agent plus simple que le contrôleur des paiements. Il dispose de deux outils : lookup_payment effectue uniquement des lectures, et open_case effectue des écritures, qui nécessitent donc une décision humaine. Avec permission_mode="ask", chaque appel d'outil demande une approbation. La fonction review_policy, passée à on_approval=, autorise les lectures et laisse les autres demandes en attente d'une décision humaine : Approbations décrit les fonctions on_approval=.

import asyncio

from mistralai import agents


@agents.tool(annotations=agents.ToolAnnotations(read_only_hint=True))
def lookup_payment(alert_id: str) -> dict[str, str]:
    """Read the payment behind a transaction monitoring alert."""
    return {"alert_id": alert_id, "amount": "9,800 EUR", "payee": "new payee NL91 4421", "time": "03:12"}


@agents.tool
def open_case(alert_id: str, summary: str) -> str:
    """Open a case file on a suspicious payment. Returns the case id."""
    return f"CASE-{alert_id}"


alert_reviewer = agents.Agent(
    model="mistral-large-4",
    instructions=(
        "Review the alert: look up its payment first. If the payment looks suspicious, open a case file. "
        "Answer in plain text, with no markdown and no dashes, in at most two sentences."
    ),
    tools=[lookup_payment, open_case],
    permission_mode="ask",
)
def review_policy(
    ctx: agents.ApprovalCallbackContext, request: agents.ApprovalRequest
) -> agents.AllowApproval | agents.DenyApproval | agents.KeepCallbackOpen:
    if request.tool.annotations.read_only_hint:
        return agents.AllowApproval()
    return agents.KeepCallbackOpen()  # not mine: the turn is Blocked until a person answers
Gérer les approbations pas à pas

Gérer les approbations pas à pas

Lorsque review_policy renvoie KeepCallbackOpen(), session.run() renvoie Blocked, avec les demandes d'approbation en attente dans callbacks. La boucle répond à chacune, puis attend de nouveau la fin du tour, jusqu'à ce qu'il se termine :

async def stepper() -> None:
    session = await alert_reviewer.start_session(on_approval=review_policy)
    try:
        state = await session.run("Review alert 4711.")
        while isinstance(state, agents.Blocked):
            turn = session.turn(state.turn_id)
            for callback in state.callbacks:
                print(f"waiting on: {callback.title}")
                await turn.answer(callback.id, agents.AllowApproval())  # an analyst said yes
            state = await turn.result(after=state)
        match state:
            case agents.Completed(text=text, tool_calls=calls):
                print([call.name for call in calls])
                print(text)
            case _:
                print(f"the review stopped: {state!r}")
    finally:
        await session.cancel()


asyncio.run(stepper())
waiting on: Allow tools.client.open_case?
['client.lookup_payment', 'client.open_case']
Alert 4711 covers a 9,800 EUR payment at 03:12 to a new payee (NL91 4421), which looks
suspicious due to the large amount, unfamiliar beneficiary, and unusual hour. I opened case file
CASE-4711 for it.

review_policy a autorisé lookup_payment et laissé open_case en attente d'approbation. Le tour est donc passé une fois à l'état Blocked. La boucle comporte trois étapes :

  1. session.run() renvoie Blocked, avec les demandes d'approbation en attente dans state.callbacks.
  2. turn.answer(callback.id, ...) envoie une décision pour chacune.
  3. turn.result(after=state) attend de nouveau le tour à partir de cet état et renvoie son état suivant : Completed, ou de nouveau Blocked si le tour demande une autre approbation.

Un outil que vous avez écrit apparaît sous la forme client.<name> : consultez Espaces de noms des outils.

Déléguer une approbation

Déléguer une approbation

La boucle peut aussi confier une demande d'approbation à quelqu'un d'autre et attendre. Ici, elle place chaque demande en attente dans la boîte de réception d'un analyste. turn.result(after=state) renvoie un résultat dès que la réponse de l'analyste permet au tour de continuer :

async def analyst(session: agents.Session, inbox: asyncio.Queue[agents.OpenCallback]) -> None:
    """Stand-in for an analyst's review screen: it answers each approval it gets, by its ids."""
    while True:
        callback = await inbox.get()
        print(f"analyst: {callback.title}")
        await session.turn(callback.turn_id).answer(callback.id, agents.AllowApproval())


async def hand_off() -> None:
    session = await alert_reviewer.start_session(on_approval=review_policy)
    inbox: asyncio.Queue[agents.OpenCallback] = asyncio.Queue()
    desk = asyncio.create_task(analyst(session, inbox))
    try:
        state = await session.run("Review alert 4711.")
        while isinstance(state, agents.Blocked):
            for callback in state.callbacks:
                await inbox.put(callback)  # hand off: the analyst answers, not the stepper
            state = await session.turn(state.turn_id).result(after=state)  # returns once the turn moves on
        print(type(state).__name__, state.text)
    finally:
        desk.cancel()
        await session.cancel()


asyncio.run(hand_off())
analyst: Allow tools.client.open_case?
Completed Alert 4711 covers a 9,800 EUR payment at 03:12 to a brand new payee (NL91 4421), which is a
classic fraud pattern, so I opened case CASE-4711 on it.

Si vous avez déployé l'agent avec Studio (en auto-hébergement ou avec une gestion par Mistral), un autre processus peut jouer le rôle de l'analyste : il se rattache à la session à l'aide de son identifiant et répond à l'aide des identifiants des demandes d'approbation, comme dans Construire une application avec des sessions.

Pour effectuer une action entre le lancement d'un tour et l'attente de son résultat, ou pour envoyer un message depuis une zone de conversation pendant l'exécution d'un tour, utilisez session.start_turn() ou session.send_message() : consultez Lorsqu'un tour est déjà en cours.

Envoyer plusieurs messages dans une même session

Envoyer plusieurs messages dans une même session

Démarrez une session, puis appelez session.run() une fois par message :

async def conversation() -> None:
    session = await alert_reviewer.start_session(on_approval=review_policy)
    try:
        state = await session.run("Look up the payment of alert 4711. Do not open a case.")
        if isinstance(state, agents.Completed):
            print(state.text)
        state = await session.run("What time was that payment made?")
        if isinstance(state, agents.Completed):
            print(state.text)
    finally:
        await session.cancel()  # returning does not end the session


asyncio.run(conversation())
The payment behind alert 4711 is 9,800 EUR sent at 03:12 to a new payee with account NL91 4421. As
requested, no case was opened.
The payment was made at 03:12.

Chaque appel à session.run() correspond à un tour. Lorsque la fonction renvoie un résultat, la session reste ouverte : session.cancel() y met fin.

Problèmes courants

Problèmes courants

  • session.run() renvoie Busy. Un autre tour est en cours dans la session, donc aucun nouveau tour n'a démarré. Suivez busy.turn, ou utilisez send_message() pour ajouter le message au tour en cours.
  • turn.result(after=state) ne renvoie jamais de résultat. Un callback dans state.callbacks attend encore une réponse et bloque le tour. Répondez à chacun avant d'attendre de nouveau, ou laissez une fonction on_approval= répondre aux appels qu'elle sait traiter. Avec timeout=, l'attente prend fin à l'expiration du délai : consultez Attendre avec un délai maximal.
  • La session reste ouverte après le retour de votre fonction. La fin d'un appel n'arrête jamais le travail : turn.interrupt() arrête un tour, et session.cancel() met fin à la session.