1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5#[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#[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#[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#[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#[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
74pub struct ConversationSummarizer {
76 summaries: Vec<ConversationSummary>,
77 summarization_threshold: usize, 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, max_summary_length: 2000, compression_target_ratio: 0.3, }
90 }
91
92 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); let long_conversation = conversation_length >= self.summarization_threshold;
101 let has_errors = context_size > (context_limit * 60 / 100); approaching_limit || long_conversation || has_errors
104 }
105
106 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 let key_decisions = self.extract_key_decisions(decision_history, conversation_turns);
124
125 let completed_tasks = self.extract_completed_tasks(conversation_turns);
127
128 let error_patterns = self.analyze_error_patterns(error_history);
130
131 let context_recommendations = self.generate_context_recommendations(
133 conversation_turns.len(),
134 error_history.len(),
135 session_duration,
136 );
137
138 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 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 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 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 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 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 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 fn analyze_error_patterns(&self, error_history: &[ErrorInfo]) -> Vec<ErrorPattern> {
244 let mut error_counts = HashMap::new();
245
246 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 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 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 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 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 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 fn calculate_decision_importance(
370 &self,
371 decision: &DecisionInfo,
372 conversation_turns: &[ConversationTurn],
373 ) -> f64 {
374 let mut score = 0.5; 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 if let Some(outcome) = &decision.outcome {
386 if outcome.contains("success") || outcome.contains("completed") {
387 score += 0.2;
388 }
389 }
390
391 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 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 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; confidence += decision_count.min(10) as f64 * 0.02;
418 confidence += task_count.min(10) as f64 * 0.03;
419
420 confidence -= error_count.min(10) as f64 * 0.05;
422
423 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 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 pub fn get_summaries(&self) -> &[ConversationSummary] {
444 &self.summaries
445 }
446
447 pub fn get_latest_summary(&self) -> Option<&ConversationSummary> {
449 self.summaries.last()
450 }
451}
452
453#[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#[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#[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#[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#[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}