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,351 @@
|
|
|
1
|
+
"""M1 tier (b), white-box — capture-then-compute probe generation.
|
|
2
|
+
|
|
3
|
+
Black-box generated probes (:mod:`probe_generator`) only see the model's text
|
|
4
|
+
outputs. Mechanism hypotheses from M3 ("structural tokens act as attention
|
|
5
|
+
sinks", "attention never leaves the text region", …) need **internal** evidence
|
|
6
|
+
— but the loaded model cannot be shipped into a sandbox. The split here keeps
|
|
7
|
+
the M2-tier(b) safety model intact:
|
|
8
|
+
|
|
9
|
+
1. **Host captures** (trusted code): one ``model.forward`` per case with
|
|
10
|
+
``Capability.ATTENTION``; the heavy trace is immediately reduced to a small
|
|
11
|
+
standard bundle — head-averaged attention from the last query position for
|
|
12
|
+
every layer ``(n_layers, seq)`` float16 — plus the token strings and the
|
|
13
|
+
image-token mask, dumped to the sandbox workdir.
|
|
14
|
+
2. **Sandboxed compute** (generated code): an LLM/CLI-written numpy script
|
|
15
|
+
reads the dump, computes a per-case scalar/boolean signal for the
|
|
16
|
+
hypothesised mechanism, and prints a strict ``PROBE_RESULT_JSON=`` line.
|
|
17
|
+
3. The parsed findings become a normal :class:`~evalrx.core.result.Result`
|
|
18
|
+
whose ``per_case`` entries flow into M2's stats layer and M4's routing.
|
|
19
|
+
|
|
20
|
+
Dump layout (documented verbatim in the generation prompt):
|
|
21
|
+
|
|
22
|
+
m1_whitebox_manifest.json {"cases": [{"id", "label", "seq_len",
|
|
23
|
+
"n_layers", "tokens": [...]}, ...]}
|
|
24
|
+
m1_whitebox/<case_id>.npz attn_last: (n_layers, seq) float16
|
|
25
|
+
image_token_mask: (seq,) bool
|
|
26
|
+
|
|
27
|
+
Scope (v1): read-only statistics over the captured attention bundle. Causal
|
|
28
|
+
interventions (activation patching, attention knockout) need in-process
|
|
29
|
+
execution with the model handle and are deliberately out of scope.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import logging
|
|
36
|
+
import re
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
40
|
+
|
|
41
|
+
import numpy as np
|
|
42
|
+
|
|
43
|
+
from evalrx.agent_runtime.sandbox import ExperimentSandbox
|
|
44
|
+
from evalrx.core.capability import Capability
|
|
45
|
+
from evalrx.core.result import Result
|
|
46
|
+
from evalrx.eval_agent.prompts.whitebox_probe_generator import (
|
|
47
|
+
_DUMP_DIR,
|
|
48
|
+
_GENERATE_PROMPT,
|
|
49
|
+
_MANIFEST,
|
|
50
|
+
_RESULT_MARKER,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from evalrx.agent_runtime.cli_types import CliAgentConfig
|
|
55
|
+
from evalrx.core.case import CaseBatch
|
|
56
|
+
from evalrx.core.model import Model
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class GeneratedWhiteboxProbe:
|
|
64
|
+
"""A validated, sandbox-executed white-box probe (re-runnable on new cases)."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
code: str
|
|
68
|
+
need: str = ""
|
|
69
|
+
source: str = ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class WhiteboxProbeGenerator:
|
|
73
|
+
"""Generate and run capture-then-compute attention probes.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
judge: LLM for the single-pass code-writing path.
|
|
77
|
+
cli_config: CLI coding-agent config (used before the judge when set).
|
|
78
|
+
sandbox: Execution sandbox (fresh temp dir when ``None``).
|
|
79
|
+
timeout_sec: Wall-clock limit per sandbox run.
|
|
80
|
+
max_cases: Cap on captured cases (one ATTENTION forward each); 0 (the default) = every case.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
judge: "Model | None" = None,
|
|
86
|
+
cli_config: "CliAgentConfig | None" = None,
|
|
87
|
+
sandbox: "ExperimentSandbox | None" = None,
|
|
88
|
+
timeout_sec: int = 60,
|
|
89
|
+
max_cases: int = 0,
|
|
90
|
+
run_logger: "Any | None" = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
self._judge = judge
|
|
93
|
+
self._cli_config = cli_config
|
|
94
|
+
self._timeout_sec = timeout_sec
|
|
95
|
+
self._max_cases = max_cases
|
|
96
|
+
self._sandbox = sandbox or ExperimentSandbox()
|
|
97
|
+
# Optional RunLogger — records each attention-probe code-writing attempt.
|
|
98
|
+
self.run_logger = run_logger
|
|
99
|
+
self._last_prompt: str = ""
|
|
100
|
+
self._last_raw: str = ""
|
|
101
|
+
self._last_raw_stream: str = ""
|
|
102
|
+
self._last_usage: dict | None = None
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def available(self) -> bool:
|
|
106
|
+
return self._judge is not None or (
|
|
107
|
+
self._cli_config is not None and self._cli_config.provider != "llm"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# Public interface
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def generate(
|
|
115
|
+
self,
|
|
116
|
+
need: str,
|
|
117
|
+
model: "Model",
|
|
118
|
+
cases: "CaseBatch",
|
|
119
|
+
name: str = "custom",
|
|
120
|
+
) -> "tuple[Result | None, GeneratedWhiteboxProbe | None]":
|
|
121
|
+
"""Capture internals, write+run a probe over the dump, return (Result, probe)."""
|
|
122
|
+
if not self.available:
|
|
123
|
+
logger.debug("WhiteboxProbeGenerator: no code-writing backend configured")
|
|
124
|
+
return None, None
|
|
125
|
+
if Capability.ATTENTION not in getattr(model, "capabilities", frozenset()):
|
|
126
|
+
logger.debug("WhiteboxProbeGenerator: model lacks ATTENTION capability")
|
|
127
|
+
return None, None
|
|
128
|
+
|
|
129
|
+
n = self._capture(model, cases)
|
|
130
|
+
if n == 0:
|
|
131
|
+
logger.warning("WhiteboxProbeGenerator: no case could be captured")
|
|
132
|
+
return None, None
|
|
133
|
+
|
|
134
|
+
self._last_prompt = ""
|
|
135
|
+
self._last_raw = ""
|
|
136
|
+
self._last_raw_stream = ""
|
|
137
|
+
self._last_usage = None
|
|
138
|
+
try:
|
|
139
|
+
code, source = self._write_code(need)
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
logger.warning("WhiteboxProbeGenerator: code writing failed: %s", exc)
|
|
142
|
+
self._emit_codegen(name, need, "", "", ok=False, error=f"code writing failed: {exc}")
|
|
143
|
+
return None, None
|
|
144
|
+
if not code.strip():
|
|
145
|
+
self._emit_codegen(name, need, source, "", ok=False, error="empty code produced")
|
|
146
|
+
return None, None
|
|
147
|
+
|
|
148
|
+
result = self._run_code(code, name, model, cases)
|
|
149
|
+
self._emit_codegen(
|
|
150
|
+
name, need, source, code, ok=result is not None,
|
|
151
|
+
error="" if result is not None else "sandbox produced no parseable result",
|
|
152
|
+
)
|
|
153
|
+
if result is None:
|
|
154
|
+
return None, None
|
|
155
|
+
probe = GeneratedWhiteboxProbe(name=name, code=code, need=need, source=source)
|
|
156
|
+
return result, probe
|
|
157
|
+
|
|
158
|
+
def _emit_codegen(
|
|
159
|
+
self, name: str, need: str, source: str, code: str, *, ok: bool, error: str = ""
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Record one attention-probe code-writing attempt to the RunLogger."""
|
|
162
|
+
if self.run_logger is None:
|
|
163
|
+
return
|
|
164
|
+
extra = ({"cli_usage": self._last_usage}
|
|
165
|
+
if source.startswith("cli:") and self._last_usage else None)
|
|
166
|
+
try:
|
|
167
|
+
self.run_logger.log_tool_codegen(
|
|
168
|
+
module="m1_whitebox", name=name, need=need, source=source, ok=ok,
|
|
169
|
+
code=code, prompt=self._last_prompt, raw_output=self._last_raw,
|
|
170
|
+
raw_stream=self._last_raw_stream, error=error,
|
|
171
|
+
extra=extra,
|
|
172
|
+
)
|
|
173
|
+
except Exception as exc: # logging must never break generation
|
|
174
|
+
logger.debug("WhiteboxProbeGenerator: log_tool_codegen failed: %s", exc)
|
|
175
|
+
|
|
176
|
+
def run_cached(
|
|
177
|
+
self,
|
|
178
|
+
probe: GeneratedWhiteboxProbe,
|
|
179
|
+
model: "Model",
|
|
180
|
+
cases: "CaseBatch",
|
|
181
|
+
) -> "Result | None":
|
|
182
|
+
"""Re-capture and re-run an already-generated probe (no LLM call)."""
|
|
183
|
+
if self._capture(model, cases) == 0:
|
|
184
|
+
return None
|
|
185
|
+
return self._run_code(probe.code, probe.name, model, cases)
|
|
186
|
+
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
# Host capture (trusted)
|
|
189
|
+
# ------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
def _capture(self, model: "Model", cases: "CaseBatch") -> int:
|
|
192
|
+
"""Forward each case, reduce, and dump the attention bundle. Returns n captured."""
|
|
193
|
+
workdir = Path(self._sandbox.workdir)
|
|
194
|
+
dump_dir = workdir / _DUMP_DIR
|
|
195
|
+
dump_dir.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
|
|
197
|
+
manifest: list[dict[str, Any]] = []
|
|
198
|
+
selected = list(cases)
|
|
199
|
+
if self._max_cases > 0: # 0 = every case
|
|
200
|
+
selected = selected[: self._max_cases]
|
|
201
|
+
if getattr(self.run_logger, "preserve_full_model_io", False):
|
|
202
|
+
from evalrx.eval_agent.model_instrumentation import InstrumentedModel
|
|
203
|
+
|
|
204
|
+
model = InstrumentedModel(
|
|
205
|
+
model, self.run_logger,
|
|
206
|
+
cycle=int(getattr(self.run_logger, "current_cycle", -1)),
|
|
207
|
+
analyzer="generated_whitebox_probe_capture",
|
|
208
|
+
case_prompts={c.inputs.prompt: c.id for c in selected},
|
|
209
|
+
batch_case_ids=[c.id for c in selected],
|
|
210
|
+
)
|
|
211
|
+
for case in selected:
|
|
212
|
+
try:
|
|
213
|
+
trace = model.forward(case.inputs, capture={Capability.ATTENTION})
|
|
214
|
+
attns = trace.require(Capability.ATTENTION)
|
|
215
|
+
rows = np.stack([
|
|
216
|
+
a.float().mean(dim=0)[-1].cpu().numpy() for a in attns
|
|
217
|
+
]).astype(np.float16) # (n_layers, seq)
|
|
218
|
+
mask = trace.extras.get("image_token_mask")
|
|
219
|
+
mask_np = (
|
|
220
|
+
mask.cpu().numpy().astype(bool)
|
|
221
|
+
if mask is not None else np.zeros(rows.shape[1], dtype=bool)
|
|
222
|
+
)
|
|
223
|
+
tokens = list(trace.tokens)
|
|
224
|
+
except Exception as exc: # noqa: BLE001 - skip uncapturable cases
|
|
225
|
+
logger.debug("whitebox capture failed for %s: %s", case.id, exc)
|
|
226
|
+
continue
|
|
227
|
+
finally:
|
|
228
|
+
trace = None # free the heavy attention tensors promptly
|
|
229
|
+
|
|
230
|
+
np.savez_compressed(
|
|
231
|
+
dump_dir / f"{case.id}.npz",
|
|
232
|
+
attn_last=rows, image_token_mask=mask_np,
|
|
233
|
+
)
|
|
234
|
+
label = getattr(case, "label", None)
|
|
235
|
+
manifest.append({
|
|
236
|
+
"id": case.id,
|
|
237
|
+
"label": getattr(label, "value", None),
|
|
238
|
+
"seq_len": int(rows.shape[1]),
|
|
239
|
+
"n_layers": int(rows.shape[0]),
|
|
240
|
+
"tokens": tokens,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
(workdir / _MANIFEST).write_text(
|
|
244
|
+
json.dumps({"cases": manifest}), encoding="utf-8"
|
|
245
|
+
)
|
|
246
|
+
return len(manifest)
|
|
247
|
+
|
|
248
|
+
# ------------------------------------------------------------------
|
|
249
|
+
# Code writing + sandbox run (mirrors probe_generator)
|
|
250
|
+
# ------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def _write_code(self, need: str) -> tuple[str, str]:
|
|
253
|
+
if self._cli_config is not None and self._cli_config.provider != "llm":
|
|
254
|
+
code = self._write_code_cli(need)
|
|
255
|
+
if code:
|
|
256
|
+
return code, f"cli:{self._cli_config.provider}"
|
|
257
|
+
prompt = self._build_prompt(need, fenced=True)
|
|
258
|
+
self._last_prompt = prompt
|
|
259
|
+
raw = self._judge.generate(prompt) # type: ignore[union-attr]
|
|
260
|
+
self._last_raw = str(raw)
|
|
261
|
+
return _extract_code(str(raw)), "llm"
|
|
262
|
+
|
|
263
|
+
def _write_code_cli(self, need: str) -> str:
|
|
264
|
+
from evalrx.agent_runtime.codegen import CodegenRunner
|
|
265
|
+
|
|
266
|
+
prompt = self._build_prompt(need, fenced=False)
|
|
267
|
+
self._last_prompt = prompt
|
|
268
|
+
result = CodegenRunner(self._cli_config).write_code( # type: ignore[arg-type]
|
|
269
|
+
prompt,
|
|
270
|
+
workdir=Path(self._sandbox.workdir),
|
|
271
|
+
timeout_sec=self._timeout_sec,
|
|
272
|
+
preferred_filenames=("probe.py",),
|
|
273
|
+
)
|
|
274
|
+
self._last_raw = result.raw_output
|
|
275
|
+
self._last_raw_stream = ""
|
|
276
|
+
if result.raw_stream_path:
|
|
277
|
+
try:
|
|
278
|
+
self._last_raw_stream = (
|
|
279
|
+
Path(self._sandbox.workdir) / result.raw_stream_path
|
|
280
|
+
).read_text(encoding="utf-8")
|
|
281
|
+
except OSError:
|
|
282
|
+
pass
|
|
283
|
+
self._last_usage = result.usage
|
|
284
|
+
return result.code
|
|
285
|
+
|
|
286
|
+
def _build_prompt(self, need: str, *, fenced: bool) -> str:
|
|
287
|
+
return _GENERATE_PROMPT.format(
|
|
288
|
+
need=need.strip() or "Probe the attention pattern behind the failures.",
|
|
289
|
+
manifest=_MANIFEST,
|
|
290
|
+
dump_dir=_DUMP_DIR,
|
|
291
|
+
marker=_RESULT_MARKER,
|
|
292
|
+
fences_hint=" inside a ```python code block" if fenced else
|
|
293
|
+
", written to a file named probe.py",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def _run_code(
|
|
297
|
+
self, code: str, name: str, model: "Model", cases: "CaseBatch",
|
|
298
|
+
) -> "Result | None":
|
|
299
|
+
sandbox_result = self._sandbox.run(code, timeout_sec=self._timeout_sec)
|
|
300
|
+
if not sandbox_result.ok:
|
|
301
|
+
logger.warning(
|
|
302
|
+
"WhiteboxProbeGenerator: sandbox run failed (rc=%s): %s",
|
|
303
|
+
sandbox_result.returncode, (sandbox_result.stderr or "").strip()[:200],
|
|
304
|
+
)
|
|
305
|
+
return None
|
|
306
|
+
return _parse_result(sandbox_result.stdout, name, model, cases)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# Module helpers (shared shape with probe_generator)
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
def _extract_code(raw: str) -> str:
|
|
314
|
+
cleaned = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL)
|
|
315
|
+
fence = re.search(r"```(?:python)?\s*\n(.*?)```", cleaned, flags=re.DOTALL)
|
|
316
|
+
if fence:
|
|
317
|
+
return fence.group(1).strip()
|
|
318
|
+
return cleaned.strip()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _parse_result(
|
|
322
|
+
stdout: str, name: str, model: "Model", cases: "CaseBatch",
|
|
323
|
+
) -> "Result | None":
|
|
324
|
+
marker_line: Optional[str] = None
|
|
325
|
+
for line in stdout.splitlines():
|
|
326
|
+
s = line.strip()
|
|
327
|
+
if s.startswith(_RESULT_MARKER):
|
|
328
|
+
marker_line = s[len(_RESULT_MARKER):]
|
|
329
|
+
if marker_line is None:
|
|
330
|
+
logger.warning("WhiteboxProbeGenerator: no PROBE_RESULT_JSON line in output")
|
|
331
|
+
return None
|
|
332
|
+
try:
|
|
333
|
+
data = json.loads(marker_line)
|
|
334
|
+
except json.JSONDecodeError as exc:
|
|
335
|
+
logger.warning("WhiteboxProbeGenerator: unparseable PROBE_RESULT_JSON: %s", exc)
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
findings: dict[str, Any] = {}
|
|
339
|
+
if isinstance(data.get("findings"), dict):
|
|
340
|
+
findings.update(data["findings"])
|
|
341
|
+
per_case = data.get("per_case")
|
|
342
|
+
if isinstance(per_case, list):
|
|
343
|
+
findings["per_case"] = [e for e in per_case if isinstance(e, dict)]
|
|
344
|
+
|
|
345
|
+
return Result(
|
|
346
|
+
analyzer=f"generated_wb:{name}",
|
|
347
|
+
model=repr(model),
|
|
348
|
+
cases=cases,
|
|
349
|
+
findings=findings,
|
|
350
|
+
metadata={"generated": True, "whitebox": True, "probe": name},
|
|
351
|
+
)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Store — the persistent memory that makes self-evolution possible.
|
|
2
|
+
|
|
3
|
+
Without memory, an agent re-explores the same ground forever. The store
|
|
4
|
+
accumulates the corpus the agent evolves over: cases it has constructed,
|
|
5
|
+
results it has produced, and hypotheses it has tested. The loop writes to it
|
|
6
|
+
each cycle and reads from it to decide what to try next.
|
|
7
|
+
|
|
8
|
+
This module defines the interface, an in-memory reference, and a JSONL-backed
|
|
9
|
+
persistent implementation (JsonlStore).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from evalrx.core.case import FailureCase
|
|
22
|
+
from evalrx.core.result import Result
|
|
23
|
+
from evalrx.eval_agent.hypothesis import Hypothesis
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Store(ABC):
|
|
29
|
+
"""Persistent memory interface for the self-evolving loop."""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def add_case(self, case: "FailureCase") -> None: ...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def add_result(self, result: "Result") -> None: ...
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def add_hypothesis(self, hypothesis: "Hypothesis") -> None: ...
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def query(self, **filters: Any) -> list[Any]:
|
|
42
|
+
"""Retrieve stored items matching *filters* (tags, label, status, …)."""
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def summarize(self) -> dict[str, Any]:
|
|
46
|
+
"""Aggregate view the agent reads to spot gaps and patterns."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class InMemoryStore(Store):
|
|
50
|
+
"""Minimal non-persistent reference store (Stage-2 will add durability)."""
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self.cases: list[FailureCase] = []
|
|
54
|
+
self.results: list[Result] = []
|
|
55
|
+
self.hypotheses: list[Hypothesis] = []
|
|
56
|
+
|
|
57
|
+
def add_case(self, case: "FailureCase") -> None:
|
|
58
|
+
self.cases.append(case)
|
|
59
|
+
|
|
60
|
+
def add_result(self, result: "Result") -> None:
|
|
61
|
+
self.results.append(result)
|
|
62
|
+
|
|
63
|
+
def add_hypothesis(self, hypothesis: "Hypothesis") -> None:
|
|
64
|
+
self.hypotheses.append(hypothesis)
|
|
65
|
+
|
|
66
|
+
def query(self, kind: str | None = None, **filters: Any) -> list[Any]:
|
|
67
|
+
"""Retrieve stored items. ``kind`` in {cases, results, hypotheses}; filters:
|
|
68
|
+
``label`` / ``tags`` (cases), ``status`` (hypotheses), ``analyzer`` (results)."""
|
|
69
|
+
out: list[Any] = []
|
|
70
|
+
if kind in (None, "cases"):
|
|
71
|
+
for c in self.cases:
|
|
72
|
+
if filters.get("label") is not None and getattr(c, "label", None) != filters["label"]:
|
|
73
|
+
continue
|
|
74
|
+
tags = filters.get("tags")
|
|
75
|
+
if tags is not None and not set(tags).issubset(getattr(c, "tags", set())):
|
|
76
|
+
continue
|
|
77
|
+
out.append(c)
|
|
78
|
+
if kind in (None, "results"):
|
|
79
|
+
for r in self.results:
|
|
80
|
+
if filters.get("analyzer") is not None and getattr(r, "analyzer", None) != filters["analyzer"]:
|
|
81
|
+
continue
|
|
82
|
+
out.append(r)
|
|
83
|
+
if kind in (None, "hypotheses"):
|
|
84
|
+
for h in self.hypotheses:
|
|
85
|
+
if filters.get("status") is not None and getattr(h, "status", None) != filters["status"]:
|
|
86
|
+
continue
|
|
87
|
+
out.append(h)
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
def summarize(self) -> dict[str, Any]:
|
|
91
|
+
return {
|
|
92
|
+
"n_cases": len(self.cases),
|
|
93
|
+
"n_results": len(self.results),
|
|
94
|
+
"n_hypotheses": len(self.hypotheses),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class JsonlStore(Store):
|
|
99
|
+
"""Persistent JSONL-backed store.
|
|
100
|
+
|
|
101
|
+
Files written under *store_dir*:
|
|
102
|
+
- ``hypotheses.jsonl`` — one serialised :class:`Hypothesis` per line
|
|
103
|
+
- ``results.jsonl`` — one scalar-only result summary per line
|
|
104
|
+
- ``cases.jsonl`` — one case summary per line
|
|
105
|
+
|
|
106
|
+
Hypotheses survive process restart; cases and results store lightweight
|
|
107
|
+
summaries only (no numpy arrays or heavy objects).
|
|
108
|
+
|
|
109
|
+
Thread safety: appends use ``O_APPEND`` which is atomic on Linux for
|
|
110
|
+
small writes; reads reload from disk each call.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, store_dir: Path | str) -> None:
|
|
114
|
+
self._dir = Path(store_dir)
|
|
115
|
+
self._dir.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
self._hyp_path = self._dir / "hypotheses.jsonl"
|
|
117
|
+
self._res_path = self._dir / "results.jsonl"
|
|
118
|
+
self._case_path = self._dir / "cases.jsonl"
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
# Store interface
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def add_case(self, case: "FailureCase") -> None:
|
|
125
|
+
record: dict[str, Any] = {
|
|
126
|
+
"id": getattr(case, "id", ""),
|
|
127
|
+
"label": getattr(case, "label", None),
|
|
128
|
+
"tags": list(getattr(case, "tags", [])),
|
|
129
|
+
"prompt_snippet": str(getattr(getattr(case, "inputs", None), "prompt", ""))[:200],
|
|
130
|
+
}
|
|
131
|
+
self._append(self._case_path, record)
|
|
132
|
+
|
|
133
|
+
def add_result(self, result: "Result") -> None:
|
|
134
|
+
findings = getattr(result, "findings", {}) or {}
|
|
135
|
+
scalar_findings: dict[str, Any] = {}
|
|
136
|
+
for k, v in findings.items():
|
|
137
|
+
try:
|
|
138
|
+
scalar_findings[k] = float(v) if hasattr(v, "__float__") else str(v)
|
|
139
|
+
except Exception:
|
|
140
|
+
scalar_findings[k] = str(v)
|
|
141
|
+
record: dict[str, Any] = {
|
|
142
|
+
"analyzer": getattr(result, "analyzer", ""),
|
|
143
|
+
"findings": scalar_findings,
|
|
144
|
+
"metadata": {
|
|
145
|
+
k: str(v) for k, v in (getattr(result, "metadata", {}) or {}).items()
|
|
146
|
+
if isinstance(k, str)
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
self._append(self._res_path, record)
|
|
150
|
+
|
|
151
|
+
def add_hypothesis(self, hypothesis: "Hypothesis") -> None:
|
|
152
|
+
from evalrx.eval_agent.hypothesis import hypothesis_to_dict
|
|
153
|
+
self._append(self._hyp_path, hypothesis_to_dict(hypothesis))
|
|
154
|
+
|
|
155
|
+
def query(self, kind: str | None = None, **filters: Any) -> list[Any]:
|
|
156
|
+
"""Retrieve stored items matching *filters*.
|
|
157
|
+
|
|
158
|
+
Returns reconstructed :class:`Hypothesis` objects for ``kind="hypotheses"``;
|
|
159
|
+
raw dicts for cases and results.
|
|
160
|
+
"""
|
|
161
|
+
out: list[Any] = []
|
|
162
|
+
|
|
163
|
+
if kind in (None, "cases"):
|
|
164
|
+
for rec in self._load(self._case_path):
|
|
165
|
+
if filters.get("label") is not None and rec.get("label") != filters["label"]:
|
|
166
|
+
continue
|
|
167
|
+
tags = filters.get("tags")
|
|
168
|
+
if tags is not None and not set(tags).issubset(set(rec.get("tags", []))):
|
|
169
|
+
continue
|
|
170
|
+
out.append(rec)
|
|
171
|
+
|
|
172
|
+
if kind in (None, "results"):
|
|
173
|
+
for rec in self._load(self._res_path):
|
|
174
|
+
if filters.get("analyzer") is not None and rec.get("analyzer") != filters["analyzer"]:
|
|
175
|
+
continue
|
|
176
|
+
out.append(rec)
|
|
177
|
+
|
|
178
|
+
if kind in (None, "hypotheses"):
|
|
179
|
+
from evalrx.eval_agent.hypothesis import hypothesis_from_dict
|
|
180
|
+
for rec in self._load(self._hyp_path):
|
|
181
|
+
h = hypothesis_from_dict(rec)
|
|
182
|
+
if filters.get("status") is not None:
|
|
183
|
+
wanted = filters["status"]
|
|
184
|
+
# Accept both enum and string comparisons
|
|
185
|
+
h_status = h.status.value if h.status else None
|
|
186
|
+
if wanted != h.status and wanted != h_status:
|
|
187
|
+
continue
|
|
188
|
+
out.append(h)
|
|
189
|
+
|
|
190
|
+
return out
|
|
191
|
+
|
|
192
|
+
def summarize(self) -> dict[str, Any]:
|
|
193
|
+
n_hyp = sum(1 for _ in self._load(self._hyp_path))
|
|
194
|
+
n_res = sum(1 for _ in self._load(self._res_path))
|
|
195
|
+
n_cas = sum(1 for _ in self._load(self._case_path))
|
|
196
|
+
return {
|
|
197
|
+
"n_cases": n_cas,
|
|
198
|
+
"n_results": n_res,
|
|
199
|
+
"n_hypotheses": n_hyp,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
# Internal helpers
|
|
204
|
+
# ------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _append(path: Path, record: dict[str, Any]) -> None:
|
|
208
|
+
try:
|
|
209
|
+
with path.open("a", encoding="utf-8") as f:
|
|
210
|
+
f.write(json.dumps(record, default=str) + "\n")
|
|
211
|
+
except OSError as exc:
|
|
212
|
+
logger.warning("JsonlStore: failed to append to %s: %s", path, exc)
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _load(path: Path) -> list[dict[str, Any]]:
|
|
216
|
+
if not path.exists():
|
|
217
|
+
return []
|
|
218
|
+
records: list[dict[str, Any]] = []
|
|
219
|
+
try:
|
|
220
|
+
with path.open(encoding="utf-8") as f:
|
|
221
|
+
for line in f:
|
|
222
|
+
line = line.strip()
|
|
223
|
+
if not line:
|
|
224
|
+
continue
|
|
225
|
+
try:
|
|
226
|
+
records.append(json.loads(line))
|
|
227
|
+
except json.JSONDecodeError:
|
|
228
|
+
logger.debug("JsonlStore: skipping malformed line in %s", path)
|
|
229
|
+
except OSError as exc:
|
|
230
|
+
logger.warning("JsonlStore: failed to read %s: %s", path, exc)
|
|
231
|
+
return records
|
evalrx/logging_utils.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Console logging convenience for evalrx' existing per-module loggers.
|
|
2
|
+
|
|
3
|
+
Every submodule follows the standard library idiom
|
|
4
|
+
``logger = logging.getLogger(__name__)`` and already narrates real events —
|
|
5
|
+
M1-M5 stage transitions, capability negotiation, paper-method dispatch
|
|
6
|
+
(``vcd``/``icd``/...), fix-candidate generation and validation, audio decode
|
|
7
|
+
fallbacks. None of it is visible by default: Python's logging module only
|
|
8
|
+
invokes an implicit "handler of last resort" for ``WARNING``-and-above
|
|
9
|
+
records when NO handler exists anywhere in a logger's propagation chain, and
|
|
10
|
+
even that prints unformatted straight to stderr. ``.info()``/``.debug()``
|
|
11
|
+
calls — most of the narration — never appear at all without a handler.
|
|
12
|
+
|
|
13
|
+
A **library** must never call ``logging.basicConfig()`` itself: that
|
|
14
|
+
configures the ROOT logger process-wide and would clobber a host
|
|
15
|
+
application's own logging setup the moment ``import evalrx`` runs. The
|
|
16
|
+
standard, documented pattern (see the stdlib logging HOWTO's "Configuring
|
|
17
|
+
Logging for a Library" section) is instead:
|
|
18
|
+
|
|
19
|
+
* attach a :class:`logging.NullHandler` to the package's top-level logger
|
|
20
|
+
(done in :mod:`evalrx` on import) so the library is silent — no
|
|
21
|
+
"No handlers could be found" noise, no surprise raw stderr output — until
|
|
22
|
+
a caller opts in;
|
|
23
|
+
* expose an explicit, opt-in helper for callers who *do* want to see it.
|
|
24
|
+
That's this module.
|
|
25
|
+
|
|
26
|
+
Usage::
|
|
27
|
+
|
|
28
|
+
import evalrx
|
|
29
|
+
evalrx.enable_console_logging() # INFO and above, readable
|
|
30
|
+
model = evalrx.load("qwen2.5-7b-instruct")
|
|
31
|
+
... # now narrated to stdout
|
|
32
|
+
|
|
33
|
+
evalrx.enable_console_logging(level=logging.DEBUG) # noisier
|
|
34
|
+
evalrx.disable_console_logging() # back to quiet
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import logging
|
|
40
|
+
import sys
|
|
41
|
+
from typing import IO, Optional
|
|
42
|
+
|
|
43
|
+
# Every module's `logging.getLogger(__name__)` (e.g. "evalrx.eval_agent.loop")
|
|
44
|
+
# is a child of this in the dot-hierarchy, so one handler here reaches all of
|
|
45
|
+
# them via normal propagation -- except RunLogger's own "evalrx.run.<dir>"
|
|
46
|
+
# tree, which sets propagate=False by design to keep its structured JSONL
|
|
47
|
+
# event stream a separate channel from this ad-hoc narration.
|
|
48
|
+
TOP_LEVEL_LOGGER_NAME = "evalrx"
|
|
49
|
+
|
|
50
|
+
# Marks a handler as "ours" so enable_console_logging() is idempotent (calling
|
|
51
|
+
# it twice reconfigures the level instead of stacking a second handler) and
|
|
52
|
+
# disable_console_logging() only ever removes handlers it installed itself --
|
|
53
|
+
# never a caller's own handler on the same logger.
|
|
54
|
+
_MARKER_ATTR = "_evalrx_console_handler"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _ConsoleFormatter(logging.Formatter):
|
|
58
|
+
"""Compact single-line format: time, level, short module name, message."""
|
|
59
|
+
|
|
60
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
61
|
+
record.short_name = record.name.rsplit(".", 1)[-1]
|
|
62
|
+
return super().format(record)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def enable_console_logging(
|
|
66
|
+
level: int = logging.INFO, *, stream: Optional[IO[str]] = None
|
|
67
|
+
) -> logging.Handler:
|
|
68
|
+
"""Attach a readable stdout handler to evalrx' logger tree.
|
|
69
|
+
|
|
70
|
+
Idempotent: a second call reconfigures the level of the existing handler
|
|
71
|
+
rather than attaching a duplicate (no doubled lines).
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
level: Minimum level to show. ``logging.INFO`` (default) surfaces the
|
|
75
|
+
stage-by-stage narration most callers want; ``logging.DEBUG``
|
|
76
|
+
is noisier (per-candidate detail, low-level capability checks).
|
|
77
|
+
stream: Defaults to ``sys.stdout``.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The attached :class:`logging.Handler`, in case a caller wants to
|
|
81
|
+
remove or further customise it directly.
|
|
82
|
+
"""
|
|
83
|
+
top_logger = logging.getLogger(TOP_LEVEL_LOGGER_NAME)
|
|
84
|
+
for handler in top_logger.handlers:
|
|
85
|
+
if getattr(handler, _MARKER_ATTR, False):
|
|
86
|
+
handler.setLevel(level)
|
|
87
|
+
top_logger.setLevel(level)
|
|
88
|
+
return handler
|
|
89
|
+
|
|
90
|
+
handler = logging.StreamHandler(stream or sys.stdout)
|
|
91
|
+
handler.setFormatter(
|
|
92
|
+
_ConsoleFormatter(
|
|
93
|
+
fmt="%(asctime)s %(levelname)-7s %(short_name)s: %(message)s",
|
|
94
|
+
datefmt="%H:%M:%S",
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
setattr(handler, _MARKER_ATTR, True)
|
|
98
|
+
top_logger.addHandler(handler)
|
|
99
|
+
top_logger.setLevel(level)
|
|
100
|
+
return handler
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def disable_console_logging() -> None:
|
|
104
|
+
"""Remove the handler(s) installed by :func:`enable_console_logging`.
|
|
105
|
+
|
|
106
|
+
A no-op if none was ever attached. Never touches handlers a caller added
|
|
107
|
+
themselves directly to ``logging.getLogger("evalrx")`` or below.
|
|
108
|
+
"""
|
|
109
|
+
top_logger = logging.getLogger(TOP_LEVEL_LOGGER_NAME)
|
|
110
|
+
for handler in list(top_logger.handlers):
|
|
111
|
+
if getattr(handler, _MARKER_ATTR, False):
|
|
112
|
+
top_logger.removeHandler(handler)
|