kagent-adk 0.7.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kagent/adk/__init__.py +8 -0
- kagent/adk/_a2a.py +178 -0
- kagent/adk/_agent_executor.py +335 -0
- kagent/adk/_lifespan.py +36 -0
- kagent/adk/_session_service.py +178 -0
- kagent/adk/_token.py +80 -0
- kagent/adk/artifacts/__init__.py +13 -0
- kagent/adk/artifacts/artifacts_toolset.py +56 -0
- kagent/adk/artifacts/return_artifacts_tool.py +160 -0
- kagent/adk/artifacts/session_path.py +106 -0
- kagent/adk/artifacts/stage_artifacts_tool.py +170 -0
- kagent/adk/cli.py +249 -0
- kagent/adk/converters/__init__.py +0 -0
- kagent/adk/converters/error_mappings.py +60 -0
- kagent/adk/converters/event_converter.py +322 -0
- kagent/adk/converters/part_converter.py +206 -0
- kagent/adk/converters/request_converter.py +35 -0
- kagent/adk/models/__init__.py +3 -0
- kagent/adk/models/_openai.py +564 -0
- kagent/adk/models/_ssl.py +245 -0
- kagent/adk/sandbox_code_executer.py +77 -0
- kagent/adk/skill_fetcher.py +103 -0
- kagent/adk/tools/README.md +217 -0
- kagent/adk/tools/__init__.py +15 -0
- kagent/adk/tools/bash_tool.py +74 -0
- kagent/adk/tools/file_tools.py +192 -0
- kagent/adk/tools/skill_tool.py +104 -0
- kagent/adk/tools/skills_plugin.py +49 -0
- kagent/adk/tools/skills_toolset.py +68 -0
- kagent/adk/types.py +268 -0
- kagent_adk-0.7.11.dist-info/METADATA +35 -0
- kagent_adk-0.7.11.dist-info/RECORD +34 -0
- kagent_adk-0.7.11.dist-info/WHEEL +4 -0
- kagent_adk-0.7.11.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from a2a.server.events import Event as A2AEvent
|
|
9
|
+
from a2a.types import DataPart, Message, Role, Task, TaskState, TaskStatus, TaskStatusUpdateEvent, TextPart
|
|
10
|
+
from a2a.types import Part as A2APart
|
|
11
|
+
from google.adk.agents.invocation_context import InvocationContext
|
|
12
|
+
from google.adk.events.event import Event
|
|
13
|
+
from google.adk.flows.llm_flows.functions import REQUEST_EUC_FUNCTION_CALL_NAME
|
|
14
|
+
from google.genai import types as genai_types
|
|
15
|
+
|
|
16
|
+
from kagent.core.a2a import (
|
|
17
|
+
A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY,
|
|
18
|
+
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
|
|
19
|
+
A2A_DATA_PART_METADATA_TYPE_KEY,
|
|
20
|
+
get_kagent_metadata_key,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .error_mappings import _get_error_message, _is_normal_completion
|
|
24
|
+
from .part_converter import (
|
|
25
|
+
convert_genai_part_to_a2a_part,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Constants
|
|
29
|
+
|
|
30
|
+
ARTIFACT_ID_SEPARATOR = "-"
|
|
31
|
+
|
|
32
|
+
# Logger
|
|
33
|
+
logger = logging.getLogger("kagent_adk." + __name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _serialize_metadata_value(value: Any) -> str:
|
|
37
|
+
"""Safely serializes metadata values to string format.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
value: The value to serialize.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
String representation of the value.
|
|
44
|
+
"""
|
|
45
|
+
if hasattr(value, "model_dump"):
|
|
46
|
+
try:
|
|
47
|
+
return value.model_dump(exclude_none=True, by_alias=True)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.warning("Failed to serialize metadata value: %s", e)
|
|
50
|
+
return str(value)
|
|
51
|
+
return str(value)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_context_metadata(event: Event, invocation_context: InvocationContext) -> Dict[str, str]:
|
|
55
|
+
"""Gets the context metadata for the event.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
event: The ADK event to extract metadata from.
|
|
59
|
+
invocation_context: The invocation context containing session information.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A dictionary containing the context metadata.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If required fields are missing from event or context.
|
|
66
|
+
"""
|
|
67
|
+
if not event:
|
|
68
|
+
raise ValueError("Event cannot be None")
|
|
69
|
+
if not invocation_context:
|
|
70
|
+
raise ValueError("Invocation context cannot be None")
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
metadata = {
|
|
74
|
+
get_kagent_metadata_key("app_name"): invocation_context.app_name,
|
|
75
|
+
get_kagent_metadata_key("user_id"): invocation_context.user_id,
|
|
76
|
+
get_kagent_metadata_key("session_id"): invocation_context.session.id,
|
|
77
|
+
get_kagent_metadata_key("invocation_id"): event.invocation_id,
|
|
78
|
+
get_kagent_metadata_key("author"): event.author,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Add optional metadata fields if present
|
|
82
|
+
optional_fields = [
|
|
83
|
+
("branch", event.branch),
|
|
84
|
+
("grounding_metadata", event.grounding_metadata),
|
|
85
|
+
("custom_metadata", event.custom_metadata),
|
|
86
|
+
("usage_metadata", event.usage_metadata),
|
|
87
|
+
("error_code", event.error_code),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
for field_name, field_value in optional_fields:
|
|
91
|
+
if field_value is not None:
|
|
92
|
+
metadata[get_kagent_metadata_key(field_name)] = _serialize_metadata_value(field_value)
|
|
93
|
+
|
|
94
|
+
return metadata
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error("Failed to create context metadata: %s", e)
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _create_artifact_id(app_name: str, user_id: str, session_id: str, filename: str, version: int) -> str:
|
|
102
|
+
"""Creates a unique artifact ID.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
app_name: The application name.
|
|
106
|
+
user_id: The user ID.
|
|
107
|
+
session_id: The session ID.
|
|
108
|
+
filename: The artifact filename.
|
|
109
|
+
version: The artifact version.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
A unique artifact ID string.
|
|
113
|
+
"""
|
|
114
|
+
components = [app_name, user_id, session_id, filename, str(version)]
|
|
115
|
+
return ARTIFACT_ID_SEPARATOR.join(components)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _process_long_running_tool(a2a_part: A2APart, event: Event) -> None:
|
|
119
|
+
"""Processes long-running tool metadata for an A2A part.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
a2a_part: The A2A part to potentially mark as long-running.
|
|
123
|
+
event: The ADK event containing long-running tool information.
|
|
124
|
+
"""
|
|
125
|
+
if (
|
|
126
|
+
isinstance(a2a_part.root, DataPart)
|
|
127
|
+
and event.long_running_tool_ids
|
|
128
|
+
and a2a_part.root.metadata
|
|
129
|
+
and a2a_part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY))
|
|
130
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
131
|
+
and a2a_part.root.data.get("id") in event.long_running_tool_ids
|
|
132
|
+
):
|
|
133
|
+
a2a_part.root.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY)] = True
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def convert_event_to_a2a_message(
|
|
137
|
+
event: Event, invocation_context: InvocationContext, role: Role = Role.agent
|
|
138
|
+
) -> Optional[Message]:
|
|
139
|
+
"""Converts an ADK event to an A2A message.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
event: The ADK event to convert.
|
|
143
|
+
invocation_context: The invocation context.
|
|
144
|
+
role: The role attribute for the message (default: Role.agent).
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
An A2A Message if the event has content, None otherwise.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ValueError: If required parameters are invalid.
|
|
151
|
+
"""
|
|
152
|
+
if not event:
|
|
153
|
+
raise ValueError("Event cannot be None")
|
|
154
|
+
if not invocation_context:
|
|
155
|
+
raise ValueError("Invocation context cannot be None")
|
|
156
|
+
|
|
157
|
+
if not event.content or not event.content.parts:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
a2a_parts = []
|
|
162
|
+
for part in event.content.parts:
|
|
163
|
+
a2a_part = convert_genai_part_to_a2a_part(part)
|
|
164
|
+
if a2a_part:
|
|
165
|
+
a2a_parts.append(a2a_part)
|
|
166
|
+
_process_long_running_tool(a2a_part, event)
|
|
167
|
+
|
|
168
|
+
if a2a_parts:
|
|
169
|
+
# Include adk_partial in message metadata so TaskStore can filter
|
|
170
|
+
# partial streaming messages from history before saving
|
|
171
|
+
message_metadata = {"adk_partial": event.partial}
|
|
172
|
+
return Message(message_id=str(uuid.uuid4()), role=role, parts=a2a_parts, metadata=message_metadata)
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error("Failed to convert event to status message: %s", e)
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _create_error_status_event(
|
|
182
|
+
event: Event,
|
|
183
|
+
invocation_context: InvocationContext,
|
|
184
|
+
task_id: Optional[str] = None,
|
|
185
|
+
context_id: Optional[str] = None,
|
|
186
|
+
) -> TaskStatusUpdateEvent:
|
|
187
|
+
"""Creates a TaskStatusUpdateEvent for error scenarios.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
event: The ADK event containing error information.
|
|
191
|
+
invocation_context: The invocation context.
|
|
192
|
+
task_id: Optional task ID to use for generated events.
|
|
193
|
+
context_id: Optional Context ID to use for generated events.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
A TaskStatusUpdateEvent with FAILED state.
|
|
197
|
+
"""
|
|
198
|
+
error_message = getattr(event, "error_message", None)
|
|
199
|
+
|
|
200
|
+
# Get context metadata and add error code
|
|
201
|
+
event_metadata = _get_context_metadata(event, invocation_context)
|
|
202
|
+
if event.error_code:
|
|
203
|
+
event_metadata[get_kagent_metadata_key("error_code")] = str(event.error_code)
|
|
204
|
+
|
|
205
|
+
if not error_message:
|
|
206
|
+
error_message = _get_error_message(event.error_code)
|
|
207
|
+
|
|
208
|
+
return TaskStatusUpdateEvent(
|
|
209
|
+
task_id=task_id,
|
|
210
|
+
context_id=context_id,
|
|
211
|
+
metadata=event_metadata,
|
|
212
|
+
status=TaskStatus(
|
|
213
|
+
state=TaskState.failed,
|
|
214
|
+
message=Message(
|
|
215
|
+
message_id=str(uuid.uuid4()),
|
|
216
|
+
role=Role.agent,
|
|
217
|
+
parts=[A2APart(TextPart(text=error_message))],
|
|
218
|
+
metadata={get_kagent_metadata_key("error_code"): str(event.error_code)} if event.error_code else {},
|
|
219
|
+
),
|
|
220
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
221
|
+
),
|
|
222
|
+
final=False,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _create_status_update_event(
|
|
227
|
+
message: Message,
|
|
228
|
+
invocation_context: InvocationContext,
|
|
229
|
+
event: Event,
|
|
230
|
+
task_id: Optional[str] = None,
|
|
231
|
+
context_id: Optional[str] = None,
|
|
232
|
+
) -> TaskStatusUpdateEvent:
|
|
233
|
+
"""Creates a TaskStatusUpdateEvent for running scenarios.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
message: The A2A message to include.
|
|
237
|
+
invocation_context: The invocation context.
|
|
238
|
+
event: The ADK event.
|
|
239
|
+
task_id: Optional task ID to use for generated events.
|
|
240
|
+
context_id: Optional Context ID to use for generated events.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
A TaskStatusUpdateEvent with RUNNING state.
|
|
245
|
+
"""
|
|
246
|
+
status = TaskStatus(
|
|
247
|
+
state=TaskState.working,
|
|
248
|
+
message=message,
|
|
249
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if any(
|
|
253
|
+
part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY))
|
|
254
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
255
|
+
and part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY)) is True
|
|
256
|
+
and part.root.data.get("name") == REQUEST_EUC_FUNCTION_CALL_NAME
|
|
257
|
+
for part in message.parts
|
|
258
|
+
if part.root.metadata
|
|
259
|
+
):
|
|
260
|
+
status.state = TaskState.auth_required
|
|
261
|
+
elif any(
|
|
262
|
+
part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY))
|
|
263
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
264
|
+
and part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY)) is True
|
|
265
|
+
for part in message.parts
|
|
266
|
+
if part.root.metadata
|
|
267
|
+
):
|
|
268
|
+
status.state = TaskState.input_required
|
|
269
|
+
|
|
270
|
+
return TaskStatusUpdateEvent(
|
|
271
|
+
task_id=task_id,
|
|
272
|
+
context_id=context_id,
|
|
273
|
+
status=status,
|
|
274
|
+
metadata=_get_context_metadata(event, invocation_context),
|
|
275
|
+
final=False,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def convert_event_to_a2a_events(
|
|
280
|
+
event: Event,
|
|
281
|
+
invocation_context: InvocationContext,
|
|
282
|
+
task_id: Optional[str] = None,
|
|
283
|
+
context_id: Optional[str] = None,
|
|
284
|
+
) -> List[A2AEvent]:
|
|
285
|
+
"""Converts a GenAI event to a list of A2A events.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
event: The ADK event to convert.
|
|
289
|
+
invocation_context: The invocation context.
|
|
290
|
+
task_id: Optional task ID to use for generated events.
|
|
291
|
+
context_id: Optional Context ID to use for generated events.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
A list of A2A events representing the converted ADK event.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
ValueError: If required parameters are invalid.
|
|
298
|
+
"""
|
|
299
|
+
if not event:
|
|
300
|
+
raise ValueError("Event cannot be None")
|
|
301
|
+
if not invocation_context:
|
|
302
|
+
raise ValueError("Invocation context cannot be None")
|
|
303
|
+
|
|
304
|
+
a2a_events = []
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
# Handle error scenarios
|
|
308
|
+
if event.error_code and not _is_normal_completion(event.error_code):
|
|
309
|
+
error_event = _create_error_status_event(event, invocation_context, task_id, context_id)
|
|
310
|
+
a2a_events.append(error_event)
|
|
311
|
+
|
|
312
|
+
# Handle regular message content
|
|
313
|
+
message = convert_event_to_a2a_message(event, invocation_context)
|
|
314
|
+
if message:
|
|
315
|
+
running_event = _create_status_update_event(message, invocation_context, event, task_id, context_id)
|
|
316
|
+
a2a_events.append(running_event)
|
|
317
|
+
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error("Failed to convert event to A2A events: %s", e)
|
|
320
|
+
raise
|
|
321
|
+
|
|
322
|
+
return a2a_events
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
"""
|
|
16
|
+
module containing utilities for conversion between A2A Part and Google GenAI Part
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import base64
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from a2a import types as a2a_types
|
|
27
|
+
from google.genai import types as genai_types
|
|
28
|
+
|
|
29
|
+
from kagent.core.a2a import (
|
|
30
|
+
A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT,
|
|
31
|
+
A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE,
|
|
32
|
+
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
|
|
33
|
+
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
|
|
34
|
+
A2A_DATA_PART_METADATA_TYPE_KEY,
|
|
35
|
+
get_kagent_metadata_key,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("kagent_adk." + __name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def convert_a2a_part_to_genai_part(
|
|
42
|
+
a2a_part: a2a_types.Part,
|
|
43
|
+
) -> Optional[genai_types.Part]:
|
|
44
|
+
"""Convert an A2A Part to a Google GenAI Part."""
|
|
45
|
+
part = a2a_part.root
|
|
46
|
+
if isinstance(part, a2a_types.TextPart):
|
|
47
|
+
return genai_types.Part(text=part.text)
|
|
48
|
+
|
|
49
|
+
if isinstance(part, a2a_types.FilePart):
|
|
50
|
+
if isinstance(part.file, a2a_types.FileWithUri):
|
|
51
|
+
return genai_types.Part(
|
|
52
|
+
file_data=genai_types.FileData(file_uri=part.file.uri, mime_type=part.file.mime_type)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
elif isinstance(part.file, a2a_types.FileWithBytes):
|
|
56
|
+
return genai_types.Part(
|
|
57
|
+
inline_data=genai_types.Blob(
|
|
58
|
+
data=base64.b64decode(part.file.bytes),
|
|
59
|
+
mime_type=part.file.mime_type,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
logger.warning(
|
|
64
|
+
"Cannot convert unsupported file type: %s for A2A part: %s",
|
|
65
|
+
type(part.file),
|
|
66
|
+
a2a_part,
|
|
67
|
+
)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
if isinstance(part, a2a_types.DataPart):
|
|
71
|
+
# Convert the Data Part to funcall and function response.
|
|
72
|
+
# This is mainly for converting human in the loop and auth request and
|
|
73
|
+
# response.
|
|
74
|
+
# TODO once A2A defined how to suervice such information, migrate below
|
|
75
|
+
# logic accordinlgy
|
|
76
|
+
if part.metadata and get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY) in part.metadata:
|
|
77
|
+
if (
|
|
78
|
+
part.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
|
79
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
80
|
+
):
|
|
81
|
+
return genai_types.Part(function_call=genai_types.FunctionCall.model_validate(part.data, by_alias=True))
|
|
82
|
+
if (
|
|
83
|
+
part.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
|
84
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
|
85
|
+
):
|
|
86
|
+
return genai_types.Part(
|
|
87
|
+
function_response=genai_types.FunctionResponse.model_validate(part.data, by_alias=True)
|
|
88
|
+
)
|
|
89
|
+
if (
|
|
90
|
+
part.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
|
91
|
+
== A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
|
|
92
|
+
):
|
|
93
|
+
return genai_types.Part(
|
|
94
|
+
code_execution_result=genai_types.CodeExecutionResult.model_validate(part.data, by_alias=True)
|
|
95
|
+
)
|
|
96
|
+
if (
|
|
97
|
+
part.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
|
98
|
+
== A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
|
|
99
|
+
):
|
|
100
|
+
return genai_types.Part(
|
|
101
|
+
executable_code=genai_types.ExecutableCode.model_validate(part.data, by_alias=True)
|
|
102
|
+
)
|
|
103
|
+
return genai_types.Part(text=json.dumps(part.data))
|
|
104
|
+
|
|
105
|
+
logger.warning(
|
|
106
|
+
"Cannot convert unsupported part type: %s for A2A part: %s",
|
|
107
|
+
type(part),
|
|
108
|
+
a2a_part,
|
|
109
|
+
)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def convert_genai_part_to_a2a_part(
|
|
114
|
+
part: genai_types.Part,
|
|
115
|
+
) -> Optional[a2a_types.Part]:
|
|
116
|
+
"""Convert a Google GenAI Part to an A2A Part."""
|
|
117
|
+
|
|
118
|
+
if part.text:
|
|
119
|
+
a2a_part = a2a_types.TextPart(text=part.text)
|
|
120
|
+
if part.thought is not None:
|
|
121
|
+
a2a_part.metadata = {get_kagent_metadata_key("thought"): part.thought}
|
|
122
|
+
return a2a_types.Part(root=a2a_part)
|
|
123
|
+
|
|
124
|
+
if part.file_data:
|
|
125
|
+
return a2a_types.Part(
|
|
126
|
+
root=a2a_types.FilePart(
|
|
127
|
+
file=a2a_types.FileWithUri(
|
|
128
|
+
uri=part.file_data.file_uri,
|
|
129
|
+
mime_type=part.file_data.mime_type,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if part.inline_data:
|
|
135
|
+
a2a_part = a2a_types.FilePart(
|
|
136
|
+
file=a2a_types.FileWithBytes(
|
|
137
|
+
bytes=base64.b64encode(part.inline_data.data).decode("utf-8"),
|
|
138
|
+
mime_type=part.inline_data.mime_type,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if part.video_metadata:
|
|
143
|
+
a2a_part.metadata = {
|
|
144
|
+
get_kagent_metadata_key("video_metadata"): part.video_metadata.model_dump(
|
|
145
|
+
by_alias=True, exclude_none=True
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return a2a_types.Part(root=a2a_part)
|
|
150
|
+
|
|
151
|
+
# Convert the funcall and function response to A2A DataPart.
|
|
152
|
+
# This is mainly for converting human in the loop and auth request and
|
|
153
|
+
# response.
|
|
154
|
+
# TODO once A2A defined how to suervice such information, migrate below
|
|
155
|
+
# logic accordinlgy
|
|
156
|
+
if part.function_call:
|
|
157
|
+
return a2a_types.Part(
|
|
158
|
+
root=a2a_types.DataPart(
|
|
159
|
+
data=part.function_call.model_dump(by_alias=True, exclude_none=True),
|
|
160
|
+
metadata={
|
|
161
|
+
get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if part.function_response:
|
|
167
|
+
return a2a_types.Part(
|
|
168
|
+
root=a2a_types.DataPart(
|
|
169
|
+
data=part.function_response.model_dump(by_alias=True, exclude_none=True),
|
|
170
|
+
metadata={
|
|
171
|
+
get_kagent_metadata_key(
|
|
172
|
+
A2A_DATA_PART_METADATA_TYPE_KEY
|
|
173
|
+
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if part.code_execution_result:
|
|
179
|
+
return a2a_types.Part(
|
|
180
|
+
root=a2a_types.DataPart(
|
|
181
|
+
data=part.code_execution_result.model_dump(by_alias=True, exclude_none=True),
|
|
182
|
+
metadata={
|
|
183
|
+
get_kagent_metadata_key(
|
|
184
|
+
A2A_DATA_PART_METADATA_TYPE_KEY
|
|
185
|
+
): A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if part.executable_code:
|
|
191
|
+
return a2a_types.Part(
|
|
192
|
+
root=a2a_types.DataPart(
|
|
193
|
+
data=part.executable_code.model_dump(by_alias=True, exclude_none=True),
|
|
194
|
+
metadata={
|
|
195
|
+
get_kagent_metadata_key(
|
|
196
|
+
A2A_DATA_PART_METADATA_TYPE_KEY
|
|
197
|
+
): A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
logger.warning(
|
|
203
|
+
"Cannot convert unsupported part for Google GenAI part: %s",
|
|
204
|
+
part,
|
|
205
|
+
)
|
|
206
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from a2a.server.agent_execution import RequestContext
|
|
4
|
+
from google.adk.agents.run_config import StreamingMode
|
|
5
|
+
from google.adk.runners import RunConfig
|
|
6
|
+
from google.genai import types as genai_types
|
|
7
|
+
|
|
8
|
+
from .part_converter import convert_a2a_part_to_genai_part
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_user_id(request: RequestContext) -> str:
|
|
12
|
+
# Get user from call context if available (auth is enabled on a2a server)
|
|
13
|
+
if request.call_context and request.call_context.user and request.call_context.user.user_name:
|
|
14
|
+
return request.call_context.user.user_name
|
|
15
|
+
|
|
16
|
+
# Get user from context id
|
|
17
|
+
return f"A2A_USER_{request.context_id}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def convert_a2a_request_to_adk_run_args(
|
|
21
|
+
request: RequestContext,
|
|
22
|
+
stream: bool = False,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
if not request.message:
|
|
25
|
+
raise ValueError("Request message cannot be None")
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
"user_id": _get_user_id(request),
|
|
29
|
+
"session_id": request.context_id,
|
|
30
|
+
"new_message": genai_types.Content(
|
|
31
|
+
role="user",
|
|
32
|
+
parts=[convert_a2a_part_to_genai_part(part) for part in request.message.parts],
|
|
33
|
+
),
|
|
34
|
+
"run_config": RunConfig(streaming_mode=StreamingMode.SSE if stream else StreamingMode.NONE),
|
|
35
|
+
}
|