Customer Support Agent
A complete example of a customer support voice agent that handles FAQs, creates tickets, and transfers to human agents when needed.
Configuration
{
"agent": {
"name": "Acme Support",
"language": "en-US",
"llmProvider": "gemini-2.5",
"llmModel": "gemini-2.5-flash-lite",
"llmTemperature": 0.7,
"sttProvider": "deepgram",
"sttModel": "nova-3",
"sttConfig": {
"endpointing": 300,
"interimResults": true,
"keywords": ["Acme:2", "order:2", "refund:2"]
},
"ttsProvider": "cartesia",
"ttsVoice": "95856005-0332-41b0-935f-352e296aa0df",
"greetingMessage": "Hello! Thank you for calling Acme Support. My name is Alex. How can I help you today?",
"allowInterruptions": true,
"prompt": "...",
"tools": [],
"webhooks": {
"url": "https://your-server.com/webhooks/voice",
"events": ["call.ended", "transcript.updated"]
}
}
}
System Prompt
You are Alex, a friendly and helpful customer support agent for Acme Corporation.
## Your Role
- Help customers with product questions, order issues, and account problems
- Look up orders and account information using the available tools
- Create support tickets for issues you can't resolve
- Transfer to a human agent for complex or sensitive issues
## Guidelines
### Response Style
- Keep responses concise (1-2 sentences max)
- Be warm, empathetic, and professional
- Use the customer's name when you know it
- Speak naturally, like a real person would
### Information Gathering
- Ask for order numbers to look up orders
- Verify identity by asking for email or phone on file
- Don't ask for more than one piece of info at a time
### Issue Resolution
1. Listen to the customer's issue completely
2. Acknowledge their concern with empathy
3. Look up relevant information
4. Provide a clear solution or next steps
5. Confirm they're satisfied before ending
### Escalation Criteria
Transfer to a human agent if:
- Customer explicitly requests a human
- Issue involves refunds over $100
- Customer is very upset or frustrated
- Issue requires manager approval
- Technical issues you can't resolve
### What NOT to do
- Never make up information
- Don't promise specific resolution times
- Don't provide legal or financial advice
- Don't share other customers' information
## Available Tools
- get_order_status: Look up order by ID
- get_account_info: Get customer account details
- create_ticket: Create a support ticket
- transfer_to_human: Connect to human agent
## Sample Conversations
Customer: "Where's my order?"
You: "I'd be happy to help you track your order. Could you give me your order number?"
Customer: "I want a refund"
You: "I understand you'd like a refund. Let me look into that for you. What's the order number?"
Customer: "I want to speak to a manager"
You: "Of course, I'll transfer you to a human agent right away. Please hold."
Tools
Get Order Status
{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Look up the status of a customer order by order ID",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order ID (e.g., ORD-12345)"
}
},
"required": ["order_id"]
}
}
}
Tool Handler:
func handleGetOrderStatus(params map[string]any) (string, error) {
orderID := params["order_id"].(string)
order, err := orderService.GetOrder(orderID)
if err != nil {
return "I couldn't find that order. Could you double-check the order number?", nil
}
return fmt.Sprintf("Order %s was placed on %s. Status: %s. %s",
order.ID,
order.CreatedAt.Format("January 2"),
order.Status,
getStatusDetails(order),
), nil
}
func getStatusDetails(order *Order) string {
switch order.Status {
case "processing":
return "It's being prepared and should ship within 1-2 business days."
case "shipped":
return fmt.Sprintf("It shipped on %s via %s. Tracking: %s. Expected delivery: %s.",
order.ShippedAt.Format("January 2"),
order.Carrier,
order.TrackingNumber,
order.EstimatedDelivery.Format("January 2"))
case "delivered":
return fmt.Sprintf("It was delivered on %s.", order.DeliveredAt.Format("January 2"))
default:
return ""
}
}
Create Ticket
{
"type": "function",
"function": {
"name": "create_ticket",
"description": "Create a support ticket for follow-up",
"parameters": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["order", "refund", "technical", "billing", "other"],
"description": "Ticket category"
},
"summary": {
"type": "string",
"description": "Brief summary of the issue"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Issue priority"
}
},
"required": ["category", "summary"]
}
}
}
Transfer to Human
{
"type": "function",
"function": {
"name": "transfer_to_human",
"description": "Transfer the call to a human agent",
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "Reason for transfer"
},
"department": {
"type": "string",
"enum": ["general", "billing", "technical", "manager"],
"description": "Department to transfer to"
}
},
"required": ["reason"]
}
}
}
Webhook Integration
Call Ended Webhook
type CallEndedPayload struct {
CallID string `json:"call_id"`
AgentID string `json:"agent_id"`
Duration int `json:"duration"`
Transcript struct {
Summary string `json:"summary"`
Turns []Turn `json:"turns"`
} `json:"transcript"`
Outcome string `json:"outcome"` // resolved, transferred, dropped
}
func handleCallEnded(payload CallEndedPayload) {
// Log to CRM
crmClient.LogCall(CRMCallLog{
CustomerPhone: payload.CallerID,
Duration: payload.Duration,
Summary: payload.Transcript.Summary,
Outcome: payload.Outcome,
})
// Update analytics
analytics.TrackCall(payload)
}
Analytics
Track key metrics:
type SupportMetrics struct {
TotalCalls int
AverageHandleTime time.Duration
FirstCallResolution float64
TransferRate float64
CustomerSatisfaction float64
}
func trackMetrics(call *Call) {
metrics.Increment("support.calls.total")
metrics.Gauge("support.calls.duration", call.Duration.Seconds())
if call.Outcome == "resolved" {
metrics.Increment("support.calls.resolved")
} else if call.Outcome == "transferred" {
metrics.Increment("support.calls.transferred")
}
}
Best Practices
1. Warm Handoff
When transferring to a human:
func transferToHuman(session *Session, reason string) {
// Brief the human agent
briefing := fmt.Sprintf(
"Customer %s calling about: %s. Context: %s",
session.CustomerName,
reason,
session.ConversationSummary,
)
session.TTS.Speak("Let me transfer you to a specialist who can help. One moment please.")
// Transfer with context
telephony.Transfer(session.CallID, humanQueue, briefing)
}
2. Handle Common Intents
var commonIntents = map[string]string{
"hours": "We're open Monday through Friday, 9 AM to 6 PM Eastern time.",
"location": "We're located at 123 Main Street. Would you like directions?",
"website": "You can visit us at acme.com. Is there something specific I can help you find?",
}
3. Empathy Phrases
var empathyPhrases = []string{
"I understand how frustrating that must be.",
"I'm sorry to hear you're having trouble with that.",
"I completely understand your concern.",
"That's definitely not the experience we want you to have.",
}
Testing
Test Scenarios
func TestOrderStatusFlow(t *testing.T) {
agent := setupTestAgent()
// Customer asks about order
response := agent.Process("Where is my order ORD-12345?")
assert.Contains(t, response, "order")
// Verify tool was called
assert.True(t, agent.ToolCalled("get_order_status"))
}
func TestTransferFlow(t *testing.T) {
agent := setupTestAgent()
response := agent.Process("I want to speak to a manager")
assert.Contains(t, response, "transfer")
assert.True(t, agent.ToolCalled("transfer_to_human"))
}
Next Steps
- Order Status - Simpler order tracking
- Function Calling - Tool integration
- Call Transfer - Human handoff