From d8112a3b57d2a21ee16dd6a5000e9af63bfe3321 Mon Sep 17 00:00:00 2001 From: Sushant Date: Wed, 5 Aug 2026 23:40:29 +0530 Subject: [PATCH] feat: add Streamlit demo dashboard --- frontend/app.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 frontend/app.py diff --git a/frontend/app.py b/frontend/app.py new file mode 100644 index 0000000..a77089c --- /dev/null +++ b/frontend/app.py @@ -0,0 +1,133 @@ +"""AgentLoop - Closed-loop outcome intelligence Demo Dashboard. + +Run with: streamlit run frontend/app.py (from the AgentLoop repo root) +Connects to a running backend (uvicorn app.main:app in backend/) if reachable; +otherwise shows a synthetic demo dataset. Requires: pandas, plotly, streamlit, requests. +""" + +from __future__ import annotations + +import random +from datetime import datetime, timedelta + +import pandas as pd +import plotly.express as px +import requests +import streamlit as st + +st.set_page_config( + page_title="AgentLoop", + page_icon="", + layout="wide", + initial_sidebar_state="expanded", +) + +DEFAULT_API = "http://localhost:8000" + + +def make_demo_data() -> tuple[pd.DataFrame, dict]: + random.seed(42) + paths = ["plan->tool->answer", "retrieve->rerank->generate", "agent->human", "direct"] + outcomes = ["resolved", "escalated", "converted", "retained"] + n = 120 + rows = [] + now = datetime.utcnow() + for i in range(n): + rows.append( + { + "session_id": f"s{i:04d}", + "agent_version": f"v{random.randint(1, 3)}", + "path": random.choice(paths), + "outcome": random.choice(outcomes), + "latency_ms": random.randint(80, 4200), + "cost_usd": round(random.uniform(0.01, 0.9), 3), + "csat": random.randint(1, 5), + "created_at": now - timedelta(days=random.randint(0, 29), hours=random.randint(0, 23)), + } + ) + df = pd.DataFrame(rows) + metrics = { + "total_sessions": n, + "resolution_rate": round((df.outcome != "escalated").mean(), 3), + "avg_latency_ms": int(df.latency_ms.mean()), + "avg_csat": round(df.csat.mean(), 2), + } + return df, metrics + + +def fetch_live(api: str): + try: + r = requests.get(f"{api}/api/v1/analytics", timeout=5) + r.raise_for_status() + return r.json(), None + except Exception as exc: # noqa: BLE001 + return None, str(exc) + + +def main(): + with st.sidebar: + st.header("Backend") + api = st.text_input("API base URL", value=DEFAULT_API) + use_live = st.toggle("Use live backend", value=False) + refresh = st.button("Refresh", type="primary", use_container_width=True) + + if use_live: + payload, err = fetch_live(api) + if err is not None: + st.warning(f"Could not reach backend: {err}\n\nShowing synthetic demo data instead.") + use_live = False + + if use_live: + st.caption(f"Live data from {api}") + path_df = pd.DataFrame(payload.get("path_analysis", [])) + comp_df = pd.DataFrame(payload.get("agent_comparison", [])) + om = payload.get("outcome_metrics") or {} + else: + df, metrics = make_demo_data() + path_df = ( + df.groupby("path") + .agg(total_sessions=("session_id", "count"), success_count=("outcome", lambda s: (s != "escalated").sum())) + .reset_index() + ) + path_df["success_rate"] = (path_df.success_count / path_df.total_sessions).round(3) + path_df["avg_latency_ms"] = df.groupby("path").latency_ms.mean().values.astype(int) + comp_df = ( + df.groupby("agent_version") + .agg(session_count=("session_id", "count")) + .reset_index() + ) + comp_df["success_rate"] = df.groupby("agent_version").outcome.apply(lambda s: (s != "escalated").mean()).values + om = { + "total_sessions": metrics["total_sessions"], + "resolution_rate": metrics["resolution_rate"], + "avg_latency_ms": metrics["avg_latency_ms"], + "avg_csat": metrics["avg_csat"], + } + st.caption("Synthetic demo data (start the backend to see live analytics)") + + st.title("AgentLoop") + st.caption("Closed-loop outcome intelligence platform") + + c1, c2, c3, c4 = st.columns(4) + c1.metric("Sessions", om.get("total_sessions", 0)) + c2.metric("Resolution rate", f"{om.get('resolution_rate', 0)*100:.1f}%") + c3.metric("Avg latency", f"{om.get('avg_latency_ms', 0)} ms") + c4.metric("Avg CSAT", om.get("avg_csat", "-")) + + st.subheader("Path Analysis") + if not path_df.empty: + fig = px.bar(path_df, x="path", y="total_sessions", color="success_rate", color_continuous_scale="greens") + st.plotly_chart(fig, use_container_width=True) + st.dataframe(path_df) + + st.subheader("Agent Version Comparison") + if not comp_df.empty: + fig2 = px.bar(comp_df, x="agent_version", y="session_count", color="success_rate", color_continuous_scale="blues") + st.plotly_chart(fig2, use_container_width=True) + st.dataframe(comp_df) + + if not use_live and refresh: + pass + + +main()