open_agent/tools/tool.rs
1/// Tool definition for function calling, over either wire protocol.
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}