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::entry::{MemoryCategory, MemoryEntry};
10use super::extractor::infer_category_from_content;
11use super::manager::AutoMemory;
12use super::retrieval::extract_context_keywords;
13
14// ============================================================================
15// Feedback Detection
16// ============================================================================
17
18/// Action to take when user feedback is detected.
19#[derive(Debug, Clone, PartialEq)]
20pub enum FeedbackAction {
21    Correct,
22    Delete,
23    Add,
24    NegativePreference,
25}
26
27/// Result of feedback detection.
28#[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
37/// Detect user feedback patterns - generic detection, not exhaustive.
38/// The model should use its understanding to detect nuances.
39pub fn detect_feedback_patterns(text: &str) -> Vec<FeedbackResult> {
40    let mut results = Vec::new();
41    let text_lower = text.to_lowercase();
42
43    // Generic correction signals
44    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; // Only one correction per message
57            }
58        }
59    }
60
61    // Generic delete signals
62    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    // Generic add signals
82    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    // Generic negative preference signals
100    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    // Use case-insensitive search but track position in original text
122    // to avoid Unicode byte length mismatches from lowercase conversion
123    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    // Find the actual position in original text by counting chars
132    // (lowercase conversion can change byte lengths for some Unicode chars)
133    let char_pos = text_lower[..pos].chars().count();
134    let start_char_idx = char_pos + pattern.chars().count();
135
136    // Get remaining text by char indices
137    let remaining: String = text.chars().skip(start_char_idx).collect();
138    if remaining.is_empty() {
139        return String::new();
140    }
141
142    // Find end delimiter (first ., 。, or \n, or up to 100 chars)
143    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
149        .chars()
150        .take(end_char_count)
151        .collect::<String>()
152        .trim()
153        .to_string()
154}
155
156/// Apply feedback to memory.
157pub fn apply_feedback_to_memory(memory: &mut AutoMemory, feedback: &FeedbackResult) -> usize {
158    let mut changes = 0;
159
160    match feedback.action {
161        FeedbackAction::Correct => {
162            if let Some(ref content) = feedback.new_content {
163                for entry in &mut memory.entries {
164                    if feedback
165                        .search_keywords
166                        .iter()
167                        .any(|k| entry.content.to_lowercase().contains(&k.to_lowercase()))
168                    {
169                        entry.content = content.clone();
170                        entry.importance = entry.importance.max(80.0);
171                        changes += 1;
172                    }
173                }
174                if changes == 0 {
175                    let category = feedback.category.unwrap_or(MemoryCategory::Finding);
176                    memory.add_memory(category, content.clone(), None);
177                    changes += 1;
178                }
179            }
180        }
181        FeedbackAction::Delete => {
182            let ids_to_delete: Vec<String> = memory
183                .entries
184                .iter()
185                .filter(|e| {
186                    feedback
187                        .search_keywords
188                        .iter()
189                        .any(|k| e.content.to_lowercase().contains(&k.to_lowercase()))
190                })
191                .take(3)
192                .map(|e| e.id.clone())
193                .collect();
194
195            for id in ids_to_delete {
196                if memory.remove(&id) {
197                    changes += 1;
198                }
199            }
200        }
201        FeedbackAction::Add => {
202            if let Some(ref content) = feedback.new_content {
203                let category = feedback.category.unwrap_or(MemoryCategory::Finding);
204                let entry = MemoryEntry::manual_global(category, content.clone());
205                memory.add(entry);
206                changes += 1;
207            }
208        }
209        FeedbackAction::NegativePreference => {
210            if let Some(ref content) = feedback.new_content {
211                let mut entry =
212                    MemoryEntry::manual_global(MemoryCategory::Preference, content.clone());
213                entry.tags.push("negative".to_string());
214                memory.add(entry);
215                changes += 1;
216            }
217        }
218    }
219
220    changes
221}
222
223// ============================================================================
224// Behavior Inference - Generic Pattern Detection
225// ============================================================================
226
227/// Configuration for behavior inference.
228#[derive(Clone)]
229pub struct BehaviorInferenceConfig {
230    pub min_occurrences: usize,
231    pub min_confidence: f64,
232    pub max_inferences: usize,
233}
234
235impl Default for BehaviorInferenceConfig {
236    fn default() -> Self {
237        Self {
238            min_occurrences: 2,
239            min_confidence: 0.6,
240            max_inferences: 5,
241        }
242    }
243}
244
245/// Result of behavior inference.
246#[derive(Debug, Clone)]
247pub struct BehaviorInference {
248    pub content: String,
249    pub confidence: f64,
250    pub occurrences: usize,
251    pub keywords: Vec<String>,
252}
253
254/// Infer patterns from behavior - generic word frequency analysis.
255/// Let the model decide what's meaningful, not hardcoded tech patterns.
256pub fn infer_preferences_from_behavior(
257    messages: &[crate::providers::Message],
258    config: &BehaviorInferenceConfig,
259) -> Vec<BehaviorInference> {
260    let user_texts: Vec<String> = messages
261        .iter()
262        .filter_map(|msg| {
263            if msg.role == crate::providers::Role::User {
264                match &msg.content {
265                    crate::providers::MessageContent::Text(t) => Some(t.clone()),
266                    crate::providers::MessageContent::Blocks(blocks) => Some(
267                        blocks
268                            .iter()
269                            .filter_map(|b| {
270                                if let crate::providers::ContentBlock::Text { text } = b {
271                                    Some(text.as_str())
272                                } else {
273                                    None
274                                }
275                            })
276                            .collect::<Vec<_>>()
277                            .join(" "),
278                    ),
279                }
280            } else {
281                None
282            }
283        })
284        .collect();
285
286    if user_texts.len() < config.min_occurrences {
287        return Vec::new();
288    }
289
290    // Generic word frequency analysis
291    let mut word_freq: HashMap<String, usize> = HashMap::new();
292    for text in &user_texts {
293        for word in text.to_lowercase().split_whitespace() {
294            if word.len() > 3 {
295                // Skip short words
296                *word_freq.entry(word.to_string()).or_default() += 1;
297            }
298        }
299    }
300
301    // Extract high-frequency words as potential preferences
302    let inferences: Vec<BehaviorInference> = word_freq
303        .iter()
304        .filter(|(_, count)| **count >= config.min_occurrences)
305        .map(|(word, count)| {
306            let confidence = (*count as f64 / user_texts.len() as f64).min(1.0);
307            BehaviorInference {
308                content: format!("用户多次提及 '{}'", word),
309                confidence,
310                occurrences: *count,
311                keywords: vec![word.clone()],
312            }
313        })
314        .filter(|inf| inf.confidence >= config.min_confidence)
315        .take(config.max_inferences)
316        .collect();
317
318    inferences
319}
320
321/// Convert inference to memory entry (global preference).
322pub fn inference_to_memory_entry(inference: &BehaviorInference) -> MemoryEntry {
323    let mut entry = MemoryEntry::new(
324        MemoryCategory::Preference,
325        inference.content.clone(),
326        None,
327        None,
328    );
329    entry.importance = (inference.confidence * 70.0 + 30.0).min(80.0);
330    entry.tags = inference.keywords.clone();
331    entry
332}
333
334/// Apply behavior inferences to memory.
335pub fn apply_behavior_inferences_to_memory(
336    messages: &[crate::providers::Message],
337    memory: &mut AutoMemory,
338    config: Option<&BehaviorInferenceConfig>,
339) -> usize {
340    let cfg = config.cloned().unwrap_or_default();
341    let inferences = infer_preferences_from_behavior(messages, &cfg);
342
343    let mut added = 0;
344    for inference in inferences {
345        let entry = inference_to_memory_entry(&inference);
346        if !memory.entries.iter().any(|e| e.content == entry.content) {
347            memory.entries.push(entry);
348            added += 1;
349        }
350    }
351
352    added
353}
354
355/// Generic tool learning - let model decide what to remember.
356/// This is a placeholder for future AI-driven learning.
357pub fn apply_tool_learning_to_memory(
358    _tool_name: &str,
359    _tool_input: &serde_json::Value,
360    _tool_result: &str,
361    _is_error: bool,
362    _memory: &mut AutoMemory,
363) -> usize {
364    // Future: Use AI to analyze tool execution and extract learnings
365    // Current: Let the model handle this through its own analysis
366    0
367}