use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Config directory not found: {0}")]
DirectoryNotFound(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to parse config file: {0}")]
ParseError(#[from] toml::de::Error),
#[error("Failed to serialize config file: {0}")]
SerializeError(#[from] toml::ser::Error),
#[error("Invalid configuration: {0}")]
#[allow(dead_code)] ValidationError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasonKitConfig {
pub general: GeneralConfig,
pub providers: ProviderConfigs,
pub thinktools: ThinkToolConfig,
#[cfg(feature = "memory")]
pub knowledge_base: KnowledgeBaseConfig,
pub plugins: PluginConfigs,
pub output: OutputConfig,
pub performance: PerformanceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneralConfig {
pub data_dir: PathBuf,
pub config_file: Option<PathBuf>,
pub enable_telemetry: bool,
pub log_level: String,
pub check_updates: bool,
pub experimental_features: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfigs {
pub default_provider: String,
#[serde(default)]
pub api_keys: HashMap<String, String>,
#[serde(default)]
pub configurations: HashMap<String, ProviderConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
pub default_model: String,
pub endpoint: Option<String>,
pub timeout: Option<u64>,
pub rate_limit: Option<RateLimitConfig>,
pub fallbacks: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
pub requests_per_minute: u32,
pub tokens_per_minute: Option<u64>,
pub concurrent_requests: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThinkToolConfig {
pub default_profile: String,
#[serde(default)]
pub protocols: HashMap<String, ProtocolConfig>,
pub confidence: ConfidenceThresholds,
pub budget_defaults: BudgetDefaults,
pub limits: ThinkToolLimits,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtocolConfig {
pub temperature: f64,
pub max_tokens: u32,
pub timeout_ms: Option<u64>,
pub enable_cross_validation: bool,
pub min_confidence: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfidenceThresholds {
pub quick: f64,
pub balanced: f64,
pub deep: f64,
pub paranoid: f64,
pub decide: f64,
pub scientific: f64,
pub graph: f64,
pub consistent: f64,
pub verified: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetDefaults {
pub cost: Option<f64>,
pub tokens: Option<u64>,
pub time: Option<u64>,
pub steps: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThinkToolLimits {
pub max_steps: usize,
pub max_queries_per_day: Option<u32>,
pub max_concurrent: usize,
pub max_cache_size_mb: u64,
}
#[cfg(feature = "memory")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeBaseConfig {
pub backend: String,
pub index: IndexConfig,
pub retrieval: RetrievalConfig,
pub embedding: EmbeddingConfig,
}
#[cfg(feature = "memory")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexConfig {
pub chunk_size: usize,
pub chunk_overlap: usize,
pub rebuild_interval_hours: u32,
pub real_time: bool,
}
#[cfg(feature = "memory")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetrievalConfig {
pub default_top_k: usize,
pub bm25_weight: f32,
pub vector_weight: f32,
pub raptor_depth: usize,
pub reranking_enabled: bool,
}
#[cfg(feature = "memory")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingConfig {
pub model: String,
pub dimension: usize,
pub cache_enabled: bool,
pub cache_ttl_hours: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginConfigs {
pub plugin_dir: PathBuf,
#[serde(default)]
pub enabled: Vec<String>,
#[serde(default)]
pub configurations: HashMap<String, PluginConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginConfig {
pub version: Option<String>,
pub author: Option<String>,
pub description: Option<String>,
#[serde(default)]
pub settings: HashMap<String, toml::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
pub default_format: String,
pub color: bool,
pub ansi: bool,
pub pretty_json: bool,
pub progress_bars: bool,
pub verbosity: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
pub caching_enabled: bool,
pub cache_dir: PathBuf,
pub cache_ttl_hours: u32,
pub concurrent_workers: usize,
pub memory_limit_mb: Option<u64>,
pub monitoring_enabled: bool,
pub alert_threshold: f64,
}
pub struct ConfigManager {
config_dir: PathBuf,
config_file: PathBuf,
config: ReasonKitConfig,
#[allow(dead_code)] overrides: HashMap<String, String>,
}
#[allow(dead_code)] impl ConfigManager {
pub fn new() -> Result<Self, ConfigError> {
let config_dir = Self::get_config_dir()?;
let config_file = config_dir.join("config.toml");
let config = if config_file.exists() {
Self::load_config(&config_file)?
} else {
Self::default_config()
};
Ok(Self {
config_dir,
config_file,
config,
overrides: HashMap::new(),
})
}
pub fn get_config_dir() -> Result<PathBuf, ConfigError> {
let mut config_dir = dirs::config_dir()
.ok_or_else(|| ConfigError::DirectoryNotFound("User config directory".to_string()))?;
config_dir.push("reasonkit");
if !config_dir.exists() {
fs::create_dir_all(&config_dir)
.map_err(|e| ConfigError::DirectoryNotFound(e.to_string()))?;
}
Ok(config_dir)
}
pub fn load_config(path: &Path) -> Result<ReasonKitConfig, ConfigError> {
let content = fs::read_to_string(path)?;
let config = toml::from_str(&content)?;
Ok(config)
}
pub fn default_config() -> ReasonKitConfig {
ReasonKitConfig {
general: GeneralConfig {
data_dir: dirs::data_dir()
.map(|mut d| {
d.push("reasonkit");
d
})
.unwrap_or_else(|| PathBuf::from("./data")),
config_file: None,
enable_telemetry: true,
log_level: "info".to_string(),
check_updates: true,
experimental_features: Vec::new(),
},
providers: ProviderConfigs {
default_provider: "anthropic".to_string(),
api_keys: HashMap::new(),
configurations: HashMap::new(),
},
thinktools: ThinkToolConfig {
default_profile: "balanced".to_string(),
protocols: HashMap::new(),
confidence: ConfidenceThresholds {
quick: 0.70,
balanced: 0.80,
deep: 0.85,
paranoid: 0.95,
decide: 0.85,
scientific: 0.90,
graph: 0.80,
consistent: 0.85,
verified: 0.90,
},
budget_defaults: BudgetDefaults {
cost: None,
tokens: Some(10000),
time: Some(30),
steps: Some(10),
},
limits: ThinkToolLimits {
max_steps: 20,
max_queries_per_day: Some(100),
max_concurrent: 4,
max_cache_size_mb: 100,
},
},
#[cfg(feature = "memory")]
knowledge_base: KnowledgeBaseConfig {
backend: "sqlite".to_string(),
index: IndexConfig {
chunk_size: 1000,
chunk_overlap: 200,
rebuild_interval_hours: 24,
real_time: true,
},
retrieval: RetrievalConfig {
default_top_k: 5,
bm25_weight: 0.5,
vector_weight: 0.5,
raptor_depth: 3,
reranking_enabled: true,
},
embedding: EmbeddingConfig {
model: "bge-base-en-v1.5".to_string(),
dimension: 768,
cache_enabled: true,
cache_ttl_hours: 24,
},
},
plugins: PluginConfigs {
plugin_dir: Self::get_config_dir()
.unwrap_or_else(|_| PathBuf::from("./plugins"))
.join("plugins"),
enabled: Vec::new(),
configurations: HashMap::new(),
},
output: OutputConfig {
default_format: "text".to_string(),
color: true,
ansi: true,
pretty_json: true,
progress_bars: true,
verbosity: 1,
},
performance: PerformanceConfig {
caching_enabled: true,
cache_dir: Self::get_config_dir()
.unwrap_or_else(|_| PathBuf::from("./cache"))
.join("cache"),
cache_ttl_hours: 24,
concurrent_workers: 4,
memory_limit_mb: Some(512),
monitoring_enabled: true,
alert_threshold: 0.05,
},
}
}
pub fn save(&self) -> Result<(), ConfigError> {
let content = toml::to_string_pretty(&self.config)?;
fs::write(&self.config_file, content)?;
Ok(())
}
pub fn config(&self) -> &ReasonKitConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut ReasonKitConfig {
&mut self.config
}
pub fn config_dir(&self) -> &Path {
&self.config_dir
}
pub fn config_file(&self) -> &Path {
&self.config_file
}
pub fn set_override(&mut self, key: String, value: String) {
self.overrides.insert(key, value);
}
pub fn get_with_overrides(&self) -> ReasonKitConfig {
self.config.clone()
}
pub fn reset_defaults(&mut self) -> Result<(), ConfigError> {
self.config = Self::default_config();
self.save()
}
pub fn validate(&self) -> Result<(), ConfigError> {
let thresholds = &self.config.thinktools.confidence;
if thresholds.quick < 0.0 || thresholds.quick > 1.0 {
return Err(ConfigError::ValidationError(
"quick confidence must be between 0.0 and 1.0".to_string(),
));
}
if thresholds.balanced < 0.0 || thresholds.balanced > 1.0 {
return Err(ConfigError::ValidationError(
"balanced confidence must be between 0.0 and 1.0".to_string(),
));
}
if thresholds.deep < 0.0 || thresholds.deep > 1.0 {
return Err(ConfigError::ValidationError(
"deep confidence must be between 0.0 and 1.0".to_string(),
));
}
if thresholds.paranoid < 0.0 || thresholds.paranoid > 1.0 {
return Err(ConfigError::ValidationError(
"paranoid confidence must be between 0.0 and 1.0".to_string(),
));
}
if !(thresholds.quick <= thresholds.balanced
&& thresholds.balanced <= thresholds.deep
&& thresholds.deep <= thresholds.paranoid)
{
return Err(ConfigError::ValidationError(
"confidence thresholds should be increasing: quick <= balanced <= deep <= paranoid"
.to_string(),
));
}
if self.config.performance.alert_threshold < 0.0
|| self.config.performance.alert_threshold > 1.0
{
return Err(ConfigError::ValidationError(
"alert threshold must be between 0.0 and 1.0".to_string(),
));
}
Ok(())
}
pub fn show(&self) -> String {
let mut output = String::new();
output.push_str(&format!("Config file: {}\n", self.config_file.display()));
output.push_str(&format!(
"Config directory: {}\n\n",
self.config_dir.display()
));
output.push_str("General Settings:\n");
output.push_str(&format!(
" Data directory: {}\n",
self.config.general.data_dir.display()
));
output.push_str(&format!(" Log level: {}\n", self.config.general.log_level));
output.push_str(&format!(
" Telemetry: {}\n",
self.config.general.enable_telemetry
));
output.push_str("\nThinkTool Settings:\n");
output.push_str(&format!(
" Default profile: {}\n",
self.config.thinktools.default_profile
));
output.push_str(&format!(
" Max steps: {}\n",
self.config.thinktools.limits.max_steps
));
output.push_str(" Confidence thresholds:\n");
output.push_str(&format!(
" Quick: {:.0}%\n",
self.config.thinktools.confidence.quick * 100.0
));
output.push_str(&format!(
" Balanced: {:.0}%\n",
self.config.thinktools.confidence.balanced * 100.0
));
output.push_str(&format!(
" Deep: {:.0}%\n",
self.config.thinktools.confidence.deep * 100.0
));
output.push_str(&format!(
" Paranoid: {:.0}%\n",
self.config.thinktools.confidence.paranoid * 100.0
));
output.push_str("\nOutput Settings:\n");
output.push_str(&format!(
" Default format: {}\n",
self.config.output.default_format
));
output.push_str(&format!(" Color output: {}\n", self.config.output.color));
output.push_str(&format!(" Verbosity: {}\n", self.config.output.verbosity));
output.push_str("\nPerformance Settings:\n");
output.push_str(&format!(
" Caching: {}\n",
self.config.performance.caching_enabled
));
output.push_str(&format!(
" Concurrent workers: {}\n",
self.config.performance.concurrent_workers
));
output.push_str(&format!(
" Alert threshold: {:.1}%\n",
self.config.performance.alert_threshold * 100.0
));
output
}
pub fn generate_example_config() -> String {
let config = Self::default_config();
toml::to_string_pretty(&config)
.unwrap_or_else(|_| "# ReasonKit Configuration Example\n# Auto-generated\n".to_string())
}
}