helios_engine/
auto_forest.rs

1//! # AutoForest Module
2//!
3//! This module implements automatic orchestration of a forest of agents.
4//! Given a high-level task, the AutoForest orchestrator intelligently:
5//! - Determines the optimal number of agents to spawn
6//! - Generates specialized prompts for each agent
7//! - Distributes available tools among agents
8//! - Coordinates task execution and result aggregation
9//!
10//! # Example
11//!
12//! ```rust,no_run
13//! use helios_engine::{AutoForest, Config, CalculatorTool};
14//!
15//! #[tokio::main]
16//! async fn main() -> helios_engine::Result<()> {
17//!     let config = Config::new_default();
18//!     
19//!     let mut auto_forest = AutoForest::new(config)
20//!         .with_tools(vec![Box::new(CalculatorTool)])
21//!         .build()
22//!         .await?;
23//!
24//!     let task = "Analyze sales data and identify trends";
25//!     let result = auto_forest.execute_task(task).await?;
26//!     println!("Result: {}", result);
27//!     Ok(())
28//! }
29//! ```
30
31use crate::agent::Agent;
32use crate::config::Config;
33use crate::error::{HeliosError, Result};
34use crate::tools::Tool;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37
38/// Configuration for an agent spawned by the orchestrator
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct AgentConfig {
41    /// Name of the agent
42    pub name: String,
43    /// Specialized system prompt for this agent
44    pub system_prompt: String,
45    /// Indices of tools this agent has access to
46    #[serde(default)]
47    pub tool_indices: Vec<usize>,
48    /// Role/specialty of this agent
49    pub role: String,
50}
51
52/// Internal struct for deserializing the orchestration plan from LLM response
53#[derive(Debug, Deserialize)]
54struct OrchestrationPlanJson {
55    num_agents: usize,
56    reasoning: String,
57    agents: Vec<AgentConfig>,
58    task_breakdown: HashMap<String, String>,
59}
60
61/// An auto-spawned agent with its assigned configuration
62pub struct SpawnedAgent {
63    /// The agent instance
64    pub agent: Agent,
65    /// Configuration for this agent
66    pub config: AgentConfig,
67    /// Result from this agent's execution
68    pub result: Option<String>,
69}
70
71/// Orchestration plan generated for a task
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct OrchestrationPlan {
74    /// Overall task description
75    pub task: String,
76    /// Number of agents to spawn
77    pub num_agents: usize,
78    /// Reasoning for the chosen configuration
79    pub reasoning: String,
80    /// Configurations for each agent
81    pub agents: Vec<AgentConfig>,
82    /// Task breakdown for each agent
83    pub task_breakdown: HashMap<String, String>,
84}
85
86/// The AutoForest orchestrator - manages automatic agent spawning and coordination
87pub struct AutoForest {
88    config: Config,
89    tools: Vec<Box<dyn Tool>>,
90    spawned_agents: Vec<SpawnedAgent>,
91    orchestration_plan: Option<OrchestrationPlan>,
92    orchestrator_agent: Option<Agent>,
93}
94
95impl AutoForest {
96    /// Creates a new AutoForest orchestrator builder
97    #[allow(clippy::new_ret_no_self)]
98    pub fn new(config: Config) -> AutoForestBuilder {
99        AutoForestBuilder::new(config)
100    }
101
102    /// Gets the current orchestration plan
103    pub fn orchestration_plan(&self) -> Option<&OrchestrationPlan> {
104        self.orchestration_plan.as_ref()
105    }
106
107    /// Gets the spawned agents
108    pub fn spawned_agents(&self) -> &[SpawnedAgent] {
109        &self.spawned_agents
110    }
111
112    /// Generates an orchestration plan for the given task
113    async fn generate_orchestration_plan(&mut self, task: &str) -> Result<OrchestrationPlan> {
114        // Create a system prompt for the orchestrator
115        let tools_info = self
116            .tools
117            .iter()
118            .enumerate()
119            .map(|(i, tool)| format!("- Tool {}: {} ({})", i, tool.name(), tool.description()))
120            .collect::<Vec<_>>()
121            .join("\n");
122
123        let orchestrator_prompt = format!(
124            r#"You are an expert task orchestrator. Your job is to analyze a task and create an optimal plan for a forest of AI agents to complete it.
125
126Available tools:
127{}
128
129Given the task, you must:
1301. Determine the optimal number of agents (1-5)
1312. Define each agent's role and specialization
1323. Create specialized system prompts for each agent
1334. Assign tools to each agent based on their role
1345. Break down the task into subtasks for each agent
135
136Respond with ONLY a JSON object with this structure (no markdown, no extra text):
137{{
138  "num_agents": <number>,
139  "reasoning": "<brief explanation>",
140  "agents": [
141    {{
142      "name": "<agent_name>",
143      "role": "<role>",
144      "system_prompt": "<specialized_prompt>",
145      "tool_indices": [<indices>]
146    }}
147  ],
148  "task_breakdown": {{
149    "<agent_name>": "<specific_task_for_this_agent>"
150  }}
151}}"#,
152            tools_info
153        );
154
155        // Create orchestrator agent if not exists
156        if self.orchestrator_agent.is_none() {
157            let orchestrator = Agent::builder("Orchestrator")
158                .config(self.config.clone())
159                .system_prompt(&orchestrator_prompt)
160                .build()
161                .await?;
162            self.orchestrator_agent = Some(orchestrator);
163        }
164
165        // Get the orchestrator agent
166        let orchestrator = self.orchestrator_agent.as_mut().ok_or_else(|| {
167            HeliosError::AgentError("Failed to create orchestrator agent".to_string())
168        })?;
169
170        // Ask orchestrator to generate plan
171        let response = orchestrator.chat(&format!("Task: {}", task)).await?;
172
173        // Parse the response as JSON using serde for type-safe deserialization
174        let plan_data: OrchestrationPlanJson = serde_json::from_str(&response).map_err(|e| {
175            HeliosError::AgentError(format!("Failed to parse orchestration plan: {}", e))
176        })?;
177
178        // Construct the orchestration plan from the parsed data
179        let plan = OrchestrationPlan {
180            task: task.to_string(),
181            num_agents: plan_data.num_agents,
182            reasoning: plan_data.reasoning,
183            agents: plan_data.agents,
184            task_breakdown: plan_data.task_breakdown,
185        };
186
187        self.orchestration_plan = Some(plan.clone());
188        Ok(plan)
189    }
190
191    /// Spawns agents according to the orchestration plan
192    async fn spawn_agents_from_plan(&mut self, plan: &OrchestrationPlan) -> Result<()> {
193        self.spawned_agents.clear();
194
195        for agent_config in &plan.agents {
196            // Create the agent
197            let agent = Agent::builder(&agent_config.name)
198                .config(self.config.clone())
199                .system_prompt(&agent_config.system_prompt)
200                .build()
201                .await?;
202
203            // Note: Tools would be assigned here if we had a mechanism to clone tools
204            // For now, agents are created without tools and rely on the LLM's capabilities
205
206            let spawned = SpawnedAgent {
207                agent,
208                config: agent_config.clone(),
209                result: None,
210            };
211
212            self.spawned_agents.push(spawned);
213        }
214
215        Ok(())
216    }
217
218    /// Executes a task using the auto-forest orchestration
219    pub async fn execute_task(&mut self, task: &str) -> Result<String> {
220        // Generate orchestration plan
221        let plan = self.generate_orchestration_plan(task).await?;
222
223        // Spawn agents according to plan
224        self.spawn_agents_from_plan(&plan).await?;
225
226        // Execute tasks on spawned agents IN PARALLEL using tokio::join_all
227        let mut futures = Vec::new();
228
229        for spawned_agent in self.spawned_agents.drain(..) {
230            let agent_task = plan
231                .task_breakdown
232                .get(&spawned_agent.config.name)
233                .cloned()
234                .unwrap_or_else(|| format!("Complete your assigned portion of: {}", task));
235
236            let future = async move {
237                let mut agent = spawned_agent.agent;
238                let config = spawned_agent.config;
239                let result = agent.chat(&agent_task).await;
240                (agent, config, result)
241            };
242
243            futures.push(future);
244        }
245
246        // Wait for all agents to complete in parallel
247        let completed_agents = futures::future::join_all(futures).await;
248
249        // Collect results and restore agents
250        let mut results = HashMap::new();
251        self.spawned_agents.clear(); // Ensure vector is empty before repopulating
252
253        for (agent, config, result) in completed_agents {
254            let agent_name = config.name.clone();
255            let (result_string, result_for_map) = match result {
256                Ok(output) => (Some(output.clone()), output),
257                Err(e) => {
258                    let err_msg = format!("Error: {}", e);
259                    (Some(err_msg.clone()), err_msg)
260                }
261            };
262            results.insert(agent_name, result_for_map);
263
264            self.spawned_agents.push(SpawnedAgent {
265                agent,
266                config,
267                result: result_string,
268            });
269        }
270
271        // Aggregate results
272        let aggregated_result = self.aggregate_results(&results, task).await?;
273
274        Ok(aggregated_result)
275    }
276
277    /// Shorthand: execute a task with just one method call
278    pub async fn do_task(&mut self, task: &str) -> Result<String> {
279        self.execute_task(task).await
280    }
281
282    /// Ultra-simple: shorthand for asking the forest to complete a task
283    pub async fn run(&mut self, task: &str) -> Result<String> {
284        self.execute_task(task).await
285    }
286
287    /// Aggregates results from all agents into a final response
288    async fn aggregate_results(
289        &mut self,
290        results: &HashMap<String, String>,
291        task: &str,
292    ) -> Result<String> {
293        let mut result_text = String::new();
294        result_text.push_str("## Task Execution Summary\n\n");
295        result_text.push_str(&format!("**Task**: {}\n\n", task));
296        result_text.push_str("### Agent Results:\n\n");
297
298        for (agent_name, result) in results {
299            result_text.push_str(&format!("**{}**:\n{}\n\n", agent_name, result));
300        }
301
302        // Use orchestrator to synthesize final answer if multiple agents
303        if results.len() > 1 {
304            result_text.push_str("### Synthesized Analysis:\n\n");
305            let orchestrator = self
306                .orchestrator_agent
307                .as_mut()
308                .ok_or_else(|| HeliosError::AgentError("Orchestrator not available".to_string()))?;
309
310            let synthesis_prompt = format!(
311                "Synthesize these agent results into a cohesive answer:\n{}",
312                result_text
313            );
314            let synthesis = orchestrator.chat(&synthesis_prompt).await?;
315            result_text.push_str(&synthesis);
316        }
317
318        Ok(result_text)
319    }
320}
321
322/// Builder for AutoForest
323pub struct AutoForestBuilder {
324    config: Config,
325    tools: Vec<Box<dyn Tool>>,
326}
327
328impl AutoForestBuilder {
329    /// Creates a new AutoForestBuilder with the given config
330    pub fn new(config: Config) -> Self {
331        Self {
332            config,
333            tools: Vec::new(),
334        }
335    }
336
337    /// Sets the tools available to the forest
338    pub fn with_tools(mut self, tools: Vec<Box<dyn Tool>>) -> Self {
339        self.tools = tools;
340        self
341    }
342
343    /// Builds the AutoForest orchestrator
344    pub async fn build(self) -> Result<AutoForest> {
345        Ok(AutoForest {
346            config: self.config,
347            tools: self.tools,
348            spawned_agents: Vec::new(),
349            orchestration_plan: None,
350            orchestrator_agent: None,
351        })
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_agent_config_creation() {
361        let config = AgentConfig {
362            name: "TestAgent".to_string(),
363            system_prompt: "You are helpful".to_string(),
364            tool_indices: vec![0, 1],
365            role: "Analyzer".to_string(),
366        };
367
368        assert_eq!(config.name, "TestAgent");
369        assert_eq!(config.tool_indices.len(), 2);
370    }
371
372    #[test]
373    fn test_orchestration_plan_creation() {
374        let plan = OrchestrationPlan {
375            task: "Test task".to_string(),
376            num_agents: 2,
377            reasoning: "Two agents needed".to_string(),
378            agents: vec![],
379            task_breakdown: HashMap::new(),
380        };
381
382        assert_eq!(plan.num_agents, 2);
383        assert_eq!(plan.task, "Test task");
384    }
385}