Skip to content
Back to Blog
1 min read

Function Calling Updates: What's New in September 2024

I wrote “Function Calling Updates: What’s New in September 2024” to share practical, production-minded guidance on this topic.

Updated Function Calling Syntax

from openai import OpenAI
from typing import List, Optional
import json

client = OpenAI()

# Define tools with the updated schema format
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_products",
            "description": "Search for products in the catalog",
            "strict": True,  # NEW: Enforce parameter schema
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Search query"
                    },
                    "category": {
                        "type": "string",
                        "enum": ["electronics", "clothing", "home", "sports"]
                    },
                    "min_price": {
                        "type": "number",
                        "minimum": 0
                    },
                    "max_price": {
                        "type": "number",
                        "minimum": 0
                    },
                    "in_stock": {
                        "type": "boolean",
                        "default": True
                    }
                },
                "required": ["query"],
                "additionalProperties": False
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_product_details",
            "description": "Get detailed information about a specific product",
            "strict": True,
            "parameters": {
                "type": "object",
                "properties": {
                    "product_id": {
                        "type": "string",
                        "description": "The product ID"
                    }
                },
                "required": ["product_id"],
                "additionalProperties": False
            }
        }
    }
]

def chat_with_tools(user_message: str) -> str:
    """Run a conversation with function calling"""

    messages = [
        {"role": "system", "content": "You are a shopping assistant. Use tools to help customers find products."},
        {"role": "user", "content": user_message}
    ]

    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )

    return response

Handling Tool Calls

def execute_tool(tool_call) -> str:
    """Execute a tool call and return the result"""

    name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    if name == "search_products":
        return search_products(**args)
    elif name == "get_product_details":
        return get_product_details(**args)
    else:
        return json.dumps({"error": f"Unknown function: {name}"})

def search_products(query: str, category: str = None,
                   min_price: float = None, max_price: float = None,
                   in_stock: bool = True) -> str:
    """Mock product search"""
    # In reality, this would query your database
    results = [
        {"id": "P001", "name": f"Product matching '{query}'", "price": 99.99},
        {"id": "P002", "name": f"Another {query} item", "price": 149.99}
    ]
    return json.dumps(results)

def get_product_details(product_id: str) -> str:
    """Mock product details"""
    return json.dumps({
        "id": product_id,
        "name": "Sample Product",
        "description": "A great product",
        "price": 99.99,
        "stock": 50
    })

def complete_tool_conversation(user_message: str) -> str:
    """Complete a conversation that may involve multiple tool calls"""

    messages = [
        {"role": "system", "content": "You are a shopping assistant."},
        {"role": "user", "content": user_message}
    ]

    while True:
        response = client.chat.completions.create(
            model="gpt-4o-2024-08-06",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )

        assistant_message = response.choices[0].message

        # Check if the model wants to call tools
        if assistant_message.tool_calls:
            # Add assistant message with tool calls
            messages.append(assistant_message)

            # Execute each tool call
            for tool_call in assistant_message.tool_calls:
                result = execute_tool(tool_call)

                # Add tool result
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result
                })
        else:
            # No more tool calls - return final response
            return assistant_message.content

Strict Mode for Function Parameters

# Strict mode ensures parameters match the schema exactly
strict_tool = {
    "type": "function",
    "function": {
        "name": "create_event",
        "description": "Create a calendar event",
        "strict": True,  # Model MUST provide valid parameters
        "parameters": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "start_time": {
                    "type": "string",
                    "description": "ISO 8601 datetime"
                },
                "end_time": {
                    "type": "string",
                    "description": "ISO 8601 datetime"
                },
                "attendees": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "email": {"type": "string"},
                            "required": {"type": "boolean"}
                        },
                        "required": ["email", "required"],
                        "additionalProperties": False
                    }
                },
                "recurrence": {
                    "type": ["object", "null"],  # Explicitly nullable
                    "properties": {
                        "frequency": {
                            "type": "string",
                            "enum": ["daily", "weekly", "monthly"]
                        },
                        "count": {"type": "integer"}
                    },
                    "required": ["frequency"],
                    "additionalProperties": False
                }
            },
            "required": ["title", "start_time", "end_time", "attendees", "recurrence"],
            "additionalProperties": False
        }
    }
}

Tool Choice Control

def controlled_tool_use(user_message: str, force_tool: str = None):
    """Control which tools the model can use"""

    # Option 1: Let model decide
    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[{"role": "user", "content": user_message}],
        tools=tools,
        tool_choice="auto"  # Model decides if/which tool to use
    )

    # Option 2: Force a specific tool
    if force_tool:
        response = client.chat.completions.create(
            model="gpt-4o-2024-08-06",
            messages=[{"role": "user", "content": user_message}],
            tools=tools,
            tool_choice={"type": "function", "function": {"name": force_tool}}
        )

    # Option 3: Require tool use but let model choose
    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[{"role": "user", "content": user_message}],
        tools=tools,
        tool_choice="required"  # Must use a tool
    )

    # Option 4: Disable tool use
    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[{"role": "user", "content": user_message}],
        tools=tools,
        tool_choice="none"  # Cannot use tools
    )

    return response

Pydantic Integration for Tools

from pydantic import BaseModel, Field
from typing import Literal

class SearchParams(BaseModel):
    query: str = Field(description="Search query")
    category: Literal["electronics", "clothing", "home", "sports"] = Field(
        default=None,
        description="Product category filter"
    )
    min_price: float = Field(default=None, ge=0)
    max_price: float = Field(default=None, ge=0)

def pydantic_to_tool(model: type[BaseModel], name: str, description: str) -> dict:
    """Convert a Pydantic model to a tool definition"""

    schema = model.model_json_schema()

    # Remove Pydantic-specific fields
    schema.pop("title", None)

    return {
        "type": "function",
        "function": {
            "name": name,
            "description": description,
            "strict": True,
            "parameters": schema
        }
    }

# Create tool from Pydantic model
search_tool = pydantic_to_tool(
    SearchParams,
    "search_products",
    "Search for products in the catalog"
)

# When handling the call, parse directly to Pydantic
def handle_tool_call(tool_call) -> str:
    if tool_call.function.name == "search_products":
        params = SearchParams.model_validate_json(tool_call.function.arguments)
        # Now params is a validated Pydantic object
        return search_products_impl(params)

Error Handling for Tool Calls

class ToolError(Exception):
    """Error during tool execution"""
    def __init__(self, message: str, recoverable: bool = True):
        self.message = message
        self.recoverable = recoverable
        super().__init__(message)

def safe_tool_execution(tool_call, max_retries: int = 3) -> str:
    """Execute tool with error handling and retries"""

    for attempt in range(max_retries):
        try:
            result = execute_tool(tool_call)
            return result

        except json.JSONDecodeError as e:
            return json.dumps({
                "error": "Invalid tool arguments",
                "details": str(e)
            })

        except ToolError as e:
            if not e.recoverable:
                return json.dumps({
                    "error": e.message,
                    "recoverable": False
                })
            if attempt == max_retries - 1:
                return json.dumps({
                    "error": e.message,
                    "attempts": max_retries
                })

        except Exception as e:
            return json.dumps({
                "error": "Unexpected error",
                "details": str(e)
            })

Function calling is the foundation of agentic AI. These patterns help you build reliable, type-safe tool integrations that scale.\n\n## Takeaways\n\nAdd a concise, personal takeaway and recommended next steps here.\n

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.