Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions contributing/samples/a2a_root/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The A2A Root sample consists of:

### 4. **Simple Deployment Pattern**
- Uses the `to_a2a()` utility to convert a standard ADK agent to an A2A service
- Publishes the agent card URL with a single `service_url` string
- Minimal configuration required for remote agent deployment

## Setup and Usage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,4 @@ async def check_prime(nums: list[int]) -> str:
),
)

a2a_app = to_a2a(root_agent, port=8001)
a2a_app = to_a2a(root_agent, service_url='http://localhost:8001')
25 changes: 24 additions & 1 deletion src/google/adk/a2a/utils/agent_to_a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,27 @@ def _load_agent_card(
return agent_card


def _build_rpc_url(
*,
host: str,
port: int,
protocol: str,
service_url: Optional[str],
) -> str:
"""Build the RPC URL published in the generated agent card."""
if service_url is not None:
return service_url
return f"{protocol}://{host}:{port}/"


@a2a_experimental
def to_a2a(
agent: BaseAgent,
*,
host: str = "localhost",
port: int = 8000,
protocol: str = "http",
service_url: Optional[str] = None,
agent_card: Optional[Union[AgentCard, str]] = None,
push_config_store: Optional[PushNotificationConfigStore] = None,
runner: Optional[Runner] = None,
Expand All @@ -94,6 +108,8 @@ def to_a2a(
host: The host for the A2A RPC URL (default: "localhost")
port: The port for the A2A RPC URL (default: 8000)
protocol: The protocol for the A2A RPC URL (default: "http")
service_url: Optional full service URL to publish in the generated
agent card. When provided, it takes precedence over host/port/protocol.
agent_card: Optional pre-built AgentCard object or path to agent card
JSON. If not provided, will be built automatically from the
agent.
Expand All @@ -116,6 +132,8 @@ def to_a2a(
app = to_a2a(agent, host="localhost", port=8000, protocol="http")
# Then run with: uvicorn module:app --host localhost --port 8000
app = to_a2a(agent, service_url="https://my-agent.run.app")
# Or with custom agent card:
app = to_a2a(agent, agent_card=my_custom_agent_card)
Expand Down Expand Up @@ -161,7 +179,12 @@ async def create_runner() -> Runner:
)

# Use provided agent card or build one from the agent
rpc_url = f"{protocol}://{host}:{port}/"
rpc_url = _build_rpc_url(
host=host,
port=port,
protocol=protocol,
service_url=service_url,
)
provided_agent_card = _load_agent_card(agent_card)

card_builder = AgentCardBuilder(
Expand Down
81 changes: 81 additions & 0 deletions tests/unittests/a2a/utils/test_agent_to_a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,87 @@ def test_to_a2a_custom_host_port(
agent=self.mock_agent, rpc_url="http://example.com:9000/"
)

@patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor")
@patch("google.adk.a2a.utils.agent_to_a2a.DefaultRequestHandler")
@patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore")
@patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder")
@patch("google.adk.a2a.utils.agent_to_a2a.Starlette")
def test_to_a2a_with_service_url(
self,
mock_starlette_class,
mock_card_builder_class,
mock_task_store_class,
mock_request_handler_class,
mock_agent_executor_class,
):
"""Test to_a2a with a full service URL."""
# Arrange
mock_app = Mock(spec=Starlette)
mock_starlette_class.return_value = mock_app
mock_task_store = Mock(spec=InMemoryTaskStore)
mock_task_store_class.return_value = mock_task_store
mock_agent_executor = Mock(spec=A2aAgentExecutor)
mock_agent_executor_class.return_value = mock_agent_executor
mock_request_handler = Mock(spec=DefaultRequestHandler)
mock_request_handler_class.return_value = mock_request_handler
mock_card_builder = Mock(spec=AgentCardBuilder)
mock_card_builder_class.return_value = mock_card_builder

# Act
result = to_a2a(
self.mock_agent,
service_url="https://my-agent.europe-west1.run.app",
)

# Assert
assert result == mock_app
mock_card_builder_class.assert_called_once_with(
agent=self.mock_agent,
rpc_url="https://my-agent.europe-west1.run.app",
)

@patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor")
@patch("google.adk.a2a.utils.agent_to_a2a.DefaultRequestHandler")
@patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore")
@patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder")
@patch("google.adk.a2a.utils.agent_to_a2a.Starlette")
def test_to_a2a_service_url_takes_precedence_over_host_port_protocol(
self,
mock_starlette_class,
mock_card_builder_class,
mock_task_store_class,
mock_request_handler_class,
mock_agent_executor_class,
):
"""Test service_url overrides host, port, and protocol for the card URL."""
# Arrange
mock_app = Mock(spec=Starlette)
mock_starlette_class.return_value = mock_app
mock_task_store = Mock(spec=InMemoryTaskStore)
mock_task_store_class.return_value = mock_task_store
mock_agent_executor = Mock(spec=A2aAgentExecutor)
mock_agent_executor_class.return_value = mock_agent_executor
mock_request_handler = Mock(spec=DefaultRequestHandler)
mock_request_handler_class.return_value = mock_request_handler
mock_card_builder = Mock(spec=AgentCardBuilder)
mock_card_builder_class.return_value = mock_card_builder

# Act
result = to_a2a(
self.mock_agent,
host="ignored.example.com",
port=1234,
protocol="http",
service_url="https://service.example.com/a2a",
)

# Assert
assert result == mock_app
mock_card_builder_class.assert_called_once_with(
agent=self.mock_agent,
rpc_url="https://service.example.com/a2a",
)

@patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor")
@patch("google.adk.a2a.utils.agent_to_a2a.DefaultRequestHandler")
@patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore")
Expand Down