Skip to content

Commit 133fca9

Browse files
committed
feat(a2a): Infer agent cards from ADK agents
Allow A2A setup in get_fast_api_app to serve ADK agent directories without a checked-in agent.json by building the AgentCard from the loaded ADK agent. Fixes #2237 Tests: uv run python -m pytest tests/unittests/cli/test_fast_api_a2a.py tests/unittests/cli/test_fast_api.py -k a2a tests/unittests/a2a/utils/test_agent_card_builder.py tests/unittests/a2a/utils/test_agent_to_a2a.py -q uv run pre-commit run --files src/google/adk/cli/fast_api.py tests/unittests/cli/test_fast_api_a2a.py
1 parent 6bc9c9f commit 133fca9

2 files changed

Lines changed: 118 additions & 6 deletions

File tree

src/google/adk/cli/fast_api.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
import asyncio
1718
from contextlib import asynccontextmanager
1819
import importlib
1920
import json
@@ -49,6 +50,8 @@
4950
from starlette.types import Lifespan
5051
from watchdog.observers import Observer
5152

53+
from ..a2a.utils.agent_card_builder import AgentCardBuilder
54+
from ..apps.app import App
5255
from ..auth.credential_service.in_memory_credential_service import InMemoryCredentialService
5356
from ..runners import Runner
5457
from ..telemetry._agent_engine import get_propagated_context
@@ -691,12 +694,14 @@ async def _get_a2a_runner_async() -> Runner:
691694
return _get_a2a_runner_async
692695

693696
for p in base_path.iterdir():
694-
# only folders with an agent.json file representing agent card are valid
695-
# a2a agents
697+
has_agent_card = (p / "agent.json").is_file()
698+
has_agent_definition = (
699+
is_single_agent_directory(p) or (p / "__init__.py").is_file()
700+
)
696701
if (
697702
p.is_file()
698703
or p.name.startswith((".", "__pycache__"))
699-
or not (p / "agent.json").is_file()
704+
or not (has_agent_card or has_agent_definition)
700705
):
701706
continue
702707

@@ -716,9 +721,23 @@ async def _get_a2a_runner_async() -> Runner:
716721
push_config_store=push_config_store,
717722
)
718723

719-
with (p / "agent.json").open("r", encoding="utf-8") as f:
720-
data = json.load(f)
721-
agent_card = AgentCard(**data)
724+
if has_agent_card:
725+
with (p / "agent.json").open("r", encoding="utf-8") as f:
726+
data = json.load(f)
727+
agent_card = AgentCard(**data)
728+
else:
729+
loaded_agent = agent_loader.load_agent(app_name)
730+
agent = (
731+
loaded_agent.root_agent
732+
if isinstance(loaded_agent, App)
733+
else loaded_agent
734+
)
735+
agent_card = asyncio.run(
736+
AgentCardBuilder(
737+
agent=agent,
738+
rpc_url=f"http://{host}:{port}/a2a/{app_name}",
739+
).build()
740+
)
722741

723742
a2a_app = A2AStarletteApplication(
724743
agent_card=agent_card,
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from unittest.mock import MagicMock
16+
from unittest.mock import patch
17+
18+
from google.adk.agents.base_agent import BaseAgent
19+
from google.adk.cli.fast_api import get_fast_api_app
20+
21+
22+
def test_a2a_infers_agent_card_without_agent_json(tmp_path, monkeypatch):
23+
"""A2A setup builds an agent card from agent.py when agent.json is absent."""
24+
25+
class _TestAgent(BaseAgent):
26+
pass
27+
28+
agent_dir = tmp_path / "test_a2a_agent"
29+
agent_dir.mkdir()
30+
(agent_dir / "agent.py").write_text("root_agent = None\n")
31+
agent = _TestAgent(
32+
name="test_a2a_agent",
33+
description="Generated card from ADK agent",
34+
)
35+
agent_loader = MagicMock()
36+
agent_loader.load_agent.return_value = agent
37+
38+
with (
39+
patch(
40+
"google.adk.cli.fast_api.create_session_service_from_options",
41+
return_value=MagicMock(),
42+
),
43+
patch(
44+
"google.adk.cli.fast_api.create_artifact_service_from_options",
45+
return_value=MagicMock(),
46+
),
47+
patch(
48+
"google.adk.cli.fast_api.create_memory_service_from_options",
49+
return_value=MagicMock(),
50+
),
51+
patch(
52+
"google.adk.cli.fast_api.LocalEvalSetsManager",
53+
return_value=MagicMock(),
54+
),
55+
patch(
56+
"google.adk.cli.fast_api.LocalEvalSetResultsManager",
57+
return_value=MagicMock(),
58+
),
59+
patch(
60+
"google.adk.cli.fast_api._create_task_store_from_options",
61+
return_value=MagicMock(),
62+
),
63+
patch(
64+
"google.adk.a2a.executor.a2a_agent_executor.A2aAgentExecutor",
65+
return_value=MagicMock(),
66+
),
67+
patch(
68+
"a2a.server.request_handlers.DefaultRequestHandler",
69+
return_value=MagicMock(),
70+
),
71+
patch("a2a.server.apps.A2AStarletteApplication") as mock_a2a_app,
72+
):
73+
mock_a2a_app.return_value.routes.return_value = []
74+
monkeypatch.chdir(tmp_path)
75+
76+
get_fast_api_app(
77+
agents_dir=".",
78+
agent_loader=agent_loader,
79+
web=False,
80+
session_service_uri="",
81+
artifact_service_uri="",
82+
memory_service_uri="",
83+
a2a=True,
84+
host="127.0.0.1",
85+
port=8000,
86+
)
87+
88+
agent_loader.load_agent.assert_called_once_with("test_a2a_agent")
89+
mock_a2a_app.assert_called_once()
90+
agent_card = mock_a2a_app.call_args.kwargs["agent_card"]
91+
assert agent_card.name == "test_a2a_agent"
92+
assert agent_card.description == "Generated card from ADK agent"
93+
assert agent_card.url == "http://127.0.0.1:8000/a2a/test_a2a_agent"

0 commit comments

Comments
 (0)