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,15 @@
|
|
|
1
|
+
"""Prompt templates for M4 hypothesis testing."""
|
|
2
|
+
|
|
3
|
+
_CONSISTENCY_PROMPT = """\
|
|
4
|
+
Experiment protocol:
|
|
5
|
+
{protocol_text}
|
|
6
|
+
|
|
7
|
+
Hypothesis under review:
|
|
8
|
+
Statement: {statement}
|
|
9
|
+
Predicted failure mode: {failure_mode}
|
|
10
|
+
|
|
11
|
+
Does this hypothesis address a failure mode that is relevant to the experiment protocol above?
|
|
12
|
+
Answer YES or NO on the first line, then give a one-sentence reason.
|
|
13
|
+
|
|
14
|
+
Answer YES if the hypothesis explains a failure that the protocol would care about.
|
|
15
|
+
Answer NO if the hypothesis is about something the protocol does not mention or test."""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Prompt templates for natural-language diagnosis scaffolding."""
|
|
2
|
+
|
|
3
|
+
_AGENT_PROMPT_TEMPLATE = """\
|
|
4
|
+
Generate a complete `run.py` for an EvalRX diagnosis experiment.
|
|
5
|
+
|
|
6
|
+
DESCRIPTION OF FAILURE TO DIAGNOSE
|
|
7
|
+
===================================
|
|
8
|
+
{description}
|
|
9
|
+
|
|
10
|
+
TARGET MODEL
|
|
11
|
+
============
|
|
12
|
+
{model_key}
|
|
13
|
+
|
|
14
|
+
REQUIREMENTS
|
|
15
|
+
============
|
|
16
|
+
1. Import from `evalrx` and `evalrx.eval_agent`.
|
|
17
|
+
2. Create an `ExperimentProtocol` whose `description` field captures the
|
|
18
|
+
failure described above (verbatim or paraphrased).
|
|
19
|
+
3. Build a `CaseBatch` with 6-10 `FailureCase` objects that probe the
|
|
20
|
+
described failure. Include at least two "easy" control cases that the
|
|
21
|
+
model should PASS and several "hard" cases that expose the failure.
|
|
22
|
+
Use `Inputs(prompt=..., image=...)` — image is optional for text-only failures.
|
|
23
|
+
4. Load the model with `compose("{model_key}", "hf_local", ...)`.
|
|
24
|
+
5. Run `VLDiagnoseLoop` with `ProbeAgent`, `StatsAnalysisAgent`,
|
|
25
|
+
`DiagnosisAgent`, and `AgyModel` as judge (with a try/except fallback
|
|
26
|
+
when the agy binary is absent).
|
|
27
|
+
6. Accept CLI flags: --model, --device, --dtype, --max-cycles,
|
|
28
|
+
--max-analyzers, --smoke-test, --run-dir.
|
|
29
|
+
7. Write outputs via `RunContext(args.run_dir)` (from `evalrx.eval_agent`),
|
|
30
|
+
defaulting `--run-dir` to `Path(__file__).parent / "outputs"`. This
|
|
31
|
+
defaults to the V2 logger (`run.json` + `M1..M5/log.json`, no flat
|
|
32
|
+
`run_log.jsonl`, no `README.txt`/`manifest.json`) — don't assume those
|
|
33
|
+
V1-only files exist when printing a completion summary.
|
|
34
|
+
8. Include a `--smoke-test` path with a `_SmokeModel` stand-in so the
|
|
35
|
+
script can be verified without a GPU.
|
|
36
|
+
|
|
37
|
+
Write ONLY `run.py` — nothing else. No markdown, no explanation.
|
|
38
|
+
"""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Prompt templates for M1 probe-agent analyzer selection."""
|
|
2
|
+
|
|
3
|
+
_SELECTION_PROMPT_TMPL = """\
|
|
4
|
+
You are selecting diagnostic analyzers for a model evaluation experiment.
|
|
5
|
+
Choose the analyzers that would surface the most useful evidence given the \
|
|
6
|
+
researcher's description.
|
|
7
|
+
|
|
8
|
+
EXPERIMENT DESCRIPTION:
|
|
9
|
+
{description}
|
|
10
|
+
{task_domain_section}
|
|
11
|
+
{success_criteria_section}
|
|
12
|
+
{failure_patterns_section}{cases_section}
|
|
13
|
+
MODEL TYPE: {model_kind}
|
|
14
|
+
|
|
15
|
+
AVAILABLE ANALYZERS ({n_available} compatible with this model):
|
|
16
|
+
{analyzer_list}
|
|
17
|
+
{failed_section}{prior_hypotheses_section}
|
|
18
|
+
Select up to {max_n} analyzers. Return ONLY a JSON object, no other text:
|
|
19
|
+
{{"analyzers": ["name1", "name2", ...], "rationale": "one sentence explaining the selection"}}
|
|
20
|
+
|
|
21
|
+
If the analyzers you can actually rely on (excluding any listed as failed)
|
|
22
|
+
cannot adequately surface the suspected mechanism, additionally include
|
|
23
|
+
"need_custom": "<one line describing the probe to generate>" in the JSON —
|
|
24
|
+
a bespoke probe will be code-generated and run in a sandbox.
|
|
25
|
+
"""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Prompt for ProbeLLM-style Macro/Micro question paraphrasing (VLM probe search)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
PARAPHRASE_PROMPT = """\
|
|
6
|
+
You are helping stress-test a vision-language model. Below is a question that
|
|
7
|
+
was originally asked about an image. Rewrite it as {style}, while preserving
|
|
8
|
+
its exact meaning and its correct answer — do not change what is being asked
|
|
9
|
+
about, only how it is phrased.
|
|
10
|
+
|
|
11
|
+
Original question: {question}
|
|
12
|
+
|
|
13
|
+
Reply with ONLY the rewritten question text — no quotes, no explanation, no
|
|
14
|
+
"Question:" prefix."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Prompt templates for generated code-writing stages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
_INPUT_FILENAME = "m1_probe_input.json"
|
|
6
|
+
_RESULT_MARKER = "PROBE_RESULT_JSON="
|
|
7
|
+
_MAX_OUTPUT_CHARS = 4000
|
|
8
|
+
|
|
9
|
+
_GENERATE_PROMPT = """\
|
|
10
|
+
You are writing a self-contained Python script that probes a model's text outputs
|
|
11
|
+
for a specific failure pattern.
|
|
12
|
+
|
|
13
|
+
GOAL (what to probe for):
|
|
14
|
+
{need}
|
|
15
|
+
|
|
16
|
+
DATA: a JSON file named "{input_filename}" sits in the current working directory:
|
|
17
|
+
{{
|
|
18
|
+
"cases": [
|
|
19
|
+
{{"id": "<case id>", "prompt": "<input prompt>", "expected": <expected or null>,
|
|
20
|
+
"label": "pass"|"fail"|"unknown"|null, "output": "<the model's text output>"}}
|
|
21
|
+
]
|
|
22
|
+
}}
|
|
23
|
+
|
|
24
|
+
REQUIREMENTS:
|
|
25
|
+
- Read "{input_filename}" from the current directory; do NOT hardcode the data.
|
|
26
|
+
- Compute, per case, a numeric or boolean signal for the failure pattern above
|
|
27
|
+
(e.g. 1 if the output refuses / drifts language / breaks format, else 0).
|
|
28
|
+
- Standard library + `import numpy` only. No network, no file writes, no model calls.
|
|
29
|
+
- The LAST line of stdout MUST be exactly:
|
|
30
|
+
{marker}{{"findings": {{"<aggregate_metric>": <number>}}, "per_case": [{{"sample_id": "<case id>", "<signal_name>": <number_or_bool>}}, ...]}}
|
|
31
|
+
- Every per_case entry MUST carry "sample_id" equal to the case "id".
|
|
32
|
+
- Print NOTHING after that line. Keep the script under ~60 lines.
|
|
33
|
+
|
|
34
|
+
Return ONLY the Python code{fences_hint}."""
|
|
35
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Prompt templates for generated code-writing stages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
_MANIFEST = "m1_whitebox_manifest.json"
|
|
6
|
+
_DUMP_DIR = "m1_whitebox"
|
|
7
|
+
_RESULT_MARKER = "PROBE_RESULT_JSON="
|
|
8
|
+
|
|
9
|
+
_GENERATE_PROMPT = """\
|
|
10
|
+
You are writing a self-contained Python script that probes a vision-language
|
|
11
|
+
model's INTERNAL attention for a specific failure mechanism.
|
|
12
|
+
|
|
13
|
+
GOAL (what mechanism to probe for):
|
|
14
|
+
{need}
|
|
15
|
+
|
|
16
|
+
DATA in the current working directory:
|
|
17
|
+
- "{manifest}": {{"cases": [{{"id": str, "label": "pass"|"fail"|null,
|
|
18
|
+
"seq_len": int, "n_layers": int, "tokens": [str, ...]}}, ...]}}
|
|
19
|
+
- "{dump_dir}/<case_id>.npz" per case with arrays:
|
|
20
|
+
- attn_last: float16 (n_layers, seq) — head-averaged attention from
|
|
21
|
+
the LAST query position to every key position, per layer
|
|
22
|
+
- image_token_mask: bool (seq,) — True at image-patch token positions
|
|
23
|
+
The "tokens" list in the manifest gives the token string at each position —
|
|
24
|
+
use it to locate structural/special tokens (e.g. positions whose token
|
|
25
|
+
contains "im_start", "image_pad", "vision").
|
|
26
|
+
|
|
27
|
+
REQUIREMENTS:
|
|
28
|
+
- numpy only (`import numpy as np`); no network, no other file writes.
|
|
29
|
+
- Compute ONE per-case numeric signal for the mechanism above (e.g. attention
|
|
30
|
+
mass on structural tokens, image-vs-text attention ratio at a layer,
|
|
31
|
+
attention entropy inside the image region).
|
|
32
|
+
- The LAST line of stdout MUST be exactly:
|
|
33
|
+
{marker}{{"findings": {{"<aggregate_metric>": <number>}}, "per_case": [{{"sample_id": "<case id>", "<signal_name>": <number>}}, ...]}}
|
|
34
|
+
- Every per_case entry MUST use "sample_id" equal to the case "id".
|
|
35
|
+
- Print NOTHING after that line. Keep the script under ~60 lines.
|
|
36
|
+
|
|
37
|
+
Return ONLY the Python code{fences_hint}."""
|
|
38
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Diagnostic report — turn a statistical verdict (+ its pre-registration) into output.
|
|
2
|
+
|
|
3
|
+
Carries the corrected decision, effect size + CI, e-value, the underpowered flag,
|
|
4
|
+
and the pre-registration hash that proves the hypothesis preceded the test.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from evalrx.stats import StatResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DiagnosticReport:
|
|
15
|
+
"""Format an A/B / hypothesis-test conclusion as a dict or markdown."""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def from_comparison(
|
|
19
|
+
stat: StatResult,
|
|
20
|
+
*,
|
|
21
|
+
title: str = "A/B comparison",
|
|
22
|
+
hypothesis=None,
|
|
23
|
+
prereg_hash: Optional[str] = None,
|
|
24
|
+
split: Optional[str] = None,
|
|
25
|
+
) -> dict:
|
|
26
|
+
return {
|
|
27
|
+
"title": title,
|
|
28
|
+
"decision": "REJECT H0" if stat.reject else "inconclusive",
|
|
29
|
+
"effect": stat.effect,
|
|
30
|
+
"ci": list(stat.ci),
|
|
31
|
+
"e_value": stat.e_value,
|
|
32
|
+
"underpowered": stat.underpowered,
|
|
33
|
+
"method": stat.method,
|
|
34
|
+
"alpha": stat.alpha,
|
|
35
|
+
"split": split,
|
|
36
|
+
"preregistered": prereg_hash is not None,
|
|
37
|
+
"prereg_hash": prereg_hash,
|
|
38
|
+
"hypothesis": getattr(hypothesis, "statement", None) or getattr(hypothesis, "predicate", None),
|
|
39
|
+
"details": stat.details,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def to_markdown(report: dict) -> str:
|
|
44
|
+
lines = [
|
|
45
|
+
f"## {report['title']}",
|
|
46
|
+
f"- **Decision**: {report['decision']}"
|
|
47
|
+
+ (" ⚠️ underpowered" if report.get("underpowered") else ""),
|
|
48
|
+
f"- **Effect (B−A)**: {report['effect']:+.4f} CI {report['ci'][0]:+.4f}..{report['ci'][1]:+.4f}",
|
|
49
|
+
f"- **e-value**: {report['e_value']:.3g}" if report.get("e_value") is not None else "- e-value: n/a",
|
|
50
|
+
f"- **Method**: {report['method']} (α={report['alpha']})",
|
|
51
|
+
]
|
|
52
|
+
if report.get("hypothesis"):
|
|
53
|
+
lines.append(f"- **Hypothesis**: {report['hypothesis']}")
|
|
54
|
+
if report.get("preregistered"):
|
|
55
|
+
lines.append(f"- **Pre-registered**: yes (hash `{report['prereg_hash']}`, split `{report.get('split')}`)")
|
|
56
|
+
else:
|
|
57
|
+
lines.append("- **Pre-registered**: NO — exploratory, not confirmatory")
|
|
58
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""RunContext — single owner of one diagnosis run's output directory.
|
|
2
|
+
|
|
3
|
+
Historically each ``examples/*/run.py`` glued together four independent output
|
|
4
|
+
producers by hand: the example wrote report files at the run root, the run
|
|
5
|
+
logger was buried under a ``logs/`` subdir, :class:`StatsAnalysisAgent`
|
|
6
|
+
figures went to a hand-built ``logs/figures/`` path, and the sandbox wrote
|
|
7
|
+
experiment code into an ephemeral temp dir. ``RunContext`` replaces that
|
|
8
|
+
gluing with one library-owned object that owns the whole run directory and
|
|
9
|
+
hands every producer its subdirectory.
|
|
10
|
+
|
|
11
|
+
The logger is :class:`~evalrx.eval_agent.run_logger_v2.RunLoggerV2` (see
|
|
12
|
+
``evalrx/eval_agent/RUN_LOGGER_V2.md``): one ``run.json`` plus one
|
|
13
|
+
``M1/log.json``..``M5/log.json`` per stage, no flat event file, no persisted
|
|
14
|
+
``prompts/``/``experiments/``/``tools/``/``workspace/``/``fixes/`` — generated
|
|
15
|
+
text/code is captured inline into the relevant stage's JSON via an ephemeral
|
|
16
|
+
runtime tree that :meth:`finalize` deletes, and no ``manifest.json``/
|
|
17
|
+
``README.txt`` is written (the manifest lives inside ``run.json`` instead).
|
|
18
|
+
|
|
19
|
+
Layout::
|
|
20
|
+
|
|
21
|
+
<root>/
|
|
22
|
+
├── run.json run-wide events: run_start, cases, diagnose_reports, manifest, …
|
|
23
|
+
├── M1/log.json … one JSON document per stage, plus each stage's own artifacts/
|
|
24
|
+
│ M5/log.json
|
|
25
|
+
├── contract/ one validated JSON per stage (see evalrx.contract), if emitted
|
|
26
|
+
├── artifacts/ M1 heavy numeric data (.npy / .json) written outside any stage
|
|
27
|
+
└── langfuse_trace.json
|
|
28
|
+
|
|
29
|
+
``fixes/`` and ``experiments/`` attempts (:meth:`RunContext.new_trial`) — one
|
|
30
|
+
numbered attempt per fix candidate or M5 experiment — live under an ephemeral
|
|
31
|
+
:attr:`runtime_root` instead: generated code, the sandbox it ran in, and
|
|
32
|
+
judge prompt/output are inlined into the owning stage's JSON, and the
|
|
33
|
+
runtime tree is deleted at :meth:`finalize`, so "what did attempt #14 do" is
|
|
34
|
+
one entry in that stage's log, not a filename-slug hunt across loose folders.
|
|
35
|
+
|
|
36
|
+
Usage::
|
|
37
|
+
|
|
38
|
+
from evalrx.eval_agent import RunContext, VLDiagnoseLoop
|
|
39
|
+
|
|
40
|
+
with RunContext("examples/foo/outputs", verbose=True) as ctx:
|
|
41
|
+
stats_agent = StatsAnalysisAgent(judge=judge, figure_dir=str(ctx.figures_dir))
|
|
42
|
+
loop = VLDiagnoseLoop(..., run_logger=ctx.logger)
|
|
43
|
+
report = loop.run(cases)
|
|
44
|
+
ctx.write_diagnose_report(report, cases, discovery=discovery_rows)
|
|
45
|
+
# run.json/M*/log.json are flushed incrementally throughout the run;
|
|
46
|
+
# finalize() just closes them out and inlines the runtime tree.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
import json
|
|
52
|
+
import re
|
|
53
|
+
import shutil
|
|
54
|
+
import tempfile
|
|
55
|
+
from datetime import datetime
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
from typing import TYPE_CHECKING, Any
|
|
58
|
+
|
|
59
|
+
if TYPE_CHECKING:
|
|
60
|
+
from evalrx.eval_agent.run_logger_v2 import RunLoggerV2
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Trial:
|
|
64
|
+
"""One self-contained attempt — a fix candidate or an M5 experiment.
|
|
65
|
+
|
|
66
|
+
Returned by :meth:`RunContext.new_trial`. Everything about this one
|
|
67
|
+
attempt (generated code, the sandbox it ran in, judge prompt/output, and
|
|
68
|
+
its result/record) is written under :attr:`root`, so reviewing "what did
|
|
69
|
+
attempt #14 do" never requires hopping across run-global category dirs.
|
|
70
|
+
:attr:`root` lives under :attr:`RunContext.runtime_root` — an ephemeral
|
|
71
|
+
tree captured into the owning stage's JSON and deleted at
|
|
72
|
+
:meth:`RunContext.finalize`, not a permanent directory.
|
|
73
|
+
|
|
74
|
+
Directory creation is lazy: nothing is written to disk until the first
|
|
75
|
+
call to :meth:`write` / :attr:`workspace`, so an attempt that is discarded
|
|
76
|
+
before producing anything leaves no empty folder behind.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, root: Path) -> None:
|
|
80
|
+
self.root = root
|
|
81
|
+
self._workspace: "Path | None" = None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def workspace(self) -> Path:
|
|
85
|
+
"""``<trial>/workspace/`` — where the sandbox for this attempt runs."""
|
|
86
|
+
if self._workspace is None:
|
|
87
|
+
self._workspace = self.root / "workspace"
|
|
88
|
+
self._workspace.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
return self._workspace
|
|
90
|
+
|
|
91
|
+
def write(self, name: str, content: "str | bytes") -> Path:
|
|
92
|
+
"""Write *content* to ``<trial>/<name>``; return the path."""
|
|
93
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
path = self.root / name
|
|
95
|
+
if isinstance(content, bytes):
|
|
96
|
+
path.write_bytes(content)
|
|
97
|
+
else:
|
|
98
|
+
path.write_text(content, encoding="utf-8")
|
|
99
|
+
return path
|
|
100
|
+
|
|
101
|
+
def write_record(self, markdown: str) -> Path:
|
|
102
|
+
"""Write the human-readable summary to ``<trial>/record.md``."""
|
|
103
|
+
return self.write("record.md", markdown)
|
|
104
|
+
|
|
105
|
+
def write_result(self, data: "dict[str, Any]") -> Path:
|
|
106
|
+
"""Write the machine-readable outcome to ``<trial>/result.json``."""
|
|
107
|
+
return self.write("result.json", json.dumps(data, indent=2, default=str))
|
|
108
|
+
|
|
109
|
+
def __repr__(self) -> str:
|
|
110
|
+
return f"Trial(root={str(self.root)!r})"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class RunContext:
|
|
114
|
+
"""Owns one run's output directory and every producer's subdirectory.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
root: Run root directory. Created if missing. Defaults to
|
|
118
|
+
``runs/<YYYYMMDD_HHMMSS>/`` relative to cwd.
|
|
119
|
+
run_id: Optional identifier recorded in the manifest; defaults to the
|
|
120
|
+
root directory name.
|
|
121
|
+
verbose: Forwarded to the logger (human-readable stdout).
|
|
122
|
+
narrate: Forwarded to the logger — live, aligned M1-M5 terminal
|
|
123
|
+
narration instead of `verbose`'s raw one-liner (see
|
|
124
|
+
:class:`evalrx.eval_agent.narration.LoopNarrator`).
|
|
125
|
+
Supersedes *verbose* rather than stacking with it.
|
|
126
|
+
config: Optional run-configuration dict recorded verbatim in the
|
|
127
|
+
manifest (model, judge, protocol, …).
|
|
128
|
+
|
|
129
|
+
The logger allocates producer sandboxes in an external ephemeral runtime
|
|
130
|
+
tree; their text is inlined into JSON and their media is copied before
|
|
131
|
+
finalization removes the tree.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
root: "str | Path | None" = None,
|
|
137
|
+
*,
|
|
138
|
+
run_id: "str | None" = None,
|
|
139
|
+
verbose: bool = False,
|
|
140
|
+
narrate: bool = False,
|
|
141
|
+
config: "dict[str, Any] | None" = None,
|
|
142
|
+
observability_mode: str | None = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
if root is None:
|
|
145
|
+
root = Path("runs") / datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
146
|
+
# Resolved to absolute: sandbox subprocesses run with cwd=<some
|
|
147
|
+
# workdir under root> *and* a script path built from that same
|
|
148
|
+
# workdir (see ExperimentSandbox._run_script / run_coded_pipeline) —
|
|
149
|
+
# if root stayed relative, the child process would resolve that
|
|
150
|
+
# script path a second time relative to its new cwd, doubling it.
|
|
151
|
+
self.root = Path(root).resolve()
|
|
152
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
self.run_id = run_id or self.root.name
|
|
154
|
+
self.config = dict(config or {})
|
|
155
|
+
self._verbose = verbose
|
|
156
|
+
self._narrate = narrate
|
|
157
|
+
self._observability_mode = observability_mode
|
|
158
|
+
self._logger: "RunLoggerV2 | None" = None
|
|
159
|
+
self._workdir_seq = 0
|
|
160
|
+
self._trial_seq: "dict[str, int]" = {}
|
|
161
|
+
self._runtime_root: "Path | None" = None
|
|
162
|
+
self._finalized = False
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def runtime_root(self) -> Path:
|
|
166
|
+
"""Ephemeral execution workspace used by producers.
|
|
167
|
+
|
|
168
|
+
Generated text/code is captured by the logger before this tree is
|
|
169
|
+
removed; it never becomes part of the portable run artifact.
|
|
170
|
+
"""
|
|
171
|
+
if self._runtime_root is None:
|
|
172
|
+
self._runtime_root = Path(tempfile.mkdtemp(prefix=f"evalrx-{self.run_id}-"))
|
|
173
|
+
return self._runtime_root
|
|
174
|
+
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
# Directory properties — each lazily created on first access.
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
def _sub(self, name: str) -> Path:
|
|
180
|
+
d = self.root / name
|
|
181
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
return d
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def figures_dir(self) -> Path:
|
|
186
|
+
return self._sub("M2/artifacts")
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def explore_dir(self) -> Path:
|
|
190
|
+
"""The in-cycle explore step's report, tables and rendered figures
|
|
191
|
+
(``VLDiagnoseLoop(explorer=..., explore_dir=ctx.explore_dir)``; the
|
|
192
|
+
loop derives the same path from ``ctx.logger`` when not given).
|
|
193
|
+
Ephemeral — captured into ``M2/log.json`` and deleted at
|
|
194
|
+
:meth:`finalize`, not a permanent directory."""
|
|
195
|
+
d = self.runtime_root / "explore"
|
|
196
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
return d
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def artifacts_dir(self) -> Path:
|
|
201
|
+
return self._sub("artifacts")
|
|
202
|
+
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
# Logging component
|
|
205
|
+
# ------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def logger(self) -> "RunLoggerV2":
|
|
209
|
+
"""The logger bound to this context (created on first use)."""
|
|
210
|
+
if self._logger is None:
|
|
211
|
+
from evalrx.eval_agent.run_logger_v2 import RunLoggerV2
|
|
212
|
+
|
|
213
|
+
self._logger = RunLoggerV2(
|
|
214
|
+
run_dir=self.root, verbose=self._verbose, narrate=self._narrate,
|
|
215
|
+
observability_mode=self._observability_mode,
|
|
216
|
+
context=self,
|
|
217
|
+
)
|
|
218
|
+
return self._logger
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
# Producer-facing path allocation
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def new_workdir(self, label: str) -> Path:
|
|
225
|
+
"""Return a fresh sandbox working directory under the runtime tree.
|
|
226
|
+
|
|
227
|
+
Replaces ``tempfile.mkdtemp()`` so the experiment code the agent
|
|
228
|
+
writes is captured with the rest of the run instead of vanishing
|
|
229
|
+
untracked. *label* is slugified; a monotonic counter guarantees
|
|
230
|
+
uniqueness.
|
|
231
|
+
"""
|
|
232
|
+
self._workdir_seq += 1
|
|
233
|
+
slug = re.sub(r"[^a-zA-Z0-9]+", "_", label).strip("_") or "work"
|
|
234
|
+
d = self.runtime_root / "workspace" / f"{self._workdir_seq:02d}_{slug}"
|
|
235
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
return d
|
|
237
|
+
|
|
238
|
+
def new_trial(self, category: str, label: str) -> "Trial":
|
|
239
|
+
"""Allocate one self-contained attempt folder under the runtime tree.
|
|
240
|
+
|
|
241
|
+
A *trial* is one fix candidate or one M5 experiment: its generated
|
|
242
|
+
code, the sandbox it actually ran in, judge prompt/output, and its
|
|
243
|
+
result/record all live together under one numbered folder.
|
|
244
|
+
*category* is ``"fixes"`` or ``"experiments"``; numbering is
|
|
245
|
+
monotonic per category.
|
|
246
|
+
|
|
247
|
+
The trial's own folder (and its ``workspace/``) is created lazily on
|
|
248
|
+
first write — a candidate discarded before producing anything (e.g. a
|
|
249
|
+
deduped fix proposal) leaves no empty folder on disk; the numbering
|
|
250
|
+
still advances, so a gap in the sequence honestly means "proposed,
|
|
251
|
+
then discarded," not a missing record.
|
|
252
|
+
"""
|
|
253
|
+
if category not in ("fixes", "experiments"):
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"new_trial: unknown category {category!r} "
|
|
256
|
+
"(expected 'fixes' or 'experiments')"
|
|
257
|
+
)
|
|
258
|
+
seq = self._trial_seq.get(category, 0) + 1
|
|
259
|
+
self._trial_seq[category] = seq
|
|
260
|
+
slug = re.sub(r"[^a-zA-Z0-9]+", "_", label).strip("_") or "trial"
|
|
261
|
+
return Trial(self.runtime_root / category / f"{seq:02d}_{slug}")
|
|
262
|
+
|
|
263
|
+
def figure_path(self, name: str) -> Path:
|
|
264
|
+
"""Return ``figures_dir/<name>`` (created if needed)."""
|
|
265
|
+
if not name.lower().endswith((".png", ".jpg", ".jpeg", ".svg", ".pdf")):
|
|
266
|
+
name = f"{name}.png"
|
|
267
|
+
return self.figures_dir / name
|
|
268
|
+
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
# Report API — absorbs the per-example boilerplate.
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def write_diagnose_report(
|
|
274
|
+
self,
|
|
275
|
+
report: Any,
|
|
276
|
+
cases: "list[Any]",
|
|
277
|
+
*,
|
|
278
|
+
discovery: "list[dict[str, Any]] | None" = None,
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Inline the standard post-diagnosis report into ``run.json``.
|
|
281
|
+
|
|
282
|
+
*report* is an :class:`AutoDiagnoseReport` (all three loops return
|
|
283
|
+
this one unified class — ``VLDiagnoseReport`` is an alias for it).
|
|
284
|
+
*discovery* (optional) is a list of already-serialised case rows —
|
|
285
|
+
examples that compute task-specific columns (e.g. parsed yes/no)
|
|
286
|
+
build the rows themselves. Delegates to
|
|
287
|
+
:meth:`~evalrx.eval_agent.run_logger_v2.RunLoggerV2.log_diagnose_report`.
|
|
288
|
+
"""
|
|
289
|
+
self.logger.log_diagnose_report(report, cases, discovery=discovery)
|
|
290
|
+
|
|
291
|
+
def publish_report(
|
|
292
|
+
self,
|
|
293
|
+
*,
|
|
294
|
+
model: "Any | None" = None,
|
|
295
|
+
example_dir: "str | Path | None" = None,
|
|
296
|
+
) -> "Any":
|
|
297
|
+
"""Publish the dynamic report after all diagnostic/fix stages finish."""
|
|
298
|
+
from evalrx.reporting.dynamic import publish_report
|
|
299
|
+
|
|
300
|
+
return publish_report(
|
|
301
|
+
self.root, example_dir=example_dir, model=model, run_logger=self._logger,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
# Lifecycle
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def finalize(self) -> None:
|
|
309
|
+
"""Close the logger, inline the runtime tree, and delete it. Idempotent."""
|
|
310
|
+
if self._finalized:
|
|
311
|
+
return
|
|
312
|
+
if self._logger is not None or self._runtime_root is not None:
|
|
313
|
+
logger = self.logger
|
|
314
|
+
if self._runtime_root is not None:
|
|
315
|
+
logger.log_runtime_snapshot(self._runtime_root)
|
|
316
|
+
logger.close()
|
|
317
|
+
# close() materializes the trace bundle and every M1-M5 log;
|
|
318
|
+
# index only afterwards so the manifest is complete.
|
|
319
|
+
logger.log_manifest(run_id=self.run_id, config=self.config)
|
|
320
|
+
# write_contract_index() only looks for an on-disk contract/ dir
|
|
321
|
+
# and the logger's trace_id. Without it, a run that emitted
|
|
322
|
+
# contract/ payloads (evalrx.contract.emit, independent of
|
|
323
|
+
# RunContext) never gets the index.json a reader opening the run
|
|
324
|
+
# without its producer needs.
|
|
325
|
+
self.write_contract_index()
|
|
326
|
+
if self._runtime_root is not None:
|
|
327
|
+
shutil.rmtree(self._runtime_root, ignore_errors=True)
|
|
328
|
+
self._finalized = True
|
|
329
|
+
|
|
330
|
+
def write_contract_index(self) -> "Path | None":
|
|
331
|
+
"""Write ``contract/index.json`` when any stage emitted a payload.
|
|
332
|
+
|
|
333
|
+
Written here rather than by the emitter so it describes the finished
|
|
334
|
+
directory: a reader opening this run — or a zip of it — gets one file
|
|
335
|
+
naming every payload, its stage and cycle, and the wire model that
|
|
336
|
+
decodes it, without having to infer any of that from filenames.
|
|
337
|
+
"""
|
|
338
|
+
if not (self.root / "contract").is_dir():
|
|
339
|
+
return None
|
|
340
|
+
try:
|
|
341
|
+
from evalrx.contract.emit import write_index
|
|
342
|
+
except ImportError: # contract extra not installed
|
|
343
|
+
return None
|
|
344
|
+
trace_id = getattr(self._logger, "trace_id", "") if self._logger else ""
|
|
345
|
+
return write_index(self.root, trace_id=trace_id)
|
|
346
|
+
|
|
347
|
+
def __enter__(self) -> "RunContext":
|
|
348
|
+
return self
|
|
349
|
+
|
|
350
|
+
def __exit__(self, *_: Any) -> None:
|
|
351
|
+
self.finalize()
|
|
352
|
+
|
|
353
|
+
def __repr__(self) -> str:
|
|
354
|
+
return f"RunContext(root={str(self.root)!r})"
|