codex_memory/memory/
event_triggers.rs

1//! Event-triggered scoring system for immediate evaluation of critical content patterns
2//!
3//! This module implements a sophisticated pattern detection system that can identify
4//! critical content types and boost their importance scores by 2x, ensuring they
5//! bypass normal processing pipelines for immediate attention.
6
7use crate::memory::error::{MemoryError, Result};
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13use tokio::sync::RwLock;
14
15/// Five core trigger event types for pattern detection
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
17pub enum TriggerEvent {
18    /// Security-related content (vulnerabilities, threats, incidents)
19    Security,
20    /// Error conditions and critical failures
21    Error,
22    /// Performance bottlenecks and optimization opportunities
23    Performance,
24    /// Business-critical decisions and strategic insights
25    BusinessCritical,
26    /// User feedback and experience issues
27    UserExperience,
28}
29
30impl TriggerEvent {
31    /// Get all trigger event types
32    pub fn all_types() -> Vec<TriggerEvent> {
33        vec![
34            TriggerEvent::Security,
35            TriggerEvent::Error,
36            TriggerEvent::Performance,
37            TriggerEvent::BusinessCritical,
38            TriggerEvent::UserExperience,
39        ]
40    }
41
42    /// Get human-readable description
43    pub fn description(&self) -> &'static str {
44        match self {
45            TriggerEvent::Security => "Security vulnerabilities, threats, and incidents",
46            TriggerEvent::Error => "Error conditions and critical system failures",
47            TriggerEvent::Performance => "Performance bottlenecks and optimization needs",
48            TriggerEvent::BusinessCritical => "Strategic decisions and business-critical insights",
49            TriggerEvent::UserExperience => "User feedback and experience issues",
50        }
51    }
52
53    /// Get priority level for conflict resolution (higher = more important)
54    pub fn priority(&self) -> u8 {
55        match self {
56            TriggerEvent::Security => 100,        // Highest priority
57            TriggerEvent::Error => 90,            // Critical system issues
58            TriggerEvent::Performance => 70,      // Important but not critical
59            TriggerEvent::BusinessCritical => 80, // High business impact
60            TriggerEvent::UserExperience => 60,   // Important but lower priority
61        }
62    }
63}
64
65/// Pattern configuration for trigger detection
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TriggerPattern {
68    /// Regular expression for pattern matching
69    pub regex: String,
70    /// Compiled regex (not serialized)
71    #[serde(skip)]
72    pub compiled_regex: Option<Regex>,
73    /// Keywords that indicate this trigger type
74    pub keywords: Vec<String>,
75    /// Context words that boost confidence
76    pub context_boosters: Vec<String>,
77    /// Minimum confidence threshold (0.0-1.0)
78    pub confidence_threshold: f64,
79    /// Whether this pattern is enabled
80    pub enabled: bool,
81}
82
83impl TriggerPattern {
84    /// Create new trigger pattern
85    pub fn new(regex: String, keywords: Vec<String>) -> Result<Self> {
86        let compiled_regex = Some(
87            Regex::new(&regex)
88                .map_err(|e| MemoryError::Configuration(format!("Invalid regex pattern: {e}")))?,
89        );
90
91        Ok(TriggerPattern {
92            regex,
93            compiled_regex,
94            keywords,
95            context_boosters: Vec::new(),
96            confidence_threshold: 0.7,
97            enabled: true,
98        })
99    }
100
101    /// Check if content matches this pattern
102    pub fn matches(&self, content: &str) -> bool {
103        if !self.enabled {
104            return false;
105        }
106
107        // Check regex match
108        if let Some(ref regex) = self.compiled_regex {
109            if regex.is_match(content) {
110                return true;
111            }
112        }
113
114        // Check keyword matches
115        let content_lower = content.to_lowercase();
116        self.keywords
117            .iter()
118            .any(|keyword| content_lower.contains(&keyword.to_lowercase()))
119    }
120
121    /// Calculate confidence score for a match (0.0-1.0)
122    pub fn calculate_confidence(&self, content: &str) -> f64 {
123        if !self.matches(content) {
124            return 0.0;
125        }
126
127        let content_lower = content.to_lowercase();
128        let mut confidence = 0.4; // Base confidence for any match
129
130        // High-value security terms get extra boost
131        let high_value_security_terms = [
132            "xss",
133            "injection",
134            "csrf",
135            "vulnerability",
136            "exploit",
137            "malware",
138            "phishing",
139        ];
140        let has_high_value_security = high_value_security_terms
141            .iter()
142            .any(|term| content_lower.contains(term));
143
144        // Boost for keyword matches - more generous scoring
145        let keyword_matches = self
146            .keywords
147            .iter()
148            .filter(|keyword| content_lower.contains(&keyword.to_lowercase()))
149            .count() as f64;
150        if keyword_matches > 0.0 {
151            // Give a good boost for any keyword matches, with diminishing returns
152            confidence += 0.3 + (keyword_matches / self.keywords.len() as f64) * 0.2;
153
154            // Extra boost for high-value security terms
155            if has_high_value_security
156                && self
157                    .keywords
158                    .iter()
159                    .any(|k| high_value_security_terms.contains(&k.as_str()))
160            {
161                confidence += 0.1;
162            }
163        }
164
165        // Boost for context words
166        let context_matches = self
167            .context_boosters
168            .iter()
169            .filter(|booster| content_lower.contains(&booster.to_lowercase()))
170            .count() as f64;
171        if !self.context_boosters.is_empty() && context_matches > 0.0 {
172            confidence += (context_matches / self.context_boosters.len() as f64) * 0.1;
173        }
174
175        confidence.min(1.0)
176    }
177}
178
179/// Configuration for the entire trigger system
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct TriggerConfig {
182    /// Patterns for each trigger type
183    pub patterns: HashMap<TriggerEvent, TriggerPattern>,
184    /// Importance multiplier for triggered events (default: 2.0)
185    pub importance_multiplier: f64,
186    /// Maximum processing time for triggers (default: 50ms)
187    pub max_processing_time_ms: u64,
188    /// Whether to enable A/B testing
189    pub enable_ab_testing: bool,
190    /// User-specific customizations
191    pub user_customizations: HashMap<String, HashMap<TriggerEvent, TriggerPattern>>,
192}
193
194impl Default for TriggerConfig {
195    fn default() -> Self {
196        let mut patterns = HashMap::new();
197
198        // Security patterns
199        if let Ok(mut security_pattern) = TriggerPattern::new(
200            r"(?i)(vulnerability|exploit|attack|breach|security|threat|malware|phishing|xss|injection|csrf)".to_string(),
201            vec![
202                "vulnerability".to_string(),
203                "exploit".to_string(),
204                "attack".to_string(),
205                "breach".to_string(),
206                "security".to_string(),
207                "threat".to_string(),
208                "malware".to_string(),
209                "phishing".to_string(),
210                "xss".to_string(),
211                "injection".to_string(),
212                "csrf".to_string(),
213            ]
214        ) {
215            security_pattern.confidence_threshold = 0.6; // Lower threshold for better matching
216            patterns.insert(TriggerEvent::Security, security_pattern);
217        }
218
219        // Error patterns
220        if let Ok(mut error_pattern) = TriggerPattern::new(
221            r"(?i)(error|exception|failure|crash|panic|fatal|critical)".to_string(),
222            vec![
223                "error".to_string(),
224                "exception".to_string(),
225                "failure".to_string(),
226                "crash".to_string(),
227                "panic".to_string(),
228                "fatal".to_string(),
229                "critical".to_string(),
230            ],
231        ) {
232            error_pattern.confidence_threshold = 0.6;
233            patterns.insert(TriggerEvent::Error, error_pattern);
234        }
235
236        // Performance patterns
237        if let Ok(mut performance_pattern) = TriggerPattern::new(
238            r"(?i)(slow|latency|bottleneck|performance|optimization|memory leak|timeout)"
239                .to_string(),
240            vec![
241                "slow".to_string(),
242                "latency".to_string(),
243                "bottleneck".to_string(),
244                "performance".to_string(),
245                "optimization".to_string(),
246            ],
247        ) {
248            performance_pattern.confidence_threshold = 0.6;
249            patterns.insert(TriggerEvent::Performance, performance_pattern);
250        }
251
252        // Business critical patterns
253        if let Ok(mut business_pattern) = TriggerPattern::new(
254            r"(?i)(revenue|profit|loss|critical|strategic|decision|customer|retention)".to_string(),
255            vec![
256                "revenue".to_string(),
257                "profit".to_string(),
258                "critical".to_string(),
259                "strategic".to_string(),
260                "decision".to_string(),
261            ],
262        ) {
263            business_pattern.confidence_threshold = 0.6;
264            patterns.insert(TriggerEvent::BusinessCritical, business_pattern);
265        }
266
267        // User experience patterns
268        if let Ok(mut ux_pattern) = TriggerPattern::new(
269            r"(?i)(user|usability|feedback|complaint|satisfaction|experience|ui|ux)".to_string(),
270            vec![
271                "user".to_string(),
272                "usability".to_string(),
273                "feedback".to_string(),
274                "complaint".to_string(),
275                "satisfaction".to_string(),
276            ],
277        ) {
278            ux_pattern.confidence_threshold = 0.6;
279            patterns.insert(TriggerEvent::UserExperience, ux_pattern);
280        }
281
282        TriggerConfig {
283            patterns,
284            importance_multiplier: 2.0,
285            max_processing_time_ms: 50,
286            enable_ab_testing: false,
287            user_customizations: HashMap::new(),
288        }
289    }
290}
291
292/// Metrics for trigger frequency and performance
293#[derive(Debug, Clone, Serialize, Deserialize, Default)]
294pub struct TriggerMetrics {
295    /// Total triggers fired by type
296    pub triggers_by_type: HashMap<TriggerEvent, u64>,
297    /// Total processing time by type
298    pub processing_time_by_type: HashMap<TriggerEvent, Duration>,
299    /// Detection accuracy by type (for A/B testing)
300    pub accuracy_by_type: HashMap<TriggerEvent, f64>,
301    /// Total memories processed
302    pub total_memories_processed: u64,
303    /// Total triggered memories
304    pub total_triggered_memories: u64,
305    /// Average processing time
306    pub average_processing_time: Duration,
307}
308
309/// Result of trigger detection
310#[derive(Debug, Clone)]
311pub struct TriggerDetectionResult {
312    /// Whether any trigger was detected
313    pub triggered: bool,
314    /// The specific trigger type detected (if any)
315    pub trigger_type: Option<TriggerEvent>,
316    /// Confidence score for the detection
317    pub confidence: f64,
318    /// Original importance score
319    pub original_importance: f64,
320    /// Boosted importance score (if triggered)
321    pub boosted_importance: f64,
322    /// Processing time
323    pub processing_time: Duration,
324}
325
326/// Main event-triggered scoring engine
327pub struct EventTriggeredScoringEngine {
328    config: Arc<RwLock<TriggerConfig>>,
329    metrics: Arc<RwLock<TriggerMetrics>>,
330}
331
332impl EventTriggeredScoringEngine {
333    /// Create new event-triggered scoring engine
334    pub fn new(config: TriggerConfig) -> Self {
335        Self {
336            config: Arc::new(RwLock::new(config)),
337            metrics: Arc::new(RwLock::new(TriggerMetrics::default())),
338        }
339    }
340
341    /// Create with default configuration
342    pub fn with_default_config() -> Self {
343        Self::new(TriggerConfig::default())
344    }
345
346    /// Analyze content for trigger patterns with immediate processing
347    pub async fn analyze_content(
348        &self,
349        content: &str,
350        original_importance: f64,
351        user_id: Option<&str>,
352    ) -> Result<TriggerDetectionResult> {
353        let start_time = Instant::now();
354        let config = self.config.read().await;
355
356        // Check for timeout early
357        let max_duration = Duration::from_millis(config.max_processing_time_ms);
358
359        // Get applicable patterns (user-specific or default)
360        let patterns = if let Some(user) = user_id {
361            config
362                .user_customizations
363                .get(user)
364                .unwrap_or(&config.patterns)
365        } else {
366            &config.patterns
367        };
368
369        let mut best_match: Option<(TriggerEvent, f64)> = None;
370
371        // Check each pattern type
372        for (trigger_type, pattern) in patterns {
373            // Check timeout
374            if start_time.elapsed() > max_duration {
375                break;
376            }
377
378            if pattern.matches(content) {
379                let confidence = pattern.calculate_confidence(content);
380                if confidence >= pattern.confidence_threshold {
381                    if let Some((current_type, current_confidence)) = &best_match {
382                        // Use priority as tiebreaker for close confidence scores
383                        let confidence_diff = confidence - current_confidence;
384                        let should_replace = if confidence_diff.abs() < 0.05 {
385                            // If confidence is very close, use priority
386                            trigger_type.priority() > current_type.priority()
387                        } else {
388                            // Otherwise, use confidence
389                            confidence > *current_confidence
390                        };
391
392                        if should_replace {
393                            best_match = Some((trigger_type.clone(), confidence));
394                        }
395                    } else {
396                        best_match = Some((trigger_type.clone(), confidence));
397                    }
398                }
399            }
400        }
401
402        let processing_time = start_time.elapsed();
403
404        // Create result
405        let result = if let Some((trigger_type, confidence)) = best_match {
406            let boosted_importance = original_importance * config.importance_multiplier;
407
408            // Update metrics
409            self.update_metrics(&trigger_type, processing_time, true)
410                .await;
411
412            TriggerDetectionResult {
413                triggered: true,
414                trigger_type: Some(trigger_type),
415                confidence,
416                original_importance,
417                boosted_importance,
418                processing_time,
419            }
420        } else {
421            // Update metrics for non-triggered content
422            self.update_metrics_non_triggered(processing_time).await;
423
424            TriggerDetectionResult {
425                triggered: false,
426                trigger_type: None,
427                confidence: 0.0,
428                original_importance,
429                boosted_importance: original_importance,
430                processing_time,
431            }
432        };
433
434        Ok(result)
435    }
436
437    /// Update configuration (hot-reloadable)
438    pub async fn update_config(&self, new_config: TriggerConfig) -> Result<()> {
439        let mut config = self.config.write().await;
440        *config = new_config;
441        Ok(())
442    }
443
444    /// Get current metrics
445    pub async fn get_metrics(&self) -> TriggerMetrics {
446        self.metrics.read().await.clone()
447    }
448
449    /// Reset metrics
450    pub async fn reset_metrics(&self) -> Result<()> {
451        let mut metrics = self.metrics.write().await;
452        *metrics = TriggerMetrics::default();
453        Ok(())
454    }
455
456    /// Add user-specific customization
457    pub async fn add_user_customization(
458        &self,
459        user_id: String,
460        customizations: HashMap<TriggerEvent, TriggerPattern>,
461    ) -> Result<()> {
462        let mut config = self.config.write().await;
463        config.user_customizations.insert(user_id, customizations);
464        Ok(())
465    }
466
467    // Private helper methods
468    async fn update_metrics(
469        &self,
470        trigger_type: &TriggerEvent,
471        processing_time: Duration,
472        triggered: bool,
473    ) {
474        let mut metrics = self.metrics.write().await;
475
476        metrics.total_memories_processed += 1;
477        if triggered {
478            metrics.total_triggered_memories += 1;
479            *metrics
480                .triggers_by_type
481                .entry(trigger_type.clone())
482                .or_insert(0) += 1;
483            *metrics
484                .processing_time_by_type
485                .entry(trigger_type.clone())
486                .or_insert(Duration::ZERO) += processing_time;
487        }
488
489        // Update average processing time
490        let total_time = metrics.average_processing_time
491            * (metrics.total_memories_processed - 1) as u32
492            + processing_time;
493        metrics.average_processing_time = total_time / metrics.total_memories_processed as u32;
494    }
495
496    async fn update_metrics_non_triggered(&self, processing_time: Duration) {
497        let mut metrics = self.metrics.write().await;
498        metrics.total_memories_processed += 1;
499
500        // Update average processing time
501        let total_time = metrics.average_processing_time
502            * (metrics.total_memories_processed - 1) as u32
503            + processing_time;
504        metrics.average_processing_time = total_time / metrics.total_memories_processed as u32;
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    #[test]
513    fn test_trigger_event_types() {
514        let all_types = TriggerEvent::all_types();
515        assert_eq!(all_types.len(), 5);
516        assert!(all_types.contains(&TriggerEvent::Security));
517        assert!(all_types.contains(&TriggerEvent::Error));
518        assert!(all_types.contains(&TriggerEvent::Performance));
519        assert!(all_types.contains(&TriggerEvent::BusinessCritical));
520        assert!(all_types.contains(&TriggerEvent::UserExperience));
521    }
522
523    #[test]
524    fn test_trigger_pattern_creation() {
525        let pattern = TriggerPattern::new(
526            r"(?i)(error|exception)".to_string(),
527            vec!["error".to_string(), "exception".to_string()],
528        )
529        .unwrap();
530
531        assert!(pattern.matches("An error occurred"));
532        assert!(pattern.matches("Exception thrown"));
533        assert!(!pattern.matches("Everything is fine"));
534    }
535
536    #[test]
537    fn test_confidence_calculation() {
538        let mut pattern = TriggerPattern::new(
539            r"(?i)(error|exception)".to_string(),
540            vec!["error".to_string(), "exception".to_string()],
541        )
542        .unwrap();
543
544        pattern.context_boosters = vec!["critical".to_string(), "fatal".to_string()];
545
546        let confidence1 = pattern.calculate_confidence("An error occurred");
547        let confidence2 = pattern.calculate_confidence("A critical error occurred");
548        let confidence3 = pattern.calculate_confidence("A critical fatal error occurred");
549
550        assert!(confidence2 > confidence1);
551        assert!(confidence3 > confidence2);
552        assert!(confidence1 > 0.0);
553        assert!(confidence3 <= 1.0);
554    }
555
556    #[tokio::test]
557    async fn test_event_triggered_scoring_engine() {
558        let engine = EventTriggeredScoringEngine::with_default_config();
559
560        // Test security trigger
561        let result = engine
562            .analyze_content(
563                "Security vulnerability detected in the authentication system",
564                0.5,
565                None,
566            )
567            .await
568            .unwrap();
569
570        assert!(result.triggered);
571        assert!(matches!(result.trigger_type, Some(TriggerEvent::Security)));
572        assert_eq!(result.boosted_importance, 1.0); // 0.5 * 2.0
573        assert!(result.confidence > 0.7);
574        assert!(result.processing_time.as_millis() < 50);
575    }
576
577    #[tokio::test]
578    async fn test_performance_within_limits() {
579        let engine = EventTriggeredScoringEngine::with_default_config();
580
581        let result = engine
582            .analyze_content(
583                "Performance bottleneck detected in database queries",
584                0.6,
585                None,
586            )
587            .await
588            .unwrap();
589
590        assert!(result.triggered);
591        assert!(matches!(
592            result.trigger_type,
593            Some(TriggerEvent::Performance)
594        ));
595        assert!(result.processing_time.as_millis() < 50);
596    }
597
598    #[tokio::test]
599    async fn test_metrics_tracking() {
600        let engine = EventTriggeredScoringEngine::with_default_config();
601
602        // Process several samples
603        let _result1 = engine
604            .analyze_content("Error in system", 0.5, None)
605            .await
606            .unwrap();
607        let _result2 = engine
608            .analyze_content("Normal content", 0.5, None)
609            .await
610            .unwrap();
611        let _result3 = engine
612            .analyze_content("Security breach", 0.5, None)
613            .await
614            .unwrap();
615
616        let metrics = engine.get_metrics().await;
617        assert_eq!(metrics.total_memories_processed, 3);
618        assert_eq!(metrics.total_triggered_memories, 2);
619        assert!(metrics.triggers_by_type.contains_key(&TriggerEvent::Error));
620        assert!(metrics
621            .triggers_by_type
622            .contains_key(&TriggerEvent::Security));
623    }
624}