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}