Skip to content

fix: dev ui url works in sub-apps #2568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
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 @@ -1024,7 +1053,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