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,106 @@
|
|
|
1
|
+
"""Experiment — a declarative spec that bridges the agent and the core.
|
|
2
|
+
|
|
3
|
+
The self-evolving agent does not call analyzers directly; it emits *experiments*
|
|
4
|
+
("run analyzer X on model Y over cases Z"), which an :class:`ExperimentRunner`
|
|
5
|
+
executes, caches, and turns into results. Making the experiment a first-class
|
|
6
|
+
object lets the agent queue, log, replay, and reason about its own actions.
|
|
7
|
+
|
|
8
|
+
This is intentionally minimal in Stage 1: enough structure for the agent layer
|
|
9
|
+
to target, with caching as the one piece of real behaviour.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from evalrx.core.analyzer import Analyzer
|
|
21
|
+
from evalrx.core.model import Model
|
|
22
|
+
from evalrx.core.result import Result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _data_fingerprint(data: Any) -> Any:
|
|
26
|
+
"""Deterministic content fingerprint for cache keys.
|
|
27
|
+
|
|
28
|
+
Crucially does NOT call ``as_casebatch`` (which mints fresh uuids for raw
|
|
29
|
+
inputs) — that would make the same data hash differently each call. Uses
|
|
30
|
+
stable case ids when present, otherwise the prompt/content text.
|
|
31
|
+
"""
|
|
32
|
+
from evalrx.core.case import CaseBatch, FailureCase, Inputs
|
|
33
|
+
|
|
34
|
+
if isinstance(data, CaseBatch):
|
|
35
|
+
return [c.id for c in data]
|
|
36
|
+
if isinstance(data, FailureCase):
|
|
37
|
+
return [data.id]
|
|
38
|
+
if isinstance(data, Inputs):
|
|
39
|
+
return [f"inputs:{data.prompt}"]
|
|
40
|
+
if isinstance(data, str):
|
|
41
|
+
return [f"str:{data}"]
|
|
42
|
+
if isinstance(data, (list, tuple)):
|
|
43
|
+
return [_data_fingerprint(x) for x in data]
|
|
44
|
+
return [repr(data)]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Experiment:
|
|
49
|
+
"""A declarative analysis request.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
model: The model to analyse.
|
|
53
|
+
analyzer: The configured analyzer to run.
|
|
54
|
+
data: Cases (or anything :func:`as_casebatch` accepts).
|
|
55
|
+
metadata: Free-form context (e.g. the hypothesis this tests).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
model: "Model"
|
|
59
|
+
analyzer: "Analyzer"
|
|
60
|
+
data: Any
|
|
61
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
def fingerprint(self) -> str:
|
|
64
|
+
"""A stable CONTENT hash of (model, analyzer+params, data).
|
|
65
|
+
|
|
66
|
+
Used as the cache key so that two *equivalent* experiments dedupe (and a
|
|
67
|
+
rerun after restart hits cache) — unlike ``id()``, which is a transient
|
|
68
|
+
memory address. The data fingerprint uses case ids when available.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
params = self.analyzer.get_params()
|
|
72
|
+
except Exception: # non-estimator analyzer
|
|
73
|
+
params = {}
|
|
74
|
+
data_fp = _data_fingerprint(self.data)
|
|
75
|
+
payload = json.dumps(
|
|
76
|
+
{
|
|
77
|
+
"model": repr(self.model),
|
|
78
|
+
"analyzer": getattr(self.analyzer, "name", type(self.analyzer).__name__),
|
|
79
|
+
"params": params,
|
|
80
|
+
"data": data_fp,
|
|
81
|
+
},
|
|
82
|
+
sort_keys=True,
|
|
83
|
+
default=repr,
|
|
84
|
+
)
|
|
85
|
+
return hashlib.sha1(payload.encode()).hexdigest()[:16]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ExperimentRunner:
|
|
89
|
+
"""Execute :class:`Experiment` specs, caching results by experiment CONTENT."""
|
|
90
|
+
|
|
91
|
+
def __init__(self) -> None:
|
|
92
|
+
self._cache: dict[str, "Result"] = {}
|
|
93
|
+
|
|
94
|
+
def run(self, experiment: Experiment, use_cache: bool = True) -> "Result":
|
|
95
|
+
"""Run an experiment, returning (and caching) its :class:`Result`."""
|
|
96
|
+
key = experiment.fingerprint()
|
|
97
|
+
if use_cache and key in self._cache:
|
|
98
|
+
return self._cache[key]
|
|
99
|
+
result = experiment.analyzer.run(experiment.model, experiment.data)
|
|
100
|
+
result.metadata.setdefault("experiment", experiment.metadata)
|
|
101
|
+
result.metadata.setdefault("fingerprint", key)
|
|
102
|
+
self._cache[key] = result
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
def run_many(self, experiments: list[Experiment]) -> list["Result"]:
|
|
106
|
+
return [self.run(exp) for exp in experiments]
|
evalrx/core/model.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Model — the abstract contract every analyzable model implements.
|
|
2
|
+
|
|
3
|
+
The key design move: a model exposes **one** rich forward pass,
|
|
4
|
+
``forward(inputs, capture={...}) -> Trace``, where ``capture`` names the
|
|
5
|
+
internals to record (attention, hidden states, …). Analyzers consume the
|
|
6
|
+
``Trace``, not the model's framework-specific guts. This decouples "run the
|
|
7
|
+
model and grab internals" from "interpret the internals", so one model serves
|
|
8
|
+
every compatible analyzer and one analyzer works across every compatible model.
|
|
9
|
+
|
|
10
|
+
The hybrid convenience API (``model.call_attention(prompt)``) is provided here
|
|
11
|
+
by ``__getattr__``: it resolves ``attention`` in the analyzer registry, checks
|
|
12
|
+
the model supports it, and delegates to ``AttentionAnalyzer().run(self, ...)``.
|
|
13
|
+
No mixins, no per-model wiring — the shim is derived from capabilities.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
21
|
+
|
|
22
|
+
from evalrx.core.capability import Capability
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
import torch
|
|
26
|
+
|
|
27
|
+
from evalrx.core.tokentype import TokenTypeMap
|
|
28
|
+
from evalrx.core.tool import ChatTurn
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CaptureSpec:
|
|
33
|
+
"""Optional knobs that bound what ``forward`` records, to control memory.
|
|
34
|
+
|
|
35
|
+
Capturing every layer/head of attention for a long sequence is O(L^2) and
|
|
36
|
+
can be tens of GB. A backend that honours this spec captures only the
|
|
37
|
+
requested layers/heads and can keep tensors off the CPU. ``None`` fields
|
|
38
|
+
mean "everything" (the simple default). Backends that don't support
|
|
39
|
+
sub-selection ignore it — the ``capture`` set alone still works.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
layers: "list[int] | None" = None # which decoder layers to keep (None = all)
|
|
43
|
+
heads: "list[int] | None" = None # which attention heads to keep (None = all)
|
|
44
|
+
to_cpu: bool = True # move captured tensors to CPU (frees GPU mem)
|
|
45
|
+
with_grad: bool = False # keep the graph for GRADIENTS (else no_grad)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class TokenLogprob:
|
|
50
|
+
"""Logprob of one output token (+ optional top-k alternatives).
|
|
51
|
+
|
|
52
|
+
The black-box uncertainty primitive: retrievable from an API that returns
|
|
53
|
+
``logprobs`` (OpenAI) — no model internals needed.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
token: str
|
|
57
|
+
logprob: float
|
|
58
|
+
top: dict[str, float] = field(default_factory=dict) # token -> logprob (top-k alternatives)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Trace:
|
|
63
|
+
"""Captured internals from a single forward pass.
|
|
64
|
+
|
|
65
|
+
Only the fields named in the ``capture`` set are populated; the rest stay
|
|
66
|
+
``None``. ``provided`` records what was actually captured.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
tokens: list[str]
|
|
70
|
+
token_ids: list[int]
|
|
71
|
+
provided: set[Capability] = field(default_factory=set)
|
|
72
|
+
attentions: "list[torch.Tensor] | None" = None # per layer: (heads, seq, seq)
|
|
73
|
+
hidden_states: "list[torch.Tensor] | None" = None # per layer: (seq, dim)
|
|
74
|
+
logits: "torch.Tensor | None" = None # (seq, vocab)
|
|
75
|
+
token_type_map: "TokenTypeMap | None" = None # VLM: image-token positions + patch grid
|
|
76
|
+
extras: dict[str, Any] = field(default_factory=dict)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def seq_len(self) -> int:
|
|
80
|
+
return len(self.tokens)
|
|
81
|
+
|
|
82
|
+
def require(self, capability: Capability) -> Any:
|
|
83
|
+
"""Return the field for *capability*, raising if it wasn't captured."""
|
|
84
|
+
attr = {
|
|
85
|
+
Capability.ATTENTION: "attentions",
|
|
86
|
+
Capability.HIDDEN_STATES: "hidden_states",
|
|
87
|
+
Capability.LOGITS: "logits",
|
|
88
|
+
}.get(capability)
|
|
89
|
+
value = getattr(self, attr) if attr else None
|
|
90
|
+
if value is None:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Trace does not contain {capability.value!r}. "
|
|
93
|
+
f"Was it included in the forward(capture=...) set? "
|
|
94
|
+
f"Captured: {sorted(c.value for c in self.provided)}"
|
|
95
|
+
)
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Model(ABC):
|
|
100
|
+
"""Abstract base for every analyzable model.
|
|
101
|
+
|
|
102
|
+
Subclasses must:
|
|
103
|
+
1. set the ``capabilities`` class attribute,
|
|
104
|
+
2. implement :meth:`generate` and :meth:`forward`.
|
|
105
|
+
|
|
106
|
+
They get ``call_<analyzer>(data, **kwargs)`` for free via :meth:`__getattr__`.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
#: Capabilities this model provides. Override in subclasses.
|
|
110
|
+
capabilities: frozenset[Capability] = frozenset()
|
|
111
|
+
|
|
112
|
+
#: Modalities this model handles (e.g. {"text"}, {"text","image"}). Analyzers
|
|
113
|
+
#: declare ``applies_to_modalities`` and are matched against this.
|
|
114
|
+
modalities: frozenset[str] = frozenset({"text"})
|
|
115
|
+
|
|
116
|
+
def unembed_weight(self):
|
|
117
|
+
"""Optional: the unembedding matrix ``(vocab, dim)`` for logit-lens analyzers.
|
|
118
|
+
|
|
119
|
+
Returns ``None`` unless a white-box backend exposes it.
|
|
120
|
+
"""
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def generate(self, inputs: Any, **kwargs) -> str:
|
|
125
|
+
"""Produce a text response for *inputs* (a prompt or :class:`Inputs`)."""
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def forward(
|
|
129
|
+
self,
|
|
130
|
+
inputs: Any,
|
|
131
|
+
capture: set[Capability],
|
|
132
|
+
spec: "CaptureSpec | None" = None,
|
|
133
|
+
) -> Trace:
|
|
134
|
+
"""Run one forward pass, capturing the requested internals into a :class:`Trace`.
|
|
135
|
+
|
|
136
|
+
``capture`` names *what* to record; the optional ``spec`` bounds *how
|
|
137
|
+
much* (layers/heads/device) for memory control. Backends free to ignore
|
|
138
|
+
``spec`` when they cannot sub-select.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def chat(self, messages: list, tools: "list | None" = None) -> "ChatTurn":
|
|
143
|
+
"""One tool-aware turn for agent mode (the ``Agent`` loop calls this).
|
|
144
|
+
|
|
145
|
+
Returns a :class:`~evalrx.core.tool.ChatTurn` (assistant text + any
|
|
146
|
+
native tool_calls). Only tool-capable handles override this; the default
|
|
147
|
+
signals that this model cannot drive an agent loop.
|
|
148
|
+
"""
|
|
149
|
+
raise NotImplementedError(
|
|
150
|
+
f"{type(self).__name__} does not implement chat(); it is not TOOL_CALLS-capable."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def logprobs(self, inputs: Any, **kwargs) -> "list[TokenLogprob]":
|
|
154
|
+
"""Generate and return per-output-token logprobs (the LOGPROBS capability).
|
|
155
|
+
|
|
156
|
+
Black-box-feasible (OpenAI-style ``logprobs``). Only LOGPROBS-capable
|
|
157
|
+
handles override this.
|
|
158
|
+
"""
|
|
159
|
+
raise NotImplementedError(
|
|
160
|
+
f"{type(self).__name__} does not implement logprobs(); it is not LOGPROBS-capable."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
# Capability checks
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def supports(self, required: frozenset[Capability] | set[Capability]) -> bool:
|
|
169
|
+
"""True iff this model provides every capability in *required*."""
|
|
170
|
+
return set(required).issubset(self.capabilities)
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
# Hybrid convenience shim: model.call_<analyzer>(data, **kwargs)
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
def __getattr__(self, attr: str) -> Callable[..., Any]:
|
|
177
|
+
# __getattr__ only fires for attributes not found normally, so this
|
|
178
|
+
# never shadows real methods/fields.
|
|
179
|
+
if attr.startswith("call_"):
|
|
180
|
+
analyzer_name = attr[len("call_"):]
|
|
181
|
+
from evalrx.core.registry import registry
|
|
182
|
+
|
|
183
|
+
if registry.analyzers.has(analyzer_name):
|
|
184
|
+
analyzer_cls = registry.analyzers.get(analyzer_name)
|
|
185
|
+
|
|
186
|
+
def _shim(data: Any, **kwargs) -> Any:
|
|
187
|
+
return analyzer_cls(**kwargs).run(self, data)
|
|
188
|
+
|
|
189
|
+
_shim.__name__ = attr
|
|
190
|
+
_shim.__doc__ = (
|
|
191
|
+
f"Convenience shim → {analyzer_cls.__name__}(**kwargs).run(self, data). "
|
|
192
|
+
f"Auto-derived from capabilities + registry."
|
|
193
|
+
)
|
|
194
|
+
return _shim
|
|
195
|
+
|
|
196
|
+
raise AttributeError(
|
|
197
|
+
f"{type(self).__name__!r} object has no attribute {attr!r}"
|
|
198
|
+
)
|
evalrx/core/pipeline.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Pipeline — sklearn-style composition of analyzers.
|
|
2
|
+
|
|
3
|
+
Run several analyzers over the same ``(model, cases)`` and collect their
|
|
4
|
+
results under their step names::
|
|
5
|
+
|
|
6
|
+
pipe = Pipeline([
|
|
7
|
+
("attention", AttentionAnalyzer(layer=-1)),
|
|
8
|
+
# ("saliency", SaliencyAnalyzer()), # Stage 2
|
|
9
|
+
])
|
|
10
|
+
results = pipe.run(qwen, "The capital of France is")
|
|
11
|
+
results["attention"].summary()
|
|
12
|
+
|
|
13
|
+
Each analyzer's capabilities are validated independently by its own ``run``.
|
|
14
|
+
Cross-step data flow (e.g. perturb → analyze) is a Stage-2 extension.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
from evalrx.core.case import as_casebatch
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from evalrx.core.analyzer import Analyzer
|
|
25
|
+
from evalrx.core.model import Model
|
|
26
|
+
from evalrx.core.result import Result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Pipeline:
|
|
30
|
+
"""Ordered collection of named analyzers run over shared inputs."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, steps: list[tuple[str, "Analyzer"]]) -> None:
|
|
33
|
+
self.steps = steps
|
|
34
|
+
|
|
35
|
+
def run(self, model: "Model", data: Any) -> dict[str, "Result"]:
|
|
36
|
+
"""Run every step over the same normalised case batch."""
|
|
37
|
+
cases = as_casebatch(data)
|
|
38
|
+
return {name: analyzer.run(model, cases) for name, analyzer in self.steps}
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
names = ", ".join(name for name, _ in self.steps)
|
|
42
|
+
return f"Pipeline([{names}])"
|
evalrx/core/registry.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Registries — the agent's discovery surface.
|
|
2
|
+
|
|
3
|
+
Analyzers register themselves by name. An agent (or the framework) enumerates
|
|
4
|
+
them and matches them to a model by capability::
|
|
5
|
+
|
|
6
|
+
registry.analyzers.list() # ["attention", "token_entropy", ...]
|
|
7
|
+
registry.analyzers.compatible_with(model) # [AttentionAnalyzer, ...]
|
|
8
|
+
|
|
9
|
+
Model *identity* lives in :mod:`evalrx.specs` (``ModelSpec``), not here;
|
|
10
|
+
``registry.models`` is a deprecated shim that delegates to it. This is what
|
|
11
|
+
makes the package programmatically explorable rather than something you have to
|
|
12
|
+
read source code to use.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from evalrx.core.analyzer import Analyzer
|
|
21
|
+
from evalrx.core.model import Model
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Registry(Generic[T]):
|
|
27
|
+
"""Name → class mapping with a registration decorator."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, kind: str) -> None:
|
|
30
|
+
self._kind = kind
|
|
31
|
+
self._items: dict[str, type[T]] = {}
|
|
32
|
+
|
|
33
|
+
def register(self, name: str) -> Callable[[type[T]], type[T]]:
|
|
34
|
+
"""Decorator: ``@registry.register("qwen")``."""
|
|
35
|
+
|
|
36
|
+
def decorator(cls: type[T]) -> type[T]:
|
|
37
|
+
key = name.lower()
|
|
38
|
+
if key in self._items:
|
|
39
|
+
raise ValueError(f"{self._kind} '{name}' is already registered.")
|
|
40
|
+
self._items[key] = cls
|
|
41
|
+
return cls
|
|
42
|
+
|
|
43
|
+
return decorator
|
|
44
|
+
|
|
45
|
+
def get(self, name: str) -> type[T]:
|
|
46
|
+
key = name.lower()
|
|
47
|
+
if key not in self._items:
|
|
48
|
+
raise KeyError(
|
|
49
|
+
f"Unknown {self._kind} '{name}'. Registered: {self.list()}"
|
|
50
|
+
)
|
|
51
|
+
return self._items[key]
|
|
52
|
+
|
|
53
|
+
def has(self, name: str) -> bool:
|
|
54
|
+
return name.lower() in self._items
|
|
55
|
+
|
|
56
|
+
def list(self) -> list[str]:
|
|
57
|
+
return sorted(self._items)
|
|
58
|
+
|
|
59
|
+
def all(self) -> dict[str, type[T]]:
|
|
60
|
+
return dict(self._items)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AnalyzerRegistry(_Registry["Analyzer"]):
|
|
64
|
+
"""Analyzer registry with capability-based matching."""
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _matches(cls: type["Analyzer"], model: "Model") -> bool:
|
|
68
|
+
"""Analyzer runs on *model* iff capabilities are satisfied AND modalities overlap."""
|
|
69
|
+
if not model.supports(cls.requires):
|
|
70
|
+
return False
|
|
71
|
+
model_modalities = getattr(model, "modalities", frozenset({"text"}))
|
|
72
|
+
return bool(cls.applies_to_modalities & model_modalities)
|
|
73
|
+
|
|
74
|
+
def compatible_with(self, model: "Model") -> list[type["Analyzer"]]:
|
|
75
|
+
"""Return analyzer classes runnable on *model* (capability + modality match)."""
|
|
76
|
+
return [cls for cls in self._items.values() if self._matches(cls, model)]
|
|
77
|
+
|
|
78
|
+
def names_compatible_with(self, model: "Model") -> list[str]:
|
|
79
|
+
"""Same as :meth:`compatible_with` but returns registered names."""
|
|
80
|
+
return sorted(name for name, cls in self._items.items() if self._matches(cls, model))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _DeprecatedModelRegistry(_Registry["Model"]):
|
|
84
|
+
"""Deprecated shim: model identity now lives in :mod:`evalrx.specs`.
|
|
85
|
+
|
|
86
|
+
``registry.models`` is kept so older code keeps working, but it delegates to
|
|
87
|
+
the spec registry and warns. Use ``evalrx.list_specs()`` /
|
|
88
|
+
``evalrx.get_spec()`` (or ``evalrx.load`` / ``compose``) instead.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _warn() -> None:
|
|
93
|
+
import warnings
|
|
94
|
+
|
|
95
|
+
warnings.warn(
|
|
96
|
+
"registry.models is deprecated; model identity now lives in "
|
|
97
|
+
"evalrx.specs (use evalrx.list_specs() / get_spec(), or "
|
|
98
|
+
"evalrx.load / compose to build a model).",
|
|
99
|
+
DeprecationWarning,
|
|
100
|
+
stacklevel=3,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def list(self) -> list[str]:
|
|
104
|
+
self._warn()
|
|
105
|
+
from evalrx.specs import list_specs
|
|
106
|
+
|
|
107
|
+
return list_specs()
|
|
108
|
+
|
|
109
|
+
def has(self, name: str) -> bool:
|
|
110
|
+
self._warn()
|
|
111
|
+
from evalrx.specs import REGISTRY
|
|
112
|
+
|
|
113
|
+
return name.lower() in REGISTRY
|
|
114
|
+
|
|
115
|
+
def get(self, name: str): # returns a ModelSpec, not a class
|
|
116
|
+
self._warn()
|
|
117
|
+
from evalrx.specs import get_spec
|
|
118
|
+
|
|
119
|
+
return get_spec(name.lower())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Registry:
|
|
123
|
+
"""Top-level namespace bundling the model and analyzer registries."""
|
|
124
|
+
|
|
125
|
+
def __init__(self) -> None:
|
|
126
|
+
# Model identity is the spec registry's job now; this is a back-compat shim.
|
|
127
|
+
self.models: _DeprecatedModelRegistry = _DeprecatedModelRegistry("model")
|
|
128
|
+
self.analyzers: AnalyzerRegistry = AnalyzerRegistry("analyzer")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Singleton used across the package.
|
|
132
|
+
registry = Registry()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def register_model(name: str):
|
|
136
|
+
"""Class decorator registering a model under *name*."""
|
|
137
|
+
return registry.models.register(name)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def register_analyzer(name: str):
|
|
141
|
+
"""Class decorator registering an analyzer under *name*."""
|
|
142
|
+
return registry.analyzers.register(name)
|
evalrx/core/result.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Result — the structured, agent-readable output of every analyzer.
|
|
2
|
+
|
|
3
|
+
A result has two faces:
|
|
4
|
+
- ``findings``: a light, JSON-serialisable dict that an LLM agent reads to
|
|
5
|
+
decide what to do next (e.g. top attended tokens, entropy stats).
|
|
6
|
+
- ``artifacts``: heavy objects (attention tensors, hidden states) for humans,
|
|
7
|
+
plotting, and downstream numeric analysis — never sent to the agent verbatim.
|
|
8
|
+
|
|
9
|
+
Subclasses (e.g. ``AttentionResult``) add domain-specific convenience methods
|
|
10
|
+
backed by ``artifacts`` while keeping the uniform ``findings`` contract.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from evalrx.core.case import CaseBatch
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Result:
|
|
25
|
+
"""Uniform analyzer output.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
analyzer: Registered name of the analyzer that produced this.
|
|
29
|
+
model: ``repr()`` of the analysed model.
|
|
30
|
+
findings: Light, JSON-serialisable summary (the agent-facing answer).
|
|
31
|
+
artifacts: Heavy objects (tensors, arrays) — not serialised by default.
|
|
32
|
+
cases: The :class:`~evalrx.core.case.CaseBatch` analysed.
|
|
33
|
+
metadata: Free-form extra info.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
analyzer: str
|
|
37
|
+
model: str
|
|
38
|
+
findings: dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
artifacts: dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
cases: "CaseBatch | None" = None
|
|
41
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
def summary(self) -> str:
|
|
44
|
+
"""Human/LLM-readable one-screen summary built from ``findings``."""
|
|
45
|
+
lines = [f"[{self.analyzer}] on {self.model}"]
|
|
46
|
+
for key, value in self.findings.items():
|
|
47
|
+
lines.append(f" {key}: {value}")
|
|
48
|
+
return "\n".join(lines)
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
"""JSON-serialisable view (``findings`` only — artifacts are dropped)."""
|
|
52
|
+
return {
|
|
53
|
+
"analyzer": self.analyzer,
|
|
54
|
+
"model": self.model,
|
|
55
|
+
"findings": self.findings,
|
|
56
|
+
"metadata": self.metadata,
|
|
57
|
+
"n_cases": len(self.cases) if self.cases is not None else 0,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def to_json(self, **kwargs) -> str:
|
|
61
|
+
return json.dumps(self.to_dict(), **kwargs)
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
return f"{type(self).__name__}(analyzer={self.analyzer!r}, model={self.model!r})"
|