pvanand commited on
Commit
36ec72f
·
verified ·
1 Parent(s): 84d79ad

Update observability.py

Browse files
Files changed (1) hide show
  1. observability.py +153 -60
observability.py CHANGED
@@ -110,67 +110,160 @@ class LLMObservabilityManager:
110
  column_names = [description[0] for description in cursor.description]
111
  return [dict(zip(column_names, row)) for row in rows]
112
 
113
- ## OBSERVABILITY
114
- from uuid import uuid4
115
- import csv
116
- from io import StringIO
117
- from fastapi import APIRouter, HTTPException
118
- from pydantic import BaseModel
119
- from starlette.responses import StreamingResponse
120
-
121
-
122
-
123
- router = APIRouter(
124
- prefix="/observability",
125
- tags=["observability"]
126
- )
127
-
128
- class ObservationResponse(BaseModel):
129
- observations: List[Dict]
130
-
131
- def create_csv_response(observations: List[Dict]) -> StreamingResponse:
132
- def iter_csv(data):
133
- output = StringIO()
134
- writer = csv.DictWriter(output, fieldnames=data[0].keys() if data else [])
135
- writer.writeheader()
136
- for row in data:
137
- writer.writerow(row)
138
- output.seek(0)
139
- yield output.read()
140
-
141
- headers = {
142
- 'Content-Disposition': 'attachment; filename="observations.csv"'
143
- }
144
- return StreamingResponse(iter_csv(observations), media_type="text/csv", headers=headers)
145
-
146
-
147
- @router.get("/last-observations/{limit}")
148
- async def get_last_observations(limit: int = 10, format: str = "json"):
149
- observability_manager = LLMObservabilityManager()
150
-
151
- try:
152
- # Get all observations, sorted by created_at in descending order
153
- all_observations = observability_manager.get_observations()
154
- all_observations.sort(key=lambda x: x['created_at'], reverse=True)
155
 
156
- # Get the last conversation_id
157
- if all_observations:
158
- last_conversation_id = all_observations[0]['conversation_id']
159
 
160
- # Filter observations for the last conversation
161
- last_conversation_observations = [
162
- obs for obs in all_observations
163
- if obs['conversation_id'] == last_conversation_id
164
- ][:limit]
165
 
166
- if format.lower() == "csv":
167
- return create_csv_response(last_conversation_observations)
168
- else:
169
- return ObservationResponse(observations=last_conversation_observations)
170
- else:
171
- if format.lower() == "csv":
172
- return create_csv_response([])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  else:
174
- return ObservationResponse(observations=[])
175
- except Exception as e:
176
- raise HTTPException(status_code=500, detail=f"Failed to retrieve observations: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  column_names = [description[0] for description in cursor.description]
111
  return [dict(zip(column_names, row)) for row in rows]
112
 
113
+ def get_dashboard_statistics(self, days: Optional[int] = None, time_series_interval: str = 'day') -> Dict[str, Any]:
114
+ """
115
+ Get statistical metrics for LLM usage dashboard with time series data.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ Args:
118
+ days (int, optional): Number of days to look back. If None, returns all-time statistics
119
+ time_series_interval (str): Interval for time series data ('hour', 'day', 'week', 'month')
120
 
121
+ Returns:
122
+ Dict containing dashboard statistics and time series data
123
+ """
124
+ with sqlite3.connect(self.db_path) as conn:
125
+ cursor = conn.cursor()
126
 
127
+ # Build time filter
128
+ time_filter = ""
129
+ if days is not None:
130
+ time_filter = f"WHERE created_at >= datetime('now', '-{days} days')"
131
+
132
+ # Get general statistics
133
+ cursor.execute(f"""
134
+ SELECT
135
+ COUNT(*) as total_requests,
136
+ COUNT(DISTINCT conversation_id) as unique_conversations,
137
+ COUNT(DISTINCT user) as unique_users,
138
+ SUM(total_tokens) as total_tokens,
139
+ SUM(cost) as total_cost,
140
+ AVG(latency) as avg_latency,
141
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count
142
+ FROM llm_observations
143
+ {time_filter}
144
+ """)
145
+ general_stats = dict(zip([col[0] for col in cursor.description], cursor.fetchone()))
146
+
147
+ # Get model distribution
148
+ cursor.execute(f"""
149
+ SELECT model, COUNT(*) as count
150
+ FROM llm_observations
151
+ {time_filter}
152
+ GROUP BY model
153
+ ORDER BY count DESC
154
+ """)
155
+ model_distribution = {row[0]: row[1] for row in cursor.fetchall()}
156
+
157
+ # Get average tokens per request
158
+ cursor.execute(f"""
159
+ SELECT
160
+ AVG(prompt_tokens) as avg_prompt_tokens,
161
+ AVG(completion_tokens) as avg_completion_tokens
162
+ FROM llm_observations
163
+ {time_filter}
164
+ """)
165
+ token_averages = dict(zip([col[0] for col in cursor.description], cursor.fetchone()))
166
+
167
+ # Get top users by request count
168
+ cursor.execute(f"""
169
+ SELECT user, COUNT(*) as request_count,
170
+ SUM(total_tokens) as total_tokens,
171
+ SUM(cost) as total_cost
172
+ FROM llm_observations
173
+ {time_filter}
174
+ GROUP BY user
175
+ ORDER BY request_count DESC
176
+ LIMIT 5
177
+ """)
178
+ top_users = [
179
+ {
180
+ "user": row[0],
181
+ "request_count": row[1],
182
+ "total_tokens": row[2],
183
+ "total_cost": round(row[3], 2)
184
+ }
185
+ for row in cursor.fetchall()
186
+ ]
187
+
188
+ # Get time series data
189
+ time_series_format = {
190
+ 'hour': "%Y-%m-%d %H:00:00",
191
+ 'day': "%Y-%m-%d",
192
+ 'week': "%Y-%W",
193
+ 'month': "%Y-%m"
194
+ }
195
+
196
+ format_string = time_series_format[time_series_interval]
197
+
198
+ cursor.execute(f"""
199
+ SELECT
200
+ strftime('{format_string}', created_at) as time_bucket,
201
+ COUNT(*) as request_count,
202
+ SUM(total_tokens) as total_tokens,
203
+ SUM(cost) as total_cost,
204
+ AVG(latency) as avg_latency,
205
+ COUNT(DISTINCT user) as unique_users,
206
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count
207
+ FROM llm_observations
208
+ {time_filter}
209
+ GROUP BY time_bucket
210
+ ORDER BY time_bucket
211
+ """)
212
+
213
+ time_series = [
214
+ {
215
+ "timestamp": row[0],
216
+ "request_count": row[1],
217
+ "total_tokens": row[2],
218
+ "total_cost": round(row[3], 2),
219
+ "avg_latency": round(row[4], 2),
220
+ "unique_users": row[5],
221
+ "error_count": row[6]
222
+ }
223
+ for row in cursor.fetchall()
224
+ ]
225
+
226
+ # Calculate usage trends (percentage change)
227
+ if len(time_series) >= 2:
228
+ current = time_series[-1]
229
+ previous = time_series[-2]
230
+ trends = {
231
+ "request_trend": calculate_percentage_change(
232
+ previous["request_count"], current["request_count"]),
233
+ "cost_trend": calculate_percentage_change(
234
+ previous["total_cost"], current["total_cost"]),
235
+ "token_trend": calculate_percentage_change(
236
+ previous["total_tokens"], current["total_tokens"])
237
+ }
238
  else:
239
+ trends = {
240
+ "request_trend": 0,
241
+ "cost_trend": 0,
242
+ "token_trend": 0
243
+ }
244
+
245
+ return {
246
+ "general_stats": {
247
+ "total_requests": general_stats["total_requests"],
248
+ "unique_conversations": general_stats["unique_conversations"],
249
+ "unique_users": general_stats["unique_users"],
250
+ "total_tokens": general_stats["total_tokens"],
251
+ "total_cost": round(general_stats["total_cost"], 2),
252
+ "avg_latency": round(general_stats["avg_latency"], 2),
253
+ "error_rate": round(general_stats["error_count"] / general_stats["total_requests"] * 100, 2)
254
+ },
255
+ "model_distribution": model_distribution,
256
+ "token_metrics": {
257
+ "avg_prompt_tokens": round(token_averages["avg_prompt_tokens"], 2),
258
+ "avg_completion_tokens": round(token_averages["avg_completion_tokens"], 2)
259
+ },
260
+ "top_users": top_users,
261
+ "time_series": time_series,
262
+ "trends": trends
263
+ }
264
+
265
+ def calculate_percentage_change(old_value: float, new_value: float) -> float:
266
+ """Calculate percentage change between two values."""
267
+ if old_value == 0:
268
+ return 100 if new_value > 0 else 0
269
+ return round(((new_value - old_value) / old_value) * 100, 2)