agentloop-profiler 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.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"""Frozen, label-blind backend comparisons with explicit missing-case accounting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import random
|
|
8
|
+
from collections import Counter, defaultdict
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from fractions import Fraction
|
|
11
|
+
from itertools import combinations
|
|
12
|
+
from statistics import fmean
|
|
13
|
+
|
|
14
|
+
from agentloop.judgment_benchmark_types import BENCHMARK_VERSION, JudgmentBenchmark
|
|
15
|
+
from agentloop.judgment_types import JudgeIdentity, JudgmentBackend, canonical, fingerprint, text
|
|
16
|
+
from agentloop.judgments import JudgmentSession, validate_judgment_record
|
|
17
|
+
from agentloop.markdown import markdown_table_cell, markdown_text
|
|
18
|
+
|
|
19
|
+
_CATEGORIES = {"deterministic", "local_predictor", "llm", "typed_service"}
|
|
20
|
+
_AVAILABILITY = {"ready", "not_configured", "missing_credentials"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class BenchmarkBackend:
|
|
25
|
+
name: str
|
|
26
|
+
backend: JudgmentBackend
|
|
27
|
+
category: str
|
|
28
|
+
simulated: bool = False
|
|
29
|
+
|
|
30
|
+
def __post_init__(self):
|
|
31
|
+
text(self.name, "backend name")
|
|
32
|
+
if self.category not in _CATEGORIES or type(self.simulated) is not bool:
|
|
33
|
+
raise ValueError("backend requires a supported category and boolean simulated flag")
|
|
34
|
+
if type(self.backend.identity) is not JudgeIdentity or not callable(self.backend.judge):
|
|
35
|
+
raise ValueError("backend must implement JudgmentBackend")
|
|
36
|
+
|
|
37
|
+
def declaration(self):
|
|
38
|
+
return {
|
|
39
|
+
"name": self.name,
|
|
40
|
+
"category": self.category,
|
|
41
|
+
"simulated": self.simulated,
|
|
42
|
+
"judge": asdict(self.backend.identity),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _copy(value):
|
|
47
|
+
return json.loads(canonical(value))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _availability(backend):
|
|
51
|
+
value = getattr(backend, "availability", "ready")
|
|
52
|
+
if value not in _AVAILABILITY:
|
|
53
|
+
raise ValueError("unsupported backend availability declaration")
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_judgment_benchmark(
|
|
58
|
+
protocol: JudgmentBenchmark, backends, *, enabled=False, max_invocations=10000
|
|
59
|
+
):
|
|
60
|
+
"""Execute a finite schedule; imported artifacts never configure executable code.
|
|
61
|
+
|
|
62
|
+
Backends are explicit host-owned objects. No retries, warmups, cache hits or
|
|
63
|
+
production routing are hidden in the benchmark. This bounds call count, not
|
|
64
|
+
provider spend; the host must authorize and bound any remote transport.
|
|
65
|
+
"""
|
|
66
|
+
if type(protocol) is not JudgmentBenchmark or type(enabled) is not bool:
|
|
67
|
+
raise ValueError("benchmark requires a typed protocol and boolean enabled flag")
|
|
68
|
+
if (
|
|
69
|
+
not isinstance(backends, (list, tuple))
|
|
70
|
+
or not backends
|
|
71
|
+
or any(type(item) is not BenchmarkBackend for item in backends)
|
|
72
|
+
):
|
|
73
|
+
raise ValueError("backends must be a nonempty list or tuple of BenchmarkBackend")
|
|
74
|
+
names = [item.name for item in backends]
|
|
75
|
+
if len(set(names)) != len(names):
|
|
76
|
+
raise ValueError("backend names must be unique")
|
|
77
|
+
count = len(backends) * len(protocol.examples) * protocol.repetitions
|
|
78
|
+
if type(max_invocations) is not int or max_invocations < 1 or count > max_invocations:
|
|
79
|
+
raise ValueError("planned benchmark exceeds max_invocations")
|
|
80
|
+
configurations = {item.name: item for item in backends}
|
|
81
|
+
declarations = [configurations[name].declaration() for name in sorted(names)]
|
|
82
|
+
plan = {"protocol": protocol.to_dict(), "backends": declarations}
|
|
83
|
+
sessions = {name: JudgmentSession(configurations[name].backend, cache_size=0) for name in names}
|
|
84
|
+
jobs = [
|
|
85
|
+
(name, index, repeat)
|
|
86
|
+
for name in sorted(names)
|
|
87
|
+
for index in range(len(protocol.examples))
|
|
88
|
+
for repeat in range(protocol.repetitions)
|
|
89
|
+
]
|
|
90
|
+
random.Random(protocol.seed).shuffle(jobs) # nosec B311
|
|
91
|
+
rows = []
|
|
92
|
+
for ordinal, (name, index, repeat) in enumerate(jobs):
|
|
93
|
+
example = protocol.examples[index]
|
|
94
|
+
row = {
|
|
95
|
+
"ordinal": ordinal,
|
|
96
|
+
"backend": name,
|
|
97
|
+
"example_id": example.example_id,
|
|
98
|
+
"repetition": repeat,
|
|
99
|
+
"availability": "ready",
|
|
100
|
+
"receipt": None,
|
|
101
|
+
}
|
|
102
|
+
try:
|
|
103
|
+
row["availability"] = _availability(configurations[name].backend)
|
|
104
|
+
# Expected answers and label references are never passed to a backend.
|
|
105
|
+
row["receipt"] = sessions[name].evaluate(
|
|
106
|
+
example.request, enabled=enabled, timeout_s=protocol.timeout_s
|
|
107
|
+
)
|
|
108
|
+
row["status"] = "recorded"
|
|
109
|
+
except Exception:
|
|
110
|
+
row["status"] = "execution_error"
|
|
111
|
+
rows.append(row)
|
|
112
|
+
return {
|
|
113
|
+
"schema_version": BENCHMARK_VERSION,
|
|
114
|
+
**plan,
|
|
115
|
+
"plan_hash": fingerprint(plan),
|
|
116
|
+
"enabled": enabled,
|
|
117
|
+
"environment": {
|
|
118
|
+
"python": platform.python_version(),
|
|
119
|
+
"implementation": platform.python_implementation(),
|
|
120
|
+
"system": platform.system(),
|
|
121
|
+
"architecture": platform.machine(),
|
|
122
|
+
},
|
|
123
|
+
"records": rows,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _load_bundle(bundle):
|
|
128
|
+
owned = _copy(bundle)
|
|
129
|
+
try:
|
|
130
|
+
if owned["schema_version"] != BENCHMARK_VERSION or type(owned["enabled"]) is not bool:
|
|
131
|
+
raise ValueError("unsupported benchmark bundle")
|
|
132
|
+
protocol = JudgmentBenchmark.from_dict(owned["protocol"])
|
|
133
|
+
declarations = owned["backends"]
|
|
134
|
+
if not isinstance(declarations, list) or not declarations:
|
|
135
|
+
raise ValueError("benchmark requires backend declarations")
|
|
136
|
+
for item in declarations:
|
|
137
|
+
text(item["name"], "backend name")
|
|
138
|
+
JudgeIdentity(**item["judge"])
|
|
139
|
+
if item["category"] not in _CATEGORIES or type(item["simulated"]) is not bool:
|
|
140
|
+
raise ValueError("invalid backend declaration")
|
|
141
|
+
if len({item["name"] for item in declarations}) != len(declarations):
|
|
142
|
+
raise ValueError("duplicate backend declarations")
|
|
143
|
+
if owned["plan_hash"] != fingerprint(
|
|
144
|
+
{"protocol": protocol.to_dict(), "backends": declarations}
|
|
145
|
+
):
|
|
146
|
+
raise ValueError("benchmark plan hash mismatch")
|
|
147
|
+
if not isinstance(owned["records"], list):
|
|
148
|
+
raise ValueError("benchmark records must be a list")
|
|
149
|
+
environment = owned["environment"]
|
|
150
|
+
if (
|
|
151
|
+
not isinstance(environment, dict)
|
|
152
|
+
or set(environment) != {"python", "implementation", "system", "architecture"}
|
|
153
|
+
or any(not isinstance(value, str) for value in environment.values())
|
|
154
|
+
):
|
|
155
|
+
raise ValueError("invalid benchmark environment")
|
|
156
|
+
return owned, protocol, {item["name"]: item for item in declarations}
|
|
157
|
+
except (KeyError, TypeError, AttributeError):
|
|
158
|
+
raise ValueError("invalid judgment benchmark bundle") from None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _slots(bundle, protocol, declarations):
|
|
162
|
+
examples = {item.example_id: item for item in protocol.examples}
|
|
163
|
+
indexed, unmatched = defaultdict(list), []
|
|
164
|
+
for index, row in enumerate(bundle["records"]):
|
|
165
|
+
if (
|
|
166
|
+
not isinstance(row, dict)
|
|
167
|
+
or not isinstance(row.get("backend"), str)
|
|
168
|
+
or row["backend"] not in declarations
|
|
169
|
+
or not isinstance(row.get("example_id"), str)
|
|
170
|
+
or row["example_id"] not in examples
|
|
171
|
+
or type(row.get("repetition")) is not int
|
|
172
|
+
or not 0 <= row["repetition"] < protocol.repetitions
|
|
173
|
+
):
|
|
174
|
+
unmatched.append({"record_index": index, "reason": "unmatched_record"})
|
|
175
|
+
continue
|
|
176
|
+
indexed[(row["backend"], row["example_id"], row["repetition"])].append(row)
|
|
177
|
+
slots = {}
|
|
178
|
+
invocation_ids = Counter()
|
|
179
|
+
for rows in indexed.values():
|
|
180
|
+
for row in rows:
|
|
181
|
+
invocation = (
|
|
182
|
+
row.get("receipt", {}).get("invocation", {})
|
|
183
|
+
if isinstance(row.get("receipt"), dict)
|
|
184
|
+
else {}
|
|
185
|
+
)
|
|
186
|
+
if isinstance(invocation, dict) and isinstance(invocation.get("id"), str):
|
|
187
|
+
invocation_ids[invocation["id"]] += 1
|
|
188
|
+
for name, declaration in declarations.items():
|
|
189
|
+
for example in protocol.examples:
|
|
190
|
+
for repeat in range(protocol.repetitions):
|
|
191
|
+
key = name, example.example_id, repeat
|
|
192
|
+
rows = indexed.get(key, [])
|
|
193
|
+
slot = {
|
|
194
|
+
"backend": name,
|
|
195
|
+
"example_id": example.example_id,
|
|
196
|
+
"repetition": repeat,
|
|
197
|
+
"status": "missing",
|
|
198
|
+
"value": None,
|
|
199
|
+
"receipt": None,
|
|
200
|
+
"availability": "unknown",
|
|
201
|
+
}
|
|
202
|
+
if len(rows) > 1:
|
|
203
|
+
slot["status"] = "duplicate"
|
|
204
|
+
elif rows:
|
|
205
|
+
row = rows[0]
|
|
206
|
+
try:
|
|
207
|
+
if row.get("availability") not in _AVAILABILITY:
|
|
208
|
+
raise ValueError("invalid availability")
|
|
209
|
+
slot["availability"] = row["availability"]
|
|
210
|
+
if row.get("status") == "execution_error" and row.get("receipt") is None:
|
|
211
|
+
slot["status"] = "error"
|
|
212
|
+
elif row.get("status") == "recorded":
|
|
213
|
+
receipt = validate_judgment_record(row["receipt"])
|
|
214
|
+
if (
|
|
215
|
+
receipt["request"] != example.request.to_dict()
|
|
216
|
+
or receipt["judge"] != declaration["judge"]
|
|
217
|
+
or receipt["invocation"]["cache_hit"]
|
|
218
|
+
or invocation_ids[receipt["invocation"]["id"]] != 1
|
|
219
|
+
or (not bundle["enabled"] and receipt["invocation"]["dispatched"])
|
|
220
|
+
):
|
|
221
|
+
raise ValueError("receipt does not match its frozen benchmark slot")
|
|
222
|
+
result = receipt["evaluation"]
|
|
223
|
+
slot.update(
|
|
224
|
+
status=result["status"], value=result["value"], receipt=receipt
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
raise ValueError("invalid record status")
|
|
228
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
229
|
+
slot["status"] = "invalid_record"
|
|
230
|
+
slots[key] = slot
|
|
231
|
+
return slots, unmatched
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _mean(values):
|
|
235
|
+
if not values:
|
|
236
|
+
return None
|
|
237
|
+
try:
|
|
238
|
+
return fmean(values)
|
|
239
|
+
except OverflowError:
|
|
240
|
+
return float(sum((Fraction(str(item)) for item in values), Fraction()) / len(values))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _median(values):
|
|
244
|
+
if not values:
|
|
245
|
+
return None
|
|
246
|
+
ordered = sorted(values)
|
|
247
|
+
middle = len(ordered) // 2
|
|
248
|
+
return ordered[middle] if len(ordered) % 2 else _mean(ordered[middle - 1 : middle + 1])
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _normalized_difference(left, right, low, high):
|
|
252
|
+
return float(
|
|
253
|
+
abs(Fraction(str(left)) - Fraction(str(right))) / (Fraction(str(high)) - Fraction(str(low)))
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _calibration(cases):
|
|
258
|
+
"""Descriptive probability calibration of per-example mean forecasts."""
|
|
259
|
+
if len(cases) < 20 or len({label for _, label in cases}) < 2:
|
|
260
|
+
return {
|
|
261
|
+
"status": "insufficient_support",
|
|
262
|
+
"example_count": len(cases),
|
|
263
|
+
"minimum_examples": 20,
|
|
264
|
+
"expected_calibration_error": None,
|
|
265
|
+
"bins": [],
|
|
266
|
+
"calibrated": False,
|
|
267
|
+
}
|
|
268
|
+
bins = [[] for _ in range(10)]
|
|
269
|
+
for probability, label in cases:
|
|
270
|
+
bins[min(int(probability * 10), 9)].append((probability, label))
|
|
271
|
+
summaries, error = [], 0.0
|
|
272
|
+
for index, values in enumerate(bins):
|
|
273
|
+
forecast = _mean([value for value, _ in values])
|
|
274
|
+
frequency = _mean([int(label) for _, label in values])
|
|
275
|
+
if values:
|
|
276
|
+
error += len(values) / len(cases) * abs(forecast - frequency)
|
|
277
|
+
summaries.append(
|
|
278
|
+
{
|
|
279
|
+
"lower": index / 10,
|
|
280
|
+
"upper": (index + 1) / 10,
|
|
281
|
+
"count": len(values),
|
|
282
|
+
"mean_probability": forecast,
|
|
283
|
+
"positive_frequency": frequency,
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
return {
|
|
287
|
+
"status": "descriptive",
|
|
288
|
+
"example_count": len(cases),
|
|
289
|
+
"expected_calibration_error": error,
|
|
290
|
+
"bins": summaries,
|
|
291
|
+
"calibrated": False,
|
|
292
|
+
"basis": "one mean probability per completely observed independently labelled example",
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _quality(protocol, name, slots):
|
|
297
|
+
groups = defaultdict(list)
|
|
298
|
+
for example in protocol.examples:
|
|
299
|
+
groups[fingerprint(asdict(example.request.spec))].append(example)
|
|
300
|
+
results = []
|
|
301
|
+
for key, examples in groups.items():
|
|
302
|
+
spec = examples[0].request.spec
|
|
303
|
+
labelled = [item for item in examples if item.label_status == "independent"]
|
|
304
|
+
scores, probabilities = [], []
|
|
305
|
+
for example in labelled:
|
|
306
|
+
values = [
|
|
307
|
+
slots[name, example.example_id, repeat] for repeat in range(protocol.repetitions)
|
|
308
|
+
]
|
|
309
|
+
if any(item["status"] != "known" for item in values):
|
|
310
|
+
continue
|
|
311
|
+
answers = [item["value"] for item in values]
|
|
312
|
+
if spec.kind == "probability":
|
|
313
|
+
scores.append(fmean((value - int(example.expected)) ** 2 for value in answers))
|
|
314
|
+
probabilities.append((fmean(answers), example.expected))
|
|
315
|
+
elif spec.kind == "score":
|
|
316
|
+
scores.append(
|
|
317
|
+
fmean(
|
|
318
|
+
_normalized_difference(value, example.expected, spec.minimum, spec.maximum)
|
|
319
|
+
for value in answers
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
scores.append(fmean(float(value == example.expected) for value in answers))
|
|
324
|
+
metric = {"probability": "brier_loss", "score": "normalized_absolute_error"}.get(
|
|
325
|
+
spec.kind, "accuracy"
|
|
326
|
+
)
|
|
327
|
+
complete = len(scores) == len(examples) and bool(scores)
|
|
328
|
+
result = {
|
|
329
|
+
"cohort_id": key,
|
|
330
|
+
"spec": asdict(spec),
|
|
331
|
+
"planned_examples": len(examples),
|
|
332
|
+
"independently_labelled_examples": len(labelled),
|
|
333
|
+
"complete_labelled_examples": len(scores),
|
|
334
|
+
"unlabelled_examples": len(examples) - len(labelled),
|
|
335
|
+
"incomplete_labelled_examples": len(labelled) - len(scores),
|
|
336
|
+
"coverage": len(scores) / len(examples),
|
|
337
|
+
"complete": complete,
|
|
338
|
+
"metric": metric,
|
|
339
|
+
"value": _mean(scores) if complete else None,
|
|
340
|
+
"observed_value": _mean(scores),
|
|
341
|
+
"better": "higher" if metric == "accuracy" else "lower",
|
|
342
|
+
"weighting": "equal examples; mean across planned repetitions only when all are known",
|
|
343
|
+
"label_versions": sorted({item.label_version for item in labelled}),
|
|
344
|
+
}
|
|
345
|
+
if spec.kind == "probability":
|
|
346
|
+
result["calibration"] = {**_calibration(probabilities), "full_coverage": complete}
|
|
347
|
+
results.append(result)
|
|
348
|
+
return results
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _performance(values):
|
|
352
|
+
timings, costs, inputs, outputs = [], [], [], []
|
|
353
|
+
cost_bases, token_bases = Counter(), Counter()
|
|
354
|
+
for slot in values:
|
|
355
|
+
if slot["receipt"] is None:
|
|
356
|
+
continue
|
|
357
|
+
invocation = slot["receipt"]["invocation"]
|
|
358
|
+
if invocation["dispatched"]:
|
|
359
|
+
timings.append(invocation["latency_ms"])
|
|
360
|
+
usage = invocation["usage"]
|
|
361
|
+
for key, target in (
|
|
362
|
+
("cost_usd", costs),
|
|
363
|
+
("input_tokens", inputs),
|
|
364
|
+
("output_tokens", outputs),
|
|
365
|
+
):
|
|
366
|
+
if usage[key] is not None:
|
|
367
|
+
target.append(usage[key])
|
|
368
|
+
cost_bases[usage["cost_basis"]] += 1
|
|
369
|
+
token_bases[usage["token_basis"]] += 1
|
|
370
|
+
|
|
371
|
+
def total(items):
|
|
372
|
+
# Exact decimal accumulation avoids an intermediate overflow or a large
|
|
373
|
+
# sum silently becoming infinity in portable JSON.
|
|
374
|
+
value = sum((Fraction(str(item)) for item in items), Fraction())
|
|
375
|
+
try:
|
|
376
|
+
return float(value)
|
|
377
|
+
except OverflowError:
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
known_cost = total(costs)
|
|
381
|
+
return {
|
|
382
|
+
"latency_ms": {
|
|
383
|
+
"mean": _mean(timings),
|
|
384
|
+
"median": _median(timings),
|
|
385
|
+
"observed_invocations": len(timings),
|
|
386
|
+
"planned_invocations": len(values),
|
|
387
|
+
},
|
|
388
|
+
"cost": {
|
|
389
|
+
"known_usd": known_cost,
|
|
390
|
+
"complete": len(costs) == len(values) and known_cost is not None,
|
|
391
|
+
"aggregation_status": "out_of_range" if known_cost is None else "finite",
|
|
392
|
+
"observed_invocations": len(costs),
|
|
393
|
+
"basis_counts": dict(cost_bases),
|
|
394
|
+
},
|
|
395
|
+
"tokens": {
|
|
396
|
+
"known_input": sum(inputs),
|
|
397
|
+
"known_output": sum(outputs),
|
|
398
|
+
"input_complete": len(inputs) == len(values),
|
|
399
|
+
"output_complete": len(outputs) == len(values),
|
|
400
|
+
"basis_counts": dict(token_bases),
|
|
401
|
+
},
|
|
402
|
+
"interpretation": "Observed offline invocation overhead; unknown/missing measurements are not zero. No cached invocations are admitted.",
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _disagreements(protocol, declarations, slots):
|
|
407
|
+
result = []
|
|
408
|
+
cohort_ids = {
|
|
409
|
+
example.example_id: fingerprint(asdict(example.request.spec))
|
|
410
|
+
for example in protocol.examples
|
|
411
|
+
}
|
|
412
|
+
for left, right in combinations(sorted(declarations), 2):
|
|
413
|
+
groups = defaultdict(list)
|
|
414
|
+
for example in protocol.examples:
|
|
415
|
+
spec = example.request.spec
|
|
416
|
+
for repeat in range(protocol.repetitions):
|
|
417
|
+
a, b = (
|
|
418
|
+
slots[left, example.example_id, repeat],
|
|
419
|
+
slots[right, example.example_id, repeat],
|
|
420
|
+
)
|
|
421
|
+
pair = {
|
|
422
|
+
"example_id": example.example_id,
|
|
423
|
+
"repetition": repeat,
|
|
424
|
+
"status": "unavailable",
|
|
425
|
+
"left": None,
|
|
426
|
+
"right": None,
|
|
427
|
+
"difference": None,
|
|
428
|
+
}
|
|
429
|
+
if a["status"] == b["status"] == "known":
|
|
430
|
+
pair.update(status="observed", left=a["value"], right=b["value"])
|
|
431
|
+
if spec.kind in {"boolean", "choice"}:
|
|
432
|
+
pair["difference"] = float(a["value"] != b["value"])
|
|
433
|
+
else:
|
|
434
|
+
low, high = (
|
|
435
|
+
(0, 1) if spec.kind == "probability" else (spec.minimum, spec.maximum)
|
|
436
|
+
)
|
|
437
|
+
pair["difference"] = _normalized_difference(
|
|
438
|
+
a["value"], b["value"], low, high
|
|
439
|
+
)
|
|
440
|
+
groups[cohort_ids[example.example_id]].append(pair)
|
|
441
|
+
for key, pairs in groups.items():
|
|
442
|
+
observed = [item["difference"] for item in pairs if item["status"] == "observed"]
|
|
443
|
+
result.append(
|
|
444
|
+
{
|
|
445
|
+
"left_backend": left,
|
|
446
|
+
"right_backend": right,
|
|
447
|
+
"cohort_id": key,
|
|
448
|
+
"planned_pairs": len(pairs),
|
|
449
|
+
"observed_pairs": len(observed),
|
|
450
|
+
"observed_mean_difference": _mean(observed),
|
|
451
|
+
"pairs": pairs,
|
|
452
|
+
"interpretation": "Disagreement is not independent correctness evidence.",
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
return result
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def summarize_judgment_benchmark(bundle, *, max_invocations=10000, max_pairwise_comparisons=100000):
|
|
459
|
+
"""Analyze saved receipts without importing, configuring or executing a backend."""
|
|
460
|
+
owned, protocol, declarations = _load_bundle(bundle)
|
|
461
|
+
planned = len(protocol.examples) * protocol.repetitions * len(declarations)
|
|
462
|
+
if type(max_invocations) is not int or max_invocations < 1 or planned > max_invocations:
|
|
463
|
+
raise ValueError("planned benchmark exceeds max_invocations")
|
|
464
|
+
pairwise = (
|
|
465
|
+
len(declarations)
|
|
466
|
+
* (len(declarations) - 1)
|
|
467
|
+
// 2
|
|
468
|
+
* len(protocol.examples)
|
|
469
|
+
* protocol.repetitions
|
|
470
|
+
)
|
|
471
|
+
if (
|
|
472
|
+
type(max_pairwise_comparisons) is not int
|
|
473
|
+
or max_pairwise_comparisons < 0
|
|
474
|
+
or pairwise > max_pairwise_comparisons
|
|
475
|
+
):
|
|
476
|
+
raise ValueError("planned benchmark exceeds max_pairwise_comparisons")
|
|
477
|
+
slots, unmatched = _slots(owned, protocol, declarations)
|
|
478
|
+
summaries = []
|
|
479
|
+
by_backend = defaultdict(list)
|
|
480
|
+
for item in slots.values():
|
|
481
|
+
by_backend[item["backend"]].append(item)
|
|
482
|
+
for name, declaration in declarations.items():
|
|
483
|
+
values = by_backend[name]
|
|
484
|
+
counts = Counter(item["status"] for item in values)
|
|
485
|
+
availability = Counter(item["availability"] for item in values)
|
|
486
|
+
denominator = len(values)
|
|
487
|
+
summaries.append(
|
|
488
|
+
{
|
|
489
|
+
**declaration,
|
|
490
|
+
"planned_invocations": denominator,
|
|
491
|
+
"status_counts": dict(counts),
|
|
492
|
+
"known_rate": counts["known"] / denominator,
|
|
493
|
+
"unavailable_rate": (denominator - counts["known"]) / denominator,
|
|
494
|
+
"failure_rate": sum(
|
|
495
|
+
counts[key] for key in ("error", "invalid_result", "invalid_record")
|
|
496
|
+
)
|
|
497
|
+
/ denominator,
|
|
498
|
+
"timeout_rate": counts["timeout"] / denominator,
|
|
499
|
+
"abstention_rate": counts["unknown"] / denominator,
|
|
500
|
+
"availability_counts": dict(availability),
|
|
501
|
+
"quality": _quality(protocol, name, slots),
|
|
502
|
+
"performance": _performance(values),
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
return {
|
|
506
|
+
"schema_version": BENCHMARK_VERSION,
|
|
507
|
+
"name": protocol.name,
|
|
508
|
+
"version": protocol.version,
|
|
509
|
+
"plan_hash": owned["plan_hash"],
|
|
510
|
+
"protocol_hash": protocol.protocol_hash,
|
|
511
|
+
"source_bundle_hash": fingerprint(owned),
|
|
512
|
+
"environment": owned["environment"],
|
|
513
|
+
"synthetic": protocol.synthetic,
|
|
514
|
+
"simulated_backends": [item["name"] for item in declarations.values() if item["simulated"]],
|
|
515
|
+
"enabled": owned["enabled"],
|
|
516
|
+
"planned_invocations": len(slots),
|
|
517
|
+
"record_count": len(owned["records"]),
|
|
518
|
+
"unmatched_records": unmatched,
|
|
519
|
+
"backends": summaries,
|
|
520
|
+
"disagreements": _disagreements(protocol, declarations, slots),
|
|
521
|
+
"cases": list(slots.values()),
|
|
522
|
+
"interpretation": "Descriptive comparison on frozen examples and caller-declared independent labels. Agreement is not ground truth; no backend ranking or production substitution is authorized.",
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def judgment_benchmark_markdown(report):
|
|
527
|
+
lines = [
|
|
528
|
+
f"# Judgment benchmark: {markdown_text(report['name'])}",
|
|
529
|
+
"",
|
|
530
|
+
report["interpretation"],
|
|
531
|
+
"",
|
|
532
|
+
]
|
|
533
|
+
if report["synthetic"] or report["simulated_backends"]:
|
|
534
|
+
lines.extend(
|
|
535
|
+
[
|
|
536
|
+
"Synthetic inputs or simulated backends are present. These results do not establish real-model performance or empirical optimization gains.",
|
|
537
|
+
"",
|
|
538
|
+
]
|
|
539
|
+
)
|
|
540
|
+
lines.extend(
|
|
541
|
+
[
|
|
542
|
+
"## Availability and performance",
|
|
543
|
+
"",
|
|
544
|
+
"| Backend | Planned | Known | Failure | Timeout | Abstention | Known cost USD | Cost complete | Observed latency ms |",
|
|
545
|
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | ---: |",
|
|
546
|
+
]
|
|
547
|
+
)
|
|
548
|
+
for backend in report["backends"]:
|
|
549
|
+
cells = [
|
|
550
|
+
backend["name"],
|
|
551
|
+
backend["planned_invocations"],
|
|
552
|
+
backend["known_rate"],
|
|
553
|
+
backend["failure_rate"],
|
|
554
|
+
backend["timeout_rate"],
|
|
555
|
+
backend["abstention_rate"],
|
|
556
|
+
backend["performance"]["cost"]["known_usd"],
|
|
557
|
+
backend["performance"]["cost"]["complete"],
|
|
558
|
+
backend["performance"]["latency_ms"]["mean"],
|
|
559
|
+
]
|
|
560
|
+
lines.append(
|
|
561
|
+
"| "
|
|
562
|
+
+ " | ".join(
|
|
563
|
+
markdown_table_cell("unavailable" if item is None else str(item)) for item in cells
|
|
564
|
+
)
|
|
565
|
+
+ " |"
|
|
566
|
+
)
|
|
567
|
+
lines.extend(
|
|
568
|
+
[
|
|
569
|
+
"",
|
|
570
|
+
"## Independent-label quality",
|
|
571
|
+
"",
|
|
572
|
+
"Full metric values require complete labelled coverage. Observed values are conditional on the completely observed labelled examples; coverage and failures remain part of the comparison.",
|
|
573
|
+
"",
|
|
574
|
+
"| Backend | Question | Metric | Full value | Observed value | Coverage | Calibration |",
|
|
575
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
576
|
+
]
|
|
577
|
+
)
|
|
578
|
+
for backend in report["backends"]:
|
|
579
|
+
for cohort in backend["quality"]:
|
|
580
|
+
cells = [
|
|
581
|
+
backend["name"],
|
|
582
|
+
cohort["spec"]["question"],
|
|
583
|
+
cohort["metric"],
|
|
584
|
+
cohort["value"],
|
|
585
|
+
cohort["observed_value"],
|
|
586
|
+
cohort["coverage"],
|
|
587
|
+
cohort.get("calibration", {}).get("status", "not applicable"),
|
|
588
|
+
]
|
|
589
|
+
lines.append(
|
|
590
|
+
"| "
|
|
591
|
+
+ " | ".join(
|
|
592
|
+
markdown_table_cell("unavailable" if item is None else str(item))
|
|
593
|
+
for item in cells
|
|
594
|
+
)
|
|
595
|
+
+ " |"
|
|
596
|
+
)
|
|
597
|
+
lines.extend(
|
|
598
|
+
[
|
|
599
|
+
"",
|
|
600
|
+
"## Backend disagreement",
|
|
601
|
+
"",
|
|
602
|
+
"Agreement with another backend is not independent correctness evidence. Pairwise counts, differences, unavailable pairs and original receipts are retained in the JSON report.",
|
|
603
|
+
"",
|
|
604
|
+
]
|
|
605
|
+
)
|
|
606
|
+
return "\n".join(lines)
|