Skip to content

Commit 07875fc

Browse files
committed
Merge remote-tracking branch 'upstream/main' into delete-minimal-catalog
2 parents 04cb589 + e05dd96 commit 07875fc

10 files changed

Lines changed: 602 additions & 311 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
"""Module for the A2UI Event Converter.
16+
17+
This module provides the `A2uiEventConverter` which intercepts ADK events and automatically
18+
translates GenAI model outputs (both tool-based A2UI function calls and text-based delimited A2UI blocks)
19+
into A2A (Agent-to-Agent) event structures with A2UI payloads, using the session catalog if available.
20+
21+
Key Components:
22+
* `A2uiEventConverter`: An event converter that automatically injects the A2UI catalog into part conversion.
23+
24+
Usage Example:
25+
26+
Configure the ADK executor to use the A2UI event converter:
27+
28+
```python
29+
config = A2aAgentExecutorConfig(
30+
event_converter=A2uiEventConverter()
31+
)
32+
executor = A2aAgentExecutor(config)
33+
```
34+
"""
35+
36+
from typing import TYPE_CHECKING, Optional
37+
38+
from a2ui.adk.a2a.part_converter import A2uiPartConverter
39+
from google.adk.a2a.converters import part_converter
40+
from google.adk.utils.feature_decorator import experimental
41+
42+
if TYPE_CHECKING:
43+
from a2a.server.events import Event as A2AEvent
44+
from google.adk.a2a.converters.part_converter import GenAIPartToA2APartConverter
45+
from google.adk.agents.invocation_context import InvocationContext
46+
from google.adk.events.event import Event
47+
48+
49+
@experimental
50+
class A2uiEventConverter:
51+
"""An event converter that automatically injects the A2UI catalog into part conversion.
52+
53+
This allows text-based A2UI extraction and validation to work even when the
54+
catalog is session-specific.
55+
"""
56+
57+
def __init__(
58+
self,
59+
catalog_key: str = "system:a2ui_catalog",
60+
bypass_tool_check: bool = False,
61+
fallback_text: Optional[str] = None,
62+
):
63+
self._catalog_key = catalog_key
64+
self._bypass_tool_check = bypass_tool_check
65+
self._fallback_text = fallback_text
66+
67+
def __call__(
68+
self,
69+
event: "Event",
70+
invocation_context: "InvocationContext",
71+
task_id: Optional[str] = None,
72+
context_id: Optional[str] = None,
73+
part_converter_func: "GenAIPartToA2APartConverter" = part_converter.convert_genai_part_to_a2a_part,
74+
) -> list["A2AEvent"]:
75+
"""Converts an ADK event to A2A events, using the session catalog if available."""
76+
from google.adk.a2a.converters.event_converter import (
77+
convert_event_to_a2a_events,
78+
)
79+
80+
catalog = invocation_context.session.state.get(self._catalog_key)
81+
if catalog:
82+
# Use the catalog-aware part converter
83+
effective_converter = A2uiPartConverter(
84+
catalog,
85+
bypass_tool_check=self._bypass_tool_check,
86+
fallback_text=self._fallback_text,
87+
).convert
88+
else:
89+
effective_converter = part_converter_func
90+
91+
return convert_event_to_a2a_events(
92+
event,
93+
invocation_context,
94+
task_id,
95+
context_id,
96+
effective_converter,
97+
)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
"""Module for the A2UI Part Converter.
16+
17+
This module provides the `A2uiPartConverter` which acts as a catalog-aware GenAI to A2A
18+
part converter. It handles both tool-based A2UI (via the `send_a2ui_json_to_client` tool response)
19+
and text-based A2UI (extracted and healed via A2UI custom tags), validating the structures
20+
against the active A2UI catalog schema.
21+
22+
Key Components:
23+
* `A2uiPartConverter`: A catalog-aware GenAI to A2A part converter.
24+
25+
Usage Example:
26+
27+
Typically used internally by the A2uiEventConverter or manually configured for custom part conversions:
28+
29+
```python
30+
converter = A2uiPartConverter(a2ui_catalog=my_catalog)
31+
a2a_parts = converter.convert(genai_part)
32+
```
33+
"""
34+
35+
import logging
36+
from typing import Optional
37+
38+
39+
from a2a import types as a2a_types
40+
from a2ui.a2a.parts import create_a2ui_part, parse_response_to_parts
41+
from a2ui.parser.parser import has_a2ui_parts
42+
from a2ui.schema import constants
43+
from a2ui.schema.catalog import A2uiCatalog
44+
from google.adk.a2a.converters import part_converter
45+
from google.adk.utils.feature_decorator import experimental
46+
from google.genai import types as genai_types
47+
48+
logger = logging.getLogger(__name__)
49+
50+
51+
@experimental
52+
class A2uiPartConverter:
53+
"""A catalog-aware GenAI to A2A part converter.
54+
55+
This converter handles both tool-based A2UI (via `send_a2ui_json_to_client`)
56+
and text-based A2UI (via A2UI delimiter tags). It uses the provided
57+
catalog to validate and fix JSON payloads.
58+
"""
59+
60+
def __init__(
61+
self,
62+
a2ui_catalog: A2uiCatalog,
63+
bypass_tool_check: bool = False,
64+
fallback_text: Optional[str] = None,
65+
):
66+
self._catalog = a2ui_catalog
67+
self._bypass_tool_check = bypass_tool_check
68+
self._fallback_text = fallback_text
69+
70+
def convert(self, part: genai_types.Part) -> list[a2a_types.Part]:
71+
"""Converts a GenAI part to A2A parts, with A2UI validation.
72+
73+
Args:
74+
part: The GenAI part to convert.
75+
76+
Returns:
77+
A list of A2A parts.
78+
"""
79+
# 1. Handle Tool Responses (FunctionResponse)
80+
if function_response := part.function_response:
81+
is_send_a2ui_json_to_client_response = (
82+
function_response.name == constants.A2UI_TOOL_NAME
83+
)
84+
85+
if is_send_a2ui_json_to_client_response or self._bypass_tool_check:
86+
response_dict = function_response.response or {}
87+
88+
if constants.A2UI_TOOL_ERROR_KEY in response_dict:
89+
logger.warning(
90+
f"A2UI tool call failed: {response_dict[constants.A2UI_TOOL_ERROR_KEY]}"
91+
)
92+
return []
93+
94+
if (
95+
isinstance(response_dict, dict)
96+
and constants.A2UI_VALIDATED_JSON_KEY in response_dict
97+
):
98+
json_data = response_dict.get(constants.A2UI_VALIDATED_JSON_KEY)
99+
if json_data:
100+
return [create_a2ui_part(message) for message in json_data]
101+
102+
if is_send_a2ui_json_to_client_response:
103+
logger.info("No result in A2UI tool response")
104+
return []
105+
106+
# Handle generic/other tool responses that returned a string containing A2UI tags.
107+
if function_response.response and function_response.response.get("result"):
108+
result = function_response.response.get("result")
109+
if has_a2ui_parts(result):
110+
return parse_response_to_parts(
111+
result,
112+
validator=self._catalog.validator,
113+
fallback_text=self._fallback_text,
114+
)
115+
116+
# 2. Handle Tool Calls (FunctionCall) - Skip sending to client
117+
if (
118+
function_call := part.function_call
119+
) and function_call.name == constants.A2UI_TOOL_NAME:
120+
return []
121+
122+
# 3. Handle Text-based A2UI (TextPart)
123+
if text := part.text:
124+
if has_a2ui_parts(text):
125+
return parse_response_to_parts(
126+
text,
127+
validator=self._catalog.validator,
128+
fallback_text=self._fallback_text,
129+
)
130+
131+
# 4. Default conversion for other parts
132+
converted_part = part_converter.convert_genai_part_to_a2a_part(part)
133+
return [converted_part] if converted_part else []

0 commit comments

Comments
 (0)