feat: Add a new option eval_storage_uri in adk web & adk eval to specify GCS bucket to store eval data

PiperOrigin-RevId: 774947795
This commit is contained in:
Joseph Pagadora
2025-06-23 15:24:57 -07:00
committed by Copybara-Service
parent 120cbabeb2
commit fa025d7559
5 changed files with 139 additions and 15 deletions
+59 -5
View File
@@ -31,12 +31,15 @@ import uvicorn
from . import cli_create
from . import cli_deploy
from .. import version
from ..evaluation.gcs_eval_set_results_manager import GcsEvalSetResultsManager
from ..evaluation.gcs_eval_sets_manager import GcsEvalSetsManager
from ..evaluation.local_eval_set_results_manager import LocalEvalSetResultsManager
from ..sessions.in_memory_session_service import InMemorySessionService
from .cli import run_cli
from .cli_eval import MISSING_EVAL_DEPENDENCIES_MESSAGE
from .fast_api import get_fast_api_app
from .utils import envs
from .utils import evals
from .utils import logs
LOG_LEVELS = click.Choice(
@@ -282,11 +285,21 @@ def cli_run(
default=False,
help="Optional. Whether to print detailed results on console or not.",
)
@click.option(
"--eval_storage_uri",
type=str,
help=(
"Optional. The evals storage URI to store agent evals,"
" supported URIs: gs://<bucket name>."
),
default=None,
)
def cli_eval(
agent_module_file_path: str,
eval_set_file_path: tuple[str],
eval_set_file_path: list[str],
config_file_path: str,
print_detailed_results: bool,
eval_storage_uri: Optional[str] = None,
):
"""Evaluates an agent given the eval sets.
@@ -338,11 +351,32 @@ def cli_eval(
root_agent = get_root_agent(agent_module_file_path)
reset_func = try_get_reset_func(agent_module_file_path)
gcs_eval_sets_manager = None
eval_set_results_manager = None
if eval_storage_uri:
gcs_eval_managers = evals.create_gcs_eval_managers_from_uri(
eval_storage_uri
)
gcs_eval_sets_manager = gcs_eval_managers.eval_sets_manager
eval_set_results_manager = gcs_eval_managers.eval_set_results_manager
else:
eval_set_results_manager = LocalEvalSetResultsManager(
agents_dir=os.path.dirname(agent_module_file_path)
)
eval_set_file_path_to_evals = parse_and_get_evals_to_run(eval_set_file_path)
eval_set_id_to_eval_cases = {}
# Read the eval_set files and get the cases.
for eval_set_file_path, eval_case_ids in eval_set_file_path_to_evals.items():
if gcs_eval_sets_manager:
eval_set = gcs_eval_sets_manager._load_eval_set_from_blob(
eval_set_file_path
)
if not eval_set:
raise click.ClickException(
f"Eval set {eval_set_file_path} not found in GCS."
)
else:
eval_set = load_eval_set_from_file(eval_set_file_path, eval_set_file_path)
eval_cases = eval_set.eval_cases
@@ -378,16 +412,13 @@ def cli_eval(
raise click.ClickException(MISSING_EVAL_DEPENDENCIES_MESSAGE)
# Write eval set results.
local_eval_set_results_manager = LocalEvalSetResultsManager(
agents_dir=os.path.dirname(agent_module_file_path)
)
eval_set_id_to_eval_results = collections.defaultdict(list)
for eval_case_result in eval_results:
eval_set_id = eval_case_result.eval_set_id
eval_set_id_to_eval_results[eval_set_id].append(eval_case_result)
for eval_set_id, eval_case_results in eval_set_id_to_eval_results.items():
local_eval_set_results_manager.save_eval_set_result(
eval_set_results_manager.save_eval_set_result(
app_name=os.path.basename(agent_module_file_path),
eval_set_id=eval_set_id,
eval_case_results=eval_case_results,
@@ -444,6 +475,15 @@ def adk_services_options():
),
default=None,
)
@click.option(
"--eval_storage_uri",
type=str,
help=(
"Optional. The evals storage URI to store agent evals,"
" supported URIs: gs://<bucket name>."
),
default=None,
)
@click.option(
"--memory_service_uri",
type=str,
@@ -564,6 +604,7 @@ def fast_api_common_options():
)
def cli_web(
agents_dir: str,
eval_storage_uri: Optional[str] = None,
log_level: str = "INFO",
allow_origins: Optional[list[str]] = None,
host: str = "127.0.0.1",
@@ -616,6 +657,7 @@ def cli_web(
session_service_uri=session_service_uri,
artifact_service_uri=artifact_service_uri,
memory_service_uri=memory_service_uri,
eval_storage_uri=eval_storage_uri,
allow_origins=allow_origins,
web=True,
trace_to_cloud=trace_to_cloud,
@@ -654,6 +696,7 @@ def cli_web(
)
def cli_api_server(
agents_dir: str,
eval_storage_uri: Optional[str] = None,
log_level: str = "INFO",
allow_origins: Optional[list[str]] = None,
host: str = "127.0.0.1",
@@ -685,6 +728,7 @@ def cli_api_server(
session_service_uri=session_service_uri,
artifact_service_uri=artifact_service_uri,
memory_service_uri=memory_service_uri,
eval_storage_uri=eval_storage_uri,
allow_origins=allow_origins,
web=False,
trace_to_cloud=trace_to_cloud,
@@ -771,6 +815,15 @@ def cli_api_server(
" version in the dev environment)"
),
)
@click.option(
"--eval_storage_uri",
type=str,
help=(
"Optional. The evals storage URI to store agent evals,"
" supported URIs: gs://<bucket name>."
),
default=None,
)
@adk_services_options()
@deprecated_adk_services_options()
@click.argument(
@@ -797,6 +850,7 @@ def cli_deploy_cloud_run(
session_service_uri: Optional[str] = None,
artifact_service_uri: Optional[str] = None,
memory_service_uri: Optional[str] = None,
eval_storage_uri: Optional[str] = None,
session_db_url: Optional[str] = None, # Deprecated
artifact_storage_uri: Optional[str] = None, # Deprecated
):
+13
View File
@@ -65,6 +65,8 @@ from ..evaluation.eval_metrics import EvalMetric
from ..evaluation.eval_metrics import EvalMetricResult
from ..evaluation.eval_metrics import EvalMetricResultPerInvocation
from ..evaluation.eval_result import EvalSetResult
from ..evaluation.gcs_eval_set_results_manager import GcsEvalSetResultsManager
from ..evaluation.gcs_eval_sets_manager import GcsEvalSetsManager
from ..evaluation.local_eval_set_results_manager import LocalEvalSetResultsManager
from ..evaluation.local_eval_sets_manager import LocalEvalSetsManager
from ..events.event import Event
@@ -198,6 +200,7 @@ def get_fast_api_app(
session_service_uri: Optional[str] = None,
artifact_service_uri: Optional[str] = None,
memory_service_uri: Optional[str] = None,
eval_storage_uri: Optional[str] = None,
allow_origins: Optional[list[str]] = None,
web: bool,
trace_to_cloud: bool = False,
@@ -256,6 +259,16 @@ def get_fast_api_app(
runner_dict = {}
# Set up eval managers.
eval_sets_manager = None
eval_set_results_manager = None
if eval_storage_uri:
gcs_eval_managers = evals.create_gcs_eval_managers_from_uri(
eval_storage_uri
)
eval_sets_manager = gcs_eval_managers.eval_sets_manager
eval_set_results_manager = gcs_eval_managers.eval_set_results_manager
else:
eval_sets_manager = LocalEvalSetsManager(agents_dir=agents_dir)
eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=agents_dir)
+53
View File
@@ -14,17 +14,36 @@
from __future__ import annotations
import dataclasses
import os
from typing import Any
from typing import Tuple
from google.genai import types as genai_types
from pydantic import alias_generators
from pydantic import BaseModel
from pydantic import ConfigDict
from typing_extensions import deprecated
from ...evaluation.eval_case import IntermediateData
from ...evaluation.eval_case import Invocation
from ...evaluation.gcs_eval_set_results_manager import GcsEvalSetResultsManager
from ...evaluation.gcs_eval_sets_manager import GcsEvalSetsManager
from ...sessions.session import Session
class GcsEvalManagers(BaseModel):
model_config = ConfigDict(
alias_generator=alias_generators.to_camel,
populate_by_name=True,
arbitrary_types_allowed=True,
)
eval_sets_manager: GcsEvalSetsManager
eval_set_results_manager: GcsEvalSetResultsManager
@deprecated('Use convert_session_to_eval_invocations instead.')
def convert_session_to_eval_format(session: Session) -> list[dict[str, Any]]:
"""Converts a session data into eval format.
@@ -176,3 +195,37 @@ def convert_session_to_eval_invocations(session: Session) -> list[Invocation]:
)
return invocations
def create_gcs_eval_managers_from_uri(
eval_storage_uri: str,
) -> GcsEvalManagers:
"""Creates GcsEvalManagers from eval_storage_uri.
Args:
eval_storage_uri: The evals storage URI to use. Supported URIs:
gs://<bucket name>. If a path is provided, the bucket will be extracted.
Returns:
GcsEvalManagers: The GcsEvalManagers object.
Raises:
ValueError: If the eval_storage_uri is not supported.
"""
if eval_storage_uri.startswith('gs://'):
gcs_bucket = eval_storage_uri.split('://')[1]
eval_sets_manager = GcsEvalSetsManager(
bucket_name=gcs_bucket, project=os.environ['GOOGLE_CLOUD_PROJECT']
)
eval_set_results_manager = GcsEvalSetResultsManager(
bucket_name=gcs_bucket, project=os.environ['GOOGLE_CLOUD_PROJECT']
)
return GcsEvalManagers(
eval_sets_manager=eval_sets_manager,
eval_set_results_manager=eval_set_results_manager,
)
else:
raise ValueError(
f'Unsupported evals storage URI: {eval_storage_uri}. Supported URIs:'
' gs://<bucket name>'
)
@@ -72,6 +72,13 @@ class GcsEvalSetsManager(EvalSetsManager):
f"Invalid {id_name}. {id_name} should have the `{pattern}` format",
)
def _load_eval_set_from_blob(self, blob_name: str) -> Optional[EvalSet]:
blob = self.bucket.blob(blob_name)
if not blob.exists():
return None
eval_set_data = blob.download_as_text()
return EvalSet.model_validate_json(eval_set_data)
def _write_eval_set_to_blob(self, blob_name: str, eval_set: EvalSet):
"""Writes an EvalSet to GCS."""
blob = self.bucket.blob(blob_name)
@@ -88,11 +95,7 @@ class GcsEvalSetsManager(EvalSetsManager):
def get_eval_set(self, app_name: str, eval_set_id: str) -> Optional[EvalSet]:
"""Returns an EvalSet identified by an app_name and eval_set_id."""
eval_set_blob_name = self._get_eval_set_blob_name(app_name, eval_set_id)
blob = self.bucket.blob(eval_set_blob_name)
if not blob.exists():
return None
eval_set_data = blob.download_as_text()
return EvalSet.model_validate_json(eval_set_data)
return self._load_eval_set_from_blob(eval_set_blob_name)
@override
def create_eval_set(self, app_name: str, eval_set_id: str):
+3 -2
View File
@@ -40,7 +40,7 @@ logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
logger = logging.getLogger("google_adk." + __name__)
# Here we create a dummy agent module that get_fast_api_app expects
@@ -138,6 +138,7 @@ async def mock_run_evals_for_fast_api(*args, **kwargs):
final_eval_status=1, # Matches expected (assuming 1 is PASSED)
user_id="test_user", # Placeholder, adapt if needed
session_id="test_session_for_eval_case", # Placeholder
eval_set_file="test_eval_set_file", # Placeholder
overall_eval_metric_results=[{ # Matches expected
"metricName": "tool_trajectory_avg_score",
"threshold": 0.5,
@@ -372,7 +373,7 @@ def mock_eval_sets_manager():
@pytest.fixture
def mock_eval_set_results_manager():
"""Create a mock eval set results manager."""
"""Create a mock local eval set results manager."""
# Storage for eval set results.
eval_set_results = {}