""" Chat Routes - Patient-level chat with image analysis tools """ import asyncio import json import threading from typing import Optional from fastapi import APIRouter, HTTPException, UploadFile, File, Form from fastapi.responses import StreamingResponse from data.case_store import get_case_store from backend.services.chat_service import get_chat_service router = APIRouter() @router.get("/{patient_id}/chat") def get_chat_history(patient_id: str): """Get patient-level chat history""" store = get_case_store() if not store.get_patient(patient_id): raise HTTPException(status_code=404, detail="Patient not found") messages = store.get_patient_chat_history(patient_id) return {"messages": messages} @router.delete("/{patient_id}/chat") def clear_chat(patient_id: str): """Clear patient-level chat history""" store = get_case_store() if not store.get_patient(patient_id): raise HTTPException(status_code=404, detail="Patient not found") store.clear_patient_chat_history(patient_id) return {"success": True} @router.post("/{patient_id}/chat") async def post_chat_message( patient_id: str, content: str = Form(""), image: Optional[UploadFile] = File(None), ): """Send a chat message, optionally with an image — SSE streaming response. The sync ML generator runs in a background thread so it never blocks the event loop. Events flow through an asyncio.Queue, so each SSE event is flushed to the browser the moment it is produced (spinner shows instantly). """ store = get_case_store() if not store.get_patient(patient_id): raise HTTPException(status_code=404, detail="Patient not found") image_bytes = None if image and image.filename: image_bytes = await image.read() chat_service = get_chat_service() async def generate(): loop = asyncio.get_event_loop() queue: asyncio.Queue = asyncio.Queue() _SENTINEL = object() def run_sync(): try: for event in chat_service.stream_chat(patient_id, content, image_bytes): loop.call_soon_threadsafe(queue.put_nowait, event) except Exception as e: loop.call_soon_threadsafe( queue.put_nowait, {"type": "error", "message": str(e)}, ) finally: loop.call_soon_threadsafe(queue.put_nowait, _SENTINEL) thread = threading.Thread(target=run_sync, daemon=True) thread.start() while True: event = await queue.get() if event is _SENTINEL: break yield f"data: {json.dumps(event)}\n\n" yield f"data: {json.dumps({'type': 'done'})}\n\n" return StreamingResponse( generate(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", }, )