diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 0fc1652be8..8fd9ca6d99 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -14,6 +14,7 @@ from __future__ import annotations +import asyncio from contextlib import asynccontextmanager import importlib import json @@ -49,6 +50,8 @@ from starlette.types import Lifespan from watchdog.observers import Observer +from ..a2a.utils.agent_card_builder import AgentCardBuilder +from ..apps.app import App from ..auth.credential_service.in_memory_credential_service import InMemoryCredentialService from ..runners import Runner from ..telemetry._agent_engine import get_propagated_context @@ -691,12 +694,14 @@ async def _get_a2a_runner_async() -> Runner: return _get_a2a_runner_async for p in base_path.iterdir(): - # only folders with an agent.json file representing agent card are valid - # a2a agents + has_agent_card = (p / "agent.json").is_file() + has_agent_definition = ( + is_single_agent_directory(p) or (p / "__init__.py").is_file() + ) if ( p.is_file() or p.name.startswith((".", "__pycache__")) - or not (p / "agent.json").is_file() + or not (has_agent_card or has_agent_definition) ): continue @@ -716,9 +721,23 @@ async def _get_a2a_runner_async() -> Runner: push_config_store=push_config_store, ) - with (p / "agent.json").open("r", encoding="utf-8") as f: - data = json.load(f) - agent_card = AgentCard(**data) + if has_agent_card: + with (p / "agent.json").open("r", encoding="utf-8") as f: + data = json.load(f) + agent_card = AgentCard(**data) + else: + loaded_agent = agent_loader.load_agent(app_name) + agent = ( + loaded_agent.root_agent + if isinstance(loaded_agent, App) + else loaded_agent + ) + agent_card = asyncio.run( + AgentCardBuilder( + agent=agent, + rpc_url=f"http://{host}:{port}/a2a/{app_name}", + ).build() + ) a2a_app = A2AStarletteApplication( agent_card=agent_card, diff --git a/tests/unittests/cli/test_fast_api_a2a.py b/tests/unittests/cli/test_fast_api_a2a.py new file mode 100644 index 0000000000..53387addff --- /dev/null +++ b/tests/unittests/cli/test_fast_api_a2a.py @@ -0,0 +1,93 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest.mock import MagicMock +from unittest.mock import patch + +from google.adk.agents.base_agent import BaseAgent +from google.adk.cli.fast_api import get_fast_api_app + + +def test_a2a_infers_agent_card_without_agent_json(tmp_path, monkeypatch): + """A2A setup builds an agent card from agent.py when agent.json is absent.""" + + class _TestAgent(BaseAgent): + pass + + agent_dir = tmp_path / "test_a2a_agent" + agent_dir.mkdir() + (agent_dir / "agent.py").write_text("root_agent = None\n") + agent = _TestAgent( + name="test_a2a_agent", + description="Generated card from ADK agent", + ) + agent_loader = MagicMock() + agent_loader.load_agent.return_value = agent + + with ( + patch( + "google.adk.cli.fast_api.create_session_service_from_options", + return_value=MagicMock(), + ), + patch( + "google.adk.cli.fast_api.create_artifact_service_from_options", + return_value=MagicMock(), + ), + patch( + "google.adk.cli.fast_api.create_memory_service_from_options", + return_value=MagicMock(), + ), + patch( + "google.adk.cli.fast_api.LocalEvalSetsManager", + return_value=MagicMock(), + ), + patch( + "google.adk.cli.fast_api.LocalEvalSetResultsManager", + return_value=MagicMock(), + ), + patch( + "google.adk.cli.fast_api._create_task_store_from_options", + return_value=MagicMock(), + ), + patch( + "google.adk.a2a.executor.a2a_agent_executor.A2aAgentExecutor", + return_value=MagicMock(), + ), + patch( + "a2a.server.request_handlers.DefaultRequestHandler", + return_value=MagicMock(), + ), + patch("a2a.server.apps.A2AStarletteApplication") as mock_a2a_app, + ): + mock_a2a_app.return_value.routes.return_value = [] + monkeypatch.chdir(tmp_path) + + get_fast_api_app( + agents_dir=".", + agent_loader=agent_loader, + web=False, + session_service_uri="", + artifact_service_uri="", + memory_service_uri="", + a2a=True, + host="127.0.0.1", + port=8000, + ) + + agent_loader.load_agent.assert_called_once_with("test_a2a_agent") + mock_a2a_app.assert_called_once() + agent_card = mock_a2a_app.call_args.kwargs["agent_card"] + assert agent_card.name == "test_a2a_agent" + assert agent_card.description == "Generated card from ADK agent" + assert agent_card.url == "http://127.0.0.1:8000/a2a/test_a2a_agent"