Skip to content

Commit 8fa05c7

Browse files
committed
fix: sub-app works with dev-ui
1 parent 108b727 commit 8fa05c7

File tree

3 files changed

+128
-2
lines changed

3 files changed

+128
-2
lines changed

src/google/adk/cli/adk_web_server.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from contextlib import asynccontextmanager
1919
import logging
2020
import os
21+
from pathlib import Path
2122
import time
2223
import traceback
2324
import typing
@@ -31,6 +32,7 @@
3132
from fastapi import HTTPException
3233
from fastapi import Query
3334
from fastapi.middleware.cors import CORSMiddleware
35+
from fastapi.responses import JSONResponse
3436
from fastapi.responses import RedirectResponse
3537
from fastapi.responses import StreamingResponse
3638
from fastapi.staticfiles import StaticFiles
@@ -44,6 +46,7 @@
4446
from opentelemetry.sdk.trace import TracerProvider
4547
from pydantic import Field
4648
from pydantic import ValidationError
49+
from starlette.responses import Response
4750
from starlette.types import Lifespan
4851
from typing_extensions import override
4952
from watchdog.observers import Observer
@@ -193,6 +196,28 @@ class GetEventGraphResult(common.BaseModel):
193196
dot_src: str
194197

195198

199+
class ConfigInjectingStaticFiles(StaticFiles):
200+
"""
201+
Custom StaticFiles that injects config.json for dev-ui.
202+
Fixes https://github.com/google/adk-python/issues/2072
203+
"""
204+
205+
def __init__(self, *, directory, base_url: Optional[str], **kwargs):
206+
super().__init__(directory=directory, **kwargs)
207+
if base_url is None:
208+
base_url = ""
209+
self.base_url = base_url
210+
211+
async def get_response(self, path: str, scope) -> Response:
212+
# Check if the request is for config.json
213+
if Path(path).as_posix() == "assets/config/runtime-config.json":
214+
config = {"backendUrl": self.base_url}
215+
return JSONResponse(content=config)
216+
217+
# Otherwise, serve static files normally
218+
return await super().get_response(path, scope)
219+
220+
196221
class AdkWebServer:
197222
"""Helper class for setting up and running the ADK web server on FastAPI.
198223
@@ -284,6 +309,7 @@ def get_fast_api_app(
284309
[Observer, "AdkWebServer"], None
285310
] = lambda o, s: None,
286311
register_processors: Callable[[TracerProvider], None] = lambda o: None,
312+
base_url: Optional[str] = None,
287313
):
288314
"""Creates a FastAPI app for the ADK web server.
289315
@@ -300,6 +326,8 @@ def get_fast_api_app(
300326
tear_down_observer: Callback for cleaning up the file system observer.
301327
register_processors: Callback for additional Span processors to be added
302328
to the TracerProvider.
329+
base_url: The base URL for the web-ui, useful if fastapi app is mounted as
330+
a sub-application. If none is provided, the host is used in the frontend.
303331
304332
Returns:
305333
A FastAPI app instance.
@@ -996,7 +1024,12 @@ async def redirect_dev_ui_add_slash():
9961024

9971025
app.mount(
9981026
"/dev-ui/",
999-
StaticFiles(directory=web_assets_dir, html=True, follow_symlink=True),
1027+
ConfigInjectingStaticFiles(
1028+
directory=web_assets_dir,
1029+
base_url=base_url,
1030+
html=True,
1031+
follow_symlink=True,
1032+
),
10001033
name="static",
10011034
)
10021035

src/google/adk/cli/fast_api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@
2727
from fastapi import FastAPI
2828
from fastapi import UploadFile
2929
from fastapi.responses import FileResponse
30+
from fastapi.responses import JSONResponse
3031
from fastapi.responses import PlainTextResponse
32+
from fastapi.staticfiles import StaticFiles
3133
from opentelemetry.sdk.trace import export
3234
from opentelemetry.sdk.trace import TracerProvider
35+
from starlette.responses import Response
3336
from starlette.types import Lifespan
3437
from watchdog.observers import Observer
3538

@@ -188,7 +191,9 @@ def _parse_agent_engine_resource_name(agent_engine_id_or_resource_name):
188191
)
189192

190193
# Callbacks & other optional args for when constructing the FastAPI instance
191-
extra_fast_api_args = {}
194+
extra_fast_api_args = dict(
195+
base_url=base_url,
196+
)
192197

193198
if trace_to_cloud:
194199
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter

tests/unittests/cli/test_fast_api.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from unittest.mock import MagicMock
2525
from unittest.mock import patch
2626

27+
from fastapi import FastAPI
2728
from fastapi.testclient import TestClient
2829
from google.adk.agents.base_agent import BaseAgent
2930
from google.adk.agents.run_config import RunConfig
@@ -895,5 +896,92 @@ def test_a2a_disabled_by_default(test_app):
895896
logger.info("A2A disabled by default test passed")
896897

897898

899+
def test_runtime_config_contains_base_url(test_app):
900+
"""Test that ./assets/config/runtime-config.json contains the base_url passed to the FastAPI app."""
901+
# The test_app fixture configures the FastAPI app with base_url="http://127.0.0.1:8000"
902+
expected_base_url = "http://127.0.0.1:8000"
903+
904+
# Make a request to the runtime config endpoint
905+
response = test_app.get("/dev-ui/assets/config/runtime-config.json")
906+
907+
# Verify the response
908+
assert response.status_code == 200
909+
data = response.json()
910+
911+
# Verify the structure and content
912+
assert isinstance(data, dict)
913+
assert "backendUrl" in data
914+
assert data["backendUrl"] == expected_base_url
915+
916+
logger.info(f"Runtime config test passed - base_url: {data['backendUrl']}")
917+
918+
919+
def test_runtime_config_with_custom_base_url(
920+
mock_session_service,
921+
mock_artifact_service,
922+
mock_memory_service,
923+
mock_agent_loader,
924+
mock_eval_sets_manager,
925+
mock_eval_set_results_manager,
926+
):
927+
"""Test that runtime-config.json contains a custom base_url when provided."""
928+
custom_base_url = "https://example.com:9000/adk"
929+
930+
# Create a FastAPI app with a custom base_url
931+
with (
932+
patch("signal.signal", return_value=None),
933+
patch(
934+
"google.adk.cli.fast_api.InMemorySessionService",
935+
return_value=mock_session_service,
936+
),
937+
patch(
938+
"google.adk.cli.fast_api.InMemoryArtifactService",
939+
return_value=mock_artifact_service,
940+
),
941+
patch(
942+
"google.adk.cli.fast_api.InMemoryMemoryService",
943+
return_value=mock_memory_service,
944+
),
945+
patch(
946+
"google.adk.cli.fast_api.AgentLoader",
947+
return_value=mock_agent_loader,
948+
),
949+
patch(
950+
"google.adk.cli.fast_api.LocalEvalSetsManager",
951+
return_value=mock_eval_sets_manager,
952+
),
953+
patch(
954+
"google.adk.cli.fast_api.LocalEvalSetResultsManager",
955+
return_value=mock_eval_set_results_manager,
956+
),
957+
):
958+
adk_app = get_fast_api_app(
959+
agents_dir=".",
960+
web=True,
961+
session_service_uri="",
962+
artifact_service_uri="",
963+
memory_service_uri="",
964+
allow_origins=["*"],
965+
a2a=False,
966+
base_url=custom_base_url,
967+
)
968+
app = FastAPI()
969+
app.mount("/adk", adk_app)
970+
971+
client = TestClient(app)
972+
973+
# Make a request to the runtime config endpoint
974+
response = client.get("/adk/dev-ui/assets/config/runtime-config.json")
975+
976+
# Verify the response contains the custom base_url
977+
assert response.status_code == 200
978+
data = response.json()
979+
assert isinstance(data, dict)
980+
assert "backendUrl" in data
981+
assert data["backendUrl"] == custom_base_url
982+
983+
logger.info(f"Custom runtime config test passed - base_url: {data['backendUrl']}")
984+
985+
898986
if __name__ == "__main__":
899987
pytest.main(["-xvs", __file__])

0 commit comments

Comments
 (0)