Skip to main content

open_agent/tools/
tool.rs

1/// Tool definition for OpenAI-compatible function calling.
2///
3/// A `Tool` encapsulates everything needed for an LLM to understand and execute
4/// a function: its identity, purpose, expected inputs, and implementation.
5///
6/// ## Design Philosophy
7///
8/// Tools are **immutable by design**. Once created, their metadata and handler
9/// cannot be changed. This ensures:
10/// - Thread safety through simple cloning (all fields are cheaply cloned)
11/// - Predictable behavior - a tool's signature never changes mid-execution
12/// - Safe concurrent access without locks
13///
14/// ## Cloning Behavior
15///
16/// The `Clone` implementation is efficient:
17/// - `name` and `description`: String clones (heap allocation)
18/// - `input_schema`: JSON Value clone (reference counted internally in some cases)
19/// - `handler`: Arc clone (only increments atomic counter, shares same handler)
20///
21/// This means cloning a tool is relatively cheap and won't duplicate the actual
22/// handler implementation.
23///
24/// ## Thread Safety
25///
26/// Tools are fully thread-safe:
27/// - All fields are `Send + Sync`
28/// - Handler is wrapped in `Arc` for shared ownership
29/// - Can be stored in agent registries accessed by multiple threads
30/// - Can be cloned and sent across thread boundaries
31///
32/// ## Examples
33///
34/// ```rust,no_run
35/// use open_agent::Tool;
36/// use serde_json::json;
37///
38/// // Create a tool using the constructor
39/// let calculator = Tool::new(
40///     "multiply",
41///     "Multiply two numbers together",
42///     json!({
43///         "a": "number",
44///         "b": "number"
45///     }),
46///     |args| Box::pin(async move {
47///         let a = args["a"].as_f64().unwrap_or(1.0);
48///         let b = args["b"].as_f64().unwrap_or(1.0);
49///         Ok(json!({"result": a * b}))
50///     })
51/// );
52///
53/// // Access tool metadata
54/// println!("Tool: {}", calculator.name());
55/// println!("Description: {}", calculator.description());
56/// println!("Schema: {}", calculator.input_schema());
57/// ```
58#[derive(Clone)]
59pub struct Tool {
60    /// Unique identifier for the tool.
61    ///
62    /// The name should be descriptive and follow these conventions:
63    /// - Use lowercase with underscores (snake_case): `get_weather`, `search_database`
64    /// - Be concise but clear: prefer `search` over `s`, but avoid overly long names
65    /// - Avoid special characters that might cause issues in different contexts
66    ///
67    /// The LLM uses this name when deciding to invoke the tool, and it appears in
68    /// function call responses. Choose names that clearly indicate the tool's purpose.
69    ///
70    /// # Examples
71    /// - `get_weather` - Fetches weather data
72    /// - `calculate` - Performs calculations
73    /// - `search_documents` - Searches through document store
74    name: String,
75
76    /// Human-readable description of what the tool does.
77    ///
78    /// This description is sent to the LLM and significantly influences when the tool
79    /// is invoked. A good description should:
80    ///
81    /// - Clearly state the tool's purpose and capabilities
82    /// - Mention key parameters and what they control
83    /// - Include any important limitations or requirements
84    /// - Be concise but complete (typically 1-3 sentences)
85    ///
86    /// The LLM relies heavily on this description to determine if the tool is
87    /// appropriate for a given user request.
88    ///
89    /// # Examples
90    ///
91    /// Good: "Get current weather conditions for a specific location. Requires a
92    /// location name and optional temperature units (celsius/fahrenheit)."
93    ///
94    /// Poor: "Weather tool" (too vague, doesn't explain parameters or behavior)
95    description: String,
96
97    /// JSON Schema defining the tool's input parameters.
98    ///
99    /// This schema describes what arguments the tool expects and is automatically
100    /// converted to OpenAI's function calling format. The schema serves two purposes:
101    ///
102    /// 1. **LLM Guidance**: Tells the LLM what arguments to provide when calling the tool
103    /// 2. **Validation**: Can be used to validate arguments before handler execution
104    ///
105    /// The schema is stored in OpenAI's expected format after conversion:
106    /// ```json
107    /// {
108    ///   "type": "object",
109    ///   "properties": {
110    ///     "param_name": {
111    ///       "type": "string",
112    ///       "description": "Parameter description"
113    ///     }
114    ///   },
115    ///   "required": ["param_name"]
116    /// }
117    /// ```
118    ///
119    /// See [`Tool::new`] for details on how simple schemas are converted to this format.
120    input_schema: Value,
121
122    /// Async handler function that executes the tool's logic.
123    ///
124    /// The handler receives arguments as a JSON [`Value`] and returns a `Result<Value>`.
125    /// It's wrapped in an [`Arc`] for efficient sharing and cloning.
126    ///
127    /// ## Argument Structure
128    ///
129    /// Arguments are passed as a JSON object matching the `input_schema`:
130    /// ```json
131    /// {
132    ///   "param1": "value1",
133    ///   "param2": 42,
134    ///   "param3": [1, 2, 3]
135    /// }
136    /// ```
137    ///
138    /// ## Return Value
139    ///
140    /// Handlers should return a JSON value that will be sent back to the LLM.
141    /// The structure is flexible but should be informative:
142    ///
143    /// ```json
144    /// // Success response
145    /// {
146    ///   "status": "success",
147    ///   "data": { /* results */ }
148    /// }
149    ///
150    /// // Or just the data directly
151    /// {
152    ///   "temperature": 22,
153    ///   "conditions": "sunny"
154    /// }
155    /// ```
156    ///
157    /// ## Error Handling
158    ///
159    /// If the handler returns `Err()`, the error will be propagated to the agent
160    /// which can decide how to handle it (retry, report to LLM, etc.).
161    ///
162    /// ## Example Handler
163    ///
164    /// ```ignore
165    /// use serde_json::{json, Value};
166    /// use open_agent::{Result, Error};
167    ///
168    /// let handler = |args: Value| Box::pin(async move {
169    ///     // Extract and validate arguments
170    ///     let query = args["query"].as_str()
171    ///         .ok_or_else(|| Error::tool("Missing query parameter"))?;
172    ///
173    ///     // Perform async operation
174    ///     let results = perform_search(query).await?;
175    ///
176    ///     // Return structured response
177    ///     Ok(json!({
178    ///         "results": results,
179    ///         "count": results.len()
180    ///     }))
181    /// });
182    /// # async fn perform_search(query: &str) -> Result<Vec<String>> { Ok(vec![]) }
183    /// ```
184    handler: ToolHandler,
185}
186
187impl Tool {
188    /// Create a new tool with flexible schema definition.
189    ///
190    /// This constructor handles schema conversion automatically, accepting multiple formats:
191    ///
192    /// ## Schema Formats
193    ///
194    /// ### 1. Simple Type Notation
195    /// ```json
196    /// {
197    ///   "location": "string",
198    ///   "temperature": "number"
199    /// }
200    /// ```
201    /// All parameters are marked as required by default.
202    ///
203    /// ### 2. Extended Property Schema
204    /// ```json
205    /// {
206    ///   "query": {
207    ///     "type": "string",
208    ///     "description": "Search query"
209    ///   },
210    ///   "limit": {
211    ///     "type": "integer",
212    ///     "optional": true
213    ///   }
214    /// }
215    /// ```
216    /// Use `"optional": true` or `"required": false` to mark parameters as optional.
217    ///
218    /// ### 3. Full JSON Schema
219    /// ```json
220    /// {
221    ///   "type": "object",
222    ///   "properties": {
223    ///     "name": {"type": "string"}
224    ///   },
225    ///   "required": ["name"]
226    /// }
227    /// ```
228    /// Already valid JSON Schema - passed through as-is.
229    ///
230    /// ## Handler Requirements
231    ///
232    /// The handler must satisfy several trait bounds:
233    ///
234    /// - `Fn(Value) -> Fut`: Takes JSON arguments, returns a future
235    /// - `Send + Sync`: Can be shared across threads safely
236    /// - `'static`: No non-static references (must own all data)
237    /// - `Fut: Future<Output = Result<Value>> + Send`: Future is sendable and produces Result
238    ///
239    /// The constructor automatically wraps the handler in `Arc<...>` and boxes the futures,
240    /// so you don't need to do this manually.
241    ///
242    /// ## Generic Parameters
243    ///
244    /// - `F`: The handler function type
245    /// - `Fut`: The future type returned by the handler
246    ///
247    /// These are inferred automatically from the handler you provide.
248    ///
249    /// # Examples
250    ///
251    /// ## Simple Calculator Tool
252    ///
253    /// ```rust,no_run
254    /// use open_agent::Tool;
255    /// use serde_json::json;
256    ///
257    /// let add_tool = Tool::new(
258    ///     "add",
259    ///     "Add two numbers together",
260    ///     json!({
261    ///         "a": "number",
262    ///         "b": "number"
263    ///     }),
264    ///     |args| {
265    ///         Box::pin(async move {
266    ///             let a = args.get("a")
267    ///                 .and_then(|v| v.as_f64())
268    ///                 .ok_or_else(|| open_agent::Error::invalid_input("Parameter 'a' must be a number"))?;
269    ///             let b = args.get("b")
270    ///                 .and_then(|v| v.as_f64())
271    ///                 .ok_or_else(|| open_agent::Error::invalid_input("Parameter 'b' must be a number"))?;
272    ///             Ok(json!({"result": a + b}))
273    ///         })
274    ///     }
275    /// );
276    /// ```
277    ///
278    /// ## Tool with Optional Parameters
279    ///
280    /// ```rust,no_run
281    /// use open_agent::Tool;
282    /// use serde_json::json;
283    ///
284    /// let search_tool = Tool::new(
285    ///     "search",
286    ///     "Search for information",
287    ///     json!({
288    ///         "query": {
289    ///             "type": "string",
290    ///             "description": "What to search for"
291    ///         },
292    ///         "max_results": {
293    ///             "type": "integer",
294    ///             "description": "Maximum results to return",
295    ///             "optional": true,
296    ///             "default": 10
297    ///         }
298    ///     }),
299    ///     |args| Box::pin(async move {
300    ///         let query = args["query"].as_str().unwrap_or("");
301    ///         let max = args.get("max_results")
302    ///             .and_then(|v| v.as_i64())
303    ///             .unwrap_or(10);
304    ///
305    ///         // Perform search...
306    ///         Ok(json!({"results": [], "query": query, "limit": max}))
307    ///     })
308    /// );
309    /// ```
310    ///
311    /// ## Tool with External State
312    ///
313    /// ```rust,no_run
314    /// use open_agent::Tool;
315    /// use serde_json::json;
316    /// use std::sync::Arc;
317    ///
318    /// // State that needs to be shared
319    /// let api_key = Arc::new("secret-key".to_string());
320    ///
321    /// let tool = Tool::new(
322    ///     "api_call",
323    ///     "Make an API call",
324    ///     json!({"endpoint": "string"}),
325    ///     move |args| {
326    ///         // Clone Arc to move into async block
327    ///         let api_key = api_key.clone();
328    ///         Box::pin(async move {
329    ///             let endpoint = args["endpoint"].as_str().unwrap_or("");
330    ///             // Use api_key in async operation
331    ///             println!("Calling {} with key {}", endpoint, api_key);
332    ///             Ok(json!({"status": "success"}))
333    ///         })
334    ///     }
335    /// );
336    /// ```
337    pub fn new<F, Fut>(
338        name: impl Into<String>,
339        description: impl Into<String>,
340        input_schema: Value,
341        handler: F,
342    ) -> Self
343    where
344        F: Fn(Value) -> Fut + Send + Sync + 'static,
345        Fut: Future<Output = Result<Value>> + Send + 'static,
346    {
347        // Convert inputs to owned types
348        let name = name.into();
349        let description = description.into();
350
351        // Convert the provided schema to OpenAI's expected JSON Schema format
352        // This handles simple type notation, extended schemas, and full JSON Schema
353        let input_schema = convert_schema_to_openai(input_schema);
354
355        Self {
356            name,
357            description,
358            input_schema,
359            // Wrap the handler in Arc for cheap cloning and thread-safe sharing
360            // Box::pin converts the future to a pinned, heap-allocated trait object
361            handler: Arc::new(move |args| Box::pin(handler(args))),
362        }
363    }
364
365    /// Execute the tool with the provided arguments.
366    ///
367    /// This method invokes the tool's handler asynchronously, passing the arguments
368    /// and awaiting the result. It's the primary way to run a tool's logic.
369    ///
370    /// ## Execution Flow
371    ///
372    /// 1. Call the handler function (stored in `Arc`) with arguments
373    /// 2. The handler returns a `Pin<Box<dyn Future>>`
374    /// 3. Await the future to get the `Result<Value>`
375    /// 4. Return the result (success value or error)
376    ///
377    /// ## Arguments
378    ///
379    /// Arguments should be a JSON object matching the tool's `input_schema`:
380    /// ```json
381    /// {
382    ///   "param1": "value1",
383    ///   "param2": 42
384    /// }
385    /// ```
386    ///
387    /// The handler is responsible for extracting and validating these arguments.
388    ///
389    /// ## Error Handling
390    ///
391    /// If the handler returns an error, it's propagated directly. The agent
392    /// calling this method should handle errors appropriately (e.g., retry logic,
393    /// error reporting to the LLM).
394    ///
395    /// # Examples
396    ///
397    /// ```rust,no_run
398    /// # use open_agent::Tool;
399    /// # use serde_json::json;
400    /// # async fn example() -> open_agent::Result<()> {
401    /// let calculator = Tool::new(
402    ///     "add",
403    ///     "Add numbers",
404    ///     json!({"a": "number", "b": "number"}),
405    ///     |args| Box::pin(async move {
406    ///         let sum = args["a"].as_f64().unwrap() + args["b"].as_f64().unwrap();
407    ///         Ok(json!({"result": sum}))
408    ///     })
409    /// );
410    ///
411    /// // Execute the tool
412    /// let result = calculator.execute(json!({"a": 5.0, "b": 3.0})).await?;
413    /// assert_eq!(result["result"], 8.0);
414    /// # Ok(())
415    /// # }
416    /// ```
417    pub async fn execute(&self, arguments: Value) -> Result<Value> {
418        // Invoke the handler function with the arguments
419        // The handler returns Pin<Box<dyn Future>>, which we immediately await
420        (self.handler)(arguments).await
421    }
422
423    /// Convert the tool definition to OpenAI's function calling format.
424    ///
425    /// This method generates the JSON structure expected by OpenAI's Chat Completion
426    /// API when using function calling. The format is also compatible with other
427    /// LLM providers that follow OpenAI's conventions.
428    ///
429    /// ## Output Format
430    ///
431    /// Returns a JSON structure like:
432    /// ```json
433    /// {
434    ///   "type": "function",
435    ///   "function": {
436    ///     "name": "tool_name",
437    ///     "description": "Tool description",
438    ///     "parameters": {
439    ///       "type": "object",
440    ///       "properties": { ... },
441    ///       "required": [ ... ]
442    ///     }
443    ///   }
444    /// }
445    /// ```
446    ///
447    /// ## Usage in API Calls
448    ///
449    /// This format is typically used when constructing the `tools` array for
450    /// API requests:
451    /// ```json
452    /// {
453    ///   "model": "gpt-4",
454    ///   "messages": [...],
455    ///   "tools": [
456    ///     // Output of to_openai_format() for each tool
457    ///   ]
458    /// }
459    /// ```
460    ///
461    /// # Examples
462    ///
463    /// ```rust,no_run
464    /// # use open_agent::tool;
465    /// # use serde_json::json;
466    /// let my_tool = tool("search", "Search for information")
467    ///     .param("query", "string")
468    ///     .build(|_| async { Ok(json!({})) });
469    ///
470    /// let openai_format = my_tool.to_openai_format();
471    ///
472    /// // Verify the structure
473    /// assert_eq!(openai_format["type"], "function");
474    /// assert_eq!(openai_format["function"]["name"], "search");
475    /// assert_eq!(openai_format["function"]["description"], "Search for information");
476    /// assert!(openai_format["function"]["parameters"].is_object());
477    /// ```
478    pub fn to_openai_format(&self) -> Value {
479        serde_json::json!({
480            "type": "function",
481            "function": {
482                "name": self.name,
483                "description": self.description,
484                "parameters": self.input_schema
485            }
486        })
487    }
488
489    /// Returns the tool's name.
490    pub fn name(&self) -> &str {
491        &self.name
492    }
493
494    /// Returns the tool's description.
495    pub fn description(&self) -> &str {
496        &self.description
497    }
498
499    /// Returns a reference to the tool's input schema.
500    pub fn input_schema(&self) -> &Value {
501        &self.input_schema
502    }
503}
504
505/// Custom Debug implementation for Tool.
506///
507/// The handler field is omitted from debug output because:
508/// - Function pointers/closures don't have meaningful debug representations
509/// - The `Arc<dyn Fn...>` type is complex and not useful to display
510/// - Showing the handler would just print something like "Arc { ... }"
511///
512/// Only the metadata fields (name, description, input_schema) are shown,
513/// which are the most useful for debugging tool definitions.
514impl std::fmt::Debug for Tool {
515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516        f.debug_struct("Tool")
517            .field("name", &self.name)
518            .field("description", &self.description)
519            .field("input_schema", &self.input_schema)
520            // Handler is intentionally omitted - it's not debuggable
521            .finish()
522    }
523}