reasonkit/telemetry/
events.rs

1//! Telemetry Event Types
2//!
3//! Defines all event types that can be recorded by the telemetry system.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// Query event - recorded for each user query
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct QueryEvent {
12    /// Event ID
13    pub id: Uuid,
14    /// Session ID
15    pub session_id: Uuid,
16    /// Timestamp
17    pub timestamp: DateTime<Utc>,
18    /// Original query text (will be hashed, not stored)
19    pub query_text: String,
20    /// Query type classification
21    pub query_type: QueryType,
22    /// Execution latency in milliseconds
23    pub latency_ms: u64,
24    /// Number of tool calls made
25    pub tool_calls: u32,
26    /// Number of documents retrieved
27    pub retrieval_count: u32,
28    /// Result count
29    pub result_count: u32,
30    /// Quality score (0.0 - 1.0)
31    pub quality_score: Option<f64>,
32    /// Error occurred
33    pub error: Option<QueryError>,
34    /// Reasoning profile used
35    pub profile: Option<String>,
36    /// Tools used (list of tool names)
37    pub tools_used: Vec<String>,
38}
39
40impl QueryEvent {
41    /// Create a new query event
42    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    /// Set query type
61    pub fn with_type(mut self, query_type: QueryType) -> Self {
62        self.query_type = query_type;
63        self
64    }
65
66    /// Set latency
67    pub fn with_latency(mut self, latency_ms: u64) -> Self {
68        self.latency_ms = latency_ms;
69        self
70    }
71
72    /// Set tools used
73    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/// Query type classification
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83#[derive(Default)]
84pub enum QueryType {
85    /// Search/retrieval query
86    Search,
87    /// Reasoning/analysis query
88    Reason,
89    /// Code generation/editing
90    Code,
91    /// General conversation
92    #[default]
93    General,
94    /// File operations
95    File,
96    /// System commands
97    System,
98}
99
100/// Query error information
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct QueryError {
103    /// Error category
104    pub category: ErrorCategory,
105    /// Error code (if applicable)
106    pub code: Option<String>,
107    /// Is recoverable
108    pub recoverable: bool,
109}
110
111/// Error category classification
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum ErrorCategory {
115    /// Network/connectivity error
116    Network,
117    /// API error (rate limit, auth, etc.)
118    Api,
119    /// Parsing error
120    Parse,
121    /// Timeout
122    Timeout,
123    /// Resource not found
124    NotFound,
125    /// Permission denied
126    Permission,
127    /// Internal error
128    Internal,
129    /// Unknown error
130    Unknown,
131}
132
133/// Feedback event - user feedback on results
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct FeedbackEvent {
136    /// Event ID
137    pub id: Uuid,
138    /// Session ID
139    pub session_id: Uuid,
140    /// Related query ID (optional)
141    pub query_id: Option<Uuid>,
142    /// Timestamp
143    pub timestamp: DateTime<Utc>,
144    /// Feedback type
145    pub feedback_type: FeedbackType,
146    /// Rating (1-5, if explicit)
147    pub rating: Option<u8>,
148    /// Category of feedback
149    pub category: Option<FeedbackCategory>,
150    /// Context hash (for dedup)
151    pub context_hash: Option<String>,
152}
153
154impl FeedbackEvent {
155    /// Create thumbs up feedback
156    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    /// Create thumbs down feedback
170    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    /// Create explicit rating feedback
184    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    /// Set feedback category
198    pub fn with_category(mut self, category: FeedbackCategory) -> Self {
199        self.category = Some(category);
200        self
201    }
202}
203
204/// Feedback type
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "snake_case")]
207pub enum FeedbackType {
208    /// Positive quick feedback
209    ThumbsUp,
210    /// Negative quick feedback
211    ThumbsDown,
212    /// Explicit rating (1-5)
213    Explicit,
214    /// Implicit (inferred from behavior)
215    Implicit,
216}
217
218/// Feedback category
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "snake_case")]
221pub enum FeedbackCategory {
222    /// Accuracy of response
223    Accuracy,
224    /// Relevance to query
225    Relevance,
226    /// Speed of response
227    Speed,
228    /// Format/presentation
229    Format,
230    /// Completeness
231    Completeness,
232    /// Other
233    Other,
234}
235
236/// Reasoning trace event - ThinkTool execution trace
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct TraceEvent {
239    /// Event ID
240    pub id: Uuid,
241    /// Session ID
242    pub session_id: Uuid,
243    /// Related query ID (optional)
244    pub query_id: Option<Uuid>,
245    /// Timestamp
246    pub timestamp: DateTime<Utc>,
247    /// ThinkTool name
248    pub thinktool_name: String,
249    /// Number of reasoning steps
250    pub step_count: u32,
251    /// Total execution time in milliseconds
252    pub total_ms: u64,
253    /// Average step time in milliseconds
254    pub avg_step_ms: Option<f64>,
255    /// Coherence score (0.0 - 1.0)
256    pub coherence_score: Option<f64>,
257    /// Depth score (0.0 - 1.0)
258    pub depth_score: Option<f64>,
259    /// Step types (for analysis)
260    pub step_types: Vec<String>,
261}
262
263impl TraceEvent {
264    /// Create a new trace event
265    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    /// Set execution metrics
282    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    /// Set quality metrics
292    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    /// Set step types
299    pub fn with_steps(mut self, step_types: Vec<String>) -> Self {
300        self.step_types = step_types;
301        self
302    }
303}
304
305/// Tool usage event
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ToolUsageEvent {
308    /// Event ID
309    pub id: Uuid,
310    /// Session ID
311    pub session_id: Uuid,
312    /// Related query ID (optional)
313    pub query_id: Option<Uuid>,
314    /// Timestamp
315    pub timestamp: DateTime<Utc>,
316    /// Tool name
317    pub tool_name: String,
318    /// Tool category
319    pub tool_category: ToolCategory,
320    /// Execution time in milliseconds
321    pub execution_ms: u64,
322    /// Success flag
323    pub success: bool,
324    /// Error type (if failed)
325    pub error_type: Option<String>,
326    /// Input size in bytes
327    pub input_size: Option<u64>,
328    /// Output size in bytes
329    pub output_size: Option<u64>,
330}
331
332/// Tool category
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335#[derive(Default)]
336pub enum ToolCategory {
337    /// Search/retrieval tools
338    Search,
339    /// File system operations
340    File,
341    /// Shell/command execution
342    Shell,
343    /// MCP server tools
344    Mcp,
345    /// Reasoning tools
346    Reasoning,
347    /// Web/network tools
348    Web,
349    /// Other
350    #[default]
351    Other,
352}
353
354/// Session event
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct SessionEvent {
357    /// Session ID
358    pub id: Uuid,
359    /// Start timestamp
360    pub started_at: DateTime<Utc>,
361    /// End timestamp (None if still active)
362    pub ended_at: Option<DateTime<Utc>>,
363    /// Duration in milliseconds
364    pub duration_ms: Option<u64>,
365    /// Reasoning profile used
366    pub profile: Option<String>,
367    /// Client version
368    pub client_version: String,
369    /// OS family (sanitized)
370    pub os_family: String,
371}
372
373impl SessionEvent {
374    /// Create a new session
375    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    /// End the session
388    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)); // Clamped to max
418
419        let event = FeedbackEvent::rating(session_id, None, 0);
420        assert_eq!(event.rating, Some(1)); // Clamped to min
421    }
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}