agentloop-profiler 0.4.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.

Potentially problematic release.


This version of agentloop-profiler might be problematic. Click here for more details.

Files changed (50) hide show
  1. agentloop/__init__.py +68 -0
  2. agentloop/audit.py +34 -0
  3. agentloop/autoinstrument.py +102 -0
  4. agentloop/ci.py +135 -0
  5. agentloop/cli.py +876 -0
  6. agentloop/client.py +170 -0
  7. agentloop/config.py +50 -0
  8. agentloop/costs.py +15 -0
  9. agentloop/decorators.py +160 -0
  10. agentloop/demo.py +283 -0
  11. agentloop/doctor.py +344 -0
  12. agentloop/events.py +49 -0
  13. agentloop/exporters.py +60 -0
  14. agentloop/findings.py +215 -0
  15. agentloop/graph.py +312 -0
  16. agentloop/integrations/__init__.py +1 -0
  17. agentloop/integrations/crewai.py +144 -0
  18. agentloop/integrations/langgraph.py +140 -0
  19. agentloop/integrations/openai.py +369 -0
  20. agentloop/integrations/openai_agents.py +212 -0
  21. agentloop/integrations/sdk.py +82 -0
  22. agentloop/integrations/vercel_ai.py +83 -0
  23. agentloop/issues.py +88 -0
  24. agentloop/markdown.py +102 -0
  25. agentloop/metrics.py +123 -0
  26. agentloop/optimizer.py +323 -0
  27. agentloop/otel.py +302 -0
  28. agentloop/otel_ids.py +81 -0
  29. agentloop/patches.py +583 -0
  30. agentloop/plan_export.py +72 -0
  31. agentloop/quality.py +222 -0
  32. agentloop/replay.py +424 -0
  33. agentloop/runtime.py +244 -0
  34. agentloop/savings.py +135 -0
  35. agentloop/server.py +292 -0
  36. agentloop/store.py +840 -0
  37. agentloop/timing.py +124 -0
  38. agentloop/tracer.py +356 -0
  39. agentloop/value.py +243 -0
  40. agentloop/version.py +3 -0
  41. agentloop_profiler-0.4.0.dist-info/METADATA +598 -0
  42. agentloop_profiler-0.4.0.dist-info/RECORD +50 -0
  43. agentloop_profiler-0.4.0.dist-info/WHEEL +5 -0
  44. agentloop_profiler-0.4.0.dist-info/entry_points.txt +2 -0
  45. agentloop_profiler-0.4.0.dist-info/licenses/LICENSE +201 -0
  46. agentloop_profiler-0.4.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +54 -0
  47. agentloop_profiler-0.4.0.dist-info/top_level.txt +2 -0
  48. dashboard/__init__.py +1 -0
  49. dashboard/app.py +698 -0
  50. dashboard/value_view.py +101 -0
dashboard/app.py ADDED
@@ -0,0 +1,698 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ import streamlit as st
8
+
9
+ from agentloop.ci import build_ci_report, ci_report_to_markdown
10
+ from agentloop.demo import run_baseline, run_langgraph_style, run_optimized, run_proof_pair
11
+ from agentloop.findings import build_diagnosis
12
+ from agentloop.issues import build_issue_drafts, issue_drafts_to_markdown
13
+ from agentloop.optimizer import build_optimization_plan
14
+ from agentloop.patches import build_patch_plan
15
+ from agentloop.quality import build_quality_report, quality_report_to_markdown
16
+ from agentloop.replay import ReplayGates, build_replay_report
17
+ from agentloop.store import get_store
18
+ from agentloop.tracer import AgentTrace
19
+ from agentloop.value import build_value_report
20
+
21
+ try:
22
+ from dashboard.value_view import assumption_inputs, render_value_report
23
+ except ModuleNotFoundError:
24
+ from value_view import assumption_inputs, render_value_report
25
+
26
+ st.set_page_config(page_title="AgentLoop Dashboard", layout="wide")
27
+
28
+ runs_dir = Path("runs")
29
+ runs_dir.mkdir(parents=True, exist_ok=True)
30
+
31
+
32
+ @st.cache_resource
33
+ def load_store():
34
+ db = get_store()
35
+ db.init()
36
+ return db
37
+
38
+
39
+ def money(value: float) -> str:
40
+ return f"${float(value):,.4f}"
41
+
42
+
43
+ def seconds(ms: float) -> str:
44
+ return f"{float(ms) / 1000:,.2f}s"
45
+
46
+
47
+ def load_trace_for_project(run_id: str, project_id: str) -> AgentTrace | None:
48
+ return load_store().get_trace(run_id=run_id, project_id=project_id)
49
+
50
+
51
+ def select_trace(traces: list[dict], label: str = "Choose trace") -> AgentTrace | None:
52
+ labels = {f"{t['name']} · {t['run_id']}": t["run_id"] for t in traces}
53
+ selected_label = st.selectbox(label, list(labels.keys()))
54
+ return load_trace_for_project(labels[selected_label], project_id)
55
+
56
+
57
+ def trace_options(traces: list[dict]) -> dict[str, str]:
58
+ return {f"{t['name']} - {t['run_id']}": t["run_id"] for t in traces}
59
+
60
+
61
+ def selected_trace_from_options(traces: list[dict], label: str) -> AgentTrace | None:
62
+ options = trace_options(traces)
63
+ selected = st.selectbox(label, list(options.keys()))
64
+ return load_trace_for_project(options[selected], project_id)
65
+
66
+
67
+ def render_gate_table(results: list[dict]) -> None:
68
+ rows = [
69
+ {
70
+ "gate": item["name"],
71
+ "status": "pass" if item["passed"] else "fail",
72
+ "detail": item["detail"],
73
+ }
74
+ for item in results
75
+ ]
76
+ st.dataframe(pd.DataFrame(rows), width="stretch", hide_index=True)
77
+
78
+
79
+ st.sidebar.title("AgentLoop")
80
+ st.sidebar.caption("Agent-loop performance control panel")
81
+
82
+ project_id = st.sidebar.text_input("Project", value=st.session_state.get("project_id", "default"))
83
+ st.session_state["project_id"] = project_id
84
+
85
+ page = st.sidebar.radio(
86
+ "Navigate",
87
+ [
88
+ "Overview",
89
+ "Traces",
90
+ "Optimization Queue",
91
+ "Optimization",
92
+ "Diagnosis",
93
+ "Patch Plan",
94
+ "Replay Proof",
95
+ "Quality Gates",
96
+ "Value & Pricing",
97
+ "API Keys",
98
+ "Ingest",
99
+ "Setup",
100
+ ],
101
+ )
102
+
103
+ store = load_store()
104
+
105
+ st.title("AgentLoop Dashboard")
106
+ st.caption("Cost, latency, and execution-graph optimization for production AI agents")
107
+
108
+ if page == "Overview":
109
+ summary = store.usage_summary(project_id=project_id)
110
+ traces = store.list_traces(project_id=project_id)
111
+
112
+ c1, c2, c3, c4 = st.columns(4)
113
+ c1.metric("Runs", int(summary.get("run_count", 0)))
114
+ c2.metric("Total runtime", seconds(summary.get("total_runtime_ms", 0)))
115
+ c3.metric("Estimated LLM cost", money(summary.get("estimated_cost_usd", 0)))
116
+ c4.metric("Retries", int(summary.get("retry_count", 0)))
117
+
118
+ c5, c6, c7, c8 = st.columns(4)
119
+ c5.metric("Input tokens", int(summary.get("input_tokens", 0)))
120
+ c6.metric("Output tokens", int(summary.get("output_tokens", 0)))
121
+ c7.metric("Model calls", int(summary.get("model_call_count", 0)))
122
+ c8.metric("Tool calls", int(summary.get("tool_call_count", 0)))
123
+
124
+ st.subheader("Recent runs")
125
+ if traces:
126
+ df = pd.DataFrame(traces)
127
+ st.dataframe(df, width="stretch")
128
+
129
+ chart_df = df[["name", "total_runtime_ms", "estimated_cost_usd"]].copy()
130
+ chart_df["runtime_s"] = chart_df["total_runtime_ms"] / 1000
131
+ st.subheader("Runtime by run")
132
+ st.bar_chart(chart_df, x="name", y="runtime_s")
133
+ else:
134
+ st.info(
135
+ "No stored traces yet. Use the Ingest page or run "
136
+ "`uv run agentloop store-trace --path TRACE.json`."
137
+ )
138
+
139
+ elif page == "Traces":
140
+ st.subheader("Stored traces")
141
+ traces = store.list_traces(project_id=project_id)
142
+ if not traces:
143
+ st.info("No traces stored for this project yet.")
144
+ else:
145
+ df = pd.DataFrame(traces)
146
+ st.dataframe(df, width="stretch")
147
+ selected_run = st.selectbox("Open run", df["run_id"].tolist())
148
+ trace = load_trace_for_project(selected_run, project_id)
149
+ if trace is not None:
150
+ report = trace.report()
151
+ st.markdown(f"### {trace.name}")
152
+ c1, c2, c3, c4 = st.columns(4)
153
+ c1.metric("Runtime", seconds(report["total_runtime_ms"]))
154
+ c2.metric("Cost", money(report["estimated_cost_usd"]))
155
+ c3.metric("Model calls", report["model_call_count"])
156
+ c4.metric("Tool calls", report["tool_call_count"])
157
+
158
+ events = pd.DataFrame(report["events"])
159
+ st.subheader("Event timeline")
160
+ if not events.empty:
161
+ cols = [
162
+ "event_type",
163
+ "name",
164
+ "duration_ms",
165
+ "model",
166
+ "input_tokens",
167
+ "output_tokens",
168
+ "status",
169
+ ]
170
+ for col in cols:
171
+ if col not in events.columns:
172
+ events[col] = None
173
+ st.dataframe(events[cols], width="stretch")
174
+
175
+ st.download_button(
176
+ "Download trace JSON",
177
+ data=json.dumps(trace.to_dict(), indent=2),
178
+ file_name=f"{trace.run_id}.json",
179
+ mime="application/json",
180
+ )
181
+
182
+ elif page == "Optimization Queue":
183
+ st.subheader("Optimization queue")
184
+ queue = store.optimization_queue(project_id=project_id)
185
+ findings = store.list_findings(project_id=project_id)
186
+
187
+ c1, c2, c3, c4 = st.columns(4)
188
+ c1.metric("Open opportunities", len(queue))
189
+ c2.metric("Persisted findings", len(findings))
190
+ c3.metric("Patchable findings", sum(1 for finding in findings if finding["patchable"]))
191
+ c4.metric(
192
+ "Estimated latency savings",
193
+ seconds(sum(item["estimated_latency_savings_ms"] for item in queue)),
194
+ )
195
+
196
+ if not queue:
197
+ st.info(
198
+ "No optimization opportunities have been persisted yet. Store traces or run diagnosis first."
199
+ )
200
+ else:
201
+ queue_df = pd.DataFrame(queue)
202
+ visible_cols = [
203
+ "priority_score",
204
+ "severity",
205
+ "type",
206
+ "title",
207
+ "occurrence_count",
208
+ "run_count",
209
+ "patchable_count",
210
+ "quality_risk",
211
+ "requires_scorer",
212
+ "safe_to_auto_patch",
213
+ "estimated_latency_savings_ms",
214
+ "estimated_cost_savings_usd",
215
+ ]
216
+ st.dataframe(queue_df[visible_cols], width="stretch", hide_index=True)
217
+
218
+ selected_title = st.selectbox("Open queue item", queue_df["title"].tolist())
219
+ item = next(row for row in queue if row["title"] == selected_title)
220
+ st.markdown(f"### {item['title']}")
221
+ c5, c6, c7, c8 = st.columns(4)
222
+ c5.metric("Priority", item["priority_score"])
223
+ c6.metric("Severity", item["severity"])
224
+ c7.metric("Affected runs", item["run_count"])
225
+ c8.metric("Patchable", item["patchable_count"])
226
+ st.write(f"Type: `{item['type']}`")
227
+ st.write(f"Status: `{item['status']}`")
228
+ st.write(f"Quality risk: `{item['quality_risk']}`")
229
+ st.write(f"Requires scorer: `{item['requires_scorer']}`")
230
+ st.write(f"Safe to auto-patch: `{item['safe_to_auto_patch']}`")
231
+ st.write(
232
+ f"Estimated savings: {seconds(item['estimated_latency_savings_ms'])} / {money(item['estimated_cost_savings_usd'])}"
233
+ )
234
+ st.write("Affected runs")
235
+ st.code("\n".join(item["affected_runs"]), language="text")
236
+
237
+ item_findings = [
238
+ finding
239
+ for finding in findings
240
+ if finding["type"] == item["type"] and finding["title"] == item["title"]
241
+ ]
242
+ if item_findings:
243
+ st.subheader("Finding instances")
244
+ st.dataframe(pd.DataFrame(item_findings), width="stretch", hide_index=True)
245
+
246
+ st.download_button(
247
+ "Download optimization queue JSON",
248
+ data=json.dumps({"project_id": project_id, "queue": queue}, indent=2),
249
+ file_name="agentloop_optimization_queue.json",
250
+ mime="application/json",
251
+ )
252
+
253
+ st.subheader("GitHub issue drafts")
254
+ drafts = build_issue_drafts(queue)
255
+ if not drafts:
256
+ st.info("No patchable queue items available for issue drafts.")
257
+ else:
258
+ selected_issue = st.selectbox(
259
+ "Preview issue draft", [draft["title"] for draft in drafts]
260
+ )
261
+ draft = next(item for item in drafts if item["title"] == selected_issue)
262
+ st.write(f"Labels: `{', '.join(draft['labels'])}`")
263
+ st.code(draft["body"], language="markdown")
264
+ st.download_button(
265
+ "Download issue drafts Markdown",
266
+ data=issue_drafts_to_markdown(drafts),
267
+ file_name="agentloop_issue_drafts.md",
268
+ mime="text/markdown",
269
+ )
270
+
271
+ elif page == "Optimization":
272
+ traces = store.list_traces(project_id=project_id)
273
+ st.subheader("Optimization plans")
274
+ if not traces:
275
+ st.info("No traces stored for this project yet.")
276
+ else:
277
+ trace = select_trace(traces)
278
+ if trace is not None:
279
+ report = trace.report()
280
+ plan = build_optimization_plan(trace, report)
281
+ current = plan["current"]
282
+ after = plan["estimated_after"]
283
+
284
+ c1, c2, c3, c4 = st.columns(4)
285
+ c1.metric("Current runtime", seconds(current["runtime_ms"]))
286
+ c2.metric("Estimated runtime", seconds(after["runtime_ms"]))
287
+ c3.metric("Latency reduction", f"{after['latency_reduction_pct']:.1f}%")
288
+ c4.metric("Cost reduction", f"{after['cost_reduction_pct']:.1f}%")
289
+
290
+ with st.expander("Show value and pricing estimate", expanded=False):
291
+ rpm, rate, incident_cost = assumption_inputs("optimization_")
292
+ value = build_value_report(
293
+ trace,
294
+ runs_per_month=rpm,
295
+ engineer_hourly_rate_usd=rate,
296
+ incident_cost_usd=incident_cost,
297
+ )
298
+ render_value_report(value, show_download=False)
299
+
300
+ st.subheader("Optimization cards")
301
+ cards = plan["optimization_cards"]
302
+ if not cards:
303
+ st.info("No major optimization cards detected yet.")
304
+ for card in cards:
305
+ with st.expander(
306
+ f"{card['title']} · confidence: {card['confidence']}", expanded=True
307
+ ):
308
+ st.write(card["why"])
309
+ st.code(card["rewrite_hint"])
310
+ st.write(
311
+ f"Estimated latency savings: {seconds(card['estimated_latency_savings_ms'])}"
312
+ )
313
+ st.write(f"Estimated cost savings: {money(card['estimated_cost_savings_usd'])}")
314
+
315
+ st.subheader("Execution graph")
316
+ graph = plan["graph"]
317
+ st.write("Bottlenecks")
318
+ st.dataframe(pd.DataFrame(graph["bottlenecks"]), width="stretch")
319
+ st.write("Parallelizable groups")
320
+ if graph["parallelizable_groups"]:
321
+ st.dataframe(pd.DataFrame(graph["parallelizable_groups"]), width="stretch")
322
+ else:
323
+ st.info("No obvious repeated independent tool-call groups found yet.")
324
+ st.write("Edges")
325
+ st.dataframe(pd.DataFrame(graph["edges"]), width="stretch")
326
+
327
+ st.download_button(
328
+ "Download optimization plan JSON",
329
+ data=json.dumps(plan, indent=2),
330
+ file_name="agentloop_optimization_plan.json",
331
+ mime="application/json",
332
+ )
333
+
334
+ elif page == "Diagnosis":
335
+ traces = store.list_traces(project_id=project_id)
336
+ st.subheader("Machine-actionable diagnosis")
337
+ if not traces:
338
+ st.info("No traces stored for this project yet.")
339
+ else:
340
+ trace = selected_trace_from_options(traces, "Choose trace for diagnosis")
341
+ if trace is not None:
342
+ diagnosis = build_diagnosis(trace)
343
+ summary = diagnosis["summary"]
344
+ c1, c2, c3, c4 = st.columns(4)
345
+ c1.metric("Findings", summary["finding_count"])
346
+ c2.metric("High severity", summary["high_severity_count"])
347
+ c3.metric("Patchable", summary["patchable_count"])
348
+ c4.metric("Latency reduction", f"{summary['estimated_latency_reduction_pct']:.1f}%")
349
+
350
+ findings = diagnosis.get("findings", [])
351
+ if not findings:
352
+ st.info("No machine-actionable findings detected for this trace.")
353
+ for finding in findings:
354
+ label = f"{finding['severity'].upper()} - {finding['title']} ({finding['type']})"
355
+ with st.expander(label, expanded=finding["severity"] == "high"):
356
+ st.write(finding["metadata"].get("why", ""))
357
+ st.write(f"Finding ID: `{finding['finding_id']}`")
358
+ st.write(f"Confidence: `{finding['confidence']}`")
359
+ st.write(f"Affected spans: `{', '.join(finding['affected_spans'])}`")
360
+ st.write(f"Rewrite: {finding['rewrite']['hint']}")
361
+ st.write(f"Validation: {finding['validation']['acceptance_criteria']}")
362
+ st.write(
363
+ "Estimated savings: "
364
+ f"{seconds(finding['savings']['estimated_latency_savings_ms'])}, "
365
+ f"{money(finding['savings']['estimated_cost_savings_usd'])}"
366
+ )
367
+ if finding["evidence"]:
368
+ st.dataframe(
369
+ pd.DataFrame(finding["evidence"]), width="stretch", hide_index=True
370
+ )
371
+
372
+ st.download_button(
373
+ "Download diagnosis JSON",
374
+ data=json.dumps(diagnosis, indent=2),
375
+ file_name="agentloop_diagnosis.json",
376
+ mime="application/json",
377
+ )
378
+
379
+ elif page == "Patch Plan":
380
+ traces = store.list_traces(project_id=project_id)
381
+ st.subheader("Trace-to-patch planning")
382
+ if not traces:
383
+ st.info("No traces stored for this project yet.")
384
+ else:
385
+ trace = selected_trace_from_options(traces, "Choose trace for patch planning")
386
+ repo_path = st.text_input("Repository path", value=str(Path.cwd()))
387
+ if trace is not None:
388
+ plan = build_patch_plan(trace, repo_path=repo_path)
389
+ summary = plan["summary"]
390
+ c1, c2, c3 = st.columns(3)
391
+ c1.metric("Patch plans", summary["patch_count"])
392
+ c2.metric("Unsupported", summary["unsupported_finding_count"])
393
+ c3.metric("Frameworks", ", ".join(summary["frameworks_detected"]) or "none")
394
+
395
+ for item in plan.get("patch_plans", []):
396
+ with st.expander(f"{item['patch_id']} - {item['title']}", expanded=True):
397
+ st.write(f"Type: `{item['type']}`")
398
+ st.write(f"Risk: `{item['risk']}`")
399
+ st.write(f"Framework: `{item['framework']}`")
400
+ st.write(f"Finding: `{item['finding_id']}`")
401
+ st.write(f"Before pattern: {item['before_pattern']}")
402
+ st.write(f"Proposed rewrite: {item['proposed_rewrite']}")
403
+ st.code(item["suggested_diff"], language="text")
404
+ st.write(f"Validation command: `{item['validation_command']}`")
405
+ st.write(f"Acceptance: {item['acceptance_criteria']}")
406
+ if item["files"]:
407
+ st.dataframe(pd.DataFrame(item["files"]), width="stretch", hide_index=True)
408
+ if item["notes"]:
409
+ for note in item["notes"]:
410
+ st.write(f"- {note}")
411
+
412
+ if plan.get("unsupported_findings"):
413
+ st.subheader("Unsupported findings")
414
+ st.dataframe(
415
+ pd.DataFrame(plan["unsupported_findings"]), width="stretch", hide_index=True
416
+ )
417
+
418
+ st.download_button(
419
+ "Download patch plan JSON",
420
+ data=json.dumps(plan, indent=2),
421
+ file_name="agentloop_patch_plan.json",
422
+ mime="application/json",
423
+ )
424
+
425
+ elif page == "Replay Proof":
426
+ traces = store.list_traces(project_id=project_id)
427
+ st.subheader("Before/after replay proof")
428
+ if len(traces) < 2:
429
+ st.info("Store at least two traces to compare a baseline and candidate.")
430
+ else:
431
+ options = trace_options(traces)
432
+ labels = list(options.keys())
433
+ baseline_label = st.selectbox("Baseline trace", labels, index=0)
434
+ candidate_label = st.selectbox("Candidate trace", labels, index=1 if len(labels) > 1 else 0)
435
+ baseline = load_trace_for_project(options[baseline_label], project_id)
436
+ candidate = load_trace_for_project(options[candidate_label], project_id)
437
+
438
+ st.markdown("#### Gates")
439
+ g1, g2, g3, g4 = st.columns(4)
440
+ min_latency = g1.number_input("Min latency improvement %", min_value=0.0, value=20.0)
441
+ min_cost = g2.number_input("Min cost improvement %", min_value=0.0, value=5.0)
442
+ require_schema = g3.checkbox("Require schema valid", value=False)
443
+ min_quality = g4.number_input("Min quality score", min_value=0.0, max_value=1.0, value=0.0)
444
+ quality_threshold = min_quality if min_quality > 0 else None
445
+
446
+ if baseline is not None and candidate is not None:
447
+ gates = ReplayGates(
448
+ min_latency_improvement_pct=min_latency,
449
+ min_cost_improvement_pct=min_cost,
450
+ require_schema_valid=require_schema,
451
+ min_quality_score=quality_threshold,
452
+ )
453
+ replay = build_replay_report(baseline, candidate, gates=gates)
454
+ ci_report = build_ci_report(baseline, candidate, gates=gates)
455
+ status = "passed" if replay["gates"]["passed"] else "failed"
456
+ c1, c2, c3, c4 = st.columns(4)
457
+ c1.metric("Status", status)
458
+ c2.metric("Latency improvement", f"{replay['deltas']['latency_improvement_pct']:.1f}%")
459
+ c3.metric("Cost improvement", f"{replay['deltas']['cost_improvement_pct']:.1f}%")
460
+ c4.metric("Retry delta", replay["deltas"]["retry_count_delta"])
461
+
462
+ st.subheader("Gate results")
463
+ render_gate_table(replay["gates"]["results"])
464
+
465
+ st.subheader("Metric deltas")
466
+ metric_rows = [
467
+ {"metric": key, "value": value}
468
+ for key, value in replay["deltas"].items()
469
+ if key.endswith("_delta") or key.endswith("_pct")
470
+ ]
471
+ st.dataframe(pd.DataFrame(metric_rows), width="stretch", hide_index=True)
472
+
473
+ st.subheader("PR comment preview")
474
+ st.code(
475
+ "Trace in. Rewrite plan out. Replay proof in the PR.\n\n"
476
+ + ci_report_to_markdown(ci_report)
477
+ )
478
+
479
+ col_json, col_md = st.columns(2)
480
+ col_json.download_button(
481
+ "Download replay JSON",
482
+ data=json.dumps(replay, indent=2),
483
+ file_name="agentloop_replay_report.json",
484
+ mime="application/json",
485
+ )
486
+ col_md.download_button(
487
+ "Download PR Markdown",
488
+ data=ci_report_to_markdown(ci_report),
489
+ file_name="agentloop_ci.md",
490
+ mime="text/markdown",
491
+ )
492
+
493
+ elif page == "Quality Gates":
494
+ traces = store.list_traces(project_id=project_id)
495
+ st.subheader("Quality gates")
496
+ st.caption(
497
+ "Score production-derived fixtures so replay proof can show faster, cheaper, and still correct."
498
+ )
499
+ if len(traces) < 2:
500
+ st.info("Store at least two traces to score baseline and candidate quality.")
501
+ else:
502
+ options = trace_options(traces)
503
+ labels = list(options.keys())
504
+ baseline_label = st.selectbox("Baseline trace", labels, index=0, key="quality_baseline")
505
+ candidate_label = st.selectbox(
506
+ "Candidate trace", labels, index=1 if len(labels) > 1 else 0, key="quality_candidate"
507
+ )
508
+ baseline = load_trace_for_project(options[baseline_label], project_id)
509
+ candidate = load_trace_for_project(options[candidate_label], project_id)
510
+ min_score = st.number_input(
511
+ "Minimum candidate quality score", min_value=0.0, max_value=1.0, value=0.9
512
+ )
513
+ uploaded = st.file_uploader(
514
+ "Upload quality fixture JSON", type=["json"], key="quality_fixture_upload"
515
+ )
516
+ default_fixture = [
517
+ {
518
+ "id": "required_summary_fields",
519
+ "input": "Summarize one source.",
520
+ "candidate_output": {
521
+ "summary": "AgentLoop proves rewrites.",
522
+ "sources": ["source-a"],
523
+ },
524
+ "baseline_output": {
525
+ "summary": "AgentLoop proves rewrites.",
526
+ "sources": ["source-a"],
527
+ },
528
+ "scorer": {"type": "required_fields", "required": ["summary", "sources"]},
529
+ }
530
+ ]
531
+ fixtures_text = st.text_area(
532
+ "Fixture JSON",
533
+ value=json.dumps({"fixtures": default_fixture}, indent=2),
534
+ height=260,
535
+ )
536
+ if uploaded is not None:
537
+ fixtures_payload = json.loads(uploaded.read().decode("utf-8"))
538
+ else:
539
+ fixtures_payload = json.loads(fixtures_text)
540
+ fixtures = (
541
+ fixtures_payload
542
+ if isinstance(fixtures_payload, list)
543
+ else fixtures_payload.get("fixtures", [])
544
+ )
545
+
546
+ if baseline is not None and candidate is not None:
547
+ quality = build_quality_report(
548
+ fixtures,
549
+ baseline_trace=baseline,
550
+ candidate_trace=candidate,
551
+ min_score=min_score,
552
+ )
553
+ status = "passed" if quality["passed"] else "failed"
554
+ c1, c2, c3, c4 = st.columns(4)
555
+ c1.metric("Status", status)
556
+ c2.metric("Candidate score", f"{quality['candidate_score']:.4f}")
557
+ c3.metric("Quality delta", f"{quality['quality_delta']:.4f}")
558
+ c4.metric("Failed cases", quality["failed_case_count"])
559
+ st.dataframe(pd.DataFrame(quality["cases"]), width="stretch", hide_index=True)
560
+ st.download_button(
561
+ "Download quality report JSON",
562
+ data=json.dumps(quality, indent=2),
563
+ file_name="agentloop_quality_report.json",
564
+ mime="application/json",
565
+ )
566
+ st.download_button(
567
+ "Download quality report Markdown",
568
+ data=quality_report_to_markdown(quality),
569
+ file_name="agentloop_quality_report.md",
570
+ mime="text/markdown",
571
+ )
572
+
573
+ elif page == "Value & Pricing":
574
+ traces = store.list_traces(project_id=project_id)
575
+ st.subheader("Value report and pricing scenario")
576
+ st.caption(
577
+ "Turn an agent trace into operational value, reliability risk, and a configurable pricing scenario."
578
+ )
579
+ if not traces:
580
+ st.info(
581
+ "No traces stored for this project yet. Generate demo traces from the Ingest page first."
582
+ )
583
+ else:
584
+ trace = select_trace(traces, label="Choose trace for value report")
585
+ if trace is not None:
586
+ st.markdown("#### Assumptions")
587
+ rpm, rate, incident_cost = assumption_inputs("value_page_")
588
+ value = build_value_report(
589
+ trace,
590
+ runs_per_month=rpm,
591
+ engineer_hourly_rate_usd=rate,
592
+ incident_cost_usd=incident_cost,
593
+ )
594
+ render_value_report(value)
595
+ st.subheader("CLI command")
596
+ st.code(
597
+ "uv run agentloop value-report --path runs/research_agent_baseline.json "
598
+ f"--runs-per-month {rpm} "
599
+ f"--engineer-hourly-rate-usd {rate} "
600
+ f"--incident-cost-usd {incident_cost} "
601
+ "--out runs/value_report.json",
602
+ language="bash",
603
+ )
604
+
605
+ elif page == "API Keys":
606
+ st.subheader("Create project API key")
607
+ st.warning("API keys are only shown once. Copy the generated key before leaving this page.")
608
+ key_name = st.text_input("Key name", value="local-dev")
609
+ if st.button("Create API key"):
610
+ created = store.create_api_key(project_id=project_id, name=key_name)
611
+ st.success(f"Created key for project `{project_id}`")
612
+ st.code(created["api_key"])
613
+
614
+ st.subheader("Use this key")
615
+ st.code(
616
+ "uv run agentloop upload --path runs/research_agent_baseline.json "
617
+ "--api-url http://127.0.0.1:8000 --api-key YOUR_KEY",
618
+ language="bash",
619
+ )
620
+
621
+ elif page == "Ingest":
622
+ st.subheader("Generate or upload traces")
623
+ col_a, col_b, col_c, col_d = st.columns(4)
624
+ generated_path: Path | None = None
625
+ if col_a.button("Generate baseline"):
626
+ generated_path = run_baseline(runs_dir)
627
+ if col_b.button("Generate optimized"):
628
+ generated_path = run_optimized(runs_dir)
629
+ if col_c.button("Generate LangGraph-style"):
630
+ generated_path = run_langgraph_style(runs_dir)
631
+ if col_d.button("Generate all"):
632
+ proof_baseline, proof_candidate = run_proof_pair(runs_dir)
633
+ for path in [
634
+ run_baseline(runs_dir),
635
+ run_optimized(runs_dir),
636
+ run_langgraph_style(runs_dir),
637
+ proof_baseline,
638
+ proof_candidate,
639
+ ]:
640
+ store.save_trace(AgentTrace.from_json(path), project_id=project_id)
641
+ st.success(f"Generated and stored all demo traces under project `{project_id}`")
642
+
643
+ if st.button("Generate proof demo pair"):
644
+ proof_baseline, proof_candidate = run_proof_pair(runs_dir)
645
+ for path in [proof_baseline, proof_candidate]:
646
+ store.save_trace(AgentTrace.from_json(path), project_id=project_id)
647
+ st.success(f"Generated and stored proof demo traces under project `{project_id}`")
648
+
649
+ if generated_path is not None:
650
+ trace = AgentTrace.from_json(generated_path)
651
+ store.save_trace(trace, project_id=project_id)
652
+ st.success(f"Generated and stored `{trace.name}` as `{trace.run_id}`")
653
+
654
+ uploaded = st.file_uploader("Upload AgentLoop trace JSON", type=["json"])
655
+ if uploaded is not None:
656
+ trace = AgentTrace.from_dict(json.loads(uploaded.read().decode("utf-8")))
657
+ if st.button("Store uploaded trace"):
658
+ store.save_trace(trace, project_id=project_id)
659
+ st.success(f"Stored uploaded trace `{trace.run_id}` under project `{project_id}`")
660
+
661
+ st.subheader("CLI ingest")
662
+ st.code(
663
+ "uv run agentloop demo-all\n"
664
+ f"uv run agentloop store-trace --path runs/research_agent_baseline.json --project-id {project_id}\n"
665
+ f"uv run agentloop list-stored-traces --project-id {project_id}",
666
+ language="bash",
667
+ )
668
+
669
+ elif page == "Setup":
670
+ st.subheader("Local development")
671
+ st.code(
672
+ "uv sync --locked --all-extras --dev\n"
673
+ "uv run agentloop init-store\n"
674
+ "uv run agentloop demo-all\n"
675
+ f"uv run agentloop store-trace --path runs/research_agent_baseline.json --project-id {project_id}\n"
676
+ "uv run streamlit run dashboard/app.py",
677
+ language="bash",
678
+ )
679
+
680
+ st.subheader("Hosted API")
681
+ st.code(
682
+ "uv run agentloop create-api-key --project-id acme --name local-dev\n"
683
+ "uv run agentloop server --host 127.0.0.1 --port 8000\n"
684
+ "uv run agentloop upload --path runs/research_agent_baseline.json "
685
+ "--api-url http://127.0.0.1:8000 --api-key al_xxx\n"
686
+ "uv run agentloop remote-usage --api-url http://127.0.0.1:8000 --api-key al_xxx",
687
+ language="bash",
688
+ )
689
+
690
+ st.subheader("Postgres deployment")
691
+ st.code(
692
+ "uv sync --locked --extra server --extra postgres --no-dev\n"
693
+ "export AGENTLOOP_STORE_BACKEND=postgres\n"
694
+ "export AGENTLOOP_DATABASE_URL=postgresql://agentloop:agentloop@localhost:5432/agentloop\n"
695
+ "uv run agentloop init-store\n"
696
+ "uv run agentloop server --host 0.0.0.0 --port 8000",
697
+ language="bash",
698
+ )