use anyhow::Result;
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigLevel {
Default,
Global,
GlobalLocal,
Project,
ProjectLocal,
Environment,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct Config {
pub general: GeneralConfig,
pub aisp: AispConfig,
pub llm: LlmConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeneralConfig {
pub log_level: String,
pub telemetry: bool,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
log_level: "info".to_string(),
telemetry: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AispConfig {
pub default_tier: String,
pub confidence_threshold: f64,
pub enable_llm_fallback: bool,
pub max_correction_attempts: usize,
}
impl Default for AispConfig {
fn default() -> Self {
Self {
default_tier: "auto".to_string(),
confidence_threshold: 0.8,
enable_llm_fallback: true,
max_correction_attempts: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct LlmConfig {
pub default_model: String,
pub simple_model: String,
pub complex_model: String,
}
impl Default for LlmConfig {
fn default() -> Self {
Self {
default_model: "sonnet".to_string(),
simple_model: "haiku".to_string(),
complex_model: "opus".to_string(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let mut config = Self::default();
if let Some(global_path) = Self::global_config_path()
&& global_path.exists() {
let content = std::fs::read_to_string(&global_path)?;
let global: Config = toml::from_str(&content)?;
config.merge(global);
}
let project_path = PathBuf::from(".infinite-probability/config.toml");
if project_path.exists() {
let content = std::fs::read_to_string(&project_path)?;
let project: Config = toml::from_str(&content)?;
config.merge(project);
}
config.apply_env();
Ok(config)
}
pub fn global_config_dir() -> Option<PathBuf> {
ProjectDirs::from("dev", "epiphytic", "infinite-probability").map(|dirs| dirs.config_dir().to_path_buf())
}
pub fn global_config_path() -> Option<PathBuf> {
Self::global_config_dir().map(|dir| dir.join("config.toml"))
}
fn merge(&mut self, other: Config) {
if other.general.log_level != GeneralConfig::default().log_level {
self.general.log_level = other.general.log_level;
}
if other.general.telemetry != GeneralConfig::default().telemetry {
self.general.telemetry = other.general.telemetry;
}
if other.aisp.default_tier != AispConfig::default().default_tier {
self.aisp.default_tier = other.aisp.default_tier;
}
if other.aisp.confidence_threshold != AispConfig::default().confidence_threshold {
self.aisp.confidence_threshold = other.aisp.confidence_threshold;
}
if other.aisp.enable_llm_fallback != AispConfig::default().enable_llm_fallback {
self.aisp.enable_llm_fallback = other.aisp.enable_llm_fallback;
}
if other.llm.default_model != LlmConfig::default().default_model {
self.llm.default_model = other.llm.default_model;
}
}
fn apply_env(&mut self) {
if let Ok(level) = std::env::var("INFINITE_PROBABILITY_LOG_LEVEL") {
self.general.log_level = level;
}
if let Ok(val) = std::env::var("INFINITE_PROBABILITY_AISP_TIER") {
self.aisp.default_tier = val;
}
if let Ok(val) = std::env::var("INFINITE_PROBABILITY_AISP_CONFIDENCE")
&& let Ok(threshold) = val.parse() {
self.aisp.confidence_threshold = threshold;
}
if let Ok(val) = std::env::var("INFINITE_PROBABILITY_LLM_MODEL") {
self.llm.default_model = val;
}
}
pub fn save(&self, path: &PathBuf) -> Result<()> {
let content = toml::to_string_pretty(self)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, content)?;
Ok(())
}
pub fn get(&self, key: &str) -> Option<String> {
match key {
"general.log_level" => Some(self.general.log_level.clone()),
"general.telemetry" => Some(self.general.telemetry.to_string()),
"aisp.default_tier" => Some(self.aisp.default_tier.clone()),
"aisp.confidence_threshold" => Some(self.aisp.confidence_threshold.to_string()),
"aisp.enable_llm_fallback" => Some(self.aisp.enable_llm_fallback.to_string()),
"llm.default_model" => Some(self.llm.default_model.clone()),
"llm.simple_model" => Some(self.llm.simple_model.clone()),
"llm.complex_model" => Some(self.llm.complex_model.clone()),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.general.log_level, "info");
assert_eq!(config.aisp.confidence_threshold, 0.8);
}
#[test]
fn test_get_config_value() {
let config = Config::default();
assert_eq!(config.get("general.log_level"), Some("info".to_string()));
assert_eq!(config.get("unknown.key"), None);
}
}