google-adk 1.6.1__py3-none-any.whl → 1.7.0__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 (81) hide show
  1. google/adk/a2a/converters/event_converter.py +5 -85
  2. google/adk/a2a/executor/a2a_agent_executor.py +45 -16
  3. google/adk/agents/__init__.py +5 -0
  4. google/adk/agents/agent_config.py +46 -0
  5. google/adk/agents/base_agent.py +234 -41
  6. google/adk/agents/callback_context.py +41 -0
  7. google/adk/agents/common_configs.py +79 -0
  8. google/adk/agents/config_agent_utils.py +184 -0
  9. google/adk/agents/config_schemas/AgentConfig.json +544 -0
  10. google/adk/agents/invocation_context.py +5 -1
  11. google/adk/agents/llm_agent.py +190 -9
  12. google/adk/agents/loop_agent.py +29 -0
  13. google/adk/agents/parallel_agent.py +24 -3
  14. google/adk/agents/remote_a2a_agent.py +15 -3
  15. google/adk/agents/sequential_agent.py +22 -1
  16. google/adk/artifacts/gcs_artifact_service.py +24 -2
  17. google/adk/auth/auth_handler.py +3 -3
  18. google/adk/auth/credential_manager.py +23 -23
  19. google/adk/auth/credential_service/base_credential_service.py +6 -6
  20. google/adk/auth/credential_service/in_memory_credential_service.py +10 -8
  21. google/adk/auth/credential_service/session_state_credential_service.py +8 -8
  22. google/adk/auth/exchanger/oauth2_credential_exchanger.py +3 -3
  23. google/adk/auth/oauth2_credential_util.py +2 -2
  24. google/adk/auth/refresher/oauth2_credential_refresher.py +4 -4
  25. google/adk/cli/agent_graph.py +3 -1
  26. google/adk/cli/browser/index.html +1 -1
  27. google/adk/cli/browser/main-SRBSE46V.js +3914 -0
  28. google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
  29. google/adk/cli/fast_api.py +42 -2
  30. google/adk/cli/utils/agent_loader.py +35 -1
  31. google/adk/code_executors/base_code_executor.py +14 -19
  32. google/adk/code_executors/built_in_code_executor.py +4 -1
  33. google/adk/evaluation/base_eval_service.py +46 -2
  34. google/adk/evaluation/evaluation_generator.py +1 -1
  35. google/adk/evaluation/in_memory_eval_sets_manager.py +151 -0
  36. google/adk/evaluation/local_eval_service.py +389 -0
  37. google/adk/evaluation/local_eval_sets_manager.py +23 -8
  38. google/adk/flows/llm_flows/auto_flow.py +6 -11
  39. google/adk/flows/llm_flows/base_llm_flow.py +41 -23
  40. google/adk/flows/llm_flows/contents.py +16 -10
  41. google/adk/flows/llm_flows/functions.py +76 -33
  42. google/adk/memory/in_memory_memory_service.py +20 -14
  43. google/adk/models/anthropic_llm.py +44 -5
  44. google/adk/models/google_llm.py +11 -6
  45. google/adk/models/lite_llm.py +21 -4
  46. google/adk/plugins/__init__.py +17 -0
  47. google/adk/plugins/base_plugin.py +317 -0
  48. google/adk/plugins/plugin_manager.py +265 -0
  49. google/adk/runners.py +122 -18
  50. google/adk/sessions/database_session_service.py +26 -28
  51. google/adk/sessions/vertex_ai_session_service.py +14 -7
  52. google/adk/tools/agent_tool.py +1 -0
  53. google/adk/tools/apihub_tool/apihub_toolset.py +38 -39
  54. google/adk/tools/application_integration_tool/application_integration_toolset.py +35 -37
  55. google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -3
  56. google/adk/tools/base_tool.py +9 -9
  57. google/adk/tools/base_toolset.py +7 -5
  58. google/adk/tools/bigquery/__init__.py +3 -3
  59. google/adk/tools/enterprise_search_tool.py +4 -2
  60. google/adk/tools/google_api_tool/google_api_tool.py +16 -1
  61. google/adk/tools/google_api_tool/google_api_toolset.py +9 -7
  62. google/adk/tools/google_api_tool/google_api_toolsets.py +41 -20
  63. google/adk/tools/google_search_tool.py +4 -2
  64. google/adk/tools/langchain_tool.py +2 -3
  65. google/adk/tools/long_running_tool.py +21 -0
  66. google/adk/tools/mcp_tool/mcp_toolset.py +27 -28
  67. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +8 -8
  68. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +4 -6
  69. google/adk/tools/retrieval/vertex_ai_rag_retrieval.py +3 -2
  70. google/adk/tools/tool_context.py +0 -10
  71. google/adk/tools/url_context_tool.py +4 -2
  72. google/adk/tools/vertex_ai_search_tool.py +4 -2
  73. google/adk/utils/model_name_utils.py +90 -0
  74. google/adk/version.py +1 -1
  75. {google_adk-1.6.1.dist-info → google_adk-1.7.0.dist-info}/METADATA +2 -2
  76. {google_adk-1.6.1.dist-info → google_adk-1.7.0.dist-info}/RECORD +79 -69
  77. google/adk/cli/browser/main-RXDVX3K6.js +0 -3914
  78. google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
  79. {google_adk-1.6.1.dist-info → google_adk-1.7.0.dist-info}/WHEEL +0 -0
  80. {google_adk-1.6.1.dist-info → google_adk-1.7.0.dist-info}/entry_points.txt +0 -0
  81. {google_adk-1.6.1.dist-info → google_adk-1.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -153,37 +153,67 @@ async def handle_function_calls_async(
153
153
  # do not use "args" as the variable name, because it is a reserved keyword
154
154
  # in python debugger.
155
155
  function_args = function_call.args or {}
156
- function_response: Optional[dict] = None
157
156
 
158
- for callback in agent.canonical_before_tool_callbacks:
159
- function_response = callback(
160
- tool=tool, args=function_args, tool_context=tool_context
161
- )
162
- if inspect.isawaitable(function_response):
163
- function_response = await function_response
164
- if function_response:
165
- break
157
+ # Step 1: Check if plugin before_tool_callback overrides the function
158
+ # response.
159
+ function_response = (
160
+ await invocation_context.plugin_manager.run_before_tool_callback(
161
+ tool=tool, tool_args=function_args, tool_context=tool_context
162
+ )
163
+ )
166
164
 
167
- if not function_response:
165
+ # Step 2: If no overrides are provided from the plugins, further run the
166
+ # canonical callback.
167
+ if function_response is None:
168
+ for callback in agent.canonical_before_tool_callbacks:
169
+ function_response = callback(
170
+ tool=tool, args=function_args, tool_context=tool_context
171
+ )
172
+ if inspect.isawaitable(function_response):
173
+ function_response = await function_response
174
+ if function_response:
175
+ break
176
+
177
+ # Step 3: Otherwise, proceed calling the tool normally.
178
+ if function_response is None:
168
179
  function_response = await __call_tool_async(
169
180
  tool, args=function_args, tool_context=tool_context
170
181
  )
171
182
 
172
- for callback in agent.canonical_after_tool_callbacks:
173
- altered_function_response = callback(
174
- tool=tool,
175
- args=function_args,
176
- tool_context=tool_context,
177
- tool_response=function_response,
178
- )
179
- if inspect.isawaitable(altered_function_response):
180
- altered_function_response = await altered_function_response
181
- if altered_function_response is not None:
182
- function_response = altered_function_response
183
- break
183
+ # Step 4: Check if plugin after_tool_callback overrides the function
184
+ # response.
185
+ altered_function_response = (
186
+ await invocation_context.plugin_manager.run_after_tool_callback(
187
+ tool=tool,
188
+ tool_args=function_args,
189
+ tool_context=tool_context,
190
+ result=function_response,
191
+ )
192
+ )
193
+
194
+ # Step 5: If no overrides are provided from the plugins, further run the
195
+ # canonical after_tool_callbacks.
196
+ if altered_function_response is None:
197
+ for callback in agent.canonical_after_tool_callbacks:
198
+ altered_function_response = callback(
199
+ tool=tool,
200
+ args=function_args,
201
+ tool_context=tool_context,
202
+ tool_response=function_response,
203
+ )
204
+ if inspect.isawaitable(altered_function_response):
205
+ altered_function_response = await altered_function_response
206
+ if altered_function_response:
207
+ break
208
+
209
+ # Step 6: If alternative response exists from after_tool_callback, use it
210
+ # instead of the original function response.
211
+ if altered_function_response is not None:
212
+ function_response = altered_function_response
184
213
 
185
214
  if tool.is_long_running:
186
- # Allow long running function to return None to not provide function response.
215
+ # Allow long running function to return None to not provide function
216
+ # response.
187
217
  if not function_response:
188
218
  continue
189
219
 
@@ -264,6 +294,7 @@ async def handle_function_calls_live(
264
294
  # )
265
295
  # if new_response:
266
296
  # function_response = new_response
297
+ altered_function_response = None
267
298
  if agent.after_tool_callback:
268
299
  altered_function_response = agent.after_tool_callback(
269
300
  tool=tool,
@@ -273,8 +304,8 @@ async def handle_function_calls_live(
273
304
  )
274
305
  if inspect.isawaitable(altered_function_response):
275
306
  altered_function_response = await altered_function_response
276
- if altered_function_response is not None:
277
- function_response = altered_function_response
307
+ if altered_function_response is not None:
308
+ function_response = altered_function_response
278
309
 
279
310
  if tool.is_long_running:
280
311
  # Allow async function to return None to not provide function response.
@@ -480,6 +511,16 @@ def __build_response_event(
480
511
  return function_response_event
481
512
 
482
513
 
514
+ def deep_merge_dicts(d1: dict, d2: dict) -> dict:
515
+ """Recursively merges d2 into d1."""
516
+ for key, value in d2.items():
517
+ if key in d1 and isinstance(d1[key], dict) and isinstance(value, dict):
518
+ d1[key] = deep_merge_dicts(d1[key], value)
519
+ else:
520
+ d1[key] = value
521
+ return d1
522
+
523
+
483
524
  def merge_parallel_function_response_events(
484
525
  function_response_events: list['Event'],
485
526
  ) -> 'Event':
@@ -498,15 +539,17 @@ def merge_parallel_function_response_events(
498
539
  base_event = function_response_events[0]
499
540
 
500
541
  # Merge actions from all events
501
-
502
- merged_actions = EventActions()
503
- merged_requested_auth_configs = {}
542
+ merged_actions_data = {}
504
543
  for event in function_response_events:
505
- merged_requested_auth_configs.update(event.actions.requested_auth_configs)
506
- merged_actions = merged_actions.model_copy(
507
- update=event.actions.model_dump()
508
- )
509
- merged_actions.requested_auth_configs = merged_requested_auth_configs
544
+ if event.actions:
545
+ # Use `by_alias=True` because it converts the model to a dictionary while respecting field aliases, ensuring that the enum fields are correctly handled without creating a duplicate.
546
+ merged_actions_data = deep_merge_dicts(
547
+ merged_actions_data,
548
+ event.actions.model_dump(exclude_none=True, by_alias=True),
549
+ )
550
+
551
+ merged_actions = EventActions.model_validate(merged_actions_data)
552
+
510
553
  # Create the new merged event
511
554
  merged_event = Event(
512
555
  invocation_id=Event.new_id(),
@@ -14,6 +14,7 @@
14
14
  from __future__ import annotations
15
15
 
16
16
  import re
17
+ import threading
17
18
  from typing import TYPE_CHECKING
18
19
 
19
20
  from typing_extensions import override
@@ -42,38 +43,43 @@ class InMemoryMemoryService(BaseMemoryService):
42
43
 
43
44
  Uses keyword matching instead of semantic search.
44
45
 
45
- It is not suitable for multi-threaded production environments. Use it for
46
- testing and development only.
46
+ This class is thread-safe, however, it should be used for testing and
47
+ development only.
47
48
  """
48
49
 
49
50
  def __init__(self):
51
+ self._lock = threading.Lock()
52
+
50
53
  self._session_events: dict[str, dict[str, list[Event]]] = {}
51
- """Keys are app_name/user_id, session_id. Values are session event lists."""
54
+ """Keys are "{app_name}/{user_id}". Values are dicts of session_id to
55
+ session event lists.
56
+ """
52
57
 
53
58
  @override
54
59
  async def add_session_to_memory(self, session: Session):
55
60
  user_key = _user_key(session.app_name, session.user_id)
56
- self._session_events[user_key] = self._session_events.get(
57
- _user_key(session.app_name, session.user_id), {}
58
- )
59
- self._session_events[user_key][session.id] = [
60
- event
61
- for event in session.events
62
- if event.content and event.content.parts
63
- ]
61
+
62
+ with self._lock:
63
+ self._session_events[user_key] = self._session_events.get(user_key, {})
64
+ self._session_events[user_key][session.id] = [
65
+ event
66
+ for event in session.events
67
+ if event.content and event.content.parts
68
+ ]
64
69
 
65
70
  @override
66
71
  async def search_memory(
67
72
  self, *, app_name: str, user_id: str, query: str
68
73
  ) -> SearchMemoryResponse:
69
74
  user_key = _user_key(app_name, user_id)
70
- if user_key not in self._session_events:
71
- return SearchMemoryResponse()
75
+
76
+ with self._lock:
77
+ session_event_lists = self._session_events.get(user_key, {})
72
78
 
73
79
  words_in_query = set(query.lower().split())
74
80
  response = SearchMemoryResponse()
75
81
 
76
- for session_events in self._session_events[user_key].values():
82
+ for session_events in session_event_lists.values():
77
83
  for event in session_events:
78
84
  if not event.content or not event.content.parts:
79
85
  continue
@@ -16,6 +16,7 @@
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import base64
19
20
  from functools import cached_property
20
21
  import logging
21
22
  import os
@@ -45,7 +46,7 @@ __all__ = ["Claude"]
45
46
 
46
47
  logger = logging.getLogger("google_adk." + __name__)
47
48
 
48
- MAX_TOKEN = 1024
49
+ MAX_TOKEN = 8192
49
50
 
50
51
 
51
52
  class ClaudeRequest(BaseModel):
@@ -70,6 +71,14 @@ def to_google_genai_finish_reason(
70
71
  return "FINISH_REASON_UNSPECIFIED"
71
72
 
72
73
 
74
+ def _is_image_part(part: types.Part) -> bool:
75
+ return (
76
+ part.inline_data
77
+ and part.inline_data.mime_type
78
+ and part.inline_data.mime_type.startswith("image")
79
+ )
80
+
81
+
73
82
  def part_to_message_block(
74
83
  part: types.Part,
75
84
  ) -> Union[
@@ -80,7 +89,7 @@ def part_to_message_block(
80
89
  ]:
81
90
  if part.text:
82
91
  return anthropic_types.TextBlockParam(text=part.text, type="text")
83
- if part.function_call:
92
+ elif part.function_call:
84
93
  assert part.function_call.name
85
94
 
86
95
  return anthropic_types.ToolUseBlockParam(
@@ -89,7 +98,7 @@ def part_to_message_block(
89
98
  input=part.function_call.args,
90
99
  type="tool_use",
91
100
  )
92
- if part.function_response:
101
+ elif part.function_response:
93
102
  content = ""
94
103
  if (
95
104
  "result" in part.function_response.response
@@ -105,15 +114,45 @@ def part_to_message_block(
105
114
  content=content,
106
115
  is_error=False,
107
116
  )
108
- raise NotImplementedError("Not supported yet.")
117
+ elif _is_image_part(part):
118
+ data = base64.b64encode(part.inline_data.data).decode()
119
+ return anthropic_types.ImageBlockParam(
120
+ type="image",
121
+ source=dict(
122
+ type="base64", media_type=part.inline_data.mime_type, data=data
123
+ ),
124
+ )
125
+ elif part.executable_code:
126
+ return anthropic_types.TextBlockParam(
127
+ type="text",
128
+ text="Code:```python\n" + part.executable_code.code + "\n```",
129
+ )
130
+ elif part.code_execution_result:
131
+ return anthropic_types.TextBlockParam(
132
+ text="Execution Result:```code_output\n"
133
+ + part.code_execution_result.output
134
+ + "\n```",
135
+ type="text",
136
+ )
137
+
138
+ raise NotImplementedError(f"Not supported yet: {part}")
109
139
 
110
140
 
111
141
  def content_to_message_param(
112
142
  content: types.Content,
113
143
  ) -> anthropic_types.MessageParam:
144
+ message_block = []
145
+ for part in content.parts or []:
146
+ # Image data is not supported in Claude for model turns.
147
+ if _is_image_part(part):
148
+ logger.warning("Image data is not supported in Claude for model turns.")
149
+ continue
150
+
151
+ message_block.append(part_to_message_block(part))
152
+
114
153
  return {
115
154
  "role": to_claude_role(content.role),
116
- "content": [part_to_message_block(part) for part in content.parts or []],
155
+ "content": message_block,
117
156
  }
118
157
 
119
158
 
@@ -27,6 +27,7 @@ from typing import Union
27
27
 
28
28
  from google.genai import Client
29
29
  from google.genai import types
30
+ from google.genai.types import FinishReason
30
31
  from typing_extensions import override
31
32
 
32
33
  from .. import version
@@ -150,12 +151,10 @@ class Gemini(BaseLlm):
150
151
  thought_text = ''
151
152
  text = ''
152
153
  yield llm_response
153
- if (
154
- (text or thought_text)
155
- and response
156
- and response.candidates
157
- and response.candidates[0].finish_reason == types.FinishReason.STOP
158
- ):
154
+
155
+ # generate an aggregated content at the end regardless the
156
+ # response.candidates[0].finish_reason
157
+ if (text or thought_text) and response and response.candidates:
159
158
  parts = []
160
159
  if thought_text:
161
160
  parts.append(types.Part(text=thought_text, thought=True))
@@ -163,6 +162,12 @@ class Gemini(BaseLlm):
163
162
  parts.append(types.Part.from_text(text=text))
164
163
  yield LlmResponse(
165
164
  content=types.ModelContent(parts=parts),
165
+ error_code=None
166
+ if response.candidates[0].finish_reason == FinishReason.STOP
167
+ else response.candidates[0].finish_reason,
168
+ error_message=None
169
+ if response.candidates[0].finish_reason == FinishReason.STOP
170
+ else response.candidates[0].finish_message,
166
171
  usage_metadata=usage_metadata,
167
172
  )
168
173
 
@@ -35,11 +35,14 @@ from litellm import acompletion
35
35
  from litellm import ChatCompletionAssistantMessage
36
36
  from litellm import ChatCompletionAssistantToolCall
37
37
  from litellm import ChatCompletionDeveloperMessage
38
+ from litellm import ChatCompletionFileObject
39
+ from litellm import ChatCompletionImageObject
38
40
  from litellm import ChatCompletionImageUrlObject
39
41
  from litellm import ChatCompletionMessageToolCall
40
42
  from litellm import ChatCompletionTextObject
41
43
  from litellm import ChatCompletionToolMessage
42
44
  from litellm import ChatCompletionUserMessage
45
+ from litellm import ChatCompletionVideoObject
43
46
  from litellm import ChatCompletionVideoUrlObject
44
47
  from litellm import completion
45
48
  from litellm import CustomStreamWrapper
@@ -249,17 +252,31 @@ def _get_content(
249
252
  data_uri = f"data:{part.inline_data.mime_type};base64,{base64_string}"
250
253
 
251
254
  if part.inline_data.mime_type.startswith("image"):
255
+ # Extract format from mime type (e.g., "image/png" -> "png")
256
+ format_type = part.inline_data.mime_type.split("/")[-1]
252
257
  content_objects.append(
253
- ChatCompletionImageUrlObject(
258
+ ChatCompletionImageObject(
254
259
  type="image_url",
255
- image_url=data_uri,
260
+ image_url=ChatCompletionImageUrlObject(
261
+ url=data_uri, format=format_type
262
+ ),
256
263
  )
257
264
  )
258
265
  elif part.inline_data.mime_type.startswith("video"):
266
+ # Extract format from mime type (e.g., "video/mp4" -> "mp4")
267
+ format_type = part.inline_data.mime_type.split("/")[-1]
259
268
  content_objects.append(
260
- ChatCompletionVideoUrlObject(
269
+ ChatCompletionVideoObject(
261
270
  type="video_url",
262
- video_url=data_uri,
271
+ video_url=ChatCompletionVideoUrlObject(
272
+ url=data_uri, format=format_type
273
+ ),
274
+ )
275
+ )
276
+ elif part.inline_data.mime_type == "application/pdf":
277
+ content_objects.append(
278
+ ChatCompletionFileObject(
279
+ type="file", file={"file_data": data_uri, "format": "pdf"}
263
280
  )
264
281
  )
265
282
  else:
@@ -0,0 +1,17 @@
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 in 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 .base_plugin import BasePlugin
16
+
17
+ __all__ = ['BasePlugin']