Files
HOALedgerIQ_Website/chatwoot-agent-bot/index.js
olsch01 5319bcd30b feat: Add Chatwoot Agent Bot prototype and FAQ knowledge base
- Created chatwoot-agent-bot/ with Node.js webhook server
- Bot detects intent (greeting, billing, technical, features, account)
- Auto-responds from FAQ knowledge base or escalates to human
- FAQ-KB.md: Living knowledge base that grows with customer questions
- CHATWOOT-SETUP.md: Complete deployment and configuration guide
- Supports Telegram notifications on escalation
- Bot runs on port 3001, ready for Chatwoot webhook integration
2026-04-01 16:26:05 -04:00

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