oli_tui/app/
history.rs

1use crate::agent::core::Agent;
2use crate::apis::api_client::Message;
3use crate::app::state::{App, AppState};
4use crate::app::utils::Scrollable;
5use crate::prompts::CONVERSATION_SUMMARY_PROMPT;
6use anyhow::Result;
7use std::time::Instant;
8
9/// Message content threshold before considering summarization (in chars)
10const DEFAULT_SUMMARIZATION_CHAR_THRESHOLD: usize = 1000000;
11/// Message count threshold before considering summarization
12const DEFAULT_SUMMARIZATION_COUNT_THRESHOLD: usize = 1000;
13/// Maximum number of messages to keep unsummarized (recent history)
14const DEFAULT_KEEP_RECENT_COUNT: usize = 20;
15
16/// Represents a conversation summary
17pub struct ConversationSummary {
18    /// The summarized content
19    pub content: String,
20    /// When the summary was created
21    pub created_at: Instant,
22    /// Number of messages summarized
23    pub messages_count: usize,
24    /// Original character count that was summarized
25    pub original_chars: usize,
26}
27
28impl ConversationSummary {
29    pub fn new(content: String, messages_count: usize, original_chars: usize) -> Self {
30        Self {
31            content,
32            created_at: Instant::now(),
33            messages_count,
34            original_chars,
35        }
36    }
37}
38
39/// Context compression management trait for the application
40pub trait ContextCompressor {
41    /// Generate a summary of the conversation history
42    fn compress_context(&mut self) -> Result<()>;
43
44    /// Check if conversation should be summarized based on thresholds
45    fn should_compress(&self) -> bool;
46
47    /// Get the total character count of conversation history
48    fn conversation_char_count(&self) -> usize;
49
50    /// Get summaries count
51    fn summary_count(&self) -> usize;
52
53    /// Clear all summaries and history
54    fn clear_history(&mut self);
55
56    /// Convert display messages to session messages
57    fn display_to_session_messages(&self, display_messages: &[String]) -> Vec<Message>;
58
59    /// Convert session messages to display messages
60    fn session_to_display_messages(&self, session_messages: &[Message]) -> Vec<String>;
61}
62
63impl ContextCompressor for App {
64    fn compress_context(&mut self) -> Result<()> {
65        // Don't summarize if no messages
66        if self.messages.is_empty() {
67            return Ok(());
68        }
69
70        // Check if we have an agent for summarization
71        let agent = match &self.agent {
72            Some(agent) => agent.clone(),
73            None => return Err(anyhow::anyhow!("No agent available for summarization")),
74        };
75
76        // Keep the most recent messages unsummarized
77        let keep_recent = DEFAULT_KEEP_RECENT_COUNT.min(self.messages.len());
78        let to_summarize = self.messages.len().saturating_sub(keep_recent);
79
80        // If nothing to summarize, just return
81        if to_summarize == 0 {
82            return Ok(());
83        }
84
85        // Get the messages to summarize
86        let messages_to_summarize = self.messages[0..to_summarize].join("\n");
87        let messages_chars = messages_to_summarize.len();
88
89        // Show a message that we're summarizing
90        self.messages
91            .push("[wait] ⚪ Summarizing conversation history...".into());
92
93        // Generate the summary using the agent
94        let summary = self.generate_summary_with_agent(&agent, &messages_to_summarize)?;
95
96        // Create a new summary record
97        let summary_record =
98            ConversationSummary::new(summary.clone(), to_summarize, messages_chars);
99
100        // Add the summary to the list
101        self.conversation_summaries.push(summary_record);
102
103        // Remove the summarized messages
104        self.messages.drain(0..to_summarize);
105
106        // Add the summary marker to the message list
107        self.messages.insert(
108            0,
109            format!("💬 [CONVERSATION SUMMARY]\n{}\n[END SUMMARY]", summary),
110        );
111
112        // Update the session manager if it exists
113        // First collect the messages to convert
114        let messages_to_keep = self.messages[to_summarize..].to_vec();
115
116        // Convert display messages to API messages format
117        let session_messages = self.display_to_session_messages(&messages_to_keep);
118
119        // Update the session manager if it exists
120        if let Some(session) = &mut self.session_manager {
121            // Replace the session with the summary and recent messages
122            session.replace_with_summary(summary.clone());
123
124            // Add the recent messages that weren't summarized
125            for msg in session_messages {
126                session.add_message(msg.clone());
127            }
128        }
129
130        // Add a notification
131        self.messages.push(format!(
132            "[success] ⏺ Summarized {} messages ({} chars)",
133            to_summarize, messages_chars
134        ));
135
136        // Make sure to auto-scroll
137        self.auto_scroll_to_bottom();
138
139        Ok(())
140    }
141
142    fn should_compress(&self) -> bool {
143        // Don't summarize in non-chat state
144        if self.state != AppState::Chat {
145            return false;
146        }
147
148        // Check both message count and character count thresholds
149        let message_count = self.messages.len();
150        let char_count = self.conversation_char_count();
151
152        // Also check the session manager if available
153        let session_count = self
154            .session_manager
155            .as_ref()
156            .map_or(0, |s| s.message_count());
157
158        message_count > DEFAULT_SUMMARIZATION_COUNT_THRESHOLD
159            || char_count > DEFAULT_SUMMARIZATION_CHAR_THRESHOLD
160            || session_count > DEFAULT_SUMMARIZATION_COUNT_THRESHOLD
161    }
162
163    fn conversation_char_count(&self) -> usize {
164        self.messages.iter().map(|m| m.len()).sum()
165    }
166
167    fn summary_count(&self) -> usize {
168        self.conversation_summaries.len()
169    }
170
171    fn clear_history(&mut self) {
172        self.messages.clear();
173        self.conversation_summaries.clear();
174
175        // Reset both new and legacy scroll positions
176        self.message_scroll.scroll_to_top();
177        self.scroll_position = 0;
178
179        // Also clear agent's conversation history if it exists
180        if let Some(agent) = &mut self.agent {
181            agent.clear_history();
182        }
183
184        // Clear session manager if it exists
185        if let Some(session) = &mut self.session_manager {
186            session.clear();
187        }
188    }
189
190    fn display_to_session_messages(&self, display_messages: &[String]) -> Vec<Message> {
191        let mut session_messages = Vec::new();
192        let mut current_role = "user";
193
194        for msg in display_messages {
195            // Try to determine the role based on common message patterns
196            if msg.starts_with("[user]") || msg.starts_with("User:") {
197                current_role = "user";
198                let content = msg
199                    .replace("[user]", "")
200                    .replace("User:", "")
201                    .trim()
202                    .to_string();
203                session_messages.push(Message::user(content));
204            } else if msg.starts_with("[assistant]") || msg.starts_with("Assistant:") {
205                current_role = "assistant";
206                let content = msg
207                    .replace("[assistant]", "")
208                    .replace("Assistant:", "")
209                    .trim()
210                    .to_string();
211                session_messages.push(Message::assistant(content));
212            } else if msg.starts_with("[system]") || msg.starts_with("System:") {
213                current_role = "system";
214                let content = msg
215                    .replace("[system]", "")
216                    .replace("System:", "")
217                    .trim()
218                    .to_string();
219                session_messages.push(Message::system(content));
220            } else if !msg.starts_with("[wait]")
221                && !msg.starts_with("[success]")
222                && !msg.starts_with("[info]")
223            {
224                // For unmarked messages, use the current role context
225                match current_role {
226                    "user" => session_messages.push(Message::user(msg.clone())),
227                    "assistant" => session_messages.push(Message::assistant(msg.clone())),
228                    "system" => session_messages.push(Message::system(msg.clone())),
229                    _ => session_messages.push(Message::user(msg.clone())),
230                }
231            }
232        }
233
234        session_messages
235    }
236
237    fn session_to_display_messages(&self, session_messages: &[Message]) -> Vec<String> {
238        session_messages
239            .iter()
240            .map(|msg| match msg.role.as_str() {
241                "user" => format!("[user] {}", msg.content),
242                "assistant" => format!("[assistant] {}", msg.content),
243                "system" => format!("[system] {}", msg.content),
244                _ => msg.content.clone(),
245            })
246            .collect()
247    }
248}
249
250impl App {
251    /// Internal method to generate a summary with the agent
252    fn generate_summary_with_agent(&mut self, agent: &Agent, content: &str) -> Result<String> {
253        // Create a tokio runtime if needed
254        let runtime = match &self.tokio_runtime {
255            Some(rt) => rt,
256            None => return Err(anyhow::anyhow!("Async runtime not available")),
257        };
258
259        // Create a cloned agent to avoid borrowing issues
260        let agent_clone = agent.clone();
261
262        // Copy the content for the async block
263        let content_to_summarize = content.to_string();
264
265        // Define the summarization prompt
266        let prompt = format!("{}{}", CONVERSATION_SUMMARY_PROMPT, content_to_summarize);
267
268        // Execute the summarization
269        let result = runtime.block_on(async { agent_clone.execute(&prompt).await })?;
270
271        Ok(result)
272    }
273}