Spaces:
Sleeping
Sleeping
Download tests/test_response_format_retry.py from adelevett/ELBench-live: direct link, hf CLI and curl.
- Browser
- Download file 21.5 kB
-
https://huggingface.co/spaces/adelevett/ELBench-live/resolve/main/tests/test_response_format_retry.py
- Command line
-
hf download hf://spaces/adelevett/ELBench-live/tests/test_response_format_retry.py
-
curl -L -o test_response_format_retry.py https://huggingface.co/spaces/adelevett/ELBench-live/resolve/main/tests/test_response_format_retry.py
21.5 kB
| import asyncio | |
| import json | |
| import shutil | |
| import sys | |
| import unittest | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| SRC = ROOT / "src" | |
| if str(SRC) not in sys.path: | |
| sys.path.insert(0, str(SRC)) | |
| from elbench.config import load_project_config # noqa: E402 | |
| from elbench.execution import BenchmarkRunner, RunOptions # noqa: E402 | |
| from elbench.execution.rate_limit import SlidingWindowRateLimiter # noqa: E402 | |
| from elbench.execution.runner import _build_concise_answer_retry_prompt # noqa: E402 | |
| from elbench.execution.response_format import ( # noqa: E402 | |
| ResponseFormatSpec, | |
| attach_format_metadata, | |
| build_concise_retry_prompt, | |
| build_retry_followup, | |
| get_response_format_spec, | |
| should_force_concise_retry, | |
| ) | |
| from elbench.persistence import CheckpointStore, JsonlWriter # noqa: E402 | |
| from elbench.schemas.config import RateLimitConfig, RetryConfig # noqa: E402 | |
| from elbench.schemas.evaluation import GenerationRequest, ModelResponse, Sample # noqa: E402 | |
| class ResponseFormatRetryTest(unittest.TestCase): | |
| def test_empty_length_response_with_reasoning_tokens_uses_concise_retry(self) -> None: | |
| response = ModelResponse( | |
| text="", | |
| raw_payload={ | |
| "choices": [ | |
| { | |
| "message": {"role": "assistant", "content": ""}, | |
| "finish_reason": "length", | |
| } | |
| ] | |
| }, | |
| usage={ | |
| "completion_tokens": 8188, | |
| "completion_tokens_details": {"reasoning_tokens": 7861}, | |
| }, | |
| ) | |
| spec = ResponseFormatSpec( | |
| name="single_choice_en", | |
| instruction="The last line must be exactly: ANSWER: [LETTER].", | |
| reminder_template="unused", | |
| valid_letters=list("ABCD"), | |
| answer_prefixes=["ANSWER"], | |
| ) | |
| self.assertTrue(should_force_concise_retry(response)) | |
| followup = build_retry_followup(spec, response) | |
| self.assertEqual(followup["retry_reason"], "format_empty_after_reasoning") | |
| self.assertEqual(followup["max_tokens"], 64) | |
| self.assertIsNone(followup["assistant_content"]) | |
| self.assertIn("Do not include any reasoning", followup["user_reminder"]) | |
| self.assertIn("ANSWER: [LETTER]", followup["user_reminder"]) | |
| def test_concise_retry_prompt_removes_step_by_step_instruction(self) -> None: | |
| base_prompt = "\n".join( | |
| [ | |
| "Question:", | |
| "Example question?", | |
| "Options:", | |
| "A) example", | |
| "ANSWER: A", | |
| "", | |
| "Answer the following multiple choice question.", | |
| "Think step by step before answering.", | |
| "The last line of your response must be exactly: ANSWER: [LETTER].", | |
| "", | |
| "Question:", | |
| "What is 2 + 2?", | |
| "Options:", | |
| "A) 3", | |
| "B) 4", | |
| ] | |
| ) | |
| spec = ResponseFormatSpec( | |
| name="single_choice_en", | |
| instruction="The last line must be exactly: ANSWER: [LETTER].", | |
| reminder_template="unused", | |
| valid_letters=list("AB"), | |
| answer_prefixes=["ANSWER"], | |
| ) | |
| retry_prompt = build_concise_retry_prompt(base_prompt, spec) | |
| self.assertNotIn("Think step by step", retry_prompt) | |
| self.assertIn("exactly one uppercase option letter", retry_prompt) | |
| self.assertIn("Question:", retry_prompt) | |
| self.assertIn("What is 2 + 2?", retry_prompt) | |
| self.assertNotIn("Example question?", retry_prompt) | |
| self.assertIn("Your answer:", retry_prompt) | |
| def test_nonempty_response_keeps_normal_format_retry(self) -> None: | |
| response = ModelResponse( | |
| text="I think the answer is A.", | |
| raw_payload={ | |
| "choices": [ | |
| { | |
| "message": {"role": "assistant", "content": "I think the answer is A."}, | |
| "finish_reason": "stop", | |
| } | |
| ] | |
| }, | |
| usage={"completion_tokens": 12}, | |
| ) | |
| spec = ResponseFormatSpec( | |
| name="single_choice_en", | |
| instruction="The last line must be exactly: ANSWER: [LETTER].", | |
| reminder_template="Your previous response did not follow the required answer format.", | |
| valid_letters=list("ABCD"), | |
| answer_prefixes=["ANSWER"], | |
| ) | |
| self.assertFalse(should_force_concise_retry(response)) | |
| followup = build_retry_followup(spec, response) | |
| self.assertEqual(followup["retry_reason"], "format_invalid") | |
| self.assertEqual(followup["assistant_content"], "I think the answer is A.") | |
| self.assertIn("did not follow the required answer format", followup["user_reminder"]) | |
| def test_nonempty_length_response_uses_concise_retry(self) -> None: | |
| response = ModelResponse( | |
| text="Long reasoning that used the full output budget before the final answer.", | |
| raw_payload={ | |
| "choices": [ | |
| { | |
| "message": { | |
| "role": "assistant", | |
| "content": "Long reasoning that used the full output budget before the final answer.", | |
| }, | |
| "finish_reason": "length", | |
| } | |
| ] | |
| }, | |
| usage={"completion_tokens": 1024}, | |
| ) | |
| spec = ResponseFormatSpec( | |
| name="single_choice_en", | |
| instruction="The last line must be exactly: ANSWER: [LETTER].", | |
| reminder_template="unused", | |
| valid_letters=list("ABCD"), | |
| answer_prefixes=["ANSWER"], | |
| ) | |
| self.assertTrue(should_force_concise_retry(response)) | |
| followup = build_retry_followup(spec, response) | |
| self.assertEqual(followup["retry_reason"], "format_empty_after_reasoning") | |
| self.assertEqual(followup["max_tokens"], 64) | |
| self.assertIsNone(followup["assistant_content"]) | |
| def test_failed_choice_checkpoint_retries_with_short_concise_prompt(self) -> None: | |
| class ConciseChoiceClient: | |
| def __init__(self) -> None: | |
| self.requests = [] | |
| async def generate(self, sample, request): | |
| self.requests.append(request) | |
| return ModelResponse( | |
| text="ANSWER: A", | |
| raw_payload={ | |
| "choices": [ | |
| { | |
| "message": {"role": "assistant", "content": "ANSWER: A"}, | |
| "finish_reason": "stop", | |
| } | |
| ] | |
| }, | |
| usage={"completion_tokens": 3}, | |
| ) | |
| temp_dir = ROOT / "tmp_test_artifacts" / "failed_choice_checkpoint" | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| temp_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| runner = BenchmarkRunner(load_project_config(ROOT / "configs")) | |
| client = ConciseChoiceClient() | |
| sample = Sample( | |
| sample_id="mmlu1", | |
| source_file="mmlu_pro_sampled.jsonl", | |
| source_path="mmlu_pro_sampled.jsonl", | |
| module="general", | |
| subset="mmlu_pro", | |
| task="mmlu_pro", | |
| prompt=( | |
| "Answer the following multiple choice question.\n" | |
| "Think step by step before answering.\n" | |
| "Options:\nA. one\nB. two" | |
| ), | |
| reference={"answer": "A"}, | |
| ) | |
| request = GenerationRequest( | |
| prompt=sample.prompt, | |
| temperature=0, | |
| max_tokens=1024, | |
| stream=False, | |
| messages=[{"role": "user", "content": sample.prompt}], | |
| provider_kwargs={}, | |
| ) | |
| checkpoint = CheckpointStore(temp_dir / "checkpoint.json") | |
| checkpoint.failed_ids.add("mmlu_pro_sampled.jsonl::mmlu1") | |
| response, retry_count = asyncio.run( | |
| runner._generate_with_retry( | |
| sample=sample, | |
| client=client, | |
| request=request, | |
| retry_policy=RetryConfig( | |
| max_attempts=2, | |
| initial_delay_seconds=0, | |
| max_delay_seconds=0, | |
| jitter_ratio=0, | |
| ), | |
| limiter=SlidingWindowRateLimiter(RateLimitConfig(max_concurrency=1)), | |
| retry_writer=JsonlWriter(temp_dir / "retries.jsonl"), | |
| checkpoint=checkpoint, | |
| ) | |
| ) | |
| self.assertEqual(retry_count, 0) | |
| self.assertEqual(response.text, "ANSWER: A") | |
| self.assertEqual(len(client.requests), 1) | |
| self.assertEqual(client.requests[0].max_tokens, 64) | |
| self.assertNotIn("Think step by step", client.requests[0].prompt) | |
| self.assertIn("exactly one uppercase option letter", client.requests[0].prompt) | |
| finally: | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| def test_math_length_response_retries_with_concise_prompt(self) -> None: | |
| class TruncatedMathClient: | |
| def __init__(self) -> None: | |
| self.requests = [] | |
| async def generate(self, sample, request): | |
| self.requests.append(request) | |
| if len(self.requests) == 1: | |
| return ModelResponse( | |
| text="Long derivation without a final boxed answer.", | |
| raw_payload={ | |
| "choices": [ | |
| { | |
| "message": { | |
| "role": "assistant", | |
| "content": "Long derivation without a final boxed answer.", | |
| }, | |
| "finish_reason": "length", | |
| } | |
| ] | |
| }, | |
| usage={"completion_tokens": 1024}, | |
| ) | |
| return ModelResponse( | |
| text="\\boxed{42}", | |
| raw_payload={ | |
| "choices": [ | |
| { | |
| "message": {"role": "assistant", "content": "\\boxed{42}"}, | |
| "finish_reason": "stop", | |
| } | |
| ] | |
| }, | |
| usage={"completion_tokens": 8}, | |
| ) | |
| temp_dir = ROOT / "tmp_test_artifacts" / "math_length_retry" | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| temp_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| runner = BenchmarkRunner(load_project_config(ROOT / "configs")) | |
| client = TruncatedMathClient() | |
| sample = Sample( | |
| sample_id="math1", | |
| source_file="math_500_sampled.jsonl", | |
| source_path="math_500_sampled.jsonl", | |
| module="general", | |
| subset="math_500", | |
| task="math_500", | |
| prompt="Solve this. Please reason step by step, and put your final answer within \\boxed{}.", | |
| reference={"target": "42"}, | |
| ) | |
| request = GenerationRequest( | |
| prompt=sample.prompt, | |
| temperature=0, | |
| max_tokens=1024, | |
| stream=False, | |
| messages=[{"role": "user", "content": sample.prompt}], | |
| provider_kwargs={}, | |
| ) | |
| response, retry_count = asyncio.run( | |
| runner._generate_with_retry( | |
| sample=sample, | |
| client=client, | |
| request=request, | |
| retry_policy=RetryConfig( | |
| max_attempts=2, | |
| initial_delay_seconds=0, | |
| max_delay_seconds=0, | |
| jitter_ratio=0, | |
| ), | |
| limiter=SlidingWindowRateLimiter(RateLimitConfig(max_concurrency=1)), | |
| retry_writer=JsonlWriter(temp_dir / "retries.jsonl"), | |
| checkpoint=CheckpointStore(temp_dir / "checkpoint.json"), | |
| ) | |
| ) | |
| self.assertEqual(retry_count, 1) | |
| self.assertEqual(response.text, "\\boxed{42}") | |
| self.assertEqual(len(client.requests), 2) | |
| self.assertIn("Do not include any reasoning", client.requests[1].prompt) | |
| self.assertEqual(client.requests[1].max_tokens, 1024) | |
| finally: | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| def test_math_retryable_error_retries_with_concise_prompt(self) -> None: | |
| class FailingMathClient: | |
| def __init__(self) -> None: | |
| self.requests = [] | |
| async def generate(self, sample, request): | |
| self.requests.append(request) | |
| if len(self.requests) == 1: | |
| raise RuntimeError("transient gateway timeout") | |
| return ModelResponse( | |
| text="\\boxed{42}", | |
| raw_payload={ | |
| "choices": [ | |
| { | |
| "message": {"role": "assistant", "content": "\\boxed{42}"}, | |
| "finish_reason": "stop", | |
| } | |
| ] | |
| }, | |
| usage={"completion_tokens": 8}, | |
| ) | |
| temp_dir = ROOT / "tmp_test_artifacts" / "math_error_retry" | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| temp_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| runner = BenchmarkRunner(load_project_config(ROOT / "configs")) | |
| client = FailingMathClient() | |
| sample = Sample( | |
| sample_id="math1", | |
| source_file="math_500_sampled.jsonl", | |
| source_path="math_500_sampled.jsonl", | |
| module="general", | |
| subset="math_500", | |
| task="math_500", | |
| prompt="Solve this. Please reason step by step, and put your final answer within \\boxed{}.", | |
| reference={"target": "42"}, | |
| ) | |
| request = GenerationRequest( | |
| prompt=sample.prompt, | |
| temperature=0, | |
| max_tokens=1024, | |
| stream=False, | |
| messages=[{"role": "user", "content": sample.prompt}], | |
| provider_kwargs={}, | |
| ) | |
| response, retry_count = asyncio.run( | |
| runner._generate_with_retry( | |
| sample=sample, | |
| client=client, | |
| request=request, | |
| retry_policy=RetryConfig( | |
| max_attempts=2, | |
| initial_delay_seconds=0, | |
| max_delay_seconds=0, | |
| jitter_ratio=0, | |
| ), | |
| limiter=SlidingWindowRateLimiter(RateLimitConfig(max_concurrency=1)), | |
| retry_writer=JsonlWriter(temp_dir / "retries.jsonl"), | |
| checkpoint=CheckpointStore(temp_dir / "checkpoint.json"), | |
| ) | |
| ) | |
| self.assertEqual(retry_count, 1) | |
| self.assertEqual(response.text, "\\boxed{42}") | |
| self.assertEqual(len(client.requests), 2) | |
| self.assertIn("Do not include any reasoning", client.requests[1].prompt) | |
| self.assertEqual(client.requests[1].max_tokens, 1024) | |
| finally: | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| def test_concise_math_retry_prompt_preserves_question_when_instruction_is_inline(self) -> None: | |
| prompt = ( | |
| "Convert the point $(0,3)$ in rectangular coordinates to polar coordinates. " | |
| "Please reason step by step, and put your final answer within \\boxed{}." | |
| ) | |
| retry_prompt = _build_concise_answer_retry_prompt(prompt) | |
| self.assertIn("Convert the point", retry_prompt) | |
| self.assertNotIn("reason step by step", retry_prompt.lower()) | |
| self.assertIn("\\boxed{}", retry_prompt) | |
| def test_highlevel_omni_format_accepts_multiple_choice_answer(self) -> None: | |
| sample = Sample( | |
| sample_id="o_multi", | |
| source_file="高阶育人-omni.jsonl", | |
| source_path="高阶育人-omni.jsonl", | |
| module="高阶育人", | |
| subset="omni", | |
| task="highlevel_omni", | |
| prompt="dummy", | |
| reference={"answer": "AC", "reasonableness": 1}, | |
| ) | |
| response = ModelResponse(text="答案:AC") | |
| response = attach_format_metadata(response, get_response_format_spec(sample)) | |
| self.assertTrue(response.format_valid) | |
| self.assertEqual(response.format_metadata["parsed_answer"], "AC") | |
| def test_single_choice_invalid_format_triggers_retry_and_recovers(self) -> None: | |
| temp_dir = ROOT / "tmp_test_artifacts" / "elbench_format_retry" | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| temp_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| config = load_project_config(ROOT / "configs") | |
| config.app.output_root = temp_dir / "outputs" | |
| model = config.models["mock.default"].model_copy(deep=True) | |
| model.provider_kwargs["mode"] = "format_retry_probe" | |
| model.retry.max_attempts = 2 | |
| config.models["mock.default"] = model | |
| runner = BenchmarkRunner(config) | |
| result = asyncio.run( | |
| runner.run( | |
| RunOptions( | |
| model_id="mock.default", | |
| run_id="format-retry-probe", | |
| modules={"通用模型"}, | |
| source_files={"mmlu_pro_sampled.jsonl"}, | |
| max_samples=1, | |
| ) | |
| ) | |
| ) | |
| output_root = temp_dir / "outputs" | |
| retries_path = output_root / "logs" / "format-retry-probe" / "mock.default.retries.jsonl" | |
| judged_path = Path(result["judged_path"]) | |
| retries = [ | |
| json.loads(line) | |
| for line in retries_path.read_text(encoding="utf-8").splitlines() | |
| if line.strip() | |
| ] | |
| judged = [ | |
| json.loads(line) | |
| for line in judged_path.read_text(encoding="utf-8").splitlines() | |
| if line.strip() | |
| ] | |
| self.assertEqual(len(retries), 1) | |
| self.assertEqual(retries[0]["retry_reason"], "format_invalid") | |
| self.assertEqual(len(judged), 1) | |
| self.assertEqual(judged[0]["retry_count"], 1) | |
| self.assertEqual(judged[0]["judge_result"], "fail") | |
| self.assertEqual(judged[0]["model_response"], "ANSWER: A") | |
| finally: | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| def test_final_format_failure_is_counted_as_model_wrong_answer(self) -> None: | |
| temp_dir = ROOT / "tmp_test_artifacts" / "elbench_format_final_failure" | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| temp_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| config = load_project_config(ROOT / "configs") | |
| config.app.output_root = temp_dir / "outputs" | |
| model = config.models["mock.default"].model_copy(deep=True) | |
| model.provider_kwargs["mode"] = "empty" | |
| model.retry.max_attempts = 2 | |
| config.models["mock.default"] = model | |
| runner = BenchmarkRunner(config) | |
| result = asyncio.run( | |
| runner.run( | |
| RunOptions( | |
| model_id="mock.default", | |
| run_id="format-final-failure-probe", | |
| modules={"通用模型"}, | |
| source_files={"mmlu_pro_sampled.jsonl"}, | |
| max_samples=1, | |
| ) | |
| ) | |
| ) | |
| judged_path = Path(result["judged_path"]) | |
| failures_path = Path(result["failures_path"]) | |
| judged = [ | |
| json.loads(line) | |
| for line in judged_path.read_text(encoding="utf-8").splitlines() | |
| if line.strip() | |
| ] | |
| failure_lines = failures_path.read_text(encoding="utf-8").splitlines() if failures_path.exists() else [] | |
| failures = [json.loads(line) for line in failure_lines if line.strip()] | |
| self.assertEqual(len(judged), 1) | |
| self.assertEqual(len(failures), 0) | |
| self.assertEqual(judged[0]["judge_result"], "fail") | |
| self.assertEqual(judged[0]["score"], 0) | |
| self.assertEqual(judged[0]["metadata"]["failure_category"], "model_instruction_following") | |
| self.assertEqual(judged[0]["metadata"]["failure_type"], "response_format") | |
| finally: | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| if __name__ == "__main__": | |
| unittest.main() | |