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}