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"
PER_TURN_USER_SIMULATOR_QUALITY_V1 = "per_turn_user_simulator_quality_v1"
MetricName: TypeAlias = Union[str, PrebuiltMetrics]
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):
"""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 typing_extensions import TypeAlias
from .eval_case import ConversationScenario
from .eval_case import Invocation
from .eval_metrics import BaseCriterion
from .eval_metrics import EvalStatus
@@ -62,6 +63,7 @@ class Evaluator(ABC):
self,
actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]],
conversation_scenario: Optional[ConversationScenario],
) -> EvaluationResult:
"""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 the expectation is that the length of this list and actual
invocation is the same.
conversation_scenario: An optional conversation scenario for multi-turn
conversations.
"""
raise NotImplementedError()
@@ -20,6 +20,7 @@ from google.genai import types as genai_types
from typing_extensions import override
from ..dependencies.rouge_scorer import rouge_scorer
from .eval_case import ConversationScenario
from .eval_case import Invocation
from .eval_metrics import EvalMetric
from .eval_metrics import Interval
@@ -60,6 +61,7 @@ class RougeEvaluator(Evaluator):
self,
actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult:
if expected_invocations is None:
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 ._retry_options_utils import add_default_retry_options_if_not_present
from .app_details import AppDetails
from .eval_case import ConversationScenario
from .eval_case import Invocation
from .eval_case import InvocationEvent
from .eval_case import InvocationEvents
@@ -720,6 +721,7 @@ class HallucinationsV1Evaluator(Evaluator):
self,
actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult:
# 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.
@@ -29,6 +29,7 @@ from ..utils.context_utils import Aclosing
from ..utils.feature_decorator import experimental
from ._retry_options_utils import add_default_retry_options_if_not_present
from .common import EvalBaseModel
from .eval_case import ConversationScenario
from .eval_case import Invocation
from .eval_metrics import BaseCriterion
from .eval_metrics import EvalMetric
@@ -118,6 +119,7 @@ class LlmAsJudge(Evaluator):
self,
actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult:
if self._expected_invocations_required and expected_invocations is None:
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 InferenceResult
from .base_eval_service import InferenceStatus
from .eval_case import ConversationScenario
from .eval_case import Invocation
from .eval_metrics import EvalMetric
from .eval_metrics import EvalMetricResult
@@ -256,6 +257,7 @@ class LocalEvalService(BaseEvalService):
eval_metric=eval_metric,
actual_invocations=inference_result.inferences,
expected_invocations=eval_case.conversation,
conversation_scenario=eval_case.conversation_scenario,
)
except Exception as e:
# We intentionally catch the Exception as we don't want failures to
@@ -345,6 +347,7 @@ class LocalEvalService(BaseEvalService):
eval_metric: EvalMetric,
actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]],
conversation_scenario: Optional[ConversationScenario],
) -> EvaluationResult:
"""Returns EvaluationResult obtained from evaluating a metric using an Evaluator."""
@@ -359,6 +362,7 @@ class LocalEvalService(BaseEvalService):
return await metric_evaluator.evaluate_invocations(
actual_invocations=actual_invocations,
expected_invocations=expected_invocations,
conversation_scenario=conversation_scenario,
)
else:
# 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_tool_use_quality_v1 import RubricBasedToolUseV1Evaluator
from .safety_evaluator import SafetyEvaluatorV1
from .simulation.per_turn_user_simulator_quality_v1 import PerTurnUserSimulatorQualityV1
from .trajectory_evaluator import TrajectoryEvaluator
logger = logging.getLogger("google_adk." + __name__)
@@ -126,6 +127,10 @@ def _get_default_metric_evaluator_registry() -> MetricEvaluatorRegistry:
metric_info=RubricBasedToolUseV1Evaluator.get_metric_info(),
evaluator=RubricBasedToolUseV1Evaluator,
)
metric_evaluator_registry.register_evaluator(
metric_info=PerTurnUserSimulatorQualityV1.get_metric_info(),
evaluator=PerTurnUserSimulatorQualityV1,
)
return metric_evaluator_registry
@@ -18,6 +18,7 @@ from typing import Optional
from typing_extensions import override
from .eval_case import ConversationScenario
from .eval_case import Invocation
from .eval_metrics import EvalMetric
from .eval_metrics import Interval
@@ -100,6 +101,7 @@ class ResponseEvaluator(Evaluator):
self,
actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult:
# If the metric is response_match_score, just use the RougeEvaluator.
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
from typing_extensions import override
from .eval_case import ConversationScenario
from .eval_case import Invocation
from .evaluator import EvalStatus
from .evaluator import EvaluationResult
@@ -69,6 +70,7 @@ class _VertexAiEvalFacade(Evaluator):
self,
actual_invocations: list[Invocation],
expected_invocations: Optional[list[Invocation]],
_: Optional[ConversationScenario] = None,
) -> EvaluationResult:
if self._expected_invocations_required and expected_invocations is None:
raise ValueError("expected_invocations is needed by this metric.")