Skip to main content

lc_agents/
builder.rs

1// src/agents/builder.rs
2//! AgentBuilder — create an Agent in 3 lines
3//!
4//! Provides a fluent Builder API, modeled on rig's
5//! `client.agent(model).preamble(...).build()`.
6//!
7//! # Example
8//!
9//! ```ignore
10//! let agent = AgentBuilder::new()
11//!     .llm(OpenAIChat::new(OpenAIConfig::new("sk-...")))
12//!     .system("You are a helpful assistant.")
13//!     .tool(Calculator::new())
14//!     .build()?;
15//! ```
16
17use crate::{AgentError, BaseAgent, FunctionCallingAgent};
18use lc_core::language_models::BaseChatModel;
19use lc_core::tools::BaseTool;
20use lc_providers::ProviderError;
21use std::sync::Arc;
22
23/// Agent Builder — fluent API to create a FunctionCallingAgent
24///
25/// Uses the Builder pattern so users can create and run an Agent in just 3
26/// lines.
27///
28/// # Basic usage
29///
30/// ```ignore
31/// let agent = AgentBuilder::new()
32///     .llm(OpenAIChat::new(OpenAIConfig::new("sk-...")))
33///     .system("You are a helpful assistant.")
34///     .tool(Calculator::new())
35///     .build()?;
36/// ```
37///
38/// # Using `Arc<dyn BaseChatModel>`
39///
40/// ```ignore
41/// let llm = wrap_chat_model(OpenAIChat::new(config));
42/// let agent = AgentBuilder::new()
43///     .llm_from_arc(llm)
44///     .system("You are a helpful assistant.")
45///     .build()?;
46/// ```
47pub struct AgentBuilder {
48    llm: Option<Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>>,
49    system_prompt: Option<String>,
50    tools: Vec<Arc<dyn BaseTool>>,
51    max_iterations: usize,
52}
53
54impl AgentBuilder {
55    /// Creates a new AgentBuilder
56    pub fn new() -> Self {
57        Self {
58            llm: None,
59            system_prompt: None,
60            tools: Vec::new(),
61            max_iterations: 10,
62        }
63    }
64
65    /// Sets the LLM (any type implementing `BaseChatModel`)
66    ///
67    /// Automatically wraps it as `Arc<dyn BaseChatModel<Error = ProviderError>>`.
68    pub fn llm<L>(mut self, llm: L) -> Self
69    where
70        L: BaseChatModel + Send + Sync + 'static,
71        L::Error: Into<ProviderError>,
72    {
73        self.llm = Some(lc_providers::wrap_chat_model(llm));
74        self
75    }
76
77    /// Sets the LLM (from an already-wrapped `Arc<dyn BaseChatModel>`)
78    ///
79    /// For LLM instances already created via `wrap_chat_model()` or `LLMClient`.
80    pub fn llm_from_arc(
81        mut self,
82        llm: Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>,
83    ) -> Self {
84        self.llm = Some(llm);
85        self
86    }
87
88    /// Sets the system prompt
89    pub fn system(mut self, prompt: impl Into<String>) -> Self {
90        self.system_prompt = Some(prompt.into());
91        self
92    }
93
94    /// Adds a single tool
95    pub fn tool<T: BaseTool + 'static>(mut self, tool: T) -> Self {
96        self.tools.push(Arc::new(tool));
97        self
98    }
99
100    /// Adds multiple tools
101    pub fn tools(mut self, tools: Vec<Arc<dyn BaseTool>>) -> Self {
102        self.tools.extend(tools);
103        self
104    }
105
106    /// Sets max iterations (clamped to [1, 100] to prevent 0 or runaway limits)
107    pub fn max_iterations(mut self, n: usize) -> Self {
108        const MIN_MAX_ITERATIONS: usize = 1;
109        const MAX_MAX_ITERATIONS: usize = 100;
110        self.max_iterations = n.clamp(MIN_MAX_ITERATIONS, MAX_MAX_ITERATIONS);
111        if n > MAX_MAX_ITERATIONS {
112            log::warn!("max_iterations {} clamped to {}", n, MAX_MAX_ITERATIONS);
113        }
114        self
115    }
116
117    /// Builds a FunctionCallingAgent
118    ///
119    /// # Errors
120    ///
121    /// Returns `AgentError::Other` if no LLM was set.
122    pub fn build(self) -> Result<FunctionCallingAgent, AgentError> {
123        let llm = self.llm.ok_or_else(|| {
124            AgentError::Other("AgentBuilder: LLM is required. Call .llm() first.".into())
125        })?;
126
127        Ok(FunctionCallingAgent::from_arc(
128            llm,
129            self.tools,
130            self.system_prompt,
131        ))
132    }
133
134    /// Builds and wraps as `Arc<dyn BaseAgent>` for direct use with `AgentExecutor`
135    ///
136    /// # Errors
137    ///
138    /// Returns `AgentError::Other` if no LLM was set.
139    pub fn build_as_agent(self) -> Result<Arc<dyn BaseAgent>, AgentError> {
140        let agent = self.build()?;
141        Ok(Arc::new(agent) as Arc<dyn BaseAgent>)
142    }
143
144    /// Returns the max iterations
145    pub fn get_max_iterations(&self) -> usize {
146        self.max_iterations
147    }
148}
149
150impl Default for AgentBuilder {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use lc_providers::{OpenAIChat, OpenAIConfig};
160    use lc_tools::Calculator;
161
162    #[test]
163    fn test_builder_with_openai() {
164        let config = OpenAIConfig::new("test_key").with_base_url("http://localhost:8080/v1");
165        let agent = AgentBuilder::new()
166            .llm(OpenAIChat::new(config))
167            .system("You are a test assistant.")
168            .tool(Calculator::new())
169            .build()
170            .unwrap();
171
172        assert_eq!(agent.tools_count(), 1);
173        assert_eq!(agent.system_prompt(), Some("You are a test assistant."));
174    }
175
176    #[test]
177    fn test_builder_missing_llm() {
178        let result = AgentBuilder::new().system("test").build();
179
180        assert!(result.is_err());
181        let err = result.unwrap_err();
182        assert!(err.to_string().contains("LLM is required"));
183    }
184
185    #[test]
186    fn test_builder_multiple_tools() {
187        let config = OpenAIConfig::new("test_key").with_base_url("http://localhost:8080/v1");
188        let agent = AgentBuilder::new()
189            .llm(OpenAIChat::new(config))
190            .tool(Calculator::new())
191            .tool(Calculator::new())
192            .build()
193            .unwrap();
194
195        assert_eq!(agent.tools_count(), 2);
196    }
197
198    #[test]
199    fn test_builder_tools_vec() {
200        let config = OpenAIConfig::new("test_key").with_base_url("http://localhost:8080/v1");
201        let tools: Vec<Arc<dyn BaseTool>> =
202            vec![Arc::new(Calculator::new()), Arc::new(Calculator::new())];
203        let agent = AgentBuilder::new()
204            .llm(OpenAIChat::new(config))
205            .tools(tools)
206            .build()
207            .unwrap();
208
209        assert_eq!(agent.tools_count(), 2);
210    }
211
212    #[test]
213    fn test_builder_build_as_agent() {
214        let config = OpenAIConfig::new("test_key").with_base_url("http://localhost:8080/v1");
215        let agent = AgentBuilder::new()
216            .llm(OpenAIChat::new(config))
217            .system("test")
218            .build_as_agent()
219            .unwrap();
220
221        let allowed = agent.get_allowed_tools();
222        assert!(allowed.is_some());
223    }
224
225    #[test]
226    fn test_builder_max_iterations() {
227        let builder = AgentBuilder::new().max_iterations(5);
228        assert_eq!(builder.get_max_iterations(), 5);
229    }
230
231    #[test]
232    fn test_builder_max_iterations_clamped_to_min() {
233        let builder = AgentBuilder::new().max_iterations(0);
234        assert_eq!(builder.get_max_iterations(), 1);
235    }
236
237    #[test]
238    fn test_builder_max_iterations_clamped_to_max() {
239        let builder = AgentBuilder::new().max_iterations(1_000_000_000);
240        assert_eq!(builder.get_max_iterations(), 100);
241    }
242
243    #[test]
244    fn test_builder_default() {
245        let builder = AgentBuilder::default();
246        assert_eq!(builder.get_max_iterations(), 10);
247        assert!(builder.llm.is_none());
248    }
249}