Skip to main content

agentforge_parser/formats/
crewai.rs

1use agentforge_core::{
2    AgentFile, AgentForgeError, EvalHints, ModelConfig, ModelProvider, Result, ToolDefinition,
3};
4
5/// Normalize CrewAI agent YAML into `AgentFile`.
6/// CrewAI format: role, goal, backstory, tools[], llm
7pub fn normalize(value: &serde_json::Value) -> Result<AgentFile> {
8    // Handle both single agent and agents array
9    let agent = if value.get("agents").is_some() {
10        value
11            .get("agents")
12            .and_then(|a| a.as_array())
13            .and_then(|a| a.first())
14            .ok_or_else(|| {
15                AgentForgeError::ValidationError("CrewAI: 'agents' array is empty".to_string())
16            })?
17    } else {
18        value
19    };
20
21    let role = agent
22        .get("role")
23        .and_then(|r| r.as_str())
24        .ok_or_else(|| AgentForgeError::ValidationError("CrewAI: missing 'role'".to_string()))?;
25
26    let goal = agent.get("goal").and_then(|g| g.as_str()).unwrap_or("");
27
28    let backstory = agent
29        .get("backstory")
30        .and_then(|b| b.as_str())
31        .unwrap_or("");
32
33    let name = agent
34        .get("name")
35        .and_then(|n| n.as_str())
36        .unwrap_or(role)
37        .to_string();
38
39    // Construct system prompt from CrewAI fields
40    let system_prompt = format!("You are {role}.\n\nGoal: {goal}\n\nBackstory: {backstory}");
41
42    let model_id = agent
43        .get("llm")
44        .and_then(|l| l.as_str())
45        .or_else(|| value.get("llm").and_then(|l| l.as_str()))
46        .unwrap_or("gpt-4o")
47        .to_string();
48
49    let model = ModelConfig {
50        provider: if model_id.contains("claude") {
51            ModelProvider::Anthropic
52        } else {
53            ModelProvider::Openai
54        },
55        model_id,
56        temperature: None,
57        max_tokens: None,
58        top_p: None,
59    };
60
61    // CrewAI tools are just tool names (strings), not full definitions
62    let tools: Vec<ToolDefinition> = agent
63        .get("tools")
64        .and_then(|t| t.as_array())
65        .map(|arr| {
66            arr.iter()
67                .filter_map(|t| {
68                    t.as_str().map(|name| ToolDefinition {
69                        name: name.to_string(),
70                        description: format!("Tool: {name}"),
71                        parameters: serde_json::json!({"type": "object", "properties": {}}),
72                    })
73                })
74                .collect()
75        })
76        .unwrap_or_default();
77
78    Ok(AgentFile {
79        agentforge_schema_version: "1".to_string(),
80        name,
81        version: "1.0.0".to_string(),
82        model,
83        system_prompt,
84        tools,
85        output_schema: None,
86        constraints: vec![],
87        eval_hints: Some(EvalHints::default()),
88        metadata: None,
89    })
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use serde_json::json;
96
97    #[test]
98    fn normalizes_crewai_agent() {
99        let v = json!({
100            "role": "Support Specialist",
101            "goal": "Help customers resolve issues",
102            "backstory": "You are an expert support agent with 10 years of experience.",
103            "llm": "gpt-4o",
104            "tools": ["search_tool", "order_lookup_tool"]
105        });
106        let agent = normalize(&v).unwrap();
107        assert!(agent.system_prompt.contains("Support Specialist"));
108        assert!(agent.system_prompt.contains("Help customers"));
109        assert_eq!(agent.tools.len(), 2);
110    }
111
112    #[test]
113    fn normalizes_crewai_agents_array() {
114        let v = json!({
115            "agents": [
116                {
117                    "role": "Researcher",
118                    "goal": "Research topics",
119                    "backstory": "Expert researcher"
120                }
121            ]
122        });
123        let agent = normalize(&v).unwrap();
124        assert!(agent.system_prompt.contains("Researcher"));
125    }
126
127    #[test]
128    fn rejects_missing_role() {
129        let v = json!({"goal": "Help", "backstory": "Expert"});
130        assert!(normalize(&v).is_err());
131    }
132}