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
- Call Transfer - Transfer to human agents
- Webhooks - Post-call integrations
- Variables - Dynamic content