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,38 @@
1
+ """Langfuse observability & exporter facade for backward compatibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from evalrx.observability.tracer import (
8
+ DiagnosticTracer,
9
+ backfill_run_to_langfuse,
10
+ export_to_langfuse_bundle,
11
+ sync_to_langfuse_live,
12
+ )
13
+
14
+ __all__ = [
15
+ "DiagnosticTracer",
16
+ "backfill_run_to_langfuse",
17
+ "export_to_langfuse_bundle",
18
+ "sync_to_langfuse_live",
19
+ ]
20
+
21
+
22
+ def main() -> None:
23
+ import argparse
24
+ ap = argparse.ArgumentParser(description="Export an EvalRX run into a Langfuse ingestion bundle.")
25
+ ap.add_argument("run_dir", nargs="?", default="outputs", help="Run directory.")
26
+ ap.add_argument("--out", "-o", default=None, help="Output JSON path.")
27
+ ap.add_argument("--sync", action="store_true", help="Push live to Langfuse API.")
28
+ args = ap.parse_args()
29
+
30
+ if args.sync:
31
+ sync_to_langfuse_live(args.run_dir)
32
+ else:
33
+ out_p = args.out or (Path(args.run_dir) / "langfuse_trace.json")
34
+ export_to_langfuse_bundle(args.run_dir, out_p)
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
@@ -0,0 +1,155 @@
1
+ """Langfuse-backed run reader for the EvalRX HTML report.
2
+
3
+ Langfuse's observations API is row-oriented. A run whose durable events were
4
+ delivered through the outbox queue arrives as one EVENT observation per event
5
+ (``input={"event": {...}}``); this adapter recovers the ordered stream from
6
+ those without making the renderer understand Langfuse-specific response
7
+ types, then re-buckets it into a V2-shaped cache (:func:`materialize`) so the
8
+ normal ``run.json``/``M<n>/log.json`` read path can render it.
9
+
10
+ Caveat: :class:`~evalrx.eval_agent.run_logger_v2.RunLoggerV2` does not
11
+ currently push structured events through that outbox (it relies on live
12
+ spans/generations instead), so a V2 run's recovered events presently carry
13
+ no ``stage`` tag and land in ``run.json``'s ``unrouted`` bucket rather than
14
+ a real ``M<n>/log.json`` — full V2 support here is unfinished, not something
15
+ this module claims to solve.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import base64
21
+ import json
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+
26
+ class LangfuseRunSource:
27
+ """Read EvalRX event envelopes from Langfuse Observations API v2."""
28
+
29
+ def __init__(self, client: Any | None = None) -> None:
30
+ if client is None:
31
+ try:
32
+ from langfuse import Langfuse
33
+ except ImportError as exc:
34
+ raise ImportError(
35
+ "Langfuse report input needs `pip install evalrx[observability]`."
36
+ ) from exc
37
+ client = Langfuse()
38
+ self.client = client
39
+
40
+ def events(self, trace_id: str) -> list[dict[str, Any]]:
41
+ """Return the exact EvalRX events for *trace_id*, ordered by sequence."""
42
+ return [_public_event(event) for event in self._raw_events(trace_id)]
43
+
44
+ def _raw_events(self, trace_id: str) -> list[dict[str, Any]]:
45
+ cursor: str | None = None
46
+ recovered: list[tuple[int, dict[str, Any]]] = []
47
+ # The SDK uses the canonical 32-hex Langfuse trace id. Our local UUID
48
+ # carries dashes, so accept either spelling at the CLI boundary.
49
+ langfuse_trace_id = trace_id.replace("-", "")
50
+ while True:
51
+ try:
52
+ response = self.client.api.observations.get_many(
53
+ trace_id=langfuse_trace_id,
54
+ fields="io,metadata,time",
55
+ expand_metadata="event_id,event_seq,stage,cycle,artifact_refs",
56
+ limit=1000,
57
+ cursor=cursor,
58
+ )
59
+ except Exception as exc:
60
+ raise RuntimeError(
61
+ "Could not query Langfuse observations. Set LANGFUSE_PUBLIC_KEY, "
62
+ "LANGFUSE_SECRET_KEY, and LANGFUSE_HOST (if self-hosted)."
63
+ ) from exc
64
+ for observation in response.data:
65
+ event = _event_from_observation(observation)
66
+ if event is not None:
67
+ metadata = _as_dict(getattr(observation, "metadata", None))
68
+ recovered.append((int(metadata.get("event_seq") or event.get("event_seq") or 0), event))
69
+ cursor = getattr(getattr(response, "meta", None), "cursor", None)
70
+ if not cursor:
71
+ break
72
+ recovered.sort(key=lambda item: item[0])
73
+ return [event for _, event in recovered]
74
+
75
+ def materialize(self, trace_id: str, destination: str | Path) -> Path:
76
+ """Write a renderer-compatible cache; it is not a source of truth."""
77
+ from evalrx.reporting.run_events import write_v2_bundle
78
+
79
+ root = Path(destination)
80
+ root.mkdir(parents=True, exist_ok=True)
81
+ root.chmod(0o700)
82
+ events = self._raw_events(trace_id)
83
+ if not events:
84
+ raise LookupError(f"No EvalRX events found for Langfuse trace {trace_id!r}")
85
+ for event in events:
86
+ self._materialize_artifacts(event, root)
87
+ write_v2_bundle(root, [_public_event(event) for event in events])
88
+ return root
89
+
90
+ def _materialize_artifacts(self, event: dict[str, Any], root: Path) -> None:
91
+ attachments = event.pop("_langfuse_artifacts", [])
92
+ refs = event.pop("_langfuse_artifact_refs", [])
93
+ if not attachments or not refs or not hasattr(self.client, "resolve_media_references"):
94
+ return
95
+ resolved = self.client.resolve_media_references(
96
+ obj=attachments, resolve_with="base64_data_uri",
97
+ )
98
+ ref_by_id = {str(ref.get("artifact_id")): ref for ref in refs if isinstance(ref, dict)}
99
+ for attachment in resolved:
100
+ if not isinstance(attachment, dict):
101
+ continue
102
+ ref = ref_by_id.get(str(attachment.get("artifact_id")))
103
+ content = attachment.get("content")
104
+ if ref is None or not isinstance(content, str):
105
+ continue
106
+ _write_data_uri(root, str(ref.get("path") or ""), content)
107
+
108
+
109
+ def _event_from_observation(observation: Any) -> dict[str, Any] | None:
110
+ """Extract our payload from an ObservationV2 or a lightweight test double."""
111
+ input_data = _as_dict(getattr(observation, "input", None))
112
+ event = input_data.get("event")
113
+ if not isinstance(event, dict):
114
+ return None
115
+ result = dict(event)
116
+ attachments = input_data.get("artifacts")
117
+ metadata = _as_dict(getattr(observation, "metadata", None))
118
+ refs = metadata.get("artifact_refs")
119
+ if isinstance(attachments, list):
120
+ result["_langfuse_artifacts"] = attachments
121
+ if isinstance(refs, list):
122
+ result["_langfuse_artifact_refs"] = refs
123
+ return result
124
+
125
+
126
+ def _as_dict(value: Any) -> dict[str, Any]:
127
+ if isinstance(value, dict):
128
+ return value
129
+ if isinstance(value, str):
130
+ try:
131
+ parsed = json.loads(value)
132
+ except json.JSONDecodeError:
133
+ return {}
134
+ return parsed if isinstance(parsed, dict) else {}
135
+ return {}
136
+
137
+
138
+ def _public_event(event: dict[str, Any]) -> dict[str, Any]:
139
+ return {key: value for key, value in event.items() if not key.startswith("_langfuse_")}
140
+
141
+
142
+ def _write_data_uri(root: Path, relative_path: str, data_uri: str) -> None:
143
+ """Write a resolved Langfuse media value inside the renderer cache only."""
144
+ if not data_uri.startswith("data:") or "," not in data_uri:
145
+ return
146
+ target = (root / relative_path).resolve()
147
+ if root.resolve() not in target.parents:
148
+ return
149
+ try:
150
+ payload = base64.b64decode(data_uri.split(",", 1)[1])
151
+ except ValueError:
152
+ return
153
+ target.parent.mkdir(parents=True, exist_ok=True)
154
+ target.write_bytes(payload)
155
+ target.chmod(0o600)
@@ -0,0 +1,151 @@
1
+ """Structured diagnostic report model for claim-first dashboards.
2
+
3
+ The raw EvalRX artifacts are optimized for auditability: JSONL events,
4
+ candidate signal dicts, CSV tables, and rendered chart files. This module
5
+ defines the semantic layer above those artifacts. The dashboard should render
6
+ this model instead of re-interpreting raw logs ad hoc.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Literal
13
+
14
+ ClaimStatus = Literal["supported", "inconclusive", "refuted", "descriptive"]
15
+ Confidence = Literal["high", "medium", "low", "unknown"]
16
+
17
+
18
+ @dataclass
19
+ class Evidence:
20
+ id: str
21
+ kind: str
22
+ title: str
23
+ summary: str = ""
24
+ artifact: dict[str, Any] = field(default_factory=dict)
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ return {
28
+ "id": self.id,
29
+ "kind": self.kind,
30
+ "title": self.title,
31
+ "summary": self.summary,
32
+ "artifact": self.artifact,
33
+ }
34
+
35
+
36
+ @dataclass
37
+ class Claim:
38
+ id: str
39
+ text: str
40
+ status: ClaimStatus
41
+ evidence_ids: list[str] = field(default_factory=list)
42
+ counter_evidence_ids: list[str] = field(default_factory=list)
43
+ interpretation: str = ""
44
+ do_not_infer: str = ""
45
+ downstream: list[str] = field(default_factory=list)
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ return {
49
+ "id": self.id,
50
+ "text": self.text,
51
+ "status": self.status,
52
+ "evidence_ids": self.evidence_ids,
53
+ "counter_evidence_ids": self.counter_evidence_ids,
54
+ "interpretation": self.interpretation,
55
+ "do_not_infer": self.do_not_infer,
56
+ "downstream": self.downstream,
57
+ }
58
+
59
+
60
+ @dataclass
61
+ class ReportStep:
62
+ stage: str
63
+ title: str
64
+ summary: str
65
+ artifact_ids: list[str] = field(default_factory=list)
66
+
67
+ def to_dict(self) -> dict[str, Any]:
68
+ return {
69
+ "stage": self.stage,
70
+ "title": self.title,
71
+ "summary": self.summary,
72
+ "artifact_ids": self.artifact_ids,
73
+ }
74
+
75
+
76
+ @dataclass
77
+ class DiagnosticReport:
78
+ question: str = ""
79
+ answer: str = ""
80
+ confidence: Confidence = "unknown"
81
+ claims: list[Claim] = field(default_factory=list)
82
+ evidence: list[Evidence] = field(default_factory=list)
83
+ timeline: list[ReportStep] = field(default_factory=list)
84
+ visual_decisions: list[dict[str, Any]] = field(default_factory=list)
85
+ chart_readings: list[dict[str, Any]] = field(default_factory=list)
86
+ dashboard_storyboard: list[dict[str, Any]] = field(default_factory=list)
87
+ critique: list[str] = field(default_factory=list)
88
+ caveats: list[str] = field(default_factory=list)
89
+ next_actions: list[str] = field(default_factory=list)
90
+
91
+ def to_dict(self) -> dict[str, Any]:
92
+ return {
93
+ "question": self.question,
94
+ "answer": self.answer,
95
+ "confidence": self.confidence,
96
+ "claims": [c.to_dict() for c in self.claims],
97
+ "evidence": [e.to_dict() for e in self.evidence],
98
+ "timeline": [s.to_dict() for s in self.timeline],
99
+ "visual_decisions": self.visual_decisions,
100
+ "chart_readings": self.chart_readings,
101
+ "dashboard_storyboard": self.dashboard_storyboard,
102
+ "critique": self.critique,
103
+ "caveats": self.caveats,
104
+ "next_actions": self.next_actions,
105
+ }
106
+
107
+
108
+ @dataclass
109
+ class ReaderFinding:
110
+ """One claim phrased for a reader who does not know the pipeline."""
111
+
112
+ title: str
113
+ summary: str
114
+ why_it_matters: str
115
+ evidence_level: str
116
+ limitation: str = ""
117
+
118
+ def to_dict(self) -> dict[str, str]:
119
+ return {
120
+ "title": self.title,
121
+ "summary": self.summary,
122
+ "why_it_matters": self.why_it_matters,
123
+ "evidence_level": self.evidence_level,
124
+ "limitation": self.limitation,
125
+ }
126
+
127
+
128
+ @dataclass
129
+ class ReaderReport:
130
+ """Stable, audience-first view model rendered before technical evidence."""
131
+
132
+ headline: str
133
+ question: str
134
+ answer: str
135
+ confidence: str
136
+ what_we_did: list[str] = field(default_factory=list)
137
+ key_findings: list[ReaderFinding] = field(default_factory=list)
138
+ open_questions: list[str] = field(default_factory=list)
139
+ next_steps: list[str] = field(default_factory=list)
140
+
141
+ def to_dict(self) -> dict[str, Any]:
142
+ return {
143
+ "headline": self.headline,
144
+ "question": self.question,
145
+ "answer": self.answer,
146
+ "confidence": self.confidence,
147
+ "what_we_did": self.what_we_did,
148
+ "key_findings": [finding.to_dict() for finding in self.key_findings],
149
+ "open_questions": self.open_questions,
150
+ "next_steps": self.next_steps,
151
+ }
@@ -0,0 +1,184 @@
1
+ """Read tidy RunLoggerV2 documents through the existing event-view contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ _STAGES = ("M1", "M2", "M3", "M4", "M5")
10
+
11
+
12
+ def resolve_v2_root(root: str | Path) -> Path | None:
13
+ """Return the directory containing V2 ``run.json`` and stage folders."""
14
+ root = Path(root).resolve()
15
+ candidates = (root, root / "logs")
16
+ for candidate in candidates:
17
+ if (candidate / "run.json").is_file() and any(
18
+ (candidate / stage / "log.json").is_file() for stage in _STAGES
19
+ ):
20
+ return candidate
21
+ return None
22
+
23
+
24
+ def read_v2_events(root: str | Path) -> list[dict[str, Any]]:
25
+ """Flatten V2 bucketed documents into the legacy read-only event view.
26
+
27
+ The V2 files remain authoritative and are never rewritten by this adapter.
28
+ It exists so current report/UI code can migrate independently from the
29
+ storage layout.
30
+ """
31
+ v2_root = resolve_v2_root(root)
32
+ if v2_root is None:
33
+ return []
34
+ try:
35
+ run = json.loads((v2_root / "run.json").read_text(encoding="utf-8"))
36
+ except (OSError, json.JSONDecodeError):
37
+ return []
38
+
39
+ trace_id = str((run.get("run_start") or {}).get("trace_id") or run.get("trace_id") or "")
40
+ events: list[dict[str, Any]] = []
41
+
42
+ def append(event: str, payload: Any, *, stage: str = "RUN") -> None:
43
+ if not isinstance(payload, dict):
44
+ return
45
+ record = dict(payload)
46
+ record.setdefault("event", event)
47
+ record.setdefault("stage", stage)
48
+ # Older V2 bundles encode this only in the folder name. Existing
49
+ # case-study/report consumers still use the V1 module discriminator.
50
+ if event == "surgery":
51
+ record.setdefault("module", stage.lower())
52
+ elif event == "fix":
53
+ record.setdefault("module", "fix")
54
+ if trace_id:
55
+ record.setdefault("trace_id", trace_id)
56
+ events.append(record)
57
+
58
+ append("run_start", run.get("run_start"))
59
+ for row in run.get("cases") or []:
60
+ append("case_record", row)
61
+ for key in (
62
+ "report_published", "diagnose_reports", "loop_end",
63
+ "agent_decisions", "agent_tool_calls", "unrouted",
64
+ ):
65
+ event_name = {
66
+ "diagnose_reports": "diagnose_report",
67
+ "agent_decisions": "agent_decision",
68
+ "agent_tool_calls": "agent_tool",
69
+ "unrouted": "unrouted",
70
+ }.get(key, key)
71
+ for row in run.get(key) or []:
72
+ append(event_name, row)
73
+
74
+ for stage in _STAGES:
75
+ path = v2_root / stage / "log.json"
76
+ if not path.is_file():
77
+ continue
78
+ try:
79
+ doc = json.loads(path.read_text(encoding="utf-8"))
80
+ except (OSError, json.JSONDecodeError):
81
+ continue
82
+ if not isinstance(doc, dict):
83
+ continue
84
+ for event, rows in doc.items():
85
+ if not isinstance(rows, list):
86
+ continue
87
+ for row in rows:
88
+ append("model_call" if event == "model_calls" else event, row, stage=stage)
89
+
90
+ # New bundles persist a global sequence at write time, even when calls
91
+ # finish concurrently or the wall clock moves backwards. Legacy bundles
92
+ # retain timestamp ordering; never replace a persisted event identity.
93
+ if all(isinstance(event.get("event_seq"), int) for event in events):
94
+ events.sort(key=lambda event: event["event_seq"])
95
+ else:
96
+ events.sort(key=lambda event: str(event.get("ts") or ""))
97
+ seq = max((event.get("event_seq", 0) for event in events), default=0)
98
+ for event in events:
99
+ if "event_seq" not in event:
100
+ seq += 1
101
+ event["event_seq"] = seq
102
+ return events
103
+
104
+
105
+ # Inverse of the RUN-stage bucket mapping `read_v2_events` flattens — event
106
+ # name -> run.json key. These names are always RUN-level regardless of any
107
+ # "stage" tag on the recovered event.
108
+ _RUN_EVENT_TO_KEY = {
109
+ "case_record": "cases",
110
+ "diagnose_report": "diagnose_reports",
111
+ "agent_decision": "agent_decisions",
112
+ "agent_tool": "agent_tool_calls",
113
+ "report_published": "report_published",
114
+ "loop_end": "loop_end",
115
+ }
116
+
117
+ # A source that recovers events out-of-band (e.g. Langfuse observations
118
+ # reconstructed from a durable-outbox delivery, which predates per-event
119
+ # "stage" tagging) may hand back events with no "stage" field at all. Route
120
+ # by the event's own name to its canonical stage rather than dumping it into
121
+ # run.json's "unrouted" bucket, where `resolve_v2_root` would never find it
122
+ # (it requires at least one real M<n>/log.json to recognize a V2 root).
123
+ _NAME_TO_STAGE = {
124
+ "probe": "M1",
125
+ "analysis": "M2", "explore": "M2",
126
+ "diagnosis": "M3",
127
+ "experiment": "M5", # module=... on the row disambiguates M4 vs M5 below
128
+ "fix": "M5",
129
+ }
130
+
131
+
132
+ def _fallback_stage(name: "str | None", row: dict[str, Any]) -> "str | None":
133
+ """Best-effort stage for an event with no "stage" tag of its own."""
134
+ if name == "surgery" or name == "tool_codegen" or name == "experiment":
135
+ module = str(row.get("module") or "").lower()
136
+ if module.startswith("m") and module[1:].isdigit():
137
+ return f"M{module[1:]}"
138
+ return "M5" if name == "experiment" else "M4"
139
+ return _NAME_TO_STAGE.get(name or "")
140
+
141
+
142
+ def write_v2_bundle(root: str | Path, events: list[dict[str, Any]]) -> None:
143
+ """Reconstruct ``run.json`` + ``M<n>/log.json`` from a flat event list.
144
+
145
+ The exact inverse of :func:`read_v2_events`'s bucketing, for callers that
146
+ recover events from an out-of-band source (e.g. Langfuse) and need to
147
+ write a V2-shaped cache directory back for the report/dashboard's
148
+ normal V2-only read path to find.
149
+ """
150
+ root = Path(root)
151
+ run_doc: dict[str, Any] = {}
152
+ stage_docs: dict[str, dict[str, list[dict[str, Any]]]] = {}
153
+
154
+ for event in events:
155
+ row = {k: v for k, v in event.items() if k not in ("event", "stage")}
156
+ stage = event.get("stage")
157
+ name = event.get("event")
158
+ if name == "run_start":
159
+ run_doc["run_start"] = row
160
+ continue
161
+ if name in _RUN_EVENT_TO_KEY:
162
+ run_doc.setdefault(_RUN_EVENT_TO_KEY[name], []).append(row)
163
+ continue
164
+ if stage not in _STAGES:
165
+ stage = _fallback_stage(name, row) or stage
166
+ if stage not in _STAGES:
167
+ run_doc.setdefault("unrouted", []).append(row)
168
+ continue
169
+ key = "model_calls" if name == "model_call" else (name or "unrouted")
170
+ stage_docs.setdefault(stage, {}).setdefault(key, []).append(row)
171
+
172
+ root.mkdir(parents=True, exist_ok=True)
173
+ (root / "run.json").write_text(
174
+ json.dumps(run_doc, ensure_ascii=False, default=str), encoding="utf-8",
175
+ )
176
+ for stage, doc in stage_docs.items():
177
+ stage_dir = root / stage
178
+ stage_dir.mkdir(parents=True, exist_ok=True)
179
+ (stage_dir / "log.json").write_text(
180
+ json.dumps(doc, ensure_ascii=False, default=str), encoding="utf-8",
181
+ )
182
+
183
+
184
+ __all__ = ["read_v2_events", "resolve_v2_root", "write_v2_bundle"]