Building AI Agents with Function Calling: Complete Implementation Guide
Build production-ready AI agents that can execute code, call APIs, query databases, and interact with external systems through function calling.
Introduction
Function calling transforms LLMs from text generators into action-taking agents. This guide covers building robust function-calling agents with proper validation, error handling, and production patterns.
Function Calling Architecture
USER REQUEST │ ▼ ┌─────────────┐ │ LLM │ ──▶ Analyzes request, decides function calls │ Reasoning │ └─────────────┘ │ ▼ (if function needed) ┌─────────────┐ │ Function │ ──▶ Validates, executes, returns result │ Executor │ └─────────────┘ │ ▼ ┌─────────────┐ │ LLM │ ──▶ Synthesizes final response │ Response │ └─────────────┘ Part 1: Function Definition & Schema
JSON Schema for Functions
# functions/schemas.py from typing: Dict, List, Any, Optional, get_type_hints from pydantic import BaseModel, Field, create_model from dataclasses import dataclass import inspect import json @dataclass class FunctionSpec: name: str description: str parameters: Dict # JSON Schema required: List[str] = None returns: Dict = None category: str = "general" version: str = "1.0" def function_to_schema(func: callable) -> FunctionSpec: """Auto-generate function schema from Python function.""" sig = inspect.signature(func) type_hints = get_type_hints(func) docstring = inspect.getdoc(func) or "" # Parse docstring for param descriptions param_descriptions = _parse_docstring_params(docstring) properties = {} required = [] for name, param in sig.parameters.items(): if name in ("self", "cls"): continue param_type = type_hints.get(name, Any) json_schema = _python_type_to_json_schema(param_type) json_schema["description"] = param_descriptions.get(name, "") properties[name] = json_schema if param.default == inspect.Parameter.empty: required.append(name) return FunctionSpec( name=func.__name__, description=docstring.split("\n")[0] if docstring else "", parameters={ "type": "object", "properties": properties, "required": required }, required=required, returns=_python_type_to_json_schema(sig.return_annotation) if sig.return_annotation != inspect.Signature.empty else {} ) def _python_type_to_json_schema(py_type: type) -> Dict: """Convert Python type to JSON Schema.""" origin = getattr(py_type, "__origin__", None) args = getattr(py_type, "__args__", ()) if py_type is str: return {"type": "string"} elif py_type is int: return {"type": "integer"} elif py_type is float: return {"type": "number"} elif py_type is bool: return {"type": "boolean"} elif origin is list or origin is List: return {"type": "array", "items": _python_type_to_json_schema(args[0]) if args else {}} elif origin is dict or origin is Dict: return {"type": "object", "additionalProperties": _python_type_to_json_schema(args[1]) if len(args) > 1 else {}} elif origin is Optional or origin is Union: # Handle Optional[T] = Union[T, None] non_none = [a for a in args if a is not type(None)] if non_none: return _python_type_to_json_schema(non_none[0]) return {"type": "null"} elif hasattr(py_type, "__annotations__"): # Pydantic model return py_type.model_json_schema() else: return {"type": "string"} # fallback # Example functions with rich schemas class FunctionLibrary: @staticmethod def execute_python(code: str, timeout: int = 30) -> Dict: """Execute Python code in sandboxed environment. Args: code: Python code to execute timeout: Maximum execution time in seconds """ pass @staticmethod def http_request( method: str, url: str, headers: Dict = None, body: str = None, timeout: int = 30 ) -> Dict: """Make HTTP request to external API. Args: method: HTTP method (GET, POST, PUT, DELETE, etc.) url: Target URL headers: Optional request headers body: Optional request body timeout: Request timeout in seconds """ pass @staticmethod def query_database(sql: str, params: List = None) -> Dict: """Execute read-only SQL query. Args: sql: SELECT query to execute params: Query parameters for prepared statement """ pass @staticmethod def search_web(query: str, max_results: int = 10) -> Dict: """Search the web for information. Args: query: Search query max_results: Maximum number of results to return """ pass @staticmethod def file_operation( operation: str, path: str, content: str = None ) -> Dict: """Perform file operations. Args: operation: read, write, append, delete, list path: File or directory path content: Content for write/append operations """ pass Function Registry
# functions/registry.py from typing: Dict, List, Callable, Any from dataclasses import dataclass, field import json @dataclass class RegisteredFunction: spec: FunctionSpec handler: Callable permissions: List[str] = field(default_factory=list) # required permissions rate_limit: int = 100 # calls per minute timeout: int = 30 class FunctionRegistry: def __init__(self): self.functions: Dict[str, RegisteredFunction] = {} self.categories: Dict[str, List[str]] = {} def register( self, func: Callable, spec: FunctionSpec = None, permissions: List[str] = None, rate_limit: int = 100, timeout: int = 30 ): """Register a function for LLM use.""" if spec is None: spec = function_to_schema(func) registered = RegisteredFunction( spec=spec, handler=func, permissions=permissions or [], rate_limit=rate_limit, timeout=timeout ) self.functions[spec.name] = registered if spec.category not in self.categories: self.categories[spec.category] = [] self.categories[spec.category].append(spec.name) def get_schema(self, name: str) -> Dict: """Get OpenAI-compatible function schema.""" if name not in self.functions: raise ValueError(f"Function {name} not registered") spec = self.functions[name].spec return { "name": spec.name, "description": spec.description, "parameters": spec.parameters } def get_all_schemas(self, category: str = None) -> List[Dict]: """Get all function schemas, optionally filtered by category.""" names = self.categories.get(category, []) if category else list(self.functions.keys()) return [self.get_schema(name) for name in names] def call(self, name: str, arguments: Dict) -> Any: """Execute a registered function.""" if name not in self.functions: raise ValueError(f"Function {name} not registered") registered = self.functions[name] # Validate arguments against schema # (Would use jsonschema.validate in production) # Execute return registered.handler(**arguments) async def acall(self, name: str, arguments: Dict) -> Any: """Async execute a registered function.""" if name not in self.functions: raise ValueError(f"Function {name} not registered") registered = self.functions[name] if asyncio.iscoroutinefunction(registered.handler): return await registered.handler(**arguments) else: # Run sync function in executor loop = asyncio.get_event_loop() return await loop.run_in_executor(None, registered.handler, **arguments) # Global registry registry = FunctionRegistry() # Register built-in functions registry.register(FunctionLibrary.execute_python, category="code") registry.register(FunctionLibrary.http_request, category="network") registry.register(FunctionLibrary.query_database, category="data") registry.register(FunctionLibrary.search_web, category="research") registry.register(FunctionLibrary.file_operation, category="filesystem") Part 2: Agent Implementation
Core Agent Loop
# agents/function_agent.py from typing: Dict, List, Any, Optional from dataclasses import dataclass, field from openai import AsyncOpenAI import asyncio import json @dataclass class FunctionCall: name: str arguments: Dict call_id: str @dataclass class AgentResponse: content: str function_calls: List[FunctionCall] = field(default_factory=list) finish_reason: str = "stop" usage: Dict = field(default_factory=dict) class FunctionCallingAgent: def __init__( self, client: AsyncOpenAI, model: str = "gpt-4o", registry: FunctionRegistry = None, system_prompt: str = None, max_iterations: int = 10, temperature: float = 0.1 ): self.client = client self.model = model self.registry = registry or FunctionRegistry() self.system_prompt = system_prompt or self._default_system_prompt() self.max_iterations = max_iterations self.temperature = temperature self.conversation_history: List[Dict] = [] def _default_system_prompt(self) -> str: return """You are an AI agent with access to various tools. Use tools when needed to answer questions or complete tasks. Think step by step. If you need to use a tool, call it. Always provide a final answer to the user.""" def _build_messages(self, user_message: str) -> List[Dict]: messages = [{"role": "system", "content": self.system_prompt}] messages.extend(self.conversation_history) messages.append({"role": "user", "content": user_message}) return messages def _get_function_schemas(self) -> List[Dict]: return self.registry.get_all_schemas() async def run(self, user_message: str) -> AgentResponse: """Run agent loop until completion.""" messages = self._build_messages(user_message) for iteration in range(self.max_iterations): # Call LLM with function definitions response = await self.client.chat.completions.create( model=self.model, messages=messages, tools=[{"type": "function", "function": schema} for schema in self._get_function_schemas()], tool_choice="auto", temperature=self.temperature, max_tokens=4000 ) message = response.choices[0].message # Add assistant message to history messages.append({ "role": "assistant", "content": message.content, "tool_calls": message.tool_calls }) # Check for function calls if not message.tool_calls: # No function calls - we're done self.conversation_history.append({"role": "user", "content": user_message}) self.conversation_history.append({"role": "assistant", "content": message.content}) return AgentResponse( content=message.content or "", finish_reason=response.choices[0].finish_reason, usage=response.usage.model_dump() if response.usage else {} ) # Execute function calls function_calls = [] for tool_call in message.tool_calls: call = FunctionCall( name=tool_call.function.nameSource:https://dev.to/ipsphil_digital/building-ai-agents-with-function-calling-tools-patterns-production-systems-3j5iPreviousCloud Run Jobs vs. Cloud Functions for Batch Security Scans: What I Actually Run in ProductionNextBMI Calculator: Free Online BMI Calculator — How-To Guide
Related posts
Sharp Non-Asymptotic Analysis of the Penalized Challenger in $\beta$-EB-TCI for Bernoulli Bandits
0arXiv stat.ML
Toward Optimal Switching Regret for Multi-Armed Bandits with Oblivious Adversary
0arXiv stat.ML
Spectral Condition for $\mu$P under Width-Depth Scaling
0arXiv stat.ML
A Function-Space Stability Boundary for Generalization in Interpolating Learning Systems
0arXiv stat.ML
Who Gets Watched? Allocating Scarce Escalation Resources on Pediatric Wards with Neural Index Policies
0arXiv stat.ML
Minimax Alternating Regret for the Experts Problem and Online Convex Optimization
0arXiv stat.ML