#![allow(clippy::uninlined_format_args)]
use openai_ergonomic::{
builders::chat::tool_function, responses::chat::ToolCallExt, Client, Result,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Serialize, Deserialize)]
struct CalculatorParams {
operation: String,
a: f64,
b: f64,
}
#[derive(Debug, Serialize, Deserialize)]
struct MemoryParams {
key: String,
value: Option<String>,
}
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
fn get_calculator_tool() -> openai_client_base::models::ChatCompletionTool {
tool_function(
"calculator",
"Perform basic arithmetic operations: add, subtract, multiply, divide",
json!({
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The arithmetic operation to perform"
},
"a": {
"type": "number",
"description": "The first number"
},
"b": {
"type": "number",
"description": "The second number"
}
},
"required": ["operation", "a", "b"]
}),
)
}
fn get_memory_tool() -> openai_client_base::models::ChatCompletionTool {
tool_function(
"memory",
"Store or retrieve a value from memory. If value is provided, store it. Otherwise, retrieve it.",
json!({
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store or retrieve"
},
"value": {
"type": "string",
"description": "The value to store (omit to retrieve)"
}
},
"required": ["key"]
}),
)
}
fn execute_calculator(params: &CalculatorParams) -> String {
let result = match params.operation.as_str() {
"add" => params.a + params.b,
"subtract" => params.a - params.b,
"multiply" => params.a * params.b,
"divide" => {
if params.b == 0.0 {
return json!({ "error": "Division by zero" }).to_string();
}
params.a / params.b
}
_ => return json!({ "error": "Unknown operation" }).to_string(),
};
json!({
"operation": params.operation,
"a": params.a,
"b": params.b,
"result": result
})
.to_string()
}
fn execute_memory(params: &MemoryParams, storage: &Arc<Mutex<HashMap<String, String>>>) -> String {
let mut store = storage.lock().unwrap();
if let Some(value) = ¶ms.value {
store.insert(params.key.clone(), value.clone());
json!({
"action": "stored",
"key": params.key,
"value": value
})
.to_string()
} else {
store.get(¶ms.key).map_or_else(
|| {
json!({
"action": "not_found",
"key": params.key,
"message": "Key not found in memory"
})
.to_string()
},
|value| {
json!({
"action": "retrieved",
"key": params.key,
"value": value
})
.to_string()
},
)
}
}
fn execute_tool(
tool_name: &str,
arguments: &str,
storage: &Arc<Mutex<HashMap<String, String>>>,
) -> Result<String> {
match tool_name {
"calculator" => {
let params: CalculatorParams = serde_json::from_str(arguments)?;
Ok(execute_calculator(¶ms))
}
"memory" => {
let params: MemoryParams = serde_json::from_str(arguments)?;
Ok(execute_memory(¶ms, storage))
}
_ => Ok(json!({ "error": format!("Unknown tool: {}", tool_name) }).to_string()),
}
}
async fn handle_tool_loop(
client: &Client,
mut chat_builder: openai_ergonomic::builders::chat::ChatCompletionBuilder,
tools: &[openai_client_base::models::ChatCompletionTool],
storage: &Arc<Mutex<HashMap<String, String>>>,
) -> Result<String> {
const MAX_ITERATIONS: usize = 10; let mut iteration = 0;
loop {
iteration += 1;
if iteration > MAX_ITERATIONS {
return Err(std::io::Error::other("Max iterations reached in tool loop").into());
}
println!("\n [Iteration {}]", iteration);
let request = chat_builder.clone().tools(tools.to_vec());
let response = client.send_chat(request).await?;
let tool_calls = response.tool_calls();
if tool_calls.is_empty() {
if let Some(content) = response.content() {
return Ok(content.to_string());
}
return Err(std::io::Error::other("No content in final response").into());
}
println!(" Tool calls: {}", tool_calls.len());
chat_builder = chat_builder.assistant_with_tool_calls(
response.content().unwrap_or(""),
tool_calls.iter().map(|tc| (*tc).clone()).collect(),
);
for tool_call in tool_calls {
let tool_name = tool_call.function_name();
let tool_args = tool_call.function_arguments();
let tool_id = tool_call.id();
println!(" → {}: {}", tool_name, tool_args);
let result = match execute_tool(tool_name, tool_args, storage) {
Ok(result) => {
println!(" ✓ Result: {}", result);
result
}
Err(e) => {
let error_msg = format!("Error: {}", e);
eprintln!(" ✗ {}", error_msg);
error_msg
}
};
chat_builder = chat_builder.tool(tool_id, result);
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
println!("=== Multi-turn Tool Calling Example ===\n");
let client = Client::from_env()?.build();
let storage = Arc::new(Mutex::new(HashMap::new()));
let tools = vec![get_calculator_tool(), get_memory_tool()];
println!("Available tools:");
println!(" - calculator: Perform arithmetic operations");
println!(" - memory: Store and retrieve values");
println!();
println!("Example 1: Single Tool Call");
println!("User: What is 15 + 27?");
{
let chat_builder = client
.chat()
.system("You are a helpful assistant with access to a calculator and memory storage.")
.user("What is 15 + 27?");
let result = handle_tool_loop(&client, chat_builder, &tools, &storage).await?;
println!("Assistant: {}", result);
}
println!("\n\nExample 2: Multiple Sequential Tool Calls");
println!("User: Calculate 10 * 5 and store the result in memory as 'product'");
{
let chat_builder = client
.chat()
.system("You are a helpful assistant with access to a calculator and memory storage.")
.user("Calculate 10 * 5 and store the result in memory as 'product'");
let result = handle_tool_loop(&client, chat_builder, &tools, &storage).await?;
println!("Assistant: {}", result);
}
println!("\n\nExample 3: Retrieve from Memory");
println!("User: What did I store in 'product'?");
{
let chat_builder = client
.chat()
.system("You are a helpful assistant with access to a calculator and memory storage.")
.user("What did I store in 'product'?");
let result = handle_tool_loop(&client, chat_builder, &tools, &storage).await?;
println!("Assistant: {}", result);
}
println!("\n\nExample 4: Complex Multi-step Task");
println!("User: Calculate 100 / 4, multiply that by 3, and tell me the final result");
{
let chat_builder = client
.chat()
.system("You are a helpful assistant with access to a calculator and memory storage.")
.user("Calculate 100 / 4, multiply that by 3, and tell me the final result");
let result = handle_tool_loop(&client, chat_builder, &tools, &storage).await?;
println!("Assistant: {}", result);
}
println!("\n\nExample 5: Conversation with History");
{
let mut chat_builder = client
.chat()
.system("You are a helpful assistant with access to a calculator and memory storage.");
println!("User: What is 8 + 7?");
chat_builder = chat_builder.user("What is 8 + 7?");
let result = handle_tool_loop(&client, chat_builder.clone(), &tools, &storage).await?;
println!("Assistant: {}", result);
chat_builder = chat_builder.assistant(&result);
println!("\nUser: Now multiply that by 3");
chat_builder = chat_builder.user("Now multiply that by 3");
let result = handle_tool_loop(&client, chat_builder.clone(), &tools, &storage).await?;
println!("Assistant: {}", result);
}
println!("\n\n=== All examples completed successfully ===");
println!("\nKey Takeaway:");
println!(" When implementing multi-turn tool calling, ALWAYS use");
println!(" assistant_with_tool_calls() to maintain proper conversation");
println!(" history. This is essential for the model to understand the");
println!(" tool results and continue the conversation correctly.");
Ok(())
}