use std::collections::HashMap;
use std::path::Path;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct ProviderEntry {
#[serde(default)]
pub api_key: String,
#[serde(default = "default_base_url")]
pub base_url: String,
}
fn default_base_url() -> String {
"https://api.openai.com/v1".to_string()
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct Config {
pub active_provider: String,
pub active_model: String,
pub providers: HashMap<String, ProviderEntry>,
pub provider: String,
pub model: String,
pub api_key: String,
pub base_url: String,
pub system_prompt: String,
pub max_tokens: Option<u32>,
pub max_iterations: u32,
pub temperature: f32,
pub tool_modules: Option<String>,
pub memory_enabled: bool,
pub memory_dir: Option<String>,
pub memory_db: Option<String>,
pub verbose: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
active_provider: "default".into(),
active_model: "gpt-4o".into(),
providers: HashMap::new(),
provider: "openai".into(),
model: "gpt-4o".into(),
api_key: String::new(),
base_url: default_base_url(),
system_prompt: "You are Cortex, an autonomous agent. Answer from your own knowledge first.".into(),
max_tokens: None,
max_iterations: 10,
temperature: 0.7,
tool_modules: Some("cortex.tools.core,cortex.tools.memory_tools,cortex.tools.skill_tools,cortex.tools.self_tools".into()),
memory_enabled: true,
memory_dir: None,
memory_db: None,
verbose: false,
}
}
}
fn resolve_env_vars(value: serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::String(s) => {
let mut result = s;
while let Some(start) = result.find("${") {
if let Some(end) = result[start..].find('}') {
let var_name = &result[start + 2..start + end];
let env_val = std::env::var(var_name).unwrap_or_default();
result.replace_range(start..=start + end, &env_val);
} else {
break;
}
}
serde_json::Value::String(result)
}
serde_json::Value::Object(map) => {
let new_map: serde_json::Map<String, serde_json::Value> = map
.into_iter()
.map(|(k, v)| (k, resolve_env_vars(v)))
.collect();
serde_json::Value::Object(new_map)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(resolve_env_vars).collect())
}
other => other,
}
}
impl Config {
pub fn from_yaml(path: &str) -> anyhow::Result<Self> {
let path = Path::new(path);
if !path.exists() {
return Ok(Self::default());
}
let contents = std::fs::read_to_string(path)?;
let raw: serde_json::Value = serde_yaml::from_str(&contents)?;
let resolved = resolve_env_vars(raw);
let config: Self = serde_json::from_value(resolved)?;
Ok(config)
}
pub fn get_active_provider_config(&self) -> (String, String, String) {
let name = &self.active_provider;
if let Some(entry) = self.providers.get(name) {
return (name.clone(), entry.api_key.clone(), entry.base_url.clone());
}
(self.provider.clone(), self.api_key.clone(), self.base_url.clone())
}
pub fn get_provider_names(&self) -> Vec<String> {
let names: Vec<String> = self.providers.keys().cloned().collect();
if names.is_empty() {
vec![if self.provider.is_empty() {
"default".into()
} else {
self.provider.clone()
}]
} else {
names
}
}
#[allow(dead_code)]
pub fn provider_summary(&self) -> String {
let (_name, _key, url) = self.get_active_provider_config();
let short_url = url
.replace("https://", "")
.split('/')
.next()
.unwrap_or(&url)
.to_string();
format!("{} (via {})", self.active_model, short_url)
}
pub fn memory_dir_resolved(&self) -> String {
self.memory_dir
.clone()
.unwrap_or_else(|| {
let home = std::env::var("HOME").unwrap_or_else(|_| "~".into());
format!("{}/.cortex/memory", home)
})
.replace('~', &std::env::var("HOME").unwrap_or_default())
}
pub fn memory_db_resolved(&self) -> String {
self.memory_db.clone().unwrap_or_else(|| "cortex.db".into())
}
}