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,439 @@
1
+ """M1 — StrategyProbe: select which analyzers to run given a model and a batch.
2
+
3
+ Ranking composes per-SLOT priority lists rather than looking up one per-kind
4
+ list, because a model is not one kind. ``LLM`` / ``VLM`` / ``ALM`` / ``AVLM``
5
+ are four subsets of ``{text, image, audio, video}``, and an enum over the
6
+ subsets grows as 2^n while composing over the members grows as n.
7
+
8
+ Which slots to compose over is decided by the BATCH, not by the model. An omni
9
+ model declares every modality it can consume; evaluated on an audio benchmark it
10
+ still declares image, and ranking on the declaration put image analyzers at the
11
+ top of an audio run. :func:`~evalrx.core.case.probed_modalities` answers
12
+ what is actually under test, and the model's declaration is the fallback for a
13
+ batch that fills no media slot at all — no evidence, rather than evidence of
14
+ absence.
15
+
16
+ Usage::
17
+
18
+ probe = StrategyProbe()
19
+ slots = probe.routed_slots(model, cases) # {"text", "audio"} for an ALM run
20
+ kind = probe.detect_kind(model) # ModelKind.VLM / ALM / AVLM / AGENT / LLM
21
+ names = probe.select(model, max_analyzers=4, data=cases)
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from enum import Enum
27
+ from typing import TYPE_CHECKING, Any, Iterable
28
+
29
+ from evalrx.core.capability import Capability
30
+ from evalrx.core.case import MEDIA_SLOTS, probed_modalities
31
+ from evalrx.core.registry import registry
32
+
33
+
34
+ def _carries_trajectories(data: Any) -> bool:
35
+ """True when *data* (CaseBatch / iterable of cases) has any trajectory-carrying case."""
36
+ try:
37
+ return any(getattr(c, "trajectory", None) is not None for c in data)
38
+ except TypeError:
39
+ return False
40
+
41
+ if TYPE_CHECKING:
42
+ from evalrx.core.model import Model
43
+
44
+
45
+ class ModelKind(str, Enum):
46
+ """A coarse label for a model's modality set.
47
+
48
+ Kept for display, logging and the ``priority_override`` API — NOT for
49
+ routing. It is a lossy rendering of a set (an AVLM answering a text-only
50
+ question is still AVLM here), which is exactly why :meth:`StrategyProbe.select`
51
+ composes over :meth:`StrategyProbe.routed_slots` instead of branching on this.
52
+ """
53
+
54
+ VLM = "vlm" # image — hallucination + attention analyzers first
55
+ ALM = "alm" # audio, no image — listening / grounding analyzers first
56
+ AVLM = "avlm" # audio AND image — both, audio ranked first
57
+ AGENT = "agent" # trajectories or TOOL_CALLS — behavioral analyzers first
58
+ LLM = "llm" # text only — interpretability analyzers first
59
+
60
+
61
+ # Maps failure-mode tags (from M3 hypotheses) to the analyzers most likely to
62
+ # surface evidence for that mode. Used in cycle 2+ to focus the probe on what
63
+ # the diagnosis agent flagged rather than running the same generic priority list.
64
+ _FAILURE_MODE_TO_ANALYZERS: dict[str, list[str]] = {
65
+ "attention_sink": ["attention_sink"],
66
+ "attention": ["attention", "attention_rollout"],
67
+ "hallucination": ["pope", "chair", "selfcheck_consistency"],
68
+ "low_consistency": ["self_consistency"],
69
+ "unstable_generation": ["self_consistency"],
70
+ "overconfidence": ["verbalized_confidence", "calibration"],
71
+ "miscalibrated_confidence": ["verbalized_confidence", "calibration"],
72
+ "format_bias": ["format_sensitivity"],
73
+ "position_bias": ["format_sensitivity"],
74
+ "unfaithful_reasoning": ["cot_faithfulness"],
75
+ "post_hoc_reasoning": ["cot_faithfulness"],
76
+ "context_ignored": ["context_shap"],
77
+ "premature_layer_divergence": ["layer_contrast"],
78
+ "confident_inconsistency": ["self_consistency", "verbalized_confidence"],
79
+ "loop": ["loop_detect"],
80
+ "ignored_obs": ["ignored_obs"],
81
+ # Decision-layer mechanisms — discriminated by prompt interventions.
82
+ "language_prior_bias": ["prompt_contrast"],
83
+ "negative_bias": ["prompt_contrast", "pope"],
84
+ "threshold_miscalibration": ["prompt_contrast"],
85
+ "prompt_formatting_bias": ["prompt_contrast"],
86
+ "instruction_following": ["prompt_contrast"],
87
+ "entropy": ["token_entropy", "logprob_entropy"],
88
+ "perplexity": ["logprob_entropy"],
89
+ "logit_lens": ["logit_lens"],
90
+ "representational_collapse": ["cka"],
91
+ "numerical_hallucination": ["self_consistency", "verbalized_confidence"],
92
+ # Text-reasoning mechanisms (2026-08). The hygiene pair is listed under the
93
+ # names an M3 hypothesis uses when it blames the harness rather than the model.
94
+ "answer_extraction": ["answer_extraction_audit"],
95
+ "parse_failure": ["answer_extraction_audit"],
96
+ "truncation": ["termination_audit"],
97
+ "degenerate_repetition": ["termination_audit"],
98
+ "premature_termination": ["termination_audit"],
99
+ "arithmetic_error": ["arith_audit"],
100
+ "computation_slip": ["arith_audit"],
101
+ "chain_break": ["arith_audit", "step_rollout_value"],
102
+ "reasoning_break": ["step_rollout_value", "arith_audit"],
103
+ "overthinking": ["cot_faithfulness", "step_rollout_value"],
104
+ "self_correction_failure": ["self_repair"],
105
+ "knowledge_gap": ["knowledge_reasoning_split"],
106
+ "compositionality_gap": ["knowledge_reasoning_split"],
107
+ "selection_failure": ["coverage_verification_gap"],
108
+ "verification_gap": ["coverage_verification_gap"],
109
+ "brittleness": ["perturbation_battery"],
110
+ "surface_form_sensitivity": ["perturbation_battery", "format_sensitivity"],
111
+ "memorization": ["contamination_score", "perturbation_battery"],
112
+ "contamination": ["contamination_score"],
113
+ "semantic_uncertainty": ["self_consistency"],
114
+ }
115
+
116
+ # Per-SLOT ordered priority: high → low diagnostic value for false attribution.
117
+ #
118
+ # Composed, not looked up: ``select`` concatenates the lists for the slots the
119
+ # batch actually fills (agent, then video, audio, image, then always text) and
120
+ # appends the remaining compatible analyzers alphabetically. Adding a modality
121
+ # is one more entry here; it does not multiply the table the way a per-kind
122
+ # table does, and an AVLM needs no entry of its own — it is audio + image.
123
+ _SLOT_PRIORITY: dict[str, list[str]] = {
124
+ "agent": [
125
+ "loop_detect", "ignored_obs", # behavioral heuristics
126
+ "first_error_judge", "trajectory_rubric", # LLM-judge localisation + classification
127
+ "counterfactual", "reliability_probe", # re-run probes: step perturbation, pass@k
128
+ "tool_shap", # re-run probe: tool-subset Shapley
129
+ ],
130
+ "video": [
131
+ "modality_ablation", # is the video load-bearing at all?
132
+ "relative_attn", # attention across frames
133
+ "mm_shap", # per-slot reliance
134
+ ],
135
+ "audio": [
136
+ # Grounding first, for the same reason the text list leads with hygiene:
137
+ # an ALM that answers from the language prior alone produces failures that
138
+ # mimic every mechanism below, and nothing else here would reveal it.
139
+ "modality_ablation", # does replacing the audio change the answer?
140
+ "mm_shap", # audio vs text reliance
141
+ "answer_extraction_audit", # is the FAIL label real or a parse miss?
142
+ "termination_audit", # truncated / degenerate / gave up?
143
+ "prompt_contrast", # are failures prompt-repairable?
144
+ "selfcheck_consistency", # hallucinated content in the description
145
+ "self_consistency", "logprob_entropy",
146
+ "attention", "attention_sink", # where does it attend among audio tokens?
147
+ "verbalized_confidence", "calibration",
148
+ ],
149
+ "image": [
150
+ "pope", "chair", # hallucination metrics
151
+ "modality_ablation", # is the image load-bearing at all?
152
+ "attention", "attention_rollout", # where is the model looking?
153
+ "attention_sink", # sink collapse?
154
+ "prompt_contrast", # are failures prompt-repairable?
155
+ "mm_shap", # text vs image reliance
156
+ "logprob_entropy", "self_consistency",
157
+ ],
158
+ "text": [], # filled below from the legacy LLM list
159
+ }
160
+
161
+ # Back-compat view: ``StrategyProbe(priority_override=...)`` and the docs are
162
+ # written against ModelKind keys. Composition is the default path; an override
163
+ # still selects one list by kind, exactly as before.
164
+ _PRIORITY: dict[str, list[str]] = {
165
+ ModelKind.VLM: [
166
+ "pope", "chair", # hallucination metrics
167
+ "attention", "attention_rollout", # where is the model looking?
168
+ "attention_sink", # sink collapse?
169
+ "prompt_contrast", # are failures prompt-repairable?
170
+ "mm_shap", # text vs image reliance
171
+ "logprob_entropy", "self_consistency",
172
+ ],
173
+ ModelKind.AGENT: [
174
+ "loop_detect", "ignored_obs", # behavioral heuristics
175
+ "first_error_judge", "trajectory_rubric", # LLM-judge localisation + classification
176
+ "counterfactual", "reliability_probe", # re-run probes: step perturbation, pass@k
177
+ "tool_shap", # re-run probe: tool-subset Shapley
178
+ ],
179
+ ModelKind.LLM: [
180
+ # Hygiene first: both produce confounds that mimic every mechanism
181
+ # column below, so a finding read before them is not interpretable.
182
+ "answer_extraction_audit", # is the FAIL label real or a parse miss?
183
+ "termination_audit", # truncated / degenerate / gave up?
184
+ "arith_audit", # computation slip vs chain break (free)
185
+ "selfcheck_consistency", # text hallucination (black-box)
186
+ "format_sensitivity", # MC position bias vs content-tracking
187
+ "cot_faithfulness", # is the reasoning load-bearing?
188
+ "coverage_verification_gap", # cannot solve vs cannot select
189
+ "perturbation_battery", # invariance breaks / missing sensitivity
190
+ "self_repair", # detect / correct / DAMAGE
191
+ "knowledge_reasoning_split", # missing fact vs broken composition
192
+ "calibration", # ECE / overconfidence vs labels
193
+ "attention", "logit_lens", # interpretability
194
+ "layer_contrast", # DoLa/DeCo divergence signal
195
+ "token_entropy", "logprob_entropy",
196
+ "attention_sink", "attention_rollout",
197
+ "prompt_contrast", # are failures prompt-repairable?
198
+ "context_shap", # RAG context dependence
199
+ "cka", "self_consistency", "verbalized_confidence",
200
+ "step_rollout_value", # where the chain broke (expensive)
201
+ "contamination_score", # is the benchmark measuring recall?
202
+ ],
203
+ }
204
+
205
+ # text IS the legacy LLM list: every model has a prompt, so this is the floor
206
+ # every composition starts from rather than a peer of the media slots.
207
+ _SLOT_PRIORITY["text"] = _PRIORITY[ModelKind.LLM]
208
+
209
+ # Slots each ModelKind stands for, in composition order. Only the back-compat
210
+ # ``_PRIORITY`` view and ``detect_kind`` read this; ``select`` uses the batch.
211
+ _KIND_SLOTS: dict[str, tuple[str, ...]] = {
212
+ ModelKind.AGENT: ("agent", "text"),
213
+ ModelKind.AVLM: ("audio", "image", "text"),
214
+ ModelKind.ALM: ("audio", "text"),
215
+ ModelKind.VLM: ("image", "text"),
216
+ ModelKind.LLM: ("text",),
217
+ }
218
+
219
+
220
+ def _compose(slots: "Iterable[str]") -> "list[str]":
221
+ """Concatenate the slot lists in ranking order, first occurrence wins.
222
+
223
+ ``dict.fromkeys`` rather than a set: an analyzer listed under two slots (a
224
+ VLM run also ranks ``mm_shap`` under image) must keep the position its
225
+ highest-priority slot gave it, and a set would lose the order entirely.
226
+ """
227
+ ordered: list[str] = []
228
+ for slot in slots:
229
+ ordered.extend(_SLOT_PRIORITY.get(slot, ()))
230
+ return list(dict.fromkeys(ordered))
231
+
232
+
233
+ # ALM / AVLM complete the back-compat view; they never had an entry because the
234
+ # enum had no member for them.
235
+ for _kind, _slots in _KIND_SLOTS.items():
236
+ _PRIORITY.setdefault(_kind, _compose(_slots))
237
+
238
+
239
+ def get_analyzer_catalog(model: "Model") -> dict[str, str]:
240
+ """Return ``{name: description}`` for all analyzers compatible with *model*.
241
+
242
+ Descriptions come from each analyzer class's ``description`` attribute or,
243
+ if absent, the first non-empty line of its docstring. Used by
244
+ :class:`~evalrx.eval_agent.probe_agent.ProbeAgent` to build the LLM
245
+ selection prompt so the judge understands what each analyzer measures.
246
+ """
247
+ compatible = set(registry.analyzers.names_compatible_with(model))
248
+ catalog: dict[str, str] = {}
249
+ for name in sorted(compatible):
250
+ cls = registry.analyzers.get(name)
251
+ if cls is None:
252
+ continue
253
+ desc: str | None = getattr(cls, "description", None)
254
+ if not desc and cls.__doc__:
255
+ for line in cls.__doc__.strip().splitlines():
256
+ line = line.strip()
257
+ if line:
258
+ desc = line
259
+ break
260
+ catalog[name] = desc or name
261
+ return catalog
262
+
263
+
264
+ class StrategyProbe:
265
+ """Selects analyzers appropriate for a given model.
266
+
267
+ Args:
268
+ priority_override: Replaces the built-in per-kind priority tables
269
+ (keyed by ``ModelKind``). Useful for domain-specific orderings.
270
+ """
271
+
272
+ def __init__(self, priority_override: dict[str, list[str]] | None = None) -> None:
273
+ #: ``None`` selects the composed-per-slot path. A caller-supplied table
274
+ #: is keyed by ModelKind and is looked up, not composed — an override is
275
+ #: an explicit statement about ordering and must not be reordered by the
276
+ #: batch's modality slots.
277
+ self._priority = priority_override
278
+
279
+ @staticmethod
280
+ def slot_starved(names: "set[str]", data: Any = None, model: "Model | None" = None) -> "set[str]":
281
+ """Analyzers whose required modality slot the batch never fills.
282
+
283
+ The registry match is an intersection against the MODEL's declaration,
284
+ so one shared member (``"text"``, which every model has) is enough to
285
+ make an image analyzer "compatible" with an audio run. It is then
286
+ selected, runs, reads an empty slot and reports a number computed over
287
+ nothing — the same silent-emptiness failure ``requires_trajectories``
288
+ was added to close.
289
+
290
+ A batch that fills NO media slot is ambiguous — media that was never
291
+ persisted looks identical to media that was never there — so nothing is
292
+ dropped, unless *model* is given and declares no media modality either.
293
+ Then there is no ambiguity left to protect: a text-only model cannot have
294
+ had an image, and keeping the gate off would offer a text run every
295
+ media analyzer in the registry.
296
+
297
+ ``data=None`` is no evidence at all; nothing is dropped.
298
+ """
299
+ if data is None:
300
+ return set()
301
+ filled = probed_modalities(data)
302
+ if not (filled & set(MEDIA_SLOTS)):
303
+ declared = set(getattr(model, "modalities", None) or ()) if model is not None else None
304
+ if declared is None or (declared & set(MEDIA_SLOTS)):
305
+ return set()
306
+ starved: set[str] = set()
307
+ for name in names:
308
+ cls = registry.analyzers.get(name)
309
+ required = set(getattr(cls, "requires_modalities", frozenset()) or ())
310
+ if required and not (required & filled):
311
+ starved.add(name)
312
+ return starved
313
+
314
+ # -- what a run is about -------------------------------------------
315
+ @staticmethod
316
+ def is_agent_run(model: "Model", data: Any = None) -> bool:
317
+ """Whether this run should be ranked as an agent run.
318
+
319
+ Orthogonal to modality: a VLM driving a tool loop is both. Trajectories
320
+ in the batch are definitive; without them, declared TOOL_CALLS is the
321
+ weaker fallback.
322
+ """
323
+ if data is not None and _carries_trajectories(data):
324
+ return True
325
+ return Capability.TOOL_CALLS in getattr(model, "capabilities", frozenset())
326
+
327
+ @staticmethod
328
+ def routed_slots(model: "Model", data: Any = None) -> set[str]:
329
+ """The modality slots ranking should compose over.
330
+
331
+ Normally the model's declared modalities intersected with the slots the
332
+ batch actually fills, because both have to hold: an analyzer needs the
333
+ model to accept the modality AND the data to contain it.
334
+
335
+ Falls back to the model's declaration when the batch fills no media slot
336
+ at all. A batch with no images is not evidence that images are not under
337
+ test — it may be a text-only sample of a larger benchmark, or a batch
338
+ whose media never got persisted — and treating "nothing observed" as
339
+ "modality absent" is the same collapse the contract's rule 4 forbids.
340
+ The two are recorded separately in ``AnalyzerSelection`` so a reader can
341
+ see which path was taken.
342
+ """
343
+ declared = set(getattr(model, "modalities", frozenset({"text"})) or {"text"})
344
+ probed = probed_modalities(data) if data is not None else {"text"}
345
+ if not (probed & set(MEDIA_SLOTS)):
346
+ return declared | {"text"}
347
+ return (declared & probed) | {"text"}
348
+
349
+ def detect_kind(self, model: "Model", data: Any = None) -> ModelKind:
350
+ """Coarse label for the run — display and ``priority_override`` only.
351
+
352
+ Trajectory-carrying *data* is the definitive agent signal and wins
353
+ outright: a VLM that drove a tool loop should get the AGENT analyzer
354
+ priority (loop detection, first-error attribution), not the VLM one.
355
+ Without trajectories, media modality takes priority over TOOL_CALLS so
356
+ that VLMs that merely *support* tool use (e.g. Qwen3-VL) are treated
357
+ as VLMs, not agents.
358
+
359
+ Reads the MODEL's declaration, not the batch — the label answers "what
360
+ is this model", and narrowing it by the data would make an omni model
361
+ report a different identity per benchmark. :meth:`routed_slots` is the
362
+ one that narrows, and it is what ranking actually uses.
363
+ """
364
+ if data is not None and _carries_trajectories(data):
365
+ return ModelKind.AGENT
366
+ modalities = set(getattr(model, "modalities", frozenset({"text"})) or {"text"})
367
+ has_image = bool(modalities & {"image", "video"})
368
+ has_audio = "audio" in modalities
369
+ if has_image and has_audio:
370
+ return ModelKind.AVLM
371
+ if has_image:
372
+ return ModelKind.VLM
373
+ if has_audio:
374
+ return ModelKind.ALM
375
+ if Capability.TOOL_CALLS in getattr(model, "capabilities", frozenset()):
376
+ return ModelKind.AGENT
377
+ return ModelKind.LLM
378
+
379
+ def select(
380
+ self,
381
+ model: "Model",
382
+ max_analyzers: int | None = None,
383
+ hint_failure_modes: list[str] | None = None,
384
+ data: Any = None,
385
+ ) -> list[str]:
386
+ """Return compatible analyzer names ranked by diagnostic priority.
387
+
388
+ Args:
389
+ model: The model to analyse.
390
+ max_analyzers: If given, cap the returned list at this length.
391
+ hint_failure_modes: Failure-mode tags from outstanding M3 hypotheses.
392
+ Analyzers that match a hint are promoted to the
393
+ front of the ranked list for focused follow-up.
394
+ data: The case batch about to be probed, when available.
395
+ Trajectory-carrying cases flip the ranking to the
396
+ AGENT priority list (see :meth:`detect_kind`).
397
+
398
+ Returns:
399
+ Ordered list of registered analyzer names. Hint-matched items
400
+ come first, then the standard priority-list items, then remaining
401
+ compatible analyzers sorted alphabetically.
402
+ """
403
+ compatible = set(registry.analyzers.names_compatible_with(model))
404
+ compatible -= self.slot_starved(compatible, data, model)
405
+
406
+ if self._priority is not None:
407
+ # A caller-supplied table is an allowlist as well as an ordering.
408
+ # In particular, benchmark ``pinned`` mode must not silently fill
409
+ # an unsupported pinned slot with an unrelated alphabetical probe.
410
+ priority = self._priority.get(self.detect_kind(model, data), [])
411
+ ranked = [name for name in priority if name in compatible]
412
+ else:
413
+ slots = self.routed_slots(model, data)
414
+ media = [s for s in ("video", "audio", "image") if s in slots]
415
+ # Trajectories in the batch are definitive and lead outright. Merely
416
+ # DECLARING tool support does not: a VLM that supports tools (Qwen3-VL)
417
+ # is still being asked a vision question, so media outranks the
418
+ # capability — the capability only leads when no media slot is in play.
419
+ agent_leads = (data is not None and _carries_trajectories(data)) or (
420
+ not media and self.is_agent_run(model, data)
421
+ )
422
+ priority = _compose((["agent"] if agent_leads else []) + media + ["text"])
423
+ ranked = [name for name in priority if name in compatible]
424
+ ranked += sorted(compatible - set(ranked))
425
+
426
+ if hint_failure_modes:
427
+ # Promote analyzers that map to outstanding failure modes, preserving
428
+ # their relative order and avoiding duplicates.
429
+ boosted = dict.fromkeys(
430
+ a
431
+ for mode in hint_failure_modes
432
+ for a in _FAILURE_MODE_TO_ANALYZERS.get(mode.lower().replace(" ", "_"), [])
433
+ if a in compatible and (self._priority is None or a in ranked)
434
+ )
435
+ ranked = list(boosted) + [a for a in ranked if a not in boosted]
436
+
437
+ if max_analyzers is not None:
438
+ ranked = ranked[:max_analyzers]
439
+ return ranked