use crate::agent::core::Agent;
use crate::apis::api_client::Message;
use crate::app::core::{App, AppState};
use crate::prompts::CONVERSATION_SUMMARY_PROMPT;
use anyhow::Result;
use std::time::Instant;
const DEFAULT_SUMMARIZATION_CHAR_THRESHOLD: usize = 1000000;
const DEFAULT_SUMMARIZATION_COUNT_THRESHOLD: usize = 1000;
const DEFAULT_KEEP_RECENT_COUNT: usize = 20;
#[derive(Clone)]
pub struct ConversationSummary {
pub content: String,
pub created_at: Instant,
pub messages_count: usize,
pub original_chars: usize,
}
impl ConversationSummary {
pub fn new(content: String, messages_count: usize, original_chars: usize) -> Self {
Self {
content,
created_at: Instant::now(),
messages_count,
original_chars,
}
}
}
pub trait ContextCompressor {
fn compress_context(&mut self) -> Result<()>;
fn should_compress(&self) -> bool;
fn conversation_char_count(&self) -> usize;
fn summary_count(&self) -> usize;
fn clear_history(&mut self);
fn display_to_session_messages(&self, display_messages: &[String]) -> Vec<Message>;
fn session_to_display_messages(&self, session_messages: &[Message]) -> Vec<String>;
}
impl ContextCompressor for App {
fn compress_context(&mut self) -> Result<()> {
if self.messages.is_empty() {
return Ok(());
}
let agent = match &self.agent {
Some(agent) => agent.clone(),
None => return Err(anyhow::anyhow!("No agent available for summarization")),
};
let keep_recent = DEFAULT_KEEP_RECENT_COUNT.min(self.messages.len());
let to_summarize = self.messages.len().saturating_sub(keep_recent);
if to_summarize == 0 {
return Ok(());
}
let messages_to_summarize = self.messages[0..to_summarize].join("\n");
let messages_chars = messages_to_summarize.len();
self.messages
.push("[wait] ⚪ Summarizing conversation history...".into());
let summary = self.generate_summary_with_agent(&agent, &messages_to_summarize)?;
let summary_record =
ConversationSummary::new(summary.clone(), to_summarize, messages_chars);
self.conversation_summaries.push(summary_record);
self.messages.drain(0..to_summarize);
self.messages.insert(
0,
format!("💬 [CONVERSATION SUMMARY]\n{}\n[END SUMMARY]", summary),
);
let messages_to_keep = self.messages[to_summarize..].to_vec();
let session_messages = self.display_to_session_messages(&messages_to_keep);
if let Some(session) = &mut self.session_manager {
session.replace_with_summary(summary.clone());
for msg in session_messages {
session.add_message(msg.clone());
}
}
self.messages.push(format!(
"[success] ⏺ Summarized {} messages ({} chars)",
to_summarize, messages_chars
));
Ok(())
}
fn should_compress(&self) -> bool {
if self.state != AppState::Chat {
return false;
}
let message_count = self.messages.len();
let char_count = self.conversation_char_count();
let session_count = self
.session_manager
.as_ref()
.map_or(0, |s| s.message_count());
message_count > DEFAULT_SUMMARIZATION_COUNT_THRESHOLD
|| char_count > DEFAULT_SUMMARIZATION_CHAR_THRESHOLD
|| session_count > DEFAULT_SUMMARIZATION_COUNT_THRESHOLD
}
fn conversation_char_count(&self) -> usize {
self.messages.iter().map(|m| m.len()).sum()
}
fn summary_count(&self) -> usize {
self.conversation_summaries.len()
}
fn clear_history(&mut self) {
self.messages.clear();
self.conversation_summaries.clear();
if let Some(agent) = &mut self.agent {
agent.clear_history();
}
if let Some(session) = &mut self.session_manager {
session.clear();
}
self.messages.push("[info] Chat history cleared".into());
}
fn display_to_session_messages(&self, display_messages: &[String]) -> Vec<Message> {
let mut session_messages = Vec::new();
let mut current_role = "user";
for msg in display_messages {
if msg.starts_with("[user]") || msg.starts_with("User:") {
current_role = "user";
let content = msg
.replace("[user]", "")
.replace("User:", "")
.trim()
.to_string();
session_messages.push(Message::user(content));
} else if msg.starts_with("[assistant]") || msg.starts_with("Assistant:") {
current_role = "assistant";
let content = msg
.replace("[assistant]", "")
.replace("Assistant:", "")
.trim()
.to_string();
session_messages.push(Message::assistant(content));
} else if msg.starts_with("[system]") || msg.starts_with("System:") {
current_role = "system";
let content = msg
.replace("[system]", "")
.replace("System:", "")
.trim()
.to_string();
session_messages.push(Message::system(content));
} else if !msg.starts_with("[wait]")
&& !msg.starts_with("[success]")
&& !msg.starts_with("[info]")
{
match current_role {
"user" => session_messages.push(Message::user(msg.clone())),
"assistant" => session_messages.push(Message::assistant(msg.clone())),
"system" => session_messages.push(Message::system(msg.clone())),
_ => session_messages.push(Message::user(msg.clone())),
}
}
}
session_messages
}
fn session_to_display_messages(&self, session_messages: &[Message]) -> Vec<String> {
session_messages
.iter()
.map(|msg| match msg.role.as_str() {
"user" => format!("[user] {}", msg.content),
"assistant" => format!("[assistant] {}", msg.content),
"system" => format!("[system] {}", msg.content),
_ => msg.content.clone(),
})
.collect()
}
}
impl App {
fn generate_summary_with_agent(&mut self, agent: &Agent, content: &str) -> Result<String> {
let runtime = match &self.tokio_runtime {
Some(rt) => rt,
None => return Err(anyhow::anyhow!("Async runtime not available")),
};
let agent_clone = agent.clone();
let content_to_summarize = content.to_string();
let prompt = format!("{}{}", CONVERSATION_SUMMARY_PROMPT, content_to_summarize);
let result = runtime.block_on(async { agent_clone.execute(&prompt).await })?;
Ok(result)
}
}