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 hardcode_config: HardcodeConfig,
45}
46
47impl Default for SemanticCompressor {
48 fn default() -> Self {
49 Self {
50 hardcode_config: HardcodeConfig::default(),
51 }
52 }
53}
54
55impl SemanticCompressor {
56 pub fn new() -> Self {
57 Self {
58 hardcode_config: HardcodeConfig::default(),
59 }
60 }
61
62 pub fn extract_key_info(message: &Message) -> KeyInfo {
64 let mut info = KeyInfo::default();
65
66 if let MessageContent::Text(text) = &message.content {
68 if text.contains("decided") || text.contains("decision")
70 || text.contains("决定") || text.contains("choose") || text.contains("selected") {
71 info.has_decision = true;
72 }
73
74 if text.contains("error") || text.contains("failed")
76 || text.contains("错误") || text.contains("失败") || text.contains("异常") {
77 info.has_error = true;
78 }
79
80 if text.contains("tool") || text.contains("function") {
82 info.has_tool_use = true;
83 }
84
85 if text.contains("```") || text.contains("fn ") || text.contains("function ") {
87 info.has_code = true;
88 }
89 }
90
91 if let MessageContent::Blocks(blocks) = &message.content {
93 for block in blocks {
94 match block {
95 crate::providers::ContentBlock::ToolUse { name, .. } => {
96 info.tool_names.push(name.clone());
97 info.has_tool_use = true;
98 }
99 crate::providers::ContentBlock::ToolResult { content, .. } => {
100 if content.contains("error") || content.contains("failed") {
101 info.has_error = true;
102 }
103 }
104 _ => {}
105 }
106 }
107 }
108
109 info
110 }
111
112 pub fn should_summarize(&self, messages: &[Message]) -> bool {
114 if messages.is_empty() {
115 return false;
116 }
117
118 let has_substantial_content = messages.iter().any(|m| {
120 matches!(&m.content, MessageContent::Text(t) if t.len() > self.hardcode_config.summary_length_threshold)
121 });
122
123 has_substantial_content && messages.len() >= 3
125 }
126
127 pub fn create_summary_prompt(messages: &[Message]) -> String {
129 let mut conversation = String::new();
130
131 for msg in messages {
132 let role = match msg.role {
133 Role::User => "用户",
134 Role::Assistant => "助手",
135 Role::System => "系统",
136 Role::Tool => "工具",
137 };
138
139 if let MessageContent::Text(text) = &msg.content {
140 conversation.push_str(&format!("{}: {}\n", role, text));
141 } else if let MessageContent::Blocks(blocks) = &msg.content {
142 for block in blocks {
143 if let crate::providers::ContentBlock::Text { text } = block {
144 conversation.push_str(&format!("{}: {}\n", role, text));
145 }
146 }
147 }
148 }
149
150 SUMMARY_PROMPT.replace("{conversation}", &conversation)
151 }
152
153 pub fn create_summary_message(summary: ConversationSummary) -> Message {
155 let mut content = String::new();
156 content.push_str("📝 **对话摘要**\n\n");
157
158 if !summary.decisions.is_empty() {
159 content.push_str("**决策:**\n");
160 for decision in &summary.decisions {
161 content.push_str(&format!("- {}\n", decision));
162 }
163 content.push('\n');
164 }
165
166 if !summary.facts.is_empty() {
167 content.push_str("**关键事实:**\n");
168 for fact in &summary.facts {
169 content.push_str(&format!("- {}\n", fact));
170 }
171 content.push('\n');
172 }
173
174 if !summary.tool_usage.is_empty() {
175 content.push_str("**使用的工具:**\n");
176 for tool in &summary.tool_usage {
177 content.push_str(&format!("- {}: {}\n", tool.tool_name, tool.outcome));
178 }
179 content.push('\n');
180 }
181
182 if !summary.issues.is_empty() {
183 content.push_str("**解决的问题:**\n");
184 for issue in &summary.issues {
185 content.push_str(&format!("- 问题: {}\n 解决: {}\n", issue.problem, issue.solution));
186 }
187 content.push('\n');
188 }
189
190 content.push_str(&format!("**Overall:** {}", summary.summary));
191
192 Message {
193 role: Role::System,
194 content: MessageContent::Text(content),
195 }
196 }
197}
198
199#[derive(Debug, Default)]
201pub struct KeyInfo {
202 pub has_decision: bool,
203 pub has_error: bool,
204 pub has_tool_use: bool,
205 pub has_code: bool,
206 pub tool_names: Vec<String>,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub enum SemanticStrategy {
212 None,
214 OldOnly,
216 Aggressive,
218}
219
220impl Default for SemanticStrategy {
221 fn default() -> Self {
222 Self::OldOnly
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::providers::{ContentBlock, Message, MessageContent, Role};
230
231 #[test]
232 fn test_extract_key_info_decision() {
233 let msg = Message {
234 role: Role::Assistant,
235 content: MessageContent::Text("I decided to use Rust for the project.".to_string()),
236 };
237 let info = SemanticCompressor::extract_key_info(&msg);
238 assert!(info.has_decision);
239 }
240
241 #[test]
242 fn test_extract_key_info_error() {
243 let msg = Message {
244 role: Role::Assistant,
245 content: MessageContent::Text("The operation failed with error code 404.".to_string()),
246 };
247 let info = SemanticCompressor::extract_key_info(&msg);
248 assert!(info.has_error);
249 }
250
251 #[test]
252 fn test_extract_key_info_tool() {
253 let msg = Message {
254 role: Role::Assistant,
255 content: MessageContent::Blocks(vec![ContentBlock::ToolUse {
256 id: "tool_1".to_string(),
257 name: "bash".to_string(),
258 input: serde_json::json!({"command": "ls"}),
259 }]),
260 };
261 let info = SemanticCompressor::extract_key_info(&msg);
262 assert!(info.has_tool_use);
263 assert!(info.tool_names.contains(&"bash".to_string()));
264 }
265
266 #[test]
267 fn test_should_summarize() {
268 let messages = vec![Message {
270 role: Role::User,
271 content: MessageContent::Text("Hello".to_string()),
272 }];
273 let compressor = SemanticCompressor::default();
274 assert!(!compressor.should_summarize(&messages));
275
276 let messages = vec![
278 Message {
279 role: Role::User,
280 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()),
281 },
282 Message {
283 role: Role::Assistant,
284 content: MessageContent::Text("Response 1".to_string()),
285 },
286 Message {
287 role: Role::User,
288 content: MessageContent::Text("Query 2".to_string()),
289 },
290 ];
291 let compressor = SemanticCompressor::default();
292 assert!(compressor.should_summarize(&messages));
293 }
294
295 #[test]
296 fn test_create_summary_message() {
297 let summary = ConversationSummary {
298 decisions: vec!["Use Rust for backend".to_string()],
299 facts: vec!["Project uses PostgreSQL".to_string()],
300 tool_usage: vec![ToolUsage {
301 tool_name: "bash".to_string(),
302 purpose: "Run tests".to_string(),
303 outcome: "All tests passed".to_string(),
304 }],
305 issues: vec![Issue {
306 problem: "Compilation error".to_string(),
307 solution: "Fixed missing import".to_string(),
308 }],
309 summary: "Completed initial setup and testing.".to_string(),
310 };
311
312 let msg = SemanticCompressor::create_summary_message(summary);
313 assert!(matches!(msg.role, Role::System));
314
315 if let MessageContent::Text(text) = &msg.content {
316 assert!(text.contains("决策"));
317 assert!(text.contains("关键事实"));
318 assert!(text.contains("使用的工具"));
319 assert!(text.contains("解决的问题"));
320 } else {
321 panic!("Expected text content");
322 }
323 }
324}