tool_calling_multiturn/
tool_calling_multiturn.rs

1#![allow(clippy::uninlined_format_args)]
2//! Multi-turn tool calling example demonstrating proper conversation history management.
3//!
4//! This example demonstrates:
5//! - Multi-turn tool calling with proper message history
6//! - Using `assistant_with_tool_calls()` to maintain context
7//! - Complete tool calling loop implementation
8//! - Real-world tool execution patterns
9//!
10//! Run with: `cargo run --example tool_calling_multiturn`
11
12use openai_ergonomic::{
13    builders::chat::tool_function, responses::chat::ToolCallExt, Client, Result,
14};
15use serde::{Deserialize, Serialize};
16use serde_json::json;
17
18#[derive(Debug, Serialize, Deserialize)]
19struct CalculatorParams {
20    operation: String,
21    a: f64,
22    b: f64,
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26struct MemoryParams {
27    key: String,
28    value: Option<String>,
29}
30
31// Simple in-memory storage for the memory tool
32use std::collections::HashMap;
33use std::sync::{Arc, Mutex};
34
35fn get_calculator_tool() -> openai_client_base::models::ChatCompletionTool {
36    tool_function(
37        "calculator",
38        "Perform basic arithmetic operations: add, subtract, multiply, divide",
39        json!({
40            "type": "object",
41            "properties": {
42                "operation": {
43                    "type": "string",
44                    "enum": ["add", "subtract", "multiply", "divide"],
45                    "description": "The arithmetic operation to perform"
46                },
47                "a": {
48                    "type": "number",
49                    "description": "The first number"
50                },
51                "b": {
52                    "type": "number",
53                    "description": "The second number"
54                }
55            },
56            "required": ["operation", "a", "b"]
57        }),
58    )
59}
60
61fn get_memory_tool() -> openai_client_base::models::ChatCompletionTool {
62    tool_function(
63        "memory",
64        "Store or retrieve a value from memory. If value is provided, store it. Otherwise, retrieve it.",
65        json!({
66            "type": "object",
67            "properties": {
68                "key": {
69                    "type": "string",
70                    "description": "The key to store or retrieve"
71                },
72                "value": {
73                    "type": "string",
74                    "description": "The value to store (omit to retrieve)"
75                }
76            },
77            "required": ["key"]
78        }),
79    )
80}
81
82fn execute_calculator(params: &CalculatorParams) -> String {
83    let result = match params.operation.as_str() {
84        "add" => params.a + params.b,
85        "subtract" => params.a - params.b,
86        "multiply" => params.a * params.b,
87        "divide" => {
88            if params.b == 0.0 {
89                return json!({ "error": "Division by zero" }).to_string();
90            }
91            params.a / params.b
92        }
93        _ => return json!({ "error": "Unknown operation" }).to_string(),
94    };
95
96    json!({
97        "operation": params.operation,
98        "a": params.a,
99        "b": params.b,
100        "result": result
101    })
102    .to_string()
103}
104
105fn execute_memory(params: &MemoryParams, storage: &Arc<Mutex<HashMap<String, String>>>) -> String {
106    let mut store = storage.lock().unwrap();
107
108    if let Some(value) = &params.value {
109        // Store value
110        store.insert(params.key.clone(), value.clone());
111        json!({
112            "action": "stored",
113            "key": params.key,
114            "value": value
115        })
116        .to_string()
117    } else {
118        // Retrieve value
119        store.get(&params.key).map_or_else(
120            || {
121                json!({
122                    "action": "not_found",
123                    "key": params.key,
124                    "message": "Key not found in memory"
125                })
126                .to_string()
127            },
128            |value| {
129                json!({
130                    "action": "retrieved",
131                    "key": params.key,
132                    "value": value
133                })
134                .to_string()
135            },
136        )
137    }
138}
139
140// Execute a tool call and return the result
141fn execute_tool(
142    tool_name: &str,
143    arguments: &str,
144    storage: &Arc<Mutex<HashMap<String, String>>>,
145) -> Result<String> {
146    match tool_name {
147        "calculator" => {
148            let params: CalculatorParams = serde_json::from_str(arguments)?;
149            Ok(execute_calculator(&params))
150        }
151        "memory" => {
152            let params: MemoryParams = serde_json::from_str(arguments)?;
153            Ok(execute_memory(&params, storage))
154        }
155        _ => Ok(json!({ "error": format!("Unknown tool: {}", tool_name) }).to_string()),
156    }
157}
158
159// Handle the complete tool calling loop - this is the key function!
160async fn handle_tool_loop(
161    client: &Client,
162    mut chat_builder: openai_ergonomic::builders::chat::ChatCompletionBuilder,
163    tools: &[openai_client_base::models::ChatCompletionTool],
164    storage: &Arc<Mutex<HashMap<String, String>>>,
165) -> Result<String> {
166    const MAX_ITERATIONS: usize = 10; // Prevent infinite loops
167    let mut iteration = 0;
168
169    loop {
170        iteration += 1;
171        if iteration > MAX_ITERATIONS {
172            return Err(std::io::Error::other("Max iterations reached in tool loop").into());
173        }
174
175        println!("\n  [Iteration {}]", iteration);
176
177        // Send request with tools
178        let request = chat_builder.clone().tools(tools.to_vec());
179        let response = client.send_chat(request).await?;
180
181        // Check if there are tool calls
182        let tool_calls = response.tool_calls();
183        if tool_calls.is_empty() {
184            // No more tool calls, return the final response
185            if let Some(content) = response.content() {
186                return Ok(content.to_string());
187            }
188            return Err(std::io::Error::other("No content in final response").into());
189        }
190
191        // Process tool calls
192        println!("  Tool calls: {}", tool_calls.len());
193
194        // IMPORTANT: Add assistant message with tool calls to history
195        // This is the key step that maintains proper conversation context!
196        chat_builder = chat_builder.assistant_with_tool_calls(
197            response.content().unwrap_or(""),
198            tool_calls.iter().map(|tc| (*tc).clone()).collect(),
199        );
200
201        // Execute each tool call and add results to history
202        for tool_call in tool_calls {
203            let tool_name = tool_call.function_name();
204            let tool_args = tool_call.function_arguments();
205            let tool_id = tool_call.id();
206
207            println!("    → {}: {}", tool_name, tool_args);
208
209            let result = match execute_tool(tool_name, tool_args, storage) {
210                Ok(result) => {
211                    println!("    ✓ Result: {}", result);
212                    result
213                }
214                Err(e) => {
215                    let error_msg = format!("Error: {}", e);
216                    eprintln!("    ✗ {}", error_msg);
217                    error_msg
218                }
219            };
220
221            // Add tool result to the conversation
222            chat_builder = chat_builder.tool(tool_id, result);
223        }
224    }
225}
226
227#[tokio::main]
228async fn main() -> Result<()> {
229    println!("=== Multi-turn Tool Calling Example ===\n");
230
231    // Initialize client
232    let client = Client::from_env()?.build();
233
234    // Create storage for the memory tool
235    let storage = Arc::new(Mutex::new(HashMap::new()));
236
237    // Define available tools
238    let tools = vec![get_calculator_tool(), get_memory_tool()];
239
240    println!("Available tools:");
241    println!("  - calculator: Perform arithmetic operations");
242    println!("  - memory: Store and retrieve values");
243    println!();
244
245    // Example 1: Single tool call
246    println!("Example 1: Single Tool Call");
247    println!("User: What is 15 + 27?");
248    {
249        let chat_builder = client
250            .chat()
251            .system("You are a helpful assistant with access to a calculator and memory storage.")
252            .user("What is 15 + 27?");
253
254        let result = handle_tool_loop(&client, chat_builder, &tools, &storage).await?;
255        println!("Assistant: {}", result);
256    }
257
258    // Example 2: Multiple sequential tool calls
259    println!("\n\nExample 2: Multiple Sequential Tool Calls");
260    println!("User: Calculate 10 * 5 and store the result in memory as 'product'");
261    {
262        let chat_builder = client
263            .chat()
264            .system("You are a helpful assistant with access to a calculator and memory storage.")
265            .user("Calculate 10 * 5 and store the result in memory as 'product'");
266
267        let result = handle_tool_loop(&client, chat_builder, &tools, &storage).await?;
268        println!("Assistant: {}", result);
269    }
270
271    // Example 3: Retrieve from memory
272    println!("\n\nExample 3: Retrieve from Memory");
273    println!("User: What did I store in 'product'?");
274    {
275        let chat_builder = client
276            .chat()
277            .system("You are a helpful assistant with access to a calculator and memory storage.")
278            .user("What did I store in 'product'?");
279
280        let result = handle_tool_loop(&client, chat_builder, &tools, &storage).await?;
281        println!("Assistant: {}", result);
282    }
283
284    // Example 4: Complex multi-step task
285    println!("\n\nExample 4: Complex Multi-step Task");
286    println!("User: Calculate 100 / 4, multiply that by 3, and tell me the final result");
287    {
288        let chat_builder = client
289            .chat()
290            .system("You are a helpful assistant with access to a calculator and memory storage.")
291            .user("Calculate 100 / 4, multiply that by 3, and tell me the final result");
292
293        let result = handle_tool_loop(&client, chat_builder, &tools, &storage).await?;
294        println!("Assistant: {}", result);
295    }
296
297    // Example 5: Conversation with history
298    println!("\n\nExample 5: Conversation with History");
299    {
300        let mut chat_builder = client
301            .chat()
302            .system("You are a helpful assistant with access to a calculator and memory storage.");
303
304        // First question
305        println!("User: What is 8 + 7?");
306        chat_builder = chat_builder.user("What is 8 + 7?");
307        let result = handle_tool_loop(&client, chat_builder.clone(), &tools, &storage).await?;
308        println!("Assistant: {}", result);
309
310        // Add assistant response to history
311        chat_builder = chat_builder.assistant(&result);
312
313        // Follow-up question that depends on previous context
314        println!("\nUser: Now multiply that by 3");
315        chat_builder = chat_builder.user("Now multiply that by 3");
316        let result = handle_tool_loop(&client, chat_builder.clone(), &tools, &storage).await?;
317        println!("Assistant: {}", result);
318    }
319
320    println!("\n\n=== All examples completed successfully ===");
321    println!("\nKey Takeaway:");
322    println!("  When implementing multi-turn tool calling, ALWAYS use");
323    println!("  assistant_with_tool_calls() to maintain proper conversation");
324    println!("  history. This is essential for the model to understand the");
325    println!("  tool results and continue the conversation correctly.");
326
327    Ok(())
328}