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
dashboard/app.py
ADDED
|
@@ -0,0 +1,922 @@
|
|
|
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.costs import format_cost_usd
|
|
11
|
+
from agentloop.demo import run_baseline, run_langgraph_style, run_optimized, run_proof_pair
|
|
12
|
+
from agentloop.findings import build_diagnosis
|
|
13
|
+
from agentloop.issues import build_issue_drafts, issue_drafts_to_markdown
|
|
14
|
+
from agentloop.operations import operation_kind
|
|
15
|
+
from agentloop.optimizer import build_optimization_plan
|
|
16
|
+
from agentloop.patches import RepositoryPathError, build_patch_plan
|
|
17
|
+
from agentloop.quality import (
|
|
18
|
+
QualityValidationError,
|
|
19
|
+
build_quality_report,
|
|
20
|
+
parse_quality_fixtures,
|
|
21
|
+
quality_report_to_markdown,
|
|
22
|
+
)
|
|
23
|
+
from agentloop.ranking import SORT_FIELDS, sort_ranked
|
|
24
|
+
from agentloop.replay import ReplayGates, build_replay_report
|
|
25
|
+
from agentloop.store import (
|
|
26
|
+
ALLOWED_FINDING_TRANSITIONS,
|
|
27
|
+
FindingNotFoundError,
|
|
28
|
+
FindingTransitionError,
|
|
29
|
+
get_store,
|
|
30
|
+
)
|
|
31
|
+
from agentloop.tracer import AgentTrace
|
|
32
|
+
from agentloop.value import build_value_report
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from dashboard.trace_upload import TraceUploadError, parse_uploaded_trace
|
|
36
|
+
from dashboard.value_view import assumption_inputs, percentage, render_value_report
|
|
37
|
+
except ModuleNotFoundError:
|
|
38
|
+
from trace_upload import TraceUploadError, parse_uploaded_trace
|
|
39
|
+
from value_view import assumption_inputs, percentage, render_value_report
|
|
40
|
+
|
|
41
|
+
st.set_page_config(page_title="AgentLoop Dashboard", layout="wide")
|
|
42
|
+
|
|
43
|
+
runs_dir = Path("runs")
|
|
44
|
+
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@st.cache_resource
|
|
48
|
+
def load_store():
|
|
49
|
+
db = get_store()
|
|
50
|
+
db.init()
|
|
51
|
+
return db
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def money(value: float | None) -> str:
|
|
55
|
+
return "unavailable" if value is None else f"${float(value):,.4f}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def seconds(ms: float | None) -> str:
|
|
59
|
+
return "unavailable" if ms is None else f"{float(ms) / 1000:,.2f}s"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def render_estimate_details(item: dict) -> None:
|
|
63
|
+
if item.get("evidence_level"):
|
|
64
|
+
st.write(f"Evidence level: {item['evidence_level']}")
|
|
65
|
+
estimate = item.get("estimate")
|
|
66
|
+
if estimate:
|
|
67
|
+
calibration = "calibrated" if estimate.get("calibrated") is True else "uncalibrated"
|
|
68
|
+
st.write(
|
|
69
|
+
f"Estimator: {estimate['estimator_id']} version {estimate['estimator_version']} "
|
|
70
|
+
f"({estimate['method']}, {calibration}; predicted savings)"
|
|
71
|
+
)
|
|
72
|
+
st.write("Assumptions: " + "; ".join(estimate.get("assumptions", [])))
|
|
73
|
+
st.caption(estimate["formula"])
|
|
74
|
+
st.json(
|
|
75
|
+
{"parameters": estimate.get("parameters", {}), "inputs": estimate.get("inputs", {})}
|
|
76
|
+
)
|
|
77
|
+
elif item.get("evidence_level"):
|
|
78
|
+
st.write("Assumptions: " + "; ".join(item.get("assumptions", [])))
|
|
79
|
+
st.caption(item.get("estimate_formula") or item.get("savings", {}).get("formula", ""))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_trace_for_project(run_id: str, project_id: str) -> AgentTrace | None:
|
|
83
|
+
return load_store().get_trace(run_id=run_id, project_id=project_id)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def select_trace(traces: list[dict], label: str = "Choose trace") -> AgentTrace | None:
|
|
87
|
+
labels = {f"{t['name']} · {t['run_id']}": t["run_id"] for t in traces}
|
|
88
|
+
selected_label = st.selectbox(label, list(labels.keys()))
|
|
89
|
+
return load_trace_for_project(labels[selected_label], project_id)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def trace_options(traces: list[dict]) -> dict[str, str]:
|
|
93
|
+
return {f"{t['name']} - {t['run_id']}": t["run_id"] for t in traces}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def selected_trace_from_options(traces: list[dict], label: str) -> AgentTrace | None:
|
|
97
|
+
options = trace_options(traces)
|
|
98
|
+
selected = st.selectbox(label, list(options.keys()))
|
|
99
|
+
return load_trace_for_project(options[selected], project_id)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def render_gate_table(results: list[dict]) -> None:
|
|
103
|
+
rows = [
|
|
104
|
+
{
|
|
105
|
+
"gate": item["name"],
|
|
106
|
+
"status": "pass" if item["passed"] else "fail",
|
|
107
|
+
"detail": item["detail"],
|
|
108
|
+
}
|
|
109
|
+
for item in results
|
|
110
|
+
]
|
|
111
|
+
st.dataframe(pd.DataFrame(rows), width="stretch", hide_index=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
st.sidebar.title("AgentLoop")
|
|
115
|
+
st.sidebar.caption("Execution profiling and evidence")
|
|
116
|
+
|
|
117
|
+
project_id = st.sidebar.text_input("Project", value=st.session_state.get("project_id", "default"))
|
|
118
|
+
st.session_state["project_id"] = project_id
|
|
119
|
+
|
|
120
|
+
page = st.sidebar.radio(
|
|
121
|
+
"Navigate",
|
|
122
|
+
[
|
|
123
|
+
"Overview",
|
|
124
|
+
"Traces",
|
|
125
|
+
"Optimization Queue",
|
|
126
|
+
"Optimization",
|
|
127
|
+
"Diagnosis",
|
|
128
|
+
"Patch Plan",
|
|
129
|
+
"Replay Proof",
|
|
130
|
+
"Quality Gates",
|
|
131
|
+
"Value & Pricing",
|
|
132
|
+
"API Keys",
|
|
133
|
+
"Ingest",
|
|
134
|
+
"Setup",
|
|
135
|
+
],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
store = load_store()
|
|
139
|
+
|
|
140
|
+
st.title("AgentLoop Dashboard")
|
|
141
|
+
st.caption("Cost, latency, and execution-graph optimization for production AI agents")
|
|
142
|
+
|
|
143
|
+
if page == "Overview":
|
|
144
|
+
summary = store.usage_summary(project_id=project_id)
|
|
145
|
+
traces = store.list_traces(project_id=project_id)
|
|
146
|
+
|
|
147
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
148
|
+
c1.metric("Runs", int(summary.get("run_count", 0)))
|
|
149
|
+
c2.metric("Total runtime", seconds(summary.get("total_runtime_ms", 0)))
|
|
150
|
+
c3.metric(
|
|
151
|
+
"Estimated LLM cost",
|
|
152
|
+
format_cost_usd(summary.get("estimated_cost_usd"), summary.get("cost_status", "complete")),
|
|
153
|
+
)
|
|
154
|
+
c4.metric("Retries", int(summary.get("retry_count", 0)))
|
|
155
|
+
|
|
156
|
+
c5, c6, c7, c8 = st.columns(4)
|
|
157
|
+
c5.metric("Input tokens", int(summary.get("input_tokens", 0)))
|
|
158
|
+
c6.metric("Output tokens", int(summary.get("output_tokens", 0)))
|
|
159
|
+
c7.metric("Model calls", int(summary.get("model_call_count", 0)))
|
|
160
|
+
c8.metric("Tool calls", int(summary.get("tool_call_count", 0)))
|
|
161
|
+
|
|
162
|
+
st.subheader("Recent runs")
|
|
163
|
+
if traces:
|
|
164
|
+
df = pd.DataFrame(traces)
|
|
165
|
+
st.dataframe(df, width="stretch")
|
|
166
|
+
|
|
167
|
+
chart_df = df[["name", "total_runtime_ms"]].copy()
|
|
168
|
+
chart_df["runtime_s"] = chart_df["total_runtime_ms"] / 1000
|
|
169
|
+
st.subheader("Runtime by run")
|
|
170
|
+
st.bar_chart(chart_df, x="name", y="runtime_s")
|
|
171
|
+
else:
|
|
172
|
+
st.info(
|
|
173
|
+
"No stored traces yet. Use the Ingest page or run "
|
|
174
|
+
"`uv run agentloop store-trace --path TRACE.json`."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
elif page == "Traces":
|
|
178
|
+
st.subheader("Stored traces")
|
|
179
|
+
|
|
180
|
+
cursor_key = f"traces_cursor_stack::{project_id}"
|
|
181
|
+
if cursor_key not in st.session_state:
|
|
182
|
+
st.session_state[cursor_key] = [None] # cursor to fetch the *current* page
|
|
183
|
+
|
|
184
|
+
page_index = len(st.session_state[cursor_key]) - 1
|
|
185
|
+
page_size = st.number_input("Page size", min_value=5, max_value=200, value=25, step=5)
|
|
186
|
+
page = store.list_traces_page(
|
|
187
|
+
project_id=project_id, limit=int(page_size), cursor=st.session_state[cursor_key][-1]
|
|
188
|
+
)
|
|
189
|
+
traces = page["items"]
|
|
190
|
+
|
|
191
|
+
nav_cols = st.columns([1, 1, 6])
|
|
192
|
+
if nav_cols[0].button("◀ Previous", disabled=page_index == 0):
|
|
193
|
+
st.session_state[cursor_key].pop()
|
|
194
|
+
st.rerun()
|
|
195
|
+
if nav_cols[1].button("Next ▶", disabled=page["next_cursor"] is None):
|
|
196
|
+
st.session_state[cursor_key].append(page["next_cursor"])
|
|
197
|
+
st.rerun()
|
|
198
|
+
st.caption(f"Page {page_index + 1}")
|
|
199
|
+
|
|
200
|
+
if not traces:
|
|
201
|
+
st.info("No traces stored for this project yet.")
|
|
202
|
+
else:
|
|
203
|
+
df = pd.DataFrame(traces)
|
|
204
|
+
st.dataframe(df, width="stretch")
|
|
205
|
+
selected_run = st.selectbox("Open run", df["run_id"].tolist())
|
|
206
|
+
trace = load_trace_for_project(selected_run, project_id)
|
|
207
|
+
if trace is not None:
|
|
208
|
+
report = trace.report()
|
|
209
|
+
st.markdown(f"### {trace.name}")
|
|
210
|
+
if report.get("execution") is not None:
|
|
211
|
+
execution = report["execution"]
|
|
212
|
+
st.caption(
|
|
213
|
+
f"Workflow {execution.get('workflow_id', 'unknown')} · version {execution.get('version', 'unknown')} · {execution.get('status', 'unknown')}"
|
|
214
|
+
)
|
|
215
|
+
with st.expander("Workflow references"):
|
|
216
|
+
st.json(execution)
|
|
217
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
218
|
+
c1.metric("Runtime", seconds(report["total_runtime_ms"]))
|
|
219
|
+
c2.metric(
|
|
220
|
+
"Recorded model cost" if "execution" in report else "Cost",
|
|
221
|
+
format_cost_usd(report["estimated_cost_usd"], report.get("cost_status")),
|
|
222
|
+
)
|
|
223
|
+
c3.metric("Model calls", report["model_call_count"])
|
|
224
|
+
c4.metric("Tool calls", report["tool_call_count"])
|
|
225
|
+
|
|
226
|
+
events = pd.DataFrame(report["events"])
|
|
227
|
+
st.subheader("Event timeline")
|
|
228
|
+
if not events.empty:
|
|
229
|
+
cols = [
|
|
230
|
+
"event_type",
|
|
231
|
+
"name",
|
|
232
|
+
"duration_ms",
|
|
233
|
+
"model",
|
|
234
|
+
"input_tokens",
|
|
235
|
+
"output_tokens",
|
|
236
|
+
"status",
|
|
237
|
+
]
|
|
238
|
+
if "execution" in report or "stages" in report:
|
|
239
|
+
events["operation_kind"] = [operation_kind(event) for event in trace.events]
|
|
240
|
+
summaries = [
|
|
241
|
+
report.get("stages", {}).get(event.event_id, {}) for event in trace.events
|
|
242
|
+
]
|
|
243
|
+
stage_columns = {
|
|
244
|
+
"Stage": "stage_id",
|
|
245
|
+
"Stage kind": "kind",
|
|
246
|
+
"Stage version": "version",
|
|
247
|
+
"Input schema": "input_schema_ref",
|
|
248
|
+
"Output schema": "output_schema_ref",
|
|
249
|
+
"Input reference": "input_ref",
|
|
250
|
+
"Output reference": "output_ref",
|
|
251
|
+
"Outcome": "outcome",
|
|
252
|
+
"Dependencies": "depends_on",
|
|
253
|
+
}
|
|
254
|
+
for label, key in stage_columns.items():
|
|
255
|
+
events[label] = [value.get(key) for value in summaries]
|
|
256
|
+
cols = [
|
|
257
|
+
"operation_kind",
|
|
258
|
+
"Stage",
|
|
259
|
+
"Stage kind",
|
|
260
|
+
"Stage version",
|
|
261
|
+
"parent_id",
|
|
262
|
+
*cols,
|
|
263
|
+
"Input schema",
|
|
264
|
+
"Output schema",
|
|
265
|
+
"Input reference",
|
|
266
|
+
"Output reference",
|
|
267
|
+
"Outcome",
|
|
268
|
+
"Dependencies",
|
|
269
|
+
]
|
|
270
|
+
for col in cols:
|
|
271
|
+
if col not in events.columns:
|
|
272
|
+
events[col] = None
|
|
273
|
+
st.dataframe(events[cols], width="stretch")
|
|
274
|
+
|
|
275
|
+
st.download_button(
|
|
276
|
+
"Download trace JSON",
|
|
277
|
+
data=json.dumps(trace.to_dict(), indent=2),
|
|
278
|
+
file_name=f"{trace.run_id}.json",
|
|
279
|
+
mime="application/json",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
elif page == "Optimization Queue":
|
|
283
|
+
st.subheader("Optimization queue")
|
|
284
|
+
dimension = st.selectbox(
|
|
285
|
+
"Sort investigations by", SORT_FIELDS, format_func=lambda value: value.replace("_", " ")
|
|
286
|
+
)
|
|
287
|
+
queue = sort_ranked(store.optimization_queue(project_id=project_id), dimension)
|
|
288
|
+
findings = store.list_findings(project_id=project_id)
|
|
289
|
+
|
|
290
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
291
|
+
c1.metric("Open opportunities", len(queue))
|
|
292
|
+
c2.metric("Persisted findings", len(findings))
|
|
293
|
+
c3.metric("Patchable findings", sum(1 for finding in findings if finding["patchable"]))
|
|
294
|
+
c4.metric(
|
|
295
|
+
"Estimated latency savings",
|
|
296
|
+
seconds(
|
|
297
|
+
None
|
|
298
|
+
if any(item["estimated_latency_savings_ms"] is None for item in queue)
|
|
299
|
+
else sum(item["estimated_latency_savings_ms"] for item in queue)
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if not queue:
|
|
304
|
+
st.info(
|
|
305
|
+
"No optimization opportunities have been persisted yet. Store traces or run diagnosis first."
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
queue_df = pd.DataFrame(queue)
|
|
309
|
+
queue_df["readiness"] = [item["ranking"]["status"].replace("_", " ") for item in queue]
|
|
310
|
+
visible_cols = [
|
|
311
|
+
"priority_score",
|
|
312
|
+
"readiness",
|
|
313
|
+
"severity",
|
|
314
|
+
"type",
|
|
315
|
+
"title",
|
|
316
|
+
"occurrence_count",
|
|
317
|
+
"run_count",
|
|
318
|
+
"patchable_count",
|
|
319
|
+
"quality_risk",
|
|
320
|
+
"requires_scorer",
|
|
321
|
+
"safe_to_auto_patch",
|
|
322
|
+
"estimated_latency_savings_ms",
|
|
323
|
+
"estimated_cost_savings_usd",
|
|
324
|
+
]
|
|
325
|
+
st.dataframe(queue_df[visible_cols], width="stretch", hide_index=True)
|
|
326
|
+
|
|
327
|
+
selected_title = st.selectbox("Open queue item", queue_df["title"].tolist())
|
|
328
|
+
item = next(row for row in queue if row["title"] == selected_title)
|
|
329
|
+
st.markdown(f"### {item['title']}")
|
|
330
|
+
c5, c6, c7, c8 = st.columns(4)
|
|
331
|
+
c5.metric("Investigation rank", item["ranking"]["priority_rank"])
|
|
332
|
+
c6.metric("Severity", item["severity"])
|
|
333
|
+
c7.metric("Affected runs", item["run_count"])
|
|
334
|
+
c8.metric("Patchable", item["patchable_count"])
|
|
335
|
+
st.write(f"Type: `{item['type']}`")
|
|
336
|
+
st.write(f"Status: `{item['status']}`")
|
|
337
|
+
st.write(f"Quality risk: `{item['quality_risk']}`")
|
|
338
|
+
st.write("Test readiness: " + item["ranking"]["status"].replace("_", " "))
|
|
339
|
+
with st.expander("Ranking inputs and provenance"):
|
|
340
|
+
st.json(item["ranking"])
|
|
341
|
+
st.write(f"Requires scorer: `{item['requires_scorer']}`")
|
|
342
|
+
st.write(f"Safe to auto-patch: `{item['safe_to_auto_patch']}`")
|
|
343
|
+
st.write(
|
|
344
|
+
f"Estimated savings: {seconds(item['estimated_latency_savings_ms'])} / {money(item['estimated_cost_savings_usd'])}"
|
|
345
|
+
)
|
|
346
|
+
st.write("Affected runs")
|
|
347
|
+
st.code("\n".join(item["affected_runs"]), language="text")
|
|
348
|
+
|
|
349
|
+
item_findings = [
|
|
350
|
+
finding
|
|
351
|
+
for finding in findings
|
|
352
|
+
if finding["type"] == item["type"] and finding["title"] == item["title"]
|
|
353
|
+
]
|
|
354
|
+
if item_findings:
|
|
355
|
+
st.subheader("Finding instances")
|
|
356
|
+
st.dataframe(pd.DataFrame(item_findings), width="stretch", hide_index=True)
|
|
357
|
+
|
|
358
|
+
st.markdown("**Update finding status**")
|
|
359
|
+
finding_labels = {
|
|
360
|
+
f"{f['run_id']} · {f['finding_id']} (currently {f['status']})": f
|
|
361
|
+
for f in item_findings
|
|
362
|
+
}
|
|
363
|
+
selected_finding_label = st.selectbox(
|
|
364
|
+
"Finding", list(finding_labels.keys()), key=f"finding_select_{item['queue_id']}"
|
|
365
|
+
)
|
|
366
|
+
selected_finding = finding_labels[selected_finding_label]
|
|
367
|
+
allowed_targets = sorted(
|
|
368
|
+
ALLOWED_FINDING_TRANSITIONS.get(selected_finding["status"], ())
|
|
369
|
+
)
|
|
370
|
+
if allowed_targets:
|
|
371
|
+
new_status = st.selectbox(
|
|
372
|
+
"New status", allowed_targets, key=f"status_select_{item['queue_id']}"
|
|
373
|
+
)
|
|
374
|
+
if st.button("Apply transition", key=f"apply_transition_{item['queue_id']}"):
|
|
375
|
+
try:
|
|
376
|
+
store.update_finding_status(
|
|
377
|
+
project_id,
|
|
378
|
+
selected_finding["run_id"],
|
|
379
|
+
selected_finding["finding_id"],
|
|
380
|
+
new_status,
|
|
381
|
+
)
|
|
382
|
+
except (FindingNotFoundError, FindingTransitionError) as exc:
|
|
383
|
+
st.error(str(exc))
|
|
384
|
+
else:
|
|
385
|
+
st.success(f"{selected_finding['finding_id']} is now {new_status}")
|
|
386
|
+
st.rerun()
|
|
387
|
+
else:
|
|
388
|
+
st.caption(f"No transitions available from '{selected_finding['status']}'.")
|
|
389
|
+
|
|
390
|
+
st.download_button(
|
|
391
|
+
"Download optimization queue JSON",
|
|
392
|
+
data=json.dumps({"project_id": project_id, "queue": queue}, indent=2),
|
|
393
|
+
file_name="agentloop_optimization_queue.json",
|
|
394
|
+
mime="application/json",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
st.subheader("GitHub issue drafts")
|
|
398
|
+
drafts = build_issue_drafts(queue)
|
|
399
|
+
if not drafts:
|
|
400
|
+
st.info("No patchable queue items available for issue drafts.")
|
|
401
|
+
else:
|
|
402
|
+
selected_issue = st.selectbox(
|
|
403
|
+
"Preview issue draft", [draft["title"] for draft in drafts]
|
|
404
|
+
)
|
|
405
|
+
draft = next(item for item in drafts if item["title"] == selected_issue)
|
|
406
|
+
st.write(f"Labels: `{', '.join(draft['labels'])}`")
|
|
407
|
+
st.code(draft["body"], language="markdown")
|
|
408
|
+
st.download_button(
|
|
409
|
+
"Download issue drafts Markdown",
|
|
410
|
+
data=issue_drafts_to_markdown(drafts),
|
|
411
|
+
file_name="agentloop_issue_drafts.md",
|
|
412
|
+
mime="text/markdown",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
elif page == "Optimization":
|
|
416
|
+
traces = store.list_traces(project_id=project_id)
|
|
417
|
+
st.subheader("Optimization plans")
|
|
418
|
+
if not traces:
|
|
419
|
+
st.info("No traces stored for this project yet.")
|
|
420
|
+
else:
|
|
421
|
+
trace = select_trace(traces)
|
|
422
|
+
if trace is not None:
|
|
423
|
+
report = trace.report()
|
|
424
|
+
plan = build_optimization_plan(trace, report)
|
|
425
|
+
if (
|
|
426
|
+
plan.get("savings_aggregation", {}).get("latency_estimate_complete") is False
|
|
427
|
+
or plan.get("savings_aggregation", {}).get("cost_estimate_complete") is False
|
|
428
|
+
):
|
|
429
|
+
st.caption("Some savings are unavailable; totals cover modeled candidates only.")
|
|
430
|
+
current = plan["current"]
|
|
431
|
+
after = plan["estimated_after"]
|
|
432
|
+
|
|
433
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
434
|
+
c1.metric("Current runtime", seconds(current["runtime_ms"]))
|
|
435
|
+
c2.metric("Estimated runtime", seconds(after["runtime_ms"]))
|
|
436
|
+
c3.metric("Latency reduction", f"{after['latency_reduction_pct']:.1f}%")
|
|
437
|
+
c4.metric("Cost reduction", percentage(after["cost_reduction_pct"]))
|
|
438
|
+
|
|
439
|
+
with st.expander("Show value and pricing estimate", expanded=False):
|
|
440
|
+
rpm, rate, incident_cost = assumption_inputs("optimization_")
|
|
441
|
+
value = build_value_report(
|
|
442
|
+
trace,
|
|
443
|
+
runs_per_month=rpm,
|
|
444
|
+
engineer_hourly_rate_usd=rate,
|
|
445
|
+
incident_cost_usd=incident_cost,
|
|
446
|
+
)
|
|
447
|
+
render_value_report(value, show_download=False)
|
|
448
|
+
|
|
449
|
+
st.subheader("Optimization cards")
|
|
450
|
+
if plan.get("rule_errors"):
|
|
451
|
+
st.warning(
|
|
452
|
+
"Analysis incomplete; failed rules: "
|
|
453
|
+
+ ", ".join(error["rule_id"] for error in plan["rule_errors"])
|
|
454
|
+
)
|
|
455
|
+
cards = plan["optimization_cards"]
|
|
456
|
+
if not cards:
|
|
457
|
+
st.info("No major optimization cards detected yet.")
|
|
458
|
+
for card in cards:
|
|
459
|
+
with st.expander(
|
|
460
|
+
f"{card['title']} · confidence: {card['confidence']}", expanded=True
|
|
461
|
+
):
|
|
462
|
+
st.write(card["why"])
|
|
463
|
+
render_estimate_details(card)
|
|
464
|
+
st.code(card["rewrite_hint"])
|
|
465
|
+
st.write(
|
|
466
|
+
f"Estimated latency savings: {seconds(card['estimated_latency_savings_ms'])}"
|
|
467
|
+
)
|
|
468
|
+
st.write(f"Estimated cost savings: {money(card['estimated_cost_savings_usd'])}")
|
|
469
|
+
|
|
470
|
+
st.subheader("Execution graph")
|
|
471
|
+
graph = plan["graph"]
|
|
472
|
+
if graph.get("execution") is not None:
|
|
473
|
+
st.json(graph["execution"])
|
|
474
|
+
if graph.get("dependency_evidence") is not None:
|
|
475
|
+
st.json(graph["dependency_evidence"])
|
|
476
|
+
st.write("Bottlenecks")
|
|
477
|
+
st.dataframe(pd.DataFrame(graph["bottlenecks"]), width="stretch")
|
|
478
|
+
st.write("Parallelization candidates")
|
|
479
|
+
if graph["parallelizable_groups"]:
|
|
480
|
+
st.dataframe(pd.DataFrame(graph["parallelizable_groups"]), width="stretch")
|
|
481
|
+
else:
|
|
482
|
+
st.info(
|
|
483
|
+
"No repeated tool-call groups currently qualify as parallelization candidates."
|
|
484
|
+
)
|
|
485
|
+
st.write("Edges")
|
|
486
|
+
st.dataframe(pd.DataFrame(graph["edges"]), width="stretch")
|
|
487
|
+
|
|
488
|
+
st.download_button(
|
|
489
|
+
"Download optimization plan JSON",
|
|
490
|
+
data=json.dumps(plan, indent=2),
|
|
491
|
+
file_name="agentloop_optimization_plan.json",
|
|
492
|
+
mime="application/json",
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
elif page == "Diagnosis":
|
|
496
|
+
traces = store.list_traces(project_id=project_id)
|
|
497
|
+
st.subheader("Machine-actionable diagnosis")
|
|
498
|
+
if not traces:
|
|
499
|
+
st.info("No traces stored for this project yet.")
|
|
500
|
+
else:
|
|
501
|
+
trace = selected_trace_from_options(traces, "Choose trace for diagnosis")
|
|
502
|
+
if trace is not None:
|
|
503
|
+
dimension = st.selectbox(
|
|
504
|
+
"Sort findings by", SORT_FIELDS, format_func=lambda value: value.replace("_", " ")
|
|
505
|
+
)
|
|
506
|
+
diagnosis = build_diagnosis(trace, sort_by=dimension)
|
|
507
|
+
if diagnosis.get("rule_errors"):
|
|
508
|
+
st.warning(
|
|
509
|
+
"Analysis incomplete; failed rules: "
|
|
510
|
+
+ ", ".join(error["rule_id"] for error in diagnosis["rule_errors"])
|
|
511
|
+
)
|
|
512
|
+
summary = diagnosis["summary"]
|
|
513
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
514
|
+
c1.metric("Findings", summary["finding_count"])
|
|
515
|
+
c2.metric("High severity", summary["high_severity_count"])
|
|
516
|
+
c3.metric("Patchable", summary["patchable_count"])
|
|
517
|
+
c4.metric("Latency reduction", f"{summary['estimated_latency_reduction_pct']:.1f}%")
|
|
518
|
+
|
|
519
|
+
findings = diagnosis.get("findings", [])
|
|
520
|
+
if not findings:
|
|
521
|
+
st.info("No machine-actionable findings detected for this trace.")
|
|
522
|
+
for finding in findings:
|
|
523
|
+
label = f"{finding['severity'].upper()} - {finding['title']} ({finding['type']})"
|
|
524
|
+
with st.expander(label, expanded=finding["severity"] == "high"):
|
|
525
|
+
st.write(finding["metadata"].get("why", ""))
|
|
526
|
+
render_estimate_details(finding)
|
|
527
|
+
st.write("Test readiness: " + finding["ranking"]["status"].replace("_", " "))
|
|
528
|
+
st.json(finding["ranking"])
|
|
529
|
+
st.write(f"Finding ID: `{finding['finding_id']}`")
|
|
530
|
+
st.write(f"Confidence: `{finding['confidence']}`")
|
|
531
|
+
st.write(f"Affected spans: `{', '.join(finding['affected_spans'])}`")
|
|
532
|
+
st.write(f"Rewrite: {finding['rewrite']['hint']}")
|
|
533
|
+
st.write(f"Validation: {finding['validation']['acceptance_criteria']}")
|
|
534
|
+
st.write(
|
|
535
|
+
"Estimated savings: "
|
|
536
|
+
f"{seconds(finding['savings']['estimated_latency_savings_ms'])}, "
|
|
537
|
+
f"{money(finding['savings']['estimated_cost_savings_usd'])}"
|
|
538
|
+
)
|
|
539
|
+
if finding["evidence"]:
|
|
540
|
+
st.dataframe(
|
|
541
|
+
pd.DataFrame(finding["evidence"]), width="stretch", hide_index=True
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
st.download_button(
|
|
545
|
+
"Download diagnosis JSON",
|
|
546
|
+
data=json.dumps(diagnosis, indent=2),
|
|
547
|
+
file_name="agentloop_diagnosis.json",
|
|
548
|
+
mime="application/json",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
elif page == "Patch Plan":
|
|
552
|
+
traces = store.list_traces(project_id=project_id)
|
|
553
|
+
st.subheader("Trace-to-patch planning")
|
|
554
|
+
if not traces:
|
|
555
|
+
st.info("No traces stored for this project yet.")
|
|
556
|
+
else:
|
|
557
|
+
trace = selected_trace_from_options(traces, "Choose trace for patch planning")
|
|
558
|
+
repo_path = st.text_input("Repository path", value=str(Path.cwd()))
|
|
559
|
+
if trace is not None:
|
|
560
|
+
try:
|
|
561
|
+
plan = build_patch_plan(trace, repo_path=repo_path)
|
|
562
|
+
except RepositoryPathError as exc:
|
|
563
|
+
# The path is free text re-read on every rerun, so a half-typed one
|
|
564
|
+
# reaches build_patch_plan before the operator has finished editing.
|
|
565
|
+
# Only the path error is caught: anything else planning raises is a
|
|
566
|
+
# bug in planning, and swallowing it here would hide it.
|
|
567
|
+
st.error(f"Fix the repository path: {exc}.")
|
|
568
|
+
st.stop()
|
|
569
|
+
summary = plan["summary"]
|
|
570
|
+
c1, c2, c3 = st.columns(3)
|
|
571
|
+
c1.metric("Patch plans", summary["patch_count"])
|
|
572
|
+
c2.metric("Unsupported", summary["unsupported_finding_count"])
|
|
573
|
+
c3.metric("Frameworks", ", ".join(summary["frameworks_detected"]) or "none")
|
|
574
|
+
|
|
575
|
+
for item in plan.get("patch_plans", []):
|
|
576
|
+
with st.expander(f"{item['patch_id']} - {item['title']}", expanded=True):
|
|
577
|
+
st.write(f"Type: `{item['type']}`")
|
|
578
|
+
st.write(f"Risk: `{item['risk']}`")
|
|
579
|
+
st.write(f"Framework: `{item['framework']}`")
|
|
580
|
+
st.write(f"Finding: `{item['finding_id']}`")
|
|
581
|
+
st.write(f"Before pattern: {item['before_pattern']}")
|
|
582
|
+
st.write(f"Proposed rewrite: {item['proposed_rewrite']}")
|
|
583
|
+
st.code(item["suggested_diff"], language="text")
|
|
584
|
+
st.write(f"Validation command: `{item['validation_command']}`")
|
|
585
|
+
st.write(f"Acceptance: {item['acceptance_criteria']}")
|
|
586
|
+
if item["files"]:
|
|
587
|
+
st.dataframe(pd.DataFrame(item["files"]), width="stretch", hide_index=True)
|
|
588
|
+
if item["notes"]:
|
|
589
|
+
for note in item["notes"]:
|
|
590
|
+
st.write(f"- {note}")
|
|
591
|
+
|
|
592
|
+
if plan.get("unsupported_findings"):
|
|
593
|
+
st.subheader("Unsupported findings")
|
|
594
|
+
st.dataframe(
|
|
595
|
+
pd.DataFrame(plan["unsupported_findings"]), width="stretch", hide_index=True
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
st.download_button(
|
|
599
|
+
"Download patch plan JSON",
|
|
600
|
+
data=json.dumps(plan, indent=2),
|
|
601
|
+
file_name="agentloop_patch_plan.json",
|
|
602
|
+
mime="application/json",
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
elif page == "Replay Proof":
|
|
606
|
+
traces = store.list_traces(project_id=project_id)
|
|
607
|
+
st.subheader("Before/after replay proof")
|
|
608
|
+
if len(traces) < 2:
|
|
609
|
+
st.info("Store at least two traces to compare a baseline and candidate.")
|
|
610
|
+
else:
|
|
611
|
+
options = trace_options(traces)
|
|
612
|
+
labels = list(options.keys())
|
|
613
|
+
baseline_label = st.selectbox("Baseline trace", labels, index=0)
|
|
614
|
+
candidate_label = st.selectbox("Candidate trace", labels, index=1 if len(labels) > 1 else 0)
|
|
615
|
+
baseline = load_trace_for_project(options[baseline_label], project_id)
|
|
616
|
+
candidate = load_trace_for_project(options[candidate_label], project_id)
|
|
617
|
+
|
|
618
|
+
st.markdown("#### Gates")
|
|
619
|
+
g1, g2, g3, g4 = st.columns(4)
|
|
620
|
+
min_latency = g1.number_input("Min latency improvement %", min_value=0.0, value=20.0)
|
|
621
|
+
min_cost = g2.number_input("Min cost improvement %", min_value=0.0, value=5.0)
|
|
622
|
+
require_schema = g3.checkbox("Require schema valid", value=False)
|
|
623
|
+
min_quality = g4.number_input("Min quality score", min_value=0.0, max_value=1.0, value=0.0)
|
|
624
|
+
quality_threshold = min_quality if min_quality > 0 else None
|
|
625
|
+
|
|
626
|
+
if baseline is not None and candidate is not None:
|
|
627
|
+
gates = ReplayGates(
|
|
628
|
+
min_latency_improvement_pct=min_latency,
|
|
629
|
+
min_cost_improvement_pct=min_cost,
|
|
630
|
+
require_schema_valid=require_schema,
|
|
631
|
+
min_quality_score=quality_threshold,
|
|
632
|
+
)
|
|
633
|
+
replay = build_replay_report(baseline, candidate, gates=gates)
|
|
634
|
+
ci_report = build_ci_report(baseline, candidate, gates=gates)
|
|
635
|
+
status = "passed" if replay["gates"]["passed"] else "failed"
|
|
636
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
637
|
+
c1.metric("Status", status)
|
|
638
|
+
c2.metric("Latency improvement", f"{replay['deltas']['latency_improvement_pct']:.1f}%")
|
|
639
|
+
cost_improvement = replay["deltas"]["cost_improvement_pct"]
|
|
640
|
+
c3.metric("Cost improvement", percentage(cost_improvement))
|
|
641
|
+
c4.metric("Retry delta", replay["deltas"]["retry_count_delta"])
|
|
642
|
+
|
|
643
|
+
st.subheader("Gate results")
|
|
644
|
+
render_gate_table(replay["gates"]["results"])
|
|
645
|
+
|
|
646
|
+
st.subheader("Metric deltas")
|
|
647
|
+
metric_rows = [
|
|
648
|
+
{"metric": key, "value": value}
|
|
649
|
+
for key, value in replay["deltas"].items()
|
|
650
|
+
if key.endswith("_delta") or key.endswith("_pct")
|
|
651
|
+
]
|
|
652
|
+
st.dataframe(pd.DataFrame(metric_rows), width="stretch", hide_index=True)
|
|
653
|
+
|
|
654
|
+
st.subheader("PR comment preview")
|
|
655
|
+
st.code(
|
|
656
|
+
"Trace in. Rewrite plan out. Replay proof in the PR.\n\n"
|
|
657
|
+
+ ci_report_to_markdown(ci_report)
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
col_json, col_md = st.columns(2)
|
|
661
|
+
col_json.download_button(
|
|
662
|
+
"Download replay JSON",
|
|
663
|
+
data=json.dumps(replay, indent=2),
|
|
664
|
+
file_name="agentloop_replay_report.json",
|
|
665
|
+
mime="application/json",
|
|
666
|
+
)
|
|
667
|
+
col_md.download_button(
|
|
668
|
+
"Download PR Markdown",
|
|
669
|
+
data=ci_report_to_markdown(ci_report),
|
|
670
|
+
file_name="agentloop_ci.md",
|
|
671
|
+
mime="text/markdown",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
elif page == "Quality Gates":
|
|
675
|
+
traces = store.list_traces(project_id=project_id)
|
|
676
|
+
st.subheader("Quality gates")
|
|
677
|
+
st.caption(
|
|
678
|
+
"Compare caller-defined quality criteria alongside recorded cost and latency. "
|
|
679
|
+
"Legacy scorers and versioned structured decision, multilabel, numeric, extraction and matching scorers are supported. Custom Python scorers are trusted local code."
|
|
680
|
+
)
|
|
681
|
+
if len(traces) < 2:
|
|
682
|
+
st.info("Store at least two traces to score baseline and candidate quality.")
|
|
683
|
+
else:
|
|
684
|
+
options = trace_options(traces)
|
|
685
|
+
labels = list(options.keys())
|
|
686
|
+
default_fixture = [
|
|
687
|
+
{
|
|
688
|
+
"id": "required_summary_fields",
|
|
689
|
+
"input": "Summarize one source.",
|
|
690
|
+
"candidate_output": {
|
|
691
|
+
"summary": "AgentLoop proves rewrites.",
|
|
692
|
+
"sources": ["source-a"],
|
|
693
|
+
},
|
|
694
|
+
"baseline_output": {
|
|
695
|
+
"summary": "AgentLoop proves rewrites.",
|
|
696
|
+
"sources": ["source-a"],
|
|
697
|
+
},
|
|
698
|
+
"scorer": {"type": "required_fields", "required": ["summary", "sources"]},
|
|
699
|
+
}
|
|
700
|
+
]
|
|
701
|
+
with st.form("quality_gate_form"):
|
|
702
|
+
baseline_label = st.selectbox("Baseline trace", labels, index=0, key="quality_baseline")
|
|
703
|
+
candidate_label = st.selectbox(
|
|
704
|
+
"Candidate trace",
|
|
705
|
+
labels,
|
|
706
|
+
index=1 if len(labels) > 1 else 0,
|
|
707
|
+
key="quality_candidate",
|
|
708
|
+
)
|
|
709
|
+
min_score = st.number_input(
|
|
710
|
+
"Minimum candidate quality score", min_value=0.0, max_value=1.0, value=0.9
|
|
711
|
+
)
|
|
712
|
+
uploaded = st.file_uploader(
|
|
713
|
+
"Upload quality fixture JSON", type=["json"], key="quality_fixture_upload"
|
|
714
|
+
)
|
|
715
|
+
fixtures_text = st.text_area(
|
|
716
|
+
"Fixture JSON",
|
|
717
|
+
value=json.dumps({"fixtures": default_fixture}, indent=2),
|
|
718
|
+
height=260,
|
|
719
|
+
)
|
|
720
|
+
submitted = st.form_submit_button("Run quality gate")
|
|
721
|
+
|
|
722
|
+
if submitted:
|
|
723
|
+
try:
|
|
724
|
+
raw_fixtures = (
|
|
725
|
+
uploaded.read().decode("utf-8") if uploaded is not None else fixtures_text
|
|
726
|
+
)
|
|
727
|
+
fixtures = parse_quality_fixtures(json.loads(raw_fixtures))
|
|
728
|
+
baseline = load_trace_for_project(options[baseline_label], project_id)
|
|
729
|
+
candidate = load_trace_for_project(options[candidate_label], project_id)
|
|
730
|
+
if baseline is None or candidate is None:
|
|
731
|
+
raise QualityValidationError(
|
|
732
|
+
"selected baseline or candidate trace is unavailable"
|
|
733
|
+
)
|
|
734
|
+
quality = build_quality_report(
|
|
735
|
+
fixtures,
|
|
736
|
+
baseline_trace=baseline,
|
|
737
|
+
candidate_trace=candidate,
|
|
738
|
+
min_score=min_score,
|
|
739
|
+
)
|
|
740
|
+
except json.JSONDecodeError as exc:
|
|
741
|
+
st.error(
|
|
742
|
+
"Fix the quality fixture JSON: "
|
|
743
|
+
f"{exc.msg} at line {exc.lineno}, column {exc.colno}."
|
|
744
|
+
)
|
|
745
|
+
except (UnicodeError, QualityValidationError) as exc:
|
|
746
|
+
st.error(f"Fix the quality fixture JSON: {exc}.")
|
|
747
|
+
else:
|
|
748
|
+
status = "passed" if quality["passed"] else "failed"
|
|
749
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
750
|
+
c1.metric("Status", status)
|
|
751
|
+
c2.metric(
|
|
752
|
+
"Candidate score",
|
|
753
|
+
"unavailable"
|
|
754
|
+
if quality["candidate_score"] is None
|
|
755
|
+
else str(quality["candidate_score"])
|
|
756
|
+
if quality.get("schema_version") == "2.0"
|
|
757
|
+
else f"{quality['candidate_score']:.4f}",
|
|
758
|
+
)
|
|
759
|
+
c3.metric(
|
|
760
|
+
"Quality delta",
|
|
761
|
+
"unavailable"
|
|
762
|
+
if quality["quality_delta"] is None
|
|
763
|
+
else str(quality["quality_delta"])
|
|
764
|
+
if quality.get("schema_version") == "2.0"
|
|
765
|
+
else f"{quality['quality_delta']:.4f}",
|
|
766
|
+
)
|
|
767
|
+
c4.metric("Failed cases", quality["failed_case_count"])
|
|
768
|
+
if quality.get("schema_version") == "2.0":
|
|
769
|
+
st.caption(
|
|
770
|
+
f"Indeterminate cases: {quality['indeterminate_case_count']}; incomplete evidence cannot pass the quality gate."
|
|
771
|
+
)
|
|
772
|
+
with st.expander("Quality denominators and classification metrics"):
|
|
773
|
+
st.json(
|
|
774
|
+
{
|
|
775
|
+
"summaries": quality["summaries"],
|
|
776
|
+
"classification": quality["classification"],
|
|
777
|
+
}
|
|
778
|
+
)
|
|
779
|
+
st.dataframe(pd.DataFrame(quality["cases"]), width="stretch", hide_index=True)
|
|
780
|
+
st.download_button(
|
|
781
|
+
"Download quality report JSON",
|
|
782
|
+
data=json.dumps(quality, indent=2),
|
|
783
|
+
file_name="agentloop_quality_report.json",
|
|
784
|
+
mime="application/json",
|
|
785
|
+
)
|
|
786
|
+
st.download_button(
|
|
787
|
+
"Download quality report Markdown",
|
|
788
|
+
data=quality_report_to_markdown(quality),
|
|
789
|
+
file_name="agentloop_quality_report.md",
|
|
790
|
+
mime="text/markdown",
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
elif page == "Value & Pricing":
|
|
794
|
+
traces = store.list_traces(project_id=project_id)
|
|
795
|
+
st.subheader("Value report and pricing scenario")
|
|
796
|
+
st.caption(
|
|
797
|
+
"Turn an agent trace into operational value, reliability risk, and a configurable pricing scenario."
|
|
798
|
+
)
|
|
799
|
+
if not traces:
|
|
800
|
+
st.info(
|
|
801
|
+
"No traces stored for this project yet. Generate demo traces from the Ingest page first."
|
|
802
|
+
)
|
|
803
|
+
else:
|
|
804
|
+
trace = select_trace(traces, label="Choose trace for value report")
|
|
805
|
+
if trace is not None:
|
|
806
|
+
st.markdown("#### Assumptions")
|
|
807
|
+
rpm, rate, incident_cost = assumption_inputs("value_page_")
|
|
808
|
+
value = build_value_report(
|
|
809
|
+
trace,
|
|
810
|
+
runs_per_month=rpm,
|
|
811
|
+
engineer_hourly_rate_usd=rate,
|
|
812
|
+
incident_cost_usd=incident_cost,
|
|
813
|
+
)
|
|
814
|
+
render_value_report(value)
|
|
815
|
+
st.subheader("CLI command")
|
|
816
|
+
st.code(
|
|
817
|
+
"uv run agentloop value-report --path runs/research_agent_baseline.json "
|
|
818
|
+
f"--runs-per-month {rpm} "
|
|
819
|
+
f"--engineer-hourly-rate-usd {rate} "
|
|
820
|
+
f"--incident-cost-usd {incident_cost} "
|
|
821
|
+
"--out runs/value_report.json",
|
|
822
|
+
language="bash",
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
elif page == "API Keys":
|
|
826
|
+
st.subheader("Create project API key")
|
|
827
|
+
st.warning("API keys are only shown once. Copy the generated key before leaving this page.")
|
|
828
|
+
key_name = st.text_input("Key name", value="local-dev")
|
|
829
|
+
if st.button("Create API key"):
|
|
830
|
+
created = store.create_api_key(project_id=project_id, name=key_name)
|
|
831
|
+
st.success(f"Created key for project `{project_id}`")
|
|
832
|
+
st.code(created["api_key"])
|
|
833
|
+
|
|
834
|
+
st.subheader("Use this key")
|
|
835
|
+
st.code(
|
|
836
|
+
"uv run agentloop upload --path runs/research_agent_baseline.json "
|
|
837
|
+
"--api-url http://127.0.0.1:8000 --api-key YOUR_KEY",
|
|
838
|
+
language="bash",
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
elif page == "Ingest":
|
|
842
|
+
st.subheader("Generate or upload traces")
|
|
843
|
+
col_a, col_b, col_c, col_d = st.columns(4)
|
|
844
|
+
generated_path: Path | None = None
|
|
845
|
+
if col_a.button("Generate baseline"):
|
|
846
|
+
generated_path = run_baseline(runs_dir)
|
|
847
|
+
if col_b.button("Generate optimized"):
|
|
848
|
+
generated_path = run_optimized(runs_dir)
|
|
849
|
+
if col_c.button("Generate LangGraph-style"):
|
|
850
|
+
generated_path = run_langgraph_style(runs_dir)
|
|
851
|
+
if col_d.button("Generate all"):
|
|
852
|
+
proof_baseline, proof_candidate = run_proof_pair(runs_dir)
|
|
853
|
+
for path in [
|
|
854
|
+
run_baseline(runs_dir),
|
|
855
|
+
run_optimized(runs_dir),
|
|
856
|
+
run_langgraph_style(runs_dir),
|
|
857
|
+
proof_baseline,
|
|
858
|
+
proof_candidate,
|
|
859
|
+
]:
|
|
860
|
+
store.save_trace(AgentTrace.from_json(path), project_id=project_id)
|
|
861
|
+
st.success(f"Generated and stored all demo traces under project `{project_id}`")
|
|
862
|
+
|
|
863
|
+
if st.button("Generate proof demo pair"):
|
|
864
|
+
proof_baseline, proof_candidate = run_proof_pair(runs_dir)
|
|
865
|
+
for path in [proof_baseline, proof_candidate]:
|
|
866
|
+
store.save_trace(AgentTrace.from_json(path), project_id=project_id)
|
|
867
|
+
st.success(f"Generated and stored proof demo traces under project `{project_id}`")
|
|
868
|
+
|
|
869
|
+
if generated_path is not None:
|
|
870
|
+
trace = AgentTrace.from_json(generated_path)
|
|
871
|
+
store.save_trace(trace, project_id=project_id)
|
|
872
|
+
st.success(f"Generated and stored `{trace.name}` as `{trace.run_id}`")
|
|
873
|
+
|
|
874
|
+
uploaded = st.file_uploader("Upload AgentLoop trace JSON", type=["json"])
|
|
875
|
+
if uploaded is not None:
|
|
876
|
+
try:
|
|
877
|
+
trace = parse_uploaded_trace(uploaded.read())
|
|
878
|
+
except TraceUploadError as exc:
|
|
879
|
+
st.error(f"Fix the uploaded trace: {exc}.")
|
|
880
|
+
else:
|
|
881
|
+
if st.button("Store uploaded trace"):
|
|
882
|
+
store.save_trace(trace, project_id=project_id)
|
|
883
|
+
st.success(f"Stored uploaded trace `{trace.run_id}` under project `{project_id}`")
|
|
884
|
+
|
|
885
|
+
st.subheader("CLI ingest")
|
|
886
|
+
st.code(
|
|
887
|
+
"uv run agentloop demo-all\n"
|
|
888
|
+
f"uv run agentloop store-trace --path runs/research_agent_baseline.json --project-id {project_id}\n"
|
|
889
|
+
f"uv run agentloop list-stored-traces --project-id {project_id}",
|
|
890
|
+
language="bash",
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
elif page == "Setup":
|
|
894
|
+
st.subheader("Local development")
|
|
895
|
+
st.code(
|
|
896
|
+
"uv sync --locked --all-extras --dev\n"
|
|
897
|
+
"uv run agentloop init-store\n"
|
|
898
|
+
"uv run agentloop demo-all\n"
|
|
899
|
+
f"uv run agentloop store-trace --path runs/research_agent_baseline.json --project-id {project_id}\n"
|
|
900
|
+
"uv run streamlit run dashboard/app.py",
|
|
901
|
+
language="bash",
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
st.subheader("Hosted API")
|
|
905
|
+
st.code(
|
|
906
|
+
"uv run agentloop create-api-key --project-id acme --name local-dev\n"
|
|
907
|
+
"uv run agentloop server --host 127.0.0.1 --port 8000\n"
|
|
908
|
+
"uv run agentloop upload --path runs/research_agent_baseline.json "
|
|
909
|
+
"--api-url http://127.0.0.1:8000 --api-key al_xxx\n"
|
|
910
|
+
"uv run agentloop remote-usage --api-url http://127.0.0.1:8000 --api-key al_xxx",
|
|
911
|
+
language="bash",
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
st.subheader("Postgres deployment")
|
|
915
|
+
st.code(
|
|
916
|
+
"uv sync --locked --extra server --extra postgres --no-dev\n"
|
|
917
|
+
"export AGENTLOOP_STORE_BACKEND=postgres\n"
|
|
918
|
+
"export AGENTLOOP_DATABASE_URL=postgresql://agentloop:agentloop@localhost:5432/agentloop\n"
|
|
919
|
+
"uv run agentloop init-store\n"
|
|
920
|
+
"uv run agentloop server --host 0.0.0.0 --port 8000",
|
|
921
|
+
language="bash",
|
|
922
|
+
)
|