use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::core::TokenUsage;
use crate::storage::Message;
pub mod monitor;
pub mod summarizer;
pub mod service;
pub use monitor::TokenMonitor;
pub use summarizer::ConversationSummarizer;
pub use service::SummarizationServiceImpl;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SummarizationConfig {
pub enabled: bool,
pub token_threshold: u32,
pub message_threshold: u32,
pub context_window_percentage: f32,
pub summarizer_model: String,
pub summary_max_tokens: u32,
pub preserve_recent_messages: u32,
}
impl Default for SummarizationConfig {
fn default() -> Self {
Self {
enabled: true,
token_threshold: 100000,
message_threshold: 50,
context_window_percentage: 0.95,
summarizer_model: "gpt-4".to_string(),
summary_max_tokens: 1000,
preserve_recent_messages: 3,
}
}
}
#[derive(Debug, Clone)]
pub struct ConversationMetrics {
pub total_tokens: u32,
pub message_count: u32,
pub context_window: u32,
pub usage_percentage: f32,
pub needs_summarization: bool,
}
#[derive(Debug, Clone)]
pub struct ConversationSummary {
pub original_session_id: String,
pub summary_text: String,
pub preserved_messages: Vec<Message>,
pub summary_tokens: u32,
pub summarizer_model: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, thiserror::Error)]
pub enum SummarizationError {
#[error("Session not found: {0}")]
SessionNotFound(String),
#[error("Storage error: {0}")]
StorageError(String),
#[error("Provider error: {0}")]
ProviderError(String),
#[error("Session manager error: {0}")]
SessionManagerError(String),
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Summarization disabled")]
Disabled,
#[error("Empty conversation")]
EmptyConversation,
#[error("Summary generation failed: {0}")]
SummaryGenerationFailed(String),
}
#[async_trait]
pub trait SummarizationService: Send + Sync {
async fn monitor_session(&self, session_id: &str) -> Result<ConversationMetrics, SummarizationError>;
async fn needs_summarization(&self, session_id: &str) -> Result<bool, SummarizationError>;
async fn summarize_conversation(&self, session_id: &str) -> Result<ConversationSummary, SummarizationError>;
async fn create_continuation_session(&self, summary: ConversationSummary) -> Result<String, SummarizationError>;
async fn auto_summarize_if_needed(&self, session_id: &str) -> Result<Option<String>, SummarizationError>;
async fn update_token_usage(&self, session_id: &str, usage: TokenUsage) -> Result<(), SummarizationError>;
}
pub mod utils {
use super::*;
pub fn get_model_context_window(model_id: &str) -> u32 {
match model_id {
"gpt-4" | "gpt-4-0613" => 8192,
"gpt-4-32k" | "gpt-4-32k-0613" => 32768,
"gpt-4-turbo" | "gpt-4-turbo-preview" => 128000,
"gpt-4o" | "gpt-4o-mini" => 128000,
"gpt-3.5-turbo" | "gpt-3.5-turbo-0125" => 16385,
"claude-3-5-sonnet-20241022" | "claude-3-5-sonnet-20240620" => 200000,
"claude-3-opus-20240229" => 200000,
"claude-3-sonnet-20240229" => 200000,
"claude-3-haiku-20240307" => 200000,
"gemini-1.5-pro" | "gemini-1.5-pro-latest" => 2000000,
"gemini-1.5-flash" | "gemini-1.5-flash-latest" => 1000000,
"gemini-pro" => 32768,
_ if model_id.contains("llama") => 32768,
_ if model_id.contains("mistral") => 32768,
_ if model_id.contains("mixtral") => 32768,
_ if model_id.contains("codellama") => 16384,
_ => 4096,
}
}
pub fn calculate_usage_percentage(total_tokens: u32, context_window: u32) -> f32 {
if context_window == 0 {
return 0.0;
}
total_tokens as f32 / context_window as f32
}
pub fn should_summarize(
metrics: &ConversationMetrics,
config: &SummarizationConfig,
) -> bool {
if !config.enabled {
return false;
}
metrics.usage_percentage >= config.context_window_percentage
|| metrics.message_count >= config.message_threshold
|| metrics.total_tokens >= config.token_threshold
}
pub fn create_summary_prompt() -> String {
"Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including:
1. What we discussed and accomplished
2. What we're currently working on
3. Which files or components we're focusing on
4. What we plan to do next
5. Any important decisions or conclusions reached
6. Key technical details or requirements
Keep the summary comprehensive but concise, focusing on actionable information that maintains conversation continuity.".to_string()
}
pub fn estimate_token_count(text: &str) -> u32 {
(text.len() as f32 / 4.0).ceil() as u32
}
pub fn format_summary_for_session(summary: &ConversationSummary) -> String {
format!(
"## Previous Conversation Summary\n\n{}\n\n---\n\n*Summary generated from session {} on {}*\n\nContinuing conversation...",
summary.summary_text,
summary.original_session_id,
summary.created_at.format("%Y-%m-%d %H:%M:%S UTC")
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_summarization_config_default() {
let config = SummarizationConfig::default();
assert!(config.enabled);
assert_eq!(config.token_threshold, 100000);
assert_eq!(config.message_threshold, 50);
assert_eq!(config.context_window_percentage, 0.95);
assert_eq!(config.summarizer_model, "gpt-4");
assert_eq!(config.summary_max_tokens, 1000);
assert_eq!(config.preserve_recent_messages, 3);
}
#[test]
fn test_model_context_windows() {
assert_eq!(utils::get_model_context_window("gpt-4"), 8192);
assert_eq!(utils::get_model_context_window("gpt-4-turbo"), 128000);
assert_eq!(utils::get_model_context_window("claude-3-5-sonnet-20241022"), 200000);
assert_eq!(utils::get_model_context_window("gemini-1.5-pro"), 2000000);
assert_eq!(utils::get_model_context_window("unknown-model"), 4096);
}
#[test]
fn test_usage_percentage_calculation() {
assert_eq!(utils::calculate_usage_percentage(1000, 2000), 0.5);
assert_eq!(utils::calculate_usage_percentage(1900, 2000), 0.95);
assert_eq!(utils::calculate_usage_percentage(0, 2000), 0.0);
assert_eq!(utils::calculate_usage_percentage(1000, 0), 0.0);
}
#[test]
fn test_should_summarize() {
let config = SummarizationConfig::default();
let metrics = ConversationMetrics {
total_tokens: 150000,
message_count: 10,
context_window: 200000,
usage_percentage: 0.75,
needs_summarization: false,
};
assert!(utils::should_summarize(&metrics, &config));
let metrics = ConversationMetrics {
total_tokens: 10000,
message_count: 60,
context_window: 200000,
usage_percentage: 0.05,
needs_summarization: false,
};
assert!(utils::should_summarize(&metrics, &config));
let metrics = ConversationMetrics {
total_tokens: 190000,
message_count: 10,
context_window: 200000,
usage_percentage: 0.95,
needs_summarization: false,
};
assert!(utils::should_summarize(&metrics, &config));
let mut disabled_config = config.clone();
disabled_config.enabled = false;
assert!(!utils::should_summarize(&metrics, &disabled_config));
}
#[test]
fn test_estimate_token_count() {
let text = "Hello, world!";
let estimated = utils::estimate_token_count(text);
assert!(estimated > 0);
assert!(estimated <= text.len() as u32); }
#[test]
fn test_create_summary_prompt() {
let prompt = utils::create_summary_prompt();
assert!(prompt.contains("summary"));
assert!(prompt.contains("conversation"));
assert!(prompt.contains("continuity"));
}
}