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.
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ from ._openai import AzureOpenAI, OpenAI
2
+
3
+ __all__ = ["OpenAI", "AzureOpenAI"]