Skip to main content

aster/session/
statistics.rs

1//! Session Statistics Support
2//!
3//! Provides detailed statistics and reporting for sessions.
4
5use crate::session::{Session, SessionManager};
6use anyhow::Result;
7use serde::Serialize;
8use std::collections::HashMap;
9
10/// Detailed session statistics
11#[derive(Debug, Clone, Serialize)]
12pub struct SessionStatistics {
13    /// Total number of sessions
14    pub total_sessions: usize,
15    /// Total messages across all sessions
16    pub total_messages: usize,
17    /// Total tokens used
18    pub total_tokens: i64,
19    /// Average messages per session
20    pub average_messages: f64,
21    /// Average tokens per session
22    pub average_tokens: f64,
23    /// Session type distribution
24    pub type_distribution: HashMap<String, usize>,
25    /// Oldest session info
26    pub oldest_session: Option<SessionSummary>,
27    /// Newest session info
28    pub newest_session: Option<SessionSummary>,
29    /// Most active session (by message count)
30    pub most_active_session: Option<SessionSummary>,
31}
32
33/// Brief session summary for statistics
34#[derive(Debug, Clone, Serialize)]
35pub struct SessionSummary {
36    pub id: String,
37    pub name: String,
38    pub message_count: usize,
39    pub total_tokens: Option<i32>,
40    pub created_at: chrono::DateTime<chrono::Utc>,
41    pub updated_at: chrono::DateTime<chrono::Utc>,
42}
43
44impl From<&Session> for SessionSummary {
45    fn from(session: &Session) -> Self {
46        Self {
47            id: session.id.clone(),
48            name: session.name.clone(),
49            message_count: session.message_count,
50            total_tokens: session.total_tokens,
51            created_at: session.created_at,
52            updated_at: session.updated_at,
53        }
54    }
55}
56
57/// Calculate statistics from a list of sessions
58pub fn calculate_statistics(sessions: &[Session]) -> SessionStatistics {
59    let total_sessions = sessions.len();
60
61    if total_sessions == 0 {
62        return SessionStatistics {
63            total_sessions: 0,
64            total_messages: 0,
65            total_tokens: 0,
66            average_messages: 0.0,
67            average_tokens: 0.0,
68            type_distribution: HashMap::new(),
69            oldest_session: None,
70            newest_session: None,
71            most_active_session: None,
72        };
73    }
74
75    let mut total_messages = 0usize;
76    let mut total_tokens = 0i64;
77    let mut type_distribution: HashMap<String, usize> = HashMap::new();
78
79    let mut oldest: Option<&Session> = None;
80    let mut newest: Option<&Session> = None;
81    let mut most_active: Option<&Session> = None;
82
83    for session in sessions {
84        total_messages += session.message_count;
85        total_tokens += session.total_tokens.unwrap_or(0) as i64;
86
87        // Type distribution
88        let type_str = session.session_type.to_string();
89        *type_distribution.entry(type_str).or_insert(0) += 1;
90
91        // Track oldest
92        if oldest.is_none() || session.created_at < oldest.unwrap().created_at {
93            oldest = Some(session);
94        }
95
96        // Track newest
97        if newest.is_none() || session.updated_at > newest.unwrap().updated_at {
98            newest = Some(session);
99        }
100
101        // Track most active
102        if most_active.is_none() || session.message_count > most_active.unwrap().message_count {
103            most_active = Some(session);
104        }
105    }
106
107    SessionStatistics {
108        total_sessions,
109        total_messages,
110        total_tokens,
111        average_messages: total_messages as f64 / total_sessions as f64,
112        average_tokens: total_tokens as f64 / total_sessions as f64,
113        type_distribution,
114        oldest_session: oldest.map(SessionSummary::from),
115        newest_session: newest.map(SessionSummary::from),
116        most_active_session: most_active.map(SessionSummary::from),
117    }
118}
119
120/// Get statistics for all sessions
121pub async fn get_all_statistics() -> Result<SessionStatistics> {
122    let sessions = SessionManager::list_sessions().await?;
123    Ok(calculate_statistics(&sessions))
124}
125
126/// Generate a text report of session statistics
127pub fn generate_report(stats: &SessionStatistics) -> String {
128    let mut lines = Vec::new();
129
130    lines.push("=".repeat(60));
131    lines.push("SESSION REPORT".to_string());
132    lines.push("=".repeat(60));
133    lines.push(String::new());
134    lines.push(format!("Generated: {}", chrono::Utc::now().to_rfc3339()));
135    lines.push(String::new());
136
137    lines.push("Statistics:".to_string());
138    lines.push(format!("  Total Sessions: {}", stats.total_sessions));
139    lines.push(format!("  Total Messages: {}", stats.total_messages));
140    lines.push(format!("  Total Tokens: {}", stats.total_tokens));
141    lines.push(String::new());
142    lines.push(format!(
143        "  Average Messages per Session: {:.2}",
144        stats.average_messages
145    ));
146    lines.push(format!(
147        "  Average Tokens per Session: {:.2}",
148        stats.average_tokens
149    ));
150    lines.push(String::new());
151
152    if !stats.type_distribution.is_empty() {
153        lines.push("Session Type Distribution:".to_string());
154        for (type_name, count) in &stats.type_distribution {
155            let pct = (*count as f64 / stats.total_sessions as f64) * 100.0;
156            lines.push(format!("  {}: {} ({:.1}%)", type_name, count, pct));
157        }
158        lines.push(String::new());
159    }
160
161    if let Some(oldest) = &stats.oldest_session {
162        lines.push("Oldest Session:".to_string());
163        lines.push(format!("  ID: {}", oldest.id));
164        lines.push(format!("  Created: {}", oldest.created_at));
165        lines.push(String::new());
166    }
167
168    if let Some(newest) = &stats.newest_session {
169        lines.push("Newest Session:".to_string());
170        lines.push(format!("  ID: {}", newest.id));
171        lines.push(format!("  Updated: {}", newest.updated_at));
172        lines.push(String::new());
173    }
174
175    if let Some(active) = &stats.most_active_session {
176        lines.push("Most Active Session:".to_string());
177        lines.push(format!("  ID: {}", active.id));
178        lines.push(format!("  Messages: {}", active.message_count));
179        lines.push(String::new());
180    }
181
182    lines.push("=".repeat(60));
183
184    lines.join("\n")
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_empty_statistics() {
193        let stats = calculate_statistics(&[]);
194        assert_eq!(stats.total_sessions, 0);
195        assert_eq!(stats.total_messages, 0);
196        assert!(stats.oldest_session.is_none());
197    }
198
199    #[test]
200    fn test_generate_report() {
201        let stats = SessionStatistics {
202            total_sessions: 10,
203            total_messages: 100,
204            total_tokens: 50000,
205            average_messages: 10.0,
206            average_tokens: 5000.0,
207            type_distribution: HashMap::from([("user".to_string(), 10)]),
208            oldest_session: None,
209            newest_session: None,
210            most_active_session: None,
211        };
212
213        let report = generate_report(&stats);
214        assert!(report.contains("Total Sessions: 10"));
215        assert!(report.contains("Total Messages: 100"));
216    }
217}