repobench 0.1.0__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.
- agentfit/__init__.py +3 -0
- agentfit/analysis/__init__.py +0 -0
- agentfit/analysis/metrics.py +58 -0
- agentfit/analysis/recommendation.py +104 -0
- agentfit/analysis/statistics.py +71 -0
- agentfit/benchmark/__init__.py +0 -0
- agentfit/benchmark/coverage.py +51 -0
- agentfit/benchmark/health.py +191 -0
- agentfit/benchmark/sampling.py +152 -0
- agentfit/cli/__init__.py +0 -0
- agentfit/cli/analyze.py +576 -0
- agentfit/cli/app.py +210 -0
- agentfit/cli/benchmark.py +295 -0
- agentfit/cli/candidates.py +119 -0
- agentfit/cli/config_cmd.py +106 -0
- agentfit/cli/doctor.py +240 -0
- agentfit/cli/init.py +284 -0
- agentfit/cli/report.py +224 -0
- agentfit/cli/run.py +358 -0
- agentfit/cli/task.py +169 -0
- agentfit/cli/telemetry.py +103 -0
- agentfit/cli/utils.py +46 -0
- agentfit/config.py +32 -0
- agentfit/harbor/__init__.py +0 -0
- agentfit/harbor/exporter.py +206 -0
- agentfit/harbor/parser.py +109 -0
- agentfit/harbor/runner.py +321 -0
- agentfit/logging.py +46 -0
- agentfit/mining/__init__.py +0 -0
- agentfit/mining/candidates.py +219 -0
- agentfit/models.py +375 -0
- agentfit/reporting/__init__.py +0 -0
- agentfit/reporting/json.py +61 -0
- agentfit/reporting/terminal.py +171 -0
- agentfit/repository/__init__.py +0 -0
- agentfit/repository/detection.py +424 -0
- agentfit/repository/git.py +309 -0
- agentfit/repository/github.py +261 -0
- agentfit/repository/workload.py +339 -0
- agentfit/storage/__init__.py +0 -0
- agentfit/storage/database.py +486 -0
- agentfit/storage/migrations/__init__.py +0 -0
- agentfit/tasks/__init__.py +0 -0
- agentfit/tasks/instruction.py +165 -0
- agentfit/tasks/leakage.py +139 -0
- agentfit/tasks/verifier.py +220 -0
- agentfit/utils.py +80 -0
- agentfit/validation/__init__.py +0 -0
- agentfit/validation/determinism.py +52 -0
- agentfit/validation/environment.py +160 -0
- agentfit/validation/noop.py +81 -0
- agentfit/validation/oracle.py +102 -0
- repobench-0.1.0.dist-info/METADATA +247 -0
- repobench-0.1.0.dist-info/RECORD +56 -0
- repobench-0.1.0.dist-info/WHEEL +4 -0
- repobench-0.1.0.dist-info/entry_points.txt +2 -0
agentfit/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Configuration metrics aggregation from trials."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import statistics
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from agentfit.logging import get_logger
|
|
9
|
+
from agentfit.models import ConfigMetrics, Trial
|
|
10
|
+
from agentfit.analysis.statistics import wilson_ci
|
|
11
|
+
|
|
12
|
+
log = get_logger("analysis.metrics")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute_config_metrics(trials: Iterable[Trial]) -> ConfigMetrics:
|
|
16
|
+
"""Compute aggregated metrics for one agent configuration."""
|
|
17
|
+
trials = list(trials)
|
|
18
|
+
metrics = ConfigMetrics()
|
|
19
|
+
|
|
20
|
+
if not trials:
|
|
21
|
+
return metrics
|
|
22
|
+
|
|
23
|
+
solved = [t for t in trials if t.solved]
|
|
24
|
+
metrics.solved = len(solved)
|
|
25
|
+
metrics.total = len(trials)
|
|
26
|
+
metrics.pass_rate = len(solved) / len(trials)
|
|
27
|
+
|
|
28
|
+
# Wilson 95% CI
|
|
29
|
+
ci_lower, ci_upper = wilson_ci(len(solved), len(trials))
|
|
30
|
+
metrics.ci_lower = ci_lower
|
|
31
|
+
metrics.ci_upper = ci_upper
|
|
32
|
+
|
|
33
|
+
# Economics
|
|
34
|
+
costs = [t.cost_usd for t in trials if t.cost_usd is not None]
|
|
35
|
+
if costs:
|
|
36
|
+
metrics.total_cost = sum(costs)
|
|
37
|
+
metrics.mean_cost_task = metrics.total_cost / len(costs)
|
|
38
|
+
if metrics.solved > 0:
|
|
39
|
+
metrics.cost_per_solve = metrics.total_cost / metrics.solved
|
|
40
|
+
|
|
41
|
+
# Efficiency
|
|
42
|
+
prompt_tokens = [t.prompt_tokens for t in trials if t.prompt_tokens is not None]
|
|
43
|
+
completion_tokens = [t.completion_tokens for t in trials if t.completion_tokens is not None]
|
|
44
|
+
metrics.total_prompt_tokens = sum(prompt_tokens)
|
|
45
|
+
metrics.total_completion_tokens = sum(completion_tokens)
|
|
46
|
+
if metrics.solved > 0:
|
|
47
|
+
total_tokens = metrics.total_prompt_tokens + metrics.total_completion_tokens
|
|
48
|
+
metrics.tokens_per_solve = round(total_tokens / metrics.solved)
|
|
49
|
+
|
|
50
|
+
# Performance
|
|
51
|
+
durations = [t.duration_ms for t in trials if t.duration_ms is not None]
|
|
52
|
+
if durations:
|
|
53
|
+
metrics.p50_duration_ms = round(statistics.median(durations))
|
|
54
|
+
sorted_d = sorted(durations)
|
|
55
|
+
p90_idx = min(len(sorted_d) - 1, round(0.9 * (len(sorted_d) - 1)))
|
|
56
|
+
metrics.p90_duration_ms = sorted_d[p90_idx]
|
|
57
|
+
|
|
58
|
+
return metrics
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Cost-aware recommendation logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from agentfit.logging import get_logger
|
|
8
|
+
from agentfit.models import ConfigMetrics
|
|
9
|
+
|
|
10
|
+
log = get_logger("analysis.recommendation")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def recommend(
|
|
14
|
+
metrics_dict: dict[str, ConfigMetrics],
|
|
15
|
+
) -> tuple[str | None, str]:
|
|
16
|
+
"""Recommend a default agent configuration.
|
|
17
|
+
|
|
18
|
+
Policy (``cost_effective``):
|
|
19
|
+
1. Find the configuration with the highest observed pass rate.
|
|
20
|
+
2. Identify configurations whose quality difference is not
|
|
21
|
+
statistically conclusive (overlapping Wilson CIs).
|
|
22
|
+
3. Within that set, choose the lowest cost per verified solve.
|
|
23
|
+
|
|
24
|
+
Returns (config_name, reason).
|
|
25
|
+
"""
|
|
26
|
+
if not metrics_dict:
|
|
27
|
+
return None, "No configurations with results available."
|
|
28
|
+
|
|
29
|
+
# Consider only configs with trials
|
|
30
|
+
with_results = {name: m for name, m in metrics_dict.items() if m.total > 0}
|
|
31
|
+
if not with_results:
|
|
32
|
+
return None, "No configurations have completed trials."
|
|
33
|
+
|
|
34
|
+
# 1. Best observed pass rate
|
|
35
|
+
best_name = max(with_results, key=lambda n: with_results[n].pass_rate)
|
|
36
|
+
best = with_results[best_name]
|
|
37
|
+
|
|
38
|
+
# 2. Statistically indistinguishable set (overlapping Wilson CIs)
|
|
39
|
+
indistinguishable = []
|
|
40
|
+
for name, m in with_results.items():
|
|
41
|
+
if _cis_overlap(best, m):
|
|
42
|
+
indistinguishable.append(name)
|
|
43
|
+
|
|
44
|
+
# 3. Lowest cost per verified solve among indistinguishable
|
|
45
|
+
with_cost = [
|
|
46
|
+
(name, with_results[name])
|
|
47
|
+
for name in indistinguishable
|
|
48
|
+
if with_results[name].cost_per_solve is not None
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
if with_cost:
|
|
52
|
+
recommended = min(with_cost, key=lambda nm: nm[1].cost_per_solve)[0]
|
|
53
|
+
reason = (
|
|
54
|
+
f"lowest cost per verified solve among configurations "
|
|
55
|
+
f"statistically indistinguishable from observed best quality."
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
# No cost data: pick best observed pass rate
|
|
59
|
+
recommended = best_name
|
|
60
|
+
reason = "best observed pass rate (no cost data available)."
|
|
61
|
+
|
|
62
|
+
log.info("Recommendation: %s (%s)", recommended, reason)
|
|
63
|
+
return recommended, reason
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def pareto_frontier(
|
|
67
|
+
metrics_dict: dict[str, ConfigMetrics],
|
|
68
|
+
) -> list[str]:
|
|
69
|
+
"""Identify configurations on the Pareto frontier.
|
|
70
|
+
|
|
71
|
+
A configuration is dominated if another config has >= pass rate AND
|
|
72
|
+
<= cost per solve (with at least one strict inequality).
|
|
73
|
+
|
|
74
|
+
Returns config names on the frontier (highest quality first).
|
|
75
|
+
"""
|
|
76
|
+
with_cost = {
|
|
77
|
+
name: m for name, m in metrics_dict.items()
|
|
78
|
+
if m.total > 0 and m.cost_per_solve is not None
|
|
79
|
+
}
|
|
80
|
+
if not with_cost:
|
|
81
|
+
return list(metrics_dict.keys())
|
|
82
|
+
|
|
83
|
+
frontier: list[str] = []
|
|
84
|
+
for name, m in with_cost.items():
|
|
85
|
+
dominated = False
|
|
86
|
+
for other_name, other in with_cost.items():
|
|
87
|
+
if other_name == name:
|
|
88
|
+
continue
|
|
89
|
+
if (other.pass_rate >= m.pass_rate
|
|
90
|
+
and other.cost_per_solve <= m.cost_per_solve
|
|
91
|
+
and (other.pass_rate > m.pass_rate
|
|
92
|
+
or other.cost_per_solve < m.cost_per_solve)):
|
|
93
|
+
dominated = True
|
|
94
|
+
break
|
|
95
|
+
if not dominated:
|
|
96
|
+
frontier.append(name)
|
|
97
|
+
|
|
98
|
+
frontier.sort(key=lambda n: with_cost[n].pass_rate, reverse=True)
|
|
99
|
+
return frontier
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _cis_overlap(a: ConfigMetrics, b: ConfigMetrics) -> bool:
|
|
103
|
+
"""Check if two Wilson CIs overlap (statistically indistinguishable)."""
|
|
104
|
+
return not (a.ci_upper < b.ci_lower or b.ci_upper < a.ci_lower)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Statistical utilities: Wilson CI and paired bootstrap."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import random
|
|
7
|
+
from typing import Iterable, Sequence
|
|
8
|
+
|
|
9
|
+
from agentfit.logging import get_logger
|
|
10
|
+
|
|
11
|
+
log = get_logger("analysis.statistics")
|
|
12
|
+
|
|
13
|
+
_Z_95 = 1.959963984540054 # z-value for 95% confidence
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def wilson_ci(successes: int, total: int, z: float = _Z_95) -> tuple[float, float]:
|
|
17
|
+
"""Wilson score interval for a binomial proportion.
|
|
18
|
+
|
|
19
|
+
Returns (lower, upper) bounds. If total is 0, returns (0, 0).
|
|
20
|
+
"""
|
|
21
|
+
if total <= 0:
|
|
22
|
+
return 0.0, 0.0
|
|
23
|
+
|
|
24
|
+
p = successes / total
|
|
25
|
+
z2 = z * z
|
|
26
|
+
denom = 1 + z2 / total
|
|
27
|
+
center = (p + z2 / (2 * total)) / denom
|
|
28
|
+
margin = z * math.sqrt(p * (1 - p) / total + z2 / (4 * total * total)) / denom
|
|
29
|
+
|
|
30
|
+
return max(0.0, center - margin), min(1.0, center + margin)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def paired_bootstrap_difference(
|
|
34
|
+
outcomes_a: Sequence[bool],
|
|
35
|
+
outcomes_b: Sequence[bool],
|
|
36
|
+
n_bootstrap: int = 10_000,
|
|
37
|
+
seed: int = 42,
|
|
38
|
+
) -> tuple[float, float, float]:
|
|
39
|
+
"""Paired bootstrap difference of pass rates.
|
|
40
|
+
|
|
41
|
+
Performs a paired bootstrap (resampling task indices with replacement)
|
|
42
|
+
with a deterministic seed.
|
|
43
|
+
|
|
44
|
+
Returns (observed_difference_pp, ci_lower_pp, ci_upper_pp) where
|
|
45
|
+
difference = pass_rate_a - pass_rate_b (in percentage points).
|
|
46
|
+
"""
|
|
47
|
+
if len(outcomes_a) != len(outcomes_b):
|
|
48
|
+
raise ValueError("Paired bootstrap requires equal-length outcome arrays")
|
|
49
|
+
|
|
50
|
+
n = len(outcomes_a)
|
|
51
|
+
if n == 0:
|
|
52
|
+
return 0.0, 0.0, 0.0
|
|
53
|
+
|
|
54
|
+
a = [1 if x else 0 for x in outcomes_a]
|
|
55
|
+
b = [1 if x else 0 for x in outcomes_b]
|
|
56
|
+
|
|
57
|
+
observed_diff = (sum(a) - sum(b)) / n * 100
|
|
58
|
+
|
|
59
|
+
rng = random.Random(seed)
|
|
60
|
+
diffs: list[float] = []
|
|
61
|
+
for _ in range(n_bootstrap):
|
|
62
|
+
idx = [rng.randrange(n) for _ in range(n)]
|
|
63
|
+
sum_a = sum(a[i] for i in idx)
|
|
64
|
+
sum_b = sum(b[i] for i in idx)
|
|
65
|
+
diffs.append((sum_a - sum_b) / n * 100)
|
|
66
|
+
|
|
67
|
+
diffs.sort()
|
|
68
|
+
lo_idx = max(0, int(0.025 * n_bootstrap) - 1)
|
|
69
|
+
hi_idx = min(n_bootstrap - 1, int(0.975 * n_bootstrap) - 1)
|
|
70
|
+
|
|
71
|
+
return observed_diff, diffs[lo_idx], diffs[hi_idx]
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Coverage calculation: TVD converted to coverage percentages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def calculate_coverage(
|
|
10
|
+
benchmark_dist: dict[str, dict[str, float]],
|
|
11
|
+
workload_dist: dict[str, dict[str, float]],
|
|
12
|
+
) -> dict[str, float]:
|
|
13
|
+
"""Calculate coverage per dimension.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
benchmark_dist: {dimension: {category: probability}}
|
|
17
|
+
workload_dist: {dimension: {category: probability}}
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
{dimension: coverage_percentage} where coverage = 100 * (1 - TVD).
|
|
21
|
+
"""
|
|
22
|
+
coverage: dict[str, float] = {}
|
|
23
|
+
|
|
24
|
+
all_dims = set(benchmark_dist.keys()) | set(workload_dist.keys())
|
|
25
|
+
for dim in all_dims:
|
|
26
|
+
b = benchmark_dist.get(dim, {})
|
|
27
|
+
w = workload_dist.get(dim, {})
|
|
28
|
+
tvd = _tvd_from_dicts(b, w)
|
|
29
|
+
coverage[dim] = round(100 * (1 - tvd), 1)
|
|
30
|
+
|
|
31
|
+
return coverage
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def distributions_from_counts(
|
|
35
|
+
benchmark_counts: dict[str, Counter],
|
|
36
|
+
workload_counts: dict[str, Counter],
|
|
37
|
+
) -> dict[str, float]:
|
|
38
|
+
"""Convenience: compute coverage from Counter objects."""
|
|
39
|
+
b = {dim: {k: c / sum(c.values()) if sum(c.values()) else 0.0
|
|
40
|
+
for k, c in counts.items()} for dim, counts in benchmark_counts.items()}
|
|
41
|
+
w = {dim: {k: c / sum(c.values()) if sum(c.values()) else 0.0
|
|
42
|
+
for k, c in counts.items()} for dim, counts in workload_counts.items()}
|
|
43
|
+
return calculate_coverage(b, w)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _tvd_from_dicts(bench: dict[str, float], workload: dict[str, float]) -> float:
|
|
47
|
+
"""Total Variation Distance between two probability dicts."""
|
|
48
|
+
keys = set(bench.keys()) | set(workload.keys())
|
|
49
|
+
if not keys:
|
|
50
|
+
return 0.0
|
|
51
|
+
return 0.5 * sum(abs(bench.get(k, 0.0) - workload.get(k, 0.0)) for k in keys)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Benchmark Health calculation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agentfit.logging import get_logger
|
|
9
|
+
from agentfit.models import BenchmarkHealth, CandidateTask, NetworkIsolation
|
|
10
|
+
|
|
11
|
+
log = get_logger("benchmark.health")
|
|
12
|
+
|
|
13
|
+
# Weights from the PRD
|
|
14
|
+
_W_REPRESENTATIVENESS = 0.40
|
|
15
|
+
_W_VALIDATION = 0.20
|
|
16
|
+
_W_LEAKAGE = 0.15
|
|
17
|
+
_W_RECENCY = 0.15
|
|
18
|
+
_W_DIVERSITY = 0.10
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def calculate_health(
|
|
22
|
+
benchmark_tasks: list[CandidateTask],
|
|
23
|
+
workload_universe: list[CandidateTask] | None = None,
|
|
24
|
+
) -> BenchmarkHealth:
|
|
25
|
+
"""Compute the Benchmark Health score components.
|
|
26
|
+
|
|
27
|
+
Components (0-100 each):
|
|
28
|
+
- Representativeness: TVD-based coverage of workload distributions
|
|
29
|
+
- Validation: no-op/oracle/determinism validation strength
|
|
30
|
+
- Leakage: history sanitization, network isolation, credential removal
|
|
31
|
+
- Recency: temporal distribution of tasks
|
|
32
|
+
- Diversity: repetition of subsystem/complexity
|
|
33
|
+
|
|
34
|
+
Overall = weighted sum.
|
|
35
|
+
"""
|
|
36
|
+
health = BenchmarkHealth()
|
|
37
|
+
|
|
38
|
+
if not benchmark_tasks:
|
|
39
|
+
return health
|
|
40
|
+
|
|
41
|
+
health.representativeness = _representativeness(benchmark_tasks, workload_universe)
|
|
42
|
+
health.validation = _validation(benchmark_tasks)
|
|
43
|
+
health.leakage = _leakage(benchmark_tasks)
|
|
44
|
+
health.recency = _recency(benchmark_tasks)
|
|
45
|
+
health.diversity = _diversity(benchmark_tasks)
|
|
46
|
+
|
|
47
|
+
health.overall = round(
|
|
48
|
+
_W_REPRESENTATIVENESS * health.representativeness
|
|
49
|
+
+ _W_VALIDATION * health.validation
|
|
50
|
+
+ _W_LEAKAGE * health.leakage
|
|
51
|
+
+ _W_RECENCY * health.recency
|
|
52
|
+
+ _W_DIVERSITY * health.diversity
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return health
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _representativeness(
|
|
59
|
+
tasks: list[CandidateTask],
|
|
60
|
+
workload: list[CandidateTask] | None,
|
|
61
|
+
) -> int:
|
|
62
|
+
"""Representativeness from distribution coverage (TVD-based)."""
|
|
63
|
+
if not workload:
|
|
64
|
+
return 100
|
|
65
|
+
|
|
66
|
+
from agentfit.benchmark.coverage import calculate_coverage
|
|
67
|
+
|
|
68
|
+
# Build distributions
|
|
69
|
+
def _dist(tasks_list: list[CandidateTask], key: str) -> dict[str, float]:
|
|
70
|
+
from collections import Counter
|
|
71
|
+
counts = Counter(getattr(t, key) for t in tasks_list)
|
|
72
|
+
total = sum(counts.values())
|
|
73
|
+
if total == 0:
|
|
74
|
+
return {}
|
|
75
|
+
return {k: v / total for k, v in counts.items()}
|
|
76
|
+
|
|
77
|
+
bench_dist = {
|
|
78
|
+
"task_type": _dist(tasks, "task_type"),
|
|
79
|
+
"subsystem": _dist(tasks, "subsystem"),
|
|
80
|
+
"complexity": _dist(tasks, "complexity"),
|
|
81
|
+
}
|
|
82
|
+
work_dist = {
|
|
83
|
+
"task_type": _dist(workload, "task_type"),
|
|
84
|
+
"subsystem": _dist(workload, "subsystem"),
|
|
85
|
+
"complexity": _dist(workload, "complexity"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
coverage = calculate_coverage(bench_dist, work_dist)
|
|
89
|
+
return round(sum(coverage.values()) / max(len(coverage), 1))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _validation(tasks: list[CandidateTask]) -> int:
|
|
93
|
+
"""Validation confidence from eligibility flags."""
|
|
94
|
+
if not tasks:
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
scores = []
|
|
98
|
+
for t in tasks:
|
|
99
|
+
score = 100
|
|
100
|
+
e = t.eligibility
|
|
101
|
+
|
|
102
|
+
# No-op validated (must fail)
|
|
103
|
+
if getattr(e, "noop", None) is not True:
|
|
104
|
+
score -= 30
|
|
105
|
+
# Oracle validated (must pass)
|
|
106
|
+
if getattr(e, "oracle", None) is not True:
|
|
107
|
+
score -= 30
|
|
108
|
+
# Regression validated
|
|
109
|
+
if getattr(e, "regression", None) is not True:
|
|
110
|
+
score -= 10
|
|
111
|
+
# Determinism validated
|
|
112
|
+
if getattr(e, "determinism", None) is not True:
|
|
113
|
+
score -= 20
|
|
114
|
+
# Instruction provenance: Tier C reduces confidence
|
|
115
|
+
prov = getattr(t, "instruction_provenance", None)
|
|
116
|
+
if prov is not None and prov.value == "C":
|
|
117
|
+
score -= 15
|
|
118
|
+
|
|
119
|
+
scores.append(max(0, min(score, 100)))
|
|
120
|
+
|
|
121
|
+
return round(sum(scores) / len(scores))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _leakage(tasks: list[CandidateTask]) -> int:
|
|
125
|
+
"""Leakage resistance score."""
|
|
126
|
+
if not tasks:
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
scores = []
|
|
130
|
+
for t in tasks:
|
|
131
|
+
score = 100
|
|
132
|
+
# Network isolation
|
|
133
|
+
net = getattr(t, "network_isolation", NetworkIsolation.NONE)
|
|
134
|
+
if net == NetworkIsolation.NONE:
|
|
135
|
+
score -= 25
|
|
136
|
+
elif net == NetworkIsolation.PARTIAL:
|
|
137
|
+
score -= 10
|
|
138
|
+
|
|
139
|
+
# Leakage risk from scanner
|
|
140
|
+
risk = getattr(t, "leakage_risk", 0.0)
|
|
141
|
+
score -= round(risk * 40)
|
|
142
|
+
|
|
143
|
+
# History eligibility (sanitized snapshot implies history available)
|
|
144
|
+
e = getattr(t, "eligibility", None)
|
|
145
|
+
if e is not None and getattr(e, "leakage", None) is True:
|
|
146
|
+
pass # history sanitized
|
|
147
|
+
elif e is not None and getattr(e, "leakage", None) is False:
|
|
148
|
+
score -= 30
|
|
149
|
+
|
|
150
|
+
scores.append(max(0, min(score, 100)))
|
|
151
|
+
|
|
152
|
+
return round(sum(scores) / len(scores))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _recency(tasks: list[CandidateTask]) -> int:
|
|
156
|
+
"""Recency score: how recent are the tasks (0-100)."""
|
|
157
|
+
now = datetime.now(timezone.utc)
|
|
158
|
+
scores = []
|
|
159
|
+
for t in tasks:
|
|
160
|
+
created = getattr(t, "created_at", None)
|
|
161
|
+
if created is None:
|
|
162
|
+
scores.append(50)
|
|
163
|
+
continue
|
|
164
|
+
if created.tzinfo is None:
|
|
165
|
+
created = created.replace(tzinfo=timezone.utc)
|
|
166
|
+
age_days = max(0, (now - created).days)
|
|
167
|
+
# 180-day window: recency decays linearly
|
|
168
|
+
score = max(0, 100 - age_days * 100 / 180)
|
|
169
|
+
scores.append(score)
|
|
170
|
+
|
|
171
|
+
if not scores:
|
|
172
|
+
return 0
|
|
173
|
+
return round(sum(scores) / len(scores))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _diversity(tasks: list[CandidateTask]) -> int:
|
|
177
|
+
"""Diversity: penalize repeated subsystem/complexity combinations."""
|
|
178
|
+
if not tasks:
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
from collections import Counter
|
|
182
|
+
combos = Counter(
|
|
183
|
+
(t.subsystem or "unknown", t.complexity.value if t.complexity else "unknown")
|
|
184
|
+
for t in tasks
|
|
185
|
+
)
|
|
186
|
+
total = len(tasks)
|
|
187
|
+
max_repeat = max(combos.values())
|
|
188
|
+
|
|
189
|
+
# Ideal: all distinct (max_repeat=1). Score drops as max_repeat grows.
|
|
190
|
+
score = max(0, 100 - (max_repeat - 1) * 100 / max(total, 1))
|
|
191
|
+
return round(score)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Representative benchmark sampling via greedy stratified optimization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from agentfit.config import AgentFitConfig
|
|
9
|
+
from agentfit.logging import get_logger
|
|
10
|
+
from agentfit.models import CandidateTask, TaskStatus, TaskType, Complexity
|
|
11
|
+
|
|
12
|
+
log = get_logger("benchmark.sampling")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class _Dist:
|
|
17
|
+
"""A categorical distribution for one dimension."""
|
|
18
|
+
|
|
19
|
+
counts: Counter[str] = field(default_factory=Counter)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def total(self) -> int:
|
|
23
|
+
return sum(self.counts.values())
|
|
24
|
+
|
|
25
|
+
def prob(self, key: str) -> float:
|
|
26
|
+
if self.total == 0:
|
|
27
|
+
return 0.0
|
|
28
|
+
return self.counts.get(key, 0) / self.total
|
|
29
|
+
|
|
30
|
+
def add(self, key: str) -> None:
|
|
31
|
+
self.counts[key] += 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def workload_distribution(
|
|
35
|
+
workload: list[CandidateTask],
|
|
36
|
+
) -> tuple[_Dist, _Dist, _Dist]:
|
|
37
|
+
"""Build task_type, subsystem, and complexity distributions."""
|
|
38
|
+
type_dist = _Dist()
|
|
39
|
+
sub_dist = _Dist()
|
|
40
|
+
comp_dist = _Dist()
|
|
41
|
+
|
|
42
|
+
for task in workload:
|
|
43
|
+
type_dist.add(task.task_type.value)
|
|
44
|
+
sub_dist.add(task.subsystem or "unknown")
|
|
45
|
+
comp_dist.add(task.complexity.value)
|
|
46
|
+
|
|
47
|
+
return type_dist, sub_dist, comp_dist
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def select_benchmark(
|
|
51
|
+
candidates: list[CandidateTask],
|
|
52
|
+
workload: list[CandidateTask],
|
|
53
|
+
config: AgentFitConfig,
|
|
54
|
+
) -> list[CandidateTask]:
|
|
55
|
+
"""Select a representative sample from VALID candidates.
|
|
56
|
+
|
|
57
|
+
Implements greedy stratified optimization: for each slot, pick the
|
|
58
|
+
candidate that most reduces the TVD between the benchmark distribution
|
|
59
|
+
and the workload distribution, with a diversity penalty.
|
|
60
|
+
|
|
61
|
+
Returns a list of selected candidates (up to config.benchmark.size).
|
|
62
|
+
"""
|
|
63
|
+
valid = [c for c in candidates if c.status == TaskStatus.VALID]
|
|
64
|
+
if not valid:
|
|
65
|
+
log.warning("No VALID candidates available for sampling")
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
size = config.benchmark.size
|
|
69
|
+
if size <= 0:
|
|
70
|
+
size = 24
|
|
71
|
+
|
|
72
|
+
size = min(size, len(valid))
|
|
73
|
+
weights = config.benchmark.dimensions
|
|
74
|
+
|
|
75
|
+
# Workload distributions (target)
|
|
76
|
+
w_type, w_sub, w_comp = workload_distribution(workload or valid)
|
|
77
|
+
|
|
78
|
+
# Benchmark distributions (current selection)
|
|
79
|
+
b_type = _Dist()
|
|
80
|
+
b_sub = _Dist()
|
|
81
|
+
b_comp = _Dist()
|
|
82
|
+
|
|
83
|
+
selected: list[CandidateTask] = []
|
|
84
|
+
available = list(valid)
|
|
85
|
+
|
|
86
|
+
for _ in range(size):
|
|
87
|
+
best_candidate = None
|
|
88
|
+
best_score = float("inf")
|
|
89
|
+
|
|
90
|
+
for cand in available:
|
|
91
|
+
# Simulate inclusion
|
|
92
|
+
sim_type = _Dist(b_type.counts.copy())
|
|
93
|
+
sim_sub = _Dist(b_sub.counts.copy())
|
|
94
|
+
sim_comp = _Dist(b_comp.counts.copy())
|
|
95
|
+
sim_type.add(cand.task_type.value)
|
|
96
|
+
sim_sub.add(cand.subsystem or "unknown")
|
|
97
|
+
sim_comp.add(cand.complexity.value)
|
|
98
|
+
|
|
99
|
+
# Total variation distance per dimension
|
|
100
|
+
tvd_type = _tvd(sim_type, w_type)
|
|
101
|
+
tvd_sub = _tvd(sim_sub, w_sub)
|
|
102
|
+
tvd_comp = _tvd(sim_comp, w_comp)
|
|
103
|
+
|
|
104
|
+
score = (
|
|
105
|
+
weights.task_type * tvd_type
|
|
106
|
+
+ weights.subsystem * tvd_sub
|
|
107
|
+
+ weights.complexity * tvd_comp
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Diversity penalty: penalize candidates too similar to selected
|
|
111
|
+
penalty = _diversity_penalty(cand, selected)
|
|
112
|
+
score += penalty
|
|
113
|
+
|
|
114
|
+
if score < best_score:
|
|
115
|
+
best_score = score
|
|
116
|
+
best_candidate = cand
|
|
117
|
+
|
|
118
|
+
if best_candidate is None:
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
selected.append(best_candidate)
|
|
122
|
+
available.remove(best_candidate)
|
|
123
|
+
b_type.add(best_candidate.task_type.value)
|
|
124
|
+
b_sub.add(best_candidate.subsystem or "unknown")
|
|
125
|
+
b_comp.add(best_candidate.complexity.value)
|
|
126
|
+
|
|
127
|
+
log.info("Selected %d tasks for benchmark (requested %d)", len(selected), size)
|
|
128
|
+
return selected
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _tvd(bench: _Dist, workload: _Dist) -> float:
|
|
132
|
+
"""Total Variation Distance between two categorical distributions."""
|
|
133
|
+
keys = set(bench.counts.keys()) | set(workload.counts.keys())
|
|
134
|
+
return 0.5 * sum(abs(bench.prob(k) - workload.prob(k)) for k in keys)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _diversity_penalty(candidate: CandidateTask, selected: list[CandidateTask]) -> float:
|
|
138
|
+
"""Penalize candidates that are too similar to already-selected ones."""
|
|
139
|
+
if not selected:
|
|
140
|
+
return 0.0
|
|
141
|
+
|
|
142
|
+
penalty = 0.0
|
|
143
|
+
for sel in selected:
|
|
144
|
+
# Same PR family or same subsystem + complexity => high similarity
|
|
145
|
+
if sel.subsystem == candidate.subsystem and sel.complexity == candidate.complexity:
|
|
146
|
+
penalty += 0.10
|
|
147
|
+
if sel.task_type == candidate.task_type and sel.subsystem == candidate.subsystem:
|
|
148
|
+
penalty += 0.08
|
|
149
|
+
if sel.subsystem == candidate.subsystem:
|
|
150
|
+
penalty += 0.04
|
|
151
|
+
|
|
152
|
+
return penalty
|
agentfit/cli/__init__.py
ADDED
|
File without changes
|