Back to Blog
2 min read

Function Calling Patterns: Building Reliable AI Tools

Function calling enables LLMs to interact with external systems. Here’s how to build reliable tool integrations.

Function Calling Best Practices

from azure.ai.openai import AzureOpenAI
from pydantic import BaseModel, Field
from typing import Callable, Any
import json

class ToolDefinition(BaseModel):
    """Schema for tool definitions."""
    name: str
    description: str
    parameters: dict
    function: Callable

class FunctionCallingAgent:
    def __init__(self, openai_client: AzureOpenAI):
        self.openai = openai_client
        self.tools: dict[str, ToolDefinition] = {}

    def register_tool(self, name: str, description: str, parameters: dict, function: Callable):
        """Register a tool with validation."""
        self.tools[name] = ToolDefinition(
            name=name,
            description=description,
            parameters=parameters,
            function=function
        )

    def get_tool_schemas(self) -> list:
        """Get OpenAI-compatible tool schemas."""
        return [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.parameters
            }
        } for tool in self.tools.values()]

    async def execute_tool(self, name: str, arguments: dict) -> Any:
        """Execute tool with error handling."""
        if name not in self.tools:
            return {"error": f"Unknown tool: {name}"}

        tool = self.tools[name]
        try:
            # Validate arguments against schema
            self.validate_arguments(arguments, tool.parameters)
            result = await tool.function(**arguments)
            return {"success": True, "result": result}
        except Exception as e:
            return {"success": False, "error": str(e)}

    async def run(self, messages: list) -> str:
        """Run agent with automatic tool execution."""
        while True:
            response = await self.openai.chat.completions.create(
                model="gpt-4o",
                messages=messages,
                tools=self.get_tool_schemas(),
                tool_choice="auto"
            )

            message = response.choices[0].message

            if not message.tool_calls:
                return message.content

            # Execute all tool calls
            messages.append(message)
            for tool_call in message.tool_calls:
                result = await self.execute_tool(
                    tool_call.function.name,
                    json.loads(tool_call.function.arguments)
                )
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps(result)
                })

# Example tool registration
agent.register_tool(
    name="query_database",
    description="Execute SQL query and return results",
    parameters={
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "SQL query to execute"}
        },
        "required": ["query"]
    },
    function=execute_sql
)

Well-designed function calling creates powerful AI applications that interact safely with real systems.

Michael John Peña

Michael John Peña

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