use super::schema::AgentConfig;
use config::FileFormat;
use mofa_kernel::config::{ConfigError, detect_format, from_str, load_config, load_merged};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConfigFormat {
Yaml,
Toml,
Json,
Ini,
Ron,
Json5,
}
#[derive(Debug, thiserror::Error)]
pub enum AgentConfigError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Config parse error: {0}")]
Parse(String),
#[error("Config serialization error: {0}")]
Serialization(String),
#[error("Unsupported config format: {0}")]
UnsupportedFormat(String),
#[error("Config validation failed: {0}")]
Validation(String),
}
pub type AgentResult<T> = Result<T, AgentConfigError>;
impl ConfigFormat {
pub fn from_extension(path: &str) -> Option<Self> {
match detect_format(path) {
Ok(FileFormat::Yaml) => Some(Self::Yaml),
Ok(FileFormat::Toml) => Some(Self::Toml),
Ok(FileFormat::Json) => Some(Self::Json),
Ok(FileFormat::Ini) => Some(Self::Ini),
Ok(FileFormat::Ron) => Some(Self::Ron),
Ok(FileFormat::Json5) => Some(Self::Json5),
_ => None,
}
}
pub fn to_file_format(self) -> FileFormat {
match self {
Self::Yaml => FileFormat::Yaml,
Self::Toml => FileFormat::Toml,
Self::Json => FileFormat::Json,
Self::Ini => FileFormat::Ini,
Self::Ron => FileFormat::Ron,
Self::Json5 => FileFormat::Json5,
}
}
pub fn name(&self) -> &str {
match self {
Self::Yaml => "yaml",
Self::Toml => "toml",
Self::Json => "json",
Self::Ini => "ini",
Self::Ron => "ron",
Self::Json5 => "json5",
}
}
pub fn default_extension(&self) -> &str {
match self {
Self::Yaml => "yml",
Self::Toml => "toml",
Self::Json => "json",
Self::Ini => "ini",
Self::Ron => "ron",
Self::Json5 => "json5",
}
}
}
pub struct ConfigLoader;
impl ConfigLoader {
pub fn from_str(content: &str, format: ConfigFormat) -> AgentResult<AgentConfig> {
from_str(content, format.to_file_format()).map_err(|e| match e {
ConfigError::Parse(e) => AgentConfigError::Parse(e.to_string()),
ConfigError::Serialization(e) => AgentConfigError::Serialization(e),
ConfigError::UnsupportedFormat(e) => AgentConfigError::UnsupportedFormat(e),
_ => AgentConfigError::Parse(e.to_string()),
})
}
pub fn from_yaml(content: &str) -> AgentResult<AgentConfig> {
Self::from_str(content, ConfigFormat::Yaml)
}
pub fn from_toml(content: &str) -> AgentResult<AgentConfig> {
Self::from_str(content, ConfigFormat::Toml)
}
pub fn from_json(content: &str) -> AgentResult<AgentConfig> {
Self::from_str(content, ConfigFormat::Json)
}
pub fn from_ini(content: &str) -> AgentResult<AgentConfig> {
Self::from_str(content, ConfigFormat::Ini)
}
pub fn from_ron(content: &str) -> AgentResult<AgentConfig> {
Self::from_str(content, ConfigFormat::Ron)
}
pub fn from_json5(content: &str) -> AgentResult<AgentConfig> {
Self::from_str(content, ConfigFormat::Json5)
}
pub fn load_file(path: &str) -> AgentResult<AgentConfig> {
let config: AgentConfig = load_config(path).map_err(|e| match e {
ConfigError::Io(e) => AgentConfigError::Io(e),
ConfigError::Parse(e) => AgentConfigError::Parse(e),
ConfigError::Serialization(e) => AgentConfigError::Serialization(e),
ConfigError::UnsupportedFormat(e) => AgentConfigError::UnsupportedFormat(e),
})?;
config
.validate()
.map_err(|errors| AgentConfigError::Validation(errors.join(", ")))?;
Ok(config)
}
pub fn load_yaml(path: &str) -> AgentResult<AgentConfig> {
Self::load_file(path)
}
pub fn load_toml(path: &str) -> AgentResult<AgentConfig> {
Self::load_file(path)
}
pub fn load_json(path: &str) -> AgentResult<AgentConfig> {
Self::load_file(path)
}
pub fn load_ini(path: &str) -> AgentResult<AgentConfig> {
Self::load_file(path)
}
pub fn load_ron(path: &str) -> AgentResult<AgentConfig> {
Self::load_file(path)
}
pub fn load_json5(path: &str) -> AgentResult<AgentConfig> {
Self::load_file(path)
}
pub fn to_string(config: &AgentConfig, format: ConfigFormat) -> AgentResult<String> {
let content = match format {
ConfigFormat::Yaml => serde_yaml::to_string(config).map_err(|e| {
AgentConfigError::Serialization(format!("Failed to serialize to YAML: {}", e))
})?,
ConfigFormat::Toml => toml::to_string_pretty(config).map_err(|e| {
AgentConfigError::Serialization(format!("Failed to serialize to TOML: {}", e))
})?,
ConfigFormat::Json => serde_json::to_string_pretty(config).map_err(|e| {
AgentConfigError::Serialization(format!("Failed to serialize to JSON: {}", e))
})?,
ConfigFormat::Ini => {
return Err(AgentConfigError::Serialization(
"INI serialization not directly supported. Use JSON, YAML, or TOML for saving."
.to_string(),
));
}
ConfigFormat::Ron => {
return Err(AgentConfigError::Serialization(
"RON serialization not directly supported. Use JSON, YAML, or TOML for saving."
.to_string(),
));
}
ConfigFormat::Json5 => {
serde_json::to_string_pretty(config).map_err(|e| {
AgentConfigError::Serialization(format!("Failed to serialize to JSON5: {}", e))
})?
}
};
Ok(content)
}
pub fn save_file(config: &AgentConfig, path: &str) -> AgentResult<()> {
let format = ConfigFormat::from_extension(path).ok_or_else(|| {
AgentConfigError::UnsupportedFormat(format!(
"Unable to determine config format from file extension: {}",
path
))
})?;
let content = Self::to_string(config, format)?;
std::fs::write(path, content).map_err(|e| AgentConfigError::Io(e))?;
Ok(())
}
pub fn load_directory(dir_path: &str) -> AgentResult<Vec<AgentConfig>> {
let mut configs = Vec::new();
let entries = std::fs::read_dir(dir_path).map_err(|e| AgentConfigError::Io(e))?;
let supported_extensions = ["yaml", "yml", "toml", "json", "ini", "ron", "json5"];
for entry in entries {
let entry = entry.map_err(|e| AgentConfigError::Io(e))?;
let path = entry.path();
if path.is_file()
&& let Some(ext) = path.extension().and_then(|e| e.to_str())
{
let ext_lower = ext.to_lowercase();
if supported_extensions.contains(&ext_lower.as_str()) {
let path_str = path.to_string_lossy().to_string();
match Self::load_file(&path_str) {
Ok(config) => configs.push(config),
Err(e) => {
tracing::warn!("Failed to load config '{}': {}", path_str, e);
}
}
}
}
}
Ok(configs)
}
pub fn merge(base: AgentConfig, overlay: AgentConfig) -> AgentConfig {
AgentConfig {
id: if overlay.id.is_empty() {
base.id
} else {
overlay.id
},
name: if overlay.name.is_empty() {
base.name
} else {
overlay.name
},
description: overlay.description.or(base.description),
agent_type: overlay.agent_type,
components: ComponentsConfig {
reasoner: overlay.components.reasoner.or(base.components.reasoner),
memory: overlay.components.memory.or(base.components.memory),
coordinator: overlay
.components
.coordinator
.or(base.components.coordinator),
},
capabilities: if overlay.capabilities.tags.is_empty() {
base.capabilities
} else {
overlay.capabilities
},
custom: {
let mut merged = base.custom;
merged.extend(overlay.custom);
merged
},
env_mappings: {
let mut merged = base.env_mappings;
merged.extend(overlay.env_mappings);
merged
},
enabled: overlay.enabled,
version: overlay.version.or(base.version),
}
}
pub fn load_merged_files(paths: &[&str]) -> AgentResult<AgentConfig> {
load_merged(paths).map_err(|e| match e {
ConfigError::Io(e) => AgentConfigError::Io(e),
ConfigError::Parse(e) => AgentConfigError::Parse(e.to_string()),
ConfigError::Serialization(e) => AgentConfigError::Serialization(e),
ConfigError::UnsupportedFormat(e) => AgentConfigError::UnsupportedFormat(e),
})
}
}
use super::schema::ComponentsConfig;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_from_extension() {
assert_eq!(
ConfigFormat::from_extension("config.yaml"),
Some(ConfigFormat::Yaml)
);
assert_eq!(
ConfigFormat::from_extension("config.yml"),
Some(ConfigFormat::Yaml)
);
assert_eq!(
ConfigFormat::from_extension("config.toml"),
Some(ConfigFormat::Toml)
);
assert_eq!(
ConfigFormat::from_extension("config.json"),
Some(ConfigFormat::Json)
);
assert_eq!(
ConfigFormat::from_extension("config.ini"),
Some(ConfigFormat::Ini)
);
assert_eq!(
ConfigFormat::from_extension("config.ron"),
Some(ConfigFormat::Ron)
);
assert_eq!(
ConfigFormat::from_extension("config.json5"),
Some(ConfigFormat::Json5)
);
assert_eq!(ConfigFormat::from_extension("config.txt"), None);
}
#[test]
fn test_format_to_file_format() {
assert_eq!(ConfigFormat::Yaml.to_file_format(), FileFormat::Yaml);
assert_eq!(ConfigFormat::Toml.to_file_format(), FileFormat::Toml);
assert_eq!(ConfigFormat::Json.to_file_format(), FileFormat::Json);
assert_eq!(ConfigFormat::Ini.to_file_format(), FileFormat::Ini);
assert_eq!(ConfigFormat::Ron.to_file_format(), FileFormat::Ron);
assert_eq!(ConfigFormat::Json5.to_file_format(), FileFormat::Json5);
}
#[test]
fn test_load_yaml_string() {
let yaml = r#"
id: test-agent
name: Test Agent
type: llm
model: gpt-4
temperature: 0.8
"#;
let config = ConfigLoader::from_yaml(yaml).unwrap();
assert_eq!(config.id, "test-agent");
assert_eq!(config.name, "Test Agent");
}
#[test]
fn test_load_json_string() {
let json = r#"{
"id": "test-agent",
"name": "Test Agent",
"type": "llm",
"model": "gpt-4"
}"#;
let config = ConfigLoader::from_json(json).unwrap();
assert_eq!(config.id, "test-agent");
assert_eq!(config.name, "Test Agent");
}
#[test]
fn test_load_toml_string() {
let toml = r#"
id = "test-agent"
name = "Test Agent"
type = "llm"
model = "gpt-4"
"#;
let config = ConfigLoader::from_toml(toml).unwrap();
assert_eq!(config.id, "test-agent");
assert_eq!(config.name, "Test Agent");
}
#[test]
fn test_load_ini_string() {
let ini = r#"
id = "test-agent"
name = "Test Agent"
type = "llm"
model = "gpt-4"
"#;
let config = ConfigLoader::from_ini(ini).unwrap();
assert_eq!(config.id, "test-agent");
assert_eq!(config.name, "Test Agent");
}
#[test]
fn test_load_ron_string() {
let ron = r#"
(
id: "test-agent",
name: "Test Agent",
type: "llm",
model: "gpt-4",
)
"#;
let config = ConfigLoader::from_ron(ron).unwrap();
assert_eq!(config.id, "test-agent");
assert_eq!(config.name, "Test Agent");
}
#[test]
fn test_load_json5_string() {
let json5 = r#"{
// JSON5 allows comments
id: "test-agent",
name: "Test Agent",
type: "llm",
model: "gpt-4",
}
"#;
let config = ConfigLoader::from_json5(json5).unwrap();
assert_eq!(config.id, "test-agent");
assert_eq!(config.name, "Test Agent");
}
#[test]
fn test_serialize_config() {
let config = AgentConfig::new("my-agent", "My Agent");
let yaml = ConfigLoader::to_string(&config, ConfigFormat::Yaml).unwrap();
assert!(yaml.contains("my-agent"));
let json = ConfigLoader::to_string(&config, ConfigFormat::Json).unwrap();
assert!(json.contains("my-agent"));
let toml = ConfigLoader::to_string(&config, ConfigFormat::Toml).unwrap();
assert!(toml.contains("my-agent"));
}
#[test]
fn test_merge_configs() {
let base =
AgentConfig::new("base-agent", "Base Agent").with_description("Base description");
let overlay = AgentConfig {
id: String::new(), name: "Override Name".to_string(),
description: Some("Override description".to_string()),
..Default::default()
};
let merged = ConfigLoader::merge(base, overlay);
assert_eq!(merged.id, "base-agent"); assert_eq!(merged.name, "Override Name"); assert_eq!(merged.description, Some("Override description".to_string())); }
#[test]
fn test_format_names() {
assert_eq!(ConfigFormat::Yaml.name(), "yaml");
assert_eq!(ConfigFormat::Toml.name(), "toml");
assert_eq!(ConfigFormat::Json.name(), "json");
assert_eq!(ConfigFormat::Ini.name(), "ini");
assert_eq!(ConfigFormat::Ron.name(), "ron");
assert_eq!(ConfigFormat::Json5.name(), "json5");
}
#[test]
fn test_default_extensions() {
assert_eq!(ConfigFormat::Yaml.default_extension(), "yml");
assert_eq!(ConfigFormat::Toml.default_extension(), "toml");
assert_eq!(ConfigFormat::Json.default_extension(), "json");
assert_eq!(ConfigFormat::Ini.default_extension(), "ini");
assert_eq!(ConfigFormat::Ron.default_extension(), "ron");
assert_eq!(ConfigFormat::Json5.default_extension(), "json5");
}
}