Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/google/adk/cli/cli_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,10 @@ def get_eval_sets_manager(
except ModuleNotFoundError as mnf:
raise click.ClickException(MISSING_EVAL_DEPENDENCIES_MESSAGE) from mnf

if eval_storage_uri:
eval_storage = evals.resolve_eval_storage(eval_storage_uri, agents_dir)
if eval_storage.gcs_uri:
gcs_eval_managers = evals.create_gcs_eval_managers_from_uri(
eval_storage_uri
eval_storage.gcs_uri
)
return gcs_eval_managers.eval_sets_manager
else:
return LocalEvalSetsManager(agents_dir=agents_dir)
return LocalEvalSetsManager(agents_dir=eval_storage.local_dir)
24 changes: 15 additions & 9 deletions src/google/adk/cli/cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,7 +1206,9 @@ def decorator(func):
type=str,
help=(
"Optional. The evals storage URI to store agent evals,"
" supported URIs: gs://<bucket name>."
" supported URIs: gs://<bucket name> or file://<path>. When"
" omitted, ADK_EVAL_STORAGE_URI then ADK_EVAL_STORAGE_DIR are"
" used."
),
default=None,
)
Expand Down Expand Up @@ -1355,6 +1357,7 @@ def cli_eval(
from .cli_eval import get_app_or_root_agent
from .cli_eval import parse_and_get_evals_to_run
from .cli_eval import pretty_print_eval_result
from .utils import evals
except ModuleNotFoundError as mnf:
raise click.ClickException(_missing_eval_dependencies_message()) from mnf

Expand All @@ -1364,16 +1367,17 @@ def cli_eval(
eval_sets_manager = None
eval_set_results_manager = None

if eval_storage_uri:
from .utils import evals

eval_storage = evals.resolve_eval_storage(eval_storage_uri, agents_dir)
if eval_storage.gcs_uri:
gcs_eval_managers = evals.create_gcs_eval_managers_from_uri(
eval_storage_uri
eval_storage.gcs_uri
)
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=agents_dir)
eval_set_results_manager = LocalEvalSetResultsManager(
agents_dir=eval_storage.local_dir
)

inference_requests = []
eval_set_file_or_id_to_evals = parse_and_get_evals_to_run(
Expand Down Expand Up @@ -1440,8 +1444,8 @@ def cli_eval(
# We assume that what we have are eval set ids instead.
eval_sets_manager = (
eval_sets_manager
if eval_storage_uri
else LocalEvalSetsManager(agents_dir=agents_dir)
if eval_storage.gcs_uri
else LocalEvalSetsManager(agents_dir=eval_storage.local_dir)
)

for eval_set_id_key, eval_case_ids in eval_set_file_or_id_to_evals.items():
Expand Down Expand Up @@ -2023,7 +2027,9 @@ def decorator(func):
type=str,
help=(
"Optional. The evals storage URI to store agent evals,"
" supported URIs: gs://<bucket name>."
" supported URIs: gs://<bucket name> or file://<path>. When"
" omitted, ADK_EVAL_STORAGE_URI then ADK_EVAL_STORAGE_DIR are"
" used."
),
default=None,
)
Expand Down
21 changes: 13 additions & 8 deletions src/google/adk/cli/fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,10 @@ def get_fast_api_app(
memory_service_uri: URI for the memory service. Uses local memory service if
None.
use_local_storage: Whether to use local storage for session and artifacts.
eval_storage_uri: URI for evaluation storage. If provided, uses GCS
managers.
eval_storage_uri: URI for evaluation storage. Supports ``gs://<bucket>``
for GCS and ``file://<path>`` for a local directory. When omitted,
``ADK_EVAL_STORAGE_URI`` then ``ADK_EVAL_STORAGE_DIR`` are used, and
local eval files default to ``agents_dir``.
allow_origins: List of allowed origins for CORS.
web: Whether to enable the web UI and serve its assets.
a2a: Whether to enable Agent-to-Agent (A2A) protocol support.
Expand Down Expand Up @@ -215,19 +217,22 @@ def get_fast_api_app(
agents_dir = str(agents_path.parent)

# Set up eval managers.
if eval_storage_uri:
from .utils import evals
from .utils import evals

eval_storage = evals.resolve_eval_storage(eval_storage_uri, agents_dir)
this_module = sys.modules[__name__]
if eval_storage.gcs_uri:
gcs_eval_managers = evals.create_gcs_eval_managers_from_uri(
eval_storage_uri
eval_storage.gcs_uri
)
eval_sets_manager = gcs_eval_managers.eval_sets_manager
eval_set_results_manager = gcs_eval_managers.eval_set_results_manager
else:
this_module = sys.modules[__name__]
eval_sets_manager = this_module.LocalEvalSetsManager(agents_dir=agents_dir)
eval_sets_manager = this_module.LocalEvalSetsManager(
agents_dir=eval_storage.local_dir
)
eval_set_results_manager = this_module.LocalEvalSetResultsManager(
agents_dir=agents_dir
agents_dir=eval_storage.local_dir
)

# initialize Agent Loader if not passed as argument
Expand Down
126 changes: 126 additions & 0 deletions src/google/adk/cli/utils/evals.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@

from __future__ import annotations

import logging
import os
from pathlib import Path
from typing import NamedTuple
from typing import TYPE_CHECKING
from urllib.parse import unquote
from urllib.parse import urlparse

from pydantic import alias_generators
from pydantic import BaseModel
Expand All @@ -29,6 +34,11 @@
from ...evaluation.gcs_eval_set_results_manager import GcsEvalSetResultsManager
from ...evaluation.gcs_eval_sets_manager import GcsEvalSetsManager

logger = logging.getLogger('google_adk.' + __name__)

ADK_EVAL_STORAGE_URI_ENV = 'ADK_EVAL_STORAGE_URI'
ADK_EVAL_STORAGE_DIR_ENV = 'ADK_EVAL_STORAGE_DIR'


class GcsEvalManagers(BaseModel):
model_config = ConfigDict(
Expand All @@ -42,6 +52,122 @@ class GcsEvalManagers(BaseModel):
eval_set_results_manager: 'GcsEvalSetResultsManager'


class ResolvedEvalStorage(NamedTuple):
"""Where eval sets and results should be stored.

Attributes:
gcs_uri: ``gs://`` URI when using Cloud Storage, otherwise None.
local_dir: Directory for the local eval managers. Unused when ``gcs_uri``
is set.
"""

gcs_uri: str | None
local_dir: str


def resolve_eval_storage_uri(eval_storage_uri: str | None) -> str | None:
"""Resolves the eval storage URI from an argument or the environment.

Precedence is the explicit ``eval_storage_uri``, then
``ADK_EVAL_STORAGE_URI``, then ``ADK_EVAL_STORAGE_DIR`` converted to a
``file://`` URI.

Args:
eval_storage_uri: Explicit URI from a flag or ``get_fast_api_app``.

Returns:
The URI to use, or None to store evals under ``agents_dir``.
"""
if eval_storage_uri:
return eval_storage_uri

env_uri = os.environ.get(ADK_EVAL_STORAGE_URI_ENV)
if env_uri:
logger.info(
'Using eval storage URI from %s: %s', ADK_EVAL_STORAGE_URI_ENV, env_uri
)
return env_uri

env_dir = os.environ.get(ADK_EVAL_STORAGE_DIR_ENV)
if env_dir:
file_uri = Path(os.path.abspath(os.path.expanduser(env_dir))).as_uri()
logger.info(
'Using eval storage directory from %s: %s',
ADK_EVAL_STORAGE_DIR_ENV,
env_dir,
)
return file_uri

return None


def local_path_from_file_uri(eval_storage_uri: str) -> str:
"""Returns the filesystem path for a ``file://`` eval storage URI.

Args:
eval_storage_uri: A ``file://`` URI pointing at a local directory.

Returns:
The decoded filesystem path.

Raises:
ValueError: If the URI is not a ``file://`` URI.
"""
parsed = urlparse(eval_storage_uri)
if parsed.scheme != 'file':
raise ValueError(
f'Unsupported evals storage URI: {eval_storage_uri}. Supported URIs:'
' gs://<bucket name>, file://<path>'
)

path = unquote(parsed.path)
if os.name == 'nt':
if parsed.netloc and parsed.netloc.lower() != 'localhost':
return '\\\\' + parsed.netloc + path.replace('/', '\\')
if path.startswith('/') and len(path) >= 3 and path[2] == ':':
path = path[1:]
return path.replace('/', '\\')

if parsed.netloc and parsed.netloc.lower() != 'localhost':
return '//' + parsed.netloc + path
return path


def prepare_local_eval_dir(path: str) -> str:
"""Creates ``path`` if needed and returns it."""
os.makedirs(path, exist_ok=True)
return path


def resolve_eval_storage(
eval_storage_uri: str | None, agents_dir: str
) -> ResolvedEvalStorage:
"""Resolves GCS vs local eval storage from a URI, env vars, or agents_dir.

Args:
eval_storage_uri: Explicit URI from a flag or ``get_fast_api_app``.
agents_dir: Fallback directory when no URI or env override is set.

Returns:
A ``ResolvedEvalStorage`` with either a GCS URI or a local directory.

Raises:
ValueError: If the resolved URI is neither ``gs://`` nor ``file://``.
"""
resolved_uri = resolve_eval_storage_uri(eval_storage_uri)
if not resolved_uri:
return ResolvedEvalStorage(gcs_uri=None, local_dir=agents_dir)
if resolved_uri.startswith('gs://'):
return ResolvedEvalStorage(gcs_uri=resolved_uri, local_dir=agents_dir)
if resolved_uri.startswith('file:'):
local_dir = prepare_local_eval_dir(local_path_from_file_uri(resolved_uri))
return ResolvedEvalStorage(gcs_uri=None, local_dir=local_dir)
raise ValueError(
f'Unsupported evals storage URI: {resolved_uri}. Supported URIs:'
' gs://<bucket name>, file://<path>'
)


def convert_session_to_eval_invocations(session: Session) -> list[Invocation]:
"""Converts a session data into a list of Invocation.

Expand Down
55 changes: 55 additions & 0 deletions tests/unittests/cli/test_fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,61 @@ def _create_test_client(
return TestClient(app)


def test_get_fast_api_app_creates_file_eval_storage_dir(
tmp_path,
monkeypatch,
mock_session_service,
mock_artifact_service,
mock_memory_service,
mock_agent_loader,
mock_eval_sets_manager,
mock_eval_set_results_manager,
):
"""file:// eval_storage_uri creates that directory for local eval managers."""
monkeypatch.delenv("ADK_EVAL_STORAGE_URI", raising=False)
monkeypatch.delenv("ADK_EVAL_STORAGE_DIR", raising=False)
storage_dir = tmp_path / "adk_evals"

_create_test_client(
mock_session_service,
mock_artifact_service,
mock_memory_service,
mock_agent_loader,
mock_eval_sets_manager,
mock_eval_set_results_manager,
eval_storage_uri=storage_dir.as_uri(),
)

assert storage_dir.is_dir()


def test_get_fast_api_app_honors_eval_storage_dir_env(
tmp_path,
monkeypatch,
mock_session_service,
mock_artifact_service,
mock_memory_service,
mock_agent_loader,
mock_eval_sets_manager,
mock_eval_set_results_manager,
):
"""ADK_EVAL_STORAGE_DIR is used when eval_storage_uri is omitted."""
monkeypatch.delenv("ADK_EVAL_STORAGE_URI", raising=False)
storage_dir = tmp_path / "from_env"
monkeypatch.setenv("ADK_EVAL_STORAGE_DIR", str(storage_dir))

_create_test_client(
mock_session_service,
mock_artifact_service,
mock_memory_service,
mock_agent_loader,
mock_eval_sets_manager,
mock_eval_set_results_manager,
)

assert storage_dir.is_dir()


def test_agent_with_bigquery_analytics_plugin(
tmp_path,
mock_session_service,
Expand Down
Loading
Loading