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,246 @@
|
|
|
1
|
+
"""Self-repair — can the model detect its own error, and can it fix it without breaking what worked?
|
|
2
|
+
|
|
3
|
+
Three capabilities get conflated as "self-correction", and they dissociate:
|
|
4
|
+
|
|
5
|
+
* **detection** — does the model know the answer is wrong when asked?
|
|
6
|
+
* **correction** — given that it says so, does the revision land on the gold?
|
|
7
|
+
* **damage** — does the same revision pass BREAK answers that were already right?
|
|
8
|
+
|
|
9
|
+
The third is the one that decides deployment. A self-critique loop with a 40%
|
|
10
|
+
repair rate and a 25% damage rate is a net loss on any batch that is mostly
|
|
11
|
+
passing, and reporting repair alone hides that completely — which is why this
|
|
12
|
+
probe deliberately spends its generations on PASS cases too.
|
|
13
|
+
|
|
14
|
+
Cost: 2 generations per case (critique + blind revision), 3 with
|
|
15
|
+
``revise_with_critique=True``.
|
|
16
|
+
|
|
17
|
+
References:
|
|
18
|
+
- Large Language Models Cannot Self-Correct Reasoning Yet — Huang et al.,
|
|
19
|
+
ICLR 2024 — arXiv:2310.01798 (intrinsic self-correction degrades accuracy)
|
|
20
|
+
- Self-Refine: Iterative Refinement with Self-Feedback — Madaan et al.,
|
|
21
|
+
NeurIPS 2023 — arXiv:2303.17651
|
|
22
|
+
- Teaching Large Language Models to Self-Debug — Chen et al., ICLR 2024 —
|
|
23
|
+
arXiv:2304.05128
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import dataclasses
|
|
29
|
+
import re
|
|
30
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
31
|
+
|
|
32
|
+
from evalrx.analyzers.reasoning._text import (
|
|
33
|
+
answer_equal,
|
|
34
|
+
extract_answer,
|
|
35
|
+
normalize_answer,
|
|
36
|
+
)
|
|
37
|
+
from evalrx.core.analyzer import Analyzer
|
|
38
|
+
from evalrx.core.capability import Capability
|
|
39
|
+
from evalrx.core.registry import register_analyzer
|
|
40
|
+
from evalrx.core.result import Result
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from evalrx.core.case import CaseBatch, FailureCase
|
|
44
|
+
from evalrx.core.model import Model
|
|
45
|
+
|
|
46
|
+
_CRITIQUE = (
|
|
47
|
+
"Here is a candidate answer to the question above:\n\n{answer}\n\n"
|
|
48
|
+
"Is this answer correct? Reply with exactly one word: CORRECT or INCORRECT."
|
|
49
|
+
)
|
|
50
|
+
_REVISE_BLIND = (
|
|
51
|
+
"Here is a candidate answer to the question above:\n\n{answer}\n\n"
|
|
52
|
+
"Review it and produce your best final answer. Give the answer on its own "
|
|
53
|
+
"last line as 'Answer: <answer>'."
|
|
54
|
+
)
|
|
55
|
+
_REVISE_TOLD = (
|
|
56
|
+
"Here is a candidate answer to the question above:\n\n{answer}\n\n"
|
|
57
|
+
"This answer contains an error. Find it and produce a corrected final "
|
|
58
|
+
"answer on its own last line as 'Answer: <answer>'."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@register_analyzer("self_repair")
|
|
63
|
+
class SelfRepairAnalyzer(Analyzer):
|
|
64
|
+
"""Measure self-detection, repair rate, and — the part that decides deployment — damage rate.
|
|
65
|
+
|
|
66
|
+
Hyper-parameters:
|
|
67
|
+
revise_with_critique: also run the "you made an error" revision
|
|
68
|
+
(+1 generation; measures repair under an oracle
|
|
69
|
+
error signal the deployed loop will not have).
|
|
70
|
+
max_cases: label-stratified cap; 0 (the default) = every case.
|
|
71
|
+
PASS cases are REQUIRED here, they are what makes
|
|
72
|
+
damage measurable.
|
|
73
|
+
generate_missing: produce a baseline answer when ``observed`` is empty.
|
|
74
|
+
grader: ``callable(prediction, case) -> bool | None``.
|
|
75
|
+
answer_fn: ``callable(text) -> str`` answer extractor.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
name = "self_repair"
|
|
79
|
+
requires = frozenset({Capability.GENERATE})
|
|
80
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
revise_with_critique: bool = False,
|
|
85
|
+
max_cases: int = 0,
|
|
86
|
+
generate_missing: bool = True,
|
|
87
|
+
grader: Optional[Callable[[Any, "FailureCase"], Optional[bool]]] = None,
|
|
88
|
+
answer_fn: Optional[Callable[[Any], str]] = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
super().__init__(
|
|
91
|
+
revise_with_critique=revise_with_critique,
|
|
92
|
+
max_cases=max_cases,
|
|
93
|
+
generate_missing=generate_missing,
|
|
94
|
+
)
|
|
95
|
+
# ctor names, so sklearn-style get_params() reflection works
|
|
96
|
+
self.grader = grader or _default_grader
|
|
97
|
+
self.answer_fn = answer_fn or extract_answer
|
|
98
|
+
|
|
99
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
100
|
+
per_case: list[dict[str, Any]] = []
|
|
101
|
+
for case in cases.stratified_head(self.max_cases):
|
|
102
|
+
baseline = case.observed
|
|
103
|
+
if not str(baseline or "").strip():
|
|
104
|
+
if not self.generate_missing:
|
|
105
|
+
per_case.append(
|
|
106
|
+
{"sample_id": case.id, "skipped": "no stored answer to revise"}
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
baseline = str(model.generate(case.inputs))
|
|
110
|
+
entry = self._probe_case(model, case, str(baseline))
|
|
111
|
+
per_case.append(entry)
|
|
112
|
+
|
|
113
|
+
graded = [c for c in per_case if "baseline_correct" in c]
|
|
114
|
+
failed = [c for c in graded if c["baseline_correct"] == 0]
|
|
115
|
+
passed = [c for c in graded if c["baseline_correct"] == 1]
|
|
116
|
+
# A real model sometimes answers the critique in prose that parses to
|
|
117
|
+
# nothing; those cases carry self_says_incorrect=None and must be
|
|
118
|
+
# dropped from the detection rates rather than counted as "said correct".
|
|
119
|
+
detected = [c for c in graded if c.get("detection_correct") is not None]
|
|
120
|
+
verdicts = [c for c in passed if c.get("self_says_incorrect") is not None]
|
|
121
|
+
|
|
122
|
+
repair_rate = (
|
|
123
|
+
round(sum(c["repaired"] for c in failed) / len(failed), 4) if failed else None
|
|
124
|
+
)
|
|
125
|
+
damage_rate = (
|
|
126
|
+
round(sum(c["damaged"] for c in passed) / len(passed), 4) if passed else None
|
|
127
|
+
)
|
|
128
|
+
findings: dict[str, Any] = {
|
|
129
|
+
"n_cases": len(per_case),
|
|
130
|
+
"n_graded": len(graded),
|
|
131
|
+
"n_baseline_fail": len(failed),
|
|
132
|
+
"n_baseline_pass": len(passed),
|
|
133
|
+
"detection_accuracy": (
|
|
134
|
+
round(sum(c["detection_correct"] for c in detected) / len(detected), 4)
|
|
135
|
+
if detected
|
|
136
|
+
else None
|
|
137
|
+
),
|
|
138
|
+
"n_critique_unparsed": sum(
|
|
139
|
+
1 for c in graded if c.get("self_says_incorrect") is None
|
|
140
|
+
),
|
|
141
|
+
"false_alarm_rate": (
|
|
142
|
+
round(sum(c["self_says_incorrect"] for c in verdicts) / len(verdicts), 4)
|
|
143
|
+
if verdicts
|
|
144
|
+
else None
|
|
145
|
+
),
|
|
146
|
+
"repair_rate": repair_rate,
|
|
147
|
+
"damage_rate": damage_rate,
|
|
148
|
+
"net_revision_gain": (
|
|
149
|
+
round(
|
|
150
|
+
(sum(c["repaired"] for c in failed) - sum(c["damaged"] for c in passed))
|
|
151
|
+
/ len(graded),
|
|
152
|
+
4,
|
|
153
|
+
)
|
|
154
|
+
if graded
|
|
155
|
+
else None
|
|
156
|
+
),
|
|
157
|
+
"per_case": per_case,
|
|
158
|
+
"_caveat": (
|
|
159
|
+
"repair_rate WITHOUT damage_rate is not a result: a revision "
|
|
160
|
+
"pass is only a fix if net_revision_gain > 0 on the deployed "
|
|
161
|
+
"PASS/FAIL mix, and this batch is label-stratified, so its mix "
|
|
162
|
+
"is NOT the deployment mix — re-weight before promoting an L1 "
|
|
163
|
+
"self-critique fix. Cases needing PASS mass: damage_rate is "
|
|
164
|
+
"None when the batch has no passing cases, and a repair-only "
|
|
165
|
+
"reading is then unsafe. With revise_with_critique=True the "
|
|
166
|
+
"told_* columns use an ORACLE error signal (we told it there is "
|
|
167
|
+
"an error) and overstate what a deployed loop achieves. "
|
|
168
|
+
"INTERVENTIONAL — held-out confirmation must re-run every call."
|
|
169
|
+
),
|
|
170
|
+
}
|
|
171
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
def _probe_case(self, model: "Model", case: "FailureCase", baseline: str) -> dict[str, Any]:
|
|
175
|
+
entry: dict[str, Any] = {"sample_id": case.id}
|
|
176
|
+
base_grade = self.grader(baseline, case)
|
|
177
|
+
|
|
178
|
+
critique = str(model.generate(self._ask(case, _CRITIQUE, baseline)))
|
|
179
|
+
says_incorrect = _parse_verdict(critique)
|
|
180
|
+
revised = str(model.generate(self._ask(case, _REVISE_BLIND, baseline)))
|
|
181
|
+
revised_grade = self.grader(revised, case)
|
|
182
|
+
|
|
183
|
+
entry["self_says_incorrect"] = (
|
|
184
|
+
int(says_incorrect) if says_incorrect is not None else None
|
|
185
|
+
)
|
|
186
|
+
entry["critique_parsed"] = int(says_incorrect is not None)
|
|
187
|
+
entry["changed_answer"] = int(
|
|
188
|
+
normalize_answer(self.answer_fn(revised))
|
|
189
|
+
!= normalize_answer(self.answer_fn(baseline))
|
|
190
|
+
)
|
|
191
|
+
if base_grade is None or revised_grade is None:
|
|
192
|
+
entry["skipped"] = "ungradable (no gold answer)"
|
|
193
|
+
return entry
|
|
194
|
+
|
|
195
|
+
entry.update(
|
|
196
|
+
{
|
|
197
|
+
"baseline_correct": int(base_grade),
|
|
198
|
+
"revised_correct": int(revised_grade),
|
|
199
|
+
"repaired": int(not base_grade and revised_grade),
|
|
200
|
+
"damaged": int(base_grade and not revised_grade),
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
if says_incorrect is not None:
|
|
204
|
+
entry["detection_correct"] = int(says_incorrect == (not base_grade))
|
|
205
|
+
|
|
206
|
+
if self.revise_with_critique:
|
|
207
|
+
told = str(model.generate(self._ask(case, _REVISE_TOLD, baseline)))
|
|
208
|
+
told_grade = self.grader(told, case)
|
|
209
|
+
if told_grade is not None:
|
|
210
|
+
entry["told_correct"] = int(told_grade)
|
|
211
|
+
entry["told_repaired"] = int(not base_grade and told_grade)
|
|
212
|
+
entry["told_damaged"] = int(base_grade and not told_grade)
|
|
213
|
+
return entry
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _ask(case: "FailureCase", template: str, answer: str):
|
|
217
|
+
prompt = f"{case.inputs.prompt or ''}\n\n{template.format(answer=answer)}"
|
|
218
|
+
return dataclasses.replace(case.inputs, prompt=prompt)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _default_grader(prediction: Any, case: "FailureCase") -> Optional[bool]:
|
|
222
|
+
if case.expected is None:
|
|
223
|
+
return None
|
|
224
|
+
return answer_equal(extract_answer(prediction), case.expected)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
#: Negated forms matter: "not correct" is a verdict of INCORRECT, and a plain
|
|
228
|
+
#: search for "correct" reads it backwards. ``\bcorrect\b`` cannot match inside
|
|
229
|
+
#: "incorrect" (no word boundary there), so the two patterns stay disjoint.
|
|
230
|
+
_SAYS_INCORRECT = re.compile(
|
|
231
|
+
r"\b(?:incorrect|wrong|false|not\s+(?:correct|right)|isn'?t\s+(?:correct|right))\b"
|
|
232
|
+
)
|
|
233
|
+
_SAYS_CORRECT = re.compile(r"\b(?:correct|right|yes)\b")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _parse_verdict(text: Any) -> Optional[bool]:
|
|
237
|
+
"""``True`` = the model called the answer incorrect; ``None`` = unparseable."""
|
|
238
|
+
lowered = str(text or "").lower()
|
|
239
|
+
# Take whichever verdict appears FIRST — models restate the question
|
|
240
|
+
# ("is this correct? ...") before delivering the verdict.
|
|
241
|
+
hits: list[tuple[int, bool]] = [
|
|
242
|
+
(m.start(), True) for m in _SAYS_INCORRECT.finditer(lowered)
|
|
243
|
+
] + [(m.start(), False) for m in _SAYS_CORRECT.finditer(lowered)]
|
|
244
|
+
if not hits:
|
|
245
|
+
return None
|
|
246
|
+
return min(hits)[1]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Step-value rollouts — *where* did the reasoning chain break?
|
|
2
|
+
|
|
3
|
+
Knowing that a chain failed is not actionable; knowing it was already doomed at
|
|
4
|
+
step 2 is. Math-Shepherd's insight is that a step's value can be estimated
|
|
5
|
+
without any human step labels: continue from that prefix ``n`` times and count
|
|
6
|
+
how often the completion reaches the gold answer. The step where that value
|
|
7
|
+
collapses is the break point — everything after it is downstream noise, and a
|
|
8
|
+
fix targeted at the last visible mistake is aimed at the wrong place.
|
|
9
|
+
|
|
10
|
+
This is the text counterpart of the agent-side ``first_error_judge``: same
|
|
11
|
+
question (which step is the first error?), answered by rollout statistics
|
|
12
|
+
instead of an LLM judge, so it carries no judge bias — at the cost of
|
|
13
|
+
``n_steps × n_rollouts`` generations — on a large batch pass ``max_cases`` to
|
|
14
|
+
bound it (the default, like every analyzer, measures every case).
|
|
15
|
+
|
|
16
|
+
References:
|
|
17
|
+
- Math-Shepherd: Verify and Reinforce LLMs Step-by-step without Human
|
|
18
|
+
Annotations — Wang et al., ACL 2024 — arXiv:2312.08935
|
|
19
|
+
- Let's Verify Step by Step — Lightman et al., ICLR 2024 — arXiv:2305.20050
|
|
20
|
+
- Improve Mathematical Reasoning with Process Supervision — Luo et al., 2024 —
|
|
21
|
+
arXiv:2406.06592 (binary-search variant of the same estimator)
|
|
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 answer_equal, extract_answer
|
|
31
|
+
from evalrx.core.analyzer import Analyzer
|
|
32
|
+
from evalrx.core.capability import Capability
|
|
33
|
+
from evalrx.core.registry import register_analyzer
|
|
34
|
+
from evalrx.core.result import Result
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from evalrx.core.case import CaseBatch, FailureCase
|
|
38
|
+
from evalrx.core.model import Model
|
|
39
|
+
|
|
40
|
+
_COT_SUFFIX = (
|
|
41
|
+
"Think step by step, one step per line. After the reasoning, give the final "
|
|
42
|
+
"answer on its own last line as 'Answer: <answer>'."
|
|
43
|
+
)
|
|
44
|
+
_CONTINUE = (
|
|
45
|
+
"Reasoning so far:\n{prefix}\n\nContinue from here and finish the solution. "
|
|
46
|
+
"Give the final answer on its own last line as 'Answer: <answer>'."
|
|
47
|
+
)
|
|
48
|
+
_ANSWER_LINE = re.compile(r"^\s*(?:final\s+)?answer\s*[:=]", re.IGNORECASE)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def split_steps(text: Any, max_steps: int) -> list[str]:
|
|
52
|
+
"""Split a chain into reasoning steps (lines first, sentences as fallback)."""
|
|
53
|
+
raw = str(text or "")
|
|
54
|
+
lines = [ln.strip() for ln in raw.splitlines()]
|
|
55
|
+
steps = [ln for ln in lines if ln and not _ANSWER_LINE.match(ln)]
|
|
56
|
+
if len(steps) < 2: # single-paragraph chain — fall back to sentences
|
|
57
|
+
body = _ANSWER_LINE.split(raw)[0]
|
|
58
|
+
steps = [s.strip() for s in re.split(r"(?<=[.!?])\s+", body) if s.strip()]
|
|
59
|
+
if len(steps) <= max_steps:
|
|
60
|
+
return steps
|
|
61
|
+
# keep the shape of the chain: evenly spaced cut points, always incl. the last
|
|
62
|
+
idx = sorted({round(i * (len(steps) - 1) / (max_steps - 1)) for i in range(max_steps)})
|
|
63
|
+
return [steps[i] for i in idx]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@register_analyzer("step_rollout_value")
|
|
67
|
+
class StepRolloutValueAnalyzer(Analyzer):
|
|
68
|
+
"""Estimate a per-step success value by rollouts and locate the break step.
|
|
69
|
+
|
|
70
|
+
Hyper-parameters:
|
|
71
|
+
n_rollouts: completions sampled per step prefix.
|
|
72
|
+
max_steps: cap on probed steps (the chain is subsampled evenly).
|
|
73
|
+
drop_threshold: value drop between consecutive steps that counts as the break.
|
|
74
|
+
max_cases: label-stratified cap — cost is n_steps × n_rollouts each; 0 (the default) = every case.
|
|
75
|
+
gen_kwargs: passed to ``model.generate`` (rollouts NEED temperature > 0;
|
|
76
|
+
a deterministic model makes every rollout identical and the
|
|
77
|
+
values collapse to 0/1 — the caveat says so).
|
|
78
|
+
grader/answer_fn: grading of each rollout against ``case.expected``.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name = "step_rollout_value"
|
|
82
|
+
requires = frozenset({Capability.GENERATE})
|
|
83
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
n_rollouts: int = 3,
|
|
88
|
+
max_steps: int = 7,
|
|
89
|
+
drop_threshold: float = 0.34,
|
|
90
|
+
max_cases: int = 0,
|
|
91
|
+
gen_kwargs: Optional[dict] = None,
|
|
92
|
+
grader: Optional[Callable[[Any, "FailureCase"], Optional[bool]]] = None,
|
|
93
|
+
answer_fn: Optional[Callable[[Any], str]] = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
super().__init__(
|
|
96
|
+
n_rollouts=n_rollouts,
|
|
97
|
+
max_steps=max_steps,
|
|
98
|
+
drop_threshold=drop_threshold,
|
|
99
|
+
max_cases=max_cases,
|
|
100
|
+
gen_kwargs=dict(gen_kwargs or {}),
|
|
101
|
+
)
|
|
102
|
+
self.grader = grader or _default_grader
|
|
103
|
+
self.answer_fn = answer_fn or extract_answer
|
|
104
|
+
|
|
105
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
106
|
+
per_case: list[dict[str, Any]] = []
|
|
107
|
+
step_values_by_case: dict[str, list[float]] = {}
|
|
108
|
+
for case in cases.stratified_head(self.max_cases):
|
|
109
|
+
if case.expected is None:
|
|
110
|
+
per_case.append({"sample_id": case.id, "skipped": "no gold answer"})
|
|
111
|
+
continue
|
|
112
|
+
chain = case.observed
|
|
113
|
+
if not str(chain or "").strip():
|
|
114
|
+
chain = str(
|
|
115
|
+
model.generate(
|
|
116
|
+
dataclasses.replace(
|
|
117
|
+
case.inputs, prompt=f"{case.inputs.prompt or ''}\n\n{_COT_SUFFIX}"
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
entry = self._probe_case(model, case, str(chain))
|
|
122
|
+
# Contract: a per-case row carries one level of scalars — a numeric
|
|
123
|
+
# vector there looks like a signal and reaches no statistic. The
|
|
124
|
+
# trajectory moves to findings["step_values_by_case"]; every scalar
|
|
125
|
+
# the stats read (initial/final/min/max_value_drop/break_step_idx)
|
|
126
|
+
# stays on the row.
|
|
127
|
+
values = entry.pop("step_values", None)
|
|
128
|
+
if values is not None:
|
|
129
|
+
step_values_by_case[entry["sample_id"]] = values
|
|
130
|
+
per_case.append(entry)
|
|
131
|
+
|
|
132
|
+
scored = [c for c in per_case if "break_step_idx" in c]
|
|
133
|
+
breaks = [c["break_step_idx"] for c in scored if c["break_step_idx"] is not None]
|
|
134
|
+
findings: dict[str, Any] = {
|
|
135
|
+
"n_cases": len(per_case),
|
|
136
|
+
"n_scored": len(scored),
|
|
137
|
+
"n_rollouts": self.n_rollouts,
|
|
138
|
+
"gen_kwargs": dict(self.gen_kwargs),
|
|
139
|
+
"mean_initial_value": _mean([c["initial_value"] for c in scored]),
|
|
140
|
+
"mean_final_value": _mean([c["final_value"] for c in scored]),
|
|
141
|
+
"mean_break_depth": _mean([c["break_depth"] for c in scored
|
|
142
|
+
if c.get("break_depth") is not None]),
|
|
143
|
+
"n_with_break": len(breaks),
|
|
144
|
+
"per_case": per_case,
|
|
145
|
+
"step_values_by_case": step_values_by_case,
|
|
146
|
+
"_caveat": (
|
|
147
|
+
"Step values are Monte-Carlo estimates from n_rollouts "
|
|
148
|
+
"completions: with n_rollouts=3 a value is one of {0, .33, .67, "
|
|
149
|
+
"1} and a single-step 'drop' of 0.33 is within sampling noise — "
|
|
150
|
+
"raise n_rollouts before trusting a break_step_idx on an "
|
|
151
|
+
"individual case, and prefer the batch-level mean_break_depth. "
|
|
152
|
+
"The estimator REQUIRES sampling: at temperature 0 every rollout "
|
|
153
|
+
"from a prefix is identical, values are 0/1, and break_step_idx "
|
|
154
|
+
"degenerates into 'the first step whose continuation is wrong'. "
|
|
155
|
+
"initial_value is the model's own unconditional success rate — a "
|
|
156
|
+
"chain that starts near 0 has no break point to find, it never "
|
|
157
|
+
"worked. INTERVENTIONAL: held-out confirmation must re-roll."
|
|
158
|
+
),
|
|
159
|
+
}
|
|
160
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
def _probe_case(self, model: "Model", case: "FailureCase", chain: str) -> dict[str, Any]:
|
|
164
|
+
steps = split_steps(chain, self.max_steps)
|
|
165
|
+
entry: dict[str, Any] = {"sample_id": case.id, "n_steps": len(steps)}
|
|
166
|
+
if not steps:
|
|
167
|
+
entry["skipped"] = "no reasoning steps to roll out from"
|
|
168
|
+
return entry
|
|
169
|
+
|
|
170
|
+
values: list[float] = []
|
|
171
|
+
for i in range(len(steps)):
|
|
172
|
+
prefix = "\n".join(steps[: i + 1])
|
|
173
|
+
prompt = f"{case.inputs.prompt or ''}\n\n{_CONTINUE.format(prefix=prefix)}"
|
|
174
|
+
hits = 0
|
|
175
|
+
graded = 0
|
|
176
|
+
for _ in range(self.n_rollouts):
|
|
177
|
+
out = model.generate(
|
|
178
|
+
dataclasses.replace(case.inputs, prompt=prompt), **self.gen_kwargs
|
|
179
|
+
)
|
|
180
|
+
verdict = self.grader(out, case)
|
|
181
|
+
if verdict is None:
|
|
182
|
+
continue
|
|
183
|
+
graded += 1
|
|
184
|
+
hits += int(verdict)
|
|
185
|
+
values.append(round(hits / graded, 4) if graded else 0.0)
|
|
186
|
+
|
|
187
|
+
entry["step_values"] = values
|
|
188
|
+
entry["initial_value"] = values[0]
|
|
189
|
+
entry["final_value"] = values[-1]
|
|
190
|
+
entry["min_value"] = min(values)
|
|
191
|
+
drops = [values[i - 1] - values[i] for i in range(1, len(values))]
|
|
192
|
+
worst = max(drops) if drops else 0.0
|
|
193
|
+
entry["max_value_drop"] = round(worst, 4)
|
|
194
|
+
break_idx = next(
|
|
195
|
+
(i + 1 for i, d in enumerate(drops) if d >= self.drop_threshold), None
|
|
196
|
+
)
|
|
197
|
+
entry["break_step_idx"] = break_idx
|
|
198
|
+
entry["break_depth"] = (
|
|
199
|
+
round(break_idx / max(len(steps) - 1, 1), 4) if break_idx is not None else None
|
|
200
|
+
)
|
|
201
|
+
entry["break_step_text"] = steps[break_idx][:200] if break_idx is not None else None
|
|
202
|
+
# A chain whose value never recovers is unsalvageable by continuation;
|
|
203
|
+
# one that dips and recovers is a detour, not a break.
|
|
204
|
+
entry["recoverable"] = int(values[-1] > 0)
|
|
205
|
+
return entry
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _mean(values: list) -> Optional[float]:
|
|
209
|
+
clean = [v for v in values if v is not None]
|
|
210
|
+
return round(sum(clean) / len(clean), 4) if clean else None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _default_grader(prediction: Any, case: "FailureCase") -> Optional[bool]:
|
|
214
|
+
if case.expected is None:
|
|
215
|
+
return None
|
|
216
|
+
return answer_equal(extract_answer(prediction), case.expected)
|