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
9 changes: 7 additions & 2 deletions src/google/adk/a2a/utils/agent_card_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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')
)
Comment on lines +65 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic can be simplified by using the or operator to provide the default URL, making the code more concise and readable.

    self._rpc_url = normalize_public_url(
        rpc_url or 'http://localhost:80/a2a'
    )

self._capabilities = capabilities or AgentCapabilities()
self._doc_url = doc_url
self._provider = provider
Expand All @@ -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,
Expand Down
26 changes: 22 additions & 4 deletions src/google/adk/a2a/utils/agent_to_a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions src/google/adk/a2a/utils/url_utils.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +8 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic here is a bit dense, especially path = (path or "/").strip(). While it works, a more explicit implementation would be more readable and easier to maintain.

def normalize_path(path: str) -> str:
  """Normalize an application path to a canonical mount path."""
  path = (path or "").strip()
  if not path or path == "/":
    return "/"
  if not path.startswith("/"):
    path = f"/{path}"
  return path.rstrip("/")



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}"
Comment on lines +34 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation always includes the port in the generated URL. For standard ports (80 for HTTP, 443 for HTTPS), it's common practice to omit them from the URL for better canonicalization.

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)
  port_str = f":{port}"
  if (protocol.lower() == "http" and port == 80) or (
      protocol.lower() == "https" and port == 443
  ):
    port_str = ""
  base = f"{protocol}://{host}{port_str}"
  if normalized_path == "/":
    return base
  return f"{base}{normalized_path}"