223 lines
7.4 KiB
JavaScript
223 lines
7.4 KiB
JavaScript
const express = require('express');
|
|
const bodyParser = require('body-parser');
|
|
const axios = require('axios');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
require('dotenv').config();
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3001;
|
|
|
|
// Middleware
|
|
app.use(bodyParser.json());
|
|
|
|
// Chatwoot API helper
|
|
const chatwootAPI = axios.create({
|
|
baseURL: process.env.CHATWOOT_URL,
|
|
headers: {
|
|
'api_token': process.env.CHATWOOT_API_TOKEN,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
// Load FAQ/Knowledge Base
|
|
function loadKnowledgeBase() {
|
|
const kbPath = path.join(__dirname, '../FAQ-KB.md');
|
|
if (fs.existsSync(kbPath)) {
|
|
return fs.readFileSync(kbPath, 'utf8');
|
|
}
|
|
return '';
|
|
}
|
|
|
|
// Simple keyword-based intent detection
|
|
function detectIntent(message) {
|
|
const lower = message.toLowerCase();
|
|
|
|
if (lower.includes('price') || lower.includes('cost') || lower.includes('billing') || lower.includes('payment')) {
|
|
return { intent: 'billing', confidence: 0.8 };
|
|
}
|
|
|
|
if (lower.includes('bug') || lower.includes('error') || lower.includes('not working') || lower.includes('issue')) {
|
|
return { intent: 'technical_issue', confidence: 0.85 };
|
|
}
|
|
|
|
if (lower.includes('how to') || lower.includes('feature') || lower.includes('use')) {
|
|
return { intent: 'feature_request', confidence: 0.75 };
|
|
}
|
|
|
|
if (lower.includes('account') || lower.includes('login') || lower.includes('password')) {
|
|
return { intent: 'account', confidence: 0.8 };
|
|
}
|
|
|
|
if (lower.includes('hello') || lower.includes('hi') || lower.includes('help')) {
|
|
return { intent: 'greeting', confidence: 0.9 };
|
|
}
|
|
|
|
return { intent: 'unknown', confidence: 0.5 };
|
|
}
|
|
|
|
// Search knowledge base for answer
|
|
function searchKnowledgeBase(question) {
|
|
const kb = loadKnowledgeBase();
|
|
const keywords = question.toLowerCase().split(' ').filter(w => w.length > 3);
|
|
|
|
// Simple keyword matching - can be enhanced with embeddings later
|
|
const sections = kb.split('## ');
|
|
for (const section of sections) {
|
|
const score = keywords.filter(k => section.toLowerCase().includes(k)).length;
|
|
if (score >= 2) {
|
|
return section.trim();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Send message to Chatwoot conversation
|
|
async function sendMessage(conversationId, content) {
|
|
try {
|
|
await chatwootAPI.post(`/api/v1/accounts/${process.env.CHATWOOT_ACCOUNT_ID}/conversations/${conversationId}/messages`, {
|
|
content: content,
|
|
message_type: 'outgoing'
|
|
});
|
|
console.log(`Sent message to conversation ${conversationId}`);
|
|
} catch (error) {
|
|
console.error('Error sending message:', error.response?.data || error.message);
|
|
}
|
|
}
|
|
|
|
// Escalate conversation to human agent
|
|
async function escalateToHuman(conversationId, contactName, reason) {
|
|
try {
|
|
// Change status to 'open' to assign to human agent
|
|
await chatwootAPI.patch(`/api/v1/accounts/${process.env.CHATWOOT_ACCOUNT_ID}/conversations/${conversationId}`, {
|
|
status: 'open'
|
|
});
|
|
|
|
console.log(`Escalated conversation ${conversationId} to human`);
|
|
|
|
// Send Telegram notification
|
|
await sendTelegramNotification(contactName, reason, conversationId);
|
|
} catch (error) {
|
|
console.error('Error escalating:', error.response?.data || error.message);
|
|
}
|
|
}
|
|
|
|
// Send Telegram notification on escalation
|
|
async function sendTelegramNotification(contactName, reason, conversationId) {
|
|
if (!process.env.TELEGRAM_BOT_TOKEN || !process.env.TELEGRAM_CHAT_ID) {
|
|
console.log('Telegram not configured, skipping notification');
|
|
return;
|
|
}
|
|
|
|
const message = `🔔 *Support Escalation*\n\n` +
|
|
`*Customer:* ${contactName}\n` +
|
|
`*Reason:* ${reason}\n` +
|
|
`*Conversation:* ${conversationId}\n\n` +
|
|
`Jump in: ${process.env.CHATWOOT_URL}/app/accounts/${process.env.CHATWOOT_ACCOUNT_ID}/conversations/${conversationId}`;
|
|
|
|
try {
|
|
await axios.post(
|
|
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
|
|
{
|
|
chat_id: process.env.TELEGRAM_CHAT_ID,
|
|
text: message,
|
|
parse_mode: 'Markdown'
|
|
}
|
|
);
|
|
console.log('Telegram notification sent');
|
|
} catch (error) {
|
|
console.error('Error sending Telegram:', error.message);
|
|
}
|
|
}
|
|
|
|
// Handle incoming webhook from Chatwoot
|
|
app.post('/webhook', async (req, res) => {
|
|
const event = req.body;
|
|
console.log('Received webhook:', event.event);
|
|
|
|
// Only process message_created events
|
|
if (event.event !== 'message_created') {
|
|
return res.status(200).send('OK');
|
|
}
|
|
|
|
const { conversation, message } = event.data;
|
|
|
|
// Ignore outgoing messages (from agents/bots)
|
|
if (message.message_type !== 'incoming') {
|
|
return res.status(200).send('OK');
|
|
}
|
|
|
|
const conversationId = conversation.id;
|
|
const contactName = conversation.meta?.sender?.name || 'Unknown';
|
|
const userMessage = message.content;
|
|
|
|
console.log(`Message from ${contactName}: ${userMessage}`);
|
|
|
|
// Detect intent
|
|
const { intent, confidence } = detectIntent(userMessage);
|
|
console.log(`Intent: ${intent} (${confidence})`);
|
|
|
|
// Handle based on intent
|
|
if (intent === 'greeting') {
|
|
await sendMessage(conversationId,
|
|
`Hi ${contactName}! 👋 Welcome to HOA Ledger IQ support. \n\n` +
|
|
`I'm here to help you get started. Could you tell me a bit about what you're looking to accomplish?\n\n` +
|
|
`If you need immediate assistance from our team, just let me know!`
|
|
);
|
|
}
|
|
else if (intent === 'billing' || intent === 'account' || intent === 'feature_request') {
|
|
// Try to find answer in knowledge base
|
|
const answer = searchKnowledgeBase(userMessage);
|
|
|
|
if (answer) {
|
|
await sendMessage(conversationId,
|
|
`Thanks for your question! Here's what I found:\n\n${answer}\n\n` +
|
|
`Does this answer your question? If you need more help, I can connect you with Chris.`
|
|
);
|
|
} else {
|
|
// No KB answer - escalate
|
|
await sendMessage(conversationId,
|
|
`Great question! Let me get Chris to help you with this. One moment! 👍`
|
|
);
|
|
await escalateToHuman(conversationId, contactName, `${intent} - no KB answer`);
|
|
}
|
|
}
|
|
else if (intent === 'technical_issue') {
|
|
// Technical issues usually need human attention
|
|
await sendMessage(conversationId,
|
|
`I understand you're experiencing an issue. Let me get Chris to help you troubleshoot this. \n\n` +
|
|
`In the meantime, could you share:\n` +
|
|
`• What browser/device you're using?\n` +
|
|
`• Any error messages you're seeing?\n` +
|
|
`• Steps to reproduce the issue?`
|
|
);
|
|
await escalateToHuman(conversationId, contactName, `technical_issue`);
|
|
}
|
|
else {
|
|
// Unknown intent - ask for clarification or escalate
|
|
await sendMessage(conversationId,
|
|
`Thanks for reaching out! To make sure I direct you to the right place, could you tell me a bit more about what you need help with?\n\n` +
|
|
`• Billing or pricing questions\n` +
|
|
`• Feature questions\n` +
|
|
`• Technical issues\n` +
|
|
`• Account access\n\n` +
|
|
`Or I can connect you directly with Chris if you prefer!`
|
|
);
|
|
}
|
|
|
|
res.status(200).send('OK');
|
|
});
|
|
|
|
// Health check endpoint
|
|
app.get('/health', (req, res) => {
|
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
console.log(`🤖 Chatwoot Agent Bot running on port ${PORT}`);
|
|
console.log(`Webhook endpoint: http://localhost:${PORT}/webhook`);
|
|
console.log(`Health check: http://localhost:${PORT}/health`);
|
|
});
|