Preserve and recover solver outputs

This commit is contained in:
2026-09-23 00:42:12 +08:00
parent 0f0594bc41
commit 06cdd9c556
5 changed files with 51 additions and 6 deletions
+1
View File
@@ -32,6 +32,7 @@ jobs:
run: |
test -f artifacts/short-smoke/skeletons.jsonl && cat artifacts/short-smoke/skeletons.jsonl || true
test -f artifacts/short-smoke/rendered.jsonl && cat artifacts/short-smoke/rendered.jsonl || true
test -f artifacts/short-smoke/solver-outputs.jsonl && cat artifacts/short-smoke/solver-outputs.jsonl || true
test -f artifacts/short-smoke/results.jsonl && cat artifacts/short-smoke/results.jsonl || true
test -f artifacts/short-smoke/summary.json && cat artifacts/short-smoke/summary.json || true
+1
View File
@@ -58,6 +58,7 @@ Outputs under the task's `output_dir` are:
- `skeletons.jsonl`: full hidden programs, sampled behavior, exact probabilities, gold labels, and latent traces; written before any LLM call;
- `rendered.jsonl`: each natural-language problem, checkpointed immediately after rendering;
- `solver-outputs.jsonl`: each complete raw solver response, checkpointed before parsing;
- `results.jsonl`: rendered problems, complete solver responses, parsed labels, gold probabilities, and scores;
- `summary.json`: aggregate accuracy and structural metadata.
+2 -2
View File
@@ -119,7 +119,7 @@ class LLMClient:
{"role": "system", "content": system},
{"role": "user", "content": user},
],
"max_output_tokens": 512,
"max_output_tokens": 1024,
}
else:
path = "/chat/completions"
@@ -129,7 +129,7 @@ class LLMClient:
{"role": "system", "content": system},
{"role": "user", "content": user},
],
"max_tokens": 512,
"max_tokens": 1024,
}
if self.config.enable_thinking is not None:
body["enable_thinking"] = self.config.enable_thinking
+37 -3
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import re
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Protocol
@@ -33,6 +34,7 @@ class EvaluationResult:
gold_present_probability: float
solver_output: str
model_answer: str
format_compliant: bool
correct: bool
@@ -79,8 +81,33 @@ def parse_marked_answer(output: str, answer_options: tuple[str, ...]) -> str:
return answer
def parse_solver_answer(output: str, answer_options: tuple[str, ...]) -> tuple[str, bool]:
"""Prefer the marker protocol, then recover one unambiguous label from the final line."""
try:
return parse_marked_answer(output, answer_options), True
except RuntimeError as strict_error:
lines = [line.strip() for line in output.splitlines() if line.strip()]
if not lines:
raise strict_error
final_line = lines[-1]
matches = [
option
for option in answer_options
if re.search(rf"(?<![A-Za-z0-9_]){re.escape(option)}(?![A-Za-z0-9_])", final_line)
]
if len(matches) == 1:
return matches[0], False
raise strict_error
def solve_problem(client: JsonModel, problem: str, answer_options: tuple[str, ...]) -> tuple[str, str]:
output = client.request_text(
output = request_solver_output(client, problem, answer_options)
answer, _format_compliant = parse_solver_answer(output, answer_options)
return output, answer
def request_solver_output(client: JsonModel, problem: str, answer_options: tuple[str, ...]) -> str:
return client.request_text(
system=(
"Infer the stable hidden mechanism from the repeated behavioral observations, then predict which "
"query outcome is more likely. Event order, attributes, stochastic frequencies, interactions, and "
@@ -90,7 +117,6 @@ def solve_problem(client: JsonModel, problem: str, answer_options: tuple[str, ..
),
user=f"{problem}\n\nPermitted answer labels: {', '.join(answer_options)}",
)
return output, parse_marked_answer(output, answer_options)
def _write_jsonl(path: Path, records: list[dict[str, Any]]) -> None:
@@ -114,11 +140,15 @@ def run_evaluation(
results: list[EvaluationResult] = []
rendered_records: list[dict[str, str]] = []
solver_records: list[dict[str, str]] = []
for skeleton in skeletons:
problem = render_skeleton(renderer, skeleton)
rendered_records.append({"skeleton_id": skeleton.id, "rendered_problem": problem})
_write_jsonl(output_dir / "rendered.jsonl", rendered_records)
solver_output, answer = solve_problem(solver, problem, skeleton.answer_options)
solver_output = request_solver_output(solver, problem, skeleton.answer_options)
solver_records.append({"skeleton_id": skeleton.id, "solver_output": solver_output})
_write_jsonl(output_dir / "solver-outputs.jsonl", solver_records)
answer, format_compliant = parse_solver_answer(solver_output, skeleton.answer_options)
results.append(
EvaluationResult(
skeleton_id=skeleton.id,
@@ -130,16 +160,20 @@ def run_evaluation(
gold_present_probability=skeleton.gold_present_probability,
solver_output=solver_output,
model_answer=answer,
format_compliant=format_compliant,
correct=answer == skeleton.gold_answer,
)
)
_write_jsonl(output_dir / "results.jsonl", [asdict(item) for item in results])
correct = sum(item.correct for item in results)
format_compliant = sum(item.format_compliant for item in results)
summary = {
"count": len(results),
"correct": correct,
"accuracy": correct / len(results),
"format_compliant": format_compliant,
"format_compliance": format_compliant / len(results),
"length": length,
"complexity": skeletons[0].complexity,
"seed": seed,
+10 -1
View File
@@ -2,7 +2,7 @@ import tempfile
import unittest
from pathlib import Path
from logicbench.pipeline import parse_marked_answer, run_evaluation
from logicbench.pipeline import parse_marked_answer, parse_solver_answer, run_evaluation
class QueueClient:
@@ -49,9 +49,11 @@ class PipelineTests(unittest.TestCase):
)
self.assertTrue((output / "skeletons.jsonl").is_file())
self.assertTrue((output / "rendered.jsonl").is_file())
self.assertTrue((output / "solver-outputs.jsonl").is_file())
self.assertTrue((output / "results.jsonl").is_file())
self.assertEqual(summary["count"], 5)
self.assertEqual(summary["correct"], 5)
self.assertEqual(summary["format_compliant"], 5)
def test_answer_parser_requires_final_marker_and_permitted_label(self):
self.assertEqual(parse_marked_answer("Reason.\n#### YES", ("YES", "NO")), "YES")
@@ -60,6 +62,13 @@ class PipelineTests(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "invalid answer"):
parse_marked_answer("#### MAYBE", ("YES", "NO"))
def test_answer_parser_recovers_unique_label_on_final_line(self):
self.assertEqual(
parse_solver_answer("Reasoning.\nFinal answer: YES", ("YES", "NO")),
("YES", False),
)
self.assertEqual(parse_solver_answer("Reasoning.\n#### YES", ("YES", "NO")), ("YES", True))
def test_rendered_problem_is_checkpointed_before_solver_failure(self):
renderer = QueueClient([{"problem": "A compact behavioral problem."}])
with tempfile.TemporaryDirectory() as directory: