diff --git a/.gitignore b/.gitignore index ab7127e..3e113e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,9 @@ -# Byte-compiled / optimized / DLL files +``` +# Python __pycache__/ *.py[cod] *$py.class - -# C extensions *.so - -# Distribution / packaging .Python build/ develop-eggs/ @@ -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 +``` \ No newline at end of file diff --git a/analytics.db b/analytics.db new file mode 100644 index 0000000..7b93046 Binary files /dev/null and b/analytics.db differ diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..7a0133b Binary files /dev/null and b/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/analytics/__pycache__/db.cpython-312.pyc b/src/analytics/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000..6b9ee94 Binary files /dev/null and b/src/analytics/__pycache__/db.cpython-312.pyc differ diff --git a/src/analytics/__pycache__/models.cpython-312.pyc b/src/analytics/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..8bb83fd Binary files /dev/null and b/src/analytics/__pycache__/models.cpython-312.pyc differ diff --git a/src/api/__pycache__/__init__.cpython-312.pyc b/src/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..949d93c Binary files /dev/null and b/src/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/api/__pycache__/dependencies.cpython-312.pyc b/src/api/__pycache__/dependencies.cpython-312.pyc new file mode 100644 index 0000000..5064a16 Binary files /dev/null and b/src/api/__pycache__/dependencies.cpython-312.pyc differ diff --git a/src/api/__pycache__/main.cpython-312.pyc b/src/api/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..e1702a1 Binary files /dev/null and b/src/api/__pycache__/main.cpython-312.pyc differ diff --git a/src/api/dependencies.py b/src/api/dependencies.py index 4e997fd..6736545 100644 --- a/src/api/dependencies.py +++ b/src/api/dependencies.py @@ -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() diff --git a/src/api/routes/__pycache__/__init__.cpython-312.pyc b/src/api/routes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..dc641c7 Binary files /dev/null and b/src/api/routes/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/api/routes/__pycache__/agent_webhook.cpython-312.pyc b/src/api/routes/__pycache__/agent_webhook.cpython-312.pyc new file mode 100644 index 0000000..4c21b78 Binary files /dev/null and b/src/api/routes/__pycache__/agent_webhook.cpython-312.pyc differ diff --git a/src/api/routes/__pycache__/webhook.cpython-312.pyc b/src/api/routes/__pycache__/webhook.cpython-312.pyc new file mode 100644 index 0000000..8d3ef67 Binary files /dev/null and b/src/api/routes/__pycache__/webhook.cpython-312.pyc differ diff --git a/src/api/routes/webhook.py b/src/api/routes/webhook.py index 90019bd..60d4f78 100644 --- a/src/api/routes/webhook.py +++ b/src/api/routes/webhook.py @@ -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 @@ -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: @@ -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: @@ -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. """ @@ -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)) @@ -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. @@ -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 = { @@ -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", @@ -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 @@ -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, @@ -343,6 +397,7 @@ async def receive_webhook( orchestrator, settings.WHATSAPP_ACCESS_TOKEN, background_tasks, + http_client, media_id, media_mime_type, ) diff --git a/src/bot/__pycache__/__init__.cpython-312.pyc b/src/bot/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..dd4f993 Binary files /dev/null and b/src/bot/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/bot/__pycache__/session.cpython-312.pyc b/src/bot/__pycache__/session.cpython-312.pyc new file mode 100644 index 0000000..2a6b3a7 Binary files /dev/null and b/src/bot/__pycache__/session.cpython-312.pyc differ diff --git a/src/handoff/__pycache__/__init__.cpython-312.pyc b/src/handoff/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..004a791 Binary files /dev/null and b/src/handoff/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/handoff/__pycache__/client.cpython-312.pyc b/src/handoff/__pycache__/client.cpython-312.pyc new file mode 100644 index 0000000..1b29833 Binary files /dev/null and b/src/handoff/__pycache__/client.cpython-312.pyc differ diff --git a/src/handoff/__pycache__/payload.cpython-312.pyc b/src/handoff/__pycache__/payload.cpython-312.pyc new file mode 100644 index 0000000..e95f8f6 Binary files /dev/null and b/src/handoff/__pycache__/payload.cpython-312.pyc differ diff --git a/src/intelligence/__pycache__/__init__.cpython-312.pyc b/src/intelligence/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..846b94c Binary files /dev/null and b/src/intelligence/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/intelligence/__pycache__/tracing.cpython-312.pyc b/src/intelligence/__pycache__/tracing.cpython-312.pyc new file mode 100644 index 0000000..db43e6d Binary files /dev/null and b/src/intelligence/__pycache__/tracing.cpython-312.pyc differ diff --git a/src/orchestrator/__pycache__/__init__.cpython-312.pyc b/src/orchestrator/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1e33d3c Binary files /dev/null and b/src/orchestrator/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/orchestrator/__pycache__/engine.cpython-312.pyc b/src/orchestrator/__pycache__/engine.cpython-312.pyc new file mode 100644 index 0000000..4ff2c0b Binary files /dev/null and b/src/orchestrator/__pycache__/engine.cpython-312.pyc differ diff --git a/src/orchestrator/__pycache__/fallback.cpython-312.pyc b/src/orchestrator/__pycache__/fallback.cpython-312.pyc new file mode 100644 index 0000000..c616a8f Binary files /dev/null and b/src/orchestrator/__pycache__/fallback.cpython-312.pyc differ diff --git a/src/orchestrator/__pycache__/triggers.cpython-312.pyc b/src/orchestrator/__pycache__/triggers.cpython-312.pyc new file mode 100644 index 0000000..26eb5b9 Binary files /dev/null and b/src/orchestrator/__pycache__/triggers.cpython-312.pyc differ diff --git a/src/utils/__pycache__/__init__.cpython-312.pyc b/src/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1fd7315 Binary files /dev/null and b/src/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/utils/__pycache__/config.cpython-312.pyc b/src/utils/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..5d35d52 Binary files /dev/null and b/src/utils/__pycache__/config.cpython-312.pyc differ diff --git a/src/utils/__pycache__/logger.cpython-312.pyc b/src/utils/__pycache__/logger.cpython-312.pyc new file mode 100644 index 0000000..aa3350b Binary files /dev/null and b/src/utils/__pycache__/logger.cpython-312.pyc differ diff --git a/src/utils/__pycache__/pii_masker.cpython-312.pyc b/src/utils/__pycache__/pii_masker.cpython-312.pyc new file mode 100644 index 0000000..8a43b67 Binary files /dev/null and b/src/utils/__pycache__/pii_masker.cpython-312.pyc differ diff --git a/src/utils/__pycache__/whatsapp_flows.cpython-312.pyc b/src/utils/__pycache__/whatsapp_flows.cpython-312.pyc new file mode 100644 index 0000000..3527a18 Binary files /dev/null and b/src/utils/__pycache__/whatsapp_flows.cpython-312.pyc differ diff --git a/src/utils/__pycache__/whatsapp_formatter.cpython-312.pyc b/src/utils/__pycache__/whatsapp_formatter.cpython-312.pyc new file mode 100644 index 0000000..1c8ece4 Binary files /dev/null and b/src/utils/__pycache__/whatsapp_formatter.cpython-312.pyc differ diff --git a/src/utils/__pycache__/whatsapp_media.cpython-312.pyc b/src/utils/__pycache__/whatsapp_media.cpython-312.pyc new file mode 100644 index 0000000..35dafbd Binary files /dev/null and b/src/utils/__pycache__/whatsapp_media.cpython-312.pyc differ diff --git a/src/utils/__pycache__/whatsapp_signature.cpython-312.pyc b/src/utils/__pycache__/whatsapp_signature.cpython-312.pyc new file mode 100644 index 0000000..ca208cb Binary files /dev/null and b/src/utils/__pycache__/whatsapp_signature.cpython-312.pyc differ diff --git a/src/utils/__pycache__/whatsapp_templates.cpython-312.pyc b/src/utils/__pycache__/whatsapp_templates.cpython-312.pyc new file mode 100644 index 0000000..ff0ec83 Binary files /dev/null and b/src/utils/__pycache__/whatsapp_templates.cpython-312.pyc differ diff --git a/src/whatsapp_support_bot.egg-info/PKG-INFO b/src/whatsapp_support_bot.egg-info/PKG-INFO new file mode 100644 index 0000000..a45f5b4 --- /dev/null +++ b/src/whatsapp_support_bot.egg-info/PKG-INFO @@ -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)* diff --git a/src/whatsapp_support_bot.egg-info/SOURCES.txt b/src/whatsapp_support_bot.egg-info/SOURCES.txt new file mode 100644 index 0000000..6ec280b --- /dev/null +++ b/src/whatsapp_support_bot.egg-info/SOURCES.txt @@ -0,0 +1,52 @@ +LICENSE +README.md +pyproject.toml +src/__init__.py +src/analytics/db.py +src/analytics/models.py +src/api/__init__.py +src/api/dependencies.py +src/api/main.py +src/api/routes/__init__.py +src/api/routes/agent_webhook.py +src/api/routes/webhook.py +src/bot/__init__.py +src/bot/session.py +src/handoff/__init__.py +src/handoff/client.py +src/handoff/payload.py +src/intelligence/__init__.py +src/intelligence/tracing.py +src/orchestrator/__init__.py +src/orchestrator/engine.py +src/orchestrator/fallback.py +src/orchestrator/triggers.py +src/utils/__init__.py +src/utils/config.py +src/utils/logger.py +src/utils/pii_masker.py +src/utils/whatsapp_flows.py +src/utils/whatsapp_formatter.py +src/utils/whatsapp_media.py +src/utils/whatsapp_signature.py +src/utils/whatsapp_templates.py +src/whatsapp_support_bot.egg-info/PKG-INFO +src/whatsapp_support_bot.egg-info/SOURCES.txt +src/whatsapp_support_bot.egg-info/dependency_links.txt +src/whatsapp_support_bot.egg-info/requires.txt +src/whatsapp_support_bot.egg-info/top_level.txt +tests/test_agent_webhook.py +tests/test_analytics.py +tests/test_degradation.py +tests/test_docker.py +tests/test_fallback.py +tests/test_flows.py +tests/test_handoff.py +tests/test_health.py +tests/test_media.py +tests/test_orchestrator_handoff.py +tests/test_pii_masker.py +tests/test_signature.py +tests/test_templates.py +tests/test_triggers.py +tests/test_webhook.py \ No newline at end of file diff --git a/src/whatsapp_support_bot.egg-info/dependency_links.txt b/src/whatsapp_support_bot.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/whatsapp_support_bot.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/whatsapp_support_bot.egg-info/requires.txt b/src/whatsapp_support_bot.egg-info/requires.txt new file mode 100644 index 0000000..761f6da --- /dev/null +++ b/src/whatsapp_support_bot.egg-info/requires.txt @@ -0,0 +1,22 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +pydantic-settings>=2.5.0 +httpx>=0.27.0 +structlog>=24.4.0 +redis>=5.0.0 +langfuse>=2.0.0 +sqlalchemy[asyncio]>=2.0.0 +aiosqlite>=0.20.0 +prometheus-fastapi-instrumentator>=7.0.0 +openai>=1.50.0 +qdrant-client>=1.11.0 +tiktoken>=0.7.0 +pypdf>=4.0.0 + +[dev] +pytest>=8.3.0 +pytest-asyncio>=0.24.0 +pytest-cov>=5.0.0 +respx>=0.21.0 +ruff>=0.6.0 +mypy>=1.11.0 diff --git a/src/whatsapp_support_bot.egg-info/top_level.txt b/src/whatsapp_support_bot.egg-info/top_level.txt new file mode 100644 index 0000000..5d4600a --- /dev/null +++ b/src/whatsapp_support_bot.egg-info/top_level.txt @@ -0,0 +1,8 @@ +__init__ +analytics +api +bot +handoff +intelligence +orchestrator +utils diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b0051ec Binary files /dev/null and b/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/tests/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..d3b8b71 Binary files /dev/null and b/tests/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_agent_webhook.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_agent_webhook.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..a352f49 Binary files /dev/null and b/tests/__pycache__/test_agent_webhook.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_analytics.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_analytics.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..27dec1c Binary files /dev/null and b/tests/__pycache__/test_analytics.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_degradation.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_degradation.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..344dbac Binary files /dev/null and b/tests/__pycache__/test_degradation.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_docker.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_docker.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..e7c0d81 Binary files /dev/null and b/tests/__pycache__/test_docker.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_fallback.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_fallback.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..01a92e1 Binary files /dev/null and b/tests/__pycache__/test_fallback.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_flows.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_flows.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..87ee4c1 Binary files /dev/null and b/tests/__pycache__/test_flows.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_handoff.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_handoff.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..83db246 Binary files /dev/null and b/tests/__pycache__/test_handoff.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_health.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_health.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..06ba4d4 Binary files /dev/null and b/tests/__pycache__/test_health.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_media.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_media.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..78cbb5f Binary files /dev/null and b/tests/__pycache__/test_media.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_orchestrator_handoff.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_orchestrator_handoff.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..ebc0de4 Binary files /dev/null and b/tests/__pycache__/test_orchestrator_handoff.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_pii_masker.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_pii_masker.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..0918c31 Binary files /dev/null and b/tests/__pycache__/test_pii_masker.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_signature.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_signature.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..9023406 Binary files /dev/null and b/tests/__pycache__/test_signature.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_templates.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_templates.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..210d37c Binary files /dev/null and b/tests/__pycache__/test_templates.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_triggers.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_triggers.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..719102c Binary files /dev/null and b/tests/__pycache__/test_triggers.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_webhook.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_webhook.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..e4b7992 Binary files /dev/null and b/tests/__pycache__/test_webhook.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/test_docker.py b/tests/test_docker.py index 724da31..1781121 100644 --- a/tests/test_docker.py +++ b/tests/test_docker.py @@ -1,13 +1,13 @@ -import os import shutil import subprocess +from pathlib import Path def test_dockerfile_and_compose_structure() -> None: """Verifies that the Dockerfile and docker compose files exist and have required production directives.""" # Check Dockerfile - assert os.path.exists("Dockerfile") - with open("Dockerfile") as f: + assert Path("Dockerfile").exists() + with Path("Dockerfile").open() as f: dockerfile_content = f.read() assert "FROM python:3.11-slim AS builder" in dockerfile_content @@ -17,8 +17,8 @@ def test_dockerfile_and_compose_structure() -> None: assert "uvicorn" in dockerfile_content # Check docker-compose.yml - assert os.path.exists("docker-compose.yml") - with open("docker-compose.yml") as f: + assert Path("docker-compose.yml").exists() + with Path("docker-compose.yml").open() as f: compose_content = f.read() assert "redis:" in compose_content assert "qdrant:" in compose_content @@ -27,8 +27,8 @@ def test_dockerfile_and_compose_structure() -> None: assert "healthcheck:" in compose_content # Check docker-compose.prod.yml - assert os.path.exists("infra/docker-compose.prod.yml") - with open("infra/docker-compose.prod.yml") as f: + assert Path("infra/docker-compose.prod.yml").exists() + with Path("infra/docker-compose.prod.yml").open() as f: prod_compose_content = f.read() assert "restart: unless-stopped" in prod_compose_content assert "limits:" in prod_compose_content @@ -43,7 +43,7 @@ def test_docker_build_integration() -> None: # Check if docker daemon is running try: - subprocess.run(["docker", "info"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.run(["docker", "info"], check=True, capture_output=True) except (subprocess.CalledProcessError, FileNotFoundError): # Docker daemon is not running or accessible, skip return