api_xai/
enhanced_tools.rs

1mod private
2{
3  //! Enhanced function calling utilities for parallel and sequential tool execution.
4  //!
5  //! This module provides higher-level abstractions over the raw function calling
6  //! API to simplify common patterns like executing multiple tools in parallel.
7  //!
8  //! # Design Decisions
9  //!
10  //! ## Why Two Separate Functions?
11  //!
12  //! We provide both `execute_tool_calls_parallel()` and `execute_tool_calls_sequential()`
13  //! as separate functions instead of a single function with a mode parameter because:
14  //!
15  //! 1. **Type Safety**: Parallel execution requires `Send + 'static` bounds that
16  //!    sequential execution doesn't need. Separate functions allow precise bounds.
17  //!
18  //! 2. **API Clarity**: The choice between parallel and sequential is fundamental
19  //!    to the application's correctness (dependent tools must be sequential).
20  //!    Making this explicit in the function name prevents accidental misuse.
21  //!
22  //! 3. **Performance Contract**: Parallel execution provides better performance but
23  //!    requires independent tools. Sequential provides ordering guarantees but is
24  //!    slower. The function name makes this tradeoff explicit.
25  //!
26  //! ## Parallel Execution with `tokio::spawn`
27  //!
28  //! The parallel executor uses `tokio::spawn` instead of alternatives:
29  //!
30  //! - **`FuturesUnordered`**: Would require collecting futures first, less flexible
31  //! - **`join_all`**: Would await in the caller's task, blocking other operations
32  //! - **`tokio::spawn`**: Allows true concurrent execution on tokio runtime, with
33  //!   proper error isolation (one tool failure doesn't crash the executor)
34  //!
35  //! **Tradeoff**: Requires `Send + 'static` bounds, which means tool executors
36  //! can't borrow from the calling context. This is acceptable because tool
37  //! execution should be self-contained anyway.
38  //!
39  //! ## Independent Error Handling
40  //!
41  //! Each tool result is wrapped in `Result< ToolResult, Box< dyn Error > >` rather than
42  //! failing fast on the first error. This design choice:
43  //!
44  //! 1. **Maximizes Useful Work**: With parallel execution, some tools may succeed
45  //!    even if others fail. Collecting all results allows the application to use
46  //!    partial results instead of throwing everything away.
47  //!
48  //! 2. **Debugging**: Applications can log all failures at once instead of seeing
49  //!    only the first failure and having to re-run to discover subsequent ones.
50  //!
51  //! 3. **Agent Patterns**: LLM agents often benefit from partial tool results -
52  //!    they can reason about failures and adjust their strategy.
53  //!
54  //! ## Generic Executor Pattern
55  //!
56  //! Both functions accept a generic `Exec : Fn(ToolCall) -> Future< Output = Result<...> >`
57  //! instead of requiring a specific trait. This provides maximum flexibility:
58  //!
59  //! - **Closures**: Can capture context for tool execution
60  //! - **Async blocks**: Natural Rust syntax
61  //! - **Function pointers**: For simple stateless tools
62  //! - **No trait boilerplate**: No need to define and implement custom traits
63  //!
64  //! The executor receives ownership of `ToolCall` (not `&ToolCall`) because:
65  //! 1. Parallel execution needs `'static` lifetime (can't borrow)
66  //! 2. Tool execution often needs to serialize arguments, which requires owned data
67  //! 3. `ToolCall` is small enough that cloning is cheap
68
69  use crate::components::ToolCall;
70  use serde_json::Value;
71  use std::future::Future;
72
73  /// Result of executing a tool call.
74  ///
75  /// Contains the tool call ID and the result as a JSON value.
76  #[ derive( Debug, Clone ) ]
77  pub struct ToolResult
78  {
79    /// Tool call ID (matches the ID from the original `ToolCall`).
80    pub tool_call_id : String,
81
82    /// Result of the tool execution (as JSON string).
83    pub result : String,
84  }
85
86  impl ToolResult
87  {
88    /// Creates a new tool result.
89    ///
90    /// # Arguments
91    ///
92    /// * `tool_call_id` - The ID of the tool call this result corresponds to
93    /// * `result` - The result value (will be serialized to JSON string)
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use api_xai::ToolResult;
99    /// use serde_json::json;
100    ///
101    /// let result = ToolResult::new(
102    ///   "call_123".to_string(),
103    ///   &json!({ "temperature": 72, "conditions": "sunny" })
104    /// );
105    /// ```
106    pub fn new( tool_call_id : String, result : &Value ) -> Self
107    {
108      Self
109      {
110        tool_call_id,
111        result : result.to_string(),
112      }
113    }
114
115    /// Creates a new tool result from a string.
116    ///
117    /// # Arguments
118    ///
119    /// * `tool_call_id` - The ID of the tool call this result corresponds to
120    /// * `result_str` - The result as a JSON string
121    pub fn from_string( tool_call_id : String, result_str : String ) -> Self
122    {
123      Self
124      {
125        tool_call_id,
126        result : result_str,
127      }
128    }
129
130    /// Creates a new tool result from an error.
131    ///
132    /// # Arguments
133    ///
134    /// * `tool_call_id` - The ID of the tool call that failed
135    /// * `error` - The error message
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use api_xai::ToolResult;
141    ///
142    /// let result = ToolResult::from_error(
143    ///   "call_123".to_string(),
144    ///   "Function execution failed : invalid parameters"
145    /// );
146    /// ```
147    pub fn from_error( tool_call_id : String, error : &str ) -> Self
148    {
149      let error_json = serde_json::json!({
150        "error": error
151      });
152
153      Self
154      {
155        tool_call_id,
156        result : error_json.to_string(),
157      }
158    }
159  }
160
161  /// Executes multiple tool calls in parallel.
162  ///
163  /// Takes a list of tool calls and an executor function, runs them
164  /// concurrently using `tokio::spawn`, and returns all results.
165  ///
166  /// # Arguments
167  ///
168  /// * `tool_calls` - List of tool calls to execute
169  /// * `executor` - Async function that executes a single tool call
170  ///
171  /// # Type Parameters
172  ///
173  /// * `F` - Future returned by the executor
174  /// * `Exec` - Executor function type
175  ///
176  /// # Returns
177  ///
178  /// Vector of tool results in the same order as input tool calls.
179  ///
180  /// # Examples
181  ///
182  /// ```no_run
183  /// use api_xai::{ ToolCall, ToolResult, execute_tool_calls_parallel };
184  /// use serde_json::json;
185  ///
186  /// # async fn example( tool_calls : Vec< ToolCall > ) -> Result< (), Box< dyn std::error::Error > > {
187  /// // Execute all tool calls in parallel
188  /// let results = execute_tool_calls_parallel( tool_calls, | call | async move {
189  ///   // Your tool execution logic here
190  ///   match call.function.name.as_str() {
191  ///     "get_weather" => {
192  ///       let result = json!({ "temperature": 72, "conditions": "sunny" });
193  ///       Ok( ToolResult::new( call.id, &result ) )
194  ///     }
195  ///     _ => {
196  ///       Err( format!( "Unknown function : {name}", name = call.function.name ).into() )
197  ///     }
198  ///   }
199  /// } ).await;
200  /// # Ok( () )
201  /// # }
202  /// ```
203  pub async fn execute_tool_calls_parallel< F, Exec >(
204    tool_calls : Vec< ToolCall >,
205    executor : Exec
206  ) -> Vec< Result< ToolResult, Box< dyn std::error::Error + Send + Sync > > >
207  where
208    F : Future< Output = Result< ToolResult, Box< dyn std::error::Error + Send + Sync > > > + Send + 'static,
209    Exec : Fn( ToolCall ) -> F,
210  {
211    let mut handles = Vec::new();
212
213    // Spawn a task for each tool call
214    for call in tool_calls
215    {
216      let future = executor( call );
217      let handle = tokio::spawn( future );
218      handles.push( handle );
219    }
220
221    // Collect results
222    let mut results = Vec::new();
223    for handle in handles
224    {
225      match handle.await
226      {
227        Ok( result ) => results.push( result ),
228        Err( e ) => results.push( Err( format!( "Task join error : {e}" ).into() ) ),
229      }
230    }
231
232    results
233  }
234
235  /// Executes multiple tool calls sequentially.
236  ///
237  /// Takes a list of tool calls and an executor function, runs them
238  /// one by one, and returns all results.
239  ///
240  /// # Arguments
241  ///
242  /// * `tool_calls` - List of tool calls to execute
243  /// * `executor` - Async function that executes a single tool call
244  ///
245  /// # Type Parameters
246  ///
247  /// * `F` - Future returned by the executor
248  /// * `Exec` - Executor function type
249  ///
250  /// # Returns
251  ///
252  /// Vector of tool results in the same order as input tool calls.
253  ///
254  /// # Examples
255  ///
256  /// ```no_run
257  /// use api_xai::{ ToolCall, ToolResult, execute_tool_calls_sequential };
258  /// use serde_json::json;
259  ///
260  /// # async fn example( tool_calls : Vec< ToolCall > ) -> Result< (), Box< dyn std::error::Error > > {
261  /// // Execute all tool calls sequentially
262  /// let results = execute_tool_calls_sequential( tool_calls, | call | async move {
263  ///   // Your tool execution logic here
264  ///   match call.function.name.as_str() {
265  ///     "get_weather" => {
266  ///       let result = json!({ "temperature": 72, "conditions": "sunny" });
267  ///       Ok( ToolResult::new( call.id, &result ) )
268  ///     }
269  ///     _ => {
270  ///       Err( format!( "Unknown function : {name}", name = call.function.name ).into() )
271  ///     }
272  ///   }
273  /// } ).await;
274  /// # Ok( () )
275  /// # }
276  /// ```
277  pub async fn execute_tool_calls_sequential< F, Exec >(
278    tool_calls : Vec< ToolCall >,
279    executor : Exec
280  ) -> Vec< Result< ToolResult, Box< dyn std::error::Error + Send + Sync > > >
281  where
282    F : Future< Output = Result< ToolResult, Box< dyn std::error::Error + Send + Sync > > > + Send + 'static,
283    Exec : Fn( ToolCall ) -> F,
284  {
285    let mut results = Vec::new();
286
287    for call in tool_calls
288    {
289      let result = executor( call ).await;
290      results.push( result );
291    }
292
293    results
294  }
295}
296
297crate::mod_interface!
298{
299  exposed use
300  {
301    ToolResult,
302    execute_tool_calls_parallel,
303    execute_tool_calls_sequential,
304  };
305}