|
28 | 28 | from google.adk.evaluation.rubric_based_evaluator import DefaultAutoRaterResponseParser |
29 | 29 | from google.adk.evaluation.rubric_based_evaluator import MajorityVotePerInvocationResultsAggregator |
30 | 30 | from google.adk.evaluation.rubric_based_evaluator import MeanInvocationResultsSummarizer |
| 31 | +from google.adk.evaluation.rubric_based_evaluator import _normalize_text |
31 | 32 | from google.adk.evaluation.rubric_based_evaluator import RubricBasedEvaluator |
32 | 33 | from google.adk.models.llm_response import LlmResponse |
33 | 34 | from google.genai import types as genai_types |
@@ -658,3 +659,115 @@ def test_create_effective_rubrics_filters_by_rubric_type( |
658 | 659 | "2", |
659 | 660 | "test_type_rubric", |
660 | 661 | } |
| 662 | + |
| 663 | + |
| 664 | +class TestNormalizeText: |
| 665 | + """Validate _normalize_text handles common judge-model garbling patterns.""" |
| 666 | + |
| 667 | + RUBRIC = "the response correctly uses tools" |
| 668 | + |
| 669 | + @pytest.mark.parametrize( |
| 670 | + "label,input_text", |
| 671 | + [ |
| 672 | + ("exact", "The response correctly uses tools"), |
| 673 | + ("markdown_bullet", "- The response correctly uses tools"), |
| 674 | + ("bullet_bold", "* **The response correctly uses tools**"), |
| 675 | + ( |
| 676 | + "smart_double_quotes", |
| 677 | + "“The response correctly uses tools”", |
| 678 | + ), |
| 679 | + ("double_spaces", "The response correctly uses tools"), |
| 680 | + ( |
| 681 | + "em_dash_prefix", |
| 682 | + "— The response correctly uses tools", |
| 683 | + ), |
| 684 | + ( |
| 685 | + "en_dash_prefix", |
| 686 | + "– The response correctly uses tools", |
| 687 | + ), |
| 688 | + ( |
| 689 | + "unicode_bullet", |
| 690 | + "• The response correctly uses tools", |
| 691 | + ), |
| 692 | + ( |
| 693 | + "leading_whitespace", |
| 694 | + " The response correctly uses tools", |
| 695 | + ), |
| 696 | + ], |
| 697 | + ) |
| 698 | + def test_garbled_text_matches_rubric(self, label, input_text): |
| 699 | + assert _normalize_text(input_text) == self.RUBRIC |
| 700 | + |
| 701 | + def test_ellipsis_normalized(self): |
| 702 | + assert ( |
| 703 | + _normalize_text("The response… uses tools") |
| 704 | + == "the response... uses tools" |
| 705 | + ) |
| 706 | + |
| 707 | + def test_accented_chars_preserved(self): |
| 708 | + assert _normalize_text("réponse") == "réponse" |
| 709 | + |
| 710 | + def test_non_string_returns_empty(self): |
| 711 | + assert _normalize_text(None) == "" |
| 712 | + assert _normalize_text(42) == "" |
| 713 | + |
| 714 | + def test_empty_string(self): |
| 715 | + assert _normalize_text("") == "" |
| 716 | + |
| 717 | + |
| 718 | +class TestSubstringFallbackUniquenessGuard: |
| 719 | + """Verify substring fallback only matches when exactly one candidate exists. |
| 720 | +
|
| 721 | + The convert_auto_rater_response_to_score method falls back to substring |
| 722 | + matching when exact normalized match fails. When multiple rubrics share |
| 723 | + a common substring, the fallback must reject the ambiguous match. |
| 724 | + """ |
| 725 | + |
| 726 | + def _build_evaluator_and_score(self, rubric_texts, judge_property_text): |
| 727 | + """Helper: build a FakeRubricBasedEvaluator and score a judge response.""" |
| 728 | + rubrics = [] |
| 729 | + for i, text in enumerate(rubric_texts): |
| 730 | + rubrics.append( |
| 731 | + Rubric( |
| 732 | + rubric_id=f"rubric_{i}", |
| 733 | + rubric_content=RubricContent(text_property=text), |
| 734 | + ) |
| 735 | + ) |
| 736 | + |
| 737 | + metric = EvalMetric( |
| 738 | + metric_name="test_metric", |
| 739 | + threshold=0.5, |
| 740 | + criterion=RubricsBasedCriterion(rubrics=rubrics, threshold=0.5), |
| 741 | + ) |
| 742 | + evaluator = FakeRubricBasedEvaluator(eval_metric=metric) |
| 743 | + evaluator.create_effective_rubrics_list([]) |
| 744 | + |
| 745 | + response_text = ( |
| 746 | + f"Property: {judge_property_text}\n" |
| 747 | + "Rationale: test rationale\n" |
| 748 | + "Verdict: yes" |
| 749 | + ) |
| 750 | + response = LlmResponse( |
| 751 | + content=genai_types.Content( |
| 752 | + parts=[genai_types.Part(text=response_text)] |
| 753 | + ) |
| 754 | + ) |
| 755 | + return evaluator.convert_auto_rater_response_to_score(response) |
| 756 | + |
| 757 | + def test_unique_substring_match_accepted(self): |
| 758 | + result = self._build_evaluator_and_score( |
| 759 | + rubric_texts=["Uses tools correctly"], |
| 760 | + judge_property_text="Uses tools correctly", |
| 761 | + ) |
| 762 | + assert len(result.rubric_scores) == 1 |
| 763 | + assert result.rubric_scores[0].rubric_id == "rubric_0" |
| 764 | + |
| 765 | + def test_ambiguous_substring_match_rejected(self): |
| 766 | + result = self._build_evaluator_and_score( |
| 767 | + rubric_texts=[ |
| 768 | + "Uses tools correctly", |
| 769 | + "Uses tools efficiently", |
| 770 | + ], |
| 771 | + judge_property_text="Uses tools", |
| 772 | + ) |
| 773 | + assert len(result.rubric_scores) == 0 |
0 commit comments