Agentic Architecture Pattern: Function Calling

Written by

in


    """
Pattern 93: Function Calling Pattern

Agents directly call Python functions based on natural language requests.
Uses OpenAI's function calling API to map queries to function invocations.
"""

from typing import Dict, Any, List, Optional, Callable
from dataclasses import dataclass, field
from datetime import datetime
import json
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.utils.function_calling import convert_to_openai_function


@dataclass
class FunctionDefinition:
    """Definition of callable function."""
    name: str
    description: str
    function: Callable
    parameters: Dict[str, Any]
    returns: str


@dataclass
class FunctionCall:
    """Record of function call."""
    call_id: str
    function_name: str
    arguments: Dict[str, Any]
    result: Any
    success: bool
    timestamp: datetime = field(default_factory=datetime.now)
    error: Optional[str] = None


# Example functions that agents can call
def calculate_discount(price: float, discount_percent: float) -> float:
    """Calculate final price after discount.
    
    Args:
        price: Original price
        discount_percent: Discount percentage (0-100)
    
    Returns:
        Final price after discount
    """
    return price * (1 - discount_percent / 100)


def schedule_meeting(date: str, time: str, attendees: List[str], topic: str) -> str:
    """Schedule a meeting.
    
    Args:
        date: Meeting date (YYYY-MM-DD)
        time: Meeting time (HH:MM)
        attendees: List of attendee names
        topic: Meeting topic
    
    Returns:
        Confirmation message
    """
    return f"Meeting scheduled for {date} at {time} with {len(attendees)} attendees. Topic: {topic}"


def send_email(to: str, subject: str, body: str) -> str:
    """Send an email.
    
    Args:
        to: Recipient email address
        subject: Email subject
        body: Email body
    
    Returns:
        Confirmation message
    """
    return f"Email sent to {to} with subject '{subject}'"


def query_database(table: str, filters: Dict[str, Any]) -> List[Dict]:
    """Query database table.
    
    Args:
        table: Table name
        filters: Dictionary of filter conditions
    
    Returns:
        List of matching records
    """
    # Simulated database query
    return [
        {"id": 1, "name": "Record 1", "value": 100},
        {"id": 2, "name": "Record 2", "value": 200}
    ]


def create_report(report_type: str, data_source: str, format: str = "pdf") -> str:
    """Generate a report.
    
    Args:
        report_type: Type of report (sales, analytics, etc.)
        data_source: Data source name
        format: Output format (pdf, excel, html)
    
    Returns:
        Report generation confirmation
    """
    return f"Generated {report_type} report from {data_source} in {format} format"


class FunctionCallingAgent:
    """Agent that can call functions based on natural language."""
    
    def __init__(self, name: str, model_name: str = "gpt-4"):
        self.name = name
        self.llm = ChatOpenAI(model=model_name, temperature=0)
        self.functions: Dict[str, FunctionDefinition] = {}
        self.call_history: List[FunctionCall] = []
    
    def register_function(self, func_def: FunctionDefinition):
        """Register a function that can be called."""
        self.functions[func_def.name] = func_def
    
    def execute_query(self, query: str) -> Dict[str, Any]:
        """Execute natural language query by calling appropriate functions."""
        
        # Prepare function descriptions for LLM
        functions_for_llm = []
        for func_def in self.functions.values():
            functions_for_llm.append({
                "name": func_def.name,
                "description": func_def.description,
                "parameters": func_def.parameters
            })
        
        # Create LLM with function calling
        llm_with_functions = self.llm.bind(functions=functions_for_llm)
        
        prompt = ChatPromptTemplate.from_messages([
            ("system", f"You are {self.name}, an AI assistant that can call functions to help users. "
                      f"Analyze the user's request and call the appropriate function."),
            ("human", "{query}")
        ])
        
        chain = prompt | llm_with_functions
        
        try:
            response = chain.invoke({"query": query})
            
            # Check if function was called
            if hasattr(response, 'additional_kwargs') and 'function_call' in response.additional_kwargs:
                function_call = response.additional_kwargs['function_call']
                function_name = function_call['name']
                arguments = json.loads(function_call['arguments'])
                
                # Execute the function
                if function_name in self.functions:
                    func_def = self.functions[function_name]
                    result = func_def.function(**arguments)
                    
                    # Record call
                    call = FunctionCall(
                        call_id=f"call_{len(self.call_history)}",
                        function_name=function_name,
                        arguments=arguments,
                        result=result,
                        success=True
                    )
                    self.call_history.append(call)
                    
                    return {
                        "success": True,
                        "function_called": function_name,
                        "arguments": arguments,
                        "result": result,
                        "agent": self.name
                    }
                else:
                    return {
                        "success": False,
                        "error": f"Function {function_name} not registered"
                    }
            else:
                # No function called, just return text response
                return {
                    "success": True,
                    "response": str(response.content),
                    "function_called": None
                }
        
        except Exception as e:
            call = FunctionCall(
                call_id=f"call_{len(self.call_history)}",
                function_name="unknown",
                arguments={},
                result=None,
                success=False,
                error=str(e)
            )
            self.call_history.append(call)
            
            return {
                "success": False,
                "error": str(e)
            }


class FunctionCallingPattern:
    """
    Function Calling Pattern - Natural language to function execution.
    
    Agents interpret natural language requests and automatically call
    appropriate Python functions with correct parameters.
    """
    
    def __init__(self):
        self.agents: Dict[str, FunctionCallingAgent] = {}
        self.available_functions: Dict[str, FunctionDefinition] = {}
    
    def register_function(self, func: Callable, description: str):
        """Register a function for agents to call."""
        # Extract function signature
        import inspect
        sig = inspect.signature(func)
        
        # Build parameter schema
        parameters = {
            "type": "object",
            "properties": {},
            "required": []
        }
        
        for param_name, param in sig.parameters.items():
            if param_name != 'self':
                param_type = "string"  # Simplified
                if param.annotation in [int, float]:
                    param_type = "number"
                elif param.annotation == bool:
                    param_type = "boolean"
                elif param.annotation == list:
                    param_type = "array"
                
                parameters["properties"][param_name] = {
                    "type": param_type,
                    "description": f"Parameter {param_name}"
                }
                
                if param.default == inspect.Parameter.empty:
                    parameters["required"].append(param_name)
        
        func_def = FunctionDefinition(
            name=func.__name__,
            description=description,
            function=func,
            parameters=parameters,
            returns="Function result"
        )
        
        self.available_functions[func.__name__] = func_def
    
    def create_agent(self, name: str, function_names: Optional[List[str]] = None) -> FunctionCallingAgent:
        """Create agent with access to specified functions."""
        agent = FunctionCallingAgent(name)
        
        # Register functions to agent
        if function_names:
            for func_name in function_names:
                if func_name in self.available_functions:
                    agent.register_function(self.available_functions[func_name])
        else:
            # Register all functions
            for func_def in self.available_functions.values():
                agent.register_function(func_def)
        
        self.agents[name] = agent
        return agent
    
    def execute_query(self, agent_name: str, query: str) -> Dict[str, Any]:
        """Execute query with specified agent."""
        if agent_name not in self.agents:
            return {"success": False, "error": "Agent not found"}
        
        agent = self.agents[agent_name]
        result = agent.execute_query(query)
        
        if result.get("success"):
            if result.get("function_called"):
                print(f"\n   ✓ {agent_name} called {result['function_called']}")
                print(f"     Arguments: {result['arguments']}")
                print(f"     Result: {result['result']}")
            else:
                print(f"\n   ℹ️ {agent_name}: {result.get('response', 'No response')[:80]}")
        else:
            print(f"\n   ✗ {agent_name}: {result.get('error', 'Unknown error')}")
        
        return result
    
    def run(self, scenario: str) -> Dict[str, Any]:
        """Execute function calling pattern."""
        total_calls = sum(len(agent.call_history) for agent in self.agents.values())
        successful_calls = sum(
            sum(1 for call in agent.call_history if call.success)
            for agent in self.agents.values()
        )
        
        return {
            "pattern": "FunctionCalling",
            "total_agents": len(self.agents),
            "available_functions": len(self.available_functions),
            "total_calls": total_calls,
            "successful_calls": successful_calls,
            "scenario": scenario
        }


# Example usage
if __name__ == "__main__":
    print("=== Function Calling Pattern Demo ===\n")
    
    # Create pattern
    pattern = FunctionCallingPattern()
    
    # Register functions
    print("1. Registering callable functions...")
    pattern.register_function(calculate_discount, "Calculate final price after applying discount")
    pattern.register_function(schedule_meeting, "Schedule a meeting with date, time, and attendees")
    pattern.register_function(send_email, "Send an email to a recipient")
    pattern.register_function(query_database, "Query database table with filters")
    pattern.register_function(create_report, "Generate a report from data source")
    
    print(f"   Registered {len(pattern.available_functions)} functions")
    
    # Create agents
    print("\n2. Creating function-calling agents...")
    assistant = pattern.create_agent("VirtualAssistant")
    print(f"   VirtualAssistant: {len(assistant.functions)} functions available")
    
    # Execute natural language queries
    print("\n3. Executing natural language queries...")
    
    queries = [
        "Calculate the final price for an item that costs $100 with a 20% discount",
        "Schedule a meeting for tomorrow at 2pm with Alice, Bob, and Carol to discuss Q4 planning",
        "Send an email to john@example.com with subject 'Project Update' and body 'The project is on track'",
        "Query the customers table and filter by status active",
        "Create a sales report from the Q4 data in PDF format"
    ]
    
    for query in queries:
        print(f"\n   Query: {query}")
        pattern.execute_query("VirtualAssistant", query)
    
    # Summary
    print("\n4. Execution Summary:")
    summary = pattern.run("Virtual assistant with function calling")
    print(f"   Total Functions: {summary['available_functions']}")
    print(f"   Function Calls: {summary['total_calls']}")
    print(f"   Successful: {summary['successful_calls']}")
    
    print("\n5. Call History:")
    for call in assistant.call_history:
        status = "✓" if call.success else "✗"
        print(f"   {status} {call.function_name}: {call.arguments}")
    
    print("\n✓ Function calling pattern executed natural language to function calls!")

    

Agentic Architecture Pattern: Function Calling

Pattern 93: 93 Function Calling – Line-by-Line Explanation

Overview

Pattern 93: Function Calling Pattern

Agents directly call Python functions based on natural language requests. Uses OpenAI’s function calling API to map queries to function invocations.


Code Structure Analysis

File Information

  • Pattern Number: 93
  • Pattern Name: 93 Function Calling
  • File Path: patterns\tool_resource\93_function_calling.py
  • Total Lines: 368

Detailed Code Explanation

Module Docstring

"""
Pattern 93: Function Calling Pattern

Agents directly call Python functions based on natural language requests.
Uses OpenAI's function calling API to map queries to function invocations.
"""

Explanation: This module docstring describes the purpose and key features of the 93 Function Calling pattern.


Imports Section

from typing import Dict, Any, List, Optional, Callable
from dataclasses import dataclass, field
from datetime import datetime
import json
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.utils.function_calling import convert_to_openai_function
        import inspect  # First 20 imports shown

Explanation: The module imports necessary dependencies: – from typing import Dict, Any, List, Optional, Callable: Type hints for better code documentation – from dataclasses import dataclass, field: Data class decorators for clean data structures – from datetime import datetime: Date and time utilities – import json: JSON parsing and serialization – from langchain_core.prompts import ChatPromptTemplate: LangChain framework integration – from langchain_openai import ChatOpenAI: LangChain framework integration – from langchain_core.utils.function_calling import convert_to_openai_function: LangChain framework integration


Classes

FunctionDefinition (Line 18)

Description: Definition of callable function.

FunctionCall (Line 28)

Description: Record of function call.

FunctionCallingAgent (Line 113)

Description: Agent that can call functions based on natural language.

Methods:

__init__ (Line 116)
def __init__(self, name, model_name):
    """No method docstring."""

Purpose: No method docstring.

Parameters: – self: Instance reference – name: Identifier or label – model_name: LLM model to use (e.g., “gpt-4”)

register_function (Line 122)
def register_function(self, func_def):
    """Register a function that can be called."""

Purpose: Register a function that can be called.

Parameters: – self: Instance reference – func_def: Parameter for func_def

execute_query (Line 126)
def execute_query(self, query):
    """Execute natural language query by calling appropriate functions."""

Purpose: Execute natural language query by calling appropriate functions.

Parameters: – self: Instance reference – query: Parameter for query

FunctionCallingPattern (Line 210)

Description: Function Calling Pattern – Natural language to function execution.

Agents interpret natural language requests and automatically call appropriate Python functions with correct parameters.

Methods:

__init__ (Line 218)
def __init__(self):
    """No method docstring."""

Purpose: No method docstring.

Parameters: – self: Instance reference

register_function (Line 222)
def register_function(self, func, description):
    """Register a function for agents to call."""

Purpose: Register a function for agents to call.

Parameters: – self: Instance reference – func: Parameter for func – description: Parameter for description

create_agent (Line 263)
def create_agent(self, name, function_names):
    """Create agent with access to specified functions."""

Purpose: Create agent with access to specified functions.

Parameters: – self: Instance reference – name: Identifier or label – function_names: Parameter for function_names

execute_query (Line 280)
def execute_query(self, agent_name, query):
    """Execute query with specified agent."""

Purpose: Execute query with specified agent.

Parameters: – self: Instance reference – agent_name: Parameter for agent_name – query: Parameter for query

run (Line 300)
def run(self, scenario):
    """Execute function calling pattern."""

Purpose: Execute function calling pattern.

Parameters: – self: Instance reference – scenario: Parameter for scenario


Functions

calculate_discount (Line 40)

def calculate_discount(price, discount_percent):
    """Calculate final price after discount.

Args:
    price: Original price
    discount_percent: Discount percentage (0-100)

Returns:
    Final price after discount"""

Purpose: Calculate final price after discount.

Args: price: Original price discount_percent: Discount percentage (0-100)

Returns: Final price after discount

schedule_meeting (Line 53)

def schedule_meeting(date, time, attendees, topic):
    """Schedule a meeting.

Args:
    date: Meeting date (YYYY-MM-DD)
    time: Meeting time (HH:MM)
    attendees: List of attendee names
    topic: Meeting topic

Returns:
    Confirmation message"""

Purpose: Schedule a meeting.

Args: date: Meeting date (YYYY-MM-DD) time: Meeting time (HH:MM) attendees: List of attendee names topic: Meeting topic

Returns: Confirmation message

send_email (Line 68)

def send_email(to, subject, body):
    """Send an email.

Args:
    to: Recipient email address
    subject: Email subject
    body: Email body

Returns:
    Confirmation message"""

Purpose: Send an email.

Args: to: Recipient email address subject: Email subject body: Email body

Returns: Confirmation message

query_database (Line 82)

def query_database(table, filters):
    """Query database table.

Args:
    table: Table name
    filters: Dictionary of filter conditions

Returns:
    List of matching records"""

Purpose: Query database table.

Args: table: Table name filters: Dictionary of filter conditions

Returns: List of matching records

create_report (Line 99)

def create_report(report_type, data_source, format):
    """Generate a report.

Args:
    report_type: Type of report (sales, analytics, etc.)
    data_source: Data source name
    format: Output format (pdf, excel, html)

Returns:
    Report generation confirmation"""

Purpose: Generate a report.

Args: report_type: Type of report (sales, analytics, etc.) data_source: Data source name format: Output format (pdf, excel, html)

Returns: Report generation confirmation


Design Patterns Used

  • Strategy Pattern: Different execution strategies for various scenarios
  • State Pattern: Managing different states in the workflow
  • Chain of Responsibility: Sequential processing through workflow nodes
  • Observer Pattern: Tracking and monitoring pattern execution
  • Factory Pattern: Creating instances with proper configuration

Usage Example

# Initialize the pattern
pattern = FunctionDefinition(name="example", model_name="gpt-4")

# Configure and execute
result = pattern.execute(data={"key": "value"})

# Check results
print(result)

Key Concepts

  • LLM Integration: Uses LangChain/OpenAI for intelligent decision-making
  • Workflow Orchestration: LangGraph manages execution flow through nodes
  • State Management: TypedDict defines workflow state structure
  • Error Handling: Comprehensive error handling and fallback mechanisms
  • Extensibility: Designed for easy customization and extension

Extension Points

This pattern can be extended by: 1. Adding custom workflow nodes 2. Implementing additional validation logic 3. Integrating with external services 4. Customizing LLM prompts and responses 5. Adding monitoring and logging capabilities

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *