jdesiree commited on
Commit
94a931c
·
verified ·
1 Parent(s): cd76ec5

Create metrics.py

Browse files

Added a metrics recording system

Files changed (1) hide show
  1. metrics.py +215 -0
metrics.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import json
3
+ from datetime import datetime
4
+ from dataclasses import dataclass
5
+ from typing import List, Optional
6
+ import threading
7
+
8
+ @dataclass
9
+ class InteractionMetric:
10
+ """Single interaction metrics"""
11
+ timestamp: str
12
+ mode: str
13
+ query_length: int
14
+ response_time: float
15
+ input_tokens: int
16
+ output_tokens: int
17
+ total_tokens: int
18
+ streaming_chunks: int
19
+ provider_latency: float
20
+ error_occurred: bool
21
+ error_message: Optional[str] = None
22
+
23
+ class EduBotMetrics:
24
+ """Metrics collection and analysis for EduBot"""
25
+
26
+ def __init__(self, save_file: str = "edubot_metrics.json"):
27
+ self.metrics: List[InteractionMetric] = []
28
+ self.save_file = save_file
29
+ self.lock = threading.Lock() # Thread-safe for concurrent requests
30
+
31
+ # Load existing metrics if file exists
32
+ self.load_metrics()
33
+
34
+ def start_timing(self) -> dict:
35
+ """Start timing an interaction - returns timing context"""
36
+ return {
37
+ 'start_time': time.time(),
38
+ 'provider_start': None,
39
+ 'chunk_count': 0,
40
+ 'chunks_timing': []
41
+ }
42
+
43
+ def mark_provider_start(self, timing_context: dict):
44
+ """Mark when provider API call starts"""
45
+ timing_context['provider_start'] = time.time()
46
+
47
+ def mark_provider_end(self, timing_context: dict):
48
+ """Mark when provider API call ends and calculate latency"""
49
+ if timing_context['provider_start']:
50
+ timing_context['provider_latency'] = time.time() - timing_context['provider_start']
51
+ else:
52
+ timing_context['provider_latency'] = 0.0
53
+
54
+ def record_chunk(self, timing_context: dict):
55
+ """Record a streaming chunk"""
56
+ timing_context['chunk_count'] += 1
57
+ timing_context['chunks_timing'].append(time.time())
58
+
59
+ def count_tokens(self, text: str) -> int:
60
+ """Simple token counting (approximation)"""
61
+ # Rough approximation: 1 token ≈ 4 characters for most models
62
+ return len(text) // 4
63
+
64
+ def log_interaction(self,
65
+ mode: str,
66
+ query: str,
67
+ response: str,
68
+ timing_context: dict,
69
+ error_occurred: bool = False,
70
+ error_message: str = None):
71
+ """Log a complete interaction with all metrics"""
72
+
73
+ end_time = time.time()
74
+ response_time = end_time - timing_context['start_time']
75
+
76
+ # Count tokens
77
+ input_tokens = self.count_tokens(query)
78
+ output_tokens = self.count_tokens(response)
79
+ total_tokens = input_tokens + output_tokens
80
+
81
+ # Get provider latency
82
+ provider_latency = timing_context.get('provider_latency', 0.0)
83
+
84
+ # Create metric record
85
+ metric = InteractionMetric(
86
+ timestamp=datetime.now().isoformat(),
87
+ mode=mode,
88
+ query_length=len(query),
89
+ response_time=response_time,
90
+ input_tokens=input_tokens,
91
+ output_tokens=output_tokens,
92
+ total_tokens=total_tokens,
93
+ streaming_chunks=timing_context['chunk_count'],
94
+ provider_latency=provider_latency,
95
+ error_occurred=error_occurred,
96
+ error_message=error_message
97
+ )
98
+
99
+ # Thread-safe append
100
+ with self.lock:
101
+ self.metrics.append(metric)
102
+
103
+ # Auto-save every 10 interactions
104
+ if len(self.metrics) % 10 == 0:
105
+ self.save_metrics()
106
+
107
+ def save_metrics(self):
108
+ """Save metrics to JSON file"""
109
+ try:
110
+ with self.lock:
111
+ data = [
112
+ {
113
+ 'timestamp': m.timestamp,
114
+ 'mode': m.mode,
115
+ 'query_length': m.query_length,
116
+ 'response_time': m.response_time,
117
+ 'input_tokens': m.input_tokens,
118
+ 'output_tokens': m.output_tokens,
119
+ 'total_tokens': m.total_tokens,
120
+ 'streaming_chunks': m.streaming_chunks,
121
+ 'provider_latency': m.provider_latency,
122
+ 'error_occurred': m.error_occurred,
123
+ 'error_message': m.error_message
124
+ }
125
+ for m in self.metrics
126
+ ]
127
+
128
+ with open(self.save_file, 'w') as f:
129
+ json.dump(data, f, indent=2)
130
+
131
+ except Exception as e:
132
+ print(f"Error saving metrics: {e}")
133
+
134
+ def load_metrics(self):
135
+ """Load existing metrics from file"""
136
+ try:
137
+ with open(self.save_file, 'r') as f:
138
+ data = json.load(f)
139
+
140
+ self.metrics = [
141
+ InteractionMetric(**item) for item in data
142
+ ]
143
+
144
+ except FileNotFoundError:
145
+ # File doesn't exist yet, start fresh
146
+ self.metrics = []
147
+ except Exception as e:
148
+ print(f"Error loading metrics: {e}")
149
+ self.metrics = []
150
+
151
+ def get_summary_stats(self) -> dict:
152
+ """Get summary statistics"""
153
+ if not self.metrics:
154
+ return {"message": "No metrics recorded yet"}
155
+
156
+ response_times = [m.response_time for m in self.metrics]
157
+ provider_latencies = [m.provider_latency for m in self.metrics]
158
+ token_counts = [m.total_tokens for m in self.metrics]
159
+ chunk_counts = [m.streaming_chunks for m in self.metrics]
160
+ error_count = sum(1 for m in self.metrics if m.error_occurred)
161
+
162
+ return {
163
+ "total_interactions": len(self.metrics),
164
+ "error_rate": (error_count / len(self.metrics)) * 100,
165
+ "avg_response_time": sum(response_times) / len(response_times),
166
+ "avg_provider_latency": sum(provider_latencies) / len(provider_latencies),
167
+ "avg_tokens": sum(token_counts) / len(token_counts),
168
+ "avg_chunks": sum(chunk_counts) / len(chunk_counts),
169
+ "mode_distribution": self._get_mode_distribution()
170
+ }
171
+
172
+ def _get_mode_distribution(self) -> dict:
173
+ """Get distribution of modes used"""
174
+ mode_counts = {}
175
+ for metric in self.metrics:
176
+ mode_counts[metric.mode] = mode_counts.get(metric.mode, 0) + 1
177
+
178
+ total = len(self.metrics)
179
+ return {mode: (count / total) * 100 for mode, count in mode_counts.items()}
180
+
181
+ def export_csv(self, filename: str = None):
182
+ """Export metrics to CSV format"""
183
+ if filename is None:
184
+ filename = f"edubot_metrics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
185
+
186
+ try:
187
+ import csv
188
+
189
+ with open(filename, 'w', newline='') as csvfile:
190
+ fieldnames = [
191
+ 'timestamp', 'mode', 'query_length', 'response_time',
192
+ 'input_tokens', 'output_tokens', 'total_tokens',
193
+ 'streaming_chunks', 'provider_latency', 'error_occurred'
194
+ ]
195
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
196
+ writer.writeheader()
197
+
198
+ for metric in self.metrics:
199
+ writer.writerow({
200
+ 'timestamp': metric.timestamp,
201
+ 'mode': metric.mode,
202
+ 'query_length': metric.query_length,
203
+ 'response_time': metric.response_time,
204
+ 'input_tokens': metric.input_tokens,
205
+ 'output_tokens': metric.output_tokens,
206
+ 'total_tokens': metric.total_tokens,
207
+ 'streaming_chunks': metric.streaming_chunks,
208
+ 'provider_latency': metric.provider_latency,
209
+ 'error_occurred': metric.error_occurred
210
+ })
211
+
212
+ return f"Metrics exported to {filename}"
213
+
214
+ except Exception as e:
215
+ return f"Error exporting CSV: {e}"