feat: initial commit

This commit is contained in:
deepvasoya 2025-05-09 19:15:53 +05:30
commit 80c61dc127
54 changed files with 2195 additions and 0 deletions

108
.gitignore vendored Normal file
View File

@ -0,0 +1,108 @@
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# 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
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.idea/*

119
alembic.ini Normal file
View File

@ -0,0 +1,119 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
version_path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = postgresql://postgres:/*7984@localhost:5432/ai-appointment
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

14
apis/__init__.py Normal file
View File

@ -0,0 +1,14 @@
from fastapi import APIRouter, Depends
from middleware.auth_dependency import auth_required
from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth
api_router = APIRouter()
# api_router.include_router(twilio.router, prefix="/twilio")
api_router.include_router(clinics.router, prefix="/clinics", tags=["clinics"])
api_router.include_router(doctors.router, prefix="/doctors", tags=["doctors"])
api_router.include_router(calender.router, prefix="/calender", tags=["calender"])
api_router.include_router(appointments.router, prefix="/appointments", tags=["appointments"])
api_router.include_router(patients.router, prefix="/patients", tags=["patients"])
api_router.include_router(admin.router, prefix="/admin", dependencies=[Depends(auth_required)], tags=["admin"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])

View File

8
apis/endpoints/admin.py Normal file
View File

@ -0,0 +1,8 @@
from fastapi import APIRouter, status
router = APIRouter()
@router.get("/", status_code=status.HTTP_200_OK)
def get_admin():
return {"message": "Admin"}

View File

@ -0,0 +1,117 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
# database
from database import get_db
from schemas.ResponseSchemas import AppointmentDetailed, AppointmentSchema
from models.Appointments import Appointments
from schemas.CreateSchemas import AppointmentCreate, AppointmentCreateWithNames
from models.Doctors import Doctors
from models.Patients import Patients
router = APIRouter()
@router.get(
"/", response_model=List[AppointmentDetailed], status_code=status.HTTP_200_OK
)
def get_appointments(
doc_name: str | None = None,
patient_name: str | None = None,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
):
"""
Get a list of appointments with optional pagination.
"""
try:
query = db.query(Appointments)
if doc_name:
query = query.join(Appointments.doctor).filter(
Doctors.name.ilike(f"%{doc_name}%")
)
if patient_name:
query = query.join(Appointments.patient).filter(
Patients.name.ilike(f"%{patient_name}%")
)
appointments = query.offset(skip).limit(limit).all()
return appointments
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e.__cause__),
) from e
# @router.post("/", response_model=AppointmentSchema, status_code=status.HTTP_201_CREATED)
# def create_appointment(appointment: AppointmentCreate, db: Session = Depends(get_db)):
# """
# Create a new appointment.
# """
# try:
# db_appointment = Appointments(**appointment.model_dump())
# db.add(db_appointment)
# db.commit()
# db.refresh(db_appointment)
# return db_appointment
# except Exception as e:
# db.rollback()
# raise HTTPException(
# status_code=500,
# detail=str(e.__cause__),
# ) from e
@router.post("/", response_model=AppointmentSchema, status_code=status.HTTP_201_CREATED)
def create_appointment_with_names(
appointment: AppointmentCreateWithNames, db: Session = Depends(get_db)
):
"""
Create a new appointment using doctor name and patient name instead of IDs.
"""
try:
# Find doctor by name
doctor = (
db.query(Doctors).filter(Doctors.name == appointment.doctor_name).first()
)
if not doctor:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Doctor with name '{appointment.doctor_name}' not found",
)
# Find patient by name
patient = (
db.query(Patients).filter(Patients.name == appointment.patient_name).first()
)
if not patient:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Patient with name '{appointment.patient_name}' not found",
)
# Create appointment with doctor_id and patient_id
db_appointment = Appointments(
doctor_id=doctor.id,
patient_id=patient.id,
appointment_time=appointment.appointment_time,
status=appointment.status,
)
db.add(db_appointment)
db.commit()
db.refresh(db_appointment)
return db_appointment
except HTTPException:
db.rollback()
raise
except Exception as e:
db.rollback()
raise HTTPException(
status_code=500,
detail=str(e.__cause__),
) from e

23
apis/endpoints/auth.py Normal file
View File

@ -0,0 +1,23 @@
from fastapi import APIRouter
from services.authService import AuthService
from schemas.CreateSchemas import UserCreate
from schemas.ApiResponse import ApiResponse
router = APIRouter()
@router.post("/login")
async def login(email: str, password: str):
response = await AuthService().login(email, password)
return ApiResponse(
data=response,
message="Login successful"
)
@router.post("/register")
async def register(user_data: UserCreate):
response = await AuthService().register(user_data)
return ApiResponse(
data=response,
message="User registered successfully"
)

View File

@ -0,0 +1,41 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
# database
from database import get_db
from models.Calendar import Calenders
from schemas.CreateSchemas import CalendarCreate
from schemas.ResponseSchemas import Calendar
router = APIRouter()
@router.get("/", response_model=List[Calendar])
def get_calendar_events(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
"""
Get a list of calendar events with optional pagination.
"""
# Placeholder for actual database query
events = db.query("CalendarEvents").offset(skip).limit(limit).all()
return events
@router.post("/", response_model=Calendar, status_code=status.HTTP_201_CREATED)
def create_calendar_event(event: CalendarCreate, db: Session = Depends(get_db)):
"""
Create a new calendar event.
"""
try:
db_event = Calenders(**event.model_dump())
db.add(db_event)
db.commit()
db.refresh(db_event)
return db_event
except Exception as e:
db.rollback()
raise HTTPException(
status_code=500,
detail=str(e.__cause__),
) from e

79
apis/endpoints/clinics.py Normal file
View File

@ -0,0 +1,79 @@
from asyncio.log import logger
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
# database
from database import get_db
# schemas
from schemas.ResponseSchemas import Clinic, ClinicWithDoctors
from schemas.CreateSchemas import ClinicCreate
from schemas.UpdateSchemas import ClinicUpdate
from models.Clinics import Clinics
# Constants
from utils.constants import DEFAULT_SKIP, DEFAULT_LIMIT
router = APIRouter()
@router.get("/", response_model=List[Clinic])
async def get_clinics(
skip: int = DEFAULT_SKIP, limit: int = DEFAULT_LIMIT, db: Session = Depends(get_db)
):
clinics = db.query(Clinics).offset(skip).limit(limit).all()
return clinics
@router.get("/{clinic_id}", response_model=ClinicWithDoctors)
async def get_clinic(clinic_id: int, db: Session = Depends(get_db)):
db_clinic = db.query(Clinics).where(Clinics.id == clinic_id).first()
if db_clinic is None:
raise HTTPException(status_code=404, detail="Clinic not found")
return db_clinic
@router.post("/", response_model=Clinic, status_code=status.HTTP_201_CREATED)
async def create_clinic(clinic: ClinicCreate, db: Session = Depends(get_db)):
try:
db_clinic = Clinics(**clinic.model_dump())
db.add(db_clinic)
db.commit()
db.refresh(db_clinic)
return db_clinic
except Exception as e:
db.rollback()
raise HTTPException(
status_code=500,
detail=str(e.__cause__),
) from e
@router.put("/{clinic_id}", response_model=Clinic)
async def update_clinic(
clinic_id: int, clinic: ClinicUpdate, db: Session = Depends(get_db)
):
db_clinic = db.query(Clinics).filter(Clinics.id == clinic_id).first()
if db_clinic is None:
raise HTTPException(status_code=404, detail="Clinic not found")
update_data = clinic.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(db_clinic, key, value)
db.commit()
db.refresh(db_clinic)
return db_clinic
@router.delete("/{clinic_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_clinic(clinic_id: int, db: Session = Depends(get_db)):
db_clinic = db.query(Clinics).where(Clinics.id == clinic_id).first()
if db_clinic is None:
raise HTTPException(status_code=404, detail="Clinic not found")
db.delete(db_clinic)
db.commit()
return None

143
apis/endpoints/doctors.py Normal file
View File

@ -0,0 +1,143 @@
from asyncio.log import logger
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Dict
from datetime import datetime, timedelta
from sqlalchemy import and_
# database
from database import get_db
# schemas
from models.Doctors import Doctors
from models.Appointments import Appointments
from models.Calendar import Calenders
from schemas.ResponseSchemas import (
Doctor,
DoctorWithAppointments,
DoctorWithCalendar,
CalendarTimeSchema,
)
from schemas.CreateSchemas import DoctorCreate
from schemas.UpdateSchemas import DoctorUpdate
from enums.enums import AppointmentStatus
router = APIRouter()
@router.post("/", response_model=Doctor, status_code=status.HTTP_201_CREATED)
def create_doctor(doctor: DoctorCreate, db: Session = Depends(get_db)):
try:
db_doctor = Doctors(**doctor.model_dump())
db.add(db_doctor)
db.commit()
db.refresh(db_doctor)
return db_doctor
except Exception as e:
db.rollback()
raise HTTPException(
status_code=500,
detail=str(e.__cause__),
) from e
@router.get("/", response_model=List[DoctorWithCalendar])
def read_doctors(
doctor_name: str | None = None,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
):
query = db.query(Doctors)
if doctor_name:
query = query.filter(Doctors.name.ilike(f"%{doctor_name}%"))
doctors = query.offset(skip).limit(limit).all()
return doctors
@router.get("/available-slots/{doctor_name}", response_model=Dict[str, List[str]])
def get_available_slots(
doctor_name: str | None = None,
date: str | None = datetime.now().strftime("%Y-%m-%d"),
db: Session = Depends(get_db),
):
"""
Get available slots for a doctor on a specific date.
date format: YYYY-MM-DD
"""
# Get the doctor
print(f"-----------------doctor_name: {doctor_name}")
doctor = db.query(Doctors).filter(Doctors.name.ilike(f"%{doctor_name}%")).first()
if not doctor:
raise HTTPException(status_code=404, detail="Doctor not found")
# Get all calendar slots for the doctor
calendar_slots = db.query(Calenders).filter(Calenders.doc_id == doctor.id).all()
if not calendar_slots:
return {"available_slots": []}
available_slots = [slot.time for slot in calendar_slots]
try:
target_date = datetime.strptime(date, "%Y-%m-%d").date()
except ValueError:
raise HTTPException(
status_code=400, detail="Invalid date format. Use YYYY-MM-DD"
)
# Get all appointments for the doctor on the specified date
appointments = (
db.query(Appointments)
.filter(
and_(
Appointments.doctor_id == doctor.id,
Appointments.appointment_time >= target_date,
Appointments.appointment_time < target_date + timedelta(days=1),
)
)
.all()
)
# Remove slots that have appointments
for appointment in appointments:
appointment_time = appointment.appointment_time.strftime("%H:%M")
if appointment_time in available_slots and (
not appointment.status == AppointmentStatus.COMPLETED
):
available_slots.remove(appointment_time)
return {"available_slots": available_slots}
# @router.get("/{doctor_name}", response_model=DoctorWithAppointments)
# def read_doctor(doctor_name: str, db: Session = Depends(get_db)):
# db_doctor = db.query(Doctors).filter(Doctors.name.ilike(f"%{doctor_name}%")).all()
# return db_doctor
@router.put("/{doctor_id}", response_model=Doctor)
def update_doctor(doctor_id: int, doctor: DoctorUpdate, db: Session = Depends(get_db)):
db_doctor = db.query(Doctors).filter(Doctors.id == doctor_id).first()
if db_doctor is None:
raise HTTPException(status_code=404, detail="Doctor not found")
update_data = doctor.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(db_doctor, key, value)
db.commit()
db.refresh(db_doctor)
return db_doctor
@router.delete("/{doctor_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_doctor(doctor_id: int, db: Session = Depends(get_db)):
db_doctor = db.query(Doctors).filter(Doctors.id == doctor_id).first()
if db_doctor is None:
raise HTTPException(status_code=404, detail="Doctor not found")
db.delete(db_doctor)
db.commit()
return None

View File

@ -0,0 +1,50 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
# database
from database import get_db
from models.Patients import Patients
from schemas.CreateSchemas import PatientCreate
from schemas.ResponseSchemas import Patient
router = APIRouter()
@router.get("/", response_model=List[Patient])
def read_patients(
name: str | None = None,
dob: str | None = None,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
):
"""
Get a list of patients with optional pagination.
"""
query = db.query(Patients)
if name:
query = query.filter(Patients.name.ilike(f"%{name}%"))
if dob:
query = query.filter(Patients.dob == dob)
patients = query.offset(skip).limit(limit).all()
return patients
@router.post("/", response_model=Patient, status_code=status.HTTP_201_CREATED)
def create_patient(patient: PatientCreate, db: Session = Depends(get_db)):
"""
Create a new patient.
"""
try:
db_patient = Patients(**patient.model_dump())
db.add(db_patient)
db.commit()
db.refresh(db_patient)
return db_patient
except Exception as e:
db.rollback()
raise HTTPException(
status_code=500,
detail=str(e.__cause__),
) from e

183
apis/endpoints/twilio.py Normal file
View File

@ -0,0 +1,183 @@
# import asyncio
# import json
# import logging
# import os
# from fastapi import APIRouter, Request, WebSocket, status
# from twilio.twiml.voice_response import VoiceResponse, Connect
# from twilio.rest import Client
# from fastapi import WebSocket, Request, Response
# from enum import Enum
# from typing import Optional
# from services.bot import run_bot
# from services.call_state import CallState
# logger = logging.getLogger(__name__)
# router = APIRouter()
# BASE_WS_URL = "wss://13.229.247.61:8000/api/twilio"
# BASE_URL = "http://13.229.247.61:8000/api/twilio"
# DTMF_SWITCH_KEY = "*" # Key to switch between models
# LOG_EVENT_TYPES = [
# "error",
# "response.content.done",
# "rate_limits.updated",
# "response.done",
# "input_audio_buffer.committed",
# "input_audio_buffer.speech_stopped",
# "input_audio_buffer.speech_started",
# "session.created",
# ]
# SHOW_TIMING_MATH = False
# MENU_OPTIONS = """
# Press 1 for Model 1.
# Press 2 for Model 2.
# Press 3 for Model 3.
# Press 4 for Model 4.
# Press 5 for Model 5.
# Press 0 to repeat the options.
# """
# call_state = CallState
# # Define model options as enum for type safety
# class ModelOption(int, Enum):
# MODEL_1 = 1
# MODEL_2 = 2
# MODEL_3 = 3
# MODEL_4 = 4
# MODEL_5 = 5
# @router.post("/call")
# async def get_call():
# SID = os.getenv("TWILIO_SID")
# AUTH_TOKEN = os.getenv("TWILIO_AUTH")
# client = Client(SID, AUTH_TOKEN)
# call = client.calls.create(
# from_="+14149466486",
# to="+917984372159",
# record=True,
# url=f"{BASE_URL}/receive",
# )
# return status.HTTP_200_OK
# # @router.websocket("/media-stream/{id}")
# # async def handle_media_stream(websocket: WebSocket, id: int):
# # """Handle WebSocket connections between Twilio and OpenAI."""
# # print(f"Client connected with id: {id}")
# # await websocket.accept()
# # start_data = websocket.iter_text()
# # await start_data.__anext__()
# # call_data = json.loads(await start_data.__anext__())
# # print(call_data, flush=True)
# # stream_sid = call_data["start"]["streamSid"]
# # print("WebSocket connection accepted")
# # await run_bot(websocket, stream_sid, False, option=id)
# @router.websocket("/media-stream/{id}")
# async def handle_media_stream(websocket: WebSocket, id: int):
# """Handle WebSocket connections between Twilio and OpenAI."""
# logger.info(f"Client connected with id: {id}")
# print(f"Client connected with id: {id}")
# await websocket.accept()
# start_data = websocket.iter_text()
# await start_data.__anext__()
# call_data = json.loads(await start_data.__anext__())
# print(call_data, flush=True)
# logger.info(call_data)
# stream_sid = call_data["start"]["streamSid"]
# print("WebSocket connection accepted")
# logger.info("WebSocket connection accepted")
# await run_bot(websocket, stream_sid, False, option=id)
# # @router.post("/receive")
# # async def receive_call(req: Request):
# # print("----------------- received call -----------------")
# # resp = VoiceResponse()
# # connect = Connect()
# # connect.stream(
# # url=f"wss://allegedly-known-wasp.ngrok-free.app/api/twilio/media-stream"
# # )
# # resp.append(connect)
# # return Response(content=str(resp), media_type="application/xml")
# @router.post("/receive")
# async def receive_call(req: Request):
# print("----------------- received call -----------------")
# resp = VoiceResponse()
# # Gather DTMF input
# gather = resp.gather(
# num_digits=1, action="/api/twilio/handle-key", method="POST", timeout=10
# )
# # Initial greeting and menu options
# gather.say(MENU_OPTIONS)
# # If no input received, redirect back to the main menu
# resp.redirect("/api/twilio/receive")
# return Response(content=str(resp), media_type="application/xml")
# @router.post("/handle-key")
# async def handle_key_press(req: Request):
# """Process the key pressed by the caller and connect to the appropriate model."""
# try:
# form_data = await req.form()
# digits_pressed = form_data.get("Digits", "")
# print(f"User pressed: {digits_pressed}")
# resp = VoiceResponse()
# if digits_pressed == "0":
# # Repeat options
# resp.redirect("/api/twilio/receive")
# elif digits_pressed in "12345":
# # Valid model selection
# model_id = int(digits_pressed)
# resp.say(f"You have selected model {model_id}.")
# # Connect to WebSocket
# connect = Connect()
# connect.stream(url=f"{BASE_WS_URL}/media-stream/{model_id}")
# resp.append(connect)
# else:
# # Invalid selection
# resp.say("Invalid selection. Please try again.")
# resp.redirect("/api/twilio/receive")
# return Response(content=str(resp), media_type="application/xml")
# except Exception as e:
# print(f"Error handling key press: {str(e)}")
# resp = VoiceResponse()
# resp.say(
# "We encountered a problem processing your request. Please try again later."
# )
# return Response(content=str(resp), media_type="application/xml")
# @router.post("/error")
# async def read_item(req: Request):
# print(await req.body())
# print(await req.form())
# logger.error(await req.body())
# return status.HTTP_200_OK

28
database.py Normal file
View File

@ -0,0 +1,28 @@
import dotenv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
dotenv.load_dotenv()
engine = create_engine(
os.getenv("DB_URL"),
pool_pre_ping=True, # Check connection before using it
pool_size=5, # Connection pool size
max_overflow=10, # Max extra connections when pool is full
pool_recycle=3600, # Recycle connections after 1 hour
echo=True, # Log SQL queries
)
Base = declarative_base() # Base class for ORM models
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

0
enums/__init__.py Normal file
View File

37
enums/enums.py Normal file
View File

@ -0,0 +1,37 @@
from enum import Enum
class AppointmentStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
CANCELLED = "cancelled"
COMPLETED = "completed"
class ClinicStatus(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
UNDER_REVIEW = "under_review"
REQUESTED_DOCTOR = "requested_doctor"
REJECTED = "rejected"
PAYMENT_DUE = "payment_due"
class ClinicUserRoles(Enum):
DIRECTOR = "director"
PRACTICE_MANAGER = "practice_manager"
class ClinicDoctorStatus(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
class ClinicDoctorType(Enum):
DOCTOR = "doctor"
NURSE = "nurse"
class UserType(Enum):
SUPER_ADMIN = "super_admin"
CLINIC_ADMIN = "clinic_admin"
class Integration(Enum):
BP = "bp"
MEDICAL_DIRECTOR = "medical_director"

17
exceptions/__init__.py Normal file
View File

@ -0,0 +1,17 @@
from .api_exceptions import ApiException
from .business_exception import BusinessValidationException
from .validation_exception import ValidationException
from .forbidden_exception import ForbiddenException
from .internal_server_error_exception import InternalServerErrorException
from .resource_not_found_exception import ResourceNotFoundException
from .unauthorized_exception import UnauthorizedException
__all__ = [
"ApiException",
"BusinessValidationException",
"ValidationException",
"ForbiddenException",
"InternalServerErrorException",
"ResourceNotFoundException",
"UnauthorizedException",
]

View File

@ -0,0 +1,7 @@
class ApiException(Exception):
"""Base API exception class for HTTP errors."""
def __init__(self, status_code: int, message: str):
self.status_code = status_code
self.message = message
super().__init__(self.message)

View File

@ -0,0 +1,6 @@
class BusinessValidationException(Exception):
"""Exception for business logic validation errors."""
def __init__(self, message: str):
self.message = message
super().__init__(self.message)

View File

@ -0,0 +1,9 @@
from http import HTTPStatus
from .api_exceptions import ApiException
class ForbiddenException(ApiException):
"""Exception for forbidden access errors."""
def __init__(self, message: str = "Forbidden"):
super().__init__(HTTPStatus.FORBIDDEN, message)

View File

@ -0,0 +1,12 @@
from http import HTTPStatus
from .api_exceptions import ApiException
class InternalServerErrorException(ApiException):
"""Exception for internal server errors."""
def __init__(self):
super().__init__(
HTTPStatus.INTERNAL_SERVER_ERROR,
"An unexpected error has occurred. Please contact the administrator."
)

View File

@ -0,0 +1,9 @@
from http import HTTPStatus
from .api_exceptions import ApiException
class ResourceNotFoundException(ApiException):
"""Exception for resource not found errors."""
def __init__(self, message: str):
super().__init__(HTTPStatus.NOT_FOUND, message)

View File

@ -0,0 +1,9 @@
from http import HTTPStatus
from .api_exceptions import ApiException
class UnauthorizedException(ApiException):
"""Exception for unauthorized access errors."""
def __init__(self, message: str = "Failed to authenticate."):
super().__init__(HTTPStatus.UNAUTHORIZED, message)

View File

@ -0,0 +1,6 @@
class ValidationException(Exception):
"""Exception for data validation errors."""
def __init__(self, message: str):
self.message = message
super().__init__(self.message)

71
main.py Normal file
View File

@ -0,0 +1,71 @@
import dotenv
from fastapi import FastAPI
from contextlib import asynccontextmanager
import logging
from fastapi.middleware.cors import CORSMiddleware
# db
from database import Base, engine
# IMPORTANT: Import all models to register them with SQLAlchemy
# from models.Clinics import Clinics
# from models.Doctors import Doctors
# from models.Patients import Patients
# from models.Appointments import Appointments
# from models.Calendar import Calenders
# routers
from apis import api_router
# middleware
from middleware.ErrorHandlerMiddleware import ErrorHandlerMiddleware, configure_exception_handlers
dotenv.load_dotenv()
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Starting application")
try:
Base.metadata.create_all(bind=engine)
logger.info("Created database tables")
except Exception as e:
logger.error(f"Error creating database tables: {e}")
raise e
yield
logger.info("Stopping application")
app = FastAPI(
lifespan=lifespan,
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allows all origins
allow_credentials=True,
allow_methods=["*"], # Allows all methods
allow_headers=["*"], # Allows all headers
)
# Error handler middleware
app.add_middleware(ErrorHandlerMiddleware)
# Configure exception handlers
configure_exception_handlers(app)
@app.get("/")
async def hello_world():
return {"Hello": "World"}
# Routes
app.include_router(api_router, prefix="/api")

View File

@ -0,0 +1,124 @@
from fastapi import Request, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException
import traceback
from exceptions import (
ApiException,
BusinessValidationException,
ValidationException
)
from schemas.ApiResponse import ApiResponse
class ErrorHandlerMiddleware(BaseHTTPMiddleware):
"""Middleware for handling exceptions globally in the application."""
async def dispatch(self, request: Request, call_next):
try:
return await call_next(request)
except Exception as exc:
return self.handle_exception(exc)
def handle_exception(self, exc: Exception) -> JSONResponse:
if isinstance(exc, ApiException):
return JSONResponse(
status_code=exc.status_code,
content=ApiResponse.from_api_exception(exc)
)
elif isinstance(exc, StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content=ApiResponse(
message=str(exc.detail),
error=str(traceback.format_exc())
).model_dump(exclude_none=True)
)
elif isinstance(exc, (ValidationException, BusinessValidationException)):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=ApiResponse(
message=str(exc),
error=str(traceback.format_exc())
).model_dump(exclude_none=True)
)
elif isinstance(exc, RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=ApiResponse(
message="Validation error",
error=str(exc.errors())
).model_dump(exclude_none=True)
)
else:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=ApiResponse(
message=str(exc),
error=str(traceback.format_exc())
).model_dump(exclude_none=True)
)
# Exception handlers for FastAPI
def configure_exception_handlers(app):
"""Configure exception handlers for the FastAPI application."""
@app.exception_handler(ApiException)
async def api_exception_handler(request: Request, exc: ApiException):
return JSONResponse(
status_code=exc.status_code,
content=ApiResponse.from_api_exception(exc)
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=ApiResponse(
message="Validation error",
error=str(exc.errors())
).model_dump(exclude_none=True)
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content=ApiResponse(
message=str(exc.detail),
error=str(traceback.format_exc())
).model_dump(exclude_none=True)
)
@app.exception_handler(BusinessValidationException)
async def business_validation_exception_handler(request: Request, exc: BusinessValidationException):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=ApiResponse(
message=str(exc),
error=str(traceback.format_exc())
).model_dump(exclude_none=True)
)
@app.exception_handler(ValidationException)
async def custom_validation_exception_handler(request: Request, exc: ValidationException):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=ApiResponse(
message=str(exc),
error=str(traceback.format_exc())
).model_dump(exclude_none=True)
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=ApiResponse(
message=str(exc),
error=str(traceback.format_exc())
).model_dump(exclude_none=True)
)

0
middleware/__init__.py Normal file
View File

View File

@ -0,0 +1,27 @@
from fastapi import HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from services.jwtService import verify_jwt_token
from services.userServices import UserServices
from fastapi import Request
security = HTTPBearer()
async def auth_required(request: Request ,credentials: HTTPAuthorizationCredentials = Depends(security)):
"""
Dependency function to verify JWT token for protected routes
"""
if credentials.scheme != "Bearer":
raise HTTPException(status_code=401, detail="Invalid authentication scheme")
payload = verify_jwt_token(credentials.credentials)
if payload is None:
raise HTTPException(status_code=401, detail="Invalid authentication token")
# Get user from database
user = UserServices().get_user(payload["user_id"])
# set user to request state
request.state.user = user
request.state.payload = payload
return True

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

83
migrations/env.py Normal file
View File

@ -0,0 +1,83 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
from database import Base, engine
from models import *
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

28
migrations/script.py.mako Normal file
View File

@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

20
models/Appointments.py Normal file
View File

@ -0,0 +1,20 @@
from sqlalchemy import Column, DateTime, Enum, Integer, ForeignKey
from sqlalchemy.orm import relationship
from enums.enums import AppointmentStatus
from database import Base
from .CustomBase import CustomBase
class Appointments(Base, CustomBase):
__tablename__ = "appointments"
id = Column(Integer, primary_key=True, index=True)
appointment_time = Column(DateTime)
status = Column(Enum(AppointmentStatus))
doctor_id = Column(Integer, ForeignKey("doctors.id"), index=True)
doctor = relationship("Doctors", back_populates="appointments")
patient_id = Column(Integer, ForeignKey("patients.id"), index=True)
patient = relationship("Patients", back_populates="appointments")

16
models/Calendar.py Normal file
View File

@ -0,0 +1,16 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
from .CustomBase import CustomBase
class Calenders(Base, CustomBase):
__tablename__ = "calenders"
id = Column(Integer, primary_key=True, index=True)
doc_id = Column(Integer, ForeignKey("doctors.id"), nullable=False, index=True)
# rrule = Column(String)
time = Column(String)
doctor = relationship("Doctors", back_populates="calendars")

17
models/Clinics.py Normal file
View File

@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from database import Base
from .CustomBase import CustomBase
class Clinics(Base, CustomBase):
__tablename__ = "clinics"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
address = Column(String, nullable=True)
phone = Column(String, unique=True, index=True)
email = Column(String, unique=True, index=True, nullable=True)
doctors = relationship("Doctors", back_populates="clinic")

8
models/CustomBase.py Normal file
View File

@ -0,0 +1,8 @@
from sqlalchemy import Column, DateTime, func
class CustomBase:
create_time = Column(DateTime(timezone=True), server_default=func.now())
update_time = Column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)

22
models/Doctors.py Normal file
View File

@ -0,0 +1,22 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
from .CustomBase import CustomBase
class Doctors(Base, CustomBase):
__tablename__ = "doctors"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
age = Column(Integer, nullable=True)
email = Column(String, unique=True, index=True, nullable=True)
phone = Column(String, unique=True, index=True)
address = Column(String, nullable=True)
clinic_id = Column(Integer, ForeignKey("clinics.id"), nullable=False, index=True)
clinic = relationship("Clinics", back_populates="doctors")
appointments = relationship("Appointments", back_populates="doctor")
calendars = relationship("Calenders", back_populates="doctor")

19
models/Patients.py Normal file
View File

@ -0,0 +1,19 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from database import Base
from .CustomBase import CustomBase
class Patients(Base, CustomBase):
__tablename__ = "patients"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
age = Column(Integer, nullable=True)
email = Column(String, unique=True, index=True, nullable=True)
phone = Column(String, unique=True, index=True)
address = Column(String, nullable=True)
dob = Column(String, nullable=True)
appointments = relationship("Appointments", back_populates="patient")

14
models/Users.py Normal file
View File

@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String
from database import Base
from sqlalchemy import Enum
from enums.enums import ClinicUserRoles, UserType
from models.CustomBase import CustomBase
class Users(Base, CustomBase):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, index=True)
email = Column(String, unique=True, index=True)
password = Column(String)
clinicRole = Column(Enum(ClinicUserRoles), nullable=True)
userType = Column(Enum(UserType), nullable=True)

15
models/__init__.py Normal file
View File

@ -0,0 +1,15 @@
from .Users import Users
from .Clinics import Clinics
from .Doctors import Doctors
from .Patients import Patients
from .Appointments import Appointments
from .Calendar import Calenders
__all__ = [
"Users",
"Clinics",
"Doctors",
"Patients",
"Appointments",
"Calenders",
]

BIN
requirements.txt Normal file

Binary file not shown.

25
schemas/ApiResponse.py Normal file
View File

@ -0,0 +1,25 @@
from typing import Any, Optional, TypeVar, Generic
from pydantic import Field
from pydantic import BaseModel
from exceptions import ApiException
T = TypeVar('T')
class ApiResponse(BaseModel, Generic[T]):
"""Standard API response model matching Node.js implementation."""
data: Optional[T] = Field(default=None, description="Response data")
error: Optional[Any] = Field(default=None, description="Error details")
message: Optional[str] = Field(default=None, description="Response message")
@classmethod
def from_api_exception(cls, exception: ApiException) -> dict:
"""Create an API response from an API exception."""
import traceback
return cls(
data=None,
message=exception.message,
error=traceback.format_exc() if exception else None
).model_dump(exclude_none=True)

52
schemas/BaseSchemas.py Normal file
View File

@ -0,0 +1,52 @@
# schemas.py
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, EmailStr
from enums.enums import AppointmentStatus, ClinicUserRoles, UserType
# Base schemas (shared attributes for create/read operations)
class ClinicBase(BaseModel):
name: str
address: Optional[str] = None
phone: str
email: Optional[EmailStr] = None
class DoctorBase(BaseModel):
name: str
age: Optional[int] = None
email: Optional[EmailStr] = None
phone: str
address: Optional[str] = None
clinic_id: int
class PatientBase(BaseModel):
name: str
age: Optional[int] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
address: Optional[str] = None
dob: Optional[str] = None
class AppointmentBase(BaseModel):
doctor_id: int
patient_id: int
appointment_time: datetime
status: AppointmentStatus = AppointmentStatus.CONFIRMED
class CalendarBase(BaseModel):
doc_id: int
# rrule: str # Recurrence rule in iCalendar format
time: str
class UserBase(BaseModel):
username: str
email: EmailStr
password: str
clinicRole: Optional[ClinicUserRoles] = None
userType: Optional[UserType] = None

36
schemas/CreateSchemas.py Normal file
View File

@ -0,0 +1,36 @@
from .BaseSchemas import *
from datetime import datetime
from typing import Optional
from enums.enums import AppointmentStatus
# Create schemas (used for creating new records)
class ClinicCreate(ClinicBase):
pass
class DoctorCreate(DoctorBase):
pass
class PatientCreate(PatientBase):
pass
class AppointmentCreate(AppointmentBase):
pass
class CalendarCreate(CalendarBase):
pass
class AppointmentCreateWithNames(BaseModel):
doctor_name: str
patient_name: str
appointment_time: datetime
status: AppointmentStatus = AppointmentStatus.CONFIRMED
class UserCreate(UserBase):
pass

126
schemas/ResponseSchemas.py Normal file
View File

@ -0,0 +1,126 @@
from datetime import datetime
from typing import List
from .BaseSchemas import *
from pydantic import Field
# Response schemas (used for API responses)
class Clinic(ClinicBase):
id: int
create_time: datetime
update_time: datetime
class Config:
orm_mode = True
class UserResponse(UserBase):
id: int
create_time: datetime
update_time: datetime
password: str = Field(exclude=True)
class Config:
orm_mode = True
allow_population_by_field_name = True
class Doctor(DoctorBase):
id: int
create_time: datetime
update_time: datetime
class Config:
orm_mode = True
class Patient(PatientBase):
id: int
create_time: datetime
update_time: datetime
class Config:
orm_mode = True
class AppointmentSchema(AppointmentBase):
id: int
create_time: datetime
update_time: datetime
class Config:
orm_mode = True
class Calendar(CalendarBase):
id: int
create_time: datetime
update_time: datetime
class Config:
orm_mode = True
# custom schema for response
class CalendarTimeSchema(BaseModel):
time: str
class Config:
orm_mode = True
class ClinicSchema(BaseModel):
id: int
name: str
address: str
phone: str
email: str
class Config:
orm_mode = True
# Detailed response schemas with nested relationships
class ClinicWithDoctors(Clinic):
doctors: List[Doctor] = []
class DoctorWithAppointments(Doctor):
appointments: List[AppointmentSchema] = []
calendars: List[CalendarTimeSchema] = []
clinic: ClinicSchema
class DoctorWithCalendar(Doctor):
calendars: List[CalendarTimeSchema] = []
clinic: ClinicSchema
class PatientWithAppointments(Patient):
appointments: List[AppointmentSchema] = []
class AppointmentDetailed(AppointmentSchema):
class Doctor(BaseModel):
id: int
name: str
age: int
email: str
phone: str
address: str
class Config:
orm_mode = True
class Patient(BaseModel):
id: int
name: str
age: int
email: str
phone: str
address: str
dob: str
class Config:
orm_mode = True
doctor: Doctor
patient: Patient

38
schemas/UpdateSchemas.py Normal file
View File

@ -0,0 +1,38 @@
from .BaseSchemas import *
# Update schemas (all fields optional for partial updates)
class ClinicUpdate(BaseModel):
name: Optional[str] = None
address: Optional[str] = None
phone: Optional[str] = None
email: Optional[EmailStr] = None
class DoctorUpdate(BaseModel):
name: Optional[str] = None
age: Optional[int] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
address: Optional[str] = None
clinic_id: Optional[int] = None
class PatientUpdate(BaseModel):
name: Optional[str] = None
age: Optional[int] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
address: Optional[str] = None
class AppointmentUpdate(BaseModel):
doctor_id: Optional[int] = None
patient_id: Optional[int] = None
appointment_time: Optional[datetime] = None
status: Optional[AppointmentStatus] = None
class CalendarUpdate(BaseModel):
doc_id: Optional[int] = None
rrule: Optional[str] = None

0
schemas/__init__.py Normal file
View File

0
services/__init__.py Normal file
View File

32
services/authService.py Normal file
View File

@ -0,0 +1,32 @@
from database import get_db
from services.jwtService import create_jwt_token
from services.userServices import UserServices
from utils.password_utils import verify_password
from schemas.CreateSchemas import UserCreate
from exceptions.unauthorized_exception import UnauthorizedException
class AuthService:
def __init__(self):
self.user_service = UserServices()
async def login(self, email, password) -> str:
# get user
user = await self.user_service.get_user_by_email(email)
# verify password
if not verify_password(password, user.password):
raise UnauthorizedException("Invalid credentials")
# remove password from user dict
user_dict = user.__dict__.copy()
user_dict.pop("password", None)
# create token
token = create_jwt_token(user_dict)
return token
async def register(self, user_data: UserCreate) -> str:
user = await self.user_service.create_user(user_data)
token = create_jwt_token(user)
return token

227
services/bot.py Normal file
View File

@ -0,0 +1,227 @@
#
# Copyright (c) 2025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import datetime
import io
import os
import sys
import wave
import aiofiles
from dotenv import load_dotenv
from fastapi import WebSocket
from loguru import logger
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.processors.audio.audio_buffer_processor import AudioBufferProcessor
from pipecat.serializers.twilio import TwilioFrameSerializer
from pipecat.services.elevenlabs import ElevenLabsTTSService
from pipecat.services.playht import PlayHTTTSService, Language
from pipecat.services.deepgram import DeepgramSTTService
from pipecat.services.fish import FishAudioTTSService
from pipecat.services.rime import RimeTTSService
from pipecat.services.cartesia import CartesiaTTSService
from pipecat.services.openai_realtime_beta import (
OpenAIRealtimeBetaLLMService,
SessionProperties,
TurnDetection,
)
from pipecat.services.anthropic import AnthropicLLMService
from pipecat.services.openai import OpenAILLMService
from pipecat.services.google import GoogleLLMService, GoogleLLMContext
from pipecat.transports.network.fastapi_websocket import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
load_dotenv(override=True)
logger.remove(0)
logger.add(sys.stderr, level="DEBUG")
async def save_audio(
server_name: str, audio: bytes, sample_rate: int, num_channels: int
):
if len(audio) > 0:
filename = f"{server_name}_recording_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.wav"
with io.BytesIO() as buffer:
with wave.open(buffer, "wb") as wf:
wf.setsampwidth(2)
wf.setnchannels(num_channels)
wf.setframerate(sample_rate)
wf.writeframes(audio)
async with aiofiles.open(filename, "wb") as file:
await file.write(buffer.getvalue())
logger.info(f"Merged audio saved to {filename}")
else:
logger.info("No audio data to save")
async def run_bot(
websocket_client: WebSocket, stream_sid: str, testing: bool, option: int = 1
):
transport = FastAPIWebsocketTransport(
websocket=websocket_client,
params=FastAPIWebsocketParams(
audio_in_enabled=True,
audio_out_enabled=True,
add_wav_header=False,
vad_enabled=True,
vad_analyzer=SileroVADAnalyzer(),
vad_audio_passthrough=True,
serializer=TwilioFrameSerializer(stream_sid),
),
)
# llm = OpenAIRealtimeBetaLLMService(
# api_key=os.getenv("OPENAI_API_KEY"),
# session_properties=SessionProperties(
# modalities=["text"],
# turn_detection=TurnDetection(threshold=0.5, silence_duration_ms=800),
# voice=None,
# ),
# )
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o")
# llm = AnthropicLLMService(api_key=os.getenv("ANTRHOPIC_API_KEY"))
# llm = GoogleLLMService(api_key=os.getenv("GOOGLE_API_KEY"), model="phone_call")
stt = DeepgramSTTService(
api_key=os.getenv("DEEPGRAM_API_KEY"), audio_passthrough=True
)
# tts = PlayHTTTSService(
# api_key=os.getenv("PLAYHT_SECRE_KEY"),
# user_id=os.getenv("PLAYHT_USERID"),
# voice_url="s3://voice-cloning-zero-shot/80ba8839-a6e6-470c-8f68-7c1e5d3ee2ff/abigailsaad/manifest.json",
# params=PlayHTTTSService.InputParams(
# language=Language.EN,
# speed=1.0,
# ),
# ) # not working
# tts = FishAudioTTSService(
# api_key=os.getenv("FISH_AUDIO_API_KEY"),
# model="b545c585f631496c914815291da4e893", # Get this from Fish Audio playground
# output_format="pcm", # Choose output format
# sample_rate=24000, # Set sample rate
# params=FishAudioTTSService.InputParams(latency="normal", prosody_speed=1.0),
# ) # not working
if option == 1:
tts = CartesiaTTSService(
api_key=os.getenv("CARTESIA_API_KEY"),
voice_id="156fb8d2-335b-4950-9cb3-a2d33befec77", # British Lady
push_silence_after_stop=testing,
)
elif option == 2:
tts = RimeTTSService(
api_key=os.getenv("RIME_API_KEY"),
voice_id="stream",
model="mistv2",
)
elif option == 3:
tts = ElevenLabsTTSService(
api_key=os.getenv("ELEVEN_LABS_API_KEY"),
voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22",
push_silence_after_stop=testing,
)
elif option == 4:
tts = RimeTTSService(
api_key=os.getenv("RIME_API_KEY"),
voice_id="breeze",
model="mistv2",
)
elif option == 5:
tts = CartesiaTTSService(
api_key=os.getenv("CARTESIA_API_KEY"),
voice_id="1d3ba41a-96e6-44ad-aabb-9817c56caa68", # British Lady
push_silence_after_stop=testing,
)
else:
tts = RimeTTSService(
api_key=os.getenv("RIME_API_KEY"),
voice_id="peak",
model="mistv2",
)
messages = [
{
"role": "system",
"content": f"""
Welcome to 365 Days Medical Centre Para Hills - we care about you.
If this is an emergency, please call triple zero.
We are open from 8 AM to 8 PM every day of the year.
All calls are recorded for training and quality purposes - please let us know if you do not wish to be recorded.
I am Nishka, your 24/7 healthcare receptionist. Which language would you like to speak?
""",
}
]
context = OpenAILLMContext(messages)
context_aggregator = llm.create_context_aggregator(context)
# NOTE: Watch out! This will save all the conversation in memory. You can
# pass `buffer_size` to get periodic callbacks.
audiobuffer = AudioBufferProcessor(user_continuous_stream=not testing)
pipeline = Pipeline(
[
transport.input(), # Websocket input from client
stt, # Speech-To-Text
context_aggregator.user(), # User context
llm, # LLM
tts, # Text-To-Speech
transport.output(), # Websocket output to client
audiobuffer, # Used to buffer the audio in the pipeline
context_aggregator.assistant(), # Assistant context
]
)
task = PipelineTask(
pipeline,
params=PipelineParams(
audio_in_sample_rate=8000,
audio_out_sample_rate=8000,
allow_interruptions=True,
),
)
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
# Start recording.
await audiobuffer.start_recording()
# Kick off the conversation.
messages.append(
{"role": "system", "content": "Please introduce yourself to the user."}
)
await task.queue_frames([context_aggregator.user().get_context_frame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):
await task.cancel()
# @audiobuffer.event_handler("on_audio_data")
# async def on_audio_data(buffer, audio, sample_rate, num_channels):
# server_name = f"server_{websocket_client.client.port}"
# await save_audio(server_name, audio, sample_rate, num_channels)
# We use `handle_sigint=False` because `uvicorn` is controlling keyboard
# interruptions. We use `force_gc=True` to force garbage collection after
# the runner finishes running a task which could be useful for long running
# applications with multiple clients connecting.
runner = PipelineRunner(handle_sigint=False, force_gc=True)
await runner.run(task)

33
services/jwtService.py Normal file
View File

@ -0,0 +1,33 @@
from datetime import datetime, timedelta, timezone
import jwt
from enum import Enum
from utils.constants import JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_MINUTES
def create_jwt_token(data: dict):
# Create a copy of the data and handle Enum and datetime serialization
to_encode = {}
for key, value in data.items():
if isinstance(value, Enum):
to_encode[key] = value.value # Convert Enum to its string value
elif isinstance(value, datetime):
to_encode[key] = value.isoformat() # Convert datetime to ISO format string
else:
to_encode[key] = value
# Safely evaluate the JWT_EXPIRE_MINUTES expression
minutes = eval(JWT_EXPIRE_MINUTES) if isinstance(JWT_EXPIRE_MINUTES, str) else JWT_EXPIRE_MINUTES
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
to_encode.update({"exp": expire.timestamp()}) # Use timestamp for expiration
encoded_jwt = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
return encoded_jwt
def verify_jwt_token(token: str):
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None

93
services/userServices.py Normal file
View File

@ -0,0 +1,93 @@
from loguru import logger
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from sqlalchemy import or_
from database import get_db
from models.Users import Users
from exceptions.validation_exception import ValidationException
from schemas.ResponseSchemas import UserResponse
from utils.password_utils import hash_password
from schemas.CreateSchemas import UserCreate
from exceptions.resource_not_found_exception import ResourceNotFoundException
class UserServices:
def __init__(self):
self.db: Session = next(get_db())
async def create_user(self, user_data: UserCreate):
try:
# Check if user with same username or email exists
existing_user = (
self.db.query(Users)
.filter(Users.email == user_data.email.lower())
.first()
)
if existing_user:
raise ValidationException(
"User with same email already exists"
)
# Create a new user instance
new_user = Users(
username=user_data.username,
email=user_data.email.lower(),
password=hash_password(user_data.password),
clinicRole=user_data.clinicRole,
userType=user_data.userType,
)
# Add to database and commit
self.db.add(new_user)
self.db.commit()
self.db.refresh(new_user)
user_dict = new_user.__dict__.copy()
user_response = UserResponse(**user_dict).model_dump()
return user_response
except Exception as e:
logger.error("Error creating user", e)
self.db.rollback()
if e.__class__ == ValidationException:
raise ValidationException(e.message)
if e.__class__ == ResourceNotFoundException:
raise ResourceNotFoundException(e.message)
raise e
def get_user(self, user_id) -> UserResponse:
# Query the user by ID
user = self.db.query(Users).filter(Users.id == user_id).first()
if not user:
logger.error("User not found")
raise ResourceNotFoundException("User not found")
user_dict = user.__dict__.copy()
user_response = UserResponse(**user_dict).model_dump()
return user_response
async def get_user_by_email(self, email: str) -> UserResponse:
user = self.db.query(Users).filter(Users.email == email.lower()).first()
if not user:
logger.error("User not found")
raise ResourceNotFoundException("User not found")
user_dict = user.__dict__.copy()
user_response = UserResponse(**user_dict)
return user_response
def update_user(self, user_id, user):
return user
def delete_user(self, user_id):
return user

3
utils.py Normal file
View File

@ -0,0 +1,3 @@
# Database pagination constants
DEFAULT_SKIP = 0
DEFAULT_LIMIT = 10

10
utils/__init__.py Normal file
View File

@ -0,0 +1,10 @@
from .password_utils import hash_password, verify_password
from .constants import JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_MINUTES
__all__ = [
"hash_password",
"verify_password",
"JWT_SECRET",
"JWT_ALGORITHM",
"JWT_EXPIRE_MINUTES",
]

14
utils/constants.py Normal file
View File

@ -0,0 +1,14 @@
import dotenv
import os
dotenv.load_dotenv()
DEFAULT_SKIP = 0
DEFAULT_PAGE = 1
DEFAULT_LIMIT = 10
DEFAULT_ORDER_BY = "id"
DEFAULT_ORDER = "desc"
# jwt
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM")
JWT_SECRET = os.getenv("JWT_SECRET")
JWT_EXPIRE_MINUTES = os.getenv("JWT_EXPIRE_MINUTES")

16
utils/password_utils.py Normal file
View File

@ -0,0 +1,16 @@
from passlib.context import CryptContext
# Create a password context for hashing and verifying
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
"""
Hash a password using bcrypt
"""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify a password against a hash
"""
return pwd_context.verify(plain_password, hashed_password)