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