Map `eval/lib/grader.py`'s judge output onto the EvalPort interchange format (portable ResultSet for WorldVQA/SimpleQA/exact-match)
eval/lib/grader.py already produces something very close to a portable eval-results format — it's just not written to disk in one. I maintain EvalPort (Apache-2.0), an open interchange spec + Python/TS SDK for LLM eval test cases, graders, and results, and I think there's a genuine, low-effort fit here. Posting as a proposal/question, not a PR — happy to build it if there's interest, or take it entirely to the EvalPort side if you'd rather this repo stay judge-only.
What grade_file already has
Reading the real code (not paraphrasing):
grade_file(task, path, grader_model=DEFAULT_GRADER_MODEL, concurrency=16, llm_judge=False)readsrows = [json.loads(l) for l in open(path)], each row carryingproblem,original_data, andfinal_response.- For WorldVQA-family tasks (
WORLDVQA_TASKS),build_ground_truth(task, original_data)produces the reference string (e.g."Any of: " + " | ".join(refs)forencyclopedic_vqa), andJUDGE_WORLDQA_PROMPT_EN.format(question=..., model_answer=..., ground_truth_answer=...)builds the judge prompt. - Each row's verdict comes from
parse_label(judge_text)→ one of"correct" | "incorrect" | "unattempted". grade_filereturns exactly this shape:{ "task": task, "file": path, "n": n, "correct": c, "incorrect": inc, "unattempted": una, "errors": len(errs), "score": c / n if n else 0.0, }- SimpleQA tasks go through the same per-row loop but with
SIMPLEQA_GRADER_TEMPLATEand an A/B/C→correct/incorrect/unattempted mapping;nq/nq_tables/triviaqausegrade_exact_match, which returns the identical dict shape via string normalization instead of an LLM call.
That per-row verdict + aggregate dict is basically EvalPort's Result/ResultSet shape already, just not serialized as one.
The mapping
grader.py |
EvalPort |
|---|---|
one row (problem, original_data, final_response) |
TestCase (input = problem, expected_output = build_ground_truth(...)) |
JUDGE_WORLDQA_PROMPT_EN / SIMPLEQA_GRADER_TEMPLATE |
Grader (type: "llm_judge", the real prompt template in params.prompt_template, params.model = grader_model) |
parse_label(...) verdict for one row |
Result.grader_results[0] — passed = (label == "correct"), with the raw correct/incorrect/unattempted label kept in metadata since EvalPort's passed is boolean and would otherwise collapse "unattempted" into "failed" |
grade_exact_match's per-row is_exact_match(...) |
Grader(type="exact_match") — EvalPort already has this as a zero-config native type, no mapping loss |
the aggregate dict grade_file returns |
ResultSet.metadata.pixelrag.aggregate — preserved verbatim, same convention the adapters below use, so nothing about the paper's own scoring (score = correct/n) is recomputed or reinterpreted |
Sketch (real field names, not illustrative pseudo-fields)
from openeval.validate import validate_suite, validate_result_set
def row_to_test_case(task: str, i: int, row: dict) -> dict:
od = row.get("original_data", {})
return {
"id": f"{task}_{i}",
"input": row.get("problem", ""),
"expected_output": build_ground_truth(task, od), # from lib/grader.py, unmodified
"graders": ["worldvqa_judge"],
}
def row_to_result(task: str, i: int, row: dict, label: str) -> dict:
return {
"test_case_id": f"{task}_{i}",
"grader_results": [{
"grader_id": "worldvqa_judge",
"score": {"correct": 1.0, "incorrect": 0.0, "unattempted": 0.0}[label],
"passed": label == "correct",
"metadata": {"pixelrag_label": label}, # keeps "unattempted" distinguishable from "failed"
}],
"passed": label == "correct",
}
def result_set_from_grade_file(res: dict, rows: list[dict], labels: list[str], suite_id: str, run_id: str) -> dict:
# res is grade_file()'s own return value — untouched.
return {
"version": "1.0.0",
"suite_id": suite_id,
"run_id": run_id,
"results": [row_to_result(res["task"], i, r, l) for i, (r, l) in enumerate(zip(rows, labels))],
"metadata": {"pixelrag": {"aggregate": res}}, # grade_file's dict, verbatim
}grader.py would need to hand back per-row labels alongside the aggregate (right now labels is a local in grade_file and only the aggregate is returned/printed) — a one-line change if this is worth doing, not a rewrite.
Precedent in EvalPort for this exact shape of problem
Two adapters that hit the same "framework returns real per-item scores + a real aggregate, don't fabricate either" constraint, verified by reading their code:
lm-eval-harness-openeval-adapter—lm-eval'ssimple_evaluate(..., log_samples=True)already returns one real score per document; the adapter maps that 1:1 toResults and preserves the framework's own aggregate verbatim undermetadata["lm_eval"]["aggregate"]— the same pattern proposed above.huggingface-evaluate-openeval-adapter— handles the harder case where the framework's API is aggregate-only by design, computing real per-example scores via repeated.compute()calls rather than interpolating from the aggregate.
grader.py's case is actually simpler than either: it already computes a real per-row verdict (labels[i]) before collapsing to the aggregate, so nothing needs to be recomputed — just retained and re-shaped.
What I'm asking
Not proposing a PR against this repo. Two honest options and I'd rather hear which (if either) is wanted before writing code:
- A ~50-line
pixelrag-openeval-adapterliving entirely in the EvalPort repo (adapters/), depending on nothing from PixelRAG except the JSONL shapegrade_filealready reads/writes. Zero footprint here. - If useful upstream, a small opt-in
--openeval-output <path>flag ongrader.py's CLI that writes theResultSetalongside the normal printed summary — happy to open that as an actual PR with tests if it's wanted.
If neither is useful, no worries — closing this is a fine outcome too.
— Sahi, independent contributor (not affiliated with this project)
Source: StarTrail-org/PixelRAG