use super::config::{ConversationConfig, SummarizationStrategy};
use super::managed::{ManagedConversation, TrackedMessage};
use crate::types::Message;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionResult {
pub messages_removed: usize,
pub tokens_before: usize,
pub tokens_after: usize,
pub summary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactingConversation {
conversation: ManagedConversation,
config: ConversationConfig,
summaries: Vec<String>,
total_messages_added: usize,
compaction_count: usize,
}
impl Default for CompactingConversation {
fn default() -> Self {
Self::new(ConversationConfig::default())
}
}
impl CompactingConversation {
pub fn new(config: ConversationConfig) -> Self {
Self {
conversation: ManagedConversation::new(),
config,
summaries: Vec::new(),
total_messages_added: 0,
compaction_count: 0,
}
}
pub fn with_defaults() -> Self {
Self::default()
}
pub fn with_system(system: impl Into<String>, config: ConversationConfig) -> Self {
let mut conv = Self::new(config);
conv.conversation.set_system(system);
conv
}
pub fn set_system(&mut self, content: impl Into<String>) {
self.conversation.set_system(content);
}
pub fn system_message(&self) -> Option<&Message> {
self.conversation.system_message()
}
pub fn add_user_message(&mut self, content: impl Into<String>) {
self.conversation.add_user_message(content);
self.total_messages_added += 1;
}
pub fn add_assistant_message(&mut self, content: impl Into<String>) {
self.conversation.add_assistant_message(content);
self.total_messages_added += 1;
}
pub fn add_message(&mut self, message: Message) {
self.conversation.add_message(message);
self.total_messages_added += 1;
}
pub fn needs_compaction(&self) -> bool {
self.conversation.estimated_tokens() > self.config.max_tokens
}
pub fn compact(&mut self) -> Option<CompactionResult> {
if !self.needs_compaction() {
return None;
}
let tokens_before = self.conversation.estimated_tokens();
let messages_before = self.conversation.len();
let summary = match self.config.strategy {
SummarizationStrategy::KeepRecent => self.compact_keep_recent(),
SummarizationStrategy::Summarize => self.compact_with_summary(),
SummarizationStrategy::ChunkedSummary => self.compact_chunked(),
SummarizationStrategy::PreserveSystem => self.compact_preserve_system(),
};
let tokens_after = self.conversation.estimated_tokens();
let messages_after = self.conversation.len();
self.compaction_count += 1;
Some(CompactionResult {
messages_removed: messages_before - messages_after,
tokens_before,
tokens_after,
summary,
})
}
fn compact_keep_recent(&mut self) -> Option<String> {
let preserve = self.config.preserve_recent;
if self.conversation.len() > preserve {
let to_remove = self.conversation.len() - preserve;
self.conversation.remove_first(to_remove);
}
None
}
fn compact_with_summary(&mut self) -> Option<String> {
let preserve = self.config.preserve_recent;
if self.conversation.len() <= preserve {
return None;
}
let to_remove = self.conversation.len() - preserve;
let tracked = self.conversation.tracked_messages();
let messages_to_summarize: Vec<&TrackedMessage> = tracked.iter().take(to_remove).collect();
let summary = self.create_summary_text(&messages_to_summarize);
self.summaries.push(summary.clone());
self.conversation.remove_first(to_remove);
Some(summary)
}
fn compact_chunked(&mut self) -> Option<String> {
self.compact_with_summary()
}
fn compact_preserve_system(&mut self) -> Option<String> {
self.compact_with_summary()
}
fn create_summary_text(&self, messages: &[&TrackedMessage]) -> String {
let mut text = String::from("[Previous conversation summary]\n");
for tracked in messages {
let role = match tracked.message.role.as_str() {
"user" => "User",
"assistant" => "Assistant",
other => other,
};
if let Some(content) = &tracked.message.content {
let content = if content.len() > 200 {
format!("{}...", &content[..200])
} else {
content.clone()
};
text.push_str(&format!("- {}: {}\n", role, content));
}
}
text
}
pub fn messages(&self) -> Vec<Message> {
let mut messages = Vec::new();
if !self.summaries.is_empty() {
let combined_summary = self.summaries.join("\n\n");
messages.push(Message::system(format!(
"Previous conversation context:\n{}",
combined_summary
)));
}
messages.extend(self.conversation.messages());
messages
}
pub fn messages_for_api(&self) -> Vec<Message> {
self.messages()
}
pub fn len(&self) -> usize {
self.conversation.len()
}
pub fn is_empty(&self) -> bool {
self.conversation.is_empty()
}
pub fn estimated_tokens(&self) -> usize {
self.conversation.estimated_tokens()
}
pub fn config(&self) -> &ConversationConfig {
&self.config
}
pub fn set_config(&mut self, config: ConversationConfig) {
self.config = config;
}
pub fn total_messages_added(&self) -> usize {
self.total_messages_added
}
pub fn compaction_count(&self) -> usize {
self.compaction_count
}
pub fn summaries(&self) -> &[String] {
&self.summaries
}
pub fn clear(&mut self, keep_system: bool) {
self.conversation.clear(keep_system);
self.summaries.clear();
}
pub fn inner(&self) -> &ManagedConversation {
&self.conversation
}
pub fn inner_mut(&mut self) -> &mut ManagedConversation {
&mut self.conversation
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.conversation.set_metadata(key, value);
}
pub fn get_metadata(&self, key: &str) -> Option<&str> {
self.conversation.get_metadata(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn small_config() -> ConversationConfig {
ConversationConfig {
max_tokens: 100,
target_tokens: 50,
preserve_recent: 2,
preserve_system: true,
strategy: SummarizationStrategy::KeepRecent,
summarization_model: None,
summarization_prompt: String::new(),
}
}
#[test]
fn test_compacting_conversation_new() {
let conv = CompactingConversation::with_defaults();
assert!(conv.is_empty());
assert_eq!(conv.compaction_count(), 0);
}
#[test]
fn test_compacting_add_messages() {
let mut conv = CompactingConversation::with_defaults();
conv.add_user_message("Hello");
conv.add_assistant_message("Hi!");
assert_eq!(conv.len(), 2);
assert_eq!(conv.total_messages_added(), 2);
}
#[test]
fn test_compacting_needs_compaction() {
let mut conv = CompactingConversation::new(small_config());
conv.add_user_message("Short");
assert!(!conv.needs_compaction());
for i in 0..20 {
conv.add_user_message(format!("Message number {} with some content", i));
}
assert!(conv.needs_compaction());
}
#[test]
fn test_compact_keep_recent() {
let mut conv = CompactingConversation::new(small_config());
for i in 0..10 {
conv.add_user_message(format!("Message {}", i));
}
if conv.needs_compaction() {
let result = conv.compact();
assert!(result.is_some());
let result = result.unwrap();
assert!(result.messages_removed > 0);
assert_eq!(conv.len(), 2); }
}
#[test]
fn test_compact_with_summary() {
let config = ConversationConfig {
max_tokens: 100,
target_tokens: 50,
preserve_recent: 2,
preserve_system: true,
strategy: SummarizationStrategy::Summarize,
summarization_model: None,
summarization_prompt: String::new(),
};
let mut conv = CompactingConversation::new(config);
for i in 0..10 {
conv.add_user_message(format!("Message {}", i));
}
if conv.needs_compaction() {
let result = conv.compact();
assert!(result.is_some());
let result = result.unwrap();
assert!(result.summary.is_some());
assert_eq!(conv.summaries().len(), 1);
}
}
#[test]
fn test_messages_include_summary() {
let config = ConversationConfig {
max_tokens: 100,
target_tokens: 50,
preserve_recent: 2,
preserve_system: true,
strategy: SummarizationStrategy::Summarize,
summarization_model: None,
summarization_prompt: String::new(),
};
let mut conv = CompactingConversation::new(config);
for i in 0..10 {
conv.add_user_message(format!("Message {}", i));
}
while conv.needs_compaction() {
conv.compact();
}
let messages = conv.messages();
if !conv.summaries().is_empty() {
assert!(messages.iter().any(|m| m.role == "system"));
}
}
#[test]
fn test_with_system_message() {
let config = small_config();
let mut conv = CompactingConversation::with_system("You are helpful.", config);
assert!(conv.system_message().is_some());
conv.add_user_message("Hello");
let messages = conv.messages();
assert!(messages.iter().any(|m| m.role == "system"));
}
#[test]
fn test_compaction_count() {
let mut conv = CompactingConversation::new(small_config());
for i in 0..10 {
conv.add_user_message(format!("Message {}", i));
}
let mut count = 0;
while conv.needs_compaction() {
conv.compact();
count += 1;
}
assert_eq!(conv.compaction_count(), count);
}
#[test]
fn test_clear() {
let mut conv = CompactingConversation::with_system("System", small_config());
conv.add_user_message("Hello");
for i in 0..10 {
conv.add_user_message(format!("Msg {}", i));
}
while conv.needs_compaction() {
conv.compact();
}
conv.clear(true);
assert!(conv.is_empty());
assert!(conv.summaries().is_empty());
assert!(conv.system_message().is_some());
}
#[test]
fn test_metadata() {
let mut conv = CompactingConversation::with_defaults();
conv.set_metadata("key", "value");
assert_eq!(conv.get_metadata("key"), Some("value"));
}
}