google-adk 1.2.1__py3-none-any.whl → 1.4.1__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 (91) 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 +177 -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 +4 -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 +261 -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 +21 -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/oauth2_credential_util.py +107 -0
  20. google/adk/auth/refresher/__init__.py +21 -0
  21. google/adk/auth/refresher/base_credential_refresher.py +74 -0
  22. google/adk/auth/refresher/credential_refresher_registry.py +59 -0
  23. google/adk/auth/refresher/oauth2_credential_refresher.py +126 -0
  24. google/adk/cli/agent_graph.py +34 -32
  25. google/adk/cli/browser/index.html +2 -2
  26. google/adk/cli/browser/main-JAAWEV7F.js +92 -0
  27. google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
  28. google/adk/cli/cli.py +10 -0
  29. google/adk/cli/cli_deploy.py +80 -21
  30. google/adk/cli/cli_tools_click.py +132 -61
  31. google/adk/cli/fast_api.py +46 -41
  32. google/adk/cli/utils/agent_loader.py +15 -2
  33. google/adk/code_executors/container_code_executor.py +10 -6
  34. google/adk/code_executors/vertex_ai_code_executor.py +8 -2
  35. google/adk/evaluation/_eval_set_results_manager_utils.py +44 -0
  36. google/adk/evaluation/_eval_sets_manager_utils.py +108 -0
  37. google/adk/evaluation/eval_metrics.py +0 -5
  38. google/adk/evaluation/eval_result.py +12 -7
  39. google/adk/evaluation/eval_set_results_manager.py +6 -1
  40. google/adk/evaluation/gcs_eval_set_results_manager.py +121 -0
  41. google/adk/evaluation/gcs_eval_sets_manager.py +196 -0
  42. google/adk/evaluation/local_eval_set_results_manager.py +6 -18
  43. google/adk/evaluation/local_eval_sets_manager.py +27 -78
  44. google/adk/flows/llm_flows/basic.py +9 -0
  45. google/adk/flows/llm_flows/functions.py +1 -2
  46. google/adk/models/anthropic_llm.py +1 -1
  47. google/adk/models/gemini_llm_connection.py +2 -0
  48. google/adk/models/google_llm.py +57 -16
  49. google/adk/models/lite_llm.py +2 -1
  50. google/adk/platform/__init__.py +13 -0
  51. google/adk/platform/internal/__init__.py +15 -0
  52. google/adk/platform/internal/thread.py +30 -0
  53. google/adk/platform/thread.py +31 -0
  54. google/adk/runners.py +8 -2
  55. google/adk/sessions/in_memory_session_service.py +12 -1
  56. google/adk/sessions/vertex_ai_session_service.py +71 -50
  57. google/adk/tools/__init__.py +2 -0
  58. google/adk/tools/_automatic_function_calling_util.py +1 -0
  59. google/adk/tools/_forwarding_artifact_service.py +96 -0
  60. google/adk/tools/_function_parameter_parse_util.py +1 -0
  61. google/adk/tools/agent_tool.py +5 -39
  62. google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -2
  63. google/adk/tools/authenticated_function_tool.py +107 -0
  64. google/adk/tools/base_authenticated_tool.py +107 -0
  65. google/adk/tools/bigquery/bigquery_credentials.py +6 -4
  66. google/adk/tools/bigquery/bigquery_tool.py +22 -9
  67. google/adk/tools/bigquery/bigquery_toolset.py +9 -3
  68. google/adk/tools/bigquery/client.py +7 -3
  69. google/adk/tools/bigquery/config.py +46 -0
  70. google/adk/tools/bigquery/metadata_tool.py +114 -91
  71. google/adk/tools/bigquery/query_tool.py +141 -23
  72. google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +7 -4
  73. google/adk/tools/google_search_tool.py +0 -1
  74. google/adk/tools/mcp_tool/__init__.py +6 -0
  75. google/adk/tools/mcp_tool/mcp_session_manager.py +271 -149
  76. google/adk/tools/mcp_tool/mcp_tool.py +73 -22
  77. google/adk/tools/mcp_tool/mcp_toolset.py +32 -29
  78. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +3 -3
  79. google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +55 -33
  80. google/adk/tools/retrieval/files_retrieval.py +7 -1
  81. google/adk/tools/url_context_tool.py +61 -0
  82. google/adk/tools/vertex_ai_search_tool.py +13 -2
  83. google/adk/utils/feature_decorator.py +175 -0
  84. google/adk/version.py +1 -1
  85. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/METADATA +10 -2
  86. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/RECORD +89 -59
  87. google/adk/cli/browser/main-CS5OLUMF.js +0 -91
  88. google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
  89. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/WHEEL +0 -0
  90. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/entry_points.txt +0 -0
  91. {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -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 datetime
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
- print('No CODE_INTERPRETER_ID found in the environment. Create a new one.')
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
@@ -32,11 +32,6 @@ class EvalMetric(BaseModel):
32
32
  populate_by_name=True,
33
33
  )
34
34
 
35
- model_config = ConfigDict(
36
- alias_generator=alias_generators.to_camel,
37
- populate_by_name=True,
38
- )
39
-
40
35
  metric_name: str
41
36
  """The name of the metric."""
42
37
 
@@ -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]] = Field(
53
- deprecated=True,
54
- description=(
55
- "This field is deprecated, use overall_eval_metric_results instead."
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 an EvalSetResult identified by app_name and eval_set_result_id."""
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)
@@ -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)