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.
- google/adk/a2a/converters/event_converter.py +257 -36
- google/adk/a2a/converters/part_converter.py +93 -25
- google/adk/a2a/converters/request_converter.py +12 -32
- google/adk/a2a/converters/utils.py +22 -4
- google/adk/a2a/executor/__init__.py +13 -0
- google/adk/a2a/executor/a2a_agent_executor.py +260 -0
- google/adk/a2a/executor/task_result_aggregator.py +71 -0
- google/adk/a2a/logs/__init__.py +13 -0
- google/adk/a2a/logs/log_utils.py +349 -0
- google/adk/agents/base_agent.py +54 -0
- google/adk/agents/llm_agent.py +15 -0
- google/adk/agents/remote_a2a_agent.py +532 -0
- google/adk/artifacts/in_memory_artifact_service.py +6 -3
- google/adk/cli/browser/chunk-EQDQRRRY.js +1 -0
- google/adk/cli/browser/chunk-TXJFAAIW.js +2 -0
- google/adk/cli/browser/index.html +4 -3
- google/adk/cli/browser/main-RXDVX3K6.js +3914 -0
- google/adk/cli/browser/polyfills-FFHMD2TL.js +17 -0
- google/adk/cli/cli_deploy.py +4 -1
- google/adk/cli/cli_eval.py +8 -6
- google/adk/cli/cli_tools_click.py +30 -10
- google/adk/cli/fast_api.py +120 -5
- google/adk/cli/utils/agent_loader.py +12 -0
- google/adk/evaluation/agent_evaluator.py +107 -10
- google/adk/evaluation/base_eval_service.py +157 -0
- google/adk/evaluation/constants.py +20 -0
- google/adk/evaluation/eval_case.py +3 -3
- google/adk/evaluation/eval_metrics.py +39 -0
- google/adk/evaluation/evaluation_generator.py +1 -1
- google/adk/evaluation/final_response_match_v2.py +230 -0
- google/adk/evaluation/llm_as_judge.py +141 -0
- google/adk/evaluation/llm_as_judge_utils.py +48 -0
- google/adk/evaluation/metric_evaluator_registry.py +89 -0
- google/adk/evaluation/response_evaluator.py +38 -211
- google/adk/evaluation/safety_evaluator.py +54 -0
- google/adk/evaluation/trajectory_evaluator.py +16 -2
- google/adk/evaluation/vertex_ai_eval_facade.py +147 -0
- google/adk/events/event.py +2 -4
- google/adk/flows/llm_flows/base_llm_flow.py +2 -0
- google/adk/memory/in_memory_memory_service.py +3 -2
- google/adk/models/lite_llm.py +50 -10
- google/adk/runners.py +27 -10
- google/adk/sessions/database_session_service.py +25 -7
- google/adk/sessions/in_memory_session_service.py +5 -1
- google/adk/sessions/vertex_ai_session_service.py +67 -42
- google/adk/tools/bigquery/config.py +11 -1
- google/adk/tools/bigquery/query_tool.py +306 -12
- google/adk/tools/enterprise_search_tool.py +2 -2
- google/adk/tools/function_tool.py +7 -1
- google/adk/tools/google_search_tool.py +1 -1
- google/adk/tools/mcp_tool/mcp_session_manager.py +44 -30
- google/adk/tools/mcp_tool/mcp_tool.py +44 -7
- google/adk/version.py +1 -1
- {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/METADATA +6 -4
- {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/RECORD +58 -42
- google/adk/cli/browser/main-JAAWEV7F.js +0 -92
- google/adk/cli/browser/polyfills-B6TNHZQ6.js +0 -17
- {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/WHEEL +0 -0
- {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/entry_points.txt +0 -0
- {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
|
+
"""
|
google/adk/agents/base_agent.py
CHANGED
@@ -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,
|
google/adk/agents/llm_agent.py
CHANGED
@@ -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
|
)
|