Skip to content

Commit 0b9f025

Browse files
jer96jer
andauthored
docs(a2a): update client examples to use latest A2A API (#206)
Co-authored-by: jer <jerebill@amazon.com>
1 parent 3de21df commit 0b9f025

File tree

1 file changed

+125
-47
lines changed

1 file changed

+125
-47
lines changed

docs/user-guide/concepts/multi-agent/agent-to-agent.md

Lines changed: 125 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Learn more about the A2A protocol:
2121
- [A2A Python SDK](https://github.com/a2aproject/a2a-python)
2222
- [A2A Documentation](https://a2aproject.github.io/A2A/latest/)
2323

24+
!!! tip "Complete Examples Available"
25+
Check out the [Native A2A Support samples](https://github.com/strands-agents/samples/tree/main/03-integrations/Native-A2A-Support) for complete, ready-to-run client, server and tool implementations.
26+
2427
## Installation
2528

2629
To use A2A functionality with Strands, install the package with the A2A extra:
@@ -67,12 +70,16 @@ a2a_server.serve()
6770
The `A2AServer` constructor accepts several configuration options:
6871

6972
- `agent`: The Strands Agent to wrap with A2A compatibility
70-
- `host`: Hostname or IP address to bind to (default: "0.0.0.0")
73+
- `host`: Hostname or IP address to bind to (default: "127.0.0.1")
7174
- `port`: Port to bind to (default: 9000)
7275
- `version`: Version of the agent (default: "0.0.1")
7376
- `skills`: Custom list of agent skills (default: auto-generated from tools)
7477
- `http_url`: Public HTTP URL where this agent will be accessible (optional, enables path-based mounting)
7578
- `serve_at_root`: Forces server to serve at root path regardless of http_url path (default: False)
79+
- `task_store`: Custom task storage implementation (defaults to InMemoryTaskStore)
80+
- `queue_manager`: Custom message queue management (optional)
81+
- `push_config_store`: Custom push notification configuration storage (optional)
82+
- `push_sender`: Custom push notification sender implementation (optional)
7683

7784
### Advanced Server Customization
7885

@@ -97,9 +104,50 @@ starlette_app = a2a_server.to_starlette_app()
97104
# Customize as needed
98105

99106
# You can then serve the customized app directly
100-
uvicorn.run(fastapi_app, host="0.0.0.0", port=9000)
107+
uvicorn.run(fastapi_app, host="127.0.0.1", port=9000)
101108
```
102109

110+
#### Configurable Request Handler Components
111+
112+
The `A2AServer` supports configurable request handler components for advanced customization:
113+
114+
```python
115+
from strands import Agent
116+
from strands.multiagent.a2a import A2AServer
117+
from a2a.server.tasks import TaskStore, PushNotificationConfigStore, PushNotificationSender
118+
from a2a.server.events import QueueManager
119+
120+
# Custom task storage implementation
121+
class CustomTaskStore(TaskStore):
122+
# Implementation details...
123+
pass
124+
125+
# Custom queue manager
126+
class CustomQueueManager(QueueManager):
127+
# Implementation details...
128+
pass
129+
130+
# Create agent with custom components
131+
agent = Agent(name="My Agent", description="A customizable agent", callback_handler=None)
132+
133+
a2a_server = A2AServer(
134+
agent=agent,
135+
task_store=CustomTaskStore(),
136+
queue_manager=CustomQueueManager(),
137+
push_config_store=MyPushConfigStore(),
138+
push_sender=MyPushSender()
139+
)
140+
```
141+
142+
**Interface Requirements:**
143+
144+
Custom implementations must follow these interfaces:
145+
146+
- `task_store`: Must implement `TaskStore` interface from `a2a.server.tasks`
147+
- `queue_manager`: Must implement `QueueManager` interface from `a2a.server.events`
148+
- `push_config_store`: Must implement `PushNotificationConfigStore` interface from `a2a.server.tasks`
149+
- `push_sender`: Must implement `PushNotificationSender` interface from `a2a.server.tasks`
150+
103151
#### Path-Based Mounting for Containerized Deployments
104152

105153
The `A2AServer` supports automatic path-based mounting for deployment scenarios involving load balancers or reverse proxies. This allows you to deploy agents behind load balancers with different path prefixes.
@@ -135,6 +183,7 @@ This flexibility allows you to:
135183
- Add custom middleware
136184
- Implement additional API endpoints
137185
- Deploy agents behind load balancers with different path prefixes
186+
- Configure custom task storage and event handling components
138187

139188
## A2A Client Examples
140189

@@ -145,42 +194,58 @@ Here's how to create a client that communicates with an A2A server synchronously
145194
```python
146195
import asyncio
147196
import logging
148-
from typing import Any
149197
from uuid import uuid4
198+
150199
import httpx
151-
from a2a.client import A2ACardResolver, A2AClient
152-
from a2a.types import MessageSendParams, SendMessageRequest
200+
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
201+
from a2a.types import Message, Part, Role, TextPart
153202

154203
logging.basicConfig(level=logging.INFO)
155204
logger = logging.getLogger(__name__)
156205

157206
DEFAULT_TIMEOUT = 300 # set request timeout to 5 minutes
158207

159-
def create_message_payload(*, role: str = "user", text: str) -> dict[str, Any]:
160-
return {
161-
"message": {
162-
"role": role,
163-
"parts": [{"kind": "text", "text": text}],
164-
"messageId": uuid4().hex,
165-
},
166-
}
208+
def create_message(*, role: Role = Role.user, text: str) -> Message:
209+
return Message(
210+
kind="message",
211+
role=role,
212+
parts=[Part(TextPart(kind="text", text=text))],
213+
message_id=uuid4().hex,
214+
)
167215

168-
async def send_sync_message(message: str, base_url: str = "http://localhost:9000"):
216+
async def send_sync_message(message: str, base_url: str = "http://127.0.0.1:9000"):
169217
async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as httpx_client:
170218
# Get agent card
171219
resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url)
172220
agent_card = await resolver.get_agent_card()
173221

174-
# Create client
175-
client = A2AClient(httpx_client=httpx_client, agent_card=agent_card)
176-
177-
# Send message
178-
payload = create_message_payload(text=message)
179-
request = SendMessageRequest(id=str(uuid4()), params=MessageSendParams(**payload))
180-
181-
response = await client.send_message(request)
182-
logger.info(response.model_dump_json(exclude_none=True, indent=2))
183-
return response
222+
# Create client using factory
223+
config = ClientConfig(
224+
httpx_client=httpx_client,
225+
streaming=False, # Use non-streaming mode for sync response
226+
)
227+
factory = ClientFactory(config)
228+
client = factory.create(agent_card)
229+
230+
# Create and send message
231+
msg = create_message(text=message)
232+
233+
# With streaming=False, this will yield exactly one result
234+
async for event in client.send_message(msg):
235+
if isinstance(event, Message):
236+
logger.info(event.model_dump_json(exclude_none=True, indent=2))
237+
return event
238+
elif isinstance(event, tuple) and len(event) == 2:
239+
# (Task, UpdateEvent) tuple
240+
task, update_event = event
241+
logger.info(f"Task: {task.model_dump_json(exclude_none=True, indent=2)}")
242+
if update_event:
243+
logger.info(f"Update: {update_event.model_dump_json(exclude_none=True, indent=2)}")
244+
return task
245+
else:
246+
# Fallback for other response types
247+
logger.info(f"Response: {str(event)}")
248+
return event
184249

185250
# Usage
186251
asyncio.run(send_sync_message("what is 101 * 11"))
@@ -193,41 +258,54 @@ For streaming responses, use the streaming client:
193258
```python
194259
import asyncio
195260
import logging
196-
from typing import Any
197261
from uuid import uuid4
262+
198263
import httpx
199-
from a2a.client import A2ACardResolver, A2AClient
200-
from a2a.types import MessageSendParams, SendStreamingMessageRequest
264+
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
265+
from a2a.types import Message, Part, Role, TextPart
201266

202267
logging.basicConfig(level=logging.INFO)
203268
logger = logging.getLogger(__name__)
204269

205270
DEFAULT_TIMEOUT = 300 # set request timeout to 5 minutes
206271

207-
def create_message_payload(*, role: str = "user", text: str) -> dict[str, Any]:
208-
return {
209-
"message": {
210-
"role": role,
211-
"parts": [{"kind": "text", "text": text}],
212-
"messageId": uuid4().hex,
213-
},
214-
}
272+
def create_message(*, role: Role = Role.user, text: str) -> Message:
273+
return Message(
274+
kind="message",
275+
role=role,
276+
parts=[Part(TextPart(kind="text", text=text))],
277+
message_id=uuid4().hex,
278+
)
215279

216-
async def send_streaming_message(message: str, base_url: str = "http://localhost:9000"):
280+
async def send_streaming_message(message: str, base_url: str = "http://127.0.0.1:9000"):
217281
async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as httpx_client:
218282
# Get agent card
219283
resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url)
220284
agent_card = await resolver.get_agent_card()
221285

222-
# Create client
223-
client = A2AClient(httpx_client=httpx_client, agent_card=agent_card)
224-
225-
# Send streaming message
226-
payload = create_message_payload(text=message)
227-
request = SendStreamingMessageRequest(id=str(uuid4()), params=MessageSendParams(**payload))
228-
229-
async for event in client.send_message_streaming(request):
230-
logger.info(event.model_dump_json(exclude_none=True, indent=2))
286+
# Create client using factory
287+
config = ClientConfig(
288+
httpx_client=httpx_client,
289+
streaming=True, # Use streaming mode
290+
)
291+
factory = ClientFactory(config)
292+
client = factory.create(agent_card)
293+
294+
# Create and send message
295+
msg = create_message(text=message)
296+
297+
async for event in client.send_message(msg):
298+
if isinstance(event, Message):
299+
logger.info(event.model_dump_json(exclude_none=True, indent=2))
300+
elif isinstance(event, tuple) and len(event) == 2:
301+
# (Task, UpdateEvent) tuple
302+
task, update_event = event
303+
logger.info(f"Task: {task.model_dump_json(exclude_none=True, indent=2)}")
304+
if update_event:
305+
logger.info(f"Update: {update_event.model_dump_json(exclude_none=True, indent=2)}")
306+
else:
307+
# Fallback for other response types
308+
logger.info(f"Response: {str(event)}")
231309

232310
# Usage
233311
asyncio.run(send_streaming_message("what is 101 * 11"))
@@ -255,9 +333,9 @@ logging.basicConfig(level=logging.INFO)
255333
logger = logging.getLogger(__name__)
256334

257335
# Create A2A client tool provider with known agent URLs
258-
# Assuming you have an A2A server running on localhost:9000
336+
# Assuming you have an A2A server running on 127.0.0.1:9000
259337
# known_agent_urls is optional
260-
provider = A2AClientToolProvider(known_agent_urls=["http://localhost:9000"])
338+
provider = A2AClientToolProvider(known_agent_urls=["http://127.0.0.1:9000"])
261339

262340
# Create agent with A2A client tools
263341
agent = Agent(tools=provider.tools)

0 commit comments

Comments
 (0)