Saltar al contenido

01 · Lo mínimo: un agente, una herramienta

Esto es un fichero que se ejecuta: examples/agentes/01_triaje.py ↗ Esta página lo transcribe y enseña lo que imprime. Si dejan de coincidir, falla un test.

Dominio: Argus — la plataforma de observabilidad. Su camino caliente detecta un incidente en ~2 s sin tocar la base de datos, y deja una señal. Alguien —o algo— tiene que mirarla y decir si merece despertar a una persona.

Ese "algo" es el agente más simple que se puede escribir con este framework.

Cómo correrlo

uv run python examples/agentes/01_triaje.py

No hace falta configurar nada: sin modelo, las respuestas van guionizadas y todo lo demás es real — las herramientas se ejecutan, el journal se escribe, el consumo se mide. Con AXONIUM_CLIENT_ID o SYNAPTUM_BASE_URL en el entorno, el mismo fichero sin tocar habla con un modelo de verdad; lo que cambia entonces es lo que diga el modelo, no el código. Ver Modelos.

Lo que imprime

01 · Triaje de una señal
────────────────────────
sin inferencia · respuestas guionizadas (exporta SYNAPTUM_BASE_URL para usar un modelo real)

  → leer_señal({'señal_id': 'sig-4471'})

  Despertar ahora. La latencia del gateway de inferencia se multiplicó por ocho y afecta a tres servicios: todo lo que dependa de un modelo está degradado, no solo lento.

  consumo: entrada=200 salida=40
from __future__ import annotations

import asyncio
import sys
from pathlib import Path
from typing import Annotated

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from synaptum import Agent, FinalStep, Phase, Session, ToolStep, tool
from synaptum.testing import calls, says

from comun import encabezado, gateway, nombre_del_modelo

Datos que en Argus vendrían de ClickHouse

SEÑALES = {
    "sig-4471": {
        "servicio": "prometheus-gateway",
        "sintoma": "latencia p95 de /v1/chat/completions pasó de 820ms a 6.4s",
        "desde": "2026-09-18T02:14:00Z",
        "afectados": 3,
        "spans": 412,
    }
}

La herramienta

El esquema sale de la firma. Un esquema escrito aparte se desincroniza, y cuando lo hace el modelo manda argumentos que la función no acepta — lejos del cambio que lo causó y con una inferencia ya pagada.

@tool
async def leer_señal(
    señal_id: Annotated[str, "Identificador de la señal, por ejemplo 'sig-4471'"],
) -> str:
    """Devuelve los datos de una señal detectada por el camino caliente."""
    señal = SEÑALES.get(señal_id)
    if señal is None:
        return f"No existe la señal {señal_id}."
    return (
        f"servicio: {señal['servicio']}\n"
        f"síntoma: {señal['sintoma']}\n"
        f"desde: {señal['desde']}\n"
        f"servicios afectados: {señal['afectados']}\n"
        f"spans implicados: {señal['spans']}"
    )

El agente

async def main() -> None:
    encabezado("01 · Triaje de una señal")

    agente = Agent(
        "triaje",
        model=nombre_del_modelo(),
        instructions=(
            "Eres el triaje de una plataforma de observabilidad. Lee la señal y "
            "decide si despertar a una persona ahora o dejarlo para mañana. "
            "Responde en dos frases: el veredicto y por qué."
        ),
        tools=[leer_señal],
    )

    # El guion solo se usa cuando no hay modelo configurado.  Con uno, el agente
    # es exactamente el mismo: no sabe quién hay al otro lado.
    guion = [
        calls("leer_señal", señal_id="sig-4471"),
        says(
            "Despertar ahora. La latencia del gateway de inferencia se multiplicó "
            "por ocho y afecta a tres servicios: todo lo que dependa de un modelo "
            "está degradado, no solo lento."
        ),
    ]

    sesion = Session("triaje-4471", gateway(guion, tools=[leer_señal]))

    async for paso in agente.run("Tría la señal sig-4471.", session=sesion):
        match paso:
            case ToolStep(phase=Phase.ATTEMPTED, call=llamada) if llamada:
                print(f"  → {llamada.name}({llamada.arguments})")
            case FinalStep(output=veredicto, usage=consumo):
                print(f"\n  {veredicto}\n")
                print(f"  consumo: entrada={consumo.input} salida={consumo.output}")

if __name__ == "__main__":
    asyncio.run(main())

Lo que esto enseña

Eso es todo. Tres piezas y ninguna más:

@tool        una función con tipos, que se ejecuta de verdad
Agent        el modelo, las instrucciones y las herramientas
Session      dónde se ejecuta (gateway) y dónde se recuerda (checkpointer)

El async for no es decoración: el bucle cede el control en cada frontera, así que ver lo que hace el agente es iterar, y pararlo es dejar de iterar.


El fichero entero, para clonarlo y tocarlo: examples/agentes/01_triaje.py

Está en examples/ con los otros quince, y todos corren igual.