matrixcode_core/compress/
semantic.rs1use crate::providers::{Message, MessageContent, Role};
7use crate::compress::hardcode_config::HardcodeConfig;
8use super::prompts_zh::SUMMARY_PROMPT;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ConversationSummary {
14 pub decisions: Vec<String>,
16 pub facts: Vec<String>,
18 pub tool_usage: Vec<ToolUsage>,
20 pub issues: Vec<Issue>,
22 pub summary: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ToolUsage {
29 pub tool_name: String,
30 pub purpose: String,
31 pub outcome: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Issue {
37 pub problem: String,
38 pub solution: String,
39}
40
41pub struct SemanticCompressor {
43 min_tokens_for_summary: u32,
45 target_ratio: f32,
47 hardcode_config: HardcodeConfig,
49}
50
51impl Default for SemanticCompressor {
52 fn default() -> Self {
53 Self {
54 min_tokens_for_summary: 1000, target_ratio: 0.3, hardcode_config: HardcodeConfig::default(),
57 }
58 }
59}
60
61impl SemanticCompressor {
62 pub fn new(min_tokens: u32, target_ratio: f32) -> Self {
63 Self {
64 min_tokens_for_summary: min_tokens,
65 target_ratio,
66 hardcode_config: HardcodeConfig::default(),
67 }
68 }
69
70 pub fn extract_key_info(message: &Message) -> KeyInfo {
72 let mut info = KeyInfo::default();
73
74 if let MessageContent::Text(text) = &message.content {
76 if text.contains("decided") || text.contains("decision")
78 || text.contains("决定") || text.contains("choose") || text.contains("selected") {
79 info.has_decision = true;
80 }
81
82 if text.contains("error") || text.contains("failed")
84 || text.contains("错误") || text.contains("失败") || text.contains("异常") {
85 info.has_error = true;
86 }
87
88 if text.contains("tool") || text.contains("function") {
90 info.has_tool_use = true;
91 }
92
93 if text.contains("```") || text.contains("fn ") || text.contains("function ") {
95 info.has_code = true;
96 }
97 }
98
99 if let MessageContent::Blocks(blocks) = &message.content {
101 for block in blocks {
102 match block {
103 crate::providers::ContentBlock::ToolUse { name, .. } => {
104 info.tool_names.push(name.clone());
105 info.has_tool_use = true;
106 }
107 crate::providers::ContentBlock::ToolResult { content, .. } => {
108 if content.contains("error") || content.contains("failed") {
109 info.has_error = true;
110 }
111 }
112 _ => {}
113 }
114 }
115 }
116
117 info
118 }
119
120 pub fn should_summarize(&self, messages: &[Message]) -> bool {
122 if messages.is_empty() {
123 return false;
124 }
125
126 let has_substantial_content = messages.iter().any(|m| {
128 matches!(&m.content, MessageContent::Text(t) if t.len() > self.hardcode_config.summary_length_threshold)
129 });
130
131 has_substantial_content && messages.len() >= 3
133 }
134
135 pub fn create_summary_prompt(messages: &[Message]) -> String {
137 let mut conversation = String::new();
138
139 for msg in messages {
140 let role = match msg.role {
141 Role::User => "用户",
142 Role::Assistant => "助手",
143 Role::System => "系统",
144 Role::Tool => "工具",
145 };
146
147 if let MessageContent::Text(text) = &msg.content {
148 conversation.push_str(&format!("{}: {}\n", role, text));
149 } else if let MessageContent::Blocks(blocks) = &msg.content {
150 for block in blocks {
151 if let crate::providers::ContentBlock::Text { text } = block {
152 conversation.push_str(&format!("{}: {}\n", role, text));
153 }
154 }
155 }
156 }
157
158 SUMMARY_PROMPT.replace("{conversation}", &conversation)
159 }
160
161 pub fn create_summary_message(summary: ConversationSummary) -> Message {
163 let mut content = String::new();
164 content.push_str("📝 **对话摘要**\n\n");
165
166 if !summary.decisions.is_empty() {
167 content.push_str("**决策:**\n");
168 for decision in &summary.decisions {
169 content.push_str(&format!("- {}\n", decision));
170 }
171 content.push('\n');
172 }
173
174 if !summary.facts.is_empty() {
175 content.push_str("**关键事实:**\n");
176 for fact in &summary.facts {
177 content.push_str(&format!("- {}\n", fact));
178 }
179 content.push('\n');
180 }
181
182 if !summary.tool_usage.is_empty() {
183 content.push_str("**使用的工具:**\n");
184 for tool in &summary.tool_usage {
185 content.push_str(&format!("- {}: {}\n", tool.tool_name, tool.outcome));
186 }
187 content.push('\n');
188 }
189
190 if !summary.issues.is_empty() {
191 content.push_str("**解决的问题:**\n");
192 for issue in &summary.issues {
193 content.push_str(&format!("- 问题: {}\n 解决: {}\n", issue.problem, issue.solution));
194 }
195 content.push('\n');
196 }
197
198 content.push_str(&format!("**Overall:** {}", summary.summary));
199
200 Message {
201 role: Role::System,
202 content: MessageContent::Text(content),
203 }
204 }
205}
206
207#[derive(Debug, Default)]
209pub struct KeyInfo {
210 pub has_decision: bool,
211 pub has_error: bool,
212 pub has_tool_use: bool,
213 pub has_code: bool,
214 pub tool_names: Vec<String>,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum SemanticStrategy {
220 None,
222 OldOnly,
224 Aggressive,
226}
227
228impl Default for SemanticStrategy {
229 fn default() -> Self {
230 Self::OldOnly
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::providers::{ContentBlock, Message, MessageContent, Role};
238
239 #[test]
240 fn test_extract_key_info_decision() {
241 let msg = Message {
242 role: Role::Assistant,
243 content: MessageContent::Text("I decided to use Rust for the project.".to_string()),
244 };
245 let info = SemanticCompressor::extract_key_info(&msg);
246 assert!(info.has_decision);
247 }
248
249 #[test]
250 fn test_extract_key_info_error() {
251 let msg = Message {
252 role: Role::Assistant,
253 content: MessageContent::Text("The operation failed with error code 404.".to_string()),
254 };
255 let info = SemanticCompressor::extract_key_info(&msg);
256 assert!(info.has_error);
257 }
258
259 #[test]
260 fn test_extract_key_info_tool() {
261 let msg = Message {
262 role: Role::Assistant,
263 content: MessageContent::Blocks(vec![ContentBlock::ToolUse {
264 id: "tool_1".to_string(),
265 name: "bash".to_string(),
266 input: serde_json::json!({"command": "ls"}),
267 }]),
268 };
269 let info = SemanticCompressor::extract_key_info(&msg);
270 assert!(info.has_tool_use);
271 assert!(info.tool_names.contains(&"bash".to_string()));
272 }
273
274 #[test]
275 fn test_should_summarize() {
276 let messages = vec![Message {
278 role: Role::User,
279 content: MessageContent::Text("Hello".to_string()),
280 }];
281 let compressor = SemanticCompressor::default();
282 assert!(!compressor.should_summarize(&messages));
283
284 let messages = vec![
286 Message {
287 role: Role::User,
288 content: MessageContent::Text("This is a longer message with more than two hundred characters to test the substantial content check. We need to make sure it's long enough. Adding more text to ensure the message has sufficient length for the test requirement.".to_string()),
289 },
290 Message {
291 role: Role::Assistant,
292 content: MessageContent::Text("Response 1".to_string()),
293 },
294 Message {
295 role: Role::User,
296 content: MessageContent::Text("Query 2".to_string()),
297 },
298 ];
299 let compressor = SemanticCompressor::default();
300 assert!(compressor.should_summarize(&messages));
301 }
302
303 #[test]
304 fn test_create_summary_message() {
305 let summary = ConversationSummary {
306 decisions: vec!["Use Rust for backend".to_string()],
307 facts: vec!["Project uses PostgreSQL".to_string()],
308 tool_usage: vec![ToolUsage {
309 tool_name: "bash".to_string(),
310 purpose: "Run tests".to_string(),
311 outcome: "All tests passed".to_string(),
312 }],
313 issues: vec![Issue {
314 problem: "Compilation error".to_string(),
315 solution: "Fixed missing import".to_string(),
316 }],
317 summary: "Completed initial setup and testing.".to_string(),
318 };
319
320 let msg = SemanticCompressor::create_summary_message(summary);
321 assert!(matches!(msg.role, Role::System));
322
323 if let MessageContent::Text(text) = &msg.content {
324 assert!(text.contains("决策"));
325 assert!(text.contains("关键事实"));
326 assert!(text.contains("使用的工具"));
327 assert!(text.contains("解决的问题"));
328 } else {
329 panic!("Expected text content");
330 }
331 }
332}