← back to A2a Lab

client_sdk.py

69 lines

"""SDK-native A2A client — uses a2a-sdk's own client stack (not raw JSON-RPC).

Contrast with client.py (hand-rolled JSON-RPC over httpx): this drives the official
a2a-sdk 1.1.2 client — A2ACardResolver to fetch the agent card, ClientFactory.create to
build a transport-appropriate Client, and the async send_message() streaming iterator.
The SDK handles the proto SendMessageRequest, the SendMessage method name, and response
aggregation; we still supply the A2A-Version:1.0 header via the httpx client.

Run (server up): . .venv/bin/activate && A2A_BASE=http://127.0.0.1:41241 python client_sdk.py "hello"
"""
from __future__ import annotations

import asyncio
import os
import sys
import uuid

import httpx
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
from a2a.types import Message, Part, Role, SendMessageRequest

BASE = os.environ.get("A2A_BASE", "http://127.0.0.1:41241")


def _text_of(resp) -> str | None:
    """Pull text out of a StreamResponse (oneof message/task/status_update/artifact_update)."""
    if resp.HasField("message"):
        return "".join(p.text for p in resp.message.parts if p.text)
    if resp.HasField("task"):
        st = resp.task.status
        if st and st.message:
            return "".join(p.text for p in st.message.parts if p.text)
        return f"[task {resp.task.id} state={resp.task.status.state}]"
    return None


async def run(base: str, text: str) -> int:
    async with httpx.AsyncClient(headers={"A2A-Version": "1.0"}, timeout=30) as hx:
        card = await A2ACardResolver(hx, base).get_agent_card()
        print(f"① DISCOVERED (SDK): {card.name} v{card.version}")
        client = ClientFactory(ClientConfig(httpx_client=hx, streaming=False)).create(card)
        req = SendMessageRequest(
            message=Message(
                message_id=str(uuid.uuid4()),
                role=Role.ROLE_USER,
                parts=[Part(text=text)],
            )
        )
        print(f"② SEND (SDK send_message): {text!r}")
        got = False
        async for resp in client.send_message(req):
            out = _text_of(resp)
            if out is not None:
                print("③ RESPONSE:\n" + out)
                got = True
        if not got:
            print("③ (no message payload in stream)")
        close = getattr(client, "close", None)
        if close:
            res = close()
            if asyncio.iscoroutine(res):
                await res
    return 0


if __name__ == "__main__":
    text = sys.argv[1] if len(sys.argv) > 1 else "hello"
    raise SystemExit(asyncio.run(run(BASE, text)))