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
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
media/
|
|
||||||
*.db
|
|
||||||
whitelist.txt
|
|
||||||
ai_docs/
|
|
||||||
specs/
|
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
test_cases.py
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
@ -26,170 +20,37 @@ parts/
|
|||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.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
|
# Unit test / coverage reports
|
||||||
htmlcov/
|
htmlcov/
|
||||||
.tox/
|
.tox/
|
||||||
.nox/
|
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
.cache
|
.cache
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
*.cover
|
*.cover
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.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
|
# Environments
|
||||||
.env*
|
.env
|
||||||
!.env.sample
|
|
||||||
.venv
|
.venv
|
||||||
.blog_env/
|
|
||||||
env/
|
env/
|
||||||
venv*
|
venv/
|
||||||
*venv/
|
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# IDE specific files
|
||||||
# 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*
|
|
||||||
|
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.dump.rdb
|
.vscode/
|
||||||
.celery.log
|
*.swp
|
||||||
docker-compose.yaml
|
*.swo
|
||||||
# project analysis result
|
|
||||||
analysis_results.json
|
|
||||||
|
|
||||||
**/.claude/settings.local.json
|
# Project specific
|
||||||
*.aider
|
/storage/
|
||||||
.claude/
|
.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