coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! Auto-Summarization System
//!
//! This module provides automatic conversation summarization to manage long coding sessions.
//! When conversations approach context window limits, the system automatically summarizes
//! the conversation and creates a new session with the summary as context.
//!
//! ## Features
//!
//! - **Token Monitoring**: Tracks token usage vs context window limits
//! - **Automatic Triggers**: Summarizes at configurable thresholds (95% capacity)
//! - **Session Continuation**: Creates new sessions with conversation summary
//! - **Context Preservation**: Maintains conversation continuity
//! - **Configurable Behavior**: Customizable thresholds and summarization models

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;

/// Configuration for auto-summarization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SummarizationConfig {
    /// Enable auto-summarization
    pub enabled: bool,
    
    /// Token threshold to trigger summarization
    pub token_threshold: u32,
    
    /// Message count threshold to trigger summarization
    pub message_threshold: u32,
    
    /// Context window percentage threshold (0.95 = 95%)
    pub context_window_percentage: f32,
    
    /// Model to use for summarization
    pub summarizer_model: String,
    
    /// Maximum tokens for summary
    pub summary_max_tokens: u32,
    
    /// Number of recent messages to preserve in new session
    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,
        }
    }
}

/// Conversation metrics for monitoring
#[derive(Debug, Clone)]
pub struct ConversationMetrics {
    /// Total tokens used in the session
    pub total_tokens: u32,
    
    /// Number of messages in the session
    pub message_count: u32,
    
    /// Context window size for the model
    pub context_window: u32,
    
    /// Usage percentage (0.0 to 1.0)
    pub usage_percentage: f32,
    
    /// Whether summarization is needed
    pub needs_summarization: bool,
}

/// Result of conversation summarization
#[derive(Debug, Clone)]
pub struct ConversationSummary {
    /// Original session ID
    pub original_session_id: String,
    
    /// Generated summary text
    pub summary_text: String,
    
    /// Messages preserved from original session
    pub preserved_messages: Vec<Message>,
    
    /// Token count of the summary
    pub summary_tokens: u32,
    
    /// Model used for summarization
    pub summarizer_model: String,
    
    /// Timestamp when summary was created
    pub created_at: chrono::DateTime<chrono::Utc>,
}

/// Summarization-related errors
#[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),
}

/// Trait for summarization services
#[async_trait]
pub trait SummarizationService: Send + Sync {
    /// Monitor a session and return current metrics
    async fn monitor_session(&self, session_id: &str) -> Result<ConversationMetrics, SummarizationError>;
    
    /// Check if a session needs summarization
    async fn needs_summarization(&self, session_id: &str) -> Result<bool, SummarizationError>;
    
    /// Summarize a conversation and return the summary
    async fn summarize_conversation(&self, session_id: &str) -> Result<ConversationSummary, SummarizationError>;
    
    /// Create a new session with the conversation summary
    async fn create_continuation_session(&self, summary: ConversationSummary) -> Result<String, SummarizationError>;
    
    /// Perform automatic summarization if needed
    async fn auto_summarize_if_needed(&self, session_id: &str) -> Result<Option<String>, SummarizationError>;
    
    /// Update token usage for a session
    async fn update_token_usage(&self, session_id: &str, usage: TokenUsage) -> Result<(), SummarizationError>;
}

/// Utility functions for summarization
pub mod utils {
    use super::*;
    
    /// Get default context window for a model
    pub fn get_model_context_window(model_id: &str) -> u32 {
        match model_id {
            // OpenAI models
            "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,
            
            // Anthropic models
            "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,
            
            // Google models
            "gemini-1.5-pro" | "gemini-1.5-pro-latest" => 2000000,
            "gemini-1.5-flash" | "gemini-1.5-flash-latest" => 1000000,
            "gemini-pro" => 32768,
            
            // Other providers (conservative defaults)
            _ if model_id.contains("llama") => 32768,
            _ if model_id.contains("mistral") => 32768,
            _ if model_id.contains("mixtral") => 32768,
            _ if model_id.contains("codellama") => 16384,
            
            // Default fallback
            _ => 4096,
        }
    }
    
    /// Calculate usage percentage
    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
    }
    
    /// Check if summarization is needed based on thresholds
    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
    }
    
    /// Generate a summary prompt for the AI
    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()
    }
    
    /// Estimate token count for text (rough approximation)
    pub fn estimate_token_count(text: &str) -> u32 {
        // Rough approximation: 1 token ≈ 4 characters for English text
        // This is a conservative estimate; actual tokenization varies by model
        (text.len() as f32 / 4.0).ceil() as u32
    }
    
    /// Format summary for new session
    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();
        
        // Test token threshold
        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));
        
        // Test message threshold
        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));
        
        // Test percentage threshold
        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));
        
        // Test disabled
        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); // Should be less than character count
    }
    
    #[test]
    fn test_create_summary_prompt() {
        let prompt = utils::create_summary_prompt();
        assert!(prompt.contains("summary"));
        assert!(prompt.contains("conversation"));
        assert!(prompt.contains("continuity"));
    }
}