evalrx 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- evalrx-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Answer-extraction audit — is this a wrong answer, or an unparsed one?
|
|
2
|
+
|
|
3
|
+
**Run this probe first.** Every other per-case column in M1 is conditioned on
|
|
4
|
+
the PASS/FAIL label, and that label comes from a grader that has to find the
|
|
5
|
+
answer inside free-form text. When the grader misses — the model wrote
|
|
6
|
+
``\\boxed{18}`` and the grader wanted ``Answer: 18``, or it answered in a
|
|
7
|
+
sentence, or the generation was cut off before the tag — the case is labelled
|
|
8
|
+
FAIL for a reason that has nothing to do with the model's reasoning. Those
|
|
9
|
+
cases then enter every downstream correlation as noise pointing at whatever the
|
|
10
|
+
other probes happen to measure, and M2 reports a real-looking effect for a
|
|
11
|
+
harness bug.
|
|
12
|
+
|
|
13
|
+
Costs nothing by default (it reads ``observed``); with ``reask=True`` it spends
|
|
14
|
+
one strict-format generation per suspect case to settle whether the model
|
|
15
|
+
actually had the answer.
|
|
16
|
+
|
|
17
|
+
References:
|
|
18
|
+
- Answer-extraction sensitivity in LLM math evaluation — Language Model
|
|
19
|
+
Evaluation Harness, answer-extraction regex family (EleutherAI, 2023-2025)
|
|
20
|
+
- Let's Verify Step by Step — Lightman et al., ICLR 2024 — arXiv:2305.20050
|
|
21
|
+
(grading protocol for free-form numeric answers)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import dataclasses
|
|
27
|
+
import re
|
|
28
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
29
|
+
|
|
30
|
+
from evalrx.analyzers.reasoning._text import (
|
|
31
|
+
_ANSWER_TAG,
|
|
32
|
+
_BOXED,
|
|
33
|
+
answer_equal,
|
|
34
|
+
binary_direction,
|
|
35
|
+
binary_gold,
|
|
36
|
+
extract_answer,
|
|
37
|
+
has_answer_tag,
|
|
38
|
+
looks_like_give_up,
|
|
39
|
+
looks_truncated,
|
|
40
|
+
normalize_answer,
|
|
41
|
+
)
|
|
42
|
+
from evalrx.core.analyzer import Analyzer
|
|
43
|
+
from evalrx.core.capability import Capability, CapabilityError
|
|
44
|
+
from evalrx.core.registry import register_analyzer
|
|
45
|
+
from evalrx.core.result import Result
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from evalrx.core.case import CaseBatch, FailureCase
|
|
49
|
+
from evalrx.core.model import Model
|
|
50
|
+
|
|
51
|
+
_REASK_SUFFIX = (
|
|
52
|
+
"Reply with ONLY the final answer, no explanation, no units, no punctuation."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _gold_scalar(expected: Any) -> str:
|
|
57
|
+
if isinstance(expected, (list, tuple)):
|
|
58
|
+
return str(expected[0]) if expected else ""
|
|
59
|
+
return str(expected)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _contract_answer(text: Any, contract: dict[str, Any]) -> str:
|
|
63
|
+
"""Return a committed short answer under a benchmark output contract.
|
|
64
|
+
|
|
65
|
+
A single-letter gold must never match the first character of prose such as
|
|
66
|
+
``Audio Analysis``. We accept an explicit Answer/Final marker or a final
|
|
67
|
+
line containing only the answer, matching the benchmark's response shape.
|
|
68
|
+
"""
|
|
69
|
+
raw = str(text or "")
|
|
70
|
+
kind = str(contract.get("kind") or "").lower()
|
|
71
|
+
lines = [line.strip() for line in raw.splitlines() if line.strip()]
|
|
72
|
+
final_line = lines[-1] if lines else raw.strip()
|
|
73
|
+
|
|
74
|
+
if kind == "multiple_choice_letter":
|
|
75
|
+
choices = "".join(str(c).strip()[:1].upper() for c in contract.get("choices", [])) or "ABCD"
|
|
76
|
+
cls = re.escape(choices)
|
|
77
|
+
marked = re.findall(
|
|
78
|
+
rf"(?:\banswer|\bfinal(?:\s+answer)?)\s*(?:is\b\s*|[:=\-]\s*)"
|
|
79
|
+
rf"[\(\[]?([{cls}])[\)\]]?\b",
|
|
80
|
+
raw,
|
|
81
|
+
flags=re.IGNORECASE,
|
|
82
|
+
)
|
|
83
|
+
boxed = re.findall(rf"\\boxed\s*\{{\s*([{cls}])\s*\}}", raw, flags=re.IGNORECASE)
|
|
84
|
+
if marked or boxed:
|
|
85
|
+
return (marked + boxed)[-1].upper()
|
|
86
|
+
bare = re.fullmatch(rf"\s*[\(\[]?([{cls}])[\)\]]?[\s.!]*", final_line, re.IGNORECASE)
|
|
87
|
+
return bare.group(1).upper() if bare else ""
|
|
88
|
+
|
|
89
|
+
if kind == "yes_no":
|
|
90
|
+
marked = re.findall(
|
|
91
|
+
r"(?:\banswer|\bfinal(?:\s+answer)?)\s*(?:is\b\s*|[:=\-]\s*)"
|
|
92
|
+
r"(yes|no)\b",
|
|
93
|
+
raw,
|
|
94
|
+
flags=re.IGNORECASE,
|
|
95
|
+
)
|
|
96
|
+
if marked:
|
|
97
|
+
return marked[-1].capitalize()
|
|
98
|
+
bare = re.fullmatch(r"\s*(yes|no)[\s.!]*", final_line, re.IGNORECASE)
|
|
99
|
+
return bare.group(1).capitalize() if bare else ""
|
|
100
|
+
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _contract_anywhere(text: Any, expected: Any, contract: dict[str, Any]) -> bool:
|
|
105
|
+
"""Loose upper bound for contracted answers, without substring matching."""
|
|
106
|
+
raw = str(text or "")
|
|
107
|
+
gold = _gold_scalar(expected).strip()
|
|
108
|
+
kind = str(contract.get("kind") or "").lower()
|
|
109
|
+
if _contract_answer(raw, contract).lower() == gold.lower():
|
|
110
|
+
return True
|
|
111
|
+
if kind == "multiple_choice_letter" and len(gold) == 1:
|
|
112
|
+
return bool(re.search(rf"(?<![A-Za-z]){re.escape(gold)}(?![A-Za-z])", raw, re.IGNORECASE))
|
|
113
|
+
if kind == "yes_no":
|
|
114
|
+
return bool(re.search(rf"\b{re.escape(gold)}\b", raw, re.IGNORECASE))
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@register_analyzer("answer_extraction_audit")
|
|
119
|
+
class AnswerExtractionAudit(Analyzer):
|
|
120
|
+
"""Split FAIL cases into *really wrong* and *merely unparsed*.
|
|
121
|
+
|
|
122
|
+
Hyper-parameters:
|
|
123
|
+
reask: re-ask each suspect case with a strict output format
|
|
124
|
+
(1 generation per suspect; needs ``GENERATE``).
|
|
125
|
+
max_cases: label-stratified cap; 0 (the default) = every case.
|
|
126
|
+
tail_chars: size of the trailing window treated as the answer region.
|
|
127
|
+
answer_fn: ``callable(text) -> str`` answer extractor.
|
|
128
|
+
match_fn: ``callable(prediction, gold) -> bool`` equality test.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
name = "answer_extraction_audit"
|
|
132
|
+
# Capability-free on the default (read-only) path so it can audit an
|
|
133
|
+
# offline record dump; the re-ask path checks GENERATE at run time.
|
|
134
|
+
requires = frozenset()
|
|
135
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
136
|
+
signal_docs = {
|
|
137
|
+
'answered_yes': ('Model said yes', 'On a yes/no question, whether the MODEL answered yes. Its direction, not whether it was right.'),
|
|
138
|
+
'gold_yes': ('Correct answer is yes', 'On a yes/no question, whether the CORRECT answer is yes. A property of the question, not the model.'),
|
|
139
|
+
'extracted_answer': 'The answer the grader pulled out of the text.',
|
|
140
|
+
'extraction_point_miss': 'The right answer was in the right place and still did not match — a formatting mismatch.',
|
|
141
|
+
'extraction_suspect': ('Possible grading miss', 'This case was marked wrong, but the right answer is there — a likely grading mistake.'),
|
|
142
|
+
'gave_up': ('Model gave up', 'Whether the model said it could not answer.'),
|
|
143
|
+
'gold_in_answer_region': ('Right answer in place', 'Whether the right answer appears in the part of the text the grader reads.'),
|
|
144
|
+
'gold_in_output': ('Right answer somewhere', 'Whether the right answer appears anywhere in the text, even if not where it was asked for.'),
|
|
145
|
+
'has_answer_tag': ('Used the answer format', 'Whether the model used the answer format it was asked for.'),
|
|
146
|
+
'has_output': ('Answered at all', 'Whether the model wrote anything at all.'),
|
|
147
|
+
'label_disagrees': "The benchmark's verdict and a strict re-check disagree about this case.",
|
|
148
|
+
'labelled_fail': 'Whether the benchmark marked this answer wrong.',
|
|
149
|
+
'missing_tag_rate': ('Wrong answer format', 'Share of answers that never used the requested answer format.'),
|
|
150
|
+
'n_cases': 'How many answers were inspected.',
|
|
151
|
+
'n_extraction_point_miss': 'Cases where the answer was in the right place but the exact-match check still failed — usually formatting.',
|
|
152
|
+
'n_extraction_suspect': 'Wrong answers where the right answer was in fact present in the text — the grader may have missed it.',
|
|
153
|
+
'n_gradable': 'How many answers had a gold answer to check against.',
|
|
154
|
+
'n_labelled_fail': "How many were marked wrong by the benchmark's own grader.",
|
|
155
|
+
'n_reask_confirmed': 'How many of those re-asks confirmed the model really was wrong.',
|
|
156
|
+
'n_reasked': 'How many suspect cases were asked again to check.',
|
|
157
|
+
'output_chars': ('Answer length', 'How long the answer was, in characters. Very long usually means the model rambled instead of answering.'),
|
|
158
|
+
'output_truncated': ('Answer was cut off', 'Whether the answer was cut off by the length limit rather than finished.'),
|
|
159
|
+
'reask_answer': 'What the model said when asked the same question again.',
|
|
160
|
+
'strict_match': ('Exactly matched gold', 'Whether the answer matched the gold answer exactly.'),
|
|
161
|
+
'suspect_rate': ('Possible grading misses', 'Share of wrong answers that may be grading mistakes rather than model mistakes. High means the failure numbers are not trustworthy yet.'),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
reask: bool = False,
|
|
167
|
+
max_cases: int = 0,
|
|
168
|
+
tail_chars: int = 200,
|
|
169
|
+
answer_fn: Optional[Callable[[Any], str]] = None,
|
|
170
|
+
match_fn: Optional[Callable[[Any, Any], bool]] = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
super().__init__(reask=reask, max_cases=max_cases, tail_chars=tail_chars)
|
|
173
|
+
# ctor names, so sklearn-style get_params() reflection works
|
|
174
|
+
self.answer_fn = answer_fn or extract_answer
|
|
175
|
+
self.match_fn = match_fn or answer_equal
|
|
176
|
+
|
|
177
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
178
|
+
if self.reask and Capability.GENERATE not in getattr(
|
|
179
|
+
model, "capabilities", frozenset()
|
|
180
|
+
):
|
|
181
|
+
raise CapabilityError(
|
|
182
|
+
analyzer=self.name, model=repr(model), missing={Capability.GENERATE}
|
|
183
|
+
)
|
|
184
|
+
per_case: list[dict[str, Any]] = []
|
|
185
|
+
n_reasked = 0
|
|
186
|
+
for case in cases.stratified_head(self.max_cases):
|
|
187
|
+
entry = self._audit_case(case)
|
|
188
|
+
if self.reask and entry.get("extraction_suspect") == 1:
|
|
189
|
+
entry.update(self._reask(model, case))
|
|
190
|
+
n_reasked += 1
|
|
191
|
+
per_case.append(entry)
|
|
192
|
+
|
|
193
|
+
gradable = [c for c in per_case if "extraction_suspect" in c]
|
|
194
|
+
failing = [c for c in gradable if c["labelled_fail"] == 1]
|
|
195
|
+
suspects = [c for c in failing if c["extraction_suspect"] == 1]
|
|
196
|
+
confirmed = [c for c in suspects if c.get("reask_correct") == 1]
|
|
197
|
+
suspect_rate = round(len(suspects) / len(failing), 4) if failing else None
|
|
198
|
+
|
|
199
|
+
findings: dict[str, Any] = {
|
|
200
|
+
"n_cases": len(per_case),
|
|
201
|
+
"n_gradable": len(gradable),
|
|
202
|
+
"n_labelled_fail": len(failing),
|
|
203
|
+
"n_extraction_suspect": len(suspects),
|
|
204
|
+
"suspect_rate": suspect_rate,
|
|
205
|
+
"n_reasked": n_reasked,
|
|
206
|
+
"n_reask_confirmed": len(confirmed) if self.reask else None,
|
|
207
|
+
"n_extraction_point_miss": sum(c["extraction_point_miss"] for c in gradable),
|
|
208
|
+
"missing_tag_rate": (
|
|
209
|
+
round(sum(1 - c["has_answer_tag"] for c in gradable) / len(gradable), 4)
|
|
210
|
+
if gradable
|
|
211
|
+
else None
|
|
212
|
+
),
|
|
213
|
+
"per_case": per_case,
|
|
214
|
+
"_caveat": (
|
|
215
|
+
"suspect_rate is the fraction of FAIL cases whose gold answer "
|
|
216
|
+
"sits in the ANSWER REGION of the output (tail window or a "
|
|
217
|
+
"tagged/boxed span) — HARNESS failures wearing a model-failure "
|
|
218
|
+
"label. gold_in_output is the loose upper bound and over-counts: "
|
|
219
|
+
"it fires on a gold that only appears as an intermediate "
|
|
220
|
+
"quantity, so never read it as the suspect rate. Above ~5% the "
|
|
221
|
+
"labels feeding M2 are contaminated and every other probe's "
|
|
222
|
+
"correlations should be re-read after fixing extraction (or "
|
|
223
|
+
"after dropping extraction_suspect==1 cases). This probe is "
|
|
224
|
+
"OBSERVATIONAL by default: it re-reads stored outputs and "
|
|
225
|
+
"introduces no new sampling. With reask=True the reask_* columns "
|
|
226
|
+
"are interventional and must be re-run for held-out confirmation. "
|
|
227
|
+
"On yes/no (true/false) tasks answered_yes and gold_yes are the "
|
|
228
|
+
"answer's and the gold's DIRECTION, two separate marginals (not "
|
|
229
|
+
"re-grades): a directional hypothesis is tested on them, e.g. "
|
|
230
|
+
"answered_yes HIGHER on failing cases."
|
|
231
|
+
),
|
|
232
|
+
}
|
|
233
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
def _audit_case(self, case: "FailureCase") -> dict[str, Any]:
|
|
237
|
+
from evalrx.core.case import Label
|
|
238
|
+
|
|
239
|
+
text = case.observed
|
|
240
|
+
truncated = looks_truncated(text)
|
|
241
|
+
finish_reason = (getattr(case, "metadata", {}) or {}).get("finish_reason")
|
|
242
|
+
if finish_reason == "stop":
|
|
243
|
+
truncated = False
|
|
244
|
+
elif finish_reason == "length":
|
|
245
|
+
truncated = True
|
|
246
|
+
entry: dict[str, Any] = {
|
|
247
|
+
"sample_id": case.id,
|
|
248
|
+
"has_output": int(bool(str(text or "").strip())),
|
|
249
|
+
"has_answer_tag": int(has_answer_tag(text)),
|
|
250
|
+
"output_truncated": int(truncated),
|
|
251
|
+
"gave_up": int(looks_like_give_up(text)),
|
|
252
|
+
"output_chars": len(str(text or "")),
|
|
253
|
+
}
|
|
254
|
+
if case.expected is None or not str(text or "").strip():
|
|
255
|
+
entry["skipped"] = "no gold answer or no stored output"
|
|
256
|
+
return entry
|
|
257
|
+
|
|
258
|
+
raw = str(text)
|
|
259
|
+
metadata = getattr(case, "metadata", {}) or {}
|
|
260
|
+
contract = metadata.get("output_contract") or {}
|
|
261
|
+
contract_kind = str(contract.get("kind") or "").lower()
|
|
262
|
+
contracted = contract_kind in {"multiple_choice_letter", "yes_no"}
|
|
263
|
+
extracted = _contract_answer(raw, contract) if contracted else self.answer_fn(text)
|
|
264
|
+
strict = (
|
|
265
|
+
extracted.lower() == _gold_scalar(case.expected).strip().lower()
|
|
266
|
+
if contracted else bool(self.match_fn(extracted, case.expected))
|
|
267
|
+
)
|
|
268
|
+
gold = normalize_answer(case.expected)
|
|
269
|
+
# Anywhere in the output — the loose UPPER bound; a gold that merely
|
|
270
|
+
# appears as an intermediate quantity counts here and should not.
|
|
271
|
+
anywhere = (
|
|
272
|
+
_contract_anywhere(raw, case.expected, contract)
|
|
273
|
+
if contracted
|
|
274
|
+
else bool(self.match_fn(raw, case.expected)) or gold in normalize_answer(raw)
|
|
275
|
+
)
|
|
276
|
+
# In the answer REGION: the tail of the generation, or any tagged /
|
|
277
|
+
# boxed span. This is the one that means "the model did answer this".
|
|
278
|
+
tail = raw[-self.tail_chars :]
|
|
279
|
+
tagged_spans = " ".join(_BOXED.findall(raw) + _ANSWER_TAG.findall(raw))
|
|
280
|
+
in_tail = (
|
|
281
|
+
_contract_answer(tail, contract).lower() == _gold_scalar(case.expected).strip().lower()
|
|
282
|
+
if contracted
|
|
283
|
+
else bool(self.match_fn(tail, case.expected)) or bool(
|
|
284
|
+
tagged_spans and self.match_fn(tagged_spans, case.expected)
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
entry.update(
|
|
288
|
+
{
|
|
289
|
+
"labelled_fail": int(case.label == Label.FAIL),
|
|
290
|
+
"strict_match": int(strict),
|
|
291
|
+
"gold_in_output": int(anywhere),
|
|
292
|
+
"gold_in_answer_region": int(in_tail),
|
|
293
|
+
"extracted_answer": str(extracted)[:120],
|
|
294
|
+
# THE diagnosis: the harness called it a failure, yet the gold
|
|
295
|
+
# answer sits in the answer region of the output.
|
|
296
|
+
"extraction_suspect": int(case.label == Label.FAIL and in_tail),
|
|
297
|
+
# narrower, grader-independent: present in the text but not at
|
|
298
|
+
# the point the extractor reads
|
|
299
|
+
"extraction_point_miss": int(in_tail and not strict),
|
|
300
|
+
# the stored label disagrees with re-grading the stored output
|
|
301
|
+
"label_disagrees": int((case.label == Label.FAIL) == strict),
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
# Binary tasks only: the DIRECTION of the gold and of the answer, as two
|
|
305
|
+
# separate marginals. Neither is a function of the label -- answered_yes
|
|
306
|
+
# is model behaviour, gold_yes is a question covariate -- so both may
|
|
307
|
+
# enter M2's tested family, and M4 can check a directional hypothesis
|
|
308
|
+
# ("a Yes prior: answered_yes HIGHER on failures; failures concentrate
|
|
309
|
+
# on gold_yes=0"). Their CONJUNCTION (answered Yes on a gold No) is a
|
|
310
|
+
# subset of FAIL by construction and must never be a column here.
|
|
311
|
+
# Live motivation: audiocaps_hallucination 2026-08-20, where M3 named
|
|
312
|
+
# extracted_answer / labelled_fail and M4 had nothing numeric to test.
|
|
313
|
+
gold_dir = binary_gold(case.expected)
|
|
314
|
+
if gold_dir is not None:
|
|
315
|
+
entry["gold_yes"] = int(gold_dir == "yes")
|
|
316
|
+
answer_dir = binary_direction(extracted) or binary_direction(tail, last=True)
|
|
317
|
+
if answer_dir is not None:
|
|
318
|
+
entry["answered_yes"] = int(answer_dir == "yes")
|
|
319
|
+
return entry
|
|
320
|
+
|
|
321
|
+
def _reask(self, model: "Model", case: "FailureCase") -> dict[str, Any]:
|
|
322
|
+
prompt = f"{case.inputs.prompt or ''}\n\n{_REASK_SUFFIX}"
|
|
323
|
+
answer = str(model.generate(dataclasses.replace(case.inputs, prompt=prompt)))
|
|
324
|
+
return {
|
|
325
|
+
"reask_answer": answer[:120],
|
|
326
|
+
"reask_correct": int(bool(self.match_fn(answer, case.expected))),
|
|
327
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Arithmetic audit — computation slip or broken reasoning chain?
|
|
2
|
+
|
|
3
|
+
"The model is bad at math" is two failures wearing one label. Either the plan
|
|
4
|
+
was right and one multiplication came out wrong (a *computation slip*, fixed by
|
|
5
|
+
a calculator tool or a verification pass), or every stated computation checks
|
|
6
|
+
out and the answer is still wrong (a *chain break*, fixed by better
|
|
7
|
+
decomposition — a calculator changes nothing). M5 picks opposite tiers for the
|
|
8
|
+
two, so collapsing them wastes the whole arc.
|
|
9
|
+
|
|
10
|
+
The split needs **no extra generations**: every ``a op b = c`` statement in the
|
|
11
|
+
stored reasoning is re-evaluated, and the first wrong one is forward-substituted
|
|
12
|
+
— if repairing that single value reproduces the gold answer (directly, or by
|
|
13
|
+
propagating the difference/ratio to the model's own final number), the error is
|
|
14
|
+
a slip; if the arithmetic is clean and the answer is wrong, the chain broke.
|
|
15
|
+
|
|
16
|
+
References:
|
|
17
|
+
- Training Verifiers to Solve Math Word Problems — Cobbe et al., 2021 —
|
|
18
|
+
arXiv:2110.14168 (calculator annotations: arithmetic vs planning errors)
|
|
19
|
+
- Let's Verify Step by Step — Lightman et al., ICLR 2024 — arXiv:2305.20050
|
|
20
|
+
- GSM-Symbolic: Understanding the Limitations of Mathematical Reasoning in LLMs
|
|
21
|
+
— Mirzadeh et al., ICLR 2025 — arXiv:2410.05229
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import dataclasses
|
|
27
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
28
|
+
|
|
29
|
+
from evalrx.analyzers.reasoning._text import (
|
|
30
|
+
answer_equal,
|
|
31
|
+
as_number,
|
|
32
|
+
extract_answer,
|
|
33
|
+
find_equations,
|
|
34
|
+
numbers_in,
|
|
35
|
+
)
|
|
36
|
+
from evalrx.core.analyzer import Analyzer
|
|
37
|
+
from evalrx.core.capability import Capability, CapabilityError
|
|
38
|
+
from evalrx.core.registry import register_analyzer
|
|
39
|
+
from evalrx.core.result import Result
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from evalrx.core.case import CaseBatch, FailureCase
|
|
43
|
+
from evalrx.core.model import Model
|
|
44
|
+
|
|
45
|
+
_COT_SUFFIX = (
|
|
46
|
+
"Show every calculation as an explicit 'a op b = c' line. After the "
|
|
47
|
+
"reasoning, give the final answer on its own last line as 'Answer: <answer>'."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
SLIP = "computation_slip"
|
|
51
|
+
CHAIN_BREAK = "chain_break"
|
|
52
|
+
COMPOUND = "compound"
|
|
53
|
+
CLEAN = "clean"
|
|
54
|
+
LUCKY = "errors_but_correct"
|
|
55
|
+
UNKNOWN = "unknown"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _close(a: float, b: float, rel_tol: float) -> bool:
|
|
59
|
+
return abs(a - b) <= rel_tol * max(1.0, abs(b))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@register_analyzer("arith_audit")
|
|
63
|
+
class ArithmeticAudit(Analyzer):
|
|
64
|
+
"""Re-check every arithmetic step in the reasoning and classify the failure.
|
|
65
|
+
|
|
66
|
+
Hyper-parameters:
|
|
67
|
+
rel_tol: relative tolerance for "the stated value is right".
|
|
68
|
+
generate_missing: generate a chain when ``case.observed`` is empty
|
|
69
|
+
(1 generation; needs ``GENERATE``). Off by default —
|
|
70
|
+
the probe is meant to be free.
|
|
71
|
+
max_cases: label-stratified cap; 0 (the default) = every case.
|
|
72
|
+
answer_fn/match_fn: answer extraction / equality (see ``_text``).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name = "arith_audit"
|
|
76
|
+
# Free and capability-free on the default path (it re-reads stored chains);
|
|
77
|
+
# the generate_missing path checks GENERATE at run time.
|
|
78
|
+
requires = frozenset()
|
|
79
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
rel_tol: float = 1e-6,
|
|
84
|
+
generate_missing: bool = False,
|
|
85
|
+
max_cases: int = 0,
|
|
86
|
+
answer_fn: Optional[Callable[[Any], str]] = None,
|
|
87
|
+
match_fn: Optional[Callable[[Any, Any], bool]] = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
super().__init__(
|
|
90
|
+
rel_tol=rel_tol, generate_missing=generate_missing, max_cases=max_cases
|
|
91
|
+
)
|
|
92
|
+
self.answer_fn = answer_fn or extract_answer
|
|
93
|
+
self.match_fn = match_fn or answer_equal
|
|
94
|
+
|
|
95
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
96
|
+
if self.generate_missing and Capability.GENERATE not in getattr(
|
|
97
|
+
model, "capabilities", frozenset()
|
|
98
|
+
):
|
|
99
|
+
raise CapabilityError(
|
|
100
|
+
analyzer=self.name, model=repr(model), missing={Capability.GENERATE}
|
|
101
|
+
)
|
|
102
|
+
per_case: list[dict[str, Any]] = []
|
|
103
|
+
for case in cases.stratified_head(self.max_cases):
|
|
104
|
+
text = case.observed
|
|
105
|
+
if not str(text or "").strip() and self.generate_missing:
|
|
106
|
+
prompt = f"{case.inputs.prompt or ''}\n\n{_COT_SUFFIX}"
|
|
107
|
+
text = str(model.generate(dataclasses.replace(case.inputs, prompt=prompt)))
|
|
108
|
+
per_case.append(self._audit(case, text))
|
|
109
|
+
|
|
110
|
+
classified = [c for c in per_case if c.get("error_class") not in (None, UNKNOWN)]
|
|
111
|
+
wrong = [c for c in classified if c["answer_correct"] == 0]
|
|
112
|
+
checked = [c for c in per_case if c.get("n_equations", 0) > 0]
|
|
113
|
+
|
|
114
|
+
def _rate(cls: str) -> Optional[float]:
|
|
115
|
+
return round(sum(1 for c in wrong if c["error_class"] == cls) / len(wrong), 4) \
|
|
116
|
+
if wrong else None
|
|
117
|
+
|
|
118
|
+
findings: dict[str, Any] = {
|
|
119
|
+
"n_cases": len(per_case),
|
|
120
|
+
"n_with_equations": len(checked),
|
|
121
|
+
"n_wrong_answers": len(wrong),
|
|
122
|
+
"mean_arith_error_rate": (
|
|
123
|
+
round(sum(c["arith_error_rate"] for c in checked) / len(checked), 4)
|
|
124
|
+
if checked
|
|
125
|
+
else None
|
|
126
|
+
),
|
|
127
|
+
"computation_slip_rate": _rate(SLIP),
|
|
128
|
+
"chain_break_rate": _rate(CHAIN_BREAK),
|
|
129
|
+
"compound_rate": _rate(COMPOUND),
|
|
130
|
+
"n_errors_but_correct": sum(1 for c in classified if c["error_class"] == LUCKY),
|
|
131
|
+
"per_case": per_case,
|
|
132
|
+
"_caveat": (
|
|
133
|
+
"The slip/chain-break split is only defined over cases whose "
|
|
134
|
+
"reasoning actually WRITES its arithmetic: n_equations==0 means "
|
|
135
|
+
"'not measured', not 'no arithmetic errors' — a model that "
|
|
136
|
+
"computes silently is unmeasurable here, so read "
|
|
137
|
+
"n_with_equations before the rates. computation_slip ⇒ an L1 "
|
|
138
|
+
"tool/verification fix; chain_break ⇒ decomposition, where a "
|
|
139
|
+
"calculator changes nothing. errors_but_correct is its own "
|
|
140
|
+
"signal: arithmetic that is wrong yet lands on the gold answer "
|
|
141
|
+
"means the written chain is decorative (cross-check with "
|
|
142
|
+
"cot_faithfulness). OBSERVATIONAL — it re-reads stored outputs "
|
|
143
|
+
"and adds no sampling, so it needs no re-run to confirm."
|
|
144
|
+
),
|
|
145
|
+
}
|
|
146
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
def _audit(self, case: "FailureCase", text: Any) -> dict[str, Any]:
|
|
150
|
+
equations = find_equations(text)
|
|
151
|
+
errors = [
|
|
152
|
+
(i, expr, stated, computed)
|
|
153
|
+
for i, (expr, stated, computed) in enumerate(equations)
|
|
154
|
+
if not _close(stated, computed, self.rel_tol)
|
|
155
|
+
]
|
|
156
|
+
entry: dict[str, Any] = {
|
|
157
|
+
"sample_id": case.id,
|
|
158
|
+
"n_equations": len(equations),
|
|
159
|
+
"n_arith_errors": len(errors),
|
|
160
|
+
"arith_error_rate": (
|
|
161
|
+
round(len(errors) / len(equations), 4) if equations else 0.0
|
|
162
|
+
),
|
|
163
|
+
}
|
|
164
|
+
if errors:
|
|
165
|
+
idx, expr, stated, computed = errors[0]
|
|
166
|
+
entry.update(
|
|
167
|
+
{
|
|
168
|
+
"first_error_idx": idx,
|
|
169
|
+
"first_error_expr": expr[:120],
|
|
170
|
+
"first_error_stated": stated,
|
|
171
|
+
"first_error_correct": computed,
|
|
172
|
+
# how deep into the written chain the first slip happened
|
|
173
|
+
"first_error_depth": round(idx / len(equations), 4),
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
predicted = self.answer_fn(text)
|
|
178
|
+
graded = (
|
|
179
|
+
bool(self.match_fn(predicted, case.expected))
|
|
180
|
+
if case.expected is not None
|
|
181
|
+
else None
|
|
182
|
+
)
|
|
183
|
+
if graded is None:
|
|
184
|
+
entry["error_class"] = UNKNOWN
|
|
185
|
+
entry["_note"] = "no gold answer: arithmetic checked, failure not classified"
|
|
186
|
+
return entry
|
|
187
|
+
|
|
188
|
+
entry["answer_correct"] = int(graded)
|
|
189
|
+
entry["predicted_answer"] = str(predicted)[:120]
|
|
190
|
+
if graded:
|
|
191
|
+
entry["error_class"] = LUCKY if errors else CLEAN
|
|
192
|
+
return entry
|
|
193
|
+
if not errors:
|
|
194
|
+
entry["error_class"] = CHAIN_BREAK if equations else UNKNOWN
|
|
195
|
+
return entry
|
|
196
|
+
|
|
197
|
+
explains = self._slip_explains_final(errors[0], predicted, case.expected)
|
|
198
|
+
entry["slip_explains_final"] = int(explains)
|
|
199
|
+
entry["error_class"] = SLIP if explains else COMPOUND
|
|
200
|
+
return entry
|
|
201
|
+
|
|
202
|
+
def _slip_explains_final(
|
|
203
|
+
self, error: tuple[int, str, float, float], predicted: Any, gold: Any
|
|
204
|
+
) -> bool:
|
|
205
|
+
"""Would repairing the FIRST wrong value have produced the gold answer?
|
|
206
|
+
|
|
207
|
+
Three propagation paths cover the realistic cases without re-running the
|
|
208
|
+
model: the repaired value IS the answer; the error carried through
|
|
209
|
+
additively; or it carried through multiplicatively.
|
|
210
|
+
"""
|
|
211
|
+
_, _, stated, computed = error
|
|
212
|
+
gold_num = as_number(gold)
|
|
213
|
+
if gold_num is None:
|
|
214
|
+
return False
|
|
215
|
+
tol = self.rel_tol
|
|
216
|
+
if _close(computed, gold_num, tol):
|
|
217
|
+
return True
|
|
218
|
+
final_numbers = numbers_in(predicted)
|
|
219
|
+
if not final_numbers:
|
|
220
|
+
return False
|
|
221
|
+
final = final_numbers[-1]
|
|
222
|
+
if _close(final + (computed - stated), gold_num, tol):
|
|
223
|
+
return True
|
|
224
|
+
if stated != 0 and _close(final * (computed / stated), gold_num, tol):
|
|
225
|
+
return True
|
|
226
|
+
return False
|