Skip to main content

matrixcode_core/memory/
conversation_pattern.rs

1//! Conversation pattern types for pattern-based memory system.
2//!
3//! This module defines the core data structures for conversation patterns,
4//! which capture reusable reference and code patterns from conversations.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9// ============================================================================
10// Pattern Types
11// ============================================================================
12
13/// Types of conversation patterns.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum PatternType {
17    /// Reference pattern - how to refer to things (e.g., "PR", "issue", "commit")
18    Reference,
19    /// Code pattern - code style and structure patterns
20    Code,
21}
22
23impl PatternType {
24    /// Get display name for the pattern type.
25    pub fn display_name(&self) -> &'static str {
26        match self {
27            PatternType::Reference => "引用模式",
28            PatternType::Code => "代码模式",
29        }
30    }
31
32    /// Get icon for the pattern type.
33    pub fn icon(&self) -> &'static str {
34        match self {
35            PatternType::Reference => "🔗",
36            PatternType::Code => "💻",
37        }
38    }
39}
40
41// ============================================================================
42// Pattern Sources
43// ============================================================================
44
45/// Source of a conversation pattern.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(tag = "type", rename_all = "snake_case")]
48pub enum PatternSource {
49    /// Learned from user conversation.
50    UserConversation {
51        /// Example context where this pattern was observed.
52        example: String,
53    },
54    /// Derived from project code style.
55    ProjectCodeStyle {
56        /// Programming language for this style.
57        language: String,
58    },
59    /// System preset pattern (built-in defaults).
60    SystemPreset,
61    /// Manually added by user.
62    Manual,
63}
64
65impl PatternSource {
66    /// Create a user conversation source.
67    pub fn user_conversation(example: impl Into<String>) -> Self {
68        PatternSource::UserConversation {
69            example: example.into(),
70        }
71    }
72
73    /// Create a project code style source.
74    pub fn project_code_style(language: impl Into<String>) -> Self {
75        PatternSource::ProjectCodeStyle {
76            language: language.into(),
77        }
78    }
79
80    /// Check if this is a system preset.
81    pub fn is_preset(&self) -> bool {
82        matches!(self, PatternSource::SystemPreset)
83    }
84
85    /// Check if this is manually added.
86    pub fn is_manual(&self) -> bool {
87        matches!(self, PatternSource::Manual)
88    }
89
90    /// Get display name for the source.
91    pub fn display_name(&self) -> &'static str {
92        match self {
93            PatternSource::UserConversation { .. } => "用户对话",
94            PatternSource::ProjectCodeStyle { .. } => "项目风格",
95            PatternSource::SystemPreset => "系统预设",
96            PatternSource::Manual => "手动添加",
97        }
98    }
99}
100
101// ============================================================================
102// Conversation Pattern
103// ============================================================================
104
105/// A conversation pattern captured from user interactions.
106///
107/// Patterns represent reusable conventions like:
108/// - How to refer to pull requests ("PR #123" vs "pull request #123")
109/// - Code style preferences (naming conventions, formatting)
110/// - Common phrases and their variations
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ConversationPattern {
113    /// Unique identifier.
114    pub id: String,
115    /// Type of pattern.
116    pub pattern_type: PatternType,
117    /// The pattern string (regex or literal).
118    pub pattern: String,
119    /// Source where this pattern was learned from.
120    pub source: PatternSource,
121    /// Number of times this pattern has been used/matched.
122    pub frequency: u32,
123    /// When this pattern was last used.
124    pub last_used: DateTime<Utc>,
125    /// Confidence score (0.0-1.0), higher means more certain.
126    pub confidence: f32,
127    /// Whether this pattern is currently active.
128    pub is_active: bool,
129    /// Optional description of what this pattern represents.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub description: Option<String>,
132    /// Tags for categorization and search.
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    pub tags: Vec<String>,
135}
136
137impl ConversationPattern {
138    /// Create a new conversation pattern.
139    pub fn new(
140        pattern_type: PatternType,
141        pattern: impl Into<String>,
142        source: PatternSource,
143    ) -> Self {
144        let id = uuid::Uuid::new_v4().to_string();
145        Self {
146            id,
147            pattern_type,
148            pattern: pattern.into(),
149            source,
150            frequency: 1,
151            last_used: Utc::now(),
152            confidence: 0.5,
153            is_active: true,
154            description: None,
155            tags: Vec::new(),
156        }
157    }
158
159    /// Create a system preset pattern.
160    pub fn preset(pattern_type: PatternType, pattern: impl Into<String>) -> Self {
161        let mut p = Self::new(pattern_type, pattern, PatternSource::SystemPreset);
162        p.confidence = 1.0;
163        p.frequency = 100; // Start with high frequency for presets
164        p
165    }
166
167    /// Create a manually added pattern.
168    pub fn manual(pattern_type: PatternType, pattern: impl Into<String>) -> Self {
169        let mut p = Self::new(pattern_type, pattern, PatternSource::Manual);
170        p.confidence = 0.9;
171        p.is_active = true;
172        p
173    }
174
175    /// Set the description.
176    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
177        self.description = Some(desc.into());
178        self
179    }
180
181    /// Add a tag.
182    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
183        self.tags.push(tag.into());
184        self
185    }
186
187    /// Mark this pattern as used (increment frequency, update timestamp).
188    pub fn mark_used(&mut self) {
189        self.frequency = self.frequency.saturating_add(1);
190        self.last_used = Utc::now();
191        // Boost confidence slightly with usage
192        self.confidence = (self.confidence + 0.01).min(1.0);
193    }
194
195    /// Deactivate this pattern.
196    pub fn deactivate(&mut self) {
197        self.is_active = false;
198    }
199
200    /// Activate this pattern.
201    pub fn activate(&mut self) {
202        self.is_active = true;
203    }
204
205    /// Format for display.
206    pub fn format_line(&self) -> String {
207        let active_marker = if self.is_active { "" } else { "[inactive] " };
208        let freq_marker = if self.frequency > 10 { "★" } else { "" };
209        format!(
210            "{}{} {} {} (freq: {}, conf: {:.2}) {}",
211            active_marker,
212            self.pattern_type.icon(),
213            self.pattern_type.display_name(),
214            &self.pattern,
215            self.frequency,
216            self.confidence,
217            freq_marker
218        )
219    }
220
221    /// Format for inclusion in system prompt.
222    pub fn format_for_prompt(&self) -> String {
223        match &self.description {
224            Some(desc) => format!("{}: {}", &self.pattern, desc),
225            None => self.pattern.clone(),
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    // =========================================================================
235    // PatternType Tests
236    // =========================================================================
237
238    #[test]
239    fn test_pattern_type_display_name() {
240        assert_eq!(PatternType::Reference.display_name(), "引用模式");
241        assert_eq!(PatternType::Code.display_name(), "代码模式");
242    }
243
244    #[test]
245    fn test_pattern_type_icon() {
246        assert_eq!(PatternType::Reference.icon(), "🔗");
247        assert_eq!(PatternType::Code.icon(), "💻");
248    }
249
250    #[test]
251    fn test_pattern_type_equality() {
252        assert_eq!(PatternType::Reference, PatternType::Reference);
253        assert_eq!(PatternType::Code, PatternType::Code);
254        assert_ne!(PatternType::Reference, PatternType::Code);
255    }
256
257    #[test]
258    fn test_pattern_type_hash() {
259        use std::collections::HashSet;
260        let mut set = HashSet::new();
261        set.insert(PatternType::Reference);
262        set.insert(PatternType::Code);
263        set.insert(PatternType::Reference); // Duplicate
264
265        assert_eq!(set.len(), 2);
266    }
267
268    #[test]
269    fn test_pattern_type_serialization() {
270        let pt = PatternType::Reference;
271        let json = serde_json::to_string(&pt).unwrap();
272        assert_eq!(json, "\"reference\"");
273
274        let decoded: PatternType = serde_json::from_str(&json).unwrap();
275        assert_eq!(decoded, PatternType::Reference);
276
277        let pt2 = PatternType::Code;
278        let json2 = serde_json::to_string(&pt2).unwrap();
279        assert_eq!(json2, "\"code\"");
280    }
281
282    // =========================================================================
283    // PatternSource Tests
284    // =========================================================================
285
286    #[test]
287    fn test_pattern_source_user_conversation() {
288        let source = PatternSource::user_conversation("User mentioned PR #123");
289        match source {
290            PatternSource::UserConversation { example } => {
291                assert_eq!(example, "User mentioned PR #123");
292            }
293            _ => panic!("Expected UserConversation variant"),
294        }
295    }
296
297    #[test]
298    fn test_pattern_source_project_code_style() {
299        let source = PatternSource::project_code_style("rust");
300        match source {
301            PatternSource::ProjectCodeStyle { language } => {
302                assert_eq!(language, "rust");
303            }
304            _ => panic!("Expected ProjectCodeStyle variant"),
305        }
306    }
307
308    #[test]
309    fn test_pattern_source_is_preset() {
310        assert!(PatternSource::SystemPreset.is_preset());
311        assert!(!PatternSource::user_conversation("test").is_preset());
312        assert!(!PatternSource::project_code_style("rust").is_preset());
313        assert!(!PatternSource::Manual.is_preset());
314    }
315
316    #[test]
317    fn test_pattern_source_is_manual() {
318        assert!(PatternSource::Manual.is_manual());
319        assert!(!PatternSource::SystemPreset.is_manual());
320        assert!(!PatternSource::user_conversation("test").is_manual());
321        assert!(!PatternSource::project_code_style("rust").is_manual());
322    }
323
324    #[test]
325    fn test_pattern_source_display_name() {
326        assert_eq!(PatternSource::user_conversation("test").display_name(), "用户对话");
327        assert_eq!(PatternSource::project_code_style("rust").display_name(), "项目风格");
328        assert_eq!(PatternSource::SystemPreset.display_name(), "系统预设");
329        assert_eq!(PatternSource::Manual.display_name(), "手动添加");
330    }
331
332    #[test]
333    fn test_pattern_source_serialization() {
334        // UserConversation
335        let source = PatternSource::user_conversation("example context");
336        let json = serde_json::to_string(&source).unwrap();
337        let decoded: PatternSource = serde_json::from_str(&json).unwrap();
338        assert_eq!(decoded, source);
339
340        // ProjectCodeStyle
341        let source2 = PatternSource::project_code_style("typescript");
342        let json2 = serde_json::to_string(&source2).unwrap();
343        let decoded2: PatternSource = serde_json::from_str(&json2).unwrap();
344        assert_eq!(decoded2, source2);
345
346        // SystemPreset
347        let source3 = PatternSource::SystemPreset;
348        let json3 = serde_json::to_string(&source3).unwrap();
349        assert!(json3.contains("system_preset"));
350
351        // Manual
352        let source4 = PatternSource::Manual;
353        let json4 = serde_json::to_string(&source4).unwrap();
354        assert!(json4.contains("manual"));
355    }
356
357    // =========================================================================
358    // ConversationPattern Creation Tests
359    // =========================================================================
360
361    #[test]
362    fn test_pattern_creation() {
363        let pattern = ConversationPattern::new(
364            PatternType::Reference,
365            r"PR #\d+",
366            PatternSource::user_conversation("User mentioned PR #123"),
367        );
368        assert!(pattern.is_active);
369        assert_eq!(pattern.frequency, 1);
370        assert_eq!(pattern.confidence, 0.5);
371        assert!(pattern.description.is_none());
372        assert!(pattern.tags.is_empty());
373        assert!(!pattern.id.is_empty()); // UUID should be generated
374    }
375
376    #[test]
377    fn test_pattern_creation_with_all_types() {
378        // Reference type
379        let ref_pattern = ConversationPattern::new(
380            PatternType::Reference,
381            r"issue #\d+",
382            PatternSource::Manual,
383        );
384        assert_eq!(ref_pattern.pattern_type, PatternType::Reference);
385
386        // Code type
387        let code_pattern = ConversationPattern::new(
388            PatternType::Code,
389            r"fn \w+\(",
390            PatternSource::SystemPreset,
391        );
392        assert_eq!(code_pattern.pattern_type, PatternType::Code);
393    }
394
395    #[test]
396    fn test_pattern_preset() {
397        let pattern = ConversationPattern::preset(PatternType::Reference, r"PR #\d+");
398
399        assert!(pattern.source.is_preset());
400        assert!(pattern.is_active);
401        assert_eq!(pattern.confidence, 1.0);
402        assert_eq!(pattern.frequency, 100); // Presets start with high frequency
403    }
404
405    #[test]
406    fn test_pattern_manual() {
407        let pattern = ConversationPattern::manual(PatternType::Code, "custom-pattern");
408
409        assert!(pattern.source.is_manual());
410        assert!(pattern.is_active);
411        assert_eq!(pattern.confidence, 0.9);
412    }
413
414    #[test]
415    fn test_pattern_with_description() {
416        let pattern = ConversationPattern::new(
417            PatternType::Reference,
418            "test-pattern",
419            PatternSource::Manual,
420        )
421        .with_description("This is a test pattern");
422
423        assert_eq!(pattern.description, Some("This is a test pattern".to_string()));
424    }
425
426    #[test]
427    fn test_pattern_with_tag() {
428        let pattern = ConversationPattern::new(
429            PatternType::Code,
430            "test-pattern",
431            PatternSource::Manual,
432        )
433        .with_tag("rust")
434        .with_tag("async");
435
436        assert_eq!(pattern.tags, vec!["rust", "async"]);
437    }
438
439    #[test]
440    fn test_pattern_builder_chain() {
441        let pattern = ConversationPattern::preset(PatternType::Reference, r"\bPR\s*#\d+\b")
442            .with_description("Pull Request reference")
443            .with_tag("git")
444            .with_tag("github");
445
446        assert_eq!(pattern.pattern, r"\bPR\s*#\d+\b");
447        assert_eq!(pattern.description, Some("Pull Request reference".to_string()));
448        assert_eq!(pattern.tags, vec!["git", "github"]);
449        assert!(pattern.source.is_preset());
450    }
451
452    // =========================================================================
453    // ConversationPattern State Change Tests
454    // =========================================================================
455
456    #[test]
457    fn test_pattern_mark_used() {
458        let mut pattern = ConversationPattern::new(
459            PatternType::Code,
460            "fn test()",
461            PatternSource::Manual,
462        );
463        let initial_confidence = pattern.confidence;
464        let initial_last_used = pattern.last_used;
465
466        pattern.mark_used();
467
468        assert_eq!(pattern.frequency, 2);
469        assert!(pattern.confidence > initial_confidence);
470        assert!(pattern.last_used >= initial_last_used);
471    }
472
473    #[test]
474    fn test_pattern_mark_used_confidence_cap() {
475        let mut pattern = ConversationPattern::new(
476            PatternType::Code,
477            "test",
478            PatternSource::Manual,
479        );
480
481        // Set confidence near the cap
482        pattern.confidence = 0.999;
483
484        pattern.mark_used();
485
486        // Confidence should not exceed 1.0
487        assert!(pattern.confidence <= 1.0);
488    }
489
490    #[test]
491    fn test_pattern_mark_used_frequency_overflow() {
492        let mut pattern = ConversationPattern::new(
493            PatternType::Code,
494            "test",
495            PatternSource::Manual,
496        );
497
498        // Set frequency near max
499        pattern.frequency = u32::MAX - 1;
500
501        pattern.mark_used();
502
503        // Should saturate at max instead of overflowing
504        assert_eq!(pattern.frequency, u32::MAX);
505    }
506
507    #[test]
508    fn test_pattern_deactivate() {
509        let mut pattern = ConversationPattern::new(
510            PatternType::Reference,
511            "test",
512            PatternSource::Manual,
513        );
514
515        assert!(pattern.is_active);
516        pattern.deactivate();
517        assert!(!pattern.is_active);
518    }
519
520    #[test]
521    fn test_pattern_activate() {
522        let mut pattern = ConversationPattern::new(
523            PatternType::Reference,
524            "test",
525            PatternSource::Manual,
526        );
527
528        pattern.deactivate();
529        assert!(!pattern.is_active);
530
531        pattern.activate();
532        assert!(pattern.is_active);
533    }
534
535    #[test]
536    fn test_pattern_activate_deactivate_cycle() {
537        let mut pattern = ConversationPattern::preset(PatternType::Code, "test");
538
539        // Multiple cycles
540        for _ in 0..3 {
541            pattern.deactivate();
542            assert!(!pattern.is_active);
543            pattern.activate();
544            assert!(pattern.is_active);
545        }
546    }
547
548    // =========================================================================
549    // ConversationPattern Formatting Tests
550    // =========================================================================
551
552    #[test]
553    fn test_format_line_active_high_frequency() {
554        let mut pattern = ConversationPattern::preset(PatternType::Reference, "test-pattern");
555        pattern.frequency = 15; // Above the 10 threshold for star marker
556
557        let line = pattern.format_line();
558
559        assert!(line.contains("🔗"));
560        assert!(line.contains("引用模式"));
561        assert!(line.contains("test-pattern"));
562        assert!(line.contains("freq: 15"));
563        assert!(line.contains("★")); // High frequency marker
564        assert!(!line.contains("[inactive]"));
565    }
566
567    #[test]
568    fn test_format_line_active_low_frequency() {
569        let pattern = ConversationPattern::new(
570            PatternType::Code,
571            "test-pattern",
572            PatternSource::Manual,
573        );
574
575        let line = pattern.format_line();
576
577        assert!(line.contains("💻"));
578        assert!(line.contains("代码模式"));
579        assert!(!line.contains("★")); // Low frequency, no star
580        assert!(!line.contains("[inactive]"));
581    }
582
583    #[test]
584    fn test_format_line_inactive() {
585        let mut pattern = ConversationPattern::preset(PatternType::Reference, "test-pattern");
586        pattern.deactivate();
587
588        let line = pattern.format_line();
589
590        assert!(line.contains("[inactive]"));
591    }
592
593    #[test]
594    fn test_format_for_prompt_with_description() {
595        let pattern = ConversationPattern::preset(PatternType::Reference, r"\bPR\s*#\d+\b")
596            .with_description("Pull Request reference format");
597
598        let prompt = pattern.format_for_prompt();
599
600        assert_eq!(prompt, r"\bPR\s*#\d+\b: Pull Request reference format");
601    }
602
603    #[test]
604    fn test_format_for_prompt_without_description() {
605        let pattern = ConversationPattern::preset(PatternType::Reference, "simple-pattern");
606
607        let prompt = pattern.format_for_prompt();
608
609        assert_eq!(prompt, "simple-pattern");
610    }
611
612    // =========================================================================
613    // Serialization Tests
614    // =========================================================================
615
616    #[test]
617    fn test_serialization() {
618        let pattern = ConversationPattern::preset(PatternType::Reference, r"PR #\d+")
619            .with_description("Test pattern");
620
621        let json = serde_json::to_string(&pattern).unwrap();
622        let decoded: ConversationPattern = serde_json::from_str(&json).unwrap();
623
624        assert_eq!(decoded.pattern, pattern.pattern);
625        assert_eq!(decoded.pattern_type, PatternType::Reference);
626        assert_eq!(decoded.description, Some("Test pattern".to_string()));
627    }
628
629    #[test]
630    fn test_serialization_with_tags() {
631        let pattern = ConversationPattern::preset(PatternType::Code, r"fn \w+")
632            .with_tag("rust")
633            .with_tag("function");
634
635        let json = serde_json::to_string(&pattern).unwrap();
636        let decoded: ConversationPattern = serde_json::from_str(&json).unwrap();
637
638        assert_eq!(decoded.tags, vec!["rust", "function"]);
639    }
640
641    #[test]
642    fn test_serialization_roundtrip() {
643        let original = ConversationPattern::new(
644            PatternType::Reference,
645            r"issue #\d+",
646            PatternSource::user_conversation("User said issue #42"),
647        )
648        .with_description("Issue reference")
649        .with_tag("git");
650
651        let json = serde_json::to_string(&original).unwrap();
652        let decoded: ConversationPattern = serde_json::from_str(&json).unwrap();
653
654        assert_eq!(decoded.id, original.id);
655        assert_eq!(decoded.pattern_type, original.pattern_type);
656        assert_eq!(decoded.pattern, original.pattern);
657        assert_eq!(decoded.source, original.source);
658        assert_eq!(decoded.frequency, original.frequency);
659        assert_eq!(decoded.confidence, original.confidence);
660        assert_eq!(decoded.is_active, original.is_active);
661        assert_eq!(decoded.description, original.description);
662        assert_eq!(decoded.tags, original.tags);
663    }
664
665    // =========================================================================
666    // Edge Cases and Boundary Tests
667    // =========================================================================
668
669    #[test]
670    fn test_empty_pattern_string() {
671        let pattern = ConversationPattern::new(
672            PatternType::Reference,
673            "",
674            PatternSource::Manual,
675        );
676
677        assert_eq!(pattern.pattern, "");
678    }
679
680    #[test]
681    fn test_special_regex_chars_in_pattern() {
682        let pattern = ConversationPattern::new(
683            PatternType::Code,
684            r"fn\s+\w+\s*\([^)]*\)\s*\{",
685            PatternSource::Manual,
686        );
687
688        assert_eq!(pattern.pattern, r"fn\s+\w+\s*\([^)]*\)\s*\{");
689    }
690
691    #[test]
692    fn test_unicode_pattern() {
693        let pattern = ConversationPattern::new(
694            PatternType::Reference,
695            "中文模式",
696            PatternSource::user_conversation("测试"),
697        );
698
699        assert_eq!(pattern.pattern, "中文模式");
700    }
701
702    #[test]
703    fn test_very_long_pattern() {
704        let long_pattern = "x".repeat(10000);
705        let pattern = ConversationPattern::new(
706            PatternType::Code,
707            long_pattern.clone(),
708            PatternSource::Manual,
709        );
710
711        assert_eq!(pattern.pattern.len(), 10000);
712    }
713
714    #[test]
715    fn test_confidence_boundary() {
716        let mut pattern = ConversationPattern::new(
717            PatternType::Code,
718            "test",
719            PatternSource::Manual,
720        );
721
722        // Test minimum confidence
723        pattern.confidence = 0.0;
724        assert_eq!(pattern.confidence, 0.0);
725
726        // Test maximum confidence
727        pattern.confidence = 1.0;
728        assert_eq!(pattern.confidence, 1.0);
729    }
730
731    #[test]
732    fn test_unique_ids() {
733        let p1 = ConversationPattern::new(PatternType::Code, "test", PatternSource::Manual);
734        let p2 = ConversationPattern::new(PatternType::Code, "test", PatternSource::Manual);
735
736        // Each pattern should have a unique ID
737        assert_ne!(p1.id, p2.id);
738    }
739}