testspace / evals /src /evals_verifier.py
nilshoehing's picture
Upload TopoBench Space app
67acd34 verified
Raw
History Blame Contribute Delete
11.5 kB
from __future__ import annotations
import ast
import importlib.util
import json
import re
import sys
from pathlib import Path
from typing import Any
import pandas as pd
from json_repair import repair_json
from evals import load_json, load_jsonl, write_json
from puzzles import MAPPINGS_DIR, REPORTS_DIR, SUBMODULES_DIR, PUZZLES, build_row_lookup, load_dataset_frame
RLP_PATH = SUBMODULES_DIR / "rlp"
FLOWFREE_PATH = SUBMODULES_DIR / "flowfree"
sys.path.insert(0, str(RLP_PATH))
from rlp.ascii_parser import ( # type: ignore
check_bridges_structural_validity,
check_galaxies_structural_validity,
check_undead_structural_validity,
)
from rlp.puzzle import Puzzle as RLPPuzzle # type: ignore
from verifier import verify_ascii_state # type: ignore
MAPPING_FILES = {
"bridges": "bridges.json",
"flow_free": "flow_free.json",
"galaxies": "galaxies.json",
"loopy": "loopy.json",
"pattern": "pattern.json",
"undead": "undead.json",
}
_mapping_cache: dict[str, dict[str, str]] = {}
_puzzle_cache: dict[tuple[str, str], RLPPuzzle] = {}
def _flowfree_verifier_module():
spec = importlib.util.spec_from_file_location(
"flowfree_verifier",
str(FLOWFREE_PATH / "verifier.py"),
)
if spec is None or spec.loader is None:
raise RuntimeError("Could not load FlowFree verifier module.")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def load_mapping(puzzle_type: str) -> dict[str, str]:
if puzzle_type not in _mapping_cache:
payload = json.loads(
(MAPPINGS_DIR / MAPPING_FILES[puzzle_type]).read_text(encoding="utf-8")
)
_mapping_cache[puzzle_type] = payload["char_map"]
return _mapping_cache[puzzle_type]
def decode_intformat_board(encoded_board: str, puzzle_type: str) -> str:
reverse_map = {value: key for key, value in load_mapping(puzzle_type).items()}
rows = []
for line in encoded_board.strip().splitlines():
cells = [cell.strip() for cell in line.split(",")]
rows.append("".join(reverse_map.get(cell, "?") for cell in cells))
return "\n".join(rows)
def decode_intformat_json_board(encoded_board: str, puzzle_type: str) -> str:
reverse_map = {value: key for key, value in load_mapping(puzzle_type).items()}
try:
grid = json.loads(encoded_board)
except json.JSONDecodeError:
try:
grid = ast.literal_eval(encoded_board)
except (SyntaxError, ValueError):
return encoded_board
if not isinstance(grid, list):
return encoded_board
rows = []
for row in grid:
if not isinstance(row, list):
return encoded_board
rows.append("".join(reverse_map.get(str(cell), "?") for cell in row))
return "\n".join(rows)
def extract_board(response_text: str) -> str | None:
if not response_text:
return None
candidates: list[str] = []
stripped = response_text.strip()
if stripped.startswith("{") or stripped.startswith("["):
candidates.append(stripped)
fenced = re.findall(r"```(?:json)?\s*(.*?)\s*```", response_text, re.DOTALL)
candidates.extend(fenced)
object_matches = re.findall(r"(\{.*?\})", response_text, re.DOTALL)
candidates.extend(object_matches)
for candidate in reversed(candidates):
try:
fixed = repair_json(candidate)
parsed = json.loads(fixed)
except Exception:
continue
if isinstance(parsed, dict) and "response" in parsed:
value = parsed["response"]
return value if isinstance(value, str) else json.dumps(value)
if isinstance(parsed, list):
return json.dumps(parsed)
return None
def verify_flow_free(problem_ascii: str, extracted_board: str) -> dict[str, Any]:
verifier = _flowfree_verifier_module()
solver_src = str(FLOWFREE_PATH / "flowfree_all_solutions.c")
solver_bin = str(FLOWFREE_PATH / "flowfree_all_solutions")
result = {
"board_exists": bool(extracted_board),
"board_valid": False,
"board_modified": False,
"correct": False,
}
if not extracted_board:
return result
is_valid, _ = verifier.verify_solution(
problem_ascii,
extracted_board,
solver_src=solver_src,
solver_bin=solver_bin,
print_solutions=False,
)
result["correct"] = bool(is_valid)
problem_lines = [line for line in problem_ascii.strip().splitlines() if line]
solution_lines = [line for line in extracted_board.strip().splitlines() if line]
if len(problem_lines) == len(solution_lines) and problem_lines:
result["board_valid"] = all(
len(problem_row) == len(solution_row)
for problem_row, solution_row in zip(problem_lines, solution_lines)
)
modified = False
for problem_row, solution_row in zip(problem_lines, solution_lines):
for start_cell, end_cell in zip(problem_row, solution_row):
if start_cell != "." and start_cell != end_cell:
modified = True
break
if modified:
break
result["board_modified"] = modified
return result
def get_or_create_rlp_puzzle(puzzle_type: str, args: str) -> RLPPuzzle:
cache_key = (puzzle_type, args)
if cache_key not in _puzzle_cache:
try:
puzzle = RLPPuzzle(puzzle_type, arg=args, headless=True)
except OSError as exc:
raise RuntimeError(
"RLP verifier libraries are not built yet. "
"Run 'bash scripts/install.sh' first."
) from exc
puzzle.new_game()
_puzzle_cache[cache_key] = puzzle
return _puzzle_cache[cache_key]
def verify_with_rlp(
*,
puzzle_type: str,
problem_ascii: str,
extracted_board: str,
args: str,
) -> dict[str, Any]:
result = {
"board_exists": bool(extracted_board),
"board_valid": False,
"board_modified": False,
"correct": False,
}
if not extracted_board:
return result
if puzzle_type == "bridges":
result["board_valid"] = bool(
check_bridges_structural_validity(extracted_board, problem_ascii)
)
elif puzzle_type == "galaxies":
result["board_valid"] = bool(
check_galaxies_structural_validity(extracted_board, problem_ascii)
)
elif puzzle_type == "undead":
result["board_valid"] = bool(check_undead_structural_validity(extracted_board))
else:
result["board_valid"] = True
if not result["board_valid"]:
result["board_modified"] = True
return result
puzzle = get_or_create_rlp_puzzle(puzzle_type, args)
result["correct"] = (
verify_ascii_state(
puzzle,
extracted_board,
problem_ascii=problem_ascii,
)
== "SOLVED"
)
return result
def decode_board_for_variant(board: str | None, variant: str, puzzle_type: str) -> str | None:
if board is None:
return None
if variant == "plain":
return board
if variant == "intformat":
return decode_intformat_board(board, puzzle_type)
if variant == "intformat_json":
return decode_intformat_json_board(board, puzzle_type)
raise ValueError(f"Unknown variant '{variant}'")
def summarize(frame: pd.DataFrame) -> pd.DataFrame:
summary = (
frame.groupby(["provider", "model", "variant", "puzzlename", "difficulty"], dropna=False)
.agg(
total=("filename", "count"),
board_exists=("board_exists", "sum"),
board_valid=("board_valid", "sum"),
correct=("correct", "sum"),
avg_output_tokens=("output_tokens", "mean"),
avg_total_tokens=("total_tokens", "mean"),
)
.reset_index()
)
summary["accuracy"] = summary["correct"] / summary["total"]
return summary.sort_values(
["puzzlename", "difficulty", "provider", "model"]
).reset_index(drop=True)
def verify_run(run_dir: Path) -> dict[str, Path]:
manifest = load_json(run_dir / "manifest.json")
records = load_jsonl(run_dir / "responses.jsonl")
variant = manifest["variant"]
puzzle_frames = {
key: load_dataset_frame(
spec,
variant=variant,
difficulty="all",
limit=None,
)
for key, spec in PUZZLES.items()
}
row_lookups = {
key: build_row_lookup(frame)
for key, frame in puzzle_frames.items()
}
detail_rows: list[dict[str, Any]] = []
for record in records:
puzzlename = record["puzzlename"]
spec = PUZZLES[puzzlename]
row = row_lookups[puzzlename][record["filename"]]
extracted_board = extract_board(record.get("response_text", ""))
decoded_problem = decode_board_for_variant(
row["problem"],
variant,
spec.verifier_type,
)
decoded_board = decode_board_for_variant(
extracted_board,
variant,
spec.verifier_type,
)
args = row.get("args") or record.get("args") or spec.default_args
if spec.verifier_type == "flow_free":
verification = verify_flow_free(decoded_problem or "", decoded_board or "")
else:
verification = verify_with_rlp(
puzzle_type=spec.verifier_type,
problem_ascii=decoded_problem or "",
extracted_board=decoded_board or "",
args=args,
)
detail_rows.append(
{
"filename": record["filename"],
"puzzlename": puzzlename,
"difficulty": row["difficulty"],
"provider": record["provider"],
"model": record["model"],
"variant": variant,
"args": args,
"prompt_file": record["prompt_file"],
"board_exists": verification["board_exists"],
"board_valid": verification["board_valid"],
"board_modified": verification["board_modified"],
"correct": verification["correct"],
"input_tokens": record.get("input_tokens"),
"output_tokens": record.get("output_tokens"),
"total_tokens": record.get("total_tokens"),
"response_text": record.get("response_text", ""),
"decoded_problem": decoded_problem,
"decoded_board": decoded_board,
}
)
detail_frame = pd.DataFrame(detail_rows)
summary_frame = summarize(detail_frame)
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
detail_csv = REPORTS_DIR / f"{run_dir.name}_details.csv"
summary_csv = REPORTS_DIR / f"{run_dir.name}_summary.csv"
detail_frame.to_csv(detail_csv, index=False)
summary_frame.to_csv(summary_csv, index=False)
write_json(
REPORTS_DIR / f"{run_dir.name}_summary.json",
{
"run_dir": str(run_dir),
"rows": len(detail_frame),
"summary_rows": len(summary_frame),
},
)
print(summary_frame.to_string(index=False))
print(f"\nDetailed CSV: {detail_csv}")
print(f"Summary CSV: {summary_csv}")
return {"detail_csv": detail_csv, "summary_csv": summary_csv}