open_agent/
tools.rs

1//! # Tool System for Open Agent SDK
2//!
3//! This module provides a comprehensive tool definition system compatible with OpenAI's
4//! function calling API and similar LLM tool-use patterns.
5//!
6//! ## Architecture Overview
7//!
8//! The tool system is built around three core concepts:
9//!
10//! 1. **Tool Definition** - The [`Tool`] struct contains metadata (name, description, schema)
11//!    and an async handler function that executes the tool's logic.
12//!
13//! 2. **Schema Flexibility** - Supports both simple type notation and full JSON Schema,
14//!    automatically converting to the OpenAI function calling format.
15//!
16//! 3. **Async Execution** - Tools run asynchronously with a pinned, boxed future pattern
17//!    that enables dynamic dispatch and easy integration with async runtimes.
18//!
19//! ## Tool Lifecycle
20//!
21//! ```text
22//! 1. Definition:   Create tool with name, description, schema, and handler
23//! 2. Registration: Add tool to agent's tool registry
24//! 3. Invocation:   LLM decides to call tool with specific arguments
25//! 4. Execution:    Handler processes arguments and returns result
26//! 5. Response:     Result is sent back to LLM for further processing
27//! ```
28//!
29//! ## Schema Conversion
30//!
31//! The system intelligently handles multiple schema formats:
32//!
33//! - **Simple notation**: `{"location": "string", "units": "string"}`
34//! - **Typed schema**: `{"param": {"type": "number", "description": "A value"}}`
35//! - **Full JSON Schema**: Already valid JSON Schema with "type" and "properties"
36//!
37//! All formats are normalized to OpenAI's expected JSON Schema structure.
38//!
39//! ## Handler Pattern
40//!
41//! Tool handlers use `Pin<Box<dyn Future>>` for several critical reasons:
42//!
43//! - **Type Erasure**: Different async functions have different concrete types.
44//!   Boxing allows storing handlers with varying types in a single collection.
45//!
46//! - **Pinning**: Futures in Rust must be pinned to a memory location before polling.
47//!   Pin guarantees the future won't move, which is essential for self-referential types.
48//!
49//! - **Send + Sync**: These bounds ensure handlers can be safely shared across threads,
50//!   crucial for concurrent agent operations.
51//!
52//! ## Examples
53//!
54//! ### Creating a Simple Tool
55//!
56//! ```rust,no_run
57//! use open_agent::{tool, Result};
58//! use serde_json::json;
59//!
60//! // Using the builder pattern
61//! let weather_tool = tool("get_weather", "Get current weather for a location")
62//!     .param("location", "string")
63//!     .param("units", "string")
64//!     .build(|args| async move {
65//!         let location = args["location"].as_str().unwrap_or("Unknown");
66//!         let units = args["units"].as_str().unwrap_or("celsius");
67//!
68//!         // Simulate API call
69//!         Ok(json!({
70//!             "location": location,
71//!             "temperature": 22,
72//!             "units": units
73//!         }))
74//!     });
75//! ```
76//!
77//! ### Creating a Tool with Complex Schema
78//!
79//! ```rust,no_run
80//! use open_agent::Tool;
81//! use serde_json::json;
82//!
83//! let search_tool = Tool::new(
84//!     "search",
85//!     "Search the web for information",
86//!     json!({
87//!         "query": {
88//!             "type": "string",
89//!             "description": "Search query"
90//!         },
91//!         "max_results": {
92//!             "type": "integer",
93//!             "description": "Maximum number of results",
94//!             "optional": true
95//!         }
96//!     }),
97//!     |args| Box::pin(async move {
98//!         // Implementation
99//!         Ok(json!({"results": []}))
100//!     })
101//! );
102//! ```
103
104use crate::Result;
105use serde_json::Value;
106use std::future::Future;
107use std::pin::Pin;
108use std::sync::Arc;
109
110/// Type alias for tool handler functions.
111///
112/// ## Handler Anatomy
113///
114/// A tool handler is a complex type that enables dynamic async execution:
115///
116/// ```text
117/// Arc<                                      // Thread-safe reference counting
118///   dyn Fn(Value)                          // Function taking JSON arguments
119///     -> Pin<Box<                           // Pinned heap allocation
120///       dyn Future<Output = Result<Value>>  // Async computation
121///         + Send>>                          // Can cross thread boundaries
122///     + Send + Sync>                        // Handler itself is thread-safe
123/// ```
124///
125/// ### Why Arc?
126///
127/// [`Arc`] (Atomic Reference Counted) allows multiple parts of the system to hold
128/// references to the same handler without worrying about ownership. This is essential
129/// because tools may be:
130/// - Stored in an agent's tool registry
131/// - Cloned when creating tool definitions for API calls
132/// - Accessed concurrently by multiple agent threads
133///
134/// The atomic reference counting ensures thread-safe access without locks on the
135/// handler reference itself (though the handler may still use internal synchronization).
136///
137/// ### Why Pin<Box<>>?
138///
139/// **Pinning** guarantees that the future won't be moved in memory after creation.
140/// This is critical because async functions can create self-referential structures
141/// (e.g., a future holding a reference to its own data). Moving such a structure
142/// would invalidate internal pointers.
143///
144/// **Boxing** (heap allocation) enables:
145/// - Storing futures of different concrete types (different handlers) in one container
146/// - Having a predictable, small stack footprint (just a pointer, not the whole future)
147/// - Dynamic dispatch - the actual future type is erased but still executable
148///
149/// ### Why Send + Sync?
150///
151/// - **Send**: The future can be sent across thread boundaries. Essential for
152///   multi-threaded async runtimes (like Tokio) that may move tasks between threads.
153///
154/// - **Sync**: Multiple threads can safely hold references to the handler. This allows
155///   tools to be called concurrently by different parts of the system.
156///
157/// ## Example Usage
158///
159/// ```rust,no_run
160/// use std::sync::Arc;
161/// use std::pin::Pin;
162/// use std::future::Future;
163/// use serde_json::{json, Value};
164/// use open_agent::Result;
165///
166/// // Define a handler that matches ToolHandler type
167/// let handler: Arc<dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value>> + Send>> + Send + Sync> =
168///     Arc::new(|args| {
169///         Box::pin(async move {
170///             // Handler implementation
171///             Ok(json!({"status": "success"}))
172///         })
173///     });
174///
175/// // Can be cloned cheaply (only increments Arc counter)
176/// let handler_clone = handler.clone();
177/// ```
178pub type ToolHandler =
179    Arc<dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value>> + Send>> + Send + Sync>;
180
181/// Tool definition for OpenAI-compatible function calling.
182///
183/// A `Tool` encapsulates everything needed for an LLM to understand and execute
184/// a function: its identity, purpose, expected inputs, and implementation.
185///
186/// ## Design Philosophy
187///
188/// Tools are **immutable by design**. Once created, their metadata and handler
189/// cannot be changed. This ensures:
190/// - Thread safety through simple cloning (all fields are cheaply cloned)
191/// - Predictable behavior - a tool's signature never changes mid-execution
192/// - Safe concurrent access without locks
193///
194/// ## Cloning Behavior
195///
196/// The `Clone` implementation is efficient:
197/// - `name` and `description`: String clones (heap allocation)
198/// - `input_schema`: JSON Value clone (reference counted internally in some cases)
199/// - `handler`: Arc clone (only increments atomic counter, shares same handler)
200///
201/// This means cloning a tool is relatively cheap and won't duplicate the actual
202/// handler implementation.
203///
204/// ## Thread Safety
205///
206/// Tools are fully thread-safe:
207/// - All fields are `Send + Sync`
208/// - Handler is wrapped in `Arc` for shared ownership
209/// - Can be stored in agent registries accessed by multiple threads
210/// - Can be cloned and sent across thread boundaries
211///
212/// ## Examples
213///
214/// ```rust,no_run
215/// use open_agent::Tool;
216/// use serde_json::json;
217///
218/// // Create a tool using the constructor
219/// let calculator = Tool::new(
220///     "multiply",
221///     "Multiply two numbers together",
222///     json!({
223///         "a": "number",
224///         "b": "number"
225///     }),
226///     |args| Box::pin(async move {
227///         let a = args["a"].as_f64().unwrap_or(1.0);
228///         let b = args["b"].as_f64().unwrap_or(1.0);
229///         Ok(json!({"result": a * b}))
230///     })
231/// );
232///
233/// // Access tool metadata
234/// println!("Tool: {}", calculator.name());
235/// println!("Description: {}", calculator.description());
236/// println!("Schema: {}", calculator.input_schema());
237/// ```
238#[derive(Clone)]
239pub struct Tool {
240    /// Unique identifier for the tool.
241    ///
242    /// The name should be descriptive and follow these conventions:
243    /// - Use lowercase with underscores (snake_case): `get_weather`, `search_database`
244    /// - Be concise but clear: prefer `search` over `s`, but avoid overly long names
245    /// - Avoid special characters that might cause issues in different contexts
246    ///
247    /// The LLM uses this name when deciding to invoke the tool, and it appears in
248    /// function call responses. Choose names that clearly indicate the tool's purpose.
249    ///
250    /// # Examples
251    /// - `get_weather` - Fetches weather data
252    /// - `calculate` - Performs calculations
253    /// - `search_documents` - Searches through document store
254    name: String,
255
256    /// Human-readable description of what the tool does.
257    ///
258    /// This description is sent to the LLM and significantly influences when the tool
259    /// is invoked. A good description should:
260    ///
261    /// - Clearly state the tool's purpose and capabilities
262    /// - Mention key parameters and what they control
263    /// - Include any important limitations or requirements
264    /// - Be concise but complete (typically 1-3 sentences)
265    ///
266    /// The LLM relies heavily on this description to determine if the tool is
267    /// appropriate for a given user request.
268    ///
269    /// # Examples
270    ///
271    /// Good: "Get current weather conditions for a specific location. Requires a
272    /// location name and optional temperature units (celsius/fahrenheit)."
273    ///
274    /// Poor: "Weather tool" (too vague, doesn't explain parameters or behavior)
275    description: String,
276
277    /// JSON Schema defining the tool's input parameters.
278    ///
279    /// This schema describes what arguments the tool expects and is automatically
280    /// converted to OpenAI's function calling format. The schema serves two purposes:
281    ///
282    /// 1. **LLM Guidance**: Tells the LLM what arguments to provide when calling the tool
283    /// 2. **Validation**: Can be used to validate arguments before handler execution
284    ///
285    /// The schema is stored in OpenAI's expected format after conversion:
286    /// ```json
287    /// {
288    ///   "type": "object",
289    ///   "properties": {
290    ///     "param_name": {
291    ///       "type": "string",
292    ///       "description": "Parameter description"
293    ///     }
294    ///   },
295    ///   "required": ["param_name"]
296    /// }
297    /// ```
298    ///
299    /// See [`Tool::new`] for details on how simple schemas are converted to this format.
300    input_schema: Value,
301
302    /// Async handler function that executes the tool's logic.
303    ///
304    /// The handler receives arguments as a JSON [`Value`] and returns a `Result<Value>`.
305    /// It's wrapped in an [`Arc`] for efficient sharing and cloning.
306    ///
307    /// ## Argument Structure
308    ///
309    /// Arguments are passed as a JSON object matching the `input_schema`:
310    /// ```json
311    /// {
312    ///   "param1": "value1",
313    ///   "param2": 42,
314    ///   "param3": [1, 2, 3]
315    /// }
316    /// ```
317    ///
318    /// ## Return Value
319    ///
320    /// Handlers should return a JSON value that will be sent back to the LLM.
321    /// The structure is flexible but should be informative:
322    ///
323    /// ```json
324    /// // Success response
325    /// {
326    ///   "status": "success",
327    ///   "data": { /* results */ }
328    /// }
329    ///
330    /// // Or just the data directly
331    /// {
332    ///   "temperature": 22,
333    ///   "conditions": "sunny"
334    /// }
335    /// ```
336    ///
337    /// ## Error Handling
338    ///
339    /// If the handler returns `Err()`, the error will be propagated to the agent
340    /// which can decide how to handle it (retry, report to LLM, etc.).
341    ///
342    /// ## Example Handler
343    ///
344    /// ```ignore
345    /// use serde_json::{json, Value};
346    /// use open_agent::{Result, Error};
347    ///
348    /// let handler = |args: Value| Box::pin(async move {
349    ///     // Extract and validate arguments
350    ///     let query = args["query"].as_str()
351    ///         .ok_or_else(|| Error::tool("Missing query parameter"))?;
352    ///
353    ///     // Perform async operation
354    ///     let results = perform_search(query).await?;
355    ///
356    ///     // Return structured response
357    ///     Ok(json!({
358    ///         "results": results,
359    ///         "count": results.len()
360    ///     }))
361    /// });
362    /// # async fn perform_search(query: &str) -> Result<Vec<String>> { Ok(vec![]) }
363    /// ```
364    handler: ToolHandler,
365}
366
367impl Tool {
368    /// Create a new tool with flexible schema definition.
369    ///
370    /// This constructor handles schema conversion automatically, accepting multiple formats:
371    ///
372    /// ## Schema Formats
373    ///
374    /// ### 1. Simple Type Notation
375    /// ```json
376    /// {
377    ///   "location": "string",
378    ///   "temperature": "number"
379    /// }
380    /// ```
381    /// All parameters are marked as required by default.
382    ///
383    /// ### 2. Extended Property Schema
384    /// ```json
385    /// {
386    ///   "query": {
387    ///     "type": "string",
388    ///     "description": "Search query"
389    ///   },
390    ///   "limit": {
391    ///     "type": "integer",
392    ///     "optional": true
393    ///   }
394    /// }
395    /// ```
396    /// Use `"optional": true` or `"required": false` to mark parameters as optional.
397    ///
398    /// ### 3. Full JSON Schema
399    /// ```json
400    /// {
401    ///   "type": "object",
402    ///   "properties": {
403    ///     "name": {"type": "string"}
404    ///   },
405    ///   "required": ["name"]
406    /// }
407    /// ```
408    /// Already valid JSON Schema - passed through as-is.
409    ///
410    /// ## Handler Requirements
411    ///
412    /// The handler must satisfy several trait bounds:
413    ///
414    /// - `Fn(Value) -> Fut`: Takes JSON arguments, returns a future
415    /// - `Send + Sync`: Can be shared across threads safely
416    /// - `'static`: No non-static references (must own all data)
417    /// - `Fut: Future<Output = Result<Value>> + Send`: Future is sendable and produces Result
418    ///
419    /// The constructor automatically wraps the handler in `Arc<...>` and boxes the futures,
420    /// so you don't need to do this manually.
421    ///
422    /// ## Generic Parameters
423    ///
424    /// - `F`: The handler function type
425    /// - `Fut`: The future type returned by the handler
426    ///
427    /// These are inferred automatically from the handler you provide.
428    ///
429    /// # Examples
430    ///
431    /// ## Simple Calculator Tool
432    ///
433    /// ```rust,no_run
434    /// use open_agent::Tool;
435    /// use serde_json::json;
436    ///
437    /// let add_tool = Tool::new(
438    ///     "add",
439    ///     "Add two numbers together",
440    ///     json!({
441    ///         "a": "number",
442    ///         "b": "number"
443    ///     }),
444    ///     |args| {
445    ///         Box::pin(async move {
446    ///             let a = args.get("a")
447    ///                 .and_then(|v| v.as_f64())
448    ///                 .ok_or_else(|| open_agent::Error::invalid_input("Parameter 'a' must be a number"))?;
449    ///             let b = args.get("b")
450    ///                 .and_then(|v| v.as_f64())
451    ///                 .ok_or_else(|| open_agent::Error::invalid_input("Parameter 'b' must be a number"))?;
452    ///             Ok(json!({"result": a + b}))
453    ///         })
454    ///     }
455    /// );
456    /// ```
457    ///
458    /// ## Tool with Optional Parameters
459    ///
460    /// ```rust,no_run
461    /// use open_agent::Tool;
462    /// use serde_json::json;
463    ///
464    /// let search_tool = Tool::new(
465    ///     "search",
466    ///     "Search for information",
467    ///     json!({
468    ///         "query": {
469    ///             "type": "string",
470    ///             "description": "What to search for"
471    ///         },
472    ///         "max_results": {
473    ///             "type": "integer",
474    ///             "description": "Maximum results to return",
475    ///             "optional": true,
476    ///             "default": 10
477    ///         }
478    ///     }),
479    ///     |args| Box::pin(async move {
480    ///         let query = args["query"].as_str().unwrap_or("");
481    ///         let max = args.get("max_results")
482    ///             .and_then(|v| v.as_i64())
483    ///             .unwrap_or(10);
484    ///
485    ///         // Perform search...
486    ///         Ok(json!({"results": [], "query": query, "limit": max}))
487    ///     })
488    /// );
489    /// ```
490    ///
491    /// ## Tool with External State
492    ///
493    /// ```rust,no_run
494    /// use open_agent::Tool;
495    /// use serde_json::json;
496    /// use std::sync::Arc;
497    ///
498    /// // State that needs to be shared
499    /// let api_key = Arc::new("secret-key".to_string());
500    ///
501    /// let tool = Tool::new(
502    ///     "api_call",
503    ///     "Make an API call",
504    ///     json!({"endpoint": "string"}),
505    ///     move |args| {
506    ///         // Clone Arc to move into async block
507    ///         let api_key = api_key.clone();
508    ///         Box::pin(async move {
509    ///             let endpoint = args["endpoint"].as_str().unwrap_or("");
510    ///             // Use api_key in async operation
511    ///             println!("Calling {} with key {}", endpoint, api_key);
512    ///             Ok(json!({"status": "success"}))
513    ///         })
514    ///     }
515    /// );
516    /// ```
517    pub fn new<F, Fut>(
518        name: impl Into<String>,
519        description: impl Into<String>,
520        input_schema: Value,
521        handler: F,
522    ) -> Self
523    where
524        F: Fn(Value) -> Fut + Send + Sync + 'static,
525        Fut: Future<Output = Result<Value>> + Send + 'static,
526    {
527        // Convert inputs to owned types
528        let name = name.into();
529        let description = description.into();
530
531        // Convert the provided schema to OpenAI's expected JSON Schema format
532        // This handles simple type notation, extended schemas, and full JSON Schema
533        let input_schema = convert_schema_to_openai(input_schema);
534
535        Self {
536            name,
537            description,
538            input_schema,
539            // Wrap the handler in Arc for cheap cloning and thread-safe sharing
540            // Box::pin converts the future to a pinned, heap-allocated trait object
541            handler: Arc::new(move |args| Box::pin(handler(args))),
542        }
543    }
544
545    /// Execute the tool with the provided arguments.
546    ///
547    /// This method invokes the tool's handler asynchronously, passing the arguments
548    /// and awaiting the result. It's the primary way to run a tool's logic.
549    ///
550    /// ## Execution Flow
551    ///
552    /// 1. Call the handler function (stored in `Arc`) with arguments
553    /// 2. The handler returns a `Pin<Box<dyn Future>>`
554    /// 3. Await the future to get the `Result<Value>`
555    /// 4. Return the result (success value or error)
556    ///
557    /// ## Arguments
558    ///
559    /// Arguments should be a JSON object matching the tool's `input_schema`:
560    /// ```json
561    /// {
562    ///   "param1": "value1",
563    ///   "param2": 42
564    /// }
565    /// ```
566    ///
567    /// The handler is responsible for extracting and validating these arguments.
568    ///
569    /// ## Error Handling
570    ///
571    /// If the handler returns an error, it's propagated directly. The agent
572    /// calling this method should handle errors appropriately (e.g., retry logic,
573    /// error reporting to the LLM).
574    ///
575    /// # Examples
576    ///
577    /// ```rust,no_run
578    /// # use open_agent::Tool;
579    /// # use serde_json::json;
580    /// # async fn example() -> open_agent::Result<()> {
581    /// let calculator = Tool::new(
582    ///     "add",
583    ///     "Add numbers",
584    ///     json!({"a": "number", "b": "number"}),
585    ///     |args| Box::pin(async move {
586    ///         let sum = args["a"].as_f64().unwrap() + args["b"].as_f64().unwrap();
587    ///         Ok(json!({"result": sum}))
588    ///     })
589    /// );
590    ///
591    /// // Execute the tool
592    /// let result = calculator.execute(json!({"a": 5.0, "b": 3.0})).await?;
593    /// assert_eq!(result["result"], 8.0);
594    /// # Ok(())
595    /// # }
596    /// ```
597    pub async fn execute(&self, arguments: Value) -> Result<Value> {
598        // Invoke the handler function with the arguments
599        // The handler returns Pin<Box<dyn Future>>, which we immediately await
600        (self.handler)(arguments).await
601    }
602
603    /// Convert the tool definition to OpenAI's function calling format.
604    ///
605    /// This method generates the JSON structure expected by OpenAI's Chat Completion
606    /// API when using function calling. The format is also compatible with other
607    /// LLM providers that follow OpenAI's conventions.
608    ///
609    /// ## Output Format
610    ///
611    /// Returns a JSON structure like:
612    /// ```json
613    /// {
614    ///   "type": "function",
615    ///   "function": {
616    ///     "name": "tool_name",
617    ///     "description": "Tool description",
618    ///     "parameters": {
619    ///       "type": "object",
620    ///       "properties": { ... },
621    ///       "required": [ ... ]
622    ///     }
623    ///   }
624    /// }
625    /// ```
626    ///
627    /// ## Usage in API Calls
628    ///
629    /// This format is typically used when constructing the `tools` array for
630    /// API requests:
631    /// ```json
632    /// {
633    ///   "model": "gpt-4",
634    ///   "messages": [...],
635    ///   "tools": [
636    ///     // Output of to_openai_format() for each tool
637    ///   ]
638    /// }
639    /// ```
640    ///
641    /// # Examples
642    ///
643    /// ```rust,no_run
644    /// # use open_agent::tool;
645    /// # use serde_json::json;
646    /// let my_tool = tool("search", "Search for information")
647    ///     .param("query", "string")
648    ///     .build(|_| async { Ok(json!({})) });
649    ///
650    /// let openai_format = my_tool.to_openai_format();
651    ///
652    /// // Verify the structure
653    /// assert_eq!(openai_format["type"], "function");
654    /// assert_eq!(openai_format["function"]["name"], "search");
655    /// assert_eq!(openai_format["function"]["description"], "Search for information");
656    /// assert!(openai_format["function"]["parameters"].is_object());
657    /// ```
658    pub fn to_openai_format(&self) -> Value {
659        serde_json::json!({
660            "type": "function",
661            "function": {
662                "name": self.name,
663                "description": self.description,
664                "parameters": self.input_schema
665            }
666        })
667    }
668
669    /// Returns the tool's name.
670    pub fn name(&self) -> &str {
671        &self.name
672    }
673
674    /// Returns the tool's description.
675    pub fn description(&self) -> &str {
676        &self.description
677    }
678
679    /// Returns a reference to the tool's input schema.
680    pub fn input_schema(&self) -> &Value {
681        &self.input_schema
682    }
683}
684
685/// Custom Debug implementation for Tool.
686///
687/// The handler field is omitted from debug output because:
688/// - Function pointers/closures don't have meaningful debug representations
689/// - The `Arc<dyn Fn...>` type is complex and not useful to display
690/// - Showing the handler would just print something like "Arc { ... }"
691///
692/// Only the metadata fields (name, description, input_schema) are shown,
693/// which are the most useful for debugging tool definitions.
694impl std::fmt::Debug for Tool {
695    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696        f.debug_struct("Tool")
697            .field("name", &self.name)
698            .field("description", &self.description)
699            .field("input_schema", &self.input_schema)
700            // Handler is intentionally omitted - it's not debuggable
701            .finish()
702    }
703}
704
705/// Convert various schema formats to OpenAI's JSON Schema format.
706///
707/// This function is the core of the schema conversion system. It accepts multiple
708/// input formats and normalizes them to the standard JSON Schema structure expected
709/// by OpenAI's function calling API.
710///
711/// ## Conversion Logic
712///
713/// ### 1. Full JSON Schema (Pass-through)
714/// If the input already has `"type": "object"` and `"properties"`, it's assumed to
715/// be a complete JSON Schema and returned as-is:
716/// ```json
717/// {
718///   "type": "object",
719///   "properties": { "name": {"type": "string"} },
720///   "required": ["name"]
721/// }
722/// // → Returned unchanged
723/// ```
724///
725/// ### 2. Simple Type Notation
726/// A flat object with type strings is expanded to full JSON Schema:
727/// ```json
728/// {"location": "string", "temperature": "number"}
729/// // → Converts to:
730/// {
731///   "type": "object",
732///   "properties": {
733///     "location": {"type": "string"},
734///     "temperature": {"type": "number"}
735///   },
736///   "required": ["location", "temperature"]
737/// }
738/// ```
739/// All parameters become required by default.
740///
741/// ### 3. Extended Property Schema
742/// Object values with additional metadata (description, optional, etc.):
743/// ```json
744/// {
745///   "query": {
746///     "type": "string",
747///     "description": "Search query"
748///   },
749///   "limit": {
750///     "type": "integer",
751///     "optional": true
752///   }
753/// }
754/// // → Converts to JSON Schema with "query" required, "limit" optional
755/// ```
756///
757/// ## Required vs Optional Parameters
758///
759/// The function determines if a parameter is required using this logic:
760/// 1. If `"required": true` is explicitly set → required
761/// 2. If `"required": false` is explicitly set → optional
762/// 3. If `"optional": true` is set → optional
763/// 4. If parameter has a `"default"` value → optional
764/// 5. Otherwise → required (default behavior)
765///
766/// The `"optional"` and `"required"` keys are removed from the final schema
767/// as they're not part of standard JSON Schema (the `required` array is used instead).
768///
769/// ## Type Mapping
770///
771/// Simple type strings are converted via [`type_to_json_schema`]:
772/// - `"string"`, `"str"` → `{"type": "string"}`
773/// - `"number"`, `"float"`, `"f32"`, `"f64"` → `{"type": "number"}`
774/// - `"integer"`, `"int"`, `"i32"`, `"i64"` → `{"type": "integer"}`
775/// - `"boolean"`, `"bool"` → `{"type": "boolean"}`
776/// - `"array"`, `"list"`, `"vec"` → `{"type": "array"}`
777/// - `"object"`, `"dict"`, `"map"` → `{"type": "object"}`
778///
779/// ## Examples
780///
781/// See the test cases in this module for concrete examples of each conversion path.
782fn convert_schema_to_openai(schema: Value) -> Value {
783    // Check if the input is already a complete JSON Schema
784    // A complete schema has both "type": "object" and a "properties" field
785    if schema.is_object() {
786        let obj = schema
787            .as_object()
788            .expect("BUG: is_object() returned true but as_object() returned None");
789        if obj.contains_key("type") && obj.contains_key("properties") {
790            // This is already a full JSON Schema - pass it through unchanged
791            return schema;
792        }
793
794        // If we get here, we need to convert to full JSON Schema format
795        // Initialize the properties map and required array
796        let mut properties = serde_json::Map::new();
797        let mut required = Vec::new();
798
799        // Iterate through each parameter in the input schema
800        for (param_name, param_type) in obj {
801            if let Some(type_str) = param_type.as_str() {
802                // Case 1: Simple type notation like "string", "number", etc.
803                // Convert the type string to a proper JSON Schema type object
804                properties.insert(param_name.clone(), type_to_json_schema(type_str));
805
806                // Simple notation always means required (no way to specify optional)
807                required.push(param_name.clone());
808            } else if param_type.is_object() {
809                // Case 2: Extended property schema with metadata
810                // Clone the property schema so we can modify it
811                let mut prop = param_type.clone();
812                let prop_obj = prop
813                    .as_object_mut()
814                    .expect("BUG: is_object() returned true but as_object_mut() returned None");
815
816                // Extract and remove the "optional" flag (not standard JSON Schema)
817                let is_optional = prop_obj
818                    .remove("optional")
819                    .and_then(|v| v.as_bool())
820                    .unwrap_or(false);
821
822                // Extract and remove the "required" flag (not standard JSON Schema)
823                // This is different from the "required" array - it's per-property
824                let is_required = prop_obj.remove("required").and_then(|v| v.as_bool());
825
826                // Check if the property has a default value
827                // Properties with defaults are typically optional
828                let has_default = prop_obj.contains_key("default");
829
830                // Add the cleaned property schema to the properties map
831                properties.insert(param_name.clone(), prop);
832
833                // Determine if this parameter should be in the required array
834                // Priority order:
835                // 1. Explicit required: true → add to required
836                // 2. Explicit optional: true OR required: false → don't add
837                // 3. Has default value → don't add (defaults make params optional)
838                // 4. Otherwise → add to required (conservative default)
839                if let Some(true) = is_required {
840                    required.push(param_name.clone());
841                } else if is_optional || is_required == Some(false) {
842                    // Explicitly optional - don't add to required array
843                } else if !has_default {
844                    // No explicit optionality and no default → required
845                    required.push(param_name.clone());
846                }
847                // Note: if has_default is true and no explicit required/optional,
848                // we don't add to required (defaults imply optional)
849            }
850        }
851
852        // Build and return the complete JSON Schema object
853        return serde_json::json!({
854            "type": "object",
855            "properties": properties,
856            "required": required
857        });
858    }
859
860    // Fallback case: input is not an object (unexpected but handled gracefully)
861    // Return an empty object schema that accepts any properties
862    serde_json::json!({
863        "type": "object",
864        "properties": {},
865        "required": []
866    })
867}
868
869/// Convert a type string to a JSON Schema type object.
870///
871/// This function maps friendly, Rust-like type names to their JSON Schema equivalents.
872/// It's designed to accept common variations developers might use, making tool
873/// definition more intuitive.
874///
875/// ## Type Mappings
876///
877/// | Input Types | JSON Schema Type | Use Case |
878/// |-------------|------------------|----------|
879/// | `"string"`, `"str"` | `"string"` | Text data |
880/// | `"number"`, `"float"`, `"f32"`, `"f64"` | `"number"` | Floating point numbers |
881/// | `"integer"`, `"int"`, `"i32"`, `"i64"`, `"u32"`, `"u64"` | `"integer"` | Whole numbers |
882/// | `"boolean"`, `"bool"` | `"boolean"` | True/false values |
883/// | `"array"`, `"list"`, `"vec"` | `"array"` | Lists/arrays |
884/// | `"object"`, `"dict"`, `"map"` | `"object"` | Nested objects/maps |
885/// | anything else | `"string"` | Default fallback |
886///
887/// ## Design Rationale
888///
889/// The function accepts multiple aliases for each type to accommodate different
890/// naming conventions:
891/// - Standard JSON Schema names (`"string"`, `"integer"`, `"boolean"`)
892/// - Common programming abbreviations (`"str"`, `"int"`, `"bool"`)
893/// - Rust-specific types (`"i32"`, `"f64"`, `"vec"`)
894/// - Python-style names (`"dict"`, `"list"`)
895///
896/// ## Default Behavior
897///
898/// Unknown type strings default to `"string"` rather than causing an error.
899/// This prevents tool creation from failing due to typos, though it may lead
900/// to unexpected schema behavior. Consider validating type strings at a higher
901/// level if strict type checking is needed.
902///
903/// ## Output Format
904///
905/// Always returns a JSON object with a single `"type"` field:
906/// ```json
907/// {"type": "string"}
908/// {"type": "number"}
909/// {"type": "integer"}
910/// // etc.
911/// ```
912///
913/// ## Examples
914///
915/// ```rust
916/// # use serde_json::json;
917/// # fn type_to_json_schema(type_str: &str) -> serde_json::Value {
918/// #     let json_type = match type_str {
919/// #         "string" | "str" => "string",
920/// #         "integer" | "int" | "i32" | "i64" | "u32" | "u64" => "integer",
921/// #         "number" | "float" | "f32" | "f64" => "number",
922/// #         "boolean" | "bool" => "boolean",
923/// #         "array" | "list" | "vec" => "array",
924/// #         "object" | "dict" | "map" => "object",
925/// #         _ => "string",
926/// #     };
927/// #     json!({ "type": json_type })
928/// # }
929/// assert_eq!(type_to_json_schema("string"), json!({"type": "string"}));
930/// assert_eq!(type_to_json_schema("i64"), json!({"type": "integer"}));
931/// assert_eq!(type_to_json_schema("f32"), json!({"type": "number"}));
932/// assert_eq!(type_to_json_schema("bool"), json!({"type": "boolean"}));
933/// assert_eq!(type_to_json_schema("vec"), json!({"type": "array"}));
934/// assert_eq!(type_to_json_schema("unknown"), json!({"type": "string"})); // fallback
935/// ```
936fn type_to_json_schema(type_str: &str) -> Value {
937    // Match against known type strings (case-sensitive)
938    // The match is designed to be comprehensive but not exhaustive
939    let json_type = match type_str {
940        // String types
941        "string" | "str" => "string",
942
943        // Integer types (various Rust integer types accepted)
944        "integer" | "int" | "i32" | "i64" | "u32" | "u64" => "integer",
945
946        // Floating point types
947        "number" | "float" | "f32" | "f64" => "number",
948
949        // Boolean types
950        "boolean" | "bool" => "boolean",
951
952        // Array/list types
953        "array" | "list" | "vec" => "array",
954
955        // Object/map types
956        "object" | "dict" | "map" => "object",
957
958        // Unknown type - default to string for safety
959        // This prevents errors but may hide typos
960        _ => "string",
961    };
962
963    // Return a JSON Schema type object
964    serde_json::json!({ "type": json_type })
965}
966
967/// Builder for creating tools with a fluent API.
968///
969/// The `ToolBuilder` provides a convenient, readable way to construct tools
970/// using method chaining. It's especially useful when building tools incrementally
971/// or when the schema structure is determined dynamically.
972///
973/// ## Builder Pattern Benefits
974///
975/// - **Readability**: Method chains read like natural language
976/// - **Flexibility**: Add parameters conditionally
977/// - **Type safety**: Catches errors at compile time
978/// - **Discoverability**: IDE autocomplete shows available options
979///
980/// ## Workflow
981///
982/// 1. Create builder with [`tool()`] or [`ToolBuilder::new()`]
983/// 2. Add parameters with [`.param()`](ToolBuilder::param)
984/// 3. Optionally set schema with [`.schema()`](ToolBuilder::schema)
985/// 4. Finalize with [`.build()`](ToolBuilder::build) and provide handler
986///
987/// ## Examples
988///
989/// See the [`tool()`] function for detailed examples.
990///
991/// ## Note on Schema Mutation
992///
993/// If you call `.schema()` after `.param()`, the parameters will be replaced
994/// by the new schema. Similarly, calling `.param()` after `.schema()` will
995/// reset a non-object schema to an empty object before adding the parameter.
996/// Generally, use either `.schema()` or `.param()`, not both.
997pub struct ToolBuilder {
998    /// The tool's unique identifier
999    name: String,
1000
1001    /// Human-readable description of the tool's purpose
1002    description: String,
1003
1004    /// The input schema, built up through .param() calls or set via .schema()
1005    schema: Value,
1006}
1007
1008impl ToolBuilder {
1009    /// Start building a new tool with a name and description.
1010    ///
1011    /// This creates a builder with an empty schema. You can then add parameters
1012    /// using [`.param()`](ToolBuilder::param) or set a complete schema with
1013    /// [`.schema()`](ToolBuilder::schema).
1014    ///
1015    /// ## Parameters
1016    ///
1017    /// - `name`: Tool identifier (converted to String via Into trait)
1018    /// - `description`: Human-readable explanation of what the tool does
1019    ///
1020    /// ## Examples
1021    ///
1022    /// ```rust
1023    /// # use open_agent::ToolBuilder;
1024    /// let builder = ToolBuilder::new("search", "Search for information");
1025    /// // builder.param(...).build(...)
1026    /// ```
1027    ///
1028    /// Typically you'll use the [`tool()`] convenience function instead of calling
1029    /// this directly.
1030    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
1031        Self {
1032            name: name.into(),
1033            description: description.into(),
1034            // Start with an empty object schema
1035            schema: serde_json::json!({}),
1036        }
1037    }
1038
1039    /// Set the complete input schema.
1040    ///
1041    /// This replaces any schema or parameters set previously. Use this when you
1042    /// have a pre-built schema object (especially useful for complex schemas
1043    /// with nested structures).
1044    ///
1045    /// ## Schema Format
1046    ///
1047    /// Accepts any of the formats supported by [`Tool::new`]:
1048    /// - Simple type notation: `{"param": "string"}`
1049    /// - Extended schema: `{"param": {"type": "string", "description": "..."}}`
1050    /// - Full JSON Schema: `{"type": "object", "properties": {...}, "required": [...]}`
1051    ///
1052    /// ## Warning
1053    ///
1054    /// This overwrites any parameters added via `.param()`. Generally, choose
1055    /// one approach: either use `.param()` for simple cases or `.schema()` for
1056    /// complex cases, but not both.
1057    ///
1058    /// ## Examples
1059    ///
1060    /// ```rust
1061    /// # use open_agent::tool;
1062    /// # use serde_json::json;
1063    /// let my_tool = tool("api_call", "Make an API call")
1064    ///     .schema(json!({
1065    ///         "endpoint": {
1066    ///             "type": "string",
1067    ///             "description": "API endpoint URL",
1068    ///             "pattern": "^https://"
1069    ///         },
1070    ///         "method": {
1071    ///             "type": "string",
1072    ///             "enum": ["GET", "POST", "PUT", "DELETE"]
1073    ///         }
1074    ///     }))
1075    ///     .build(|_| async { Ok(json!({})) });
1076    /// ```
1077    pub fn schema(mut self, schema: Value) -> Self {
1078        // Replace the current schema entirely
1079        self.schema = schema;
1080        self
1081    }
1082
1083    /// Add a single parameter to the schema.
1084    ///
1085    /// This is a convenience method for building schemas incrementally. Each call
1086    /// adds one parameter with a simple type string.
1087    ///
1088    /// ## Parameters
1089    ///
1090    /// - `name`: Parameter name (will be required in tool calls)
1091    /// - `type_str`: Type string like "string", "number", "boolean", etc.
1092    ///   Supported types: "string", "number", "integer", "boolean", "array", "object".
1093    ///
1094    /// ## Behavior
1095    ///
1096    /// - If the current schema is not an object (e.g., you called `.schema()` with
1097    ///   a non-object value), it will be reset to an empty object first.
1098    /// - All parameters added via `.param()` are marked as required.
1099    /// - For optional parameters, use `.schema()` with extended property format.
1100    ///
1101    /// ## Method Chaining
1102    ///
1103    /// This method consumes `self` and returns it, enabling method chaining:
1104    /// ```rust
1105    /// # use open_agent::tool;
1106    /// # use serde_json::json;
1107    /// let my_tool = tool("calculate", "Perform calculation")
1108    ///     .param("operation", "string")
1109    ///     .param("x", "number")
1110    ///     .param("y", "number")
1111    ///     .build(|_| async { Ok(json!({})) });
1112    /// ```
1113    ///
1114    /// ## Examples
1115    ///
1116    /// ```rust
1117    /// # use open_agent::tool;
1118    /// # use serde_json::json;
1119    /// // Add multiple parameters
1120    /// let weather_tool = tool("get_weather", "Get weather for a location")
1121    ///     .param("location", "string")
1122    ///     .param("units", "string")
1123    ///     .build(|args| async move {
1124    ///         // Implementation
1125    ///         Ok(json!({"temp": 72}))
1126    ///     });
1127    /// ```
1128    pub fn param(mut self, name: &str, type_str: &str) -> Self {
1129        // Ensure schema is an object, reset if not
1130        // This handles the edge case where .schema() was called with a non-object
1131        if !self.schema.is_object() {
1132            self.schema = serde_json::json!({});
1133        }
1134
1135        // Get mutable reference to the object. This should always succeed because we just
1136        // ensured it's an object above, but we use expect() for defensive programming.
1137        let obj = self
1138            .schema
1139            .as_object_mut()
1140            .expect("BUG: schema should be an object after initialization");
1141
1142        // Insert the parameter as a simple type string
1143        // This will be converted to proper JSON Schema by convert_schema_to_openai
1144        obj.insert(name.to_string(), Value::String(type_str.to_string()));
1145
1146        self
1147    }
1148
1149    /// Build the final Tool with a handler function.
1150    ///
1151    /// This consumes the builder and produces a [`Tool`] ready for use. The handler
1152    /// function defines what happens when the tool is called.
1153    ///
1154    /// ## Handler Requirements
1155    ///
1156    /// The handler must be:
1157    /// - An async function or closure
1158    /// - Accept a single `Value` argument (the tool's input parameters)
1159    /// - Return a `Future<Output = Result<Value>>`
1160    /// - Implement `Send + Sync + 'static` for thread safety
1161    ///
1162    /// ## Generic Parameters
1163    ///
1164    /// - `F`: The handler function type (inferred from the closure/function you provide)
1165    /// - `Fut`: The future type returned by the handler (inferred automatically)
1166    ///
1167    /// ## Examples
1168    ///
1169    /// ### Simple Handler
1170    /// ```rust
1171    /// # use open_agent::tool;
1172    /// # use serde_json::json;
1173    /// let my_tool = tool("echo", "Echo back the input")
1174    ///     .param("message", "string")
1175    ///     .build(|args| async move {
1176    ///         Ok(args) // Echo arguments back
1177    ///     });
1178    /// ```
1179    ///
1180    /// ### Handler with External State
1181    /// ```rust
1182    /// # use open_agent::tool;
1183    /// # use serde_json::json;
1184    /// # use std::sync::Arc;
1185    /// let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
1186    ///
1187    /// let my_tool = tool("increment", "Increment a counter")
1188    ///     .build(move |_args| {
1189    ///         let counter = counter.clone();
1190    ///         async move {
1191    ///             let val = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1192    ///             Ok(json!({"count": val + 1}))
1193    ///         }
1194    ///     });
1195    /// ```
1196    ///
1197    /// ### Handler with Error Handling
1198    /// ```rust
1199    /// # use open_agent::{tool, Error};
1200    /// # use serde_json::json;
1201    /// let my_tool = tool("divide", "Divide two numbers")
1202    ///     .param("a", "number")
1203    ///     .param("b", "number")
1204    ///     .build(|args| async move {
1205    ///         let a = args["a"].as_f64().ok_or_else(|| Error::tool("Invalid 'a' parameter"))?;
1206    ///         let b = args["b"].as_f64().ok_or_else(|| Error::tool("Invalid 'b' parameter"))?;
1207    ///
1208    ///         if b == 0.0 {
1209    ///             return Err(Error::tool("Division by zero"));
1210    ///         }
1211    ///
1212    ///         Ok(json!({"result": a / b}))
1213    ///     });
1214    /// ```
1215    pub fn build<F, Fut>(self, handler: F) -> Tool
1216    where
1217        F: Fn(Value) -> Fut + Send + Sync + 'static,
1218        Fut: Future<Output = Result<Value>> + Send + 'static,
1219    {
1220        // Delegate to Tool::new which handles schema conversion and handler wrapping
1221        Tool::new(self.name, self.description, self.schema, handler)
1222    }
1223}
1224
1225/// Create a tool using the builder pattern (convenience function).
1226///
1227/// This is the recommended way to create tools. It returns a [`ToolBuilder`] that
1228/// allows you to fluently configure the tool's schema and handler.
1229///
1230/// ## Typical Usage Pattern
1231///
1232/// ```text
1233/// tool(name, description)
1234///     .param(name, type)  // Add parameters (optional, can repeat)
1235///     .build(handler)     // Provide handler and create Tool
1236/// ```
1237///
1238/// ## Why Use This Instead of Tool::new?
1239///
1240/// - **More readable**: The builder pattern reads like natural language
1241/// - **Incremental schema building**: Add parameters one at a time
1242/// - **Flexible**: Can conditionally add parameters or use `.schema()` for complex cases
1243/// - **Type-safe**: Method chaining ensures you can't forget the handler
1244///
1245/// ## Parameters
1246///
1247/// - `name`: Unique identifier for the tool (snake_case recommended)
1248/// - `description`: Human-readable explanation of what the tool does
1249///
1250/// Both parameters accept any type that implements `Into<String>`, so you can
1251/// pass string literals, `String` values, or anything else convertible to String.
1252///
1253/// ## Examples
1254///
1255/// ### Basic Calculator Tool
1256///
1257/// ```rust,no_run
1258/// use open_agent::tool;
1259/// use serde_json::json;
1260///
1261/// let add_tool = tool("add", "Add two numbers")
1262///     .param("a", "number")
1263///     .param("b", "number")
1264///     .build(|args| async move {
1265///         let a = args.get("a")
1266///             .and_then(|v| v.as_f64())
1267///             .ok_or_else(|| open_agent::Error::invalid_input("Parameter 'a' must be a number"))?;
1268///         let b = args.get("b")
1269///             .and_then(|v| v.as_f64())
1270///             .ok_or_else(|| open_agent::Error::invalid_input("Parameter 'b' must be a number"))?;
1271///         Ok(json!({"result": a + b}))
1272///     });
1273/// ```
1274///
1275/// ### Tool with External HTTP Client
1276///
1277/// ```rust,no_run
1278/// use open_agent::{tool, Error};
1279/// use serde_json::json;
1280/// # use std::sync::Arc;
1281///
1282/// // Shared HTTP client (example - use your actual HTTP client)
1283/// # struct HttpClient;
1284/// # impl HttpClient {
1285/// #     fn new() -> Self { HttpClient }
1286/// #     async fn get(&self, url: &str) -> Result<String, Box<dyn std::error::Error>> {
1287/// #         Ok("response".to_string())
1288/// #     }
1289/// # }
1290/// let http_client = Arc::new(HttpClient::new());
1291///
1292/// let fetch_tool = tool("fetch_url", "Fetch content from a URL")
1293///     .param("url", "string")
1294///     .build(move |args| {
1295///         let client = http_client.clone();
1296///         async move {
1297///             let url = args["url"].as_str().unwrap_or("");
1298///             let content = client.get(url).await
1299///                 .map_err(|e| Error::tool(format!("Failed to fetch: {}", e)))?;
1300///             Ok(json!({"content": content}))
1301///         }
1302///     });
1303/// ```
1304///
1305/// ### Tool with Complex Schema
1306///
1307/// ```rust,no_run
1308/// use open_agent::tool;
1309/// use serde_json::json;
1310///
1311/// let search_tool = tool("search", "Search for information")
1312///     .schema(json!({
1313///         "query": {
1314///             "type": "string",
1315///             "description": "Search query"
1316///         },
1317///         "filters": {
1318///             "type": "object",
1319///             "description": "Optional filters",
1320///             "optional": true,
1321///             "properties": {
1322///                 "date_from": {"type": "string"},
1323///                 "date_to": {"type": "string"}
1324///             }
1325///         },
1326///         "max_results": {
1327///             "type": "integer",
1328///             "default": 10,
1329///             "optional": true
1330///         }
1331///     }))
1332///     .build(|args| async move {
1333///         // Implementation
1334///         Ok(json!({"results": []}))
1335///     });
1336/// ```
1337///
1338/// ### Conditional Parameter Addition
1339///
1340/// ```rust,no_run
1341/// use open_agent::tool;
1342/// use serde_json::json;
1343///
1344/// # let enable_advanced = true;
1345/// let mut builder = tool("process", "Process data")
1346///     .param("input", "string");
1347///
1348/// // Conditionally add parameters
1349/// if enable_advanced {
1350///     builder = builder.param("advanced_mode", "boolean");
1351/// }
1352///
1353/// let my_tool = builder.build(|args| async move {
1354///     Ok(json!({"status": "processed"}))
1355/// });
1356/// ```
1357///
1358/// ### Integration with Agent
1359///
1360/// ```rust,no_run
1361/// use open_agent::{Client, AgentOptions, tool};
1362/// use serde_json::json;
1363///
1364/// # async fn example() -> open_agent::Result<()> {
1365/// let weather_tool = tool("get_weather", "Get weather for a location")
1366///     .param("location", "string")
1367///     .build(|args| async move {
1368///         Ok(json!({"temp": 72, "conditions": "sunny"}))
1369///     });
1370///
1371/// let options = AgentOptions::builder()
1372///     .model("gpt-4")
1373///     .base_url("http://localhost:1234/v1")
1374///     .tool(weather_tool)
1375///     .build()?;
1376///
1377/// let client = Client::new(options)?;
1378/// // Client can now use the tool when responding to queries
1379/// # Ok(())
1380/// # }
1381/// ```
1382///
1383/// ## See Also
1384///
1385/// - [`Tool::new`] - Direct constructor if you prefer not using the builder
1386/// - [`ToolBuilder`] - The builder type returned by this function
1387/// - [`Tool`] - The final tool type produced by `.build()`
1388pub fn tool(name: impl Into<String>, description: impl Into<String>) -> ToolBuilder {
1389    ToolBuilder::new(name, description)
1390}
1391
1392#[cfg(test)]
1393mod tests {
1394    use super::*;
1395    use crate::Error;
1396    use serde_json::json;
1397
1398    #[test]
1399    fn test_type_to_json_schema() {
1400        assert_eq!(type_to_json_schema("string"), json!({"type": "string"}));
1401        assert_eq!(type_to_json_schema("integer"), json!({"type": "integer"}));
1402        assert_eq!(type_to_json_schema("number"), json!({"type": "number"}));
1403        assert_eq!(type_to_json_schema("bool"), json!({"type": "boolean"}));
1404    }
1405
1406    #[test]
1407    fn test_convert_simple_schema() {
1408        let schema = json!({
1409            "location": "string",
1410            "units": "string"
1411        });
1412
1413        let result = convert_schema_to_openai(schema);
1414
1415        assert_eq!(result["type"], "object");
1416        assert_eq!(result["properties"]["location"]["type"], "string");
1417        assert_eq!(result["properties"]["units"]["type"], "string");
1418        assert_eq!(result["required"], json!(["location", "units"]));
1419    }
1420
1421    #[test]
1422    fn test_convert_full_schema() {
1423        let schema = json!({
1424            "type": "object",
1425            "properties": {
1426                "name": {"type": "string"}
1427            },
1428            "required": ["name"]
1429        });
1430
1431        let result = convert_schema_to_openai(schema.clone());
1432        assert_eq!(result, schema);
1433    }
1434
1435    #[tokio::test]
1436    async fn test_tool_creation() {
1437        let add_tool = tool("add", "Add two numbers")
1438            .param("a", "number")
1439            .param("b", "number")
1440            .build(|args| async move {
1441                let a = args
1442                    .get("a")
1443                    .and_then(|v| v.as_f64())
1444                    .ok_or_else(|| Error::invalid_input("Parameter 'a' must be a number"))?;
1445                let b = args
1446                    .get("b")
1447                    .and_then(|v| v.as_f64())
1448                    .ok_or_else(|| Error::invalid_input("Parameter 'b' must be a number"))?;
1449                Ok(json!({"result": a + b}))
1450            });
1451
1452        assert_eq!(add_tool.name, "add");
1453        assert_eq!(add_tool.description, "Add two numbers");
1454
1455        let result = add_tool.execute(json!({"a": 5.0, "b": 3.0})).await.unwrap();
1456        assert_eq!(result["result"], 8.0);
1457    }
1458
1459    #[test]
1460    fn test_tool_to_openai_format() {
1461        let tool = tool("test", "Test tool")
1462            .param("param1", "string")
1463            .build(|_| async { Ok(json!({})) });
1464
1465        let format = tool.to_openai_format();
1466
1467        assert_eq!(format["type"], "function");
1468        assert_eq!(format["function"]["name"], "test");
1469        assert_eq!(format["function"]["description"], "Test tool");
1470        assert!(format["function"]["parameters"].is_object());
1471    }
1472
1473    #[test]
1474    fn test_param_after_non_object_schema() {
1475        // Edge case: calling .param() after setting schema to non-object
1476        // Should reset schema and add param without panicking
1477        let tool = tool("test", "Test tool")
1478            .schema(json!("string")) // Set to non-object
1479            .param("key", "number") // Should reset schema to {} and add param
1480            .build(|_| async { Ok(json!({})) });
1481
1482        let format = tool.to_openai_format();
1483
1484        // Verify it worked - schema should be object with the param
1485        assert!(format["function"]["parameters"].is_object());
1486        assert!(format["function"]["parameters"]["properties"]["key"].is_object());
1487    }
1488}