1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct QueryEvent {
12 pub id: Uuid,
14 pub session_id: Uuid,
16 pub timestamp: DateTime<Utc>,
18 pub query_text: String,
20 pub query_type: QueryType,
22 pub latency_ms: u64,
24 pub tool_calls: u32,
26 pub retrieval_count: u32,
28 pub result_count: u32,
30 pub quality_score: Option<f64>,
32 pub error: Option<QueryError>,
34 pub profile: Option<String>,
36 pub tools_used: Vec<String>,
38}
39
40impl QueryEvent {
41 pub fn new(session_id: Uuid, query_text: String) -> Self {
43 Self {
44 id: Uuid::new_v4(),
45 session_id,
46 timestamp: Utc::now(),
47 query_text,
48 query_type: QueryType::General,
49 latency_ms: 0,
50 tool_calls: 0,
51 retrieval_count: 0,
52 result_count: 0,
53 quality_score: None,
54 error: None,
55 profile: None,
56 tools_used: Vec::new(),
57 }
58 }
59
60 pub fn with_type(mut self, query_type: QueryType) -> Self {
62 self.query_type = query_type;
63 self
64 }
65
66 pub fn with_latency(mut self, latency_ms: u64) -> Self {
68 self.latency_ms = latency_ms;
69 self
70 }
71
72 pub fn with_tools(mut self, tools: Vec<String>) -> Self {
74 self.tool_calls = tools.len() as u32;
75 self.tools_used = tools;
76 self
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83#[derive(Default)]
84pub enum QueryType {
85 Search,
87 Reason,
89 Code,
91 #[default]
93 General,
94 File,
96 System,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct QueryError {
103 pub category: ErrorCategory,
105 pub code: Option<String>,
107 pub recoverable: bool,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum ErrorCategory {
115 Network,
117 Api,
119 Parse,
121 Timeout,
123 NotFound,
125 Permission,
127 Internal,
129 Unknown,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct FeedbackEvent {
136 pub id: Uuid,
138 pub session_id: Uuid,
140 pub query_id: Option<Uuid>,
142 pub timestamp: DateTime<Utc>,
144 pub feedback_type: FeedbackType,
146 pub rating: Option<u8>,
148 pub category: Option<FeedbackCategory>,
150 pub context_hash: Option<String>,
152}
153
154impl FeedbackEvent {
155 pub fn thumbs_up(session_id: Uuid, query_id: Option<Uuid>) -> Self {
157 Self {
158 id: Uuid::new_v4(),
159 session_id,
160 query_id,
161 timestamp: Utc::now(),
162 feedback_type: FeedbackType::ThumbsUp,
163 rating: None,
164 category: None,
165 context_hash: None,
166 }
167 }
168
169 pub fn thumbs_down(session_id: Uuid, query_id: Option<Uuid>) -> Self {
171 Self {
172 id: Uuid::new_v4(),
173 session_id,
174 query_id,
175 timestamp: Utc::now(),
176 feedback_type: FeedbackType::ThumbsDown,
177 rating: None,
178 category: None,
179 context_hash: None,
180 }
181 }
182
183 pub fn rating(session_id: Uuid, query_id: Option<Uuid>, rating: u8) -> Self {
185 Self {
186 id: Uuid::new_v4(),
187 session_id,
188 query_id,
189 timestamp: Utc::now(),
190 feedback_type: FeedbackType::Explicit,
191 rating: Some(rating.clamp(1, 5)),
192 category: None,
193 context_hash: None,
194 }
195 }
196
197 pub fn with_category(mut self, category: FeedbackCategory) -> Self {
199 self.category = Some(category);
200 self
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "snake_case")]
207pub enum FeedbackType {
208 ThumbsUp,
210 ThumbsDown,
212 Explicit,
214 Implicit,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "snake_case")]
221pub enum FeedbackCategory {
222 Accuracy,
224 Relevance,
226 Speed,
228 Format,
230 Completeness,
232 Other,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct TraceEvent {
239 pub id: Uuid,
241 pub session_id: Uuid,
243 pub query_id: Option<Uuid>,
245 pub timestamp: DateTime<Utc>,
247 pub thinktool_name: String,
249 pub step_count: u32,
251 pub total_ms: u64,
253 pub avg_step_ms: Option<f64>,
255 pub coherence_score: Option<f64>,
257 pub depth_score: Option<f64>,
259 pub step_types: Vec<String>,
261}
262
263impl TraceEvent {
264 pub fn new(session_id: Uuid, thinktool_name: String) -> Self {
266 Self {
267 id: Uuid::new_v4(),
268 session_id,
269 query_id: None,
270 timestamp: Utc::now(),
271 thinktool_name,
272 step_count: 0,
273 total_ms: 0,
274 avg_step_ms: None,
275 coherence_score: None,
276 depth_score: None,
277 step_types: Vec::new(),
278 }
279 }
280
281 pub fn with_execution(mut self, step_count: u32, total_ms: u64) -> Self {
283 self.step_count = step_count;
284 self.total_ms = total_ms;
285 if step_count > 0 {
286 self.avg_step_ms = Some(total_ms as f64 / step_count as f64);
287 }
288 self
289 }
290
291 pub fn with_quality(mut self, coherence: f64, depth: f64) -> Self {
293 self.coherence_score = Some(coherence.clamp(0.0, 1.0));
294 self.depth_score = Some(depth.clamp(0.0, 1.0));
295 self
296 }
297
298 pub fn with_steps(mut self, step_types: Vec<String>) -> Self {
300 self.step_types = step_types;
301 self
302 }
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ToolUsageEvent {
308 pub id: Uuid,
310 pub session_id: Uuid,
312 pub query_id: Option<Uuid>,
314 pub timestamp: DateTime<Utc>,
316 pub tool_name: String,
318 pub tool_category: ToolCategory,
320 pub execution_ms: u64,
322 pub success: bool,
324 pub error_type: Option<String>,
326 pub input_size: Option<u64>,
328 pub output_size: Option<u64>,
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335#[derive(Default)]
336pub enum ToolCategory {
337 Search,
339 File,
341 Shell,
343 Mcp,
345 Reasoning,
347 Web,
349 #[default]
351 Other,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct SessionEvent {
357 pub id: Uuid,
359 pub started_at: DateTime<Utc>,
361 pub ended_at: Option<DateTime<Utc>>,
363 pub duration_ms: Option<u64>,
365 pub profile: Option<String>,
367 pub client_version: String,
369 pub os_family: String,
371}
372
373impl SessionEvent {
374 pub fn start(client_version: String) -> Self {
376 Self {
377 id: Uuid::new_v4(),
378 started_at: Utc::now(),
379 ended_at: None,
380 duration_ms: None,
381 profile: None,
382 client_version,
383 os_family: std::env::consts::OS.to_string(),
384 }
385 }
386
387 pub fn end(mut self) -> Self {
389 let now = Utc::now();
390 let duration = now.signed_duration_since(self.started_at);
391 self.ended_at = Some(now);
392 self.duration_ms = Some(duration.num_milliseconds().max(0) as u64);
393 self
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_query_event_creation() {
403 let session_id = Uuid::new_v4();
404 let event = QueryEvent::new(session_id, "test query".to_string())
405 .with_type(QueryType::Search)
406 .with_latency(100);
407
408 assert_eq!(event.session_id, session_id);
409 assert_eq!(event.query_type, QueryType::Search);
410 assert_eq!(event.latency_ms, 100);
411 }
412
413 #[test]
414 fn test_feedback_rating_clamp() {
415 let session_id = Uuid::new_v4();
416 let event = FeedbackEvent::rating(session_id, None, 10);
417 assert_eq!(event.rating, Some(5)); let event = FeedbackEvent::rating(session_id, None, 0);
420 assert_eq!(event.rating, Some(1)); }
422
423 #[test]
424 fn test_session_lifecycle() {
425 let session = SessionEvent::start(crate::VERSION.to_string());
426 assert!(session.ended_at.is_none());
427
428 let ended = session.end();
429 assert!(ended.ended_at.is_some());
430 assert!(ended.duration_ms.is_some());
431 }
432}