From 2596af813adf4c09414a7c708725b28af2c77cf1 Mon Sep 17 00:00:00 2001 From: kemurayama <7068107+kemurayama@users.noreply.github.com> Date: Tue, 2 Jun 2026 06:59:30 +0000 Subject: [PATCH 1/4] feat(cli): add --lifespan option to api_server command --- src/google/adk/cli/cli_tools_click.py | 29 +++++++++++++++++ .../cli/utils/test_cli_tools_click.py | 32 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index 60253454e2..3ed427caf3 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -19,6 +19,7 @@ from datetime import datetime import functools import hashlib +import importlib import json import logging import os @@ -1844,6 +1845,18 @@ async def _lifespan(app: FastAPI): server.run() +def _load_lifespan_handler(lifespan_path: str) -> Any: + """Dynamically import a lifespan handler from a string path.""" + try: + module_name, func_name = lifespan_path.rsplit(".", 1) + module = importlib.import_module(module_name) + return getattr(module, func_name) + except Exception as e: + raise click.ClickException( + f"Failed to load lifespan handler '{lifespan_path}': {e}" + ) from e + + @main.command("api_server") @feature_options() # The directory of agents, where each subdirectory is a single agent. @@ -1866,6 +1879,15 @@ async def _lifespan(app: FastAPI): "Automatically create a session if it doesn't exist when calling /run." ), ) +@click.option( + "--lifespan", + type=str, + default=None, + help=( + "Optional. The import path to a lifespan context manager (e.g.," + " 'path.to.module.lifespan_handler')." + ), +) def cli_api_server( agents_dir: str, eval_storage_uri: Optional[str] = None, @@ -1888,6 +1910,7 @@ def cli_api_server( extra_plugins: Optional[list[str]] = None, auto_create_session: bool = False, trigger_sources: Optional[list[str]] = None, + lifespan: Optional[str] = None, ): """Starts a FastAPI server for agents. @@ -1902,6 +1925,11 @@ def cli_api_server( artifact_service_uri = artifact_service_uri or artifact_storage_uri logs.setup_adk_logger(getattr(logging, log_level.upper())) + if agents_dir and agents_dir not in sys.path: + sys.path.insert(0, agents_dir) + + lifespan_handler = _load_lifespan_handler(lifespan) if lifespan else None + config = uvicorn.Config( get_fast_api_app( agents_dir=agents_dir, @@ -1922,6 +1950,7 @@ def cli_api_server( extra_plugins=extra_plugins, auto_create_session=auto_create_session, trigger_sources=trigger_sources, + lifespan=lifespan_handler, ), host=host, port=port, diff --git a/tests/unittests/cli/utils/test_cli_tools_click.py b/tests/unittests/cli/utils/test_cli_tools_click.py index 0406442b80..ec15bd4a74 100644 --- a/tests/unittests/cli/utils/test_cli_tools_click.py +++ b/tests/unittests/cli/utils/test_cli_tools_click.py @@ -1161,6 +1161,38 @@ def test_cli_api_server_invokes_uvicorn( assert _patch_uvicorn.calls, "uvicorn.Server.run must be called" +def test_cli_api_server_passes_lifespan( + tmp_path: Path, _patch_uvicorn: _Recorder, monkeypatch: pytest.MonkeyPatch +) -> None: + """`adk api_server` should pass loaded lifespan handler to get_fast_api_app.""" + agents_dir = tmp_path / "agents_api_lifespan" + agents_dir.mkdir() + lifespan_file = agents_dir / "dummy_lifespan.py" + lifespan_file.write_text(""" +from contextlib import asynccontextmanager +@asynccontextmanager +async def dummy_handler(app): + yield +""") + mock_get_app = _Recorder() + monkeypatch.setattr(cli_tools_click, "get_fast_api_app", mock_get_app) + runner = CliRunner() + result = runner.invoke( + cli_tools_click.main, + [ + "api_server", + str(agents_dir), + "--lifespan", + "dummy_lifespan.dummy_handler", + ], + ) + assert result.exit_code == 0, f"Output: {result.output}" + assert mock_get_app.calls + called_kwargs = mock_get_app.calls[0][1] + assert called_kwargs.get("lifespan") is not None + assert called_kwargs.get("lifespan").__name__ == "dummy_handler" + + def test_cli_web_passes_service_uris( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, _patch_uvicorn: _Recorder ) -> None: From d6598a6b5c396740110db1cb895c9033062b5a63 Mon Sep 17 00:00:00 2001 From: kemurayama <7068107+kemurayama@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:24:05 +0000 Subject: [PATCH 2/4] fix(cli): Import Any to fix NameError and shorten test docstring --- src/google/adk/cli/cli_tools_click.py | 2 ++ tests/unittests/cli/utils/test_cli_tools_click.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index 3ed427caf3..29cf69985f 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -27,8 +27,10 @@ import sys import tempfile import textwrap +from typing import Any from typing import Optional + import click from click.core import ParameterSource from fastapi import FastAPI diff --git a/tests/unittests/cli/utils/test_cli_tools_click.py b/tests/unittests/cli/utils/test_cli_tools_click.py index ec15bd4a74..a25bd86b7b 100644 --- a/tests/unittests/cli/utils/test_cli_tools_click.py +++ b/tests/unittests/cli/utils/test_cli_tools_click.py @@ -1164,7 +1164,7 @@ def test_cli_api_server_invokes_uvicorn( def test_cli_api_server_passes_lifespan( tmp_path: Path, _patch_uvicorn: _Recorder, monkeypatch: pytest.MonkeyPatch ) -> None: - """`adk api_server` should pass loaded lifespan handler to get_fast_api_app.""" + """api_server should pass lifespan handler to get_fast_api_app.""" agents_dir = tmp_path / "agents_api_lifespan" agents_dir.mkdir() lifespan_file = agents_dir / "dummy_lifespan.py" From 5bdc05801d54f3fd4d13fe2ac648cb470997c382 Mon Sep 17 00:00:00 2001 From: kemurayama <7068107+kemurayama@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:26:04 +0000 Subject: [PATCH 3/4] style: Clean up formatting and empty lines in cli_tools_click.py --- src/google/adk/cli/cli_tools_click.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index 29cf69985f..8051d164c8 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -30,7 +30,6 @@ from typing import Any from typing import Optional - import click from click.core import ParameterSource from fastapi import FastAPI @@ -2374,7 +2373,6 @@ def cli_migrate_session( " It can only be `root_agent` or `app`. (default: `root_agent`)" ), ) - @click.option( "--env_file", type=str, @@ -2450,7 +2448,6 @@ def cli_deploy_agent_engine( adk_app: str, adk_app_object: Optional[str], temp_folder: Optional[str], - env_file: str, requirements_file: str, absolutize_imports: bool, @@ -2491,7 +2488,6 @@ def cli_deploy_agent_engine( description=description, adk_app=adk_app, temp_folder=temp_folder, - env_file=env_file, requirements_file=requirements_file, absolutize_imports=absolutize_imports, From 36b75b0a20a41a3405ac1f5a94fff452cdf433de Mon Sep 17 00:00:00 2001 From: kemurayama <7068107+kemurayama@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:34:00 +0000 Subject: [PATCH 4/4] refactor(cli): Reference get_fast_api_app via fast_api module to allow correct patching Import the fast_api module rather than importing get_fast_api_app directly to prevent local bindings in cli_tools_click.py from bypassing the unit test monkeypatches. Additionally, use modern union type syntax (str | None) for the --lifespan option type hint. --- src/google/adk/cli/cli_tools_click.py | 8 ++++---- tests/unittests/cli/utils/test_cli_tools_click.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index 8051d164c8..70f7e4512d 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -37,13 +37,13 @@ from . import cli_create from . import cli_deploy +from . import fast_api from .. import version from ..agents.run_config import StreamingMode from ..evaluation.constants import MISSING_EVAL_DEPENDENCIES_MESSAGE from ..features import FeatureName from ..features import override_feature_enabled from .cli import run_cli -from .fast_api import get_fast_api_app from .utils import envs from .utils import evals from .utils import logs @@ -1813,7 +1813,7 @@ async def _lifespan(app: FastAPI): fg="green", ) - app = get_fast_api_app( + app = fast_api.get_fast_api_app( agents_dir=agents_dir, session_service_uri=session_service_uri, artifact_service_uri=artifact_service_uri, @@ -1911,7 +1911,7 @@ def cli_api_server( extra_plugins: Optional[list[str]] = None, auto_create_session: bool = False, trigger_sources: Optional[list[str]] = None, - lifespan: Optional[str] = None, + lifespan: str | None = None, ): """Starts a FastAPI server for agents. @@ -1932,7 +1932,7 @@ def cli_api_server( lifespan_handler = _load_lifespan_handler(lifespan) if lifespan else None config = uvicorn.Config( - get_fast_api_app( + fast_api.get_fast_api_app( agents_dir=agents_dir, session_service_uri=session_service_uri, artifact_service_uri=artifact_service_uri, diff --git a/tests/unittests/cli/utils/test_cli_tools_click.py b/tests/unittests/cli/utils/test_cli_tools_click.py index a25bd86b7b..d7af06ffdd 100644 --- a/tests/unittests/cli/utils/test_cli_tools_click.py +++ b/tests/unittests/cli/utils/test_cli_tools_click.py @@ -1138,7 +1138,7 @@ def test_cli_web_invokes_uvicorn( agents_dir = tmp_path / "agents" agents_dir.mkdir() monkeypatch.setattr( - cli_tools_click, "get_fast_api_app", lambda **_k: object() + "google.adk.cli.fast_api.get_fast_api_app", lambda **_k: object() ) runner = CliRunner() result = runner.invoke(cli_tools_click.main, ["web", str(agents_dir)]) @@ -1153,7 +1153,7 @@ def test_cli_api_server_invokes_uvicorn( agents_dir = tmp_path / "agents_api" agents_dir.mkdir() monkeypatch.setattr( - cli_tools_click, "get_fast_api_app", lambda **_k: object() + "google.adk.cli.fast_api.get_fast_api_app", lambda **_k: object() ) runner = CliRunner() result = runner.invoke(cli_tools_click.main, ["api_server", str(agents_dir)]) @@ -1175,7 +1175,7 @@ async def dummy_handler(app): yield """) mock_get_app = _Recorder() - monkeypatch.setattr(cli_tools_click, "get_fast_api_app", mock_get_app) + monkeypatch.setattr("google.adk.cli.fast_api.get_fast_api_app", mock_get_app) runner = CliRunner() result = runner.invoke( cli_tools_click.main, @@ -1201,7 +1201,7 @@ def test_cli_web_passes_service_uris( agents_dir.mkdir() mock_get_app = _Recorder() - monkeypatch.setattr(cli_tools_click, "get_fast_api_app", mock_get_app) + monkeypatch.setattr("google.adk.cli.fast_api.get_fast_api_app", mock_get_app) runner = CliRunner() result = runner.invoke( @@ -1236,7 +1236,7 @@ def test_cli_web_warns_and_maps_deprecated_uris( agents_dir.mkdir() mock_get_app = _Recorder() - monkeypatch.setattr(cli_tools_click, "get_fast_api_app", mock_get_app) + monkeypatch.setattr("google.adk.cli.fast_api.get_fast_api_app", mock_get_app) runner = CliRunner() result = runner.invoke(