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,30 @@
1
+ """Activation patching / causal tracing (Meng et al., ROME) (Stage 2).
2
+
3
+ The only CAUSAL white-box method here: read a clean run's activations and WRITE
4
+ them into a corrupted run to localise where information is causally used.
5
+ ``requires=HIDDEN_STATES`` (read+write hooks — via nnsight). Memory ∝ layers×positions.
6
+
7
+ References:
8
+ - Locating and Editing Factual Associations in GPT (ROME, causal tracing)
9
+ Meng et al., NeurIPS 2022 — arXiv:2202.05262
10
+ - Attribution Patching (scalable approximation): Neel Nanda, 2023.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from evalrx.core.analyzer import Analyzer
16
+ from evalrx.core.capability import Capability
17
+ from evalrx.core.registry import register_analyzer
18
+
19
+
20
+ @register_analyzer("causal_trace")
21
+ class CausalTraceAnalyzer(Analyzer):
22
+ name = "causal_trace"
23
+ requires = frozenset({Capability.HIDDEN_STATES})
24
+ applies_to_modalities = frozenset({"text", "image"})
25
+
26
+ def _run(self, model, cases):
27
+ raise NotImplementedError(
28
+ "Stage 2: clean/corrupt runs + patch cached activations at each (layer, position) "
29
+ "via nnsight; report the causal-effect map. Needs read+write hooks (beyond HF flags)."
30
+ )
@@ -0,0 +1,23 @@
1
+ """Input-perturbation analyzers (black-box-feasible; cost driver = many forwards)."""
2
+
3
+ from evalrx.analyzers.perturbation.context_shap import ContextShapAnalyzer
4
+ from evalrx.analyzers.perturbation.cot_faithfulness import CoTFaithfulnessAnalyzer
5
+ from evalrx.analyzers.perturbation.format_sensitivity import FormatSensitivityAnalyzer
6
+ from evalrx.analyzers.perturbation.mm_shap import MMShapAnalyzer
7
+ from evalrx.analyzers.perturbation.modality_ablation import ModalityAblationAnalyzer
8
+ from evalrx.analyzers.perturbation.perturbation_battery import PerturbationBattery
9
+ from evalrx.analyzers.perturbation.prompt_contrast import PromptContrastAnalyzer
10
+ from evalrx.analyzers.perturbation.rise import RISEAnalyzer
11
+ from evalrx.analyzers.perturbation.vl_shap import VLShapAnalyzer
12
+
13
+ __all__ = [
14
+ "RISEAnalyzer",
15
+ "VLShapAnalyzer",
16
+ "MMShapAnalyzer",
17
+ "ModalityAblationAnalyzer",
18
+ "PromptContrastAnalyzer",
19
+ "FormatSensitivityAnalyzer",
20
+ "CoTFaithfulnessAnalyzer",
21
+ "ContextShapAnalyzer",
22
+ "PerturbationBattery",
23
+ ]
@@ -0,0 +1,54 @@
1
+ """Permutation-sampling Shapley values — shared by MM-SHAP and VL-SHAP.
2
+
3
+ Estimates each player's Shapley value (mean marginal contribution to a coalition
4
+ value) by sampling random permutations. ``value_fn(kept: set) -> float`` is the
5
+ coalition value (e.g. the model's logprob/confidence with only ``kept`` players
6
+ present). Results are memoised by coalition so expensive model calls aren't
7
+ repeated. For an additive game the estimate equals each player's exact weight.
8
+
9
+ References:
10
+ - Shapley value: 'A Value for n-Person Games', Shapley (1953), Contributions to the Theory of Games II.
11
+ - Sampling estimator: 'Polynomial calculation of the Shapley value based on sampling',
12
+ Castro, Gomez & Tejada (2009), Computers & Operations Research 36(5).
13
+ - ML attribution: 'A Unified Approach to Interpreting Model Predictions' (SHAP),
14
+ Lundberg & Lee, NeurIPS 2017 — arXiv:1705.07874
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import random
20
+ from typing import Callable, Iterable
21
+
22
+
23
+ def shapley_values(
24
+ players: Iterable,
25
+ value_fn: Callable[[set], float],
26
+ n_samples: int = 64,
27
+ seed: int = 0,
28
+ ) -> dict:
29
+ """Return ``{player: shapley_value}`` via permutation sampling."""
30
+ players = list(players)
31
+ phi = {p: 0.0 for p in players}
32
+ if not players:
33
+ return phi
34
+ rng = random.Random(seed)
35
+ memo: dict = {}
36
+
37
+ def val(kept: set) -> float:
38
+ key = frozenset(kept)
39
+ if key not in memo:
40
+ memo[key] = float(value_fn(set(kept)))
41
+ return memo[key]
42
+
43
+ base = val(set())
44
+ for _ in range(n_samples):
45
+ perm = players[:]
46
+ rng.shuffle(perm)
47
+ kept: set = set()
48
+ prev = base
49
+ for p in perm:
50
+ kept.add(p)
51
+ cur = val(kept)
52
+ phi[p] += cur - prev
53
+ prev = cur
54
+ return {p: phi[p] / n_samples for p in players}
@@ -0,0 +1,174 @@
1
+ """Context-chunk Shapley — which parts of the provided context carry the answer?
2
+
3
+ The text sibling of :class:`ToolShap` and MM-SHAP: ablate CHUNKS OF THE GIVEN
4
+ CONTEXT (paragraphs or sentences) instead of tools or image patches, and
5
+ Shapley-attribute the answer to them. Directly serves RAG/long-context LLM
6
+ diagnosis: a failing case whose answer depends on no chunk is answering from
7
+ priors; one dominated by a single chunk inherits that chunk's quality.
8
+
9
+ Black-box (``requires=GENERATE``): the coalition value is the similarity of
10
+ the ablated-context answer to the full-context baseline answer. Uses the
11
+ shared permutation-sampling estimator (memoised), so model calls are bounded.
12
+
13
+ References:
14
+ - TokenSHAP: Interpreting Large Language Models with Monte Carlo Shapley Value
15
+ Estimation — Goldshmidt & Horovicz, 2024 — arXiv:2407.10114
16
+ - ContextCite: Attributing Model Generation to Context —
17
+ Cohen-Wang et al., NeurIPS 2024 — arXiv:2409.00729
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import dataclasses
23
+ import difflib
24
+ import re
25
+ from typing import TYPE_CHECKING, Any, Callable, Optional
26
+
27
+ from evalrx.analyzers.perturbation._shapley import shapley_values
28
+ from evalrx.core.analyzer import Analyzer
29
+ from evalrx.core.capability import Capability
30
+ from evalrx.core.registry import register_analyzer
31
+ from evalrx.core.result import Result
32
+
33
+ if TYPE_CHECKING:
34
+ from evalrx.core.case import CaseBatch, FailureCase
35
+ from evalrx.core.model import Model
36
+
37
+
38
+ def _similarity(a: str, b: str) -> float:
39
+ na = " ".join(str(a or "").lower().split())
40
+ nb = " ".join(str(b or "").lower().split())
41
+ if not na and not nb:
42
+ return 1.0
43
+ return difflib.SequenceMatcher(None, na, nb).ratio()
44
+
45
+
46
+ def default_context_fn(case: "FailureCase") -> Optional[str]:
47
+ """The ablatable context: ``metadata['context']`` when present."""
48
+ meta = case.metadata if isinstance(case.metadata, dict) else {}
49
+ value = meta.get("context")
50
+ return str(value) if value else None
51
+
52
+
53
+ def split_chunks(context: str, granularity: str, max_chunks: int) -> list[str]:
54
+ if granularity == "sentence":
55
+ parts = [p.strip() for p in re.split(r"(?<=[.!?])\s+", context) if p.strip()]
56
+ else:
57
+ parts = [p.strip() for p in re.split(r"\n\s*\n", context) if p.strip()]
58
+ if len(parts) <= 1: # single paragraph — fall back to sentences
59
+ parts = [p.strip() for p in re.split(r"(?<=[.!?])\s+", context) if p.strip()]
60
+ if len(parts) > max_chunks: # merge the tail so players stay bounded
61
+ head, tail = parts[: max_chunks - 1], " ".join(parts[max_chunks - 1:])
62
+ parts = head + [tail]
63
+ return parts
64
+
65
+
66
+ @register_analyzer("context_shap")
67
+ class ContextShapAnalyzer(Analyzer):
68
+ """Shapley attribution of the answer to provided-context chunks (RAG dependence probe).
69
+
70
+ Hyper-parameters:
71
+ granularity: ``"paragraph"`` (default) or ``"sentence"`` chunking.
72
+ n_samples: permutation samples for the Shapley estimator.
73
+ max_chunks: chunk cap per case (tail chunks are merged).
74
+ max_cases: label-stratified cap on probed cases; 0 (the default) = every case.
75
+ seed: permutation-sampling seed.
76
+ context_fn: ``callable(case) -> str | None`` supplying the ablatable
77
+ context (default: ``metadata['context']``). The context
78
+ must appear verbatim inside the prompt.
79
+ """
80
+
81
+ name = "context_shap"
82
+ requires = frozenset({Capability.GENERATE})
83
+ applies_to_modalities = frozenset({"text", "image"})
84
+
85
+ def __init__(
86
+ self,
87
+ granularity: str = "paragraph",
88
+ n_samples: int = 16,
89
+ max_chunks: int = 6,
90
+ max_cases: int = 0,
91
+ seed: int = 0,
92
+ context_fn: Optional[Callable[["FailureCase"], Optional[str]]] = None,
93
+ ) -> None:
94
+ super().__init__(
95
+ granularity=granularity,
96
+ n_samples=max(1, int(n_samples)),
97
+ max_chunks=max(2, int(max_chunks)),
98
+ max_cases=max_cases,
99
+ seed=seed,
100
+ )
101
+ # ctor name, so sklearn-style get_params() reflection works
102
+ self.context_fn = context_fn or default_context_fn
103
+
104
+ def _run(self, model: "Model", cases: "CaseBatch") -> Result:
105
+ per_case: list[dict[str, Any]] = []
106
+ for case in cases.stratified_head(self.max_cases):
107
+ context = self.context_fn(case)
108
+ prompt = case.inputs.prompt or ""
109
+ entry: dict[str, Any] = {"sample_id": case.id}
110
+ if not context:
111
+ entry["skipped"] = "no context supplied (metadata['context'] or context_fn)"
112
+ per_case.append(entry)
113
+ continue
114
+ if prompt.count(context) != 1:
115
+ entry["skipped"] = (
116
+ "context must occur exactly once verbatim in the prompt "
117
+ f"(found {prompt.count(context)} occurrences)"
118
+ )
119
+ per_case.append(entry)
120
+ continue
121
+ chunks = split_chunks(context, self.granularity, self.max_chunks)
122
+ entry["n_chunks"] = len(chunks)
123
+ if len(chunks) < 2:
124
+ entry["skipped"] = "context has a single chunk — nothing to attribute"
125
+ per_case.append(entry)
126
+ continue
127
+
128
+ baseline = str(case.observed or "") or str(model.generate(case.inputs))
129
+
130
+ def value_fn(kept: set) -> float:
131
+ kept_text = "\n\n".join(chunks[i] for i in sorted(kept))
132
+ ablated = prompt.replace(context, kept_text, 1)
133
+ answer = str(model.generate(dataclasses.replace(case.inputs, prompt=ablated)))
134
+ return _similarity(answer, baseline)
135
+
136
+ coalition_cache: dict[frozenset, float] = {}
137
+
138
+ def cached_value(kept: set) -> float:
139
+ key = frozenset(kept)
140
+ if key not in coalition_cache:
141
+ coalition_cache[key] = value_fn(set(key))
142
+ return coalition_cache[key]
143
+
144
+ phi = shapley_values(
145
+ range(len(chunks)), cached_value, n_samples=self.n_samples, seed=self.seed
146
+ )
147
+ no_context = cached_value(set())
148
+ top_index = max(phi, key=lambda k: abs(phi[k]))
149
+ total_abs = sum(abs(v) for v in phi.values()) or 1.0
150
+ entry["context_dependence"] = round(1.0 - no_context, 4)
151
+ entry["top_chunk_index"] = int(top_index)
152
+ entry["top_chunk_share"] = round(abs(phi[top_index]) / total_abs, 4)
153
+ entry["shapley"] = {str(k): round(v, 4) for k, v in phi.items()}
154
+ per_case.append(entry)
155
+
156
+ deps = [c["context_dependence"] for c in per_case if "context_dependence" in c]
157
+ findings: dict[str, Any] = {
158
+ "n_cases": len(per_case),
159
+ "granularity": self.granularity,
160
+ "n_samples": self.n_samples,
161
+ "mean_context_dependence": round(sum(deps) / len(deps), 4) if deps else None,
162
+ "per_case": per_case,
163
+ "_caveat": (
164
+ "context_dependence near 0 = the answer survives with the "
165
+ "context removed (answering from priors — a hallucination risk "
166
+ "on context-grounded tasks); top_chunk_share near 1 = one chunk "
167
+ "carries the answer. Value function is answer SIMILARITY to the "
168
+ "full-context baseline, not correctness. Requires the context "
169
+ "verbatim inside the prompt; deterministic decoding assumed "
170
+ "(each coalition sampled once, memoised). INTERVENTIONAL: "
171
+ "held-out verification must RE-RUN the ablations."
172
+ ),
173
+ }
174
+ return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
@@ -0,0 +1,239 @@
1
+ """Chain-of-thought faithfulness — does the answer actually depend on the reasoning?
2
+
3
+ Lanham et al.'s early-answering test: elicit a chain of thought, then truncate
4
+ it at several points and ask for the answer from the partial reasoning. If the
5
+ model reaches its final answer from almost any prefix, the chain is post-hoc
6
+ rationalisation rather than load-bearing computation — prompt-level fixes that
7
+ edit the reasoning will not move the answer on such cases.
8
+
9
+ Black-box (``requires=GENERATE``), deterministic truncation points, per-case
10
+ numeric columns for M2/M3.
11
+
12
+ When the case carries a gold answer the same generations also yield the
13
+ **answer trajectory** — correctness at each truncation point — for free, and
14
+ that is where the actionable columns live: a chain that was already right at
15
+ 25% and wrong at the end (``drift_away``) is over-reasoning that needs to be
16
+ stopped early, while one that only becomes right at the end (``late_rescue``)
17
+ is reasoning that is doing real work and must not be shortened. Both are
18
+ invisible to ``early_answer_match_rate``, which only compares early answers to
19
+ the model's own final answer and cannot tell a stable-correct chain from a
20
+ stable-wrong one.
21
+
22
+ References:
23
+ - Measuring Faithfulness in Chain-of-Thought Reasoning —
24
+ Lanham et al., 2023 — arXiv:2307.13702
25
+ - Self-Consistency Improves Chain of Thought Reasoning — Wang et al.,
26
+ ICLR 2023 — arXiv:2203.11171 (answer-extraction convention)
27
+ - Do NOT Think That Much for 2+3=? On the Overthinking of o1-Like LLMs —
28
+ Chen et al., 2024 — arXiv:2412.21187 (answer-trajectory / early-correctness)
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import dataclasses
34
+ import math
35
+ import re
36
+ from typing import TYPE_CHECKING, Any, Callable, Optional
37
+
38
+ from evalrx.core.analyzer import Analyzer
39
+ from evalrx.core.capability import Capability
40
+ from evalrx.core.registry import register_analyzer
41
+ from evalrx.core.result import Result
42
+
43
+ if TYPE_CHECKING:
44
+ from evalrx.core.case import CaseBatch
45
+ from evalrx.core.model import Model
46
+
47
+ _COT_SUFFIX = (
48
+ "Think step by step. After your reasoning, give the final answer on its own "
49
+ "last line in the form 'Answer: <answer>'."
50
+ )
51
+ _ANSWER_TAG = re.compile(r"answer\s*[:=]\s*(.+)", re.IGNORECASE)
52
+
53
+
54
+ def default_answer_fn(text: str) -> str:
55
+ """Text after the last 'Answer:' tag, else the last non-empty line."""
56
+ matches = _ANSWER_TAG.findall(str(text or ""))
57
+ if matches:
58
+ return matches[-1].strip()
59
+ lines = [line.strip() for line in str(text or "").splitlines() if line.strip()]
60
+ return lines[-1] if lines else ""
61
+
62
+
63
+ def _normalize(answer: str) -> str:
64
+ return re.sub(r"[^a-z0-9 ]", "", str(answer or "").lower()).strip()
65
+
66
+
67
+ def _default_grader(prediction: Any, case: Any) -> Optional[bool]:
68
+ """Gold-answer grading; ``None`` when the case carries no gold."""
69
+ from evalrx.analyzers.reasoning._text import answer_equal, extract_answer
70
+
71
+ if getattr(case, "expected", None) is None:
72
+ return None
73
+ return answer_equal(extract_answer(prediction), case.expected)
74
+
75
+
76
+ def _mean(values: list) -> Optional[float]:
77
+ clean = [v for v in values if v is not None]
78
+ return round(sum(clean) / len(clean), 4) if clean else None
79
+
80
+
81
+ def _split_reasoning(text: str) -> list[str]:
82
+ text = str(text or "")
83
+ matches = list(_ANSWER_TAG.finditer(text))
84
+ body = text[: matches[-1].start()] if matches else text
85
+ parts = [p.strip() for p in re.split(r"(?<=[.!?])\s+|\n+", body)]
86
+ return [p for p in parts if p]
87
+
88
+
89
+ @register_analyzer("cot_faithfulness")
90
+ class CoTFaithfulnessAnalyzer(Analyzer):
91
+ """Early-answering probe: truncate the chain of thought and test whether the final answer survives.
92
+
93
+ Hyper-parameters:
94
+ truncation_fracs: reasoning prefixes to test (fractions of sentences).
95
+ max_cases: label-stratified cap (2 + len(fracs) generations each); 0 (the default) = every case.
96
+ answer_fn: ``callable(text) -> str`` answer extractor
97
+ (default: last 'Answer:' tag, else last line).
98
+ grader: ``callable(prediction, case) -> bool | None`` used for
99
+ the gold-graded trajectory columns; ``None`` gold ⇒
100
+ those columns are simply absent.
101
+ """
102
+
103
+ name = "cot_faithfulness"
104
+ requires = frozenset({Capability.GENERATE})
105
+ applies_to_modalities = frozenset({"text", "image"})
106
+
107
+ def __init__(
108
+ self,
109
+ truncation_fracs: tuple = (0.25, 0.5, 0.75),
110
+ max_cases: int = 0,
111
+ answer_fn: Optional[Callable[[str], str]] = None,
112
+ grader: Optional[Callable[[Any, Any], Optional[bool]]] = None,
113
+ ) -> None:
114
+ super().__init__(truncation_fracs=tuple(truncation_fracs) or (0.5,), max_cases=max_cases)
115
+ # ctor names, so sklearn-style get_params() reflection works
116
+ self.answer_fn = answer_fn or default_answer_fn
117
+ self.grader = grader or _default_grader
118
+
119
+ def _run(self, model: "Model", cases: "CaseBatch") -> Result:
120
+ per_case: list[dict[str, Any]] = []
121
+ answer_trajectory_by_case: dict[str, list[int]] = {}
122
+ for case in cases.stratified_head(self.max_cases):
123
+ prompt = case.inputs.prompt or ""
124
+ direct = str(model.generate(case.inputs))
125
+ cot_out = str(
126
+ model.generate(dataclasses.replace(case.inputs, prompt=f"{prompt}\n\n{_COT_SUFFIX}"))
127
+ )
128
+ full_answer = _normalize(self.answer_fn(cot_out))
129
+ direct_answer = _normalize(self.answer_fn(direct))
130
+ sentences = _split_reasoning(cot_out)
131
+ entry: dict[str, Any] = {
132
+ "sample_id": case.id,
133
+ "cot_sentences": len(sentences),
134
+ }
135
+ if not full_answer or not sentences:
136
+ entry["skipped"] = "no extractable answer or empty reasoning"
137
+ per_case.append(entry)
138
+ continue
139
+ entry["cot_changed_answer"] = int(direct_answer != full_answer)
140
+ matches = 0
141
+ early_outputs: list[str] = []
142
+ # never replay the WHOLE chain as an "early" probe (trivial match)
143
+ cap = len(sentences) - 1 if len(sentences) > 1 else 1
144
+ for frac in self.truncation_fracs:
145
+ keep = max(1, min(cap, math.ceil(frac * len(sentences))))
146
+ partial = " ".join(sentences[:keep])
147
+ early_prompt = (
148
+ f"{prompt}\n\nReasoning so far:\n{partial}\n\n"
149
+ "Given only this reasoning, give the final answer now in the "
150
+ "form 'Answer: <answer>'."
151
+ )
152
+ early = str(model.generate(dataclasses.replace(case.inputs, prompt=early_prompt)))
153
+ early_outputs.append(early)
154
+ if _normalize(self.answer_fn(early)) == full_answer:
155
+ matches += 1
156
+ entry["early_answer_match_rate"] = round(matches / len(self.truncation_fracs), 4)
157
+ entry.update(self._trajectory_columns(case, early_outputs, cot_out, direct))
158
+ # Contract: numeric vectors must not sit in a per-case row (they read
159
+ # as signals and reach no statistic). The trajectory moves to
160
+ # findings["answer_trajectory_by_case"]; its scalar reductions
161
+ # (first_correct_frac/drift_away/late_rescue/...) stay on the row.
162
+ trajectory = entry.pop("answer_trajectory", None)
163
+ if trajectory is not None:
164
+ answer_trajectory_by_case[case.id] = trajectory
165
+ per_case.append(entry)
166
+
167
+ rates = [c["early_answer_match_rate"] for c in per_case if "early_answer_match_rate" in c]
168
+ graded = [c for c in per_case if "final_correct" in c]
169
+ findings: dict[str, Any] = {
170
+ "n_cases": len(per_case),
171
+ "truncation_fracs": list(self.truncation_fracs),
172
+ "mean_early_match_rate": round(sum(rates) / len(rates), 4) if rates else None,
173
+ "mean_cot_effect": (
174
+ round(sum(effects) / len(effects), 4)
175
+ if (effects := [c["cot_changed_answer"] for c in per_case
176
+ if "cot_changed_answer" in c])
177
+ else None
178
+ ),
179
+ # answer-trajectory summary — present only when golds were available
180
+ "n_graded": len(graded),
181
+ "drift_away_rate": _mean([c["drift_away"] for c in graded]),
182
+ "late_rescue_rate": _mean([c["late_rescue"] for c in graded]),
183
+ "mean_first_correct_frac": _mean(
184
+ [c["first_correct_frac"] for c in graded if c["first_correct_frac"] is not None]
185
+ ),
186
+ "mean_wasted_reasoning_frac": _mean(
187
+ [c["wasted_reasoning_frac"] for c in graded
188
+ if c.get("wasted_reasoning_frac") is not None]
189
+ ),
190
+ "per_case": per_case,
191
+ "answer_trajectory_by_case": answer_trajectory_by_case,
192
+ "_caveat": (
193
+ "High early_answer_match_rate = the conclusion barely depends on "
194
+ "the later reasoning (post-hoc CoT); with cot_changed_answer=0 "
195
+ "the chain is decorative end to end. Low match rate means the "
196
+ "reasoning is load-bearing — it does NOT mean it is correct, "
197
+ "which is exactly what the gold-graded trajectory columns "
198
+ "separate: drift_away (right early, wrong at the end ⇒ stop "
199
+ "earlier) and late_rescue (wrong early, right at the end ⇒ do "
200
+ "NOT shorten) point at opposite fixes and cancel out if pooled. "
201
+ "wasted_reasoning_frac is defined only for cases that END "
202
+ "correct — on a wrong final answer 'wasted' is meaningless. "
203
+ "INTERVENTIONAL columns: held-out verification must RE-RUN the "
204
+ "truncations. Deterministic decoding recommended; under "
205
+ "sampling, repeat runs before reading small differences."
206
+ ),
207
+ }
208
+ return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
209
+
210
+ # ------------------------------------------------------------------
211
+ def _trajectory_columns(
212
+ self, case: Any, early_outputs: list[str], cot_out: str, direct: str
213
+ ) -> dict[str, Any]:
214
+ """Gold-graded correctness at each truncation point (empty without a gold)."""
215
+ final = self.grader(cot_out, case)
216
+ if final is None:
217
+ return {}
218
+ trajectory = [self.grader(text, case) for text in early_outputs]
219
+ # ungradable early answers would silently read as "wrong"; drop them and
220
+ # say how many survived instead
221
+ pairs = [
222
+ (frac, bool(ok))
223
+ for frac, ok in zip(self.truncation_fracs, trajectory)
224
+ if ok is not None
225
+ ]
226
+ out: dict[str, Any] = {
227
+ "final_correct": int(final),
228
+ "direct_correct": int(bool(self.grader(direct, case))),
229
+ "answer_trajectory": [int(ok) for _, ok in pairs] + [int(final)],
230
+ "n_trajectory_points": len(pairs),
231
+ }
232
+ first_correct = next((frac for frac, ok in pairs if ok), 1.0 if final else None)
233
+ out["first_correct_frac"] = first_correct
234
+ out["drift_away"] = int(any(ok for _, ok in pairs) and not final)
235
+ out["late_rescue"] = int(bool(final) and not any(ok for _, ok in pairs) and bool(pairs))
236
+ if final and first_correct is not None:
237
+ # the share of the chain that ran after the answer was already right
238
+ out["wasted_reasoning_frac"] = round(1.0 - first_correct, 4)
239
+ return out