From 76105fbaaea163c075b6c7bab09e74e781768474 Mon Sep 17 00:00:00 2001 From: Attila <29815676+arthapraha@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:47:38 +0100 Subject: [PATCH] fix(scorers): a digit inside a name is not a count _extract_count_from_text accepted any answer containing exactly one number, so "Chiller 6" parsed as 6 -- and so did "Boiler 6". Two differently-named assets then compared equal on a digit that happened to be inside a name, and a wrong asset scored a perfect strict match while a different wrong asset scored zero. The docstring already states the intent: count-only or nearly count-only. This enforces it. Once the number and the usual count wording are removed, any remaining word means the number was part of a phrase -- most importantly, part of a name. "The count is 6" and "There are 5 assets" still parse. "Chiller 6" no longer does. Note for reviewers: 6 tests in evaluation/tests/ already fail on main at e11d1c1 (test_car_metadata_*, test_static_json_scorer_uses_car_metadata_score). This change does not touch them; the suite goes from 6 failed / 90 passed to 6 failed / 92 passed. Co-Authored-By: Claude Signed-off-by: Attila <29815676+arthapraha@users.noreply.github.com> --- src/evaluation/scorers/static_json.py | 29 ++++++++++++++++++- .../tests/test_static_json_scorer.py | 22 ++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/evaluation/scorers/static_json.py b/src/evaluation/scorers/static_json.py index 7afa9d56..5d377297 100644 --- a/src/evaluation/scorers/static_json.py +++ b/src/evaluation/scorers/static_json.py @@ -209,6 +209,16 @@ def _parse_json_or_python(content: str) -> Any: return _PARSE_MISSING +# Wording that may surround a bare count without making the answer something +# other than a count. Anything else left over means the number was part of a +# phrase -- most importantly, part of a name. +_COUNT_WORDS = re.compile( + r"\b(?:the|a|an|final|answer|count|result|total|is|are|there|of|" + r"assets?|items?|records?|rows?|entries)\b", + flags=re.IGNORECASE, +) + + def _extract_count_from_text(content: str) -> int | float | None: """Extract a count when the answer is count-only or nearly count-only.""" stripped = content.strip() @@ -225,7 +235,24 @@ def _extract_count_from_text(content: str) -> int | float | None: ) if len(numbers) == 1: number = numbers[0] - return float(number) if "." in number else int(number) + # A digit inside a name is not a count. + # + # This function is documented as accepting answers that are "count-only + # or nearly count-only", but a single number anywhere in the text + # satisfied that test. So "Chiller 6" parsed as 6 -- and so did + # "Boiler 6", making two differently-named assets compare equal on a + # digit that happened to be inside a name. + # + # Require the residue, once the number and the usual count wording are + # removed, to contain no remaining words: that is what "nearly + # count-only" means. "The count is 6" still parses; "Chiller 6" no + # longer does. + residue = re.sub( + r"(?