Overview
KAIU maintains stateful conversation sessions for each WhatsApp user, tracking conversation history, bot status, and handover state. Sessions expire after 24 hours of inactivity.
Session Schema
model WhatsAppSession {
id String @id @default ( cuid ())
phoneNumber String @unique
isBotActive Boolean @default ( true )
handoverTrigger String ? // "KEYWORD_DETECTED" | "MANUAL" | null
expiresAt DateTime
sessionContext Json // { history: [...] }
createdAt DateTime @default ( now ())
updatedAt DateTime @updatedAt
}
Session Creation
Sessions are created automatically when a user sends their first message.
let session = await prisma . whatsAppSession . findUnique ({ where: { phoneNumber: from } });
if ( ! session ) {
session = await prisma . whatsAppSession . create ({
data: {
phoneNumber: from ,
isBotActive: true ,
expiresAt: new Date ( Date . now () + 24 * 60 * 60 * 1000 ),
sessionContext: { history: [] }
}
});
// Emit New Session Event
if ( io ) io . emit ( 'session_new' , {
id: session . id ,
phone: from ,
time: session . updatedAt
});
}
Session Properties
User’s WhatsApp phone number (unique identifier)
Indicates if the AI bot is active for this session. Set to false during handover to human agents.
Session expiration timestamp (24 hours from creation)
Reason for handover: KEYWORD_DETECTED (automatic) or MANUAL (dashboard action)
JSON object containing conversation history and metadata {
"history" : [
{ "role" : "user" , "content" : "Hola, necesito ayuda" },
{ "role" : "assistant" , "content" : "Hola, estoy aquí para ayudarte" }
]
}
Conversation History
History Structure
Conversation history is stored in sessionContext.history as an array of message objects:
let history = ( session . sessionContext && session . sessionContext . history )
? session . sessionContext . history
: [];
const cleanText = redactPII ( text );
const userMsg = { role: 'user' , content: cleanText };
history . push ( userMsg );
// Keep only last 10 messages
if ( history . length > 10 ) history = history . slice ( - 10 );
History is limited to the last 10 messages to reduce AI context window costs and maintain performance.
PII Redaction
Before storing user messages in history, Personally Identifiable Information (PII) is redacted:
const cleanText = redactPII ( text );
While PII is redacted in stored history , the original text is sent to the AI for processing to maintain conversation quality. Only historical context is redacted.
Adding AI Responses
const aiMsg = { role: 'assistant' , content: finalText || "(Envía una imagen sin texto)" };
if ( imageUrls . length > 0 ) aiMsg . images = imageUrls ;
history . push ( aiMsg );
AI responses can include optional images array for product recommendations.
Saving History
await prisma . whatsAppSession . update ({
where: { id: session . id },
data: {
sessionContext: { ... session . sessionContext , history }
}
});
Bot Status Control
Checking Bot Status
if ( ! session . isBotActive ) {
console . log ( `⏸️ Bot inactive for ${ from } . Skipping.` );
return ;
}
When isBotActive is false, the worker skips AI processing, allowing human agents to take over.
Handover Protocol
KAIU implements an intelligent handover system that transfers conversations from AI to human agents.
Automatic Handover (Keyword Detection)
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 }
}
});
// 2. Emit Dashboard Update
if ( io ) io . emit ( 'session_update' , { id: session . id , status: 'handover' });
// 3. Send Transition 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
}
Handover Keywords (Spanish)
The following keywords trigger automatic handover:
humano - human
agente - agent
asesor - advisor
persona - person
queja - complaint
reclamo - claim
ayuda - help
contactar - contact
hablar con alguien - talk to someone
Keywords are case-insensitive and use word boundary matching to avoid false positives.
Handover Flow
Manual Handover
Agents can manually disable the bot from the dashboard:
// Dashboard action
await prisma . whatsAppSession . update ({
where: { id: sessionId },
data: {
isBotActive: false ,
handoverTrigger: "MANUAL"
}
});
Real-time Dashboard Events
The session system emits Socket.IO events for real-time dashboard updates.
New Session Event
if ( io ) io . emit ( 'session_new' , { id: session . id , phone: from , time: session . updatedAt });
Emitted when a new session is created.
New Message Event
if ( io ) {
io . to ( `session_ ${ session . id } ` ). emit ( 'new_message' , {
sessionId: session . id ,
message: { role: 'user' , content: text , time: "Just now" }
});
io . emit ( 'chat_list_update' , { sessionId: session . id });
}
Emitted when new messages (user or AI) are added to a session.
Session Update Event
if ( io ) io . emit ( 'session_update' , { id: session . id , status: 'handover' });
Emitted when session status changes (e.g., handover triggered).
Joining Session Rooms
Dashboard clients join session-specific Socket.IO rooms:
socket . on ( 'join_session' , ( sessionId ) => {
socket . join ( `session_ ${ sessionId } ` );
console . log ( `Socket ${ socket . id } joined session_ ${ sessionId } ` );
});
Session Expiration
Sessions expire 24 hours after creation:
expiresAt : new Date ( Date . now () + 24 * 60 * 60 * 1000 )
Expired sessions should be cleaned up periodically. Consider implementing a cron job to delete sessions where expiresAt < NOW().
Recommended Cleanup Job
// Run daily
import { prisma } from './db.js' ;
export async function cleanupExpiredSessions () {
const result = await prisma . whatsAppSession . deleteMany ({
where: {
expiresAt: { lt: new Date () }
}
});
console . log ( `🧹 Cleaned up ${ result . count } expired sessions` );
}
Best Practices
Context Window Management
Keeping only the last 10 messages reduces:
AI API costs
Database storage
Response latency
Privacy Compliance
PII redaction ensures:
GDPR compliance
Data minimization
Secure storage
Handover UX
Immediate response ("Te estoy transfiriendo...") ensures:
User knows handover is happening
No confusion during transition
Professional experience
Environment Variables
WhatsApp Business phone number ID for sending messages
WhatsApp API access token
Next Steps
Webhooks Learn about webhook endpoints
Queue System Understand message processing