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.
- google/adk/a2a/__init__.py +13 -0
- google/adk/a2a/converters/__init__.py +13 -0
- google/adk/a2a/converters/part_converter.py +166 -0
- google/adk/agents/invocation_context.py +2 -0
- google/adk/agents/llm_agent.py +1 -6
- google/adk/agents/run_config.py +11 -0
- google/adk/auth/auth_credential.py +5 -0
- google/adk/auth/auth_handler.py +22 -96
- google/adk/auth/auth_preprocessor.py +3 -3
- google/adk/auth/auth_tool.py +46 -0
- google/adk/auth/credential_manager.py +265 -0
- google/adk/auth/credential_service/__init__.py +13 -0
- google/adk/auth/credential_service/base_credential_service.py +75 -0
- google/adk/auth/credential_service/in_memory_credential_service.py +64 -0
- google/adk/auth/exchanger/__init__.py +23 -0
- google/adk/auth/exchanger/base_credential_exchanger.py +57 -0
- google/adk/auth/exchanger/credential_exchanger_registry.py +58 -0
- google/adk/auth/exchanger/oauth2_credential_exchanger.py +104 -0
- google/adk/auth/exchanger/service_account_credential_exchanger.py +104 -0
- google/adk/auth/oauth2_credential_util.py +107 -0
- google/adk/auth/refresher/__init__.py +21 -0
- google/adk/auth/refresher/base_credential_refresher.py +74 -0
- google/adk/auth/refresher/credential_refresher_registry.py +59 -0
- google/adk/auth/refresher/oauth2_credential_refresher.py +154 -0
- google/adk/cli/agent_graph.py +34 -32
- google/adk/cli/browser/index.html +2 -2
- google/adk/cli/browser/main-JAAWEV7F.js +92 -0
- google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
- google/adk/cli/cli.py +10 -0
- google/adk/cli/cli_deploy.py +80 -21
- google/adk/cli/cli_tools_click.py +132 -61
- google/adk/cli/fast_api.py +46 -41
- google/adk/cli/utils/agent_loader.py +15 -2
- google/adk/cli/utils/evals.py +4 -2
- google/adk/code_executors/container_code_executor.py +10 -6
- google/adk/code_executors/vertex_ai_code_executor.py +8 -2
- google/adk/evaluation/_eval_set_results_manager_utils.py +44 -0
- google/adk/evaluation/_eval_sets_manager_utils.py +108 -0
- google/adk/evaluation/eval_metrics.py +0 -5
- google/adk/evaluation/eval_result.py +12 -7
- google/adk/evaluation/eval_set_results_manager.py +6 -1
- google/adk/evaluation/gcs_eval_set_results_manager.py +121 -0
- google/adk/evaluation/gcs_eval_sets_manager.py +196 -0
- google/adk/evaluation/local_eval_set_results_manager.py +6 -18
- google/adk/evaluation/local_eval_sets_manager.py +27 -78
- google/adk/evaluation/response_evaluator.py +5 -5
- google/adk/evaluation/trajectory_evaluator.py +9 -6
- google/adk/flows/llm_flows/basic.py +9 -0
- google/adk/models/anthropic_llm.py +1 -1
- google/adk/models/gemini_llm_connection.py +2 -0
- google/adk/models/google_llm.py +57 -16
- google/adk/models/lite_llm.py +2 -1
- google/adk/platform/__init__.py +13 -0
- google/adk/platform/internal/__init__.py +15 -0
- google/adk/platform/internal/thread.py +30 -0
- google/adk/platform/thread.py +31 -0
- google/adk/runners.py +8 -2
- google/adk/sessions/in_memory_session_service.py +12 -1
- google/adk/sessions/vertex_ai_session_service.py +71 -50
- google/adk/tools/__init__.py +2 -0
- google/adk/tools/_automatic_function_calling_util.py +1 -0
- google/adk/tools/_forwarding_artifact_service.py +96 -0
- google/adk/tools/_function_parameter_parse_util.py +1 -0
- google/adk/tools/agent_tool.py +5 -39
- google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -2
- google/adk/tools/authenticated_function_tool.py +107 -0
- google/adk/tools/base_authenticated_tool.py +107 -0
- google/adk/tools/bigquery/bigquery_credentials.py +6 -4
- google/adk/tools/bigquery/bigquery_tool.py +22 -9
- google/adk/tools/bigquery/bigquery_toolset.py +9 -3
- google/adk/tools/bigquery/client.py +7 -3
- google/adk/tools/bigquery/config.py +46 -0
- google/adk/tools/bigquery/metadata_tool.py +114 -91
- google/adk/tools/bigquery/query_tool.py +141 -23
- google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +7 -4
- google/adk/tools/google_search_tool.py +0 -1
- google/adk/tools/mcp_tool/__init__.py +6 -0
- google/adk/tools/mcp_tool/mcp_session_manager.py +271 -149
- google/adk/tools/mcp_tool/mcp_tool.py +79 -22
- google/adk/tools/mcp_tool/mcp_toolset.py +32 -29
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +3 -3
- google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +56 -33
- google/adk/tools/retrieval/files_retrieval.py +7 -1
- google/adk/tools/url_context_tool.py +61 -0
- google/adk/tools/vertex_ai_search_tool.py +13 -2
- google/adk/utils/feature_decorator.py +175 -0
- google/adk/version.py +2 -2
- {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/METADATA +10 -1
- {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/RECORD +92 -61
- google/adk/cli/browser/main-CS5OLUMF.js +0 -91
- google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
- {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/WHEEL +0 -0
- {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/entry_points.txt +0 -0
- {google_adk-1.2.0.dist-info → google_adk-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -12,7 +12,10 @@
|
|
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 atexit
|
18
|
+
import logging
|
16
19
|
import os
|
17
20
|
from typing import Optional
|
18
21
|
|
@@ -27,6 +30,7 @@ from .base_code_executor import BaseCodeExecutor
|
|
27
30
|
from .code_execution_utils import CodeExecutionInput
|
28
31
|
from .code_execution_utils import CodeExecutionResult
|
29
32
|
|
33
|
+
logger = logging.getLogger('google_adk.' + __name__)
|
30
34
|
DEFAULT_IMAGE_TAG = 'adk-code-executor:latest'
|
31
35
|
|
32
36
|
|
@@ -151,13 +155,13 @@ class ContainerCodeExecutor(BaseCodeExecutor):
|
|
151
155
|
if not os.path.exists(self.docker_path):
|
152
156
|
raise FileNotFoundError(f'Invalid Docker path: {self.docker_path}')
|
153
157
|
|
154
|
-
|
158
|
+
logger.info('Building Docker image...')
|
155
159
|
self._client.images.build(
|
156
160
|
path=self.docker_path,
|
157
161
|
tag=self.image,
|
158
162
|
rm=True,
|
159
163
|
)
|
160
|
-
|
164
|
+
logger.info('Docker image: %s built.', self.image)
|
161
165
|
|
162
166
|
def _verify_python_installation(self):
|
163
167
|
"""Verifies the container has python3 installed."""
|
@@ -173,13 +177,13 @@ class ContainerCodeExecutor(BaseCodeExecutor):
|
|
173
177
|
if self.docker_path:
|
174
178
|
self._build_docker_image()
|
175
179
|
|
176
|
-
|
180
|
+
logger.info('Starting container for ContainerCodeExecutor...')
|
177
181
|
self._container = self._client.containers.run(
|
178
182
|
image=self.image,
|
179
183
|
detach=True,
|
180
184
|
tty=True,
|
181
185
|
)
|
182
|
-
|
186
|
+
logger.info('Container %s started.', self._container.id)
|
183
187
|
|
184
188
|
# Verify the container is able to run python3.
|
185
189
|
self._verify_python_installation()
|
@@ -189,7 +193,7 @@ class ContainerCodeExecutor(BaseCodeExecutor):
|
|
189
193
|
if not self._container:
|
190
194
|
return
|
191
195
|
|
192
|
-
|
196
|
+
logger.info('[Cleanup] Stopping the container...')
|
193
197
|
self._container.stop()
|
194
198
|
self._container.remove()
|
195
|
-
|
199
|
+
logger.info('Container %s stopped and removed.', self._container.id)
|
@@ -12,7 +12,9 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
import
|
15
|
+
from __future__ import annotations
|
16
|
+
|
17
|
+
import logging
|
16
18
|
import mimetypes
|
17
19
|
import os
|
18
20
|
from typing import Any
|
@@ -27,6 +29,8 @@ from .code_execution_utils import CodeExecutionInput
|
|
27
29
|
from .code_execution_utils import CodeExecutionResult
|
28
30
|
from .code_execution_utils import File
|
29
31
|
|
32
|
+
logger = logging.getLogger('google_adk.' + __name__)
|
33
|
+
|
30
34
|
_SUPPORTED_IMAGE_TYPES = ['png', 'jpg', 'jpeg']
|
31
35
|
_SUPPORTED_DATA_FILE_TYPES = ['csv']
|
32
36
|
|
@@ -89,7 +93,9 @@ def _get_code_interpreter_extension(resource_name: str = None):
|
|
89
93
|
if resource_name:
|
90
94
|
new_code_interpreter = Extension(resource_name)
|
91
95
|
else:
|
92
|
-
|
96
|
+
logger.info(
|
97
|
+
'No CODE_INTERPRETER_ID found in the environment. Create a new one.'
|
98
|
+
)
|
93
99
|
new_code_interpreter = Extension.from_hub('code_interpreter')
|
94
100
|
os.environ['CODE_INTERPRETER_EXTENSION_NAME'] = (
|
95
101
|
new_code_interpreter.gca_resource.name
|
@@ -0,0 +1,44 @@
|
|
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 time
|
18
|
+
|
19
|
+
from .eval_result import EvalCaseResult
|
20
|
+
from .eval_result import EvalSetResult
|
21
|
+
|
22
|
+
|
23
|
+
def _sanitize_eval_set_result_name(eval_set_result_name: str) -> str:
|
24
|
+
"""Sanitizes the eval set result name."""
|
25
|
+
return eval_set_result_name.replace("/", "_")
|
26
|
+
|
27
|
+
|
28
|
+
def create_eval_set_result(
|
29
|
+
app_name: str,
|
30
|
+
eval_set_id: str,
|
31
|
+
eval_case_results: list[EvalCaseResult],
|
32
|
+
) -> EvalSetResult:
|
33
|
+
"""Creates a new EvalSetResult given eval_case_results."""
|
34
|
+
timestamp = time.time()
|
35
|
+
eval_set_result_id = f"{app_name}_{eval_set_id}_{timestamp}"
|
36
|
+
eval_set_result_name = _sanitize_eval_set_result_name(eval_set_result_id)
|
37
|
+
eval_set_result = EvalSetResult(
|
38
|
+
eval_set_result_id=eval_set_result_id,
|
39
|
+
eval_set_result_name=eval_set_result_name,
|
40
|
+
eval_set_id=eval_set_id,
|
41
|
+
eval_case_results=eval_case_results,
|
42
|
+
creation_timestamp=timestamp,
|
43
|
+
)
|
44
|
+
return eval_set_result
|
@@ -0,0 +1,108 @@
|
|
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 logging
|
18
|
+
from typing import Optional
|
19
|
+
|
20
|
+
from ..errors.not_found_error import NotFoundError
|
21
|
+
from .eval_case import EvalCase
|
22
|
+
from .eval_set import EvalSet
|
23
|
+
from .eval_sets_manager import EvalSetsManager
|
24
|
+
|
25
|
+
logger = logging.getLogger("google_adk." + __name__)
|
26
|
+
|
27
|
+
|
28
|
+
def get_eval_set_from_app_and_id(
|
29
|
+
eval_sets_manager: EvalSetsManager, app_name: str, eval_set_id: str
|
30
|
+
) -> EvalSet:
|
31
|
+
"""Returns an EvalSet if found, otherwise raises NotFoundError."""
|
32
|
+
eval_set = eval_sets_manager.get_eval_set(app_name, eval_set_id)
|
33
|
+
if not eval_set:
|
34
|
+
raise NotFoundError(f"Eval set `{eval_set_id}` not found.")
|
35
|
+
return eval_set
|
36
|
+
|
37
|
+
|
38
|
+
def get_eval_case_from_eval_set(
|
39
|
+
eval_set: EvalSet, eval_case_id: str
|
40
|
+
) -> Optional[EvalCase]:
|
41
|
+
"""Returns an EvalCase if found, otherwise None."""
|
42
|
+
eval_case_to_find = None
|
43
|
+
|
44
|
+
# Look up the eval case by eval_case_id
|
45
|
+
for eval_case in eval_set.eval_cases:
|
46
|
+
if eval_case.eval_id == eval_case_id:
|
47
|
+
eval_case_to_find = eval_case
|
48
|
+
break
|
49
|
+
|
50
|
+
return eval_case_to_find
|
51
|
+
|
52
|
+
|
53
|
+
def add_eval_case_to_eval_set(
|
54
|
+
eval_set: EvalSet, eval_case: EvalCase
|
55
|
+
) -> EvalSet:
|
56
|
+
"""Adds an eval case to an eval set and returns the updated eval set."""
|
57
|
+
eval_case_id = eval_case.eval_id
|
58
|
+
|
59
|
+
if [x for x in eval_set.eval_cases if x.eval_id == eval_case_id]:
|
60
|
+
raise ValueError(
|
61
|
+
f"Eval id `{eval_case_id}` already exists in `{eval_set.eval_set_id}`"
|
62
|
+
" eval set.",
|
63
|
+
)
|
64
|
+
|
65
|
+
eval_set.eval_cases.append(eval_case)
|
66
|
+
return eval_set
|
67
|
+
|
68
|
+
|
69
|
+
def update_eval_case_in_eval_set(
|
70
|
+
eval_set: EvalSet, updated_eval_case: EvalCase
|
71
|
+
) -> EvalSet:
|
72
|
+
"""Updates an eval case in an eval set and returns the updated eval set."""
|
73
|
+
# Find the eval case to be updated.
|
74
|
+
eval_case_id = updated_eval_case.eval_id
|
75
|
+
eval_case_to_update = get_eval_case_from_eval_set(eval_set, eval_case_id)
|
76
|
+
|
77
|
+
if not eval_case_to_update:
|
78
|
+
raise NotFoundError(
|
79
|
+
f"Eval case `{eval_case_id}` not found in eval set"
|
80
|
+
f" `{eval_set.eval_set_id}`."
|
81
|
+
)
|
82
|
+
|
83
|
+
# Remove the existing eval case and add the updated eval case.
|
84
|
+
eval_set.eval_cases.remove(eval_case_to_update)
|
85
|
+
eval_set.eval_cases.append(updated_eval_case)
|
86
|
+
return eval_set
|
87
|
+
|
88
|
+
|
89
|
+
def delete_eval_case_from_eval_set(
|
90
|
+
eval_set: EvalSet, eval_case_id: str
|
91
|
+
) -> EvalSet:
|
92
|
+
"""Deletes an eval case from an eval set and returns the updated eval set."""
|
93
|
+
# Find the eval case to be deleted.
|
94
|
+
eval_case_to_delete = get_eval_case_from_eval_set(eval_set, eval_case_id)
|
95
|
+
|
96
|
+
if not eval_case_to_delete:
|
97
|
+
raise NotFoundError(
|
98
|
+
f"Eval case `{eval_case_id}` not found in eval set"
|
99
|
+
f" `{eval_set.eval_set_id}`."
|
100
|
+
)
|
101
|
+
|
102
|
+
# Remove the existing eval case.
|
103
|
+
logger.info(
|
104
|
+
"EvalCase`%s` was found in the eval set. It will be removed permanently.",
|
105
|
+
eval_case_id,
|
106
|
+
)
|
107
|
+
eval_set.eval_cases.remove(eval_case_to_delete)
|
108
|
+
return eval_set
|
@@ -36,8 +36,9 @@ class EvalCaseResult(BaseModel):
|
|
36
36
|
populate_by_name=True,
|
37
37
|
)
|
38
38
|
|
39
|
-
eval_set_file: str = Field(
|
39
|
+
eval_set_file: Optional[str] = Field(
|
40
40
|
deprecated=True,
|
41
|
+
default=None,
|
41
42
|
description="This field is deprecated, use eval_set_id instead.",
|
42
43
|
)
|
43
44
|
eval_set_id: str = ""
|
@@ -49,11 +50,15 @@ class EvalCaseResult(BaseModel):
|
|
49
50
|
final_eval_status: EvalStatus
|
50
51
|
"""Final eval status for this eval case."""
|
51
52
|
|
52
|
-
eval_metric_results: list[tuple[EvalMetric, EvalMetricResult]] =
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
53
|
+
eval_metric_results: Optional[list[tuple[EvalMetric, EvalMetricResult]]] = (
|
54
|
+
Field(
|
55
|
+
deprecated=True,
|
56
|
+
default=None,
|
57
|
+
description=(
|
58
|
+
"This field is deprecated, use overall_eval_metric_results"
|
59
|
+
" instead."
|
60
|
+
),
|
61
|
+
)
|
57
62
|
)
|
58
63
|
|
59
64
|
overall_eval_metric_results: list[EvalMetricResult]
|
@@ -80,7 +85,7 @@ class EvalSetResult(BaseModel):
|
|
80
85
|
populate_by_name=True,
|
81
86
|
)
|
82
87
|
eval_set_result_id: str
|
83
|
-
eval_set_result_name: str
|
88
|
+
eval_set_result_name: Optional[str] = None
|
84
89
|
eval_set_id: str
|
85
90
|
eval_case_results: list[EvalCaseResult] = Field(default_factory=list)
|
86
91
|
creation_timestamp: float = 0.0
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
16
16
|
|
17
17
|
from abc import ABC
|
18
18
|
from abc import abstractmethod
|
19
|
+
from typing import Optional
|
19
20
|
|
20
21
|
from .eval_result import EvalCaseResult
|
21
22
|
from .eval_result import EvalSetResult
|
@@ -38,7 +39,11 @@ class EvalSetResultsManager(ABC):
|
|
38
39
|
def get_eval_set_result(
|
39
40
|
self, app_name: str, eval_set_result_id: str
|
40
41
|
) -> EvalSetResult:
|
41
|
-
"""Returns
|
42
|
+
"""Returns the EvalSetResult from app_name and eval_set_result_id.
|
43
|
+
|
44
|
+
Raises:
|
45
|
+
NotFoundError: If the EvalSetResult is not found.
|
46
|
+
"""
|
42
47
|
raise NotImplementedError()
|
43
48
|
|
44
49
|
@abstractmethod
|
@@ -0,0 +1,121 @@
|
|
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 logging
|
18
|
+
|
19
|
+
from google.cloud import exceptions as cloud_exceptions
|
20
|
+
from google.cloud import storage
|
21
|
+
from typing_extensions import override
|
22
|
+
|
23
|
+
from ..errors.not_found_error import NotFoundError
|
24
|
+
from ._eval_set_results_manager_utils import create_eval_set_result
|
25
|
+
from .eval_result import EvalCaseResult
|
26
|
+
from .eval_result import EvalSetResult
|
27
|
+
from .eval_set_results_manager import EvalSetResultsManager
|
28
|
+
|
29
|
+
logger = logging.getLogger("google_adk." + __name__)
|
30
|
+
|
31
|
+
_EVAL_HISTORY_DIR = "evals/eval_history"
|
32
|
+
_EVAL_SET_RESULT_FILE_EXTENSION = ".evalset_result.json"
|
33
|
+
|
34
|
+
|
35
|
+
class GcsEvalSetResultsManager(EvalSetResultsManager):
|
36
|
+
"""An EvalSetResultsManager that stores eval results in a GCS bucket."""
|
37
|
+
|
38
|
+
def __init__(self, bucket_name: str, **kwargs):
|
39
|
+
"""Initializes the GcsEvalSetsManager.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
bucket_name: The name of the bucket to use.
|
43
|
+
**kwargs: Keyword arguments to pass to the Google Cloud Storage client.
|
44
|
+
"""
|
45
|
+
self.bucket_name = bucket_name
|
46
|
+
self.storage_client = storage.Client(**kwargs)
|
47
|
+
self.bucket = self.storage_client.bucket(self.bucket_name)
|
48
|
+
# Check if the bucket exists.
|
49
|
+
if not self.bucket.exists():
|
50
|
+
raise ValueError(
|
51
|
+
f"Bucket `{self.bucket_name}` does not exist. Please create it before"
|
52
|
+
" using the GcsEvalSetsManager."
|
53
|
+
)
|
54
|
+
|
55
|
+
def _get_eval_history_dir(self, app_name: str) -> str:
|
56
|
+
return f"{app_name}/{_EVAL_HISTORY_DIR}"
|
57
|
+
|
58
|
+
def _get_eval_set_result_blob_name(
|
59
|
+
self, app_name: str, eval_set_result_id: str
|
60
|
+
) -> str:
|
61
|
+
eval_history_dir = self._get_eval_history_dir(app_name)
|
62
|
+
return f"{eval_history_dir}/{eval_set_result_id}{_EVAL_SET_RESULT_FILE_EXTENSION}"
|
63
|
+
|
64
|
+
def _write_eval_set_result(
|
65
|
+
self, blob_name: str, eval_set_result: EvalSetResult
|
66
|
+
):
|
67
|
+
"""Writes an EvalSetResult to GCS."""
|
68
|
+
blob = self.bucket.blob(blob_name)
|
69
|
+
blob.upload_from_string(
|
70
|
+
eval_set_result.model_dump_json(indent=2),
|
71
|
+
content_type="application/json",
|
72
|
+
)
|
73
|
+
|
74
|
+
@override
|
75
|
+
def save_eval_set_result(
|
76
|
+
self,
|
77
|
+
app_name: str,
|
78
|
+
eval_set_id: str,
|
79
|
+
eval_case_results: list[EvalCaseResult],
|
80
|
+
) -> None:
|
81
|
+
"""Creates and saves a new EvalSetResult given eval_case_results."""
|
82
|
+
eval_set_result = create_eval_set_result(
|
83
|
+
app_name, eval_set_id, eval_case_results
|
84
|
+
)
|
85
|
+
|
86
|
+
eval_set_result_blob_name = self._get_eval_set_result_blob_name(
|
87
|
+
app_name, eval_set_result.eval_set_result_id
|
88
|
+
)
|
89
|
+
logger.info("Writing eval result to blob: %s", eval_set_result_blob_name)
|
90
|
+
self._write_eval_set_result(eval_set_result_blob_name, eval_set_result)
|
91
|
+
|
92
|
+
@override
|
93
|
+
def get_eval_set_result(
|
94
|
+
self, app_name: str, eval_set_result_id: str
|
95
|
+
) -> EvalSetResult:
|
96
|
+
"""Returns an EvalSetResult from app_name and eval_set_result_id."""
|
97
|
+
eval_set_result_blob_name = self._get_eval_set_result_blob_name(
|
98
|
+
app_name, eval_set_result_id
|
99
|
+
)
|
100
|
+
blob = self.bucket.blob(eval_set_result_blob_name)
|
101
|
+
if not blob.exists():
|
102
|
+
raise NotFoundError(f"Eval set result `{eval_set_result_id}` not found.")
|
103
|
+
eval_set_result_data = blob.download_as_text()
|
104
|
+
return EvalSetResult.model_validate_json(eval_set_result_data)
|
105
|
+
|
106
|
+
@override
|
107
|
+
def list_eval_set_results(self, app_name: str) -> list[str]:
|
108
|
+
"""Returns the eval result ids that belong to the given app_name."""
|
109
|
+
eval_history_dir = self._get_eval_history_dir(app_name)
|
110
|
+
eval_set_results = []
|
111
|
+
try:
|
112
|
+
for blob in self.bucket.list_blobs(prefix=eval_history_dir):
|
113
|
+
eval_set_result_id = blob.name.split("/")[-1].removesuffix(
|
114
|
+
_EVAL_SET_RESULT_FILE_EXTENSION
|
115
|
+
)
|
116
|
+
eval_set_results.append(eval_set_result_id)
|
117
|
+
return sorted(eval_set_results)
|
118
|
+
except cloud_exceptions.NotFound as e:
|
119
|
+
raise ValueError(
|
120
|
+
f"App `{app_name}` not found in GCS bucket `{self.bucket_name}`."
|
121
|
+
) from e
|
@@ -0,0 +1,196 @@
|
|
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 logging
|
18
|
+
import re
|
19
|
+
import time
|
20
|
+
from typing import Optional
|
21
|
+
|
22
|
+
from google.cloud import exceptions as cloud_exceptions
|
23
|
+
from google.cloud import storage
|
24
|
+
from typing_extensions import override
|
25
|
+
|
26
|
+
from ._eval_sets_manager_utils import add_eval_case_to_eval_set
|
27
|
+
from ._eval_sets_manager_utils import delete_eval_case_from_eval_set
|
28
|
+
from ._eval_sets_manager_utils import get_eval_case_from_eval_set
|
29
|
+
from ._eval_sets_manager_utils import get_eval_set_from_app_and_id
|
30
|
+
from ._eval_sets_manager_utils import update_eval_case_in_eval_set
|
31
|
+
from .eval_case import EvalCase
|
32
|
+
from .eval_set import EvalSet
|
33
|
+
from .eval_sets_manager import EvalSetsManager
|
34
|
+
|
35
|
+
logger = logging.getLogger("google_adk." + __name__)
|
36
|
+
|
37
|
+
_EVAL_SETS_DIR = "evals/eval_sets"
|
38
|
+
_EVAL_SET_FILE_EXTENSION = ".evalset.json"
|
39
|
+
|
40
|
+
|
41
|
+
class GcsEvalSetsManager(EvalSetsManager):
|
42
|
+
"""An EvalSetsManager that stores eval sets in a GCS bucket."""
|
43
|
+
|
44
|
+
def __init__(self, bucket_name: str, **kwargs):
|
45
|
+
"""Initializes the GcsEvalSetsManager.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
bucket_name: The name of the bucket to use.
|
49
|
+
**kwargs: Keyword arguments to pass to the Google Cloud Storage client.
|
50
|
+
"""
|
51
|
+
self.bucket_name = bucket_name
|
52
|
+
self.storage_client = storage.Client(**kwargs)
|
53
|
+
self.bucket = self.storage_client.bucket(self.bucket_name)
|
54
|
+
# Check if the bucket exists.
|
55
|
+
if not self.bucket.exists():
|
56
|
+
raise ValueError(
|
57
|
+
f"Bucket `{self.bucket_name}` does not exist. Please create it "
|
58
|
+
"before using the GcsEvalSetsManager."
|
59
|
+
)
|
60
|
+
|
61
|
+
def _get_eval_sets_dir(self, app_name: str) -> str:
|
62
|
+
return f"{app_name}/{_EVAL_SETS_DIR}"
|
63
|
+
|
64
|
+
def _get_eval_set_blob_name(self, app_name: str, eval_set_id: str) -> str:
|
65
|
+
eval_sets_dir = self._get_eval_sets_dir(app_name)
|
66
|
+
return f"{eval_sets_dir}/{eval_set_id}{_EVAL_SET_FILE_EXTENSION}"
|
67
|
+
|
68
|
+
def _validate_id(self, id_name: str, id_value: str):
|
69
|
+
pattern = r"^[a-zA-Z0-9_]+$"
|
70
|
+
if not bool(re.fullmatch(pattern, id_value)):
|
71
|
+
raise ValueError(
|
72
|
+
f"Invalid {id_name}. {id_name} should have the `{pattern}` format",
|
73
|
+
)
|
74
|
+
|
75
|
+
def _write_eval_set_to_blob(self, blob_name: str, eval_set: EvalSet):
|
76
|
+
"""Writes an EvalSet to GCS."""
|
77
|
+
blob = self.bucket.blob(blob_name)
|
78
|
+
blob.upload_from_string(
|
79
|
+
eval_set.model_dump_json(indent=2),
|
80
|
+
content_type="application/json",
|
81
|
+
)
|
82
|
+
|
83
|
+
def _save_eval_set(self, app_name: str, eval_set_id: str, eval_set: EvalSet):
|
84
|
+
eval_set_blob_name = self._get_eval_set_blob_name(app_name, eval_set_id)
|
85
|
+
self._write_eval_set_to_blob(eval_set_blob_name, eval_set)
|
86
|
+
|
87
|
+
@override
|
88
|
+
def get_eval_set(self, app_name: str, eval_set_id: str) -> Optional[EvalSet]:
|
89
|
+
"""Returns an EvalSet identified by an app_name and eval_set_id."""
|
90
|
+
eval_set_blob_name = self._get_eval_set_blob_name(app_name, eval_set_id)
|
91
|
+
blob = self.bucket.blob(eval_set_blob_name)
|
92
|
+
if not blob.exists():
|
93
|
+
return None
|
94
|
+
eval_set_data = blob.download_as_text()
|
95
|
+
return EvalSet.model_validate_json(eval_set_data)
|
96
|
+
|
97
|
+
@override
|
98
|
+
def create_eval_set(self, app_name: str, eval_set_id: str):
|
99
|
+
"""Creates an empty EvalSet and saves it to GCS."""
|
100
|
+
self._validate_id(id_name="Eval Set Id", id_value=eval_set_id)
|
101
|
+
new_eval_set_blob_name = self._get_eval_set_blob_name(app_name, eval_set_id)
|
102
|
+
if self.bucket.blob(new_eval_set_blob_name).exists():
|
103
|
+
raise ValueError(
|
104
|
+
f"Eval set `{eval_set_id}` already exists for app `{app_name}`."
|
105
|
+
)
|
106
|
+
logger.info("Creating eval set blob: `%s`", new_eval_set_blob_name)
|
107
|
+
new_eval_set = EvalSet(
|
108
|
+
eval_set_id=eval_set_id,
|
109
|
+
name=eval_set_id,
|
110
|
+
eval_cases=[],
|
111
|
+
creation_timestamp=time.time(),
|
112
|
+
)
|
113
|
+
self._write_eval_set_to_blob(new_eval_set_blob_name, new_eval_set)
|
114
|
+
|
115
|
+
@override
|
116
|
+
def list_eval_sets(self, app_name: str) -> list[str]:
|
117
|
+
"""Returns a list of EvalSet ids that belong to the given app_name."""
|
118
|
+
eval_sets_dir = self._get_eval_sets_dir(app_name)
|
119
|
+
eval_sets = []
|
120
|
+
try:
|
121
|
+
for blob in self.bucket.list_blobs(prefix=eval_sets_dir):
|
122
|
+
if not blob.name.endswith(_EVAL_SET_FILE_EXTENSION):
|
123
|
+
continue
|
124
|
+
eval_set_id = blob.name.split("/")[-1].removesuffix(
|
125
|
+
_EVAL_SET_FILE_EXTENSION
|
126
|
+
)
|
127
|
+
eval_sets.append(eval_set_id)
|
128
|
+
return sorted(eval_sets)
|
129
|
+
except cloud_exceptions.NotFound as e:
|
130
|
+
raise ValueError(
|
131
|
+
f"App `{app_name}` not found in GCS bucket `{self.bucket_name}`."
|
132
|
+
) from e
|
133
|
+
|
134
|
+
@override
|
135
|
+
def get_eval_case(
|
136
|
+
self, app_name: str, eval_set_id: str, eval_case_id: str
|
137
|
+
) -> Optional[EvalCase]:
|
138
|
+
"""Returns an EvalCase identified by an app_name, eval_set_id and eval_case_id."""
|
139
|
+
eval_set = self.get_eval_set(app_name, eval_set_id)
|
140
|
+
if not eval_set:
|
141
|
+
return None
|
142
|
+
return get_eval_case_from_eval_set(eval_set, eval_case_id)
|
143
|
+
|
144
|
+
@override
|
145
|
+
def add_eval_case(self, app_name: str, eval_set_id: str, eval_case: EvalCase):
|
146
|
+
"""Adds the given EvalCase to an existing EvalSet.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
app_name: The name of the app.
|
150
|
+
eval_set_id: The id of the eval set containing the eval case to update.
|
151
|
+
eval_case: The EvalCase to add.
|
152
|
+
|
153
|
+
Raises:
|
154
|
+
NotFoundError: If the eval set is not found.
|
155
|
+
ValueError: If the eval case already exists in the eval set.
|
156
|
+
"""
|
157
|
+
eval_set = get_eval_set_from_app_and_id(self, app_name, eval_set_id)
|
158
|
+
updated_eval_set = add_eval_case_to_eval_set(eval_set, eval_case)
|
159
|
+
self._save_eval_set(app_name, eval_set_id, updated_eval_set)
|
160
|
+
|
161
|
+
@override
|
162
|
+
def update_eval_case(
|
163
|
+
self, app_name: str, eval_set_id: str, updated_eval_case: EvalCase
|
164
|
+
):
|
165
|
+
"""Updates an existing EvalCase.
|
166
|
+
|
167
|
+
Args:
|
168
|
+
app_name: The name of the app.
|
169
|
+
eval_set_id: The id of the eval set containing the eval case to update.
|
170
|
+
updated_eval_case: The updated EvalCase. Overwrites the existing EvalCase
|
171
|
+
using the eval_id field.
|
172
|
+
|
173
|
+
Raises:
|
174
|
+
NotFoundError: If the eval set or the eval case is not found.
|
175
|
+
"""
|
176
|
+
eval_set = get_eval_set_from_app_and_id(self, app_name, eval_set_id)
|
177
|
+
updated_eval_set = update_eval_case_in_eval_set(eval_set, updated_eval_case)
|
178
|
+
self._save_eval_set(app_name, eval_set_id, updated_eval_set)
|
179
|
+
|
180
|
+
@override
|
181
|
+
def delete_eval_case(
|
182
|
+
self, app_name: str, eval_set_id: str, eval_case_id: str
|
183
|
+
):
|
184
|
+
"""Deletes the EvalCase with the given eval_case_id from the given EvalSet.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
app_name: The name of the app.
|
188
|
+
eval_set_id: The id of the eval set containing the eval case to delete.
|
189
|
+
eval_case_id: The id of the eval case to delete.
|
190
|
+
|
191
|
+
Raises:
|
192
|
+
NotFoundError: If the eval set or the eval case to delete is not found.
|
193
|
+
"""
|
194
|
+
eval_set = get_eval_set_from_app_and_id(self, app_name, eval_set_id)
|
195
|
+
updated_eval_set = delete_eval_case_from_eval_set(eval_set, eval_case_id)
|
196
|
+
self._save_eval_set(app_name, eval_set_id, updated_eval_set)
|