codex_memory/memory/
trigger_config_loader.rs

1//! Configuration loader for event-triggered scoring system with hot-reloading support
2
3use crate::memory::error::{MemoryError, Result};
4use crate::memory::event_triggers::{TriggerConfig, TriggerEvent, TriggerPattern};
5use regex::Regex;
6use serde_json::Value;
7use std::collections::HashMap;
8use std::fs;
9use std::path::Path;
10use std::sync::Arc;
11use std::time::{Duration, SystemTime};
12use tokio::sync::RwLock;
13use tokio::time::interval;
14use tracing::{error, info, warn};
15
16/// Configuration loader with hot-reloading capabilities
17pub struct TriggerConfigLoader {
18    config_path: String,
19    last_modified: Arc<RwLock<Option<SystemTime>>>,
20    current_config: Arc<RwLock<TriggerConfig>>,
21    hot_reload_enabled: bool,
22}
23
24impl TriggerConfigLoader {
25    /// Create new configuration loader
26    pub fn new(config_path: String) -> Self {
27        Self {
28            config_path,
29            last_modified: Arc::new(RwLock::new(None)),
30            current_config: Arc::new(RwLock::new(TriggerConfig::default())),
31            hot_reload_enabled: false,
32        }
33    }
34
35    /// Enable hot-reloading with specified check interval
36    pub fn enable_hot_reload(&mut self, check_interval: Duration) {
37        self.hot_reload_enabled = true;
38
39        let config_path = self.config_path.clone();
40        let last_modified = self.last_modified.clone();
41        let current_config = self.current_config.clone();
42
43        tokio::spawn(async move {
44            let mut timer = interval(check_interval);
45
46            loop {
47                timer.tick().await;
48
49                if let Err(e) =
50                    Self::check_and_reload_config(&config_path, &last_modified, &current_config)
51                        .await
52                {
53                    error!("Failed to check/reload config: {}", e);
54                }
55            }
56        });
57    }
58
59    /// Load configuration from file
60    pub async fn load_config(&self) -> Result<TriggerConfig> {
61        let config = Self::load_config_from_file(&self.config_path).await?;
62
63        // Update current config and last modified time
64        {
65            let mut current = self.current_config.write().await;
66            *current = config.clone();
67        }
68
69        if let Ok(metadata) = fs::metadata(&self.config_path) {
70            if let Ok(modified) = metadata.modified() {
71                let mut last_mod = self.last_modified.write().await;
72                *last_mod = Some(modified);
73            }
74        }
75
76        Ok(config)
77    }
78
79    /// Get current configuration
80    pub async fn get_current_config(&self) -> TriggerConfig {
81        self.current_config.read().await.clone()
82    }
83
84    /// Save configuration to file
85    pub async fn save_config(&self, config: &TriggerConfig) -> Result<()> {
86        Self::save_config_to_file(&self.config_path, config).await?;
87
88        // Update current config
89        {
90            let mut current = self.current_config.write().await;
91            *current = config.clone();
92        }
93
94        Ok(())
95    }
96
97    /// Validate configuration without loading
98    pub async fn validate_config_file(&self) -> Result<()> {
99        Self::validate_config_file_at_path(&self.config_path).await
100    }
101
102    // Private methods
103    async fn load_config_from_file(config_path: &str) -> Result<TriggerConfig> {
104        if !Path::new(config_path).exists() {
105            info!("Config file not found, creating default: {}", config_path);
106            let default_config = TriggerConfig::default();
107            Self::save_config_to_file(config_path, &default_config).await?;
108            return Ok(default_config);
109        }
110
111        let content = fs::read_to_string(config_path).map_err(|e| {
112            MemoryError::Configuration(format!("Failed to read config file {config_path}: {e}"))
113        })?;
114
115        let json_value: Value = serde_json::from_str(&content).map_err(|e| {
116            MemoryError::Configuration(format!("Invalid JSON in config file {config_path}: {e}"))
117        })?;
118
119        Self::parse_config_from_json(json_value).await
120    }
121
122    async fn parse_config_from_json(json_value: Value) -> Result<TriggerConfig> {
123        let obj = json_value.as_object().ok_or_else(|| {
124            MemoryError::Configuration("Config must be a JSON object".to_string())
125        })?;
126
127        // Parse basic settings
128        let importance_multiplier = obj
129            .get("importance_multiplier")
130            .and_then(|v| v.as_f64())
131            .unwrap_or(2.0);
132
133        let max_processing_time_ms = obj
134            .get("max_processing_time_ms")
135            .and_then(|v| v.as_u64())
136            .unwrap_or(50);
137
138        let enable_ab_testing = obj
139            .get("enable_ab_testing")
140            .and_then(|v| v.as_bool())
141            .unwrap_or(false);
142
143        // Parse patterns
144        let mut patterns = HashMap::new();
145        if let Some(patterns_obj) = obj.get("patterns").and_then(|v| v.as_object()) {
146            for (trigger_name, pattern_value) in patterns_obj {
147                let trigger_event = Self::parse_trigger_event(trigger_name)?;
148                let pattern = Self::parse_trigger_pattern(pattern_value).await?;
149                patterns.insert(trigger_event, pattern);
150            }
151        }
152
153        // Parse user customizations
154        let mut user_customizations = HashMap::new();
155        if let Some(customizations_obj) = obj.get("user_customizations").and_then(|v| v.as_object())
156        {
157            for (user_id, user_patterns_value) in customizations_obj {
158                if let Some(user_patterns_obj) = user_patterns_value.as_object() {
159                    let mut user_patterns = HashMap::new();
160                    for (trigger_name, pattern_value) in user_patterns_obj {
161                        let trigger_event = Self::parse_trigger_event(trigger_name)?;
162                        let pattern = Self::parse_trigger_pattern(pattern_value).await?;
163                        user_patterns.insert(trigger_event, pattern);
164                    }
165                    user_customizations.insert(user_id.clone(), user_patterns);
166                }
167            }
168        }
169
170        Ok(TriggerConfig {
171            patterns,
172            importance_multiplier,
173            max_processing_time_ms,
174            enable_ab_testing,
175            user_customizations,
176        })
177    }
178
179    fn parse_trigger_event(trigger_name: &str) -> Result<TriggerEvent> {
180        match trigger_name {
181            "Security" => Ok(TriggerEvent::Security),
182            "Error" => Ok(TriggerEvent::Error),
183            "Performance" => Ok(TriggerEvent::Performance),
184            "BusinessCritical" => Ok(TriggerEvent::BusinessCritical),
185            "UserExperience" => Ok(TriggerEvent::UserExperience),
186            _ => Err(MemoryError::Configuration(format!(
187                "Unknown trigger event type: {trigger_name}"
188            ))),
189        }
190    }
191
192    async fn parse_trigger_pattern(pattern_value: &Value) -> Result<TriggerPattern> {
193        let pattern_obj = pattern_value.as_object().ok_or_else(|| {
194            MemoryError::Configuration("Trigger pattern must be an object".to_string())
195        })?;
196
197        let regex = pattern_obj
198            .get("regex")
199            .and_then(|v| v.as_str())
200            .ok_or_else(|| MemoryError::Configuration("Missing regex field".to_string()))?
201            .to_string();
202
203        // Validate regex
204        Regex::new(&regex)
205            .map_err(|e| MemoryError::Configuration(format!("Invalid regex pattern: {e}")))?;
206
207        let keywords = pattern_obj
208            .get("keywords")
209            .and_then(|v| v.as_array())
210            .map(|arr| {
211                arr.iter()
212                    .filter_map(|v| v.as_str())
213                    .map(|s| s.to_string())
214                    .collect()
215            })
216            .unwrap_or_default();
217
218        let context_boosters = pattern_obj
219            .get("context_boosters")
220            .and_then(|v| v.as_array())
221            .map(|arr| {
222                arr.iter()
223                    .filter_map(|v| v.as_str())
224                    .map(|s| s.to_string())
225                    .collect()
226            })
227            .unwrap_or_default();
228
229        let confidence_threshold = pattern_obj
230            .get("confidence_threshold")
231            .and_then(|v| v.as_f64())
232            .unwrap_or(0.7);
233
234        let enabled = pattern_obj
235            .get("enabled")
236            .and_then(|v| v.as_bool())
237            .unwrap_or(true);
238
239        let mut pattern = TriggerPattern::new(regex, keywords)?;
240        pattern.context_boosters = context_boosters;
241        pattern.confidence_threshold = confidence_threshold;
242        pattern.enabled = enabled;
243
244        Ok(pattern)
245    }
246
247    async fn save_config_to_file(config_path: &str, config: &TriggerConfig) -> Result<()> {
248        let json_value = Self::config_to_json(config).await?;
249        let content = serde_json::to_string_pretty(&json_value)
250            .map_err(|e| MemoryError::Configuration(format!("Failed to serialize config: {e}")))?;
251
252        fs::write(config_path, content).map_err(|e| {
253            MemoryError::Configuration(format!("Failed to write config file {config_path}: {e}"))
254        })?;
255
256        info!("Configuration saved to: {}", config_path);
257        Ok(())
258    }
259
260    async fn config_to_json(config: &TriggerConfig) -> Result<Value> {
261        let mut patterns_obj = serde_json::Map::new();
262        for (trigger_event, pattern) in &config.patterns {
263            let trigger_name = match trigger_event {
264                TriggerEvent::Security => "Security",
265                TriggerEvent::Error => "Error",
266                TriggerEvent::Performance => "Performance",
267                TriggerEvent::BusinessCritical => "BusinessCritical",
268                TriggerEvent::UserExperience => "UserExperience",
269            };
270
271            let pattern_obj = serde_json::json!({
272                "regex": pattern.regex,
273                "keywords": pattern.keywords,
274                "context_boosters": pattern.context_boosters,
275                "confidence_threshold": pattern.confidence_threshold,
276                "enabled": pattern.enabled
277            });
278
279            patterns_obj.insert(trigger_name.to_string(), pattern_obj);
280        }
281
282        let mut user_customizations_obj = serde_json::Map::new();
283        for (user_id, user_patterns) in &config.user_customizations {
284            let mut user_patterns_obj = serde_json::Map::new();
285            for (trigger_event, pattern) in user_patterns {
286                let trigger_name = match trigger_event {
287                    TriggerEvent::Security => "Security",
288                    TriggerEvent::Error => "Error",
289                    TriggerEvent::Performance => "Performance",
290                    TriggerEvent::BusinessCritical => "BusinessCritical",
291                    TriggerEvent::UserExperience => "UserExperience",
292                };
293
294                let pattern_obj = serde_json::json!({
295                    "regex": pattern.regex,
296                    "keywords": pattern.keywords,
297                    "context_boosters": pattern.context_boosters,
298                    "confidence_threshold": pattern.confidence_threshold,
299                    "enabled": pattern.enabled
300                });
301
302                user_patterns_obj.insert(trigger_name.to_string(), pattern_obj);
303            }
304            user_customizations_obj.insert(user_id.clone(), Value::Object(user_patterns_obj));
305        }
306
307        Ok(serde_json::json!({
308            "importance_multiplier": config.importance_multiplier,
309            "max_processing_time_ms": config.max_processing_time_ms,
310            "enable_ab_testing": config.enable_ab_testing,
311            "patterns": Value::Object(patterns_obj),
312            "user_customizations": Value::Object(user_customizations_obj)
313        }))
314    }
315
316    async fn validate_config_file_at_path(config_path: &str) -> Result<()> {
317        if !Path::new(config_path).exists() {
318            return Err(MemoryError::Configuration(format!(
319                "Config file does not exist: {config_path}"
320            )));
321        }
322
323        // Try to load and parse the config
324        Self::load_config_from_file(config_path).await?;
325        Ok(())
326    }
327
328    async fn check_and_reload_config(
329        config_path: &str,
330        last_modified: &Arc<RwLock<Option<SystemTime>>>,
331        current_config: &Arc<RwLock<TriggerConfig>>,
332    ) -> Result<()> {
333        if let Ok(metadata) = fs::metadata(config_path) {
334            if let Ok(modified) = metadata.modified() {
335                let should_reload = {
336                    let last_mod = last_modified.read().await;
337                    match &*last_mod {
338                        Some(last) => modified > *last,
339                        None => true,
340                    }
341                };
342
343                if should_reload {
344                    match Self::load_config_from_file(config_path).await {
345                        Ok(new_config) => {
346                            {
347                                let mut config = current_config.write().await;
348                                *config = new_config;
349                            }
350                            {
351                                let mut last_mod = last_modified.write().await;
352                                *last_mod = Some(modified);
353                            }
354                            info!("Configuration hot-reloaded from: {}", config_path);
355                        }
356                        Err(e) => {
357                            warn!("Failed to reload config (keeping current): {}", e);
358                        }
359                    }
360                }
361            }
362        }
363
364        Ok(())
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use tempfile::NamedTempFile;
372    use tokio::time::sleep;
373
374    #[tokio::test]
375    async fn test_load_default_config() {
376        let temp_file = NamedTempFile::new().unwrap();
377        let config_path = temp_file.path().to_str().unwrap().to_string();
378
379        // Remove the temp file so loader creates default
380        std::fs::remove_file(&config_path).unwrap();
381
382        let loader = TriggerConfigLoader::new(config_path.clone());
383        let config = loader.load_config().await.unwrap();
384
385        assert_eq!(config.importance_multiplier, 2.0);
386        assert_eq!(config.max_processing_time_ms, 50);
387        assert_eq!(config.patterns.len(), 5);
388
389        // Verify file was created
390        assert!(Path::new(&config_path).exists());
391    }
392
393    #[tokio::test]
394    async fn test_load_custom_config() {
395        let temp_file = NamedTempFile::new().unwrap();
396        let config_path = temp_file.path().to_str().unwrap().to_string();
397
398        let custom_config = r#"{
399            "importance_multiplier": 3.0,
400            "max_processing_time_ms": 100,
401            "enable_ab_testing": true,
402            "patterns": {
403                "Security": {
404                    "regex": "(?i)(security|threat)",
405                    "keywords": ["security", "threat"],
406                    "context_boosters": ["critical"],
407                    "confidence_threshold": 0.9,
408                    "enabled": true
409                }
410            },
411            "user_customizations": {}
412        }"#;
413
414        std::fs::write(&config_path, custom_config).unwrap();
415
416        let loader = TriggerConfigLoader::new(config_path);
417        let config = loader.load_config().await.unwrap();
418
419        assert_eq!(config.importance_multiplier, 3.0);
420        assert_eq!(config.max_processing_time_ms, 100);
421        assert!(config.enable_ab_testing);
422        assert_eq!(config.patterns.len(), 1);
423    }
424
425    #[tokio::test]
426    async fn test_save_and_reload() {
427        let temp_file = NamedTempFile::new().unwrap();
428        let config_path = temp_file.path().to_str().unwrap().to_string();
429
430        let loader = TriggerConfigLoader::new(config_path.clone());
431
432        // Create a custom config
433        let mut config = TriggerConfig::default();
434        config.importance_multiplier = 4.0;
435
436        // Save it
437        loader.save_config(&config).await.unwrap();
438
439        // Create new loader and load
440        let new_loader = TriggerConfigLoader::new(config_path);
441        let loaded_config = new_loader.load_config().await.unwrap();
442
443        assert_eq!(loaded_config.importance_multiplier, 4.0);
444    }
445
446    #[tokio::test]
447    async fn test_config_validation() {
448        let temp_file = NamedTempFile::new().unwrap();
449        let config_path = temp_file.path().to_str().unwrap().to_string();
450
451        // Write invalid JSON
452        std::fs::write(&config_path, "invalid json").unwrap();
453
454        let loader = TriggerConfigLoader::new(config_path);
455        assert!(loader.validate_config_file().await.is_err());
456    }
457
458    #[tokio::test]
459    async fn test_hot_reload() {
460        let temp_file = NamedTempFile::new().unwrap();
461        let config_path = temp_file.path().to_str().unwrap().to_string();
462
463        // Initial config
464        let initial_config = r#"{
465            "importance_multiplier": 2.0,
466            "max_processing_time_ms": 50,
467            "enable_ab_testing": false,
468            "patterns": {},
469            "user_customizations": {}
470        }"#;
471        std::fs::write(&config_path, initial_config).unwrap();
472
473        let mut loader = TriggerConfigLoader::new(config_path.clone());
474        loader.enable_hot_reload(Duration::from_millis(50));
475
476        // Load initial config
477        let config = loader.load_config().await.unwrap();
478        assert_eq!(config.importance_multiplier, 2.0);
479
480        // Wait a bit for hot-reload timer to start
481        sleep(Duration::from_millis(100)).await;
482
483        // Update config file
484        let updated_config = r#"{
485            "importance_multiplier": 5.0,
486            "max_processing_time_ms": 50,
487            "enable_ab_testing": false,
488            "patterns": {},
489            "user_customizations": {}
490        }"#;
491        std::fs::write(&config_path, updated_config).unwrap();
492
493        // Wait for hot-reload to pick up changes
494        sleep(Duration::from_millis(200)).await;
495
496        let current_config = loader.get_current_config().await;
497        assert_eq!(current_config.importance_multiplier, 5.0);
498    }
499}