Skip to content
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
44 changes: 38 additions & 6 deletions src/google/adk/cli/fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import asyncio
from contextlib import asynccontextmanager
import importlib
import json
Expand Down Expand Up @@ -49,10 +50,14 @@
from starlette.types import Lifespan
from watchdog.observers import Observer

from ..a2a.utils.agent_card_builder import AgentCardBuilder
from ..agents.base_agent import BaseAgent
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
from ..telemetry._agent_engine import TopSpanProcessor
from ..workflow._workflow import Workflow
from .api_server import ApiServer
from .cli_deploy import _AGENT_ENGINE_CLASS_METHODS
from .dev_server import DevServer
Expand Down Expand Up @@ -96,6 +101,21 @@ def __getattr__(name: str):
return attr


def _get_a2a_agent(agent_or_app: BaseAgent | App) -> BaseAgent | Workflow:
if isinstance(agent_or_app, App):
agent = agent_or_app.root_agent
else:
agent = agent_or_app

if isinstance(agent, (BaseAgent, Workflow)):
return agent

raise TypeError(
"AgentCardBuilder requires a BaseAgent or Workflow, got "
f"{type(agent).__name__}."
)


def _register_builder_endpoints(app: FastAPI, web: bool, agents_dir: str):
"""Registers builder endpoints if web is enabled and multipart is installed."""
if not web:
Expand Down Expand Up @@ -691,12 +711,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

Expand All @@ -716,9 +738,19 @@ 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 = _get_a2a_agent(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,
Expand Down
93 changes: 93 additions & 0 deletions tests/unittests/cli/test_fast_api_a2a.py
Original file line number Diff line number Diff line change
@@ -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"