Python: Reorganize A2A samples and use package A2AExecutor (#6165)

* Reorganize A2A samples: client demos in 02-agents, use package A2AExecutor

- Move client samples (agent_with_a2a, a2a_agent_as_function_tools) to samples/02-agents/a2a/
- Add new concept samples: polling, stream reconnection, protocol selection
- Replace sample agent_executor.py with package-level A2AExecutor (stream=True)
- Update 04-hosting/a2a to focus on server-side, point to 02-agents for clients
- Add README.md for the new 02-agents/a2a/ sample collection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix streaming artifact coalescing and address PR review feedback

A2AExecutor fix:
- Generate a stable artifact_id per stream in _run_stream so all streaming
  chunks share the same ID, enabling proper append=True coalescing per the
  A2A spec (TaskArtifactUpdateEvent with same artifactId).
- Previously, item.message_id was None for OpenAI/Foundry streaming updates,
  causing the SDK to generate a new random UUID per token (100+ separate
  artifacts instead of 1 appended artifact).

Sample improvements:
- Replace join workaround with response.text now that coalescing works
- Add background=True to stream reconnection resume call (required for
  continuation token emission on in-progress tasks)
- Fix type ignore specificity in polling sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Giles Odigwe
2026-06-01 00:09:11 -07:00
committed by GitHub
Unverified
parent edcc786651
commit 5affc9c333
10 changed files with 409 additions and 138 deletions
+13 -35
View File
@@ -1,39 +1,30 @@
# A2A Agent Examples
# A2A Server Hosting Examples
This sample demonstrates how to host and consume agents using the [A2A (Agent2Agent) protocol](https://a2a-protocol.org/latest/) with the `agent_framework` package. There are three runnable entry points:
This sample demonstrates how to **host** Agent Framework agents as A2A-compliant servers using the [A2A (Agent2Agent) protocol](https://a2a-protocol.org/latest/).
> **Looking for client samples?** See [`samples/02-agents/a2a/`](../../02-agents/a2a/) for consuming remote A2A agents.
## Server Samples
| Run this file | To... |
|---------------|-------|
| **[`a2a_server.py`](a2a_server.py)** | Host an Agent Framework agent as an A2A-compliant server. |
| **[`agent_with_a2a.py`](agent_with_a2a.py)** | Connect to an A2A server and send requests (non-streaming and streaming). |
| **[`a2a_agent_as_function_tools.py`](a2a_agent_as_function_tools.py)** | Convert A2A agent skills into function tools for a host agent. |
| **[`a2a_server.py`](a2a_server.py)** | Host an Agent Framework agent as an A2A-compliant server (multi-agent). |
| **[`agent_framework_to_a2a.py`](agent_framework_to_a2a.py)** | Minimal example: expose a single agent as an A2A server. |
The remaining files are supporting modules used by the server:
## Supporting Modules
| File | Description |
|------|-------------|
| [`agent_framework_to_a2a.py`](agent_framework_to_a2a.py) | Exposes an agent_framework agent as an A2A-compliant server. Demonstrates how to wrap an agent_framework agent and expose it as an A2A service that other A2A clients can discover and communicate with. |
| [`agent_definitions.py`](agent_definitions.py) | Agent and AgentCard factory definitions for invoice, policy, and logistics agents. |
| [`agent_executor.py`](agent_executor.py) | Bridges the a2a-sdk `AgentExecutor` interface to Agent Framework agents. |
| [`invoice_data.py`](invoice_data.py) | Mock invoice data and tool functions for the invoice agent. |
| [`a2a_server.http`](a2a_server.http) | REST Client requests for testing the server directly from VS Code. |
## Environment Variables
Make sure to set the following environment variables before running the examples:
### Required (Server)
- `FOUNDRY_PROJECT_ENDPOINT` — Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL` — Model deployment name (e.g. `gpt-4o`)
### Required (Client)
- `A2A_AGENT_HOST` — URL of the A2A server (e.g. `http://localhost:5001/`)
### Required (Function Tools Sample)
- `A2A_AGENT_HOST` — URL of the A2A server (e.g. `http://localhost:5000/`)
- `FOUNDRY_PROJECT_ENDPOINT` — Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL` — Model deployment name (e.g. `gpt-4o`)
## Quick Start
All commands below should be run from this directory:
@@ -67,7 +58,7 @@ uv run python a2a_server.py --agent-type policy
### 1. Start the A2A Server
> **Note (Option A — pip users):** Replace `uv run python` with `python` in all `uv run` commands below (e.g. `python a2a_server.py ...`). `uv` is not required once the virtual environment is activated.
> **Note (Option A — pip users):** Replace `uv run python` with `python` in all `uv run` commands below. `uv` is not required once the virtual environment is activated.
Pick an agent type and start the server (each in its own terminal):
@@ -79,25 +70,12 @@ uv run python a2a_server.py --agent-type logistics --port 5002
You can run one agent or all three — each listens on its own port.
### 2. Run the A2A Client
### 2. Run a Client
In a separate terminal (from the same directory), point the client at a running server:
Once a server is running, use any of the client samples in [`samples/02-agents/a2a/`](../../02-agents/a2a/):
```powershell
cd python/samples/02-agents/a2a
$env:A2A_AGENT_HOST = "http://localhost:5001/"
uv run python agent_with_a2a.py
# A2A server exposing an agent_framework agent
uv run python agent_framework_to_a2a.py
```
### 3. Run the Function Tools Sample
This sample resolves the remote agent's skills and registers each one as a function tool
on a host Foundry-backed agent. The host agent then autonomously selects the right skill
to handle the user's request.
```powershell
$env:A2A_AGENT_HOST = "http://localhost:5000/"
uv run python a2a_agent_as_function_tools.py
```
@@ -1,150 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import re
import httpx
from a2a.client import A2ACardResolver
from agent_framework.a2a import A2AAgent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
A2A Agent Skills as Function Tools
This sample demonstrates how to represent an A2A agent's skills as individual
function tools and register them with a host agent. Each skill advertised in the
remote agent's AgentCard becomes a separate tool that the host agent can invoke.
Key concepts demonstrated:
- Resolving an AgentCard from a remote A2A endpoint
- Converting each skill into a FunctionTool via as_tool()
- Registering those tools with a host agent
- Having the host agent autonomously select and invoke A2A skills
Prerequisites:
- Set A2A_AGENT_HOST to the URL of a running A2A server
- Set FOUNDRY_PROJECT_ENDPOINT to your Azure AI Foundry project endpoint
- Set FOUNDRY_MODEL to the model deployment name (e.g. gpt-4o)
To run this sample:
cd python/samples/04-hosting/a2a
uv run python a2a_agent_as_function_tools.py
"""
async def main() -> None:
"""Discover A2A agent skills and register them as tools on a host agent."""
# 1. Read environment configuration.
a2a_agent_host = os.getenv("A2A_AGENT_HOST")
if not a2a_agent_host:
raise ValueError("A2A_AGENT_HOST environment variable is not set")
project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT")
model = os.getenv("FOUNDRY_MODEL")
if not project_endpoint or not model:
raise ValueError("FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL must be set")
print(f"Connecting to A2A agent at: {a2a_agent_host}")
# 2. Resolve the remote agent card to discover its skills.
async with httpx.AsyncClient(timeout=60.0) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host)
agent_card = await resolver.get_agent_card()
print(f"Found agent: {agent_card.name} ({len(agent_card.skills)} skill(s))")
for skill in agent_card.skills:
print(f" - {skill.name}: {skill.description}")
# 3. Create the A2AAgent that wraps the remote endpoint.
async with A2AAgent(
name=agent_card.name,
description=agent_card.description,
agent_card=agent_card,
url=a2a_agent_host,
) as a2a_agent:
# 4. Convert each A2A skill into a FunctionTool.
# Skill names may contain spaces or special characters, so we
# sanitize them into valid tool identifiers before passing to as_tool().
skill_tools = [
a2a_agent.as_tool(
name=re.sub(r"[^0-9A-Za-z]+", "_", skill.name),
description=skill.description or "",
)
for skill in agent_card.skills
]
# 5. Create the host agent with the skill tools.
credential = AzureCliCredential()
client = FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
)
host_agent = client.as_agent(
name="assistant",
instructions="You are a helpful assistant. Use your tools to answer questions.",
tools=skill_tools,
)
# 6. Run the host agent — it will select and invoke the appropriate A2A skill tools.
query = "Show me all invoices for Contoso"
print(f"\nUser: {query}\n")
response = await host_agent.run(query)
print(f"Agent: {response}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Connecting to A2A agent at: http://localhost:5000/
Found agent: InvoiceAgent (1 skill(s))
- InvoiceQuery: Handles requests relating to invoices.
User: Show me all invoices for Contoso
Agent: Here are the invoices for Contoso:
1. **Invoice ID:** INV789
- **Date:** 2026-02-15
- **Products:**
- T-Shirts: 150 units @ $10.00 = $1,500.00
- Hats: 200 units @ $15.00 = $3,000.00
- Glasses: 300 units @ $5.00 = $1,500.00
- **Total:** $6,000.00
2. **Invoice ID:** INV333
- **Date:** 2026-03-14
- **Products:**
- T-Shirts: 400 units @ $11.00 = $4,400.00
- Hats: 600 units @ $15.00 = $9,000.00
- Glasses: 700 units @ $5.00 = $3,500.00
- **Total:** $16,900.00
3. **Invoice ID:** INV666
- **Date:** 2026-02-06
- **Products:**
- T-Shirts: 2,500 units @ $8.00 = $20,000.00
- Hats: 1,200 units @ $10.00 = $12,000.00
- Glasses: 1,000 units @ $6.00 = $6,000.00
- **Total:** $38,000.00
4. **Invoice ID:** INV999
- **Date:** 2026-03-19
- **Products:**
- T-Shirts: 1,400 units @ $10.50 = $14,700.00
- Hats: 1,100 units @ $9.00 = $9,900.00
- Glasses: 950 units @ $12.00 = $11,400.00
- **Total:** $36,000.00
If you need more details or a specific invoice, please let me know!
"""
+2 -2
View File
@@ -9,7 +9,7 @@ from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
from a2a.server.tasks import InMemoryTaskStore
from agent_definitions import AGENT_CARD_FACTORIES, AGENT_FACTORIES
from agent_executor import AgentFrameworkExecutor
from agent_framework.a2a import A2AExecutor
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -92,7 +92,7 @@ def main() -> None:
# Build the A2A server components
url = f"http://{args.host}:{args.port}/"
agent_card = AGENT_CARD_FACTORIES[args.agent_type](url)
executor = AgentFrameworkExecutor(agent)
executor = A2AExecutor(agent, stream=True)
task_store = InMemoryTaskStore()
request_handler = DefaultRequestHandler(
agent_executor=executor,
@@ -1,83 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""AgentExecutor bridge between the a2a-sdk server and Agent Framework agents.
Implements the a2a-sdk ``AgentExecutor`` interface so that incoming A2A
requests are forwarded to an Agent Framework agent and the response is
published back through the a2a-sdk event queue.
"""
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING
from a2a.helpers import new_task_from_user_message
from a2a.server.agent_execution.agent_executor import AgentExecutor
from a2a.server.tasks import TaskUpdater
from a2a.types import Part, TaskState
if TYPE_CHECKING:
from a2a.server.agent_execution.context import RequestContext
from a2a.server.events.event_queue import EventQueue
from agent_framework import Agent
class AgentFrameworkExecutor(AgentExecutor):
"""Bridges A2A protocol requests to an Agent Framework agent.
For each incoming ``execute`` call the executor:
1. Extracts the user's text from the A2A ``RequestContext``.
2. Runs the Agent Framework agent (non-streaming).
3. Publishes the result as an A2A ``Message`` to the ``EventQueue``.
"""
def __init__(self, agent: Agent) -> None:
self.agent = agent
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
"""Run the agent and publish the response."""
user_text = context.get_user_input()
if not user_text:
user_text = "Hello"
# v1.0 requires a Task object in the queue before any TaskStatusUpdateEvent
task = context.current_task
if not task and context.message:
task = new_task_from_user_message(context.message)
await event_queue.enqueue_event(task)
task_id = task.id if task else context.task_id
updater = TaskUpdater(event_queue, task_id, context.context_id)
# Signal that the agent is working
await updater.start_work()
try:
response = await self.agent.run(user_text)
# Build response text from agent messages
response_parts: list[Part] = []
for msg in response.messages:
if msg.text:
response_parts.append(Part(text=msg.text))
if not response_parts:
response_parts.append(Part(text=str(response)))
# Publish the agent's response and mark as completed
await updater.complete(
message=updater.new_agent_message(response_parts),
)
except asyncio.CancelledError:
raise
except Exception as e:
await updater.update_status(
state=TaskState.TASK_STATE_FAILED,
message=updater.new_agent_message([Part(text=f"Agent error: {e}")]),
)
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
"""Handle cancellation by publishing a canceled status."""
updater = TaskUpdater(event_queue, context.task_id, context.context_id)
await updater.update_status(state=TaskState.TASK_STATE_CANCELED)
@@ -1,112 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import httpx
from a2a.client import A2ACardResolver
from agent_framework.a2a import A2AAgent
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Agent2Agent (A2A) Protocol Integration Sample
This sample demonstrates how to connect to and communicate with external agents using
the A2A protocol. A2A is a standardized communication protocol that enables interoperability
between different agent systems, allowing agents built with different frameworks and
technologies to communicate seamlessly.
By default the A2AAgent waits for the remote agent to finish before returning (background=False).
This means long-running A2A tasks are handled transparently — the caller simply awaits the result.
For advanced scenarios where you need to poll or resubscribe to in-progress tasks, see the
background_responses sample: samples/concepts/background_responses.py
For more information about the A2A protocol specification, visit: https://a2a-protocol.org/latest/
Key concepts demonstrated:
- Discovering A2A-compliant agents using AgentCard resolution
- Creating A2AAgent instances to wrap external A2A endpoints
- Non-streaming request/response
- Streaming responses to receive incremental updates via SSE
To run this sample:
1. Set the A2A_AGENT_HOST environment variable to point to an A2A-compliant agent endpoint
Example: export A2A_AGENT_HOST="https://your-a2a-agent.example.com"
2. Ensure the target agent exposes its AgentCard at /.well-known/agent.json
3. Run: uv run python agent_with_a2a.py
Visit the README.md for more details on setting up and running A2A agents.
"""
async def main():
"""Demonstrates connecting to and communicating with an A2A-compliant agent."""
# 1. Get A2A agent host from environment.
a2a_agent_host = os.getenv("A2A_AGENT_HOST")
if not a2a_agent_host:
raise ValueError("A2A_AGENT_HOST environment variable is not set")
print(f"Connecting to A2A agent at: {a2a_agent_host}")
# 2. Resolve the agent card to discover capabilities.
async with httpx.AsyncClient(timeout=60.0) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host)
agent_card = await resolver.get_agent_card()
print(f"Found agent: {agent_card.name} - {agent_card.description}")
# 3. Create A2A agent instance.
async with A2AAgent(
name=agent_card.name,
description=agent_card.description,
agent_card=agent_card,
url=a2a_agent_host,
) as agent:
# 4. Simple request/response — the agent waits for completion internally.
# Even if the remote agent takes a while, background=False (the default)
# means the call blocks until a terminal state is reached.
print("\n--- Non-streaming response ---")
response = await agent.run("What are your capabilities?")
print("Agent Response:")
for message in response.messages:
print(f" {message.text}")
# 5. Stream a response — the natural model for A2A.
# Updates arrive as Server-Sent Events, letting you observe
# progress in real time as the remote agent works.
print("\n--- Streaming response ---")
stream = agent.run("Tell me about yourself", stream=True)
async for update in stream:
for content in update.contents:
if content.text:
print(f" {content.text}")
response = await stream.get_final_response()
print(f"\nFinal response ({len(response.messages)} message(s)):")
for message in response.messages:
print(f" {message.text}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Connecting to A2A agent at: http://localhost:5001/
Found agent: MyAgent - A helpful AI assistant
--- Non-streaming response ---
Agent Response:
I can help with code generation, analysis, and general Q&A.
--- Streaming response ---
I am an AI assistant built to help with various tasks.
Final response (1 message(s)):
I am an AI assistant built to help with various tasks.
"""