From 13b912be74c6168feb01783389f95ea5ba452c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ra=C4=8Dinsk=C3=BD?= Date: Wed, 30 Jul 2025 12:48:23 +0200 Subject: [PATCH 1/2] feat: more generic url --- contributing/samples/a2a_auth/README.md | 5 ++- contributing/samples/a2a_basic/README.md | 3 +- .../samples/a2a_human_in_loop/README.md | 3 +- src/google/adk/cli/cli_tools_click.py | 41 ++++++++++++++++--- src/google/adk/cli/fast_api.py | 9 +++- tests/unittests/cli/test_fast_api.py | 35 +++++++++------- 6 files changed, 71 insertions(+), 25 deletions(-) diff --git a/contributing/samples/a2a_auth/README.md b/contributing/samples/a2a_auth/README.md index 2e4aa204d..f6b0e6cf4 100644 --- a/contributing/samples/a2a_auth/README.md +++ b/contributing/samples/a2a_auth/README.md @@ -185,7 +185,8 @@ When deploying the remote BigQuery A2A agent to different environments (e.g., Cl } ``` -**Important:** The `url` field in `remote_a2a/bigquery_agent/agent.json` must point to the actual RPC endpoint where your remote BigQuery A2A agent is deployed and accessible. +**Important:** The `url` field in `remote_a2a/bigquery_agent/agent.json` must point to the actual RPC endpoint where your remote BigQuery A2A agent is deployed and accessible. +If the `url` field is an empty string, it will be automatically filled by the `--host` and `--port`, or `--base_url` provided to `adk api_server`. ## Troubleshooting @@ -193,7 +194,7 @@ When deploying the remote BigQuery A2A agent to different environments (e.g., Cl - Ensure the local ADK web server is running on port 8000 - Ensure the remote A2A server is running on port 8001 - Check that no firewall is blocking localhost connections -- **Verify the `url` field in `remote_a2a/bigquery_agent/agent.json` matches the actual deployed location of your remote A2A server** +- **Verify the `url` field in `remote_a2a/bigquery_agent/agent.json` matches the actual deployed location of your remote A2A server, or if it's empty, make sure the `--host` and `--port`, or `--base_url` provided to `adk api_server` match the actual deployed location of your remote A2A server** - Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server diff --git a/contributing/samples/a2a_basic/README.md b/contributing/samples/a2a_basic/README.md index ca61101c2..8eeb77516 100644 --- a/contributing/samples/a2a_basic/README.md +++ b/contributing/samples/a2a_basic/README.md @@ -136,6 +136,7 @@ When deploying the remote A2A agent to different environments (e.g., Cloud Run, ``` **Important:** The `url` field in `remote_a2a/check_prime_agent/agent.json` must point to the actual RPC endpoint where your remote A2A agent is deployed and accessible. +If the `url` field is an empty string, it will be automatically filled by the `--host` and `--port`, or `--base_url` provided to `adk api_server`. ## Troubleshooting @@ -143,7 +144,7 @@ When deploying the remote A2A agent to different environments (e.g., Cloud Run, - Ensure the local ADK web server is running on port 8000 - Ensure the remote A2A server is running on port 8001 - Check that no firewall is blocking localhost connections -- **Verify the `url` field in `remote_a2a/check_prime_agent/agent.json` matches the actual deployed location of your remote A2A server** +- **Verify the `url` field in `remote_a2a/check_prime_agent/agent.json` matches the actual deployed location of your remote A2A server, or if it's empty, make sure the `--host` and `--port`, or `--base_url` provided to `adk api_server` match the actual deployed location of your remote A2A server** - Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server diff --git a/contributing/samples/a2a_human_in_loop/README.md b/contributing/samples/a2a_human_in_loop/README.md index 5f90fad9f..466806f62 100644 --- a/contributing/samples/a2a_human_in_loop/README.md +++ b/contributing/samples/a2a_human_in_loop/README.md @@ -145,6 +145,7 @@ When deploying the remote approval A2A agent to different environments (e.g., Cl ``` **Important:** The `url` field in `remote_a2a/human_in_loop/agent.json` must point to the actual RPC endpoint where your remote approval A2A agent is deployed and accessible. +If the `url` field is an empty string, it will be automatically filled by the `--host` and `--port`, or `--base_url` provided to `adk api_server`. ## Troubleshooting @@ -152,7 +153,7 @@ When deploying the remote approval A2A agent to different environments (e.g., Cl - Ensure the local ADK web server is running on port 8000 - Ensure the remote A2A server is running on port 8001 - Check that no firewall is blocking localhost connections -- **Verify the `url` field in `remote_a2a/human_in_loop/agent.json` matches the actual deployed location of your remote A2A server** +- **Verify the `url` field in `remote_a2a/human_in_loop/agent.json` matches the actual deployed location of your remote A2A server, or if it's empty, make sure the `--host` and `--port`, or `--base_url` provided to `adk api_server` match the actual deployed location of your remote A2A server** - Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server **Agent Not Responding:** diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index c45fdd37e..95566e7b4 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -23,6 +23,7 @@ import os import tempfile from typing import Optional +from urllib.parse import urlparse import click from click.core import ParameterSource @@ -628,6 +629,16 @@ def decorator(func): help="Optional. The port of the server", default=8000, ) + @click.option( + "--base_url", + type=str, + help=( + "Optional. The base URL of the server. " + "Mutually exclusive with --host and --port. " + "Overrides the values of --host and --port if specified." + ), + show_default=True, + ) @click.option( "--allow_origins", help="Optional. Any additional origins to allow for CORS.", @@ -721,6 +732,7 @@ def cli_web( allow_origins: Optional[list[str]] = None, host: str = "127.0.0.1", port: int = 8000, + base_url: Optional[str] = None, trace_to_cloud: bool = False, reload: bool = True, session_service_uri: Optional[str] = None, @@ -741,6 +753,16 @@ def cli_web( adk web --session_service_uri=[uri] --port=[port] path/to/agents_dir """ logs.setup_adk_logger(getattr(logging, log_level.upper())) + if base_url is None: + base_url = f"http://{host}:{port}" + else: + parsed_url = urlparse(base_url) + host = parsed_url.hostname + port = parsed_url.port + logging.debug( + f"Ignoring --host and --port parameters, because --base-url is" + f" specified." + ) @asynccontextmanager async def _lifespan(app: FastAPI): @@ -749,7 +771,7 @@ async def _lifespan(app: FastAPI): +-----------------------------------------------------------------------------+ | ADK Web Server started | | | -| For local testing, access at http://{host}:{port}.{" "*(29 - len(str(port)))}| +| For local testing, access at {base_url}.{" "*(29 - len(str(port)))}| +-----------------------------------------------------------------------------+ """, fg="green", @@ -777,8 +799,7 @@ async def _lifespan(app: FastAPI): trace_to_cloud=trace_to_cloud, lifespan=_lifespan, a2a=a2a, - host=host, - port=port, + base_url=base_url, reload_agents=reload_agents, ) config = uvicorn.Config( @@ -812,6 +833,7 @@ def cli_api_server( allow_origins: Optional[list[str]] = None, host: str = "127.0.0.1", port: int = 8000, + base_url: Optional[str] = None, trace_to_cloud: bool = False, reload: bool = True, session_service_uri: Optional[str] = None, @@ -833,6 +855,16 @@ def cli_api_server( """ logs.setup_adk_logger(getattr(logging, log_level.upper())) + if base_url is None: + base_url = f"http://{host}:{port}" + else: + parsed_url = urlparse(base_url) + host = parsed_url.hostname + port = parsed_url.port + logging.debug( + f"Ignoring --host and --port parameters, because --base-url is" + f" specified." + ) session_service_uri = session_service_uri or session_db_url artifact_service_uri = artifact_service_uri or artifact_storage_uri config = uvicorn.Config( @@ -846,8 +878,7 @@ def cli_api_server( web=False, trace_to_cloud=trace_to_cloud, a2a=a2a, - host=host, - port=port, + base_url=base_url, reload_agents=reload_agents, ), host=host, diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 7d93b5436..6031e4881 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -64,8 +64,7 @@ def get_fast_api_app( allow_origins: Optional[list[str]] = None, web: bool, a2a: bool = False, - host: str = "127.0.0.1", - port: int = 8000, + base_url: str = "http://127.0.0.1:8000", trace_to_cloud: bool = False, reload_agents: bool = False, lifespan: Optional[Lifespan[FastAPI]] = None, @@ -352,6 +351,8 @@ async def _get_a2a_runner_async() -> Runner: logger.info("Setting up A2A agent: %s", app_name) try: + a2a_rpc_path = f"{base_url}/a2a/{app_name}" + agent_executor = A2aAgentExecutor( runner=create_a2a_runner_loader(app_name), ) @@ -363,6 +364,10 @@ async def _get_a2a_runner_async() -> Runner: with (p / "agent.json").open("r", encoding="utf-8") as f: data = json.load(f) agent_card = AgentCard(**data) + if ( + agent_card.url == "" + ): # empty url is a placeholder to be filled with the provided url + agent_card.url = a2a_rpc_path a2a_app = A2AStarletteApplication( agent_card=agent_card, diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 423581dfd..7f48b448c 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -24,6 +24,7 @@ from unittest.mock import MagicMock from unittest.mock import patch +from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH from fastapi.testclient import TestClient from google.adk.agents.base_agent import BaseAgent from google.adk.agents.run_config import RunConfig @@ -432,8 +433,7 @@ def test_app( memory_service_uri="", allow_origins=["*"], a2a=False, # Disable A2A for most tests - host="127.0.0.1", - port=8000, + base_url="http://127.0.0.1:8000", ) # Create a TestClient that doesn't start a real server @@ -502,11 +502,22 @@ def temp_agents_dir_with_a2a(): # Create agent.json file agent_card = { + "capabilities": {"pushNotifications": True, "streaming": True}, + "defaultInputModes": ["text", "text/plain"], + "defaultOutputModes": ["text", "text/plain"], "name": "test_a2a_agent", "description": "Test A2A agent", "version": "1.0.0", "author": "test", - "capabilities": ["text"], + "protocolVersion": "0.2.6", + "skills": [{ + "description": "Makes the tests pass", + "examples": ["Fix the tests."], + "id": "test_a2a_agent", + "name": "Test A2A agent", + "tags": ["testing"], + }], + "url": "", } with open(agent_dir / "agent.json", "w") as f: @@ -580,20 +591,12 @@ def test_app_with_a2a( patch( "a2a.server.request_handlers.DefaultRequestHandler" ) as mock_handler, - patch("a2a.server.apps.A2AStarletteApplication") as mock_a2a_app, ): # Configure mocks mock_task_store.return_value = MagicMock() mock_executor.return_value = MagicMock() mock_handler.return_value = MagicMock() - # Mock A2AStarletteApplication - mock_app_instance = MagicMock() - mock_app_instance.routes.return_value = ( - [] - ) # Return empty routes for testing - mock_a2a_app.return_value = mock_app_instance - # Change to temp directory original_cwd = os.getcwd() os.chdir(temp_agents_dir_with_a2a) @@ -607,8 +610,7 @@ def test_app_with_a2a( memory_service_uri="", allow_origins=["*"], a2a=True, - host="127.0.0.1", - port=8000, + base_url="http://127.0.0.1:8000", ) client = TestClient(app) @@ -881,9 +883,14 @@ def test_debug_trace(test_app): ) def test_a2a_agent_discovery(test_app_with_a2a): """Test that A2A agents are properly discovered and configured.""" - # This test mainly verifies that the A2A setup doesn't break the app + # This test verifies that the A2A setup doesn't break the app + # and that the well known card works response = test_app_with_a2a.get("/list-apps") assert response.status_code == 200 + response2 = test_app_with_a2a.get( + f"/a2a/test_a2a_agent{AGENT_CARD_WELL_KNOWN_PATH}" + ) + assert response2.status_code == 200 logger.info("A2A agent discovery test passed") From dd9149b8dd8e9790d852ba08d3092737bf7b6510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ra=C4=8Dinsk=C3=BD?= Date: Wed, 30 Jul 2025 12:48:23 +0200 Subject: [PATCH 2/2] feat: more generic url --- contributing/samples/a2a_auth/README.md | 5 ++- contributing/samples/a2a_basic/README.md | 3 +- .../samples/a2a_human_in_loop/README.md | 3 +- .../a2a_root/remote_a2a/hello_world/agent.py | 2 +- src/google/adk/a2a/utils/agent_to_a2a.py | 11 ++--- src/google/adk/cli/cli_tools_click.py | 41 ++++++++++++++++--- src/google/adk/cli/fast_api.py | 9 +++- .../unittests/a2a/utils/test_agent_to_a2a.py | 24 +++++------ tests/unittests/cli/test_fast_api.py | 35 +++++++++------- 9 files changed, 86 insertions(+), 47 deletions(-) diff --git a/contributing/samples/a2a_auth/README.md b/contributing/samples/a2a_auth/README.md index 2e4aa204d..f6b0e6cf4 100644 --- a/contributing/samples/a2a_auth/README.md +++ b/contributing/samples/a2a_auth/README.md @@ -185,7 +185,8 @@ When deploying the remote BigQuery A2A agent to different environments (e.g., Cl } ``` -**Important:** The `url` field in `remote_a2a/bigquery_agent/agent.json` must point to the actual RPC endpoint where your remote BigQuery A2A agent is deployed and accessible. +**Important:** The `url` field in `remote_a2a/bigquery_agent/agent.json` must point to the actual RPC endpoint where your remote BigQuery A2A agent is deployed and accessible. +If the `url` field is an empty string, it will be automatically filled by the `--host` and `--port`, or `--base_url` provided to `adk api_server`. ## Troubleshooting @@ -193,7 +194,7 @@ When deploying the remote BigQuery A2A agent to different environments (e.g., Cl - Ensure the local ADK web server is running on port 8000 - Ensure the remote A2A server is running on port 8001 - Check that no firewall is blocking localhost connections -- **Verify the `url` field in `remote_a2a/bigquery_agent/agent.json` matches the actual deployed location of your remote A2A server** +- **Verify the `url` field in `remote_a2a/bigquery_agent/agent.json` matches the actual deployed location of your remote A2A server, or if it's empty, make sure the `--host` and `--port`, or `--base_url` provided to `adk api_server` match the actual deployed location of your remote A2A server** - Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server diff --git a/contributing/samples/a2a_basic/README.md b/contributing/samples/a2a_basic/README.md index ca61101c2..8eeb77516 100644 --- a/contributing/samples/a2a_basic/README.md +++ b/contributing/samples/a2a_basic/README.md @@ -136,6 +136,7 @@ When deploying the remote A2A agent to different environments (e.g., Cloud Run, ``` **Important:** The `url` field in `remote_a2a/check_prime_agent/agent.json` must point to the actual RPC endpoint where your remote A2A agent is deployed and accessible. +If the `url` field is an empty string, it will be automatically filled by the `--host` and `--port`, or `--base_url` provided to `adk api_server`. ## Troubleshooting @@ -143,7 +144,7 @@ When deploying the remote A2A agent to different environments (e.g., Cloud Run, - Ensure the local ADK web server is running on port 8000 - Ensure the remote A2A server is running on port 8001 - Check that no firewall is blocking localhost connections -- **Verify the `url` field in `remote_a2a/check_prime_agent/agent.json` matches the actual deployed location of your remote A2A server** +- **Verify the `url` field in `remote_a2a/check_prime_agent/agent.json` matches the actual deployed location of your remote A2A server, or if it's empty, make sure the `--host` and `--port`, or `--base_url` provided to `adk api_server` match the actual deployed location of your remote A2A server** - Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server diff --git a/contributing/samples/a2a_human_in_loop/README.md b/contributing/samples/a2a_human_in_loop/README.md index 5f90fad9f..466806f62 100644 --- a/contributing/samples/a2a_human_in_loop/README.md +++ b/contributing/samples/a2a_human_in_loop/README.md @@ -145,6 +145,7 @@ When deploying the remote approval A2A agent to different environments (e.g., Cl ``` **Important:** The `url` field in `remote_a2a/human_in_loop/agent.json` must point to the actual RPC endpoint where your remote approval A2A agent is deployed and accessible. +If the `url` field is an empty string, it will be automatically filled by the `--host` and `--port`, or `--base_url` provided to `adk api_server`. ## Troubleshooting @@ -152,7 +153,7 @@ When deploying the remote approval A2A agent to different environments (e.g., Cl - Ensure the local ADK web server is running on port 8000 - Ensure the remote A2A server is running on port 8001 - Check that no firewall is blocking localhost connections -- **Verify the `url` field in `remote_a2a/human_in_loop/agent.json` matches the actual deployed location of your remote A2A server** +- **Verify the `url` field in `remote_a2a/human_in_loop/agent.json` matches the actual deployed location of your remote A2A server, or if it's empty, make sure the `--host` and `--port`, or `--base_url` provided to `adk api_server` match the actual deployed location of your remote A2A server** - Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server **Agent Not Responding:** 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 f1cb8a33e..0c31b66eb 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, base_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 d5e87561a..dc7d82df3 100644 --- a/src/google/adk/a2a/utils/agent_to_a2a.py +++ b/src/google/adk/a2a/utils/agent_to_a2a.py @@ -47,17 +47,13 @@ def to_a2a( agent: BaseAgent, *, - host: str = "localhost", - port: int = 8000, - protocol: str = "http", + base_url: str = "http://localhost:8000/", ) -> Starlette: """Convert an ADK agent to a A2A Starlette application. Args: agent: The ADK agent to convert - 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") + base_url: The base URL for the A2A RPC URL (default: "http://localhost:8000/") Returns: A Starlette application that can be run with uvicorn @@ -94,10 +90,9 @@ async def create_runner() -> Runner: ) # Build agent card - rpc_url = f"{protocol}://{host}:{port}/" card_builder = AgentCardBuilder( agent=agent, - rpc_url=rpc_url, + rpc_url=base_url, ) # Create a Starlette app that will be configured during startup diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index c45fdd37e..95566e7b4 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -23,6 +23,7 @@ import os import tempfile from typing import Optional +from urllib.parse import urlparse import click from click.core import ParameterSource @@ -628,6 +629,16 @@ def decorator(func): help="Optional. The port of the server", default=8000, ) + @click.option( + "--base_url", + type=str, + help=( + "Optional. The base URL of the server. " + "Mutually exclusive with --host and --port. " + "Overrides the values of --host and --port if specified." + ), + show_default=True, + ) @click.option( "--allow_origins", help="Optional. Any additional origins to allow for CORS.", @@ -721,6 +732,7 @@ def cli_web( allow_origins: Optional[list[str]] = None, host: str = "127.0.0.1", port: int = 8000, + base_url: Optional[str] = None, trace_to_cloud: bool = False, reload: bool = True, session_service_uri: Optional[str] = None, @@ -741,6 +753,16 @@ def cli_web( adk web --session_service_uri=[uri] --port=[port] path/to/agents_dir """ logs.setup_adk_logger(getattr(logging, log_level.upper())) + if base_url is None: + base_url = f"http://{host}:{port}" + else: + parsed_url = urlparse(base_url) + host = parsed_url.hostname + port = parsed_url.port + logging.debug( + f"Ignoring --host and --port parameters, because --base-url is" + f" specified." + ) @asynccontextmanager async def _lifespan(app: FastAPI): @@ -749,7 +771,7 @@ async def _lifespan(app: FastAPI): +-----------------------------------------------------------------------------+ | ADK Web Server started | | | -| For local testing, access at http://{host}:{port}.{" "*(29 - len(str(port)))}| +| For local testing, access at {base_url}.{" "*(29 - len(str(port)))}| +-----------------------------------------------------------------------------+ """, fg="green", @@ -777,8 +799,7 @@ async def _lifespan(app: FastAPI): trace_to_cloud=trace_to_cloud, lifespan=_lifespan, a2a=a2a, - host=host, - port=port, + base_url=base_url, reload_agents=reload_agents, ) config = uvicorn.Config( @@ -812,6 +833,7 @@ def cli_api_server( allow_origins: Optional[list[str]] = None, host: str = "127.0.0.1", port: int = 8000, + base_url: Optional[str] = None, trace_to_cloud: bool = False, reload: bool = True, session_service_uri: Optional[str] = None, @@ -833,6 +855,16 @@ def cli_api_server( """ logs.setup_adk_logger(getattr(logging, log_level.upper())) + if base_url is None: + base_url = f"http://{host}:{port}" + else: + parsed_url = urlparse(base_url) + host = parsed_url.hostname + port = parsed_url.port + logging.debug( + f"Ignoring --host and --port parameters, because --base-url is" + f" specified." + ) session_service_uri = session_service_uri or session_db_url artifact_service_uri = artifact_service_uri or artifact_storage_uri config = uvicorn.Config( @@ -846,8 +878,7 @@ def cli_api_server( web=False, trace_to_cloud=trace_to_cloud, a2a=a2a, - host=host, - port=port, + base_url=base_url, reload_agents=reload_agents, ), host=host, diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 7d93b5436..6031e4881 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -64,8 +64,7 @@ def get_fast_api_app( allow_origins: Optional[list[str]] = None, web: bool, a2a: bool = False, - host: str = "127.0.0.1", - port: int = 8000, + base_url: str = "http://127.0.0.1:8000", trace_to_cloud: bool = False, reload_agents: bool = False, lifespan: Optional[Lifespan[FastAPI]] = None, @@ -352,6 +351,8 @@ async def _get_a2a_runner_async() -> Runner: logger.info("Setting up A2A agent: %s", app_name) try: + a2a_rpc_path = f"{base_url}/a2a/{app_name}" + agent_executor = A2aAgentExecutor( runner=create_a2a_runner_loader(app_name), ) @@ -363,6 +364,10 @@ async def _get_a2a_runner_async() -> Runner: with (p / "agent.json").open("r", encoding="utf-8") as f: data = json.load(f) agent_card = AgentCard(**data) + if ( + agent_card.url == "" + ): # empty url is a placeholder to be filled with the provided url + agent_card.url = a2a_rpc_path a2a_app = A2AStarletteApplication( agent_card=agent_card, diff --git a/tests/unittests/a2a/utils/test_agent_to_a2a.py b/tests/unittests/a2a/utils/test_agent_to_a2a.py index 6e2044204..d89db5145 100644 --- a/tests/unittests/a2a/utils/test_agent_to_a2a.py +++ b/tests/unittests/a2a/utils/test_agent_to_a2a.py @@ -145,9 +145,7 @@ def test_to_a2a_custom_host_port_protocol( mock_card_builder_class.return_value = mock_card_builder # Act - result = to_a2a( - self.mock_agent, host="example.com", port=9000, protocol="https" - ) + result = to_a2a(self.mock_agent, base_url="https://example.com:9000") # Assert assert result == mock_app @@ -524,7 +522,7 @@ def test_to_a2a_with_custom_port_zero( mock_card_builder_class.return_value = mock_card_builder # Act - result = to_a2a(self.mock_agent, port=0) + result = to_a2a(self.mock_agent, base_url="http://localhost:0/") # Assert assert result == mock_app @@ -559,7 +557,7 @@ def test_to_a2a_with_empty_string_host( mock_card_builder_class.return_value = mock_card_builder # Act - result = to_a2a(self.mock_agent, host="") + result = to_a2a(self.mock_agent, base_url="http://:8000/") # Assert assert result == mock_app @@ -594,7 +592,7 @@ def test_to_a2a_with_negative_port( mock_card_builder_class.return_value = mock_card_builder # Act - result = to_a2a(self.mock_agent, port=-1) + result = to_a2a(self.mock_agent, base_url="http://localhost:-1/") # Assert assert result == mock_app @@ -664,7 +662,9 @@ def test_to_a2a_with_special_characters_in_host( mock_card_builder_class.return_value = mock_card_builder # Act - result = to_a2a(self.mock_agent, host="test-host.example.com") + result = to_a2a( + self.mock_agent, base_url="http://test-host.example.com:8000/" + ) # Assert assert result == mock_app @@ -699,7 +699,7 @@ def test_to_a2a_with_ip_address_host( mock_card_builder_class.return_value = mock_card_builder # Act - result = to_a2a(self.mock_agent, host="192.168.1.1") + result = to_a2a(self.mock_agent, base_url="http://192.168.1.1:8000/") # Assert assert result == mock_app @@ -734,7 +734,7 @@ def test_to_a2a_with_https_protocol( mock_card_builder_class.return_value = mock_card_builder # Act - result = to_a2a(self.mock_agent, protocol="https") + result = to_a2a(self.mock_agent, base_url="https://localhost:8000/") # Assert assert result == mock_app @@ -769,7 +769,7 @@ def test_to_a2a_with_custom_protocol( mock_card_builder_class.return_value = mock_card_builder # Act - result = to_a2a(self.mock_agent, protocol="ws") + result = to_a2a(self.mock_agent, base_url="ws://localhost:8000/") # Assert assert result == mock_app @@ -804,9 +804,7 @@ def test_to_a2a_with_all_custom_parameters( mock_card_builder_class.return_value = mock_card_builder # Act - result = to_a2a( - self.mock_agent, host="api.example.com", port=443, protocol="https" - ) + result = to_a2a(self.mock_agent, base_url="https://api.example.com") # Assert assert result == mock_app diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 423581dfd..7f48b448c 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -24,6 +24,7 @@ from unittest.mock import MagicMock from unittest.mock import patch +from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH from fastapi.testclient import TestClient from google.adk.agents.base_agent import BaseAgent from google.adk.agents.run_config import RunConfig @@ -432,8 +433,7 @@ def test_app( memory_service_uri="", allow_origins=["*"], a2a=False, # Disable A2A for most tests - host="127.0.0.1", - port=8000, + base_url="http://127.0.0.1:8000", ) # Create a TestClient that doesn't start a real server @@ -502,11 +502,22 @@ def temp_agents_dir_with_a2a(): # Create agent.json file agent_card = { + "capabilities": {"pushNotifications": True, "streaming": True}, + "defaultInputModes": ["text", "text/plain"], + "defaultOutputModes": ["text", "text/plain"], "name": "test_a2a_agent", "description": "Test A2A agent", "version": "1.0.0", "author": "test", - "capabilities": ["text"], + "protocolVersion": "0.2.6", + "skills": [{ + "description": "Makes the tests pass", + "examples": ["Fix the tests."], + "id": "test_a2a_agent", + "name": "Test A2A agent", + "tags": ["testing"], + }], + "url": "", } with open(agent_dir / "agent.json", "w") as f: @@ -580,20 +591,12 @@ def test_app_with_a2a( patch( "a2a.server.request_handlers.DefaultRequestHandler" ) as mock_handler, - patch("a2a.server.apps.A2AStarletteApplication") as mock_a2a_app, ): # Configure mocks mock_task_store.return_value = MagicMock() mock_executor.return_value = MagicMock() mock_handler.return_value = MagicMock() - # Mock A2AStarletteApplication - mock_app_instance = MagicMock() - mock_app_instance.routes.return_value = ( - [] - ) # Return empty routes for testing - mock_a2a_app.return_value = mock_app_instance - # Change to temp directory original_cwd = os.getcwd() os.chdir(temp_agents_dir_with_a2a) @@ -607,8 +610,7 @@ def test_app_with_a2a( memory_service_uri="", allow_origins=["*"], a2a=True, - host="127.0.0.1", - port=8000, + base_url="http://127.0.0.1:8000", ) client = TestClient(app) @@ -881,9 +883,14 @@ def test_debug_trace(test_app): ) def test_a2a_agent_discovery(test_app_with_a2a): """Test that A2A agents are properly discovered and configured.""" - # This test mainly verifies that the A2A setup doesn't break the app + # This test verifies that the A2A setup doesn't break the app + # and that the well known card works response = test_app_with_a2a.get("/list-apps") assert response.status_code == 200 + response2 = test_app_with_a2a.get( + f"/a2a/test_a2a_agent{AGENT_CARD_WELL_KNOWN_PATH}" + ) + assert response2.status_code == 200 logger.info("A2A agent discovery test passed")