matrixcode_core/compress/
summarizer.rs1use anyhow::Result;
7
8use crate::providers::{ContentBlock, ChatRequest, Message, MessageContent, Provider, Role};
9use crate::truncate::truncate_with_suffix;
10
11pub struct Summarizer {
13 fast_model: Box<dyn Provider>,
15 main_model: Option<Box<dyn Provider>>,
17}
18
19impl Summarizer {
20 pub fn new(fast_model: Box<dyn Provider>) -> Self {
22 Self {
23 fast_model,
24 main_model: None,
25 }
26 }
27
28 pub fn new_with_main(fast_model: Box<dyn Provider>, main_model: Box<dyn Provider>) -> Self {
30 Self {
31 fast_model,
32 main_model: Some(main_model),
33 }
34 }
35
36 pub async fn summarize_light(&self, content: &str) -> Result<String> {
38 let truncated = truncate_with_suffix(content, 3000);
39 let prompt = format!(
40 "将以下内容压缩为简洁摘要(保留关键信息,200字以内):\n{}",
41 truncated
42 );
43
44 let response = self.fast_model.chat(build_summary_request(prompt)).await?;
45 let summary = extract_summary_text(&response);
46
47 Ok(summary)
48 }
49
50 pub async fn summarize_deep(&self, content: &str) -> Result<String> {
52 let truncated = truncate_with_suffix(content, 5000);
53 let prompt = format!(
54 "将以下内容压缩为详细摘要(保留所有重要细节,500字以内):\n{}",
55 truncated
56 );
57
58 let model = self.main_model.as_ref().unwrap_or(&self.fast_model);
59 let response = model.chat(build_summary_request(prompt)).await?;
60 let summary = extract_summary_text(&response);
61
62 Ok(summary)
63 }
64
65 pub fn smart_truncate(content: &str, target_tokens: u32) -> String {
67 let estimated_chars = (target_tokens as f64 * 3.0) as usize;
69 truncate_with_suffix(content, estimated_chars)
70 }
71
72 pub fn truncate_preserve_ends(content: &str, target_tokens: u32) -> String {
74 let estimated_chars = (target_tokens as f64 * 3.0) as usize;
75
76 if content.len() <= estimated_chars {
77 return content.to_string();
78 }
79
80 let begin_len = (estimated_chars as f64 * 0.6) as usize;
82 let end_len = estimated_chars.saturating_sub(begin_len).saturating_sub(20); if end_len == 0 {
85 return truncate_with_suffix(content, estimated_chars);
87 }
88
89 let begin = truncate_with_suffix(content, begin_len);
90 let end_start = content.len().saturating_sub(end_len);
91
92 let end_start = find_char_boundary(content, end_start);
94 let end = &content[end_start..];
95
96 format!("{}...\n[内容截断]\n...{}", begin, end)
97 }
98
99 pub fn needs_summary(content: &str, threshold_tokens: u32) -> bool {
101 estimate_tokens_str(content) >= threshold_tokens
102 }
103}
104
105fn build_summary_request(prompt: String) -> ChatRequest {
107 ChatRequest {
108 messages: vec![Message {
109 role: Role::User,
110 content: MessageContent::Text(prompt),
111 }],
112 tools: vec![],
113 system: Some(SUMMARY_SYSTEM_PROMPT.to_string()),
114 think: false,
115 max_tokens: 512,
116 server_tools: vec![],
117 enable_caching: false,
118 }
119}
120
121fn extract_summary_text(response: &crate::providers::ChatResponse) -> String {
123 response.content.iter()
124 .filter_map(|b| {
125 if let ContentBlock::Text { text } = b {
126 Some(text.clone())
127 } else {
128 None
129 }
130 })
131 .collect::<Vec<_>>()
132 .join("\n")
133}
134
135fn find_char_boundary(s: &str, max: usize) -> usize {
137 let max = max.min(s.len());
138 let mut end = max;
139 while end > 0 && !s.is_char_boundary(end) {
140 end -= 1;
141 }
142 end
143}
144
145fn estimate_tokens_str(s: &str) -> u32 {
147 let (ascii, non_ascii) = count_chars(s);
148 let ascii_tokens = (ascii as f64 * 0.25).ceil() as u32;
149 let non_ascii_tokens = (non_ascii as f64 * 0.67).ceil() as u32;
150 ascii_tokens + non_ascii_tokens
151}
152
153fn count_chars(s: &str) -> (u32, u32) {
155 let mut ascii = 0u32;
156 let mut non_ascii = 0u32;
157 for ch in s.chars() {
158 if ch.is_ascii() {
159 ascii += 1;
160 } else {
161 non_ascii += 1;
162 }
163 }
164 (ascii, non_ascii)
165}
166
167const SUMMARY_SYSTEM_PROMPT: &str = r#"你是一个内容摘要助手。将长内容压缩为结构化摘要。
168
169输出要求:
170- 结构化:使用关键信息列表格式
171- 关键:保留重要操作、决策、结果
172- 简洁:控制在指定字数以内
173
174输出格式:
175【操作】执行的主要操作
176【结果】关键输出或结果
177【要点】重要发现或注意事项
178
179请直接输出摘要内容。"#;
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_smart_truncate() {
187 let content = "这是一段很长的内容需要截断处理";
188 let result = Summarizer::smart_truncate(content, 5);
189 assert!(result.len() <= 20);
190 assert!(result.ends_with("..."));
191 }
192
193 #[test]
194 fn test_truncate_preserve_ends() {
195 let content = "开头内容中间很长的部分结尾内容".repeat(50);
197 let result = Summarizer::truncate_preserve_ends(&content, 100);
198 assert!(result.contains("开头"));
199 assert!(result.contains("结尾"));
200 assert!(result.contains("[内容截断]"));
201 }
202
203 #[test]
204 fn test_needs_summary() {
205 let short = "短内容";
206 assert!(!Summarizer::needs_summary(short, 100));
207
208 let long = "这是一段很长的内容...".repeat(100);
209 assert!(Summarizer::needs_summary(&long, 100));
210 }
211
212 #[test]
213 fn test_estimate_tokens_str() {
214 let ascii = "hello world";
215 let tokens = estimate_tokens_str(ascii);
216 assert!(tokens > 0 && tokens < 10);
217
218 let chinese = "你好世界";
219 let tokens = estimate_tokens_str(chinese);
220 assert!(tokens > 0);
221 }
222}