bureaucat / eval /grounded.py
ravinsingh15's picture
Bureaucat β€” Build Small Hackathon submission (Qwen3-VL-8B, ZeroGPU, gr.Server)
6b5e47d
Raw
History Blame Contribute Delete
6.03 kB
"""
eval/grounded.py β€” Shared grounded matching primitives for Bureaucat.
Provides the no-invention check used by app.py (to drive the verifying mascot
state, D2-05) and by eval/run_eval.py (for bake-off accuracy gating).
Stdlib-only: only `re` and `unicodedata`. Importing this module never loads
model weights and never imports app.
"""
# ---------------------------------------------------------------------------
# Imports β€” stdlib only
# ---------------------------------------------------------------------------
import re
import unicodedata
# ---------------------------------------------------------------------------
# Normalization (D-12)
# Python 3 \s on str matches all Unicode whitespace:
# U+00A0 non-breaking space, U+2009 thin space, U+202F narrow no-break space,
# U+2007 figure space, U+3000 ideographic space β€” no literal chars needed.
# ---------------------------------------------------------------------------
_WHITESPACE_RE = re.compile(r"\s+")
def normalize(text: str) -> str:
"""
Collapse all Unicode whitespace to single ASCII space; NFC-normalize; lowercase.
Preserves digits, commas, periods (Swedish decimal format), and kr suffix.
Purpose: makes OCR whitespace/case variance transparent to value_found().
"""
# NFC normalize first β€” handles combining characters from OCR output
text = unicodedata.normalize("NFC", text)
# Collapse all Unicode whitespace variants to a single ASCII space
text = _WHITESPACE_RE.sub(" ", text)
# Lowercase and strip leading/trailing whitespace
return text.lower().strip()
# ---------------------------------------------------------------------------
# Value matching (D-12 no-invention, D-13 recall)
# ---------------------------------------------------------------------------
def value_found(gold_value: str, haystack: str) -> bool:
"""
Check if gold_value appears in haystack using normalized substring match
with non-digit boundaries on both sides.
The digit boundary (`(?<!\\d)...(?!\\d)`) prevents a gold value from matching
INSIDE a longer run of digits β€” both for short refs (e.g. "123" inside
"1234567") AND for long values (e.g. ref "12-345678" must NOT match inside
"912-3456789", and ISO date "2026-06-15" must NOT match inside "12026-06-15").
Applying the boundary universally (not only when <6 digits) closes the
superstring hole that previously let a garbled longer number satisfy the
no-invention / recall gate (WR-01, D-12).
Legitimate values are always delimited by whitespace/punctuation in the
model's output, so the boundary never rejects a real match.
"""
norm_gold = normalize(gold_value)
norm_hay = normalize(haystack)
pattern = r"(?<!\d)" + re.escape(norm_gold) + r"(?!\d)"
return bool(re.search(pattern, norm_hay))
# ---------------------------------------------------------------------------
# Value extraction from "Deadlines & money" section (D-12 no-invention)
# ---------------------------------------------------------------------------
def extract_values_from_section(section_text: str) -> list:
"""
Extract emitted verbatim values from the "Deadlines & money" section.
Exploits the locked prompt's "one item per line, verbatim value first" format:
- 15 juni 2026 β€” last day to file
- 1 234 kr - belopp
For each line:
1. Strip leading bullet marker ('- ' or '* ' or bare '-' at start) and whitespace.
2. Skip blank lines and any "None found" lines.
3. Split on the first interpretation separator: ' β€” ' (em-dash) or ' - ' (hyphen
with surrounding spaces). The surrounding-spaces requirement prevents the
internal hyphens in ISO dates (e.g., 2026-06-15) from being split.
4. The text before the separator is the verbatim value; strip it.
Returns a list of verbatim value strings. Empty list if nothing matches.
"""
values = []
for raw_line in section_text.splitlines():
line = raw_line.strip()
if not line:
continue
# Strip leading bullet markers (-, *, β€’) with optional trailing space
line = re.sub(r"^[-*β€’]\s*", "", line).strip()
if not line:
continue
# Skip "None found" variants (case-insensitive)
if re.match(r"none found\.?$", line, re.IGNORECASE):
continue
# Split on first interpretation separator:
# ' β€” ' (em-dash, U+2014, with surrounding spaces)
# ' – ' (en-dash, U+2013, with surrounding spaces β€” LLMs emit this constantly)
# ' - ' (hyphen with surrounding spaces β€” requires spaces to avoid splitting dates)
# The surrounding-spaces requirement keeps internal ISO-date hyphens intact.
sep_match = re.search(r"\s+[—–\-]\s+", line)
if sep_match:
verbatim = line[: sep_match.start()].strip()
else:
# No separator found β€” the whole line is the verbatim value
verbatim = line
if verbatim:
values.append(verbatim)
return values
# ---------------------------------------------------------------------------
# No-invention check (D-12, D2-05 verifying mascot state)
# ---------------------------------------------------------------------------
def check_no_invention(result) -> list:
"""
Returns list of values from result.deadlines that are NOT found verbatim
in result.transcription (the no-invention check, D-12).
Called from app.py to drive the verifying mascot state (D2-05).
Does NOT require a gold dict β€” safe to call on any real user letter.
Returns empty list = no invention detected.
Works on any SimpleNamespace or StructuredResult that has .deadlines
and .transcription attributes.
"""
invented = []
for emitted_val in extract_values_from_section(result.deadlines):
if not value_found(emitted_val, result.transcription):
invented.append(emitted_val)
return invented