1use super::schema::AgentConfig;
8use config::FileFormat;
9use mofa_kernel::config::{ConfigError, detect_format, from_str, load_config, load_merged};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum ConfigFormat {
15 Yaml,
17 Toml,
19 Json,
21 Ini,
23 Ron,
25 Json5,
27}
28
29#[derive(Debug, thiserror::Error)]
31pub enum AgentConfigError {
32 #[error("IO error: {0}")]
33 Io(#[from] std::io::Error),
34
35 #[error("Config parse error: {0}")]
36 Parse(String),
37
38 #[error("Config serialization error: {0}")]
39 Serialization(String),
40
41 #[error("Unsupported config format: {0}")]
42 UnsupportedFormat(String),
43
44 #[error("Config validation failed: {0}")]
45 Validation(String),
46}
47
48pub type AgentResult<T> = Result<T, AgentConfigError>;
50
51impl ConfigFormat {
52 pub fn from_extension(path: &str) -> Option<Self> {
54 match detect_format(path) {
55 Ok(FileFormat::Yaml) => Some(Self::Yaml),
56 Ok(FileFormat::Toml) => Some(Self::Toml),
57 Ok(FileFormat::Json) => Some(Self::Json),
58 Ok(FileFormat::Ini) => Some(Self::Ini),
59 Ok(FileFormat::Ron) => Some(Self::Ron),
60 Ok(FileFormat::Json5) => Some(Self::Json5),
61 _ => None,
62 }
63 }
64
65 pub fn to_file_format(self) -> FileFormat {
67 match self {
68 Self::Yaml => FileFormat::Yaml,
69 Self::Toml => FileFormat::Toml,
70 Self::Json => FileFormat::Json,
71 Self::Ini => FileFormat::Ini,
72 Self::Ron => FileFormat::Ron,
73 Self::Json5 => FileFormat::Json5,
74 }
75 }
76
77 pub fn name(&self) -> &str {
79 match self {
80 Self::Yaml => "yaml",
81 Self::Toml => "toml",
82 Self::Json => "json",
83 Self::Ini => "ini",
84 Self::Ron => "ron",
85 Self::Json5 => "json5",
86 }
87 }
88
89 pub fn default_extension(&self) -> &str {
91 match self {
92 Self::Yaml => "yml",
93 Self::Toml => "toml",
94 Self::Json => "json",
95 Self::Ini => "ini",
96 Self::Ron => "ron",
97 Self::Json5 => "json5",
98 }
99 }
100}
101
102pub struct ConfigLoader;
136
137impl ConfigLoader {
138 pub fn from_str(content: &str, format: ConfigFormat) -> AgentResult<AgentConfig> {
140 from_str(content, format.to_file_format()).map_err(|e| match e {
141 ConfigError::Parse(e) => AgentConfigError::Parse(e.to_string()),
142 ConfigError::Serialization(e) => AgentConfigError::Serialization(e),
143 ConfigError::UnsupportedFormat(e) => AgentConfigError::UnsupportedFormat(e),
144 _ => AgentConfigError::Parse(e.to_string()),
145 })
146 }
147
148 pub fn from_yaml(content: &str) -> AgentResult<AgentConfig> {
150 Self::from_str(content, ConfigFormat::Yaml)
151 }
152
153 pub fn from_toml(content: &str) -> AgentResult<AgentConfig> {
155 Self::from_str(content, ConfigFormat::Toml)
156 }
157
158 pub fn from_json(content: &str) -> AgentResult<AgentConfig> {
160 Self::from_str(content, ConfigFormat::Json)
161 }
162
163 pub fn from_ini(content: &str) -> AgentResult<AgentConfig> {
165 Self::from_str(content, ConfigFormat::Ini)
166 }
167
168 pub fn from_ron(content: &str) -> AgentResult<AgentConfig> {
170 Self::from_str(content, ConfigFormat::Ron)
171 }
172
173 pub fn from_json5(content: &str) -> AgentResult<AgentConfig> {
175 Self::from_str(content, ConfigFormat::Json5)
176 }
177
178 pub fn load_file(path: &str) -> AgentResult<AgentConfig> {
180 let config: AgentConfig = load_config(path).map_err(|e| match e {
181 ConfigError::Io(e) => AgentConfigError::Io(e),
182 ConfigError::Parse(e) => AgentConfigError::Parse(e),
183 ConfigError::Serialization(e) => AgentConfigError::Serialization(e),
184 ConfigError::UnsupportedFormat(e) => AgentConfigError::UnsupportedFormat(e),
185 })?;
186
187 config
189 .validate()
190 .map_err(|errors| AgentConfigError::Validation(errors.join(", ")))?;
191
192 Ok(config)
193 }
194
195 pub fn load_yaml(path: &str) -> AgentResult<AgentConfig> {
197 Self::load_file(path)
198 }
199
200 pub fn load_toml(path: &str) -> AgentResult<AgentConfig> {
202 Self::load_file(path)
203 }
204
205 pub fn load_json(path: &str) -> AgentResult<AgentConfig> {
207 Self::load_file(path)
208 }
209
210 pub fn load_ini(path: &str) -> AgentResult<AgentConfig> {
212 Self::load_file(path)
213 }
214
215 pub fn load_ron(path: &str) -> AgentResult<AgentConfig> {
217 Self::load_file(path)
218 }
219
220 pub fn load_json5(path: &str) -> AgentResult<AgentConfig> {
222 Self::load_file(path)
223 }
224
225 pub fn to_string(config: &AgentConfig, format: ConfigFormat) -> AgentResult<String> {
227 let content = match format {
228 ConfigFormat::Yaml => serde_yaml::to_string(config).map_err(|e| {
229 AgentConfigError::Serialization(format!("Failed to serialize to YAML: {}", e))
230 })?,
231 ConfigFormat::Toml => toml::to_string_pretty(config).map_err(|e| {
232 AgentConfigError::Serialization(format!("Failed to serialize to TOML: {}", e))
233 })?,
234 ConfigFormat::Json => serde_json::to_string_pretty(config).map_err(|e| {
235 AgentConfigError::Serialization(format!("Failed to serialize to JSON: {}", e))
236 })?,
237 ConfigFormat::Ini => {
238 return Err(AgentConfigError::Serialization(
239 "INI serialization not directly supported. Use JSON, YAML, or TOML for saving."
240 .to_string(),
241 ));
242 }
243 ConfigFormat::Ron => {
244 return Err(AgentConfigError::Serialization(
245 "RON serialization not directly supported. Use JSON, YAML, or TOML for saving."
246 .to_string(),
247 ));
248 }
249 ConfigFormat::Json5 => {
250 serde_json::to_string_pretty(config).map_err(|e| {
252 AgentConfigError::Serialization(format!("Failed to serialize to JSON5: {}", e))
253 })?
254 }
255 };
256
257 Ok(content)
258 }
259
260 pub fn save_file(config: &AgentConfig, path: &str) -> AgentResult<()> {
262 let format = ConfigFormat::from_extension(path).ok_or_else(|| {
263 AgentConfigError::UnsupportedFormat(format!(
264 "Unable to determine config format from file extension: {}",
265 path
266 ))
267 })?;
268
269 let content = Self::to_string(config, format)?;
270
271 std::fs::write(path, content).map_err(|e| AgentConfigError::Io(e))?;
272
273 Ok(())
274 }
275
276 pub fn load_directory(dir_path: &str) -> AgentResult<Vec<AgentConfig>> {
278 let mut configs = Vec::new();
279
280 let entries = std::fs::read_dir(dir_path).map_err(|e| AgentConfigError::Io(e))?;
281
282 let supported_extensions = ["yaml", "yml", "toml", "json", "ini", "ron", "json5"];
283
284 for entry in entries {
285 let entry = entry.map_err(|e| AgentConfigError::Io(e))?;
286
287 let path = entry.path();
288 if path.is_file()
289 && let Some(ext) = path.extension().and_then(|e| e.to_str())
290 {
291 let ext_lower = ext.to_lowercase();
292 if supported_extensions.contains(&ext_lower.as_str()) {
293 let path_str = path.to_string_lossy().to_string();
294 match Self::load_file(&path_str) {
295 Ok(config) => configs.push(config),
296 Err(e) => {
297 tracing::warn!("Failed to load config '{}': {}", path_str, e);
299 }
300 }
301 }
302 }
303 }
304
305 Ok(configs)
306 }
307
308 pub fn merge(base: AgentConfig, overlay: AgentConfig) -> AgentConfig {
310 AgentConfig {
311 id: if overlay.id.is_empty() {
312 base.id
313 } else {
314 overlay.id
315 },
316 name: if overlay.name.is_empty() {
317 base.name
318 } else {
319 overlay.name
320 },
321 description: overlay.description.or(base.description),
322 agent_type: overlay.agent_type,
323 components: ComponentsConfig {
324 reasoner: overlay.components.reasoner.or(base.components.reasoner),
325 memory: overlay.components.memory.or(base.components.memory),
326 coordinator: overlay
327 .components
328 .coordinator
329 .or(base.components.coordinator),
330 },
331 capabilities: if overlay.capabilities.tags.is_empty() {
332 base.capabilities
333 } else {
334 overlay.capabilities
335 },
336 custom: {
337 let mut merged = base.custom;
338 merged.extend(overlay.custom);
339 merged
340 },
341 env_mappings: {
342 let mut merged = base.env_mappings;
343 merged.extend(overlay.env_mappings);
344 merged
345 },
346 enabled: overlay.enabled,
347 version: overlay.version.or(base.version),
348 }
349 }
350
351 pub fn load_merged_files(paths: &[&str]) -> AgentResult<AgentConfig> {
353 load_merged(paths).map_err(|e| match e {
354 ConfigError::Io(e) => AgentConfigError::Io(e),
355 ConfigError::Parse(e) => AgentConfigError::Parse(e.to_string()),
356 ConfigError::Serialization(e) => AgentConfigError::Serialization(e),
357 ConfigError::UnsupportedFormat(e) => AgentConfigError::UnsupportedFormat(e),
358 })
359 }
360}
361
362use super::schema::ComponentsConfig;
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_format_from_extension() {
370 assert_eq!(
371 ConfigFormat::from_extension("config.yaml"),
372 Some(ConfigFormat::Yaml)
373 );
374 assert_eq!(
375 ConfigFormat::from_extension("config.yml"),
376 Some(ConfigFormat::Yaml)
377 );
378 assert_eq!(
379 ConfigFormat::from_extension("config.toml"),
380 Some(ConfigFormat::Toml)
381 );
382 assert_eq!(
383 ConfigFormat::from_extension("config.json"),
384 Some(ConfigFormat::Json)
385 );
386 assert_eq!(
387 ConfigFormat::from_extension("config.ini"),
388 Some(ConfigFormat::Ini)
389 );
390 assert_eq!(
391 ConfigFormat::from_extension("config.ron"),
392 Some(ConfigFormat::Ron)
393 );
394 assert_eq!(
395 ConfigFormat::from_extension("config.json5"),
396 Some(ConfigFormat::Json5)
397 );
398 assert_eq!(ConfigFormat::from_extension("config.txt"), None);
399 }
400
401 #[test]
402 fn test_format_to_file_format() {
403 assert_eq!(ConfigFormat::Yaml.to_file_format(), FileFormat::Yaml);
404 assert_eq!(ConfigFormat::Toml.to_file_format(), FileFormat::Toml);
405 assert_eq!(ConfigFormat::Json.to_file_format(), FileFormat::Json);
406 assert_eq!(ConfigFormat::Ini.to_file_format(), FileFormat::Ini);
407 assert_eq!(ConfigFormat::Ron.to_file_format(), FileFormat::Ron);
408 assert_eq!(ConfigFormat::Json5.to_file_format(), FileFormat::Json5);
409 }
410
411 #[test]
412 fn test_load_yaml_string() {
413 let yaml = r#"
414id: test-agent
415name: Test Agent
416type: llm
417model: gpt-4
418temperature: 0.8
419"#;
420
421 let config = ConfigLoader::from_yaml(yaml).unwrap();
422 assert_eq!(config.id, "test-agent");
423 assert_eq!(config.name, "Test Agent");
424 }
425
426 #[test]
427 fn test_load_json_string() {
428 let json = r#"{
429 "id": "test-agent",
430 "name": "Test Agent",
431 "type": "llm",
432 "model": "gpt-4"
433 }"#;
434
435 let config = ConfigLoader::from_json(json).unwrap();
436 assert_eq!(config.id, "test-agent");
437 assert_eq!(config.name, "Test Agent");
438 }
439
440 #[test]
441 fn test_load_toml_string() {
442 let toml = r#"
443id = "test-agent"
444name = "Test Agent"
445type = "llm"
446model = "gpt-4"
447"#;
448
449 let config = ConfigLoader::from_toml(toml).unwrap();
450 assert_eq!(config.id, "test-agent");
451 assert_eq!(config.name, "Test Agent");
452 }
453
454 #[test]
455 fn test_load_ini_string() {
456 let ini = r#"
459id = "test-agent"
460name = "Test Agent"
461type = "llm"
462model = "gpt-4"
463"#;
464
465 let config = ConfigLoader::from_ini(ini).unwrap();
466 assert_eq!(config.id, "test-agent");
467 assert_eq!(config.name, "Test Agent");
468 }
469
470 #[test]
471 fn test_load_ron_string() {
472 let ron = r#"
473(
474 id: "test-agent",
475 name: "Test Agent",
476 type: "llm",
477 model: "gpt-4",
478)
479"#;
480
481 let config = ConfigLoader::from_ron(ron).unwrap();
482 assert_eq!(config.id, "test-agent");
483 assert_eq!(config.name, "Test Agent");
484 }
485
486 #[test]
487 fn test_load_json5_string() {
488 let json5 = r#"{
489 // JSON5 allows comments
490 id: "test-agent",
491 name: "Test Agent",
492 type: "llm",
493 model: "gpt-4",
494}
495"#;
496
497 let config = ConfigLoader::from_json5(json5).unwrap();
498 assert_eq!(config.id, "test-agent");
499 assert_eq!(config.name, "Test Agent");
500 }
501
502 #[test]
503 fn test_serialize_config() {
504 let config = AgentConfig::new("my-agent", "My Agent");
505
506 let yaml = ConfigLoader::to_string(&config, ConfigFormat::Yaml).unwrap();
507 assert!(yaml.contains("my-agent"));
508
509 let json = ConfigLoader::to_string(&config, ConfigFormat::Json).unwrap();
510 assert!(json.contains("my-agent"));
511
512 let toml = ConfigLoader::to_string(&config, ConfigFormat::Toml).unwrap();
513 assert!(toml.contains("my-agent"));
514 }
515
516 #[test]
517 fn test_merge_configs() {
518 let base =
519 AgentConfig::new("base-agent", "Base Agent").with_description("Base description");
520
521 let overlay = AgentConfig {
522 id: String::new(), name: "Override Name".to_string(),
524 description: Some("Override description".to_string()),
525 ..Default::default()
526 };
527
528 let merged = ConfigLoader::merge(base, overlay);
529 assert_eq!(merged.id, "base-agent"); assert_eq!(merged.name, "Override Name"); assert_eq!(merged.description, Some("Override description".to_string())); }
533
534 #[test]
535 fn test_format_names() {
536 assert_eq!(ConfigFormat::Yaml.name(), "yaml");
537 assert_eq!(ConfigFormat::Toml.name(), "toml");
538 assert_eq!(ConfigFormat::Json.name(), "json");
539 assert_eq!(ConfigFormat::Ini.name(), "ini");
540 assert_eq!(ConfigFormat::Ron.name(), "ron");
541 assert_eq!(ConfigFormat::Json5.name(), "json5");
542 }
543
544 #[test]
545 fn test_default_extensions() {
546 assert_eq!(ConfigFormat::Yaml.default_extension(), "yml");
547 assert_eq!(ConfigFormat::Toml.default_extension(), "toml");
548 assert_eq!(ConfigFormat::Json.default_extension(), "json");
549 assert_eq!(ConfigFormat::Ini.default_extension(), "ini");
550 assert_eq!(ConfigFormat::Ron.default_extension(), "ron");
551 assert_eq!(ConfigFormat::Json5.default_extension(), "json5");
552 }
553}