Function Calling

Extend your voice agent's capabilities with function calling - connect to APIs, databases, and external systems.

Function Calling

Function calling (also known as tool use) allows your voice agent to take actions beyond conversation - look up orders, schedule appointments, transfer calls, and more.

How It Works

User: "What's the status of my order 12345?"
         │
         ▼
┌─────────────────────────────────────────────────────────┐
│                         LLM                              │
│                                                          │
│  "I need to look up order 12345"                        │
│                                                          │
│  Function Call: get_order_status(order_id: "12345")     │
└─────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────┐
│                    Tool Executor                         │
│                                                          │
│  HTTP GET https://api.example.com/orders/12345          │
│  Response: { status: "shipped", eta: "Dec 29" }         │
└─────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────┐
│                         LLM                              │
│                                                          │
│  "Your order 12345 has been shipped and will            │
│   arrive by December 29th."                             │
└─────────────────────────────────────────────────────────┘
         │
         ▼
      TTS → User hears response

Defining Functions

Function Schema

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_order_status",
        "description": "Get the current status of a customer order including shipping information",
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": {
              "type": "string",
              "description": "The unique order identifier (e.g., ORD-12345)"
            }
          },
          "required": ["order_id"]
        }
      }
    }
  ]
}

Multiple Functions

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_order_status",
        "description": "Get order status and shipping info",
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": { "type": "string" }
          },
          "required": ["order_id"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "schedule_callback",
        "description": "Schedule a callback from a human agent",
        "parameters": {
          "type": "object",
          "properties": {
            "preferred_time": {
              "type": "string",
              "description": "Preferred callback time (e.g., 'tomorrow morning')"
            },
            "phone_number": {
              "type": "string",
              "description": "Phone number to call back"
            },
            "reason": {
              "type": "string",
              "description": "Reason for callback"
            }
          },
          "required": ["preferred_time", "reason"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "transfer_to_agent",
        "description": "Transfer the call to a human agent",
        "parameters": {
          "type": "object",
          "properties": {
            "department": {
              "type": "string",
              "enum": ["sales", "support", "billing"],
              "description": "Department to transfer to"
            },
            "reason": {
              "type": "string",
              "description": "Reason for transfer"
            }
          },
          "required": ["department"]
        }
      }
    }
  ]
}

Function Types

HTTP Functions

Call external APIs:

{
  "name": "get_order_status",
  "implementation": "http",
  "httpMethod": "GET",
  "httpUrl": "https://api.example.com/orders/{{order_id}}",
  "httpHeaders": {
    "Authorization": "Bearer {{API_KEY}}",
    "Content-Type": "application/json"
  },
  "responseMapping": {
    "status": "$.data.status",
    "eta": "$.data.estimated_delivery"
  }
}

Built-in Functions

Pre-defined functions that don't require external calls:

Function Description
end_call End the call with disposition
transfer_call Transfer to another number
send_dtmf Send DTMF tones
play_audio Play an audio file
set_variable Set a session variable

Custom Functions

Implement your own function handler:

// Register custom function
tools.RegisterFunction("check_inventory", func(ctx context.Context, args map[string]any) (any, error) {
    productID := args["product_id"].(string)

    // Your custom logic
    inventory, err := inventoryService.Check(productID)
    if err != nil {
        return nil, err
    }

    return map[string]any{
        "in_stock":  inventory.Quantity > 0,
        "quantity":  inventory.Quantity,
        "warehouse": inventory.Location,
    }, nil
})

Implementation

Tool Executor

type ToolExecutor struct {
    functions map[string]FunctionConfig
    httpClient *http.Client
}

func (e *ToolExecutor) Execute(ctx context.Context, call ToolCall) (any, error) {
    fn, ok := e.functions[call.Name]
    if !ok {
        return nil, fmt.Errorf("unknown function: %s", call.Name)
    }

    switch fn.Type {
    case "http":
        return e.executeHTTP(ctx, fn, call.Arguments)
    case "builtin":
        return e.executeBuiltin(ctx, fn, call.Arguments)
    case "custom":
        return fn.Handler(ctx, call.Arguments)
    default:
        return nil, fmt.Errorf("unknown function type: %s", fn.Type)
    }
}

func (e *ToolExecutor) executeHTTP(ctx context.Context, fn FunctionConfig, args map[string]any) (any, error) {
    // Build URL with variable substitution
    url := substituteVariables(fn.HTTPUrl, args)

    // Build request
    req, err := http.NewRequestWithContext(ctx, fn.HTTPMethod, url, nil)
    if err != nil {
        return nil, err
    }

    // Add headers
    for key, value := range fn.HTTPHeaders {
        req.Header.Set(key, substituteVariables(value, args))
    }

    // Execute request
    resp, err := e.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    // Parse response
    var result map[string]any
    json.NewDecoder(resp.Body).Decode(&result)

    // Apply response mapping
    return applyMapping(result, fn.ResponseMapping), nil
}

Integration with LLM

func (p *Pipeline) processWithFunctions(ctx context.Context, transcript string) string {
    messages := append(p.context.Messages, Message{
        Role:    "user",
        Content: transcript,
    })

    for {
        // Generate response (may include function calls)
        response := p.llm.Generate(ctx, messages, p.tools)

        if len(response.ToolCalls) == 0 {
            // No function calls, return text response
            return response.Content
        }

        // Execute function calls
        for _, call := range response.ToolCalls {
            result, err := p.toolExecutor.Execute(ctx, call)

            // Add function result to context
            messages = append(messages, Message{
                Role:       "assistant",
                ToolCalls:  []ToolCall{call},
            })
            messages = append(messages, Message{
                Role:      "tool",
                ToolCallID: call.ID,
                Content:   formatResult(result, err),
            })
        }

        // Continue generation with function results
    }
}

Best Practices

1. Clear Function Descriptions

// ❌ Vague description
{
  "name": "get_data",
  "description": "Gets data"
}

// ✅ Clear description
{
  "name": "get_order_status",
  "description": "Retrieves the current status of a customer order including: order status (processing, shipped, delivered), estimated delivery date, and tracking number if available. Use when customer asks about their order."
}

2. Handle Errors Gracefully

func (e *ToolExecutor) Execute(ctx context.Context, call ToolCall) (any, error) {
    result, err := e.doExecute(ctx, call)

    if err != nil {
        // Return user-friendly error message
        return map[string]any{
            "error": true,
            "message": "I wasn't able to look that up right now. Let me try something else.",
        }, nil
    }

    return result, nil
}

3. Timeout Configuration

// Set appropriate timeouts
httpClient := &http.Client{
    Timeout: 5 * time.Second, // Don't block the call too long
}

4. Validate Arguments

func validateOrderID(orderID string) error {
    if orderID == "" {
        return errors.New("order ID is required")
    }
    if !regexp.MustCompile(`^ORD-\d+$`).MatchString(orderID) {
        return errors.New("invalid order ID format")
    }
    return nil
}

5. Mute Audio During Execution

// Prevent STT from processing while function executes
func (p *Pipeline) executeFunction(ctx context.Context, call ToolCall) {
    p.user.SetExecutingFunction(true)
    defer p.user.SetExecutingFunction(false)

    // Optional: Play hold music or "one moment please"
    p.playHoldMessage()

    result := p.toolExecutor.Execute(ctx, call)
    // ...
}

Common Function Patterns

Order Lookup

{
  "name": "get_order_status",
  "httpMethod": "GET",
  "httpUrl": "https://api.store.com/orders/{{order_id}}",
  "responseMapping": {
    "status": "$.status",
    "eta": "$.estimated_delivery",
    "tracking": "$.tracking_number"
  }
}

Appointment Scheduling

{
  "name": "book_appointment",
  "httpMethod": "POST",
  "httpUrl": "https://api.calendar.com/appointments",
  "httpBody": {
    "date": "{{date}}",
    "time": "{{time}}",
    "customer_phone": "{{phone_number}}",
    "service": "{{service_type}}"
  }
}

CRM Integration

{
  "name": "log_interaction",
  "httpMethod": "POST",
  "httpUrl": "https://crm.example.com/api/interactions",
  "httpHeaders": {
    "Authorization": "Bearer {{CRM_API_KEY}}"
  },
  "httpBody": {
    "customer_phone": "{{caller_phone}}",
    "summary": "{{interaction_summary}}",
    "sentiment": "{{sentiment}}",
    "call_sid": "{{call_sid}}"
  }
}

Next Steps