Skip to content
2 changes: 1 addition & 1 deletion contributing/samples/a2a_auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ 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 base URL provided to `get_fast_api_app`.

## Troubleshooting

Expand Down
2 changes: 1 addition & 1 deletion contributing/samples/a2a_basic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,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.
**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 base URL provided to `get_fast_api_app`.

## Troubleshooting

Expand Down
2 changes: 1 addition & 1 deletion contributing/samples/a2a_human_in_loop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,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.
**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 base URL provided to `get_fast_api_app`.

## Troubleshooting

Expand Down
36 changes: 35 additions & 1 deletion src/google/adk/cli/adk_web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from contextlib import asynccontextmanager
import logging
import os
from pathlib import Path
import time
import traceback
import typing
Expand All @@ -31,6 +32,7 @@
from fastapi import HTTPException
from fastapi import Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.responses import RedirectResponse
from fastapi.responses import StreamingResponse
from fastapi.staticfiles import StaticFiles
Expand All @@ -44,6 +46,8 @@
from opentelemetry.sdk.trace import TracerProvider
from pydantic import Field
from pydantic import ValidationError
from starlette.responses import Response
from starlette.staticfiles import PathLike
from starlette.types import Lifespan
from typing_extensions import override
from watchdog.observers import Observer
Expand Down Expand Up @@ -197,6 +201,28 @@ class GetEventGraphResult(common.BaseModel):
dot_src: str


class ConfigInjectingStaticFiles(StaticFiles):
"""
Custom StaticFiles that injects config.json for dev-ui.
Fixes https://github.com/google/adk-python/issues/2072
"""

def __init__(self, *, directory: PathLike | None = None, base_url: Optional[str] = None, **kwargs):
super().__init__(directory=directory, **kwargs)
if base_url is None:
base_url = ""
self.base_url = base_url

async def get_response(self, path: str, scope) -> Response:
# Check if the request is for config.json
if Path(path).as_posix() == "assets/config/runtime-config.json":
config = {"backendUrl": self.base_url}
return JSONResponse(content=config)

# Otherwise, serve static files normally
return await super().get_response(path, scope)


class AdkWebServer:
"""Helper class for setting up and running the ADK web server on FastAPI.

Expand Down Expand Up @@ -288,6 +314,7 @@ def get_fast_api_app(
[Observer, "AdkWebServer"], None
] = lambda o, s: None,
register_processors: Callable[[TracerProvider], None] = lambda o: None,
base_url: Optional[str] = None,
):
"""Creates a FastAPI app for the ADK web server.

Expand All @@ -304,6 +331,8 @@ def get_fast_api_app(
tear_down_observer: Callback for cleaning up the file system observer.
register_processors: Callback for additional Span processors to be added
to the TracerProvider.
base_url: The base URL for the web-ui, useful if fastapi app is mounted as
a sub-application. If none is provided, the host is used in the frontend.

Returns:
A FastAPI app instance.
Expand Down Expand Up @@ -1026,7 +1055,12 @@ async def redirect_dev_ui_add_slash():

app.mount(
"/dev-ui/",
StaticFiles(directory=web_assets_dir, html=True, follow_symlink=True),
ConfigInjectingStaticFiles(
directory=web_assets_dir,
base_url=base_url,
html=True,
follow_symlink=True,
),
name="static",
)

Expand Down
31 changes: 14 additions & 17 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 @@ -616,18 +617,12 @@ def fast_api_common_options():

def decorator(func):
@click.option(
"--host",
"--base_url",
type=str,
help="Optional. The binding host of the server",
default="127.0.0.1",
help="Optional. The base URL of the server.",
default="http://127.0.0.1:8000",
show_default=True,
)
@click.option(
"--port",
type=int,
help="Optional. The port of the server",
default=8000,
)
@click.option(
"--allow_origins",
help="Optional. Any additional origins to allow for CORS.",
Expand Down Expand Up @@ -719,8 +714,7 @@ def cli_web(
eval_storage_uri: Optional[str] = None,
log_level: str = "INFO",
allow_origins: Optional[list[str]] = None,
host: str = "127.0.0.1",
port: int = 8000,
base_url="http://127.0.0.1:8000",
trace_to_cloud: bool = False,
reload: bool = True,
session_service_uri: Optional[str] = None,
Expand All @@ -741,6 +735,9 @@ def cli_web(
adk web --session_service_uri=[uri] --port=[port] path/to/agents_dir
"""
logs.setup_adk_logger(getattr(logging, log_level.upper()))
parsed_url = urlparse(base_url)
host = parsed_url.hostname
port = parsed_url.port

@asynccontextmanager
async def _lifespan(app: FastAPI):
Expand Down Expand Up @@ -777,8 +774,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 @@ -810,8 +806,7 @@ def cli_api_server(
eval_storage_uri: Optional[str] = None,
log_level: str = "INFO",
allow_origins: Optional[list[str]] = None,
host: str = "127.0.0.1",
port: int = 8000,
base_url="http://127.0.0.1:8000",
trace_to_cloud: bool = False,
reload: bool = True,
session_service_uri: Optional[str] = None,
Expand All @@ -833,6 +828,9 @@ def cli_api_server(
"""
logs.setup_adk_logger(getattr(logging, log_level.upper()))

parsed_url = urlparse(base_url)
host = parsed_url.hostname
port = parsed_url.port
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 +844,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
11 changes: 8 additions & 3 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 @@ -189,7 +188,9 @@ def _parse_agent_engine_resource_name(agent_engine_id_or_resource_name):
)

# Callbacks & other optional args for when constructing the FastAPI instance
extra_fast_api_args = {}
extra_fast_api_args = dict(
base_url=base_url,
)

if trace_to_cloud:
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
Expand Down Expand Up @@ -352,6 +353,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 +366,8 @@ 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
Loading