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,300 @@
|
|
|
1
|
+
"""Generic held-out verification for standalone explore runs.
|
|
2
|
+
|
|
3
|
+
Generalizes the deco_hallu_explore example's ``test_hypotheses.py`` to ANY
|
|
4
|
+
dataset the explorer can load (the upload workbench's mode 2, and
|
|
5
|
+
``evalrx explore --holdout-frac F --holdout-confirm``):
|
|
6
|
+
|
|
7
|
+
1. :func:`split_records` carves the loaded records into a deterministic,
|
|
8
|
+
outcome-stratified explore/holdout partition BEFORE exploration — the
|
|
9
|
+
explorer only ever sees the explore share.
|
|
10
|
+
2. :func:`holdout_confirm` re-evaluates the explorer's FROZEN recipes verbatim
|
|
11
|
+
on the held-out rows (:func:`~evalrx.analysis.operationalize.compile_recipe`,
|
|
12
|
+
thresholds exactly as fitted — no re-fitting), rebuilds two-group sufficient
|
|
13
|
+
statistics, adjudicates with
|
|
14
|
+
:func:`~evalrx.analysis.adjudicate.adjudicate_signals`
|
|
15
|
+
(``split_label="held_out"`` — a REJECT here is a real held-out verdict), and
|
|
16
|
+
optionally has an LLM judge grade each M3 hypothesis against the held-out
|
|
17
|
+
table (supported / partial / refuted / not_testable + surgery routing).
|
|
18
|
+
|
|
19
|
+
The returned dict is the ``confirm_report.json`` shape the dashboard's
|
|
20
|
+
*Held-out Verdicts* tab renders. The judge is duck-typed (any object with
|
|
21
|
+
``generate(str) -> str``); pass ``judge=None`` for signal verdicts only —
|
|
22
|
+
hypotheses then carry ``verdict="not_judged"``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from evalrx.analysis.adjudicate import adjudicate_signals
|
|
32
|
+
from evalrx.analysis.explorer import CandidateSignal
|
|
33
|
+
from evalrx.analysis.fused_pipeline import _split_records
|
|
34
|
+
from evalrx.analysis.operationalize import RecipeError, SignalRecipe, compile_recipe
|
|
35
|
+
from evalrx.analysis.prompts.holdout import JUDGE_PROMPT
|
|
36
|
+
|
|
37
|
+
_FAIL_LIKE = ("fail", "failed", "failure", "error", "wrong", "incorrect", "no")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def split_records(
|
|
41
|
+
records: "list[dict[str, Any]]",
|
|
42
|
+
holdout_frac: float,
|
|
43
|
+
*,
|
|
44
|
+
seed: int = 0,
|
|
45
|
+
outcome_col: str | None = None,
|
|
46
|
+
) -> "tuple[list[dict[str, Any]], list[dict[str, Any]] | None]":
|
|
47
|
+
"""Deterministic ``(explore, holdout)`` partition, stratified by outcome.
|
|
48
|
+
|
|
49
|
+
Wraps the fused pipeline's split so the standalone path and the loop's
|
|
50
|
+
fused path hold out rows the same way. Returns ``(records, None)`` when
|
|
51
|
+
the fraction or batch size makes a split meaningless — callers fall back
|
|
52
|
+
to a plain in-sample run with a caveat rather than a fake split.
|
|
53
|
+
"""
|
|
54
|
+
return _split_records(
|
|
55
|
+
records, frac=holdout_frac, seed=seed, label_col=outcome_col or "label"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def failure_indicator(
|
|
60
|
+
rows: "list[dict[str, Any]]", outcome_col: str
|
|
61
|
+
) -> "tuple[list[int | None], Any, str]":
|
|
62
|
+
"""Per-row binary failure indicator for an arbitrary outcome column.
|
|
63
|
+
|
|
64
|
+
Returns ``(indicators, positive_label, note)`` where ``indicators[i]`` is
|
|
65
|
+
1 (failure), 0 (non-failure) or None (missing outcome). The positive
|
|
66
|
+
("failure") class is chosen by, in order: a fail-like string value
|
|
67
|
+
(fail/failed/error/wrong/...), the truthy pole of a boolean/0-1 column, or
|
|
68
|
+
— for any other two-valued column — the MINORITY value (failures are
|
|
69
|
+
normally the rare class); *note* says which rule fired so the report can
|
|
70
|
+
state it honestly.
|
|
71
|
+
"""
|
|
72
|
+
values = [r.get(outcome_col) for r in rows]
|
|
73
|
+
present = [v for v in values if v is not None]
|
|
74
|
+
uniq = {str(v).strip().lower() for v in present}
|
|
75
|
+
|
|
76
|
+
positive: Any = None
|
|
77
|
+
note = ""
|
|
78
|
+
fail_hits = sorted(uniq & set(_FAIL_LIKE))
|
|
79
|
+
if fail_hits:
|
|
80
|
+
positive = fail_hits[0]
|
|
81
|
+
note = f"failure = {outcome_col!r} == {positive!r} (fail-like value)"
|
|
82
|
+
elif uniq <= {"0", "1", "true", "false", "0.0", "1.0"} and uniq:
|
|
83
|
+
positive = "truthy"
|
|
84
|
+
note = f"failure = truthy {outcome_col!r} (boolean/0-1 column)"
|
|
85
|
+
elif len(uniq) == 2:
|
|
86
|
+
counts: dict[str, int] = {}
|
|
87
|
+
for v in present:
|
|
88
|
+
key = str(v).strip().lower()
|
|
89
|
+
counts[key] = counts.get(key, 0) + 1
|
|
90
|
+
positive = min(counts, key=lambda k: counts[k])
|
|
91
|
+
note = (
|
|
92
|
+
f"failure = {outcome_col!r} == {positive!r} "
|
|
93
|
+
"(minority value of a two-valued column)"
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
return [None] * len(rows), None, (
|
|
97
|
+
f"outcome column {outcome_col!r} is not binary "
|
|
98
|
+
f"({len(uniq)} distinct values) — held-out verdicts unavailable"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
out: "list[int | None]" = []
|
|
102
|
+
for v in values:
|
|
103
|
+
if v is None:
|
|
104
|
+
out.append(None)
|
|
105
|
+
continue
|
|
106
|
+
key = str(v).strip().lower()
|
|
107
|
+
if positive == "truthy":
|
|
108
|
+
out.append(1 if key in {"1", "true", "1.0"} else 0)
|
|
109
|
+
else:
|
|
110
|
+
out.append(1 if key == positive else 0)
|
|
111
|
+
return out, positive, note
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _holdout_sufficient(
|
|
115
|
+
values: "dict[str, float]",
|
|
116
|
+
id_to_fail: "dict[str, int]",
|
|
117
|
+
) -> "dict[str, Any] | None":
|
|
118
|
+
"""Two-group sufficient stats: failure indicators among signal-ABSENT (a)
|
|
119
|
+
vs signal-PRESENT (b) rows. Defined only for binary-flag recipes — a
|
|
120
|
+
continuous recipe has no frozen threshold to reuse."""
|
|
121
|
+
pairs = [(v, id_to_fail[cid]) for cid, v in values.items() if cid in id_to_fail]
|
|
122
|
+
if not pairs:
|
|
123
|
+
return None
|
|
124
|
+
if not {round(v, 6) for v, _ in pairs} <= {0.0, 1.0}:
|
|
125
|
+
return None
|
|
126
|
+
a = [f for v, f in pairs if v == 0.0]
|
|
127
|
+
b = [f for v, f in pairs if v == 1.0]
|
|
128
|
+
if not a or not b:
|
|
129
|
+
return None
|
|
130
|
+
return {"kind": "two_group", "a": a, "b": b}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _judge_json(text: str) -> "dict[str, Any] | None":
|
|
134
|
+
m = re.search(r"\{.*\}", text, re.S)
|
|
135
|
+
if not m:
|
|
136
|
+
return None
|
|
137
|
+
try:
|
|
138
|
+
parsed = json.loads(m.group(0))
|
|
139
|
+
except json.JSONDecodeError:
|
|
140
|
+
return None
|
|
141
|
+
return parsed if isinstance(parsed, dict) else None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def holdout_confirm(
|
|
145
|
+
report: "dict[str, Any]",
|
|
146
|
+
holdout_rows: "list[dict[str, Any]]",
|
|
147
|
+
*,
|
|
148
|
+
outcome_col: str = "label",
|
|
149
|
+
alpha: float = 0.05,
|
|
150
|
+
judge: Any = None,
|
|
151
|
+
judge_meta: "dict[str, Any] | None" = None,
|
|
152
|
+
) -> "dict[str, Any]":
|
|
153
|
+
"""Re-test one exploratory report's recipes + hypotheses on held-out rows.
|
|
154
|
+
|
|
155
|
+
*report* is the ``exploratory_report.json`` dict (``to_dict()`` of a live
|
|
156
|
+
report works too). Returns the ``confirm_report.json`` payload; the caller
|
|
157
|
+
persists it next to the exploratory report.
|
|
158
|
+
"""
|
|
159
|
+
rows = [dict(r) for r in holdout_rows]
|
|
160
|
+
indicators, positive_label, outcome_note = failure_indicator(rows, outcome_col)
|
|
161
|
+
|
|
162
|
+
# Stable ids: force the compile id_col so recipe values map back to rows
|
|
163
|
+
# regardless of what id-ish keys the upload carries.
|
|
164
|
+
id_to_fail: "dict[str, int]" = {}
|
|
165
|
+
for i, (row, ind) in enumerate(zip(rows, indicators)):
|
|
166
|
+
row["_holdout_id"] = f"h{i}"
|
|
167
|
+
if ind is not None:
|
|
168
|
+
id_to_fail[f"h{i}"] = ind
|
|
169
|
+
n_fail = sum(id_to_fail.values())
|
|
170
|
+
|
|
171
|
+
candidates_raw = [c for c in report.get("candidate_signals") or [] if isinstance(c, dict)]
|
|
172
|
+
hypotheses = [h for h in report.get("hypotheses") or [] if isinstance(h, dict)]
|
|
173
|
+
|
|
174
|
+
# ── 1. frozen-recipe re-evaluation on the held-out rows ─────────────────
|
|
175
|
+
signal_verdicts: "list[dict[str, Any]]" = []
|
|
176
|
+
candidates: "list[CandidateSignal]" = []
|
|
177
|
+
for c in candidates_raw:
|
|
178
|
+
entry: "dict[str, Any]" = {
|
|
179
|
+
"name": c.get("name"),
|
|
180
|
+
"display_name": c.get("display_name") or c.get("name"),
|
|
181
|
+
"recipe": c.get("recipe"),
|
|
182
|
+
"explore_effect": c.get("effect"),
|
|
183
|
+
"explore_reject_in_sample": c.get("reject"),
|
|
184
|
+
}
|
|
185
|
+
recipe_dict = c.get("recipe")
|
|
186
|
+
if not id_to_fail:
|
|
187
|
+
entry.update(status="skipped", reason=outcome_note or "no binary outcome")
|
|
188
|
+
signal_verdicts.append(entry)
|
|
189
|
+
continue
|
|
190
|
+
if not isinstance(recipe_dict, dict) or recipe_dict.get("kind") != "expr":
|
|
191
|
+
entry.update(status="skipped", reason="no expr recipe to re-evaluate")
|
|
192
|
+
signal_verdicts.append(entry)
|
|
193
|
+
continue
|
|
194
|
+
try:
|
|
195
|
+
values = compile_recipe(
|
|
196
|
+
SignalRecipe.from_dict(recipe_dict), rows, id_col="_holdout_id"
|
|
197
|
+
)
|
|
198
|
+
except (RecipeError, NotImplementedError) as exc:
|
|
199
|
+
entry.update(status="skipped", reason=f"recipe error: {exc}")
|
|
200
|
+
signal_verdicts.append(entry)
|
|
201
|
+
continue
|
|
202
|
+
sufficient = _holdout_sufficient(values, id_to_fail)
|
|
203
|
+
if sufficient is None:
|
|
204
|
+
entry.update(
|
|
205
|
+
status="skipped",
|
|
206
|
+
reason="recipe is not a binary flag on held-out rows "
|
|
207
|
+
"(no frozen threshold to reuse)",
|
|
208
|
+
)
|
|
209
|
+
signal_verdicts.append(entry)
|
|
210
|
+
continue
|
|
211
|
+
cand = CandidateSignal(
|
|
212
|
+
name=str(c.get("name") or "signal"),
|
|
213
|
+
rationale=str(c.get("rationale") or ""),
|
|
214
|
+
recipe=recipe_dict,
|
|
215
|
+
sufficient=sufficient,
|
|
216
|
+
)
|
|
217
|
+
n_a, n_b = len(sufficient["a"]), len(sufficient["b"])
|
|
218
|
+
entry.update(
|
|
219
|
+
status="adjudicated",
|
|
220
|
+
n_holdout=n_a + n_b,
|
|
221
|
+
n_flagged=n_b,
|
|
222
|
+
fail_rate_flagged=round(sum(sufficient["b"]) / n_b, 4),
|
|
223
|
+
fail_rate_unflagged=round(sum(sufficient["a"]) / n_a, 4),
|
|
224
|
+
)
|
|
225
|
+
signal_verdicts.append(entry)
|
|
226
|
+
candidates.append(cand)
|
|
227
|
+
|
|
228
|
+
meta = adjudicate_signals(candidates, alpha=alpha, split_label="held_out")
|
|
229
|
+
by_name = {c.name: c for c in candidates}
|
|
230
|
+
for entry in signal_verdicts:
|
|
231
|
+
cand = by_name.get(str(entry.get("name")))
|
|
232
|
+
if entry.get("status") == "adjudicated" and cand is not None:
|
|
233
|
+
entry.update(
|
|
234
|
+
reject=bool(cand.reject),
|
|
235
|
+
effect=cand.effect,
|
|
236
|
+
ci=cand.ci,
|
|
237
|
+
fdr_corrected=bool(cand.fdr_corrected),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# ── 2. LLM judge per hypothesis, grounded in the held-out table ─────────
|
|
241
|
+
evidence_lines = [
|
|
242
|
+
f"- {e.get('name')}: flagged-group fail rate {e.get('fail_rate_flagged')} "
|
|
243
|
+
f"vs unflagged {e.get('fail_rate_unflagged')} "
|
|
244
|
+
f"(n={e.get('n_holdout')}, effect={e.get('effect')}, CI={e.get('ci')}, "
|
|
245
|
+
f"verdict={'REJECT H0' if e.get('reject') else 'not rejected'})"
|
|
246
|
+
for e in signal_verdicts
|
|
247
|
+
if e.get("status") == "adjudicated"
|
|
248
|
+
] or ["- (no recipe could be re-evaluated on the held-out split)"]
|
|
249
|
+
evidence = "\n".join(evidence_lines)
|
|
250
|
+
|
|
251
|
+
hypothesis_verdicts: "list[dict[str, Any]]" = []
|
|
252
|
+
for h in hypotheses:
|
|
253
|
+
verdict = dict(h)
|
|
254
|
+
if judge is None:
|
|
255
|
+
verdict.update(verdict="not_judged", reasoning="", needs_surgery=True)
|
|
256
|
+
hypothesis_verdicts.append(verdict)
|
|
257
|
+
continue
|
|
258
|
+
prompt = JUDGE_PROMPT.format(
|
|
259
|
+
statement=h.get("statement", ""),
|
|
260
|
+
basis=h.get("basis", ""),
|
|
261
|
+
test_design=h.get("test_design", ""),
|
|
262
|
+
n_rows=len(rows),
|
|
263
|
+
evidence=evidence,
|
|
264
|
+
)
|
|
265
|
+
try:
|
|
266
|
+
raw = judge.generate(prompt) or ""
|
|
267
|
+
except Exception as exc: # noqa: BLE001 — judge outage must not sink the verdicts
|
|
268
|
+
raw = ""
|
|
269
|
+
verdict["judge_error"] = str(exc)
|
|
270
|
+
parsed = _judge_json(raw)
|
|
271
|
+
if parsed and parsed.get("verdict") in {"supported", "partial", "refuted", "not_testable"}:
|
|
272
|
+
verdict.update(
|
|
273
|
+
verdict=parsed["verdict"],
|
|
274
|
+
reasoning=str(parsed.get("reasoning", "")),
|
|
275
|
+
needs_surgery=bool(parsed.get("needs_surgery", False)),
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
verdict.update(
|
|
279
|
+
verdict="not_testable",
|
|
280
|
+
reasoning="judge output unparseable — defaulting to not_testable",
|
|
281
|
+
needs_surgery=True,
|
|
282
|
+
)
|
|
283
|
+
hypothesis_verdicts.append(verdict)
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"phase": "holdout_confirm",
|
|
287
|
+
"split": "held_out",
|
|
288
|
+
"n_validate_rows": len(rows),
|
|
289
|
+
"n_validate_fail": n_fail,
|
|
290
|
+
"alpha": alpha,
|
|
291
|
+
"outcome": {
|
|
292
|
+
"column": outcome_col,
|
|
293
|
+
"positive_label": positive_label,
|
|
294
|
+
"note": outcome_note,
|
|
295
|
+
},
|
|
296
|
+
"adjudication": meta,
|
|
297
|
+
"signal_verdicts": signal_verdicts,
|
|
298
|
+
"hypothesis_verdicts": hypothesis_verdicts,
|
|
299
|
+
"judge": judge_meta if judge is not None else None,
|
|
300
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""M3 — HypothesisAgent: propose falsifiable hypotheses from an exploratory
|
|
2
|
+
analysis report (an ``ExploratoryAnalysisAgent`` / M2 output).
|
|
3
|
+
|
|
4
|
+
This is a lightweight, standalone counterpart to the diagnosis loop's
|
|
5
|
+
``DiagnosisAgent`` (``evalrx.eval_agent.stages.diagnosis``): that one is
|
|
6
|
+
hard-bound to the loop's own ``AnalysisReport`` type and only accepts a judge
|
|
7
|
+
``Model`` (Gemini by default) — it cannot read an ``ExploratoryAnalysisReport``
|
|
8
|
+
without raising or silently producing nothing (there is no adapter between the
|
|
9
|
+
two report shapes). This module reads the standalone explorer's own report
|
|
10
|
+
directly and supports the same ``judge``/``cli_config`` backend flexibility as
|
|
11
|
+
``ExploratoryAnalysisAgent``, so it can run with whatever ``--backend`` the
|
|
12
|
+
``evalrx explore`` CLI was given.
|
|
13
|
+
|
|
14
|
+
Proposal only — no validation. A hypothesis here is a candidate explanation to
|
|
15
|
+
investigate further, not a conclusion; there is no confirm/test phase wired up
|
|
16
|
+
for it (see ``StatsAnalysisAgent``/``HypothesisTester`` in the diagnosis loop
|
|
17
|
+
for that, a separate, currently out-of-scope system).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import tempfile
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import TYPE_CHECKING, Any
|
|
27
|
+
|
|
28
|
+
from evalrx.analysis.plain_language import jargon_violation
|
|
29
|
+
from evalrx.analysis.prompts.hypothesis_agent import (
|
|
30
|
+
AGENT_COLUMN_MARKERS as _AGENT_COLUMN_MARKERS,
|
|
31
|
+
)
|
|
32
|
+
from evalrx.analysis.prompts.hypothesis_agent import (
|
|
33
|
+
AGENT_TRAJECTORY_HINT as _AGENT_TRAJECTORY_HINT,
|
|
34
|
+
)
|
|
35
|
+
from evalrx.analysis.prompts.hypothesis_agent import PLAIN_REPAIR_PROMPT as _PLAIN_REPAIR_PROMPT
|
|
36
|
+
from evalrx.analysis.prompts.hypothesis_agent import PROPOSE_PROMPT as _PROPOSE_PROMPT
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _is_agent_report(report: dict) -> bool:
|
|
40
|
+
"""True when the exploratory report shows agent-trajectory column families.
|
|
41
|
+
|
|
42
|
+
Detection scans the question, takeaway titles/analysis, and candidate
|
|
43
|
+
signal names/rationales for the marker substrings — the same places the
|
|
44
|
+
propose prompt quotes, so the hint fires exactly when the model will see
|
|
45
|
+
trajectory columns.
|
|
46
|
+
"""
|
|
47
|
+
pieces: list[str] = [str(report.get("question") or "")]
|
|
48
|
+
for t in report.get("takeaways") or []:
|
|
49
|
+
if isinstance(t, dict):
|
|
50
|
+
pieces.append(str(t.get("title", "")))
|
|
51
|
+
pieces.append(str(t.get("analysis", "")))
|
|
52
|
+
for s in report.get("candidate_signals") or []:
|
|
53
|
+
if isinstance(s, dict):
|
|
54
|
+
pieces.append(str(s.get("name", "")))
|
|
55
|
+
pieces.append(str(s.get("display_name", "")))
|
|
56
|
+
pieces.append(str(s.get("rationale", "")))
|
|
57
|
+
text = " ".join(pieces)
|
|
58
|
+
return any(marker in text for marker in _AGENT_COLUMN_MARKERS)
|
|
59
|
+
|
|
60
|
+
if TYPE_CHECKING:
|
|
61
|
+
from evalrx.agent_runtime.cli_types import CliAgentConfig
|
|
62
|
+
from evalrx.core.model import Model
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Hypothesis:
|
|
69
|
+
"""One M3-proposed, falsifiable candidate explanation. Proposed only —
|
|
70
|
+
``test_design`` names how it *could* be checked, not a verdict."""
|
|
71
|
+
|
|
72
|
+
statement: str = ""
|
|
73
|
+
plain_statement: str = ""
|
|
74
|
+
basis: str = ""
|
|
75
|
+
test_design: str = ""
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> dict[str, Any]:
|
|
78
|
+
return {
|
|
79
|
+
"statement": self.statement,
|
|
80
|
+
"plain_statement": self.plain_statement,
|
|
81
|
+
"basis": self.basis,
|
|
82
|
+
"test_design": self.test_design,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class HypothesisAgent:
|
|
87
|
+
"""M3: propose hypotheses from an ``ExploratoryAnalysisReport`` (as a dict).
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
judge: LLM-like object with ``generate(prompt) -> str``.
|
|
91
|
+
cli_config: Optional CLI coding-agent backend (same as
|
|
92
|
+
``ExploratoryAnalysisAgent``'s ``cli_config``); used
|
|
93
|
+
instead of ``judge`` when its provider isn't ``"llm"``.
|
|
94
|
+
timeout_sec: Timeout for a single generation call.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
judge: "Model | None" = None,
|
|
100
|
+
cli_config: "CliAgentConfig | None" = None,
|
|
101
|
+
timeout_sec: int = 90,
|
|
102
|
+
) -> None:
|
|
103
|
+
self._judge = judge
|
|
104
|
+
self._cli_config = cli_config
|
|
105
|
+
self._timeout_sec = timeout_sec
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def available(self) -> bool:
|
|
109
|
+
return self._judge is not None or (
|
|
110
|
+
self._cli_config is not None and self._cli_config.provider != "llm"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def propose(self, report: dict[str, Any], *, max_items: int = 8) -> list[Hypothesis]:
|
|
114
|
+
"""Read *report* (an ``ExploratoryAnalysisReport.to_dict()``-shaped
|
|
115
|
+
dict) and return 0-3 proposed hypotheses. Never raises — a backend
|
|
116
|
+
failure just yields no hypotheses."""
|
|
117
|
+
if not self.available:
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
takeaways = [t for t in report.get("takeaways") or [] if isinstance(t, dict)][:max_items]
|
|
121
|
+
takeaways_text = "\n".join(
|
|
122
|
+
f"- {t.get('title', '')}: {t.get('analysis', '')}" for t in takeaways
|
|
123
|
+
) or "(none recorded)"
|
|
124
|
+
observations = [str(o) for o in report.get("observations") or []][:max_items]
|
|
125
|
+
observations_text = "\n".join(f"- {o}" for o in observations) or "(none recorded)"
|
|
126
|
+
signals = [s for s in report.get("candidate_signals") or [] if isinstance(s, dict)][:max_items]
|
|
127
|
+
signals_text = "\n".join(
|
|
128
|
+
f"- {s.get('display_name') or s.get('name')}: {s.get('rationale', '')}" for s in signals
|
|
129
|
+
) or "(none recorded)"
|
|
130
|
+
|
|
131
|
+
if not takeaways and not observations and not signals:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
prompt = _PROPOSE_PROMPT.format(
|
|
135
|
+
question=report.get("question") or "Explore this dataset.",
|
|
136
|
+
takeaways_text=takeaways_text,
|
|
137
|
+
observations_text=observations_text,
|
|
138
|
+
signals_text=signals_text,
|
|
139
|
+
)
|
|
140
|
+
if _is_agent_report(report):
|
|
141
|
+
prompt += "\n\n" + _AGENT_TRAJECTORY_HINT
|
|
142
|
+
try:
|
|
143
|
+
raw = self._generate(prompt)
|
|
144
|
+
except Exception as exc: # noqa: BLE001
|
|
145
|
+
logger.warning("HypothesisAgent: generation failed: %s", exc)
|
|
146
|
+
return []
|
|
147
|
+
hypotheses = _parse_hypotheses(raw)
|
|
148
|
+
return self._repair_plain_language(raw, hypotheses)
|
|
149
|
+
|
|
150
|
+
def _repair_plain_language(self, raw: str, hypotheses: list[Hypothesis]) -> list[Hypothesis]:
|
|
151
|
+
"""One bounded retry: if any PLAIN line is missing or still jargon-y,
|
|
152
|
+
ask the backend to rewrite just those lines. Never raises — a repair
|
|
153
|
+
failure just keeps the original (possibly jargon-y) hypotheses."""
|
|
154
|
+
if not hypotheses:
|
|
155
|
+
return hypotheses
|
|
156
|
+
violations = [
|
|
157
|
+
f"hypothesis {i}'s PLAIN line {reason}: {h.plain_statement!r}"
|
|
158
|
+
for i, h in enumerate(hypotheses, start=1)
|
|
159
|
+
for reason in [jargon_violation(h.plain_statement, h.statement)]
|
|
160
|
+
if reason
|
|
161
|
+
]
|
|
162
|
+
if not violations:
|
|
163
|
+
return hypotheses
|
|
164
|
+
try:
|
|
165
|
+
repaired_raw = self._generate(
|
|
166
|
+
_PLAIN_REPAIR_PROMPT.format(raw=raw, violations="\n".join(f"- {v}" for v in violations))
|
|
167
|
+
)
|
|
168
|
+
except Exception as exc: # noqa: BLE001
|
|
169
|
+
logger.warning("HypothesisAgent: plain-language repair failed: %s", exc)
|
|
170
|
+
return hypotheses
|
|
171
|
+
repaired = _parse_hypotheses(repaired_raw)
|
|
172
|
+
return repaired or hypotheses
|
|
173
|
+
|
|
174
|
+
def _generate(self, prompt: str) -> str:
|
|
175
|
+
if self._cli_config is not None and self._cli_config.provider != "llm":
|
|
176
|
+
from evalrx.agent_runtime.codegen import CodegenRunner
|
|
177
|
+
|
|
178
|
+
with tempfile.TemporaryDirectory(prefix="evalrx_m3_") as tmp:
|
|
179
|
+
res = CodegenRunner(self._cli_config).run(
|
|
180
|
+
prompt,
|
|
181
|
+
workdir=Path(tmp),
|
|
182
|
+
timeout_sec=self._timeout_sec,
|
|
183
|
+
)
|
|
184
|
+
return res.raw_output or ""
|
|
185
|
+
return str(self._judge.generate(prompt)) # type: ignore[union-attr]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse_hypotheses(raw: str) -> list[Hypothesis]:
|
|
189
|
+
"""Parse ``HYPOTHESIS:``/``PLAIN:``/``BASIS:``/``TEST:`` blocks out of
|
|
190
|
+
*raw*. ``PLAIN:`` is optional (older/repaired responses may omit it),
|
|
191
|
+
in which case ``plain_statement`` stays "".
|
|
192
|
+
|
|
193
|
+
For the CLI-agent backend, *raw* is the full rendered tool-call
|
|
194
|
+
trajectory, not just a final answer (``CliAgentResult.raw_output``'s
|
|
195
|
+
docstring: "assistant text, every Bash/Edit/Write/Read tool call +
|
|
196
|
+
result") — an agent that narrates its plan before giving a final answer
|
|
197
|
+
can restate the same hypothesis twice in one trajectory. Dedupe by
|
|
198
|
+
statement (case-insensitive), keeping the order hypotheses first
|
|
199
|
+
appeared but the most recent basis/test for a repeated statement, since
|
|
200
|
+
later restatements tend to be the more refined ones.
|
|
201
|
+
"""
|
|
202
|
+
if "HYPOTHESIS:" not in raw.upper():
|
|
203
|
+
return []
|
|
204
|
+
by_statement: dict[str, dict[str, str]] = {}
|
|
205
|
+
order: list[str] = []
|
|
206
|
+
cur: dict[str, str] | None = None
|
|
207
|
+
cur_key: str = ""
|
|
208
|
+
|
|
209
|
+
def _flush() -> None:
|
|
210
|
+
if cur and cur.get("statement"):
|
|
211
|
+
if cur_key not in by_statement:
|
|
212
|
+
order.append(cur_key)
|
|
213
|
+
by_statement[cur_key] = cur
|
|
214
|
+
|
|
215
|
+
for line in raw.splitlines():
|
|
216
|
+
line = line.strip()
|
|
217
|
+
upper = line.upper()
|
|
218
|
+
if upper.startswith("HYPOTHESIS:"):
|
|
219
|
+
_flush()
|
|
220
|
+
statement = line.split(":", 1)[1].strip()
|
|
221
|
+
cur = {"statement": statement, "plain_statement": "", "basis": "", "test_design": ""}
|
|
222
|
+
cur_key = statement.lower()
|
|
223
|
+
elif upper.startswith("PLAIN:") and cur is not None:
|
|
224
|
+
cur["plain_statement"] = line.split(":", 1)[1].strip()
|
|
225
|
+
elif upper.startswith("BASIS:") and cur is not None:
|
|
226
|
+
cur["basis"] = line.split(":", 1)[1].strip()
|
|
227
|
+
elif upper.startswith("TEST:") and cur is not None:
|
|
228
|
+
cur["test_design"] = line.split(":", 1)[1].strip()
|
|
229
|
+
_flush()
|
|
230
|
+
return [Hypothesis(**by_statement[key]) for key in order]
|