Skip to content

Commit 5402e07

Browse files
committed
Move example A2UI tools to SDK
1 parent 2925b5b commit 5402e07

File tree

13 files changed

+1078
-450
lines changed

13 files changed

+1078
-450
lines changed

a2a_agents/python/a2ui_extension/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# A2UI Extension Implementation
22

3-
This is the Python implementation of the a2ui extension.
3+
a2ui_extension.py is the Python implementation of the a2ui extension.
4+
send_a2ui_to_client_toolset.py is an example Python implementation of using ADK toolcalls to implement A2UI.
45

56
## Running Tests
67

@@ -13,7 +14,7 @@ This is the Python implementation of the a2ui extension.
1314
2. Run the tests
1415

1516
```bash
16-
uv run --with pytest pytest tests/test_extension.py
17+
uv run --with pytest pytest tests/*.py
1718
```
1819

1920
## Disclaimer

a2a_agents/python/a2ui_extension/pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,19 @@ build-backend = "hatchling.build"
1313
[[tool.uv.index]]
1414
url = "https://pypi.org/simple"
1515
default = true
16+
17+
[tool.pyink]
18+
unstable = true
19+
target-version = []
20+
pyink-indentation = 2
21+
pyink-use-majority-quotes = true
22+
pyink-annotation-pragmas = [
23+
"noqa",
24+
"pylint:",
25+
"type: ignore",
26+
"pytype:",
27+
"mypy:",
28+
"pyright:",
29+
"pyre-",
30+
]
31+

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

Lines changed: 74 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -31,95 +31,99 @@
3131

3232
STANDARD_CATALOG_ID = "https://github.com/google/A2UI/blob/main/specification/0.8/json/standard_catalog_definition.json"
3333

34+
3435
def create_a2ui_part(a2ui_data: dict[str, Any]) -> Part:
35-
"""Creates an A2A Part containing A2UI data.
36-
37-
Args:
38-
a2ui_data: The A2UI data dictionary.
39-
40-
Returns:
41-
An A2A Part with a DataPart containing the A2UI data.
42-
"""
43-
return Part(
44-
root=DataPart(
45-
data=a2ui_data,
46-
metadata={
47-
MIME_TYPE_KEY: A2UI_MIME_TYPE,
48-
},
49-
)
50-
)
36+
"""Creates an A2A Part containing A2UI data.
37+
38+
Args:
39+
a2ui_data: The A2UI data dictionary.
40+
41+
Returns:
42+
An A2A Part with a DataPart containing the A2UI data.
43+
"""
44+
return Part(
45+
root=DataPart(
46+
data=a2ui_data,
47+
metadata={
48+
MIME_TYPE_KEY: A2UI_MIME_TYPE,
49+
},
50+
)
51+
)
5152

5253

5354
def is_a2ui_part(part: Part) -> bool:
54-
"""Checks if an A2A Part contains A2UI data.
55-
56-
Args:
57-
part: The A2A Part to check.
58-
59-
Returns:
60-
True if the part contains A2UI data, False otherwise.
61-
"""
62-
return (
63-
isinstance(part.root, DataPart)
64-
and part.root.metadata
65-
and part.root.metadata.get(MIME_TYPE_KEY) == A2UI_MIME_TYPE
66-
)
55+
"""Checks if an A2A Part contains A2UI data.
56+
57+
Args:
58+
part: The A2A Part to check.
59+
60+
Returns:
61+
True if the part contains A2UI data, False otherwise.
62+
"""
63+
return (
64+
isinstance(part.root, DataPart)
65+
and part.root.metadata
66+
and part.root.metadata.get(MIME_TYPE_KEY) == A2UI_MIME_TYPE
67+
)
6768

6869

6970
def get_a2ui_datapart(part: Part) -> Optional[DataPart]:
70-
"""Extracts the DataPart containing A2UI data from an A2A Part, if present.
71+
"""Extracts the DataPart containing A2UI data from an A2A Part, if present.
7172
72-
Args:
73-
part: The A2A Part to extract A2UI data from.
73+
Args:
74+
part: The A2A Part to extract A2UI data from.
7475
75-
Returns:
76-
The DataPart containing A2UI data if present, None otherwise.
77-
"""
78-
if is_a2ui_part(part):
79-
return part.root
80-
return None
76+
Returns:
77+
The DataPart containing A2UI data if present, None otherwise.
78+
"""
79+
if is_a2ui_part(part):
80+
return part.root
81+
return None
8182

8283

8384
AGENT_EXTENSION_SUPPORTED_CATALOG_IDS_KEY = "supportedCatalogIds"
8485
AGENT_EXTENSION_ACCEPTS_INLINE_CATALOGS_KEY = "acceptsInlineCatalogs"
8586

87+
8688
def get_a2ui_agent_extension(
8789
accepts_inline_catalogs: bool = False,
8890
supported_catalog_ids: List[str] = [],
8991
) -> AgentExtension:
90-
"""Creates the A2UI AgentExtension configuration.
91-
92-
Args:
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.
95-
96-
Returns:
97-
The configured A2UI AgentExtension.
98-
"""
99-
params = {}
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
105-
106-
return AgentExtension(
107-
uri=A2UI_EXTENSION_URI,
108-
description="Provides agent driven UI using the A2UI JSON format.",
109-
params=params if params else None,
92+
"""Creates the A2UI AgentExtension configuration.
93+
94+
Args:
95+
accepts_inline_catalogs: Whether the agent accepts inline custom catalogs.
96+
supported_catalog_ids: All pre-defined catalogs the agent is known to support.
97+
98+
Returns:
99+
The configured A2UI AgentExtension.
100+
"""
101+
params = {}
102+
if accepts_inline_catalogs:
103+
params[AGENT_EXTENSION_ACCEPTS_INLINE_CATALOGS_KEY] = (
104+
True # Only set if not default of False
110105
)
111106

107+
if supported_catalog_ids:
108+
params[AGENT_EXTENSION_SUPPORTED_CATALOG_IDS_KEY] = supported_catalog_ids
109+
110+
return AgentExtension(
111+
uri=A2UI_EXTENSION_URI,
112+
description="Provides agent driven UI using the A2UI JSON format.",
113+
params=params if params else None,
114+
)
115+
112116

113117
def try_activate_a2ui_extension(context: RequestContext) -> bool:
114-
"""Activates the A2UI extension if requested.
115-
116-
Args:
117-
context: The request context to check.
118-
119-
Returns:
120-
True if activated, False otherwise.
121-
"""
122-
if A2UI_EXTENSION_URI in context.requested_extensions:
123-
context.add_activated_extension(A2UI_EXTENSION_URI)
124-
return True
125-
return False
118+
"""Activates the A2UI extension if requested.
119+
120+
Args:
121+
context: The request context to check.
122+
123+
Returns:
124+
True if activated, False otherwise.
125+
"""
126+
if A2UI_EXTENSION_URI in context.requested_extensions:
127+
context.add_activated_extension(A2UI_EXTENSION_URI)
128+
return True
129+
return False
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2025 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+
# https://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+
"""Utilities for A2UI Schema manipulation."""
16+
17+
from typing import Any
18+
19+
20+
def wrap_as_json_array(a2ui_schema: dict[str, Any]) -> dict[str, Any]:
21+
"""Wraps the A2UI schema in an array object to support multiple parts.
22+
23+
Args:
24+
a2ui_schema: The A2UI schema to wrap.
25+
26+
Returns:
27+
The wrapped A2UI schema object.
28+
29+
Raises:
30+
ValueError: If the A2UI schema is empty.
31+
"""
32+
if not a2ui_schema:
33+
raise ValueError("A2UI schema is empty")
34+
return {"type": "array", "items": a2ui_schema}

0 commit comments

Comments
 (0)