Spaces:
Running
Running
| 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} | |