Bot WA AI dengan Sentiment Analysis
Cara membuat bot AI WhatsApp dengan sentiment analysis. Deteksi emosi customer, prioritas komplain. Tutorial lengkap!
Sentiment analysis = Pahami emosi customer!
Bot yang bisa deteksi emosi customer dapat respond lebih tepat, prioritaskan yang marah, dan escalate otomatis ke human.
Kenapa Sentiment Analysis?
📊 MANFAAT:
1. DETECT ANGRY CUSTOMERS
→ Prioritas tinggi
→ Escalate ke senior
2. ADAPT RESPONSE TONE
→ Marah: Lebih empati
→ Happy: Lebih casual
3. EARLY WARNING
→ Detect sebelum escalate
→ Proactive intervention
4. ANALYTICS
→ Track customer satisfaction
→ Identify pain pointsSentiment Categories
😊 SENTIMENT LEVELS:
POSITIVE:
- Senang, puas, terima kasih
- "Bagus banget!", "Makasih ya!"
- Score: 0.6 to 1.0
NEUTRAL:
- Informatif, bertanya
- "Mau tanya harga", "Ada warna lain?"
- Score: 0.4 to 0.6
NEGATIVE:
- Kecewa, marah, frustrasi
- "Kok lama!", "Mengecewakan!"
- Score: 0.0 to 0.4
URGENT/CRITICAL:
- Sangat marah, ancaman
- "Mau report!", "Penipuan!"
- Score: < 0.2 + keywordsImplementation
Using OpenAI for Sentiment:
javascript
async function analyzeSentiment(message) {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{
role: 'user',
content: `Analyze the sentiment of this Indonesian message.
Return JSON with:
- sentiment: "positive", "neutral", "negative", or "critical"
- score: 0.0 to 1.0 (0 = very negative, 1 = very positive)
- emotion: primary emotion detected
- urgency: "low", "medium", "high"
- keywords: negative keywords found (if any)
Message: "${message}"
Return only valid JSON, no explanation.`
}],
response_format: { type: 'json_object' }
});
return JSON.parse(response.choices[0].message.content);
}
// Example results:
// "Terima kasih, pelayanannya bagus!"
// → { sentiment: "positive", score: 0.9, emotion: "grateful", urgency: "low" }
// "Kok pesanan saya belum sampai? Sudah seminggu!"
// → { sentiment: "negative", score: 0.3, emotion: "frustrated", urgency: "high" }
// "PENIPUAN! Mau saya laporkan ke polisi!"
// → { sentiment: "critical", score: 0.1, emotion: "angry", urgency: "high", keywords: ["penipuan", "laporkan"] }Keyword-Based (Faster/Cheaper):
javascript
const sentimentKeywords = {
positive: [
'terima kasih', 'makasih', 'thanks', 'bagus', 'mantap',
'puas', 'senang', 'suka', 'keren', 'recommended',
'cepat', 'ramah', 'helpful', 'love', '👍', '❤️', '😊'
],
negative: [
'kecewa', 'marah', 'kesal', 'lambat', 'lama',
'jelek', 'buruk', 'parah', 'zonk', 'nyesel',
'bohong', 'tipu', 'rugi', 'kapok', '😡', '😤', '💢'
],
critical: [
'penipuan', 'penipu', 'scam', 'report', 'laporkan',
'polisi', 'hukum', 'viral', 'sebarkan'
]
};
function quickSentimentAnalysis(message) {
const lowerMessage = message.toLowerCase();
let positiveScore = 0;
let negativeScore = 0;
let criticalFound = false;
const foundKeywords = [];
// Check critical first
for (const keyword of sentimentKeywords.critical) {
if (lowerMessage.includes(keyword)) {
criticalFound = true;
foundKeywords.push(keyword);
}
}
if (criticalFound) {
return {
sentiment: 'critical',
score: 0.1,
urgency: 'high',
keywords: foundKeywords
};
}
// Check positive/negative
for (const keyword of sentimentKeywords.positive) {
if (lowerMessage.includes(keyword)) positiveScore++;
}
for (const keyword of sentimentKeywords.negative) {
if (lowerMessage.includes(keyword)) {
negativeScore++;
foundKeywords.push(keyword);
}
}
// Calculate final sentiment
const total = positiveScore + negativeScore;
if (total === 0) {
return { sentiment: 'neutral', score: 0.5, urgency: 'low' };
}
const score = positiveScore / total;
if (score > 0.6) {
return { sentiment: 'positive', score, urgency: 'low' };
} else if (score < 0.4) {
return {
sentiment: 'negative',
score,
urgency: negativeScore > 2 ? 'high' : 'medium',
keywords: foundKeywords
};
}
return { sentiment: 'neutral', score, urgency: 'low' };
}Integrated Chat Handler
javascript
async function handleMessageWithSentiment(userId, userMessage) {
// 1. Analyze sentiment
const sentiment = await analyzeSentiment(userMessage);
// 2. Store sentiment data
await storeSentimentData(userId, sentiment);
// 3. Handle based on sentiment
if (sentiment.sentiment === 'critical' || sentiment.urgency === 'high') {
return await handleCriticalMessage(userId, userMessage, sentiment);
}
// 4. Adapt system prompt based on sentiment
const systemPrompt = buildPromptWithSentiment(sentiment);
// 5. Generate response
const response = await generateResponse(userId, userMessage, systemPrompt);
return response;
}
function buildPromptWithSentiment(sentiment) {
let basePrompt = `Kamu adalah CS AI untuk [BRAND].`;
switch (sentiment.sentiment) {
case 'negative':
basePrompt += `
PENTING: Customer ini sedang kecewa/marah.
- Tunjukkan empati terlebih dahulu
- Minta maaf atas ketidaknyamanan
- Tawarkan solusi konkret
- Jangan defensive
- Tone: sangat sopan dan pengertian`;
break;
case 'positive':
basePrompt += `
Customer ini senang/puas.
- Apresiasi feedback positif
- Tone: ramah dan cheerful
- Boleh lebih casual`;
break;
default:
basePrompt += `
Tone: ramah dan helpful`;
}
return basePrompt;
}
async function handleCriticalMessage(userId, userMessage, sentiment) {
// 1. Send calming response
const calmResponse = `Kak, kami sangat menyesal mendengar pengalaman ini 😔
Masalah kakak adalah prioritas kami. Tim senior akan langsung menghubungi dalam 10 menit.
Mohon tunggu sebentar ya kak. Kami akan selesaikan ini. 🙏`;
// 2. Alert human agents immediately
await alertSeniorAgent({
userId,
message: userMessage,
sentiment,
priority: 'CRITICAL',
timestamp: new Date()
});
// 3. Create high-priority ticket
await createTicket({
userId,
message: userMessage,
priority: 1, // Highest
sentiment: sentiment.sentiment,
keywords: sentiment.keywords
});
return calmResponse;
}Sentiment Dashboard
javascript
// API untuk dashboard
router.get('/analytics/sentiment', async (req, res) => {
const { startDate, endDate } = req.query;
const stats = await db.sentimentLogs.aggregate([
{
$match: {
timestamp: {
$gte: new Date(startDate),
$lte: new Date(endDate)
}
}
},
{
$group: {
_id: '$sentiment',
count: { $sum: 1 },
avgScore: { $avg: '$score' }
}
}
]).toArray();
// Calculate overall satisfaction
const total = stats.reduce((acc, s) => acc + s.count, 0);
const positive = stats.find(s => s._id === 'positive')?.count || 0;
const satisfactionRate = ((positive / total) * 100).toFixed(1);
res.json({
stats,
total,
satisfactionRate: `${satisfactionRate}%`
});
});
// Sentiment trends over time
router.get('/analytics/sentiment/trend', async (req, res) => {
const trend = await db.sentimentLogs.aggregate([
{
$group: {
_id: {
date: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } },
sentiment: '$sentiment'
},
count: { $sum: 1 }
}
},
{ $sort: { '_id.date': 1 } }
]).toArray();
res.json(trend);
});
// Top negative keywords
router.get('/analytics/sentiment/keywords', async (req, res) => {
const keywords = await db.sentimentLogs.aggregate([
{ $match: { sentiment: { $in: ['negative', 'critical'] } } },
{ $unwind: '$keywords' },
{
$group: {
_id: '$keywords',
count: { $sum: 1 }
}
},
{ $sort: { count: -1 } },
{ $limit: 20 }
]).toArray();
res.json(keywords);
});Proactive Intervention
javascript
// Monitor conversation sentiment trend
async function monitorConversationSentiment(userId) {
const recentMessages = await db.sentimentLogs
.find({ oderId
userId })
.sort({ timestamp: -1 })
.limit(5)
.toArray();
// Check if sentiment is declining
const scores = recentMessages.map(m => m.score);
const avgRecent = scores.slice(0, 2).reduce((a, b) => a + b, 0) / 2;
const avgOlder = scores.slice(2).reduce((a, b) => a + b, 0) / 3;
if (avgRecent < avgOlder - 0.2) {
// Sentiment declining significantly
await triggerProactiveIntervention(userId);
}
}
async function triggerProactiveIntervention(userId) {
// Alert agent to take over
await alertAgent({
type: 'SENTIMENT_DECLINING',
userId,
message: 'Customer sentiment menurun, perlu intervensi'
});
// Or send proactive message
await sendWhatsApp(userId,
`Hai kak, sepertinya ada yang kurang memuaskan ya?
Boleh ceritakan lebih detail biar kami bisa bantu dengan lebih baik?
Atau mau langsung chat dengan tim kami? 🙏`
);
}Best Practices
DO ✅
- Respond empati untuk negative
- Escalate critical immediately
- Track sentiment trends
- Use keywords + AI combination
- Proactive intervention
- Learn from patternsDON'T ❌
- Ignore negative sentiment
- Same response for all
- No escalation process
- Rely only on keywords
- No tracking
- Defensive responsesFAQ
Seberapa akurat sentiment analysis?
AI-based: 85-95% akurat. Keyword-based: 70-80%. Kombinasi keduanya terbaik.
Perlu analyze semua message?
Ya, tapi bisa tiered. Quick keyword check dulu, AI untuk yang ambiguous.
Kesimpulan
Sentiment Analysis = Better CX!
| No Sentiment | With Sentiment |
|---|---|
| Same response | Adaptive |
| Miss angry customers | Prioritized |
| No early warning | Proactive |