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,208 @@
|
|
|
1
|
+
"""Shared run-provenance and logging-wiring helpers used by both
|
|
2
|
+
``VLDiagnoseLoop`` (loop.py) and ``AutoDiagnoseLoop`` (legacy.py).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _attach_run_logger(run_logger: Any, *agents: Any) -> None:
|
|
14
|
+
"""Point each agent's ``run_logger`` at *run_logger* when it is unset.
|
|
15
|
+
|
|
16
|
+
Lets the probe / stats generators emit ``tool_codegen`` events without the
|
|
17
|
+
caller having to wire the logger into every agent by hand. Respects an
|
|
18
|
+
explicitly-set logger (only fills in ``None``).
|
|
19
|
+
"""
|
|
20
|
+
if run_logger is None:
|
|
21
|
+
return
|
|
22
|
+
for agent in agents:
|
|
23
|
+
if agent is not None and getattr(agent, "run_logger", None) is None:
|
|
24
|
+
try:
|
|
25
|
+
agent.run_logger = run_logger
|
|
26
|
+
except Exception: # noqa: BLE001 - never let logging wiring break a run
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _data_provenance(data: Any) -> dict[str, Any]:
|
|
31
|
+
"""Fingerprint the case batch + tally its labels for the ``run_start`` record.
|
|
32
|
+
|
|
33
|
+
Returns ``{"data_fingerprint": <12-hex>, "label_distribution": {...}}`` — the
|
|
34
|
+
fingerprint is a SHA-1 over each case's ``id`` (or its prompt when no id),
|
|
35
|
+
order-independent, so the same batch always hashes the same regardless of
|
|
36
|
+
iteration order, and a different batch (even same size) hashes differently.
|
|
37
|
+
Best-effort: returns ``{}`` if *data* isn't iterable.
|
|
38
|
+
"""
|
|
39
|
+
import hashlib
|
|
40
|
+
from collections import Counter
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
cases = list(data)
|
|
44
|
+
except Exception: # noqa: BLE001
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
keys: list[str] = []
|
|
48
|
+
labels: Counter = Counter()
|
|
49
|
+
for c in cases:
|
|
50
|
+
cid = getattr(c, "id", None)
|
|
51
|
+
if not cid:
|
|
52
|
+
prompt = getattr(getattr(c, "inputs", None), "prompt", None)
|
|
53
|
+
cid = str(prompt) if prompt is not None else repr(c)
|
|
54
|
+
keys.append(str(cid))
|
|
55
|
+
label = getattr(c, "label", None)
|
|
56
|
+
labels[getattr(label, "name", "UNKNOWN")] += 1
|
|
57
|
+
|
|
58
|
+
out: dict[str, Any] = {}
|
|
59
|
+
if keys:
|
|
60
|
+
digest = hashlib.sha1("\n".join(sorted(keys)).encode("utf-8")).hexdigest()
|
|
61
|
+
out["data_fingerprint"] = digest[:12]
|
|
62
|
+
if labels:
|
|
63
|
+
out["label_distribution"] = dict(labels)
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _run_config(loop: Any, data: Any, *, loop_name: str) -> dict[str, Any]:
|
|
68
|
+
"""Build the ``run_start`` provenance dict from a loop instance + its data.
|
|
69
|
+
|
|
70
|
+
Robust to both loop types and missing agents — every field is best-effort so
|
|
71
|
+
a partially-configured loop still produces a useful config record.
|
|
72
|
+
"""
|
|
73
|
+
cfg: dict[str, Any] = {"loop": loop_name}
|
|
74
|
+
try:
|
|
75
|
+
cfg["model"] = repr(loop.model)
|
|
76
|
+
except Exception: # noqa: BLE001
|
|
77
|
+
pass
|
|
78
|
+
cfg["max_cycles"] = getattr(loop, "max_cycles", None)
|
|
79
|
+
cfg["token_budget"] = getattr(loop, "token_budget", None)
|
|
80
|
+
cfg["analysis_only"] = getattr(loop, "analysis_only", None)
|
|
81
|
+
try:
|
|
82
|
+
cfg["n_cases"] = len(data)
|
|
83
|
+
except Exception: # noqa: BLE001
|
|
84
|
+
pass
|
|
85
|
+
# Dataset provenance: a stable fingerprint over the cases plus their label
|
|
86
|
+
# breakdown. n_cases alone says how many; this says *which* (so two runs
|
|
87
|
+
# can be confirmed to use the same batch) and the base failure rate the
|
|
88
|
+
# whole diagnosis is conditioned on — both essential to interpret the run.
|
|
89
|
+
_data_prov = _data_provenance(data)
|
|
90
|
+
if _data_prov:
|
|
91
|
+
cfg.update(_data_prov)
|
|
92
|
+
# How the batch was partitioned before anything ran. ``n_cases`` above is
|
|
93
|
+
# the explore partition (``data`` has already been narrowed to it); these
|
|
94
|
+
# say whether a confirm / test partition was withheld and at what fraction,
|
|
95
|
+
# so a report can tell a two-way run from a train/val/test one.
|
|
96
|
+
for attr in ("confirm_split", "test_split", "confirm_split_seed"):
|
|
97
|
+
value = getattr(loop, attr, None)
|
|
98
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
99
|
+
cfg[attr] = value
|
|
100
|
+
|
|
101
|
+
protocol = getattr(loop, "protocol", None)
|
|
102
|
+
if protocol is not None:
|
|
103
|
+
cfg["protocol"] = {
|
|
104
|
+
"description": getattr(protocol, "description", ""),
|
|
105
|
+
"task_domain": getattr(protocol, "task_domain", None),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Judge — read off the M3 agent (or its lazy default) when present.
|
|
109
|
+
diag_agent = getattr(loop, "diagnosis_agent", None)
|
|
110
|
+
judge = getattr(diag_agent, "judge", None) if diag_agent is not None else None
|
|
111
|
+
if judge is not None:
|
|
112
|
+
cfg["judge"] = repr(judge)
|
|
113
|
+
|
|
114
|
+
# Coder — the M5 surgery writer's CLI provider/model, when configured.
|
|
115
|
+
surgery = getattr(loop, "surgery_agent", None)
|
|
116
|
+
writer = getattr(surgery, "_writer", None) if surgery is not None else None
|
|
117
|
+
cli = getattr(getattr(writer, "_cfg", None), "cli_agent", None)
|
|
118
|
+
if cli is not None:
|
|
119
|
+
provider = getattr(cli, "provider", None)
|
|
120
|
+
model = getattr(cli, "model", "")
|
|
121
|
+
if provider:
|
|
122
|
+
cfg["coder"] = f"{provider}:{model}" if model else provider
|
|
123
|
+
|
|
124
|
+
# Whether tool synthesis (codegen) is enabled on either agent.
|
|
125
|
+
cfg["allow_codegen"] = bool(
|
|
126
|
+
getattr(getattr(loop, "probe_agent", None), "allow_codegen", False)
|
|
127
|
+
or getattr(getattr(loop, "stats_agent", None), "_allow_codegen", False)
|
|
128
|
+
or getattr(getattr(loop, "fix_agent", None), "_allow_codegen", False)
|
|
129
|
+
)
|
|
130
|
+
# Whether the in-cycle explore step (free-form EDA beside M2) is configured —
|
|
131
|
+
# so a run with explorer notes in M3 is distinguishable from one without.
|
|
132
|
+
explorer = getattr(loop, "explorer", None)
|
|
133
|
+
if explorer is not None:
|
|
134
|
+
cli = getattr(explorer, "_cli_config", None)
|
|
135
|
+
provider = getattr(cli, "provider", None)
|
|
136
|
+
model = getattr(cli, "model", "")
|
|
137
|
+
cfg["explorer"] = (
|
|
138
|
+
(f"{provider}:{model}" if model else provider) if provider
|
|
139
|
+
else type(explorer).__name__
|
|
140
|
+
)
|
|
141
|
+
return cfg
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _log_generated_tools(run_logger: Any, cycle: int, module: str, agent: Any) -> None:
|
|
145
|
+
"""Snapshot an agent's active generated-tool registry for *cycle*.
|
|
146
|
+
|
|
147
|
+
Reads ``_generated_probes`` (list of ``(generator, probe)`` tuples) for the
|
|
148
|
+
probe agent or ``_generated_tools`` (list of tools) for the stats agent and
|
|
149
|
+
forwards the bare tool objects to :meth:`RunLogger.log_tool_registry`.
|
|
150
|
+
Best-effort: silently does nothing for agents without those attributes.
|
|
151
|
+
"""
|
|
152
|
+
if agent is None:
|
|
153
|
+
return
|
|
154
|
+
raw = getattr(agent, "_generated_probes", None)
|
|
155
|
+
if raw is None:
|
|
156
|
+
raw = getattr(agent, "_generated_tools", None)
|
|
157
|
+
if not raw:
|
|
158
|
+
return
|
|
159
|
+
tools = [t[1] if isinstance(t, tuple) else t for t in raw]
|
|
160
|
+
try:
|
|
161
|
+
run_logger.log_tool_registry(cycle, module, tools)
|
|
162
|
+
except Exception as exc: # noqa: BLE001
|
|
163
|
+
logger.debug("log_tool_registry failed: %s", exc)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _coerce_explore_context(value: "Any | None") -> "Any | None":
|
|
167
|
+
"""Normalise an explore_report argument into an ExploreContext or None.
|
|
168
|
+
|
|
169
|
+
Accepts an ``ExploreContext``, a report dict (e.g. ``fused_report.json``),
|
|
170
|
+
or ``None``. Never raises — a malformed value degrades to ``None``."""
|
|
171
|
+
if value is None:
|
|
172
|
+
return None
|
|
173
|
+
try:
|
|
174
|
+
from evalrx.eval_agent.stages.diagnosis import ExploreContext
|
|
175
|
+
|
|
176
|
+
if isinstance(value, ExploreContext):
|
|
177
|
+
return None if value.is_empty else value
|
|
178
|
+
if isinstance(value, dict):
|
|
179
|
+
return ExploreContext.from_report(value)
|
|
180
|
+
except Exception as exc: # explore context is optional, never fatal
|
|
181
|
+
logger.warning("could not build ExploreContext from explore_report: %s", exc)
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Internal adapter: convert HypothesisTestResult → InterventionResult-like
|
|
187
|
+
# object for RunLogger.log_surgery (which expects an InterventionResult).
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def _make_intervention_result_from_test(tr: "Any") -> Any:
|
|
191
|
+
"""Wrap a HypothesisTestResult as a minimal InterventionResult."""
|
|
192
|
+
from evalrx.eval_agent.stages.surgery import InterventionResult
|
|
193
|
+
|
|
194
|
+
return InterventionResult(
|
|
195
|
+
hypothesis=tr.hypothesis,
|
|
196
|
+
status=tr.status,
|
|
197
|
+
fixed=False,
|
|
198
|
+
evidence={
|
|
199
|
+
"m4_test_name": tr.test_name,
|
|
200
|
+
"m4_effect_size": tr.effect_size,
|
|
201
|
+
"m4_confidence": tr.confidence,
|
|
202
|
+
"m4_protocol_consistent": tr.is_consistent_with_protocol,
|
|
203
|
+
"m4_verdict": tr.verdict,
|
|
204
|
+
"m4_evidence_grade": tr.evidence_grade,
|
|
205
|
+
**tr.evidence,
|
|
206
|
+
},
|
|
207
|
+
confidence_score=tr.confidence,
|
|
208
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Stage implementations for the M1–M4 diagnostic pipeline.
|
|
2
|
+
|
|
3
|
+
Each module is a self-contained stage that can be injected into either loop:
|
|
4
|
+
|
|
5
|
+
AutoDiagnoseLoop uses M1 → M2 → M3 → M5
|
|
6
|
+
VLDiagnoseLoop uses M1 → M2 → M3 → M4 (inner), M5 called post-loop
|
|
7
|
+
|
|
8
|
+
M1 — Probe (analyzer selection + execution)
|
|
9
|
+
probe.py StrategyProbe detect model kind; rank analyzers by failure-mode hints
|
|
10
|
+
probe_agent.py ProbeAgent execute ranked analyzers; route to Docker if needed;
|
|
11
|
+
protocol-guided via LLM analyzer selection
|
|
12
|
+
protocol.py ExperimentProtocol user's NL description of what to investigate;
|
|
13
|
+
ProbingSchema records which analyzers ran and why (M2/M4 trace)
|
|
14
|
+
|
|
15
|
+
Data — Case discovery / labeling
|
|
16
|
+
case_discovery.py CaseDiscoveryAgent run candidate prompts through the model,
|
|
17
|
+
store observed outputs, and label PASS/FAIL
|
|
18
|
+
|
|
19
|
+
M2 — Analysis (statistical interpretation)
|
|
20
|
+
Lives in ``evalrx.analysis`` (not this package) so it is usable standalone:
|
|
21
|
+
analysis_module.py AnalysisModule threshold rules + VLM derived metrics → AnalysisReport
|
|
22
|
+
stats_agent.py StatsAnalysisAgent protocol-aware extension of AnalysisModule;
|
|
23
|
+
adds LLM-written conclusion + evidence chain
|
|
24
|
+
StatsAnalysisReport AnalysisReport subclass — adds conclusion,
|
|
25
|
+
evidence_chain, qualitative_findings, protocol
|
|
26
|
+
stats_tool_agent.py StatsToolAgent selects/runs deterministic exploratory
|
|
27
|
+
stats tools and visualization specs
|
|
28
|
+
stats_tools.py STATS_TOOL_CATALOG signal/label association, McNemar+e-value,
|
|
29
|
+
Friedman, single-rate e-value, rank corr
|
|
30
|
+
stats_tool_generator.py StatsToolGenerator LLM/CLI writes a new stats script when
|
|
31
|
+
no catalog tool fits
|
|
32
|
+
|
|
33
|
+
M3 — Diagnosis (hypothesis generation)
|
|
34
|
+
diagnosis.py DiagnosisAgent LLM judge reads AnalysisReport → Hypothesis list;
|
|
35
|
+
falls back to threshold-derived hypotheses on NO_ISSUE
|
|
36
|
+
|
|
37
|
+
M5 — Surgery (fix proposal + execution)
|
|
38
|
+
surgery.py SurgeryAgent four strategies: verify_fn → analyzer_params →
|
|
39
|
+
ExperimentWriter → label correlation
|
|
40
|
+
experiment_writer.py ExperimentWriter multi-phase LLM/CLI agent writes + runs
|
|
41
|
+
diagnostic Python projects in a sandbox
|
|
42
|
+
|
|
43
|
+
M4 — Hypothesis testing (statistical + protocol consistency gate)
|
|
44
|
+
hypothesis_tester.py HypothesisTester per-case signal extraction → fail-rate comparison
|
|
45
|
+
→ optional LLM protocol consistency check;
|
|
46
|
+
stopping_criteria_met() returns True when at
|
|
47
|
+
least one hypothesis is SUPPORTED + consistent
|
|
48
|
+
|
|
49
|
+
Cross-stage shared types (top level, not in this package):
|
|
50
|
+
hypothesis.py Hypothesis, HypothesisStatus used by M3, M4, M5
|
|
51
|
+
|
|
52
|
+
Shared CLI-agent runtime (evalrx.agent_runtime, not in this package):
|
|
53
|
+
codegen/ CodegenRunner shared CLI code-generation orchestration
|
|
54
|
+
providers/ CLI coding-provider adapters (agy/codex/claude/etc.)
|
|
55
|
+
judges/ CLI-backed judge model wrappers
|
|
56
|
+
"""
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Case discovery and labeling for automated VL diagnosis loops.
|
|
2
|
+
|
|
3
|
+
This stage turns candidate prompts into labeled cases by running the target
|
|
4
|
+
model, storing its observed answer, and scoring the result. It is intentionally
|
|
5
|
+
small: dataset loading and prompt generation can live outside this class, while
|
|
6
|
+
M4 receives the PASS/FAIL labels it needs for statistical testing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable
|
|
13
|
+
|
|
14
|
+
from evalrx.core.case import CaseBatch, FailureCase, Label
|
|
15
|
+
from evalrx.eval_agent.prompts.case_discovery import _JUDGE_PROMPT
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from evalrx.core.model import Model
|
|
19
|
+
from evalrx.eval_agent.stages.protocol import ExperimentProtocol
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
ScoreFn = Callable[[FailureCase, str], Label | bool | str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CaseDiscoveryReport:
|
|
27
|
+
"""Summary of one discovery pass."""
|
|
28
|
+
|
|
29
|
+
cases: CaseBatch
|
|
30
|
+
n_pass: int = 0
|
|
31
|
+
n_fail: int = 0
|
|
32
|
+
n_unknown: int = 0
|
|
33
|
+
errors: list[str] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def has_m4_groups(self) -> bool:
|
|
37
|
+
"""True when M4 has both failure and control examples."""
|
|
38
|
+
return self.n_fail > 0 and self.n_pass > 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CaseDiscoveryAgent:
|
|
42
|
+
"""Run candidate cases through a model and label PASS/FAIL outcomes.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
scorer: Deterministic scoring function. It receives the
|
|
46
|
+
case and observed answer and returns ``Label``, bool,
|
|
47
|
+
or a label string.
|
|
48
|
+
judge: Optional LLM judge used when no scorer is supplied.
|
|
49
|
+
generation_kwargs: Extra kwargs forwarded to ``model.generate``.
|
|
50
|
+
include_unknown: Keep UNKNOWN cases in the returned batch.
|
|
51
|
+
concurrency: How many cases may be generated at once. Only the
|
|
52
|
+
``model.generate`` calls fan out; scoring and
|
|
53
|
+
labelling stay sequential and in input order, so the
|
|
54
|
+
report is identical whatever this is set to.
|
|
55
|
+
|
|
56
|
+
Values above 1 are honoured ONLY for an API-backed
|
|
57
|
+
handle, following the same rule as
|
|
58
|
+
:func:`evalrx.models.agent.run_batch`: HTTP is
|
|
59
|
+
reentrant, a local backend sharing one GPU is not.
|
|
60
|
+
Against a local ``vllm serve`` this is the
|
|
61
|
+
difference between one request in flight and a
|
|
62
|
+
batch -- the server can only batch what it has been
|
|
63
|
+
given.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
scorer: ScoreFn | None = None,
|
|
69
|
+
judge: "Model | None" = None,
|
|
70
|
+
generation_kwargs: dict[str, Any] | None = None,
|
|
71
|
+
include_unknown: bool = True,
|
|
72
|
+
concurrency: int = 1,
|
|
73
|
+
) -> None:
|
|
74
|
+
self.scorer = scorer
|
|
75
|
+
self.judge = judge
|
|
76
|
+
self.generation_kwargs = generation_kwargs or {}
|
|
77
|
+
self.include_unknown = include_unknown
|
|
78
|
+
self.concurrency = max(1, int(concurrency))
|
|
79
|
+
|
|
80
|
+
def discover(
|
|
81
|
+
self,
|
|
82
|
+
model: "Model",
|
|
83
|
+
candidates: Iterable[FailureCase] | CaseBatch,
|
|
84
|
+
*,
|
|
85
|
+
protocol: "ExperimentProtocol | None" = None,
|
|
86
|
+
) -> CaseDiscoveryReport:
|
|
87
|
+
"""Return a labeled batch created from candidate cases."""
|
|
88
|
+
labeled: list[FailureCase] = []
|
|
89
|
+
errors: list[str] = []
|
|
90
|
+
counts = {Label.PASS: 0, Label.FAIL: 0, Label.UNKNOWN: 0}
|
|
91
|
+
cases = list(candidates)
|
|
92
|
+
|
|
93
|
+
for case, observed, gen_error in self._generate(model, cases):
|
|
94
|
+
try:
|
|
95
|
+
if gen_error is not None:
|
|
96
|
+
raise gen_error
|
|
97
|
+
case.observed = observed
|
|
98
|
+
label, reason = self._score_with_reason(case, str(observed), protocol)
|
|
99
|
+
except Exception as exc: # noqa: BLE001 - discovery should continue
|
|
100
|
+
observed = ""
|
|
101
|
+
label = Label.UNKNOWN
|
|
102
|
+
reason = "model generation or scoring raised an exception"
|
|
103
|
+
errors.append(f"{case.id}: {exc}")
|
|
104
|
+
case.metadata["discovery_error"] = str(exc)
|
|
105
|
+
|
|
106
|
+
case.label = label
|
|
107
|
+
case.metadata["discovery_observed"] = str(observed)
|
|
108
|
+
case.metadata["discovery_label"] = label.value
|
|
109
|
+
case.metadata["discovery_reason"] = reason
|
|
110
|
+
counts[label] += 1
|
|
111
|
+
|
|
112
|
+
if label != Label.UNKNOWN or self.include_unknown:
|
|
113
|
+
labeled.append(case)
|
|
114
|
+
|
|
115
|
+
return CaseDiscoveryReport(
|
|
116
|
+
cases=CaseBatch(labeled),
|
|
117
|
+
n_pass=counts[Label.PASS],
|
|
118
|
+
n_fail=counts[Label.FAIL],
|
|
119
|
+
n_unknown=counts[Label.UNKNOWN],
|
|
120
|
+
errors=errors,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _generate(
|
|
124
|
+
self, model: "Model", cases: list[FailureCase]
|
|
125
|
+
) -> "list[tuple[FailureCase, Any, Exception | None]]":
|
|
126
|
+
"""``(case, observed, error)`` per case, in input order.
|
|
127
|
+
|
|
128
|
+
The exception is carried rather than raised so the caller keeps its one
|
|
129
|
+
place that turns any failure into an UNKNOWN label -- a case whose
|
|
130
|
+
generation failed and a case whose scoring failed are the same
|
|
131
|
+
observation, and both must leave the batch intact.
|
|
132
|
+
"""
|
|
133
|
+
workers = self._workers(model, len(cases))
|
|
134
|
+
if workers <= 1:
|
|
135
|
+
out = []
|
|
136
|
+
for case in cases:
|
|
137
|
+
try:
|
|
138
|
+
out.append((case, model.generate(case.inputs, **self.generation_kwargs), None))
|
|
139
|
+
except Exception as exc: # noqa: BLE001
|
|
140
|
+
out.append((case, None, exc))
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
144
|
+
|
|
145
|
+
def _one(case: FailureCase):
|
|
146
|
+
try:
|
|
147
|
+
return case, model.generate(case.inputs, **self.generation_kwargs), None
|
|
148
|
+
except Exception as exc: # noqa: BLE001
|
|
149
|
+
return case, None, exc
|
|
150
|
+
|
|
151
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
152
|
+
return list(pool.map(_one, cases))
|
|
153
|
+
|
|
154
|
+
def _workers(self, model: "Model", n_cases: int) -> int:
|
|
155
|
+
"""Concurrency actually allowed for this handle.
|
|
156
|
+
|
|
157
|
+
A local backend is silently forced back to 1 rather than warned about:
|
|
158
|
+
unlike ``run_batch``, nothing here asked for parallelism per case --
|
|
159
|
+
the caller set one number for a whole run that may mix handles, and a
|
|
160
|
+
warning per discovery pass would be noise about a setting that is
|
|
161
|
+
behaving correctly.
|
|
162
|
+
"""
|
|
163
|
+
if self.concurrency <= 1:
|
|
164
|
+
return 1
|
|
165
|
+
from evalrx.models.backends.api import APIModel
|
|
166
|
+
|
|
167
|
+
# A deployed-pipeline wrapper (fix_tools.SpecPipelineModel) is exactly
|
|
168
|
+
# as reentrant as the handle it drives — judge it by its inner model.
|
|
169
|
+
model = getattr(model, "inner_model", model)
|
|
170
|
+
# InstrumentedModel is also a transparent proxy; preserve the API
|
|
171
|
+
# handle's configured discovery concurrency while recording each call.
|
|
172
|
+
model = getattr(model, "_model", model)
|
|
173
|
+
if not isinstance(model, APIModel):
|
|
174
|
+
return 1
|
|
175
|
+
return min(self.concurrency, max(1, n_cases))
|
|
176
|
+
|
|
177
|
+
def _score_with_reason(
|
|
178
|
+
self,
|
|
179
|
+
case: FailureCase,
|
|
180
|
+
observed: str,
|
|
181
|
+
protocol: "ExperimentProtocol | None",
|
|
182
|
+
) -> tuple[Label, str]:
|
|
183
|
+
if self.scorer is not None:
|
|
184
|
+
label = _coerce_label(self.scorer(case, observed))
|
|
185
|
+
return label, "scored by injected scorer"
|
|
186
|
+
if self.judge is not None:
|
|
187
|
+
judged, reason = self._judge_score(case, observed, protocol)
|
|
188
|
+
if judged != Label.UNKNOWN:
|
|
189
|
+
return judged, reason
|
|
190
|
+
fallback = _heuristic_score(case.expected, observed)
|
|
191
|
+
return fallback, f"judge returned UNKNOWN; heuristic fallback produced {fallback.value}"
|
|
192
|
+
label = _heuristic_score(case.expected, observed)
|
|
193
|
+
return label, "scored by expected-answer heuristic"
|
|
194
|
+
|
|
195
|
+
def _judge_score(
|
|
196
|
+
self,
|
|
197
|
+
case: FailureCase,
|
|
198
|
+
observed: str,
|
|
199
|
+
protocol: "ExperimentProtocol | None",
|
|
200
|
+
) -> tuple[Label, str]:
|
|
201
|
+
protocol_text = protocol.description if protocol is not None else ""
|
|
202
|
+
success_criteria = protocol.success_criteria if protocol is not None else ""
|
|
203
|
+
prompt = _JUDGE_PROMPT.format(
|
|
204
|
+
protocol=protocol_text,
|
|
205
|
+
success_criteria=success_criteria,
|
|
206
|
+
prompt=case.inputs.prompt,
|
|
207
|
+
expected=case.expected,
|
|
208
|
+
observed=observed,
|
|
209
|
+
)
|
|
210
|
+
raw = self.judge.generate(prompt) # type: ignore[union-attr]
|
|
211
|
+
text = str(raw).strip()
|
|
212
|
+
parsed = _parse_judge_json(text)
|
|
213
|
+
if parsed is not None:
|
|
214
|
+
return parsed
|
|
215
|
+
first_lines = "\n".join(text.splitlines()[:5]).upper()
|
|
216
|
+
if "PASS" in first_lines and "FAIL" not in first_lines:
|
|
217
|
+
return Label.PASS, "judge text contained PASS"
|
|
218
|
+
if "FAIL" in first_lines and "PASS" not in first_lines:
|
|
219
|
+
return Label.FAIL, "judge text contained FAIL"
|
|
220
|
+
first = text.splitlines()[0].strip().upper() if text else ""
|
|
221
|
+
if first.startswith("PASS"):
|
|
222
|
+
return Label.PASS, "judge first line was PASS"
|
|
223
|
+
if first.startswith("FAIL"):
|
|
224
|
+
return Label.FAIL, "judge first line was FAIL"
|
|
225
|
+
return Label.UNKNOWN, "judge output did not contain a parseable label"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _coerce_label(value: Label | bool | str) -> Label:
|
|
229
|
+
if isinstance(value, Label):
|
|
230
|
+
return value
|
|
231
|
+
if isinstance(value, bool):
|
|
232
|
+
return Label.PASS if value else Label.FAIL
|
|
233
|
+
text = str(value).strip().lower()
|
|
234
|
+
if text in {"pass", "passed", "true", "correct", "ok"}:
|
|
235
|
+
return Label.PASS
|
|
236
|
+
if text in {"fail", "failed", "false", "incorrect", "wrong"}:
|
|
237
|
+
return Label.FAIL
|
|
238
|
+
return Label.UNKNOWN
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _parse_judge_json(text: str) -> tuple[Label, str] | None:
|
|
242
|
+
raw = text.strip()
|
|
243
|
+
if raw.startswith("```"):
|
|
244
|
+
raw = re.sub(r"^```\w*\n?", "", raw)
|
|
245
|
+
raw = re.sub(r"\n?```\s*$", "", raw)
|
|
246
|
+
match = re.search(r"\{.*\}", raw, flags=re.DOTALL)
|
|
247
|
+
if not match:
|
|
248
|
+
return None
|
|
249
|
+
try:
|
|
250
|
+
payload = json.loads(match.group())
|
|
251
|
+
except json.JSONDecodeError:
|
|
252
|
+
return None
|
|
253
|
+
label = _coerce_label(payload.get("label", "unknown"))
|
|
254
|
+
reason = str(payload.get("reason", "judge returned structured label"))
|
|
255
|
+
return label, reason
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _heuristic_score(expected: Any, observed: str) -> Label:
|
|
259
|
+
"""Score simple expected-answer rubrics without an LLM judge.
|
|
260
|
+
|
|
261
|
+
Supported expected formats:
|
|
262
|
+
- string: substring match after normalization
|
|
263
|
+
- list/tuple/set: every item must appear
|
|
264
|
+
- dict: ``all_of``, ``any_of``, and ``none_of`` substring constraints
|
|
265
|
+
"""
|
|
266
|
+
if expected is None:
|
|
267
|
+
return Label.UNKNOWN
|
|
268
|
+
|
|
269
|
+
obs = _normalize(observed)
|
|
270
|
+
if isinstance(expected, str):
|
|
271
|
+
return Label.PASS if _normalize(expected) in obs else Label.FAIL
|
|
272
|
+
|
|
273
|
+
if isinstance(expected, (list, tuple, set)):
|
|
274
|
+
terms = [_normalize(x) for x in expected]
|
|
275
|
+
return Label.PASS if all(t in obs for t in terms if t) else Label.FAIL
|
|
276
|
+
|
|
277
|
+
if isinstance(expected, dict):
|
|
278
|
+
all_of = [_normalize(x) for x in expected.get("all_of", [])]
|
|
279
|
+
any_of = [_normalize(x) for x in expected.get("any_of", [])]
|
|
280
|
+
none_of = [_normalize(x) for x in expected.get("none_of", [])]
|
|
281
|
+
if any(t and t in obs for t in none_of):
|
|
282
|
+
return Label.FAIL
|
|
283
|
+
if all_of and not all(t in obs for t in all_of if t):
|
|
284
|
+
return Label.FAIL
|
|
285
|
+
if any_of and not any(t in obs for t in any_of if t):
|
|
286
|
+
return Label.FAIL
|
|
287
|
+
return Label.PASS
|
|
288
|
+
|
|
289
|
+
return Label.UNKNOWN
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _normalize(text: Any) -> str:
|
|
293
|
+
return re.sub(r"\s+", " ", str(text).lower()).strip()
|