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
evalrx/analysis/cli.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Command-line entrypoint for standalone single-shot exploration.
|
|
2
|
+
|
|
3
|
+
``evalrx-explore`` (and ``evalrx explore``) run one exploratory analysis
|
|
4
|
+
over JSON/JSONL logs: a CLI coding agent writes EDA code, the host adjudicates
|
|
5
|
+
any host-checkable candidate statistics, charts are rendered, and artifacts are
|
|
6
|
+
written. This replaces the retired interactive chat REPL — same backend, single
|
|
7
|
+
shot, no multi-turn state.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
|
|
14
|
+
from evalrx.analysis.explore_run import run_explore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv: list[str] | None = None) -> int:
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="evalrx-explore",
|
|
20
|
+
description="Run Lambda-style local exploratory analysis over JSON/JSONL logs (single shot).",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("path", help="File or directory of JSON/JSONL results.")
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"-q",
|
|
25
|
+
"--question",
|
|
26
|
+
default="Explore this dataset and surface the patterns that matter.",
|
|
27
|
+
help="Natural-language analysis question for the local coding agent.",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--outcome-col",
|
|
31
|
+
default=None,
|
|
32
|
+
help="Name of the target/outcome column, if any (e.g. 'label'). Omit to "
|
|
33
|
+
"auto-detect by name heuristics, or fall back to unsupervised EDA "
|
|
34
|
+
"when the data has no recognizable outcome.",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--out",
|
|
38
|
+
default="evalrx_explore_output",
|
|
39
|
+
help="Output directory for report/code/figures/tables.",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--backend",
|
|
43
|
+
"--coder-provider",
|
|
44
|
+
dest="coder_provider",
|
|
45
|
+
default="antigravity",
|
|
46
|
+
choices=["antigravity", "codex", "claude_code", "opencode", "gemini_cli", "kimi_cli"],
|
|
47
|
+
help="Local CLI coding-agent backend.",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument("--coder-model", "--model", dest="coder_model", default="",
|
|
50
|
+
help="Optional model flag for the CLI agent.")
|
|
51
|
+
parser.add_argument("--coder-binary", default="", help="Explicit path to the CLI binary.")
|
|
52
|
+
parser.add_argument("--max-rows", type=int, default=2000, help="Maximum loaded records.")
|
|
53
|
+
parser.add_argument("--max-files", type=int, default=200, help="Maximum JSON files to scan.")
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--include-tool-calls",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Also load tool_calls_*.json files. Off by default because these can dominate logs.",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument("--timeout-sec", type=int, default=120, help="Sandbox/agent timeout.")
|
|
60
|
+
parser.add_argument("--max-attempts", type=int, default=2, help="Code repair attempts.")
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--dashboard",
|
|
63
|
+
action="store_true",
|
|
64
|
+
help="Open the Streamlit dashboard on the output directory when done.",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument("--port", type=int, default=None, help="Optional dashboard port.")
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--skill",
|
|
69
|
+
action="append",
|
|
70
|
+
default=[],
|
|
71
|
+
metavar="DIR",
|
|
72
|
+
help="Path to an Agent-Skill directory (with SKILL.md) to style the "
|
|
73
|
+
"agent-authored figures, e.g. a nature-figure skill. Repeatable. "
|
|
74
|
+
"Only the claude/agy backends use skills.",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--allow-skills",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Enable the Skill tool so globally-installed (~/.claude/skills) "
|
|
80
|
+
"skills are usable too. Implied when --skill is given.",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--no-skills",
|
|
84
|
+
dest="use_bundled_skills",
|
|
85
|
+
action="store_false",
|
|
86
|
+
default=True,
|
|
87
|
+
help="Do not apply the package's bundled skills (e.g. nature-figure) "
|
|
88
|
+
"for this run.",
|
|
89
|
+
)
|
|
90
|
+
args = parser.parse_args(argv)
|
|
91
|
+
|
|
92
|
+
return run_explore(
|
|
93
|
+
args.path,
|
|
94
|
+
question=args.question,
|
|
95
|
+
out=args.out,
|
|
96
|
+
coder_provider=args.coder_provider,
|
|
97
|
+
coder_model=args.coder_model,
|
|
98
|
+
coder_binary=args.coder_binary,
|
|
99
|
+
max_rows=args.max_rows,
|
|
100
|
+
max_files=args.max_files,
|
|
101
|
+
include_tool_calls=args.include_tool_calls,
|
|
102
|
+
timeout_sec=args.timeout_sec,
|
|
103
|
+
max_attempts=args.max_attempts,
|
|
104
|
+
dashboard=args.dashboard,
|
|
105
|
+
dashboard_port=args.port,
|
|
106
|
+
skills=args.skill,
|
|
107
|
+
allow_skills=args.allow_skills,
|
|
108
|
+
use_bundled_skills=args.use_bundled_skills,
|
|
109
|
+
outcome_col=args.outcome_col,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__": # pragma: no cover
|
|
114
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Compatibility loaders plus a dependency-free HTML report server.
|
|
2
|
+
|
|
3
|
+
A single-shot pipeline produces one output directory; this loader reads it.
|
|
4
|
+
Two product shapes are recognised:
|
|
5
|
+
|
|
6
|
+
- **explore output**: an ``exploratory_report.json`` (or ``fused_report.json``)
|
|
7
|
+
directly in the directory, with ``figures/`` and ``tables/`` beside it.
|
|
8
|
+
- **loop run**: a ``logs_*/run.json`` (M2 stats / M3 hypotheses + chart
|
|
9
|
+
references / M4 / Fix) plus an optional ``fused_report.json``.
|
|
10
|
+
|
|
11
|
+
There is no multi-turn chat session anymore — ``load_run`` replaces the old
|
|
12
|
+
``load_session`` that walked ``turn_*`` subdirectories.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import json
|
|
19
|
+
import threading
|
|
20
|
+
import webbrowser
|
|
21
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from evalrx.reporting.compiler import compile_diagnostic_report
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _QuietHandler(SimpleHTTPRequestHandler):
|
|
29
|
+
"""Do not emit one log line per asset request in the report server."""
|
|
30
|
+
|
|
31
|
+
def log_message(self, _format: str, *_args: Any) -> None:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _report_path(run_dir: Path, *, no_audio: bool) -> Path:
|
|
36
|
+
"""Return a finished report, compiling it if needed."""
|
|
37
|
+
report_path = run_dir / "report.html"
|
|
38
|
+
if report_path.exists():
|
|
39
|
+
return report_path
|
|
40
|
+
from evalrx.reporting.html_report import build_html_report
|
|
41
|
+
|
|
42
|
+
return build_html_report(run_dir, out_path=report_path, no_audio=no_audio)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def serve_report(
|
|
46
|
+
run_dir: str | Path,
|
|
47
|
+
*,
|
|
48
|
+
port: int = 8501,
|
|
49
|
+
no_audio: bool = False,
|
|
50
|
+
open_browser: bool = True,
|
|
51
|
+
block: bool = True,
|
|
52
|
+
) -> int:
|
|
53
|
+
"""Serve a generated report locally without a UI framework.
|
|
54
|
+
|
|
55
|
+
The server is deliberately bound to loopback: reports may contain prompts,
|
|
56
|
+
model outputs, and embedded user media. ``block=False`` is provided for
|
|
57
|
+
callers/tests that manage the returned process lifetime themselves.
|
|
58
|
+
"""
|
|
59
|
+
root = Path(run_dir).resolve()
|
|
60
|
+
report = _report_path(root, no_audio=no_audio)
|
|
61
|
+
handler = functools.partial(_QuietHandler, directory=str(report.parent))
|
|
62
|
+
server = ThreadingHTTPServer(("127.0.0.1", port), handler)
|
|
63
|
+
url = f"http://127.0.0.1:{server.server_port}/{report.name}"
|
|
64
|
+
from evalrx.term_links import hyperlink
|
|
65
|
+
|
|
66
|
+
print(f"Serving diagnostic report at {hyperlink(url)}")
|
|
67
|
+
if open_browser:
|
|
68
|
+
webbrowser.open(url)
|
|
69
|
+
if not block:
|
|
70
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
71
|
+
return 0
|
|
72
|
+
try:
|
|
73
|
+
server.serve_forever()
|
|
74
|
+
except KeyboardInterrupt:
|
|
75
|
+
pass
|
|
76
|
+
finally:
|
|
77
|
+
server.server_close()
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def launch_dashboard(run_dir: str | Path, *, port: int | None = None) -> int:
|
|
82
|
+
"""Deprecated compatibility alias for :func:`serve_report`."""
|
|
83
|
+
print("'evalrx dashboard' is deprecated; use 'evalrx serve'.")
|
|
84
|
+
return serve_report(run_dir, port=port or 8501)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _read_json(path: Path) -> dict[str, Any] | None:
|
|
88
|
+
try:
|
|
89
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
90
|
+
except Exception:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_run(run_dir: str | Path) -> dict[str, Any]:
|
|
95
|
+
"""Load a single explore-output or loop-run directory into a view dict.
|
|
96
|
+
|
|
97
|
+
Returns ``{root, kind, runs, story, case_studies}`` where:
|
|
98
|
+
- ``kind`` is ``"explore"``, ``"loop"``, ``"casebench"`` or ``"empty"``;
|
|
99
|
+
- ``runs`` is a list of ``{name, dir, report}`` (explore reports found,
|
|
100
|
+
most-specific first); the dashboard renders these as report cards;
|
|
101
|
+
- ``story`` is a parsed loop narrative (``None`` for explore output) —
|
|
102
|
+
see :func:`load_loop_story`;
|
|
103
|
+
- ``case_studies`` is a list of :class:`~evalrx.analysis.case_studio.
|
|
104
|
+
CaseStudy` (non-empty only for ``kind == "casebench"``) — a
|
|
105
|
+
paper-method bench run under ``examples/``, whose report pairs per-case
|
|
106
|
+
outcomes with the benchmark manifest beside it so each case can be
|
|
107
|
+
played/read and answered by a human.
|
|
108
|
+
"""
|
|
109
|
+
root = Path(run_dir).resolve()
|
|
110
|
+
runs: list[dict[str, Any]] = []
|
|
111
|
+
|
|
112
|
+
for fname in ("exploratory_report.json", "fused_report.json"):
|
|
113
|
+
report = _read_json(root / fname)
|
|
114
|
+
if report is not None:
|
|
115
|
+
runs.append({"name": fname.replace(".json", ""), "dir": str(root), "report": report})
|
|
116
|
+
|
|
117
|
+
story = load_loop_story(root)
|
|
118
|
+
if story is not None:
|
|
119
|
+
# A loop run may also carry a fused_report.json (Step 1 explore artifact).
|
|
120
|
+
return {"root": str(root), "kind": "loop", "runs": runs, "story": story,
|
|
121
|
+
"case_studies": []}
|
|
122
|
+
|
|
123
|
+
if runs:
|
|
124
|
+
return {"root": str(root), "kind": "explore", "runs": runs, "story": None,
|
|
125
|
+
"case_studies": []}
|
|
126
|
+
|
|
127
|
+
# A paper-method bench run (examples/**/run.py): no explore report and no
|
|
128
|
+
# loop log, just outputs/<name>.json + the benchmark manifest it read.
|
|
129
|
+
case_studies = load_case_studies(root)
|
|
130
|
+
if case_studies:
|
|
131
|
+
return {"root": str(root), "kind": "casebench", "runs": [], "story": None,
|
|
132
|
+
"case_studies": case_studies}
|
|
133
|
+
|
|
134
|
+
# Example/project roots conventionally keep a loop run under ``outputs/``.
|
|
135
|
+
# Accept the documented project-root invocation as well as the run directory
|
|
136
|
+
# itself, while retaining case-book precedence for legacy bench examples
|
|
137
|
+
# whose report JSON also lives under ``outputs/``.
|
|
138
|
+
outputs_dir = root / "outputs"
|
|
139
|
+
if outputs_dir.is_dir():
|
|
140
|
+
nested = load_run(outputs_dir)
|
|
141
|
+
if nested["kind"] != "empty":
|
|
142
|
+
return nested
|
|
143
|
+
|
|
144
|
+
# Legacy / fallback: a directory of turn_* explore reports (pre-retirement).
|
|
145
|
+
for turn_dir in sorted(root.glob("turn_*")):
|
|
146
|
+
report = _read_json(turn_dir / "exploratory_report.json")
|
|
147
|
+
if report is not None:
|
|
148
|
+
runs.append({"name": turn_dir.name, "dir": str(turn_dir), "report": report})
|
|
149
|
+
kind = "explore" if runs else "empty"
|
|
150
|
+
return {"root": str(root), "kind": kind, "runs": runs, "story": None, "case_studies": []}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def load_case_studies(run_dir: str | Path) -> list[Any]:
|
|
154
|
+
"""Every paper-method bench report under *run_dir*, joined with its manifest.
|
|
155
|
+
|
|
156
|
+
Separate from :func:`load_run` so the upload workbench and tests can ask for
|
|
157
|
+
case books directly. Returns ``[]`` when the directory holds no bench report.
|
|
158
|
+
"""
|
|
159
|
+
from evalrx.analysis.case_studio import build_case_study, find_bench_reports
|
|
160
|
+
|
|
161
|
+
studies = [build_case_study(p) for p in find_bench_reports(Path(run_dir).resolve())]
|
|
162
|
+
return [s for s in studies if s is not None]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def load_loop_story(run_dir: str | Path) -> dict[str, Any] | None:
|
|
166
|
+
"""Parse a loop run's logged events into an ordered diagnostic story.
|
|
167
|
+
|
|
168
|
+
Returns ``None`` when no loop log is present (i.e. this is explore output).
|
|
169
|
+
|
|
170
|
+
A single run can be split across several logs when several separate
|
|
171
|
+
stage passes ran against the same root (e.g. a descriptive analysis-phase
|
|
172
|
+
pass AND a stale all-in-one confirm pass) — merging both would mix a
|
|
173
|
+
descriptive run with surgeries/verdicts from another run, so among
|
|
174
|
+
multiple M2+ sources only the single most-recent arc is kept. In
|
|
175
|
+
practice this run has exactly one source (``run.json``); the merge below
|
|
176
|
+
stays general rather than assuming that.
|
|
177
|
+
"""
|
|
178
|
+
root = Path(run_dir).resolve()
|
|
179
|
+
from evalrx.reporting.run_events import read_v2_events, resolve_v2_root
|
|
180
|
+
|
|
181
|
+
v2_root = resolve_v2_root(root)
|
|
182
|
+
if v2_root is None:
|
|
183
|
+
return None
|
|
184
|
+
candidate_paths = [v2_root / "run.json"]
|
|
185
|
+
events_by_path: dict[Path, list[dict[str, Any]]] = {candidate_paths[0]: read_v2_events(v2_root)}
|
|
186
|
+
|
|
187
|
+
_M2PLUS = {"analysis", "diagnosis", "surgery", "fix"}
|
|
188
|
+
m2plus_logs = [p for p, evs in events_by_path.items()
|
|
189
|
+
if any(str(e.get("event")) in _M2PLUS for e in evs)]
|
|
190
|
+
if len(m2plus_logs) > 1:
|
|
191
|
+
newest = max(m2plus_logs, key=lambda p: p.stat().st_mtime if p.exists() else 0.0)
|
|
192
|
+
# Drop the older/stale M2+ arcs; keep M1-only logs and the newest arc.
|
|
193
|
+
log_paths = [p for p in candidate_paths
|
|
194
|
+
if p not in m2plus_logs or p == newest]
|
|
195
|
+
else:
|
|
196
|
+
log_paths = candidate_paths
|
|
197
|
+
|
|
198
|
+
events: list[dict[str, Any]] = [e for lp in log_paths for e in events_by_path.get(lp, [])]
|
|
199
|
+
if not events:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
by_event: dict[str, list[dict[str, Any]]] = {}
|
|
203
|
+
for ev in events:
|
|
204
|
+
by_event.setdefault(str(ev.get("event", "")), []).append(ev)
|
|
205
|
+
|
|
206
|
+
# Display the log that actually carries the diagnostic arc (most events).
|
|
207
|
+
log_path = max(log_paths, key=lambda p: p.stat().st_size if p.exists() else 0)
|
|
208
|
+
explore_report, explore_dir = _find_explore_report(root, log_path)
|
|
209
|
+
|
|
210
|
+
# run_start/loop_end bracket the whole run — prefer the carrier log's own
|
|
211
|
+
# (the arc actually being displayed), falling back to any log so an M1-only
|
|
212
|
+
# log still surfaces a run_start when no M2+ arc exists yet.
|
|
213
|
+
carrier_events = events_by_path.get(log_path, [])
|
|
214
|
+
run_start = next((e for e in carrier_events if e.get("event") == "run_start"), None)
|
|
215
|
+
if run_start is None:
|
|
216
|
+
run_start = next((e for e in events if e.get("event") == "run_start"), None)
|
|
217
|
+
_carrier_loop_ends = [e for e in carrier_events if e.get("event") == "loop_end"]
|
|
218
|
+
loop_end = _carrier_loop_ends[-1] if _carrier_loop_ends else None
|
|
219
|
+
if loop_end is None:
|
|
220
|
+
_all_loop_ends = [e for e in events if e.get("event") == "loop_end"]
|
|
221
|
+
loop_end = _all_loop_ends[-1] if _all_loop_ends else None
|
|
222
|
+
|
|
223
|
+
agent_steps = _merge_agent_steps(
|
|
224
|
+
by_event.get("agent_decision", []), by_event.get("agent_tool", [])
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
story = {
|
|
228
|
+
"log_path": str(log_path),
|
|
229
|
+
"run_start": run_start,
|
|
230
|
+
"loop_end": loop_end,
|
|
231
|
+
"probes": by_event.get("probe", []),
|
|
232
|
+
"agent_steps": agent_steps,
|
|
233
|
+
"mode": "agentic" if agent_steps else "loop",
|
|
234
|
+
"analyses": by_event.get("analysis", []),
|
|
235
|
+
"diagnoses": by_event.get("diagnosis", []),
|
|
236
|
+
"surgeries": by_event.get("surgery", []),
|
|
237
|
+
"fixes": by_event.get("fix", []),
|
|
238
|
+
"n_events": len(events),
|
|
239
|
+
# The Step-1 explorer report (charts/observations M3 was allowed to consult)
|
|
240
|
+
# usually lives in a SIBLING dir (e.g. OUT/fused/) — not next to the log —
|
|
241
|
+
# so resolve it across the run tree rather than requiring co-location.
|
|
242
|
+
"explore_report": explore_report,
|
|
243
|
+
"explore_dir": explore_dir,
|
|
244
|
+
# Convention files a stage may have written next to the log (not events —
|
|
245
|
+
# so no new run_log schema surface is needed for these).
|
|
246
|
+
"m4_results": _read_sibling_json(root, log_path, "report/m4_results.json") or [],
|
|
247
|
+
"failure_modes": _read_sibling_json(root, log_path, "artifacts/failure_modes.json"),
|
|
248
|
+
}
|
|
249
|
+
if not story["diagnoses"]:
|
|
250
|
+
proposed = _read_proposed_hypotheses(root)
|
|
251
|
+
if proposed:
|
|
252
|
+
story["diagnoses"] = [{
|
|
253
|
+
"event": "diagnosis",
|
|
254
|
+
"cycle": 0,
|
|
255
|
+
"n_hypotheses": len(proposed),
|
|
256
|
+
"hypotheses": proposed,
|
|
257
|
+
"source": "analysis/proposed_hypotheses.json",
|
|
258
|
+
}]
|
|
259
|
+
story["diagnostic_report"] = compile_diagnostic_report(story, explore_report).to_dict()
|
|
260
|
+
return story
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _read_proposed_hypotheses(root: Path) -> list[dict[str, Any]]:
|
|
264
|
+
"""Fallback for analysis-phase dashboards when the M3 log event is absent."""
|
|
265
|
+
for path in (root / "analysis" / "proposed_hypotheses.json", root / "proposed_hypotheses.json"):
|
|
266
|
+
raw = _read_json(path)
|
|
267
|
+
if isinstance(raw, list):
|
|
268
|
+
return [h for h in raw if isinstance(h, dict)]
|
|
269
|
+
if isinstance(raw, dict) and isinstance(raw.get("hypotheses"), list):
|
|
270
|
+
return [h for h in raw["hypotheses"] if isinstance(h, dict)]
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _find_explore_report(root: Path, log_path: Path) -> tuple[dict[str, Any] | None, str | None]:
|
|
275
|
+
"""Locate the Step-1 explore/fused report for a loop run.
|
|
276
|
+
|
|
277
|
+
The loop writes ``run.json`` under ``logs*/`` while the fused pipeline
|
|
278
|
+
writes ``fused_report.json`` under a separate ``fused/`` dir. Search the run
|
|
279
|
+
tree and its parents/siblings so the dashboard can show what M3 consulted."""
|
|
280
|
+
bases = [root, log_path.parent, log_path.parent.parent]
|
|
281
|
+
seen: set[Path] = set()
|
|
282
|
+
for base in bases:
|
|
283
|
+
if base in seen or not base.exists():
|
|
284
|
+
continue
|
|
285
|
+
seen.add(base)
|
|
286
|
+
for pattern in (
|
|
287
|
+
"fused_report.json", "exploratory_report.json",
|
|
288
|
+
"fused/fused_report.json", "*/fused_report.json", "*/exploratory_report.json",
|
|
289
|
+
):
|
|
290
|
+
for cand in sorted(base.glob(pattern)):
|
|
291
|
+
report = _read_json(cand)
|
|
292
|
+
if report is not None and (report.get("charts") or report.get("observations")):
|
|
293
|
+
return report, str(cand.parent)
|
|
294
|
+
return None, None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _read_sibling_json(root: Path, log_path: Path, *rel_paths: str) -> Any | None:
|
|
298
|
+
"""Look for any of *rel_paths* (e.g. ``"report/m4_results.json"``) under
|
|
299
|
+
``root``, then the log's parent and grandparent — mirrors
|
|
300
|
+
:func:`_find_explore_report`'s search so convention files resolve whether
|
|
301
|
+
*run_dir* is a bare RunContext root or a ``logs*/`` subdir was selected as
|
|
302
|
+
the carrier log. Returns the first hit, or ``None``."""
|
|
303
|
+
bases = [root, log_path.parent, log_path.parent.parent]
|
|
304
|
+
seen: set[Path] = set()
|
|
305
|
+
for base in bases:
|
|
306
|
+
if base in seen or not base.exists():
|
|
307
|
+
continue
|
|
308
|
+
seen.add(base)
|
|
309
|
+
for rel in rel_paths:
|
|
310
|
+
data = _read_json(base / rel)
|
|
311
|
+
if data is not None:
|
|
312
|
+
return data
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _merge_agent_steps(
|
|
317
|
+
decisions: list[dict[str, Any]], tools: list[dict[str, Any]]
|
|
318
|
+
) -> list[dict[str, Any]]:
|
|
319
|
+
"""Merge ``agent_decision`` + ``agent_tool`` events (AgenticDiagnoseLoop)
|
|
320
|
+
into one ordered per-step list.
|
|
321
|
+
|
|
322
|
+
Each entry: ``{step, action, params, rationale, valid, repair_attempts,
|
|
323
|
+
fallback_used, judge_io, outcome}`` where ``outcome`` is
|
|
324
|
+
``{tool, ok, summary, error, duration_sec} | None`` (``None`` when the
|
|
325
|
+
matching ``agent_tool`` event wasn't logged, e.g. a truncated run)."""
|
|
326
|
+
tools_by_step = {t.get("step"): t for t in tools}
|
|
327
|
+
steps: list[dict[str, Any]] = []
|
|
328
|
+
for d in sorted(decisions, key=lambda e: e.get("step", 0)):
|
|
329
|
+
tool_ev = tools_by_step.get(d.get("step"))
|
|
330
|
+
outcome = None
|
|
331
|
+
if tool_ev is not None:
|
|
332
|
+
outcome = {
|
|
333
|
+
"tool": tool_ev.get("tool"),
|
|
334
|
+
"ok": tool_ev.get("ok"),
|
|
335
|
+
"summary": tool_ev.get("summary", ""),
|
|
336
|
+
"error": tool_ev.get("error"),
|
|
337
|
+
"duration_sec": tool_ev.get("duration_sec"),
|
|
338
|
+
}
|
|
339
|
+
steps.append({
|
|
340
|
+
"step": d.get("step"),
|
|
341
|
+
"action": d.get("action"),
|
|
342
|
+
"params": d.get("params") or {},
|
|
343
|
+
"rationale": d.get("rationale", ""),
|
|
344
|
+
"valid": d.get("valid", True),
|
|
345
|
+
"repair_attempts": d.get("repair_attempts", 0),
|
|
346
|
+
"fallback_used": d.get("fallback_used", False),
|
|
347
|
+
"judge_io": d.get("judge_io"),
|
|
348
|
+
"outcome": outcome,
|
|
349
|
+
})
|
|
350
|
+
return steps
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
eval_case_matrix.py
|
|
3
|
+
===================
|
|
4
|
+
Reconstruct a tidy *per-case* feature matrix from a frozen M1 state pickle
|
|
5
|
+
(``outputs/m1_state.pkl``). The explore step only persists aggregates (group
|
|
6
|
+
means, a single scatter pair); the raw per-case records needed for distribution
|
|
7
|
+
diagnostics, ROC, correlation heatmaps and a confusion matrix live inside the
|
|
8
|
+
analyzer Results. This module joins them into one DataFrame, keyed by case id.
|
|
9
|
+
|
|
10
|
+
No GPU / no re-inference: everything here is read from the pickle the M1 pass
|
|
11
|
+
already wrote. Columns that an analyzer did not cover are left as NaN (e.g. the
|
|
12
|
+
attention analyzer subsamples, so ``attn_*`` is populated for a subset only).
|
|
13
|
+
|
|
14
|
+
from evalrx.analysis.eval_case_matrix import load_case_matrix
|
|
15
|
+
df = load_case_matrix("outputs") # -> tidy DataFrame or None
|
|
16
|
+
|
|
17
|
+
Output columns (present when the underlying analyzer ran):
|
|
18
|
+
case_id str
|
|
19
|
+
label "FAIL" / "PASS"
|
|
20
|
+
is_fail 1 / 0
|
|
21
|
+
model_yes 1 / 0 (the model's actual Yes/No answer)
|
|
22
|
+
truth_yes 1 / 0 (ground-truth presence)
|
|
23
|
+
attn_max float relative_attention.max_relative_weight
|
|
24
|
+
attn_mean float relative_attention.mean_relative_weight
|
|
25
|
+
focus_share float relative_attention.focus_share
|
|
26
|
+
probe1_fd 0 / 1 generated:probe1.false_detection (LEAKY — see §4)
|
|
27
|
+
pc_fixed_describe bool prompt_contrast: describe-first repaired this case
|
|
28
|
+
pc_broken_describe bool
|
|
29
|
+
pc_fixed_sensitive bool
|
|
30
|
+
pc_broken_sensitive bool
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import pickle
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
import pandas as pd
|
|
40
|
+
|
|
41
|
+
# Continuous signals safe to feed to ROC / coefPlot / distribution diagnostics.
|
|
42
|
+
# probe1_fd is deliberately excluded — it is the failure label re-measured (leaky).
|
|
43
|
+
CONTINUOUS_SIGNALS = ["attn_max", "attn_mean", "focus_share"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _yesno(v: Any) -> int | None:
|
|
47
|
+
s = str(v).strip().lower()
|
|
48
|
+
if s.startswith("yes"):
|
|
49
|
+
return 1
|
|
50
|
+
if s.startswith("no"):
|
|
51
|
+
return 0
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_case_matrix(state: dict[str, Any]) -> pd.DataFrame:
|
|
56
|
+
"""Join the analyzers' per-case records into one tidy frame keyed by case id."""
|
|
57
|
+
cases = state.get("cases") or []
|
|
58
|
+
rows: dict[str, dict[str, Any]] = {}
|
|
59
|
+
for c in cases:
|
|
60
|
+
lab = "FAIL" if str(getattr(c, "label", "")).upper().endswith("FAIL") else "PASS"
|
|
61
|
+
rows[c.id] = {
|
|
62
|
+
"case_id": c.id,
|
|
63
|
+
"label": lab,
|
|
64
|
+
"is_fail": 1 if lab == "FAIL" else 0,
|
|
65
|
+
"model_yes": _yesno(getattr(c, "observed", "")),
|
|
66
|
+
"truth_yes": _yesno(getattr(c, "expected", "")),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pr = state.get("probe_results") or {}
|
|
70
|
+
|
|
71
|
+
ra = pr.get("relative_attention")
|
|
72
|
+
if ra is not None:
|
|
73
|
+
for r in (ra.findings.get("per_case") or []):
|
|
74
|
+
cid = r.get("id") or r.get("sample_id")
|
|
75
|
+
if cid in rows:
|
|
76
|
+
rows[cid]["attn_max"] = r.get("max_relative_weight")
|
|
77
|
+
rows[cid]["attn_mean"] = r.get("mean_relative_weight")
|
|
78
|
+
rows[cid]["focus_share"] = r.get("focus_share")
|
|
79
|
+
|
|
80
|
+
p1 = pr.get("generated:probe1")
|
|
81
|
+
if p1 is not None:
|
|
82
|
+
for r in (p1.findings.get("per_case") or []):
|
|
83
|
+
cid = r.get("sample_id") or r.get("id")
|
|
84
|
+
if cid in rows:
|
|
85
|
+
rows[cid]["probe1_fd"] = r.get("false_detection")
|
|
86
|
+
|
|
87
|
+
pc = pr.get("prompt_contrast")
|
|
88
|
+
if pc is not None:
|
|
89
|
+
per_case = (pc.artifacts or {}).get("per_case") or []
|
|
90
|
+
for r in per_case:
|
|
91
|
+
cid = r.get("sample_id") or r.get("id")
|
|
92
|
+
if cid in rows:
|
|
93
|
+
rows[cid]["pc_fixed_describe"] = r.get("fixed_by_describe_first")
|
|
94
|
+
rows[cid]["pc_broken_describe"] = r.get("broken_by_describe_first")
|
|
95
|
+
rows[cid]["pc_fixed_sensitive"] = r.get("fixed_by_sensitive")
|
|
96
|
+
rows[cid]["pc_broken_sensitive"] = r.get("broken_by_sensitive")
|
|
97
|
+
|
|
98
|
+
return pd.DataFrame(list(rows.values()))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_case_matrix(outputs_dir: str | Path) -> pd.DataFrame | None:
|
|
102
|
+
"""Load ``<outputs_dir>/m1_state.pkl`` and build the per-case matrix.
|
|
103
|
+
Returns None if the pickle is absent or unreadable."""
|
|
104
|
+
pkl = Path(outputs_dir) / "m1_state.pkl"
|
|
105
|
+
if not pkl.exists():
|
|
106
|
+
return None
|
|
107
|
+
try:
|
|
108
|
+
with open(pkl, "rb") as fh:
|
|
109
|
+
state = pickle.load(fh)
|
|
110
|
+
df = build_case_matrix(state)
|
|
111
|
+
return df if not df.empty else None
|
|
112
|
+
except Exception:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def continuous_signals(df: pd.DataFrame) -> list[str]:
|
|
117
|
+
"""Continuous, non-leaky signal columns that are actually populated."""
|
|
118
|
+
return [c for c in CONTINUOUS_SIGNALS if c in df.columns and df[c].notna().any()]
|