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}