mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
feat: Introduce a post-hoc, per-turn evaluator for user simulations
PiperOrigin-RevId: 844818512
This commit is contained in:
committed by
Copybara-Service
parent
69997cd5ef
commit
e515e0f321
@@ -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."""
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user