diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..68bf6a9 --- /dev/null +++ b/.env.example @@ -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="" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2f9c812..fc0a294 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..82ecd3f --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/README.md b/README.md index e8acfba..6e23add 100644 --- a/README.md +++ b/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 + 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. \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/app/api/deps.py b/app/api/deps.py new file mode 100644 index 0000000..97f6589 --- /dev/null +++ b/app/api/deps.py @@ -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 \ No newline at end of file diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/api/v1/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/app/api/v1/api.py b/app/api/v1/api.py new file mode 100644 index 0000000..70ffad2 --- /dev/null +++ b/app/api/v1/api.py @@ -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"]) \ No newline at end of file diff --git a/app/api/v1/endpoints/__init__.py b/app/api/v1/endpoints/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/api/v1/endpoints/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/app/api/v1/endpoints/health.py b/app/api/v1/endpoints/health.py new file mode 100644 index 0000000..1732d2e --- /dev/null +++ b/app/api/v1/endpoints/health.py @@ -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 \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..a9740b8 --- /dev/null +++ b/app/core/config.py @@ -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() \ No newline at end of file diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..00d8e0c --- /dev/null +++ b/app/core/security.py @@ -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) \ No newline at end of file diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/db/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/app/db/base.py b/app/db/base.py new file mode 100644 index 0000000..7c2377a --- /dev/null +++ b/app/db/base.py @@ -0,0 +1,3 @@ +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() \ No newline at end of file diff --git a/app/db/session.py b/app/db/session.py new file mode 100644 index 0000000..afa8b0d --- /dev/null +++ b/app/db/session.py @@ -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() \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/app/models/base_model.py b/app/models/base_model.py new file mode 100644 index 0000000..35ef02d --- /dev/null +++ b/app/models/base_model.py @@ -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() \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/app/schemas/token.py b/app/schemas/token.py new file mode 100644 index 0000000..ffb922c --- /dev/null +++ b/app/schemas/token.py @@ -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 \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..2bcdea1 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3fe564a --- /dev/null +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..7e1ae4d --- /dev/null +++ b/k8s/configmap.yaml @@ -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" \ No newline at end of file diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..5ffd789 --- /dev/null +++ b/k8s/deployment.yaml @@ -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 \ No newline at end of file diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..c31984d --- /dev/null +++ b/k8s/ingress.yaml @@ -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 \ No newline at end of file diff --git a/k8s/pvc.yaml b/k8s/pvc.yaml new file mode 100644 index 0000000..3c0ffb8 --- /dev/null +++ b/k8s/pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: whatsapp-medical-chatbot-api-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi \ No newline at end of file diff --git a/k8s/secrets.yaml b/k8s/secrets.yaml new file mode 100644 index 0000000..6c78710 --- /dev/null +++ b/k8s/secrets.yaml @@ -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 \ No newline at end of file diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..208a658 --- /dev/null +++ b/k8s/service.yaml @@ -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 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..0bbe537 --- /dev/null +++ b/main.py @@ -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) \ No newline at end of file diff --git a/monitoring/grafana/provisioning/datasources/datasource.yml b/monitoring/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 0000000..23942e4 --- /dev/null +++ b/monitoring/grafana/provisioning/datasources/datasource.yml @@ -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 \ No newline at end of file diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml new file mode 100644 index 0000000..3022e03 --- /dev/null +++ b/monitoring/prometheus/prometheus.yml @@ -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'] \ No newline at end of file diff --git a/monitoring/promtail/config.yml b/monitoring/promtail/config.yml new file mode 100644 index 0000000..d2ddb48 --- /dev/null +++ b/monitoring/promtail/config.yml @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f0c6061 --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file