google-adk 1.2.0__py3-none-any.whl → 1.4.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 (94) hide show
  1. google/adk/a2a/__init__.py +13 -0
  2. google/adk/a2a/converters/__init__.py +13 -0
  3. google/adk/a2a/converters/part_converter.py +166 -0
  4. google/adk/agents/invocation_context.py +2 -0
  5. google/adk/agents/llm_agent.py +1 -6
  6. google/adk/agents/run_config.py +11 -0
  7. google/adk/auth/auth_credential.py +5 -0
  8. google/adk/auth/auth_handler.py +22 -96
  9. google/adk/auth/auth_preprocessor.py +3 -3
  10. google/adk/auth/auth_tool.py +46 -0
  11. google/adk/auth/credential_manager.py +265 -0
  12. google/adk/auth/credential_service/__init__.py +13 -0
  13. google/adk/auth/credential_service/base_credential_service.py +75 -0
  14. google/adk/auth/credential_service/in_memory_credential_service.py +64 -0
  15. google/adk/auth/exchanger/__init__.py +23 -0
  16. google/adk/auth/exchanger/base_credential_exchanger.py +57 -0
  17. google/adk/auth/exchanger/credential_exchanger_registry.py +58 -0
  18. google/adk/auth/exchanger/oauth2_credential_exchanger.py +104 -0
  19. google/adk/auth/exchanger/service_account_credential_exchanger.py +104 -0
  20. google/adk/auth/oauth2_credential_util.py +107 -0
  21. google/adk/auth/refresher/__init__.py +21 -0
  22. google/adk/auth/refresher/base_credential_refresher.py +74 -0
  23. google/adk/auth/refresher/credential_refresher_registry.py +59 -0
  24. google/adk/auth/refresher/oauth2_credential_refresher.py +154 -0
  25. google/adk/cli/agent_graph.py +34 -32
  26. google/adk/cli/browser/index.html +2 -2
  27. google/adk/cli/browser/main-JAAWEV7F.js +92 -0
  28. google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
  29. google/adk/cli/cli.py +10 -0
  30. google/adk/cli/cli_deploy.py +80 -21
  31. google/adk/cli/cli_tools_click.py +132 -61
  32. google/adk/cli/fast_api.py +46 -41
  33. google/adk/cli/utils/agent_loader.py +15 -2
  34. google/adk/cli/utils/evals.py +4 -2
  35. google/adk/code_executors/container_code_executor.py +10 -6
  36. google/adk/code_executors/vertex_ai_code_executor.py +8 -2
  37. google/adk/evaluation/_eval_set_results_manager_utils.py +44 -0
  38. google/adk/evaluation/_eval_sets_manager_utils.py +108 -0
  39. google/adk/evaluation/eval_metrics.py +0 -5
  40. google/adk/evaluation/eval_result.py +12 -7
  41. google/adk/evaluation/eval_set_results_manager.py +6 -1
  42. google/adk/evaluation/gcs_eval_set_results_manager.py +121 -0
  43. google/adk/evaluation/gcs_eval_sets_manager.py +196 -0
  44. google/adk/evaluation/local_eval_set_results_manager.py +6 -18
  45. google/adk/evaluation/local_eval_sets_manager.py +27 -78
  46. google/adk/evaluation/response_evaluator.py +5 -5
  47. google/adk/evaluation/trajectory_evaluator.py +9 -6
  48. google/adk/flows/llm_flows/basic.py +9 -0
  49. google/adk/models/anthropic_llm.py +1 -1
  50. google/adk/models/gemini_llm_connection.py +2 -0
  51. google/adk/models/google_llm.py +57 -16
  52. google/adk/models/lite_llm.py +2 -1
  53. google/adk/platform/__init__.py +13 -0
  54. google/adk/platform/internal/__init__.py +15 -0
  55. google/adk/platform/internal/thread.py +30 -0
  56. google/adk/platform/thread.py +31 -0
  57. google/adk/runners.py +8 -2
  58. google/adk/sessions/in_memory_session_service.py +12 -1
  59. google/adk/sessions/vertex_ai_session_service.py +71 -50
  60. google/adk/tools/__init__.py +2 -0
  61. google/adk/tools/_automatic_function_calling_util.py +1 -0
  62. google/adk/tools/_forwarding_artifact_service.py +96 -0
  63. google/adk/tools/_function_parameter_parse_util.py +1 -0
  64. google/adk/tools/agent_tool.py +5 -39
  65. google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -2
  66. google/adk/tools/authenticated_function_tool.py +107 -0
  67. google/adk/tools/base_authenticated_tool.py +107 -0
  68. google/adk/tools/bigquery/bigquery_credentials.py +6 -4
  69. google/adk/tools/bigquery/bigquery_tool.py +22 -9
  70. google/adk/tools/bigquery/bigquery_toolset.py +9 -3
  71. google/adk/tools/bigquery/client.py +7 -3
  72. google/adk/tools/bigquery/config.py +46 -0
  73. google/adk/tools/bigquery/metadata_tool.py +114 -91
  74. google/adk/tools/bigquery/query_tool.py +141 -23
  75. google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +7 -4
  76. google/adk/tools/google_search_tool.py +0 -1
  77. google/adk/tools/mcp_tool/__init__.py +6 -0
  78. google/adk/tools/mcp_tool/mcp_session_manager.py +271 -149
  79. google/adk/tools/mcp_tool/mcp_tool.py +79 -22
  80. google/adk/tools/mcp_tool/mcp_toolset.py +32 -29
  81. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +3 -3
  82. google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +56 -33
  83. google/adk/tools/retrieval/files_retrieval.py +7 -1
  84. google/adk/tools/url_context_tool.py +61 -0
  85. google/adk/tools/vertex_ai_search_tool.py +13 -2
  86. google/adk/utils/feature_decorator.py +175 -0
  87. google/adk/version.py +2 -2
  88. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/METADATA +10 -1
  89. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/RECORD +92 -61
  90. google/adk/cli/browser/main-CS5OLUMF.js +0 -91
  91. google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
  92. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/WHEEL +0 -0
  93. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/entry_points.txt +0 -0
  94. {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -17,10 +17,11 @@ from __future__ import annotations
17
17
  import json
18
18
  import logging
19
19
  import os
20
- import time
21
20
 
22
21
  from typing_extensions import override
23
22
 
23
+ from ..errors.not_found_error import NotFoundError
24
+ from ._eval_set_results_manager_utils import create_eval_set_result
24
25
  from .eval_result import EvalCaseResult
25
26
  from .eval_result import EvalSetResult
26
27
  from .eval_set_results_manager import EvalSetResultsManager
@@ -31,10 +32,6 @@ _ADK_EVAL_HISTORY_DIR = ".adk/eval_history"
31
32
  _EVAL_SET_RESULT_FILE_EXTENSION = ".evalset_result.json"
32
33
 
33
34
 
34
- def _sanitize_eval_set_result_name(eval_set_result_name: str) -> str:
35
- return eval_set_result_name.replace("/", "_")
36
-
37
-
38
35
  class LocalEvalSetResultsManager(EvalSetResultsManager):
39
36
  """An EvalSetResult manager that stores eval set results locally on disk."""
40
37
 
@@ -49,15 +46,8 @@ class LocalEvalSetResultsManager(EvalSetResultsManager):
49
46
  eval_case_results: list[EvalCaseResult],
50
47
  ) -> None:
51
48
  """Creates and saves a new EvalSetResult given eval_case_results."""
52
- timestamp = time.time()
53
- eval_set_result_id = app_name + "_" + eval_set_id + "_" + str(timestamp)
54
- eval_set_result_name = _sanitize_eval_set_result_name(eval_set_result_id)
55
- eval_set_result = EvalSetResult(
56
- eval_set_result_id=eval_set_result_id,
57
- eval_set_result_name=eval_set_result_name,
58
- eval_set_id=eval_set_id,
59
- eval_case_results=eval_case_results,
60
- creation_timestamp=timestamp,
49
+ eval_set_result = create_eval_set_result(
50
+ app_name, eval_set_id, eval_case_results
61
51
  )
62
52
  # Write eval result file, with eval_set_result_name.
63
53
  app_eval_history_dir = self._get_eval_history_dir(app_name)
@@ -67,7 +57,7 @@ class LocalEvalSetResultsManager(EvalSetResultsManager):
67
57
  eval_set_result_json = eval_set_result.model_dump_json()
68
58
  eval_set_result_file_path = os.path.join(
69
59
  app_eval_history_dir,
70
- eval_set_result_name + _EVAL_SET_RESULT_FILE_EXTENSION,
60
+ eval_set_result.eval_set_result_name + _EVAL_SET_RESULT_FILE_EXTENSION,
71
61
  )
72
62
  logger.info("Writing eval result to file: %s", eval_set_result_file_path)
73
63
  with open(eval_set_result_file_path, "w") as f:
@@ -87,9 +77,7 @@ class LocalEvalSetResultsManager(EvalSetResultsManager):
87
77
  + _EVAL_SET_RESULT_FILE_EXTENSION
88
78
  )
89
79
  if not os.path.exists(maybe_eval_result_file_path):
90
- raise ValueError(
91
- f"Eval set result `{eval_set_result_id}` does not exist."
92
- )
80
+ raise NotFoundError(f"Eval set result `{eval_set_result_id}` not found.")
93
81
  with open(maybe_eval_result_file_path, "r") as file:
94
82
  eval_result_data = json.load(file)
95
83
  return EvalSetResult.model_validate_json(eval_result_data)
@@ -27,7 +27,11 @@ from google.genai import types as genai_types
27
27
  from pydantic import ValidationError
28
28
  from typing_extensions import override
29
29
 
30
- from ..errors.not_found_error import NotFoundError
30
+ from ._eval_sets_manager_utils import add_eval_case_to_eval_set
31
+ from ._eval_sets_manager_utils import delete_eval_case_from_eval_set
32
+ from ._eval_sets_manager_utils import get_eval_case_from_eval_set
33
+ from ._eval_sets_manager_utils import get_eval_set_from_app_and_id
34
+ from ._eval_sets_manager_utils import update_eval_case_in_eval_set
31
35
  from .eval_case import EvalCase
32
36
  from .eval_case import IntermediateData
33
37
  from .eval_case import Invocation
@@ -218,7 +222,7 @@ class LocalEvalSetsManager(EvalSetsManager):
218
222
  eval_cases=[],
219
223
  creation_timestamp=time.time(),
220
224
  )
221
- self._write_eval_set(new_eval_set_path, new_eval_set)
225
+ self._write_eval_set_to_path(new_eval_set_path, new_eval_set)
222
226
 
223
227
  @override
224
228
  def list_eval_sets(self, app_name: str) -> list[str]:
@@ -233,51 +237,27 @@ class LocalEvalSetsManager(EvalSetsManager):
233
237
 
234
238
  return sorted(eval_sets)
235
239
 
236
- @override
237
- def add_eval_case(self, app_name: str, eval_set_id: str, eval_case: EvalCase):
238
- """Adds the given EvalCase to an existing EvalSet identified by app_name and eval_set_id.
239
-
240
- Raises:
241
- NotFoundError: If the eval set is not found.
242
- """
243
- eval_case_id = eval_case.eval_id
244
- self._validate_id(id_name="Eval Case Id", id_value=eval_case_id)
245
-
246
- eval_set = self.get_eval_set(app_name, eval_set_id)
247
-
248
- if not eval_set:
249
- raise NotFoundError(f"Eval set `{eval_set_id}` not found.")
250
-
251
- if [x for x in eval_set.eval_cases if x.eval_id == eval_case_id]:
252
- raise ValueError(
253
- f"Eval id `{eval_case_id}` already exists in `{eval_set_id}`"
254
- " eval set.",
255
- )
256
-
257
- eval_set.eval_cases.append(eval_case)
258
-
259
- eval_set_file_path = self._get_eval_set_file_path(app_name, eval_set_id)
260
- self._write_eval_set(eval_set_file_path, eval_set)
261
-
262
240
  @override
263
241
  def get_eval_case(
264
242
  self, app_name: str, eval_set_id: str, eval_case_id: str
265
243
  ) -> Optional[EvalCase]:
266
244
  """Returns an EvalCase if found, otherwise None."""
267
245
  eval_set = self.get_eval_set(app_name, eval_set_id)
268
-
269
246
  if not eval_set:
270
247
  return None
248
+ return get_eval_case_from_eval_set(eval_set, eval_case_id)
271
249
 
272
- eval_case_to_find = None
250
+ @override
251
+ def add_eval_case(self, app_name: str, eval_set_id: str, eval_case: EvalCase):
252
+ """Adds the given EvalCase to an existing EvalSet identified by app_name and eval_set_id.
273
253
 
274
- # Look up the eval case by eval_case_id
275
- for eval_case in eval_set.eval_cases:
276
- if eval_case.eval_id == eval_case_id:
277
- eval_case_to_find = eval_case
278
- break
254
+ Raises:
255
+ NotFoundError: If the eval set is not found.
256
+ """
257
+ eval_set = get_eval_set_from_app_and_id(self, app_name, eval_set_id)
258
+ updated_eval_set = add_eval_case_to_eval_set(eval_set, eval_case)
279
259
 
280
- return eval_case_to_find
260
+ self._save_eval_set(app_name, eval_set_id, updated_eval_set)
281
261
 
282
262
  @override
283
263
  def update_eval_case(
@@ -288,28 +268,9 @@ class LocalEvalSetsManager(EvalSetsManager):
288
268
  Raises:
289
269
  NotFoundError: If the eval set or the eval case is not found.
290
270
  """
291
- eval_case_id = updated_eval_case.eval_id
292
-
293
- # Find the eval case to be updated.
294
- eval_case_to_update = self.get_eval_case(
295
- app_name, eval_set_id, eval_case_id
296
- )
297
-
298
- if eval_case_to_update:
299
- # Remove the eval case from the existing eval set.
300
- eval_set = self.get_eval_set(app_name, eval_set_id)
301
- eval_set.eval_cases.remove(eval_case_to_update)
302
-
303
- # Add the updated eval case to the existing eval set.
304
- eval_set.eval_cases.append(updated_eval_case)
305
-
306
- # Persit the eval set.
307
- eval_set_file_path = self._get_eval_set_file_path(app_name, eval_set_id)
308
- self._write_eval_set(eval_set_file_path, eval_set)
309
- else:
310
- raise NotFoundError(
311
- f"Eval Set `{eval_set_id}` or Eval id `{eval_case_id}` not found.",
312
- )
271
+ eval_set = get_eval_set_from_app_and_id(self, app_name, eval_set_id)
272
+ updated_eval_set = update_eval_case_in_eval_set(eval_set, updated_eval_case)
273
+ self._save_eval_set(app_name, eval_set_id, updated_eval_set)
313
274
 
314
275
  @override
315
276
  def delete_eval_case(
@@ -320,25 +281,9 @@ class LocalEvalSetsManager(EvalSetsManager):
320
281
  Raises:
321
282
  NotFoundError: If the eval set or the eval case to delete is not found.
322
283
  """
323
- # Find the eval case that needs to be deleted.
324
- eval_case_to_remove = self.get_eval_case(
325
- app_name, eval_set_id, eval_case_id
326
- )
327
-
328
- if eval_case_to_remove:
329
- logger.info(
330
- "EvalCase`%s` was found in the eval set. It will be removed"
331
- " permanently.",
332
- eval_case_id,
333
- )
334
- eval_set = self.get_eval_set(app_name, eval_set_id)
335
- eval_set.eval_cases.remove(eval_case_to_remove)
336
- eval_set_file_path = self._get_eval_set_file_path(app_name, eval_set_id)
337
- self._write_eval_set(eval_set_file_path, eval_set)
338
- else:
339
- raise NotFoundError(
340
- f"Eval Set `{eval_set_id}` or Eval id `{eval_case_id}` not found.",
341
- )
284
+ eval_set = get_eval_set_from_app_and_id(self, app_name, eval_set_id)
285
+ updated_eval_set = delete_eval_case_from_eval_set(eval_set, eval_case_id)
286
+ self._save_eval_set(app_name, eval_set_id, updated_eval_set)
342
287
 
343
288
  def _get_eval_set_file_path(self, app_name: str, eval_set_id: str) -> str:
344
289
  return os.path.join(
@@ -354,6 +299,10 @@ class LocalEvalSetsManager(EvalSetsManager):
354
299
  f"Invalid {id_name}. {id_name} should have the `{pattern}` format",
355
300
  )
356
301
 
357
- def _write_eval_set(self, eval_set_path: str, eval_set: EvalSet):
302
+ def _write_eval_set_to_path(self, eval_set_path: str, eval_set: EvalSet):
358
303
  with open(eval_set_path, "w") as f:
359
304
  f.write(eval_set.model_dump_json(indent=2))
305
+
306
+ def _save_eval_set(self, app_name: str, eval_set_id: str, eval_set: EvalSet):
307
+ eval_set_file_path = self._get_eval_set_file_path(app_name, eval_set_id)
308
+ self._write_eval_set_to_path(eval_set_file_path, eval_set)
@@ -12,13 +12,15 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  from typing import Any
16
18
  from typing import Optional
17
19
 
18
- from deprecated import deprecated
19
20
  from google.genai import types as genai_types
20
21
  import pandas as pd
21
22
  from tabulate import tabulate
23
+ from typing_extensions import deprecated
22
24
  from typing_extensions import override
23
25
  from vertexai.preview.evaluation import EvalTask
24
26
  from vertexai.preview.evaluation import MetricPromptTemplateExamples
@@ -124,10 +126,8 @@ class ResponseEvaluator(Evaluator):
124
126
 
125
127
  @staticmethod
126
128
  @deprecated(
127
- reason=(
128
- "This method has been deprecated and will be removed soon. Please use"
129
- " evaluate_invocations instead."
130
- )
129
+ "This method has been deprecated and will be removed soon. Please use"
130
+ " evaluate_invocations instead."
131
131
  )
132
132
  def evaluate(
133
133
  raw_eval_dataset: list[list[dict[str, Any]]],
@@ -12,13 +12,15 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  from typing import Any
16
18
  from typing import cast
17
19
 
18
- from deprecated import deprecated
19
20
  from google.genai import types as genai_types
20
21
  import pandas as pd
21
22
  from tabulate import tabulate
23
+ from typing_extensions import deprecated
22
24
  from typing_extensions import override
23
25
 
24
26
  from .eval_case import Invocation
@@ -100,10 +102,8 @@ class TrajectoryEvaluator(Evaluator):
100
102
 
101
103
  @staticmethod
102
104
  @deprecated(
103
- reason=(
104
- "This method has been deprecated and will be removed soon. Please use"
105
- " evaluate_invocations instead."
106
- )
105
+ "This method has been deprecated and will be removed soon. Please use"
106
+ " evaluate_invocations instead."
107
107
  )
108
108
  def evaluate(
109
109
  eval_dataset: list[list[dict[str, Any]]],
@@ -218,7 +218,10 @@ class TrajectoryEvaluator(Evaluator):
218
218
  return new_row, failure
219
219
 
220
220
  @staticmethod
221
- @deprecated()
221
+ @deprecated(
222
+ "are_tools_equal is deprecated and will be removed soon. Please use"
223
+ " TrajectoryEvaluator._are_tool_calls_equal instead."
224
+ )
222
225
  def are_tools_equal(list_a_original, list_b_original):
223
226
  # Remove other entries that we don't want to evaluate
224
227
  list_a = [
@@ -65,6 +65,15 @@ class _BasicLlmRequestProcessor(BaseLlmRequestProcessor):
65
65
  llm_request.live_connect_config.input_audio_transcription = (
66
66
  invocation_context.run_config.input_audio_transcription
67
67
  )
68
+ llm_request.live_connect_config.realtime_input_config = (
69
+ invocation_context.run_config.realtime_input_config
70
+ )
71
+ llm_request.live_connect_config.enable_affective_dialog = (
72
+ invocation_context.run_config.enable_affective_dialog
73
+ )
74
+ llm_request.live_connect_config.proactivity = (
75
+ invocation_context.run_config.proactivity
76
+ )
68
77
 
69
78
  # TODO: handle tool append here, instead of in BaseTool.process_llm_request.
70
79
 
@@ -201,7 +201,7 @@ def function_declaration_to_tool_param(
201
201
 
202
202
 
203
203
  class Claude(BaseLlm):
204
- """ "Integration with Claude models served from Vertex AI.
204
+ """Integration with Claude models served from Vertex AI.
205
205
 
206
206
  Attributes:
207
207
  model: The name of the Claude model.
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  import logging
16
18
  from typing import AsyncGenerator
17
19
 
@@ -23,6 +23,7 @@ import sys
23
23
  from typing import AsyncGenerator
24
24
  from typing import cast
25
25
  from typing import TYPE_CHECKING
26
+ from typing import Union
26
27
 
27
28
  from google.genai import Client
28
29
  from google.genai import types
@@ -94,6 +95,13 @@ class Gemini(BaseLlm):
94
95
  )
95
96
  logger.info(_build_request_log(llm_request))
96
97
 
98
+ # add tracking headers to custom headers given it will override the headers
99
+ # set in the api client constructor
100
+ if llm_request.config and llm_request.config.http_options:
101
+ if not llm_request.config.http_options.headers:
102
+ llm_request.config.http_options.headers = {}
103
+ llm_request.config.http_options.headers.update(self._tracking_headers)
104
+
97
105
  if stream:
98
106
  responses = await self.api_client.aio.models.generate_content_stream(
99
107
  model=llm_request.model,
@@ -200,24 +208,21 @@ class Gemini(BaseLlm):
200
208
  return tracking_headers
201
209
 
202
210
  @cached_property
203
- def _live_api_client(self) -> Client:
211
+ def _live_api_version(self) -> str:
204
212
  if self._api_backend == GoogleLLMVariant.VERTEX_AI:
205
213
  # use beta version for vertex api
206
- api_version = 'v1beta1'
207
- # use default api version for vertex
208
- return Client(
209
- http_options=types.HttpOptions(
210
- headers=self._tracking_headers, api_version=api_version
211
- )
212
- )
214
+ return 'v1beta1'
213
215
  else:
214
216
  # use v1alpha for using API KEY from Google AI Studio
215
- api_version = 'v1alpha'
216
- return Client(
217
- http_options=types.HttpOptions(
218
- headers=self._tracking_headers, api_version=api_version
219
- )
220
- )
217
+ return 'v1alpha'
218
+
219
+ @cached_property
220
+ def _live_api_client(self) -> Client:
221
+ return Client(
222
+ http_options=types.HttpOptions(
223
+ headers=self._tracking_headers, api_version=self._live_api_version
224
+ )
225
+ )
221
226
 
222
227
  @contextlib.asynccontextmanager
223
228
  async def connect(self, llm_request: LlmRequest) -> BaseLlmConnection:
@@ -229,6 +234,21 @@ class Gemini(BaseLlm):
229
234
  Yields:
230
235
  BaseLlmConnection, the connection to the Gemini model.
231
236
  """
237
+ # add tracking headers to custom headers and set api_version given
238
+ # the customized http options will override the one set in the api client
239
+ # constructor
240
+ if (
241
+ llm_request.live_connect_config
242
+ and llm_request.live_connect_config.http_options
243
+ ):
244
+ if not llm_request.live_connect_config.http_options.headers:
245
+ llm_request.live_connect_config.http_options.headers = {}
246
+ llm_request.live_connect_config.http_options.headers.update(
247
+ self._tracking_headers
248
+ )
249
+ llm_request.live_connect_config.http_options.api_version = (
250
+ self._live_api_version
251
+ )
232
252
 
233
253
  llm_request.live_connect_config.system_instruction = types.Content(
234
254
  role='system',
@@ -244,9 +264,18 @@ class Gemini(BaseLlm):
244
264
 
245
265
  def _preprocess_request(self, llm_request: LlmRequest) -> None:
246
266
 
247
- if llm_request.config and self._api_backend == GoogleLLMVariant.GEMINI_API:
267
+ if self._api_backend == GoogleLLMVariant.GEMINI_API:
248
268
  # Using API key from Google AI Studio to call model doesn't support labels.
249
- llm_request.config.labels = None
269
+ if llm_request.config:
270
+ llm_request.config.labels = None
271
+
272
+ if llm_request.contents:
273
+ for content in llm_request.contents:
274
+ if not content.parts:
275
+ continue
276
+ for part in content.parts:
277
+ _remove_display_name_if_present(part.inline_data)
278
+ _remove_display_name_if_present(part.file_data)
250
279
 
251
280
 
252
281
  def _build_function_declaration_log(
@@ -324,3 +353,15 @@ Raw response:
324
353
  {resp.model_dump_json(exclude_none=True)}
325
354
  -----------------------------------------------------------
326
355
  """
356
+
357
+
358
+ def _remove_display_name_if_present(
359
+ data_obj: Union[types.Blob, types.FileData, None],
360
+ ):
361
+ """Sets display_name to None for the Gemini API (non-Vertex) backend.
362
+
363
+ This backend does not support the display_name parameter for file uploads,
364
+ so it must be removed to prevent request failures.
365
+ """
366
+ if data_obj and data_obj.display_name:
367
+ data_obj.display_name = None
@@ -739,11 +739,12 @@ class LiteLlm(BaseLlm):
739
739
  _message_to_generate_content_response(
740
740
  ChatCompletionAssistantMessage(
741
741
  role="assistant",
742
- content="",
742
+ content=text,
743
743
  tool_calls=tool_calls,
744
744
  )
745
745
  )
746
746
  )
747
+ text = ""
747
748
  function_calls.clear()
748
749
  elif finish_reason == "stop" and text:
749
750
  aggregated_llm_response = _message_to_generate_content_response(
@@ -0,0 +1,13 @@
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.
@@ -0,0 +1,15 @@
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
+ # Code in this directory is internal to Google.
@@ -0,0 +1,30 @@
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
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ from typing import Callable
19
+
20
+ from google3.learning.deepmind.python.threading import g3_executor
21
+ from google3.learning.deepmind.python.threading import g3_thread
22
+
23
+ # TODO(b/423882251): Switch to copybara replacements.
24
+ # Right now this doesn't work because the ADK runs unit and integration tests,
25
+ # outside google3, before copybara replacements.
26
+
27
+
28
+ def create_thread(target: Callable[..., None], *args, **kwargs):
29
+ """Creates a thread."""
30
+ return g3_thread.G3Thread(target=target, args=args, kwargs=kwargs)
@@ -0,0 +1,31 @@
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
+ from __future__ import annotations
16
+
17
+ import threading
18
+ from typing import Callable
19
+
20
+ internal_thread = None
21
+ try:
22
+ from .internal import thread as internal_thread
23
+ except ImportError:
24
+ internal_thread = None
25
+
26
+
27
+ def create_thread(target: Callable[..., None], *args, **kwargs):
28
+ """Creates a thread."""
29
+ if internal_thread:
30
+ return internal_thread.create_thread(target, *args, **kwargs)
31
+ return threading.Thread(target=target, args=args, kwargs=kwargs)
google/adk/runners.py CHANGED
@@ -17,7 +17,6 @@ from __future__ import annotations
17
17
  import asyncio
18
18
  import logging
19
19
  import queue
20
- import threading
21
20
  from typing import AsyncGenerator
22
21
  from typing import Generator
23
22
  from typing import Optional
@@ -34,10 +33,12 @@ from .agents.llm_agent import LlmAgent
34
33
  from .agents.run_config import RunConfig
35
34
  from .artifacts.base_artifact_service import BaseArtifactService
36
35
  from .artifacts.in_memory_artifact_service import InMemoryArtifactService
36
+ from .auth.credential_service.base_credential_service import BaseCredentialService
37
37
  from .code_executors.built_in_code_executor import BuiltInCodeExecutor
38
38
  from .events.event import Event
39
39
  from .memory.base_memory_service import BaseMemoryService
40
40
  from .memory.in_memory_memory_service import InMemoryMemoryService
41
+ from .platform.thread import create_thread
41
42
  from .sessions.base_session_service import BaseSessionService
42
43
  from .sessions.in_memory_session_service import InMemorySessionService
43
44
  from .sessions.session import Session
@@ -72,6 +73,8 @@ class Runner:
72
73
  """The session service for the runner."""
73
74
  memory_service: Optional[BaseMemoryService] = None
74
75
  """The memory service for the runner."""
76
+ credential_service: Optional[BaseCredentialService] = None
77
+ """The credential service for the runner."""
75
78
 
76
79
  def __init__(
77
80
  self,
@@ -81,6 +84,7 @@ class Runner:
81
84
  artifact_service: Optional[BaseArtifactService] = None,
82
85
  session_service: BaseSessionService,
83
86
  memory_service: Optional[BaseMemoryService] = None,
87
+ credential_service: Optional[BaseCredentialService] = None,
84
88
  ):
85
89
  """Initializes the Runner.
86
90
 
@@ -96,6 +100,7 @@ class Runner:
96
100
  self.artifact_service = artifact_service
97
101
  self.session_service = session_service
98
102
  self.memory_service = memory_service
103
+ self.credential_service = credential_service
99
104
 
100
105
  def run(
101
106
  self,
@@ -139,7 +144,7 @@ class Runner:
139
144
  finally:
140
145
  event_queue.put(None)
141
146
 
142
- thread = threading.Thread(target=_asyncio_thread_main)
147
+ thread = create_thread(target=_asyncio_thread_main)
143
148
  thread.start()
144
149
 
145
150
  # consumes and re-yield the events from background thread.
@@ -417,6 +422,7 @@ class Runner:
417
422
  artifact_service=self.artifact_service,
418
423
  session_service=self.session_service,
419
424
  memory_service=self.memory_service,
425
+ credential_service=self.credential_service,
420
426
  invocation_id=invocation_id,
421
427
  agent=self.agent,
422
428
  session=session,
@@ -11,6 +11,7 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ from __future__ import annotations
14
15
 
15
16
  import copy
16
17
  import logging
@@ -223,6 +224,7 @@ class InMemorySessionService(BaseSessionService):
223
224
  sessions_without_events.append(copied_session)
224
225
  return ListSessionsResponse(sessions=sessions_without_events)
225
226
 
227
+ @override
226
228
  async def delete_session(
227
229
  self, *, app_name: str, user_id: str, session_id: str
228
230
  ) -> None:
@@ -247,7 +249,7 @@ class InMemorySessionService(BaseSessionService):
247
249
  )
248
250
  is None
249
251
  ):
250
- return None
252
+ return
251
253
 
252
254
  self.sessions[app_name][user_id].pop(session_id)
253
255
 
@@ -261,11 +263,20 @@ class InMemorySessionService(BaseSessionService):
261
263
  app_name = session.app_name
262
264
  user_id = session.user_id
263
265
  session_id = session.id
266
+
267
+ def _warning(message: str) -> None:
268
+ logger.warning(
269
+ f'Failed to append event to session {session_id}: {message}'
270
+ )
271
+
264
272
  if app_name not in self.sessions:
273
+ _warning(f'app_name {app_name} not in sessions')
265
274
  return event
266
275
  if user_id not in self.sessions[app_name]:
276
+ _warning(f'user_id {user_id} not in sessions[app_name]')
267
277
  return event
268
278
  if session_id not in self.sessions[app_name][user_id]:
279
+ _warning(f'session_id {session_id} not in sessions[app_name][user_id]')
269
280
  return event
270
281
 
271
282
  if event.actions and event.actions.state_delta: