Skip to content

Commit b61bfb3

Browse files
authored
update key names, show supported catalogs in agent card (#405)
1 parent 3b98a43 commit b61bfb3

File tree

16 files changed

+650
-2631
lines changed

16 files changed

+650
-2631
lines changed

a2a_agents/python/a2ui_extension/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
This is the Python implementation of the a2ui extension.
44

5+
## Running Tests
6+
7+
1. Navigate to the a2ui_extension dir:
8+
9+
```bash
10+
cd a2a_agents/python/a2ui_extension
11+
```
12+
13+
2. Run the tests
14+
15+
```bash
16+
uv run --with pytest pytest tests/test_extension.py
17+
```
18+
519
## Disclaimer
620

721
Important: The sample code provided is for demonstration purposes and illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.

a2a_agents/python/a2ui_extension/src/a2ui/a2ui_extension.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
import logging
16-
from typing import Any, Optional
16+
from typing import Any, Optional, List
1717

1818
from a2a.server.agent_execution import RequestContext
1919
from a2a.types import AgentExtension, Part, DataPart
@@ -29,7 +29,7 @@
2929
SUPPORTED_CATALOG_IDS_KEY = "supportedCatalogIds"
3030
INLINE_CATALOGS_KEY = "inlineCatalogs"
3131

32-
STANDARD_CATALOG_ID = "https://raw.githubusercontent.com/google/A2UI/refs/heads/main/specification/0.8/json/standard_catalog_definition.json"
32+
STANDARD_CATALOG_ID = "https://github.com/google/A2UI/blob/main/specification/0.8/json/standard_catalog_definition.json"
3333

3434
def create_a2ui_part(a2ui_data: dict[str, Any]) -> Part:
3535
"""Creates an A2A Part containing A2UI data.
@@ -80,20 +80,28 @@ def get_a2ui_datapart(part: Part) -> Optional[DataPart]:
8080
return None
8181

8282

83+
AGENT_EXTENSION_SUPPORTED_CATALOG_IDS_KEY = "supportedCatalogIds"
84+
AGENT_EXTENSION_ACCEPTS_INLINE_CATALOGS_KEY = "acceptsInlineCatalogs"
85+
8386
def get_a2ui_agent_extension(
84-
accepts_inline_custom_catalog: bool = False,
87+
accepts_inline_catalogs: bool = False,
88+
supported_catalog_ids: List[str] = [],
8589
) -> AgentExtension:
8690
"""Creates the A2UI AgentExtension configuration.
8791
8892
Args:
89-
accepts_inline_custom_catalog: Whether the agent accepts inline custom catalogs.
93+
accepts_inline_catalogs: Whether the agent accepts inline custom catalogs.
94+
supported_catalog_ids: All pre-defined catalogs the agent is known to support.
9095
9196
Returns:
9297
The configured A2UI AgentExtension.
9398
"""
9499
params = {}
95-
if accepts_inline_custom_catalog:
96-
params["acceptsInlineCustomCatalog"] = True # Only set if not default of False
100+
if accepts_inline_catalogs:
101+
params[AGENT_EXTENSION_ACCEPTS_INLINE_CATALOGS_KEY] = True # Only set if not default of False
102+
103+
if supported_catalog_ids:
104+
params[AGENT_EXTENSION_SUPPORTED_CATALOG_IDS_KEY] = supported_catalog_ids
97105

98106
return AgentExtension(
99107
uri=A2UI_EXTENSION_URI,

a2a_agents/python/a2ui_extension/tests/test_extension.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from a2a.server.agent_execution import RequestContext
1717
from a2a.types import DataPart, TextPart, Part
1818
from a2ui import a2ui_extension
19-
19+
from a2ui.a2ui_extension import AGENT_EXTENSION_ACCEPTS_INLINE_CATALOGS_KEY, AGENT_EXTENSION_SUPPORTED_CATALOG_IDS_KEY
2020
from unittest.mock import MagicMock
2121

2222

@@ -64,12 +64,24 @@ def test_get_a2ui_agent_extension():
6464
assert agent_extension.params is None
6565

6666

67-
def test_get_a2ui_agent_extension_with_inline_custom_catalog():
67+
def test_get_a2ui_agent_extension_with_accepts_inline_catalogs():
68+
accepts_inline_catalogs = True
69+
agent_extension = a2ui_extension.get_a2ui_agent_extension(
70+
accepts_inline_catalogs=accepts_inline_catalogs
71+
)
72+
assert agent_extension.uri == a2ui_extension.A2UI_EXTENSION_URI
73+
assert agent_extension.params is not None
74+
assert agent_extension.params.get(AGENT_EXTENSION_ACCEPTS_INLINE_CATALOGS_KEY) == accepts_inline_catalogs
75+
76+
77+
def test_get_a2ui_agent_extension_with_supported_catalog_ids():
78+
supported_catalog_ids = ["a", "b", "c"]
6879
agent_extension = a2ui_extension.get_a2ui_agent_extension(
69-
accepts_inline_custom_catalog=True
80+
supported_catalog_ids=supported_catalog_ids
7081
)
7182
assert agent_extension.uri == a2ui_extension.A2UI_EXTENSION_URI
7283
assert agent_extension.params is not None
84+
assert agent_extension.params.get(AGENT_EXTENSION_SUPPORTED_CATALOG_IDS_KEY) == supported_catalog_ids
7385

7486

7587
def test_try_activate_a2ui_extension():

a2a_agents/python/a2ui_extension/uv.lock

Lines changed: 511 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/agent/adk/orchestrator/__main__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ def main(host, port, subagent_urls):
5050

5151
base_url = f"http://{host}:{port}"
5252

53-
orchestrator_agent = asyncio.run(OrchestratorAgent.build_agent(subagent_urls=subagent_urls))
54-
agent_executor = OrchestratorAgentExecutor(base_url=base_url, agent=orchestrator_agent)
53+
orchestrator_agent, agent_card = asyncio.run(OrchestratorAgent.build_agent(base_url=base_url, subagent_urls=subagent_urls))
54+
agent_executor = OrchestratorAgentExecutor(agent=orchestrator_agent)
5555

5656
request_handler = DefaultRequestHandler(
5757
agent_executor=agent_executor,
5858
task_store=InMemoryTaskStore(),
5959
)
6060
server = A2AStarletteApplication(
61-
agent_card=agent_executor.get_agent_card(), http_handler=request_handler
61+
agent_card=agent_card, http_handler=request_handler
6262
)
6363
import uvicorn
6464

samples/agent/adk/orchestrator/agent.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@
3030
from google.adk.models.llm_request import LlmRequest
3131
from google.adk.models.llm_response import LlmResponse
3232
from subagent_route_manager import SubagentRouteManager
33-
from a2ui.a2ui_extension import is_a2ui_part, A2UI_EXTENSION_URI
3433
from typing import override
3534
from a2a.types import TransportProtocol as A2ATransport
3635

37-
logger = logging.getLogger(__name__)
3836
from a2a.client.middleware import ClientCallInterceptor
3937
from a2a.client.client import ClientConfig as A2AClientConfig
4038
from a2a.client.client_factory import ClientFactory as A2AClientFactory
41-
from a2ui.a2ui_extension import A2UI_CLIENT_CAPABILITIES_KEY
39+
from a2ui.a2ui_extension import is_a2ui_part, A2UI_CLIENT_CAPABILITIES_KEY, A2UI_EXTENSION_URI, AGENT_EXTENSION_SUPPORTED_CATALOG_IDS_KEY, AGENT_EXTENSION_ACCEPTS_INLINE_CATALOGS_KEY, get_a2ui_agent_extension
40+
from a2a.types import AgentCapabilities, AgentCard, AgentExtension
41+
42+
logger = logging.getLogger(__name__)
4243

4344
class A2UIMetadataInterceptor(ClientCallInterceptor):
4445
@override
@@ -115,18 +116,28 @@ async def programmtically_route_user_action_to_subagent(
115116
return None
116117

117118
@classmethod
118-
async def build_agent(cls, subagent_urls: List[str]) -> LlmAgent:
119+
async def build_agent(cls, base_url: str, subagent_urls: List[str]) -> (LlmAgent, AgentCard):
119120
"""Builds the LLM agent for the orchestrator_agent agent."""
120121

121122
subagents = []
123+
supported_catalog_ids = set()
124+
skills = []
125+
accepts_inline_catalogs = False
122126
for subagent_url in subagent_urls:
123127
async with httpx.AsyncClient() as httpx_client:
124128
resolver = A2ACardResolver(
125129
httpx_client=httpx_client,
126130
base_url=subagent_url,
127131
)
128132

129-
subagent_card = await resolver.get_agent_card()
133+
subagent_card = await resolver.get_agent_card()
134+
for extension in subagent_card.capabilities.extensions or []:
135+
if extension.uri == A2UI_EXTENSION_URI and extension.params:
136+
supported_catalog_ids.update(extension.params.get(AGENT_EXTENSION_SUPPORTED_CATALOG_IDS_KEY) or [])
137+
accepts_inline_catalogs |= bool(extension.params.get(AGENT_EXTENSION_ACCEPTS_INLINE_CATALOGS_KEY))
138+
139+
skills.extend(subagent_card.skills)
140+
130141
logger.info('Successfully fetched public agent card:' + subagent_card.model_dump_json(indent=2, exclude_none=True))
131142

132143
# clean name for adk
@@ -172,7 +183,7 @@ async def build_agent(cls, subagent_urls: List[str]) -> LlmAgent:
172183
logger.info(f'Created remote agent with description: {description}')
173184

174185
LITELLM_MODEL = os.getenv("LITELLM_MODEL", "gemini/gemini-2.5-flash")
175-
return LlmAgent(
186+
agent = LlmAgent(
176187
model=LiteLlm(model=LITELLM_MODEL),
177188
name="orchestrator_agent",
178189
description="An agent that orchestrates requests to multiple other agents",
@@ -186,3 +197,21 @@ async def build_agent(cls, subagent_urls: List[str]) -> LlmAgent:
186197
sub_agents=subagents,
187198
before_model_callback=cls.programmtically_route_user_action_to_subagent,
188199
)
200+
201+
agent_card = AgentCard(
202+
name="Orchestrator Agent",
203+
description="This agent orchestrates requests to multiple subagents.",
204+
url=base_url,
205+
version="1.0.0",
206+
default_input_modes=OrchestratorAgent.SUPPORTED_CONTENT_TYPES,
207+
default_output_modes=OrchestratorAgent.SUPPORTED_CONTENT_TYPES,
208+
capabilities=AgentCapabilities(
209+
streaming=True,
210+
extensions=[get_a2ui_agent_extension(
211+
accepts_inline_catalogs=accepts_inline_catalogs,
212+
supported_catalog_ids=list(supported_catalog_ids))],
213+
),
214+
skills=skills,
215+
)
216+
217+
return agent, agent_card

samples/agent/adk/orchestrator/agent_executor.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@
3030
A2aAgentExecutorConfig,
3131
A2aAgentExecutor,
3232
)
33-
from a2a.types import AgentCapabilities, AgentCard, AgentExtension
34-
from a2ui.a2ui_extension import is_a2ui_part, try_activate_a2ui_extension, A2UI_EXTENSION_URI, STANDARD_CATALOG_ID, SUPPORTED_CATALOG_IDS_KEY, get_a2ui_agent_extension, A2UI_CLIENT_CAPABILITIES_KEY
33+
from a2ui.a2ui_extension import is_a2ui_part, try_activate_a2ui_extension, A2UI_EXTENSION_URI, STANDARD_CATALOG_ID, SUPPORTED_CATALOG_IDS_KEY, A2UI_CLIENT_CAPABILITIES_KEY
3534
from google.adk.a2a.converters import event_converter
3635
from a2a.server.events import Event as A2AEvent
3736
from google.adk.events.event import Event
@@ -48,9 +47,7 @@
4847
class OrchestratorAgentExecutor(A2aAgentExecutor):
4948
"""Contact AgentExecutor Example."""
5049

51-
def __init__(self, base_url: str, agent: LlmAgent):
52-
self._base_url = base_url
53-
50+
def __init__(self, agent: LlmAgent):
5451
config = A2aAgentExecutorConfig(
5552
gen_ai_part_converter=part_converters.convert_genai_part_to_a2a_part,
5653
a2a_part_converter=part_converters.convert_a2a_part_to_genai_part,
@@ -117,21 +114,6 @@ def convert_event_to_a2a_events_and_save_surface_id_to_subagent_name(
117114

118115
return a2a_events
119116

120-
def get_agent_card(self) -> AgentCard:
121-
return AgentCard(
122-
name="Orchestrator Agent",
123-
description="This agent orchestrates to multiple subagents to provide.",
124-
url=self._base_url,
125-
version="1.0.0",
126-
default_input_modes=OrchestratorAgent.SUPPORTED_CONTENT_TYPES,
127-
default_output_modes=OrchestratorAgent.SUPPORTED_CONTENT_TYPES,
128-
capabilities=AgentCapabilities(
129-
streaming=True,
130-
extensions=[get_a2ui_agent_extension()],
131-
),
132-
skills=[],
133-
)
134-
135117
@override
136118
async def _prepare_session(
137119
self,

samples/agent/adk/rizzcharts/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
logger = logging.getLogger(__name__)
3333

34-
RIZZCHARTS_CATALOG_URI = "https://raw.githubusercontent.com/google/A2UI/refs/heads/main/a2a_agents/python/adk/samples/rizzcharts/rizzcharts_catalog_definition.json"
34+
RIZZCHARTS_CATALOG_URI = "https://github.com/google/A2UI/blob/main/samples/agent/adk/rizzcharts/rizzcharts_catalog_definition.json"
3535

3636
class rizzchartsAgent:
3737
"""An agent that runs an ecommerce dashboard"""

samples/agent/adk/rizzcharts/agent_executor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ def get_agent_card(self) -> AgentCard:
8484
default_output_modes=rizzchartsAgent.SUPPORTED_CONTENT_TYPES,
8585
capabilities=AgentCapabilities(
8686
streaming=True,
87-
extensions=[get_a2ui_agent_extension()],
87+
extensions=[get_a2ui_agent_extension(
88+
supported_catalog_ids=[STANDARD_CATALOG_ID, RIZZCHARTS_CATALOG_URI])],
8889
),
8990
skills=[
9091
AgentSkill(

samples/agent/adk/rizzcharts/component_catalog_builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def load_a2ui_schema(self, client_ui_capabilities: Optional[dict[str, Any]]) ->
7171
logger.info(f"Loading inline component catalog {inline_catalog_str[:200]}")
7272
catalog_json = json.loads(inline_catalog_str)
7373
else:
74-
raise ValueError("Client UI capabilities not provided")
74+
raise ValueError("No supported catalogs found in client UI capabilities")
7575

7676
logger.info(f"Loading A2UI schema at {self._a2ui_schema_path}")
7777
a2ui_schema = self.get_file_content(self._a2ui_schema_path)

0 commit comments

Comments
 (0)