talos_evolution/
extractor.rs1use crate::{Observation, Pattern, SignalType};
4
5pub struct PatternExtractor;
7
8impl PatternExtractor {
9 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 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 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}