Skip to main content

talos_evolution/
extractor.rs

1//! PatternExtractor — extracts patterns from observations.
2
3use crate::{Observation, Pattern, SignalType};
4
5/// Extracts patterns from observations using rule-based logic.
6pub struct PatternExtractor;
7
8impl PatternExtractor {
9    /// Extract a pattern from a single observation.
10    pub fn extract_from_observation(obs: &Observation) -> Option<Pattern> {
11        match obs.signal_type {
12            SignalType::Correction => Self::extract_correction_pattern(obs),
13            SignalType::Error => Self::extract_error_pattern(obs),
14            SignalType::Satisfaction => Self::extract_satisfaction_pattern(obs),
15            SignalType::Inefficiency => Self::extract_inefficiency_pattern(obs),
16        }
17    }
18
19    fn extract_correction_pattern(obs: &Observation) -> Option<Pattern> {
20        if obs.intensity < 0.5 {
21            return None;
22        }
23
24        let mut pattern = Pattern::new(
25            format!("User preference: {}", obs.context),
26            format!("Remember: {}", obs.context),
27            "preference".to_string(),
28        );
29        pattern.confidence = obs.intensity * 0.5;
30        pattern.evidence_count = 1;
31        Some(pattern)
32    }
33
34    fn extract_error_pattern(obs: &Observation) -> Option<Pattern> {
35        if obs.intensity < 0.3 {
36            return None;
37        }
38
39        let mut pattern = Pattern::new(
40            format!("Error to avoid: {}", obs.context),
41            format!("Avoid: {}", obs.context),
42            "error_avoidance".to_string(),
43        );
44        pattern.confidence = obs.intensity * 0.4;
45        pattern.evidence_count = 1;
46        Some(pattern)
47    }
48
49    fn extract_satisfaction_pattern(_obs: &Observation) -> Option<Pattern> {
50        None
51    }
52
53    fn extract_inefficiency_pattern(obs: &Observation) -> Option<Pattern> {
54        if obs.intensity < 0.4 {
55            return None;
56        }
57
58        let mut pattern = Pattern::new(
59            format!("Inefficiency detected: {}", obs.context),
60            format!("Optimize: {}", obs.context),
61            "efficiency".to_string(),
62        );
63        pattern.confidence = obs.intensity * 0.3;
64        pattern.evidence_count = 1;
65        Some(pattern)
66    }
67
68    /// Check if a new pattern contradicts an existing pattern.
69    pub fn detects_conflict(new_pattern: &Pattern, existing: &[Pattern]) -> Option<String> {
70        for p in existing {
71            if p.category == new_pattern.category {
72                let new_lower = new_pattern.description.to_lowercase();
73                let existing_lower = p.description.to_lowercase();
74
75                if new_lower.contains("avoid") && !existing_lower.contains("avoid")
76                    || !new_lower.contains("avoid") && existing_lower.contains("avoid")
77                {
78                    return Some(format!(
79                        "Conflict between '{}' and '{}'",
80                        new_pattern.description, p.description
81                    ));
82                }
83            }
84        }
85        None
86    }
87
88    /// Merge evidence from multiple observations into a pattern.
89    pub fn merge_evidence(pattern: &mut Pattern, observations: &[Observation]) {
90        let relevant: Vec<&Observation> = observations
91            .iter()
92            .filter(|o| {
93                matches!(
94                    o.signal_type,
95                    SignalType::Correction | SignalType::Error | SignalType::Inefficiency
96                )
97            })
98            .collect();
99
100        if relevant.is_empty() {
101            return;
102        }
103
104        let total_intensity: f64 = relevant.iter().map(|o| o.intensity).sum();
105        let avg_intensity = total_intensity / relevant.len() as f64;
106
107        pattern.evidence_count += relevant.len() as u32;
108        pattern.confidence = (pattern.confidence + avg_intensity) / 2.0;
109        pattern.last_updated = chrono::Utc::now();
110    }
111}
112
113#[cfg(test)]
114#[allow(warnings)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_extract_correction_pattern() {
120        let obs = Observation::new(
121            SignalType::Correction,
122            0.8,
123            "Use functional style".to_string(),
124            None,
125            None,
126        );
127
128        let pattern = PatternExtractor::extract_from_observation(&obs);
129        assert!(pattern.is_some());
130
131        let pattern = pattern.expect("operation should succeed");
132        assert_eq!(pattern.category, "preference");
133        assert!(pattern.confidence > 0.0);
134    }
135
136    #[test]
137    fn test_extract_low_intensity_returns_none() {
138        let obs = Observation::new(
139            SignalType::Correction,
140            0.2,
141            "Minor correction".to_string(),
142            None,
143            None,
144        );
145
146        let pattern = PatternExtractor::extract_from_observation(&obs);
147        assert!(pattern.is_none());
148    }
149
150    #[test]
151    fn test_detects_conflict() {
152        let existing = vec![Pattern::new(
153            "Avoid imperative style".to_string(),
154            "Use functional style".to_string(),
155            "preference".to_string(),
156        )];
157
158        let new_pattern = Pattern::new(
159            "Use imperative style".to_string(),
160            "Use imperative style".to_string(),
161            "preference".to_string(),
162        );
163
164        let conflict = PatternExtractor::detects_conflict(&new_pattern, &existing);
165        assert!(conflict.is_some());
166    }
167
168    #[test]
169    fn test_merge_evidence() {
170        let mut pattern = Pattern::new(
171            "Test pattern".to_string(),
172            "Test instruction".to_string(),
173            "test".to_string(),
174        );
175        pattern.confidence = 0.5;
176        pattern.evidence_count = 1;
177
178        let observations = vec![
179            Observation::new(
180                SignalType::Correction,
181                0.7,
182                "context1".to_string(),
183                None,
184                None,
185            ),
186            Observation::new(SignalType::Error, 0.6, "context2".to_string(), None, None),
187        ];
188
189        PatternExtractor::merge_evidence(&mut pattern, &observations);
190
191        assert_eq!(pattern.evidence_count, 3);
192        assert!(pattern.confidence > 0.5);
193    }
194}