Skip to content
Open
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
5 changes: 3 additions & 2 deletions contributing/samples/a2a_auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,16 @@ 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

**Connection Issues:**
- 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


Expand Down
3 changes: 2 additions & 1 deletion contributing/samples/a2a_basic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,15 @@ 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

**Connection Issues:**
- 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


Expand Down
3 changes: 2 additions & 1 deletion contributing/samples/a2a_human_in_loop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,15 @@ 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

**Connection Issues:**
- 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:**
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, base_url='http://localhost:8001/')
11 changes: 3 additions & 8 deletions src/google/adk/a2a/utils/agent_to_a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
41 changes: 36 additions & 5 deletions src/google/adk/cli/cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import os
import tempfile
from typing import Optional
from urllib.parse import urlparse

import click
from click.core import ParameterSource
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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,
Expand All @@ -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):
Expand All @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions src/google/adk/cli/fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
)
Expand All @@ -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,
Expand Down
16 changes: 9 additions & 7 deletions tests/unittests/a2a/utils/test_agent_to_a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def test_to_a2a_custom_host_port(
mock_card_builder_class.return_value = mock_card_builder

# Act
result = to_a2a(self.mock_agent, host="example.com", port=9000)
result = to_a2a(self.mock_agent, base_url="http://example.com:9000/")

# Assert
assert result == mock_app
Expand Down Expand Up @@ -507,7 +507,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
Expand Down Expand Up @@ -542,7 +542,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
Expand Down Expand Up @@ -577,7 +577,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
Expand Down Expand Up @@ -612,7 +612,7 @@ def test_to_a2a_with_very_large_port(
mock_card_builder_class.return_value = mock_card_builder

# Act
result = to_a2a(self.mock_agent, port=65535)
result = to_a2a(self.mock_agent, base_url="http://localhost:65535/")

# Assert
assert result == mock_app
Expand Down Expand Up @@ -647,7 +647,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
Expand Down Expand Up @@ -682,7 +684,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
Expand Down
35 changes: 21 additions & 14 deletions tests/unittests/cli/test_fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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")


Expand Down