#!/usr/bin/env python3 """Standalone eval: OFFICIAL prompt (SpreadsheetBench original) on verified-400. Usage: python scripts/eval_prompt_official.py --workers 8 python scripts/eval_prompt_official.py --workers 32 --limit 20 """ from __future__ import annotations import argparse import glob import json import os import random import re import subprocess import sys import tempfile import textwrap import time import traceback from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError as FuturesTimeoutError import openpyxl _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) _PROJECT_ROOT = os.path.dirname(_SCRIPT_DIR) if _PROJECT_ROOT not in sys.path: sys.path.insert(0, _PROJECT_ROOT) from reflact.model import ( chat_messages_with_deployment, configure_azure_openai, set_backend, set_student_deployment, ) from reflact.envs.spreadsheetbench.evaluator import evaluate # ── Config ────────────────────────────────────────────────────────────────── DATA_ROOT = "/home/azureuser/workspace-yqh/sr/spreadsheetbench/data/spreadsheetbench_verified_400" JSONL_PATH = os.path.join(DATA_ROOT, "dataset.json") MODEL = "gpt-5-mini" # ── Official Prompt (from SpreadsheetBench src/prompt.py) ─────────────────── _SYSTEM_PROMPT = ( "You are an expert Python programmer specializing in spreadsheet manipulation. " "You will be given a user instruction together with a preview of an input .xlsx file. " "Your job is to write a single self-contained Python script that reads the input file " "at the path stored in the variable INPUT_PATH, performs the requested manipulation, " "and saves the result to OUTPUT_PATH. Use only the standard library, openpyxl, and pandas. " "Do not print anything. Do not use input(). Do not hardcode file paths. " "Return ONLY the Python code inside a single ```python ... ``` fenced block." ) def build_system(skill_content: str = "") -> str: base = _SYSTEM_PROMPT if skill_content.strip(): base += f"\n\n## Skill\n{skill_content.strip()}" return base def build_user(instruction, input_xlsx, instruction_type="", answer_position=""): try: preview = _preview_workbook(input_xlsx) except Exception as e: preview = f"(failed to preview: {e})" extra = "" if instruction_type: extra += f"\nInstruction type: {instruction_type}" if answer_position: extra += f"\nExpected answer position: {answer_position}" return ( f"# Instruction\n{instruction}\n{extra}\n\n" f"# Input spreadsheet preview\n{preview}\n\n" "# Task\n" "Write a Python script that reads the workbook from the variable `INPUT_PATH`, " "applies the instruction, and writes the modified workbook to `OUTPUT_PATH`. " "Preserve all other cells unchanged. " "The preview may be truncated — do not hardcode row counts or assume the data ends at the last previewed row; " "iterate over all actual rows in the workbook instead. " "Return only a ```python``` code block." ) # ── Shared utilities (identical to custom version) ────────────────────────── def _preview_workbook(path, max_rows=5, max_cols=20): wb = openpyxl.load_workbook(path, data_only=False) chunks = [] for sn in wb.sheetnames: ws = wb[sn] chunks.append(f"## Sheet: {sn} (dim={ws.dimensions}, max_row={ws.max_row}, max_col={ws.max_column})") for row in ws.iter_rows(min_row=1, max_row=min(ws.max_row, max_rows), max_col=min(ws.max_column, max_cols), values_only=False): cells = [] for c in row: v = c.value s = "" if v is None else str(v) if len(s) > 40: s = s[:37] + "..." cells.append(f"{c.coordinate}={s}") chunks.append(" | ".join(cells)) if ws.max_row > max_rows: chunks.append(f"... ({ws.max_row - max_rows} more rows)") chunks.append("") wb.close() return "\n".join(chunks) def extract_code(text): if "```" not in text: return text.strip() start = text.find("```") nl = text.find("\n", start) end = text.find("```", nl + 1) if nl == -1 or end == -1: return text.strip() return text[nl + 1:end].strip() _PATH_RE = re.compile(r'^\s*(INPUT_PATH|OUTPUT_PATH)\s*=\s*.+$', re.MULTILINE) def strip_paths(code): return _PATH_RE.sub("", code) RUNNER_TEMPLATE = textwrap.dedent(""" import os, sys, traceback INPUT_PATH = {input_path!r} OUTPUT_PATH = {output_path!r} try: {code_indented} except Exception: traceback.print_exc() sys.exit(2) """) def run_code(code, input_path, output_path, timeout=120): os.makedirs(os.path.dirname(output_path), exist_ok=True) cleaned = strip_paths(code) indented = textwrap.indent(cleaned, " ") script = RUNNER_TEMPLATE.format(input_path=input_path, output_path=output_path, code_indented=indented) with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as f: f.write(script) tmp = f.name try: proc = subprocess.run([sys.executable, tmp], capture_output=True, text=True, timeout=timeout) if proc.returncode != 0: return False, (proc.stdout + "\n" + proc.stderr).strip() if not os.path.exists(output_path): return False, "output file was not created" return True, "" except subprocess.TimeoutExpired: return False, f"timeout after {timeout}s" finally: try: os.unlink(tmp) except OSError: pass def find_test_cases(task_dir): cases = [] for ip in sorted(glob.glob(os.path.join(task_dir, "*_input.xlsx"))): no = os.path.basename(ip).split("_", 1)[0] ap = ip.replace("_input.xlsx", "_answer.xlsx") if os.path.exists(ap): cases.append((no, ip, ap)) for ip in sorted(glob.glob(os.path.join(task_dir, "*_init.xlsx"))): no = os.path.basename(ip).split("_", 1)[0] ap = ip.replace("_init.xlsx", "_golden.xlsx") if os.path.exists(ap): cases.append((no, ip, ap)) if not cases: bare_init = os.path.join(task_dir, "initial.xlsx") bare_gold = os.path.join(task_dir, "golden.xlsx") if os.path.exists(bare_init) and os.path.exists(bare_gold): cases.append(("1", bare_init, bare_gold)) return cases def load_items(path): if path.endswith(".json"): with open(path) as f: data = json.load(f) if isinstance(data, dict): data = data.get("data") or list(data.values()) return list(data) items = [] with open(path) as f: for line in f: line = line.strip() if line: items.append(json.loads(line)) return items # ── LLM call ──────────────────────────────────────────────────────────────── def llm_call(messages, deployment, max_tokens=16384, retries=5, llm_timeout=120): raw, _ = chat_messages_with_deployment( deployment=deployment, messages=messages, max_completion_tokens=max_tokens, retries=retries, stage="rollout", timeout=llm_timeout, ) return str(raw or "") # ── Process one task ──────────────────────────────────────────────────────── def process_one(item, data_root, out_root, model): task_id = str(item["id"]) instruction = item["instruction"] instruction_type = item.get("instruction_type", "") answer_position = item.get("answer_position", "") answer_sheet = item.get("answer_sheet", "") if answer_position and answer_sheet and "!" not in answer_position: answer_position = f"{answer_sheet}!{answer_position}" sp = item.get("spreadsheet_path", f"spreadsheet/{task_id}") task_dir = sp if os.path.isabs(sp) else os.path.join(data_root, sp) result = {"id": task_id, "ok": False, "hard": 0, "soft": 0.0, "n_cases": 0, "n_pass": 0, "fail_reason": "", "error": ""} try: cases = find_test_cases(task_dir) result["n_cases"] = len(cases) if not cases: result["fail_reason"] = "no-test-cases" return result task_out = os.path.join(out_root, "predictions", task_id) os.makedirs(task_out, exist_ok=True) # LLM call system = build_system("") user = build_user(instruction, cases[0][1], instruction_type, answer_position) messages = [{"role": "system", "content": system}, {"role": "user", "content": user}] raw = llm_call(messages, model) time.sleep(3) code = extract_code(raw) with open(os.path.join(task_out, "code.py"), "w") as f: f.write(code) with open(os.path.join(task_out, "raw.txt"), "w") as f: f.write(raw) if not code.strip(): result["fail_reason"] = "empty-code" return result # Execute + evaluate each test case for no, ip, ap in cases: pred = os.path.join(task_out, f"{no}_pred.xlsx") ok_exec, err = run_code(code, ip, pred) if not ok_exec: if not result["fail_reason"]: result["fail_reason"] = f"exec: {err[:200]}" continue try: ev = evaluate(pred, ap, instruction_type, answer_position) except Exception as e: ev = {"ok": False, "reason": str(e)} if ev["ok"]: result["n_pass"] += 1 nc, np = result["n_cases"], result["n_pass"] result["soft"] = np / nc if nc else 0.0 result["hard"] = 1 if nc > 0 and np == nc else 0 result["ok"] = bool(result["hard"]) if result["ok"]: result["fail_reason"] = "" return result except Exception as e: result["fail_reason"] = f"unexpected: {e}" result["error"] = traceback.format_exc() return result # ── Main ──────────────────────────────────────────────────────────────────── def main(): ap = argparse.ArgumentParser(description="Eval OFFICIAL prompt on verified-400") ap.add_argument("--model", default=MODEL) ap.add_argument("--backend", choices=["azure_openai", "codex", "claude"], default="azure_openai") ap.add_argument("--azure_endpoint", default="") ap.add_argument("--azure_api_version", default="") ap.add_argument("--azure_api_key", default="") ap.add_argument("--workers", type=int, default=8) ap.add_argument("--limit", type=int, default=0) ap.add_argument("--out_root", default="") args = ap.parse_args() set_backend(args.backend) configure_azure_openai( endpoint=args.azure_endpoint or None, api_version=args.azure_api_version or None, api_key=args.azure_api_key or None, ) set_student_deployment(args.model) ts = time.strftime("%Y%m%d_%H%M%S") out_root = args.out_root or os.path.join(_PROJECT_ROOT, "outputs", f"prompt_official_{args.model}_{ts}") out_root = os.path.abspath(out_root) os.makedirs(out_root, exist_ok=True) items = load_items(JSONL_PATH) if args.limit: items = items[:args.limit] print(f"{'='*60}") print(f" Prompt: OFFICIAL (SpreadsheetBench original)") print(f" Model: {args.model}") print(f" Items: {len(items)}") print(f" Output: {out_root}") print(f"{'='*60}") t0 = time.time() results = [] with ThreadPoolExecutor(max_workers=args.workers) as ex: futs = {ex.submit(process_one, it, DATA_ROOT, out_root, args.model): it for it in items} for i, fut in enumerate(as_completed(futs), 1): item = futs[fut] try: res = fut.result(timeout=300) except FuturesTimeoutError: res = {"id": str(item["id"]), "ok": False, "hard": 0, "soft": 0.0, "n_cases": 0, "n_pass": 0, "fail_reason": "timeout"} except Exception as e: res = {"id": str(item["id"]), "ok": False, "hard": 0, "soft": 0.0, "n_cases": 0, "n_pass": 0, "fail_reason": str(e)} results.append(res) status = "PASS" if res.get("hard") else "FAIL" dt = time.time() - t0 print(f" {i}/{len(items)} id={res['id']:<10} {status} cases={res.get('n_pass',0)}/{res.get('n_cases',0)} dt={dt:.0f}s") # Summary hard_sum = sum(r.get("hard", 0) for r in results) soft_sum = sum(r.get("soft", 0.0) for r in results) n = len(results) print(f"\n{'='*60}") print(f" OFFICIAL prompt: hard={hard_sum}/{n}={hard_sum/n:.4f} soft={soft_sum/n:.4f}") print(f"{'='*60}") with open(os.path.join(out_root, "results.jsonl"), "w") as f: for r in results: f.write(json.dumps(r, ensure_ascii=False) + "\n") with open(os.path.join(out_root, "summary.json"), "w") as f: json.dump({"prompt": "official", "model": args.model, "n": n, "hard": hard_sum/n, "soft": soft_sum/n}, f, indent=2) if __name__ == "__main__": main()