All articles
Build Guides

Ship a WhatsApp Support Bot Backed by Your Docs Using Twilio & Groq

FDE Coach EditorialJuly 31, 202611 min read

What We’re Building

A WhatsApp bot that reads your product docs and answers customer questions in real time. When a user sends a message to your Twilio WhatsApp Sandbox number, the bot searches a local Qdrant vector database for the most relevant chunks of your documentation, feeds them to Groq’s Llama model, and returns a concise, sourced answer — all on free infrastructure.

Feature list:

  • WhatsApp-native interface via Twilio’s free Sandbox (no production approval needed).
  • Semantic search over your own markdown, PDF, or text docs using Qdrant’s vector store.
  • Fast, free LLM inference with Groq’s Llama 3.3 70B or 8B endpoints.
  • Context-grounded answers that cite the exact document sections used.
  • Stateless conversation loop — each message triggers a fresh retrieval + generation cycle.
  • Zero-cost deployment (runs on your laptop or a free-tier cloud VM).

Architecture Overview

This diagram shows the data flow from WhatsApp message to AI response. The entire pipeline runs on a single machine, with Twilio and Groq as the only external services.

Flow breakdown:

  1. A user sends a WhatsApp message to your Twilio Sandbox number.
  2. Twilio fires an HTTP POST to your Flask webhook with the message body.
  3. The webhook embeds the query using a free embedding model and searches Qdrant for the top 3 most similar doc chunks.
  4. Those chunks, plus the original question, are packed into a prompt and sent to Groq’s Llama endpoint.
  5. Groq returns a grounded answer, which Flask formats as a TwiML response.
  6. Twilio delivers the answer back to WhatsApp.

Prerequisites (All Free-Tier)

You need exactly four things. Every one has a free tier:

ToolPurposeFree Tier LimitSign-Up Link
TwilioWhatsApp Sandbox + webhook relay1 Sandbox number, unlimited test messages to verified numberstwilio.com/try-twilio
GroqFast LLM inference (Llama 3.3 70B)30 requests/minute, 14,400 tokens/minute on free tierconsole.groq.com
QdrantVector database (local Docker)Unlimited local storage, free Cloud tier also availableqdrant.tech
Sentence TransformersFree local embedding modelRuns entirely on your machinesbert.net

You’ll also need:

  • Python 3.10+
  • Docker (for Qdrant)
  • ngrok (free tier) to expose your local webhook to the internet

Step 1: Spin Up a Local Qdrant Vector Store

Pull the Qdrant Docker image and run it with a persistent volume:

docker pull qdrant/qdrant
docker run -p 6333:6333 -p 6334:6334 \
  -v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
  qdrant/qdrant

Verify it’s alive:

curl http://localhost:6333/health
# {"title":"qdrant - vector search engine","version":"1.9.0"}

Why local? Qdrant Cloud’s free tier is also fine, but running locally eliminates network latency between embedding and retrieval. If you later move to production, swap the connection string to a managed instance.

Step 2: Ingest Your Docs into Qdrant

Create ingest.py. This script reads a directory of markdown files, splits them into chunks, embeds each chunk with all-MiniLM-L6-v2 (a lightweight, free Sentence Transformer model), and upserts them into Qdrant.

# ingest.py
import os
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from sentence_transformers import SentenceTransformer

COLLECTION_NAME = "support_docs"
DOCS_DIR = "./docs"
CHUNK_SIZE = 500  # characters

client = QdrantClient(host="localhost", port=6333)
model = SentenceTransformer("all-MiniLM-L6-v2")

# Delete and recreate collection for idempotent runs
if client.collection_exists(COLLECTION_NAME):
    client.delete_collection(COLLECTION_NAME)

client.create_collection(
    collection_name=COLLECTION_NAME,
    vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)

points = []
point_id = 0

for filename in os.listdir(DOCS_DIR):
    if not filename.endswith(".md"):
        continue
    with open(os.path.join(DOCS_DIR, filename), "r") as f:
        content = f.read()
    # Naive chunking: split by characters; production would use a smarter splitter
    chunks = [content[i:i+CHUNK_SIZE] for i in range(0, len(content), CHUNK_SIZE)]
    for chunk in chunks:
        embedding = model.encode(chunk).tolist()
        points.append(PointStruct(
            id=point_id,
            vector=embedding,
            payload={"text": chunk, "source": filename}
        ))
        point_id += 1

client.upsert(collection_name=COLLECTION_NAME, points=points)
print(f"Ingested {point_id} chunks from {len(os.listdir(DOCS_DIR))} files.")

Run it:

mkdir docs
echo "# FAQ\nQ: How do I reset my password?\nA: Go to Settings > Security > Reset Password." > docs/faq.md
python ingest.py

You now have a searchable vector database of your docs.

Step 3: Create the Groq-Powered RAG Engine

Create rag.py — the retrieval-augmented generation module. It takes a user query, embeds it, fetches the top 3 chunks from Qdrant, and calls Groq.

# rag.py
import os
from groq import Groq
from qdrant_client import QdrantClient
from sentence_transformers import SentenceTransformer

COLLECTION_NAME = "support_docs"
GROQ_API_KEY = os.environ["GROQ_API_KEY"]

qdrant = QdrantClient(host="localhost", port=6333)
embedder = SentenceTransformer("all-MiniLM-L6-v2")
groq = Groq(api_key=GROQ_API_KEY)

def answer_query(user_message: str) -> str:
    # 1. Embed the query
    query_vec = embedder.encode(user_message).tolist()

    # 2. Search Qdrant
    results = qdrant.search(
        collection_name=COLLECTION_NAME,
        query_vector=query_vec,
        limit=3
    )

    # 3. Build context from retrieved chunks
    context = "\n\n---\n\n".join([hit.payload["text"] for hit in results])
    sources = list(set([hit.payload["source"] for hit in results]))

    # 4. Call Groq
    system_prompt = (
        "You are a helpful customer support assistant. "
        "Answer the user's question using ONLY the provided context. "
        "If the answer isn't in the context, say you don't know. "
        "Cite the source documents when possible."
    )

    response = groq.chat.completions.create(
        model="llama-3.3-70b-versatile",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {user_message}"}
        ],
        max_tokens=500,
        temperature=0.3,
    )

    answer = response.choices[0].message.content
    return f"{answer}\n\n📚 Sources: {', '.join(sources)}"

Test it standalone:

# test_rag.py
from rag import answer_query
print(answer_query("How do I reset my password?"))

Step 4: Wire Up the Twilio WhatsApp Sandbox

4.1 Activate the Sandbox

  1. Go to Twilio Console > Messaging > Try it out > Send a WhatsApp message.
  2. Note the Sandbox number and the join code (e.g., join <code>).
  3. Send that join code from your WhatsApp to the Sandbox number. You’re now connected.

4.2 Create the Flask Webhook

Create app.py. This is the server that receives Twilio’s POST and returns TwiML.

# app.py
from flask import Flask, request
from twilio.twiml.messaging_response import MessagingResponse
from rag import answer_query

app = Flask(__name__)

@app.route("/whatsapp", methods=["POST"])
def whatsapp_webhook():
    incoming_msg = request.values.get("Body", "").strip()
    print(f"Received: {incoming_msg}")

    resp = MessagingResponse()
    msg = resp.message()

    if not incoming_msg:
        msg.body("Hi! Ask me anything about our product.")
        return str(resp)

    try:
        answer = answer_query(incoming_msg)
        msg.body(answer[:1600])  # WhatsApp has a 1600-char limit
    except Exception as e:
        print(f"Error: {e}")
        msg.body("Sorry, I ran into an issue. Try again in a moment.")

    return str(resp)

if __name__ == "__main__":
    app.run(port=5000, debug=True)

4.3 Expose Your Local Server with ngrok

ngrok http 5000

Copy the HTTPS forwarding URL (e.g., https://abc123.ngrok.io).

4.4 Configure the Twilio Webhook

  1. In Twilio Console, go to Messaging > Try it out > WhatsApp Sandbox Settings.
  2. Paste your ngrok URL + /whatsapp into the When a message comes in field.
  3. Set method to HTTP POST.
  4. Save.

Step 5: Run the Full Pipeline Locally

Open three terminal windows:

Terminal 1 — Qdrant:

docker run -p 6333:6333 qdrant/qdrant

Terminal 2 — Flask app:

export GROQ_API_KEY="gsk_your_key_here"
python app.py

Terminal 3 — ngrok:

ngrok http 5000

Now send a WhatsApp message to your Sandbox number. The bot should respond with a grounded answer drawn from your docs.

Test message: “How do I reset my password?”

Expected response: “Go to Settings > Security > Reset Password. 📚 Sources: faq.md”

Sensible Extensions

Once the core loop works, here’s what to add next:

  1. Conversation memory. Store the last N messages per user in a lightweight dict keyed by From number. Pass them as prior messages to Groq so the bot can handle follow-ups like “what about on mobile?”

  2. Smarter chunking. Replace the naive 500-character split with a recursive text splitter that respects markdown headings. LangChain’s RecursiveCharacterTextSplitter or LlamaIndex’s SentenceSplitter both work well and are free.

  3. Multi-format ingestion. Add PDF support with PyMuPDF (free) or pdfplumber. For HTML docs, use BeautifulSoup. The pipeline stays the same — just extract text and chunk it.

  4. Hybrid search. Qdrant supports keyword + vector hybrid search out of the box. Enable it by adding a keyword index on the text payload and using search with query_filter for exact term matches alongside semantic similarity.

  5. Multi-turn escalation. If the bot detects a question it can’t answer (low confidence score or explicit “I don’t know”), forward the conversation to a human agent via Twilio’s Flex or a simple email alert.

  6. Deploy for free. Push the Flask app to Render (free tier) or Fly.io (free allowance), and swap ngrok for a real domain. Qdrant Cloud’s free tier gives you a managed vector store so you don’t need Docker in production.

Common Pitfalls & How to Avoid Them

PitfallWhy It HappensFix
Twilio Sandbox messages not arrivingYou haven’t sent the join code from your WhatsApp to the Sandbox numberSend join <code> exactly as shown in Twilio Console
ngrok URL changes every restartFree ngrok generates a new random subdomainKeep ngrok running, or use ngrok http 5000 --subdomain=yourname (requires free account)
Groq rate limit 429 errorsFree tier caps at 30 RPM / 14.4k TPMAdd exponential backoff in rag.py, or switch to llama-3.1-8b-instant which has higher free limits
Qdrant “collection not found”You forgot to run ingest.pyAlways run ingestion before starting the webhook
Embedding dimension mismatchall-MiniLM-L6-v2 outputs 384-dim vectors; Qdrant expects 384Stick with the same model for ingestion and query. If you change models, re-ingest
WhatsApp 1600-char truncationTwilio’s WhatsApp Sandbox enforces a message length limitTruncate in app.py and add a “Read more” link if needed

FAQ

Q: Do I need a Meta Business account for the WhatsApp Sandbox? No. Twilio’s Sandbox lets you test with up to 5 verified phone numbers without any Meta approval. Perfect for prototyping. When you’re ready for production, you’ll need a WhatsApp Business Profile.

Q: Can I use a different LLM? Absolutely. Swap the Groq client for OpenAI, Anthropic, or a local Ollama model. The RAG pipeline is model-agnostic. Groq is recommended here because its free tier is generous and latency is sub-200ms.

Q: How many docs can I ingest? With all-MiniLM-L6-v2, each 500-char chunk is a 384-dim vector. A million chunks would occupy roughly 1.5 GB of RAM in Qdrant. For most support doc sets (a few thousand chunks), you’re well within free-tier limits on any laptop.

Q: How do I update the docs without downtime? Re-run ingest.py — it drops and recreates the collection. For zero-downtime, use Qdrant’s collection aliases: create a new collection, ingest there, then atomically swap the alias.

Q: Is this production-ready? The core architecture is solid, but you’d want to add authentication on the webhook, persistent conversation state, and proper error monitoring before going live. For a deeper dive on shipping prototypes fast, check out From Messy Enterprise Problem to Shipped Prototype in 5 Days: An FDE Playbook.

Q: Can I extend this to index a codebase instead of docs? Yes. The same pipeline works for code. Chunk by function or class, embed, and query. For a full walkthrough on codebase Q&A, see Build a Codebase Q&A Tool That Indexes a Repo and Answers Questions in Natural Language.

Q: What if the bot gives wrong answers? Grounding is only as good as your retrieval. If the wrong chunks are fetched, the LLM hallucinates. Improve retrieval by experimenting with chunk size, adding metadata filters, or using a reranker. For a deeper discussion on agent guardrails, read GPT‑5.6 Lost $447 Running a Business: How to Structure Agent Guardrails That Actually Work.

Q: How do I handle multiple languages? Replace all-MiniLM-L6-v2 with a multilingual embedding model like paraphrase-multilingual-MiniLM-L12-v2 (also free). Groq’s Llama models handle dozens of languages natively.

Q: What does a Forward Deployed Engineer actually ship in a week? Prototypes exactly like this one — a working end-to-end integration that solves a real customer problem. For a realistic week-in-the-life breakdown, see What a Forward Deployed Engineer Actually Does in a Week: From Standup to Shipped Prototype.

#whatsapp-bot#customer-support#rag#llama

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now