use crate::session::Message;
use anyhow::Result;
pub struct SmartSummarizer;
impl SmartSummarizer {
pub fn new() -> Self {
Self
}
pub fn summarize_messages(&self, messages: &[Message]) -> Result<String> {
if messages.is_empty() {
return Ok("No messages to summarize.".to_string());
}
let mut conversation_flow = Vec::new();
let mut technical_context = Vec::new();
let mut file_modifications = Vec::new();
let mut tool_usage = Vec::new();
let mut key_decisions = Vec::new();
for msg in messages {
match msg.role.as_str() {
"system" => {
continue;
}
"user" => {
conversation_flow
.push(format!("User: {}", self.extract_key_points(&msg.content)));
if self.contains_technical_content(&msg.content) {
technical_context.push(self.extract_technical_info(&msg.content));
}
}
"assistant" => {
conversation_flow.push(format!(
"Assistant: {}",
self.extract_key_points(&msg.content)
));
if self.contains_file_modifications(&msg.content) {
file_modifications.push(self.extract_file_info(&msg.content));
}
if self.contains_decisions(&msg.content) {
key_decisions.push(self.extract_decisions(&msg.content));
}
}
"tool" => {
tool_usage.push(self.extract_tool_summary(&msg.content));
}
_ => {
conversation_flow.push(format!(
"{}: {}",
msg.role,
self.extract_key_points(&msg.content)
));
}
}
}
let mut summary_parts = Vec::new();
if !conversation_flow.is_empty() {
summary_parts.push("Conversation Overview:".to_string());
let points_to_include = std::cmp::min(5, conversation_flow.len());
for (i, point) in conversation_flow.iter().take(points_to_include).enumerate() {
summary_parts.push(format!("{}. {}", i + 1, point));
}
}
if !technical_context.is_empty() {
summary_parts.push("\nTechnical Context:".to_string());
for (i, context) in technical_context.iter().take(3).enumerate() {
summary_parts.push(format!("{}. {}", i + 1, context));
}
}
if !file_modifications.is_empty() {
summary_parts.push("\nFile Modifications:".to_string());
for (i, modification) in file_modifications.iter().take(3).enumerate() {
summary_parts.push(format!("{}. {}", i + 1, modification));
}
}
if !key_decisions.is_empty() {
summary_parts.push("\nKey Decisions:".to_string());
for (i, decision) in key_decisions.iter().take(3).enumerate() {
summary_parts.push(format!("{}. {}", i + 1, decision));
}
}
if !tool_usage.is_empty() {
summary_parts.push("\nTool Usage:".to_string());
summary_parts.push(format!(
"Used {} development tools: {}",
tool_usage.len(),
tool_usage.join(", ")
));
}
Ok(summary_parts.join("\n"))
}
fn contains_technical_content(&self, content: &str) -> bool {
let technical_keywords = [
"function",
"class",
"method",
"variable",
"import",
"export",
"struct",
"enum",
"trait",
"impl",
"mod",
"use",
"pub",
"async",
"await",
"Result",
"Error",
"Ok",
"Err",
"config",
"configuration",
"setup",
"install",
"deploy",
"api",
"endpoint",
"request",
"response",
"http",
"json",
"database",
"query",
"sql",
"table",
"index",
"test",
"testing",
"unit test",
"integration",
"bug",
"fix",
"issue",
"error",
"exception",
"refactor",
"optimize",
"performance",
"memory",
"security",
"authentication",
"authorization",
"docker",
"kubernetes",
"deployment",
"ci/cd",
];
let content_lower = content.to_lowercase();
technical_keywords
.iter()
.any(|keyword| content_lower.contains(keyword))
}
fn contains_file_modifications(&self, content: &str) -> bool {
let file_keywords = [
"created",
"modified",
"updated",
"changed",
"edited",
"added",
"removed",
"deleted",
"renamed",
"moved",
"file",
"directory",
"folder",
"path",
".rs",
".toml",
".json",
".yaml",
".md",
".txt",
"src/",
"tests/",
"docs/",
"examples/",
];
let content_lower = content.to_lowercase();
file_keywords
.iter()
.any(|keyword| content_lower.contains(keyword))
}
fn contains_decisions(&self, content: &str) -> bool {
let decision_keywords = [
"decided",
"choose",
"selected",
"option",
"approach",
"solution",
"resolved",
"implemented",
"strategy",
"recommend",
"suggest",
"best practice",
"should",
"will use",
"going with",
"final",
"conclusion",
];
let content_lower = content.to_lowercase();
decision_keywords
.iter()
.any(|keyword| content_lower.contains(keyword))
}
fn extract_key_points(&self, content: &str) -> String {
let sentences: Vec<&str> = content.split('.').collect();
if let Some(first_sentence) = sentences.first() {
if first_sentence.chars().count() <= 150 {
first_sentence.trim().to_string()
} else {
let truncated: String = first_sentence.chars().take(147).collect();
format!("{}...", truncated.trim())
}
} else if content.chars().count() <= 150 {
content.trim().to_string()
} else {
let truncated: String = content.chars().take(147).collect();
format!("{}...", truncated.trim())
}
}
fn extract_technical_info(&self, content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
for line in &lines {
if line.contains("```")
|| line.contains("fn ")
|| line.contains("struct ")
|| line.contains("impl ")
|| line.contains("use ")
{
return self.extract_key_points(line);
}
}
self.extract_key_points(content)
}
fn extract_file_info(&self, content: &str) -> String {
let words: Vec<&str> = content.split_whitespace().collect();
let mut file_info = Vec::new();
for window in words.windows(3) {
if let [action, _, file] = window {
if ["created", "modified", "updated", "added", "removed"].contains(action)
&& (file.contains('.') || file.contains('/'))
{
file_info.push(format!("{} {}", action, file));
break;
}
}
}
if file_info.is_empty() {
self.extract_key_points(content)
} else {
file_info.join(", ")
}
}
fn extract_decisions(&self, content: &str) -> String {
let sentences: Vec<&str> = content.split('.').collect();
for sentence in &sentences {
if self.contains_decisions(sentence) {
return self.extract_key_points(sentence);
}
}
self.extract_key_points(content)
}
fn extract_tool_summary(&self, content: &str) -> String {
if content.chars().count() > 50 {
let truncated: String = content.chars().take(47).collect();
format!("tool execution ({}...)", truncated)
} else {
format!("tool execution ({})", content)
}
}
}
impl Default for SmartSummarizer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_summarize_empty_messages() {
let summarizer = SmartSummarizer::new();
let result = summarizer.summarize_messages(&[]).unwrap();
assert_eq!(result, "No messages to summarize.");
}
#[test]
fn test_contains_technical_content() {
let summarizer = SmartSummarizer::new();
assert!(summarizer.contains_technical_content("Let's create a new function"));
assert!(summarizer.contains_technical_content("Update the config file"));
assert!(summarizer.contains_technical_content("Fix the API endpoint"));
assert!(!summarizer.contains_technical_content("Hello, how are you?"));
}
#[test]
fn test_contains_file_modifications() {
let summarizer = SmartSummarizer::new();
assert!(summarizer.contains_file_modifications("I created a new file"));
assert!(summarizer.contains_file_modifications("Modified src/main.rs"));
assert!(summarizer.contains_file_modifications("Updated the .toml configuration"));
assert!(!summarizer.contains_file_modifications("Just talking about code"));
}
#[test]
fn test_summarize_simple_conversation() {
let summarizer = SmartSummarizer::new();
let messages = vec![
Message {
role: "user".to_string(),
content: "Can you help me create a function to parse JSON?".to_string(),
timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
cached: false,
cache_ttl: None,
tool_call_id: None,
name: None,
tool_calls: None,
images: None,
videos: None,
thinking: None,
id: None,
},
Message {
role: "assistant".to_string(),
content: "I'll help you create a JSON parsing function. Let me create a new file for this.".to_string(),
timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
cached: false,
cache_ttl: None,
tool_call_id: None,
name: None,
tool_calls: None,
images: None,
videos: None,
thinking: None,
id: None,
},
];
let result = summarizer.summarize_messages(&messages).unwrap();
assert!(result.contains("function"));
assert!(result.contains("JSON") || result.contains("json"));
}
}