A fully serverless, AI-powered dental clinic assistant that automatically handles the WhatsApp appointment bookings — no server required, runs 100% free on GitHub Actions.
- What is This Project?
- How It Works
- Architecture
- Conversation Flow
- Project Structure
- Prerequisites
- Complete Setup Guide
- GitHub Actions Workflow Explained
- Environment Variables Reference
- How the Agent Logic Works
- Testing
- Troubleshooting
Dental AI Agent is a zero-server WhatsApp chatbot for dental clinics. When a patient sends a WhatsApp message, the AI automatically:
- ✅ Understands natural language — "Book me next Monday at 9am" works perfectly
- ✅ Remembers the conversation — collects info across multiple messages without asking twice
- ✅ Checks availability — queries Supabase to see if the slot is free before confirming
- ✅ Books the appointment — saves directly to Supabase database
- ✅ Replies instantly — sends a confirmation back via WhatsApp
No monthly server cost. Runs entirely on GitHub Actions (free tier = 2,000 minutes/month).
1. Patient sends WhatsApp message
↓
2. LeminAI receives the message
and fires a webhook to GitHub
↓
3. GitHub Actions wakes up
and runs agent.py
↓
4. agent.py reads conversation
history from chat_history/
and calls Gemini AI
↓
5. Gemini extracts booking info
and returns structured [UPDATE] tag
↓
6. If all 4 fields collected:
→ Check Supabase for conflicts
→ Insert booking if slot is free
↓
7. Send WhatsApp reply to patient
via LeminAI API
The agent collects exactly 4 fields — and only asks for what is still missing:
| Field | Example |
|---|---|
| Patient Name | Muhammad Rahman |
| Appointment Date | 2026-10-28 (understands "next Wednesday") |
| Appointment Time | 09:00:00 (understands "9am", "3pm") |
| Dental Issue | Toothache, broken tooth, cavity, etc. |
┌─────────────────────────────────────────────────────────┐
│ PATIENT (WhatsApp) │
└────────────────────────┬────────────────────────────────┘
│ WhatsApp Message
▼
┌─────────────────────────────────────────────────────────┐
│ LeminAI (WhatsApp Gateway) │
│ • Receives WhatsApp messages │
│ • Fires repository_dispatch webhook to GitHub │
│ • Sends AI reply back to patient │
└────────────────────────┬────────────────────────────────┘
│ repository_dispatch event
▼
┌─────────────────────────────────────────────────────────┐
│ GitHub Actions (Serverless) │
│ Trigger: repository_dispatch [whatsapp_message] │
│ • Spins up ubuntu-latest runner │
│ • Sets up Python 3.12 │
│ • Installs dependencies │
│ • Runs agent.py with secrets injected │
└────────────────────────┬────────────────────────────────┘
│ Runs agent.py
▼
┌─────────────────────────────────────────────────────────┐
│ agent.py (Brain) │
│ │
│ ┌──────────────┐ ┌────────────────┐ │
│ │ Conversation │ │ Google Gemini │ │
│ │ History │───▶│ 3.1 Flash Lite │ │
│ │ (JSON files) │ │ (NLP/AI) │ │
│ └──────────────┘ └───────┬────────┘ │
│ │ [UPDATE] tag │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Supabase │ │
│ │ (PostgreSQL) │ │
│ │ Check & Book │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
The agent is smart — it remembers partial information across multiple messages:
Patient : "Book me Wednesday October 28th at 9am, my name is Ahmed, I have toothache"
Agent : ✅ Appointment Confirmed!
Name : Ahmed
Date : 2026-10-28
Time : 09:00 (Pakistan time)
Issue : toothache
Patient : "I want to book tomorrow at 10am"
Agent : "Got it! 2026-10-23 at 10:00am noted. What is your name and dental issue?"
Patient : "Sara, I have a cavity"
Agent : ✅ Appointment Confirmed!
Name : Sara
Date : 2026-10-23
Time : 10:00 (Pakistan time)
Issue : cavity
Patient : "Book me Monday at 9am, I'm Ali, broken tooth"
Agent : "❌ Monday at 09:00 is already booked.
Please choose a different date or time."
Dental-AI-Agent/
│
├── agent.py # Main AI agent logic
├── .env # Local environment variables (never commit!)
├── .gitignore # Excludes .env and chat_history/
├── banner.png # README banner image
├── flow.png # Architecture flow diagram
│
├── chat_history/ # Auto-created, gitignored
│ └── 923369013306.json # Per-patient conversation state
│
└── .github/
└── workflows/
└── Agent.yml # GitHub Actions workflow
| Section | Lines | Purpose |
|---|---|---|
| Load environment | 1–21 | Reads API keys from env vars |
| Parse payload | 23–60 | Extracts phone number and message from WhatsApp webhook |
| Auth check | 62–70 | Only processes messages from allowed numbers |
| Conversation state | 72–115 | Load/save per-patient JSON history |
| Supabase helpers | 117–160 | Check availability + insert booking |
| Build prompt | 162–195 | Constructs compact system prompt with known/missing info |
| Gemini call | 197–235 | Calls AI with 3 retries on 503 errors |
| Parse UPDATE tag | 237–248 | Extracts structured booking data from AI response |
| Book appointment | 250–290 | Validates, checks Supabase, inserts row |
| Send reply | 292–310 | Sends final message back via LeminAI |
Before starting, you need accounts on these platforms (all free):
| Service | Purpose | Free Tier |
|---|---|---|
| GitHub | Hosts code + runs agent | 2,000 min/month free |
| Supabase | PostgreSQL database | 500MB free |
| LeminAI | WhatsApp gateway + webhook | Free tier available |
| Google AI Studio | Gemini AI API | Free tier available |
a) Create a Supabase project
- Go to supabase.com → New Project
- Note your Project URL (e.g.
https://xxxxxyyyyy.supabase.co)
b) Create the appointments table
Go to SQL Editor and run this SQL:
CREATE TABLE IF NOT EXISTS dental_appointments (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
patient_name TEXT NOT NULL,
phone_number TEXT NOT NULL,
dental_issue TEXT NOT NULL,
appointment_date DATE NOT NULL,
appointment_time TIME NOT NULL,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);c) Get your API key
- Go to Project Settings → API
- Copy the Secret key (
sb_secret_...) — this is yourSUPABASE_KEY - Copy the Project URL — this is your
SUPABASE_URL
⚠️ Use the Secret key, not the anon/public key. The secret key has full database access.
a) Create a LeminAI account
- Sign up at leminai.com
- Connect your WhatsApp Business number
- Go to API Settings → copy your API Key → this is your
LEMINA_API_KEY
b) Configure the Webhook
In LeminAI webhook settings, set the URL to trigger GitHub Actions:
https://api.github.com/repos/YOUR_USERNAME/YOUR_REPO/dispatches
With these headers:
Authorization: Bearer YOUR_GITHUB_PAT_TOKEN
Content-Type: application/json
And this body template:
{
"event_type": "whatsapp_message",
"client_payload": {
"from": "{{sender_phone}}",
"text": {
"body": "{{message_body}}"
}
}
}See Section 5 for the full LeminAI → GitHub connection guide.
- Go to aistudio.google.com
- Click Get API Key → Create API Key
- Copy the key → this is your
GEMINI_API_KEY
Free tier: 15 requests per minute, limited daily quota. For production use, add billing at console.cloud.google.com.
a) Fork or create the repository
git clone https://github.com/YOUR_USERNAME/Dental-AI-Agent.git
cd Dental-AI-Agentb) Add GitHub Secrets
Go to your repo → Settings → Secrets and variables → Actions → New repository secret
Add each of these:
| Secret Name | Value | Where to get it |
|---|---|---|
GEMINI_API_KEY |
AIza... |
Google AI Studio |
LEMINA_API_KEY |
lmn_sk_... |
LeminAI dashboard |
SUPABASE_URL |
https://xxx.supabase.co |
Supabase Project Settings |
SUPABASE_KEY |
sb_secret_... |
Supabase Project Settings → API |
ALLOWED_NUMBERS |
923369013306,03369013306 |
Your WhatsApp numbers (comma-separated) |
⚠️ ALLOWED_NUMBERSis a security filter — only these phone numbers will be processed. Add both formats (with and without country code) for safety.
c) Create a GitHub Personal Access Token (PAT)
This is needed for LeminAI to trigger GitHub Actions:
- GitHub → Settings (your profile) → Developer settings → Personal access tokens → Fine-grained tokens
- Click Generate new token
- Set permissions: Repository permissions → Actions → Read and Write
- Copy the token — you'll use it in LeminAI webhook config
This is the critical link that makes everything work:
In LeminAI:
- Go to Automations or Webhooks section
- Create a new automation triggered on incoming message
- Set the action to HTTP Request with:
Method: POST
URL: https://api.github.com/repos/YOUR_USERNAME/Dental-AI-Agent/dispatches
Headers:
Authorization: Bearer ghp_YOUR_GITHUB_PAT_TOKEN
Content-Type: application/json
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2022-11-28
Body:
{
"event_type": "whatsapp_message",
"client_payload": {MESSAGE_PAYLOAD_FROM_LEMIN}
}
Once connected, every WhatsApp message → LeminAI → GitHub → agent.py runs automatically.
For testing locally:
# Clone the repo
git clone https://github.com/Developer359/Dental-AI-Agent.git
cd Dental-AI-Agent
# Install dependencies
pip install requests google-genai python-dotenv
# Create your .env file
cp .env.example .env
# Edit .env and fill in your keys
# Test with a simulated message
$env:EVENT_PAYLOAD = '{"from":"923369013306","text":{"body":"Book me tomorrow at 10am, name Ali, cavity"}}'
python agent.pyFile: .github/workflows/Agent.yml
name: Dental AI Agent WhatsApp ResponderThe display name shown in the GitHub Actions tab.
on:
repository_dispatch:
types: [whatsapp_message]Trigger: The workflow runs ONLY when an external system (LeminAI) sends a POST request to the GitHub API with
event_type: "whatsapp_message". This is how a WhatsApp message wakes up the agent. No schedule, no push — purely event-driven.
jobs:
run-agent:
runs-on: ubuntu-latestJob definition: Creates a fresh Ubuntu Linux virtual machine for every single message. Each run is isolated — no state between runs (state is handled by
chat_history/*.jsonfiles in the repo).
steps:
- name: Checkout Code
uses: actions/checkout@v4Step 1 — Checkout: Downloads the latest version of your repository code (including
agent.py) onto the runner. Uses the official GitHubcheckoutaction v4.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'Step 2 — Python: Installs Python 3.12 on the runner using the official
setup-pythonaction v5. Version 3.12 is specified for compatibility with thegoogle-genaiSDK type hints.
- name: Install Dependencies
run: |
pip install requests google-genai python-dotenvStep 3 — Dependencies: Installs the 3 required Python packages:
requests— for Supabase REST API and LeminAI WhatsApp API callsgoogle-genai— official Google Gemini AI Python SDKpython-dotenv— loads.envfile for local dev (no effect in Actions since env vars are injected via secrets)
- name: Run Dental AI Agent
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
LEMINA_API_KEY: ${{ secrets.LEMINA_API_KEY }}
ALLOWED_NUMBERS: ${{ secrets.ALLOWED_NUMBERS }}
EVENT_PAYLOAD: ${{ toJson(github.event.client_payload) }}
run: |
python agent.pyStep 4 — Run Agent: The main step. Four important things:
GEMINI_API_KEY— injected from GitHub Secrets (never visible in logs)LEMINA_API_KEY— injected from GitHub SecretsALLOWED_NUMBERS— comma-separated whitelist of allowed phone numbersEVENT_PAYLOAD— this is the WhatsApp message data, passed fromgithub.event.client_payload(what LeminAI sent in the webhook body).toJson()converts it to a JSON string thatagent.pyreads withos.getenv("EVENT_PAYLOAD").
⚠️ Note:SUPABASE_URLandSUPABASE_KEYmust also be added as GitHub Secrets if you want Supabase to work in GitHub Actions (add them to theenv:block or the workflow file).
Add these to your .env file for local dev, and as GitHub Secrets for production:
# Google Gemini AI
GEMINI_API_KEY=AIzaSy...
# LeminAI WhatsApp Gateway
LEMINA_API_KEY=lmn_sk_...
# Supabase Database
SUPABASE_URL=https://xxxxxyyyyy.supabase.co
SUPABASE_KEY=sb_secret_...
# Security: only these phone numbers are processed (comma-separated)
# Add both local and international formats
ALLOWED_NUMBERS=03369013306,923369013306🔒 The
.envfile is listed in.gitignoreand will never be committed to the repository.
Every patient's conversation state is stored in chat_history/{phone}.json:
{
"messages": [
{"role": "user", "content": "I want to book tomorrow at 10am"},
{"role": "model", "content": "Got it! Could you share your name and issue?"}
],
"draft": {
"patient_name": null,
"appointment_date": "2026-10-23",
"appointment_time": "10:00:00",
"dental_issue": null
}
}On the next message, the agent:
- Loads the existing draft (date + time already known)
- Builds a prompt that says
STILL MISSING: patient_name, dental_issue - Only asks for the missing pieces
The AI always ends its reply with a structured tag:
[UPDATE: {"patient_name":"Ahmed","appointment_date":null,"appointment_time":null,"dental_issue":"toothache"}]
The Python code extracts this, merges non-null values into the draft, then removes the tag before sending the reply to the patient.
Before confirming any booking:
# Query Supabase for conflicts
SELECT id FROM dental_appointments
WHERE appointment_date = '2026-10-28'
AND appointment_time = '09:00:00'
AND status != 'cancelled'- 0 rows → slot is free → proceed with booking
- 1+ rows → slot is taken → tell patient, clear date/time, ask them to pick again
# Simulate the exact format LeminAI sends
$env:EVENT_PAYLOAD = '{
"from": "923369013306",
"text": {"body": "Book me Wednesday at 9am, name Ahmed, toothache"}
}'
python agent.pyRaw payload keys: ['from', 'text']
sender_phone=923369013306, message_body='Book me Wednesday at 9am...'
Authorised trigger from 923369013306: Book me Wednesday...
State: draft={all None}, turns=0
AI reply: ...
Draft << patient_name = Ahmed
Draft << appointment_date = 2026-10-30
Draft << appointment_time = 09:00:00
Draft << dental_issue = toothache
All fields ready: {...}
Availability check >> 200: []
Insert >> 201: [{"id": "abc-123", ...}]
>>> Booking saved to Supabase. ID=abc-123
WhatsApp >> 200: {"data": {"message_id": ...}}
Go to your Supabase dashboard → Table Editor → dental_appointments — the row should appear instantly.
| Problem | Cause | Fix |
|---|---|---|
403 Forbidden from GitHub |
Bad PAT token in LeminAI | Regenerate PAT with Actions: Read & Write permission |
ValidationError: 26 errors |
Old google-genai version |
Run pip install --upgrade google-genai |
503 UNAVAILABLE from Gemini |
Model overloaded | Agent retries 3x automatically; try again if persistent |
429 RESOURCE_EXHAUSTED |
Free tier daily quota hit | Add billing to Google Cloud project |
Message body is None |
Wrong payload format | Check LeminAI webhook body template matches expected format |
| Booking not in Supabase | SUPABASE_URL/SUPABASE_KEY not in GitHub Secrets |
Add both as repository secrets |
| Agent processes unknown numbers | ALLOWED_NUMBERS misconfigured |
Add both 03xx and 923xx formats |
chat_history/ keeps growing |
Normal operation | Files auto-delete after successful booking |
| Component | Technology | Purpose |
|---|---|---|
| Language | Python 3.12 | Agent logic |
| AI Model | Gemini 3.1 Flash Lite | Natural language understanding |
| Database | Supabase (PostgreSQL) | Appointment storage + availability check |
| LeminAI | Message gateway + webhook | |
| CI/CD | GitHub Actions | Serverless execution |
| State | JSON files (per phone) | Multi-turn conversation memory |
MIT License — free to use, modify, and distribute.
