import os import uuid import json import logging from typing import List, Optional from datetime import datetime, timedelta from pathlib import Path import jwt from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Form, Body, Header, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from pydantic import BaseModel, EmailStr, Field from passlib.context import CryptContext import ai_models import label_bridge from pymongo import MongoClient from bson.objectid import ObjectId from dotenv import load_dotenv # Load environment variables load_dotenv() # Setup logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Initialize FastAPI app app = FastAPI(title="ZemedicAI API", description="API for ZemedicAI medical image analysis") # Configure CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], # In production, replace with specific origins allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # MongoDB connection MONGO_URL = os.environ.get("MONGO_URL") DB_NAME = os.environ.get("DB_NAME", "zemedic_db") try: client = MongoClient(MONGO_URL) # Test the connection client.admin.command('ping') db = client[DB_NAME] logger.info("Successfully connected to MongoDB") except Exception as e: logger.error(f"Failed to connect to MongoDB: {str(e)}") # For demo purposes, we'll still initialize the app but won't use the database client = None db = None # JWT Configuration SECRET_KEY = os.environ.get("SECRET_KEY", "your-secret-key") # Should be properly secured in production ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 1 week # Password hashing pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # OAuth2 setup oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/token") # File upload directory UPLOAD_DIR = Path("./uploads") UPLOAD_DIR.mkdir(parents=True, exist_ok=True) # ---------------------------------------- # Models # ---------------------------------------- class UserBase(BaseModel): email: EmailStr name: str class UserCreate(UserBase): password: str class User(UserBase): id: str created_at: datetime class Config: from_attributes = True class UserInDB(User): hashed_password: str class Token(BaseModel): access_token: str token_type: str user_id: str email: str name: str class TokenData(BaseModel): user_id: Optional[str] = None class LoginData(BaseModel): email: EmailStr password: str class RegisterData(UserCreate): pass class Prediction(BaseModel): label: str confidence: float description: Optional[str] = None class AnalysisResult(BaseModel): id: str user_id: str type: str date: datetime image_url: str predictions: List[Prediction] recommendations: Optional[List[str]] = None # ---------------------------------------- # Auth functions # ---------------------------------------- def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password): return pwd_context.hash(password) def get_user(email: str): user_doc = db.users.find_one({"email": email}) if user_doc: user_doc["id"] = str(user_doc["_id"]) del user_doc["_id"] return UserInDB(**user_doc) return None def authenticate_user(email: str, password: str): user = get_user(email) if not user: return False if not verify_password(password, user.hashed_password): return False return user def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt async def get_current_user(token: str = Depends(oauth2_scheme)): credentials_exception = HTTPException( status_code=401, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) # Demo mode for MongoDB unavailability if client is None or db is None: try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id: str = payload.get("sub") if user_id is None: raise credentials_exception # Create a demo user return User( id=user_id, email="demo@example.com", name="Demo User", created_at=datetime.utcnow() ) except jwt.PyJWTError: raise credentials_exception # Normal flow with MongoDB try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id: str = payload.get("sub") if user_id is None: raise credentials_exception token_data = TokenData(user_id=user_id) except jwt.PyJWTError: raise credentials_exception user_doc = db.users.find_one({"_id": ObjectId(token_data.user_id)}) if user_doc is None: raise credentials_exception user_doc["id"] = str(user_doc["_id"]) del user_doc["_id"] return UserInDB(**user_doc) # ---------------------------------------- # Image Analysis Functions (delegated to ai_models — real models, no mock/random) # ---------------------------------------- def analyze_xray_image(image_data: bytes, patient_context: Optional[dict] = None): return ai_models.analyze_xray_image(image_data, patient_context) def analyze_skin_image(image_data: bytes, patient_context: Optional[dict] = None): return ai_models.analyze_skin_image(image_data, patient_context) def analyze_ct_scan(image_data: bytes, patient_context: Optional[dict] = None): # The legacy "CT" endpoint is repurposed as ultrasound; route to the # breast-ultrasound (BUSI) model, which is our current ultrasound model. return ai_models.analyze_breast_ultrasound(image_data, patient_context) def analyze_breast_image(image_data: bytes, patient_context: Optional[dict] = None): return ai_models.analyze_breast_ultrasound(image_data, patient_context) def _analysis_response(analysis_id: str, type_: str, analysis_result: dict, image_url: str) -> dict: """Shape the API response, passing through the richer ai_models fields (model_status, severity, risk, and the dual-language Assistant).""" return { "id": analysis_id, "type": type_, "model_status": analysis_result.get("model_status"), "predictions": analysis_result.get("predictions", []), "top_finding": analysis_result.get("top_finding"), "severity": analysis_result.get("severity"), "risk": analysis_result.get("risk"), "recommendations": analysis_result.get("recommendations", []), "assistant": analysis_result.get("assistant"), "assistant_name": analysis_result.get("assistant_name", "Zafya AI"), "region_of_interest": analysis_result.get("region_of_interest"), "disclaimer": analysis_result.get("disclaimer"), "image_url": image_url, } def _build_patient_context( age: Optional[int], sex: Optional[str], is_known_diabetic: Optional[bool], diabetes_duration_years: Optional[int], ) -> Optional[dict]: """Collects optional patient demographics submitted alongside an image. None of our vision models take these as literal model inputs (they're pure image classifiers) — but age/sex/diabetes history materially affect clinical interpretation (e.g. DR risk scales with diabetes duration; melanoma risk rises with age), so we thread them into the Zafya AI assistant's clinician-facing text and into Gemini's foot-ulcer prompt. They're also stored with the analysis record to enrich future ZemedicLabel training data with real demographic metadata. """ if age is None and not sex and is_known_diabetic is None and diabetes_duration_years is None: return None return { "age": age, "sex": sex, "is_known_diabetic": is_known_diabetic, "diabetes_duration_years": diabetes_duration_years, } def _flag_for_review_async(modality: str, image_data: bytes, filename: str, analysis_result: dict): """Fire-and-forget: push low-confidence predictions to ZemedicLabel for human review, without blocking or risking the diagnostic response.""" import threading threading.Thread( target=label_bridge.maybe_flag_for_review, args=(modality, image_data, filename, analysis_result), daemon=True, ).start() # ---------------------------------------- # API Endpoints # ---------------------------------------- @app.get("/api/health") def health_check(): """Health check endpoint.""" return {"status": "ok", "timestamp": datetime.now().isoformat()} # Model readiness — warm models in a background thread so the server is # immediately ready. Models are cached on first load (lru_cache), so requests # that arrive before warmup finishes simply trigger a lazy load for that model. def _do_warmup(): logger.info("Warming up AI models (background)...") result = ai_models.warmup() logger.info("Model status: %s", result) @app.on_event("startup") def _warmup_models(): import threading threading.Thread(target=_do_warmup, daemon=True).start() @app.get("/api/models/status") def models_status(): """Reports which AI models are loaded and ready. Use before a live demo. Reads the live, incrementally-updated status (ready models show even while a slow one is still downloading).""" return { "models": ai_models.STATUS, "model_ids": { "xray": ai_models.XRAY_WEIGHTS, "skin": ai_models.SKIN_MODEL_ID, "retina": ai_models.RETINA_MODEL_ID, "foot": "gemini-1.5-flash" if ai_models.GEMINI_API_KEY else "not-configured", "breast": ai_models.BREAST_MODEL_ID, }, "model_source": ai_models.MODEL_SOURCE, } @app.post("/api/models/reload") def reload_model(modality: str, model_path: str): """Hot-swap a ViT model to a locally fine-tuned checkpoint produced by training/finetune.py — the Label->MVP accuracy improvement loop. Usage: POST /api/models/reload?modality=skin&model_path=/abs/path/to/checkpoint""" try: return ai_models.reload_model(modality, model_path) except (ValueError, FileNotFoundError) as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: # noqa: BLE001 raise HTTPException(status_code=500, detail=f"Reload failed: {e}") @app.get("/api/active-learning/status") def active_learning_status(): """How many low-confidence predictions are queued in ZemedicLabel awaiting human review, per modality — the MVP -> Label side of the feedback loop.""" return label_bridge.queue_status() # Auth endpoints @app.post("/api/auth/register", response_model=Token) async def register(data: RegisterData): """Register a new user.""" # Demo mode for MongoDB unavailability if client is None or db is None: # Generate a demo user ID and token for testing user_id = str(uuid.uuid4()) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user_id}, expires_delta=access_token_expires ) return { "access_token": access_token, "token_type": "bearer", "user_id": user_id, "email": data.email, "name": data.name } # Normal flow with MongoDB # Check if email already exists existing_user = db.users.find_one({"email": data.email}) if existing_user: raise HTTPException( status_code=400, detail="Email already registered" ) # Create new user document hashed_password = get_password_hash(data.password) user_id = str(ObjectId()) user_data = { "_id": ObjectId(user_id), "email": data.email, "name": data.name, "hashed_password": hashed_password, "created_at": datetime.utcnow() } db.users.insert_one(user_data) # Create access token access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user_id}, expires_delta=access_token_expires ) return { "access_token": access_token, "token_type": "bearer", "user_id": user_id, "email": data.email, "name": data.name } @app.post("/api/auth/login", response_model=Token) async def login(data: LoginData): """Login and get access token.""" # Demo mode for MongoDB unavailability if client is None or db is None: # Generate a demo user ID and token for testing user_id = str(uuid.uuid4()) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user_id}, expires_delta=access_token_expires ) return { "access_token": access_token, "token_type": "bearer", "user_id": user_id, "email": data.email, "name": "Demo User" } # Normal flow with MongoDB user = authenticate_user(data.email, data.password) if not user: raise HTTPException( status_code=401, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"}, ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user.id}, expires_delta=access_token_expires ) return { "access_token": access_token, "token_type": "bearer", "user_id": user.id, "email": user.email, "name": user.name } # Analysis endpoints @app.post("/api/analyze/xray") async def analyze_xray( file: UploadFile = File(...), age: Optional[int] = Form(None), sex: Optional[str] = Form(None), is_known_diabetic: Optional[bool] = Form(None), diabetes_duration_years: Optional[int] = Form(None), current_user: User = Depends(get_current_user) ): """Analyze a chest X-ray image.""" if not file.content_type.startswith("image/"): raise HTTPException( status_code=400, detail="File must be an image" ) # Read the image file image_data = await file.read() patient_context = _build_patient_context(age, sex, is_known_diabetic, diabetes_duration_years) # Analyze the image analysis_result = analyze_xray_image(image_data, patient_context) # Generate a unique ID analysis_id = str(uuid.uuid4()) # Save the image if possible try: file_extension = file.filename.split(".")[-1] unique_filename = f"{uuid.uuid4()}.{file_extension}" file_path = UPLOAD_DIR / unique_filename with open(file_path, "wb") as f: f.write(image_data) image_url = f"/uploads/{unique_filename}" except Exception as e: logger.error(f"Error saving image: {str(e)}") # Use a placeholder if saving fails image_url = "https://images.unsplash.com/photo-1584555684040-bad07f46a21f" # Store in MongoDB if available if client is not None and db is not None: try: analysis_doc = { "_id": ObjectId(analysis_id), "user_id": current_user.id, "type": "xray", "date": datetime.utcnow(), "image_url": image_url, "predictions": analysis_result["predictions"], "recommendations": analysis_result["recommendations"], "patient_context": patient_context, # Real region-of-interest if the model produced one; X-ray's # DenseNet backbone has no Grad-CAM wired yet, so this is None. "region_of_interest": analysis_result.get("region_of_interest"), } db.analyses.insert_one(analysis_doc) except Exception as e: logger.error(f"Error storing analysis in MongoDB: {str(e)}") _flag_for_review_async("xray", image_data, file.filename, analysis_result) # Return the result return _analysis_response(analysis_id, "xray", analysis_result, image_url) @app.post("/api/analyze/skin") async def analyze_skin_lesion( file: UploadFile = File(...), age: Optional[int] = Form(None), sex: Optional[str] = Form(None), is_known_diabetic: Optional[bool] = Form(None), diabetes_duration_years: Optional[int] = Form(None), current_user: User = Depends(get_current_user) ): """Analyze a skin lesion image.""" if not file.content_type.startswith("image/"): raise HTTPException( status_code=400, detail="File must be an image" ) # Read the image file image_data = await file.read() patient_context = _build_patient_context(age, sex, is_known_diabetic, diabetes_duration_years) # Analyze the image analysis_result = analyze_skin_image(image_data, patient_context) # Generate a unique ID analysis_id = str(uuid.uuid4()) # Save the image if possible try: file_extension = file.filename.split(".")[-1] unique_filename = f"{uuid.uuid4()}.{file_extension}" file_path = UPLOAD_DIR / unique_filename with open(file_path, "wb") as f: f.write(image_data) image_url = f"/uploads/{unique_filename}" except Exception as e: logger.error(f"Error saving image: {str(e)}") # Use a placeholder if saving fails image_url = "https://images.unsplash.com/photo-1606501190025-f3ad6d3ea6ae" # Store in MongoDB if available if client is not None and db is not None: try: analysis_doc = { "_id": ObjectId(analysis_id), "user_id": current_user.id, "type": "skin", "date": datetime.utcnow(), "image_url": image_url, "predictions": analysis_result["predictions"], "recommendations": analysis_result["recommendations"], "patient_context": patient_context, "region_of_interest": analysis_result.get("region_of_interest"), } db.analyses.insert_one(analysis_doc) except Exception as e: logger.error(f"Error storing analysis in MongoDB: {str(e)}") _flag_for_review_async("skin", image_data, file.filename, analysis_result) # Return the result return _analysis_response(analysis_id, "skin", analysis_result, image_url) @app.post("/api/analyze/ct-scan") async def analyze_ct_scan_image( file: UploadFile = File(...), age: Optional[int] = Form(None), sex: Optional[str] = Form(None), is_known_diabetic: Optional[bool] = Form(None), diabetes_duration_years: Optional[int] = Form(None), current_user: User = Depends(get_current_user) ): """Analyze a Ultrasound image.""" if not file.content_type.startswith("image/"): raise HTTPException( status_code=400, detail="File must be an image" ) # Read the image file image_data = await file.read() patient_context = _build_patient_context(age, sex, is_known_diabetic, diabetes_duration_years) # Analyze the image analysis_result = analyze_ct_scan(image_data, patient_context) # Generate a unique ID analysis_id = str(uuid.uuid4()) # Save the image if possible try: file_extension = file.filename.split(".")[-1] unique_filename = f"{uuid.uuid4()}.{file_extension}" file_path = UPLOAD_DIR / unique_filename with open(file_path, "wb") as f: f.write(image_data) image_url = f"/uploads/{unique_filename}" except Exception as e: logger.error(f"Error saving image: {str(e)}") # Use a placeholder if saving fails image_url = "https://images.unsplash.com/photo-1631563019676-dade0dbdb8fc" # Store in MongoDB if available if client is not None and db is not None: try: analysis_doc = { "_id": ObjectId(analysis_id), "user_id": current_user.id, "type": "ct-scan", "date": datetime.utcnow(), "image_url": image_url, "predictions": analysis_result["predictions"], "recommendations": analysis_result["recommendations"], "patient_context": patient_context, "region_of_interest": analysis_result.get("region_of_interest"), } db.analyses.insert_one(analysis_doc) except Exception as e: logger.error(f"Error storing analysis in MongoDB: {str(e)}") _flag_for_review_async("breast", image_data, file.filename, analysis_result) # Return the result return _analysis_response(analysis_id, "ultrasound", analysis_result, image_url) @app.post("/api/analyze/breast") async def analyze_breast( file: UploadFile = File(...), age: Optional[int] = Form(None), sex: Optional[str] = Form(None), is_known_diabetic: Optional[bool] = Form(None), diabetes_duration_years: Optional[int] = Form(None), current_user: User = Depends(get_current_user) ): """Analyze a breast ultrasound image (BUSI: benign / malignant / normal).""" if not file.content_type.startswith("image/"): raise HTTPException(status_code=400, detail="File must be an image") image_data = await file.read() patient_context = _build_patient_context(age, sex, is_known_diabetic, diabetes_duration_years) analysis_result = analyze_breast_image(image_data, patient_context) analysis_id = str(uuid.uuid4()) try: file_extension = file.filename.split(".")[-1] unique_filename = f"{uuid.uuid4()}.{file_extension}" file_path = UPLOAD_DIR / unique_filename with open(file_path, "wb") as f: f.write(image_data) image_url = f"/uploads/{unique_filename}" except Exception as e: logger.error(f"Error saving image: {str(e)}") image_url = "https://images.unsplash.com/photo-1631563019676-dade0dbdb8fc" if client is not None and db is not None: try: db.analyses.insert_one({ "_id": ObjectId(analysis_id), "user_id": current_user.id, "type": "breast", "date": datetime.utcnow(), "image_url": image_url, "predictions": analysis_result.get("predictions", []), "recommendations": analysis_result.get("recommendations", []), "patient_context": patient_context, "region_of_interest": analysis_result.get("region_of_interest"), }) except Exception as e: logger.error(f"Error storing analysis in MongoDB: {str(e)}") _flag_for_review_async("breast", image_data, file.filename, analysis_result) return _analysis_response(analysis_id, "breast", analysis_result, image_url) @app.post("/api/analyze/retina") async def analyze_retina( file: UploadFile = File(...), age: Optional[int] = Form(None), sex: Optional[str] = Form(None), is_known_diabetic: Optional[bool] = Form(None), diabetes_duration_years: Optional[int] = Form(None), current_user: User = Depends(get_current_user), ): """Diabetic retinopathy grading from fundus photography. Uses a ViT fine-tuned on APTOS 2019 / EyePACS (5-class DR grading: No DR → Proliferative DR). Returns structured predictions + dual-language clinician / patient assistant output. Age/diabetes-duration are clinically relevant here — DR risk and severity correlate strongly with how long a patient has had diabetes — so they're captured and passed to the assistant.""" if not file.content_type.startswith("image/"): raise HTTPException(status_code=400, detail="File must be an image") image_data = await file.read() patient_context = _build_patient_context(age, sex, is_known_diabetic, diabetes_duration_years) analysis_result = ai_models.analyze_retina_image(image_data, patient_context) analysis_id = str(uuid.uuid4()) try: ext = file.filename.rsplit(".", 1)[-1] unique_filename = f"{uuid.uuid4()}.{ext}" with open(UPLOAD_DIR / unique_filename, "wb") as f: f.write(image_data) image_url = f"/uploads/{unique_filename}" except Exception as e: logger.error("Error saving retina image: %s", e) image_url = "" if client is not None and db is not None: try: db.analyses.insert_one({ "_id": ObjectId(analysis_id), "user_id": current_user.id, "type": "retina", "date": datetime.utcnow(), "image_url": image_url, "predictions": analysis_result.get("predictions", []), "recommendations": analysis_result.get("recommendations", []), "patient_context": patient_context, "region_of_interest": analysis_result.get("region_of_interest"), }) except Exception as e: logger.error("MongoDB insert failed (retina): %s", e) _flag_for_review_async("retina", image_data, file.filename, analysis_result) return _analysis_response(analysis_id, "retina", analysis_result, image_url) @app.post("/api/analyze/foot") async def analyze_foot( file: UploadFile = File(...), age: Optional[int] = Form(None), sex: Optional[str] = Form(None), is_known_diabetic: Optional[bool] = Form(None), diabetes_duration_years: Optional[int] = Form(None), current_user: User = Depends(get_current_user), ): """Diabetic foot ulcer classification from clinical photography. Uses Gemini 1.5 Flash vision for wound classification (normal | DFU | infected | neuropathic | ischemic | callus | healed | other). Requires GEMINI_API_KEY in the backend .env. Diabetes duration is clinically relevant to foot ulcer risk, so it's passed into the Gemini prompt as patient context alongside the image.""" if not file.content_type.startswith("image/"): raise HTTPException(status_code=400, detail="File must be an image") image_data = await file.read() patient_context = _build_patient_context(age, sex, is_known_diabetic, diabetes_duration_years) analysis_result = ai_models.analyze_foot_image(image_data, patient_context) analysis_id = str(uuid.uuid4()) try: ext = file.filename.rsplit(".", 1)[-1] unique_filename = f"{uuid.uuid4()}.{ext}" with open(UPLOAD_DIR / unique_filename, "wb") as f: f.write(image_data) image_url = f"/uploads/{unique_filename}" except Exception as e: logger.error("Error saving foot image: %s", e) image_url = "" if client is not None and db is not None: try: db.analyses.insert_one({ "_id": ObjectId(analysis_id), "user_id": current_user.id, "type": "foot", "date": datetime.utcnow(), "image_url": image_url, "predictions": analysis_result.get("predictions", []), "recommendations": analysis_result.get("recommendations", []), "patient_context": patient_context, "region_of_interest": analysis_result.get("region_of_interest"), }) except Exception as e: logger.error("MongoDB insert failed (foot): %s", e) _flag_for_review_async("foot", image_data, file.filename, analysis_result) return _analysis_response(analysis_id, "foot", analysis_result, image_url) # ---------------------------------------- # Zafya AI — conversational follow-up assistant # ---------------------------------------- import zafya_chat # noqa: E402 class ZafyaChatTurn(BaseModel): role: str # "user" | "assistant" content: str class ZafyaChatRequest(BaseModel): message: str history: List[ZafyaChatTurn] = [] audience: str = "patient" # "doctor" | "patient" context: Optional[dict] = None # {modality, top_finding, severity, risk, age, sex} @app.post("/api/zafya/chat") async def zafya_chat_endpoint( payload: ZafyaChatRequest, current_user: User = Depends(get_current_user), ): """Follow-up conversational Q&A on a screening result, tailored to the asker (doctor gets clinical register, patient gets plain-language register). Powered by Gemini; distinct from ZafyaLM (the offline on-device model).""" history = [turn.dict() for turn in payload.history] result = zafya_chat.chat(payload.message, history, payload.audience, payload.context) return result @app.get("/api/user/history") async def get_user_history( current_user: User = Depends(get_current_user) ): """Get user's analysis history.""" # Demo mode for MongoDB unavailability if client is None or db is None: # Return demo data demo_history = [ { "id": "demo1", "user_id": current_user.id, "type": "xray", "date": datetime.utcnow() - timedelta(days=2), "findings": ["Pneumonia", "Pleural Effusion"], "image_url": "https://images.unsplash.com/photo-1584555684040-bad07f46a21f", "predictions": [ {"label": "Pneumonia", "confidence": 0.82}, {"label": "Pleural Effusion", "confidence": 0.67} ] }, { "id": "demo2", "user_id": current_user.id, "type": "skin", "date": datetime.utcnow() - timedelta(days=5), "findings": ["Melanoma"], "image_url": "https://images.unsplash.com/photo-1606501190025-f3ad6d3ea6ae", "predictions": [ {"label": "Melanoma", "confidence": 0.75} ] }, { "id": "demo3", "user_id": current_user.id, "type": "ct-scan", "date": datetime.utcnow() - timedelta(days=7), "findings": ["Brain Tumor"], "image_url": "https://images.unsplash.com/photo-1631563019676-dade0dbdb8fc", "predictions": [ {"label": "Brain Tumor", "confidence": 0.68} ] } ] return demo_history # Normal flow with MongoDB try: analyses = db.analyses.find({"user_id": current_user.id}).sort("date", -1) result = [] for analysis in analyses: analysis["id"] = str(analysis["_id"]) del analysis["_id"] result.append(analysis) return result except Exception as e: logger.error(f"Error retrieving history: {str(e)}") return [] @app.get("/api/analysis/{analysis_id}") async def get_analysis_by_id( analysis_id: str, current_user: User = Depends(get_current_user) ): """Get a specific analysis by ID.""" # Demo mode for MongoDB unavailability if client is None or db is None or analysis_id.startswith("demo"): # Return demo data if analysis_id == "demo1": return { "id": "demo1", "user_id": current_user.id, "type": "xray", "date": datetime.utcnow() - timedelta(days=2), "image_url": "https://images.unsplash.com/photo-1584555684040-bad07f46a21f", "predictions": [ {"label": "Pneumonia", "confidence": 0.82}, {"label": "Pleural Effusion", "confidence": 0.67} ], "recommendations": [ "Consult with a healthcare professional for proper diagnosis", "Consider follow-up imaging to monitor any changes", "Complete the full course of prescribed antibiotics", "Rest and hydrate properly" ] } elif analysis_id == "demo2": return { "id": "demo2", "user_id": current_user.id, "type": "skin", "date": datetime.utcnow() - timedelta(days=5), "image_url": "https://images.unsplash.com/photo-1606501190025-f3ad6d3ea6ae", "predictions": [ {"label": "Melanoma", "confidence": 0.75}, {"label": "Seborrheic Keratosis", "confidence": 0.15} ], "recommendations": [ "Schedule a follow-up with a dermatologist", "Protect your skin from sun exposure", "Monitor any changes in size, shape, or color of the lesion", "Apply prescribed topical treatments as directed" ] } elif analysis_id == "demo3": return { "id": "demo3", "user_id": current_user.id, "type": "ct-scan", "date": datetime.utcnow() - timedelta(days=7), "image_url": "https://images.unsplash.com/photo-1631563019676-dade0dbdb8fc", "predictions": [ {"label": "Brain Tumor", "confidence": 0.68}, {"label": "Normal Findings", "confidence": 0.22} ], "recommendations": [ "Immediate neurosurgery consultation", "MRI with contrast for further characterization", "Discuss biopsy options if appropriate", "Consider second opinion from neuro-oncologist" ] } else: raise HTTPException( status_code=404, detail="Analysis not found" ) # Normal flow with MongoDB try: analysis = db.analyses.find_one({"_id": ObjectId(analysis_id)}) if not analysis: raise HTTPException( status_code=404, detail="Analysis not found" ) if analysis["user_id"] != current_user.id: raise HTTPException( status_code=403, detail="Not authorized to access this analysis" ) analysis["id"] = str(analysis["_id"]) del analysis["_id"] return analysis except Exception as e: logger.error(f"Error retrieving analysis: {str(e)}") raise HTTPException( status_code=500, detail=f"Error retrieving analysis: {str(e)}" ) @app.get("/api/user/profile") async def get_user_profile( current_user: User = Depends(get_current_user) ): """Get user's profile information.""" return { "id": current_user.id, "email": current_user.email, "name": current_user.name, "created_at": current_user.created_at } @app.put("/api/user/profile") async def update_user_profile( name: str = Form(None), current_user: UserInDB = Depends(get_current_user) ): """Update user's profile information.""" updates = {} if name: updates["name"] = name if updates: db.users.update_one( {"_id": ObjectId(current_user.id)}, {"$set": updates} ) updated_user = db.users.find_one({"_id": ObjectId(current_user.id)}) updated_user["id"] = str(updated_user["_id"]) del updated_user["_id"] del updated_user["hashed_password"] return updated_user if __name__ == "__main__": import uvicorn # Run the FastAPI app uvicorn.run("server:app", host="0.0.0.0", port=8001, reload=True)