0Pricing
AI Engineering Academy · Lesson

Processing Tool Calls in Your Application

Detect finish_reason tool_calls in the API response, extract function names and arguments, execute the corresponding Python function, and send the result back to the model.

Processing Tool Calls in Your Application is a free AI Engineering Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

The Tool Call Response Object

When the model decides to call a function, the API response contains a tool_calls list on the message object. Each tool call has a unique id, the function.name to call, and function.arguments — a JSON string of the arguments the model wants to pass. Your application code is responsible for parsing this and executing the function.

from openai import OpenAI
import json

client = OpenAI()

response = client.chat.completions.create(
    model='gpt-4o',
    messages=[{'role': 'user', 'content': 'What is the weather in Paris?'}],
    tools=tools  # defined previously
)

message = response.choices[0].message

if response.choices[0].finish_reason == 'tool_calls':
    for tool_call in message.tool_calls:
        print('Call ID:', tool_call.id)
        print('Function name:', tool_call.function.name)
        print('Arguments (JSON string):', tool_call.function.arguments)

Parsing Function Arguments

The function.arguments field is a JSON-encoded string, not a Python dict. You must parse it with json.loads(). Always wrap this in a try/except — the model occasionally produces malformed JSON despite schema guidance, and you need to handle that gracefully.

import json

def parse_tool_call(tool_call) -> dict:
    '''Parse a tool call's arguments from JSON string to dict.'''
    try:
        args = json.loads(tool_call.function.arguments)
        return args
    except json.JSONDecodeError as e:
        print(f'Failed to parse arguments for {tool_call.function.name}: {e}')
        print(f'Raw arguments: {tool_call.function.arguments}')
        return {}

# Usage
tool_call = message.tool_calls[0]
args = parse_tool_call(tool_call)
print('Parsed args:', args)  # {'location': 'Paris', 'unit': 'celsius'}

Dispatching to the Right Function

Use the function.name to dispatch to the correct Python function. A clean pattern is to keep your functions in a dictionary mapping name to callable. This avoids brittle if/elif chains and makes it easy to add new tools later.

def get_current_weather(location: str, unit: str = 'celsius') -> str:
    # Real implementation calls a weather API
    return f'{location}: 18{chr(176)}C, partly cloudy'

def create_calendar_event(title: str, start_time: str, duration_minutes: int, **kwargs) -> str:
    return f'Event created: {title} at {start_time} for {duration_minutes} minutes'

# Tool registry: maps function names to callables
TOOL_REGISTRY = {
    'get_current_weather': get_current_weather,
    'create_calendar_event': create_calendar_event
}

def execute_tool_call(tool_call) -> str:
    name = tool_call.function.name
    args = parse_tool_call(tool_call)

    if name not in TOOL_REGISTRY:
        return f'Unknown function: {name}'

    try:
        result = TOOL_REGISTRY[name](**args)
        return str(result)
    except Exception as e:
        return f'Function {name} raised an error: {str(e)}'

Sending Results Back to the Model

After executing the function, you must send the result back to the model in a follow-up API call. Add the assistant's message (which contains the tool_calls) to the conversation, then add a new message with role='tool', the tool_call_id, and the function result as content. Then call the API again.

def run_tool_call_loop(messages: list, tools: list) -> str:
    response = client.chat.completions.create(
        model='gpt-4o',
        messages=messages,
        tools=tools
    )
    message = response.choices[0].message
    messages.append(message)  # Add assistant's tool_calls message

    # Execute all tool calls and collect results
    for tool_call in (message.tool_calls or []):
        result = execute_tool_call(tool_call)
        # Add each tool result as a 'tool' role message
        messages.append({
            'role': 'tool',
            'tool_call_id': tool_call.id,
            'content': result
        })

    # Second API call with results appended
    final_response = client.chat.completions.create(
        model='gpt-4o',
        messages=messages,
        tools=tools
    )
    return final_response.choices[0].message.content

The Full Conversation Turn

A complete tool call interaction involves four messages in the conversation history: the user's message, the assistant's message requesting a tool call, the tool result message, and the assistant's final response incorporating the result. Understanding this structure is essential for building multi-turn tool-using assistants.

# The full message history for a tool-calling conversation:
conversation = [
    {'role': 'user', 'content': 'What is the weather in Tokyo?'},

    # Model requests a tool call (added by run_tool_call_loop)
    # {'role': 'assistant', 'content': None, 'tool_calls': [...]},

    # Application sends tool result back
    # {'role': 'tool', 'tool_call_id': 'call_abc123', 'content': 'Tokyo: 22C, sunny'},

    # Model produces final human-readable response
    # {'role': 'assistant', 'content': 'The weather in Tokyo is 22 degrees Celsius and sunny.'}
]

final_answer = run_tool_call_loop(
    [{'role': 'user', 'content': 'What is the weather in Tokyo?'}],
    tools
)
print(final_answer)

Handling the Case Where No Tool Is Called

Sometimes the model answers directly without calling any tool — finish_reason will be 'stop' rather than 'tool_calls'. Always check for this case before trying to process tool calls. A robust implementation handles both branches cleanly.

def smart_complete(user_message: str) -> str:
    messages = [{'role': 'user', 'content': user_message}]
    response = client.chat.completions.create(
        model='gpt-4o',
        messages=messages,
        tools=tools
    )
    choice = response.choices[0]

    if choice.finish_reason == 'stop':
        # Model answered directly without calling a tool
        return choice.message.content

    elif choice.finish_reason == 'tool_calls':
        # Process tool calls
        messages.append(choice.message)
        for tc in choice.message.tool_calls:
            result = execute_tool_call(tc)
            messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result})
        # Get final answer
        final = client.chat.completions.create(model='gpt-4o', messages=messages)
        return final.choices[0].message.content

    return 'Unexpected finish reason: ' + choice.finish_reason

Validating Arguments Before Execution

The model may occasionally pass arguments that fail business logic validation — a negative duration, an invalid email, or a date in the past. Validate arguments before calling the real function and return a descriptive error string if validation fails. The model can then correct its arguments in the next turn.

from pydantic import BaseModel, ValidationError
from datetime import datetime

class CreateEventArgs(BaseModel):
    title: str
    start_time: str  # ISO 8601
    duration_minutes: int

def safe_create_event(tool_call) -> str:
    try:
        raw_args = json.loads(tool_call.function.arguments)
        validated = CreateEventArgs(**raw_args)
        # Additional business rule
        event_time = datetime.fromisoformat(validated.start_time)
        if event_time < datetime.now():
            return 'Error: start_time must be in the future.'
        return create_calendar_event(**validated.dict())
    except ValidationError as e:
        return f'Invalid arguments: {e}'

Logging Tool Call Interactions

Always log tool call interactions for debugging and analytics. Log the function name, arguments, result, and execution time. This data helps you identify which tools are called most frequently, which fail, and what argument patterns the model produces — invaluable for improving your schemas and function implementations.

import time
import logging

logger = logging.getLogger('tool_calls')

def logged_execute(tool_call) -> str:
    name = tool_call.function.name
    args_str = tool_call.function.arguments
    start = time.time()
    result = execute_tool_call(tool_call)
    elapsed = time.time() - start

    logger.info(
        'Tool call executed',
        extra={
            'function': name,
            'arguments': args_str,
            'result_length': len(result),
            'elapsed_ms': round(elapsed * 1000)
        }
    )
    return result

Tool Call Security Considerations

Never execute arbitrary functions based on model output without validation. Always whitelist the exact function names in your TOOL_REGISTRY, validate all arguments, and check authorization before executing actions. The model is an untrusted caller — a malicious prompt could try to invoke destructive functions if your dispatch logic is too permissive.

  • Only allow functions explicitly listed in TOOL_REGISTRY
  • Validate inputs with Pydantic before execution
  • Require authorization for write/delete operations

Returning Rich Structured Results

Tool results don't have to be plain strings. You can return JSON-formatted data, tables, or summaries. Returning structured data as JSON lets the model parse and reference specific fields in its final answer. For large results, return a summary with key facts rather than dumping all raw data into the context.

def get_order_status(order_id: str) -> str:
    # Fetch from real database
    order = {'id': order_id, 'status': 'shipped', 'estimated_delivery': '2024-03-15', 'carrier': 'FedEx', 'tracking': 'FX123456'}
    # Return concise summary, not raw DB record
    return (
        f'Order {order_id}: Status={order["status"]}, '
        f'Estimated delivery: {order["estimated_delivery"]}, '
        f'Carrier: {order["carrier"]}, Tracking: {order["tracking"]}'
    )

Multi-Turn Tool-Using Conversations

A powerful pattern is a multi-turn conversation where the model calls tools across multiple user messages, building up context. Always maintain the full conversation history including past tool calls and results so the model can reference previous answers without re-calling tools unnecessarily.

Quick Check

Test your understanding of processing tool calls in your application.

Lesson Recap

In this lesson you learned: tool call arguments arrive as a JSON string that must be parsed with json.loads(), a TOOL_REGISTRY dictionary maps function names to callables for clean dispatch, and results go back to the model as role='tool' messages with matching tool_call_ids. Next up we handle the case where the model calls multiple functions simultaneously with parallel function calling.

Frequently asked questions

Is the “Processing Tool Calls in Your Application” lesson free?

Yes — the full text of “Processing Tool Calls in Your Application” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.

What will I learn in “Processing Tool Calls in Your Application”?

Detect finish_reason tool_calls in the API response, extract function names and arguments, execute the corresponding Python function, and send the result back to the model. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start AI Engineering Academy?

No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Processing Tool Calls in Your Application” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this AI Engineering Academy lesson?

Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Defining Function Schemas for the API
  2. Processing Tool Calls in Your Application
  3. Parallel Function Calling
  4. Building a Natural Language Database Interface
← Back to AI Engineering Academy