Spaces:
Sleeping
Sleeping
Update observability.py
Browse files- 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 |
-
|
114 |
-
|
115 |
-
|
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 |
-
|
157 |
-
|
158 |
-
|
159 |
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
else:
|
174 |
-
|
175 |
-
|
176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|