vtcode_core/core/
conversation_summarizer.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5/// Represents a conversation summary
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ConversationSummary {
8    pub id: String,
9    pub timestamp: u64,
10    pub session_duration_seconds: u64,
11    pub total_turns: usize,
12    pub key_decisions: Vec<KeyDecision>,
13    pub completed_tasks: Vec<TaskSummary>,
14    pub error_patterns: Vec<ErrorPattern>,
15    pub context_recommendations: Vec<String>,
16    pub summary_text: String,
17    pub compression_ratio: f64,
18    pub confidence_score: f64,
19}
20
21/// A key decision made during the conversation
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct KeyDecision {
24    pub turn_number: usize,
25    pub decision_type: DecisionType,
26    pub description: String,
27    pub rationale: String,
28    pub outcome: Option<String>,
29    pub importance_score: f64,
30}
31
32/// Type of decision that was made
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub enum DecisionType {
35    ToolExecution,
36    ResponseGeneration,
37    ContextCompression,
38    ErrorRecovery,
39    WorkflowChange,
40}
41
42impl std::fmt::Display for DecisionType {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        let description = match self {
45            DecisionType::ToolExecution => "Tool Execution",
46            DecisionType::ResponseGeneration => "Response Generation",
47            DecisionType::ContextCompression => "Context Compression",
48            DecisionType::ErrorRecovery => "Error Recovery",
49            DecisionType::WorkflowChange => "Workflow Change",
50        };
51        write!(f, "{}", description)
52    }
53}
54
55/// Summary of a completed task
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct TaskSummary {
58    pub task_type: String,
59    pub description: String,
60    pub success: bool,
61    pub duration_seconds: Option<u64>,
62    pub tools_used: Vec<String>,
63}
64
65/// Pattern of errors that occurred
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ErrorPattern {
68    pub error_type: String,
69    pub frequency: usize,
70    pub description: String,
71    pub recommended_solution: String,
72}
73
74/// Conversation summarizer for long-running sessions
75pub struct ConversationSummarizer {
76    summaries: Vec<ConversationSummary>,
77    summarization_threshold: usize, // Minimum conversation length to trigger summarization
78    max_summary_length: usize,
79    compression_target_ratio: f64,
80}
81
82impl ConversationSummarizer {
83    pub fn new() -> Self {
84        Self {
85            summaries: Vec::new(),
86            summarization_threshold: 20,   // Summarize after 20 turns
87            max_summary_length: 2000,      // Maximum characters in summary
88            compression_target_ratio: 0.3, // Target 30% of original length
89        }
90    }
91
92    /// Check if conversation should be summarized
93    pub fn should_summarize(
94        &self,
95        conversation_length: usize,
96        context_size: usize,
97        context_limit: usize,
98    ) -> bool {
99        let approaching_limit = context_size > (context_limit * 80 / 100); // 80% of limit
100        let long_conversation = conversation_length >= self.summarization_threshold;
101        let has_errors = context_size > (context_limit * 60 / 100); // 60% indicates potential issues
102
103        approaching_limit || long_conversation || has_errors
104    }
105
106    /// Generate a conversation summary
107    pub fn generate_summary(
108        &mut self,
109        conversation_turns: &[ConversationTurn],
110        decision_history: &[DecisionInfo],
111        error_history: &[ErrorInfo],
112        session_start_time: u64,
113    ) -> Result<ConversationSummary, SummarizationError> {
114        let now = SystemTime::now()
115            .duration_since(UNIX_EPOCH)
116            .unwrap()
117            .as_secs();
118
119        let session_duration = now - session_start_time;
120        let total_turns = conversation_turns.len();
121
122        // Extract key decisions
123        let key_decisions = self.extract_key_decisions(decision_history, conversation_turns);
124
125        // Extract completed tasks
126        let completed_tasks = self.extract_completed_tasks(conversation_turns);
127
128        // Analyze error patterns
129        let error_patterns = self.analyze_error_patterns(error_history);
130
131        // Generate context recommendations
132        let context_recommendations = self.generate_context_recommendations(
133            conversation_turns.len(),
134            error_history.len(),
135            session_duration,
136        );
137
138        // Generate summary text
139        let summary_text = self.generate_summary_text(
140            &key_decisions,
141            &completed_tasks,
142            &error_patterns,
143            session_duration,
144            total_turns,
145        );
146
147        // Calculate compression ratio
148        let original_length = self.calculate_conversation_length(conversation_turns);
149        let compression_ratio = if original_length > 0 {
150            summary_text.len() as f64 / original_length as f64
151        } else {
152            1.0
153        };
154
155        // Calculate confidence score
156        let confidence_score = self.calculate_confidence_score(
157            key_decisions.len(),
158            completed_tasks.len(),
159            error_patterns.len(),
160            compression_ratio,
161        );
162
163        let summary_id = format!("summary_{}", now);
164
165        let summary = ConversationSummary {
166            id: summary_id,
167            timestamp: now,
168            session_duration_seconds: session_duration,
169            total_turns,
170            key_decisions,
171            completed_tasks,
172            error_patterns,
173            context_recommendations,
174            summary_text,
175            compression_ratio,
176            confidence_score,
177        };
178
179        self.summaries.push(summary.clone());
180        Ok(summary)
181    }
182
183    /// Extract key decisions from conversation and decision history
184    fn extract_key_decisions(
185        &self,
186        decision_history: &[DecisionInfo],
187        conversation_turns: &[ConversationTurn],
188    ) -> Vec<KeyDecision> {
189        let mut key_decisions = Vec::new();
190
191        for (_i, decision) in decision_history.iter().enumerate() {
192            let decision_type = match decision.action_type.as_str() {
193                "tool_call" => DecisionType::ToolExecution,
194                "response" => DecisionType::ResponseGeneration,
195                "context_compression" => DecisionType::ContextCompression,
196                "error_recovery" => DecisionType::ErrorRecovery,
197                _ => DecisionType::WorkflowChange,
198            };
199
200            let importance_score = self.calculate_decision_importance(decision, conversation_turns);
201
202            if importance_score > 0.6 {
203                // Only include important decisions
204                key_decisions.push(KeyDecision {
205                    turn_number: decision.turn_number,
206                    decision_type,
207                    description: decision.description.clone(),
208                    rationale: decision.reasoning.clone(),
209                    outcome: decision.outcome.clone(),
210                    importance_score,
211                });
212            }
213        }
214
215        // Limit to top 10 most important decisions
216        key_decisions.sort_by(|a, b| b.importance_score.partial_cmp(&a.importance_score).unwrap());
217        key_decisions.truncate(10);
218        key_decisions
219    }
220
221    /// Extract completed tasks from conversation
222    fn extract_completed_tasks(&self, conversation_turns: &[ConversationTurn]) -> Vec<TaskSummary> {
223        let mut tasks = Vec::new();
224
225        for turn in conversation_turns {
226            if let Some(task_info) = &turn.task_info {
227                if task_info.completed {
228                    tasks.push(TaskSummary {
229                        task_type: task_info.task_type.clone(),
230                        description: task_info.description.clone(),
231                        success: task_info.success,
232                        duration_seconds: task_info.duration_seconds,
233                        tools_used: task_info.tools_used.clone(),
234                    });
235                }
236            }
237        }
238
239        tasks
240    }
241
242    /// Analyze patterns in error history
243    fn analyze_error_patterns(&self, error_history: &[ErrorInfo]) -> Vec<ErrorPattern> {
244        let mut error_counts = HashMap::new();
245
246        // Count errors by type
247        for error in error_history {
248            *error_counts.entry(error.error_type.clone()).or_insert(0) += 1;
249        }
250
251        let mut patterns = Vec::new();
252        for (error_type, frequency) in error_counts {
253            if frequency >= 2 {
254                // Only include errors that occurred multiple times
255                let description = format!("{} error occurred {} times", error_type, frequency);
256                let recommended_solution = self.get_error_solution(&error_type);
257
258                patterns.push(ErrorPattern {
259                    error_type,
260                    frequency,
261                    description,
262                    recommended_solution,
263                });
264            }
265        }
266
267        patterns.sort_by(|a, b| b.frequency.cmp(&a.frequency));
268        patterns
269    }
270
271    /// Generate context recommendations
272    fn generate_context_recommendations(
273        &self,
274        turn_count: usize,
275        error_count: usize,
276        session_duration: u64,
277    ) -> Vec<String> {
278        let mut recommendations = Vec::new();
279
280        if turn_count > 50 {
281            recommendations.push(
282                "Consider summarizing the conversation to maintain context efficiency".to_string(),
283            );
284        }
285
286        if error_count > 5 {
287            recommendations.push(
288                "High error rate detected - review error patterns and consider context compression"
289                    .to_string(),
290            );
291        }
292
293        if session_duration > 1800 {
294            // 30 minutes
295            recommendations.push(
296                "Long-running session detected - consider breaking into smaller tasks".to_string(),
297            );
298        }
299
300        if recommendations.is_empty() {
301            recommendations.push("Conversation is proceeding normally".to_string());
302        }
303
304        recommendations
305    }
306
307    /// Generate human-readable summary text
308    fn generate_summary_text(
309        &self,
310        key_decisions: &[KeyDecision],
311        completed_tasks: &[TaskSummary],
312        error_patterns: &[ErrorPattern],
313        session_duration: u64,
314        total_turns: usize,
315    ) -> String {
316        let mut summary = format!(
317            "Conversation Summary ({} turns, {} seconds):\n\n",
318            total_turns, session_duration
319        );
320
321        if !key_decisions.is_empty() {
322            summary.push_str("Key Decisions Made:\n");
323            for decision in key_decisions.iter().take(5) {
324                summary.push_str(&format!(
325                    "• Turn {}: {} - {}\n",
326                    decision.turn_number, decision.decision_type, decision.description
327                ));
328            }
329            summary.push('\n');
330        }
331
332        if !completed_tasks.is_empty() {
333            summary.push_str("Completed Tasks:\n");
334            for task in completed_tasks {
335                let status = if task.success {
336                    "[Success]"
337                } else {
338                    "[Failure]"
339                };
340                summary.push_str(&format!(
341                    "{} {} ({})\n",
342                    status, task.description, task.task_type
343                ));
344            }
345            summary.push('\n');
346        }
347
348        if !error_patterns.is_empty() {
349            summary.push_str("Error Patterns:\n");
350            for pattern in error_patterns {
351                summary.push_str(&format!(
352                    "• {}: {} ({} occurrences)\n",
353                    pattern.error_type, pattern.description, pattern.frequency
354                ));
355            }
356            summary.push('\n');
357        }
358
359        // Truncate if too long
360        if summary.len() > self.max_summary_length {
361            summary.truncate(self.max_summary_length - 3);
362            summary.push_str("...");
363        }
364
365        summary
366    }
367
368    /// Calculate importance score for a decision
369    fn calculate_decision_importance(
370        &self,
371        decision: &DecisionInfo,
372        conversation_turns: &[ConversationTurn],
373    ) -> f64 {
374        let mut score = 0.5; // Base score
375
376        // Increase score based on decision type importance
377        match decision.action_type.as_str() {
378            "tool_call" => score += 0.3,
379            "context_compression" => score += 0.4,
380            "error_recovery" => score += 0.2,
381            _ => {}
382        }
383
384        // Increase score if decision had significant outcome
385        if let Some(outcome) = &decision.outcome {
386            if outcome.contains("success") || outcome.contains("completed") {
387                score += 0.2;
388            }
389        }
390
391        // Increase score if decision was made in later turns (potentially more important)
392        let progress_ratio = decision.turn_number as f64 / conversation_turns.len() as f64;
393        score += progress_ratio * 0.1;
394
395        score.min(1.0)
396    }
397
398    /// Calculate conversation length in characters
399    fn calculate_conversation_length(&self, conversation_turns: &[ConversationTurn]) -> usize {
400        conversation_turns
401            .iter()
402            .map(|turn| turn.content.len())
403            .sum()
404    }
405
406    /// Calculate confidence score for the summary
407    fn calculate_confidence_score(
408        &self,
409        decision_count: usize,
410        task_count: usize,
411        error_count: usize,
412        compression_ratio: f64,
413    ) -> f64 {
414        let mut confidence = 0.7; // Base confidence
415
416        // Higher confidence with more decisions and tasks
417        confidence += decision_count.min(10) as f64 * 0.02;
418        confidence += task_count.min(10) as f64 * 0.03;
419
420        // Lower confidence with many errors
421        confidence -= error_count.min(10) as f64 * 0.05;
422
423        // Adjust based on compression ratio (closer to target = higher confidence)
424        let ratio_distance = (compression_ratio - self.compression_target_ratio).abs();
425        confidence -= ratio_distance * 0.5;
426
427        confidence.max(0.1).min(1.0)
428    }
429
430    /// Get recommended solution for error type
431    fn get_error_solution(&self, error_type: &str) -> String {
432        match error_type {
433            "tool_execution" => "Review tool parameters and ensure correct file paths".to_string(),
434            "api_call" => "Check API key and consider implementing retry logic".to_string(),
435            "context_compression" => {
436                "Monitor context size and implement proactive compression".to_string()
437            }
438            _ => "Investigate error details and consider context preservation".to_string(),
439        }
440    }
441
442    /// Get all summaries
443    pub fn get_summaries(&self) -> &[ConversationSummary] {
444        &self.summaries
445    }
446
447    /// Get latest summary
448    pub fn get_latest_summary(&self) -> Option<&ConversationSummary> {
449        self.summaries.last()
450    }
451}
452
453/// Information about a conversation turn
454#[derive(Debug, Clone)]
455pub struct ConversationTurn {
456    pub turn_number: usize,
457    pub content: String,
458    pub role: String,
459    pub task_info: Option<TaskInfo>,
460}
461
462/// Information about a task within a conversation turn
463#[derive(Debug, Clone)]
464pub struct TaskInfo {
465    pub task_type: String,
466    pub description: String,
467    pub completed: bool,
468    pub success: bool,
469    pub duration_seconds: Option<u64>,
470    pub tools_used: Vec<String>,
471}
472
473/// Information about a decision
474#[derive(Debug, Clone)]
475pub struct DecisionInfo {
476    pub turn_number: usize,
477    pub action_type: String,
478    pub description: String,
479    pub reasoning: String,
480    pub outcome: Option<String>,
481}
482
483/// Information about an error
484#[derive(Debug, Clone)]
485pub struct ErrorInfo {
486    pub error_type: String,
487    pub message: String,
488    pub turn_number: usize,
489    pub recoverable: bool,
490}
491
492/// Error that can occur during summarization
493#[derive(Debug, Clone)]
494pub enum SummarizationError {
495    InsufficientData,
496    ProcessingError(String),
497}
498
499impl std::fmt::Display for SummarizationError {
500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501        match self {
502            SummarizationError::InsufficientData => {
503                write!(f, "Insufficient data for summarization")
504            }
505            SummarizationError::ProcessingError(msg) => write!(f, "Processing error: {}", msg),
506        }
507    }
508}
509
510impl std::error::Error for SummarizationError {}
511
512impl Default for ConversationSummarizer {
513    fn default() -> Self {
514        Self::new()
515    }
516}