google-adk 1.1.0__py3-none-any.whl → 1.2.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 (72) hide show
  1. google/adk/agents/base_agent.py +0 -2
  2. google/adk/agents/invocation_context.py +3 -3
  3. google/adk/agents/parallel_agent.py +17 -7
  4. google/adk/agents/sequential_agent.py +8 -8
  5. google/adk/auth/auth_preprocessor.py +18 -17
  6. google/adk/cli/agent_graph.py +165 -23
  7. google/adk/cli/browser/assets/ADK-512-color.svg +9 -0
  8. google/adk/cli/browser/index.html +2 -2
  9. google/adk/cli/browser/{main-PKDNKWJE.js → main-CS5OLUMF.js} +59 -59
  10. google/adk/cli/browser/polyfills-FFHMD2TL.js +17 -0
  11. google/adk/cli/cli.py +9 -9
  12. google/adk/cli/cli_deploy.py +157 -0
  13. google/adk/cli/cli_tools_click.py +228 -99
  14. google/adk/cli/fast_api.py +119 -34
  15. google/adk/cli/utils/agent_loader.py +60 -44
  16. google/adk/cli/utils/envs.py +1 -1
  17. google/adk/code_executors/unsafe_local_code_executor.py +11 -0
  18. google/adk/errors/__init__.py +13 -0
  19. google/adk/errors/not_found_error.py +28 -0
  20. google/adk/evaluation/agent_evaluator.py +1 -1
  21. google/adk/evaluation/eval_sets_manager.py +36 -6
  22. google/adk/evaluation/evaluation_generator.py +5 -4
  23. google/adk/evaluation/local_eval_sets_manager.py +101 -6
  24. google/adk/flows/llm_flows/agent_transfer.py +2 -2
  25. google/adk/flows/llm_flows/base_llm_flow.py +19 -0
  26. google/adk/flows/llm_flows/contents.py +4 -4
  27. google/adk/flows/llm_flows/functions.py +140 -127
  28. google/adk/memory/vertex_ai_rag_memory_service.py +2 -2
  29. google/adk/models/anthropic_llm.py +7 -10
  30. google/adk/models/google_llm.py +46 -18
  31. google/adk/models/lite_llm.py +63 -26
  32. google/adk/py.typed +0 -0
  33. google/adk/sessions/_session_util.py +10 -16
  34. google/adk/sessions/database_session_service.py +81 -66
  35. google/adk/sessions/vertex_ai_session_service.py +32 -6
  36. google/adk/telemetry.py +91 -24
  37. google/adk/tools/_automatic_function_calling_util.py +31 -25
  38. google/adk/tools/{function_parameter_parse_util.py → _function_parameter_parse_util.py} +9 -3
  39. google/adk/tools/_gemini_schema_util.py +158 -0
  40. google/adk/tools/apihub_tool/apihub_toolset.py +3 -2
  41. google/adk/tools/application_integration_tool/clients/connections_client.py +7 -0
  42. google/adk/tools/application_integration_tool/integration_connector_tool.py +5 -7
  43. google/adk/tools/base_tool.py +4 -8
  44. google/adk/tools/bigquery/__init__.py +11 -1
  45. google/adk/tools/bigquery/bigquery_credentials.py +9 -4
  46. google/adk/tools/bigquery/bigquery_toolset.py +86 -0
  47. google/adk/tools/bigquery/client.py +33 -0
  48. google/adk/tools/bigquery/metadata_tool.py +249 -0
  49. google/adk/tools/bigquery/query_tool.py +76 -0
  50. google/adk/tools/function_tool.py +4 -4
  51. google/adk/tools/langchain_tool.py +20 -13
  52. google/adk/tools/load_memory_tool.py +1 -0
  53. google/adk/tools/mcp_tool/conversion_utils.py +4 -2
  54. google/adk/tools/mcp_tool/mcp_session_manager.py +63 -5
  55. google/adk/tools/mcp_tool/mcp_tool.py +3 -2
  56. google/adk/tools/mcp_tool/mcp_toolset.py +15 -8
  57. google/adk/tools/openapi_tool/common/common.py +4 -43
  58. google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +0 -2
  59. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.py +4 -2
  60. google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +4 -2
  61. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +7 -127
  62. google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +2 -7
  63. google/adk/tools/transfer_to_agent_tool.py +8 -1
  64. google/adk/tools/vertex_ai_search_tool.py +8 -1
  65. google/adk/utils/variant_utils.py +51 -0
  66. google/adk/version.py +1 -1
  67. {google_adk-1.1.0.dist-info → google_adk-1.2.0.dist-info}/METADATA +7 -7
  68. {google_adk-1.1.0.dist-info → google_adk-1.2.0.dist-info}/RECORD +71 -61
  69. google/adk/cli/browser/polyfills-B6TNHZQ6.js +0 -17
  70. {google_adk-1.1.0.dist-info → google_adk-1.2.0.dist-info}/WHEEL +0 -0
  71. {google_adk-1.1.0.dist-info → google_adk-1.2.0.dist-info}/entry_points.txt +0 -0
  72. {google_adk-1.1.0.dist-info → google_adk-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  import importlib
18
18
  import logging
19
19
  import sys
20
+ from typing import Optional
20
21
 
21
22
  from . import envs
22
23
  from ...agents.base_agent import BaseAgent
@@ -27,9 +28,13 @@ logger = logging.getLogger("google_adk." + __name__)
27
28
  class AgentLoader:
28
29
  """Centralized agent loading with proper isolation, caching, and .env loading.
29
30
  Support loading agents from below folder/file structures:
30
- a) agents_dir/agent_name.py (with root_agent or agent.root_agent in it)
31
- b) agents_dir/agent_name_folder/__init__.py (with root_agent or agent.root_agent in the package)
32
- c) agents_dir/agent_name_folder/agent.py (where agent.py has root_agent)
31
+ a) {agent_name}.agent as a module name:
32
+ agents_dir/{agent_name}/agent.py (with root_agent defined in the module)
33
+ b) {agent_name} as a module name
34
+ agents_dir/{agent_name}.py (with root_agent defined in the module)
35
+ c) {agent_name} as a package name
36
+ agents_dir/{agent_name}/__init__.py (with root_agent in the package)
37
+
33
38
  """
34
39
 
35
40
  def __init__(self, agents_dir: str):
@@ -37,48 +42,52 @@ class AgentLoader:
37
42
  self._original_sys_path = None
38
43
  self._agent_cache: dict[str, BaseAgent] = {}
39
44
 
40
- def _load_from_module_or_package(self, agent_name: str) -> BaseAgent:
41
- # Load for case: Import "<agent_name>" (as a package or module)
45
+ def _load_from_module_or_package(
46
+ self, agent_name: str
47
+ ) -> Optional[BaseAgent]:
48
+ # Load for case: Import "{agent_name}" (as a package or module)
42
49
  # Covers structures:
43
- # a) agents_dir/agent_name.py (with root_agent or agent.root_agent in it)
44
- # b) agents_dir/agent_name_folder/__init__.py (with root_agent or agent.root_agent in the package)
50
+ # a) agents_dir/{agent_name}.py (with root_agent in the module)
51
+ # b) agents_dir/{agent_name}/__init__.py (with root_agent in the package)
45
52
  try:
46
53
  module_candidate = importlib.import_module(agent_name)
47
- # Check for "root_agent" directly in "<agent_name>" module/package
54
+ # Check for "root_agent" directly in "{agent_name}" module/package
48
55
  if hasattr(module_candidate, "root_agent"):
49
56
  logger.debug("Found root_agent directly in %s", agent_name)
50
- return module_candidate.root_agent
51
- # Check for "<agent_name>.agent.root_agent" structure (e.g. agent_name is a package,
52
- # and it has an 'agent' submodule/attribute which in turn has 'root_agent')
53
- if hasattr(module_candidate, "agent") and hasattr(
54
- module_candidate.agent, "root_agent"
55
- ):
56
- logger.debug("Found root_agent in %s.agent attribute", agent_name)
57
- if isinstance(module_candidate.agent, BaseAgent):
58
- return module_candidate.agent.root_agent
57
+ if isinstance(module_candidate.root_agent, BaseAgent):
58
+ return module_candidate.root_agent
59
59
  else:
60
60
  logger.warning(
61
61
  "Root agent found is not an instance of BaseAgent. But a type %s",
62
- type(module_candidate.agent),
62
+ type(module_candidate.root_agent),
63
63
  )
64
- except ModuleNotFoundError:
65
- logger.debug("Module %s itself not found.", agent_name)
66
- # Re-raise as ValueError to be caught by the final error message construction
67
- raise ValueError(
68
- f"Module {agent_name} not found during import attempts."
69
- ) from None
70
- except ImportError as e:
71
- logger.warning("Error importing %s: %s", agent_name, e)
64
+ else:
65
+ logger.debug(
66
+ "Module %s has no root_agent. Trying next pattern.",
67
+ agent_name,
68
+ )
69
+
70
+ except ModuleNotFoundError as e:
71
+ if e.name == agent_name:
72
+ logger.debug("Module %s itself not found.", agent_name)
73
+ else:
74
+ # it's the case the module imported by {agent_name}.agent module is not
75
+ # found
76
+ e.msg = f"Fail to load '{agent_name}' module. " + e.msg
77
+ raise e
78
+ except Exception as e:
79
+ e.msg = f"Fail to load '{agent_name}' module. " + e.msg
80
+ raise e
72
81
 
73
82
  return None
74
83
 
75
- def _load_from_submodule(self, agent_name: str) -> BaseAgent:
76
- # Load for case: Import "<agent_name>.agent" and look for "root_agent"
77
- # Covers structure: agents_dir/agent_name_folder/agent.py (where agent.py has root_agent)
84
+ def _load_from_submodule(self, agent_name: str) -> Optional[BaseAgent]:
85
+ # Load for case: Import "{agent_name}.agent" and look for "root_agent"
86
+ # Covers structure: agents_dir/{agent_name}/agent.py (with root_agent defined in the module)
78
87
  try:
79
88
  module_candidate = importlib.import_module(f"{agent_name}.agent")
80
89
  if hasattr(module_candidate, "root_agent"):
81
- logger.debug("Found root_agent in %s.agent", agent_name)
90
+ logger.info("Found root_agent in %s.agent", agent_name)
82
91
  if isinstance(module_candidate.root_agent, BaseAgent):
83
92
  return module_candidate.root_agent
84
93
  else:
@@ -86,12 +95,23 @@ class AgentLoader:
86
95
  "Root agent found is not an instance of BaseAgent. But a type %s",
87
96
  type(module_candidate.root_agent),
88
97
  )
89
- except ModuleNotFoundError:
90
- logger.debug(
91
- "Module %s.agent not found, trying next pattern.", agent_name
92
- )
93
- except ImportError as e:
94
- logger.warning("Error importing %s.agent: %s", agent_name, e)
98
+ else:
99
+ logger.debug(
100
+ "Module %s.agent has no root_agent.",
101
+ agent_name,
102
+ )
103
+ except ModuleNotFoundError as e:
104
+ # if it's agent module not found, it's fine, search for next pattern
105
+ if e.name == f"{agent_name}.agent" or e.name == agent_name:
106
+ logger.debug("Module %s.agent not found.", agent_name)
107
+ else:
108
+ # it's the case the module imported by {agent_name}.agent module is not
109
+ # found
110
+ e.msg = f"Fail to load '{agent_name}.agent' module. " + e.msg
111
+ raise e
112
+ except Exception as e:
113
+ e.msg = f"Fail to load '{agent_name}.agent' module. " + e.msg
114
+ raise e
95
115
 
96
116
  return None
97
117
 
@@ -106,32 +126,28 @@ class AgentLoader:
106
126
  )
107
127
  envs.load_dotenv_for_agent(agent_name, str(self.agents_dir))
108
128
 
109
- root_agent = self._load_from_module_or_package(agent_name)
110
- if root_agent:
129
+ if root_agent := self._load_from_module_or_package(agent_name):
111
130
  return root_agent
112
131
 
113
- root_agent = self._load_from_submodule(agent_name)
114
- if root_agent:
132
+ if root_agent := self._load_from_submodule(agent_name):
115
133
  return root_agent
116
134
 
117
135
  # If no root_agent was found by any pattern
118
136
  raise ValueError(
119
137
  f"No root_agent found for '{agent_name}'. Searched in"
120
- f" '{agent_name}.agent.root_agent', '{agent_name}.root_agent', and"
121
- f" via an 'agent' attribute within the '{agent_name}' module/package."
138
+ f" '{agent_name}.agent.root_agent', '{agent_name}.root_agent'."
122
139
  f" Ensure '{self.agents_dir}/{agent_name}' is structured correctly,"
123
140
  " an .env file can be loaded if present, and a root_agent is"
124
141
  " exposed."
125
142
  )
126
143
 
127
144
  def load_agent(self, agent_name: str) -> BaseAgent:
128
- """Load an agent module (with caching & .env) and return its root_agent (asynchronously)."""
145
+ """Load an agent module (with caching & .env) and return its root_agent."""
129
146
  if agent_name in self._agent_cache:
130
147
  logger.debug("Returning cached agent for %s (async)", agent_name)
131
148
  return self._agent_cache[agent_name]
132
149
 
133
150
  logger.debug("Loading agent %s - not in cache.", agent_name)
134
- # Assumes this method is called when the context manager (`with self:`) is active
135
151
  agent = self._perform_load(agent_name)
136
152
  self._agent_cache[agent_name] = agent
137
153
  return agent
@@ -35,7 +35,7 @@ def _walk_to_root_until_found(folder, filename) -> str:
35
35
  def load_dotenv_for_agent(
36
36
  agent_name: str, agent_parent_folder: str, filename: str = '.env'
37
37
  ):
38
- """Lods the .env file for the agent module."""
38
+ """Loads the .env file for the agent module."""
39
39
 
40
40
  # Gets the folder of agent_module as starting_folder
41
41
  starting_folder = os.path.abspath(
@@ -12,8 +12,12 @@
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 contextlib import redirect_stdout
16
18
  import io
19
+ import re
20
+ from typing import Any
17
21
 
18
22
  from pydantic import Field
19
23
  from typing_extensions import override
@@ -24,6 +28,12 @@ from .code_execution_utils import CodeExecutionInput
24
28
  from .code_execution_utils import CodeExecutionResult
25
29
 
26
30
 
31
+ def _prepare_globals(code: str, globals_: dict[str, Any]) -> None:
32
+ """Prepare globals for code execution, injecting __name__ if needed."""
33
+ if re.search(r"if\s+__name__\s*==\s*['\"]__main__['\"]", code):
34
+ globals_['__name__'] = '__main__'
35
+
36
+
27
37
  class UnsafeLocalCodeExecutor(BaseCodeExecutor):
28
38
  """A code executor that unsafely execute code in the current local context."""
29
39
 
@@ -55,6 +65,7 @@ class UnsafeLocalCodeExecutor(BaseCodeExecutor):
55
65
  error = ''
56
66
  try:
57
67
  globals_ = {}
68
+ _prepare_globals(code_execution_input.code, globals_)
58
69
  locals_ = {}
59
70
  stdout = io.StringIO()
60
71
  with redirect_stdout(stdout):
@@ -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,28 @@
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
+
18
+ class NotFoundError(Exception):
19
+ """Represents an error that occurs when an entity is not found."""
20
+
21
+ def __init__(self, message="The requested item was not found."):
22
+ """Initializes the NotFoundError exception.
23
+
24
+ Args:
25
+ message (str): An optional custom message to describe the error.
26
+ """
27
+ self.message = message
28
+ super().__init__(self.message)
@@ -158,7 +158,7 @@ class AgentEvaluator:
158
158
  agent_module: The path to python module that contains the definition of
159
159
  the agent. There is convention in place here, where the code is going to
160
160
  look for 'root_agent' in the loaded module.
161
- eval_dataset: The eval data set. This can be either a string representing
161
+ eval_dataset_file_path_or_dir: The eval data set. This can be either a string representing
162
162
  full path to the file containing eval dataset, or a directory that is
163
163
  recursively explored for all files that have a `.test.json` suffix.
164
164
  num_runs: Number of times all entries in the eval dataset should be
@@ -12,9 +12,13 @@
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 abc import ABC
16
18
  from abc import abstractmethod
19
+ from typing import Optional
17
20
 
21
+ from ..errors.not_found_error import NotFoundError
18
22
  from .eval_case import EvalCase
19
23
  from .eval_set import EvalSet
20
24
 
@@ -23,21 +27,47 @@ class EvalSetsManager(ABC):
23
27
  """An interface to manage an Eval Sets."""
24
28
 
25
29
  @abstractmethod
26
- def get_eval_set(self, app_name: str, eval_set_id: str) -> EvalSet:
30
+ def get_eval_set(self, app_name: str, eval_set_id: str) -> Optional[EvalSet]:
27
31
  """Returns an EvalSet identified by an app_name and eval_set_id."""
28
- raise NotImplementedError()
29
32
 
30
33
  @abstractmethod
31
34
  def create_eval_set(self, app_name: str, eval_set_id: str):
32
35
  """Creates an empty EvalSet given the app_name and eval_set_id."""
33
- raise NotImplementedError()
34
36
 
35
37
  @abstractmethod
36
38
  def list_eval_sets(self, app_name: str) -> list[str]:
37
39
  """Returns a list of EvalSets that belong to the given app_name."""
38
- raise NotImplementedError()
40
+
41
+ @abstractmethod
42
+ def get_eval_case(
43
+ self, app_name: str, eval_set_id: str, eval_case_id: str
44
+ ) -> Optional[EvalCase]:
45
+ """Returns an EvalCase if found, otherwise None."""
39
46
 
40
47
  @abstractmethod
41
48
  def add_eval_case(self, app_name: str, eval_set_id: str, eval_case: EvalCase):
42
- """Adds the given EvalCase to an existing EvalSet identified by app_name and eval_set_id."""
43
- raise NotImplementedError()
49
+ """Adds the given EvalCase to an existing EvalSet identified by app_name and eval_set_id.
50
+
51
+ Raises:
52
+ NotFoundError: If the eval set is not found.
53
+ """
54
+
55
+ @abstractmethod
56
+ def update_eval_case(
57
+ self, app_name: str, eval_set_id: str, updated_eval_case: EvalCase
58
+ ):
59
+ """Updates an existing EvalCase give the app_name and eval_set_id.
60
+
61
+ Raises:
62
+ NotFoundError: If the eval set or the eval case is not found.
63
+ """
64
+
65
+ @abstractmethod
66
+ def delete_eval_case(
67
+ self, app_name: str, eval_set_id: str, eval_case_id: str
68
+ ):
69
+ """Deletes the given EvalCase identified by app_name, eval_set_id and eval_case_id.
70
+
71
+ Raises:
72
+ NotFoundError: If the eval set or the eval case to delete is not found.
73
+ """
@@ -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 importlib
16
18
  from typing import Any
17
19
  from typing import Optional
@@ -56,7 +58,7 @@ class EvaluationGenerator:
56
58
  """Returns evaluation responses for the given dataset and agent.
57
59
 
58
60
  Args:
59
- eval_dataset: The dataset that needs to be scraped for responses.
61
+ eval_set: The eval set that needs to be scraped for responses.
60
62
  agent_module_path: Path to the module that contains the root agent.
61
63
  repeat_num: Number of time the eval dataset should be repeated. This is
62
64
  usually done to remove uncertainty that a single run may bring.
@@ -209,7 +211,8 @@ class EvaluationGenerator:
209
211
  """Process the queries using the existing session data without invoking the runner."""
210
212
  responses = data.copy()
211
213
 
212
- # Iterate through the provided queries and align them with the session events
214
+ # Iterate through the provided queries and align them with the session
215
+ # events
213
216
  for index, eval_entry in enumerate(responses):
214
217
  query = eval_entry["query"]
215
218
  actual_tool_uses = []
@@ -241,5 +244,3 @@ class EvaluationGenerator:
241
244
  responses[index]["actual_tool_use"] = actual_tool_uses
242
245
  responses[index]["response"] = response
243
246
  return responses
244
- return responses
245
- return responses
@@ -12,18 +12,22 @@
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 json
16
18
  import logging
17
19
  import os
18
20
  import re
19
21
  import time
20
22
  from typing import Any
23
+ from typing import Optional
21
24
  import uuid
22
25
 
23
26
  from google.genai import types as genai_types
24
27
  from pydantic import ValidationError
25
28
  from typing_extensions import override
26
29
 
30
+ from ..errors.not_found_error import NotFoundError
27
31
  from .eval_case import EvalCase
28
32
  from .eval_case import IntermediateData
29
33
  from .eval_case import Invocation
@@ -39,9 +43,9 @@ _EVAL_SET_FILE_EXTENSION = ".evalset.json"
39
43
  def _convert_invocation_to_pydantic_schema(
40
44
  invocation_in_json_format: dict[str, Any],
41
45
  ) -> Invocation:
42
- """Converts an invocation from old json format to new Pydantic Schema"""
46
+ """Converts an invocation from old json format to new Pydantic Schema."""
43
47
  query = invocation_in_json_format["query"]
44
- reference = invocation_in_json_format["reference"]
48
+ reference = invocation_in_json_format.get("reference", "")
45
49
  expected_tool_use = []
46
50
  expected_intermediate_agent_responses = []
47
51
 
@@ -186,11 +190,14 @@ class LocalEvalSetsManager(EvalSetsManager):
186
190
  self._agents_dir = agents_dir
187
191
 
188
192
  @override
189
- def get_eval_set(self, app_name: str, eval_set_id: str) -> EvalSet:
193
+ def get_eval_set(self, app_name: str, eval_set_id: str) -> Optional[EvalSet]:
190
194
  """Returns an EvalSet identified by an app_name and eval_set_id."""
191
195
  # Load the eval set file data
192
- eval_set_file_path = self._get_eval_set_file_path(app_name, eval_set_id)
193
- return load_eval_set_from_file(eval_set_file_path, eval_set_id)
196
+ try:
197
+ eval_set_file_path = self._get_eval_set_file_path(app_name, eval_set_id)
198
+ return load_eval_set_from_file(eval_set_file_path, eval_set_id)
199
+ except FileNotFoundError:
200
+ return None
194
201
 
195
202
  @override
196
203
  def create_eval_set(self, app_name: str, eval_set_id: str):
@@ -228,12 +235,19 @@ class LocalEvalSetsManager(EvalSetsManager):
228
235
 
229
236
  @override
230
237
  def add_eval_case(self, app_name: str, eval_set_id: str, eval_case: EvalCase):
231
- """Adds the given EvalCase to an existing EvalSet identified by app_name and eval_set_id."""
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
+ """
232
243
  eval_case_id = eval_case.eval_id
233
244
  self._validate_id(id_name="Eval Case Id", id_value=eval_case_id)
234
245
 
235
246
  eval_set = self.get_eval_set(app_name, eval_set_id)
236
247
 
248
+ if not eval_set:
249
+ raise NotFoundError(f"Eval set `{eval_set_id}` not found.")
250
+
237
251
  if [x for x in eval_set.eval_cases if x.eval_id == eval_case_id]:
238
252
  raise ValueError(
239
253
  f"Eval id `{eval_case_id}` already exists in `{eval_set_id}`"
@@ -245,6 +259,87 @@ class LocalEvalSetsManager(EvalSetsManager):
245
259
  eval_set_file_path = self._get_eval_set_file_path(app_name, eval_set_id)
246
260
  self._write_eval_set(eval_set_file_path, eval_set)
247
261
 
262
+ @override
263
+ def get_eval_case(
264
+ self, app_name: str, eval_set_id: str, eval_case_id: str
265
+ ) -> Optional[EvalCase]:
266
+ """Returns an EvalCase if found, otherwise None."""
267
+ eval_set = self.get_eval_set(app_name, eval_set_id)
268
+
269
+ if not eval_set:
270
+ return None
271
+
272
+ eval_case_to_find = None
273
+
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
279
+
280
+ return eval_case_to_find
281
+
282
+ @override
283
+ def update_eval_case(
284
+ self, app_name: str, eval_set_id: str, updated_eval_case: EvalCase
285
+ ):
286
+ """Updates an existing EvalCase give the app_name and eval_set_id.
287
+
288
+ Raises:
289
+ NotFoundError: If the eval set or the eval case is not found.
290
+ """
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
+ )
313
+
314
+ @override
315
+ def delete_eval_case(
316
+ self, app_name: str, eval_set_id: str, eval_case_id: str
317
+ ):
318
+ """Deletes the given EvalCase identified by app_name, eval_set_id and eval_case_id.
319
+
320
+ Raises:
321
+ NotFoundError: If the eval set or the eval case to delete is not found.
322
+ """
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
+ )
342
+
248
343
  def _get_eval_set_file_path(self, app_name: str, eval_set_id: str) -> str:
249
344
  return os.path.join(
250
345
  self._agents_dir,
@@ -98,11 +98,11 @@ question to that agent. When transferring, do not generate any text other than
98
98
  the function call.
99
99
  """
100
100
 
101
- if agent.parent_agent:
101
+ if agent.parent_agent and not agent.disallow_transfer_to_parent:
102
102
  si += f"""
103
103
  Your parent agent is {agent.parent_agent.name}. If neither the other agents nor
104
104
  you are best for answering the question according to the descriptions, transfer
105
- to your parent agent. If you don't have parent agent, try answer by yourself.
105
+ to your parent agent.
106
106
  """
107
107
  return si
108
108
 
@@ -23,6 +23,7 @@ from typing import cast
23
23
  from typing import Optional
24
24
  from typing import TYPE_CHECKING
25
25
 
26
+ from google.genai import types
26
27
  from websockets.exceptions import ConnectionClosedOK
27
28
 
28
29
  from . import functions
@@ -50,6 +51,8 @@ if TYPE_CHECKING:
50
51
 
51
52
  logger = logging.getLogger('google_adk.' + __name__)
52
53
 
54
+ _ADK_AGENT_NAME_LABEL_KEY = 'adk_agent_name'
55
+
53
56
 
54
57
  class BaseLlmFlow(ABC):
55
58
  """A basic flow that calls the LLM in a loop until a final response is generated.
@@ -281,6 +284,12 @@ class BaseLlmFlow(ABC):
281
284
  yield event
282
285
  if not last_event or last_event.is_final_response():
283
286
  break
287
+ if last_event.partial:
288
+ # TODO: handle this in BaseLlm level.
289
+ raise ValueError(
290
+ f"Last event shouldn't be partial. LLM max output limit may be"
291
+ f' reached.'
292
+ )
284
293
 
285
294
  async def _run_one_step_async(
286
295
  self,
@@ -493,6 +502,16 @@ class BaseLlmFlow(ABC):
493
502
  yield response
494
503
  return
495
504
 
505
+ llm_request.config = llm_request.config or types.GenerateContentConfig()
506
+ llm_request.config.labels = llm_request.config.labels or {}
507
+
508
+ # Add agent name as a label to the llm_request. This will help with slicing
509
+ # the billing reports on a per-agent basis.
510
+ if _ADK_AGENT_NAME_LABEL_KEY not in llm_request.config.labels:
511
+ llm_request.config.labels[_ADK_AGENT_NAME_LABEL_KEY] = (
512
+ invocation_context.agent.name
513
+ )
514
+
496
515
  # Calls the LLM.
497
516
  llm = self.__get_llm(invocation_context)
498
517
  with tracer.start_as_current_span('call_llm'):
@@ -170,10 +170,10 @@ def _rearrange_events_for_latest_function_response(
170
170
  for idx in range(function_call_event_idx + 1, len(events) - 1):
171
171
  event = events[idx]
172
172
  function_responses = event.get_function_responses()
173
- if (
174
- function_responses
175
- and function_responses[0].id in function_responses_ids
176
- ):
173
+ if function_responses and any([
174
+ function_response.id in function_responses_ids
175
+ for function_response in function_responses
176
+ ]):
177
177
  function_response_events.append(event)
178
178
  function_response_events.append(events[-1])
179
179