use anyhow::Result;
use serde::Deserialize;
use serde::Serialize;
use std::fs;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU16;
use std::sync::atomic::Ordering;
use vibe_cluster::Cluster;
use vibe_cluster::ConnectionParams;
use vibe_cluster::Provider;
use crate::configs::action::ActionConfig;
use crate::configs::cluster::ClusterConfig;
use crate::models::actions::ActionsModel;
use crate::models::flow::FlowModel;
use crate::utils;
use crate::utils::constants;
use crate::utils::path;
use crate::utils::yaml::YamlComment;
use crate::validate::ValidateTrait;
static DEBUG_MODE: AtomicBool = AtomicBool::new(false);
static LEVEL_MODE: AtomicU16 = AtomicU16::new(1);
static GLOBAL_CONFIG: OnceLock<AppConfig> = OnceLock::new();
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub version: String,
pub action: ActionConfig,
pub cluster: Vec<ClusterConfig>,
#[serde(skip)]
pub actions_model: Option<ActionsModel>,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
version: constants::CONFIG_VERSION.to_string(),
action: ActionConfig::default(),
cluster: vec![
ClusterConfig::default(),
],
actions_model: None,
}
}
}
impl AppConfig {
pub fn is_debug() -> bool {
DEBUG_MODE.load(Ordering::Relaxed)
}
pub fn is_test() -> bool {
LEVEL_MODE.load(Ordering::Relaxed) == 6
}
pub fn instance() -> Result<&'static Self> {
if let Some(config) = GLOBAL_CONFIG.get() {
return Ok(config);
}
anyhow::bail!("Config not initialized. Call AppConfig::init() first.")
}
pub fn init(debug: String, level: String) -> Result<()> {
let debug = debug == "1" || debug.to_lowercase() == "true";
DEBUG_MODE.store(debug, Ordering::Relaxed);
let level = level.parse::<u16>().unwrap_or(0);
LEVEL_MODE.store(level, Ordering::Relaxed);
if debug {
let level_name = match level {
1 => "error",
2 => "warn",
3 => "info",
4 => "debug",
5 => "trace",
6 => "test",
_ => "debug",
};
let filter = if level_name == "test" {
format!("vibe_action=error")
} else {
format!("vibe_action={}", level_name)
};
let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init();
}
let config_path = path::config_path();
let mut config: AppConfig = if config_path.exists() && config_path.is_file() {
yaml_serde::from_str(&fs::read_to_string(&config_path)?)
.map_err(|e| anyhow::anyhow!("Failed to parse config: {}", e))?
} else {
let config = AppConfig::default();
config.save()?;
config
};
config.validate()?;
let actions_path = &path::actions_dir();
config.actions_model = Some(ActionsModel::load(&actions_path)?);
if std::env::var("VIBE_ACTION_PATH").is_err() {
ActionsModel::save_defaults(actions_path)?;
}
GLOBAL_CONFIG.set(config).ok();
Ok(())
}
pub fn save(&self) -> Result<()> {
let dir = path::config_dir();
if !dir.exists() {
fs::create_dir_all(&dir)?;
}
let commits = vec![
YamlComment::Field("version", vec!["Configuration version (do not modify)"]),
YamlComment::Field(
"action",
vec![
"Action runtime configuration.",
"",
"system - Global system prompt applied to all LLM requests",
"retries - Number of retries for failed LLM steps (0 = no retries)",
],
),
YamlComment::Field(
"cluster",
vec![
"LLM cluster nodes (local and cloud models).",
"",
"provider - Provider type: ollama, deepseek, qwen",
"host - API endpoint",
"model - LLM model name",
"timeout_secs - Request timeout in seconds",
"temperature - Sampling temperature (0.0 - 1.0)",
"seed - Random seed for reproducibility",
"num_ctx - Context window size",
"num_predict - Maximum tokens to generate",
"api_key - API key for cloud providers",
"parallel - Number of parallel connections (default 1)",
],
),
];
let yaml = yaml_serde::to_string(self)?;
let content = utils::yaml::add_comments(yaml, commits);
fs::write(path::config_path(), content)?;
Ok(())
}
pub fn find_flow(&self, name: &str) -> Result<FlowModel> {
let actions = self
.actions_model
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No actions loaded."))?;
actions
.find(name)
.cloned()
.ok_or_else(|| anyhow::anyhow!("Unknown action: {}", name))
}
pub fn create_cluster(&self) -> Result<Cluster> {
let connections: Vec<ConnectionParams> = self
.cluster
.iter()
.map(|c| {
let provider = match c.provider.as_str() {
"ollama" => Provider::Ollama,
"deepseek" => Provider::DeepSeek,
"qwen" => Provider::Qwen,
other => anyhow::bail!("Unknown provider: {}", other),
};
Ok(ConnectionParams {
provider,
host: c.host.clone(),
model: c.model.clone(),
temperature: Some(c.temperature),
seed: Some(c.seed),
num_ctx: Some(c.num_ctx),
num_predict: Some(c.num_predict),
timeout_secs: Some(c.timeout_secs),
api_key: c.api_key.clone(),
parallel: c.parallel,
})
})
.collect::<Result<Vec<_>>>()?;
if connections.is_empty() {
anyhow::bail!("No cluster nodes found in configuration.");
}
Cluster::new(connections).map_err(|e| anyhow::anyhow!(e.to_string()))
}
}