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.
Files changed (339) hide show
  1. evalrx/__init__.py +139 -0
  2. evalrx/agent_assets/__init__.py +2 -0
  3. evalrx/agent_assets/skills/README.md +28 -0
  4. evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
  5. evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
  6. evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
  7. evalrx/agent_assets/skills/nature-figure/README.md +412 -0
  8. evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
  9. evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
  10. evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
  11. evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
  12. evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
  13. evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
  14. evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
  15. evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
  16. evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
  17. evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
  18. evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
  19. evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
  20. evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
  21. evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
  22. evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
  23. evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
  24. evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
  25. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
  26. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
  27. evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
  28. evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
  29. evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
  30. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
  31. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
  32. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
  33. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
  34. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
  35. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
  36. evalrx/agent_assets/skills.py +27 -0
  37. evalrx/agent_runtime/__init__.py +78 -0
  38. evalrx/agent_runtime/_docker_runner.py +89 -0
  39. evalrx/agent_runtime/cli_runtime.py +103 -0
  40. evalrx/agent_runtime/cli_transcript.py +138 -0
  41. evalrx/agent_runtime/cli_types.py +68 -0
  42. evalrx/agent_runtime/codegen/__init__.py +5 -0
  43. evalrx/agent_runtime/codegen/runner.py +94 -0
  44. evalrx/agent_runtime/experiment_harness.py +117 -0
  45. evalrx/agent_runtime/factory.py +102 -0
  46. evalrx/agent_runtime/json_shape.py +44 -0
  47. evalrx/agent_runtime/judges/__init__.py +28 -0
  48. evalrx/agent_runtime/judges/agy.py +179 -0
  49. evalrx/agent_runtime/judges/autodetect.py +135 -0
  50. evalrx/agent_runtime/judges/claude.py +159 -0
  51. evalrx/agent_runtime/judges/codex.py +120 -0
  52. evalrx/agent_runtime/providers/__init__.py +21 -0
  53. evalrx/agent_runtime/providers/antigravity.py +31 -0
  54. evalrx/agent_runtime/providers/base.py +145 -0
  55. evalrx/agent_runtime/providers/claude_code.py +49 -0
  56. evalrx/agent_runtime/providers/codex.py +37 -0
  57. evalrx/agent_runtime/providers/gemini_cli.py +26 -0
  58. evalrx/agent_runtime/providers/kimi_cli.py +27 -0
  59. evalrx/agent_runtime/providers/opencode.py +27 -0
  60. evalrx/agent_runtime/providers/registry.py +58 -0
  61. evalrx/agent_runtime/sandbox.py +517 -0
  62. evalrx/agent_runtime/skill_audit.py +143 -0
  63. evalrx/agent_runtime/skills/__init__.py +19 -0
  64. evalrx/agent_runtime/skills/installer.py +68 -0
  65. evalrx/agent_runtime/skills/prompt_policy.py +86 -0
  66. evalrx/agent_runtime/skills/resolver.py +19 -0
  67. evalrx/analysis/__init__.py +132 -0
  68. evalrx/analysis/adjudicate.py +154 -0
  69. evalrx/analysis/analysis_module.py +361 -0
  70. evalrx/analysis/api.py +171 -0
  71. evalrx/analysis/case_studio.py +651 -0
  72. evalrx/analysis/cli.py +114 -0
  73. evalrx/analysis/dashboard.py +350 -0
  74. evalrx/analysis/eval_case_matrix.py +118 -0
  75. evalrx/analysis/eval_viz_theme.py +833 -0
  76. evalrx/analysis/explore_run.py +333 -0
  77. evalrx/analysis/explorer.py +1276 -0
  78. evalrx/analysis/failure_modes.py +607 -0
  79. evalrx/analysis/fused_pipeline.py +489 -0
  80. evalrx/analysis/holdout.py +300 -0
  81. evalrx/analysis/hypothesis_agent.py +230 -0
  82. evalrx/analysis/narration.py +177 -0
  83. evalrx/analysis/operationalize.py +442 -0
  84. evalrx/analysis/plain_language.py +42 -0
  85. evalrx/analysis/planner.py +283 -0
  86. evalrx/analysis/probe_search.py +203 -0
  87. evalrx/analysis/profile.py +268 -0
  88. evalrx/analysis/prompts/__init__.py +0 -0
  89. evalrx/analysis/prompts/explorer.py +417 -0
  90. evalrx/analysis/prompts/failure_modes.py +33 -0
  91. evalrx/analysis/prompts/holdout.py +27 -0
  92. evalrx/analysis/prompts/hypothesis_agent.py +78 -0
  93. evalrx/analysis/prompts/run_codebase.py +47 -0
  94. evalrx/analysis/prompts/stats_agent.py +72 -0
  95. evalrx/analysis/prompts/stats_tool_generator.py +43 -0
  96. evalrx/analysis/result_marker.py +47 -0
  97. evalrx/analysis/run_codebase.py +242 -0
  98. evalrx/analysis/run_view.py +205 -0
  99. evalrx/analysis/stage_views.py +93 -0
  100. evalrx/analysis/stats_agent.py +944 -0
  101. evalrx/analysis/stats_tool_agent.py +261 -0
  102. evalrx/analysis/stats_tool_generator.py +415 -0
  103. evalrx/analysis/stats_tools.py +1153 -0
  104. evalrx/analysis/trajectory_records.py +193 -0
  105. evalrx/analysis/workbench.py +431 -0
  106. evalrx/analyzers/__init__.py +42 -0
  107. evalrx/analyzers/agent/__init__.py +25 -0
  108. evalrx/analyzers/agent/counterfactual.py +84 -0
  109. evalrx/analyzers/agent/first_error_judge.py +96 -0
  110. evalrx/analyzers/agent/ignored_obs.py +81 -0
  111. evalrx/analyzers/agent/loop_detect.py +79 -0
  112. evalrx/analyzers/agent/reliability.py +165 -0
  113. evalrx/analyzers/agent/tool_shap.py +225 -0
  114. evalrx/analyzers/agent/trajectory_rubric.py +168 -0
  115. evalrx/analyzers/attention/__init__.py +19 -0
  116. evalrx/analyzers/attention/relative_attn.py +610 -0
  117. evalrx/analyzers/attention/rollout.py +73 -0
  118. evalrx/analyzers/attention/sink.py +56 -0
  119. evalrx/analyzers/attention/summary.py +190 -0
  120. evalrx/analyzers/attribution/__init__.py +6 -0
  121. evalrx/analyzers/attribution/generic_attn.py +31 -0
  122. evalrx/analyzers/attribution/gradcam.py +30 -0
  123. evalrx/analyzers/base.py +12 -0
  124. evalrx/analyzers/geometry/__init__.py +6 -0
  125. evalrx/analyzers/geometry/cka.py +70 -0
  126. evalrx/analyzers/geometry/linear_probe.py +157 -0
  127. evalrx/analyzers/hallucination/__init__.py +9 -0
  128. evalrx/analyzers/hallucination/chair.py +78 -0
  129. evalrx/analyzers/hallucination/opera.py +29 -0
  130. evalrx/analyzers/hallucination/pope.py +119 -0
  131. evalrx/analyzers/hallucination/selfcheck.py +155 -0
  132. evalrx/analyzers/hallucination/vcd.py +29 -0
  133. evalrx/analyzers/lens/__init__.py +7 -0
  134. evalrx/analyzers/lens/layer_contrast.py +133 -0
  135. evalrx/analyzers/lens/logit_lens.py +138 -0
  136. evalrx/analyzers/lens/tuned_lens.py +30 -0
  137. evalrx/analyzers/patching/__init__.py +5 -0
  138. evalrx/analyzers/patching/causal_trace.py +30 -0
  139. evalrx/analyzers/perturbation/__init__.py +23 -0
  140. evalrx/analyzers/perturbation/_shapley.py +54 -0
  141. evalrx/analyzers/perturbation/context_shap.py +174 -0
  142. evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
  143. evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
  144. evalrx/analyzers/perturbation/mm_shap.py +146 -0
  145. evalrx/analyzers/perturbation/modality_ablation.py +196 -0
  146. evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
  147. evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
  148. evalrx/analyzers/perturbation/rise.py +94 -0
  149. evalrx/analyzers/perturbation/vl_shap.py +102 -0
  150. evalrx/analyzers/reasoning/__init__.py +33 -0
  151. evalrx/analyzers/reasoning/_text.py +328 -0
  152. evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
  153. evalrx/analyzers/reasoning/arith_audit.py +226 -0
  154. evalrx/analyzers/reasoning/contamination.py +214 -0
  155. evalrx/analyzers/reasoning/knowledge_split.py +253 -0
  156. evalrx/analyzers/reasoning/self_repair.py +246 -0
  157. evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
  158. evalrx/analyzers/reasoning/termination_audit.py +258 -0
  159. evalrx/analyzers/uncertainty/__init__.py +18 -0
  160. evalrx/analyzers/uncertainty/calibration.py +174 -0
  161. evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
  162. evalrx/analyzers/uncertainty/entropy.py +90 -0
  163. evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
  164. evalrx/analyzers/uncertainty/self_consistency.py +204 -0
  165. evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
  166. evalrx/cli.py +411 -0
  167. evalrx/config.py +77 -0
  168. evalrx/contract/__init__.py +179 -0
  169. evalrx/contract/common.py +452 -0
  170. evalrx/contract/emit.py +948 -0
  171. evalrx/contract/export.py +237 -0
  172. evalrx/contract/m1.py +325 -0
  173. evalrx/contract/m2.py +317 -0
  174. evalrx/contract/m3.py +165 -0
  175. evalrx/contract/m4.py +130 -0
  176. evalrx/contract/m5.py +292 -0
  177. evalrx/contract/methodology.py +76 -0
  178. evalrx/contract/pre_m1.py +58 -0
  179. evalrx/contract/typescript.py +140 -0
  180. evalrx/core/__init__.py +85 -0
  181. evalrx/core/analyzer.py +174 -0
  182. evalrx/core/capability.py +54 -0
  183. evalrx/core/case.py +443 -0
  184. evalrx/core/experiment.py +106 -0
  185. evalrx/core/model.py +198 -0
  186. evalrx/core/pipeline.py +42 -0
  187. evalrx/core/registry.py +142 -0
  188. evalrx/core/result.py +64 -0
  189. evalrx/core/spec.py +173 -0
  190. evalrx/core/tokentype.py +165 -0
  191. evalrx/core/tool.py +92 -0
  192. evalrx/datasets/__init__.py +41 -0
  193. evalrx/datasets/base.py +68 -0
  194. evalrx/datasets/gui_os.py +52 -0
  195. evalrx/datasets/llm_qa.py +57 -0
  196. evalrx/datasets/pure_qa.py +12 -0
  197. evalrx/datasets/vlm_qa.py +695 -0
  198. evalrx/datasets/web_search_qa.py +52 -0
  199. evalrx/eval_agent/__init__.py +341 -0
  200. evalrx/eval_agent/_tools.py +81 -0
  201. evalrx/eval_agent/ab_runner.py +50 -0
  202. evalrx/eval_agent/agentic/__init__.py +43 -0
  203. evalrx/eval_agent/agentic/actions.py +216 -0
  204. evalrx/eval_agent/agentic/board.py +107 -0
  205. evalrx/eval_agent/agentic/loop.py +190 -0
  206. evalrx/eval_agent/agentic/tools.py +538 -0
  207. evalrx/eval_agent/checkpoint.py +57 -0
  208. evalrx/eval_agent/cli_agent.py +59 -0
  209. evalrx/eval_agent/cli_skills.py +5 -0
  210. evalrx/eval_agent/evolution.py +396 -0
  211. evalrx/eval_agent/git_manager.py +215 -0
  212. evalrx/eval_agent/hypothesis.py +172 -0
  213. evalrx/eval_agent/label_quarantine.py +209 -0
  214. evalrx/eval_agent/legacy.py +530 -0
  215. evalrx/eval_agent/log_schema.py +497 -0
  216. evalrx/eval_agent/loop.py +2159 -0
  217. evalrx/eval_agent/loop_reports.py +116 -0
  218. evalrx/eval_agent/model_instrumentation.py +282 -0
  219. evalrx/eval_agent/narration.py +193 -0
  220. evalrx/eval_agent/nl_runner.py +460 -0
  221. evalrx/eval_agent/orchestrator.py +61 -0
  222. evalrx/eval_agent/preregister.py +93 -0
  223. evalrx/eval_agent/prompts/__init__.py +1 -0
  224. evalrx/eval_agent/prompts/agentic.py +46 -0
  225. evalrx/eval_agent/prompts/case_discovery.py +25 -0
  226. evalrx/eval_agent/prompts/diagnosis.py +125 -0
  227. evalrx/eval_agent/prompts/experiment_writer.py +265 -0
  228. evalrx/eval_agent/prompts/explore_step.py +37 -0
  229. evalrx/eval_agent/prompts/fix_agent.py +257 -0
  230. evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
  231. evalrx/eval_agent/prompts/nl_runner.py +38 -0
  232. evalrx/eval_agent/prompts/probe_agent.py +25 -0
  233. evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
  234. evalrx/eval_agent/prompts/probe_generator.py +35 -0
  235. evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
  236. evalrx/eval_agent/report.py +58 -0
  237. evalrx/eval_agent/run_context.py +354 -0
  238. evalrx/eval_agent/run_log.schema.json +1215 -0
  239. evalrx/eval_agent/run_logger_v2.py +1764 -0
  240. evalrx/eval_agent/run_metadata.py +208 -0
  241. evalrx/eval_agent/stages/__init__.py +56 -0
  242. evalrx/eval_agent/stages/case_discovery.py +293 -0
  243. evalrx/eval_agent/stages/diagnosis.py +1017 -0
  244. evalrx/eval_agent/stages/experiment_writer.py +1634 -0
  245. evalrx/eval_agent/stages/fix_agent.py +3916 -0
  246. evalrx/eval_agent/stages/fix_internals.py +499 -0
  247. evalrx/eval_agent/stages/fix_pipeline.py +725 -0
  248. evalrx/eval_agent/stages/fix_tiers.py +187 -0
  249. evalrx/eval_agent/stages/fix_tools.py +1034 -0
  250. evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
  251. evalrx/eval_agent/stages/probe.py +439 -0
  252. evalrx/eval_agent/stages/probe_agent.py +1079 -0
  253. evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
  254. evalrx/eval_agent/stages/probe_generator.py +326 -0
  255. evalrx/eval_agent/stages/probe_search_agent.py +106 -0
  256. evalrx/eval_agent/stages/protocol.py +112 -0
  257. evalrx/eval_agent/stages/repair_catalog.py +273 -0
  258. evalrx/eval_agent/stages/surgery.py +524 -0
  259. evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
  260. evalrx/eval_agent/store.py +231 -0
  261. evalrx/logging_utils.py +112 -0
  262. evalrx/models/__init__.py +161 -0
  263. evalrx/models/_discover.py +101 -0
  264. evalrx/models/agent.py +380 -0
  265. evalrx/models/backends/__init__.py +58 -0
  266. evalrx/models/backends/api.py +169 -0
  267. evalrx/models/backends/base.py +57 -0
  268. evalrx/models/backends/gemini_compat.py +579 -0
  269. evalrx/models/backends/hf_local.py +2074 -0
  270. evalrx/models/backends/openai_compat.py +301 -0
  271. evalrx/models/backends/vllm_offline.py +116 -0
  272. evalrx/models/base.py +24 -0
  273. evalrx/models/blackbox/__init__.py +4 -0
  274. evalrx/models/blackbox/agent.py +31 -0
  275. evalrx/models/blackbox/base.py +29 -0
  276. evalrx/models/blackbox/gemini.py +279 -0
  277. evalrx/models/blackbox/llm_api.py +17 -0
  278. evalrx/models/blackbox/vlm_api.py +17 -0
  279. evalrx/models/compose.py +66 -0
  280. evalrx/models/inference.py +88 -0
  281. evalrx/models/paper_methods/__init__.py +8 -0
  282. evalrx/models/paper_methods/aad.py +53 -0
  283. evalrx/models/paper_methods/ifcd.py +204 -0
  284. evalrx/models/paper_methods/pai.py +164 -0
  285. evalrx/models/paper_methods/tcd.py +202 -0
  286. evalrx/models/paper_methods/vcd.py +45 -0
  287. evalrx/models/paper_methods/vicrop.py +137 -0
  288. evalrx/models/toolcodec.py +143 -0
  289. evalrx/models/tools/__init__.py +20 -0
  290. evalrx/models/tools/perception.py +300 -0
  291. evalrx/models/tools/visual.py +174 -0
  292. evalrx/models/whitebox/__init__.py +26 -0
  293. evalrx/models/whitebox/agent.py +31 -0
  294. evalrx/models/whitebox/base.py +24 -0
  295. evalrx/models/whitebox/qwen.py +61 -0
  296. evalrx/models/whitebox/qwen2_5_omni.py +29 -0
  297. evalrx/models/whitebox/qwen2_audio.py +25 -0
  298. evalrx/models/whitebox/qwen_omni.py +53 -0
  299. evalrx/models/whitebox/qwen_vl.py +62 -0
  300. evalrx/observability/__init__.py +21 -0
  301. evalrx/observability/envelope.py +122 -0
  302. evalrx/observability/outbox.py +111 -0
  303. evalrx/observability/tracer.py +882 -0
  304. evalrx/reporting/__init__.py +28 -0
  305. evalrx/reporting/case_study.py +947 -0
  306. evalrx/reporting/compiler.py +587 -0
  307. evalrx/reporting/dynamic.py +1882 -0
  308. evalrx/reporting/html_report.py +2225 -0
  309. evalrx/reporting/langfuse_exporter.py +38 -0
  310. evalrx/reporting/langfuse_source.py +155 -0
  311. evalrx/reporting/model.py +151 -0
  312. evalrx/reporting/run_events.py +184 -0
  313. evalrx/reporting/server.py +557 -0
  314. evalrx/reporting/stages.py +58 -0
  315. evalrx/reporting/static_export.py +142 -0
  316. evalrx/reporting/web_dist/index.html +146 -0
  317. evalrx/specs.py +727 -0
  318. evalrx/stats/__init__.py +47 -0
  319. evalrx/stats/api.py +192 -0
  320. evalrx/stats/bootstrap.py +86 -0
  321. evalrx/stats/ebh.py +27 -0
  322. evalrx/stats/evalue.py +98 -0
  323. evalrx/stats/friedman.py +138 -0
  324. evalrx/stats/mcnemar.py +40 -0
  325. evalrx/stats/multiplicity.py +159 -0
  326. evalrx/stats/subset_sampling.py +55 -0
  327. evalrx/term_links.py +43 -0
  328. evalrx/viz/__init__.py +7 -0
  329. evalrx/viz/labels.py +77 -0
  330. evalrx/viz/prompts.py +39 -0
  331. evalrx/viz/renderer.py +590 -0
  332. evalrx/viz/schema.py +36 -0
  333. evalrx/viz/style.py +134 -0
  334. evalrx-0.1.2.dist-info/METADATA +532 -0
  335. evalrx-0.1.2.dist-info/RECORD +339 -0
  336. evalrx-0.1.2.dist-info/WHEEL +5 -0
  337. evalrx-0.1.2.dist-info/entry_points.txt +3 -0
  338. evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
  339. 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)