ELBench-live / src /elbench /utils /parsing.py
adelevett's picture
Deploy ELBench-live dashboard
5db30ac verified
Raw History Blame Contribute Delete
7.62 kB
from __future__ import annotations
import ast
import json
import re
from difflib import SequenceMatcher
from typing import Any
def normalize_text(text: str | None) -> str:
if text is None:
return ""
return re.sub(r"\s+", " ", str(text)).strip().lower()
def extract_choice_letters(text: str | None) -> list[str]:
if not text:
return []
upper = str(text).upper()
letters = re.findall(r"\b([A-Z])\b", upper)
filtered = [letter for letter in letters if letter in {"A", "B", "C", "D", "E", "F", "G"}]
deduped = []
for letter in filtered:
if letter not in deduped:
deduped.append(letter)
return deduped
def extract_single_choice_answer(
text: str | None,
valid_letters: list[str],
*,
answer_prefixes: list[str],
) -> str | None:
if not text or not valid_letters or not answer_prefixes:
return None
lines = [line.strip() for line in str(text).splitlines() if line.strip()]
if not lines:
return None
valid_pattern = "".join(re.escape(letter) for letter in valid_letters)
candidate_lines = [_normalize_choice_line(line) for line in lines[-3:]]
normalized_prefixes = [_normalize_choice_line(prefix) for prefix in answer_prefixes if prefix]
for line in reversed(candidate_lines):
if not line:
continue
for prefix in normalized_prefixes:
if not prefix:
continue
match = re.fullmatch(
rf"{re.escape(prefix)}\s*[::]?\s*[\[\(οΌˆγ€γ€Œγ€Ž]?\s*([{valid_pattern}])\s*[\]\))】」』]?\s*[.γ€‚οΌŽ!!??]*",
line,
flags=re.I,
)
if match:
return match.group(1).upper()
loose_match = re.fullmatch(
rf"{re.escape(prefix)}\s*[::]?\s*[\[\(οΌˆγ€γ€Œγ€Ž]?\s*([{valid_pattern}])"
rf"(?:\s*[\]\))】」』]|[.γ€‚οΌŽγ€,,::\-]|\s+).+",
line,
flags=re.I,
)
if loose_match:
return loose_match.group(1).upper()
last_line = candidate_lines[-1]
fallback = re.fullmatch(
rf"[\[\(οΌˆγ€γ€Œγ€Ž]?\s*([{valid_pattern}])\s*[\]\))】」』]?\s*[.γ€‚οΌŽ!!??]*",
last_line,
flags=re.I,
)
if fallback:
return fallback.group(1).upper()
return None
def extract_choice_answer(
text: str | None,
valid_letters: list[str],
*,
answer_prefixes: list[str],
allow_multiple: bool = False,
) -> str | None:
if not allow_multiple:
return extract_single_choice_answer(text, valid_letters, answer_prefixes=answer_prefixes)
if not text or not valid_letters or not answer_prefixes:
return None
lines = [line.strip() for line in str(text).splitlines() if line.strip()]
if not lines:
return None
valid_pattern = "".join(re.escape(letter) for letter in valid_letters)
candidate_lines = [_normalize_choice_line(line) for line in lines[-3:]]
normalized_prefixes = [_normalize_choice_line(prefix) for prefix in answer_prefixes if prefix]
for line in reversed(candidate_lines):
if not line:
continue
for prefix in normalized_prefixes:
if not prefix:
continue
match = re.fullmatch(
rf"{re.escape(prefix)}\s*[::]?\s*[\[\(οΌˆγ€γ€Œγ€Ž]?\s*([{valid_pattern}](?:\s*[,οΌŒγ€/]\s*[{valid_pattern}]|\s*[{valid_pattern}])*)\s*[\]\))】」』]?\s*[.γ€‚οΌŸοΌ!?]*",
line,
flags=re.I,
)
if match:
return _normalize_choice_answer(match.group(1), valid_letters)
last_line = candidate_lines[-1]
fallback = re.fullmatch(
rf"[\[\(οΌˆγ€γ€Œγ€Ž]?\s*([{valid_pattern}](?:\s*[,οΌŒγ€/]\s*[{valid_pattern}]|\s*[{valid_pattern}])*)\s*[\]\))】」』]?\s*[.γ€‚οΌŸοΌ!?]*",
last_line,
flags=re.I,
)
if fallback:
return _normalize_choice_answer(fallback.group(1), valid_letters)
return None
def normalize_choice_answer(value: str | None, valid_letters: list[str]) -> str | None:
if value in (None, ""):
return None
return _normalize_choice_answer(str(value), valid_letters)
def _normalize_choice_answer(value: str, valid_letters: list[str]) -> str | None:
valid = {letter.upper() for letter in valid_letters}
letters = [letter for letter in re.findall(r"[A-Z]", value.upper()) if letter in valid]
if not letters:
return None
deduped: list[str] = []
for letter in letters:
if letter not in deduped:
deduped.append(letter)
return "".join(deduped)
def _normalize_choice_line(text: str) -> str:
normalized = str(text).translate(
str.maketrans(
{
":": ":",
"(": "(",
"οΌ‰": ")",
"【": "[",
"】": "]",
"οΌ»": "[",
"οΌ½": "]",
"γ€Œ": "[",
"」": "]",
"γ€Ž": "[",
"』": "]",
"。": ".",
".": ".",
",": ",",
}
)
)
normalized = re.sub(r"[*_`#>\-]+", " ", normalized)
normalized = re.sub(r"\s+", " ", normalized)
return normalized.strip()
def parse_score_value(value: Any) -> tuple[float | None, float | None]:
if value is None:
return None, None
text = str(value).strip()
if not text:
return None, None
if "/" in text:
left, right = text.split("/", 1)
try:
score = float(left.strip())
total = float(right.strip())
return score, total
except ValueError:
return None, None
try:
score = float(text)
return score, None
except ValueError:
return None, None
def text_similarity(left: str | None, right: str | None) -> float:
a = normalize_text(left)
b = normalize_text(right)
if not a and not b:
return 1.0
if not a or not b:
return 0.0
return SequenceMatcher(a=a, b=b).ratio()
def extract_json_object(text: str | None) -> dict[str, Any] | None:
if not text:
return None
candidate = str(text).strip()
fenced = re.findall(r"```(?:json)?\s*(\{.*?\})\s*```", candidate, flags=re.S | re.I)
if fenced:
candidate = fenced[0]
else:
match = re.search(r"\{.*\}", candidate, flags=re.S)
if match:
candidate = match.group(0)
for parser in (_try_json, _try_literal_eval):
parsed = parser(candidate)
if isinstance(parsed, dict):
return parsed
return None
def extract_required_json_keys(prompt: str) -> list[str]:
matches = re.findall(r'["\']([^"\']{1,80})["\']', prompt)
keys: list[str] = []
for match in matches:
normalized = match.strip()
if not normalized:
continue
if "\n" in normalized:
continue
if len(normalized) > 60:
continue
if normalized not in keys:
keys.append(normalized)
return keys
def _try_json(text: str) -> dict[str, Any] | None:
try:
parsed = json.loads(text)
return parsed if isinstance(parsed, dict) else None
except Exception:
return None
def _try_literal_eval(text: str) -> dict[str, Any] | None:
try:
parsed = ast.literal_eval(text)
return parsed if isinstance(parsed, dict) else None
except Exception:
return None