agent-attrib 0.1.0__tar.gz

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.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.3
2
+ Name: agent-attrib
3
+ Version: 0.1.0
4
+ Summary: Multi-factor attribution for agent release changes using factorial design of experiments
5
+ Keywords: agents,llm,attribution,factorial,design-of-experiments,statistics
6
+ Author: agent-attrib contributors
7
+ License: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
18
+ Requires-Dist: numpy>=1.26
19
+ Requires-Dist: pandas>=2.1
20
+ Requires-Dist: scipy>=1.11
21
+ Requires-Dist: statsmodels>=0.14
22
+ Requires-Dist: pydantic>=2.5
23
+ Requires-Dist: typer>=0.9
24
+ Requires-Dist: rich>=13.7
25
+ Requires-Python: >=3.11
26
+ Project-URL: Homepage, https://github.com/agent-attrib/agent-attrib
27
+ Project-URL: Issues, https://github.com/agent-attrib/agent-attrib/issues
28
+ Description-Content-Type: text/markdown
29
+
30
+ # agent-attrib
31
+
32
+ A Python package for attributing metric changes across simultaneous agent release changes using factorial design of experiments.
33
+
34
+ **The problem:** You shipped a release that changed the prompt, the model, and the retriever at once. Your eval pass rate moved. Which change caused it, and by how much?
35
+
36
+ **The solution:** Design a factorial experiment, run your agent on the configurations it specifies, and let `agent-attrib` decompose the effects with confidence intervals.
37
+
38
+ ```bash
39
+ pip install agent-attrib
40
+ ```
41
+
42
+ ## 60-second demo
43
+
44
+ ```bash
45
+ agent-attrib demo
46
+ ```
47
+
48
+ No LLM calls, no cost. Generates synthetic runs with planted effects and reports which factors are responsible.
49
+
50
+ ## Usage
51
+
52
+ ### 1. Design the experiment
53
+
54
+ ```bash
55
+ agent-attrib plan --factors prompt,model,retriever --tasks 30 --trials 2 -o design.csv
56
+ ```
57
+
58
+ ```
59
+ Design type : 2^3 full factorial
60
+ Resolution : full
61
+ Factors : prompt, model, retriever
62
+ Tasks : 30
63
+ Trials/config: 2
64
+ Configurations: 8
65
+ Total runs : 480
66
+ Seed : 42
67
+ MDE (success, α=0.05, power=0.8): 0.1283
68
+ ```
69
+
70
+ For 5-7 factors, a fractional factorial is used automatically (resolution IV by default).
71
+
72
+ ### 2. Run your agent
73
+
74
+ For each row in `design.csv`, set each factor to the specified level (`baseline` or `candidate`) and run your agent on the task. Write one line per run:
75
+
76
+ ```json
77
+ {"run_id": "r001", "task_id": "t0001", "trial": 1,
78
+ "factors": {"prompt": "candidate", "model": "baseline", "retriever": "candidate"},
79
+ "metrics": {"success": 1, "cost_usd": 0.042, "latency_s": 11.3}}
80
+ ```
81
+
82
+ ### 3. Analyze
83
+
84
+ ```bash
85
+ agent-attrib analyze runs.jsonl --metric success --metric cost_usd --margin 0.05
86
+ ```
87
+
88
+ ## Python API
89
+
90
+ ```python
91
+ from agent_attrib import plan, analyze, load_runs
92
+
93
+ design = plan(factors=["prompt", "model", "retriever"], tasks=30, trials=2, seed=7)
94
+ print(design.summary())
95
+
96
+ runset = load_runs("runs.jsonl")
97
+ result = analyze(runset, metrics=["success", "cost_usd"], margin=0.05)
98
+ print(result.to_markdown())
99
+ ```
100
+
101
+ ## Input format
102
+
103
+ **JSONL** (one object per run):
104
+
105
+ ```json
106
+ {"run_id": "r001", "task_id": "t07", "trial": 1,
107
+ "factors": {"prompt": "candidate", "model": "baseline", "retriever": "candidate"},
108
+ "metrics": {"success": 1, "cost_usd": 0.042, "latency_s": 11.3}}
109
+ ```
110
+
111
+ **CSV** with columns `run_id, task_id, trial, f_<factor>..., m_<metric>...`:
112
+
113
+ ```csv
114
+ run_id,task_id,trial,f_prompt,f_model,m_success,m_cost_usd
115
+ r001,t07,1,candidate,baseline,1,0.042
116
+ ```
117
+
118
+ Factor levels must be exactly `baseline` or `candidate`. Metrics must be numeric.
119
+
120
+ ## Method
121
+
122
+ For each metric, `analyze` fits OLS with task fixed effects:
123
+
124
+ ```
125
+ metric ~ task fixed effects + main effects + estimable two-factor interactions
126
+ ```
127
+
128
+ - **Binary `success`:** linear probability model. Effects are in percentage points.
129
+ Not logistic regression: effects stay directly interpretable as probability differences.
130
+ - **Cost and latency:** `log(metric)` model. Effects reported as percent change.
131
+ - **Standard errors:** cluster-robust, clustered by task.
132
+ - **Multiple testing:** Holm step-down correction across all effects within a metric.
133
+ - **Verdicts** (given an equivalence margin `m`):
134
+ - `IMPROVEMENT` -- 95% CI sits entirely above +m
135
+ - `REGRESSION` -- 95% CI sits entirely below -m
136
+ - `EQUIVALENT` -- 95% CI lies within +/-m
137
+ - `INCONCLUSIVE` -- CI straddles a boundary; includes approximate tasks needed to resolve
138
+ - Effect convention: switching a factor from baseline to candidate (= 2*beta under +/-1 coding).
139
+
140
+ ## Designs
141
+
142
+ | Factors | Design | Runs (30 tasks, 2 trials) |
143
+ |---------|--------|--------------------------|
144
+ | 1-4 | 2^k full factorial | 60-480 |
145
+ | 5 | 2^(5-1) res IV | 960 |
146
+ | 6 | 2^(6-2) res IV | 960 |
147
+ | 7 | 2^(7-3) res IV | 960 |
148
+
149
+ Fractional designs print their alias structure. Use `--resolution V` to request resolution V (more runs, no 2FI aliasing).
150
+
151
+ ## Limitations
152
+
153
+ - Results apply to this task suite and model versions only.
154
+ - Attribution requires designed runs -- not observational production logs.
155
+ - Maximum 7 factors in v0.1.
156
+ - Two levels per factor only (baseline vs candidate).
157
+ - No LLM-as-judge; `agent-attrib` never calls any LLM.
158
+
159
+ ## Prior art
160
+
161
+ - **agentstats, AgentAssay, deltagate:** proper A/B statistics for a single pair. Do not decompose changes across multiple simultaneous factors.
162
+ - **ablate-cli:** per-section ablation of `CLAUDE.md` files, for coding agents, runs Claude Code itself. Borrowed two ideas: four-way verdicts and printing the minimum detectable effect before any spend.
163
+ - **Research papers:** factorial ablations over agent components exist as one-off scripts.
164
+
165
+ `agent-attrib`'s niche: framework-agnostic, multi-factor, interaction-aware attribution for arbitrary release changes, decoupled from execution.
166
+
167
+ ## Development
168
+
169
+ ```bash
170
+ git clone https://github.com/agent-attrib/agent-attrib
171
+ cd agent-attrib
172
+ uv sync --all-groups
173
+ uv run pytest -m "not slow" # fast tests (~seconds)
174
+ uv run pytest # all tests including calibration (~minutes)
175
+ uv run ruff check src tests
176
+ uv run mypy src
177
+ ```
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,152 @@
1
+ # agent-attrib
2
+
3
+ A Python package for attributing metric changes across simultaneous agent release changes using factorial design of experiments.
4
+
5
+ **The problem:** You shipped a release that changed the prompt, the model, and the retriever at once. Your eval pass rate moved. Which change caused it, and by how much?
6
+
7
+ **The solution:** Design a factorial experiment, run your agent on the configurations it specifies, and let `agent-attrib` decompose the effects with confidence intervals.
8
+
9
+ ```bash
10
+ pip install agent-attrib
11
+ ```
12
+
13
+ ## 60-second demo
14
+
15
+ ```bash
16
+ agent-attrib demo
17
+ ```
18
+
19
+ No LLM calls, no cost. Generates synthetic runs with planted effects and reports which factors are responsible.
20
+
21
+ ## Usage
22
+
23
+ ### 1. Design the experiment
24
+
25
+ ```bash
26
+ agent-attrib plan --factors prompt,model,retriever --tasks 30 --trials 2 -o design.csv
27
+ ```
28
+
29
+ ```
30
+ Design type : 2^3 full factorial
31
+ Resolution : full
32
+ Factors : prompt, model, retriever
33
+ Tasks : 30
34
+ Trials/config: 2
35
+ Configurations: 8
36
+ Total runs : 480
37
+ Seed : 42
38
+ MDE (success, α=0.05, power=0.8): 0.1283
39
+ ```
40
+
41
+ For 5-7 factors, a fractional factorial is used automatically (resolution IV by default).
42
+
43
+ ### 2. Run your agent
44
+
45
+ For each row in `design.csv`, set each factor to the specified level (`baseline` or `candidate`) and run your agent on the task. Write one line per run:
46
+
47
+ ```json
48
+ {"run_id": "r001", "task_id": "t0001", "trial": 1,
49
+ "factors": {"prompt": "candidate", "model": "baseline", "retriever": "candidate"},
50
+ "metrics": {"success": 1, "cost_usd": 0.042, "latency_s": 11.3}}
51
+ ```
52
+
53
+ ### 3. Analyze
54
+
55
+ ```bash
56
+ agent-attrib analyze runs.jsonl --metric success --metric cost_usd --margin 0.05
57
+ ```
58
+
59
+ ## Python API
60
+
61
+ ```python
62
+ from agent_attrib import plan, analyze, load_runs
63
+
64
+ design = plan(factors=["prompt", "model", "retriever"], tasks=30, trials=2, seed=7)
65
+ print(design.summary())
66
+
67
+ runset = load_runs("runs.jsonl")
68
+ result = analyze(runset, metrics=["success", "cost_usd"], margin=0.05)
69
+ print(result.to_markdown())
70
+ ```
71
+
72
+ ## Input format
73
+
74
+ **JSONL** (one object per run):
75
+
76
+ ```json
77
+ {"run_id": "r001", "task_id": "t07", "trial": 1,
78
+ "factors": {"prompt": "candidate", "model": "baseline", "retriever": "candidate"},
79
+ "metrics": {"success": 1, "cost_usd": 0.042, "latency_s": 11.3}}
80
+ ```
81
+
82
+ **CSV** with columns `run_id, task_id, trial, f_<factor>..., m_<metric>...`:
83
+
84
+ ```csv
85
+ run_id,task_id,trial,f_prompt,f_model,m_success,m_cost_usd
86
+ r001,t07,1,candidate,baseline,1,0.042
87
+ ```
88
+
89
+ Factor levels must be exactly `baseline` or `candidate`. Metrics must be numeric.
90
+
91
+ ## Method
92
+
93
+ For each metric, `analyze` fits OLS with task fixed effects:
94
+
95
+ ```
96
+ metric ~ task fixed effects + main effects + estimable two-factor interactions
97
+ ```
98
+
99
+ - **Binary `success`:** linear probability model. Effects are in percentage points.
100
+ Not logistic regression: effects stay directly interpretable as probability differences.
101
+ - **Cost and latency:** `log(metric)` model. Effects reported as percent change.
102
+ - **Standard errors:** cluster-robust, clustered by task.
103
+ - **Multiple testing:** Holm step-down correction across all effects within a metric.
104
+ - **Verdicts** (given an equivalence margin `m`):
105
+ - `IMPROVEMENT` -- 95% CI sits entirely above +m
106
+ - `REGRESSION` -- 95% CI sits entirely below -m
107
+ - `EQUIVALENT` -- 95% CI lies within +/-m
108
+ - `INCONCLUSIVE` -- CI straddles a boundary; includes approximate tasks needed to resolve
109
+ - Effect convention: switching a factor from baseline to candidate (= 2*beta under +/-1 coding).
110
+
111
+ ## Designs
112
+
113
+ | Factors | Design | Runs (30 tasks, 2 trials) |
114
+ |---------|--------|--------------------------|
115
+ | 1-4 | 2^k full factorial | 60-480 |
116
+ | 5 | 2^(5-1) res IV | 960 |
117
+ | 6 | 2^(6-2) res IV | 960 |
118
+ | 7 | 2^(7-3) res IV | 960 |
119
+
120
+ Fractional designs print their alias structure. Use `--resolution V` to request resolution V (more runs, no 2FI aliasing).
121
+
122
+ ## Limitations
123
+
124
+ - Results apply to this task suite and model versions only.
125
+ - Attribution requires designed runs -- not observational production logs.
126
+ - Maximum 7 factors in v0.1.
127
+ - Two levels per factor only (baseline vs candidate).
128
+ - No LLM-as-judge; `agent-attrib` never calls any LLM.
129
+
130
+ ## Prior art
131
+
132
+ - **agentstats, AgentAssay, deltagate:** proper A/B statistics for a single pair. Do not decompose changes across multiple simultaneous factors.
133
+ - **ablate-cli:** per-section ablation of `CLAUDE.md` files, for coding agents, runs Claude Code itself. Borrowed two ideas: four-way verdicts and printing the minimum detectable effect before any spend.
134
+ - **Research papers:** factorial ablations over agent components exist as one-off scripts.
135
+
136
+ `agent-attrib`'s niche: framework-agnostic, multi-factor, interaction-aware attribution for arbitrary release changes, decoupled from execution.
137
+
138
+ ## Development
139
+
140
+ ```bash
141
+ git clone https://github.com/agent-attrib/agent-attrib
142
+ cd agent-attrib
143
+ uv sync --all-groups
144
+ uv run pytest -m "not slow" # fast tests (~seconds)
145
+ uv run pytest # all tests including calibration (~minutes)
146
+ uv run ruff check src tests
147
+ uv run mypy src
148
+ ```
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1,97 @@
1
+ [project]
2
+ name = "agent-attrib"
3
+ version = "0.1.0"
4
+ description = "Multi-factor attribution for agent release changes using factorial design of experiments"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ keywords = [
8
+ "agents",
9
+ "llm",
10
+ "attribution",
11
+ "factorial",
12
+ "design-of-experiments",
13
+ "statistics",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ "Topic :: Scientific/Engineering :: Information Analysis",
26
+ ]
27
+ dependencies = [
28
+ "numpy>=1.26",
29
+ "pandas>=2.1",
30
+ "scipy>=1.11",
31
+ "statsmodels>=0.14",
32
+ "pydantic>=2.5",
33
+ "typer>=0.9",
34
+ "rich>=13.7",
35
+ ]
36
+
37
+ [project.license]
38
+ text = "MIT"
39
+
40
+ [[project.authors]]
41
+ name = "agent-attrib contributors"
42
+
43
+ [project.scripts]
44
+ agent-attrib = "agent_attrib.cli:app"
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/agent-attrib/agent-attrib"
48
+ Issues = "https://github.com/agent-attrib/agent-attrib/issues"
49
+
50
+ [build-system]
51
+ requires = ["uv_build>=0.13.7,<0.14.0"]
52
+ build-backend = "uv_build"
53
+
54
+ [dependency-groups]
55
+ dev = [
56
+ "pytest>=8.0",
57
+ "pytest-cov>=5.0",
58
+ "hypothesis>=6.100",
59
+ "ruff>=0.4",
60
+ "mypy>=1.10",
61
+ "pandas-stubs>=2.1",
62
+ ]
63
+
64
+ [tool.ruff]
65
+ line-length = 100
66
+ target-version = "py311"
67
+
68
+ [tool.ruff.lint]
69
+ select = [
70
+ "E",
71
+ "F",
72
+ "I",
73
+ "UP",
74
+ "B",
75
+ "SIM",
76
+ ]
77
+ ignore = ["E501"]
78
+
79
+ [tool.mypy]
80
+ python_version = "3.11"
81
+ strict = true
82
+ ignore_missing_imports = true
83
+
84
+ [tool.pytest.ini_options]
85
+ testpaths = ["tests"]
86
+ markers = ["slow: marks tests as slow (deselect with -m 'not slow')"]
87
+ addopts = "--tb=short"
88
+
89
+ [tool.coverage.run]
90
+ source = ["src/agent_attrib"]
91
+ omit = ["*/cli.py"]
92
+
93
+ [tool.coverage.report]
94
+ exclude_lines = [
95
+ "pragma: no cover",
96
+ "if TYPE_CHECKING:",
97
+ ]
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "agent-attrib"
3
+ version = "0.1.0"
4
+ description = "Multi-factor attribution for agent release changes using factorial design of experiments"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ authors = [{ name = "agent-attrib contributors" }]
8
+ requires-python = ">=3.11"
9
+ keywords = ["agents", "llm", "attribution", "factorial", "design-of-experiments", "statistics"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "Intended Audience :: Science/Research",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
+ "Topic :: Scientific/Engineering :: Information Analysis",
21
+ ]
22
+ dependencies = [
23
+ "numpy>=1.26",
24
+ "pandas>=2.1",
25
+ "scipy>=1.11",
26
+ "statsmodels>=0.14",
27
+ "pydantic>=2.5",
28
+ "typer>=0.9",
29
+ "rich>=13.7",
30
+ ]
31
+
32
+ [project.scripts]
33
+ agent-attrib = "agent_attrib.cli:app"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/agent-attrib/agent-attrib"
37
+ Issues = "https://github.com/agent-attrib/agent-attrib/issues"
38
+
39
+ [build-system]
40
+ requires = ["uv_build>=0.13.7,<0.14.0"]
41
+ build-backend = "uv_build"
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "pytest>=8.0",
46
+ "pytest-cov>=5.0",
47
+ "hypothesis>=6.100",
48
+ "ruff>=0.4",
49
+ "mypy>=1.10",
50
+ "pandas-stubs>=2.1",
51
+ ]
52
+
53
+ [tool.ruff]
54
+ line-length = 100
55
+ target-version = "py311"
56
+
57
+ [tool.ruff.lint]
58
+ select = ["E", "F", "I", "UP", "B", "SIM"]
59
+ ignore = ["E501"]
60
+
61
+ [tool.mypy]
62
+ python_version = "3.11"
63
+ strict = true
64
+ ignore_missing_imports = true
65
+
66
+ [tool.pytest.ini_options]
67
+ testpaths = ["tests"]
68
+ markers = ["slow: marks tests as slow (deselect with -m 'not slow')"]
69
+ addopts = "--tb=short"
70
+
71
+ [tool.coverage.run]
72
+ source = ["src/agent_attrib"]
73
+ omit = ["*/cli.py"]
74
+
75
+ [tool.coverage.report]
76
+ exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"]
@@ -0,0 +1,124 @@
1
+ """agent-attrib: multi-factor attribution for agent release changes."""
2
+
3
+ from .model import MetricResult, analyze_runset
4
+ from .schema import Design, Run, RunSet, load_runs
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "plan",
9
+ "analyze",
10
+ "load_runs",
11
+ "RunSet",
12
+ "Run",
13
+ "Design",
14
+ "MetricResult",
15
+ "__version__",
16
+ ]
17
+
18
+
19
+ def plan(
20
+ factors: list[str],
21
+ tasks: int = 30,
22
+ trials: int = 2,
23
+ seed: int = 42,
24
+ resolution: str = "IV",
25
+ sd: float | None = None,
26
+ ) -> "Design":
27
+ """Generate a factorial experiment design.
28
+
29
+ Args:
30
+ factors: list of factor names.
31
+ tasks: number of distinct tasks.
32
+ trials: trials per configuration per task.
33
+ seed: random seed for run-order randomization.
34
+ resolution: minimum design resolution for fractional designs ("IV" or "V").
35
+ sd: assumed SD for MDE calculation on continuous metrics.
36
+
37
+ Returns:
38
+ Design object with run order and summary statistics.
39
+ """
40
+ from .design import build_design, generate_run_order
41
+ from .power import mde_binary, mde_continuous
42
+ from .schema import Design, DesignRow
43
+
44
+ dm_matrix, dm = build_design(factors, tasks=tasks, trials=trials, seed=seed, resolution=resolution)
45
+ task_ids = [f"t{i:04d}" for i in range(tasks)]
46
+ df = generate_run_order(dm_matrix, factors, task_ids, trials, seed)
47
+
48
+ n_configs = dm_matrix.shape[0]
49
+ n_total = n_configs * tasks * trials
50
+
51
+ mde_val = mde_continuous(n_configs, tasks, trials, sd=sd) if sd else mde_binary(n_configs, tasks, trials)
52
+
53
+ rows: list[DesignRow] = []
54
+ for _, row in df.iterrows():
55
+ factor_levels = {f: str(row[f"f_{f}"]) for f in factors}
56
+ rows.append(
57
+ DesignRow(
58
+ config_id=str(row["config_id"]),
59
+ task_id=str(row["task_id"]),
60
+ trial=int(row["trial"]),
61
+ run_order=int(row["run_order"]),
62
+ factors=factor_levels, # type: ignore[arg-type]
63
+ )
64
+ )
65
+
66
+ return Design(
67
+ factors=factors,
68
+ tasks=tasks,
69
+ trials=trials,
70
+ seed=seed,
71
+ design_type=dm.design_type,
72
+ resolution=dm.resolution,
73
+ alias_structure=dm.alias_structure,
74
+ runs=rows,
75
+ n_configs=n_configs,
76
+ n_total=n_total,
77
+ mde=mde_val,
78
+ mde_metric="success (binary, p=0.5)" if sd is None else "continuous",
79
+ )
80
+
81
+
82
+ def analyze(
83
+ runset: "RunSet",
84
+ metrics: list[str] | None = None,
85
+ margin: float = 0.05,
86
+ binary_metrics: list[str] | None = None,
87
+ ) -> "AnalysisResult":
88
+ """Analyze a RunSet and return an AnalysisResult.
89
+
90
+ Args:
91
+ runset: loaded runs from load_runs().
92
+ metrics: metrics to analyze (default: all in runset).
93
+ margin: equivalence margin for EQUIVALENT/INCONCLUSIVE verdicts.
94
+ binary_metrics: metric names to treat as binary (default: ["success"]).
95
+
96
+ Returns:
97
+ AnalysisResult with per-metric results and reporting methods.
98
+ """
99
+ metrics = metrics or runset.metrics
100
+ results = analyze_runset(runset, metrics=metrics, margin=margin, binary_metrics=binary_metrics)
101
+ return AnalysisResult(results)
102
+
103
+
104
+ class AnalysisResult:
105
+ """Container for per-metric analysis results with reporting methods."""
106
+
107
+ def __init__(self, results: list["MetricResult"]) -> None:
108
+ self.results = results
109
+
110
+ def to_markdown(self) -> str:
111
+ from .report import to_markdown
112
+ return to_markdown(self.results)
113
+
114
+ def to_json(self) -> str:
115
+ from .report import to_json
116
+ return to_json(self.results)
117
+
118
+ def print(self) -> None:
119
+ from .report import print_rich
120
+ print_rich(self.results)
121
+
122
+ def __repr__(self) -> str:
123
+ metrics = [r.metric for r in self.results]
124
+ return f"AnalysisResult(metrics={metrics})"