Skip to main content

Human Handover Protocol

KAIU’s AI system automatically detects when a customer needs human assistance and seamlessly transfers the conversation to a live agent. This ensures complex issues, complaints, or explicit requests for human support are handled appropriately.

Handover Flow

Keyword Detection

Implementation (queue.js:86-116)

// --- HANDOVER CHECK ---
const HANDOVER_KEYWORDS = /\b(humano|agente|asesor|persona|queja|reclamo|ayuda|contactar|hablar con alguien)\b/i;

if (HANDOVER_KEYWORDS.test(text)) {
    console.log(`🚨 Handover triggered for ${from} by text: "${text}"`);
    
    // 1. Disable Bot
    await prisma.whatsAppSession.update({
        where: { id: session.id },
        data: { 
            isBotActive: false,
            handoverTrigger: "KEYWORD_DETECTED",
            sessionContext: { ...session.sessionContext, history } // Save history including trigger msg
        }
    });

    // Emit Status Update
    if (io) io.emit('session_update', { id: session.id, status: 'handover' });

    // 2. Send "Connect to Agent" Message
    await axios.post(
        `https://graph.facebook.com/v21.0/${process.env.WHATSAPP_PHONE_ID}/messages`,
        {
            messaging_product: "whatsapp",
            to: from,
            text: { body: "Te estoy transfiriendo con un asesor humano. Un momento por favor." }
        },
        { headers: { 
            'Authorization': `Bearer ${process.env.WHATSAPP_ACCESS_TOKEN}`, 
            'Content-Type': 'application/json' 
        }}
    );
    
    console.log(`✅ Handover executed for ${from}`);
    return; // STOP AI PROCESSING
}

Trigger Keywords

The system detects these Spanish keywords (case-insensitive):
CategoryKeywords
Human Requesthumano, agente, asesor, persona
Complaintsqueja, reclamo
Help Requestsayuda, contactar
Phraseshablar con alguien
The regex uses word boundaries (\b) to prevent false positives from partial matches like “humanoid” or “agencia”.

Example Conversations

User: "Quiero hablar con un agente humano"

→ HANDOVER TRIGGERED
→ Bot disabled
→ Response: "Te estoy transfiriendo con un asesor humano. Un momento por favor."

Database State

WhatsAppSession Model

model WhatsAppSession {
  id              String   @id @default(uuid())
  phoneNumber     String   @unique
  isBotActive     Boolean  @default(true)  // Set to false on handover
  sessionContext  Json?    // Preserves conversation history
  handoverTrigger String?  // "KEYWORD_DETECTED" | "MANUAL" | "TIMEOUT"
  expiresAt       DateTime
  userId          String?  @unique
  user            User?    @relation(fields: [userId], references: [id])
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
}

State Transitions

FieldBefore HandoverAfter Handover
isBotActivetruefalse
handoverTriggernull"KEYWORD_DETECTED"
sessionContext.historyArray of messagesPreserved (includes trigger)

Real-time Dashboard Updates

Socket.IO Events (queue.js:101)

if (io) {
    io.emit('session_update', { 
        id: session.id, 
        status: 'handover' 
    });
}
The admin dashboard receives this event and:
  1. Highlights the session in the sidebar
  2. Shows a red “Handover” badge
  3. Plays an optional alert sound
  4. Opens the conversation for agent response

Bot Deactivation Behavior

Incoming Messages After Handover (queue.js:56-59)

if (!session.isBotActive) {
    console.log(`⏸️ Bot inactive for ${from}. Skipping.`);
    return; // Job completes without AI processing
}
Once isBotActive is set to false:
  • Messages are still received and stored
  • No AI processing occurs
  • Agent dashboard shows all messages
  • Agent can respond manually via dashboard
The bot will NOT automatically re-activate. An admin must manually re-enable it via the dashboard or API.

Manual Re-activation

To re-enable the bot after resolution:
// Admin API endpoint
await prisma.whatsAppSession.update({
    where: { id: sessionId },
    data: { 
        isBotActive: true,
        handoverTrigger: null
    }
});

Handover Message Customization

Edit the transfer message in queue.js:109:
await axios.post(
    `https://graph.facebook.com/v21.0/${process.env.WHATSAPP_PHONE_ID}/messages`,
    {
        messaging_product: "whatsapp",
        to: from,
        text: { 
            body: "Te estoy transfiriendo con un asesor humano. Un momento por favor." 
        }
    },
    { headers: { 
        'Authorization': `Bearer ${process.env.WHATSAPP_ACCESS_TOKEN}`, 
        'Content-Type': 'application/json' 
    }}
);

Adding Custom Keywords

English Support

const HANDOVER_KEYWORDS = /\b(humano|agente|asesor|persona|queja|reclamo|ayuda|contactar|hablar con alguien|human|agent|help|support|speak to someone)\b/i;

Category-Specific Triggers

const URGENT_KEYWORDS = /\b(urgente|emergencia|critico)\b/i;
const COMPLAINT_KEYWORDS = /\b(queja|reclamo|insatisfecho|devolucion)\b/i;

if (URGENT_KEYWORDS.test(text)) {
    handoverTrigger = "URGENT_DETECTED";
} else if (COMPLAINT_KEYWORDS.test(text)) {
    handoverTrigger = "COMPLAINT_DETECTED";
} else if (HANDOVER_KEYWORDS.test(text)) {
    handoverTrigger = "KEYWORD_DETECTED";
}

Advanced: Sentiment-Based Handover

For production, consider adding sentiment analysis:
import Sentiment from 'sentiment';

const sentiment = new Sentiment();
const result = sentiment.analyze(text);

if (result.score < -3) {
    // Very negative sentiment
    console.log(`🚨 Negative sentiment detected: ${result.score}`);
    handoverTrigger = "NEGATIVE_SENTIMENT";
    // Trigger handover
}

Conversation History Preservation

The full conversation history is preserved on handover:
await prisma.whatsAppSession.update({
    where: { id: session.id },
    data: { 
        isBotActive: false,
        handoverTrigger: "KEYWORD_DETECTED",
        sessionContext: { 
            ...session.sessionContext, 
            history // Includes the trigger message
        } 
    }
});
This allows the human agent to:
  • See the full context
  • Understand why handover was triggered
  • Continue the conversation seamlessly

Handover Analytics

Track handover metrics:
-- Count handovers by trigger
SELECT 
    "handoverTrigger", 
    COUNT(*) as count,
    AVG(EXTRACT(EPOCH FROM ("updatedAt" - "createdAt"))) as avg_duration_seconds
FROM whatsapp_sessions
WHERE "handoverTrigger" IS NOT NULL
GROUP BY "handoverTrigger"
ORDER BY count DESC;

Best Practices

Preserve Context

Always save conversation history on handover so agents have full context

Immediate Response

Send transfer message instantly to acknowledge the handover request

Clear Keywords

Use unambiguous keywords to prevent false positives

Alert Agents

Use real-time notifications to ensure agents see handover requests

Testing Handovers

// Test messages
const testCases = [
    "Quiero hablar con un agente", // Should trigger
    "Tengo una queja",             // Should trigger
    "Necesito ayuda con mi pedido", // Should trigger
    "¿Tienen lavanda?",             // Should NOT trigger
    "Esto es una humanidad"         // Should NOT trigger (partial match)
];

for (const text of testCases) {
    const matches = HANDOVER_KEYWORDS.test(text);
    console.log(`"${text}" → ${matches ? 'HANDOVER' : 'NORMAL'}`);
}

Next Steps

Dashboard Integration

Learn how agents receive handover notifications

PII Privacy

Understand how sensitive data is protected during handovers

Build docs developers (and LLMs) love