Initialize WhatsApp Medical Chatbot API project structure
This commit is contained in:
parent
b4298ad662
commit
313d8f3b49
22
.env.example
Normal file
22
.env.example
Normal file
@ -0,0 +1,22 @@
|
||||
# Application
|
||||
APP_NAME="WhatsApp Medical Chatbot API"
|
||||
API_V1_PREFIX="/api/v1"
|
||||
DEBUG=True
|
||||
ENVIRONMENT="development"
|
||||
|
||||
# Security
|
||||
SECRET_KEY="" # Generate using: openssl rand -hex 32
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
ALGORITHM="HS256"
|
||||
|
||||
# WhatsApp API
|
||||
WHATSAPP_API_URL=""
|
||||
WHATSAPP_API_TOKEN=""
|
||||
WHATSAPP_API_PHONE_NUMBER=""
|
||||
WHATSAPP_VERIFY_TOKEN=""
|
||||
|
||||
# OpenAI API for NLP tasks
|
||||
OPENAI_API_KEY=""
|
||||
|
||||
# Speech-to-Text Service
|
||||
SPEECH_TO_TEXT_API_KEY=""
|
159
.gitignore
vendored
159
.gitignore
vendored
@ -1,17 +1,11 @@
|
||||
repos*
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
media/
|
||||
*.db
|
||||
whitelist.txt
|
||||
ai_docs/
|
||||
specs/
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
test_cases.py
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
@ -26,170 +20,37 @@ parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
test_case1.py
|
||||
api/core/dependencies/mailjet.py
|
||||
tests/v1/waitlist/waitlist_test.py
|
||||
result.json
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
test_case1.py
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
case_test.py
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
*.sqlite3
|
||||
*.sqlite
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env*
|
||||
!.env.sample
|
||||
.env
|
||||
.venv
|
||||
.blog_env/
|
||||
env/
|
||||
venv*
|
||||
*venv/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
.vscode/
|
||||
jeff.py
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
**/.DS_Store
|
||||
.aider*
|
||||
|
||||
|
||||
# IDE specific files
|
||||
.idea/
|
||||
.dump.rdb
|
||||
.celery.log
|
||||
docker-compose.yaml
|
||||
# project analysis result
|
||||
analysis_results.json
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
**/.claude/settings.local.json
|
||||
*.aider
|
||||
.claude/
|
||||
# Project specific
|
||||
/storage/
|
||||
.DS_Store
|
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@ -0,0 +1,29 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libcairo2-dev \
|
||||
libpango1.0-dev \
|
||||
libffi-dev \
|
||||
shared-mime-info \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first for better caching
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /app/storage/db /app/storage/voice_notes /app/storage/reports
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
120
README.md
120
README.md
@ -1,3 +1,119 @@
|
||||
# FastAPI Application
|
||||
# WhatsApp Medical Chatbot API
|
||||
|
||||
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
|
||||
This is a FastAPI-based API for a WhatsApp medical chatbot that provides various healthcare services.
|
||||
|
||||
## Features
|
||||
|
||||
- WhatsApp integration for chat-based interactions
|
||||
- Consultation booking with healthcare professionals
|
||||
- OTC drug purchase capabilities
|
||||
- Symptom checking and clinical triage
|
||||
- Doctor report generation
|
||||
- Voice note processing for audio input
|
||||
- Monitoring with Grafana, Prometheus, Loki, and Promtail
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend**: Python 3.11+ with FastAPI
|
||||
- **Database**: SQLite with SQLAlchemy ORM
|
||||
- **Authentication**: JWT-based authentication
|
||||
- **Documentation**: OpenAPI (Swagger UI and ReDoc)
|
||||
- **Monitoring**: Prometheus, Grafana, Loki, and Promtail
|
||||
- **Deployment**: Docker, Docker Compose, and Kubernetes
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.11+
|
||||
- Docker and Docker Compose (for containerized deployment)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd whatsapp-medical-chatbot-api
|
||||
```
|
||||
|
||||
2. Create a virtual environment:
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. Create a `.env` file based on `.env.example`:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
Then edit the `.env` file to set the required environment variables.
|
||||
|
||||
### Running the Application
|
||||
|
||||
#### Using Python
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
#### Using Docker
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The application requires the following environment variables:
|
||||
|
||||
- `APP_NAME`: Name of the application
|
||||
- `API_V1_PREFIX`: Prefix for API v1 endpoints
|
||||
- `SECRET_KEY`: Secret key for JWT token generation
|
||||
- `ACCESS_TOKEN_EXPIRE_MINUTES`: JWT token expiration time in minutes
|
||||
- `WHATSAPP_API_URL`: WhatsApp API URL
|
||||
- `WHATSAPP_API_TOKEN`: WhatsApp API authentication token
|
||||
- `WHATSAPP_API_PHONE_NUMBER`: WhatsApp phone number for the chatbot
|
||||
- `WHATSAPP_VERIFY_TOKEN`: WhatsApp webhook verification token
|
||||
- `OPENAI_API_KEY`: OpenAI API key for NLP tasks
|
||||
- `SPEECH_TO_TEXT_API_KEY`: API key for speech-to-text service
|
||||
|
||||
## API Documentation
|
||||
|
||||
The API documentation is available at:
|
||||
|
||||
- Swagger UI: `http://localhost:8000/docs`
|
||||
- ReDoc: `http://localhost:8000/redoc`
|
||||
- OpenAPI JSON: `http://localhost:8000/openapi.json`
|
||||
|
||||
## Monitoring
|
||||
|
||||
The application is set up with monitoring using Prometheus, Grafana, Loki, and Promtail:
|
||||
|
||||
- Prometheus: `http://localhost:9090`
|
||||
- Grafana: `http://localhost:3000`
|
||||
- Loki: `http://localhost:3100`
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
|
||||
Kubernetes manifests are provided in the `k8s` directory.
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
1
app/api/__init__.py
Normal file
1
app/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
51
app/api/deps.py
Normal file
51
app/api/deps.py
Normal file
@ -0,0 +1,51 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import jwt, JWTError
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import verify_password
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.token import TokenPayload
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_PREFIX}/auth/login")
|
||||
|
||||
|
||||
def get_current_user(
|
||||
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
|
||||
) -> User:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
token_data = TokenPayload(**payload)
|
||||
except (JWTError, ValidationError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
user = db.query(User).filter(User.id == token_data.sub).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
|
||||
def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
def authenticate_user(db: Session, email: str, password: str) -> Optional[User]:
|
||||
user = db.query(User).filter(User.email == email).first()
|
||||
if not user:
|
||||
return None
|
||||
if not verify_password(password, user.password):
|
||||
return None
|
||||
return user
|
1
app/api/v1/__init__.py
Normal file
1
app/api/v1/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
6
app/api/v1/api.py
Normal file
6
app/api/v1/api.py
Normal file
@ -0,0 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.endpoints import health
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router, tags=["health"])
|
1
app/api/v1/endpoints/__init__.py
Normal file
1
app/api/v1/endpoints/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
23
app/api/v1/endpoints/health.py
Normal file
23
app/api/v1/endpoints/health.py
Normal file
@ -0,0 +1,23 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health", summary="Health check")
|
||||
def health_check(db: Session = Depends(get_db)):
|
||||
try:
|
||||
# Check database connection
|
||||
db.execute("SELECT 1")
|
||||
db_status = "ok"
|
||||
except Exception as e:
|
||||
db_status = f"error: {str(e)}"
|
||||
|
||||
health_status = {
|
||||
"status": "ok",
|
||||
"database": db_status,
|
||||
}
|
||||
|
||||
return health_status
|
1
app/core/__init__.py
Normal file
1
app/core/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
51
app/core/config.py
Normal file
51
app/core/config.py
Normal file
@ -0,0 +1,51 @@
|
||||
import os
|
||||
from typing import List, Optional, Union
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import AnyHttpUrl, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
APP_NAME: str = "WhatsApp Medical Chatbot API"
|
||||
API_V1_PREFIX: str = "/api/v1"
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "")
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
ALGORITHM: str = "HS256"
|
||||
|
||||
# CORS
|
||||
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
||||
|
||||
@validator("BACKEND_CORS_ORIGINS", pre=True)
|
||||
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
|
||||
if isinstance(v, str) and not v.startswith("["):
|
||||
return [i.strip() for i in v.split(",")]
|
||||
elif isinstance(v, (list, str)):
|
||||
return v
|
||||
raise ValueError(v)
|
||||
|
||||
# Database
|
||||
DB_DIR: Path = Path("/app") / "storage" / "db"
|
||||
|
||||
# WhatsApp API
|
||||
WHATSAPP_API_URL: Optional[str] = None
|
||||
WHATSAPP_API_TOKEN: Optional[str] = None
|
||||
WHATSAPP_API_PHONE_NUMBER: Optional[str] = None
|
||||
WHATSAPP_VERIFY_TOKEN: Optional[str] = None
|
||||
|
||||
# OpenAI API for NLP tasks
|
||||
OPENAI_API_KEY: Optional[str] = None
|
||||
|
||||
# Speech-to-Text Service
|
||||
SPEECH_TO_TEXT_API_KEY: Optional[str] = None
|
||||
|
||||
ENVIRONMENT: str = "development"
|
||||
DEBUG: bool = True
|
||||
|
||||
class Config:
|
||||
case_sensitive = True
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
|
||||
settings = Settings()
|
29
app/core/security.py
Normal file
29
app/core/security.py
Normal file
@ -0,0 +1,29 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def create_access_token(subject: str, expires_delta: Optional[timedelta] = None) -> str:
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode = {"exp": expire, "sub": str(subject)}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
1
app/db/__init__.py
Normal file
1
app/db/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
3
app/db/base.py
Normal file
3
app/db/base.py
Normal file
@ -0,0 +1,3 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
23
app/db/session.py
Normal file
23
app/db/session.py
Normal file
@ -0,0 +1,23 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
# Ensure DB directory exists
|
||||
settings.DB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = f"sqlite:///{settings.DB_DIR}/db.sqlite"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL,
|
||||
connect_args={"check_same_thread": False}
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
1
app/models/__init__.py
Normal file
1
app/models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
15
app/models/base_model.py
Normal file
15
app/models/base_model.py
Normal file
@ -0,0 +1,15 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, DateTime, String
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
|
||||
|
||||
class BaseModel:
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(cls) -> str:
|
||||
return cls.__name__.lower()
|
1
app/schemas/__init__.py
Normal file
1
app/schemas/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
16
app/schemas/token.py
Normal file
16
app/schemas/token.py
Normal file
@ -0,0 +1,16 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenPayload(BaseModel):
|
||||
sub: Optional[str] = None
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Optional[str] = None
|
1
app/services/__init__.py
Normal file
1
app/services/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
1
app/utils/__init__.py
Normal file
1
app/utils/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to make the directory a Python package
|
78
docker-compose.yml
Normal file
78
docker-compose.yml
Normal file
@ -0,0 +1,78 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
api:
|
||||
build: .
|
||||
container_name: whatsapp-medical-chatbot-api
|
||||
restart: always
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./app:/app/app
|
||||
- ./storage:/app/storage
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
restart: always
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
restart: always
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning
|
||||
depends_on:
|
||||
- prometheus
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
loki:
|
||||
image: grafana/loki:latest
|
||||
container_name: loki
|
||||
restart: always
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:latest
|
||||
container_name: promtail
|
||||
restart: always
|
||||
volumes:
|
||||
- ./storage/logs:/var/log
|
||||
- ./monitoring/promtail/config.yml:/etc/promtail/config.yml
|
||||
command: -config.file=/etc/promtail/config.yml
|
||||
depends_on:
|
||||
- loki
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
prometheus_data:
|
||||
grafana_data:
|
9
k8s/configmap.yaml
Normal file
9
k8s/configmap.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: whatsapp-medical-chatbot-api-config
|
||||
data:
|
||||
APP_NAME: "WhatsApp Medical Chatbot API"
|
||||
API_V1_PREFIX: "/api/v1"
|
||||
WHATSAPP_API_URL: "https://api.example.com/whatsapp"
|
||||
WHATSAPP_API_PHONE_NUMBER: "+1234567890"
|
87
k8s/deployment.yaml
Normal file
87
k8s/deployment.yaml
Normal file
@ -0,0 +1,87 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: whatsapp-medical-chatbot-api
|
||||
labels:
|
||||
app: whatsapp-medical-chatbot-api
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: whatsapp-medical-chatbot-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: whatsapp-medical-chatbot-api
|
||||
spec:
|
||||
containers:
|
||||
- name: api
|
||||
image: whatsapp-medical-chatbot-api:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
env:
|
||||
- name: APP_NAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: whatsapp-medical-chatbot-api-config
|
||||
key: APP_NAME
|
||||
- name: API_V1_PREFIX
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: whatsapp-medical-chatbot-api-config
|
||||
key: API_V1_PREFIX
|
||||
- name: SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: whatsapp-medical-chatbot-api-secrets
|
||||
key: SECRET_KEY
|
||||
- name: WHATSAPP_API_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: whatsapp-medical-chatbot-api-config
|
||||
key: WHATSAPP_API_URL
|
||||
- name: WHATSAPP_API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: whatsapp-medical-chatbot-api-secrets
|
||||
key: WHATSAPP_API_TOKEN
|
||||
- name: WHATSAPP_API_PHONE_NUMBER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: whatsapp-medical-chatbot-api-config
|
||||
key: WHATSAPP_API_PHONE_NUMBER
|
||||
- name: WHATSAPP_VERIFY_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: whatsapp-medical-chatbot-api-secrets
|
||||
key: WHATSAPP_VERIFY_TOKEN
|
||||
- name: OPENAI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: whatsapp-medical-chatbot-api-secrets
|
||||
key: OPENAI_API_KEY
|
||||
- name: SPEECH_TO_TEXT_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: whatsapp-medical-chatbot-api-secrets
|
||||
key: SPEECH_TO_TEXT_API_KEY
|
||||
volumeMounts:
|
||||
- name: storage
|
||||
mountPath: /app/storage
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: whatsapp-medical-chatbot-api-pvc
|
19
k8s/ingress.yaml
Normal file
19
k8s/ingress.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: whatsapp-medical-chatbot-api-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
rules:
|
||||
- host: api.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: whatsapp-medical-chatbot-api
|
||||
port:
|
||||
number: 80
|
10
k8s/pvc.yaml
Normal file
10
k8s/pvc.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: whatsapp-medical-chatbot-api-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
11
k8s/secrets.yaml
Normal file
11
k8s/secrets.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: whatsapp-medical-chatbot-api-secrets
|
||||
type: Opaque
|
||||
data:
|
||||
SECRET_KEY: YOUR_BASE64_ENCODED_SECRET_KEY
|
||||
WHATSAPP_API_TOKEN: YOUR_BASE64_ENCODED_WHATSAPP_API_TOKEN
|
||||
WHATSAPP_VERIFY_TOKEN: YOUR_BASE64_ENCODED_WHATSAPP_VERIFY_TOKEN
|
||||
OPENAI_API_KEY: YOUR_BASE64_ENCODED_OPENAI_API_KEY
|
||||
SPEECH_TO_TEXT_API_KEY: YOUR_BASE64_ENCODED_SPEECH_TO_TEXT_API_KEY
|
14
k8s/service.yaml
Normal file
14
k8s/service.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: whatsapp-medical-chatbot-api
|
||||
labels:
|
||||
app: whatsapp-medical-chatbot-api
|
||||
spec:
|
||||
selector:
|
||||
app: whatsapp-medical-chatbot-api
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8000
|
||||
protocol: TCP
|
||||
type: ClusterIP
|
63
main.py
Normal file
63
main.py
Normal file
@ -0,0 +1,63 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from prometheus_fastapi_instrumentator import Instrumentator
|
||||
|
||||
from app.api.v1.api import api_router
|
||||
from app.core.config import settings
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
openapi_url="/openapi.json",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
# Set up CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Allow all origins
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"], # Allow all methods
|
||||
allow_headers=["*"], # Allow all headers
|
||||
)
|
||||
|
||||
# Add Prometheus metrics
|
||||
Instrumentator().instrument(app).expose(app)
|
||||
|
||||
# Include API router
|
||||
app.include_router(api_router, prefix=settings.API_V1_PREFIX)
|
||||
|
||||
|
||||
@app.get("/", tags=["Root"])
|
||||
async def root():
|
||||
"""
|
||||
Root endpoint that returns application information.
|
||||
"""
|
||||
return {
|
||||
"name": settings.APP_NAME,
|
||||
"docs": "/docs",
|
||||
"health_check": f"{settings.API_V1_PREFIX}/health",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
# Ensure storage directories exist
|
||||
storage_dir = Path("/app/storage")
|
||||
storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
(storage_dir / "db").mkdir(exist_ok=True)
|
||||
(storage_dir / "voice_notes").mkdir(exist_ok=True)
|
||||
(storage_dir / "reports").mkdir(exist_ok=True)
|
||||
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
15
monitoring/grafana/provisioning/datasources/datasource.yml
Normal file
15
monitoring/grafana/provisioning/datasources/datasource.yml
Normal file
@ -0,0 +1,15 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: false
|
||||
|
||||
- name: Loki
|
||||
type: loki
|
||||
access: proxy
|
||||
url: http://loki:3100
|
||||
editable: false
|
12
monitoring/prometheus/prometheus.yml
Normal file
12
monitoring/prometheus/prometheus.yml
Normal file
@ -0,0 +1,12 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
- job_name: 'whatsapp-medical-chatbot-api'
|
||||
static_configs:
|
||||
- targets: ['api:8000']
|
18
monitoring/promtail/config.yml
Normal file
18
monitoring/promtail/config.yml
Normal file
@ -0,0 +1,18 @@
|
||||
server:
|
||||
http_listen_port: 9080
|
||||
grpc_listen_port: 0
|
||||
|
||||
positions:
|
||||
filename: /tmp/positions.yaml
|
||||
|
||||
clients:
|
||||
- url: http://loki:3100/loki/api/v1/push
|
||||
|
||||
scrape_configs:
|
||||
- job_name: system
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost
|
||||
labels:
|
||||
job: whatsapp-medical-chatbot-api
|
||||
__path__: /var/log/*log
|
21
requirements.txt
Normal file
21
requirements.txt
Normal file
@ -0,0 +1,21 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
pydantic==2.4.2
|
||||
pydantic-settings==2.0.3
|
||||
sqlalchemy==2.0.23
|
||||
alembic==1.12.1
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-multipart==0.0.6
|
||||
requests==2.31.0
|
||||
httpx==0.25.1
|
||||
aiofiles==23.2.1
|
||||
python-dotenv==1.0.0
|
||||
tenacity==8.2.3
|
||||
pytest==7.4.3
|
||||
speechrecognition==3.10.0
|
||||
jinja2==3.1.2
|
||||
weasyprint==60.1
|
||||
ruff==0.1.5
|
||||
prometheus-client==0.17.1
|
||||
prometheus-fastapi-instrumentator==6.1.0
|
Loading…
x
Reference in New Issue
Block a user