Skip to main content

open_agent/tools/
builder.rs

1/// Builder for creating tools with a fluent API.
2///
3/// The `ToolBuilder` provides a convenient, readable way to construct tools
4/// using method chaining. It's especially useful when building tools incrementally
5/// or when the schema structure is determined dynamically.
6///
7/// ## Builder Pattern Benefits
8///
9/// - **Readability**: Method chains read like natural language
10/// - **Flexibility**: Add parameters conditionally
11/// - **Type safety**: Catches errors at compile time
12/// - **Discoverability**: IDE autocomplete shows available options
13///
14/// ## Workflow
15///
16/// 1. Create builder with [`tool()`] or [`ToolBuilder::new()`]
17/// 2. Add parameters with [`.param()`](ToolBuilder::param)
18/// 3. Optionally set schema with [`.schema()`](ToolBuilder::schema)
19/// 4. Finalize with [`.build()`](ToolBuilder::build) and provide handler
20///
21/// ## Examples
22///
23/// See the [`tool()`] function for detailed examples.
24///
25/// ## Note on Schema Mutation
26///
27/// If you call `.schema()` after `.param()`, the parameters will be replaced
28/// by the new schema. Similarly, calling `.param()` after `.schema()` will
29/// reset a non-object schema to an empty object before adding the parameter.
30/// Generally, use either `.schema()` or `.param()`, not both.
31pub struct ToolBuilder {
32    /// The tool's unique identifier
33    name: String,
34
35    /// Human-readable description of the tool's purpose
36    description: String,
37
38    /// The input schema, built up through .param() calls or set via .schema()
39    schema: Value,
40}
41
42impl ToolBuilder {
43    /// Start building a new tool with a name and description.
44    ///
45    /// This creates a builder with an empty schema. You can then add parameters
46    /// using [`.param()`](ToolBuilder::param) or set a complete schema with
47    /// [`.schema()`](ToolBuilder::schema).
48    ///
49    /// ## Parameters
50    ///
51    /// - `name`: Tool identifier (converted to String via Into trait)
52    /// - `description`: Human-readable explanation of what the tool does
53    ///
54    /// ## Examples
55    ///
56    /// ```rust
57    /// # use open_agent::ToolBuilder;
58    /// let builder = ToolBuilder::new("search", "Search for information");
59    /// // builder.param(...).build(...)
60    /// ```
61    ///
62    /// Typically you'll use the [`tool()`] convenience function instead of calling
63    /// this directly.
64    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
65        Self {
66            name: name.into(),
67            description: description.into(),
68            // Start with an empty object schema
69            schema: serde_json::json!({}),
70        }
71    }
72
73    /// Set the complete input schema.
74    ///
75    /// This replaces any schema or parameters set previously. Use this when you
76    /// have a pre-built schema object (especially useful for complex schemas
77    /// with nested structures).
78    ///
79    /// ## Schema Format
80    ///
81    /// Accepts any of the formats supported by [`Tool::new`]:
82    /// - Simple type notation: `{"param": "string"}`
83    /// - Extended schema: `{"param": {"type": "string", "description": "..."}}`
84    /// - Full JSON Schema: `{"type": "object", "properties": {...}, "required": [...]}`
85    ///
86    /// ## Warning
87    ///
88    /// This overwrites any parameters added via `.param()`. Generally, choose
89    /// one approach: either use `.param()` for simple cases or `.schema()` for
90    /// complex cases, but not both.
91    ///
92    /// ## Examples
93    ///
94    /// ```rust
95    /// # use open_agent::tool;
96    /// # use serde_json::json;
97    /// let my_tool = tool("api_call", "Make an API call")
98    ///     .schema(json!({
99    ///         "endpoint": {
100    ///             "type": "string",
101    ///             "description": "API endpoint URL",
102    ///             "pattern": "^https://"
103    ///         },
104    ///         "method": {
105    ///             "type": "string",
106    ///             "enum": ["GET", "POST", "PUT", "DELETE"]
107    ///         }
108    ///     }))
109    ///     .build(|_| async { Ok(json!({})) });
110    /// ```
111    pub fn schema(mut self, schema: Value) -> Self {
112        // Replace the current schema entirely
113        self.schema = schema;
114        self
115    }
116
117    /// Add a single parameter to the schema.
118    ///
119    /// This is a convenience method for building schemas incrementally. Each call
120    /// adds one parameter with a simple type string.
121    ///
122    /// ## Parameters
123    ///
124    /// - `name`: Parameter name (will be required in tool calls)
125    /// - `type_str`: Type string like "string", "number", "boolean", etc.
126    ///   Supported types: "string", "number", "integer", "boolean", "array", "object".
127    ///
128    /// ## Behavior
129    ///
130    /// - If the current schema is not an object (e.g., you called `.schema()` with
131    ///   a non-object value), it will be reset to an empty object first.
132    /// - All parameters added via `.param()` are marked as required.
133    /// - For optional parameters, use `.schema()` with extended property format.
134    ///
135    /// ## Method Chaining
136    ///
137    /// This method consumes `self` and returns it, enabling method chaining:
138    /// ```rust
139    /// # use open_agent::tool;
140    /// # use serde_json::json;
141    /// let my_tool = tool("calculate", "Perform calculation")
142    ///     .param("operation", "string")
143    ///     .param("x", "number")
144    ///     .param("y", "number")
145    ///     .build(|_| async { Ok(json!({})) });
146    /// ```
147    ///
148    /// ## Examples
149    ///
150    /// ```rust
151    /// # use open_agent::tool;
152    /// # use serde_json::json;
153    /// // Add multiple parameters
154    /// let weather_tool = tool("get_weather", "Get weather for a location")
155    ///     .param("location", "string")
156    ///     .param("units", "string")
157    ///     .build(|args| async move {
158    ///         // Implementation
159    ///         Ok(json!({"temp": 72}))
160    ///     });
161    /// ```
162    pub fn param(mut self, name: &str, type_str: &str) -> Self {
163        // Ensure schema is an object, reset if not
164        // This handles the edge case where .schema() was called with a non-object
165        if !self.schema.is_object() {
166            self.schema = serde_json::json!({});
167        }
168
169        // Get mutable reference to the object. This should always succeed because we just
170        // ensured it's an object above, but we use expect() for defensive programming.
171        let obj = self
172            .schema
173            .as_object_mut()
174            .expect("BUG: schema should be an object after initialization");
175
176        // Insert the parameter as a simple type string
177        // This will be converted to proper JSON Schema by convert_schema_to_openai
178        obj.insert(name.to_string(), Value::String(type_str.to_string()));
179
180        self
181    }
182
183    /// Build the final Tool with a handler function.
184    ///
185    /// This consumes the builder and produces a [`Tool`] ready for use. The handler
186    /// function defines what happens when the tool is called.
187    ///
188    /// ## Handler Requirements
189    ///
190    /// The handler must be:
191    /// - An async function or closure
192    /// - Accept a single `Value` argument (the tool's input parameters)
193    /// - Return a `Future<Output = Result<Value>>`
194    /// - Implement `Send + Sync + 'static` for thread safety
195    ///
196    /// ## Generic Parameters
197    ///
198    /// - `F`: The handler function type (inferred from the closure/function you provide)
199    /// - `Fut`: The future type returned by the handler (inferred automatically)
200    ///
201    /// ## Examples
202    ///
203    /// ### Simple Handler
204    /// ```rust
205    /// # use open_agent::tool;
206    /// # use serde_json::json;
207    /// let my_tool = tool("echo", "Echo back the input")
208    ///     .param("message", "string")
209    ///     .build(|args| async move {
210    ///         Ok(args) // Echo arguments back
211    ///     });
212    /// ```
213    ///
214    /// ### Handler with External State
215    /// ```rust
216    /// # use open_agent::tool;
217    /// # use serde_json::json;
218    /// # use std::sync::Arc;
219    /// let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
220    ///
221    /// let my_tool = tool("increment", "Increment a counter")
222    ///     .build(move |_args| {
223    ///         let counter = counter.clone();
224    ///         async move {
225    ///             let val = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
226    ///             Ok(json!({"count": val + 1}))
227    ///         }
228    ///     });
229    /// ```
230    ///
231    /// ### Handler with Error Handling
232    /// ```rust
233    /// # use open_agent::{tool, Error};
234    /// # use serde_json::json;
235    /// let my_tool = tool("divide", "Divide two numbers")
236    ///     .param("a", "number")
237    ///     .param("b", "number")
238    ///     .build(|args| async move {
239    ///         let a = args["a"].as_f64().ok_or_else(|| Error::tool("Invalid 'a' parameter"))?;
240    ///         let b = args["b"].as_f64().ok_or_else(|| Error::tool("Invalid 'b' parameter"))?;
241    ///
242    ///         if b == 0.0 {
243    ///             return Err(Error::tool("Division by zero"));
244    ///         }
245    ///
246    ///         Ok(json!({"result": a / b}))
247    ///     });
248    /// ```
249    pub fn build<F, Fut>(self, handler: F) -> Tool
250    where
251        F: Fn(Value) -> Fut + Send + Sync + 'static,
252        Fut: Future<Output = Result<Value>> + Send + 'static,
253    {
254        // Delegate to Tool::new which handles schema conversion and handler wrapping
255        Tool::new(self.name, self.description, self.schema, handler)
256    }
257}