use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::core::error::{ConfigError, ConfigResult};
use crate::storage::{Storage, SqliteStorage, MemoryStorage};
use crate::lsp::LspConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoderLibConfig {
pub providers: HashMap<String, ProviderConfig>,
pub agents: HashMap<String, AgentConfig>,
pub storage: StorageConfig,
pub tools: ToolsConfig,
pub lsp: LspConfig,
pub mcp: crate::mcp::McpConfig,
pub debug: bool,
pub log_level: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
pub enabled: bool,
pub api_key: Option<String>,
pub base_url: Option<String>,
pub default_model: String,
pub max_tokens: Option<u32>,
pub timeout: u64,
pub rate_limit: RateLimitConfig,
pub settings: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
pub requests_per_minute: u32,
pub tokens_per_minute: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub provider: String,
pub model: String,
pub system_prompt: Option<String>,
pub max_tokens: u32,
pub temperature: f32,
pub streaming: bool,
pub tools: Vec<String>,
pub auto_summarize: AutoSummarizeConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoSummarizeConfig {
pub enabled: bool,
pub message_threshold: u32,
pub token_threshold: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub storage_type: String,
pub database_path: Option<PathBuf>,
pub pool_size: u32,
pub connection_timeout: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsConfig {
pub shell_enabled: bool,
pub file_operations_enabled: bool,
pub network_enabled: bool,
pub max_file_size: u64,
pub allowed_extensions: Vec<String>,
pub blocked_directories: Vec<PathBuf>,
}
impl Default for CoderLibConfig {
fn default() -> Self {
let mut providers = HashMap::new();
providers.insert("openai".to_string(), ProviderConfig {
enabled: false,
api_key: None,
base_url: None,
default_model: "gpt-4".to_string(),
max_tokens: Some(4000),
timeout: 30,
rate_limit: RateLimitConfig {
requests_per_minute: 60,
tokens_per_minute: 100000,
},
settings: HashMap::new(),
});
providers.insert("anthropic".to_string(), ProviderConfig {
enabled: false,
api_key: None,
base_url: None,
default_model: "claude-3-5-sonnet-20241022".to_string(),
max_tokens: Some(4000),
timeout: 30,
rate_limit: RateLimitConfig {
requests_per_minute: 60,
tokens_per_minute: 100000,
},
settings: HashMap::new(),
});
providers.insert("groq".to_string(), ProviderConfig {
enabled: false,
api_key: None,
base_url: Some("https://api.groq.com/openai/v1".to_string()),
default_model: "llama-3.1-70b-versatile".to_string(),
max_tokens: Some(4000),
timeout: 30,
rate_limit: RateLimitConfig {
requests_per_minute: 30,
tokens_per_minute: 50000,
},
settings: HashMap::new(),
});
providers.insert("cohere".to_string(), ProviderConfig {
enabled: false,
api_key: None,
base_url: Some("https://api.cohere.ai/v1".to_string()),
default_model: "command-r-plus".to_string(),
max_tokens: Some(4000),
timeout: 30,
rate_limit: RateLimitConfig {
requests_per_minute: 20,
tokens_per_minute: 40000,
},
settings: HashMap::new(),
});
providers.insert("sambanova".to_string(), ProviderConfig {
enabled: false,
api_key: None,
base_url: Some("https://api.sambanova.ai/v1".to_string()),
default_model: "Meta-Llama-3.1-70B-Instruct".to_string(),
max_tokens: Some(4000),
timeout: 30,
rate_limit: RateLimitConfig {
requests_per_minute: 20,
tokens_per_minute: 40000,
},
settings: HashMap::new(),
});
providers.insert("together".to_string(), ProviderConfig {
enabled: false,
api_key: None,
base_url: Some("https://api.together.xyz/v1".to_string()),
default_model: "meta-llama/Llama-3-70b-chat-hf".to_string(),
max_tokens: Some(4000),
timeout: 30,
rate_limit: RateLimitConfig {
requests_per_minute: 20,
tokens_per_minute: 40000,
},
settings: HashMap::new(),
});
providers.insert("gemini".to_string(), ProviderConfig {
enabled: false,
api_key: None,
base_url: None,
default_model: "gemini-1.5-pro".to_string(),
max_tokens: Some(4000),
timeout: 30,
rate_limit: RateLimitConfig {
requests_per_minute: 60,
tokens_per_minute: 100000,
},
settings: HashMap::new(),
});
let mut agents = HashMap::new();
agents.insert("coder".to_string(), AgentConfig {
provider: "openai".to_string(),
model: "gpt-4".to_string(),
system_prompt: Some("You are a helpful coding assistant.".to_string()),
max_tokens: 4000,
temperature: 0.1,
streaming: true,
tools: vec![
"file_read".to_string(),
"file_write".to_string(),
"shell_command".to_string(),
],
auto_summarize: AutoSummarizeConfig {
enabled: true,
message_threshold: 20,
token_threshold: 50000,
},
});
Self {
providers,
agents,
storage: StorageConfig {
storage_type: "sqlite".to_string(),
database_path: Some(PathBuf::from("coderlib.db")),
pool_size: 5,
connection_timeout: 30,
},
tools: ToolsConfig {
shell_enabled: true,
file_operations_enabled: true,
network_enabled: false,
max_file_size: 10 * 1024 * 1024, allowed_extensions: vec![
".rs".to_string(),
".py".to_string(),
".js".to_string(),
".ts".to_string(),
".go".to_string(),
".java".to_string(),
".cpp".to_string(),
".c".to_string(),
".h".to_string(),
".md".to_string(),
".txt".to_string(),
".json".to_string(),
".toml".to_string(),
".yaml".to_string(),
".yml".to_string(),
],
blocked_directories: vec![
PathBuf::from("/etc"),
PathBuf::from("/sys"),
PathBuf::from("/proc"),
PathBuf::from("C:\\Windows"),
PathBuf::from("C:\\System32"),
],
},
lsp: LspConfig::default(),
mcp: crate::mcp::McpConfig::default(),
debug: false,
log_level: "info".to_string(),
}
}
}
impl CoderLibConfig {
pub fn load_from_file<P: AsRef<Path>>(path: P) -> ConfigResult<Self> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| ConfigError::LoadFailed(e.to_string()))?;
let config: Self = toml::from_str(&content)
.map_err(|e| ConfigError::ParseFailed(e.to_string()))?;
config.validate()?;
Ok(config)
}
pub fn load_from_env() -> ConfigResult<Self> {
let mut config = Self::default();
if let Ok(openai_key) = std::env::var("OPENAI_API_KEY") {
if let Some(provider) = config.providers.get_mut("openai") {
provider.api_key = Some(openai_key);
provider.enabled = true;
}
}
if let Ok(anthropic_key) = std::env::var("ANTHROPIC_API_KEY") {
if let Some(provider) = config.providers.get_mut("anthropic") {
provider.api_key = Some(anthropic_key);
provider.enabled = true;
}
}
if let Ok(debug) = std::env::var("CODERLIB_DEBUG") {
config.debug = debug.parse().unwrap_or(false);
}
if let Ok(log_level) = std::env::var("CODERLIB_LOG_LEVEL") {
config.log_level = log_level;
}
config.validate()?;
Ok(config)
}
pub fn merge_with(mut self, other: Self) -> Self {
for (name, provider) in other.providers {
self.providers.insert(name, provider);
}
for (name, agent) in other.agents {
self.agents.insert(name, agent);
}
self.storage = other.storage;
self.tools = other.tools;
self.mcp = other.mcp;
self.debug = other.debug;
self.log_level = other.log_level;
self
}
pub fn validate(&self) -> ConfigResult<()> {
let enabled_providers: Vec<_> = self.providers
.iter()
.filter(|(_, config)| config.enabled)
.collect();
if enabled_providers.is_empty() {
return Err(ConfigError::InvalidValue(
"At least one provider must be enabled".to_string()
));
}
for (name, config) in &enabled_providers {
if config.api_key.is_none() {
return Err(ConfigError::MissingRequired(
format!("API key for provider '{}'", name)
));
}
}
for (name, agent) in &self.agents {
if !self.providers.contains_key(&agent.provider) {
return Err(ConfigError::InvalidValue(
format!("Agent '{}' references unknown provider '{}'", name, agent.provider)
));
}
}
Ok(())
}
pub async fn create_storage(&self) -> ConfigResult<Arc<dyn Storage>> {
match self.storage.storage_type.as_str() {
"sqlite" => {
let db_path = self.storage.database_path
.as_ref()
.ok_or_else(|| ConfigError::MissingRequired("database_path for SQLite".to_string()))?;
let storage = SqliteStorage::new(db_path).await
.map_err(|e| ConfigError::InvalidValue(format!("Failed to create SQLite storage: {}", e)))?;
Ok(Arc::new(storage))
}
"memory" => {
let storage = MemoryStorage::new();
Ok(Arc::new(storage))
}
_ => Err(ConfigError::InvalidValue(
format!("Unknown storage type: {}", self.storage.storage_type)
)),
}
}
pub fn default_agent(&self) -> ConfigResult<&AgentConfig> {
self.agents.get("coder")
.ok_or_else(|| ConfigError::MissingRequired("default agent 'coder'".to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
use std::io::Write;
#[test]
fn test_default_config() {
let config = CoderLibConfig::default();
assert!(!config.providers.is_empty());
assert!(!config.agents.is_empty());
assert_eq!(config.storage.storage_type, "sqlite");
}
#[test]
fn test_config_validation() {
let mut config = CoderLibConfig::default();
assert!(config.validate().is_err());
config.providers.get_mut("openai").unwrap().enabled = true;
config.providers.get_mut("openai").unwrap().api_key = Some("test-key".to_string());
assert!(config.validate().is_ok());
}
#[test]
fn test_config_file_loading() {
let config_content = r#"
debug = true
log_level = "debug"
[lsp]
enabled = true
timeout = { secs = 30, nanos = 0 }
max_servers = 10
[providers.openai]
enabled = true
api_key = "test-key"
default_model = "gpt-4"
max_tokens = 4000
timeout = 30
[providers.openai.settings]
base_url = "https://api.openai.com/v1"
[providers.openai.rate_limit]
requests_per_minute = 60
tokens_per_minute = 100000
[agents.coder]
provider = "openai"
model = "gpt-4"
max_tokens = 4000
temperature = 0.1
streaming = true
tools = ["file_read", "file_write"]
[agents.coder.auto_summarize]
enabled = true
message_threshold = 20
token_threshold = 50000
[storage]
storage_type = "sqlite"
database_path = "test.db"
pool_size = 5
connection_timeout = 30
[tools]
shell_enabled = true
file_operations_enabled = true
network_enabled = false
max_file_size = 10485760
allowed_extensions = [".rs", ".py"]
blocked_directories = ["/etc"]
"#;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(config_content.as_bytes()).unwrap();
let config = CoderLibConfig::load_from_file(temp_file.path()).unwrap();
assert!(config.debug);
assert_eq!(config.log_level, "debug");
assert!(config.providers.get("openai").unwrap().enabled);
}
}