Skip to main content

matrixcode_core/memory/
learning.rs

1//! Feedback learning and behavior inference.
2//!
3//! This module provides high-level learning mechanisms that let the model
4//! understand patterns from interactions, rather than hardcoded rules.
5
6use 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// ============================================================================
14// Feedback Detection
15// ============================================================================
16
17/// Action to take when user feedback is detected.
18#[derive(Debug, Clone, PartialEq)]
19pub enum FeedbackAction {
20    Correct,
21    Delete,
22    Add,
23    NegativePreference,
24}
25
26/// Result of feedback detection.
27#[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
36/// Detect user feedback patterns - generic detection, not exhaustive.
37/// The model should use its understanding to detect nuances.
38pub fn detect_feedback_patterns(text: &str) -> Vec<FeedbackResult> {
39    let mut results = Vec::new();
40    let text_lower = text.to_lowercase();
41
42    // Generic correction signals
43    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; // Only one correction per message
56            }
57        }
58    }
59
60    // Generic delete signals
61    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    // Generic add signals
81    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    // Generic negative preference signals
99    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    // Use case-insensitive search but track position in original text
121    // to avoid Unicode byte length mismatches from lowercase conversion
122    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    // Find the actual position in original text by counting chars
131    // (lowercase conversion can change byte lengths for some Unicode chars)
132    let char_pos = text_lower[..pos].chars().count();
133    let start_char_idx = char_pos + pattern.chars().count();
134
135    // Get remaining text by char indices
136    let remaining: String = text.chars().skip(start_char_idx).collect();
137    if remaining.is_empty() {
138        return String::new();
139    }
140
141    // Find end delimiter (first ., 。, or \n, or up to 100 chars)
142    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
150/// Apply feedback to memory.
151pub 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// ============================================================================
217// Behavior Inference - Generic Pattern Detection
218// ============================================================================
219
220/// Configuration for behavior inference.
221#[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/// Result of behavior inference.
239#[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
247/// Infer patterns from behavior - generic word frequency analysis.
248/// Let the model decide what's meaningful, not hardcoded tech patterns.
249pub 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    // Generic word frequency analysis
284    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 { // Skip short words
288                *word_freq.entry(word.to_string()).or_default() += 1;
289            }
290        }
291    }
292
293    // Extract high-frequency words as potential preferences
294    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
313/// Convert inference to memory entry.
314pub 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
321/// Apply behavior inferences to memory.
322pub 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
342/// Generic tool learning - let model decide what to remember.
343/// This is a placeholder for future AI-driven learning.
344pub 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    // Future: Use AI to analyze tool execution and extract learnings
352    // Current: Let the model handle this through its own analysis
353    0
354}