Tool calling

Agentic tool calling patterns

Tool use across multiple steps or conversation turns, covering multi-step and multi-turn agent loops.

Overview

To build agent loops, chain tool calls inside one response (multi-step), and conversations that thread tools across many turns (multi-turn). Both follow the same contract: append the assistant message that contains tool_calls to the history, append one tool message per call with the matching tool_call_id, then ask the model again.

Multi-step tool calling

Multi-step tool calling chains sequential function calls within one conversation turn. The model calls a function, you process the result, and the result is fed back to inform the final response.

Here’s an example of passing the result of a tool call from one completion into a second follow-up completion:

import json
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.reka.ai/v1",
    api_key=os.environ["REKA_API_KEY"],
)


# Example function to make available to the model
def get_current_weather(location, unit="fahrenheit"):
    """Get the weather for some location"""
    if "chicago" in location.lower():
        return json.dumps({"location": "Chicago", "temperature": "13", "unit": unit})
    elif "san francisco" in location.lower():
        return json.dumps({"location": "San Francisco", "temperature": "55", "unit": unit})
    elif "new york" in location.lower():
        return json.dumps({"location": "New York", "temperature": "11", "unit": unit})
    else:
        return json.dumps({"location": location, "temperature": "unknown"})


# 1. Define a list of callable tools for the model
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "Get the current weather in a given location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"]
                    }
                },
                "required": ["location"]
            }
        }
    }
]

# Create a running messages list to append to over time
messages = [
    {"role": "system", "content": "You are a helpful assistant that can access external functions. The responses from these function calls will be appended to this dialogue. Please provide responses based on the information from these function calls."},
    {"role": "user", "content": "What is the current temperature of New York, San Francisco and Chicago?"},
]

# 2. Prompt the model with tools defined
response = client.chat.completions.create(
    model="deepseek4-flash",
    messages=messages,
    tools=tools,
)

# Save function call outputs for subsequent requests
tool_calls = response.choices[0].message.tool_calls

if tool_calls:
    # Add the assistant's response with tool calls to messages
    messages.append(
        {
            "role": "assistant",
            "content": response.choices[0].message.content or "",
            "tool_calls": [tool_call.model_dump() for tool_call in tool_calls],
        }
    )

    # 3. Execute the function logic for each tool call
    for tool_call in tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)

        if function_name == "get_current_weather":
            function_response = get_current_weather(
                location=function_args.get("location"),
                unit=function_args.get("unit", "fahrenheit"),
            )

            # 4. Provide function call results to the model
            messages.append(
                {
                    "tool_call_id": tool_call.id,
                    "role": "tool",
                    "name": function_name,
                    "content": function_response,
                }
            )

    # 5. The model can now answer with the function results
    function_enriched_response = client.chat.completions.create(
        model="deepseek4-flash",
        messages=messages,
        tools=tools,
    )
    print(function_enriched_response.choices[0].message.content)

And here’s the final output from the second call:

JSON
{
  "role": "assistant",
  "content": "The current temperature in New York is 11°F, in San Francisco it is 55°F, and in Chicago it is 13°F."
}

In this run, the model generated three tool call descriptions, your code iterated over them to execute each one, and the results were passed back so the model could produce a final answer.

Keep passing tools on the follow-up request. The model may decide it needs another call before it can answer; loop until a response comes back with no tool_calls (or finish_reason is stop), and cap the number of iterations so a confused model can’t spin forever.

Multi-turn tool calling

Multi-turn tool calling maintains context across multiple conversation turns. Functions can be called at any point in the conversation, and previous function results inform future decisions.

import json
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.reka.ai/v1",
    api_key=os.environ["REKA_API_KEY"],
)

# Define all available tools for the travel assistant
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "Get the current weather in a given location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"]
                    }
                },
                "required": ["location"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_restaurant_recommendations",
            "description": "Get restaurant recommendations for a specific location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA"
                    },
                    "cuisine_type": {
                        "type": "string",
                        "description": "Type of cuisine preferred",
                        "enum": ["italian", "chinese", "mexican", "american", "french", "japanese", "any"]
                    },
                    "price_range": {
                        "type": "string",
                        "description": "Price range preference",
                        "enum": ["budget", "mid-range", "upscale", "any"]
                    }
                },
                "required": ["location"]
            }
        }
    }
]


def get_current_weather(location, unit="fahrenheit"):
    """Get the weather for some location"""
    if "chicago" in location.lower():
        return json.dumps({"location": "Chicago", "temperature": "13", "unit": unit, "condition": "cold and snowy"})
    elif "san francisco" in location.lower():
        return json.dumps({"location": "San Francisco", "temperature": "65", "unit": unit, "condition": "mild and partly cloudy"})
    elif "new york" in location.lower():
        return json.dumps({"location": "New York", "temperature": "28", "unit": unit, "condition": "cold and windy"})
    else:
        return json.dumps({"location": location, "temperature": "unknown", "condition": "unknown"})


def get_restaurant_recommendations(location, cuisine_type="any", price_range="any"):
    """Get restaurant recommendations for a location"""
    restaurants = {}
    if "san francisco" in location.lower():
        restaurants = {
            "italian": ["Tony's Little Star Pizza", "Perbacco"],
            "chinese": ["R&G Lounge", "Z&Y Restaurant"],
            "american": ["Zuni Café", "House of Prime Rib"],
        }
    elif "chicago" in location.lower():
        restaurants = {
            "italian": ["Gibsons Italia", "Piccolo Sogno"],
            "american": ["Alinea", "Girl & Goat"],
        }
    elif "new york" in location.lower():
        restaurants = {
            "italian": ["Carbone", "Don Angie"],
            "american": ["The Spotted Pig", "Gramercy Tavern"],
        }
    return json.dumps(
        {
            "location": location,
            "cuisine_filter": cuisine_type,
            "price_filter": price_range,
            "restaurants": restaurants,
        }
    )


def handle_conversation_turn(messages, user_input):
    """Handle a single conversation turn with potential function calls"""
    # 1. Add user input to messages
    messages.append({"role": "user", "content": user_input})

    # 2. Get model response with tools
    response = client.chat.completions.create(
        model="deepseek4-flash",
        messages=messages,
        tools=tools,
    )
    message = response.choices[0].message
    tool_calls = message.tool_calls

    if not tool_calls:
        # Plain text reply: keep it in the history and return it
        messages.append({"role": "assistant", "content": message.content})
        return message.content

    # 3. Add assistant response with tool calls
    messages.append(
        {
            "role": "assistant",
            "content": message.content or "",
            "tool_calls": [tool_call.model_dump() for tool_call in tool_calls],
        }
    )

    # 4. Execute each function call
    for tool_call in tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)
        print(f"Calling {function_name} with args: {function_args}")

        if function_name == "get_current_weather":
            function_response = get_current_weather(
                location=function_args.get("location"),
                unit=function_args.get("unit", "fahrenheit"),
            )
        elif function_name == "get_restaurant_recommendations":
            function_response = get_restaurant_recommendations(
                location=function_args.get("location"),
                cuisine_type=function_args.get("cuisine_type", "any"),
                price_range=function_args.get("price_range", "any"),
            )
        else:
            function_response = json.dumps({"error": f"Unknown tool {function_name}"})

        # 5. Add function response to messages
        messages.append(
            {
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": function_response,
            }
        )

    # 6. Get final response with function results
    final_response = client.chat.completions.create(
        model="deepseek4-flash",
        messages=messages,
        tools=tools,
    )
    final_content = final_response.choices[0].message.content

    # 7. Add final assistant response to messages for context retention
    messages.append({"role": "assistant", "content": final_content})
    return final_content


# Initialize conversation with system message
messages = [{"role": "system", "content": "You are a helpful travel planning assistant. You can access weather information and restaurant recommendations. Use the available tools to provide comprehensive travel advice based on the user's needs."}]

# TURN 1: Initial weather request
question_1 = "What is the current temperature of New York, San Francisco and Chicago?"
print("User:", question_1)
print("Assistant:", handle_conversation_turn(messages, question_1))

# TURN 2: Follow-up that depends on the previous context
question_2 = "Based on the weather, which city would be best for outdoor activities? And can you find some restaurant recommendations for that city?"
print("\nUser:", question_2)
print("Assistant:", handle_conversation_turn(messages, question_2))

In this example, the assistant:

  • Turn 1: calls the weather function for three cities and reports the temperatures.
  • Turn 2: remembers the previous weather data, works out which city is best for outdoor activities (San Francisco at 65°F), and calls the restaurant recommendation function for that city without being told to.

The model maintains context across turns and makes informed decisions based on previous interactions. Because the history is append-only, each turn also reuses the previous turn’s prompt from the prompt cache.

Next: Tool calling best practices.

Next steps