|
1 | | -import re |
2 | | -from google.cloud import bigquery |
| 1 | +import streamlit as st |
3 | 2 | import pandas as pd |
| 3 | +from google.cloud import bigquery |
4 | 4 | import plotly.express as px |
5 | | -import streamlit as st |
6 | | - |
| 5 | +import re |
7 | 6 |
|
8 | 7 | # --- 1. Page Configuration --- |
9 | 8 | st.set_page_config(page_title="Agent Analytics V2", layout="wide") |
|
35 | 34 |
|
36 | 35 | # --- 3. Sidebar Configuration --- |
37 | 36 | with st.sidebar: |
38 | | - st.header("Data Source") |
39 | | - project_id = st.text_input("GCP Project ID", "REPLACE WITH YOUR PROJECT ID") |
40 | | - dataset_id = st.text_input("Dataset ID", "REPLACE WITH YOUR DATASET ID") |
41 | | - table_id = st.text_input("Table ID", "REPLACE WITH YOUR TABLE ID") |
42 | | - |
43 | | - # Identifier Validation |
44 | | - for val in [project_id, dataset_id, table_id]: |
45 | | - if not re.match(r"^[\w.-]+$", val): |
46 | | - st.error(f"Invalid identifier: {val}") |
47 | | - st.stop() |
| 37 | + st.header("Data Source") |
| 38 | + project_id = st.text_input("GCP Project ID", "REPLACE WITH YOUR PROJECT ID") |
| 39 | + dataset_id = st.text_input("Dataset ID", "REPLACE WITH YOUR DATASET ID") |
| 40 | + table_id = st.text_input("Table ID", "REPLACE WITH YOUR TABLE ID") |
48 | 41 |
|
49 | | - table_ref = f"`{project_id}.{dataset_id}.{table_id}`" |
| 42 | + # Identifier Validation |
| 43 | + for val in [project_id, dataset_id, table_id]: |
| 44 | + if not re.match(r'^[\w.-]+$', val): |
| 45 | + st.error(f"Invalid identifier: {val}") |
| 46 | + st.stop() |
50 | 47 |
|
51 | | - st.divider() |
52 | | - st.header("Global Filters") |
53 | | - time_hours = st.slider("Time Range (Hours)", 1, 720, 72) |
54 | | - agent_name = st.text_input("Filter by Agent Name", "") |
| 48 | + table_ref = f"`{project_id}.{dataset_id}.{table_id}`" |
55 | 49 |
|
| 50 | + st.divider() |
| 51 | + st.header("Global Filters") |
| 52 | + time_hours = st.slider("Time Range (Hours)", 1, 720, 72) |
| 53 | + agent_name = st.text_input("Filter by Agent Name", "") |
56 | 54 |
|
57 | 55 | # --- 4. Data Connector --- |
58 | 56 | @st.cache_resource |
59 | 57 | def get_bq_client(p_id): |
60 | | - return bigquery.Client(project=p_id) |
61 | | - |
| 58 | + return bigquery.Client(project=p_id) |
62 | 59 |
|
63 | 60 | client = get_bq_client(project_id) |
64 | 61 |
|
65 | | - |
66 | 62 | def safe_query(sql, params=None): |
67 | | - try: |
68 | | - job_config = ( |
69 | | - bigquery.QueryJobConfig(query_parameters=params) if params else None |
70 | | - ) |
71 | | - formatted_sql = sql.replace("{table_ref}", table_ref) |
72 | | - return client.query(formatted_sql, job_config=job_config).to_dataframe() |
73 | | - except Exception as e: |
74 | | - st.error(f"Query Failed: {e}") |
75 | | - return pd.DataFrame() |
76 | | - |
| 63 | + try: |
| 64 | + job_config = bigquery.QueryJobConfig(query_parameters=params) if params else None |
| 65 | + formatted_sql = sql.replace("{table_ref}", table_ref) |
| 66 | + return client.query(formatted_sql, job_config=job_config).to_dataframe() |
| 67 | + except Exception as e: |
| 68 | + st.error(f"Query Failed: {e}") |
| 69 | + return pd.DataFrame() |
77 | 70 |
|
78 | 71 | # --- 5. Global Params (Resolved 400 Parameter Error) --- |
79 | 72 |
|
80 | 73 | global_params = [ |
81 | 74 | bigquery.ScalarQueryParameter("time_hours", "INT64", time_hours), |
82 | | - bigquery.ScalarQueryParameter( |
83 | | - "agent_name", "STRING", agent_name if agent_name else None |
84 | | - ), |
| 75 | + bigquery.ScalarQueryParameter("agent_name", "STRING", agent_name if agent_name else None) |
85 | 76 | ] |
86 | 77 |
|
87 | 78 | # WHERE clause for non-CTE queries (Executive Summary & Multimodal) |
88 | | -where_clause = ( |
89 | | - "timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL @time_hours HOUR)" |
90 | | -) |
| 79 | +where_clause = "timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL @time_hours HOUR)" |
91 | 80 | if agent_name: |
92 | | - where_clause += " AND agent = @agent_name" |
| 81 | + where_clause += " AND agent = @agent_name" |
93 | 82 |
|
94 | 83 | # --- 6. Executive Summary (Resolved PR Consistency Issue) --- |
95 | 84 | st.header("📊 Executive Summary") |
96 | 85 |
|
97 | | -kpi_data = safe_query( |
98 | | - f""" |
| 86 | +kpi_data = safe_query(f""" |
99 | 87 | WITH enriched AS ({ENRICHED_EVENTS_CTE}) |
100 | 88 | SELECT |
101 | 89 | COUNT(DISTINCT session_id) as sessions, |
102 | 90 | COUNT(DISTINCT trace_id) as traces, |
103 | 91 | COUNTIF(event_type = 'LLM_REQUEST') as llm_calls, |
104 | 92 | SAFE_DIVIDE(COUNTIF(is_canonical_error = TRUE), COUNT(*)) as error_rate |
105 | 93 | FROM enriched |
106 | | -""", |
107 | | - params=global_params, |
108 | | -) |
| 94 | +""", params=global_params) |
109 | 95 |
|
110 | 96 | if not kpi_data.empty: |
111 | | - c1, c2, c3, c4 = st.columns(4) |
112 | | - c1.metric("Sessions", kpi_data["sessions"][0]) |
113 | | - c2.metric("Traces", kpi_data["traces"][0]) |
114 | | - c3.metric("LLM Calls", kpi_data["llm_calls"][0]) |
115 | | - # The rate now includes TOOL_ERROR and LLM_ERROR automatically |
116 | | - c4.metric("Error Rate", f"{kpi_data['error_rate'][0]*100:.2f}%") |
117 | | - |
| 97 | + c1, c2, c3, c4 = st.columns(4) |
| 98 | + c1.metric("Sessions", kpi_data['sessions'][0]) |
| 99 | + c2.metric("Traces", kpi_data['traces'][0]) |
| 100 | + c3.metric("LLM Calls", kpi_data['llm_calls'][0]) |
| 101 | + # The rate now includes TOOL_ERROR and LLM_ERROR automatically |
| 102 | + c4.metric("Error Rate", f"{kpi_data['error_rate'][0]*100:.2f}%") |
| 103 | + |
118 | 104 |
|
119 | 105 | # --- 7. Detailed Tabs --- |
120 | | -tabs = st.tabs([ |
121 | | - "📈 Usage", |
122 | | - "⚡ Performance", |
123 | | - "🛡️ Reliability", |
124 | | - "🖼️ Multimodal", |
125 | | - "🤝 HITL", |
126 | | - "🔍 Trace Explorer", |
127 | | -]) |
| 106 | +tabs = st.tabs(["📈 Usage", "⚡ Performance", "🛡️ Reliability", "🖼️ Multimodal", "🤝 HITL", "🔍 Trace Explorer"]) |
128 | 107 |
|
129 | 108 | # TAB 0: USAGE & VOLUME |
130 | 109 | with tabs[0]: |
131 | | - st.subheader("Token Consumption") |
132 | | - token_trend = safe_query( |
133 | | - f""" |
| 110 | + st.subheader("Token Consumption") |
| 111 | + token_trend = safe_query(f""" |
134 | 112 | SELECT TIMESTAMP_TRUNC(timestamp, HOUR) as hour, |
135 | 113 | SUM(CAST(JSON_VALUE(content, '$.usage.prompt') AS INT64)) as prompt, |
136 | 114 | SUM(CAST(JSON_VALUE(content, '$.usage.completion') AS INT64)) as completion |
137 | 115 | FROM {{table_ref}} WHERE event_type = 'LLM_RESPONSE' AND {where_clause} |
138 | 116 | GROUP BY 1 ORDER BY 1 |
139 | | - """, |
140 | | - params=global_params, |
141 | | - ) |
142 | | - if not token_trend.empty: |
143 | | - fig = px.area( |
144 | | - token_trend, |
145 | | - x="hour", |
146 | | - y=["prompt", "completion"], |
147 | | - title="Tokens over Time", |
148 | | - ) |
149 | | - st.plotly_chart(fig, use_container_width=True) |
150 | | - else: |
151 | | - st.info("No token usage data found.") |
| 117 | + """, params=global_params) |
| 118 | + if not token_trend.empty: |
| 119 | + fig = px.area(token_trend, x="hour", y=["prompt", "completion"], title="Tokens over Time") |
| 120 | + st.plotly_chart(fig, use_container_width=True) |
| 121 | + else: |
| 122 | + st.info("No token usage data found.") |
152 | 123 |
|
153 | 124 | # TAB 1: PERFORMANCE |
154 | 125 | with tabs[1]: |
155 | | - st.subheader("Latency Metrics") |
156 | | - lat_data = safe_query( |
157 | | - f""" |
| 126 | + st.subheader("Latency Metrics") |
| 127 | + lat_data = safe_query(f""" |
158 | 128 | WITH enriched AS ({ENRICHED_EVENTS_CTE}) |
159 | 129 | SELECT |
160 | 130 | APPROX_QUANTILES(CAST(JSON_VALUE(latency_ms, '$.total_ms') AS FLOAT64), 100)[OFFSET(50)] as p50, |
161 | 131 | APPROX_QUANTILES(CAST(JSON_VALUE(latency_ms, '$.total_ms') AS FLOAT64), 100)[OFFSET(90)] as p90, |
162 | 132 | AVG(CAST(JSON_VALUE(latency_ms, '$.time_to_first_token_ms') AS FLOAT64)) as avg_ttft |
163 | 133 | FROM enriched WHERE event_family = 'llm' |
164 | | - """, |
165 | | - params=global_params, |
166 | | - ) |
167 | | - if not lat_data.empty and pd.notnull(lat_data["p50"][0]): |
168 | | - p1, p2, p3 = st.columns(3) |
169 | | - p1.metric("p50 Latency", f"{lat_data['p50'][0]:.0f}ms") |
170 | | - p2.metric("p90 Latency", f"{lat_data['p90'][0]:.0f}ms") |
171 | | - p3.metric("Avg TTFT", f"{lat_data['avg_ttft'][0]:.0f}ms") |
172 | | - else: |
173 | | - st.warning("No performance data found.") |
| 134 | + """, params=global_params) |
| 135 | + if not lat_data.empty and pd.notnull(lat_data['p50'][0]): |
| 136 | + p1, p2, p3 = st.columns(3) |
| 137 | + p1.metric("p50 Latency", f"{lat_data['p50'][0]:.0f}ms") |
| 138 | + p2.metric("p90 Latency", f"{lat_data['p90'][0]:.0f}ms") |
| 139 | + p3.metric("Avg TTFT", f"{lat_data['avg_ttft'][0]:.0f}ms") |
| 140 | + else: |
| 141 | + st.warning("No performance data found.") |
174 | 142 |
|
175 | 143 | # TAB 2: RELIABILITY |
176 | 144 | with tabs[2]: |
177 | | - st.subheader("Error Distribution") |
178 | | - err_data = safe_query( |
179 | | - f""" |
| 145 | + st.subheader("Error Distribution") |
| 146 | + err_data = safe_query(f""" |
180 | 147 | WITH enriched AS ({ENRICHED_EVENTS_CTE}) |
181 | 148 | SELECT event_family, event_type, COUNT(*) as errors |
182 | 149 | FROM enriched WHERE is_canonical_error = TRUE |
183 | 150 | GROUP BY 1, 2 ORDER BY errors DESC |
184 | | - """, |
185 | | - params=global_params, |
186 | | - ) |
187 | | - if not err_data.empty: |
188 | | - fig = px.bar(err_data, x="event_type", y="errors", color="event_family") |
189 | | - st.plotly_chart(fig, use_container_width=True) |
190 | | - else: |
191 | | - st.success("No errors detected.") |
| 151 | + """, params=global_params) |
| 152 | + if not err_data.empty: |
| 153 | + fig = px.bar(err_data, x="event_type", y="errors", color="event_family") |
| 154 | + st.plotly_chart(fig, use_container_width=True) |
| 155 | + else: |
| 156 | + st.success("No errors detected.") |
192 | 157 |
|
193 | 158 | # TAB 3: MULTIMODAL |
194 | 159 | with tabs[3]: |
195 | | - st.subheader("Media Parts") |
196 | | - multi_data = safe_query( |
197 | | - f""" |
| 160 | + st.subheader("Media Parts") |
| 161 | + multi_data = safe_query(f""" |
198 | 162 | SELECT cp.mime_type, COUNT(*) as count |
199 | 163 | FROM {{table_ref}}, UNNEST(content_parts) as cp |
200 | 164 | WHERE {where_clause} GROUP BY 1 |
201 | | - """, |
202 | | - params=global_params, |
203 | | - ) |
204 | | - if not multi_data.empty: |
205 | | - fig = px.pie(multi_data, values="count", names="mime_type") |
206 | | - st.plotly_chart(fig, use_container_width=True) |
207 | | - else: |
208 | | - st.info("No multimodal content found.") |
| 165 | + """, params=global_params) |
| 166 | + if not multi_data.empty: |
| 167 | + fig = px.pie(multi_data, values='count', names='mime_type') |
| 168 | + st.plotly_chart(fig, use_container_width=True) |
| 169 | + else: |
| 170 | + st.info("No multimodal content found.") |
209 | 171 |
|
210 | 172 | # TAB 4: HITL |
211 | 173 | with tabs[4]: |
212 | | - st.subheader("Human-in-the-Loop Activity") |
213 | | - hitl_data = safe_query( |
214 | | - f""" |
| 174 | + st.subheader("Human-in-the-Loop Activity") |
| 175 | + hitl_data = safe_query(f""" |
215 | 176 | WITH enriched AS ({ENRICHED_EVENTS_CTE}) |
216 | 177 | SELECT event_type, COUNT(*) as count |
217 | 178 | FROM enriched WHERE event_family = 'hitl' |
218 | 179 | GROUP BY 1 |
219 | | - """, |
220 | | - params=global_params, |
221 | | - ) |
222 | | - if not hitl_data.empty: |
223 | | - st.table(hitl_data) |
224 | | - else: |
225 | | - st.info("No HITL events found.") |
| 180 | + """, params=global_params) |
| 181 | + if not hitl_data.empty: |
| 182 | + st.table(hitl_data) |
| 183 | + else: |
| 184 | + st.info("No HITL events found.") |
226 | 185 |
|
227 | 186 | # TAB 5: TRACE EXPLORER |
228 | 187 | with tabs[5]: |
229 | | - st.subheader("Session Explorer") |
230 | | - sessions = safe_query( |
231 | | - f""" |
| 188 | + st.subheader("Session Explorer") |
| 189 | + sessions = safe_query(f""" |
232 | 190 | SELECT session_id, agent, MIN(timestamp) as ts, |
233 | 191 | FORMAT_TIMESTAMP('%m-%d %H:%M', MIN(timestamp)) as f_ts |
234 | 192 | FROM {{table_ref}} WHERE {where_clause} GROUP BY 1, 2 ORDER BY ts DESC LIMIT 50 |
235 | | - """, |
236 | | - params=global_params, |
237 | | - ) |
| 193 | + """, params=global_params) |
238 | 194 |
|
239 | | - if not sessions.empty: |
240 | | - sessions["label"] = ( |
241 | | - sessions["f_ts"] |
242 | | - + " | " |
243 | | - + sessions["agent"].fillna("?") |
244 | | - + " | " |
245 | | - + sessions["session_id"].str[:8] |
246 | | - ) |
247 | | - label_map = dict(zip(sessions["label"], sessions["session_id"])) |
248 | | - sel_label = st.selectbox("Select Session", sessions["label"]) |
249 | | - sel_id = label_map[sel_label] |
| 195 | + if not sessions.empty: |
| 196 | + sessions['label'] = sessions['f_ts'] + " | " + sessions['agent'].fillna("?") + " | " + sessions['session_id'].str[:8] |
| 197 | + label_map = dict(zip(sessions['label'], sessions['session_id'])) |
| 198 | + sel_label = st.selectbox("Select Session", sessions['label']) |
| 199 | + sel_id = label_map[sel_label] |
250 | 200 |
|
251 | | - trace_detail = safe_query( |
252 | | - """ |
| 201 | + trace_detail = safe_query(""" |
253 | 202 | SELECT timestamp, event_type, agent, status, error_message |
254 | 203 | FROM {table_ref} WHERE session_id = @sid ORDER BY timestamp |
255 | | - """, |
256 | | - params=[bigquery.ScalarQueryParameter("sid", "STRING", sel_id)], |
257 | | - ) |
258 | | - st.dataframe(trace_detail, use_container_width=True) |
| 204 | + """, params=[bigquery.ScalarQueryParameter("sid", "STRING", sel_id)]) |
| 205 | + st.dataframe(trace_detail, use_container_width=True) |
0 commit comments