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,301 @@
1
+ """Built-in OpenAI-compatible client — chat_fn/generate_fn factories for APIModel.
2
+
3
+ Covers every OpenAI-compatible endpoint with one code path: a local
4
+ ``vllm serve`` (the scale path for open Qwen3-VL agents — served with
5
+ ``--enable-auto-tool-choice --tool-call-parser hermes`` it returns native
6
+ structured ``tool_calls``), OpenAI itself, or any compatible gateway.
7
+
8
+ Wire conversion: agent messages use the transformers-style content-block
9
+ convention (``{"type": "image", "image": PIL | path | URL}``); this module
10
+ converts image blocks to OpenAI ``image_url`` blocks (data URLs for local
11
+ images). Sampling defaults to ``temperature=0`` — diagnosed runs should be
12
+ as deterministic as the endpoint allows.
13
+
14
+ Torch-free; ``openai`` and ``PIL`` are imported lazily.
15
+
16
+ Usage::
17
+
18
+ from evalrx import compose
19
+ from evalrx.models.backends.openai_compat import openai_runtime
20
+
21
+ rt = openai_runtime(base_url="http://localhost:8901/v1") # vllm serve
22
+ vlm = compose("qwen3-vl-2b-instruct", "api", runtime=rt)
23
+ Agent(vlm, tools=[...]).run(case)
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import base64
29
+ import io
30
+ from typing import Any, Callable, Optional
31
+
32
+ from evalrx.core.tool import ChatTurn
33
+ from evalrx.models.backends.api import parse_openai_logprobs
34
+ from evalrx.models.backends.base import RuntimeConfig
35
+
36
+
37
+ def _to_data_url(image: Any) -> str:
38
+ """Encode *image* (PIL / path / URL) as an ``image_url`` value.
39
+
40
+ http(s) URLs pass through; everything else becomes a base64 PNG data URL.
41
+ """
42
+ if isinstance(image, str) and image.startswith(("http://", "https://", "data:")):
43
+ return image
44
+ if not (hasattr(image, "save") and hasattr(image, "mode")): # a path -> open it
45
+ from PIL import Image
46
+
47
+ image = Image.open(image).convert("RGB")
48
+ buf = io.BytesIO()
49
+ image.save(buf, format="PNG")
50
+ return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("ascii")
51
+
52
+
53
+ _AUDIO_FORMATS = {".wav": "wav", ".mp3": "mp3", ".flac": "flac", ".ogg": "ogg", ".m4a": "m4a"}
54
+
55
+
56
+ def _to_input_audio(audio: Any, sample_rate: int = 16000) -> dict:
57
+ """Encode *audio* as an OpenAI ``input_audio`` value (``{"data", "format"}``).
58
+
59
+ Accepts a path (format from the suffix), raw bytes (assumed WAV), or a
60
+ waveform -- ``np.ndarray`` or ``(np.ndarray, sample_rate)`` -- which is
61
+ written as 16-bit PCM WAV with the stdlib ``wave`` module so the endpoint
62
+ path needs no audio library of its own.
63
+ """
64
+ import os
65
+
66
+ fmt = "wav"
67
+ if isinstance(audio, (bytes, bytearray)):
68
+ data = bytes(audio)
69
+ elif isinstance(audio, (str, os.PathLike)):
70
+ path = os.fspath(audio)
71
+ fmt = _AUDIO_FORMATS.get(os.path.splitext(path)[1].lower(), "wav")
72
+ with open(path, "rb") as handle:
73
+ data = handle.read()
74
+ else:
75
+ import io
76
+ import wave
77
+
78
+ import numpy as np
79
+
80
+ if isinstance(audio, tuple) and len(audio) == 2:
81
+ audio, sample_rate = audio
82
+ arr = np.asarray(audio)
83
+ if arr.ndim > 1: # (channels, n) or (n, channels) -> mono
84
+ arr = arr.mean(axis=0 if arr.shape[0] <= 8 else 1)
85
+ if np.issubdtype(arr.dtype, np.floating):
86
+ arr = np.clip(arr, -1.0, 1.0) * 32767.0
87
+ pcm = arr.astype("<i2").tobytes()
88
+ buf = io.BytesIO()
89
+ with wave.open(buf, "wb") as wav:
90
+ wav.setnchannels(1)
91
+ wav.setsampwidth(2)
92
+ wav.setframerate(int(sample_rate))
93
+ wav.writeframes(pcm)
94
+ data = buf.getvalue()
95
+ return {"data": base64.b64encode(data).decode("ascii"), "format": fmt}
96
+
97
+
98
+ def user_message(prompt: str, *, image: Any = None, audio: Any = None) -> dict:
99
+ """One user turn: a plain string, or content blocks when a media slot is set.
100
+
101
+ The blocks use the internal convention (``{"type": "image", "image": ...}``
102
+ / ``{"type": "audio", "audio": ...}``) that :func:`to_openai_messages`
103
+ converts, so ``chat_fn`` callers and ``generate_fn`` callers meet the wire
104
+ in one place.
105
+ """
106
+ if image is None and audio is None:
107
+ return {"role": "user", "content": prompt}
108
+ blocks: list[dict] = []
109
+ if audio is not None:
110
+ blocks.append({"type": "audio", "audio": audio})
111
+ if image is not None:
112
+ blocks.append({"type": "image", "image": image})
113
+ blocks.append({"type": "text", "text": prompt})
114
+ return {"role": "user", "content": blocks}
115
+
116
+
117
+ def to_openai_messages(messages: list) -> list[dict]:
118
+ """Convert internal content-block messages into OpenAI wire format.
119
+
120
+ Plain-string content, assistant ``tool_calls`` and ``role="tool"`` results
121
+ pass through unchanged; ``{"type": "image", "image": ...}`` blocks become
122
+ ``image_url`` blocks and ``{"type": "audio", "audio": ...}`` blocks become
123
+ ``input_audio`` blocks (base64 WAV/MP3/FLAC -- what ``vllm serve`` reads
124
+ for an audio model).
125
+ """
126
+ out: list[dict] = []
127
+ for msg in messages:
128
+ content = msg.get("content")
129
+ if not isinstance(content, list):
130
+ out.append(msg)
131
+ continue
132
+ blocks: list[dict] = []
133
+ for block in content:
134
+ if isinstance(block, dict) and block.get("type") == "image":
135
+ blocks.append(
136
+ {"type": "image_url", "image_url": {"url": _to_data_url(block.get("image"))}}
137
+ )
138
+ elif isinstance(block, dict) and block.get("type") == "audio":
139
+ blocks.append(
140
+ {"type": "input_audio", "input_audio": _to_input_audio(block.get("audio"))}
141
+ )
142
+ else:
143
+ blocks.append(block)
144
+ out.append({**msg, "content": blocks})
145
+ return out
146
+
147
+
148
+ def _raw_tool_calls(message: Any) -> Optional[list]:
149
+ calls = getattr(message, "tool_calls", None)
150
+ if not calls:
151
+ return None
152
+ return [c.model_dump() if hasattr(c, "model_dump") else dict(c) for c in calls]
153
+
154
+
155
+ def openai_chat_fn(
156
+ *,
157
+ base_url: Optional[str] = None,
158
+ api_key: Optional[str] = None,
159
+ client: Any = None,
160
+ timeout: float = 300.0,
161
+ **sampling: Any,
162
+ ) -> Callable[..., ChatTurn]:
163
+ """Build a tool-aware ``chat_fn(messages, tools, model) -> ChatTurn``.
164
+
165
+ *client* may be injected (tests, custom transports); otherwise a lazy
166
+ ``openai.OpenAI(base_url=..., api_key=...)`` is created on first call —
167
+ ``api_key`` defaults to ``"EMPTY"``, which is what a local ``vllm serve``
168
+ expects. Extra keyword arguments become sampling parameters on every
169
+ request (``temperature`` defaults to 0).
170
+ """
171
+ sampling.setdefault("temperature", 0.0)
172
+ state = {"client": client}
173
+
174
+ def _client():
175
+ if state["client"] is None:
176
+ import openai
177
+
178
+ state["client"] = openai.OpenAI(
179
+ base_url=base_url, api_key=api_key or "EMPTY", timeout=timeout
180
+ )
181
+ return state["client"]
182
+
183
+ def _fn(messages: list, tools=None, model: str = "") -> ChatTurn:
184
+ kwargs: dict[str, Any] = dict(sampling)
185
+ if tools:
186
+ kwargs["tools"] = tools
187
+ kwargs.setdefault("tool_choice", "auto")
188
+ resp = _client().chat.completions.create(
189
+ model=model, messages=to_openai_messages(messages), **kwargs
190
+ )
191
+ choice = resp.choices[0]
192
+ usage = getattr(resp, "usage", None)
193
+ return ChatTurn(
194
+ text=choice.message.content or "",
195
+ raw_tool_calls=_raw_tool_calls(choice.message),
196
+ finish_reason=choice.finish_reason,
197
+ usage=(
198
+ {
199
+ "prompt_tokens": int(getattr(usage, "prompt_tokens", 0) or 0),
200
+ "completion_tokens": int(getattr(usage, "completion_tokens", 0) or 0),
201
+ }
202
+ if usage is not None
203
+ else None
204
+ ),
205
+ )
206
+
207
+ return _fn
208
+
209
+
210
+ def openai_generate_fn(
211
+ *,
212
+ base_url: Optional[str] = None,
213
+ api_key: Optional[str] = None,
214
+ client: Any = None,
215
+ timeout: float = 300.0,
216
+ **sampling: Any,
217
+ ) -> Callable[..., str]:
218
+ """Build a simple ``generate_fn(prompt, model=...) -> str`` on the same client."""
219
+ chat = openai_chat_fn(
220
+ base_url=base_url, api_key=api_key, client=client, timeout=timeout, **sampling
221
+ )
222
+
223
+ def _fn(prompt: str, model: str = "", *, image: Any = None, audio: Any = None, **kw) -> str:
224
+ return chat([user_message(prompt, image=image, audio=audio)], tools=None, model=model).text
225
+
226
+ return _fn
227
+
228
+
229
+ def openai_logprobs_fn(
230
+ *,
231
+ base_url: Optional[str] = None,
232
+ api_key: Optional[str] = None,
233
+ client: Any = None,
234
+ timeout: float = 300.0,
235
+ top_logprobs: int = 5,
236
+ **sampling: Any,
237
+ ) -> Callable[..., list]:
238
+ """Build a ``logprobs_fn(prompt, model=...) -> list[TokenLogprob]``.
239
+
240
+ An OpenAI-compatible server returns per-token logprobs when asked, and
241
+ ``vllm serve`` is one. Without this the api backend declares only GENERATE,
242
+ so every analyzer that reads answer-token uncertainty is skipped as
243
+ unsupported -- on the LLM benchmark set that is `calibration` and
244
+ `logprob_entropy`, two of the eight pinned probes, dropped not because the
245
+ server cannot answer but because nobody asked it.
246
+ """
247
+ sampling.setdefault("temperature", 0.0)
248
+ state = {"client": client}
249
+
250
+ def _client():
251
+ if state["client"] is None:
252
+ import openai
253
+
254
+ state["client"] = openai.OpenAI(
255
+ base_url=base_url, api_key=api_key or "EMPTY", timeout=timeout
256
+ )
257
+ return state["client"]
258
+
259
+ def _fn(prompt: str, model: str = "", *, image: Any = None, audio: Any = None, **kw) -> list:
260
+ kwargs: dict[str, Any] = {**sampling, **kw}
261
+ kwargs["logprobs"] = True
262
+ kwargs["top_logprobs"] = int(kwargs.pop("top_logprobs", top_logprobs))
263
+ resp = _client().chat.completions.create(
264
+ model=model,
265
+ messages=to_openai_messages([user_message(prompt, image=image, audio=audio)]),
266
+ **kwargs,
267
+ )
268
+ logprobs = getattr(resp.choices[0], "logprobs", None)
269
+ content = getattr(logprobs, "content", None) if logprobs is not None else None
270
+ # A server that accepts `logprobs=True` and returns nothing has told us
271
+ # it does not support them. Returning [] would read as "this answer had
272
+ # no tokens", so let the caller see the empty result for what it is.
273
+ return parse_openai_logprobs(
274
+ [item.model_dump() if hasattr(item, "model_dump") else item for item in (content or [])]
275
+ )
276
+
277
+ return _fn
278
+
279
+
280
+ def openai_runtime(
281
+ *,
282
+ base_url: Optional[str] = None,
283
+ api_key: Optional[str] = None,
284
+ client: Any = None,
285
+ timeout: float = 300.0,
286
+ with_logprobs: bool = True,
287
+ **sampling: Any,
288
+ ) -> RuntimeConfig:
289
+ """A ready :class:`RuntimeConfig` for ``compose(key, "api", runtime=...)``.
290
+
291
+ ``with_logprobs`` wires the logprobs path, which is what makes the backend
292
+ claim :attr:`Capability.LOGPROBS`. Pass False for an endpoint that rejects
293
+ the parameter -- claiming a capability the server does not have turns a
294
+ skipped analyzer into a failing one.
295
+ """
296
+ kw = dict(base_url=base_url, api_key=api_key, client=client, timeout=timeout, **sampling)
297
+ return RuntimeConfig(
298
+ chat_fn=openai_chat_fn(**kw),
299
+ generate_fn=openai_generate_fn(**kw),
300
+ logprobs_fn=openai_logprobs_fn(**kw) if with_logprobs else None,
301
+ )
@@ -0,0 +1,116 @@
1
+ """vLLM offline backend — in-process, high-throughput generation + logprobs.
2
+
3
+ The *distinct* role of vLLM (NOT the same as ``api``): an in-process engine you
4
+ submit batches to, with continuous batching and ``prompt_logprobs`` — the
5
+ throughput sweet spot for perturbation sweeps (RISE / SHAP) and logprob scoring
6
+ on open weights. It is a **black-box** backend: it exposes outputs only
7
+ (``GENERATE`` / ``TOOL_CALLS`` / ``LOGPROBS``) and **never** attention / hidden
8
+ states / gradients — paged/fused kernels don't materialise them. White-box
9
+ capture is ``hf_local``'s job.
10
+
11
+ (A ``vllm serve`` HTTP endpoint, by contrast, is reached via the ``api`` backend.)
12
+
13
+ ``vllm`` is imported lazily inside ``load()`` so this module imports without it.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ from evalrx.core.capability import Capability, CapabilityError
21
+ from evalrx.core.case import Inputs
22
+ from evalrx.core.model import Model, TokenLogprob, Trace
23
+ from evalrx.core.tool import ChatTurn
24
+ from evalrx.models.backends.base import Backend, RuntimeConfig
25
+
26
+
27
+ class VLLMOfflineModel(Model):
28
+ """A locally-loaded vLLM engine driven offline (batched)."""
29
+
30
+ def __init__(self, spec, runtime: RuntimeConfig) -> None:
31
+ self.spec = spec
32
+ self.runtime = runtime
33
+ self._llm = None # lazy vllm.LLM
34
+ caps = {Capability.GENERATE, Capability.LOGPROBS}
35
+ if spec.tool_calling: # conditional, like hf_local: needs a tool chat template
36
+ caps.add(Capability.TOOL_CALLS)
37
+ self.capabilities = frozenset(caps)
38
+ self.modalities = frozenset(spec.modalities) # text / image / audio / video, from the spec
39
+
40
+ # -- lazy load -----------------------------------------------------
41
+ def load(self) -> None:
42
+ from vllm import LLM
43
+
44
+ self._llm = LLM(
45
+ model=self.spec.hf_repo,
46
+ trust_remote_code=self.spec.trust_remote_code,
47
+ dtype=self.runtime.dtype,
48
+ **self.runtime.engine_kwargs,
49
+ )
50
+
51
+ @property
52
+ def _engine(self):
53
+ if self._llm is None:
54
+ self.load()
55
+ return self._llm
56
+
57
+ @staticmethod
58
+ def _as_prompt(inputs: Any) -> str:
59
+ return inputs.prompt if isinstance(inputs, Inputs) else str(inputs)
60
+
61
+ def _sampling(self, *, max_tokens: int | None = None, temperature: float = 0.0, logprobs: int | None = None, **_):
62
+ from vllm import SamplingParams
63
+
64
+ return SamplingParams(
65
+ max_tokens=max_tokens or self.runtime.max_new_tokens,
66
+ temperature=temperature,
67
+ logprobs=logprobs,
68
+ )
69
+
70
+ # -- interface -----------------------------------------------------
71
+ def generate(self, inputs: Any, **kwargs) -> str:
72
+ out = self._engine.generate([self._as_prompt(inputs)], self._sampling(**kwargs))
73
+ return out[0].outputs[0].text
74
+
75
+ def chat(self, messages: list, tools=None) -> ChatTurn:
76
+ if Capability.TOOL_CALLS not in self.capabilities:
77
+ raise CapabilityError(analyzer="chat", model=repr(self), missing={Capability.TOOL_CALLS})
78
+ tok = self._engine.get_tokenizer()
79
+ text = tok.apply_chat_template(
80
+ messages, tools=tools, add_generation_prompt=True, tokenize=False, **self.spec.chat_template_kwargs
81
+ )
82
+ out = self._engine.generate([text], self._sampling())
83
+ return ChatTurn(text=out[0].outputs[0].text, raw_tool_calls=None) # codec parses the text
84
+
85
+ def logprobs(self, inputs: Any, top_k: int = 5, **kwargs) -> list[TokenLogprob]:
86
+ out = self._engine.generate([self._as_prompt(inputs)], self._sampling(logprobs=top_k, **kwargs))
87
+ comp = out[0].outputs[0]
88
+ tok = self._engine.get_tokenizer()
89
+ result: list[TokenLogprob] = []
90
+ for tid, lp_dict in zip(comp.token_ids, comp.logprobs or []):
91
+ entry = lp_dict.get(tid)
92
+ chosen = float(getattr(entry, "logprob", entry)) if entry is not None else 0.0
93
+ chosen_tok = getattr(entry, "decoded_token", None) or tok.decode([tid])
94
+ top = {
95
+ (getattr(v, "decoded_token", None) or tok.decode([k])): float(getattr(v, "logprob", v))
96
+ for k, v in lp_dict.items()
97
+ }
98
+ result.append(TokenLogprob(token=chosen_tok, logprob=chosen, top=top))
99
+ return result
100
+
101
+ def forward(self, inputs: Any, capture: set[Capability], spec=None) -> Trace:
102
+ raise CapabilityError( # vLLM exposes no internals — use hf_local for white-box
103
+ analyzer="forward", model=repr(self), missing=set(capture) or {Capability.HIDDEN_STATES},
104
+ )
105
+
106
+ def __repr__(self) -> str:
107
+ status = "loaded" if self._llm is not None else "lazy"
108
+ return f"VLLMOfflineModel(key={self.spec.key!r}, {status})"
109
+
110
+
111
+ class VLLMOfflineBackend(Backend):
112
+ kind = "vllm_offline"
113
+ capabilities = frozenset({Capability.GENERATE, Capability.TOOL_CALLS, Capability.LOGPROBS})
114
+
115
+ def build(self, spec, runtime: RuntimeConfig) -> VLLMOfflineModel:
116
+ return VLLMOfflineModel(spec, runtime)
evalrx/models/base.py ADDED
@@ -0,0 +1,24 @@
1
+ """Model base classes.
2
+
3
+ The abstract contract lives in :mod:`evalrx.core.model` (:class:`Model`,
4
+ :class:`Trace`). This module re-exports it and adds the agent base.
5
+ Deployment-specific bases live alongside their models:
6
+ - :class:`~evalrx.models.whitebox.base.WhiteboxModel` (local weights)
7
+ - :class:`~evalrx.models.blackbox.base.BlackboxModel` (API-only)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from abc import abstractmethod
13
+
14
+ from evalrx.core.model import Model, Trace
15
+
16
+ __all__ = ["Model", "Trace", "BaseAgent"]
17
+
18
+
19
+ class BaseAgent(Model):
20
+ """Abstract base for agent-mode models that use tools over multiple steps."""
21
+
22
+ @abstractmethod
23
+ def step(self, observation: str, **kwargs) -> str:
24
+ """Process one agent step and return the next action/response."""
@@ -0,0 +1,4 @@
1
+ from evalrx.models.blackbox.llm_api import BlackboxLLM
2
+ from evalrx.models.blackbox.vlm_api import BlackboxVLM
3
+
4
+ __all__ = ["BlackboxLLM", "BlackboxVLM"]
@@ -0,0 +1,31 @@
1
+ """Black-box API agent with tool calling.
2
+
3
+ Planned for Stage 2.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from evalrx.core.capability import Capability
11
+ from evalrx.core.model import Trace
12
+ from evalrx.models.base import BaseAgent
13
+
14
+
15
+ class BlackboxAgent(BaseAgent):
16
+ """API-based agent model."""
17
+
18
+ capabilities = frozenset({Capability.GENERATE})
19
+
20
+ def __init__(self, model_id: str, api_key: str | None = None) -> None:
21
+ self.model_id = model_id
22
+ self.api_key = api_key
23
+
24
+ def generate(self, inputs: Any, **kwargs) -> str:
25
+ raise NotImplementedError("BlackboxAgent is planned for Stage 2.")
26
+
27
+ def forward(self, inputs: Any, capture: set[Capability]) -> Trace:
28
+ raise NotImplementedError("Black-box models expose no internals.")
29
+
30
+ def step(self, observation: str, **kwargs) -> str:
31
+ raise NotImplementedError("BlackboxAgent is planned for Stage 2.")
@@ -0,0 +1,29 @@
1
+ """Base class for black-box (API-only) models.
2
+
3
+ Black-box models have no parameter access, so they provide only output-level
4
+ capabilities (``GENERATE``, sometimes ``LOGITS``). Capability matching means
5
+ attention/saliency/probing analyzers are simply *not offered* for them — the
6
+ agent sees an empty (or small) compatible-analyzer set and never tries.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from evalrx.core.capability import Capability
14
+ from evalrx.core.model import Model, Trace
15
+
16
+
17
+ class BlackboxModel(Model):
18
+ """Base for API-based models (no local weights)."""
19
+
20
+ capabilities = frozenset({Capability.GENERATE})
21
+
22
+ def __init__(self, model_id: str, api_key: str | None = None) -> None:
23
+ self.model_id = model_id
24
+ self.api_key = api_key
25
+
26
+ def forward(self, inputs: Any, capture: set[Capability]) -> Trace:
27
+ raise NotImplementedError(
28
+ "Black-box models expose no internals; only generate() is available."
29
+ )