Skip to main content

matrixcode_core/compress/
focus_config.rs

1//! Focus tracker configuration - eliminates hardcoded values.
2//!
3//! This module provides configurable settings for focus tracking,
4//! using real-time extracted keywords from AI instead of persistent registry.
5
6use serde::{Deserialize, Serialize};
7
8use crate::memory::ExtractedKeywords;
9
10/// Focus tracker configuration
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct FocusTrackerConfig {
13    /// Current keywords extracted from the conversation (real-time, not persisted).
14    /// These are set by AI extraction and used for focus detection.
15    #[serde(skip)]
16    current_keywords: Option<ExtractedKeywords>,
17
18    /// Number of words to extract when no keywords found
19    pub fallback_topic_word_count: usize,
20
21    /// Window size for focus detection (number of recent messages to analyze)
22    pub focus_window_size: usize,
23
24    /// Maximum recent context snippets to keep
25    pub max_recent_context_count: usize,
26
27    /// Maximum characters to extract for question/task
28    pub max_question_extract_length: usize,
29
30    /// Minimum text length to consider as substantial
31    pub min_substantial_text_length: usize,
32
33    /// Focus score boost for messages matching current focus
34    pub focus_score_boost: f32,
35
36    /// Maximum focus score (cap)
37    pub max_focus_score: f32,
38}
39
40impl Default for FocusTrackerConfig {
41    fn default() -> Self {
42        Self {
43            // Keywords are set in real-time via set_keywords()
44            current_keywords: None,
45
46            // Fallback: extract N words when no keywords found
47            fallback_topic_word_count: 3,
48
49            // Window sizes and limits
50            focus_window_size: 10,              // Analyze last 10 messages
51            max_recent_context_count: 5,        // Keep up to 5 context snippets
52            max_question_extract_length: 100,   // Extract up to 100 chars for question
53            min_substantial_text_length: 10,    // Minimum 10 chars to be substantial
54
55            // Scoring parameters
56            focus_score_boost: 0.3,             // Focus can boost priority by up to 0.3
57            max_focus_score: 1.0,               // Cap focus score at 1.0
58        }
59    }
60}
61
62impl FocusTrackerConfig {
63    /// Create config for simple conversations (lower thresholds)
64    pub fn simple_conversation() -> Self {
65        Self {
66            focus_window_size: 5,
67            max_recent_context_count: 3,
68            min_substantial_text_length: 5,
69            ..Self::default()
70        }
71    }
72
73    /// Create config for complex technical discussions (higher thresholds)
74    pub fn complex_technical() -> Self {
75        Self {
76            focus_window_size: 15,
77            max_recent_context_count: 7,
78            max_question_extract_length: 150,
79            min_substantial_text_length: 20,
80            focus_score_boost: 0.4,
81            ..Self::default()
82        }
83    }
84
85    /// Create config from complexity level
86    pub fn from_complexity(level: crate::compress::complexity::ComplexityLevel) -> Self {
87        match level {
88            crate::compress::complexity::ComplexityLevel::High => Self::complex_technical(),
89            crate::compress::complexity::ComplexityLevel::Medium => Self::default(),
90            crate::compress::complexity::ComplexityLevel::Low => Self::simple_conversation(),
91        }
92    }
93
94    /// Set keywords extracted from AI (real-time).
95    ///
96    /// These keywords are used for focus tracking in the current conversation
97    /// and are not persisted.
98    pub fn set_keywords(&mut self, keywords: &ExtractedKeywords) {
99        self.current_keywords = Some(keywords.clone());
100    }
101
102    /// Get current keywords (if set).
103    pub fn get_keywords(&self) -> Option<&ExtractedKeywords> {
104        self.current_keywords.as_ref()
105    }
106
107    /// Get transition keywords (from AI extraction or fallback presets).
108    pub fn transition_keywords(&self) -> Vec<String> {
109        if let Some(kw) = &self.current_keywords {
110            kw.transition.clone()
111        } else {
112            // Fallback to hardcoded presets
113            vec![
114                "however".to_string(), "but".to_string(), "switching".to_string(),
115                "转换".to_string(), "切换".to_string(), "换个话题".to_string(),
116            ]
117        }
118    }
119
120    /// Get question keywords (from AI extraction or fallback presets).
121    pub fn question_keywords(&self) -> Vec<String> {
122        if let Some(kw) = &self.current_keywords {
123            kw.question.clone()
124        } else {
125            // Fallback to hardcoded presets
126            vec![
127                "how".to_string(), "what".to_string(), "why".to_string(),
128                "如何".to_string(), "什么".to_string(), "为什么".to_string(),
129            ]
130        }
131    }
132
133    /// Get task keywords (from AI extraction or fallback presets).
134    pub fn task_keywords(&self) -> Vec<String> {
135        if let Some(kw) = &self.current_keywords {
136            kw.task.clone()
137        } else {
138            // Fallback to hardcoded presets
139            vec![
140                "implement".to_string(), "create".to_string(), "fix".to_string(),
141                "实现".to_string(), "创建".to_string(), "修复".to_string(),
142            ]
143        }
144    }
145
146    /// Get tech keywords (from AI extraction or fallback presets).
147    pub fn tech_keywords(&self) -> Vec<String> {
148        if let Some(kw) = &self.current_keywords {
149            kw.tech.clone()
150        } else {
151            // Fallback to hardcoded presets
152            vec![
153                "rust".to_string(), "python".to_string(), "javascript".to_string(),
154                "api".to_string(), "database".to_string(), "performance".to_string(),
155            ]
156        }
157    }
158
159    /// Check if text matches transition keywords.
160    pub fn matches_transition(&self, text: &str) -> bool {
161        let lower = text.to_lowercase();
162        self.transition_keywords().iter().any(|kw| lower.contains(&kw.to_lowercase()))
163    }
164
165    /// Check if text matches question keywords.
166    pub fn matches_question(&self, text: &str) -> bool {
167        let lower = text.to_lowercase();
168        self.question_keywords().iter().any(|kw| lower.contains(&kw.to_lowercase()))
169    }
170
171    /// Check if text matches task keywords.
172    pub fn matches_task(&self, text: &str) -> bool {
173        let lower = text.to_lowercase();
174        self.task_keywords().iter().any(|kw| lower.contains(&kw.to_lowercase()))
175    }
176
177    /// Find matching tech keywords in text.
178    pub fn find_tech_keywords(&self, text: &str) -> Vec<String> {
179        let lower = text.to_lowercase();
180        self.tech_keywords()
181            .iter()
182            .filter(|kw| lower.contains(&kw.to_lowercase()))
183            .cloned()
184            .collect()
185    }
186
187    /// Merge additional keywords into current keywords.
188    pub fn merge_keywords(&mut self, additional: &ExtractedKeywords) {
189        match self.current_keywords.take() {
190            Some(mut current) => {
191                current.merge(additional);
192                self.current_keywords = Some(current);
193            }
194            None => {
195                self.current_keywords = Some(additional.clone());
196            }
197        }
198    }
199
200    /// Clear current keywords (start fresh for new conversation).
201    pub fn clear_keywords(&mut self) {
202        self.current_keywords = None;
203    }
204
205    /// Validate configuration (basic parameters).
206    pub fn validate(&self) -> bool {
207        self.focus_window_size > 0 &&
208        self.max_recent_context_count > 0 &&
209        self.max_question_extract_length > 0 &&
210        self.min_substantial_text_length > 0 &&
211        self.focus_score_boost > 0.0 &&
212        self.max_focus_score > 0.0 &&
213        self.fallback_topic_word_count > 0
214    }
215}
216
217/// Keyword type for custom keyword additions (legacy compatibility).
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum KeywordType {
220    Transition,
221    Question,
222    Task,
223    Tech,
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_default_config() {
232        let config = FocusTrackerConfig::default();
233        assert!(config.validate());
234        assert_eq!(config.focus_window_size, 10);
235        assert_eq!(config.max_recent_context_count, 5);
236    }
237
238    #[test]
239    fn test_simple_conversation_config() {
240        let config = FocusTrackerConfig::simple_conversation();
241        assert_eq!(config.focus_window_size, 5);
242        assert_eq!(config.max_recent_context_count, 3);
243    }
244
245    #[test]
246    fn test_complex_technical_config() {
247        let config = FocusTrackerConfig::complex_technical();
248        assert_eq!(config.focus_window_size, 15);
249        assert_eq!(config.max_question_extract_length, 150);
250    }
251
252    #[test]
253    fn test_set_keywords() {
254        let mut config = FocusTrackerConfig::default();
255
256        // Initially no keywords
257        assert!(config.get_keywords().is_none());
258
259        // Set keywords
260        let keywords = ExtractedKeywords {
261            transition: vec!["new_transition".to_string()],
262            question: vec!["new_question".to_string()],
263            task: vec!["new_task".to_string()],
264            tech: vec!["new_tech".to_string()],
265        };
266        config.set_keywords(&keywords);
267
268        // Should now have keywords
269        assert!(config.get_keywords().is_some());
270        assert_eq!(config.transition_keywords(), vec!["new_transition".to_string()]);
271        assert_eq!(config.question_keywords(), vec!["new_question".to_string()]);
272    }
273
274    #[test]
275    fn test_fallback_keywords() {
276        let config = FocusTrackerConfig::default();
277
278        // Should use fallback presets when no keywords set
279        assert!(!config.transition_keywords().is_empty());
280        assert!(!config.question_keywords().is_empty());
281        assert!(!config.task_keywords().is_empty());
282        assert!(!config.tech_keywords().is_empty());
283
284        // Should contain expected presets
285        assert!(config.transition_keywords().contains(&"however".to_string()));
286        assert!(config.question_keywords().contains(&"how".to_string()));
287        assert!(config.task_keywords().contains(&"implement".to_string()));
288        assert!(config.tech_keywords().contains(&"rust".to_string()));
289    }
290
291    #[test]
292    fn test_matches_keywords() {
293        let config = FocusTrackerConfig::default();
294
295        // Should match fallback presets
296        assert!(config.matches_question("How do I do this?"));
297        assert!(config.matches_task("Please implement this"));
298        assert!(config.matches_transition("However, let's move on"));
299    }
300
301    #[test]
302    fn test_find_tech_keywords() {
303        let config = FocusTrackerConfig::default();
304
305        let found = config.find_tech_keywords("Using Rust and Python for development");
306        assert!(found.contains(&"rust".to_string()));
307        assert!(found.contains(&"python".to_string()));
308    }
309
310    #[test]
311    fn test_merge_keywords() {
312        let mut config = FocusTrackerConfig::default();
313
314        // Set initial keywords
315        let initial = ExtractedKeywords {
316            transition: vec!["switch".to_string()],
317            question: vec!["how".to_string()],
318            task: vec!["create".to_string()],
319            tech: vec!["rust".to_string()],
320        };
321        config.set_keywords(&initial);
322
323        // Merge additional keywords
324        let additional = ExtractedKeywords {
325            transition: vec!["new".to_string()],
326            question: vec!["why".to_string()],
327            task: vec!["delete".to_string()],
328            tech: vec!["python".to_string()],
329        };
330        config.merge_keywords(&additional);
331
332        // Should have merged keywords
333        let merged = config.get_keywords().unwrap();
334        assert!(merged.transition.contains(&"switch".to_string()));
335        assert!(merged.transition.contains(&"new".to_string()));
336        assert!(merged.tech.contains(&"rust".to_string()));
337        assert!(merged.tech.contains(&"python".to_string()));
338    }
339
340    #[test]
341    fn test_clear_keywords() {
342        let mut config = FocusTrackerConfig::default();
343
344        // Set keywords
345        let keywords = ExtractedKeywords {
346            transition: vec!["test".to_string()],
347            question: vec![],
348            task: vec![],
349            tech: vec![],
350        };
351        config.set_keywords(&keywords);
352        assert!(config.get_keywords().is_some());
353
354        // Clear keywords
355        config.clear_keywords();
356        assert!(config.get_keywords().is_none());
357
358        // Should use fallback again
359        assert!(config.transition_keywords().contains(&"however".to_string()));
360    }
361}