Backend for Sema — a WhatsApp AI agent that answers customer messages on behalf of small businesses in East Africa, in English or Swahili.
Sema is Swahili for "speak".
A shop, clinic or pharmacy connects its WhatsApp number, answers a short onboarding questionnaire, and the agent handles incoming customer messages using that business's own catalogue, FAQs, policies and scraped website — inside safety rules defined per industry.
Frontend: sugarshaneaz/Sema
A pharmacy in Dar es Salaam gets WhatsApp messages all day. Most are "do you have X" and "what time do you close". Some are "my child took two of these, is that okay". A general-purpose chatbot answers all three with equal confidence, and the third answer can hurt somebody.
So the interesting engineering here is not generating replies. It is constraining them — per industry, per business, and per gap in what the business has actually told us.
Fourteen industries ship as versioned JSON packs: restaurant, retail, clinic, salon, government office, pharmacy, hardware, electronics, beauty supply, clothing and shoes, home decor, services, general retail, other.
Each pack defines localised labels and a primer (EN/SW), the intents that
industry actually receives, the onboarding fields a business must supply, message
templates, and a rules block with three parts. The pharmacy pack:
{
"neverInvent": ["price", "stock", "medicalAdvice", "dosage",
"sideEffects", "drugInteractions", "diagnosis"],
"escalateIf": ["medical_urgent", "adverse_reaction", "medication_error",
"complaint", "mental_health_crisis"],
"style": ["WhatsApp short messages",
"professional careful tone",
"always recommend consulting pharmacist for medical questions",
"never recommend medications"]
}neverInvent is the list of things the model may not produce from its own
weights — it either has the fact from the business or it says it doesn't know.
escalateIf routes the conversation to a human instead of answering. Both are
compiled into the system prompt at request time, so the constraint travels with
every message rather than living in a document somebody read once.
Packs are loaded, schema-validated and cached at startup by
KnowledgePackService, then upserted into niches and niche_templates so the
database and the filesystem agree.
The failure mode of a business chatbot is not refusing to answer. It is answering confidently about a delivery policy the owner never entered.
buildBusinessContext() merges the niche pack, onboarding answers, scraped
website facts, catalogue, FAQs, policies and uploaded knowledge sources into one
context object — and while doing it, walks the pack's required onboarding fields
and collects every one the business has left blank into missingCriticalFields.
That list is injected into the system prompt as an explicit guardrail. The model is told, in the same breath as its instructions, precisely which facts it does not have. Hallucination control by disclosure rather than by hoping.
English and Swahili are first-class: packs carry both, and the language
preference is per business. Translation runs through a cache
(translation_cache) with daily usage accounting (translation_usage_daily),
because per-message translation of a WhatsApp inbox is a recurring cost that
needs a ceiling, not a surprise.
Businesses rarely have structured data. They have a PDF price list, a photo of a menu, and a website. So the API accepts all three:
- PDF via
pdf-parse/pdf-lib - Images via
tesseract.jsOCR withsharppreprocessing - Websites scraped into
websiteFacts, stored as JSON on the business profile and merged into the AI context
Ingestion runs under p-limit concurrency caps with p-retry backoff — the
upstream here is somebody's slow shared-hosting site, and a retry storm helps
nobody.
- WhatsApp integration — connections, message log, drafts, and a webhook event table, so a replayed or duplicated Meta webhook is detectable rather than reprocessed
- Selcom payments — mobile money, the way this market actually pays
- Object storage with an ACL layer over Google Cloud Storage
- Multi-business, multi-tenant — one admin can hold several businesses with an active-business switch
- Backward-compatible legacy models —
restaurants/menu_categories/menu_itemsstill work alongside the generalisedbusinesses/catalog_*schema, because shipped mobile clients cannot be forced to update
Express · TypeScript · Prisma 7 (@prisma/adapter-pg) · PostgreSQL · Zod ·
OpenAI · Helmet · Multer / Uppy · Sharp · Tesseract.js · pdf-parse · Cheerio ·
p-limit / p-retry · Google Cloud Storage · Vitest
src/
knowledge/niches/ 14 industry knowledge packs (JSON)
services/ knowledgePacks · i18n · fileProcessor · scrapers · selcom
integrations/ object storage + ACL
promptBuilder.ts context merge, missing-field detection, prompt assembly
prisma/ schema + migrations
tests/ vitest
npm install
npx prisma migrate deploy
npx tsx src/index.tsNeeds DATABASE_URL, an OpenAI key, WhatsApp/Meta webhook credentials, and
Selcom credentials for the payment routes.