-
Notifications
You must be signed in to change notification settings - Fork 2.7k
fix(eval): Support non-English languages in response_match_score #3923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |||||||||||||||||||||||
| from google.adk.evaluation.eval_metrics import PrebuiltMetrics | ||||||||||||||||||||||||
| from google.adk.evaluation.evaluator import EvalStatus | ||||||||||||||||||||||||
| from google.adk.evaluation.final_response_match_v1 import _calculate_rouge_1_scores | ||||||||||||||||||||||||
| from google.adk.evaluation.final_response_match_v1 import _is_latin_script | ||||||||||||||||||||||||
| from google.adk.evaluation.final_response_match_v1 import RougeEvaluator | ||||||||||||||||||||||||
| from google.genai import types as genai_types | ||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||
|
|
@@ -147,3 +148,276 @@ def test_get_metric_info(): | |||||||||||||||||||||||
| assert metric_info.metric_name == PrebuiltMetrics.RESPONSE_MATCH_SCORE.value | ||||||||||||||||||||||||
| assert metric_info.metric_value_info.interval.min_value == 0.0 | ||||||||||||||||||||||||
| assert metric_info.metric_value_info.interval.max_value == 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Tests for _is_latin_script function | ||||||||||||||||||||||||
| class TestIsLatinScript: | ||||||||||||||||||||||||
| """Tests for the _is_latin_script helper function.""" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_empty_string(self): | ||||||||||||||||||||||||
| """Empty string should default to Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("") is True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_english_text(self): | ||||||||||||||||||||||||
| """English text should be detected as Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("Hello world") is True | ||||||||||||||||||||||||
| assert _is_latin_script("The quick brown fox") is True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_portuguese_text(self): | ||||||||||||||||||||||||
| """Portuguese with accents should be detected as Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("Olá, como você está?") is True | ||||||||||||||||||||||||
| assert _is_latin_script("São Paulo é uma cidade") is True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_french_text(self): | ||||||||||||||||||||||||
| """French with accents should be detected as Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("Bonjour, comment allez-vous?") is True | ||||||||||||||||||||||||
| assert _is_latin_script("français café résumé") is True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_german_text(self): | ||||||||||||||||||||||||
| """German with umlauts should be detected as Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("Guten Tag, wie geht es Ihnen?") is True | ||||||||||||||||||||||||
| assert _is_latin_script("Größe Übung Äpfel") is True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_thai_text(self): | ||||||||||||||||||||||||
| """Thai text should be detected as non-Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("สวัสดี") is False | ||||||||||||||||||||||||
| assert _is_latin_script("สวัสดีครับ") is False | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_chinese_text(self): | ||||||||||||||||||||||||
| """Chinese text should be detected as non-Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("你好") is False | ||||||||||||||||||||||||
| assert _is_latin_script("中文测试") is False | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_arabic_text(self): | ||||||||||||||||||||||||
| """Arabic text should be detected as non-Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("مرحبا") is False | ||||||||||||||||||||||||
| assert _is_latin_script("اللغة العربية") is False | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_japanese_text(self): | ||||||||||||||||||||||||
| """Japanese text should be detected as non-Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("こんにちは") is False | ||||||||||||||||||||||||
| assert _is_latin_script("日本語テスト") is False | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_korean_text(self): | ||||||||||||||||||||||||
| """Korean text should be detected as non-Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("안녕하세요") is False | ||||||||||||||||||||||||
| assert _is_latin_script("한국어 테스트") is False | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_numbers_only(self): | ||||||||||||||||||||||||
| """Numbers only should default to Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("12345") is True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_punctuation_only(self): | ||||||||||||||||||||||||
| """Punctuation only should default to Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("!@#$%") is True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_mixed_latin_dominant(self): | ||||||||||||||||||||||||
| """Mixed text with Latin dominant should be Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("Hello 你好 world test") is True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_mixed_non_latin_dominant(self): | ||||||||||||||||||||||||
| """Mixed text with non-Latin dominant should be non-Latin.""" | ||||||||||||||||||||||||
| assert _is_latin_script("你好世界 Hi") is False | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Tests for non-English language ROUGE scoring | ||||||||||||||||||||||||
| class TestNonEnglishRougeScoring: | ||||||||||||||||||||||||
| """Tests for ROUGE scoring with non-English languages (Issue #3111). | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| These tests verify that the fix for non-English languages works correctly. | ||||||||||||||||||||||||
| The key issue was that Porter stemmer only works for English, causing | ||||||||||||||||||||||||
| match failures for other languages. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # === Thai Language Tests (Original Issue #3111) === | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_thai_greeting_identical(self): | ||||||||||||||||||||||||
| """Thai: Identical greeting should have perfect score.""" | ||||||||||||||||||||||||
| # This is the exact case from Issue #3111 | ||||||||||||||||||||||||
| candidate = "สวัสดี" | ||||||||||||||||||||||||
| reference = "สวัสดี" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_thai_sentence_with_overlap(self): | ||||||||||||||||||||||||
| """Thai: Sentences with common words should show partial match.""" | ||||||||||||||||||||||||
| # "Hello, how are you today?" vs "Hello, how is the weather?" | ||||||||||||||||||||||||
| candidate = "สวัสดี คุณ สบายดี ไหม วันนี้" | ||||||||||||||||||||||||
| reference = "สวัสดี คุณ อากาศ เป็น อย่างไร" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "สวัสดี" and "คุณ" (2 out of 5 words each) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0 | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure < 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_thai_polite_particle_variation(self): | ||||||||||||||||||||||||
| """Thai: Same meaning with polite particle should show high match.""" | ||||||||||||||||||||||||
| # "Hello" vs "Hello (polite)" | ||||||||||||||||||||||||
| candidate = "สวัสดี ครับ" | ||||||||||||||||||||||||
| reference = "สวัสดี ค่ะ" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "สวัสดี" (1 out of 2 words) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == pytest.approx(0.5, rel=0.1) | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The assertion uses a relative tolerance
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # === Chinese Language Tests === | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_chinese_greeting_identical(self): | ||||||||||||||||||||||||
| """Chinese: Identical greeting should have perfect score.""" | ||||||||||||||||||||||||
| candidate = "你好世界" | ||||||||||||||||||||||||
| reference = "你好世界" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_chinese_sentence_with_overlap(self): | ||||||||||||||||||||||||
| """Chinese: Sentences with common words should show partial match.""" | ||||||||||||||||||||||||
| # Space-separated for tokenization | ||||||||||||||||||||||||
| candidate = "今天 天气 很好" # "Today's weather is good" | ||||||||||||||||||||||||
| reference = "今天 我 很 开心" # "Today I am happy" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "今天" and "很" | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0 | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure < 1.0 | ||||||||||||||||||||||||
|
Comment on lines
+273
to
+278
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment on line 276, To align with the comment's intent and create a stronger test, I suggest splitting
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_chinese_different_sentences(self): | ||||||||||||||||||||||||
| """Chinese: Completely different sentences should have zero score.""" | ||||||||||||||||||||||||
| candidate = "苹果 橙子 香蕉" # "Apple orange banana" | ||||||||||||||||||||||||
| reference = "汽车 飞机 火车" # "Car airplane train" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == 0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # === Arabic Language Tests === | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_arabic_greeting_identical(self): | ||||||||||||||||||||||||
| """Arabic: Identical greeting should have perfect score.""" | ||||||||||||||||||||||||
| candidate = "مرحبا بالعالم" | ||||||||||||||||||||||||
| reference = "مرحبا بالعالم" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_arabic_sentence_with_overlap(self): | ||||||||||||||||||||||||
| """Arabic: Sentences with common words should show partial match.""" | ||||||||||||||||||||||||
| candidate = "أنا أحب القراءة والكتابة" # "I love reading and writing" | ||||||||||||||||||||||||
| reference = "أنا أحب السفر والموسيقى" # "I love travel and music" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "أنا" and "أحب" | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0 | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure < 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # === Japanese Language Tests === | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_japanese_greeting_identical(self): | ||||||||||||||||||||||||
| """Japanese: Identical greeting should have perfect score.""" | ||||||||||||||||||||||||
| candidate = "こんにちは" | ||||||||||||||||||||||||
| reference = "こんにちは" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_japanese_sentence_with_overlap(self): | ||||||||||||||||||||||||
| """Japanese: Sentences with common words should show partial match.""" | ||||||||||||||||||||||||
| candidate = "今日 は 天気 が いい です" # "Today the weather is good" | ||||||||||||||||||||||||
| reference = "今日 は 仕事 が 忙しい です" # "Today work is busy" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "今日", "は", "が", "です" | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0.5 | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # === Korean Language Tests === | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_korean_greeting_identical(self): | ||||||||||||||||||||||||
| """Korean: Identical greeting should have perfect score.""" | ||||||||||||||||||||||||
| candidate = "안녕하세요" | ||||||||||||||||||||||||
| reference = "안녕하세요" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_korean_sentence_with_overlap(self): | ||||||||||||||||||||||||
| """Korean: Sentences with common words should show partial match.""" | ||||||||||||||||||||||||
| candidate = "오늘 날씨가 좋습니다" # "Today's weather is good" | ||||||||||||||||||||||||
| reference = "오늘 기분이 좋습니다" # "Today my mood is good" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "오늘" and "좋습니다" | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0 | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure < 1.0 | ||||||||||||||||||||||||
|
Comment on lines
+337
to
+338
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The assertions Calculation:
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # === European Languages (Latin script with accents) === | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_portuguese_sentence_identical(self): | ||||||||||||||||||||||||
| """Portuguese: Identical sentence with accents should match perfectly.""" | ||||||||||||||||||||||||
| candidate = "Olá, como você está hoje?" | ||||||||||||||||||||||||
| reference = "Olá, como você está hoje?" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_portuguese_sentence_with_overlap(self): | ||||||||||||||||||||||||
| """Portuguese: Sentences with common words should show partial match.""" | ||||||||||||||||||||||||
| candidate = "Eu gosto de programação e música" | ||||||||||||||||||||||||
| reference = "Eu gosto de viajar e cozinhar" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "Eu", "gosto", "de", "e" | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0.5 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_french_sentence_with_accents(self): | ||||||||||||||||||||||||
| """French: Accented characters should match correctly.""" | ||||||||||||||||||||||||
| candidate = "Où est la bibliothèque s'il vous plaît?" | ||||||||||||||||||||||||
| reference = "Où est la gare s'il vous plaît?" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match most words except "bibliothèque" vs "gare" | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0.7 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_german_sentence_with_umlauts(self): | ||||||||||||||||||||||||
| """German: Umlauts should be handled correctly.""" | ||||||||||||||||||||||||
| candidate = "Ich möchte ein Brötchen und Käse" | ||||||||||||||||||||||||
| reference = "Ich möchte ein Brötchen und Wurst" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match everything except "Käse" vs "Wurst" | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0.8 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_spanish_sentence_with_accents(self): | ||||||||||||||||||||||||
| """Spanish: Accented characters should match correctly.""" | ||||||||||||||||||||||||
| candidate = "¿Cómo estás? Estoy muy bien gracias" | ||||||||||||||||||||||||
| reference = "¿Cómo estás? Estoy cansado hoy" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "¿Cómo", "estás?", "Estoy" | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0.4 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # === English Stemming Verification === | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_english_stemming_running_vs_run(self): | ||||||||||||||||||||||||
| """English: Stemming should normalize 'running' to 'run'.""" | ||||||||||||||||||||||||
| candidate = "I am running fast" | ||||||||||||||||||||||||
| reference = "I am run fast" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # With stemming: "running" -> "run", perfect match | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_english_stemming_multiple_forms(self): | ||||||||||||||||||||||||
| """English: Multiple word forms should match via stemming.""" | ||||||||||||||||||||||||
| candidate = "The dogs are running and jumping happily" | ||||||||||||||||||||||||
| reference = "The dog is run and jump happy" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Stemming normalizes: dogs->dog, running->run, jumping->jump, happily->happi | ||||||||||||||||||||||||
| # Should have high overlap | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0.7 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_english_preserves_exact_matching(self): | ||||||||||||||||||||||||
| """English: Exact matches should still work perfectly.""" | ||||||||||||||||||||||||
| candidate = "The quick brown fox jumps over the lazy dog" | ||||||||||||||||||||||||
| reference = "The quick brown fox jumps over the lazy dog" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure == 1.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # === Mixed Script Edge Cases === | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_mixed_english_chinese(self): | ||||||||||||||||||||||||
| """Mixed: English and Chinese in same text.""" | ||||||||||||||||||||||||
| candidate = "Hello 世界 welcome to Python" | ||||||||||||||||||||||||
| reference = "Hello 世界 welcome to Java" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "Hello", "世界", "welcome", "to" (4 out of 5) | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0.7 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_mixed_with_numbers(self): | ||||||||||||||||||||||||
| """Mixed: Text with numbers should work correctly.""" | ||||||||||||||||||||||||
| candidate = "订单号 12345 已确认" | ||||||||||||||||||||||||
| reference = "订单号 12345 已发货" | ||||||||||||||||||||||||
| rouge_1_score = _calculate_rouge_1_scores(candidate, reference) | ||||||||||||||||||||||||
| # Should match "订单号" and "12345" | ||||||||||||||||||||||||
| assert rouge_1_score.fmeasure > 0.5 | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assertions
> 0and< 1.0are correct but not very precise. We can calculate the exact expected F-measure to make this test stronger. Given the candidate and reference texts, the F-measure should be exactly 0.4.Calculation: