AI Voice Agents for Customer Support
Transform your customer support operations with AI voice agents that handle tier-1 inquiries, resolve FAQs instantly, create tickets automatically, and escalate to human agents when needed.
Overview
Customer support AI voice agents can reduce call center costs by 40-60% while maintaining high customer satisfaction through:
- 24/7 Availability: Handle calls outside business hours
- Instant Response: Zero wait times for common inquiries
- Consistent Quality: Same professional service every call
- Scalable Capacity: Handle surge volumes without staffing
┌─────────────────────────────────────────────────────────────────────────────┐
│ Customer Support Voice Agent Flow │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Inbound Call │
│ │ │
│ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Authentication │────▶│ Intent Detection│────▶│ Resolution Path │ │
│ │ & Greeting │ │ & Classification│ │ Selection │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │ │
│ ┌───────────────────┬───────────────────────────┤ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ FAQ Resolution │ │ Ticket Creation │ │ Human Escalation│ │
│ │ (Knowledge Base)│ │ (Helpdesk API) │ │ (Warm Transfer) │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │ │ │ │
│ └───────────────────┴───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Post-Call │ │
│ │ Analytics & CRM │ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Use Cases
1. Tier-1 Support Automation
Handle common support requests without human intervention:
| Request Type | Example Queries | Resolution Method |
|---|---|---|
| Order Status | "Where's my order?" | API lookup |
| Account Info | "What's my balance?" | Database query |
| Password Reset | "I forgot my password" | Trigger reset flow |
| Store Hours | "When are you open?" | Knowledge base |
| Refund Status | "Where's my refund?" | CRM lookup |
2. FAQ Handling
Resolve frequently asked questions instantly:
type FAQHandler struct {
knowledgeBase *KnowledgeBase
vectorStore *VectorStore
}
func (h *FAQHandler) HandleQuery(ctx context.Context, query string) (string, error) {
// Semantic search for relevant FAQ entries
results, err := h.vectorStore.Search(ctx, query, SearchOptions{
TopK: 5,
Threshold: 0.75,
Collection: "faq",
})
if err != nil {
return "", err
}
if len(results) == 0 || results[0].Score < 0.75 {
return "", ErrNoMatchingFAQ
}
// Return the best matching answer
return results[0].Answer, nil
}
3. Automated Ticket Creation
Create support tickets with full context:
type TicketCreator struct {
helpdeskClient *HelpdeskClient
}
func (t *TicketCreator) CreateTicket(ctx context.Context, params TicketParams) (*Ticket, error) {
ticket := &Ticket{
Subject: params.Summary,
Description: buildDescription(params),
Priority: inferPriority(params.Sentiment, params.IssueType),
Category: params.Category,
CustomerID: params.CustomerID,
CustomerPhone: params.CallerPhone,
Source: "voice_agent",
Tags: params.ExtractedTags,
CustomFields: map[string]any{
"call_id": params.CallID,
"transcript": params.Transcript,
"sentiment": params.Sentiment,
"call_duration": params.Duration,
},
}
return t.helpdeskClient.CreateTicket(ctx, ticket)
}
func buildDescription(params TicketParams) string {
return fmt.Sprintf(`
## Issue Summary
%s
## Customer Context
- Phone: %s
- Account ID: %s
- Call Duration: %s
## Conversation Transcript
%s
## AI Agent Analysis
- Sentiment: %s
- Category: %s
- Suggested Resolution: %s
`,
params.Summary,
params.CallerPhone,
params.CustomerID,
params.Duration,
params.Transcript,
params.Sentiment,
params.Category,
params.SuggestedResolution,
)
}
4. Smart Escalation
Route complex issues to the right human agents:
type EscalationRouter struct {
queueManager *QueueManager
skillMatrix *SkillMatrix
}
type EscalationCriteria struct {
ExplicitRequest bool // Customer asked for human
HighValueCustomer bool // VIP or high LTV
NegativeSentiment bool // Frustrated customer
ComplexIssue bool // Multi-part problem
PolicyException bool // Requires manager approval
FailedAttempts int // AI couldn't resolve
IssueCategories []string // Technical, billing, etc.
}
func (r *EscalationRouter) ShouldEscalate(ctx context.Context, criteria EscalationCriteria) bool {
// Immediate escalation triggers
if criteria.ExplicitRequest {
return true
}
if criteria.HighValueCustomer && criteria.NegativeSentiment {
return true
}
if criteria.FailedAttempts >= 2 {
return true
}
if criteria.PolicyException {
return true
}
return false
}
func (r *EscalationRouter) Route(ctx context.Context, params EscalationParams) error {
// Find best available agent based on skills
agent, err := r.skillMatrix.FindBestAgent(ctx, SkillQuery{
Categories: params.IssueCategories,
Language: params.Language,
Priority: params.Priority,
})
if err != nil {
// Fallback to general queue
return r.queueManager.AddToQueue(ctx, "general", params)
}
// Warm transfer with context
return r.transferWithBriefing(ctx, agent, params)
}
func (r *EscalationRouter) transferWithBriefing(ctx context.Context, agent *Agent, params EscalationParams) error {
briefing := fmt.Sprintf(
"Incoming transfer: %s calling about %s. Sentiment: %s. Summary: %s",
params.CustomerName,
params.IssueCategory,
params.Sentiment,
params.ConversationSummary,
)
return r.queueManager.TransferToAgent(ctx, agent.ID, params.CallID, briefing)
}
5. After-Hours Support
Handle calls when human agents are unavailable:
type AfterHoursHandler struct {
businessHours *BusinessHoursConfig
callbackQueue *CallbackQueue
voicemailBox *VoicemailService
}
func (h *AfterHoursHandler) HandleCall(ctx context.Context, session *Session) error {
isBusinessHours := h.businessHours.IsOpen(time.Now())
if !isBusinessHours {
// Play after-hours greeting
session.TTS.Speak("Thank you for calling. Our office is currently closed. " +
"I'm an AI assistant and can help with many requests.")
// Try to resolve with AI
resolved, err := h.attemptResolution(ctx, session)
if resolved {
return nil
}
// Offer callback or voicemail
return h.offerAlternatives(ctx, session)
}
return nil
}
func (h *AfterHoursHandler) offerAlternatives(ctx context.Context, session *Session) error {
session.TTS.Speak("I wasn't able to fully resolve your issue. " +
"Would you like to schedule a callback during business hours, " +
"or leave a voicemail?")
// Wait for response and route accordingly
response := session.WaitForResponse(ctx, 10*time.Second)
if containsIntent(response, "callback") {
return h.scheduleCallback(ctx, session)
}
return h.recordVoicemail(ctx, session)
}
func (h *AfterHoursHandler) scheduleCallback(ctx context.Context, session *Session) error {
session.TTS.Speak("When would you like us to call you back?")
preferredTime := session.WaitForResponse(ctx, 15*time.Second)
callback := &CallbackRequest{
CustomerPhone: session.CallerNumber,
PreferredTime: parseTimePreference(preferredTime),
Context: session.GetConversationSummary(),
Priority: inferPriority(session.Sentiment),
}
err := h.callbackQueue.Schedule(ctx, callback)
if err != nil {
session.TTS.Speak("I'm sorry, I couldn't schedule the callback. Please try again later.")
return err
}
session.TTS.Speak(fmt.Sprintf(
"I've scheduled a callback for %s. You'll receive a call from our support team.",
formatTimeForSpeech(callback.PreferredTime),
))
return nil
}
Agent Configuration
Complete Configuration Example
{
"agent": {
"name": "TechSupport AI",
"description": "Tier-1 technical support voice agent",
"language": "en-US",
"llmProvider": "gemini-2.5",
"llmModel": "gemini-2.5-flash-lite",
"llmTemperature": 0.5,
"sttProvider": "deepgram",
"sttModel": "nova-3",
"sttConfig": {
"endpointing": 300,
"interimResults": true,
"punctuate": true,
"profanityFilter": false,
"keywords": [
"order:2", "refund:2", "cancel:2",
"password:2", "account:2", "billing:2"
]
},
"ttsProvider": "cartesia",
"ttsVoice": "95856005-0332-41b0-935f-352e296aa0df",
"ttsSpeed": 1.0,
"greetingMessage": "Thank you for calling TechCorp Support. My name is Alex, your virtual assistant. How can I help you today?",
"endCallMessage": "Thank you for calling TechCorp. Have a great day!",
"allowInterruptions": true,
"interruptionThreshold": 150,
"silenceTimeout": 10000,
"maxCallDuration": 900000,
"prompt": "...",
"tools": [],
"transferConfig": {
"enabled": true,
"warmTransfer": true,
"departments": {
"technical": "+14155551001",
"billing": "+14155551002",
"sales": "+14155551003",
"manager": "+14155551004"
},
"queueIntegration": {
"type": "zendesk",
"queueId": "support_queue_1"
}
},
"webhooks": {
"url": "https://api.yourcompany.com/webhooks/voice",
"events": [
"call.started",
"call.ended",
"transcript.updated",
"ticket.created",
"transfer.initiated"
],
"headers": {
"Authorization": "Bearer {{WEBHOOK_SECRET}}"
}
},
"analytics": {
"trackSentiment": true,
"trackTopics": true,
"trackResolution": true,
"customEvents": ["faq_used", "kb_search", "escalation_reason"]
}
}
}
System Prompt
Optimized Support Agent Prompt
You are Alex, a friendly and knowledgeable customer support agent for TechCorp.
## Your Mission
Help customers resolve their issues quickly and completely. Your goal is first-call resolution whenever possible.
## Communication Style
- Speak naturally and conversationally, like a real person
- Keep responses concise: 1-2 sentences for simple queries, 2-3 for complex ones
- Use the customer's name when you know it
- Be empathetic but efficient
- Avoid filler words and robotic phrases
- Never say "I'm an AI" unless directly asked
## Authentication Flow
Before accessing account information:
1. Greet the customer warmly
2. Ask: "May I have your account number or the phone number on your account?"
3. Verify: "And can you confirm the last 4 digits of your registered email?"
4. Only proceed after verification
## Issue Resolution Framework
### Step 1: Active Listening
- Let the customer explain fully before responding
- Acknowledge their concern: "I understand you're having trouble with..."
- Clarify if needed: "Just to make sure I understand..."
### Step 2: Information Gathering
- Ask ONE question at a time
- Use available tools to look up information
- Don't make the customer repeat themselves
### Step 3: Resolution
- Provide clear, actionable solutions
- For multi-step solutions, break them down clearly
- Confirm understanding: "Does that make sense?"
### Step 4: Verification
- Ask if the issue is resolved
- Check if they need help with anything else
- Thank them for their patience
## Tool Usage Guidelines
### get_order_status
- Use when customer asks about order, delivery, or shipment
- Always provide: current status, expected date, tracking if available
### search_knowledge_base
- Use for product questions, how-to queries, policy questions
- If no good match, acknowledge and offer alternatives
### create_ticket
- Use when you cannot fully resolve the issue
- Include: detailed summary, customer sentiment, attempted resolutions
- Inform customer: "I've created a support ticket..."
### transfer_to_human
- Use for: explicit requests, billing disputes over $100, angry customers,
issues requiring system access you don't have
- Always brief the customer: "I'm connecting you to a specialist..."
## Escalation Rules
IMMEDIATELY transfer when:
- Customer says "speak to a human" or "talk to a person"
- Issue involves refunds over $100
- Customer mentions "lawyer", "sue", or "report to"
- After 2 failed resolution attempts
- Customer is clearly very upset or frustrated
## What You Can Handle
- Order status and tracking
- Account information lookups
- Password reset initiation
- FAQ and how-to questions
- Basic troubleshooting
- Appointment scheduling
- Ticket creation
## What Requires Human Agent
- Refunds over $100
- Account closures
- Billing disputes
- Technical escalations
- Complaints about staff
- Legal or compliance issues
## Prohibited Actions
- Never share other customers' information
- Never make promises about refunds without system confirmation
- Never provide legal, medical, or financial advice
- Never guess at information - look it up or ask
- Never argue with the customer
## Sample Responses
Customer: "My order hasn't arrived"
You: "I'm sorry to hear that. Let me look into it right away. Could you give me your order number?"
Customer: "This is ridiculous, I've been waiting for weeks!"
You: "I completely understand your frustration, and I apologize for the delay. Let me find out exactly where your order is and see what we can do to make this right."
Customer: "I want to cancel my subscription"
You: "I can help you with that. Before I process the cancellation, may I ask what prompted this decision? There might be something I can help with."
Customer: "Just connect me to a real person"
You: "Of course, I'll transfer you right away. One moment please."
Tools for Customer Support
1. Get Order Status
{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Retrieve the current status of a customer order including shipping details and tracking information",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order ID (e.g., ORD-12345)"
},
"customer_phone": {
"type": "string",
"description": "Customer phone number for verification"
}
},
"required": ["order_id"]
}
}
}
2. Search Knowledge Base
{
"type": "function",
"function": {
"name": "search_knowledge_base",
"description": "Search the knowledge base for answers to customer questions about products, policies, and procedures",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The customer's question or topic to search for"
},
"category": {
"type": "string",
"enum": ["products", "policies", "troubleshooting", "billing", "shipping", "returns"],
"description": "Category to search within"
}
},
"required": ["query"]
}
}
}
3. Create Support Ticket
{
"type": "function",
"function": {
"name": "create_ticket",
"description": "Create a support ticket for issues that require follow-up or human intervention",
"parameters": {
"type": "object",
"properties": {
"subject": {
"type": "string",
"description": "Brief summary of the issue (max 100 chars)"
},
"description": {
"type": "string",
"description": "Detailed description of the issue and any attempted resolutions"
},
"category": {
"type": "string",
"enum": ["order_issue", "product_defect", "billing", "technical", "account", "other"],
"description": "Issue category"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"],
"description": "Issue priority based on impact and urgency"
},
"customer_id": {
"type": "string",
"description": "Customer account ID"
}
},
"required": ["subject", "description", "category"]
}
}
}
4. Get Customer Account
{
"type": "function",
"function": {
"name": "get_customer_account",
"description": "Retrieve customer account information for verification and support",
"parameters": {
"type": "object",
"properties": {
"phone_number": {
"type": "string",
"description": "Customer's registered phone number"
},
"account_id": {
"type": "string",
"description": "Customer's account ID"
},
"email": {
"type": "string",
"description": "Customer's email address"
}
}
}
}
}
5. Transfer to Human Agent
{
"type": "function",
"function": {
"name": "transfer_to_human",
"description": "Transfer the call to a human agent. Use when customer requests it, issue is complex, or escalation criteria are met.",
"parameters": {
"type": "object",
"properties": {
"department": {
"type": "string",
"enum": ["general_support", "technical", "billing", "sales", "manager"],
"description": "Department to transfer to"
},
"reason": {
"type": "string",
"description": "Brief reason for transfer (shared with receiving agent)"
},
"priority": {
"type": "string",
"enum": ["normal", "high", "urgent"],
"description": "Transfer priority - urgent for very frustrated customers"
},
"context_summary": {
"type": "string",
"description": "Summary of conversation so far"
}
},
"required": ["department", "reason"]
}
}
}
6. Schedule Callback
{
"type": "function",
"function": {
"name": "schedule_callback",
"description": "Schedule a callback from a human agent when immediate transfer isn't possible",
"parameters": {
"type": "object",
"properties": {
"preferred_time": {
"type": "string",
"description": "Customer's preferred callback time (e.g., 'tomorrow morning', '2pm')"
},
"phone_number": {
"type": "string",
"description": "Phone number to call back"
},
"issue_summary": {
"type": "string",
"description": "Brief summary of the issue for the callback agent"
},
"department": {
"type": "string",
"enum": ["general_support", "technical", "billing"],
"description": "Department for callback"
}
},
"required": ["preferred_time", "issue_summary"]
}
}
}
Tool Handlers Implementation
package tools
import (
"context"
"encoding/json"
"fmt"
"time"
)
type SupportToolHandler struct {
orderService *OrderService
knowledgeBase *KnowledgeBaseClient
helpdeskClient *HelpdeskClient
crmClient *CRMClient
callbackQueue *CallbackQueue
transferHandler *TransferHandler
}
func (h *SupportToolHandler) Execute(ctx context.Context, call ToolCall) (any, error) {
switch call.Name {
case "get_order_status":
return h.getOrderStatus(ctx, call.Arguments)
case "search_knowledge_base":
return h.searchKnowledgeBase(ctx, call.Arguments)
case "create_ticket":
return h.createTicket(ctx, call.Arguments)
case "get_customer_account":
return h.getCustomerAccount(ctx, call.Arguments)
case "transfer_to_human":
return h.transferToHuman(ctx, call.Arguments)
case "schedule_callback":
return h.scheduleCallback(ctx, call.Arguments)
default:
return nil, fmt.Errorf("unknown function: %s", call.Name)
}
}
func (h *SupportToolHandler) getOrderStatus(ctx context.Context, args json.RawMessage) (any, error) {
var params struct {
OrderID string `json:"order_id"`
CustomerPhone string `json:"customer_phone"`
}
if err := json.Unmarshal(args, ¶ms); err != nil {
return nil, err
}
order, err := h.orderService.GetOrder(ctx, params.OrderID)
if err != nil {
return map[string]any{
"found": false,
"message": "I couldn't find that order. Please verify the order number.",
}, nil
}
response := map[string]any{
"found": true,
"order_id": order.ID,
"status": order.Status,
"created_at": order.CreatedAt.Format("January 2"),
"total": fmt.Sprintf("$%.2f", order.Total),
}
switch order.Status {
case "processing":
response["message"] = fmt.Sprintf("Your order is being processed and should ship within %d business days.",
order.EstimatedShipDays)
case "shipped":
response["shipped_at"] = order.ShippedAt.Format("January 2")
response["carrier"] = order.Carrier
response["tracking_number"] = order.TrackingNumber
response["estimated_delivery"] = order.EstimatedDelivery.Format("January 2")
response["message"] = fmt.Sprintf("Your order shipped on %s via %s. Expected delivery: %s.",
order.ShippedAt.Format("January 2"),
order.Carrier,
order.EstimatedDelivery.Format("January 2"))
case "delivered":
response["delivered_at"] = order.DeliveredAt.Format("January 2")
response["message"] = fmt.Sprintf("Your order was delivered on %s.", order.DeliveredAt.Format("January 2"))
case "cancelled":
response["message"] = "This order has been cancelled. A refund should appear within 5-7 business days."
}
return response, nil
}
func (h *SupportToolHandler) searchKnowledgeBase(ctx context.Context, args json.RawMessage) (any, error) {
var params struct {
Query string `json:"query"`
Category string `json:"category"`
}
if err := json.Unmarshal(args, ¶ms); err != nil {
return nil, err
}
results, err := h.knowledgeBase.Search(ctx, KBSearchParams{
Query: params.Query,
Category: params.Category,
TopK: 3,
Threshold: 0.7,
})
if err != nil {
return map[string]any{
"found": false,
"message": "I couldn't search the knowledge base right now. Let me try to help another way.",
}, nil
}
if len(results) == 0 {
return map[string]any{
"found": false,
"message": "I couldn't find specific information about that. Would you like me to create a ticket for follow-up?",
}, nil
}
return map[string]any{
"found": true,
"answer": results[0].Content,
"source": results[0].ArticleTitle,
"url": results[0].ArticleURL,
"related": extractTitles(results[1:]),
}, nil
}
func (h *SupportToolHandler) createTicket(ctx context.Context, args json.RawMessage) (any, error) {
var params struct {
Subject string `json:"subject"`
Description string `json:"description"`
Category string `json:"category"`
Priority string `json:"priority"`
CustomerID string `json:"customer_id"`
}
if err := json.Unmarshal(args, ¶ms); err != nil {
return nil, err
}
// Get session context
session := getSessionFromContext(ctx)
ticket, err := h.helpdeskClient.CreateTicket(ctx, &Ticket{
Subject: params.Subject,
Description: params.Description,
Category: params.Category,
Priority: normalizePriority(params.Priority),
CustomerID: params.CustomerID,
CustomerPhone: session.CallerNumber,
Source: "voice_agent",
Tags: []string{"ai_created", session.AgentID},
CustomFields: map[string]any{
"call_id": session.CallID,
"call_duration": time.Since(session.StartTime).Seconds(),
"sentiment": session.Sentiment,
"transcript": session.GetTranscript(),
},
})
if err != nil {
return map[string]any{
"success": false,
"message": "I wasn't able to create a ticket right now. Please try again later.",
}, nil
}
return map[string]any{
"success": true,
"ticket_id": ticket.ID,
"message": fmt.Sprintf("I've created ticket #%s. A support agent will follow up within %s.",
ticket.ID, getResponseTime(ticket.Priority)),
}, nil
}
func (h *SupportToolHandler) transferToHuman(ctx context.Context, args json.RawMessage) (any, error) {
var params struct {
Department string `json:"department"`
Reason string `json:"reason"`
Priority string `json:"priority"`
ContextSummary string `json:"context_summary"`
}
if err := json.Unmarshal(args, ¶ms); err != nil {
return nil, err
}
session := getSessionFromContext(ctx)
// Create transfer context
transferCtx := &TransferContext{
CustomerPhone: session.CallerNumber,
CustomerName: session.CustomerName,
Summary: params.ContextSummary,
Reason: params.Reason,
Sentiment: session.Sentiment,
CallDuration: time.Since(session.StartTime),
PreviousTopics: session.Topics,
}
err := h.transferHandler.TransferCall(ctx, TransferParams{
CallID: session.CallID,
Department: params.Department,
Priority: params.Priority,
Context: transferCtx,
Message: "I'm connecting you with a specialist who can better assist you. Please hold.",
})
if err != nil {
return map[string]any{
"success": false,
"message": "I wasn't able to connect you right now. Would you like to schedule a callback instead?",
}, nil
}
return map[string]any{
"success": true,
"message": "Transferring now.",
}, nil
}
Metrics and Analytics
Key Performance Indicators
type SupportMetrics struct {
// Resolution Metrics
FirstCallResolution float64 // % resolved without transfer
AverageHandleTime time.Duration // Total call handling time
AverageSpeedOfAnswer time.Duration // Time to answer
AbandonmentRate float64 // % calls abandoned
// Quality Metrics
CustomerSatisfaction float64 // CSAT score (1-5)
SentimentScore float64 // Average sentiment (-1 to 1)
// Volume Metrics
TotalCalls int
CallsHandledByAI int
CallsTransferred int
TicketsCreated int
// Efficiency Metrics
ContainmentRate float64 // % fully handled by AI
TransferRate float64 // % transferred to human
CallbackRate float64 // % resulted in callback
}
func (m *MetricsCollector) CalculateMetrics(period time.Duration) *SupportMetrics {
calls := m.getCallsInPeriod(period)
metrics := &SupportMetrics{
TotalCalls: len(calls),
CallsHandledByAI: countResolved(calls),
CallsTransferred: countTransferred(calls),
}
// First Call Resolution
resolved := 0
for _, call := range calls {
if call.Outcome == "resolved" && !call.WasTransferred {
resolved++
}
}
metrics.FirstCallResolution = float64(resolved) / float64(len(calls)) * 100
// Average Handle Time
totalDuration := time.Duration(0)
for _, call := range calls {
totalDuration += call.Duration
}
metrics.AverageHandleTime = totalDuration / time.Duration(len(calls))
// Containment Rate
aiHandled := 0
for _, call := range calls {
if call.FullyHandledByAI {
aiHandled++
}
}
metrics.ContainmentRate = float64(aiHandled) / float64(len(calls)) * 100
// CSAT (from post-call surveys)
surveys := m.getSurveyResponses(calls)
totalCSAT := 0.0
for _, survey := range surveys {
totalCSAT += survey.Score
}
if len(surveys) > 0 {
metrics.CustomerSatisfaction = totalCSAT / float64(len(surveys))
}
return metrics
}
Real-Time Dashboard Data
type RealtimeDashboard struct {
// Current Status
ActiveCalls int `json:"active_calls"`
AvgWaitTime int `json:"avg_wait_time_seconds"`
AgentsAvailable int `json:"agents_available"`
QueueDepth int `json:"queue_depth"`
// Today's Performance
CallsToday int `json:"calls_today"`
FCRToday float64 `json:"fcr_today"`
AvgHandleTime int `json:"avg_handle_time_seconds"`
CSATToday float64 `json:"csat_today"`
// Trends
CallVolumeTrend []int `json:"call_volume_trend"` // Last 24 hours by hour
SentimentTrend []float64 `json:"sentiment_trend"` // Last 24 hours
}
func (d *Dashboard) StreamMetrics(ctx context.Context, ws *websocket.Conn) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
metrics := d.calculateRealtimeMetrics()
ws.WriteJSON(metrics)
}
}
}
Metrics Tracking Integration
func (h *SupportToolHandler) trackMetrics(ctx context.Context, session *Session, outcome string) {
metrics.RecordCounter("support.calls.total", 1)
metrics.RecordHistogram("support.calls.duration_ms", session.Duration.Milliseconds())
metrics.RecordGauge("support.calls.sentiment", session.SentimentScore)
switch outcome {
case "resolved":
metrics.RecordCounter("support.calls.resolved", 1)
metrics.RecordCounter("support.fcr.success", 1)
case "transferred":
metrics.RecordCounter("support.calls.transferred", 1)
metrics.RecordCounter(fmt.Sprintf("support.transfers.%s", session.TransferDepartment), 1)
case "callback_scheduled":
metrics.RecordCounter("support.callbacks.scheduled", 1)
case "abandoned":
metrics.RecordCounter("support.calls.abandoned", 1)
}
// Track by category
metrics.RecordCounter(fmt.Sprintf("support.category.%s", session.IssueCategory), 1)
// Track resolution time by category
metrics.RecordHistogram(
fmt.Sprintf("support.resolution_time.%s", session.IssueCategory),
session.ResolutionTime.Milliseconds(),
)
}
Helpdesk Integrations
Zendesk Integration
package integrations
import (
"context"
"encoding/json"
"fmt"
"net/http"
)
type ZendeskClient struct {
subdomain string
email string
apiToken string
client *http.Client
}
func NewZendeskClient(subdomain, email, apiToken string) *ZendeskClient {
return &ZendeskClient{
subdomain: subdomain,
email: email,
apiToken: apiToken,
client: &http.Client{Timeout: 10 * time.Second},
}
}
type ZendeskTicket struct {
ID int64 `json:"id,omitempty"`
Subject string `json:"subject"`
Description string `json:"description"`
Priority string `json:"priority"`
Status string `json:"status"`
Type string `json:"type"`
Tags []string `json:"tags"`
CustomFields []CustomField `json:"custom_fields,omitempty"`
Requester *Requester `json:"requester,omitempty"`
}
func (c *ZendeskClient) CreateTicket(ctx context.Context, ticket *ZendeskTicket) (*ZendeskTicket, error) {
url := fmt.Sprintf("https://%s.zendesk.com/api/v2/tickets.json", c.subdomain)
payload := map[string]any{
"ticket": ticket,
}
req, err := c.newRequest(ctx, "POST", url, payload)
if err != nil {
return nil, err
}
var response struct {
Ticket ZendeskTicket `json:"ticket"`
}
if err := c.do(req, &response); err != nil {
return nil, err
}
return &response.Ticket, nil
}
func (c *ZendeskClient) SearchTickets(ctx context.Context, query string) ([]ZendeskTicket, error) {
url := fmt.Sprintf("https://%s.zendesk.com/api/v2/search.json?query=%s",
c.subdomain, url.QueryEscape(query))
req, err := c.newRequest(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
var response struct {
Results []ZendeskTicket `json:"results"`
}
if err := c.do(req, &response); err != nil {
return nil, err
}
return response.Results, nil
}
func (c *ZendeskClient) GetUser(ctx context.Context, phone string) (*ZendeskUser, error) {
query := fmt.Sprintf("type:user phone:%s", phone)
url := fmt.Sprintf("https://%s.zendesk.com/api/v2/search.json?query=%s",
c.subdomain, url.QueryEscape(query))
req, err := c.newRequest(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
var response struct {
Results []ZendeskUser `json:"results"`
}
if err := c.do(req, &response); err != nil {
return nil, err
}
if len(response.Results) == 0 {
return nil, ErrUserNotFound
}
return &response.Results[0], nil
}
// Tool handler integration
func (h *SupportToolHandler) createZendeskTicket(ctx context.Context, params CreateTicketParams) (*Ticket, error) {
session := getSessionFromContext(ctx)
// Find or create user
user, err := h.zendesk.GetUser(ctx, session.CallerNumber)
if err == ErrUserNotFound {
user, err = h.zendesk.CreateUser(ctx, &ZendeskUser{
Phone: session.CallerNumber,
Name: session.CustomerName,
})
}
if err != nil {
return nil, err
}
// Create ticket with voice context
zdTicket := &ZendeskTicket{
Subject: params.Subject,
Description: params.Description,
Priority: params.Priority,
Status: "new",
Tags: []string{"voice_agent", "ai_created"},
Requester: &Requester{ID: user.ID},
CustomFields: []CustomField{
{ID: 360012345678, Value: session.CallID}, // Call ID field
{ID: 360012345679, Value: session.Sentiment}, // Sentiment field
{ID: 360012345680, Value: session.GetTranscript()}, // Transcript
},
}
return h.zendesk.CreateTicket(ctx, zdTicket)
}
Freshdesk Integration
package integrations
type FreshdeskClient struct {
domain string
apiKey string
client *http.Client
}
func NewFreshdeskClient(domain, apiKey string) *FreshdeskClient {
return &FreshdeskClient{
domain: domain,
apiKey: apiKey,
client: &http.Client{Timeout: 10 * time.Second},
}
}
type FreshdeskTicket struct {
ID int64 `json:"id,omitempty"`
Subject string `json:"subject"`
Description string `json:"description"`
DescriptionText string `json:"description_text,omitempty"`
Status int `json:"status"` // 2=Open, 3=Pending, 4=Resolved, 5=Closed
Priority int `json:"priority"` // 1=Low, 2=Medium, 3=High, 4=Urgent
Source int `json:"source"` // 1=Email, 2=Portal, 3=Phone, 7=Chat, 9=Feedback
Type string `json:"type,omitempty"`
Tags []string `json:"tags,omitempty"`
Phone string `json:"phone,omitempty"`
Email string `json:"email,omitempty"`
RequesterID int64 `json:"requester_id,omitempty"`
GroupID int64 `json:"group_id,omitempty"`
CustomFields map[string]any `json:"custom_fields,omitempty"`
}
func (c *FreshdeskClient) CreateTicket(ctx context.Context, ticket *FreshdeskTicket) (*FreshdeskTicket, error) {
url := fmt.Sprintf("https://%s.freshdesk.com/api/v2/tickets", c.domain)
req, err := c.newRequest(ctx, "POST", url, ticket)
if err != nil {
return nil, err
}
var response FreshdeskTicket
if err := c.do(req, &response); err != nil {
return nil, err
}
return &response, nil
}
func (c *FreshdeskClient) SearchContacts(ctx context.Context, phone string) ([]FreshdeskContact, error) {
url := fmt.Sprintf("https://%s.freshdesk.com/api/v2/contacts?phone=%s",
c.domain, url.QueryEscape(phone))
req, err := c.newRequest(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
var contacts []FreshdeskContact
if err := c.do(req, &contacts); err != nil {
return nil, err
}
return contacts, nil
}
// Voice agent integration
func (h *SupportToolHandler) createFreshdeskTicket(ctx context.Context, params CreateTicketParams) (*Ticket, error) {
session := getSessionFromContext(ctx)
// Map priority to Freshdesk values
priorityMap := map[string]int{
"low": 1,
"medium": 2,
"high": 3,
"urgent": 4,
}
fdTicket := &FreshdeskTicket{
Subject: params.Subject,
Description: formatHTMLDescription(params, session),
Status: 2, // Open
Priority: priorityMap[params.Priority],
Source: 3, // Phone
Phone: session.CallerNumber,
Tags: []string{"voice_agent", "ai_created"},
CustomFields: map[string]any{
"cf_call_id": session.CallID,
"cf_sentiment": session.Sentiment,
"cf_call_duration": session.Duration.Seconds(),
},
}
return c.CreateTicket(ctx, fdTicket)
}
func formatHTMLDescription(params CreateTicketParams, session *Session) string {
return fmt.Sprintf(`
<h3>Issue Summary</h3>
<p>%s</p>
<h3>Call Details</h3>
<ul>
<li><strong>Call ID:</strong> %s</li>
<li><strong>Duration:</strong> %s</li>
<li><strong>Sentiment:</strong> %s</li>
</ul>
<h3>Conversation Transcript</h3>
<pre>%s</pre>
`,
params.Description,
session.CallID,
session.Duration.Round(time.Second),
session.Sentiment,
session.GetTranscript(),
)
}
Salesforce Service Cloud Integration
package integrations
type SalesforceClient struct {
instanceURL string
accessToken string
client *http.Client
}
type SalesforceCase struct {
ID string `json:"Id,omitempty"`
Subject string `json:"Subject"`
Description string `json:"Description"`
Status string `json:"Status"`
Priority string `json:"Priority"`
Origin string `json:"Origin"`
ContactID string `json:"ContactId,omitempty"`
AccountID string `json:"AccountId,omitempty"`
Type string `json:"Type,omitempty"`
// Custom fields
VoiceCallID__c string `json:"Voice_Call_ID__c,omitempty"`
CallTranscript__c string `json:"Call_Transcript__c,omitempty"`
CustomerSentiment__c string `json:"Customer_Sentiment__c,omitempty"`
}
func (c *SalesforceClient) CreateCase(ctx context.Context, sfCase *SalesforceCase) (*SalesforceCase, error) {
url := fmt.Sprintf("%s/services/data/v58.0/sobjects/Case", c.instanceURL)
req, err := c.newRequest(ctx, "POST", url, sfCase)
if err != nil {
return nil, err
}
var response struct {
ID string `json:"id"`
Success bool `json:"success"`
}
if err := c.do(req, &response); err != nil {
return nil, err
}
sfCase.ID = response.ID
return sfCase, nil
}
func (c *SalesforceClient) FindContactByPhone(ctx context.Context, phone string) (*SalesforceContact, error) {
query := fmt.Sprintf("SELECT Id, Name, AccountId, Phone, Email FROM Contact WHERE Phone = '%s'", phone)
url := fmt.Sprintf("%s/services/data/v58.0/query?q=%s", c.instanceURL, url.QueryEscape(query))
req, err := c.newRequest(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
var response struct {
Records []SalesforceContact `json:"records"`
}
if err := c.do(req, &response); err != nil {
return nil, err
}
if len(response.Records) == 0 {
return nil, ErrContactNotFound
}
return &response.Records[0], nil
}
Best Practices
1. Design for Resolution
Optimize for first-call resolution:
// Good: Proactive resolution
func (h *Handler) handleOrderInquiry(ctx context.Context, session *Session, orderID string) error {
order, err := h.orderService.GetOrder(ctx, orderID)
if err != nil {
return h.handleOrderNotFound(ctx, session)
}
// Proactively address common follow-up questions
response := fmt.Sprintf("Your order %s is %s.", orderID, order.Status)
switch order.Status {
case "shipped":
response += fmt.Sprintf(" It's being shipped via %s with tracking number %s. "+
"Expected delivery is %s. Would you like me to text you the tracking link?",
order.Carrier,
order.TrackingNumber,
order.EstimatedDelivery.Format("January 2"))
case "delayed":
response += fmt.Sprintf(" I see there's been a delay. The new expected date is %s. "+
"I apologize for the inconvenience. Would you like me to apply a 10%% discount to your next order?",
order.NewEstimatedDelivery.Format("January 2"))
}
return session.TTS.Speak(response)
}
2. Empathy-First Responses
Build empathy into agent responses:
var empathyResponses = map[string][]string{
"frustrated": {
"I completely understand your frustration, and I'm sorry you're dealing with this.",
"I hear you, and I want to make this right.",
"That's definitely not the experience we want for you. Let me fix this.",
},
"confused": {
"No worries, let me explain that more clearly.",
"That can be confusing. Here's how it works.",
"Great question. Let me walk you through it.",
},
"worried": {
"I understand your concern. Let me check on that right away.",
"Don't worry, I'll help you sort this out.",
"I can see why that would be worrying. Let me take a look.",
},
}
func selectEmpathyResponse(sentiment string) string {
responses := empathyResponses[sentiment]
return responses[rand.Intn(len(responses))]
}
3. Intelligent Escalation
Know when to escalate:
type EscalationAnalyzer struct {
sentimentThreshold float64
failedAttemptLimit int
highValueThreshold float64
}
func (a *EscalationAnalyzer) Analyze(session *Session) EscalationDecision {
// Score-based escalation
score := 0.0
// Customer frustration
if session.SentimentScore < a.sentimentThreshold {
score += 3.0
}
// Failed resolution attempts
if session.FailedAttempts >= a.failedAttemptLimit {
score += 2.0
}
// High-value customer
if session.CustomerLTV > a.highValueThreshold {
score += 1.5
}
// Explicit request
if session.ExplicitHumanRequest {
return EscalationDecision{
ShouldEscalate: true,
Reason: "customer_requested",
Priority: "normal",
}
}
// Policy-bound issues
if containsPolicyIssue(session.Topics) {
return EscalationDecision{
ShouldEscalate: true,
Reason: "policy_exception",
Priority: "high",
}
}
return EscalationDecision{
ShouldEscalate: score >= 4.0,
Reason: "composite_score",
Priority: getPriority(score),
}
}
4. Context Preservation
Maintain context across interactions:
type ConversationContext struct {
CustomerID string
CustomerName string
CustomerTier string
PreviousTickets []string
OpenOrders []string
LastInteraction time.Time
PreferredChannel string
Notes []string
}
func (h *Handler) loadCustomerContext(ctx context.Context, phone string) (*ConversationContext, error) {
customer, err := h.crmClient.GetCustomerByPhone(ctx, phone)
if err != nil {
return &ConversationContext{}, nil // Continue without context
}
tickets, _ := h.helpdeskClient.GetRecentTickets(ctx, customer.ID, 5)
orders, _ := h.orderService.GetOpenOrders(ctx, customer.ID)
return &ConversationContext{
CustomerID: customer.ID,
CustomerName: customer.Name,
CustomerTier: customer.Tier,
PreviousTickets: extractTicketIDs(tickets),
OpenOrders: extractOrderIDs(orders),
LastInteraction: customer.LastContactDate,
PreferredChannel: customer.PreferredChannel,
Notes: customer.SupportNotes,
}, nil
}
// Use context in conversation
func (h *Handler) greetWithContext(ctx context.Context, session *Session) string {
context := session.Context
if context.CustomerName != "" {
if time.Since(context.LastInteraction) < 24*time.Hour {
return fmt.Sprintf("Welcome back, %s! I see you called earlier today. "+
"Are you following up on the same issue?", context.CustomerName)
}
return fmt.Sprintf("Hello %s, thank you for calling. How can I help you today?",
context.CustomerName)
}
return "Thank you for calling support. How can I help you today?"
}
5. Continuous Improvement
Track and improve based on data:
type ImprovementTracker struct {
transferReasons map[string]int
unresolvedTopics map[string]int
lowCSATReasons []string
}
func (t *ImprovementTracker) Analyze() []Recommendation {
recommendations := []Recommendation{}
// High transfer rate for specific reason
for reason, count := range t.transferReasons {
if count > t.getThreshold(reason) {
recommendations = append(recommendations, Recommendation{
Type: "training_gap",
Topic: reason,
Priority: "high",
Action: fmt.Sprintf("Add knowledge base articles for: %s", reason),
})
}
}
// Unresolved topics
for topic, count := range t.unresolvedTopics {
if count > 10 {
recommendations = append(recommendations, Recommendation{
Type: "capability_gap",
Topic: topic,
Priority: "medium",
Action: fmt.Sprintf("Add tool or integration for: %s", topic),
})
}
}
return recommendations
}
Testing Support Agents
Unit Tests
func TestOrderStatusFlow(t *testing.T) {
handler := setupTestHandler()
session := createMockSession()
// Test order lookup
result, err := handler.getOrderStatus(context.Background(), json.RawMessage(`{
"order_id": "ORD-12345"
}`))
assert.NoError(t, err)
resultMap := result.(map[string]any)
assert.True(t, resultMap["found"].(bool))
assert.Contains(t, resultMap["message"].(string), "shipped")
}
func TestEscalationLogic(t *testing.T) {
analyzer := &EscalationAnalyzer{
sentimentThreshold: -0.3,
failedAttemptLimit: 2,
}
tests := []struct {
name string
session *Session
expected bool
}{
{
name: "explicit request",
session: &Session{
ExplicitHumanRequest: true,
},
expected: true,
},
{
name: "negative sentiment",
session: &Session{
SentimentScore: -0.5,
},
expected: true,
},
{
name: "happy customer",
session: &Session{
SentimentScore: 0.8,
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
decision := analyzer.Analyze(tt.session)
assert.Equal(t, tt.expected, decision.ShouldEscalate)
})
}
}
Integration Tests
func TestZendeskIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
client := NewZendeskClient(
os.Getenv("ZENDESK_SUBDOMAIN"),
os.Getenv("ZENDESK_EMAIL"),
os.Getenv("ZENDESK_API_TOKEN"),
)
// Create test ticket
ticket, err := client.CreateTicket(context.Background(), &ZendeskTicket{
Subject: "Test: Voice Agent Integration",
Description: "Automated test ticket from voice agent integration tests",
Priority: "low",
Tags: []string{"test", "automated"},
})
assert.NoError(t, err)
assert.NotEmpty(t, ticket.ID)
// Cleanup
defer client.DeleteTicket(context.Background(), ticket.ID)
}
Next Steps
- Function Calling - Deep dive into tool integration
- Call Transfer - Advanced escalation patterns
- Webhooks - Post-call integrations
- Latency Optimization - Sub-500ms responses
- Customer Support Example - Basic implementation