fix: Adding detailed information on each metric evaluation

Additionally, few other small changes.
*   Updated a test fixture to support the latest eval data schema. Somehow I missed doing that previously.
*   Updated the `evaluation_generator.py` to use `run_async`, instead of `run`.
*   Also, raise an informed error when dependencies required eval are not installed.
*   Also, changed the behavior of AgentEvaluator.evaluate method to run all the evals, instead of failing at the first eval metric failure.

PiperOrigin-RevId: 775919127
This commit is contained in:
Ankur Sharma
2025-06-25 18:32:02 -07:00
committed by Copybara-Service
parent 3901fade71
commit 04de3e197d
7 changed files with 240 additions and 49 deletions
+99 -10
View File
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import json
import logging
import os
@@ -23,16 +25,16 @@ from typing import Optional
from typing import Union
import uuid
from google.genai import types as genai_types
from pydantic import ValidationError
from .constants import MISSING_EVAL_DEPENDENCIES_MESSAGE
from .eval_case import IntermediateData
from .eval_set import EvalSet
from .evaluation_generator import EvaluationGenerator
from .evaluator import EvalStatus
from .evaluator import EvaluationResult
from .evaluator import Evaluator
from .local_eval_sets_manager import convert_eval_set_to_pydanctic_schema
from .response_evaluator import ResponseEvaluator
from .trajectory_evaluator import TrajectoryEvaluator
logger = logging.getLogger("google_adk." + __name__)
@@ -96,6 +98,7 @@ class AgentEvaluator:
criteria: dict[str, float],
num_runs=NUM_RUNS,
agent_name=None,
print_detailed_results: bool = True,
):
"""Evaluates an agent using the given EvalSet.
@@ -109,7 +112,13 @@ class AgentEvaluator:
num_runs: Number of times all entries in the eval dataset should be
assessed.
agent_name: The name of the agent.
print_detailed_results: Whether to print detailed results for each metric
evaluation.
"""
try:
from .evaluation_generator import EvaluationGenerator
except ModuleNotFoundError as e:
raise ModuleNotFoundError(MISSING_EVAL_DEPENDENCIES_MESSAGE) from e
eval_case_responses_list = await EvaluationGenerator.generate_responses(
eval_set=eval_set,
agent_module_path=agent_module,
@@ -117,6 +126,8 @@ class AgentEvaluator:
agent_name=agent_name,
)
failures = []
for eval_case_responses in eval_case_responses_list:
actual_invocations = [
invocation
@@ -139,10 +150,25 @@ class AgentEvaluator:
)
)
assert evaluation_result.overall_eval_status == EvalStatus.PASSED, (
f"{metric_name} for {agent_module} Failed. Expected {threshold},"
f" but got {evaluation_result.overall_score}."
)
if print_detailed_results:
AgentEvaluator._print_details(
evaluation_result=evaluation_result,
metric_name=metric_name,
threshold=threshold,
)
# Gather all the failures.
if evaluation_result.overall_eval_status != EvalStatus.PASSED:
failures.append(
f"{metric_name} for {agent_module} Failed. Expected {threshold},"
f" but got {evaluation_result.overall_score}."
)
assert not failures, (
"Following are all the test failures. If you looking to get more"
" details on the failures, then please re-run this test with"
" `print_details` set to `True`.\n{}".format("\n".join(failures))
)
@staticmethod
async def evaluate(
@@ -158,9 +184,10 @@ class AgentEvaluator:
agent_module: The path to python module that contains the definition of
the agent. There is convention in place here, where the code is going to
look for 'root_agent' in the loaded module.
eval_dataset_file_path_or_dir: The eval data set. This can be either a string representing
full path to the file containing eval dataset, or a directory that is
recursively explored for all files that have a `.test.json` suffix.
eval_dataset_file_path_or_dir: The eval data set. This can be either a
string representing full path to the file containing eval dataset, or a
directory that is recursively explored for all files that have a
`.test.json` suffix.
num_runs: Number of times all entries in the eval dataset should be
assessed.
agent_name: The name of the agent.
@@ -358,6 +385,11 @@ class AgentEvaluator:
@staticmethod
def _get_metric_evaluator(metric_name: str, threshold: float) -> Evaluator:
try:
from .response_evaluator import ResponseEvaluator
from .trajectory_evaluator import TrajectoryEvaluator
except ModuleNotFoundError as e:
raise ModuleNotFoundError(MISSING_EVAL_DEPENDENCIES_MESSAGE) from e
if metric_name == TOOL_TRAJECTORY_SCORE_KEY:
return TrajectoryEvaluator(threshold=threshold)
elif (
@@ -367,3 +399,60 @@ class AgentEvaluator:
return ResponseEvaluator(threshold=threshold, metric_name=metric_name)
raise ValueError(f"Unsupported eval metric: {metric_name}")
@staticmethod
def _print_details(
evaluation_result: EvaluationResult, metric_name: str, threshold: float
):
try:
from pandas import pandas as pd
from tabulate import tabulate
except ModuleNotFoundError as e:
raise ModuleNotFoundError(MISSING_EVAL_DEPENDENCIES_MESSAGE) from e
print(
f"Summary: `{evaluation_result.overall_eval_status}` for Metric:"
f" `{metric_name}`. Expected threshold: `{threshold}`, actual value:"
f" `{evaluation_result.overall_score}`."
)
data = []
for per_invocation_result in evaluation_result.per_invocation_results:
data.append({
"eval_status": per_invocation_result.eval_status,
"score": per_invocation_result.score,
"threshold": threshold,
"prompt": AgentEvaluator._convert_content_to_text(
per_invocation_result.expected_invocation.user_content
),
"expected_response": AgentEvaluator._convert_content_to_text(
per_invocation_result.expected_invocation.final_response
),
"actual_response": AgentEvaluator._convert_content_to_text(
per_invocation_result.actual_invocation.final_response
),
"expected_tool_calls": AgentEvaluator._convert_tool_calls_to_text(
per_invocation_result.expected_invocation.intermediate_data
),
"actual_tool_calls": AgentEvaluator._convert_tool_calls_to_text(
per_invocation_result.actual_invocation.intermediate_data
),
})
print(tabulate(pd.DataFrame(data), headers="keys", tablefmt="grid"))
print("\n\n") # Few empty lines for visual clarity
@staticmethod
def _convert_content_to_text(content: Optional[genai_types.Content]) -> str:
if content and content.parts:
return "\n".join([p.text for p in content.parts if p.text])
return ""
@staticmethod
def _convert_tool_calls_to_text(
intermediate_data: Optional[IntermediateData],
) -> str:
if intermediate_data and intermediate_data.tool_uses:
return "\n".join([str(t) for t in intermediate_data.tool_uses])
return ""
+20
View File
@@ -0,0 +1,20 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
MISSING_EVAL_DEPENDENCIES_MESSAGE = (
"Eval module is not installed, please install via `pip install"
" google-adk[eval]`."
)
@@ -182,7 +182,7 @@ class EvaluationGenerator:
tool_uses = []
invocation_id = ""
for event in runner.run(
async for event in runner.run_async(
user_id=user_id, session_id=session_id, new_message=user_content
):
invocation_id = (