use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryEvent {
pub id: Uuid,
pub session_id: Uuid,
pub timestamp: DateTime<Utc>,
pub query_text: String,
pub query_type: QueryType,
pub latency_ms: u64,
pub tool_calls: u32,
pub retrieval_count: u32,
pub result_count: u32,
pub quality_score: Option<f64>,
pub error: Option<QueryError>,
pub profile: Option<String>,
pub tools_used: Vec<String>,
}
impl QueryEvent {
pub fn new(session_id: Uuid, query_text: String) -> Self {
Self {
id: Uuid::new_v4(),
session_id,
timestamp: Utc::now(),
query_text,
query_type: QueryType::General,
latency_ms: 0,
tool_calls: 0,
retrieval_count: 0,
result_count: 0,
quality_score: None,
error: None,
profile: None,
tools_used: Vec::new(),
}
}
pub fn with_type(mut self, query_type: QueryType) -> Self {
self.query_type = query_type;
self
}
pub fn with_latency(mut self, latency_ms: u64) -> Self {
self.latency_ms = latency_ms;
self
}
pub fn with_tools(mut self, tools: Vec<String>) -> Self {
self.tool_calls = tools.len() as u32;
self.tools_used = tools;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum QueryType {
Search,
Reason,
Code,
#[default]
General,
File,
System,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryError {
pub category: ErrorCategory,
pub code: Option<String>,
pub recoverable: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCategory {
Network,
Api,
Parse,
Timeout,
NotFound,
Permission,
Internal,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedbackEvent {
pub id: Uuid,
pub session_id: Uuid,
pub query_id: Option<Uuid>,
pub timestamp: DateTime<Utc>,
pub feedback_type: FeedbackType,
pub rating: Option<u8>,
pub category: Option<FeedbackCategory>,
pub context_hash: Option<String>,
}
impl FeedbackEvent {
pub fn thumbs_up(session_id: Uuid, query_id: Option<Uuid>) -> Self {
Self {
id: Uuid::new_v4(),
session_id,
query_id,
timestamp: Utc::now(),
feedback_type: FeedbackType::ThumbsUp,
rating: None,
category: None,
context_hash: None,
}
}
pub fn thumbs_down(session_id: Uuid, query_id: Option<Uuid>) -> Self {
Self {
id: Uuid::new_v4(),
session_id,
query_id,
timestamp: Utc::now(),
feedback_type: FeedbackType::ThumbsDown,
rating: None,
category: None,
context_hash: None,
}
}
pub fn rating(session_id: Uuid, query_id: Option<Uuid>, rating: u8) -> Self {
Self {
id: Uuid::new_v4(),
session_id,
query_id,
timestamp: Utc::now(),
feedback_type: FeedbackType::Explicit,
rating: Some(rating.clamp(1, 5)),
category: None,
context_hash: None,
}
}
pub fn with_category(mut self, category: FeedbackCategory) -> Self {
self.category = Some(category);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FeedbackType {
ThumbsUp,
ThumbsDown,
Explicit,
Implicit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FeedbackCategory {
Accuracy,
Relevance,
Speed,
Format,
Completeness,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceEvent {
pub id: Uuid,
pub session_id: Uuid,
pub query_id: Option<Uuid>,
pub timestamp: DateTime<Utc>,
pub thinktool_name: String,
pub step_count: u32,
pub total_ms: u64,
pub avg_step_ms: Option<f64>,
pub coherence_score: Option<f64>,
pub depth_score: Option<f64>,
pub step_types: Vec<String>,
}
impl TraceEvent {
pub fn new(session_id: Uuid, thinktool_name: String) -> Self {
Self {
id: Uuid::new_v4(),
session_id,
query_id: None,
timestamp: Utc::now(),
thinktool_name,
step_count: 0,
total_ms: 0,
avg_step_ms: None,
coherence_score: None,
depth_score: None,
step_types: Vec::new(),
}
}
pub fn with_execution(mut self, step_count: u32, total_ms: u64) -> Self {
self.step_count = step_count;
self.total_ms = total_ms;
if step_count > 0 {
self.avg_step_ms = Some(total_ms as f64 / step_count as f64);
}
self
}
pub fn with_quality(mut self, coherence: f64, depth: f64) -> Self {
self.coherence_score = Some(coherence.clamp(0.0, 1.0));
self.depth_score = Some(depth.clamp(0.0, 1.0));
self
}
pub fn with_steps(mut self, step_types: Vec<String>) -> Self {
self.step_types = step_types;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolUsageEvent {
pub id: Uuid,
pub session_id: Uuid,
pub query_id: Option<Uuid>,
pub timestamp: DateTime<Utc>,
pub tool_name: String,
pub tool_category: ToolCategory,
pub execution_ms: u64,
pub success: bool,
pub error_type: Option<String>,
pub input_size: Option<u64>,
pub output_size: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum ToolCategory {
Search,
File,
Shell,
Mcp,
Reasoning,
Web,
#[default]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionEvent {
pub id: Uuid,
pub started_at: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
pub duration_ms: Option<u64>,
pub profile: Option<String>,
pub client_version: String,
pub os_family: String,
}
impl SessionEvent {
pub fn start(client_version: String) -> Self {
Self {
id: Uuid::new_v4(),
started_at: Utc::now(),
ended_at: None,
duration_ms: None,
profile: None,
client_version,
os_family: std::env::consts::OS.to_string(),
}
}
pub fn end(mut self) -> Self {
let now = Utc::now();
let duration = now.signed_duration_since(self.started_at);
self.ended_at = Some(now);
self.duration_ms = Some(duration.num_milliseconds().max(0) as u64);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_event_creation() {
let session_id = Uuid::new_v4();
let event = QueryEvent::new(session_id, "test query".to_string())
.with_type(QueryType::Search)
.with_latency(100);
assert_eq!(event.session_id, session_id);
assert_eq!(event.query_type, QueryType::Search);
assert_eq!(event.latency_ms, 100);
}
#[test]
fn test_feedback_rating_clamp() {
let session_id = Uuid::new_v4();
let event = FeedbackEvent::rating(session_id, None, 10);
assert_eq!(event.rating, Some(5));
let event = FeedbackEvent::rating(session_id, None, 0);
assert_eq!(event.rating, Some(1)); }
#[test]
fn test_session_lifecycle() {
let session = SessionEvent::start(crate::VERSION.to_string());
assert!(session.ended_at.is_none());
let ended = session.end();
assert!(ended.ended_at.is_some());
assert!(ended.duration_ms.is_some());
}
}