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/value.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from agentloop.costs import is_cost_evaluable
|
|
6
|
+
from agentloop.optimizer import build_optimization_plan
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_value_report(
|
|
10
|
+
trace: Any,
|
|
11
|
+
*,
|
|
12
|
+
runs_per_month: int = 1000,
|
|
13
|
+
engineer_hourly_rate_usd: float = 150.0,
|
|
14
|
+
incident_cost_usd: float = 500.0,
|
|
15
|
+
) -> dict[str, Any]:
|
|
16
|
+
"""Translate an optimization plan into operational value metrics.
|
|
17
|
+
|
|
18
|
+
The report connects a trace to modeled monthly cost, engineering time, latency,
|
|
19
|
+
and reliability impact. Its assumptions remain explicit so teams can use it for
|
|
20
|
+
capacity planning and optimization prioritization without treating estimates as
|
|
21
|
+
observed financial results.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from agentloop.interoperability.evidence import require_external_comparison
|
|
25
|
+
|
|
26
|
+
require_external_comparison(trace, "value estimates")
|
|
27
|
+
if runs_per_month < 0:
|
|
28
|
+
raise ValueError("runs_per_month must be non-negative")
|
|
29
|
+
if engineer_hourly_rate_usd < 0:
|
|
30
|
+
raise ValueError("engineer_hourly_rate_usd must be non-negative")
|
|
31
|
+
if incident_cost_usd < 0:
|
|
32
|
+
raise ValueError("incident_cost_usd must be non-negative")
|
|
33
|
+
|
|
34
|
+
plan = build_optimization_plan(trace)
|
|
35
|
+
current = plan["current"]
|
|
36
|
+
estimated_after = plan["estimated_after"]
|
|
37
|
+
all_cards = plan.get("optimization_cards", [])
|
|
38
|
+
cards = [
|
|
39
|
+
card
|
|
40
|
+
for card in all_cards
|
|
41
|
+
if (card.get("estimate") or {}).get("estimator_id") != "semantic_leaf_removal"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
cost_status = plan.get("cost_status", current.get("cost_status", "complete"))
|
|
45
|
+
cost_evaluable = is_cost_evaluable(cost_status)
|
|
46
|
+
current_cost_per_run = float(current.get("estimated_cost_usd", 0.0))
|
|
47
|
+
optimized_cost_raw = estimated_after.get("estimated_cost_usd")
|
|
48
|
+
optimized_cost_per_run = (
|
|
49
|
+
float(optimized_cost_raw) if cost_evaluable and optimized_cost_raw is not None else None
|
|
50
|
+
)
|
|
51
|
+
cost_savings_per_run = (
|
|
52
|
+
max(0.0, current_cost_per_run - optimized_cost_per_run)
|
|
53
|
+
if optimized_cost_per_run is not None
|
|
54
|
+
else None
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
current_runtime_ms = float(current.get("runtime_ms", 0.0))
|
|
58
|
+
optimized_runtime_ms = float(estimated_after.get("runtime_ms", current_runtime_ms))
|
|
59
|
+
latency_savings_ms_per_run = max(0.0, current_runtime_ms - optimized_runtime_ms)
|
|
60
|
+
|
|
61
|
+
retry_count = int(current.get("retry_count", 0) or 0)
|
|
62
|
+
reliability_risk_score = _risk_score(current=current, cards=cards)
|
|
63
|
+
avoided_incident_value = _avoided_incident_value(
|
|
64
|
+
retry_count=retry_count,
|
|
65
|
+
reliability_risk_score=reliability_risk_score,
|
|
66
|
+
incident_cost_usd=incident_cost_usd,
|
|
67
|
+
runs_per_month=runs_per_month,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
engineering_hours_saved_monthly = _engineering_hours_saved(cards, runs_per_month)
|
|
71
|
+
engineering_value_monthly = engineering_hours_saved_monthly * engineer_hourly_rate_usd
|
|
72
|
+
direct_model_cost_savings_monthly = (
|
|
73
|
+
cost_savings_per_run * runs_per_month if cost_savings_per_run is not None else None
|
|
74
|
+
)
|
|
75
|
+
latency_hours_saved_monthly = (latency_savings_ms_per_run * runs_per_month) / 3_600_000
|
|
76
|
+
non_cost_value_monthly = engineering_value_monthly + avoided_incident_value
|
|
77
|
+
total_value_monthly = (
|
|
78
|
+
direct_model_cost_savings_monthly + non_cost_value_monthly
|
|
79
|
+
if direct_model_cost_savings_monthly is not None
|
|
80
|
+
else None
|
|
81
|
+
)
|
|
82
|
+
pricing = (
|
|
83
|
+
_pricing_scenario(
|
|
84
|
+
monthly_value=total_value_monthly,
|
|
85
|
+
runs_per_month=runs_per_month,
|
|
86
|
+
reliability_risk_score=reliability_risk_score,
|
|
87
|
+
cards=cards,
|
|
88
|
+
)
|
|
89
|
+
if total_value_monthly is not None
|
|
90
|
+
else _unavailable_pricing_scenario(non_cost_value_monthly)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
**(
|
|
95
|
+
{"unmodeled_semantic_non_cost_findings": len(all_cards) - len(cards)}
|
|
96
|
+
if len(all_cards) != len(cards)
|
|
97
|
+
else {}
|
|
98
|
+
),
|
|
99
|
+
"run_id": plan.get("run_id"),
|
|
100
|
+
"name": plan.get("name"),
|
|
101
|
+
"cost_status": cost_status,
|
|
102
|
+
"assumptions": {
|
|
103
|
+
"runs_per_month": runs_per_month,
|
|
104
|
+
"engineer_hourly_rate_usd": engineer_hourly_rate_usd,
|
|
105
|
+
"incident_cost_usd": incident_cost_usd,
|
|
106
|
+
},
|
|
107
|
+
"current": current,
|
|
108
|
+
"estimated_after": estimated_after,
|
|
109
|
+
"per_run": {
|
|
110
|
+
"cost_savings_usd": (
|
|
111
|
+
None if cost_savings_per_run is None else round(cost_savings_per_run, 6)
|
|
112
|
+
),
|
|
113
|
+
"latency_savings_ms": round(latency_savings_ms_per_run, 3),
|
|
114
|
+
"latency_reduction_pct": estimated_after.get("latency_reduction_pct", 0.0),
|
|
115
|
+
"cost_reduction_pct": estimated_after.get("cost_reduction_pct", 0.0),
|
|
116
|
+
},
|
|
117
|
+
"monthly_value": {
|
|
118
|
+
"direct_model_cost_savings_usd": (
|
|
119
|
+
None
|
|
120
|
+
if direct_model_cost_savings_monthly is None
|
|
121
|
+
else round(direct_model_cost_savings_monthly, 2)
|
|
122
|
+
),
|
|
123
|
+
"engineering_hours_saved": round(engineering_hours_saved_monthly, 2),
|
|
124
|
+
"engineering_value_usd": round(engineering_value_monthly, 2),
|
|
125
|
+
"latency_hours_saved": round(latency_hours_saved_monthly, 2),
|
|
126
|
+
"avoided_incident_value_usd": round(avoided_incident_value, 2),
|
|
127
|
+
"non_cost_operational_value_usd": round(non_cost_value_monthly, 2),
|
|
128
|
+
"total_value_usd": (
|
|
129
|
+
None if total_value_monthly is None else round(total_value_monthly, 2)
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
"pricing": pricing,
|
|
133
|
+
"reliability": {
|
|
134
|
+
"risk_score": reliability_risk_score,
|
|
135
|
+
"retry_count": retry_count,
|
|
136
|
+
"high_confidence_fixes": sum(1 for card in cards if card.get("confidence") == "high"),
|
|
137
|
+
"top_risks": _top_risks(cards),
|
|
138
|
+
},
|
|
139
|
+
"value_summary": _value_summary(
|
|
140
|
+
monthly_value=total_value_monthly,
|
|
141
|
+
latency_savings_ms_per_run=latency_savings_ms_per_run,
|
|
142
|
+
cost_savings_per_run=cost_savings_per_run,
|
|
143
|
+
cards=cards,
|
|
144
|
+
pricing=pricing,
|
|
145
|
+
),
|
|
146
|
+
"optimization_cards": all_cards,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _risk_score(*, current: dict[str, Any], cards: list[dict[str, Any]]) -> int:
|
|
151
|
+
score = 0
|
|
152
|
+
retry_count = int(current.get("retry_count", 0) or 0)
|
|
153
|
+
repeated_context_ratio = float(current.get("repeated_context_ratio", 0.0) or 0.0)
|
|
154
|
+
input_tokens = int(current.get("input_tokens", 0) or 0)
|
|
155
|
+
|
|
156
|
+
score += min(35, retry_count * 12)
|
|
157
|
+
score += min(25, int(repeated_context_ratio * 100))
|
|
158
|
+
score += min(20, input_tokens // 5000)
|
|
159
|
+
score += min(20, len(cards) * 4)
|
|
160
|
+
return max(0, min(100, score))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _engineering_hours_saved(cards: list[dict[str, Any]], runs_per_month: int) -> float:
|
|
164
|
+
if not cards or runs_per_month == 0:
|
|
165
|
+
return 0.0
|
|
166
|
+
|
|
167
|
+
# Conservative proxy: repeated production inefficiencies create triage and refactor
|
|
168
|
+
# work. Cap it to avoid turning this into a fake precision calculator.
|
|
169
|
+
base_hours = min(12.0, len(cards) * 1.25)
|
|
170
|
+
scale = min(3.0, max(0.25, runs_per_month / 1000))
|
|
171
|
+
high_confidence_bonus = sum(0.5 for card in cards if card.get("confidence") == "high")
|
|
172
|
+
return min(40.0, (base_hours + high_confidence_bonus) * scale)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _avoided_incident_value(
|
|
176
|
+
*,
|
|
177
|
+
retry_count: int,
|
|
178
|
+
reliability_risk_score: int,
|
|
179
|
+
incident_cost_usd: float,
|
|
180
|
+
runs_per_month: int,
|
|
181
|
+
) -> float:
|
|
182
|
+
if runs_per_month == 0 or incident_cost_usd == 0:
|
|
183
|
+
return 0.0
|
|
184
|
+
monthly_failure_probability = min(0.25, (retry_count * 0.01) + (reliability_risk_score / 1000))
|
|
185
|
+
volume_multiplier = min(2.0, max(0.25, runs_per_month / 1000))
|
|
186
|
+
return incident_cost_usd * monthly_failure_probability * volume_multiplier
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _pricing_scenario(
|
|
190
|
+
*,
|
|
191
|
+
monthly_value: float,
|
|
192
|
+
runs_per_month: int,
|
|
193
|
+
reliability_risk_score: int,
|
|
194
|
+
cards: list[dict[str, Any]],
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""Build a conservative pricing scenario from modeled operational value.
|
|
197
|
+
|
|
198
|
+
This is not a billing system or price quote. It provides a configurable tier,
|
|
199
|
+
monthly amount, and value-to-price ratio for planning comparisons.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
if runs_per_month == 0 or monthly_value <= 0 or not cards:
|
|
203
|
+
suggested_plan = "free"
|
|
204
|
+
suggested_monthly_price_usd = 0
|
|
205
|
+
rationale = "No meaningful optimization value was detected for this trace."
|
|
206
|
+
elif monthly_value < 750 and runs_per_month < 1000:
|
|
207
|
+
suggested_plan = "pro"
|
|
208
|
+
suggested_monthly_price_usd = 49
|
|
209
|
+
rationale = "Low-volume workload with measurable optimization value."
|
|
210
|
+
elif monthly_value < 5000 and reliability_risk_score < 60:
|
|
211
|
+
suggested_plan = "team"
|
|
212
|
+
suggested_monthly_price_usd = 499
|
|
213
|
+
rationale = "Mid-volume workload with shared tracing and optimization needs."
|
|
214
|
+
elif monthly_value < 25000:
|
|
215
|
+
suggested_plan = "growth"
|
|
216
|
+
suggested_monthly_price_usd = 1500
|
|
217
|
+
rationale = "Higher-volume workload with material reliability and latency impact."
|
|
218
|
+
else:
|
|
219
|
+
suggested_plan = "enterprise"
|
|
220
|
+
suggested_monthly_price_usd = 5000
|
|
221
|
+
rationale = (
|
|
222
|
+
"Large modeled value; review deployment, retention, support, and usage requirements."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
value_to_price_ratio = None
|
|
226
|
+
if suggested_monthly_price_usd > 0:
|
|
227
|
+
value_to_price_ratio = round(monthly_value / suggested_monthly_price_usd, 2)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"suggested_plan": suggested_plan,
|
|
231
|
+
"suggested_monthly_price_usd": suggested_monthly_price_usd,
|
|
232
|
+
"estimated_monthly_value_usd": round(monthly_value, 2),
|
|
233
|
+
"value_to_price_ratio": value_to_price_ratio,
|
|
234
|
+
"rationale": rationale,
|
|
235
|
+
"scenario_notes": _scenario_notes(suggested_plan),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _unavailable_pricing_scenario(non_cost_value: float) -> dict[str, Any]:
|
|
240
|
+
return {
|
|
241
|
+
"suggested_plan": None,
|
|
242
|
+
"suggested_monthly_price_usd": None,
|
|
243
|
+
"estimated_monthly_value_usd": None,
|
|
244
|
+
"estimated_non_cost_monthly_value_usd": round(non_cost_value, 2),
|
|
245
|
+
"value_to_price_ratio": None,
|
|
246
|
+
"rationale": "Pricing is unavailable because one or more model-call costs are unknown.",
|
|
247
|
+
"scenario_notes": [],
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _scenario_notes(plan: str) -> list[str]:
|
|
252
|
+
if plan == "free":
|
|
253
|
+
return ["Local traces", "CLI reports", "Manual optimization plan export"]
|
|
254
|
+
if plan == "pro":
|
|
255
|
+
return ["Local + API trace upload", "Value reports", "Single project workspace"]
|
|
256
|
+
if plan == "team":
|
|
257
|
+
return ["Shared dashboard", "API keys", "Shared projects", "Optimization reports"]
|
|
258
|
+
if plan == "growth":
|
|
259
|
+
return [
|
|
260
|
+
"Higher usage limits",
|
|
261
|
+
"Postgres backend",
|
|
262
|
+
"Extended retention",
|
|
263
|
+
"Operational value review",
|
|
264
|
+
]
|
|
265
|
+
return ["Private deployment", "Custom retention", "Security review", "Custom integrations"]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _top_risks(cards: list[dict[str, Any]]) -> list[str]:
|
|
269
|
+
risks = []
|
|
270
|
+
for card in cards[:3]:
|
|
271
|
+
title = str(card.get("title", "Optimization opportunity"))
|
|
272
|
+
confidence = str(card.get("confidence", "unknown"))
|
|
273
|
+
risks.append(f"{title} ({confidence} confidence)")
|
|
274
|
+
return risks
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _value_summary(
|
|
278
|
+
*,
|
|
279
|
+
monthly_value: float | None,
|
|
280
|
+
latency_savings_ms_per_run: float,
|
|
281
|
+
cost_savings_per_run: float | None,
|
|
282
|
+
cards: list[dict[str, Any]],
|
|
283
|
+
pricing: dict[str, Any],
|
|
284
|
+
) -> str:
|
|
285
|
+
strongest_fix = cards[0]["title"] if cards else "No major bottleneck detected"
|
|
286
|
+
if monthly_value is None or cost_savings_per_run is None:
|
|
287
|
+
return (
|
|
288
|
+
f"AgentLoop found {len(cards)} optimization opportunities. "
|
|
289
|
+
f"The strongest immediate fix is: {strongest_fix}. "
|
|
290
|
+
f"Estimated latency savings are {latency_savings_ms_per_run / 1000:.2f}s per run. "
|
|
291
|
+
"Model-cost savings, total monthly value, and pricing are unavailable because "
|
|
292
|
+
"one or more model-call costs could not be priced."
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
price = pricing.get("suggested_monthly_price_usd", 0)
|
|
296
|
+
plan = pricing.get("suggested_plan", "free")
|
|
297
|
+
price_line = "free local usage" if price == 0 else f"the {plan} plan at ${price:,.0f}/month"
|
|
298
|
+
return (
|
|
299
|
+
f"AgentLoop found {len(cards)} optimization opportunities. "
|
|
300
|
+
f"The strongest immediate fix is: {strongest_fix}. "
|
|
301
|
+
f"At the provided usage assumptions, this trace represents about "
|
|
302
|
+
f"${monthly_value:,.0f}/month in modeled value, with "
|
|
303
|
+
f"{latency_savings_ms_per_run / 1000:.2f}s latency saved and "
|
|
304
|
+
f"${cost_savings_per_run:.4f} model cost saved per run. "
|
|
305
|
+
f"A modeled pricing scenario is {price_line}."
|
|
306
|
+
)
|
agentloop/version.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Portable workflow/stage metadata without runtime or framework dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, fields
|
|
9
|
+
|
|
10
|
+
from agentloop.operations import normalize_operation_kind, operation_kind
|
|
11
|
+
|
|
12
|
+
WORKFLOW_KEY = "agentloop.workflow"
|
|
13
|
+
STAGE_KEY = "agentloop.stage"
|
|
14
|
+
METADATA_VERSION = "1.0"
|
|
15
|
+
_OUTCOME = re.compile(r"[A-Za-z0-9_.:-]{1,128}\Z")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _text(value, name, *, optional=False):
|
|
19
|
+
if value is None and optional:
|
|
20
|
+
return
|
|
21
|
+
if not isinstance(value, str) or not value.strip():
|
|
22
|
+
raise ValueError(f"{name} must be nonempty text")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _copy_metadata(value):
|
|
26
|
+
if value is None:
|
|
27
|
+
return {}
|
|
28
|
+
if value is not None and not isinstance(value, dict):
|
|
29
|
+
raise ValueError("metadata must be a JSON object")
|
|
30
|
+
try:
|
|
31
|
+
return json.loads(json.dumps(value or {}, allow_nan=False))
|
|
32
|
+
except (TypeError, ValueError, RecursionError):
|
|
33
|
+
raise ValueError("metadata must contain finite JSON values") from None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _values(value):
|
|
37
|
+
# Declarations contain only validated immutable strings/None; no deep copy.
|
|
38
|
+
return {item.name: getattr(value, item.name) for item in fields(value)}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class WorkflowInfo:
|
|
43
|
+
workflow_id: str
|
|
44
|
+
version: str
|
|
45
|
+
input_schema_ref: str | None = None
|
|
46
|
+
output_schema_ref: str | None = None
|
|
47
|
+
input_ref: str | None = None
|
|
48
|
+
output_ref: str | None = None
|
|
49
|
+
|
|
50
|
+
def __post_init__(self):
|
|
51
|
+
for key, value in _values(self).items():
|
|
52
|
+
_text(value, key, optional=key not in {"workflow_id", "version"})
|
|
53
|
+
|
|
54
|
+
def to_dict(self):
|
|
55
|
+
return {"schema_version": METADATA_VERSION, **_values(self)}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class StageInfo:
|
|
60
|
+
stage_id: str
|
|
61
|
+
version: str
|
|
62
|
+
kind: str | None = None
|
|
63
|
+
input_schema_ref: str | None = None
|
|
64
|
+
output_schema_ref: str | None = None
|
|
65
|
+
input_ref: str | None = None
|
|
66
|
+
output_ref: str | None = None
|
|
67
|
+
|
|
68
|
+
def __post_init__(self):
|
|
69
|
+
for key, value in _values(self).items():
|
|
70
|
+
_text(value, key, optional=key not in {"stage_id", "version"})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def workflow_metadata(
|
|
74
|
+
info: WorkflowInfo, *, task_id=None, example_id=None, status="unknown", metadata=None
|
|
75
|
+
):
|
|
76
|
+
"""Build portable workflow metadata without importing a host framework."""
|
|
77
|
+
if type(info) is not WorkflowInfo:
|
|
78
|
+
raise ValueError("workflow must be WorkflowInfo")
|
|
79
|
+
if status not in ("unknown", "running", "completed", "failed", "cancelled", "interrupted"):
|
|
80
|
+
raise ValueError("unsupported workflow execution status")
|
|
81
|
+
result = _copy_metadata(metadata)
|
|
82
|
+
if WORKFLOW_KEY in result:
|
|
83
|
+
raise ValueError("use WorkflowInfo instead of supplying the reserved workflow namespace")
|
|
84
|
+
for key, value in (("task_id", task_id), ("example_id", example_id)):
|
|
85
|
+
if value is not None:
|
|
86
|
+
if type(value) is not int and (not isinstance(value, str) or not value):
|
|
87
|
+
raise ValueError(f"{key} must be an integer or nonempty string")
|
|
88
|
+
if key in result and (type(result[key]) is not type(value) or result[key] != value):
|
|
89
|
+
raise ValueError(f"conflicting {key}")
|
|
90
|
+
result[key] = value
|
|
91
|
+
result[WORKFLOW_KEY] = {**info.to_dict(), "status": status, "outcome": None}
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def operation_metadata(kind, *, stage=None, depends_on=None, metadata=None):
|
|
96
|
+
"""Annotate new operations or an existing model/tool integration's metadata.
|
|
97
|
+
|
|
98
|
+
``depends_on=None`` means undeclared. An explicit empty list declares a
|
|
99
|
+
root operation, not general side-effect or parallel-execution safety.
|
|
100
|
+
"""
|
|
101
|
+
_text(kind, "kind")
|
|
102
|
+
if stage is not None and type(stage) is not StageInfo:
|
|
103
|
+
raise ValueError("stage must be StageInfo")
|
|
104
|
+
result = _copy_metadata(metadata)
|
|
105
|
+
if STAGE_KEY in result:
|
|
106
|
+
raise ValueError("use StageInfo instead of supplying the reserved stage namespace")
|
|
107
|
+
if "operation_kind" in result and result["operation_kind"] != kind:
|
|
108
|
+
raise ValueError("conflicting operation_kind")
|
|
109
|
+
if depends_on is None and "depends_on" in result:
|
|
110
|
+
depends_on = result["depends_on"]
|
|
111
|
+
if depends_on is not None:
|
|
112
|
+
if not isinstance(depends_on, (list, tuple, set, frozenset)):
|
|
113
|
+
raise ValueError("depends_on must be a collection of span IDs")
|
|
114
|
+
for reference in depends_on:
|
|
115
|
+
_text(reference, "dependency reference")
|
|
116
|
+
references = sorted(set(depends_on))
|
|
117
|
+
if "depends_on" in result and (
|
|
118
|
+
not isinstance(result["depends_on"], list)
|
|
119
|
+
or any(not isinstance(item, str) for item in result["depends_on"])
|
|
120
|
+
or sorted(set(result["depends_on"])) != references
|
|
121
|
+
):
|
|
122
|
+
raise ValueError("conflicting depends_on")
|
|
123
|
+
result["depends_on"] = references
|
|
124
|
+
else:
|
|
125
|
+
references = []
|
|
126
|
+
result["operation_kind"] = kind
|
|
127
|
+
result[STAGE_KEY] = {
|
|
128
|
+
"schema_version": METADATA_VERSION,
|
|
129
|
+
"stage": _values(stage) if stage is not None else None,
|
|
130
|
+
"dependencies_declared": depends_on is not None,
|
|
131
|
+
"depends_on": references,
|
|
132
|
+
"outcome": None,
|
|
133
|
+
"output_ref": None,
|
|
134
|
+
}
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _set_outcome(value, outcome, output_ref):
|
|
139
|
+
if not isinstance(outcome, str) or not _OUTCOME.fullmatch(outcome):
|
|
140
|
+
raise ValueError("outcome must be a bounded label, not an output body")
|
|
141
|
+
_text(output_ref, "output_ref", optional=True)
|
|
142
|
+
value["outcome"] = outcome
|
|
143
|
+
if output_ref is not None:
|
|
144
|
+
value["output_ref"] = output_ref
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def set_workflow_outcome(trace, outcome, *, output_ref=None):
|
|
148
|
+
"""Record a caller-defined result label/reference, independently of task quality."""
|
|
149
|
+
info = trace.metadata.get(WORKFLOW_KEY)
|
|
150
|
+
summary = workflow_summary(trace.metadata)
|
|
151
|
+
if summary is None or summary.get("schema_status") != "supported":
|
|
152
|
+
raise ValueError("trace has no supported workflow declaration")
|
|
153
|
+
_set_outcome(info, outcome, output_ref)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def workflow_summary(metadata):
|
|
157
|
+
"""Select declared profiling fields; unknown source metadata remains untouched."""
|
|
158
|
+
raw = metadata.get(WORKFLOW_KEY) if isinstance(metadata, dict) else None
|
|
159
|
+
if not isinstance(raw, dict):
|
|
160
|
+
return None
|
|
161
|
+
if raw.get("schema_version") != METADATA_VERSION:
|
|
162
|
+
return {
|
|
163
|
+
"schema_version": raw.get("schema_version")
|
|
164
|
+
if isinstance(raw.get("schema_version"), str)
|
|
165
|
+
else None,
|
|
166
|
+
"schema_status": "unsupported",
|
|
167
|
+
}
|
|
168
|
+
if any(not isinstance(raw.get(key), str) or not raw[key] for key in ("workflow_id", "version")):
|
|
169
|
+
return {"schema_version": METADATA_VERSION, "schema_status": "invalid"}
|
|
170
|
+
return {
|
|
171
|
+
"schema_version": METADATA_VERSION,
|
|
172
|
+
"schema_status": "supported",
|
|
173
|
+
"kind": "workflow",
|
|
174
|
+
**{
|
|
175
|
+
key: metadata[key]
|
|
176
|
+
for key in ("task_id", "example_id")
|
|
177
|
+
if key in metadata
|
|
178
|
+
and (
|
|
179
|
+
type(metadata[key]) in {str, int, bool}
|
|
180
|
+
or type(metadata[key]) is float
|
|
181
|
+
and math.isfinite(metadata[key])
|
|
182
|
+
)
|
|
183
|
+
},
|
|
184
|
+
**{
|
|
185
|
+
key: raw.get(key) if isinstance(raw.get(key), str) else None
|
|
186
|
+
for key in (
|
|
187
|
+
"workflow_id",
|
|
188
|
+
"version",
|
|
189
|
+
"input_schema_ref",
|
|
190
|
+
"output_schema_ref",
|
|
191
|
+
"input_ref",
|
|
192
|
+
"output_ref",
|
|
193
|
+
"status",
|
|
194
|
+
"outcome",
|
|
195
|
+
)
|
|
196
|
+
},
|
|
197
|
+
"cost_scope": "recorded_model_calls",
|
|
198
|
+
# Measurement scope for model input/output token counts.
|
|
199
|
+
"token_scope": "recorded_model_calls", # nosec B105
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def stage_summary(event):
|
|
204
|
+
metadata = getattr(event, "metadata", None)
|
|
205
|
+
raw = metadata.get(STAGE_KEY) if isinstance(metadata, dict) else None
|
|
206
|
+
if not isinstance(raw, dict):
|
|
207
|
+
return None
|
|
208
|
+
if raw.get("schema_version") != METADATA_VERSION:
|
|
209
|
+
return {
|
|
210
|
+
"schema_version": raw.get("schema_version")
|
|
211
|
+
if isinstance(raw.get("schema_version"), str)
|
|
212
|
+
else None,
|
|
213
|
+
"schema_status": "unsupported",
|
|
214
|
+
"kind": "unknown",
|
|
215
|
+
}
|
|
216
|
+
stage = raw.get("stage")
|
|
217
|
+
stage = stage if isinstance(stage, dict) else {}
|
|
218
|
+
raw_kind = stage.get("kind")
|
|
219
|
+
kind = normalize_operation_kind(raw_kind) if raw_kind is not None else operation_kind(event)
|
|
220
|
+
dependencies = raw.get("depends_on")
|
|
221
|
+
dependencies_valid = isinstance(dependencies, list) and all(
|
|
222
|
+
isinstance(value, str) and value for value in dependencies
|
|
223
|
+
)
|
|
224
|
+
if "depends_on" in metadata:
|
|
225
|
+
parallel_dependencies = metadata["depends_on"]
|
|
226
|
+
dependencies_valid = (
|
|
227
|
+
dependencies_valid
|
|
228
|
+
and isinstance(parallel_dependencies, list)
|
|
229
|
+
and all(isinstance(value, str) and value for value in parallel_dependencies)
|
|
230
|
+
and set(parallel_dependencies) == set(dependencies)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def selected_text(value):
|
|
234
|
+
return value if isinstance(value, str) else None
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
"schema_version": METADATA_VERSION,
|
|
238
|
+
"schema_status": "supported",
|
|
239
|
+
"kind": kind,
|
|
240
|
+
"raw_kind": raw_kind if isinstance(raw_kind, str) else None,
|
|
241
|
+
**{
|
|
242
|
+
key: stage.get(key) if isinstance(stage.get(key), str) else None
|
|
243
|
+
for key in (
|
|
244
|
+
"stage_id",
|
|
245
|
+
"version",
|
|
246
|
+
"input_schema_ref",
|
|
247
|
+
"output_schema_ref",
|
|
248
|
+
"input_ref",
|
|
249
|
+
"output_ref",
|
|
250
|
+
)
|
|
251
|
+
},
|
|
252
|
+
"output_ref": selected_text(raw.get("output_ref"))
|
|
253
|
+
or selected_text(stage.get("output_ref")),
|
|
254
|
+
"outcome": selected_text(raw.get("outcome")),
|
|
255
|
+
"execution_status": selected_text(raw.get("execution_status"))
|
|
256
|
+
or {"ok": "completed", "error": "failed"}.get(getattr(event, "status", None), "unknown"),
|
|
257
|
+
"dependency_status": "invalid"
|
|
258
|
+
if raw.get("dependencies_declared") is True and not dependencies_valid
|
|
259
|
+
else "declared"
|
|
260
|
+
if raw.get("dependencies_declared") is True
|
|
261
|
+
else "undeclared",
|
|
262
|
+
"dependencies_declared": raw.get("dependencies_declared") is True and dependencies_valid,
|
|
263
|
+
"depends_on": list(dependencies) if dependencies_valid else [],
|
|
264
|
+
}
|