ricecoder_storage/markdown_config/integration/
agents.rs

1//! Integration with ricecoder-agents for markdown-based agent configuration
2
3use crate::markdown_config::error::MarkdownConfigResult;
4use crate::markdown_config::loader::{ConfigFile, ConfigFileType, ConfigurationLoader};
5use crate::markdown_config::types::AgentConfig;
6use std::path::PathBuf;
7use std::sync::Arc;
8use tracing::{debug, info, warn};
9
10/// Type alias for registration results: (success_count, error_count, errors)
11pub type RegistrationResult = (usize, usize, Vec<(String, String)>);
12
13/// Trait for registering agent configurations
14///
15/// This trait allows ricecoder-storage to register agent configurations without
16/// directly depending on ricecoder-agents, avoiding circular dependencies.
17pub trait AgentRegistrar: Send + Sync {
18    /// Register an agent configuration
19    fn register_agent(&mut self, agent: AgentConfig) -> Result<(), String>;
20}
21
22/// Integration layer for agent configuration with ricecoder-agents
23///
24/// This struct provides methods to discover, load, and register agent configurations
25/// from markdown files with the ricecoder-agents subsystem.
26pub struct AgentConfigIntegration {
27    loader: Arc<ConfigurationLoader>,
28}
29
30impl AgentConfigIntegration {
31    /// Create a new agent configuration integration
32    pub fn new(loader: Arc<ConfigurationLoader>) -> Self {
33        Self { loader }
34    }
35
36    /// Discover agent configuration files in the given paths
37    ///
38    /// # Arguments
39    /// * `paths` - Directories to search for agent markdown files
40    ///
41    /// # Returns
42    /// A vector of discovered agent configuration files
43    pub fn discover_agent_configs(&self, paths: &[PathBuf]) -> MarkdownConfigResult<Vec<ConfigFile>> {
44        let all_files = self.loader.discover(paths)?;
45
46        // Filter to only agent configuration files
47        let agent_files: Vec<ConfigFile> = all_files
48            .into_iter()
49            .filter(|f| f.config_type == ConfigFileType::Agent)
50            .collect();
51
52        debug!("Discovered {} agent configuration files", agent_files.len());
53        Ok(agent_files)
54    }
55
56    /// Load agent configurations from markdown files
57    ///
58    /// # Arguments
59    /// * `paths` - Directories to search for agent markdown files
60    ///
61    /// # Returns
62    /// A tuple of (loaded_agents, errors)
63    pub async fn load_agent_configs(
64        &self,
65        paths: &[PathBuf],
66    ) -> MarkdownConfigResult<(Vec<AgentConfig>, Vec<(PathBuf, String)>)> {
67        let files = self.discover_agent_configs(paths)?;
68
69        let mut agents = Vec::new();
70        let mut errors = Vec::new();
71
72        for file in files {
73            match self.loader.load(&file).await {
74                Ok(config) => {
75                    match config {
76                        crate::markdown_config::loader::LoadedConfig::Agent(agent) => {
77                            debug!("Loaded agent configuration: {}", agent.name);
78                            agents.push(agent);
79                        }
80                        _ => {
81                            warn!("Expected agent configuration but got different type from {}", file.path.display());
82                            errors.push((
83                                file.path,
84                                "Expected agent configuration but got different type".to_string(),
85                            ));
86                        }
87                    }
88                }
89                Err(e) => {
90                    let error_msg = e.to_string();
91                    warn!("Failed to load agent configuration from {}: {}", file.path.display(), error_msg);
92                    errors.push((file.path, error_msg));
93                }
94            }
95        }
96
97        info!("Loaded {} agent configurations", agents.len());
98        Ok((agents, errors))
99    }
100
101    /// Register agent configurations with a registrar
102    ///
103    /// This method registers agent configurations using a generic registrar trait,
104    /// allowing integration with any agent registry implementation.
105    ///
106    /// # Arguments
107    /// * `agents` - Agent configurations to register
108    /// * `registrar` - The agent registrar to register with
109    ///
110    /// # Returns
111    /// A tuple of (successful_count, error_count, errors)
112    pub fn register_agents(
113        &self,
114        agents: Vec<AgentConfig>,
115        registrar: &mut dyn AgentRegistrar,
116    ) -> MarkdownConfigResult<RegistrationResult> {
117        let mut success_count = 0;
118        let mut error_count = 0;
119        let mut errors = Vec::new();
120
121        for agent in agents {
122            // Validate agent configuration
123            if let Err(e) = agent.validate() {
124                error_count += 1;
125                let error_msg = format!("Invalid agent configuration: {}", e);
126                warn!("Failed to register agent '{}': {}", agent.name, error_msg);
127                errors.push((agent.name.clone(), error_msg));
128                continue;
129            }
130
131            debug!("Registering agent: {}", agent.name);
132
133            // Register the agent using the registrar
134            match registrar.register_agent(agent.clone()) {
135                Ok(_) => {
136                    success_count += 1;
137                    info!("Registered agent: {}", agent.name);
138                }
139                Err(e) => {
140                    error_count += 1;
141                    warn!("Failed to register agent '{}': {}", agent.name, e);
142                    errors.push((agent.name.clone(), e));
143                }
144            }
145        }
146
147        debug!(
148            "Agent registration complete: {} successful, {} failed",
149            success_count, error_count
150        );
151
152        Ok((success_count, error_count, errors))
153    }
154
155    /// Load and register agent configurations in one operation
156    ///
157    /// # Arguments
158    /// * `paths` - Directories to search for agent markdown files
159    /// * `registrar` - The agent registrar to register with
160    ///
161    /// # Returns
162    /// A tuple of (successful_count, error_count, errors)
163    pub async fn load_and_register_agents(
164        &self,
165        paths: &[PathBuf],
166        registrar: &mut dyn AgentRegistrar,
167    ) -> MarkdownConfigResult<(usize, usize, Vec<(String, String)>)> {
168        let (agents, load_errors) = self.load_agent_configs(paths).await?;
169
170        let (success, errors, mut reg_errors) = self.register_agents(agents, registrar)?;
171
172        // Combine load and registration errors
173        for (path, msg) in load_errors {
174            reg_errors.push((path.display().to_string(), msg));
175        }
176
177        Ok((success, errors, reg_errors))
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::markdown_config::registry::ConfigRegistry;
185    use std::fs;
186    use tempfile::TempDir;
187
188    fn create_test_agent_file(dir: &PathBuf, name: &str, content: &str) -> PathBuf {
189        let path = dir.join(format!("{}.agent.md", name));
190        fs::write(&path, content).unwrap();
191        path
192    }
193
194    #[test]
195    fn test_discover_agent_configs() {
196        let temp_dir = TempDir::new().unwrap();
197        let dir_path = temp_dir.path().to_path_buf();
198
199        // Create test agent files
200        create_test_agent_file(&dir_path, "agent1", "---\nname: agent1\n---\nTest");
201        create_test_agent_file(&dir_path, "agent2", "---\nname: agent2\n---\nTest");
202
203        // Create a non-agent file
204        fs::write(dir_path.join("mode1.mode.md"), "---\nname: mode1\n---\nTest").unwrap();
205
206        let registry = Arc::new(ConfigRegistry::new());
207        let loader = Arc::new(ConfigurationLoader::new(registry));
208        let integration = AgentConfigIntegration::new(loader);
209
210        let discovered = integration.discover_agent_configs(&[dir_path]).unwrap();
211
212        assert_eq!(discovered.len(), 2);
213        assert!(discovered.iter().all(|f| f.config_type == ConfigFileType::Agent));
214    }
215
216    #[tokio::test]
217    async fn test_load_agent_configs() {
218        let temp_dir = TempDir::new().unwrap();
219        let dir_path = temp_dir.path().to_path_buf();
220
221        let agent_content = r#"---
222name: test-agent
223description: A test agent
224model: gpt-4
225temperature: 0.7
226max_tokens: 2000
227---
228You are a helpful assistant"#;
229
230        create_test_agent_file(&dir_path, "test-agent", agent_content);
231
232        let registry = Arc::new(ConfigRegistry::new());
233        let loader = Arc::new(ConfigurationLoader::new(registry));
234        let integration = AgentConfigIntegration::new(loader);
235
236        let (agents, errors) = integration.load_agent_configs(&[dir_path]).await.unwrap();
237
238        assert_eq!(agents.len(), 1);
239        assert_eq!(errors.len(), 0);
240        assert_eq!(agents[0].name, "test-agent");
241        assert_eq!(agents[0].model, Some("gpt-4".to_string()));
242    }
243
244    #[tokio::test]
245    async fn test_load_agent_configs_with_errors() {
246        let temp_dir = TempDir::new().unwrap();
247        let dir_path = temp_dir.path().to_path_buf();
248
249        // Create a valid agent file
250        let valid_content = r#"---
251name: valid-agent
252---
253Valid agent"#;
254        create_test_agent_file(&dir_path, "valid-agent", valid_content);
255
256        // Create an invalid agent file (missing frontmatter)
257        fs::write(dir_path.join("invalid.agent.md"), "# No frontmatter\nJust markdown").unwrap();
258
259        let registry = Arc::new(ConfigRegistry::new());
260        let loader = Arc::new(ConfigurationLoader::new(registry));
261        let integration = AgentConfigIntegration::new(loader);
262
263        let (agents, errors) = integration.load_agent_configs(&[dir_path]).await.unwrap();
264
265        assert_eq!(agents.len(), 1);
266        assert_eq!(errors.len(), 1);
267        assert_eq!(agents[0].name, "valid-agent");
268    }
269
270    #[test]
271    fn test_register_agents() {
272        let registry = Arc::new(ConfigRegistry::new());
273        let loader = Arc::new(ConfigurationLoader::new(registry));
274        let integration = AgentConfigIntegration::new(loader);
275
276        let agents = vec![
277            AgentConfig {
278                name: "agent1".to_string(),
279                description: Some("Test agent 1".to_string()),
280                prompt: "You are agent 1".to_string(),
281                model: Some("gpt-4".to_string()),
282                temperature: Some(0.7),
283                max_tokens: Some(2000),
284                tools: vec![],
285            },
286            AgentConfig {
287                name: "agent2".to_string(),
288                description: Some("Test agent 2".to_string()),
289                prompt: "You are agent 2".to_string(),
290                model: None,
291                temperature: None,
292                max_tokens: None,
293                tools: vec![],
294            },
295        ];
296
297        struct MockRegistrar;
298        impl AgentRegistrar for MockRegistrar {
299            fn register_agent(&mut self, _agent: AgentConfig) -> Result<(), String> {
300                Ok(())
301            }
302        }
303
304        let mut registrar = MockRegistrar;
305        let (success, errors, error_list) = integration
306            .register_agents(agents, &mut registrar)
307            .unwrap();
308
309        assert_eq!(success, 2);
310        assert_eq!(errors, 0);
311        assert_eq!(error_list.len(), 0);
312    }
313
314    #[test]
315    fn test_register_invalid_agent() {
316        let registry = Arc::new(ConfigRegistry::new());
317        let loader = Arc::new(ConfigurationLoader::new(registry));
318        let integration = AgentConfigIntegration::new(loader);
319
320        let agents = vec![
321            AgentConfig {
322                name: String::new(), // Invalid: empty name
323                description: None,
324                prompt: "Test".to_string(),
325                model: None,
326                temperature: None,
327                max_tokens: None,
328                tools: vec![],
329            },
330        ];
331
332        struct MockRegistrar;
333        impl AgentRegistrar for MockRegistrar {
334            fn register_agent(&mut self, _agent: AgentConfig) -> Result<(), String> {
335                Ok(())
336            }
337        }
338
339        let mut registrar = MockRegistrar;
340        let (success, errors, error_list) = integration
341            .register_agents(agents, &mut registrar)
342            .unwrap();
343
344        assert_eq!(success, 0);
345        assert_eq!(errors, 1);
346        assert_eq!(error_list.len(), 1);
347    }
348}