Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 6 additions & 39 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
# Byte-compiled / optimized / DLL files
```
# Python
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
Expand All @@ -20,43 +17,13 @@ parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Pytest / Coverage
.pytest_cache/
# Coverage
.coverage
.coverage.*
htmlcov/
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.nox/
.tox/

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

.mypy_cache/
*.db

# IDE / Editor settings
.idea/
.vscode/
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db
# Database
analytics.db
```
Binary file added analytics.db
Binary file not shown.
Binary file added src/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added src/analytics/__pycache__/db.cpython-312.pyc
Binary file not shown.
Binary file added src/analytics/__pycache__/models.cpython-312.pyc
Binary file not shown.
Binary file added src/api/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added src/api/__pycache__/dependencies.cpython-312.pyc
Binary file not shown.
Binary file added src/api/__pycache__/main.cpython-312.pyc
Binary file not shown.
20 changes: 20 additions & 0 deletions src/api/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import httpx

from src.handoff.client import BaseHandoffClient, MockZendeskClient
from src.utils.config import Settings, get_settings

# Global shared HTTP client with connection pooling for efficient reuse
_global_http_client: httpx.AsyncClient | None = None

# Global Mock CCaaS client instance so that it acts as our ticketing database
_global_handoff_client = MockZendeskClient()


async def get_http_client() -> httpx.AsyncClient:
"""Dependency to provide a shared async HTTP client with connection pooling.

Returns:
A shared httpx.AsyncClient instance.
"""
global _global_http_client
if _global_http_client is None or _global_http_client.is_closed:
_global_http_client = httpx.AsyncClient(
timeout=httpx.Timeout(30.0, connect=10.0),
limits=httpx.Limits(max_keepalive_connections=20, max_connections=50),
)
return _global_http_client


def get_app_settings() -> Settings:
"""Dependency to provide application Settings."""
return get_settings()
Expand Down
Binary file added src/api/routes/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
75 changes: 65 additions & 10 deletions src/api/routes/webhook.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import json
import time
from typing import Any

import httpx
from fastapi import APIRouter, BackgroundTasks, Depends, Header, HTTPException, Query, Request
from fastapi.responses import PlainTextResponse

from src.api.dependencies import get_app_settings, get_handoff_client
from src.api.dependencies import get_app_settings, get_handoff_client, get_http_client
from src.bot.session import SessionManager
from src.handoff.client import BaseHandoffClient
from src.orchestrator.engine import Orchestrator
from src.utils import pii_masker
Expand All @@ -18,6 +20,43 @@
router = APIRouter()


async def check_message_idempotency(message_id: str) -> bool:
"""Check if a message has already been processed using Redis-based deduplication.

Args:
message_id: The unique WhatsApp message ID.

Returns:
True if the message was already processed, False if it's new.
"""
store = SessionManager._store_instance
key = f"wbot:processed_msg:{message_id}"

if store._redis:
try:
# Try to set the key with a 1-hour TTL (atomic operation)
# Returns True if key was set (new message), False if key already exists
result = store._redis.set(key, "1", nx=True, ex=3600)
return result is False # If result is False, key already existed
except Exception as e:
logger.warning("Redis idempotency check failed, proceeding without dedup", error=str(e))
return False

# Fallback to in-memory store (not ideal for multi-instance deployments)
if hasattr(store, "_processed_messages"):
if message_id in store._processed_messages:
return True
store._processed_messages[message_id] = time.time()
# Clean up old entries (older than 1 hour)
cutoff = time.time() - 3600
store._processed_messages = {
k: v for k, v in store._processed_messages.items() if v > cutoff
}
return False

return False


def get_orchestrator(
handoff_client: BaseHandoffClient = Depends(get_handoff_client), # noqa: B008
) -> Orchestrator:
Expand All @@ -39,6 +78,7 @@ async def process_incoming_message(
orchestrator: Orchestrator,
access_token: str,
background_tasks: BackgroundTasks,
http_client: httpx.AsyncClient,
media_id: str | None = None,
media_mime_type: str | None = None,
) -> None:
Expand All @@ -53,6 +93,7 @@ async def process_incoming_message(
orchestrator: The Orchestrator engine.
access_token: WhatsApp Access Token.
background_tasks: FastAPI BackgroundTasks runner.
http_client: Shared async HTTP client with connection pooling.
media_id: Optional ID of any uploaded media attachment.
media_mime_type: Optional mime type of the media attachment.
"""
Expand Down Expand Up @@ -112,6 +153,7 @@ async def process_incoming_message(
to=sender_wa_id,
text_body=bot_response.text,
access_token=access_token,
http_client=http_client,
)
except Exception as e:
logger.exception("Error processing incoming message in background", error=str(e))
Expand All @@ -122,6 +164,7 @@ async def send_whatsapp_reply(
to: str,
text_body: str,
access_token: str,
http_client: httpx.AsyncClient,
) -> None:
"""Asynchronously sends a reply back to WhatsApp using the Graph API.

Expand All @@ -130,6 +173,7 @@ async def send_whatsapp_reply(
to: The sender's WhatsApp ID (wa_id).
text_body: The reply text to send.
access_token: The WHATSAPP_ACCESS_TOKEN configuration.
http_client: Shared async HTTP client with connection pooling.
"""
url = f"https://graph.facebook.com/v21.0/{phone_number_id}/messages"
headers = {
Expand All @@ -145,15 +189,14 @@ async def send_whatsapp_reply(
}

try:
async with httpx.AsyncClient() as client:
response = await client.post(url, json=payload, headers=headers)
response.raise_for_status()
logger.info(
"WhatsApp reply sent successfully",
phone_number_id=phone_number_id,
to=to,
status_code=response.status_code,
)
response = await http_client.post(url, json=payload, headers=headers)
response.raise_for_status()
logger.info(
"WhatsApp reply sent successfully",
phone_number_id=phone_number_id,
to=to,
status_code=response.status_code,
)
except Exception as e:
logger.error(
"Failed to send WhatsApp reply",
Expand Down Expand Up @@ -288,6 +331,15 @@ async def receive_webhook(
return {"status": "success"}

message = messages[0]
message_id = message.get("id") # Extract WhatsApp message ID for idempotency

# Idempotency check: skip if this message was already processed
if message_id:
is_duplicate = await check_message_idempotency(message_id)
if is_duplicate:
logger.info("Duplicate message detected, skipping processing", message_id=message_id)
return {"status": "success"}

msg_type = message.get("type")

text_content = None
Expand Down Expand Up @@ -335,6 +387,8 @@ async def receive_webhook(

# Step 5 & 6: Offload the Orchestrator call and message delivery to BackgroundTasks
if phone_number_id and sender_wa_id and text_content:
# Get shared HTTP client for connection pooling
http_client = await get_http_client()
background_tasks.add_task(
process_incoming_message,
sender_wa_id,
Expand All @@ -343,6 +397,7 @@ async def receive_webhook(
orchestrator,
settings.WHATSAPP_ACCESS_TOKEN,
background_tasks,
http_client,
media_id,
media_mime_type,
)
Expand Down
Binary file added src/bot/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added src/bot/__pycache__/session.cpython-312.pyc
Binary file not shown.
Binary file added src/handoff/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added src/handoff/__pycache__/client.cpython-312.pyc
Binary file not shown.
Binary file added src/handoff/__pycache__/payload.cpython-312.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added src/utils/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added src/utils/__pycache__/config.cpython-312.pyc
Binary file not shown.
Binary file added src/utils/__pycache__/logger.cpython-312.pyc
Binary file not shown.
Binary file added src/utils/__pycache__/pii_masker.cpython-312.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
122 changes: 122 additions & 0 deletions src/whatsapp_support_bot.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
Metadata-Version: 2.4
Name: whatsapp-support-bot
Version: 0.1.0
Summary: Phase 01 WhatsApp webhook scaffolding and support bot foundation
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.115.0
Requires-Dist: uvicorn[standard]>=0.30.0
Requires-Dist: pydantic-settings>=2.5.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: structlog>=24.4.0
Requires-Dist: redis>=5.0.0
Requires-Dist: langfuse>=2.0.0
Requires-Dist: sqlalchemy[asyncio]>=2.0.0
Requires-Dist: aiosqlite>=0.20.0
Requires-Dist: prometheus-fastapi-instrumentator>=7.0.0
Requires-Dist: openai>=1.50.0
Requires-Dist: qdrant-client>=1.11.0
Requires-Dist: tiktoken>=0.7.0
Requires-Dist: pypdf>=4.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.3.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
Requires-Dist: respx>=0.21.0; extra == "dev"
Requires-Dist: ruff>=0.6.0; extra == "dev"
Requires-Dist: mypy>=1.11.0; extra == "dev"
Dynamic: license-file

# 🤖 WhatsApp Support Bot Architecture & Scaffolding

![Python](https://img.shields.io/badge/Python-3.11%2B-blue)
![FastAPI](https://img.shields.io/badge/FastAPI-0.115.0%2B-009688?logo=fastapi)
![OpenAI](https://img.shields.io/badge/OpenAI-1.50.0%2B-412991?logo=openai)
![Qdrant](https://img.shields.io/badge/Qdrant-Vector%20DB-EF3950?logo=qdrant)
![Langfuse](https://img.shields.io/badge/Langfuse-LLM%20Tracing-yellow)

## 🚀 Vision & Problem Solving

Customer support on WhatsApp can be overwhelming without the right automation. This project provides a **highly scalable, robust, and intelligent WhatsApp Support Bot** foundation.

### How it Solves the Problem:
- **Intelligent Routing**: Uses LLM-based intent recognition to route queries accurately.
- **Human-in-the-Loop**: Seamless handoff from AI to human agents when complex issues arise.
- **Privacy First**: Built-in PII (Personally Identifiable Information) masking to ensure data privacy before processing.
- **Deep Analytics**: Tracks interactions, user satisfaction, and agent performance using SQLite and Prometheus metrics.

> **Note:** **Phase 01** (WhatsApp webhook scaffolding and support bot foundation) is fully complete. **Phase 02 & Phase 03** are pending, and I will be developing them!

---

## 📈 Trending Keywords & Tech Stack
`#WhatsAppBusinessAPI` `#AI` `#LLM` `#RAG` `#VectorDB` `#FastAPI` `#OpenAI` `#Qdrant` `#Langfuse` `#Prometheus` `#Redis` `#AsyncIO` `#Python3.11` `#CustomerSupport` `#Chatbot` `#HumanHandoff`

---

## 🏗 Architecture & Pipeline

The system follows a modular, highly scalable asynchronous architecture:

1. **Webhook Reception**: Webhooks from WhatsApp Business API are received securely via **FastAPI** (`src/api/routes/webhook.py`).
2. **Security & Validation**: Every incoming request payload signature is validated (`src/utils/whatsapp_signature.py`).
3. **Orchestration**: The core routing engine (`src/orchestrator/engine.py`) takes the message, manages conversational flow, and checks configured triggers/fallbacks.
4. **Intelligence & Processing**:
- Sensitive data (PII) is masked dynamically (`src/utils/pii_masker.py`).
- Queries are analyzed using OpenAI LLMs and **Qdrant** vector search (`src/intelligence/`).
- Every reasoning step is traced using **Langfuse**.
5. **Human Handoff**: If the AI cannot resolve the issue confidently, it gracefully triggers the handoff mechanism (`src/handoff/client.py`).
6. **Analytics & Metrics**: Data is saved to an Async SQLite Database (`src/analytics/db.py`) and infrastructure metrics are exposed via **Prometheus**.

---

## 📂 File Arrangements & Structure

```text
whatsapp-support-bot/
├── pyproject.toml # Dependencies and project metadata
├── README.md # Project documentation
├── src/
│ ├── analytics/ # Database models and SQLite async setup
│ ├── api/ # FastAPI setup, dependencies, routes (webhooks)
│ ├── bot/ # Session management with Redis
│ ├── handoff/ # Logic for seamless AI-to-human transition
│ ├── intelligence/ # LLM processing, RAG, and Langfuse tracing
│ ├── orchestrator/ # Core engine, fallbacks, and triggers
│ └── utils/ # Helpers: PII masking, config, loggers, formatters
└── tests/ # Comprehensive test suite (pytest-asyncio, etc.)
```

---

## ⚙️ Requirements & Setup

### Prerequisites
- Python >= 3.11
- Redis Server
- Qdrant Vector Database
- WhatsApp Business API Account
- OpenAI API Key
- Langfuse API Keys (for LLM observability)

### Installation
```bash
# Clone the repository
git clone https://github.com/avuzmal/whatsapp-support-bot.git
cd whatsapp-support-bot

# Install dependencies
pip install -e .

# Run the API server
uvicorn src.api.main:app --host 0.0.0.0 --port 8000 --reload
```

---

## 🛣 Roadmap

- [x] **Phase 1**: Webhook scaffolding, FastAPI foundation, architecture setup.
- [ ] **Phase 2**: Full RAG pipeline integration, advanced LLM reasoning. *(In Progress)*
- [ ] **Phase 3**: Dashboard integration, advanced analytics, real-time agent console. *(Upcoming)*
Loading
Loading