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`); });