aster/session/
statistics.rs1use crate::session::{Session, SessionManager};
6use anyhow::Result;
7use serde::Serialize;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize)]
12pub struct SessionStatistics {
13 pub total_sessions: usize,
15 pub total_messages: usize,
17 pub total_tokens: i64,
19 pub average_messages: f64,
21 pub average_tokens: f64,
23 pub type_distribution: HashMap<String, usize>,
25 pub oldest_session: Option<SessionSummary>,
27 pub newest_session: Option<SessionSummary>,
29 pub most_active_session: Option<SessionSummary>,
31}
32
33#[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
57pub 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 let type_str = session.session_type.to_string();
89 *type_distribution.entry(type_str).or_insert(0) += 1;
90
91 if oldest.is_none() || session.created_at < oldest.unwrap().created_at {
93 oldest = Some(session);
94 }
95
96 if newest.is_none() || session.updated_at > newest.unwrap().updated_at {
98 newest = Some(session);
99 }
100
101 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
120pub async fn get_all_statistics() -> Result<SessionStatistics> {
122 let sessions = SessionManager::list_sessions().await?;
123 Ok(calculate_statistics(&sessions))
124}
125
126pub 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}