Skip to content

Commit 4a2e6b8

Browse files
committed
fix: handle non-ASCII languages in ROUGE-1 eval tokenizer
The `_calculate_rouge_1_scores` function in `final_response_match_v1.py` uses Signed-off-by: Ishaan <ishaangupta0408@gmail.com>
1 parent 4cee97f commit 4a2e6b8

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

src/google/adk/evaluation/final_response_match_v1.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
import re
1718
from typing import Optional
1819

1920
from google.genai import types as genai_types
@@ -96,6 +97,31 @@ def _get_eval_status(score: float, threshold: float) -> EvalStatus:
9697
return EvalStatus.PASSED if score >= threshold else EvalStatus.FAILED
9798

9899

100+
def _contains_non_ascii(text: str) -> bool:
101+
"""Returns True if the text contains any non-ASCII characters."""
102+
return bool(re.search(r"[^\x00-\x7F]", text))
103+
104+
105+
class _UnicodeTokenizer:
106+
"""A tokenizer that handles non-ASCII text by splitting on whitespace and
107+
decomposing non-ASCII tokens into individual characters."""
108+
109+
def tokenize(self, text: str) -> list[str]:
110+
tokens = []
111+
for word in text.lower().split():
112+
if _contains_non_ascii(word):
113+
# For non-ASCII words (e.g. Thai, Chinese, Arabic), treat each
114+
# Unicode character as a separate token so that ROUGE overlap is
115+
# computed at the character level instead of being discarded entirely.
116+
tokens.extend(list(word))
117+
else:
118+
# Keep ASCII tokens as-is (drop purely punctuation-only tokens).
119+
cleaned = re.sub(r"[^a-z0-9]", "", word)
120+
if cleaned:
121+
tokens.append(cleaned)
122+
return tokens
123+
124+
99125
def _calculate_rouge_1_scores(candidate: str, reference: str):
100126
"""Calculates the ROUGE-1 score between a candidate and reference text.
101127
@@ -114,7 +140,14 @@ def _calculate_rouge_1_scores(candidate: str, reference: str):
114140
Returns:
115141
A dictionary containing the ROUGE-1 precision, recall, and f-measure.
116142
"""
117-
scorer = rouge_scorer.RougeScorer(["rouge1"], use_stemmer=True)
143+
# Use a Unicode-aware tokenizer when either text contains non-ASCII
144+
# characters (e.g. Thai, Chinese, Arabic) so that the default ASCII-only
145+
# tokenizer does not strip every token and return a spurious score of 0.
146+
if _contains_non_ascii(candidate) or _contains_non_ascii(reference):
147+
tokenizer = _UnicodeTokenizer()
148+
scorer = rouge_scorer.RougeScorer(["rouge1"], tokenizer=tokenizer)
149+
else:
150+
scorer = rouge_scorer.RougeScorer(["rouge1"], use_stemmer=True)
118151

119152
# The score method returns a dictionary where keys are the ROUGE types
120153
# and values are Score objects (tuples) with precision, recall, and fmeasure.

tests/unittests/evaluation/test_final_response_match_v1.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,22 @@ def test_calculate_rouge_1_scores():
8787
assert rouge_1_score.fmeasure == pytest.approx(8 / 11)
8888

8989

90+
def test_calculate_rouge_1_scores_non_ascii_identical():
91+
# Identical non-ASCII (Thai) text should yield a perfect score, not 0.
92+
candidate = "สวัสดี"
93+
reference = "สวัสดี"
94+
rouge_1_score = _calculate_rouge_1_scores(candidate, reference)
95+
assert rouge_1_score.fmeasure == pytest.approx(1.0)
96+
97+
98+
def test_calculate_rouge_1_scores_non_ascii_different():
99+
# Completely different non-ASCII text should yield a score of 0.
100+
candidate = "สวัสดี"
101+
reference = "ขอบคุณ"
102+
rouge_1_score = _calculate_rouge_1_scores(candidate, reference)
103+
assert rouge_1_score.fmeasure == pytest.approx(0.0)
104+
105+
90106
@pytest.mark.parametrize(
91107
"candidates, references, expected_score, expected_status",
92108
[

0 commit comments

Comments
 (0)