Skip to main content

infinite_probability_core/config/
mod.rs

1//! Configuration Module
2//!
3//! Unified configuration system for Infinite Probability plugins.
4//! Supports global, local, and project-level settings.
5
6use anyhow::Result;
7use directories::ProjectDirs;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11/// Configuration hierarchy levels
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ConfigLevel {
14    /// Built-in defaults
15    Default,
16    /// User global (~/.config/infinite-probability/)
17    Global,
18    /// User local (~/.config/infinite-probability/*.local.toml)
19    GlobalLocal,
20    /// Project level (.infinite-probability/)
21    Project,
22    /// Project local (.infinite-probability/*.local.toml)
23    ProjectLocal,
24    /// Environment variables
25    Environment,
26}
27
28/// Main configuration structure
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(default)]
31#[derive(Default)]
32pub struct Config {
33    pub general: GeneralConfig,
34    pub aisp: AispConfig,
35    pub llm: LlmConfig,
36}
37
38
39/// General settings
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(default)]
42pub struct GeneralConfig {
43    pub log_level: String,
44    pub telemetry: bool,
45}
46
47impl Default for GeneralConfig {
48    fn default() -> Self {
49        Self {
50            log_level: "info".to_string(),
51            telemetry: false,
52        }
53    }
54}
55
56/// AISP conversion settings
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(default)]
59pub struct AispConfig {
60    /// Default conversion tier
61    pub default_tier: String,
62    /// Confidence threshold for LLM fallback
63    pub confidence_threshold: f64,
64    /// Enable LLM fallback
65    pub enable_llm_fallback: bool,
66    /// Maximum recursion for correction loop
67    pub max_correction_attempts: usize,
68}
69
70impl Default for AispConfig {
71    fn default() -> Self {
72        Self {
73            default_tier: "auto".to_string(),
74            confidence_threshold: 0.8,
75            enable_llm_fallback: true,
76            max_correction_attempts: 3,
77        }
78    }
79}
80
81/// LLM settings
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(default)]
84pub struct LlmConfig {
85    /// Default model for fallback
86    pub default_model: String,
87    /// Model for simple tasks
88    pub simple_model: String,
89    /// Model for complex tasks
90    pub complex_model: String,
91}
92
93impl Default for LlmConfig {
94    fn default() -> Self {
95        Self {
96            default_model: "sonnet".to_string(),
97            simple_model: "haiku".to_string(),
98            complex_model: "opus".to_string(),
99        }
100    }
101}
102
103impl Config {
104    /// Load configuration with full hierarchy
105    pub fn load() -> Result<Self> {
106        let mut config = Self::default();
107
108        // Load global config
109        if let Some(global_path) = Self::global_config_path()
110            && global_path.exists() {
111                let content = std::fs::read_to_string(&global_path)?;
112                let global: Config = toml::from_str(&content)?;
113                config.merge(global);
114            }
115
116        // Load project config
117        let project_path = PathBuf::from(".infinite-probability/config.toml");
118        if project_path.exists() {
119            let content = std::fs::read_to_string(&project_path)?;
120            let project: Config = toml::from_str(&content)?;
121            config.merge(project);
122        }
123
124        // Apply environment overrides
125        config.apply_env();
126
127        Ok(config)
128    }
129
130    /// Get global config directory
131    pub fn global_config_dir() -> Option<PathBuf> {
132        ProjectDirs::from("dev", "epiphytic", "infinite-probability").map(|dirs| dirs.config_dir().to_path_buf())
133    }
134
135    /// Get global config file path
136    pub fn global_config_path() -> Option<PathBuf> {
137        Self::global_config_dir().map(|dir| dir.join("config.toml"))
138    }
139
140    /// Merge another config into this one
141    fn merge(&mut self, other: Config) {
142        // General
143        if other.general.log_level != GeneralConfig::default().log_level {
144            self.general.log_level = other.general.log_level;
145        }
146        if other.general.telemetry != GeneralConfig::default().telemetry {
147            self.general.telemetry = other.general.telemetry;
148        }
149
150        // AISP
151        if other.aisp.default_tier != AispConfig::default().default_tier {
152            self.aisp.default_tier = other.aisp.default_tier;
153        }
154        if other.aisp.confidence_threshold != AispConfig::default().confidence_threshold {
155            self.aisp.confidence_threshold = other.aisp.confidence_threshold;
156        }
157        if other.aisp.enable_llm_fallback != AispConfig::default().enable_llm_fallback {
158            self.aisp.enable_llm_fallback = other.aisp.enable_llm_fallback;
159        }
160
161        // LLM
162        if other.llm.default_model != LlmConfig::default().default_model {
163            self.llm.default_model = other.llm.default_model;
164        }
165    }
166
167    /// Apply environment variable overrides
168    fn apply_env(&mut self) {
169        if let Ok(level) = std::env::var("INFINITE_PROBABILITY_LOG_LEVEL") {
170            self.general.log_level = level;
171        }
172        if let Ok(val) = std::env::var("INFINITE_PROBABILITY_AISP_TIER") {
173            self.aisp.default_tier = val;
174        }
175        if let Ok(val) = std::env::var("INFINITE_PROBABILITY_AISP_CONFIDENCE")
176            && let Ok(threshold) = val.parse() {
177                self.aisp.confidence_threshold = threshold;
178            }
179        if let Ok(val) = std::env::var("INFINITE_PROBABILITY_LLM_MODEL") {
180            self.llm.default_model = val;
181        }
182    }
183
184    /// Save configuration to file
185    pub fn save(&self, path: &PathBuf) -> Result<()> {
186        let content = toml::to_string_pretty(self)?;
187        if let Some(parent) = path.parent() {
188            std::fs::create_dir_all(parent)?;
189        }
190        std::fs::write(path, content)?;
191        Ok(())
192    }
193
194    /// Get a configuration value by key path
195    pub fn get(&self, key: &str) -> Option<String> {
196        match key {
197            "general.log_level" => Some(self.general.log_level.clone()),
198            "general.telemetry" => Some(self.general.telemetry.to_string()),
199            "aisp.default_tier" => Some(self.aisp.default_tier.clone()),
200            "aisp.confidence_threshold" => Some(self.aisp.confidence_threshold.to_string()),
201            "aisp.enable_llm_fallback" => Some(self.aisp.enable_llm_fallback.to_string()),
202            "llm.default_model" => Some(self.llm.default_model.clone()),
203            "llm.simple_model" => Some(self.llm.simple_model.clone()),
204            "llm.complex_model" => Some(self.llm.complex_model.clone()),
205            _ => None,
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_default_config() {
216        let config = Config::default();
217        assert_eq!(config.general.log_level, "info");
218        assert_eq!(config.aisp.confidence_threshold, 0.8);
219    }
220
221    #[test]
222    fn test_get_config_value() {
223        let config = Config::default();
224        assert_eq!(config.get("general.log_level"), Some("info".to_string()));
225        assert_eq!(config.get("unknown.key"), None);
226    }
227}