Skip to main content

mofa_runtime/agent/config/
loader.rs

1//! 配置加载器
2//!
3//! 支持多种配置格式: YAML, TOML, JSON, INI, RON, JSON5
4//!
5//! 使用统一的 config crate 提供一致的 API 接口
6
7use 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/// 配置格式
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum ConfigFormat {
15    /// YAML 格式
16    Yaml,
17    /// TOML 格式
18    Toml,
19    /// JSON 格式
20    Json,
21    /// INI 格式
22    Ini,
23    /// RON 格式
24    Ron,
25    /// JSON5 格式
26    Json5,
27}
28
29/// 配置错误类型
30#[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
48/// 配置结果类型
49pub type AgentResult<T> = Result<T, AgentConfigError>;
50
51impl ConfigFormat {
52    /// 从文件扩展名推断格式
53    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    /// 转换为 config crate 的 FileFormat
66    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    /// 获取格式名称
78    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    /// 获取默认文件扩展名
90    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
102/// 配置加载器
103///
104/// 支持从文件或字符串加载配置,支持多种格式
105///
106/// # 示例
107///
108/// ```rust,ignore
109/// use mofa_runtime::agent::config::{ConfigLoader, ConfigFormat};
110///
111/// // 从 YAML 字符串加载
112/// let yaml = r#"
113/// id: my-agent
114/// name: My Agent
115/// type: llm
116/// llm:
117///   model: gpt-4
118/// "#;
119/// let config = ConfigLoader::from_str(yaml, ConfigFormat::Yaml)?;
120///
121/// // 从文件加载 (自动检测格式)
122/// let config = ConfigLoader::load_file("agent.yaml")?;
123///
124/// // 从 TOML 字符串加载
125/// let toml = r#"
126/// id = "my-agent"
127/// name = "My Agent"
128/// type = "llm"
129/// "#;
130/// let config = ConfigLoader::from_toml(toml)?;
131///
132/// // 从 INI 文件加载
133/// let config = ConfigLoader::load_ini("agent.ini")?;
134/// ```
135pub struct ConfigLoader;
136
137impl ConfigLoader {
138    /// 从字符串加载配置
139    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    /// 从 YAML 字符串加载
149    pub fn from_yaml(content: &str) -> AgentResult<AgentConfig> {
150        Self::from_str(content, ConfigFormat::Yaml)
151    }
152
153    /// 从 TOML 字符串加载
154    pub fn from_toml(content: &str) -> AgentResult<AgentConfig> {
155        Self::from_str(content, ConfigFormat::Toml)
156    }
157
158    /// 从 JSON 字符串加载
159    pub fn from_json(content: &str) -> AgentResult<AgentConfig> {
160        Self::from_str(content, ConfigFormat::Json)
161    }
162
163    /// 从 INI 字符串加载
164    pub fn from_ini(content: &str) -> AgentResult<AgentConfig> {
165        Self::from_str(content, ConfigFormat::Ini)
166    }
167
168    /// 从 RON 字符串加载
169    pub fn from_ron(content: &str) -> AgentResult<AgentConfig> {
170        Self::from_str(content, ConfigFormat::Ron)
171    }
172
173    /// 从 JSON5 字符串加载
174    pub fn from_json5(content: &str) -> AgentResult<AgentConfig> {
175        Self::from_str(content, ConfigFormat::Json5)
176    }
177
178    /// 从文件加载配置 (自动检测格式)
179    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        // 验证配置
188        config
189            .validate()
190            .map_err(|errors| AgentConfigError::Validation(errors.join(", ")))?;
191
192        Ok(config)
193    }
194
195    /// 从文件加载 YAML 配置
196    pub fn load_yaml(path: &str) -> AgentResult<AgentConfig> {
197        Self::load_file(path)
198    }
199
200    /// 从文件加载 TOML 配置
201    pub fn load_toml(path: &str) -> AgentResult<AgentConfig> {
202        Self::load_file(path)
203    }
204
205    /// 从文件加载 JSON 配置
206    pub fn load_json(path: &str) -> AgentResult<AgentConfig> {
207        Self::load_file(path)
208    }
209
210    /// 从文件加载 INI 配置
211    pub fn load_ini(path: &str) -> AgentResult<AgentConfig> {
212        Self::load_file(path)
213    }
214
215    /// 从文件加载 RON 配置
216    pub fn load_ron(path: &str) -> AgentResult<AgentConfig> {
217        Self::load_file(path)
218    }
219
220    /// 从文件加载 JSON5 配置
221    pub fn load_json5(path: &str) -> AgentResult<AgentConfig> {
222        Self::load_file(path)
223    }
224
225    /// 将配置序列化为字符串
226    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                // JSON5 is compatible with JSON for serialization purposes
251                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    /// 将配置保存到文件
261    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    /// 加载多个配置文件
277    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                            // 记录错误但继续加载其他文件
298                            tracing::warn!("Failed to load config '{}': {}", path_str, e);
299                        }
300                    }
301                }
302            }
303        }
304
305        Ok(configs)
306    }
307
308    /// 合并多个配置 (后面的覆盖前面的)
309    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    /// 从多个文件合并加载配置
352    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        // INI format requires sections or dot notation for nesting
457        // Using dot notation to match AgentConfig's flat structure
458        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(), // Empty, should use base
523            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"); // From base
530        assert_eq!(merged.name, "Override Name"); // From overlay
531        assert_eq!(merged.description, Some("Override description".to_string())); // From overlay
532    }
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}