Skip to content

Repository files navigation

Sema API

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


The problem this is actually solving

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.


Niche knowledge packs

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.

Telling the model what it doesn't know

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.

Multilingual, with the bill in mind

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.

Knowledge ingestion

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.js OCR with sharp preprocessing
  • 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.

Also in here

  • 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 modelsrestaurants / menu_categories / menu_items still work alongside the generalised businesses / catalog_* schema, because shipped mobile clients cannot be forced to update

Stack

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

Layout

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

Running it

npm install
npx prisma migrate deploy
npx tsx src/index.ts

Needs DATABASE_URL, an OpenAI key, WhatsApp/Meta webhook credentials, and Selcom credentials for the payment routes.

About

Sema Backend API - Node.js/TypeScript/Prisma

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages