google-adk 1.5.0__py3-none-any.whl → 1.6.1__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.
Files changed (60) hide show
  1. google/adk/a2a/converters/event_converter.py +257 -36
  2. google/adk/a2a/converters/part_converter.py +93 -25
  3. google/adk/a2a/converters/request_converter.py +12 -32
  4. google/adk/a2a/converters/utils.py +22 -4
  5. google/adk/a2a/executor/__init__.py +13 -0
  6. google/adk/a2a/executor/a2a_agent_executor.py +260 -0
  7. google/adk/a2a/executor/task_result_aggregator.py +71 -0
  8. google/adk/a2a/logs/__init__.py +13 -0
  9. google/adk/a2a/logs/log_utils.py +349 -0
  10. google/adk/agents/base_agent.py +54 -0
  11. google/adk/agents/llm_agent.py +15 -0
  12. google/adk/agents/remote_a2a_agent.py +532 -0
  13. google/adk/artifacts/in_memory_artifact_service.py +6 -3
  14. google/adk/cli/browser/chunk-EQDQRRRY.js +1 -0
  15. google/adk/cli/browser/chunk-TXJFAAIW.js +2 -0
  16. google/adk/cli/browser/index.html +4 -3
  17. google/adk/cli/browser/main-RXDVX3K6.js +3914 -0
  18. google/adk/cli/browser/polyfills-FFHMD2TL.js +17 -0
  19. google/adk/cli/cli_deploy.py +4 -1
  20. google/adk/cli/cli_eval.py +8 -6
  21. google/adk/cli/cli_tools_click.py +30 -10
  22. google/adk/cli/fast_api.py +120 -5
  23. google/adk/cli/utils/agent_loader.py +12 -0
  24. google/adk/evaluation/agent_evaluator.py +107 -10
  25. google/adk/evaluation/base_eval_service.py +157 -0
  26. google/adk/evaluation/constants.py +20 -0
  27. google/adk/evaluation/eval_case.py +3 -3
  28. google/adk/evaluation/eval_metrics.py +39 -0
  29. google/adk/evaluation/evaluation_generator.py +1 -1
  30. google/adk/evaluation/final_response_match_v2.py +230 -0
  31. google/adk/evaluation/llm_as_judge.py +141 -0
  32. google/adk/evaluation/llm_as_judge_utils.py +48 -0
  33. google/adk/evaluation/metric_evaluator_registry.py +89 -0
  34. google/adk/evaluation/response_evaluator.py +38 -211
  35. google/adk/evaluation/safety_evaluator.py +54 -0
  36. google/adk/evaluation/trajectory_evaluator.py +16 -2
  37. google/adk/evaluation/vertex_ai_eval_facade.py +147 -0
  38. google/adk/events/event.py +2 -4
  39. google/adk/flows/llm_flows/base_llm_flow.py +2 -0
  40. google/adk/memory/in_memory_memory_service.py +3 -2
  41. google/adk/models/lite_llm.py +50 -10
  42. google/adk/runners.py +27 -10
  43. google/adk/sessions/database_session_service.py +25 -7
  44. google/adk/sessions/in_memory_session_service.py +5 -1
  45. google/adk/sessions/vertex_ai_session_service.py +67 -42
  46. google/adk/tools/bigquery/config.py +11 -1
  47. google/adk/tools/bigquery/query_tool.py +306 -12
  48. google/adk/tools/enterprise_search_tool.py +2 -2
  49. google/adk/tools/function_tool.py +7 -1
  50. google/adk/tools/google_search_tool.py +1 -1
  51. google/adk/tools/mcp_tool/mcp_session_manager.py +44 -30
  52. google/adk/tools/mcp_tool/mcp_tool.py +44 -7
  53. google/adk/version.py +1 -1
  54. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/METADATA +6 -4
  55. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/RECORD +58 -42
  56. google/adk/cli/browser/main-JAAWEV7F.js +0 -92
  57. google/adk/cli/browser/polyfills-B6TNHZQ6.js +0 -17
  58. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/WHEEL +0 -0
  59. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/entry_points.txt +0 -0
  60. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,349 @@
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
+ """Utility functions for structured A2A request and response logging."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import sys
21
+
22
+ try:
23
+ from a2a.types import DataPart as A2ADataPart
24
+ from a2a.types import Message as A2AMessage
25
+ from a2a.types import Part as A2APart
26
+ from a2a.types import SendMessageRequest
27
+ from a2a.types import SendMessageResponse
28
+ from a2a.types import Task as A2ATask
29
+ from a2a.types import TextPart as A2ATextPart
30
+ except ImportError as e:
31
+ if sys.version_info < (3, 10):
32
+ raise ImportError(
33
+ "A2A Tool requires Python 3.10 or above. Please upgrade your Python"
34
+ " version."
35
+ ) from e
36
+ else:
37
+ raise e
38
+
39
+
40
+ # Constants
41
+ _NEW_LINE = "\n"
42
+ _EXCLUDED_PART_FIELD = {"file": {"bytes"}}
43
+
44
+
45
+ def _is_a2a_task(obj) -> bool:
46
+ """Check if an object is an A2A Task, with fallback for isinstance issues."""
47
+ try:
48
+ return isinstance(obj, A2ATask)
49
+ except (TypeError, AttributeError):
50
+ return type(obj).__name__ == "Task" and hasattr(obj, "status")
51
+
52
+
53
+ def _is_a2a_message(obj) -> bool:
54
+ """Check if an object is an A2A Message, with fallback for isinstance issues."""
55
+ try:
56
+ return isinstance(obj, A2AMessage)
57
+ except (TypeError, AttributeError):
58
+ return type(obj).__name__ == "Message" and hasattr(obj, "role")
59
+
60
+
61
+ def _is_a2a_text_part(obj) -> bool:
62
+ """Check if an object is an A2A TextPart, with fallback for isinstance issues."""
63
+ try:
64
+ return isinstance(obj, A2ATextPart)
65
+ except (TypeError, AttributeError):
66
+ return type(obj).__name__ == "TextPart" and hasattr(obj, "text")
67
+
68
+
69
+ def _is_a2a_data_part(obj) -> bool:
70
+ """Check if an object is an A2A DataPart, with fallback for isinstance issues."""
71
+ try:
72
+ return isinstance(obj, A2ADataPart)
73
+ except (TypeError, AttributeError):
74
+ return type(obj).__name__ == "DataPart" and hasattr(obj, "data")
75
+
76
+
77
+ def build_message_part_log(part: A2APart) -> str:
78
+ """Builds a log representation of an A2A message part.
79
+
80
+ Args:
81
+ part: The A2A message part to log.
82
+
83
+ Returns:
84
+ A string representation of the part.
85
+ """
86
+ part_content = ""
87
+ if _is_a2a_text_part(part.root):
88
+ part_content = f"TextPart: {part.root.text[:100]}" + (
89
+ "..." if len(part.root.text) > 100 else ""
90
+ )
91
+ elif _is_a2a_data_part(part.root):
92
+ # For data parts, show the data keys but exclude large values
93
+ data_summary = {
94
+ k: (
95
+ f"<{type(v).__name__}>"
96
+ if isinstance(v, (dict, list)) and len(str(v)) > 100
97
+ else v
98
+ )
99
+ for k, v in part.root.data.items()
100
+ }
101
+ part_content = f"DataPart: {json.dumps(data_summary, indent=2)}"
102
+ else:
103
+ part_content = (
104
+ f"{type(part.root).__name__}:"
105
+ f" {part.model_dump_json(exclude_none=True, exclude=_EXCLUDED_PART_FIELD)}"
106
+ )
107
+
108
+ # Add part metadata if it exists
109
+ if hasattr(part.root, "metadata") and part.root.metadata:
110
+ metadata_str = json.dumps(part.root.metadata, indent=2).replace(
111
+ "\n", "\n "
112
+ )
113
+ part_content += f"\n Part Metadata: {metadata_str}"
114
+
115
+ return part_content
116
+
117
+
118
+ def build_a2a_request_log(req: SendMessageRequest) -> str:
119
+ """Builds a structured log representation of an A2A request.
120
+
121
+ Args:
122
+ req: The A2A SendMessageRequest to log.
123
+
124
+ Returns:
125
+ A formatted string representation of the request.
126
+ """
127
+ # Message parts logs
128
+ message_parts_logs = []
129
+ if req.params.message.parts:
130
+ for i, part in enumerate(req.params.message.parts):
131
+ part_log = build_message_part_log(part)
132
+ # Replace any internal newlines with indented newlines to maintain formatting
133
+ part_log_formatted = part_log.replace("\n", "\n ")
134
+ message_parts_logs.append(f"Part {i}: {part_log_formatted}")
135
+
136
+ # Configuration logs
137
+ config_log = "None"
138
+ if req.params.configuration:
139
+ config_data = {
140
+ "acceptedOutputModes": req.params.configuration.acceptedOutputModes,
141
+ "blocking": req.params.configuration.blocking,
142
+ "historyLength": req.params.configuration.historyLength,
143
+ "pushNotificationConfig": bool(
144
+ req.params.configuration.pushNotificationConfig
145
+ ),
146
+ }
147
+ config_log = json.dumps(config_data, indent=2)
148
+
149
+ # Build message metadata section
150
+ message_metadata_section = ""
151
+ if req.params.message.metadata:
152
+ message_metadata_section = f"""
153
+ Metadata:
154
+ {json.dumps(req.params.message.metadata, indent=2).replace(chr(10), chr(10) + ' ')}"""
155
+
156
+ # Build optional sections
157
+ optional_sections = []
158
+
159
+ if req.params.metadata:
160
+ optional_sections.append(
161
+ f"""-----------------------------------------------------------
162
+ Metadata:
163
+ {json.dumps(req.params.metadata, indent=2)}"""
164
+ )
165
+
166
+ optional_sections_str = _NEW_LINE.join(optional_sections)
167
+
168
+ return f"""
169
+ A2A Request:
170
+ -----------------------------------------------------------
171
+ Request ID: {req.id}
172
+ Method: {req.method}
173
+ JSON-RPC: {req.jsonrpc}
174
+ -----------------------------------------------------------
175
+ Message:
176
+ ID: {req.params.message.messageId}
177
+ Role: {req.params.message.role}
178
+ Task ID: {req.params.message.taskId}
179
+ Context ID: {req.params.message.contextId}{message_metadata_section}
180
+ -----------------------------------------------------------
181
+ Message Parts:
182
+ {_NEW_LINE.join(message_parts_logs) if message_parts_logs else "No parts"}
183
+ -----------------------------------------------------------
184
+ Configuration:
185
+ {config_log}
186
+ {optional_sections_str}
187
+ -----------------------------------------------------------
188
+ """
189
+
190
+
191
+ def build_a2a_response_log(resp: SendMessageResponse) -> str:
192
+ """Builds a structured log representation of an A2A response.
193
+
194
+ Args:
195
+ resp: The A2A SendMessageResponse to log.
196
+
197
+ Returns:
198
+ A formatted string representation of the response.
199
+ """
200
+ # Handle error responses
201
+ if hasattr(resp.root, "error"):
202
+ return f"""
203
+ A2A Response:
204
+ -----------------------------------------------------------
205
+ Type: ERROR
206
+ Error Code: {resp.root.error.code}
207
+ Error Message: {resp.root.error.message}
208
+ Error Data: {json.dumps(resp.root.error.data, indent=2) if resp.root.error.data else "None"}
209
+ -----------------------------------------------------------
210
+ Response ID: {resp.root.id}
211
+ JSON-RPC: {resp.root.jsonrpc}
212
+ -----------------------------------------------------------
213
+ """
214
+
215
+ # Handle success responses
216
+ result = resp.root.result
217
+ result_type = type(result).__name__
218
+
219
+ # Build result details based on type
220
+ result_details = []
221
+
222
+ if _is_a2a_task(result):
223
+ result_details.extend([
224
+ f"Task ID: {result.id}",
225
+ f"Context ID: {result.contextId}",
226
+ f"Status State: {result.status.state}",
227
+ f"Status Timestamp: {result.status.timestamp}",
228
+ f"History Length: {len(result.history) if result.history else 0}",
229
+ f"Artifacts Count: {len(result.artifacts) if result.artifacts else 0}",
230
+ ])
231
+
232
+ # Add task metadata if it exists
233
+ if result.metadata:
234
+ result_details.append("Task Metadata:")
235
+ metadata_formatted = json.dumps(result.metadata, indent=2).replace(
236
+ "\n", "\n "
237
+ )
238
+ result_details.append(f" {metadata_formatted}")
239
+
240
+ elif _is_a2a_message(result):
241
+ result_details.extend([
242
+ f"Message ID: {result.messageId}",
243
+ f"Role: {result.role}",
244
+ f"Task ID: {result.taskId}",
245
+ f"Context ID: {result.contextId}",
246
+ ])
247
+
248
+ # Add message parts
249
+ if result.parts:
250
+ result_details.append("Message Parts:")
251
+ for i, part in enumerate(result.parts):
252
+ part_log = build_message_part_log(part)
253
+ # Replace any internal newlines with indented newlines to maintain formatting
254
+ part_log_formatted = part_log.replace("\n", "\n ")
255
+ result_details.append(f" Part {i}: {part_log_formatted}")
256
+
257
+ # Add metadata if it exists
258
+ if result.metadata:
259
+ result_details.append("Metadata:")
260
+ metadata_formatted = json.dumps(result.metadata, indent=2).replace(
261
+ "\n", "\n "
262
+ )
263
+ result_details.append(f" {metadata_formatted}")
264
+
265
+ else:
266
+ # Handle other result types by showing their JSON representation
267
+ if hasattr(result, "model_dump_json"):
268
+ try:
269
+ result_json = result.model_dump_json()
270
+ result_details.append(f"JSON Data: {result_json}")
271
+ except Exception:
272
+ result_details.append("JSON Data: <unable to serialize>")
273
+
274
+ # Build status message section
275
+ status_message_section = "None"
276
+ if _is_a2a_task(result) and result.status.message:
277
+ status_parts_logs = []
278
+ if result.status.message.parts:
279
+ for i, part in enumerate(result.status.message.parts):
280
+ part_log = build_message_part_log(part)
281
+ # Replace any internal newlines with indented newlines to maintain formatting
282
+ part_log_formatted = part_log.replace("\n", "\n ")
283
+ status_parts_logs.append(f"Part {i}: {part_log_formatted}")
284
+
285
+ # Build status message metadata section
286
+ status_metadata_section = ""
287
+ if result.status.message.metadata:
288
+ status_metadata_section = f"""
289
+ Metadata:
290
+ {json.dumps(result.status.message.metadata, indent=2)}"""
291
+
292
+ status_message_section = f"""ID: {result.status.message.messageId}
293
+ Role: {result.status.message.role}
294
+ Task ID: {result.status.message.taskId}
295
+ Context ID: {result.status.message.contextId}
296
+ Message Parts:
297
+ {_NEW_LINE.join(status_parts_logs) if status_parts_logs else "No parts"}{status_metadata_section}"""
298
+
299
+ # Build history section
300
+ history_section = "No history"
301
+ if _is_a2a_task(result) and result.history:
302
+ history_logs = []
303
+ for i, message in enumerate(result.history):
304
+ message_parts_logs = []
305
+ if message.parts:
306
+ for j, part in enumerate(message.parts):
307
+ part_log = build_message_part_log(part)
308
+ # Replace any internal newlines with indented newlines to maintain formatting
309
+ part_log_formatted = part_log.replace("\n", "\n ")
310
+ message_parts_logs.append(f" Part {j}: {part_log_formatted}")
311
+
312
+ # Build message metadata section
313
+ message_metadata_section = ""
314
+ if message.metadata:
315
+ message_metadata_section = f"""
316
+ Metadata:
317
+ {json.dumps(message.metadata, indent=2).replace(chr(10), chr(10) + ' ')}"""
318
+
319
+ history_logs.append(
320
+ f"""Message {i + 1}:
321
+ ID: {message.messageId}
322
+ Role: {message.role}
323
+ Task ID: {message.taskId}
324
+ Context ID: {message.contextId}
325
+ Message Parts:
326
+ {_NEW_LINE.join(message_parts_logs) if message_parts_logs else " No parts"}{message_metadata_section}"""
327
+ )
328
+
329
+ history_section = _NEW_LINE.join(history_logs)
330
+
331
+ return f"""
332
+ A2A Response:
333
+ -----------------------------------------------------------
334
+ Type: SUCCESS
335
+ Result Type: {result_type}
336
+ -----------------------------------------------------------
337
+ Result Details:
338
+ {_NEW_LINE.join(result_details)}
339
+ -----------------------------------------------------------
340
+ Status Message:
341
+ {status_message_section}
342
+ -----------------------------------------------------------
343
+ History:
344
+ {history_section}
345
+ -----------------------------------------------------------
346
+ Response ID: {resp.root.id}
347
+ JSON-RPC: {resp.root.jsonrpc}
348
+ -----------------------------------------------------------
349
+ """
@@ -20,8 +20,10 @@ from typing import AsyncGenerator
20
20
  from typing import Awaitable
21
21
  from typing import Callable
22
22
  from typing import final
23
+ from typing import Mapping
23
24
  from typing import Optional
24
25
  from typing import TYPE_CHECKING
26
+ from typing import TypeVar
25
27
  from typing import Union
26
28
 
27
29
  from google.genai import types
@@ -56,6 +58,8 @@ AfterAgentCallback: TypeAlias = Union[
56
58
  list[_SingleAgentCallback],
57
59
  ]
58
60
 
61
+ SelfAgent = TypeVar('SelfAgent', bound='BaseAgent')
62
+
59
63
 
60
64
  class BaseAgent(BaseModel):
61
65
  """Base class for all agents in Agent Development Kit."""
@@ -121,6 +125,56 @@ class BaseAgent(BaseModel):
121
125
  response and appended to event history as agent response.
122
126
  """
123
127
 
128
+ def clone(
129
+ self: SelfAgent, update: Mapping[str, Any] | None = None
130
+ ) -> SelfAgent:
131
+ """Creates a copy of this agent instance.
132
+
133
+ Args:
134
+ update: Optional mapping of new values for the fields of the cloned agent.
135
+ The keys of the mapping are the names of the fields to be updated, and
136
+ the values are the new values for those fields.
137
+ For example: {"name": "cloned_agent"}
138
+
139
+ Returns:
140
+ A new agent instance with identical configuration as the original
141
+ agent except for the fields specified in the update.
142
+ """
143
+ if update is not None and 'parent_agent' in update:
144
+ raise ValueError(
145
+ 'Cannot update `parent_agent` field in clone. Parent agent is set'
146
+ ' only when the parent agent is instantiated with the sub-agents.'
147
+ )
148
+
149
+ # Only allow updating fields that are defined in the agent class.
150
+ allowed_fields = set(self.__class__.model_fields)
151
+ if update is not None:
152
+ invalid_fields = set(update) - allowed_fields
153
+ if invalid_fields:
154
+ raise ValueError(
155
+ f'Cannot update non-existent fields in {self.__class__.__name__}:'
156
+ f' {invalid_fields}'
157
+ )
158
+
159
+ cloned_agent = self.model_copy(update=update)
160
+
161
+ if update is None or 'sub_agents' not in update:
162
+ # If `sub_agents` is not provided in the update, need to recursively clone
163
+ # the sub-agents to avoid sharing the sub-agents with the original agent.
164
+ cloned_agent.sub_agents = []
165
+ for sub_agent in self.sub_agents:
166
+ cloned_sub_agent = sub_agent.clone()
167
+ cloned_sub_agent.parent_agent = cloned_agent
168
+ cloned_agent.sub_agents.append(cloned_sub_agent)
169
+ else:
170
+ for sub_agent in cloned_agent.sub_agents:
171
+ sub_agent.parent_agent = cloned_agent
172
+
173
+ # Remove the parent agent from the cloned agent to avoid sharing the parent
174
+ # agent with the cloned agent.
175
+ cloned_agent.parent_agent = None
176
+ return cloned_agent
177
+
124
178
  @final
125
179
  async def run_async(
126
180
  self,
@@ -431,16 +431,31 @@ class LlmAgent(BaseAgent):
431
431
 
432
432
  def __maybe_save_output_to_state(self, event: Event):
433
433
  """Saves the model output to state if needed."""
434
+ # skip if the event was authored by some other agent (e.g. current agent
435
+ # transferred to another agent)
436
+ if event.author != self.name:
437
+ logger.debug(
438
+ 'Skipping output save for agent %s: event authored by %s',
439
+ self.name,
440
+ event.author,
441
+ )
442
+ return
434
443
  if (
435
444
  self.output_key
436
445
  and event.is_final_response()
437
446
  and event.content
438
447
  and event.content.parts
439
448
  ):
449
+
440
450
  result = ''.join(
441
451
  [part.text if part.text else '' for part in event.content.parts]
442
452
  )
443
453
  if self.output_schema:
454
+ # If the result from the final chunk is just whitespace or empty,
455
+ # it means this is an empty final chunk of a stream.
456
+ # Do not attempt to parse it as JSON.
457
+ if not result.strip():
458
+ return
444
459
  result = self.output_schema.model_validate_json(result).model_dump(
445
460
  exclude_none=True
446
461
  )