Lección 3 — Agents A2A avec ADK

Exposer un agent ADK en serveur A2A

5 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Exposer un agent ADK en serveur A2A

Installer l'extra A2A

Le support A2A d'ADK est une dépendance optionnelle. Dans ADK 2.10, pip install "google-adk[a2a]==2.10.0" installe a2a-sdk[http-server] (version 1.2.2 au moment du test). Sans cet extra, l'import échoue avec ModuleNotFoundError: No module named 'a2a'.

Un agent ordinaire d'abord

L'agent stock est un agent ADK classique avec un outil. Rien dans son code ne parle d'A2A :

files/stock_agent.py
def check_stock(sku: str) -> dict:
    """Retourne le stock disponible d'un article, par entrepôt.

    Args:
        sku: référence de l'article, par exemple "CHAISE-ERGO".
    """
    item = STOCK.get(sku.strip().upper())
    if item is None:
        return {"status": "error", "message": f"Référence inconnue : {sku}",
                "references_connues": sorted(STOCK)}
    entrepots = {k: v for k, v in item.items() if k != "nom"}
    return {"status": "ok", "sku": sku.upper(), "nom": item["nom"],
            "par_entrepot": entrepots, "total": sum(entrepots.values())}

root_agent = Agent(
    name="stock_agent", model=MODEL,
    description="Donne le stock disponible d'un article (par entrepôt) à partir de sa référence.",
    instruction="Tu es le service stock. Pour toute question de disponibilité, appelle check_stock. ...",
    tools=[check_stock],
)

app = serve(root_agent, port=int(os.environ.get("STOCK_PORT", 8101)))

L'outil renvoie une erreur exploitable (references_connues) plutôt qu'une exception : le modèle peut corriger une référence mal saisie au lieu d'échouer.

Ce que fait to_a2a

flowchart TB
  A["Agent ADK (root_agent)"] --> R["Runner + services en mémoire (sessions, artefacts, mémoire)"]
  R --> X["A2aAgentExecutor : traduit A2A vers événements ADK"]
  A --> C["AgentCardBuilder : carte depuis nom, description, outils"]
  X --> H["DefaultRequestHandler + task store"]
  C --> W["Route GET /.well-known/agent-card.json"]
  H --> J["Route POST / (JSON-RPC)"]
  W --> S["Application Starlette"]
  J --> S

to_a2a(agent, host, port, protocol) renvoie une application Starlette (le micro-framework web sous FastAPI) prête pour uvicorn. Les routes sont ajoutées au démarrage (lifespan) : c'est là que la carte est construite, avec l'URL protocol://host:port/ qu'elle annoncera.

Le lab enveloppe cet appel dans serve() pour ajouter l'authentification (module 3) et l'URL publique (module 4) :

files/common.py
def serve(agent, port: int, token: str | None = None):
    """Transforme un agent ADK en application A2A (Starlette) protégée par jeton."""
    # Derrière Cloud Run : A2A_HOST=<service>.run.app, A2A_PROTOCOL=https, A2A_PUBLIC_PORT=443
    app = to_a2a(agent, host=HOST, port=int(os.environ.get("A2A_PUBLIC_PORT", port)),
                 protocol=os.environ.get("A2A_PROTOCOL", "http"))
    app.add_middleware(BearerAuth, token=token or a2a_token())
    return app
Paramètre de to_a2aRôleValeur du lab
host, port, protocolURL annoncée dans la carte (pas l'adresse d'écoute)localhost, 8101, http
rpc_pathpréfixe pour héberger plusieurs agents sur un même serveurnon utilisé
agent_cardcarte fournie à la main (objet ou chemin JSON)générée
task_storeoù stocker les tâches (mémoire par défaut)mémoire
runnerrunner ADK personnalisé (sessions persistantes…)par défaut

Vous lancez uvicorn stock_agent:app --port 9000 sans changer le code. Quelle URL annonce la carte ?

L'agent tarification, même recette

pricing_agent.py calcule un prix avec des paliers de remise. Le calcul est en Python, jamais confié au modèle :

files/pricing_agent.py
PALIERS = [(20, 0.15), (10, 0.10), (5, 0.05)]

def quote_price(sku: str, quantity: int) -> dict:
    ...
    remise = next((r for seuil, r in PALIERS if quantity >= seuil), 0.0)
    total = round(PRIX_UNITAIRE[sku] * quantity * (1 - remise), 2)

Puis on lance chaque agent dans son terminal :

export A2A_TOKEN=$(python -c "import secrets; print(secrets.token_urlsafe(24))")
uvicorn stock_agent:app --port 8101
uvicorn pricing_agent:app --port 8102

Pourquoi le calcul de remise est-il dans quote_price plutôt que dans l'instruction de l'agent ?

Résultat obtenu

Tests sans clé du lab, le 2026-10-15 (pytest -q dans files/) :

.............                                                            [100%]
13 passed in 0.93s

Parmi eux, test_agent_card_publique démarre l'application A2A avec le TestClient de Starlette (ce qui exécute le lifespan et construit la carte) et vérifie le nom, l'URL http://localhost:8101 et la compétence check_stock, sans aucun appel au modèle.

Exercice : un troisième spécialiste

Écrivez delivery_agent.py : un outil delivery_delay(city: str) -> dict (délais fictifs : Lyon 2 jours, Lille 3 jours, autres 5 jours) et un serveur A2A sur le port 8103. Vérifiez sa carte.

Voir la solution
import os
from google.adk.agents import Agent
from common import MODEL, serve

DELAIS = {"lyon": 2, "lille": 3}

def delivery_delay(city: str) -> dict:
    """Retourne le délai de livraison estimé, en jours ouvrés, pour une ville.

    Args:
        city: ville de livraison, par exemple "Lyon".
    """
    return {"status": "ok", "ville": city, "jours_ouvres": DELAIS.get(city.strip().lower(), 5)}

root_agent = Agent(name="delivery_agent", model=MODEL,
                   description="Estime le délai de livraison d'une commande selon la ville.",
                   instruction="Tu es le service livraison. Appelle delivery_delay et réponds en une phrase.",
                   tools=[delivery_delay])
app = serve(root_agent, port=int(os.environ.get("DELIVERY_PORT", 8103)))

uvicorn delivery_agent:app --port 8103, puis curl -s localhost:8103/.well-known/agent-card.json : la compétence delivery_delay apparaît. Vous l'ajouterez au registre dans la leçon 3.

Je sais faire

  • Installer l'extra A2A d'ADK et épingler les versions
  • Exposer un agent avec to_a2a et le lancer avec uvicorn
  • Expliquer la différence entre URL annoncée et port d'écoute
  • Garder les règles métier dans des outils testables
Siguiente