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
agentloop/replay.py
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from fractions import Fraction
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agentloop.markdown import markdown_code_span, markdown_table_cell
|
|
8
|
+
from agentloop.tokens import is_token_basis_evaluable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ReplayGates:
|
|
13
|
+
max_cost_regression_pct: float = 0.0
|
|
14
|
+
max_latency_regression_pct: float = 0.0
|
|
15
|
+
min_latency_improvement_pct: float = 0.0
|
|
16
|
+
min_cost_improvement_pct: float = 0.0
|
|
17
|
+
require_retry_non_increase: bool = True
|
|
18
|
+
require_schema_valid: bool = False
|
|
19
|
+
min_quality_score: float | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_replay_report(
|
|
23
|
+
baseline_trace: Any,
|
|
24
|
+
candidate_trace: Any,
|
|
25
|
+
*,
|
|
26
|
+
gates: ReplayGates | None = None,
|
|
27
|
+
quality_report: dict[str, Any] | None = None,
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
"""Compare a baseline and candidate trace and evaluate the configured gates.
|
|
30
|
+
|
|
31
|
+
A cost gate is only evaluated when both its inputs are trustworthy: every
|
|
32
|
+
model call has a known rate, *and* the token counts those rates multiply were
|
|
33
|
+
counted rather than approximated. When either is missing the cost gates go
|
|
34
|
+
indeterminate instead of comparing numbers that cannot support the comparison
|
|
35
|
+
(issue #133); ``gates.pricing_known`` and ``gates.token_basis_evaluable`` say
|
|
36
|
+
which input was missing.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from agentloop.retention import require_complete_evidence
|
|
40
|
+
|
|
41
|
+
require_complete_evidence(baseline_trace, "replay")
|
|
42
|
+
require_complete_evidence(candidate_trace, "replay")
|
|
43
|
+
from agentloop.integrations.harbor.trial_evidence import require_trial_pair
|
|
44
|
+
|
|
45
|
+
require_trial_pair(baseline_trace, candidate_trace)
|
|
46
|
+
from agentloop.interoperability.cohort_evidence import require_cohort_pair
|
|
47
|
+
|
|
48
|
+
require_cohort_pair(baseline_trace, candidate_trace)
|
|
49
|
+
gates = gates or ReplayGates()
|
|
50
|
+
baseline_report = baseline_trace.report()
|
|
51
|
+
candidate_report = candidate_trace.report()
|
|
52
|
+
baseline = _trace_summary(baseline_trace, baseline_report)
|
|
53
|
+
candidate = _trace_summary(candidate_trace, candidate_report)
|
|
54
|
+
if quality_report is not None:
|
|
55
|
+
if quality_report.get("schema_version") == "2.0":
|
|
56
|
+
from agentloop.structured_quality import decision_span_count, validate_report
|
|
57
|
+
|
|
58
|
+
quality_report = validate_report(
|
|
59
|
+
quality_report, baseline_trace=baseline_trace, candidate_trace=candidate_trace
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
baseline["quality_score"] = _optional_float(quality_report.get("baseline_score"))
|
|
63
|
+
candidate["quality_score"] = _optional_float(quality_report.get("candidate_score"))
|
|
64
|
+
baseline["decision_count"] = decision_span_count(baseline_trace)
|
|
65
|
+
candidate["decision_count"] = decision_span_count(candidate_trace)
|
|
66
|
+
baseline["error_span_count"] = sum(
|
|
67
|
+
event.status == "error" for event in baseline_trace.events
|
|
68
|
+
)
|
|
69
|
+
candidate["error_span_count"] = sum(
|
|
70
|
+
event.status == "error" for event in candidate_trace.events
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
baseline["quality_score"] = float(quality_report.get("baseline_score", 0.0))
|
|
74
|
+
candidate["quality_score"] = float(quality_report.get("candidate_score", 0.0))
|
|
75
|
+
pricing_known = not (
|
|
76
|
+
bool(baseline.get("has_unknown_cost")) or bool(candidate.get("has_unknown_cost"))
|
|
77
|
+
)
|
|
78
|
+
token_basis_evaluable = is_token_basis_evaluable(
|
|
79
|
+
baseline.get("token_status")
|
|
80
|
+
) and is_token_basis_evaluable(candidate.get("token_status"))
|
|
81
|
+
cost_evaluable = pricing_known and token_basis_evaluable
|
|
82
|
+
deltas = _deltas(baseline, candidate)
|
|
83
|
+
if "external_trial" in baseline or "external_trial" in candidate:
|
|
84
|
+
nullable = {
|
|
85
|
+
"input_tokens": ("input_tokens_delta", "input_token_improvement_pct"),
|
|
86
|
+
"output_tokens": ("output_tokens_delta", "output_token_improvement_pct"),
|
|
87
|
+
"retry_count": ("retry_count_delta", "retry_improvement_pct"),
|
|
88
|
+
"tool_call_count": ("tool_call_count_delta", "tool_call_improvement_pct"),
|
|
89
|
+
"model_call_count": ("model_call_count_delta", "model_call_improvement_pct"),
|
|
90
|
+
}
|
|
91
|
+
for metric, fields in nullable.items():
|
|
92
|
+
if baseline.get(metric) is None or candidate.get(metric) is None:
|
|
93
|
+
for key in fields:
|
|
94
|
+
deltas[key] = None
|
|
95
|
+
versioned_quality = quality_report is not None and quality_report.get("schema_version") == "2.0"
|
|
96
|
+
if versioned_quality or "quality_evidence" in baseline or "quality_evidence" in candidate:
|
|
97
|
+
if baseline.get("quality_score") is None or candidate.get("quality_score") is None:
|
|
98
|
+
deltas["quality_score_delta"] = None
|
|
99
|
+
else:
|
|
100
|
+
deltas["quality_score_delta"] = float(
|
|
101
|
+
Fraction(str(candidate["quality_score"])) - Fraction(str(baseline["quality_score"]))
|
|
102
|
+
)
|
|
103
|
+
if "decision_count" in baseline and "decision_count" in candidate:
|
|
104
|
+
deltas["decision_count_delta"] = (
|
|
105
|
+
candidate["decision_count"] - baseline["decision_count"]
|
|
106
|
+
)
|
|
107
|
+
deltas["error_span_count_delta"] = (
|
|
108
|
+
candidate["error_span_count"] - baseline["error_span_count"]
|
|
109
|
+
)
|
|
110
|
+
if not cost_evaluable:
|
|
111
|
+
# The cost totals are lower bounds when a model is unpriced, and a rate
|
|
112
|
+
# times a word estimate when the token basis is approximate. Either way
|
|
113
|
+
# their delta and percentages are not a valid comparison — expose them as
|
|
114
|
+
# unavailable (None) rather than let a consumer read that arithmetic as a
|
|
115
|
+
# real number. The gates stay indeterminate below.
|
|
116
|
+
deltas["cost_usd_delta"] = None
|
|
117
|
+
deltas["cost_improvement_pct"] = None
|
|
118
|
+
deltas["cost_regression_pct"] = None
|
|
119
|
+
gate_results = _gate_results(
|
|
120
|
+
deltas,
|
|
121
|
+
baseline,
|
|
122
|
+
candidate,
|
|
123
|
+
gates,
|
|
124
|
+
quality_report=quality_report,
|
|
125
|
+
cost_evaluable=cost_evaluable,
|
|
126
|
+
pricing_known=pricing_known,
|
|
127
|
+
)
|
|
128
|
+
if "external_trial" in baseline:
|
|
129
|
+
outcomes = [summary["external_trial"]["outcome"] for summary in (baseline, candidate)]
|
|
130
|
+
available = all(outcome["quality_pass"] is not None for outcome in outcomes)
|
|
131
|
+
correct = available and all(
|
|
132
|
+
outcome["quality_pass"] is True and outcome["execution_status"] == "completed"
|
|
133
|
+
for outcome in outcomes
|
|
134
|
+
)
|
|
135
|
+
gate_results.append(
|
|
136
|
+
_gate(
|
|
137
|
+
"external_task_correctness",
|
|
138
|
+
correct,
|
|
139
|
+
"matching externally supplied verifier criteria passed"
|
|
140
|
+
if correct
|
|
141
|
+
else "external task correctness failed or is unavailable",
|
|
142
|
+
indeterminate=not available,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
passed = all(item["passed"] for item in gate_results)
|
|
146
|
+
indeterminate_gates = [item["name"] for item in gate_results if item.get("indeterminate")]
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"baseline": baseline,
|
|
150
|
+
"candidate": candidate,
|
|
151
|
+
"deltas": deltas,
|
|
152
|
+
"gates": {
|
|
153
|
+
"passed": passed,
|
|
154
|
+
"cost_evaluable": cost_evaluable,
|
|
155
|
+
# Split out so a failing/indeterminate cost gate says *which* input
|
|
156
|
+
# was missing: an unpriced model, or token counts that were only
|
|
157
|
+
# approximated (issue #133).
|
|
158
|
+
"pricing_known": pricing_known,
|
|
159
|
+
"token_basis_evaluable": token_basis_evaluable,
|
|
160
|
+
"baseline_token_status": baseline.get("token_status"),
|
|
161
|
+
"candidate_token_status": candidate.get("token_status"),
|
|
162
|
+
"indeterminate": indeterminate_gates,
|
|
163
|
+
"config": {
|
|
164
|
+
"max_cost_regression_pct": gates.max_cost_regression_pct,
|
|
165
|
+
"max_latency_regression_pct": gates.max_latency_regression_pct,
|
|
166
|
+
"min_latency_improvement_pct": gates.min_latency_improvement_pct,
|
|
167
|
+
"min_cost_improvement_pct": gates.min_cost_improvement_pct,
|
|
168
|
+
"require_retry_non_increase": gates.require_retry_non_increase,
|
|
169
|
+
"require_schema_valid": gates.require_schema_valid,
|
|
170
|
+
"min_quality_score": gates.min_quality_score,
|
|
171
|
+
},
|
|
172
|
+
"results": gate_results,
|
|
173
|
+
},
|
|
174
|
+
"quality": quality_report,
|
|
175
|
+
"summary": _summary(
|
|
176
|
+
deltas, passed, cost_evaluable=cost_evaluable, pricing_known=pricing_known
|
|
177
|
+
),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def replay_report_to_markdown(report: dict[str, Any]) -> str:
|
|
182
|
+
baseline = report["baseline"]
|
|
183
|
+
candidate = report["candidate"]
|
|
184
|
+
deltas = report["deltas"]
|
|
185
|
+
gates = report["gates"]
|
|
186
|
+
status = "passed" if gates["passed"] else "failed"
|
|
187
|
+
|
|
188
|
+
lines = [
|
|
189
|
+
"# AgentLoop Replay Report",
|
|
190
|
+
"",
|
|
191
|
+
f"- Status: {status}",
|
|
192
|
+
f"- Baseline: {markdown_code_span(baseline['name'])} "
|
|
193
|
+
f"{markdown_code_span(baseline['run_id'])}",
|
|
194
|
+
f"- Candidate: {markdown_code_span(candidate['name'])} "
|
|
195
|
+
f"{markdown_code_span(candidate['run_id'])}",
|
|
196
|
+
"",
|
|
197
|
+
"## Metric Deltas",
|
|
198
|
+
"",
|
|
199
|
+
"| Metric | Baseline | Candidate | Delta | Improvement |",
|
|
200
|
+
"|---|---:|---:|---:|---:|",
|
|
201
|
+
_metric_row(
|
|
202
|
+
"Runtime",
|
|
203
|
+
baseline["runtime_ms"],
|
|
204
|
+
candidate["runtime_ms"],
|
|
205
|
+
deltas["runtime_ms_delta"],
|
|
206
|
+
deltas["latency_improvement_pct"],
|
|
207
|
+
"ms",
|
|
208
|
+
),
|
|
209
|
+
_cost_metric_row(baseline, candidate, deltas),
|
|
210
|
+
_metric_row(
|
|
211
|
+
"Input tokens",
|
|
212
|
+
baseline["input_tokens"],
|
|
213
|
+
candidate["input_tokens"],
|
|
214
|
+
deltas["input_tokens_delta"],
|
|
215
|
+
deltas["input_token_improvement_pct"],
|
|
216
|
+
"count",
|
|
217
|
+
),
|
|
218
|
+
_metric_row(
|
|
219
|
+
"Output tokens",
|
|
220
|
+
baseline["output_tokens"],
|
|
221
|
+
candidate["output_tokens"],
|
|
222
|
+
deltas["output_tokens_delta"],
|
|
223
|
+
deltas["output_token_improvement_pct"],
|
|
224
|
+
"count",
|
|
225
|
+
),
|
|
226
|
+
_metric_row(
|
|
227
|
+
"Retries",
|
|
228
|
+
baseline["retry_count"],
|
|
229
|
+
candidate["retry_count"],
|
|
230
|
+
deltas["retry_count_delta"],
|
|
231
|
+
deltas["retry_improvement_pct"],
|
|
232
|
+
"count",
|
|
233
|
+
),
|
|
234
|
+
_metric_row(
|
|
235
|
+
"Tool calls",
|
|
236
|
+
baseline["tool_call_count"],
|
|
237
|
+
candidate["tool_call_count"],
|
|
238
|
+
deltas["tool_call_count_delta"],
|
|
239
|
+
deltas["tool_call_improvement_pct"],
|
|
240
|
+
"count",
|
|
241
|
+
),
|
|
242
|
+
_metric_row(
|
|
243
|
+
"Model calls",
|
|
244
|
+
baseline["model_call_count"],
|
|
245
|
+
candidate["model_call_count"],
|
|
246
|
+
deltas["model_call_count_delta"],
|
|
247
|
+
deltas["model_call_improvement_pct"],
|
|
248
|
+
"count",
|
|
249
|
+
),
|
|
250
|
+
]
|
|
251
|
+
if (
|
|
252
|
+
baseline.get("schema_validity_pct") is not None
|
|
253
|
+
or candidate.get("schema_validity_pct") is not None
|
|
254
|
+
):
|
|
255
|
+
lines.append(
|
|
256
|
+
_metric_row(
|
|
257
|
+
"Schema validity",
|
|
258
|
+
baseline.get("schema_validity_pct") or 0.0,
|
|
259
|
+
candidate.get("schema_validity_pct") or 0.0,
|
|
260
|
+
deltas["schema_validity_delta_pct"],
|
|
261
|
+
deltas["schema_validity_improvement_pct"],
|
|
262
|
+
"pct",
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
if "decision_count_delta" in deltas:
|
|
266
|
+
lines.append(
|
|
267
|
+
_metric_row(
|
|
268
|
+
"Recorded decision spans",
|
|
269
|
+
baseline["decision_count"],
|
|
270
|
+
candidate["decision_count"],
|
|
271
|
+
deltas["decision_count_delta"],
|
|
272
|
+
None,
|
|
273
|
+
"count",
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
lines.append(
|
|
277
|
+
_metric_row(
|
|
278
|
+
"Recorded error spans",
|
|
279
|
+
baseline["error_span_count"],
|
|
280
|
+
candidate["error_span_count"],
|
|
281
|
+
deltas["error_span_count_delta"],
|
|
282
|
+
None,
|
|
283
|
+
"count",
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
versioned_quality = (
|
|
287
|
+
report.get("quality", {}).get("schema_version") == "2.0"
|
|
288
|
+
if report.get("quality") is not None
|
|
289
|
+
else False
|
|
290
|
+
)
|
|
291
|
+
versioned_quality = (
|
|
292
|
+
versioned_quality or "quality_evidence" in baseline or "quality_evidence" in candidate
|
|
293
|
+
)
|
|
294
|
+
if (
|
|
295
|
+
baseline.get("quality_score") is not None
|
|
296
|
+
or candidate.get("quality_score") is not None
|
|
297
|
+
or versioned_quality
|
|
298
|
+
):
|
|
299
|
+
lines.append(
|
|
300
|
+
_metric_row(
|
|
301
|
+
"Quality score",
|
|
302
|
+
baseline.get("quality_score")
|
|
303
|
+
if versioned_quality
|
|
304
|
+
else baseline.get("quality_score") or 0.0,
|
|
305
|
+
candidate.get("quality_score")
|
|
306
|
+
if versioned_quality
|
|
307
|
+
else candidate.get("quality_score") or 0.0,
|
|
308
|
+
deltas["quality_score_delta"],
|
|
309
|
+
deltas["quality_score_improvement_pct"],
|
|
310
|
+
"exact_score" if versioned_quality else "score",
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
if report.get("quality"):
|
|
314
|
+
quality = report["quality"]
|
|
315
|
+
lines.extend(
|
|
316
|
+
[
|
|
317
|
+
"",
|
|
318
|
+
"## Quality",
|
|
319
|
+
"",
|
|
320
|
+
f"- Cases: {quality['case_count']}",
|
|
321
|
+
f"- Candidate score: {_format_metric(quality['candidate_score'], 'exact_score' if quality.get('schema_version') == '2.0' else 'score')}",
|
|
322
|
+
f"- Quality delta: {_format_metric(quality['quality_delta'], 'exact_score' if quality.get('schema_version') == '2.0' else 'score')}",
|
|
323
|
+
f"- Failed cases: {quality['failed_case_count']}",
|
|
324
|
+
]
|
|
325
|
+
)
|
|
326
|
+
lines.extend(
|
|
327
|
+
[
|
|
328
|
+
"",
|
|
329
|
+
"## Gates",
|
|
330
|
+
"",
|
|
331
|
+
"| Gate | Status | Detail |",
|
|
332
|
+
"|---|---|---|",
|
|
333
|
+
]
|
|
334
|
+
)
|
|
335
|
+
for item in gates["results"]:
|
|
336
|
+
if item.get("indeterminate"):
|
|
337
|
+
gate_status = "indeterminate"
|
|
338
|
+
else:
|
|
339
|
+
gate_status = "pass" if item["passed"] else "fail"
|
|
340
|
+
lines.append(
|
|
341
|
+
f"| {markdown_table_cell(item['name'])} | {gate_status} | "
|
|
342
|
+
f"{markdown_table_cell(item['detail'])} |"
|
|
343
|
+
)
|
|
344
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _trace_summary(trace: Any, report: dict[str, Any]) -> dict[str, Any]:
|
|
348
|
+
metadata = getattr(trace, "metadata", {}) or {}
|
|
349
|
+
cost = report.get("cost_breakdown") or {}
|
|
350
|
+
result = {
|
|
351
|
+
**(
|
|
352
|
+
{
|
|
353
|
+
"quality_evidence": report["quality_evidence"],
|
|
354
|
+
"decision_count": report["decision_count"],
|
|
355
|
+
"error_span_count": report["error_span_count"],
|
|
356
|
+
}
|
|
357
|
+
if "quality_evidence" in report
|
|
358
|
+
else {}
|
|
359
|
+
),
|
|
360
|
+
**({"execution": report["execution"]} if "execution" in report else {}),
|
|
361
|
+
**({"stages": report["stages"]} if "stages" in report else {}),
|
|
362
|
+
"run_id": trace.run_id,
|
|
363
|
+
"name": trace.name,
|
|
364
|
+
"runtime_ms": float(report.get("total_runtime_ms", 0.0) or 0.0),
|
|
365
|
+
"estimated_cost_usd": float(report.get("estimated_cost_usd", 0.0) or 0.0),
|
|
366
|
+
# Carried so the cost gates can tell "no cost" apart from "cost unknown":
|
|
367
|
+
# estimated_cost_usd is the *known* portion, has_unknown_cost flags that
|
|
368
|
+
# some model calls had no available rate, and cost_status summarizes
|
|
369
|
+
# completeness (see costs.py / metrics.py).
|
|
370
|
+
"has_unknown_cost": bool(cost.get("has_unknown_cost", False)),
|
|
371
|
+
"cost_status": report.get("cost_status", cost.get("cost_status", "complete")),
|
|
372
|
+
# How exact the token totals below are, and therefore how exact any cost
|
|
373
|
+
# calculated from them is. "unspecified" means the trace predates token
|
|
374
|
+
# provenance; agentloop.tokens.is_token_basis_evaluable() explains why
|
|
375
|
+
# that still gates.
|
|
376
|
+
"token_status": report.get("token_status", cost.get("token_status", "unspecified")),
|
|
377
|
+
"operation_counts": report.get("operation_counts", {}),
|
|
378
|
+
"input_tokens": int(report.get("input_tokens", 0) or 0),
|
|
379
|
+
"output_tokens": int(report.get("output_tokens", 0) or 0),
|
|
380
|
+
"retry_count": int(report.get("retry_count", 0) or 0),
|
|
381
|
+
"tool_call_count": int(report.get("tool_call_count", 0) or 0),
|
|
382
|
+
"model_call_count": int(report.get("model_call_count", 0) or 0),
|
|
383
|
+
"schema_validity_pct": _optional_float(
|
|
384
|
+
report.get("schema_validity_pct")
|
|
385
|
+
if report.get("schema_validity_pct") is not None
|
|
386
|
+
else metadata.get("schema_validity_pct")
|
|
387
|
+
),
|
|
388
|
+
"schema_valid": _optional_bool(
|
|
389
|
+
report.get("schema_valid")
|
|
390
|
+
if report.get("schema_valid") is not None
|
|
391
|
+
else metadata.get("schema_valid")
|
|
392
|
+
),
|
|
393
|
+
"quality_score": _optional_float(
|
|
394
|
+
report.get("quality_score")
|
|
395
|
+
if "quality_evidence" in report or report.get("quality_score") is not None
|
|
396
|
+
else metadata.get("quality_score")
|
|
397
|
+
),
|
|
398
|
+
}
|
|
399
|
+
from agentloop.integrations.harbor.trial_evidence import read_trial, trial_study_values
|
|
400
|
+
|
|
401
|
+
trial = read_trial(trace)
|
|
402
|
+
if trial is not None:
|
|
403
|
+
measurement = trial["measurement"]
|
|
404
|
+
result.update(
|
|
405
|
+
external_trial=trial,
|
|
406
|
+
runtime_ms=measurement["runtime_ms"],
|
|
407
|
+
estimated_cost_usd=measurement["cost_usd"],
|
|
408
|
+
has_unknown_cost=measurement["cost_usd"] is None,
|
|
409
|
+
input_tokens=measurement["input_tokens"],
|
|
410
|
+
output_tokens=measurement["output_tokens"],
|
|
411
|
+
quality_score=trial_study_values(trial)[0],
|
|
412
|
+
retry_count=None,
|
|
413
|
+
tool_call_count=None,
|
|
414
|
+
model_call_count=None,
|
|
415
|
+
quality_basis="configured_binary_acceptance",
|
|
416
|
+
cost_basis="external_reported_agent_context",
|
|
417
|
+
)
|
|
418
|
+
return result
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _deltas(baseline: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]:
|
|
422
|
+
base_runtime = float(baseline.get("runtime_ms", baseline.get("total_runtime_ms", 0.0)) or 0.0)
|
|
423
|
+
cand_runtime = float(candidate.get("runtime_ms", candidate.get("total_runtime_ms", 0.0)) or 0.0)
|
|
424
|
+
base_cost = float(baseline.get("estimated_cost_usd", 0.0) or 0.0)
|
|
425
|
+
cand_cost = float(candidate.get("estimated_cost_usd", 0.0) or 0.0)
|
|
426
|
+
base_input = int(baseline.get("input_tokens", 0) or 0)
|
|
427
|
+
cand_input = int(candidate.get("input_tokens", 0) or 0)
|
|
428
|
+
base_output = int(baseline.get("output_tokens", 0) or 0)
|
|
429
|
+
cand_output = int(candidate.get("output_tokens", 0) or 0)
|
|
430
|
+
base_retries = int(baseline.get("retry_count", 0) or 0)
|
|
431
|
+
cand_retries = int(candidate.get("retry_count", 0) or 0)
|
|
432
|
+
base_tools = int(baseline.get("tool_call_count", 0) or 0)
|
|
433
|
+
cand_tools = int(candidate.get("tool_call_count", 0) or 0)
|
|
434
|
+
base_models = int(baseline.get("model_call_count", 0) or 0)
|
|
435
|
+
cand_models = int(candidate.get("model_call_count", 0) or 0)
|
|
436
|
+
base_schema = _optional_float(baseline.get("schema_validity_pct"))
|
|
437
|
+
if base_schema is None:
|
|
438
|
+
base_schema = _schema_validity_from_bool(baseline.get("schema_valid"))
|
|
439
|
+
cand_schema = _optional_float(candidate.get("schema_validity_pct"))
|
|
440
|
+
if cand_schema is None:
|
|
441
|
+
cand_schema = _schema_validity_from_bool(candidate.get("schema_valid"))
|
|
442
|
+
base_quality = _optional_float(baseline.get("quality_score"))
|
|
443
|
+
cand_quality = _optional_float(candidate.get("quality_score"))
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
"runtime_ms_delta": round(cand_runtime - base_runtime, 3),
|
|
447
|
+
"latency_improvement_pct": _improvement_pct(base_runtime, cand_runtime),
|
|
448
|
+
"latency_regression_pct": _regression_pct(base_runtime, cand_runtime),
|
|
449
|
+
"cost_usd_delta": round(cand_cost - base_cost, 6),
|
|
450
|
+
"cost_improvement_pct": _improvement_pct(base_cost, cand_cost),
|
|
451
|
+
"cost_regression_pct": _regression_pct(base_cost, cand_cost),
|
|
452
|
+
"input_tokens_delta": cand_input - base_input,
|
|
453
|
+
"input_token_improvement_pct": _improvement_pct(base_input, cand_input),
|
|
454
|
+
"output_tokens_delta": cand_output - base_output,
|
|
455
|
+
"output_token_improvement_pct": _improvement_pct(base_output, cand_output),
|
|
456
|
+
"retry_count_delta": cand_retries - base_retries,
|
|
457
|
+
"retry_improvement_pct": _improvement_pct(base_retries, cand_retries),
|
|
458
|
+
"tool_call_count_delta": cand_tools - base_tools,
|
|
459
|
+
"tool_call_improvement_pct": _improvement_pct(base_tools, cand_tools),
|
|
460
|
+
"model_call_count_delta": cand_models - base_models,
|
|
461
|
+
"model_call_improvement_pct": _improvement_pct(base_models, cand_models),
|
|
462
|
+
"schema_validity_delta_pct": round((cand_schema or 0.0) - (base_schema or 0.0), 3),
|
|
463
|
+
"schema_validity_improvement_pct": _relative_gain_pct(base_schema, cand_schema),
|
|
464
|
+
"quality_score_delta": round((cand_quality or 0.0) - (base_quality or 0.0), 4),
|
|
465
|
+
"quality_score_improvement_pct": _relative_gain_pct(base_quality, cand_quality),
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _gate_results(
|
|
470
|
+
deltas: dict[str, Any],
|
|
471
|
+
baseline: dict[str, Any],
|
|
472
|
+
candidate: dict[str, Any],
|
|
473
|
+
gates: ReplayGates,
|
|
474
|
+
*,
|
|
475
|
+
quality_report: dict[str, Any] | None = None,
|
|
476
|
+
cost_evaluable: bool = True,
|
|
477
|
+
pricing_known: bool = True,
|
|
478
|
+
) -> list[dict[str, Any]]:
|
|
479
|
+
if cost_evaluable:
|
|
480
|
+
cost_regression_gate = _gate(
|
|
481
|
+
"cost_regression",
|
|
482
|
+
deltas["cost_regression_pct"] <= gates.max_cost_regression_pct,
|
|
483
|
+
f"{deltas['cost_regression_pct']:.2f}% regression "
|
|
484
|
+
f"<= {gates.max_cost_regression_pct:.2f}% allowed",
|
|
485
|
+
)
|
|
486
|
+
cost_improvement_gate = _gate(
|
|
487
|
+
"cost_improvement",
|
|
488
|
+
deltas["cost_improvement_pct"] >= gates.min_cost_improvement_pct,
|
|
489
|
+
f"{deltas['cost_improvement_pct']:.2f}% improvement "
|
|
490
|
+
f">= {gates.min_cost_improvement_pct:.2f}% required",
|
|
491
|
+
)
|
|
492
|
+
else:
|
|
493
|
+
# A missing rate — or a token count that was only approximated — is not
|
|
494
|
+
# itself evidence of a regression, so the regression gate stays
|
|
495
|
+
# non-failing rather than blocking every latency-only optimization that
|
|
496
|
+
# uses an unpriced model. But a *required* cost improvement can't be
|
|
497
|
+
# verified, so it must fail — we won't claim an improvement we can't
|
|
498
|
+
# compute.
|
|
499
|
+
reason = _cost_indeterminate_reason(pricing_known)
|
|
500
|
+
cost_regression_gate = _indeterminate_cost_gate(
|
|
501
|
+
"cost_regression", fail=False, reason=reason
|
|
502
|
+
)
|
|
503
|
+
cost_improvement_gate = _indeterminate_cost_gate(
|
|
504
|
+
"cost_improvement", fail=gates.min_cost_improvement_pct > 0, reason=reason
|
|
505
|
+
)
|
|
506
|
+
results = [
|
|
507
|
+
_gate(
|
|
508
|
+
"latency_regression",
|
|
509
|
+
deltas["latency_regression_pct"] <= gates.max_latency_regression_pct,
|
|
510
|
+
f"{deltas['latency_regression_pct']:.2f}% regression <= {gates.max_latency_regression_pct:.2f}% allowed",
|
|
511
|
+
),
|
|
512
|
+
cost_regression_gate,
|
|
513
|
+
_gate(
|
|
514
|
+
"latency_improvement",
|
|
515
|
+
deltas["latency_improvement_pct"] >= gates.min_latency_improvement_pct,
|
|
516
|
+
f"{deltas['latency_improvement_pct']:.2f}% improvement >= {gates.min_latency_improvement_pct:.2f}% required",
|
|
517
|
+
),
|
|
518
|
+
cost_improvement_gate,
|
|
519
|
+
]
|
|
520
|
+
if gates.require_retry_non_increase:
|
|
521
|
+
if "external_trial" in baseline and (
|
|
522
|
+
baseline.get("retry_count") is None or candidate.get("retry_count") is None
|
|
523
|
+
):
|
|
524
|
+
results.append(
|
|
525
|
+
_gate(
|
|
526
|
+
"retry_non_increase",
|
|
527
|
+
False,
|
|
528
|
+
"source retry counts are unavailable",
|
|
529
|
+
indeterminate=True,
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
else:
|
|
533
|
+
base_retries = int(baseline.get("retry_count", 0) or 0)
|
|
534
|
+
cand_retries = int(candidate.get("retry_count", 0) or 0)
|
|
535
|
+
results.append(
|
|
536
|
+
_gate(
|
|
537
|
+
"retry_non_increase",
|
|
538
|
+
cand_retries <= base_retries,
|
|
539
|
+
f"{cand_retries} candidate retries <= {base_retries} baseline retries",
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
if gates.require_schema_valid:
|
|
543
|
+
schema_valid = _candidate_schema_valid(candidate)
|
|
544
|
+
results.append(
|
|
545
|
+
_gate(
|
|
546
|
+
"schema_valid",
|
|
547
|
+
schema_valid,
|
|
548
|
+
"candidate schema validity passed"
|
|
549
|
+
if schema_valid
|
|
550
|
+
else "candidate schema validity missing or failed",
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
if quality_report is not None:
|
|
554
|
+
fixture_passed = bool(quality_report.get("passed"))
|
|
555
|
+
failed_count = int(quality_report.get("failed_case_count", 0) or 0)
|
|
556
|
+
versioned = quality_report.get("schema_version") == "2.0"
|
|
557
|
+
unavailable = quality_report.get("indeterminate_case_count", 0) if versioned else 0
|
|
558
|
+
results.append(
|
|
559
|
+
_gate(
|
|
560
|
+
"quality_fixtures",
|
|
561
|
+
fixture_passed,
|
|
562
|
+
"supplied quality fixtures passed"
|
|
563
|
+
if fixture_passed
|
|
564
|
+
else (
|
|
565
|
+
f"structured quality criteria failed; {failed_count} nonpassing cases, {unavailable} indeterminate cases; configured aggregate threshold retained"
|
|
566
|
+
if versioned
|
|
567
|
+
else f"{failed_count} supplied quality fixture case(s) failed"
|
|
568
|
+
),
|
|
569
|
+
indeterminate=bool(unavailable),
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
if quality_report is None and "quality_evidence" in candidate:
|
|
573
|
+
evidence = candidate["quality_evidence"]
|
|
574
|
+
results.append(
|
|
575
|
+
_gate(
|
|
576
|
+
"quality_evidence",
|
|
577
|
+
evidence.get("status") == "complete" and evidence.get("passed") is True,
|
|
578
|
+
"attached structured quality evidence passed"
|
|
579
|
+
if evidence.get("passed")
|
|
580
|
+
else "attached structured quality evidence is nonpassing or unavailable",
|
|
581
|
+
indeterminate=evidence.get("status") != "complete",
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
if gates.min_quality_score is not None:
|
|
585
|
+
quality_score = _optional_float(candidate.get("quality_score"))
|
|
586
|
+
passed = quality_score is not None and quality_score >= gates.min_quality_score
|
|
587
|
+
detail = (
|
|
588
|
+
f"{quality_score:.4f} candidate quality >= {gates.min_quality_score:.4f} required"
|
|
589
|
+
if quality_score is not None
|
|
590
|
+
else f"candidate quality score missing; {gates.min_quality_score:.4f} required"
|
|
591
|
+
)
|
|
592
|
+
versioned = (
|
|
593
|
+
quality_report is not None
|
|
594
|
+
and quality_report.get("schema_version") == "2.0"
|
|
595
|
+
or "quality_evidence" in candidate
|
|
596
|
+
)
|
|
597
|
+
results.append(
|
|
598
|
+
_gate(
|
|
599
|
+
"quality_score", passed, detail, indeterminate=versioned and quality_score is None
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
return results
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _gate(name: str, passed: bool, detail: str, *, indeterminate: bool = False) -> dict[str, Any]:
|
|
606
|
+
return {"name": name, "passed": passed, "detail": detail, "indeterminate": indeterminate}
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _cost_indeterminate_reason(pricing_known: bool) -> str:
|
|
610
|
+
"""Explain which missing input made a cost comparison indeterminate."""
|
|
611
|
+
|
|
612
|
+
if not pricing_known:
|
|
613
|
+
return "a model call has no known pricing (see cost_breakdown.unknown_models)"
|
|
614
|
+
return (
|
|
615
|
+
"token counts were approximated from word counts, so any cost derived "
|
|
616
|
+
"from them is not a measurement (see token_status)"
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _indeterminate_cost_gate(
|
|
621
|
+
name: str, *, fail: bool, reason: str = "a model call has no known pricing"
|
|
622
|
+
) -> dict[str, Any]:
|
|
623
|
+
"""A cost gate that couldn't be evaluated from the evidence available.
|
|
624
|
+
|
|
625
|
+
Two things can make a cost comparison meaningless: an unpriced model (the
|
|
626
|
+
totals are lower bounds) or token counts that were only approximated (the
|
|
627
|
+
totals are a rate times a guess). Rather than silently comparing those
|
|
628
|
+
numbers, the gate is marked ``indeterminate``. It fails only when the caller
|
|
629
|
+
explicitly required a cost outcome (``fail``) that therefore can't be
|
|
630
|
+
verified; otherwise it is a non-failing informational gate.
|
|
631
|
+
"""
|
|
632
|
+
detail = f"cost could not be evaluated: {reason}. " + (
|
|
633
|
+
"failing because a cost outcome was required." if fail else "not gating."
|
|
634
|
+
)
|
|
635
|
+
return _gate(name, passed=not fail, detail=detail, indeterminate=True)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _summary(
|
|
639
|
+
deltas: dict[str, Any],
|
|
640
|
+
passed: bool,
|
|
641
|
+
*,
|
|
642
|
+
cost_evaluable: bool = True,
|
|
643
|
+
pricing_known: bool = True,
|
|
644
|
+
) -> str:
|
|
645
|
+
status = "passed" if passed else "failed"
|
|
646
|
+
if cost_evaluable:
|
|
647
|
+
cost_phrase = f"cost improvement {deltas['cost_improvement_pct']:.2f}%"
|
|
648
|
+
elif not pricing_known:
|
|
649
|
+
cost_phrase = "cost improvement unavailable (unknown model pricing)"
|
|
650
|
+
else:
|
|
651
|
+
cost_phrase = "cost improvement unavailable (token counts are estimates)"
|
|
652
|
+
return (
|
|
653
|
+
f"Replay {status}: latency improvement {deltas['latency_improvement_pct']:.2f}%, "
|
|
654
|
+
f"{cost_phrase}, "
|
|
655
|
+
f"retry delta {deltas['retry_count_delta']}."
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _improvement_pct(baseline: float, candidate: float) -> float:
|
|
660
|
+
if baseline == 0:
|
|
661
|
+
return 0.0 if candidate == 0 else -100.0
|
|
662
|
+
return round(((baseline - candidate) / baseline) * 100, 2)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _relative_gain_pct(baseline: float | None, candidate: float | None) -> float:
|
|
666
|
+
if baseline is None:
|
|
667
|
+
return 0.0 if candidate is None else 100.0
|
|
668
|
+
if baseline == 0:
|
|
669
|
+
return 0.0 if not candidate else 100.0
|
|
670
|
+
return round((((candidate or 0.0) - baseline) / baseline) * 100, 2)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _regression_pct(baseline: float, candidate: float) -> float:
|
|
674
|
+
if baseline == 0:
|
|
675
|
+
return 0.0 if candidate == 0 else 100.0
|
|
676
|
+
return round(max(0.0, ((candidate - baseline) / baseline) * 100), 2)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _metric_row(
|
|
680
|
+
metric: str,
|
|
681
|
+
baseline: float,
|
|
682
|
+
candidate: float,
|
|
683
|
+
delta: float | None,
|
|
684
|
+
improvement: float | None,
|
|
685
|
+
kind: str,
|
|
686
|
+
) -> str:
|
|
687
|
+
improvement_cell = "unavailable" if improvement is None else f"{improvement:.2f}%"
|
|
688
|
+
return (
|
|
689
|
+
f"| {metric} | {_format_metric(baseline, kind)} | {_format_metric(candidate, kind)} | "
|
|
690
|
+
f"{_format_metric(delta, kind)} | {improvement_cell} |"
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _cost_metric_row(
|
|
695
|
+
baseline: dict[str, Any], candidate: dict[str, Any], deltas: dict[str, Any]
|
|
696
|
+
) -> str:
|
|
697
|
+
from agentloop.costs import format_cost_usd
|
|
698
|
+
|
|
699
|
+
improvement = deltas["cost_improvement_pct"]
|
|
700
|
+
improvement_cell = "unavailable" if improvement is None else f"{improvement:.2f}%"
|
|
701
|
+
return (
|
|
702
|
+
"| Cost | "
|
|
703
|
+
f"{format_cost_usd(baseline.get('estimated_cost_usd'), baseline.get('cost_status'), decimals=6)} | "
|
|
704
|
+
f"{format_cost_usd(candidate.get('estimated_cost_usd'), candidate.get('cost_status'), decimals=6)} | "
|
|
705
|
+
f"{_format_metric(deltas['cost_usd_delta'], 'usd')} | {improvement_cell} |"
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _format_metric(value: float | None, kind: str) -> str:
|
|
710
|
+
if value is None:
|
|
711
|
+
return "unavailable"
|
|
712
|
+
if kind == "usd":
|
|
713
|
+
return f"${value:.6f}"
|
|
714
|
+
if kind == "ms":
|
|
715
|
+
return f"{value:.3f}ms"
|
|
716
|
+
if kind == "pct":
|
|
717
|
+
return f"{value:.3f}%"
|
|
718
|
+
if kind == "score":
|
|
719
|
+
return f"{value:.4f}"
|
|
720
|
+
if kind == "exact_score":
|
|
721
|
+
return str(value)
|
|
722
|
+
return str(int(value))
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _optional_float(value: Any) -> float | None:
|
|
726
|
+
if value is None or value == "":
|
|
727
|
+
return None
|
|
728
|
+
return float(value)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _optional_bool(value: Any) -> bool | None:
|
|
732
|
+
if value is None or value == "":
|
|
733
|
+
return None
|
|
734
|
+
if isinstance(value, bool):
|
|
735
|
+
return value
|
|
736
|
+
if isinstance(value, str):
|
|
737
|
+
return value.strip().lower() in {"1", "true", "yes", "pass", "passed", "valid"}
|
|
738
|
+
return bool(value)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _schema_validity_from_bool(value: Any) -> float | None:
|
|
742
|
+
parsed = _optional_bool(value)
|
|
743
|
+
if parsed is None:
|
|
744
|
+
return None
|
|
745
|
+
return 100.0 if parsed else 0.0
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _candidate_schema_valid(candidate: dict[str, Any]) -> bool:
|
|
749
|
+
schema_valid = _optional_bool(candidate.get("schema_valid"))
|
|
750
|
+
if schema_valid is not None:
|
|
751
|
+
return schema_valid
|
|
752
|
+
validity_pct = _optional_float(candidate.get("schema_validity_pct"))
|
|
753
|
+
return validity_pct is not None and validity_pct >= 100.0
|