Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# A2A Python SDK — Samples

This directory contains runnable examples demonstrating how to build and interact with an A2A-compliant agent using the Python SDK.

## Contents

| File | Role | Description |
|---|---|---|
| `hello_world_agent.py` | **Server** | A2A agent server |
| `cli.py` | **Client** | Interactive terminal client |
| `text_client_cli.py` | **Client** | Simplified text-only interactive terminal client |

All three samples are designed to work together out of the box: the agent listens on `http://127.0.0.1:41241`, which is the default URL used by both clients.
---

## `hello_world_agent.py` — Agent Server

Implements an A2A agent that responds to simple greeting messages (e.g., "hello", "how are you", "bye") with text replies, simulating a 1-second processing delay.

Demonstrates:
- Subclassing `AgentExecutor` and implementing `execute()` / `cancel()`
- Publishing streaming status updates and artifacts via `TaskUpdater`
- Exposing all three transports in both protocol versions (v1.0 and v0.3 compat) simultaneously:
- **JSON-RPC** (v1.0 and v0.3) at `http://127.0.0.1:41241/a2a/jsonrpc`
- **HTTP+JSON (REST)** (v1.0 and v0.3) at `http://127.0.0.1:41241/a2a/rest`
- **gRPC v1.0** on port `50051`
- **gRPC v0.3 (compat)** on port `50052`
- Serving the agent card at `http://127.0.0.1:41241/.well-known/agent-card.json`

**Run:**

```bash
uv run python samples/hello_world_agent.py
```

---

## `cli.py` — Client

An interactive terminal client with full visibility into the streaming event flow. Each `TaskStatusUpdate` and `TaskArtifactUpdate` event is printed as it arrives.

Features:
- Transport selection via `--transport` flag (`JSONRPC`, `HTTP+JSON`, `GRPC`)
- Session management (`context_id` persisted across messages, `task_id` per task)
- Graceful error handling for HTTP and gRPC failures

**Run:**

```bash
# Connect to the local hello_world_agent (default):
uv run python samples/cli.py

# Connect to a different URL, using gRPC:
uv run python samples/cli.py --url http://192.168.1.10:41241 --transport GRPC
```

Type `/quit` or `/exit` to stop, or press `Ctrl+C`.

---

## `text_client_cli.py` — Simple Text Client

A stripped-down interactive client using the high-level `TextClient` abstraction. It hides all streaming and event mechanics, presenting a simple request/response interface.

Ideal for understanding the **minimum code required** to call an A2A agent.

**Run:**

```bash
# Connect to the local hello_world_agent (default):
uv run python samples/text_client_cli.py

# Connect to a different URL:
uv run python samples/text_client_cli.py --url http://192.168.1.10:41241

# Use a specific transport:
uv run python samples/text_client_cli.py --transport GRPC
```

Type `/quit` or `/exit` to stop, or press `Ctrl+C`.

---


## Quick Start

In two separate terminals:

```bash
# Terminal 1 — start the agent
uv run python samples/hello_world_agent.py

# Terminal 2 — start the client
uv run python samples/cli.py
```

Then type a message like `hello` and press Enter.
15 changes: 7 additions & 8 deletions samples/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,22 @@

from a2a.client import A2ACardResolver, ClientConfig, create_client
from a2a.types import Message, Part, Role, SendMessageRequest, TaskState
from a2a.utils.message import get_message_text


async def _handle_stream(
stream: Any, current_task_id: str | None
) -> str | None:
async for event, task in stream:
if not task:
continue
async for event in stream:
if not current_task_id:
current_task_id = task.id

current_task_id = event.task.id
if event:
if event.HasField('status_update'):
state_name = TaskState.Name(event.status_update.status.state)
print(f'TaskStatusUpdate [state={state_name}]:', end=' ')
if event.status_update.status.HasField('message'):
for part in event.status_update.status.message.parts:
if part.text:
print(part.text, end=' ')
message = event.status_update.status.message
print(get_message_text(message, delimiter=' '))
print()

if (
Expand All @@ -54,32 +51,34 @@

async def main() -> None:
"""Run the A2A terminal client."""
parser = argparse.ArgumentParser(description='A2A Terminal Client')
parser.add_argument(
'--url', default='http://127.0.0.1:41241', help='Agent base URL'
)
parser.add_argument(
'--transport',
default=None,
help='Preferred transport (JSONRPC, HTTP+JSON, GRPC)',
)
args = parser.parse_args()

config = ClientConfig()
if args.transport:
config.supported_protocol_bindings = [args.transport]
if args.transport == 'GRPC':
config.grpc_channel_factory = grpc.aio.insecure_channel

print(
f'Connecting to {args.url} (preferred transport: {args.transport or "Any"})'
)

async with httpx.AsyncClient() as httpx_client:
resolver = A2ACardResolver(httpx_client, args.url)
card = await resolver.get_agent_card()
print('\n✓ Agent Card Found:')
print(f' Name: {card.name}')

client = await create_client(card, client_config=config)

Check notice on line 81 in samples/cli.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see samples/text_client_cli.py (12-39)

actual_transport = getattr(client, '_transport', client)
print(f' Picked Transport: {actual_transport.__class__.__name__}')
Expand Down
70 changes: 70 additions & 0 deletions samples/text_client_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import argparse
import asyncio

import grpc
import httpx

from a2a.client import A2ACardResolver, ClientConfig, create_text_client


async def main() -> None:
"""Run the simple A2A terminal client using TextClient."""
parser = argparse.ArgumentParser(description='A2A Simple Text Client')
parser.add_argument(
'--url', default='http://127.0.0.1:41241', help='Agent base URL'
)
parser.add_argument(
'--transport',
default=None,
help='Preferred transport (JSONRPC, HTTP+JSON, GRPC)',
)
args = parser.parse_args()

config = ClientConfig()
if args.transport:
config.supported_protocol_bindings = [args.transport]
if args.transport == 'GRPC':
config.grpc_channel_factory = grpc.aio.insecure_channel

print(
f'Connecting to {args.url} (preferred transport: {args.transport or "Any"})'
)

async with httpx.AsyncClient() as httpx_client:
resolver = A2ACardResolver(httpx_client, args.url)
card = await resolver.get_agent_card()
print('\n✓ Agent Card Found:')
print(f' Name: {card.name}')

text_client = await create_text_client(card, client_config=config)

Check notice on line 39 in samples/text_client_cli.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see samples/cli.py (54-81)

actual_transport = getattr(
text_client.client, '_transport', text_client.client
)
print(f' Picked Transport: {actual_transport.__class__.__name__}')

print('\nConnected! Send a message or type /quit to exit.')

while True:
try:
loop = asyncio.get_running_loop()
user_input = await loop.run_in_executor(None, input, 'You: ')
except KeyboardInterrupt:
break

if user_input.lower() in ('/quit', '/exit'):
break
if not user_input.strip():
continue

try:

Check notice on line 60 in samples/text_client_cli.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see samples/cli.py (89-103)
response = await text_client.send_text_message(user_input)
print(f'Agent: {response}')
except (httpx.RequestError, grpc.RpcError) as e:
print(f'Error communicating with agent: {e}')

await text_client.close()


if __name__ == '__main__':
asyncio.run(main())
4 changes: 4 additions & 0 deletions src/a2a/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from a2a.client.client_factory import (
ClientFactory,
create_client,
create_text_client,
minimal_agent_card,
)
from a2a.client.errors import (
Expand All @@ -24,6 +25,7 @@
)
from a2a.client.helpers import create_text_message_object
from a2a.client.interceptors import ClientCallInterceptor
from a2a.client.text_client import TextClient


__all__ = [
Expand All @@ -40,7 +42,9 @@
'ClientFactory',
'CredentialService',
'InMemoryContextCredentialStore',
'TextClient',
'create_client',
'create_text_client',
'create_text_message_object',
'minimal_agent_card',
]
42 changes: 42 additions & 0 deletions src/a2a/client/client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from a2a.client.base_client import BaseClient
from a2a.client.card_resolver import A2ACardResolver
from a2a.client.client import Client, ClientConfig
from a2a.client.text_client import TextClient
from a2a.client.transports.base import ClientTransport
from a2a.client.transports.jsonrpc import JsonRpcTransport
from a2a.client.transports.rest import RestTransport
Expand Down Expand Up @@ -406,6 +407,47 @@
return factory.create(agent, interceptors)


async def create_text_client( # noqa: PLR0913
agent: str | AgentCard,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agent word is too broad. Can we do ?

Suggested change
agent: str | AgentCard,
agent_card: str | AgentCard,

Copy link
Copy Markdown
Member Author

@sokoliva sokoliva Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agent can also be the base URL of the agent so renaming it to agent_card could be misleading. WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I just noticed optional str. My bad! Let's keep it as it but add 2 example in doc-string.

>>> create_text_client('http://..'
....
>>>
>>> create_text_client(agent=my_agent_card,...)

client_config: ClientConfig | None = None,
interceptors: list[ClientCallInterceptor] | None = None,
relative_card_path: str | None = None,
resolver_http_kwargs: dict[str, Any] | None = None,
signature_verifier: Callable[[AgentCard], None] | None = None,
) -> TextClient:

Check notice on line 417 in src/a2a/client/client_factory.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/client/client_factory.py (368-266)
"""Create a `TextClient` for an agent from a URL or `AgentCard`.

Convenience function that constructs a `ClientFactory` internally.
For reusing a factory across multiple agents or registering custom
transports, use `ClientFactory` directly instead.

Args:
agent: The base URL of the agent, or an `AgentCard` to use
directly.
client_config: Optional `ClientConfig`. A default config is
created if not provided.
interceptors: A list of interceptors to use for each request.
relative_card_path: The relative path when resolving the agent
card. Only used when `agent` is a URL.
resolver_http_kwargs: Dictionary of arguments to provide to the
httpx client when resolving the agent card.
signature_verifier: A callable used to verify the agent card's
signatures.

Returns:
A `TextClient` wrapping the constructed `Client`.
"""
client = await create_client(
agent=agent,
client_config=client_config,
interceptors=interceptors,
relative_card_path=relative_card_path,
resolver_http_kwargs=resolver_http_kwargs,
signature_verifier=signature_verifier,
)
return TextClient(client)


def minimal_agent_card(
url: str, transports: list[str] | None = None
) -> AgentCard:
Expand Down
Loading
Loading