187 lines
5.5 KiB
Rust
187 lines
5.5 KiB
Rust
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<Mutex<ChatSession>>,
|
|
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<Mutex<ChatSession>>,
|
|
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::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
format!(
|
|
"请将以下对话压缩为简短摘要,保留关键事实、用户偏好、决定和待办事项。只输出摘要,不要其他内容。\n\n对话:\n{}",
|
|
convo
|
|
)
|
|
}
|
|
|
|
/// 简单的摘要生成(提取最近 N 条消息的关键内容)
|
|
fn summarize_messages(messages: &[Message]) -> String {
|
|
// 简单实现:提取用户消息的前 80 个字符作为摘要
|
|
let lines: Vec<String> = 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::<String>()
|
|
)
|
|
} else {
|
|
result
|
|
}
|
|
}
|
|
}
|