Skip to content

Commit 67ab27f

Browse files
JaeCodingcopybara-github
authored andcommitted
fix: allow invocation-level rubrics
Merge #6161 Defer the missing-rubrics failure until the effective rubric list is built, after invocation rubrics have been merged. Both rubric-based prompt formatters now read rubrics through get_effective_rubrics_list(). CLI pretty printing now treats missing criterion rubrics as an empty lookup and falls back to the rubric id. PiperOrigin-RevId: 952442674
1 parent 88b388e commit 67ab27f

8 files changed

Lines changed: 197 additions & 12 deletions

src/google/adk/cli/cli_eval.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from ..evaluation.eval_case import get_all_tool_calls
3737
from ..evaluation.eval_case import IntermediateDataType
3838
from ..evaluation.eval_metrics import EvalMetric
39+
from ..evaluation.eval_metrics import RubricsBasedCriterion
3940
from ..evaluation.eval_result import EvalCaseResult
4041
from ..evaluation.eval_sets_manager import EvalSetsManager
4142
from ..utils.context_utils import Aclosing
@@ -219,9 +220,13 @@ def pretty_print_eval_result(eval_result: EvalCaseResult) -> None:
219220
)
220221
if metric_result.details and metric_result.details.rubric_scores:
221222
click.echo("Rubric Scores:")
223+
rubrics = (
224+
metric_result.criterion.rubrics
225+
if isinstance(metric_result.criterion, RubricsBasedCriterion)
226+
else None
227+
) or []
222228
rubrics_by_id = {
223-
r["rubric_id"]: r["rubric_content"]["text_property"]
224-
for r in metric_result.criterion.rubrics
229+
r.rubric_id: r.rubric_content.text_property for r in rubrics
225230
}
226231
for rubric_score in metric_result.details.rubric_scores:
227232
rubric_text = rubrics_by_id.get(rubric_score.rubric_id)
@@ -262,9 +267,13 @@ def pretty_print_eval_result(eval_result: EvalCaseResult) -> None:
262267
f"Score: {metric_result.score}"
263268
)
264269
if metric_result.details and metric_result.details.rubric_scores:
270+
rubrics = (
271+
metric_result.criterion.rubrics
272+
if isinstance(metric_result.criterion, RubricsBasedCriterion)
273+
else None
274+
) or []
265275
rubrics_by_id = {
266-
r["rubric_id"]: r["rubric_content"]["text_property"]
267-
for r in metric_result.criterion.rubrics
276+
r.rubric_id: r.rubric_content.text_property for r in rubrics
268277
}
269278
for rubric_score in metric_result.details.rubric_scores:
270279
rubric = rubrics_by_id.get(rubric_score.rubric_id)

src/google/adk/evaluation/rubric_based_evaluator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ def summarize(
300300
)
301301

302302

303-
def _normalize_text(text: str) -> str:
303+
def _normalize_text(text: object) -> str:
304304
"""Returns a normalized version of the passed in text."""
305305
if not isinstance(text, str):
306306
return ""
@@ -352,9 +352,7 @@ def __init__(
352352
self._per_invocation_results_aggregator = per_invocation_results_aggregator
353353
self._invocation_results_summarizer = invocation_results_summarizer
354354

355-
assert self._criterion.rubrics, "Rubrics are required."
356-
357-
self._rubrics: list[Rubric] = self._criterion.rubrics
355+
self._rubrics: list[Rubric] = self._criterion.rubrics or []
358356
self._effective_rubrics_list: Optional[list[Rubric]] = None
359357

360358
self._normalized_rubric_to_id_map = {
@@ -388,6 +386,8 @@ def _add_rubrics(rubrics_to_add: list[Rubric], scope_name: str) -> None:
388386
_add_rubrics(filtered_invocation_rubrics, "invocation")
389387

390388
self._effective_rubrics_list = list(rubrics_by_id.values())
389+
if not self._effective_rubrics_list:
390+
raise ValueError("Rubrics are required.")
391391

392392
def get_effective_rubrics_list(self) -> list[Rubric]:
393393
"""Returns the effective rubrics list."""

src/google/adk/evaluation/rubric_based_final_response_quality_v1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def format_auto_rater_prompt(
296296

297297
rubrics_text = "\n".join([
298298
f"* [id: {r.rubric_id}] {r.rubric_content.text_property}"
299-
for r in self._effective_rubrics_list
299+
for r in self.get_effective_rubrics_list()
300300
])
301301

302302
developer_instructions = ""

src/google/adk/evaluation/rubric_based_multi_turn_trajectory_evaluator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,11 +353,11 @@ def format_auto_rater_prompt(
353353
self.create_effective_rubrics_list(actual_invocation.rubrics)
354354
logger.debug(
355355
"format_auto_rater_prompt called (effective rubrics: %d)",
356-
len(self._effective_rubrics_list),
356+
len(self.get_effective_rubrics_list()),
357357
)
358358

359359
rubrics_list = []
360-
for r in self._effective_rubrics_list:
360+
for r in self.get_effective_rubrics_list():
361361
rubrics_dict = {
362362
"id": r.rubric_id,
363363
"property": r.rubric_content.text_property,

src/google/adk/evaluation/rubric_based_tool_use_quality_v1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def format_auto_rater_prompt(
185185

186186
rubrics_text = "\n".join([
187187
f"* [id: {r.rubric_id}] {r.rubric_content.text_property}"
188-
for r in self._effective_rubrics_list
188+
for r in self.get_effective_rubrics_list()
189189
])
190190

191191
app_details = actual_invocation.app_details
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from google.adk.cli.cli_eval import pretty_print_eval_result
18+
from google.adk.evaluation.eval_case import Invocation
19+
from google.adk.evaluation.eval_metrics import EvalMetricResult
20+
from google.adk.evaluation.eval_metrics import EvalMetricResultDetails
21+
from google.adk.evaluation.eval_metrics import EvalMetricResultPerInvocation
22+
from google.adk.evaluation.eval_metrics import PrebuiltMetrics
23+
from google.adk.evaluation.eval_metrics import RubricsBasedCriterion
24+
from google.adk.evaluation.eval_result import EvalCaseResult
25+
from google.adk.evaluation.eval_rubrics import RubricScore
26+
from google.adk.evaluation.evaluator import EvalStatus
27+
from google.genai import types as genai_types
28+
29+
30+
def test_pretty_print_eval_result_with_empty_criterion_rubrics(capsys):
31+
"""Tests pretty printing falls back to rubric id when criterion rubrics are empty."""
32+
criterion = RubricsBasedCriterion(threshold=0.5)
33+
metric_result = EvalMetricResult(
34+
metric_name=PrebuiltMetrics.RUBRIC_BASED_TOOL_USE_QUALITY_V1.value,
35+
threshold=0.5,
36+
criterion=criterion,
37+
score=1.0,
38+
eval_status=EvalStatus.PASSED,
39+
details=EvalMetricResultDetails(
40+
rubric_scores=[
41+
RubricScore(
42+
rubric_id="invocation-rubric",
43+
score=1.0,
44+
rationale="The correct tool was used.",
45+
)
46+
]
47+
),
48+
)
49+
invocation = Invocation(
50+
user_content=genai_types.Content(
51+
parts=[genai_types.Part(text="User input here.")]
52+
)
53+
)
54+
eval_result = EvalCaseResult(
55+
eval_set_id="eval-set",
56+
eval_id="eval-id",
57+
final_eval_status=EvalStatus.PASSED,
58+
overall_eval_metric_results=[metric_result],
59+
eval_metric_result_per_invocation=[
60+
EvalMetricResultPerInvocation(
61+
actual_invocation=invocation,
62+
eval_metric_results=[metric_result],
63+
)
64+
],
65+
session_id="session-id",
66+
)
67+
68+
pretty_print_eval_result(eval_result)
69+
70+
captured = capsys.readouterr()
71+
assert "Rubric: invocation-rubric" in captured.out
72+
assert "The correct tool was used." in captured.out

tests/unittests/evaluation/test_rubric_based_evaluator.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,24 @@ def test_create_effective_rubrics_list_with_no_invocation_rubrics(
711711
assert len(effective_rubrics) == 2
712712
assert {r.rubric_id for r in effective_rubrics} == {"1", "2"}
713713

714+
def test_create_effective_rubrics_list_with_no_rubrics_raises_error(self):
715+
judge_model_options = JudgeModelOptions(
716+
judge_model_config=None,
717+
num_samples=3,
718+
)
719+
criterion = RubricsBasedCriterion(
720+
threshold=0.5, judge_model_options=judge_model_options
721+
)
722+
metric = EvalMetric(
723+
metric_name=PrebuiltMetrics.RUBRIC_BASED_FINAL_RESPONSE_QUALITY_V1.value,
724+
threshold=0.5,
725+
criterion=criterion,
726+
)
727+
evaluator = FakeRubricBasedEvaluator(metric)
728+
729+
with pytest.raises(ValueError, match="Rubrics are required."):
730+
evaluator.create_effective_rubrics_list(None)
731+
714732
def test_get_effective_rubrics_list_before_creation_raises_error(
715733
self, evaluator: RubricBasedEvaluator
716734
):
@@ -771,6 +789,31 @@ def test_create_effective_rubrics_filters_by_rubric_type(
771789
"test_type_rubric",
772790
}
773791

792+
def test_create_effective_rubrics_filters_to_empty_raises_error(self):
793+
judge_model_options = JudgeModelOptions(
794+
judge_model_config=None,
795+
num_samples=3,
796+
)
797+
criterion = RubricsBasedCriterion(
798+
threshold=0.5, judge_model_options=judge_model_options
799+
)
800+
metric = EvalMetric(
801+
metric_name=PrebuiltMetrics.RUBRIC_BASED_FINAL_RESPONSE_QUALITY_V1.value,
802+
threshold=0.5,
803+
criterion=criterion,
804+
)
805+
evaluator = FakeRubricBasedEvaluator(metric, rubric_type="EXPECTED_TYPE")
806+
invocation_rubrics = [
807+
Rubric(
808+
rubric_id="wrong_type_rubric",
809+
rubric_content=RubricContent(text_property="Invocation rubric"),
810+
type="WRONG_TYPE",
811+
)
812+
]
813+
814+
with pytest.raises(ValueError, match="Rubrics are required."):
815+
evaluator.create_effective_rubrics_list(invocation_rubrics)
816+
774817
def test_convert_matches_by_id_when_text_paraphrased(
775818
self,
776819
evaluator: RubricBasedEvaluator,

tests/unittests/evaluation/test_rubric_based_tool_use_quality_v1.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,67 @@ def test_format_auto_rater_prompt_with_basic_invocation(
7979
assert "<response>\nNo intermediate steps were taken.\n</response>" in prompt
8080

8181

82+
def test_format_auto_rater_prompt_with_invocation_rubrics_only():
83+
"""Tests prompt formatting when rubrics are defined on the invocation."""
84+
judge_model_options = JudgeModelOptions(
85+
judge_model_config=None,
86+
num_samples=3,
87+
)
88+
criterion = RubricsBasedCriterion(
89+
threshold=0.5, judge_model_options=judge_model_options
90+
)
91+
metric = EvalMetric(
92+
metric_name=PrebuiltMetrics.RUBRIC_BASED_TOOL_USE_QUALITY_V1.value,
93+
threshold=0.5,
94+
criterion=criterion,
95+
)
96+
evaluator = RubricBasedToolUseV1Evaluator(metric)
97+
invocation = Invocation(
98+
user_content=genai_types.Content(
99+
parts=[genai_types.Part(text="User input here.")]
100+
),
101+
rubrics=[
102+
Rubric(
103+
rubric_id="invocation-rubric",
104+
rubric_content=RubricContent(
105+
text_property="Did the agent use the lookup tool?"
106+
),
107+
type=RubricBasedToolUseV1Evaluator.RUBRIC_TYPE,
108+
)
109+
],
110+
)
111+
112+
prompt = evaluator.format_auto_rater_prompt(invocation, None)
113+
114+
assert "User input here." in prompt
115+
assert "Did the agent use the lookup tool?" in prompt
116+
117+
118+
def test_format_auto_rater_prompt_without_effective_rubrics_raises_error():
119+
"""Tests prompt formatting fails when no criterion or invocation rubrics exist."""
120+
judge_model_options = JudgeModelOptions(
121+
judge_model_config=None,
122+
num_samples=3,
123+
)
124+
criterion = RubricsBasedCriterion(
125+
threshold=0.5, judge_model_options=judge_model_options
126+
)
127+
metric = EvalMetric(
128+
metric_name=PrebuiltMetrics.RUBRIC_BASED_TOOL_USE_QUALITY_V1.value,
129+
threshold=0.5,
130+
criterion=criterion,
131+
)
132+
evaluator = RubricBasedToolUseV1Evaluator(metric)
133+
invocation = Invocation(
134+
user_content=genai_types.Content(
135+
parts=[genai_types.Part(text="User input here.")]
136+
),
137+
)
138+
139+
with pytest.raises(ValueError, match="Rubrics are required."):
140+
evaluator.format_auto_rater_prompt(invocation, None)
141+
142+
82143
def test_format_auto_rater_prompt_with_app_details(
83144
evaluator: RubricBasedToolUseV1Evaluator,
84145
):

0 commit comments

Comments
 (0)