AI Voice Agents for Customer Support

Deploy intelligent AI voice agents for 24/7 customer support with automated tier-1 handling, FAQ resolution, ticket creation, smart escalation, and helpdesk integration.

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, &params); 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, &params); 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, &params); 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, &params); 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