1use std::collections::HashMap;
7
8use super::config::MIN_MEMORY_CONTENT_LENGTH;
9use super::extractor::infer_category_from_content;
10use super::retrieval::extract_context_keywords;
11use super::types::{AutoMemory, MemoryCategory, MemoryEntry};
12
13#[derive(Debug, Clone, PartialEq)]
19pub enum FeedbackAction {
20 Correct,
21 Delete,
22 Add,
23 NegativePreference,
24}
25
26#[derive(Debug, Clone)]
28pub struct FeedbackResult {
29 pub action: FeedbackAction,
30 pub category: Option<MemoryCategory>,
31 pub new_content: Option<String>,
32 pub search_keywords: Vec<String>,
33 pub original_text: String,
34}
35
36pub fn detect_feedback_patterns(text: &str) -> Vec<FeedbackResult> {
39 let mut results = Vec::new();
40 let text_lower = text.to_lowercase();
41
42 let correction_signals = ["不对", "错了", "不是", "no", "wrong", "should be"];
44 for signal in correction_signals {
45 if text_lower.contains(signal) {
46 let content = extract_feedback_content(text, signal);
47 if content.len() >= MIN_MEMORY_CONTENT_LENGTH {
48 results.push(FeedbackResult {
49 action: FeedbackAction::Correct,
50 category: Some(infer_category_from_content(&content)),
51 new_content: Some(content.clone()),
52 search_keywords: extract_context_keywords(&content),
53 original_text: text.to_string(),
54 });
55 break; }
57 }
58 }
59
60 let delete_signals = ["不要", "删掉", "remove", "delete", "don't need"];
62 for signal in delete_signals {
63 if text_lower.contains(signal) {
64 let content = extract_feedback_content(text, signal);
65 results.push(FeedbackResult {
66 action: FeedbackAction::Delete,
67 category: None,
68 new_content: None,
69 search_keywords: if content.is_empty() {
70 vec![signal.to_string()]
71 } else {
72 extract_context_keywords(&content)
73 },
74 original_text: text.to_string(),
75 });
76 break;
77 }
78 }
79
80 let add_signals = ["记住", "记一下", "remember", "note"];
82 for signal in add_signals {
83 if text_lower.contains(signal) {
84 let content = extract_feedback_content(text, signal);
85 if content.len() >= MIN_MEMORY_CONTENT_LENGTH {
86 results.push(FeedbackResult {
87 action: FeedbackAction::Add,
88 category: Some(infer_category_from_content(&content)),
89 new_content: Some(content),
90 search_keywords: vec![],
91 original_text: text.to_string(),
92 });
93 break;
94 }
95 }
96 }
97
98 let negative_signals = ["不喜欢", "讨厌", "dislike", "hate", "don't like"];
100 for signal in negative_signals {
101 if text_lower.contains(signal) {
102 let content = extract_feedback_content(text, signal);
103 if content.len() >= MIN_MEMORY_CONTENT_LENGTH {
104 results.push(FeedbackResult {
105 action: FeedbackAction::NegativePreference,
106 category: Some(MemoryCategory::Preference),
107 new_content: Some(format!("不喜欢: {}", content)),
108 search_keywords: extract_context_keywords(&content),
109 original_text: text.to_string(),
110 });
111 break;
112 }
113 }
114 }
115
116 results
117}
118
119fn extract_feedback_content(text: &str, pattern: &str) -> String {
120 let text_lower = text.to_lowercase();
123 let pattern_lower = pattern.to_lowercase();
124
125 let pos = match text_lower.find(&pattern_lower) {
126 Some(p) => p,
127 None => return String::new(),
128 };
129
130 let char_pos = text_lower[..pos].chars().count();
133 let start_char_idx = char_pos + pattern.chars().count();
134
135 let remaining: String = text.chars().skip(start_char_idx).collect();
137 if remaining.is_empty() {
138 return String::new();
139 }
140
141 let end_char_count = remaining
143 .find(['.', '。', '\n'])
144 .map(|i| remaining[..i].chars().count())
145 .unwrap_or(remaining.chars().count().min(100));
146
147 remaining.chars().take(end_char_count).collect::<String>().trim().to_string()
148}
149
150pub fn apply_feedback_to_memory(memory: &mut AutoMemory, feedback: &FeedbackResult) -> usize {
152 let mut changes = 0;
153
154 match feedback.action {
155 FeedbackAction::Correct => {
156 if let Some(ref content) = feedback.new_content {
157 for entry in &mut memory.entries {
158 if feedback
159 .search_keywords
160 .iter()
161 .any(|k| entry.content.to_lowercase().contains(&k.to_lowercase()))
162 {
163 entry.content = content.clone();
164 entry.importance = entry.importance.max(80.0);
165 changes += 1;
166 }
167 }
168 if changes == 0 {
169 let category = feedback.category.unwrap_or(MemoryCategory::Finding);
170 memory.add_memory(category, content.clone(), None);
171 changes += 1;
172 }
173 }
174 }
175 FeedbackAction::Delete => {
176 let ids_to_delete: Vec<String> = memory
177 .entries
178 .iter()
179 .filter(|e| {
180 feedback
181 .search_keywords
182 .iter()
183 .any(|k| e.content.to_lowercase().contains(&k.to_lowercase()))
184 })
185 .take(3)
186 .map(|e| e.id.clone())
187 .collect();
188
189 for id in ids_to_delete {
190 if memory.remove(&id) {
191 changes += 1;
192 }
193 }
194 }
195 FeedbackAction::Add => {
196 if let Some(ref content) = feedback.new_content {
197 let category = feedback.category.unwrap_or(MemoryCategory::Finding);
198 let entry = MemoryEntry::manual(category, content.clone());
199 memory.add(entry);
200 changes += 1;
201 }
202 }
203 FeedbackAction::NegativePreference => {
204 if let Some(ref content) = feedback.new_content {
205 let mut entry = MemoryEntry::manual(MemoryCategory::Preference, content.clone());
206 entry.tags.push("negative".to_string());
207 memory.add(entry);
208 changes += 1;
209 }
210 }
211 }
212
213 changes
214}
215
216#[derive(Clone)]
222pub struct BehaviorInferenceConfig {
223 pub min_occurrences: usize,
224 pub min_confidence: f64,
225 pub max_inferences: usize,
226}
227
228impl Default for BehaviorInferenceConfig {
229 fn default() -> Self {
230 Self {
231 min_occurrences: 2,
232 min_confidence: 0.6,
233 max_inferences: 5,
234 }
235 }
236}
237
238#[derive(Debug, Clone)]
240pub struct BehaviorInference {
241 pub content: String,
242 pub confidence: f64,
243 pub occurrences: usize,
244 pub keywords: Vec<String>,
245}
246
247pub fn infer_preferences_from_behavior(
250 messages: &[crate::providers::Message],
251 config: &BehaviorInferenceConfig,
252) -> Vec<BehaviorInference> {
253 let user_texts: Vec<String> = messages
254 .iter()
255 .filter_map(|msg| {
256 if msg.role == crate::providers::Role::User {
257 match &msg.content {
258 crate::providers::MessageContent::Text(t) => Some(t.clone()),
259 crate::providers::MessageContent::Blocks(blocks) => Some(
260 blocks
261 .iter()
262 .filter_map(|b| {
263 if let crate::providers::ContentBlock::Text { text } = b {
264 Some(text.as_str())
265 } else {
266 None
267 }
268 })
269 .collect::<Vec<_>>()
270 .join(" "),
271 ),
272 }
273 } else {
274 None
275 }
276 })
277 .collect();
278
279 if user_texts.len() < config.min_occurrences {
280 return Vec::new();
281 }
282
283 let mut word_freq: HashMap<String, usize> = HashMap::new();
285 for text in &user_texts {
286 for word in text.to_lowercase().split_whitespace() {
287 if word.len() > 3 { *word_freq.entry(word.to_string()).or_default() += 1;
289 }
290 }
291 }
292
293 let inferences: Vec<BehaviorInference> = word_freq
295 .iter()
296 .filter(|(_, count)| **count >= config.min_occurrences)
297 .map(|(word, count)| {
298 let confidence = (*count as f64 / user_texts.len() as f64).min(1.0);
299 BehaviorInference {
300 content: format!("用户多次提及 '{}'", word),
301 confidence,
302 occurrences: *count,
303 keywords: vec![word.clone()],
304 }
305 })
306 .filter(|inf| inf.confidence >= config.min_confidence)
307 .take(config.max_inferences)
308 .collect();
309
310 inferences
311}
312
313pub fn inference_to_memory_entry(inference: &BehaviorInference) -> MemoryEntry {
315 let mut entry = MemoryEntry::new(MemoryCategory::Preference, inference.content.clone(), None);
316 entry.importance = (inference.confidence * 70.0 + 30.0).min(80.0);
317 entry.tags = inference.keywords.clone();
318 entry
319}
320
321pub fn apply_behavior_inferences_to_memory(
323 messages: &[crate::providers::Message],
324 memory: &mut AutoMemory,
325 config: Option<&BehaviorInferenceConfig>,
326) -> usize {
327 let cfg = config.cloned().unwrap_or_default();
328 let inferences = infer_preferences_from_behavior(messages, &cfg);
329
330 let mut added = 0;
331 for inference in inferences {
332 let entry = inference_to_memory_entry(&inference);
333 if !memory.entries.iter().any(|e| e.content == entry.content) {
334 memory.entries.push(entry);
335 added += 1;
336 }
337 }
338
339 added
340}
341
342pub fn apply_tool_learning_to_memory(
345 _tool_name: &str,
346 _tool_input: &serde_json::Value,
347 _tool_result: &str,
348 _is_error: bool,
349 _memory: &mut AutoMemory,
350) -> usize {
351 0
354}