Skip to content

Commit 742478f

Browse files
seanzhougooglecopybara-github
authored andcommitted
chore: Add event converters to convert adk event to a2a event (WIP)
PiperOrigin-RevId: 773795427
1 parent ffcba70 commit 742478f

File tree

5 files changed

+1013
-4
lines changed

5 files changed

+1013
-4
lines changed
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
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+
# 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 __future__ import annotations
16+
17+
import datetime
18+
import logging
19+
from typing import Any
20+
from typing import Dict
21+
from typing import List
22+
from typing import Optional
23+
import uuid
24+
25+
from a2a.server.events import Event as A2AEvent
26+
from a2a.types import Artifact
27+
from a2a.types import DataPart
28+
from a2a.types import Message
29+
from a2a.types import Role
30+
from a2a.types import TaskArtifactUpdateEvent
31+
from a2a.types import TaskState
32+
from a2a.types import TaskStatus
33+
from a2a.types import TaskStatusUpdateEvent
34+
from a2a.types import TextPart
35+
36+
from ...agents.invocation_context import InvocationContext
37+
from ...events.event import Event
38+
from ...utils.feature_decorator import working_in_progress
39+
from .part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
40+
from .part_converter import A2A_DATA_PART_METADATA_TYPE_KEY
41+
from .part_converter import convert_genai_part_to_a2a_part
42+
from .utils import _get_adk_metadata_key
43+
44+
# Constants
45+
46+
ARTIFACT_ID_SEPARATOR = "-"
47+
DEFAULT_ERROR_MESSAGE = "An error occurred during processing"
48+
49+
# Logger
50+
logger = logging.getLogger("google_adk." + __name__)
51+
52+
53+
def _serialize_metadata_value(value: Any) -> str:
54+
"""Safely serializes metadata values to string format.
55+
56+
Args:
57+
value: The value to serialize.
58+
59+
Returns:
60+
String representation of the value.
61+
"""
62+
if hasattr(value, "model_dump"):
63+
try:
64+
return value.model_dump(exclude_none=True, by_alias=True)
65+
except Exception as e:
66+
logger.warning("Failed to serialize metadata value: %s", e)
67+
return str(value)
68+
return str(value)
69+
70+
71+
def _get_context_metadata(
72+
event: Event, invocation_context: InvocationContext
73+
) -> Dict[str, str]:
74+
"""Gets the context metadata for the event.
75+
76+
Args:
77+
event: The ADK event to extract metadata from.
78+
invocation_context: The invocation context containing session information.
79+
80+
Returns:
81+
A dictionary containing the context metadata.
82+
83+
Raises:
84+
ValueError: If required fields are missing from event or context.
85+
"""
86+
if not event:
87+
raise ValueError("Event cannot be None")
88+
if not invocation_context:
89+
raise ValueError("Invocation context cannot be None")
90+
91+
try:
92+
metadata = {
93+
_get_adk_metadata_key("app_name"): invocation_context.app_name,
94+
_get_adk_metadata_key("user_id"): invocation_context.user_id,
95+
_get_adk_metadata_key("session_id"): invocation_context.session.id,
96+
_get_adk_metadata_key("invocation_id"): event.invocation_id,
97+
_get_adk_metadata_key("author"): event.author,
98+
}
99+
100+
# Add optional metadata fields if present
101+
optional_fields = [
102+
("branch", event.branch),
103+
("grounding_metadata", event.grounding_metadata),
104+
("custom_metadata", event.custom_metadata),
105+
("usage_metadata", event.usage_metadata),
106+
("error_code", event.error_code),
107+
]
108+
109+
for field_name, field_value in optional_fields:
110+
if field_value is not None:
111+
metadata[_get_adk_metadata_key(field_name)] = _serialize_metadata_value(
112+
field_value
113+
)
114+
115+
return metadata
116+
117+
except Exception as e:
118+
logger.error("Failed to create context metadata: %s", e)
119+
raise
120+
121+
122+
def _create_artifact_id(
123+
app_name: str, user_id: str, session_id: str, filename: str, version: int
124+
) -> str:
125+
"""Creates a unique artifact ID.
126+
127+
Args:
128+
app_name: The application name.
129+
user_id: The user ID.
130+
session_id: The session ID.
131+
filename: The artifact filename.
132+
version: The artifact version.
133+
134+
Returns:
135+
A unique artifact ID string.
136+
"""
137+
components = [app_name, user_id, session_id, filename, str(version)]
138+
return ARTIFACT_ID_SEPARATOR.join(components)
139+
140+
141+
def _convert_artifact_to_a2a_events(
142+
event: Event,
143+
invocation_context: InvocationContext,
144+
filename: str,
145+
version: int,
146+
) -> TaskArtifactUpdateEvent:
147+
"""Converts a new artifact version to an A2A TaskArtifactUpdateEvent.
148+
149+
Args:
150+
event: The ADK event containing the artifact information.
151+
invocation_context: The invocation context.
152+
filename: The name of the artifact file.
153+
version: The version number of the artifact.
154+
155+
Returns:
156+
A TaskArtifactUpdateEvent representing the artifact update.
157+
158+
Raises:
159+
ValueError: If required parameters are invalid.
160+
RuntimeError: If artifact loading fails.
161+
"""
162+
if not filename:
163+
raise ValueError("Filename cannot be empty")
164+
if version < 0:
165+
raise ValueError("Version must be non-negative")
166+
167+
try:
168+
artifact_part = invocation_context.artifact_service.load_artifact(
169+
app_name=invocation_context.app_name,
170+
user_id=invocation_context.user_id,
171+
session_id=invocation_context.session.id,
172+
filename=filename,
173+
version=version,
174+
)
175+
176+
converted_part = convert_genai_part_to_a2a_part(part=artifact_part)
177+
if not converted_part:
178+
raise RuntimeError(f"Failed to convert artifact part for {filename}")
179+
180+
artifact_id = _create_artifact_id(
181+
invocation_context.app_name,
182+
invocation_context.user_id,
183+
invocation_context.session.id,
184+
filename,
185+
version,
186+
)
187+
188+
return TaskArtifactUpdateEvent(
189+
taskId=str(uuid.uuid4()),
190+
append=False,
191+
contextId=invocation_context.session.id,
192+
lastChunk=True,
193+
artifact=Artifact(
194+
artifactId=artifact_id,
195+
name=filename,
196+
metadata={
197+
"filename": filename,
198+
"version": version,
199+
},
200+
parts=[converted_part],
201+
),
202+
)
203+
except Exception as e:
204+
logger.error(
205+
"Failed to convert artifact for %s, version %s: %s",
206+
filename,
207+
version,
208+
e,
209+
)
210+
raise RuntimeError(f"Artifact conversion failed: {e}") from e
211+
212+
213+
def _process_long_running_tool(a2a_part, event: Event) -> None:
214+
"""Processes long-running tool metadata for an A2A part.
215+
216+
Args:
217+
a2a_part: The A2A part to potentially mark as long-running.
218+
event: The ADK event containing long-running tool information.
219+
"""
220+
if (
221+
isinstance(a2a_part.root, DataPart)
222+
and event.long_running_tool_ids
223+
and a2a_part.root.metadata.get(
224+
_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
225+
)
226+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
227+
and a2a_part.root.metadata.get("id") in event.long_running_tool_ids
228+
):
229+
a2a_part.root.metadata[_get_adk_metadata_key("is_long_running")] = True
230+
231+
232+
@working_in_progress
233+
def convert_event_to_a2a_status_message(
234+
event: Event, invocation_context: InvocationContext
235+
) -> Optional[Message]:
236+
"""Converts an ADK event to an A2A message.
237+
238+
Args:
239+
event: The ADK event to convert.
240+
invocation_context: The invocation context.
241+
242+
Returns:
243+
An A2A Message if the event has content, None otherwise.
244+
245+
Raises:
246+
ValueError: If required parameters are invalid.
247+
"""
248+
if not event:
249+
raise ValueError("Event cannot be None")
250+
if not invocation_context:
251+
raise ValueError("Invocation context cannot be None")
252+
253+
if not event.content or not event.content.parts:
254+
return None
255+
256+
try:
257+
a2a_parts = []
258+
for part in event.content.parts:
259+
a2a_part = convert_genai_part_to_a2a_part(part)
260+
if a2a_part:
261+
a2a_parts.append(a2a_part)
262+
_process_long_running_tool(a2a_part, event)
263+
264+
if a2a_parts:
265+
return Message(
266+
messageId=str(uuid.uuid4()), role=Role.agent, parts=a2a_parts
267+
)
268+
269+
except Exception as e:
270+
logger.error("Failed to convert event to status message: %s", e)
271+
raise
272+
273+
return None
274+
275+
276+
def _create_error_status_event(
277+
event: Event, invocation_context: InvocationContext
278+
) -> TaskStatusUpdateEvent:
279+
"""Creates a TaskStatusUpdateEvent for error scenarios.
280+
281+
Args:
282+
event: The ADK event containing error information.
283+
invocation_context: The invocation context.
284+
285+
Returns:
286+
A TaskStatusUpdateEvent with FAILED state.
287+
"""
288+
error_message = getattr(event, "error_message", None) or DEFAULT_ERROR_MESSAGE
289+
290+
return TaskStatusUpdateEvent(
291+
taskId=str(uuid.uuid4()),
292+
contextId=invocation_context.session.id,
293+
final=False,
294+
metadata=_get_context_metadata(event, invocation_context),
295+
status=TaskStatus(
296+
state=TaskState.failed,
297+
message=Message(
298+
messageId=str(uuid.uuid4()),
299+
role=Role.agent,
300+
parts=[TextPart(text=error_message)],
301+
),
302+
timestamp=datetime.datetime.now().isoformat(),
303+
),
304+
)
305+
306+
307+
def _create_running_status_event(
308+
message: Message, invocation_context: InvocationContext, event: Event
309+
) -> TaskStatusUpdateEvent:
310+
"""Creates a TaskStatusUpdateEvent for running scenarios.
311+
312+
Args:
313+
message: The A2A message to include.
314+
invocation_context: The invocation context.
315+
event: The ADK event.
316+
317+
Returns:
318+
A TaskStatusUpdateEvent with RUNNING state.
319+
"""
320+
return TaskStatusUpdateEvent(
321+
taskId=str(uuid.uuid4()),
322+
contextId=invocation_context.session.id,
323+
final=False,
324+
status=TaskStatus(
325+
state=TaskState.working,
326+
message=message,
327+
timestamp=datetime.datetime.now().isoformat(),
328+
),
329+
metadata=_get_context_metadata(event, invocation_context),
330+
)
331+
332+
333+
@working_in_progress
334+
def convert_event_to_a2a_events(
335+
event: Event, invocation_context: InvocationContext
336+
) -> List[A2AEvent]:
337+
"""Converts a GenAI event to a list of A2A events.
338+
339+
Args:
340+
event: The ADK event to convert.
341+
invocation_context: The invocation context.
342+
343+
Returns:
344+
A list of A2A events representing the converted ADK event.
345+
346+
Raises:
347+
ValueError: If required parameters are invalid.
348+
"""
349+
if not event:
350+
raise ValueError("Event cannot be None")
351+
if not invocation_context:
352+
raise ValueError("Invocation context cannot be None")
353+
354+
a2a_events = []
355+
356+
try:
357+
# Handle artifact deltas
358+
if event.actions.artifact_delta:
359+
for filename, version in event.actions.artifact_delta.items():
360+
artifact_event = _convert_artifact_to_a2a_events(
361+
event, invocation_context, filename, version
362+
)
363+
a2a_events.append(artifact_event)
364+
365+
# Handle error scenarios
366+
if event.error_code:
367+
error_event = _create_error_status_event(event, invocation_context)
368+
a2a_events.append(error_event)
369+
370+
# Handle regular message content
371+
message = convert_event_to_a2a_status_message(event, invocation_context)
372+
if message:
373+
running_event = _create_running_status_event(
374+
message, invocation_context, event
375+
)
376+
a2a_events.append(running_event)
377+
378+
except Exception as e:
379+
logger.error("Failed to convert event to A2A events: %s", e)
380+
raise
381+
382+
return a2a_events

0 commit comments

Comments
 (0)