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.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- evalrx-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Prompt templates for M2 statistical analysis."""
|
|
2
|
+
|
|
3
|
+
_ANALYSIS_PROMPT = """\
|
|
4
|
+
You are an expert in ML failure analysis for vision-language models and agentic systems.
|
|
5
|
+
|
|
6
|
+
Experiment protocol:
|
|
7
|
+
{protocol_text}
|
|
8
|
+
|
|
9
|
+
Task domain: {task_domain}
|
|
10
|
+
|
|
11
|
+
Analyzer summary:
|
|
12
|
+
{narrative}
|
|
13
|
+
|
|
14
|
+
Raw per-analyzer findings (JSON):
|
|
15
|
+
{findings_json}
|
|
16
|
+
|
|
17
|
+
WHO READS THIS: an engineer who builds and runs evaluations but does not do
|
|
18
|
+
statistics. They know what a benchmark, a prompt and a failure case are. They
|
|
19
|
+
have never met an e-value, a McNemar test, a bootstrap interval or a
|
|
20
|
+
Benjamini-Hochberg correction, and they will not go and look them up.
|
|
21
|
+
|
|
22
|
+
HOW TO WRITE FOR THEM:
|
|
23
|
+
- Answer first. The opening sentence says what is going wrong, in ordinary
|
|
24
|
+
words. Numbers, mechanism and caveats all come after it.
|
|
25
|
+
- A bare number tells this reader nothing. Give the count AND what it amounts
|
|
26
|
+
to: "the model got 45 of the 125 wrong, and almost all of those were lists
|
|
27
|
+
longer than eight words" — not "fail rate 0.36, effect +0.28".
|
|
28
|
+
- Prefer counts to rates, and give both when you have them ("24 of 30 cases,
|
|
29
|
+
80%"). For a comparison, name both groups and how many cases each holds.
|
|
30
|
+
- If a statistical term really is the clearest word, define it in the same
|
|
31
|
+
sentence the first time it appears: "an e-value of 45 — the evidence runs
|
|
32
|
+
about 45 to 1 against this being chance". Never leave one standing bare.
|
|
33
|
+
- Never make a bare metric identifier the subject of a sentence. Say what it
|
|
34
|
+
measures, then put the identifier in parentheses if the reader needs the
|
|
35
|
+
handle: "how often the model answered Yes (answer_extraction_audit.answered_yes)".
|
|
36
|
+
- No symbols standing in for words (→, ρ, σ), and no "p<0.05" shorthand.
|
|
37
|
+
- These are patterns on the analysis split, not proven causes. Write "shows up
|
|
38
|
+
more often on" or "is linked with", never "causes" or "explains", unless an
|
|
39
|
+
intervention actually tested it.
|
|
40
|
+
|
|
41
|
+
Based on the protocol and the findings above, write:
|
|
42
|
+
|
|
43
|
+
CONCLUSION: <one paragraph. FIRST sentence: what is going wrong, in plain words,
|
|
44
|
+
no numbers. Then the evidence for it, with counts. Then what this evidence
|
|
45
|
+
does NOT settle. If the model looks healthy given the protocol, say that
|
|
46
|
+
plainly in the first sentence.>
|
|
47
|
+
EVIDENCE_CHAIN:
|
|
48
|
+
- <step 1: which measurement first caught your attention — say what it measures
|
|
49
|
+
in words, not just its identifier — and why it stood out>
|
|
50
|
+
- <step 2: how it connects to the protocol's stated failure patterns>
|
|
51
|
+
- <step 3: any corroborating or contradicting signals from other analyzers>
|
|
52
|
+
QUALITATIVE:
|
|
53
|
+
- <observation 1: a pattern not captured by numbers alone>
|
|
54
|
+
- <observation 2: anything unexpected or surprising>
|
|
55
|
+
|
|
56
|
+
Keep each bullet to one sentence."""
|
|
57
|
+
|
|
58
|
+
_TOOL_SELECT_PROMPT = """\
|
|
59
|
+
You are selecting statistical tools to test why a model fails, given the data on hand.
|
|
60
|
+
|
|
61
|
+
Experiment protocol:
|
|
62
|
+
{protocol_text}
|
|
63
|
+
|
|
64
|
+
Available statistical tools:
|
|
65
|
+
{tool_catalog}
|
|
66
|
+
|
|
67
|
+
Data shape available for testing:
|
|
68
|
+
{data_shape}
|
|
69
|
+
|
|
70
|
+
Pick the tools whose data requirements are satisfied and that best test the \
|
|
71
|
+
protocol's question. Return ONLY a JSON object, no other text:
|
|
72
|
+
{{"tools": ["name1", "name2", ...], "rationale": "one sentence"}}"""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Prompt templates for generated code-writing stages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
_INPUT_FILENAME = "m2_stats_input.json"
|
|
6
|
+
_RESULT_MARKER = "STATS_RESULT_JSON="
|
|
7
|
+
|
|
8
|
+
_GENERATE_PROMPT = """\
|
|
9
|
+
You are writing a self-contained Python statistics script for a model-failure analysis.
|
|
10
|
+
|
|
11
|
+
GOAL (what statistic to compute):
|
|
12
|
+
{need}
|
|
13
|
+
|
|
14
|
+
DATA: a JSON file named "{input_filename}" sits in the current working directory with:
|
|
15
|
+
{{
|
|
16
|
+
"labels": {{case_id: is_fail_bool}}, # PASS/FAIL labels
|
|
17
|
+
"per_case": {{"analyzer.metric": {{case_id: value}}}}, # per-case signals
|
|
18
|
+
"scalars": {{"analyzer.metric": value}}, # aggregate metrics
|
|
19
|
+
"groups": {{strategy: {{case_id: success}}}} or null # strategy comparison
|
|
20
|
+
}}
|
|
21
|
+
|
|
22
|
+
Data shape available for THIS run:
|
|
23
|
+
{data_shape}
|
|
24
|
+
|
|
25
|
+
REQUIREMENTS:
|
|
26
|
+
- Read "{input_filename}" from the current directory. Do NOT hardcode the data.
|
|
27
|
+
- You MAY `import numpy`. No network, no file writes, no other I/O.
|
|
28
|
+
- You decide WHAT statistic to compute, but you do NOT adjudicate significance.
|
|
29
|
+
Do NOT emit a "reject", "e_value", or "p_value" verdict — the HOST recomputes
|
|
30
|
+
the decision from your SUFFICIENT STATISTICS with its validated,
|
|
31
|
+
multiplicity-aware core; a self-declared verdict is ignored.
|
|
32
|
+
- The LAST line of stdout MUST be exactly one line of the form:
|
|
33
|
+
{marker}{{"summary": "<one sentence>", "effect": <number or null>, "ci": [lo, hi] or null, "underpowered": <true/false>, "details": {{}}, "sufficient": <a SUFFICIENT-STATISTICS object or null>}}
|
|
34
|
+
- "sufficient" must be ONE of these host-adjudicable shapes:
|
|
35
|
+
{{"kind": "paired_binary", "b": <int: #cases that flipped the GOOD way>, "c": <int: #cases that flipped the BAD way>}}
|
|
36
|
+
{{"kind": "two_group", "a": [0/1, ...], "b": [0/1, ...]}} # two independent success/indicator vectors (e.g. is_fail among signal-absent vs signal-present)
|
|
37
|
+
If your statistic cannot be expressed as one of these, set "sufficient": null —
|
|
38
|
+
your tool is then DESCRIPTIVE (it reports effect/CI but can never claim a
|
|
39
|
+
rejection). Choose the shape that captures your test; the host owns the verdict.
|
|
40
|
+
- Print NOTHING after that line. Keep the script under ~60 lines.
|
|
41
|
+
|
|
42
|
+
Return ONLY the Python code{fences_hint}."""
|
|
43
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Shared parser for ``<MARKER>=<json>`` result lines printed by agent-written scripts.
|
|
2
|
+
|
|
3
|
+
The explorer (``EXPLORATORY_RESULT_JSON=``) and the M2 stats-tool generator
|
|
4
|
+
(``STATS_RESULT_JSON=``) both ask the generated Python to print its result as
|
|
5
|
+
one JSON object after a fixed marker. The original parsers read only the
|
|
6
|
+
remainder of the marker LINE, so ``print(f"MARKER={json.dumps(obj, indent=2)}")``
|
|
7
|
+
— a perfectly valid object that merely spans several lines — parsed as ``{`` and
|
|
8
|
+
failed (qwen3.5-2b / gsm8k, 2026-08-27: three explorer attempts in a row, the
|
|
9
|
+
report lost). The marker is the contract; the line break is not.
|
|
10
|
+
|
|
11
|
+
:func:`extract_marker_json` therefore decodes from the LAST marker to wherever
|
|
12
|
+
the JSON value ends (``json.JSONDecoder.raw_decode``), so single-line,
|
|
13
|
+
pretty-printed and trailing-noise outputs all read the same object.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_marker_json(stdout: str, marker: str) -> tuple[Any, str]:
|
|
23
|
+
"""Return ``(value, "")`` for the JSON value that follows the last *marker*
|
|
24
|
+
line in *stdout*, or ``(None, error)`` when there is none / it is invalid.
|
|
25
|
+
|
|
26
|
+
The marker must start a line (after whitespace); the value may continue on
|
|
27
|
+
the following lines and may be followed by unrelated output. When the
|
|
28
|
+
marker appears more than once the last occurrence wins, matching the
|
|
29
|
+
line-oriented behaviour these parsers always had.
|
|
30
|
+
"""
|
|
31
|
+
start = -1
|
|
32
|
+
pos = 0
|
|
33
|
+
for line in stdout.splitlines(keepends=True):
|
|
34
|
+
stripped = line.lstrip()
|
|
35
|
+
if stripped.startswith(marker):
|
|
36
|
+
start = pos + (len(line) - len(stripped)) + len(marker)
|
|
37
|
+
pos += len(line)
|
|
38
|
+
if start < 0:
|
|
39
|
+
return None, f"no {marker} line in output"
|
|
40
|
+
payload = stdout[start:].lstrip()
|
|
41
|
+
if not payload:
|
|
42
|
+
return None, f"empty {marker} payload"
|
|
43
|
+
try:
|
|
44
|
+
value, _end = json.JSONDecoder().raw_decode(payload)
|
|
45
|
+
except json.JSONDecodeError as exc:
|
|
46
|
+
return None, f"unparseable {marker} JSON: {exc}"
|
|
47
|
+
return value, ""
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Run a user's existing evaluation codebase and feed its results into
|
|
2
|
+
exploratory analysis.
|
|
3
|
+
|
|
4
|
+
``run_codebase()`` bridges the run infrastructure (:mod:`evalrx.agent_runtime`)
|
|
5
|
+
and the analysis entry (:func:`evalrx.analysis.api.explore`): given a
|
|
6
|
+
path to a codebase, a CLI coding agent runs it inside an isolated copy,
|
|
7
|
+
harvests the per-case records it produces, and (by default) hands them
|
|
8
|
+
straight to ``explore()``.
|
|
9
|
+
|
|
10
|
+
Flow::
|
|
11
|
+
|
|
12
|
+
run_codebase(path)
|
|
13
|
+
-> copy repo into an isolated workspace (the original is never touched)
|
|
14
|
+
-> CodegenRunner.run(prompt) # the agent runs the eval in-place
|
|
15
|
+
-> harvest workspace/records.json (one repair turn if empty/missing)
|
|
16
|
+
-> explore(records) # M2 + M3, unchanged
|
|
17
|
+
|
|
18
|
+
``run_codebase_cli`` is the CLI-facing counterpart: same pipeline, prints a
|
|
19
|
+
summary and optionally opens the dashboard.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import shutil
|
|
26
|
+
import tempfile
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from evalrx.agent_runtime.cli_types import CliAgentConfig
|
|
32
|
+
from evalrx.agent_runtime.codegen.runner import CodegenRunner
|
|
33
|
+
from evalrx.analysis.api import ExploreRunResult, explore
|
|
34
|
+
from evalrx.analysis.explorer import RECORDS_FILENAME, load_records_from_path
|
|
35
|
+
from evalrx.analysis.prompts.run_codebase import REPAIR_PROMPT_TEMPLATE, RUN_PROMPT_TEMPLATE
|
|
36
|
+
|
|
37
|
+
_IGNORE_DIR_NAMES = {".git", "__pycache__", "node_modules", ".venv", "venv", ".mypy_cache", ".pytest_cache"}
|
|
38
|
+
_HARVEST_MAX_ROWS = 20_000
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class CodebaseRunResult:
|
|
43
|
+
"""Outcome of one :func:`run_codebase` call."""
|
|
44
|
+
|
|
45
|
+
records: list[dict[str, Any]] = field(default_factory=list)
|
|
46
|
+
records_path: Path | None = None
|
|
47
|
+
workspace: Path | None = None
|
|
48
|
+
ran_ok: bool = False
|
|
49
|
+
explore: ExploreRunResult | None = None
|
|
50
|
+
error: str | None = None
|
|
51
|
+
audit: dict | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _copy_ignore(_dir: str, names: list[str]) -> set[str]:
|
|
55
|
+
return {n for n in names if n in _IGNORE_DIR_NAMES}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _harvest(workspace: Path, records_name: str) -> list[dict[str, Any]]:
|
|
59
|
+
target = workspace / records_name
|
|
60
|
+
if not target.exists():
|
|
61
|
+
return []
|
|
62
|
+
return load_records_from_path(target, max_rows=_HARVEST_MAX_ROWS, max_files=1)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_codebase(
|
|
66
|
+
path: "str | Path",
|
|
67
|
+
*,
|
|
68
|
+
out: "str | Path | None" = None,
|
|
69
|
+
analyze: bool = True,
|
|
70
|
+
provider: str = "claude_code",
|
|
71
|
+
model: str = "",
|
|
72
|
+
binary: str = "",
|
|
73
|
+
outcome_col: str | None = None,
|
|
74
|
+
records_name: str = RECORDS_FILENAME,
|
|
75
|
+
timeout_sec: int = 1200,
|
|
76
|
+
max_attempts: int = 2,
|
|
77
|
+
question: str = "Explore this dataset and surface the patterns that matter.",
|
|
78
|
+
progress_sink: Any = None,
|
|
79
|
+
**explore_kwargs: Any,
|
|
80
|
+
) -> CodebaseRunResult:
|
|
81
|
+
"""Run a user's evaluation codebase at *path* and analyze its results.
|
|
82
|
+
|
|
83
|
+
A CLI coding agent (*provider*) runs inside an isolated copy of *path* —
|
|
84
|
+
the original directory is never modified — and is instructed to produce
|
|
85
|
+
*records_name* (default ``"records.json"``, a JSON array or JSON-Lines
|
|
86
|
+
file with one row per evaluation case, each carrying a ``label`` plus
|
|
87
|
+
input/prediction/target fields). Those records are then handed to
|
|
88
|
+
:func:`evalrx.analysis.api.explore` (M2 + M3) unless *analyze* is
|
|
89
|
+
False.
|
|
90
|
+
|
|
91
|
+
When *out* is given, the workspace lives under ``out/workspace`` and is
|
|
92
|
+
kept on disk (harvested records plus explore artifacts are persisted
|
|
93
|
+
there too), which is useful for inspecting what the agent did — including
|
|
94
|
+
after a failed run. Without *out* everything runs in a temporary
|
|
95
|
+
directory that is removed before returning; only the returned
|
|
96
|
+
:class:`CodebaseRunResult` carries the records in memory.
|
|
97
|
+
"""
|
|
98
|
+
src = Path(path).resolve()
|
|
99
|
+
if not src.exists():
|
|
100
|
+
return CodebaseRunResult(error=f"path does not exist: {src}")
|
|
101
|
+
|
|
102
|
+
out_dir = Path(out).resolve() if out is not None else None
|
|
103
|
+
cleanup: tempfile.TemporaryDirectory | None = None
|
|
104
|
+
if out_dir is not None:
|
|
105
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
workspace = out_dir / "workspace"
|
|
107
|
+
if workspace.exists():
|
|
108
|
+
shutil.rmtree(workspace)
|
|
109
|
+
else:
|
|
110
|
+
cleanup = tempfile.TemporaryDirectory(prefix="evalrx_run_codebase_")
|
|
111
|
+
workspace = Path(cleanup.name) / "workspace"
|
|
112
|
+
shutil.copytree(src, workspace, ignore=_copy_ignore)
|
|
113
|
+
|
|
114
|
+
cli_config = CliAgentConfig(provider=provider, binary_path=binary, model=model, timeout_sec=timeout_sec)
|
|
115
|
+
runner = CodegenRunner(cli_config)
|
|
116
|
+
|
|
117
|
+
if progress_sink is not None:
|
|
118
|
+
progress_sink.emit("run", "started", f"Running codebase at {src}")
|
|
119
|
+
|
|
120
|
+
prompt = RUN_PROMPT_TEMPLATE.format(records_name=records_name, question=question)
|
|
121
|
+
result = runner.run(prompt, workdir=workspace, timeout_sec=timeout_sec)
|
|
122
|
+
audit = result.audit
|
|
123
|
+
records = _harvest(workspace, records_name)
|
|
124
|
+
|
|
125
|
+
attempts = 1
|
|
126
|
+
while not records and attempts < max_attempts:
|
|
127
|
+
reason = result.error or "no records found"
|
|
128
|
+
repair_prompt = REPAIR_PROMPT_TEMPLATE.format(records_name=records_name, reason=reason)
|
|
129
|
+
result = runner.run(repair_prompt, workdir=workspace, timeout_sec=timeout_sec)
|
|
130
|
+
audit = result.audit or audit
|
|
131
|
+
records = _harvest(workspace, records_name)
|
|
132
|
+
attempts += 1
|
|
133
|
+
|
|
134
|
+
if not records:
|
|
135
|
+
error = result.error or f"the coding agent did not produce '{records_name}'"
|
|
136
|
+
if progress_sink is not None:
|
|
137
|
+
progress_sink.emit("run", "failed", error)
|
|
138
|
+
if cleanup is not None:
|
|
139
|
+
cleanup.cleanup()
|
|
140
|
+
return CodebaseRunResult(workspace=workspace, ran_ok=False, error=error, audit=audit)
|
|
141
|
+
|
|
142
|
+
if progress_sink is not None:
|
|
143
|
+
progress_sink.emit("run", "completed", f"Harvested {len(records)} record(s)")
|
|
144
|
+
|
|
145
|
+
run_result = CodebaseRunResult(
|
|
146
|
+
records=records,
|
|
147
|
+
records_path=workspace / records_name,
|
|
148
|
+
workspace=workspace,
|
|
149
|
+
ran_ok=True,
|
|
150
|
+
audit=audit,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if analyze:
|
|
154
|
+
run_result.explore = explore(
|
|
155
|
+
records,
|
|
156
|
+
question=question,
|
|
157
|
+
out=out_dir,
|
|
158
|
+
provider=provider,
|
|
159
|
+
model=model,
|
|
160
|
+
binary=binary,
|
|
161
|
+
outcome_col=outcome_col,
|
|
162
|
+
progress_sink=progress_sink,
|
|
163
|
+
**explore_kwargs,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if out_dir is not None:
|
|
167
|
+
(out_dir / records_name).write_text(json.dumps(records, default=str), encoding="utf-8")
|
|
168
|
+
if cleanup is not None:
|
|
169
|
+
cleanup.cleanup()
|
|
170
|
+
|
|
171
|
+
return run_result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def run_codebase_cli(
|
|
175
|
+
path: "str | Path",
|
|
176
|
+
*,
|
|
177
|
+
out: "str | Path" = "evalrx_run_codebase_output",
|
|
178
|
+
coder_provider: str = "claude_code",
|
|
179
|
+
coder_model: str = "",
|
|
180
|
+
coder_binary: str = "",
|
|
181
|
+
outcome_col: str | None = None,
|
|
182
|
+
records_name: str = RECORDS_FILENAME,
|
|
183
|
+
timeout_sec: int = 1200,
|
|
184
|
+
max_attempts: int = 2,
|
|
185
|
+
question: str = "Explore this dataset and surface the patterns that matter.",
|
|
186
|
+
analyze: bool = True,
|
|
187
|
+
dashboard: bool = False,
|
|
188
|
+
dashboard_port: int | None = None,
|
|
189
|
+
progress_sink: Any = None,
|
|
190
|
+
) -> int:
|
|
191
|
+
"""Run *path* and persist its harvested records + explore artifacts under *out*.
|
|
192
|
+
|
|
193
|
+
Prints a short summary and returns a process exit code: ``0`` if records
|
|
194
|
+
were harvested (and, when *analyze*, the exploration succeeded), else
|
|
195
|
+
``1``. When *dashboard* is set, the return value is the dashboard
|
|
196
|
+
process's code instead.
|
|
197
|
+
"""
|
|
198
|
+
out_dir = Path(out).resolve()
|
|
199
|
+
result = run_codebase(
|
|
200
|
+
path,
|
|
201
|
+
out=out_dir,
|
|
202
|
+
analyze=analyze,
|
|
203
|
+
provider=coder_provider,
|
|
204
|
+
model=coder_model,
|
|
205
|
+
binary=coder_binary,
|
|
206
|
+
outcome_col=outcome_col,
|
|
207
|
+
records_name=records_name,
|
|
208
|
+
timeout_sec=timeout_sec,
|
|
209
|
+
max_attempts=max_attempts,
|
|
210
|
+
question=question,
|
|
211
|
+
progress_sink=progress_sink,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
print(f"ran_ok: {result.ran_ok}")
|
|
215
|
+
print(f"records harvested: {len(result.records)}")
|
|
216
|
+
print(f"output: {out_dir}")
|
|
217
|
+
if result.error:
|
|
218
|
+
print(f"error: {result.error}")
|
|
219
|
+
if result.explore is not None:
|
|
220
|
+
report = result.explore.report
|
|
221
|
+
print(f"explore ok: {report.ok}")
|
|
222
|
+
if report.hypotheses:
|
|
223
|
+
print("\nhypotheses (M3, proposed only — not validated):")
|
|
224
|
+
for h in report.hypotheses[:8]:
|
|
225
|
+
print(f"- {h.get('statement')}")
|
|
226
|
+
if report.observations:
|
|
227
|
+
print("\nobservations:")
|
|
228
|
+
for obs in report.observations[:8]:
|
|
229
|
+
print(f"- {obs}")
|
|
230
|
+
|
|
231
|
+
ok = result.ran_ok and (result.explore is None or result.explore.ok)
|
|
232
|
+
|
|
233
|
+
if ok:
|
|
234
|
+
from evalrx.reporting.html_report import build_html_report
|
|
235
|
+
|
|
236
|
+
build_html_report(out_dir, out_path=out_dir / "report.html")
|
|
237
|
+
|
|
238
|
+
if dashboard:
|
|
239
|
+
from evalrx.analysis.dashboard import launch_dashboard
|
|
240
|
+
|
|
241
|
+
return launch_dashboard(out_dir, port=dashboard_port)
|
|
242
|
+
return 0 if ok else 1
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Normalized, UI-facing representation of an EvalRX run.
|
|
2
|
+
|
|
3
|
+
The persisted formats predate the web workbench and intentionally differ:
|
|
4
|
+
``exploratory_report.json`` is a finished M2/M3 artifact, while a diagnostic
|
|
5
|
+
loop is an event stream. Rendering either format directly made the UI grow
|
|
6
|
+
two incompatible information architectures. This module is the compatibility
|
|
7
|
+
boundary: readers turn either source into the small, stable model the UI uses.
|
|
8
|
+
|
|
9
|
+
It deliberately does *not* replace the wire contract. New producers should
|
|
10
|
+
write the contract; these adapters keep existing report bundles and
|
|
11
|
+
``run.json``/``M<n>/log.json`` run directories readable during that migration.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Literal
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _StrEnum(str, Enum):
|
|
23
|
+
"""``enum.StrEnum`` for the Python floor this package declares.
|
|
24
|
+
|
|
25
|
+
``StrEnum`` is 3.11+, and ``pyproject.toml`` says ``requires-python >=3.10``
|
|
26
|
+
with 3.10 in the CI matrix — so importing it here broke collection of this
|
|
27
|
+
module, and of its tests, on the oldest version we claim to support.
|
|
28
|
+
|
|
29
|
+
``(str, Enum)`` is what the rest of the codebase already uses
|
|
30
|
+
(:class:`evalrx.core.case.Label`); the explicit ``__str__`` is what
|
|
31
|
+
keeps it interchangeable, since plain ``(str, Enum)`` formats as
|
|
32
|
+
``StageId.M1`` where ``StrEnum`` formats as ``M1``.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
return str(self.value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StageId(_StrEnum):
|
|
40
|
+
"""Pipeline identifiers retained for auditability, not navigation order."""
|
|
41
|
+
|
|
42
|
+
M1 = "M1"
|
|
43
|
+
M2 = "M2"
|
|
44
|
+
M3 = "M3"
|
|
45
|
+
M4 = "M4"
|
|
46
|
+
M5 = "M5"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StageState(_StrEnum):
|
|
50
|
+
"""Reader-visible stage state; absence is distinct from an empty result."""
|
|
51
|
+
|
|
52
|
+
NOT_STARTED = "not_started"
|
|
53
|
+
RUNNING = "running"
|
|
54
|
+
SUCCEEDED = "succeeded"
|
|
55
|
+
EMPTY = "empty"
|
|
56
|
+
PARTIAL = "partial"
|
|
57
|
+
FAILED = "failed"
|
|
58
|
+
UNAVAILABLE = "unavailable"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class StageView:
|
|
63
|
+
id: StageId
|
|
64
|
+
label: str
|
|
65
|
+
state: StageState
|
|
66
|
+
count: int | None = None
|
|
67
|
+
detail: str = ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class RunView:
|
|
72
|
+
"""One source-independent run consumed by the unified web shell."""
|
|
73
|
+
|
|
74
|
+
kind: Literal["explore", "diagnostic", "casebench", "empty"]
|
|
75
|
+
root: Path
|
|
76
|
+
title: str
|
|
77
|
+
stages: list[StageView]
|
|
78
|
+
report: dict[str, Any] | None = None
|
|
79
|
+
confirm: dict[str, Any] | None = None
|
|
80
|
+
fix_report: dict[str, Any] | None = None
|
|
81
|
+
story: dict[str, Any] | None = None
|
|
82
|
+
case_studies: list[Any] = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_diagnostic(self) -> bool:
|
|
86
|
+
return self.kind in {"diagnostic", "casebench"}
|
|
87
|
+
|
|
88
|
+
def stage(self, stage_id: StageId) -> StageView:
|
|
89
|
+
return next(s for s in self.stages if s.id == stage_id)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_STAGE_LABELS = {
|
|
93
|
+
StageId.M1: "Measure",
|
|
94
|
+
StageId.M2: "Explore evidence",
|
|
95
|
+
StageId.M3: "Explain hypotheses",
|
|
96
|
+
# M4 precedes M5 in the actual current loop. The UI uses this action
|
|
97
|
+
# order rather than implying the numeric labels are execution order.
|
|
98
|
+
StageId.M4: "Validate hypotheses",
|
|
99
|
+
StageId.M5: "Intervene & repair",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _stages(states: dict[StageId, tuple[StageState, int | None, str]]) -> list[StageView]:
|
|
104
|
+
return [
|
|
105
|
+
StageView(stage_id, _STAGE_LABELS[stage_id], *states[stage_id])
|
|
106
|
+
for stage_id in StageId
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _sibling_json(root: Path, name: str) -> dict[str, Any] | None:
|
|
111
|
+
import json
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
value = json.loads((root / name).read_text(encoding="utf-8"))
|
|
115
|
+
except (OSError, ValueError):
|
|
116
|
+
return None
|
|
117
|
+
return value if isinstance(value, dict) else None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def from_session(session: dict[str, Any]) -> RunView:
|
|
121
|
+
"""Adapt the dict returned by :func:`dashboard.load_run`.
|
|
122
|
+
|
|
123
|
+
Keeping the adapter dependent on the public loader result, rather than on
|
|
124
|
+
paths or event parsing, makes it usable by both ``dashboard`` and ``web``.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
root = Path(str(session.get("root") or ".")).resolve()
|
|
128
|
+
kind = str(session.get("kind") or "empty")
|
|
129
|
+
if kind == "loop":
|
|
130
|
+
return _from_loop(root, session.get("story") or {})
|
|
131
|
+
if kind == "casebench":
|
|
132
|
+
studies = list(session.get("case_studies") or [])
|
|
133
|
+
title = getattr(studies[0], "name", root.name) if studies else root.name
|
|
134
|
+
ready = StageState.SUCCEEDED if studies else StageState.EMPTY
|
|
135
|
+
return RunView(
|
|
136
|
+
kind="casebench", root=root, title=str(title), case_studies=studies,
|
|
137
|
+
stages=_stages({stage: (ready if stage is StageId.M1 else StageState.UNAVAILABLE, None, "")
|
|
138
|
+
for stage in StageId}),
|
|
139
|
+
)
|
|
140
|
+
runs = list(session.get("runs") or [])
|
|
141
|
+
if runs:
|
|
142
|
+
turn = runs[0]
|
|
143
|
+
report = turn.get("report") if isinstance(turn, dict) else None
|
|
144
|
+
artifact_dir = Path(str(turn.get("dir") or root)) if isinstance(turn, dict) else root
|
|
145
|
+
return _from_explore(root, artifact_dir, report if isinstance(report, dict) else {})
|
|
146
|
+
return RunView(
|
|
147
|
+
kind="empty", root=root, title=root.name,
|
|
148
|
+
stages=_stages({stage: (StageState.UNAVAILABLE, None, "No readable run artifact.") for stage in StageId}),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _from_explore(root: Path, artifact_dir: Path, report: dict[str, Any]) -> RunView:
|
|
153
|
+
confirm = _sibling_json(artifact_dir, "confirm_report.json")
|
|
154
|
+
fix_report = _sibling_json(artifact_dir, "fix_report.json")
|
|
155
|
+
ok = bool(report.get("ok", True))
|
|
156
|
+
m2 = StageState.SUCCEEDED if ok else StageState.FAILED
|
|
157
|
+
hypotheses = [h for h in report.get("hypotheses") or [] if isinstance(h, dict)]
|
|
158
|
+
m3 = StageState.SUCCEEDED if hypotheses else StageState.EMPTY
|
|
159
|
+
records = report.get("data_profile") or {}
|
|
160
|
+
has_measurement = bool(records or (artifact_dir / "records.json").exists())
|
|
161
|
+
title = str(report.get("plain_question") or report.get("question") or root.name)
|
|
162
|
+
states = {
|
|
163
|
+
StageId.M1: (StageState.SUCCEEDED if has_measurement else StageState.UNAVAILABLE, None,
|
|
164
|
+
"Normalized input records" if has_measurement else "No per-case measurement artifact."),
|
|
165
|
+
StageId.M2: (m2, len(report.get("takeaways") or []), "Exploratory report"),
|
|
166
|
+
StageId.M3: (m3, len(hypotheses), "Falsifiable hypotheses"),
|
|
167
|
+
StageId.M4: (StageState.SUCCEEDED if confirm else StageState.NOT_STARTED,
|
|
168
|
+
len((confirm or {}).get("hypothesis_verdicts") or []) if confirm else None,
|
|
169
|
+
"Held-out validation" if confirm else "No held-out validation artifact."),
|
|
170
|
+
StageId.M5: (StageState.SUCCEEDED if fix_report else StageState.NOT_STARTED,
|
|
171
|
+
len(((fix_report or {}).get("fix") or {}).get("attempted") or []) if fix_report else None,
|
|
172
|
+
"Intervention and repair sweep" if fix_report else "No repair artifact."),
|
|
173
|
+
}
|
|
174
|
+
return RunView("explore", root, title, _stages(states), report, confirm, fix_report)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _from_loop(root: Path, story: dict[str, Any]) -> RunView:
|
|
178
|
+
probes = list(story.get("probes") or [])
|
|
179
|
+
analyses = list(story.get("analyses") or [])
|
|
180
|
+
diagnoses = list(story.get("diagnoses") or [])
|
|
181
|
+
surgeries = list(story.get("surgeries") or [])
|
|
182
|
+
fixes = list(story.get("fixes") or [])
|
|
183
|
+
lifecycle = story.get("run_start") or {}
|
|
184
|
+
stored_name = root.parent.name if root.name.lower() in {"output", "outputs", "results", "run"} else root.name
|
|
185
|
+
title = str(lifecycle.get("protocol_description") or lifecycle.get("question") or stored_name)
|
|
186
|
+
|
|
187
|
+
def outcome(events: list[Any], detail: str) -> tuple[StageState, int | None, str]:
|
|
188
|
+
if not events:
|
|
189
|
+
return StageState.NOT_STARTED, None, detail
|
|
190
|
+
return StageState.SUCCEEDED, len(events), detail
|
|
191
|
+
|
|
192
|
+
m4 = [event for event in surgeries if str(event.get("module", "")).lower() == "m4"]
|
|
193
|
+
m5 = [event for event in surgeries if str(event.get("module", "")).lower() == "m5"]
|
|
194
|
+
# A legacy/post-loop ``fix`` event is semantically M5 even when it does
|
|
195
|
+
# not carry an explicit m5 module tag.
|
|
196
|
+
m5_count = len(m5) + len(fixes)
|
|
197
|
+
states = {
|
|
198
|
+
StageId.M1: outcome(probes, "Analyzer measurements"),
|
|
199
|
+
StageId.M2: outcome(analyses, "Evidence screening"),
|
|
200
|
+
StageId.M3: outcome(diagnoses, "Mechanism hypotheses"),
|
|
201
|
+
StageId.M4: outcome(m4, "Hypothesis validation"),
|
|
202
|
+
StageId.M5: (StageState.SUCCEEDED, m5_count, "Interventions and repair attempts")
|
|
203
|
+
if m5_count else (StageState.NOT_STARTED, None, "No intervention or repair run."),
|
|
204
|
+
}
|
|
205
|
+
return RunView("diagnostic", root, title, _stages(states), story=story)
|