From b02edd44fdb2d462af98d4329396f391e28fb507 Mon Sep 17 00:00:00 2001 From: jayy-77 <1427jay@gmail.com> Date: Sat, 14 Feb 2026 19:29:55 +0530 Subject: [PATCH] Introduce URL utilities for normalizing public URLs and paths --- .../adk/a2a/utils/agent_card_builder.py | 9 ++++- src/google/adk/a2a/utils/agent_to_a2a.py | 26 ++++++++++-- src/google/adk/a2a/utils/url_utils.py | 40 +++++++++++++++++++ 3 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 src/google/adk/a2a/utils/url_utils.py diff --git a/src/google/adk/a2a/utils/agent_card_builder.py b/src/google/adk/a2a/utils/agent_card_builder.py index 1e8cecad79..ad0abc12af 100644 --- a/src/google/adk/a2a/utils/agent_card_builder.py +++ b/src/google/adk/a2a/utils/agent_card_builder.py @@ -33,6 +33,7 @@ from ...agents.sequential_agent import SequentialAgent from ...tools.example_tool import ExampleTool from ..experimental import a2a_experimental +from .url_utils import normalize_public_url logger = logging.getLogger('google_adk.' + __name__) @@ -61,7 +62,11 @@ def __init__( raise ValueError('Agent cannot be None or empty.') self._agent = agent - self._rpc_url = rpc_url or 'http://localhost:80/a2a' + self._rpc_url = ( + normalize_public_url(rpc_url) + if rpc_url + else normalize_public_url('http://localhost:80/a2a') + ) self._capabilities = capabilities or AgentCapabilities() self._doc_url = doc_url self._provider = provider @@ -79,7 +84,7 @@ async def build(self) -> AgentCard: name=self._agent.name, description=self._agent.description or 'An ADK Agent', doc_url=self._doc_url, - url=f"{self._rpc_url.rstrip('/')}", + url=self._rpc_url, version=self._agent_version, capabilities=self._capabilities, skills=all_skills, diff --git a/src/google/adk/a2a/utils/agent_to_a2a.py b/src/google/adk/a2a/utils/agent_to_a2a.py index 155888bcab..87c7370e3e 100644 --- a/src/google/adk/a2a/utils/agent_to_a2a.py +++ b/src/google/adk/a2a/utils/agent_to_a2a.py @@ -33,6 +33,9 @@ from ..executor.a2a_agent_executor import A2aAgentExecutor from ..experimental import a2a_experimental from .agent_card_builder import AgentCardBuilder +from .url_utils import build_public_url +from .url_utils import normalize_path +from .url_utils import normalize_public_url def _load_agent_card( @@ -77,6 +80,8 @@ def to_a2a( host: str = "localhost", port: int = 8000, protocol: str = "http", + path: str = "/", + http_url: Optional[str] = None, agent_card: Optional[Union[AgentCard, str]] = None, runner: Optional[Runner] = None, ) -> Starlette: @@ -87,6 +92,10 @@ 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") + path: The URL path prefix used to expose this A2A app (default: "/"). + http_url: Optional public URL where this agent is accessible. When + provided, this value overrides host/port/protocol for the + published agent card URL. agent_card: Optional pre-built AgentCard object or path to agent card JSON. If not provided, will be built automatically from the agent. @@ -122,6 +131,7 @@ async def create_runner() -> Runner: # Create A2A components task_store = InMemoryTaskStore() + normalized_path = normalize_path(path) agent_executor = A2aAgentExecutor( runner=runner or create_runner, @@ -132,7 +142,11 @@ async def create_runner() -> Runner: ) # Use provided agent card or build one from the agent - rpc_url = f"{protocol}://{host}:{port}/" + rpc_url = ( + normalize_public_url(http_url) + if http_url + else build_public_url(protocol, host, port, normalized_path) + ) provided_agent_card = _load_agent_card(agent_card) card_builder = AgentCardBuilder( @@ -158,9 +172,13 @@ async def setup_a2a(): ) # Add A2A routes to the main app - a2a_app.add_routes_to_app( - app, - ) + if normalized_path == "/": + a2a_app.add_routes_to_app(app) + return + + routed_app = Starlette() + a2a_app.add_routes_to_app(routed_app) + app.mount(normalized_path, routed_app) # Store the setup function to be called during startup app.add_event_handler("startup", setup_a2a) diff --git a/src/google/adk/a2a/utils/url_utils.py b/src/google/adk/a2a/utils/url_utils.py new file mode 100644 index 0000000000..541665d72e --- /dev/null +++ b/src/google/adk/a2a/utils/url_utils.py @@ -0,0 +1,40 @@ +"""Utilities for normalizing A2A public URLs and mount paths.""" + +from __future__ import annotations + +from urllib.parse import urlparse + + +def normalize_path(path: str) -> str: + """Normalize an application path to a canonical mount path.""" + path = (path or "/").strip() + if not path: + return "/" + if not path.startswith("/"): + path = f"/{path}" + if path != "/": + path = path.rstrip("/") + return path + + +def normalize_public_url(url: str) -> str: + """Normalize a public URL and validate required URL components.""" + parsed = urlparse(url) + if not parsed.scheme or not parsed.netloc: + raise ValueError( + "http_url must include a scheme and host, for example " + "'https://example.com/analysis-agent'." + ) + normalized_path = normalize_path(parsed.path) + if normalized_path == "/": + return f"{parsed.scheme}://{parsed.netloc}" + return f"{parsed.scheme}://{parsed.netloc}{normalized_path}" + + +def build_public_url(protocol: str, host: str, port: int, path: str) -> str: + """Build a normalized public URL from host, port, protocol and path.""" + normalized_path = normalize_path(path) + base = f"{protocol}://{host}:{port}" + if normalized_path == "/": + return base + return f"{base}{normalized_path}"