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
31 changes: 25 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,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
Expand Down Expand Up @@ -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

Expand All @@ -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,
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"