feat: Introduce a post-hoc, per-turn evaluator for user simulations

PiperOrigin-RevId: 844818512
This commit is contained in:
Google Team Member
2025-12-15 10:05:31 -08:00
committed by Copybara-Service
parent 69997cd5ef
commit e515e0f321
11 changed files with 1157 additions and 0 deletions
+15
View File
@@ -57,6 +57,8 @@ class PrebuiltMetrics(Enum):
RUBRIC_BASED_TOOL_USE_QUALITY_V1 = "rubric_based_tool_use_quality_v1" RUBRIC_BASED_TOOL_USE_QUALITY_V1 = "rubric_based_tool_use_quality_v1"
PER_TURN_USER_SIMULATOR_QUALITY_V1 = "per_turn_user_simulator_quality_v1"
MetricName: TypeAlias = Union[str, PrebuiltMetrics] MetricName: TypeAlias = Union[str, PrebuiltMetrics]
Threshold: TypeAlias = float Threshold: TypeAlias = float
@@ -223,6 +225,19 @@ class ToolTrajectoryCriterion(BaseCriterion):
) )
class LlmBackedUserSimulatorCriterion(LlmAsAJudgeCriterion):
"""Criterion for LLM-backed User Simulator Evaluators."""
stop_signal: str = Field(
default="</finished>",
description=(
"Stop signal to validate the successful completion of a conversation."
" For optimal performance, this should match the one in the User"
" Simulator."
),
)
class EvalMetric(EvalBaseModel): class EvalMetric(EvalBaseModel):
"""A metric used to evaluate a particular aspect of an eval case.""" """A metric used to evaluate a particular aspect of an eval case."""
+4
View File
@@ -20,6 +20,7 @@ from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from typing_extensions import TypeAlias from typing_extensions import TypeAlias
from .eval_case import ConversationScenario
from .eval_case import Invocation from .eval_case import Invocation
from .eval_metrics import BaseCriterion from .eval_metrics import BaseCriterion
from .eval_metrics import EvalStatus from .eval_metrics import EvalStatus
@@ -62,6 +63,7 @@ class Evaluator(ABC):
self, self,
actual_invocations: list[Invocation], actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]], expected_invocations: Optional[list[Invocation]],
conversation_scenario: Optional[ConversationScenario],
) -> EvaluationResult: ) -> EvaluationResult:
"""Returns EvaluationResult after performing evaluations using actual and expected invocations. """Returns EvaluationResult after performing evaluations using actual and expected invocations.
@@ -72,5 +74,7 @@ class Evaluator(ABC):
usually act as a benchmark/golden response. If these are specified usually act as a benchmark/golden response. If these are specified
usually the expectation is that the length of this list and actual usually the expectation is that the length of this list and actual
invocation is the same. invocation is the same.
conversation_scenario: An optional conversation scenario for multi-turn
conversations.
""" """
raise NotImplementedError() raise NotImplementedError()
@@ -20,6 +20,7 @@ from google.genai import types as genai_types
from typing_extensions import override from typing_extensions import override
from ..dependencies.rouge_scorer import rouge_scorer from ..dependencies.rouge_scorer import rouge_scorer
from .eval_case import ConversationScenario
from .eval_case import Invocation from .eval_case import Invocation
from .eval_metrics import EvalMetric from .eval_metrics import EvalMetric
from .eval_metrics import Interval from .eval_metrics import Interval
@@ -60,6 +61,7 @@ class RougeEvaluator(Evaluator):
self, self,
actual_invocations: list[Invocation], actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]], expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult: ) -> EvaluationResult:
if expected_invocations is None: if expected_invocations is None:
raise ValueError("expected_invocations is required for this metric.") raise ValueError("expected_invocations is required for this metric.")
@@ -34,6 +34,7 @@ from ..utils.context_utils import Aclosing
from ..utils.feature_decorator import experimental from ..utils.feature_decorator import experimental
from ._retry_options_utils import add_default_retry_options_if_not_present from ._retry_options_utils import add_default_retry_options_if_not_present
from .app_details import AppDetails from .app_details import AppDetails
from .eval_case import ConversationScenario
from .eval_case import Invocation from .eval_case import Invocation
from .eval_case import InvocationEvent from .eval_case import InvocationEvent
from .eval_case import InvocationEvents from .eval_case import InvocationEvents
@@ -720,6 +721,7 @@ class HallucinationsV1Evaluator(Evaluator):
self, self,
actual_invocations: list[Invocation], actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]], expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult: ) -> EvaluationResult:
# expected_invocations are not required by the metric and if they are not # expected_invocations are not required by the metric and if they are not
# supplied, we provide a list of None to rest of the code. # supplied, we provide a list of None to rest of the code.
@@ -29,6 +29,7 @@ from ..utils.context_utils import Aclosing
from ..utils.feature_decorator import experimental from ..utils.feature_decorator import experimental
from ._retry_options_utils import add_default_retry_options_if_not_present from ._retry_options_utils import add_default_retry_options_if_not_present
from .common import EvalBaseModel from .common import EvalBaseModel
from .eval_case import ConversationScenario
from .eval_case import Invocation from .eval_case import Invocation
from .eval_metrics import BaseCriterion from .eval_metrics import BaseCriterion
from .eval_metrics import EvalMetric from .eval_metrics import EvalMetric
@@ -118,6 +119,7 @@ class LlmAsJudge(Evaluator):
self, self,
actual_invocations: list[Invocation], actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]], expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult: ) -> EvaluationResult:
if self._expected_invocations_required and expected_invocations is None: if self._expected_invocations_required and expected_invocations is None:
raise ValueError("expected_invocations is needed by this metric.") raise ValueError("expected_invocations is needed by this metric.")
@@ -40,6 +40,7 @@ from .base_eval_service import EvaluateRequest
from .base_eval_service import InferenceRequest from .base_eval_service import InferenceRequest
from .base_eval_service import InferenceResult from .base_eval_service import InferenceResult
from .base_eval_service import InferenceStatus from .base_eval_service import InferenceStatus
from .eval_case import ConversationScenario
from .eval_case import Invocation from .eval_case import Invocation
from .eval_metrics import EvalMetric from .eval_metrics import EvalMetric
from .eval_metrics import EvalMetricResult from .eval_metrics import EvalMetricResult
@@ -256,6 +257,7 @@ class LocalEvalService(BaseEvalService):
eval_metric=eval_metric, eval_metric=eval_metric,
actual_invocations=inference_result.inferences, actual_invocations=inference_result.inferences,
expected_invocations=eval_case.conversation, expected_invocations=eval_case.conversation,
conversation_scenario=eval_case.conversation_scenario,
) )
except Exception as e: except Exception as e:
# We intentionally catch the Exception as we don't want failures to # We intentionally catch the Exception as we don't want failures to
@@ -345,6 +347,7 @@ class LocalEvalService(BaseEvalService):
eval_metric: EvalMetric, eval_metric: EvalMetric,
actual_invocations: list[Invocation], actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]], expected_invocations: Optional[list[Invocation]],
conversation_scenario: Optional[ConversationScenario],
) -> EvaluationResult: ) -> EvaluationResult:
"""Returns EvaluationResult obtained from evaluating a metric using an Evaluator.""" """Returns EvaluationResult obtained from evaluating a metric using an Evaluator."""
@@ -359,6 +362,7 @@ class LocalEvalService(BaseEvalService):
return await metric_evaluator.evaluate_invocations( return await metric_evaluator.evaluate_invocations(
actual_invocations=actual_invocations, actual_invocations=actual_invocations,
expected_invocations=expected_invocations, expected_invocations=expected_invocations,
conversation_scenario=conversation_scenario,
) )
else: else:
# Metrics that perform computation synchronously, mostly these don't # Metrics that perform computation synchronously, mostly these don't
@@ -28,6 +28,7 @@ from .response_evaluator import ResponseEvaluator
from .rubric_based_final_response_quality_v1 import RubricBasedFinalResponseQualityV1Evaluator from .rubric_based_final_response_quality_v1 import RubricBasedFinalResponseQualityV1Evaluator
from .rubric_based_tool_use_quality_v1 import RubricBasedToolUseV1Evaluator from .rubric_based_tool_use_quality_v1 import RubricBasedToolUseV1Evaluator
from .safety_evaluator import SafetyEvaluatorV1 from .safety_evaluator import SafetyEvaluatorV1
from .simulation.per_turn_user_simulator_quality_v1 import PerTurnUserSimulatorQualityV1
from .trajectory_evaluator import TrajectoryEvaluator from .trajectory_evaluator import TrajectoryEvaluator
logger = logging.getLogger("google_adk." + __name__) logger = logging.getLogger("google_adk." + __name__)
@@ -126,6 +127,10 @@ def _get_default_metric_evaluator_registry() -> MetricEvaluatorRegistry:
metric_info=RubricBasedToolUseV1Evaluator.get_metric_info(), metric_info=RubricBasedToolUseV1Evaluator.get_metric_info(),
evaluator=RubricBasedToolUseV1Evaluator, evaluator=RubricBasedToolUseV1Evaluator,
) )
metric_evaluator_registry.register_evaluator(
metric_info=PerTurnUserSimulatorQualityV1.get_metric_info(),
evaluator=PerTurnUserSimulatorQualityV1,
)
return metric_evaluator_registry return metric_evaluator_registry
@@ -18,6 +18,7 @@ from typing import Optional
from typing_extensions import override from typing_extensions import override
from .eval_case import ConversationScenario
from .eval_case import Invocation from .eval_case import Invocation
from .eval_metrics import EvalMetric from .eval_metrics import EvalMetric
from .eval_metrics import Interval from .eval_metrics import Interval
@@ -100,6 +101,7 @@ class ResponseEvaluator(Evaluator):
self, self,
actual_invocations: list[Invocation], actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]], expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult: ) -> EvaluationResult:
# If the metric is response_match_score, just use the RougeEvaluator. # If the metric is response_match_score, just use the RougeEvaluator.
if self._metric_name == PrebuiltMetrics.RESPONSE_MATCH_SCORE.value: if self._metric_name == PrebuiltMetrics.RESPONSE_MATCH_SCORE.value:
File diff suppressed because it is too large Load Diff
@@ -23,6 +23,7 @@ from google.genai import types as genai_types
import pandas as pd import pandas as pd
from typing_extensions import override from typing_extensions import override
from .eval_case import ConversationScenario
from .eval_case import Invocation from .eval_case import Invocation
from .evaluator import EvalStatus from .evaluator import EvalStatus
from .evaluator import EvaluationResult from .evaluator import EvaluationResult
@@ -69,6 +70,7 @@ class _VertexAiEvalFacade(Evaluator):
self, self,
actual_invocations: list[Invocation], actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]], expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult: ) -> EvaluationResult:
if self._expected_invocations_required and expected_invocations is None: if self._expected_invocations_required and expected_invocations is None:
raise ValueError("expected_invocations is needed by this metric.") raise ValueError("expected_invocations is needed by this metric.")