diff --git a/contributing/samples/a2a_root/README.md b/contributing/samples/a2a_root/README.md index e847aa653c..4483ae11be 100644 --- a/contributing/samples/a2a_root/README.md +++ b/contributing/samples/a2a_root/README.md @@ -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 diff --git a/contributing/samples/a2a_root/remote_a2a/hello_world/agent.py b/contributing/samples/a2a_root/remote_a2a/hello_world/agent.py index 6261f4d486..b47f953c77 100755 --- a/contributing/samples/a2a_root/remote_a2a/hello_world/agent.py +++ b/contributing/samples/a2a_root/remote_a2a/hello_world/agent.py @@ -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') diff --git a/src/google/adk/a2a/utils/agent_to_a2a.py b/src/google/adk/a2a/utils/agent_to_a2a.py index 3e8ed461e2..7ca63ccd01 100644 --- a/src/google/adk/a2a/utils/agent_to_a2a.py +++ b/src/google/adk/a2a/utils/agent_to_a2a.py @@ -75,6 +75,19 @@ 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, @@ -82,6 +95,7 @@ def to_a2a( 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, @@ -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. @@ -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) @@ -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( diff --git a/tests/unittests/a2a/utils/test_agent_to_a2a.py b/tests/unittests/a2a/utils/test_agent_to_a2a.py index a9e2458ebd..0910726d3f 100644 --- a/tests/unittests/a2a/utils/test_agent_to_a2a.py +++ b/tests/unittests/a2a/utils/test_agent_to_a2a.py @@ -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")