Skip to main content

sgr_agent/
factory.rs

1//! AgentFactory — create agents from configuration.
2//!
3//! Allows defining agent type, system prompt, and options in a config Value,
4//! then instantiating the right agent variant at runtime.
5
6use crate::agent::Agent;
7use crate::client::LlmClient;
8use serde_json::Value;
9
10/// Agent type selector.
11#[derive(Debug, Clone, PartialEq)]
12pub enum AgentType {
13    /// Structured output via union schema.
14    Sgr,
15    /// Native function calling.
16    ToolCalling,
17    /// Text-based flexible parsing.
18    Flexible,
19    /// 2-phase hybrid (reasoning + action).
20    Hybrid,
21    /// Read-only planning (wraps Sgr by default).
22    Planning,
23}
24
25impl AgentType {
26    /// Parse from string (case-insensitive).
27    pub fn from_str_loose(s: &str) -> Option<Self> {
28        match s.to_lowercase().as_str() {
29            "sgr" | "structured" => Some(Self::Sgr),
30            "tool_calling" | "toolcalling" | "fc" | "function_calling" => Some(Self::ToolCalling),
31            "flexible" | "text" | "iron" => Some(Self::Flexible),
32            "hybrid" | "sgr_tool_calling" => Some(Self::Hybrid),
33            "planning" | "plan" | "read_only" => Some(Self::Planning),
34            _ => None,
35        }
36    }
37}
38
39/// Configuration for creating an agent.
40#[derive(Debug, Clone)]
41pub struct AgentConfig {
42    pub agent_type: AgentType,
43    pub system_prompt: String,
44    /// Max retry attempts for FlexibleAgent (default: 1, no retry).
45    pub max_retries: usize,
46    /// Max reasoning tools for HybridAgent (default: 1 — just ReasoningTool).
47    pub max_reasoning_tools: usize,
48}
49
50impl Default for AgentConfig {
51    fn default() -> Self {
52        Self {
53            agent_type: AgentType::Sgr,
54            system_prompt: String::new(),
55            max_retries: 1,
56            max_reasoning_tools: 1,
57        }
58    }
59}
60
61impl AgentConfig {
62    /// Parse from a JSON Value.
63    ///
64    /// Expected format:
65    /// ```json
66    /// {
67    ///   "type": "sgr",
68    ///   "system_prompt": "You are a coding agent.",
69    ///   "max_retries": 3,
70    ///   "max_reasoning_tools": 1
71    /// }
72    /// ```
73    pub fn from_value(val: &Value) -> Result<Self, String> {
74        let agent_type = val
75            .get("type")
76            .and_then(|t| t.as_str())
77            .and_then(AgentType::from_str_loose)
78            .unwrap_or(AgentType::Sgr);
79
80        let system_prompt = val
81            .get("system_prompt")
82            .and_then(|s| s.as_str())
83            .unwrap_or("")
84            .to_string();
85
86        let max_retries = val.get("max_retries").and_then(|v| v.as_u64()).unwrap_or(1) as usize;
87
88        let max_reasoning_tools = val
89            .get("max_reasoning_tools")
90            .and_then(|v| v.as_u64())
91            .unwrap_or(1) as usize;
92
93        Ok(Self {
94            agent_type,
95            system_prompt,
96            max_retries,
97            max_reasoning_tools,
98        })
99    }
100}
101
102/// Create an agent from config + LLM client.
103///
104/// Returns a boxed `dyn Agent` to erase the concrete type.
105pub fn create_agent<C: LlmClient + Clone + 'static>(
106    config: &AgentConfig,
107    client: C,
108) -> Box<dyn Agent> {
109    match config.agent_type {
110        AgentType::Sgr => Box::new(crate::agents::sgr::SgrAgent::new(
111            client,
112            &config.system_prompt,
113        )),
114        AgentType::ToolCalling => Box::new(crate::agents::tool_calling::ToolCallingAgent::new(
115            client,
116            &config.system_prompt,
117        )),
118        AgentType::Flexible => Box::new(crate::agents::flexible::FlexibleAgent::new(
119            client,
120            &config.system_prompt,
121            config.max_retries,
122        )),
123        AgentType::Hybrid => Box::new(crate::agents::hybrid::HybridAgent::new(
124            client,
125            &config.system_prompt,
126        )),
127        AgentType::Planning => {
128            let inner = Box::new(crate::agents::sgr::SgrAgent::new(
129                client,
130                &config.system_prompt,
131            ));
132            Box::new(crate::agents::planning::PlanningAgent::new(inner))
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use serde_json::json;
141
142    #[test]
143    fn agent_type_from_str() {
144        assert_eq!(AgentType::from_str_loose("sgr"), Some(AgentType::Sgr));
145        assert_eq!(
146            AgentType::from_str_loose("structured"),
147            Some(AgentType::Sgr)
148        );
149        assert_eq!(
150            AgentType::from_str_loose("tool_calling"),
151            Some(AgentType::ToolCalling)
152        );
153        assert_eq!(
154            AgentType::from_str_loose("fc"),
155            Some(AgentType::ToolCalling)
156        );
157        assert_eq!(
158            AgentType::from_str_loose("flexible"),
159            Some(AgentType::Flexible)
160        );
161        assert_eq!(AgentType::from_str_loose("text"), Some(AgentType::Flexible));
162        assert_eq!(AgentType::from_str_loose("hybrid"), Some(AgentType::Hybrid));
163        assert_eq!(AgentType::from_str_loose("unknown"), None);
164    }
165
166    #[test]
167    fn config_from_value() {
168        let val = json!({
169            "type": "flexible",
170            "system_prompt": "You are a test agent.",
171            "max_retries": 5
172        });
173        let config = AgentConfig::from_value(&val).unwrap();
174        assert_eq!(config.agent_type, AgentType::Flexible);
175        assert_eq!(config.system_prompt, "You are a test agent.");
176        assert_eq!(config.max_retries, 5);
177    }
178
179    #[test]
180    fn config_defaults() {
181        let val = json!({});
182        let config = AgentConfig::from_value(&val).unwrap();
183        assert_eq!(config.agent_type, AgentType::Sgr);
184        assert!(config.system_prompt.is_empty());
185        assert_eq!(config.max_retries, 1);
186    }
187
188    #[test]
189    fn config_default_struct() {
190        let config = AgentConfig::default();
191        assert_eq!(config.agent_type, AgentType::Sgr);
192        assert_eq!(config.max_retries, 1);
193    }
194}