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.
- google/adk/agents/base_agent.py +0 -2
- google/adk/agents/invocation_context.py +3 -3
- google/adk/agents/parallel_agent.py +17 -7
- google/adk/agents/sequential_agent.py +8 -8
- google/adk/auth/auth_preprocessor.py +18 -17
- google/adk/cli/agent_graph.py +165 -23
- google/adk/cli/browser/assets/ADK-512-color.svg +9 -0
- google/adk/cli/browser/index.html +2 -2
- google/adk/cli/browser/{main-PKDNKWJE.js → main-CS5OLUMF.js} +59 -59
- google/adk/cli/browser/polyfills-FFHMD2TL.js +17 -0
- google/adk/cli/cli.py +9 -9
- google/adk/cli/cli_deploy.py +157 -0
- google/adk/cli/cli_tools_click.py +228 -99
- google/adk/cli/fast_api.py +119 -34
- google/adk/cli/utils/agent_loader.py +60 -44
- google/adk/cli/utils/envs.py +1 -1
- google/adk/code_executors/unsafe_local_code_executor.py +11 -0
- google/adk/errors/__init__.py +13 -0
- google/adk/errors/not_found_error.py +28 -0
- google/adk/evaluation/agent_evaluator.py +1 -1
- google/adk/evaluation/eval_sets_manager.py +36 -6
- google/adk/evaluation/evaluation_generator.py +5 -4
- google/adk/evaluation/local_eval_sets_manager.py +101 -6
- google/adk/flows/llm_flows/agent_transfer.py +2 -2
- google/adk/flows/llm_flows/base_llm_flow.py +19 -0
- google/adk/flows/llm_flows/contents.py +4 -4
- google/adk/flows/llm_flows/functions.py +140 -127
- google/adk/memory/vertex_ai_rag_memory_service.py +2 -2
- google/adk/models/anthropic_llm.py +7 -10
- google/adk/models/google_llm.py +46 -18
- google/adk/models/lite_llm.py +63 -26
- google/adk/py.typed +0 -0
- google/adk/sessions/_session_util.py +10 -16
- google/adk/sessions/database_session_service.py +81 -66
- google/adk/sessions/vertex_ai_session_service.py +32 -6
- google/adk/telemetry.py +91 -24
- google/adk/tools/_automatic_function_calling_util.py +31 -25
- google/adk/tools/{function_parameter_parse_util.py → _function_parameter_parse_util.py} +9 -3
- google/adk/tools/_gemini_schema_util.py +158 -0
- google/adk/tools/apihub_tool/apihub_toolset.py +3 -2
- google/adk/tools/application_integration_tool/clients/connections_client.py +7 -0
- google/adk/tools/application_integration_tool/integration_connector_tool.py +5 -7
- google/adk/tools/base_tool.py +4 -8
- google/adk/tools/bigquery/__init__.py +11 -1
- google/adk/tools/bigquery/bigquery_credentials.py +9 -4
- google/adk/tools/bigquery/bigquery_toolset.py +86 -0
- google/adk/tools/bigquery/client.py +33 -0
- google/adk/tools/bigquery/metadata_tool.py +249 -0
- google/adk/tools/bigquery/query_tool.py +76 -0
- google/adk/tools/function_tool.py +4 -4
- google/adk/tools/langchain_tool.py +20 -13
- google/adk/tools/load_memory_tool.py +1 -0
- google/adk/tools/mcp_tool/conversion_utils.py +4 -2
- google/adk/tools/mcp_tool/mcp_session_manager.py +63 -5
- google/adk/tools/mcp_tool/mcp_tool.py +3 -2
- google/adk/tools/mcp_tool/mcp_toolset.py +15 -8
- google/adk/tools/openapi_tool/common/common.py +4 -43
- google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +0 -2
- google/adk/tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.py +4 -2
- google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +4 -2
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +7 -127
- google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +2 -7
- google/adk/tools/transfer_to_agent_tool.py +8 -1
- google/adk/tools/vertex_ai_search_tool.py +8 -1
- google/adk/utils/variant_utils.py +51 -0
- google/adk/version.py +1 -1
- {google_adk-1.1.0.dist-info → google_adk-1.2.0.dist-info}/METADATA +7 -7
- {google_adk-1.1.0.dist-info → google_adk-1.2.0.dist-info}/RECORD +71 -61
- google/adk/cli/browser/polyfills-B6TNHZQ6.js +0 -17
- {google_adk-1.1.0.dist-info → google_adk-1.2.0.dist-info}/WHEEL +0 -0
- {google_adk-1.1.0.dist-info → google_adk-1.2.0.dist-info}/entry_points.txt +0 -0
- {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)
|
31
|
-
|
32
|
-
|
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(
|
41
|
-
|
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
|
44
|
-
# b) agents_dir/
|
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 "
|
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
|
-
|
51
|
-
|
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.
|
62
|
+
type(module_candidate.root_agent),
|
63
63
|
)
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
except
|
71
|
-
|
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 "
|
77
|
-
# Covers structure: agents_dir/
|
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.
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
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
|
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'
|
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
|
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
|
google/adk/cli/utils/envs.py
CHANGED
@@ -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
|
-
"""
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
193
|
-
|
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.
|
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
|
-
|
175
|
-
|
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
|
|