AI WhatsApp Bot
Multi-tenant AI assistant with RAG
Problem
Businesses lose customers because they can’t respond to WhatsApp messages instantly 24/7. Traditional chatbots rely on rigid decision trees that break on unexpected input. We needed a system that:
- Understands natural language intent in Bahasa Indonesia
- Retrieves product/service info from database (not hardcoded)
- Respects guardrails (no off-topic, no hallucinated prices)
- Scales across multiple tenants with isolated data
Solution
A multi-tenant WhatsApp AI platform with RAG (Retrieval-Augmented Generation):
- Pipeline: WAHA webhook → Message processor → AI intent classifier → Tool execution → Response → WAHA send
- RAG: Product descriptions chunked → embedded via OpenAI → stored in pgvector → semantic search at query time
- Guardrails: Function-calling tools with strict schemas validate every AI arguments before execution
- Multi-tenant: Each organization gets isolated customers, conversations, and AI context via JWT auth
Architecture
WhatsApp → WAHA webhook → Hono pipeline
→ Customer lookup (LID + phone format)
→ Intent detection (keyword + AI fallback)
→ Tool execution (DB query, RAG search, etc.)
→ Response formatting
→ WAHA sendMessage
Key Tech Decisions
Keyword bypass for deterministic queries
gemini-3-flash often returned hallucinated text instead of calling tool functions,
even with explicit “WAJIB panggil tool X” system prompts. For product queries,
keyword-based bypass detects intent via regex, directly queries the database,
formats the response, then sends via WAHA. Skip AI entirely for deterministic
queries, saving 2-3s response time.
pgvector over Pinecone
Used PostgreSQL pgvector for embeddings instead of a separate vector database. Keeps the stack simpler (single DB), avoids vendor lock-in, and pgvector’s performance is sufficient for our scale. The embedding pipeline chunks product descriptions into ~512-token segments with 128-token overlap for context continuity.
Hono onError over try/catch middleware
Hono v4 routes errors bypass app.use('*', tryCatchMiddleware) because compose.js
catches errors at the dispatch level. Route errors go directly to app.onError.
Using middleware try/catch for route errors is useless — must use app.onError()
instead.
Metrics
| Metric | Value |
|---|---|
| API Routes | 40+ |
| Database Tables | 15+ |
| AI Models | 3 (classifier + RAG embedder + function-calling) |
| Response Time (keyword) | < 1s |
| Response Time (AI) | 3-5s |