oli_server/app/
history.rs1use crate::agent::core::Agent;
2use crate::apis::api_client::Message;
3use crate::app::core::{App, AppState};
4use crate::prompts::CONVERSATION_SUMMARY_PROMPT;
5use anyhow::Result;
6use std::time::Instant;
7
8const DEFAULT_SUMMARIZATION_CHAR_THRESHOLD: usize = 1000000;
10const DEFAULT_SUMMARIZATION_COUNT_THRESHOLD: usize = 1000;
12const DEFAULT_KEEP_RECENT_COUNT: usize = 20;
14
15#[derive(Clone)]
16pub struct ConversationSummary {
18 pub content: String,
20 pub created_at: Instant,
22 pub messages_count: usize,
24 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
39pub trait ContextCompressor {
41 fn compress_context(&mut self) -> Result<()>;
43
44 fn should_compress(&self) -> bool;
46
47 fn conversation_char_count(&self) -> usize;
49
50 fn summary_count(&self) -> usize;
52
53 fn clear_history(&mut self);
55
56 fn display_to_session_messages(&self, display_messages: &[String]) -> Vec<Message>;
58
59 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 if self.messages.is_empty() {
67 return Ok(());
68 }
69
70 let agent = match &self.agent {
72 Some(agent) => agent.clone(),
73 None => return Err(anyhow::anyhow!("No agent available for summarization")),
74 };
75
76 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 to_summarize == 0 {
82 return Ok(());
83 }
84
85 let messages_to_summarize = self.messages[0..to_summarize].join("\n");
87 let messages_chars = messages_to_summarize.len();
88
89 self.messages
91 .push("[wait] ⚪ Summarizing conversation history...".into());
92
93 let summary = self.generate_summary_with_agent(&agent, &messages_to_summarize)?;
95
96 let summary_record =
98 ConversationSummary::new(summary.clone(), to_summarize, messages_chars);
99
100 self.conversation_summaries.push(summary_record);
102
103 self.messages.drain(0..to_summarize);
105
106 self.messages.insert(
108 0,
109 format!("💬 [CONVERSATION SUMMARY]\n{}\n[END SUMMARY]", summary),
110 );
111
112 let messages_to_keep = self.messages[to_summarize..].to_vec();
115
116 let session_messages = self.display_to_session_messages(&messages_to_keep);
118
119 if let Some(session) = &mut self.session_manager {
121 session.replace_with_summary(summary.clone());
123
124 for msg in session_messages {
126 session.add_message(msg.clone());
127 }
128 }
129
130 self.messages.push(format!(
132 "[success] ⏺ Summarized {} messages ({} chars)",
133 to_summarize, messages_chars
134 ));
135
136 Ok(())
139 }
140
141 fn should_compress(&self) -> bool {
142 if self.state != AppState::Chat {
144 return false;
145 }
146
147 let message_count = self.messages.len();
149 let char_count = self.conversation_char_count();
150
151 let session_count = self
153 .session_manager
154 .as_ref()
155 .map_or(0, |s| s.message_count());
156
157 message_count > DEFAULT_SUMMARIZATION_COUNT_THRESHOLD
158 || char_count > DEFAULT_SUMMARIZATION_CHAR_THRESHOLD
159 || session_count > DEFAULT_SUMMARIZATION_COUNT_THRESHOLD
160 }
161
162 fn conversation_char_count(&self) -> usize {
163 self.messages.iter().map(|m| m.len()).sum()
164 }
165
166 fn summary_count(&self) -> usize {
167 self.conversation_summaries.len()
168 }
169
170 fn clear_history(&mut self) {
171 self.messages.clear();
172 self.conversation_summaries.clear();
173
174 if let Some(agent) = &mut self.agent {
178 agent.clear_history();
179 }
180
181 if let Some(session) = &mut self.session_manager {
183 session.clear();
184 }
185
186 self.messages.push("[info] Chat history cleared".into());
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 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 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 fn generate_summary_with_agent(&mut self, agent: &Agent, content: &str) -> Result<String> {
253 let runtime = match &self.tokio_runtime {
255 Some(rt) => rt,
256 None => return Err(anyhow::anyhow!("Async runtime not available")),
257 };
258
259 let agent_clone = agent.clone();
261
262 let content_to_summarize = content.to_string();
264
265 let prompt = format!("{}{}", CONVERSATION_SUMMARY_PROMPT, content_to_summarize);
267
268 let result = runtime.block_on(async { agent_clone.execute(&prompt).await })?;
270
271 Ok(result)
272 }
273}