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,102 @@
1
+ """VL-SHAP — Shapley attribution over image regions for VLMs.
2
+
3
+ Black-box (``LOGPROBS``): partition the image into a grid of regions, mask
4
+ subsets, score each masked image by the model's output logprob, and compute each
5
+ region's Shapley value → a spatial attribution map over the image.
6
+
7
+ The region scorer is injectable (``region_score_fn(kept_regions: set) -> float``)
8
+ for decoupled testing; the default builds a grid-masked image (PIL) and scores it
9
+ via ``model.logprobs`` (mean logprob). ``requires=LOGPROBS``; VLM-only.
10
+
11
+ Shapley values for ML:
12
+ "A Unified Approach to Interpreting Model Predictions"
13
+ Lundberg & Lee, NeurIPS 2017 — https://arxiv.org/abs/1705.07874
14
+ Code: https://github.com/shap/shap
15
+
16
+ Region-level visual attribution for VLMs (MM-SHAP framework):
17
+ "MM-SHAP: A Performance-agnostic Metric for Measuring Multimodal Contributions
18
+ in Vision and Language Models"
19
+ Parcalabescu & Frank, ACL 2022 — https://arxiv.org/abs/2212.08158
20
+ Code: https://github.com/coastalcph/mm-shap
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import math
26
+ from typing import TYPE_CHECKING, Callable, Optional
27
+
28
+ from evalrx.analyzers.perturbation._shapley import shapley_values
29
+ from evalrx.core.analyzer import Analyzer
30
+ from evalrx.core.capability import Capability
31
+ from evalrx.core.case import Inputs
32
+ from evalrx.core.registry import register_analyzer
33
+ from evalrx.core.result import Result
34
+
35
+ if TYPE_CHECKING:
36
+ from evalrx.core.case import CaseBatch
37
+ from evalrx.core.model import Model
38
+
39
+
40
+ @register_analyzer("vl_shap")
41
+ class VLShapAnalyzer(Analyzer):
42
+ """Per-region Shapley attribution over a grid of image regions."""
43
+
44
+ name = "vl_shap"
45
+ requires = frozenset({Capability.LOGPROBS})
46
+ applies_to_modalities = frozenset({"image"})
47
+ # Its default scorer dereferences case.inputs.image, so an omni model on an
48
+ # audio batch matched here and then raised on the first case.
49
+ requires_modalities = frozenset({"image"})
50
+
51
+ def __init__(
52
+ self,
53
+ n_regions: int = 16,
54
+ region_score_fn: Optional[Callable[[set], float]] = None,
55
+ n_samples: int = 64,
56
+ top_k: int = 5,
57
+ seed: int = 0,
58
+ ) -> None:
59
+ super().__init__(n_regions=n_regions, region_score_fn=region_score_fn,
60
+ n_samples=n_samples, top_k=top_k, seed=seed)
61
+
62
+ def _default_region_value(self, model: "Model", case) -> Callable[[set], float]:
63
+ from PIL import Image # lazy
64
+
65
+ image = case.inputs.image
66
+ if image is None:
67
+ raise ValueError("VLShapAnalyzer default scorer needs case.inputs.image (a PIL image).")
68
+ side = max(1, int(math.isqrt(self.n_regions)))
69
+ W, H = image.size
70
+ cw, ch = W / side, H / side
71
+
72
+ def value(kept: set) -> float:
73
+ masked = image.copy()
74
+ black = Image.new(image.mode, (max(1, int(cw)), max(1, int(ch))), 0)
75
+ for r in range(self.n_regions):
76
+ if r in kept:
77
+ continue
78
+ gx, gy = r % side, r // side
79
+ masked.paste(black, (int(gx * cw), int(gy * ch)))
80
+ lps = model.logprobs(Inputs(prompt=case.inputs.prompt, image=masked))
81
+ return sum(t.logprob for t in lps) / len(lps) if lps else 0.0
82
+ return value
83
+
84
+ def _run(self, model: "Model", cases: "CaseBatch") -> Result:
85
+ case = cases[0]
86
+ players = list(range(self.n_regions))
87
+ value = self.region_score_fn or self._default_region_value(model, case)
88
+ shap = shapley_values(players, value, n_samples=self.n_samples, seed=self.seed)
89
+ top = sorted(
90
+ ({"region": r, "shapley": round(shap[r], 4)} for r in players),
91
+ key=lambda d: -abs(d["shapley"]),
92
+ )[: self.top_k]
93
+ return Result(
94
+ analyzer=self.name, model=repr(model), cases=cases,
95
+ artifacts={"region_shapley": shap},
96
+ findings={
97
+ "n_regions": self.n_regions,
98
+ "grid_side": max(1, int(math.isqrt(self.n_regions))),
99
+ "top_regions": top,
100
+ "total_abs_attribution": round(sum(abs(v) for v in shap.values()), 4),
101
+ },
102
+ )
@@ -0,0 +1,33 @@
1
+ """Reasoning analyzers — text-chain diagnostics (black-box, ``GENERATE`` only).
2
+
3
+ Two of these are HYGIENE probes that belong before the rest, not beside them:
4
+ ``answer_extraction_audit`` (is the FAIL label real, or did the grader miss the
5
+ answer?) and ``termination_audit`` (did the generation stop cleanly, or hit the
6
+ token budget / a repetition loop?). Both produce confounds that mimic every
7
+ other probe's "bad" column, so a mechanism finding read before them is not
8
+ interpretable.
9
+
10
+ The rest decompose *how* a chain failed: ``arith_audit`` (computation slip vs
11
+ chain break, at zero extra cost), ``self_repair`` (detect / correct / damage),
12
+ ``step_rollout_value`` (where the chain broke, by Math-Shepherd rollouts),
13
+ ``knowledge_reasoning_split`` (missing fact vs broken composition), and
14
+ ``contamination_score`` (is the benchmark measuring recall?).
15
+ """
16
+
17
+ from evalrx.analyzers.reasoning.answer_extraction_audit import AnswerExtractionAudit
18
+ from evalrx.analyzers.reasoning.arith_audit import ArithmeticAudit
19
+ from evalrx.analyzers.reasoning.contamination import ContaminationProbe
20
+ from evalrx.analyzers.reasoning.knowledge_split import KnowledgeReasoningSplit
21
+ from evalrx.analyzers.reasoning.self_repair import SelfRepairAnalyzer
22
+ from evalrx.analyzers.reasoning.step_rollout_value import StepRolloutValueAnalyzer
23
+ from evalrx.analyzers.reasoning.termination_audit import TerminationAudit
24
+
25
+ __all__ = [
26
+ "AnswerExtractionAudit",
27
+ "TerminationAudit",
28
+ "ArithmeticAudit",
29
+ "SelfRepairAnalyzer",
30
+ "StepRolloutValueAnalyzer",
31
+ "KnowledgeReasoningSplit",
32
+ "ContaminationProbe",
33
+ ]
@@ -0,0 +1,328 @@
1
+ """Shared answer-handling helpers for the reasoning probes.
2
+
3
+ Every reasoning probe has to answer the same three questions — *what did the
4
+ model finally claim?*, *is that the gold answer?*, and *did the generation even
5
+ terminate?* — so the conventions live here once instead of drifting per probe.
6
+
7
+ Extraction follows the self-consistency / GSM8K convention (last ``Answer:`` tag
8
+ or ``\\boxed{}``, else the last non-empty line), and grading is numeric-aware:
9
+ for a numeric gold we compare against the LAST number in the prediction, which
10
+ is what "the answer is 18" means and what plain substring matching gets wrong
11
+ ("18" is inside "180").
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import ast
17
+ import re
18
+ from typing import TYPE_CHECKING, Any, Optional
19
+
20
+ if TYPE_CHECKING:
21
+ from evalrx.core.case import FailureCase
22
+
23
+ #: The separator is MANDATORY (":", "=", or the word "is"). With it optional the
24
+ #: bare word "answer" matches — "the answer to this is unclear" would extract
25
+ #: "to this is unclear" — and since extraction takes the LAST hit, one trailing
26
+ #: "I hope this answer helps" silently replaces a correct tagged answer.
27
+ _ANSWER_TAG = re.compile(
28
+ r"(?:final\s+answer|answer)\s*(?:[:=]|\bis\b)\s*(.+)", re.IGNORECASE
29
+ )
30
+ _BOXED = re.compile(r"\\boxed\s*\{([^{}]*)\}")
31
+ _NUMBER = re.compile(r"-?\d[\d,]*(?:\.\d+)?")
32
+ _GIVE_UP = re.compile(
33
+ r"(i (?:can(?:no|')t|am unable|do not know|don't know)"
34
+ r"|cannot (?:be )?determine[d]?|not enough (?:information|context)"
35
+ r"|insufficient (?:information|data)|unable to (?:answer|solve|determine)"
36
+ r"|无法(?:确定|回答|判断)|不知道)",
37
+ re.IGNORECASE,
38
+ )
39
+ #: Sentence/clause terminators that mark a *clean* end of generation.
40
+ _TERMINATORS = ".!?\"'`)]}\u3002\uff01\uff1f"
41
+
42
+
43
+ #: A span that is only the format placeholder the prompt asked for — models
44
+ #: restate "give it as 'Answer: <answer>'" and that echo, being LAST, would
45
+ #: otherwise replace the real answer.
46
+ _PLACEHOLDER = re.compile(r"^[<\[{(]\s*\w*\s*[>\]})]?['\".\s]*$")
47
+
48
+ #: A bare option label — ``(A)``, ``[B]``, ``C.`` — is a REAL multiple-choice
49
+ #: answer, but it is character-for-character the shape :data:`_PLACEHOLDER`
50
+ #: describes, so it needs an explicit exemption. Without one the guard throws
51
+ #: the answer away and extraction keeps walking backwards into the chain-of-
52
+ #: thought, where it picks up whatever prose came before. Measured on BBH
53
+ #: ``tracking_shuffled_objects_seven_objects`` / Qwen3.5-9B: 244 of 250 final
54
+ #: claims are a bare ``(X)``, 98 correct answers were scored FAIL, and the slice
55
+ #: read 0.592 instead of 0.984 — a mid-band dataset invented out of a saturated
56
+ #: one. A one-letter *placeholder* (``<X>``) loses to a one-letter answer here
57
+ #: on purpose: the letter is a valid option either way, so the collision costs
58
+ #: nothing, while the reverse costs the whole measurement.
59
+ _OPTION_LABEL = re.compile(r"^[<\[{(]?\s*[A-Za-z]\s*[>\]})]?[.\s]*$")
60
+
61
+
62
+ def _is_placeholder(span: str) -> bool:
63
+ """True when *span* is only the prompt's format hint echoed back."""
64
+ return bool(_PLACEHOLDER.match(span)) and not _OPTION_LABEL.match(span)
65
+
66
+
67
+ def extract_answer(text: Any) -> str:
68
+ r"""Last usable ``\boxed{}`` or ``Answer:``-tagged span, else the last non-empty line."""
69
+ raw = str(text or "")
70
+ # "Last usable" is by POSITION IN THE TEXT, across both conventions. Draining
71
+ # every \boxed{} before looking at a single "Answer:" tag made an INTERMEDIATE
72
+ # box outrank the final answer line whenever a chain boxed its working — on
73
+ # minervamath / Qwen3.5-9B that mis-scored 14 of 272 (gold 2.45e6, the model
74
+ # closed with "Answer: 2.45e6", extraction returned a mid-chain 7.353e14).
75
+ candidates: list[tuple[int, str]] = []
76
+ for match in _BOXED.finditer(raw):
77
+ span = match.group(1).strip()
78
+ if span:
79
+ candidates.append((match.end(), span))
80
+ for match in _ANSWER_TAG.finditer(raw):
81
+ # the tag regex is line-greedy; keep only the first line of the span
82
+ span = match.group(1)
83
+ candidate = span.splitlines()[0].strip() if span.splitlines() else ""
84
+ if candidate and not _is_placeholder(candidate):
85
+ candidates.append((match.end(), candidate))
86
+ if candidates:
87
+ return max(candidates, key=lambda item: item[0])[1]
88
+ lines = [line.strip() for line in raw.splitlines() if line.strip()]
89
+ return lines[-1] if lines else ""
90
+
91
+
92
+ def normalize_answer(value: Any) -> str:
93
+ """Lowercase, drop thousands separators and decoration, collapse whitespace."""
94
+ text = str(value if value is not None else "").lower().strip()
95
+ text = re.sub(r"(?<=\d),(?=\d{3}\b)", "", text) # 1,234 -> 1234
96
+ text = re.sub(r"[$€£%\\]|\*\*|\bthe\b", " ", text)
97
+ text = re.sub(r"[^\w\s./:+-]", " ", text, flags=re.UNICODE)
98
+ return " ".join(text.split()).strip(" .")
99
+
100
+
101
+ #: Binary-answer vocabulary. true/false fold onto yes/no so a True/False task
102
+ #: (BBH boolean_expressions, web_of_lies) gets the same direction columns as a
103
+ #: Yes/No one.
104
+ _BINARY_WORDS = {"yes": "yes", "no": "no", "true": "yes", "false": "no"}
105
+
106
+
107
+ def binary_direction(text: Any, *, last: bool = False) -> Optional[str]:
108
+ """``"yes"`` / ``"no"`` read off a binary answer, else ``None``.
109
+
110
+ Whole words only (``"not"``/``"none"`` never count as ``"no"``), decided
111
+ on the opening six words -- or, with ``last=True``, the closing six, for a
112
+ tail window where the verdict comes after the reasoning. ``true``/``false``
113
+ map onto ``yes``/``no`` (see :data:`_BINARY_WORDS`).
114
+ """
115
+ words = re.findall(r"[a-z]+", str(text or "").lower())
116
+ seq = words[-6:][::-1] if last else words[:6]
117
+ for word in seq:
118
+ if word in _BINARY_WORDS:
119
+ return _BINARY_WORDS[word]
120
+ return None
121
+
122
+
123
+ def binary_gold(expected: Any) -> Optional[str]:
124
+ """The gold's direction when the task is yes/no (true/false), else ``None``.
125
+
126
+ Accepts a string, a one-element list/tuple of aliases, or an
127
+ ``{"all_of": [...]}`` spec with a single entry. Anything longer than a bare
128
+ yes/no/true/false token is NOT a binary gold (``"yes, it is"`` -> ``None``),
129
+ so free-form tasks never grow direction columns by accident.
130
+ """
131
+ if isinstance(expected, dict):
132
+ values = expected.get("all_of") or []
133
+ expected = values[0] if len(values) == 1 else None
134
+ if isinstance(expected, (list, tuple)):
135
+ expected = expected[0] if len(expected) == 1 else None
136
+ if expected is None:
137
+ return None
138
+ return _BINARY_WORDS.get(normalize_answer(expected))
139
+
140
+
141
+ def as_number(value: Any) -> Optional[float]:
142
+ """Parse *value* as a number if it is one **in its entirety**, else ``None``."""
143
+ text = normalize_answer(value)
144
+ if not text:
145
+ return None
146
+ match = _NUMBER.fullmatch(text.replace(" ", ""))
147
+ if not match:
148
+ return None
149
+ try:
150
+ return float(match.group(0).replace(",", ""))
151
+ except ValueError: # pragma: no cover - regex already constrains this
152
+ return None
153
+
154
+
155
+ def numbers_in(text: Any) -> list[float]:
156
+ """Every number appearing in *text*, in order."""
157
+ out: list[float] = []
158
+ for token in _NUMBER.findall(str(text or "")):
159
+ try:
160
+ out.append(float(token.replace(",", "")))
161
+ except ValueError: # pragma: no cover - defensive
162
+ continue
163
+ return out
164
+
165
+
166
+ def answer_equal(prediction: Any, gold: Any, rel_tol: float = 1e-6) -> bool:
167
+ """Numeric-aware answer equality.
168
+
169
+ A numeric gold is matched against the LAST number in the prediction (the
170
+ "the answer is 18" convention); short non-numeric golds — MC letters, yes/no
171
+ — need a standalone token match so that ``"b"`` does not match "probably".
172
+ """
173
+ if isinstance(gold, (list, tuple, set, frozenset)):
174
+ return any(answer_equal(prediction, item, rel_tol=rel_tol) for item in gold)
175
+ pred = normalize_answer(prediction)
176
+ exp = normalize_answer(gold)
177
+ if not exp:
178
+ return False
179
+ if pred == exp:
180
+ return True
181
+ gold_num = as_number(exp)
182
+ if gold_num is not None:
183
+ found = numbers_in(pred)
184
+ if not found:
185
+ return False
186
+ # Both ends, because the prediction reaches here in two shapes: a tagged
187
+ # span that STARTS with the answer and may trail commentary ("620, since
188
+ # the broken beds don't count") — first number — and a bare sentence
189
+ # ending in it ("the answer is 18") — last number. Checking only one end
190
+ # scores the commentary's numbers on half the cases.
191
+ return any(
192
+ abs(candidate - gold_num) <= rel_tol * max(1.0, abs(gold_num))
193
+ for candidate in (found[0], found[-1])
194
+ )
195
+ if len(exp) <= 3:
196
+ return re.search(rf"(?<!\w){re.escape(exp)}(?!\w)", pred) is not None
197
+ return exp in pred
198
+
199
+
200
+ def default_grader(prediction: Any, case: "FailureCase") -> Optional[bool]:
201
+ """Grade *prediction* against ``case.expected`` (``None`` = ungradable)."""
202
+ if case.expected is None:
203
+ return None
204
+ tolerance = float((getattr(case, "metadata", {}) or {}).get("numeric_tolerance", 1e-6))
205
+ return answer_equal(extract_answer(prediction), case.expected, rel_tol=tolerance)
206
+
207
+
208
+ # ----------------------------------------------------------------------
209
+ # Generation-shape helpers (termination / degeneration)
210
+ # ----------------------------------------------------------------------
211
+
212
+ def word_ngrams(text: Any, n: int) -> list[tuple[str, ...]]:
213
+ words = str(text or "").lower().split()
214
+ if len(words) < n:
215
+ return []
216
+ return [tuple(words[i : i + n]) for i in range(len(words) - n + 1)]
217
+
218
+
219
+ def repetition_score(text: Any, n: int = 8) -> float:
220
+ """Fraction of word ``n``-grams that are repeats — 0 for clean text, →1 for a loop.
221
+
222
+ Degenerate repetition is the failure mode that makes a truncated generation
223
+ look like a reasoning failure, so it gets its own column rather than being
224
+ folded into a length heuristic.
225
+ """
226
+ grams = word_ngrams(text, n)
227
+ if not grams:
228
+ return 0.0
229
+ return round(1.0 - len(set(grams)) / len(grams), 4)
230
+
231
+
232
+ def has_answer_tag(text: Any) -> bool:
233
+ raw = str(text or "")
234
+ return bool(_BOXED.search(raw) or _ANSWER_TAG.search(raw))
235
+
236
+
237
+ def looks_truncated(text: Any) -> bool:
238
+ """True when the text stops mid-thought: no terminator and no answer tag."""
239
+ stripped = str(text or "").rstrip()
240
+ if not stripped:
241
+ return False
242
+ return stripped[-1] not in _TERMINATORS and not has_answer_tag(stripped)
243
+
244
+
245
+ def looks_like_give_up(text: Any) -> bool:
246
+ return bool(_GIVE_UP.search(str(text or "")))
247
+
248
+
249
+ # ----------------------------------------------------------------------
250
+ # Arithmetic verification
251
+ # ----------------------------------------------------------------------
252
+
253
+ _ARITH_OPS = {
254
+ ast.Add: lambda a, b: a + b,
255
+ ast.Sub: lambda a, b: a - b,
256
+ ast.Mult: lambda a, b: a * b,
257
+ ast.Div: lambda a, b: a / b,
258
+ ast.Pow: lambda a, b: a**b,
259
+ }
260
+ #: ``lhs = rhs`` where the lhs is a pure-number expression carrying an operator.
261
+ _EQUATION = re.compile(
262
+ r"(?<![\w=])((?:-?\d[\d,]*(?:\.\d+)?|[+\-*/×÷^()\s]){3,}?)\s*=\s*"
263
+ # the number regex already absorbs a decimal tail, so a trailing '.' here is
264
+ # sentence punctuation ("= 41.") and must not block the match
265
+ r"(-?\d[\d,]*(?:\.\d+)?)(?!\.?\d)"
266
+ )
267
+
268
+
269
+ def safe_eval_arithmetic(expression: str) -> Optional[float]:
270
+ """Evaluate a pure-number arithmetic expression, or ``None`` if it is not one.
271
+
272
+ Parsed with :mod:`ast` and walked against an allow-list — model output is
273
+ untrusted text and must never reach :func:`eval`.
274
+ """
275
+ cleaned = (
276
+ expression.replace("×", "*").replace("÷", "/").replace("^", "**").strip()
277
+ )
278
+ cleaned = re.sub(r"(?<=\d),(?=\d{3}\b)", "", cleaned)
279
+ if not cleaned or not re.fullmatch(r"[\d.+\-*/()\s]+", cleaned):
280
+ return None
281
+ try:
282
+ tree = ast.parse(cleaned, mode="eval")
283
+ except SyntaxError:
284
+ return None
285
+
286
+ def _eval(node: ast.AST) -> Optional[float]:
287
+ if isinstance(node, ast.Expression):
288
+ return _eval(node.body)
289
+ if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
290
+ return float(node.value)
291
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
292
+ inner = _eval(node.operand)
293
+ if inner is None:
294
+ return None
295
+ return -inner if isinstance(node.op, ast.USub) else inner
296
+ if isinstance(node, ast.BinOp) and type(node.op) in _ARITH_OPS:
297
+ left, right = _eval(node.left), _eval(node.right)
298
+ if left is None or right is None:
299
+ return None
300
+ try:
301
+ return float(_ARITH_OPS[type(node.op)](left, right))
302
+ except (ZeroDivisionError, OverflowError, ValueError):
303
+ return None
304
+ return None
305
+
306
+ try:
307
+ return _eval(tree)
308
+ except RecursionError: # pragma: no cover - pathological nesting
309
+ return None
310
+
311
+
312
+ def find_equations(text: Any) -> list[tuple[str, float, float]]:
313
+ """Every ``expr = value`` statement as ``(expr, stated_value, computed_value)``.
314
+
315
+ Only statements whose left-hand side is a pure-number expression with an
316
+ operator are returned — ``x = 5`` is a definition, not a claim we can check.
317
+ """
318
+ out: list[tuple[str, float, float]] = []
319
+ for match in _EQUATION.finditer(str(text or "")):
320
+ lhs, rhs = match.group(1).strip(), match.group(2)
321
+ if not re.search(r"[+\-*/×÷^]", lhs) or len(numbers_in(lhs)) < 2:
322
+ continue
323
+ computed = safe_eval_arithmetic(lhs)
324
+ stated = as_number(rhs)
325
+ if computed is None or stated is None:
326
+ continue
327
+ out.append((f"{lhs} = {rhs}", stated, computed))
328
+ return out