| """ |
| Main API Routes |
| Handles patient management, dashboard, and conversation endpoints. |
| """ |
|
|
| from fastapi import APIRouter, Depends, UploadFile, File, Request, Response, BackgroundTasks, HTTPException |
| from fastapi.concurrency import run_in_threadpool |
| from fastapi.responses import HTMLResponse |
| from fastapi.templating import Jinja2Templates |
| from pydantic import BaseModel |
|
|
| from core.schemas import ( |
| ResetRequest, ChatRequest, StartRequest, |
| PatientCreateRequest, EndSessionRequest |
| ) |
| from api.dependencies import ( |
| get_orchestrator, get_patient_service, |
| get_stt_provider, get_profile_store, get_session_store, |
| get_current_user, |
| ConversationOrchestrator, PatientService |
| ) |
| from core.ports import ProfileStore, SessionStore, STTProvider |
|
|
| router = APIRouter() |
| templates = Jinja2Templates(directory="templates") |
|
|
| @router.get("/health") |
| def health(): |
| """Simple health check endpoint to verify API is running.""" |
| return {"status": "ok"} |
|
|
| @router.get("/", response_class=HTMLResponse) |
| def home(request: Request): |
| """Serves the main frontend Single Page Application (SPA).""" |
| return templates.TemplateResponse(request, "index.html") |
|
|
|
|
|
|
| @router.get("/patients") |
| def get_patients(profile_store: ProfileStore = Depends(get_profile_store), user: dict = Depends(get_current_user)): |
| """Retrieve a list of all registered patients for the logged-in user.""" |
| patients = profile_store.list_patients_for_user(user["id"]) |
| if not patients: |
| |
| patient_id = profile_store.create_patient( |
| name=user.get("username", "User"), |
| age=None, |
| gender=None, |
| primary_concern=None, |
| user_id=user["id"] |
| ) |
| return [{"patient_id": patient_id, "name": user.get("username", "User")}] |
| return patients |
|
|
| @router.post("/patients/create") |
| def create_new_patient( |
| request: PatientCreateRequest, |
| profile_store: ProfileStore = Depends(get_profile_store), |
| user: dict = Depends(get_current_user) |
| ): |
| """Create a new patient record in the database.""" |
| p_id = profile_store.create_patient( |
| name=request.name, |
| age=request.age, |
| gender=request.gender, |
| occupation=request.occupation, |
| primary_concern=request.primary_concern, |
| user_id=user["id"] |
| ) |
| return {"patient_id": p_id, "name": request.name} |
|
|
| @router.get("/patients/{patient_id}/dashboard") |
| def patient_dashboard( |
| patient_id: str, |
| profile_store: ProfileStore = Depends(get_profile_store), |
| user: dict = Depends(get_current_user) |
| ): |
| """Retrieve full dashboard data for a patient (info, profile, past sessions).""" |
| patient_info = profile_store.get_patient(patient_id) |
| if not patient_info: |
| return Response(status_code=404) |
| |
| profile = profile_store.get_patient_profile(patient_id) |
| sessions = profile_store.get_patient_sessions(patient_id) |
| |
| return { |
| "patient": patient_info, |
| "profile": profile, |
| "sessions": sessions |
| } |
|
|
| @router.post("/patients/{patient_id}/reset") |
| def reset_patient( |
| patient_id: str, |
| patient_service: PatientService = Depends(get_patient_service), |
| user: dict = Depends(get_current_user) |
| ): |
| """Reset the patient's data, including sessions, messages, and profile.""" |
| patient_service.reset_patient_data(patient_id) |
| return {"status": "success", "message": "Patient data reset successfully."} |
|
|
| @router.delete("/patients/{patient_id}") |
| def delete_patient( |
| patient_id: str, |
| patient_service: PatientService = Depends(get_patient_service), |
| user: dict = Depends(get_current_user) |
| ): |
| """Delete a patient and all their associated data completely.""" |
| patient_service.delete_patient(patient_id) |
| return {"status": "success", "message": "Patient deleted successfully."} |
|
|
| @router.post("/end_session") |
| def end_session_endpoint( |
| request: EndSessionRequest, |
| background_tasks: BackgroundTasks, |
| patient_service: PatientService = Depends(get_patient_service), |
| user: dict = Depends(get_current_user) |
| ): |
| """Gracefully terminate an active session and generate a clinical summary in the background.""" |
| patient_service.end_session(request.session_id, request.patient_id) |
| background_tasks.add_task(patient_service.generate_session_summary, request.session_id, request.patient_id) |
| return {"status": "ended"} |
|
|
| @router.post("/start") |
| def start_session( |
| request: StartRequest = StartRequest(), |
| patient_service: PatientService = Depends(get_patient_service), |
| user: dict = Depends(get_current_user) |
| ): |
| """Initialize a new conversation session and generate the psychiatrist's opening remark.""" |
| try: |
| session_id, opening_message, patient_id = patient_service.create_new_session(request.patient_id) |
| return { |
| "assistant_message": opening_message, |
| "session_id": session_id, |
| "patient_id": patient_id, |
| } |
| except Exception as e: |
| error_str = str(e).lower() |
| if "connection error" in error_str or "all providers failed" in error_str or "unavailable" in error_str: |
| raise HTTPException(status_code=503, detail="LLMs are currently unavailable. Cannot start session.") |
| raise e |
|
|
|
|
| @router.post("/chat_text") |
| def chat_text( |
| request: ChatRequest, |
| orchestrator: ConversationOrchestrator = Depends(get_orchestrator), |
| patient_service: PatientService = Depends(get_patient_service), |
| session_store: SessionStore = Depends(get_session_store), |
| profile_store: ProfileStore = Depends(get_profile_store), |
| user: dict = Depends(get_current_user) |
| ): |
| """ |
| Process a user's chat message through the core orchestrator. |
| LLM2 analysis is run synchronously within the orchestrator if triggered. |
| """ |
| if not session_store.session_exists(request.session_id): |
| |
| session_id, opening_message, patient_id = patient_service.create_new_session(request.patient_id) |
| |
| request.session_id = session_id |
| request.patient_id = patient_id |
|
|
| try: |
| chat_result = orchestrator.handle_message( |
| session_id=request.session_id, |
| message=request.message, |
| emotion=request.emotion, |
| default_patient_id=request.patient_id |
| ) |
| except Exception as e: |
| error_str = str(e).lower() |
| error_type = type(e).__name__.lower() |
| if any(k in error_str for k in ("rate limit", "ratelimit", "429", "402", "tokens exhausted", "quota")) or "ratelimit" in error_type: |
| raise HTTPException(status_code=429, detail="Tokens Exhausted") |
| if "connection error" in error_str or "all providers failed" in error_str or "unavailable" in error_str: |
| raise HTTPException(status_code=503, detail="LLMs are currently unavailable. Please try again later.") |
| raise e |
|
|
|
|
| return { |
| "assistant_message": chat_result.assistant_message, |
| "intent": chat_result.intent, |
| "risk_flagged": chat_result.risk_flagged, |
| "session_id": request.session_id, |
| } |
|
|
| @router.post("/transcribe") |
| async def transcribe( |
| audio: UploadFile = File(...), |
| stt_provider: STTProvider = Depends(get_stt_provider) |
| ): |
| """ |
| Accepts raw audio bytes from the frontend and passes them to the STTProvider |
| for speech-to-text and emotional tone extraction. |
| """ |
| audio_bytes = await audio.read() |
| |
| result = await run_in_threadpool(stt_provider.transcribe, audio_bytes) |
| return { |
| "text": result.get("text", ""), |
| "emotion": result.get("emotion", "unknown"), |
| "event": result.get("event", None), |
| } |
|
|
| @router.get("/patients/{patient_id}/active_session") |
| def get_active_session( |
| patient_id: str, |
| patient_service: PatientService = Depends(get_patient_service), |
| user: dict = Depends(get_current_user) |
| ): |
| """Check if the patient currently has an active session.""" |
| session_id = patient_service.get_active_session(patient_id) |
| return {"session_id": session_id} |
|
|
| @router.get("/sessions/{session_id}/messages") |
| def get_session_messages( |
| session_id: str, |
| patient_service: PatientService = Depends(get_patient_service), |
| user: dict = Depends(get_current_user) |
| ): |
| """Retrieve all raw messages for a given session to continue a chat.""" |
| messages = patient_service.get_session_messages(session_id) |
| return {"messages": messages} |
|
|
| _BLOCKED_PATH_FRAGMENTS = { |
| ".streamlit", ".env", "secrets.toml", "config.toml", ".git", "wp-admin", ".well-known" |
| } |
|
|
| @router.get("/{full_path:path}", response_class=HTMLResponse) |
| def catch_all(request: Request, full_path: str): |
| """ |
| Catch-all route to support History API (clean URLs) in the frontend SPA. |
| Returns 404 for known config/secret paths probed by bots (e.g. .streamlit/secrets.toml). |
| Otherwise serves index.html so the frontend JS handles SPA routing. |
| """ |
| if any(fragment in full_path for fragment in _BLOCKED_PATH_FRAGMENTS): |
| return Response(status_code=404) |
| return templates.TemplateResponse(request, "index.html") |
|
|