use crate::core::session::{ChatSession, SummaryEntry, SummaryReason}; use crate::llm::conversation::Summarizer; use crate::llm::types::Message; use std::sync::Arc; use tokio::sync::Mutex; /// 触发溢出摘要:压缩 checkpoint 到当前位置之间的新消息 pub async fn trigger_overflow_summary( session: &Arc>, summarizer: Option<&Summarizer>, ) -> bool { let mut s = session.lock().await; if s.messages.is_empty() { return false; } let prev_checkpoint = s.checkpoint; let end = s.messages.len(); // checkpoint 始终在 user+assistant 对边界,直接用它作为摘要起点 let start = prev_checkpoint; if start >= end { return false; } let to_summarize: Vec<_> = s.messages[start..end] .iter() .take(40) // 最多 40 条 .cloned() .collect(); if to_summarize.is_empty() { return false; } let summary_text = generate_summary(&to_summarize, summarizer).await; s.summaries.push(SummaryEntry { checkpoint: prev_checkpoint, text: summary_text.clone(), reason: SummaryReason::Overflow, created_at: chrono::Utc::now(), }); // 持久化到数据库 s.save_summary_to_db(&summary_text, "overflow", to_summarize.len() as i32) .await; s.checkpoint = end; // 清理太旧的消息(checkpoint 之前的保留最近 100 条用于调试) if s.checkpoint > 200 { let keep = s.checkpoint - 100; s.messages.drain(0..keep); // 调整 checkpoint 和 summary checkpoint let shift = keep; s.checkpoint -= shift; for sum in &mut s.summaries { if sum.checkpoint >= shift { sum.checkpoint -= shift; } } } true } /// 触发空闲摘要(不注入,保存到 summaries 供 LLM 工具查询) pub async fn trigger_idle_summary( session: &Arc>, summarizer: Option<&Summarizer>, ) { let mut s = session.lock().await; if s.messages.is_empty() { return; } let summary_text = generate_summary(&s.messages, summarizer).await; let cp = s.messages.len(); s.summaries.push(SummaryEntry { checkpoint: cp, text: summary_text.clone(), reason: SummaryReason::Timeout, created_at: chrono::Utc::now(), }); let msg_count = cp as i32; s.save_summary_to_db(&summary_text, "timeout", msg_count) .await; s.checkpoint = s.messages.len(); // 清理旧消息(checkpoint 之前的全部清除) if s.checkpoint > 0 { let shift = s.checkpoint; s.messages.drain(0..shift); s.checkpoint = 0; for sum in &mut s.summaries { if sum.checkpoint >= shift { sum.checkpoint -= shift; } } } } /// 生成摘要:优先使用 LLM,不可用时回退到简单截断 async fn generate_summary(messages: &[Message], summarizer: Option<&Summarizer>) -> String { if let Some(summarizer) = summarizer && messages.len() >= 3 { let prompt = build_summary_prompt(messages); match summarizer(prompt).await { Ok(text) if !text.is_empty() => { tracing::info!("LLM 摘要: {:.100}...", text); return text; } _ => tracing::warn!("LLM 摘要失败,回退到简单截断"), } } summarize_messages(messages) } /// 构建给 LLM 的摘要 prompt fn build_summary_prompt(messages: &[Message]) -> String { let convo: String = messages .iter() .filter(|m| { m.role == crate::llm::types::Role::User || m.role == crate::llm::types::Role::Assistant }) .map(|m| { let role = if m.role == crate::llm::types::Role::User { "用户" } else { "助手" }; let content: String = m.content.chars().take(200).collect(); format!("{}: {}", role, content) }) .collect::>() .join("\n"); format!( "请将以下对话压缩为简短摘要,保留关键事实、用户偏好、决定和待办事项。只输出摘要,不要其他内容。\n\n对话:\n{}", convo ) } /// 简单的摘要生成(提取最近 N 条消息的关键内容) fn summarize_messages(messages: &[Message]) -> String { // 简单实现:提取用户消息的前 80 个字符作为摘要 let lines: Vec = messages .iter() .filter(|m| { m.role == crate::llm::types::Role::User || m.role == crate::llm::types::Role::Assistant }) .map(|m| { let role = match m.role { crate::llm::types::Role::User => "用户", crate::llm::types::Role::Assistant => "助手", _ => "", }; let preview: String = m.content.chars().take(80).collect(); format!("{}: {}", role, preview) }) .collect(); if lines.is_empty() { "暂无历史对话".to_string() } else { let result = format!( "历史对话摘要({} 条消息):\n{}", lines.len(), lines.join("\n") ); if result.chars().count() > 2000 { format!( "{}...(已截断)", result.chars().take(2000).collect::() ) } else { result } } }