|
|
|
|
|
""" |
|
|
Medical AI Assistant - FastAPI Only Version |
|
|
Simplified endpoints for backend integration with Swagger UI |
|
|
|
|
|
This file is Hugging Face Spaces compatible: the FastAPI app is exposed as 'app' at the module level. |
|
|
""" |
|
|
|
|
|
from fastapi import FastAPI, HTTPException, File, UploadFile, BackgroundTasks |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from fastapi.responses import JSONResponse |
|
|
from fastapi.openapi.docs import get_swagger_ui_html |
|
|
from fastapi.openapi.utils import get_openapi |
|
|
from pydantic import BaseModel, Field |
|
|
from typing import List, Optional, Dict, Any, Union |
|
|
import logging |
|
|
import uuid |
|
|
import os |
|
|
import json |
|
|
import asyncio |
|
|
from contextlib import asynccontextmanager |
|
|
import time |
|
|
|
|
|
|
|
|
logging.basicConfig( |
|
|
level=logging.INFO, |
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
|
|
) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
pipeline = None |
|
|
whisper_model = None |
|
|
|
|
|
async def load_models(): |
|
|
"""Load ML models asynchronously""" |
|
|
global pipeline, whisper_model |
|
|
try: |
|
|
logger.info("Loading Medical AI models...") |
|
|
|
|
|
from medical_ai import CompetitionMedicalAIPipeline |
|
|
pipeline = CompetitionMedicalAIPipeline() |
|
|
logger.info("β
Medical pipeline loaded successfully") |
|
|
|
|
|
try: |
|
|
from faster_whisper import WhisperModel |
|
|
model_cache = os.getenv('HF_HOME', '/tmp/models') |
|
|
whisper_model = WhisperModel( |
|
|
"medium", |
|
|
device="cpu", |
|
|
compute_type="int8", |
|
|
download_root=model_cache |
|
|
) |
|
|
logger.info("β
Whisper model loaded successfully") |
|
|
except Exception as e: |
|
|
logger.warning(f"β οΈ Could not load Whisper model: {str(e)}") |
|
|
whisper_model = None |
|
|
|
|
|
logger.info("π All models loaded successfully") |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"β Error loading models: {str(e)}", exc_info=True) |
|
|
raise |
|
|
|
|
|
@asynccontextmanager |
|
|
async def lifespan(app: FastAPI): |
|
|
"""Application lifespan management (robust for Hugging Face Spaces)""" |
|
|
try: |
|
|
await load_models() |
|
|
logger.info("β
Models loaded in lifespan.") |
|
|
except Exception as e: |
|
|
logger.error(f"β Error during startup: {str(e)}", exc_info=True) |
|
|
|
|
|
yield |
|
|
logger.info("π Shutting down...") |
|
|
|
|
|
|
|
|
def custom_openapi(): |
|
|
if app.openapi_schema: |
|
|
return app.openapi_schema |
|
|
|
|
|
openapi_schema = get_openapi( |
|
|
title="π©Ί Medical AI Assistant API", |
|
|
version="2.0.0", |
|
|
description=""" |
|
|
## π― Advanced Medical AI Assistant |
|
|
|
|
|
**Multilingual medical consultation API** supporting: |
|
|
- π French, English, and local African languages |
|
|
- π€ Audio processing with speech-to-text |
|
|
- π§ Advanced medical knowledge retrieval |
|
|
- β‘ Real-time medical consultations |
|
|
|
|
|
### π§ Main Endpoints: |
|
|
- **POST /medical/ask** - Text-based medical consultation |
|
|
- **POST /medical/audio** - Audio-based medical consultation |
|
|
- **GET /health** - System health check |
|
|
- **POST /feedback** - Submit user feedback |
|
|
|
|
|
### π Important Medical Disclaimer: |
|
|
This API provides educational medical information only. Always consult qualified healthcare professionals for medical advice. |
|
|
""", |
|
|
routes=app.routes, |
|
|
contact={ |
|
|
"name": "Medical AI Support", |
|
|
"email": "support@medicalai.com" |
|
|
}, |
|
|
license_info={ |
|
|
"name": "MIT License", |
|
|
"url": "https://opensource.org/licenses/MIT" |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
openapi_schema["tags"] = [ |
|
|
{ |
|
|
"name": "medical", |
|
|
"description": "Medical consultation endpoints" |
|
|
}, |
|
|
{ |
|
|
"name": "audio", |
|
|
"description": "Audio processing endpoints" |
|
|
}, |
|
|
{ |
|
|
"name": "system", |
|
|
"description": "System monitoring and health" |
|
|
}, |
|
|
{ |
|
|
"name": "feedback", |
|
|
"description": "User feedback and analytics" |
|
|
} |
|
|
] |
|
|
|
|
|
app.openapi_schema = openapi_schema |
|
|
return app.openapi_schema |
|
|
|
|
|
|
|
|
app = FastAPI( |
|
|
title="π©Ί Medical AI Assistant", |
|
|
description="Advanced multilingual medical consultation API", |
|
|
version="2.0.0", |
|
|
lifespan=lifespan, |
|
|
docs_url="/docs", |
|
|
redoc_url="/redoc", |
|
|
openapi_url="/openapi.json" |
|
|
) |
|
|
|
|
|
|
|
|
app.openapi = custom_openapi |
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=["*"], |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
expose_headers=["*"] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MedicalQuestion(BaseModel): |
|
|
"""Medical question request model""" |
|
|
question: str = Field(..., description="The medical question", min_length=3, max_length=1000) |
|
|
language: str = Field("auto", description="Preferred language (auto, en, fr)", pattern="^(auto|en|fr)$") |
|
|
conversation_id: Optional[str] = Field(None, description="Optional conversation ID for context") |
|
|
|
|
|
class Config: |
|
|
schema_extra = { |
|
|
"example": { |
|
|
"question": "What are the symptoms of malaria and how is it treated?", |
|
|
"language": "en", |
|
|
"conversation_id": "conv_123" |
|
|
} |
|
|
} |
|
|
|
|
|
class MedicalResponse(BaseModel): |
|
|
"""Medical response model""" |
|
|
success: bool = Field(..., description="Whether the request was successful") |
|
|
response: str = Field(..., description="The medical response") |
|
|
detected_language: str = Field(..., description="Detected or used language") |
|
|
conversation_id: str = Field(..., description="Conversation identifier") |
|
|
context_used: List[str] = Field(default_factory=list, description="Medical contexts used") |
|
|
processing_time: float = Field(..., description="Response time in seconds") |
|
|
confidence: str = Field(..., description="Response confidence level") |
|
|
|
|
|
class Config: |
|
|
schema_extra = { |
|
|
"example": { |
|
|
"success": True, |
|
|
"response": "Malaria symptoms include high fever, chills, headache...", |
|
|
"detected_language": "en", |
|
|
"conversation_id": "conv_123", |
|
|
"context_used": ["Malaria treatment protocols", "Symptom guidelines"], |
|
|
"processing_time": 2.5, |
|
|
"confidence": "high" |
|
|
} |
|
|
} |
|
|
|
|
|
class AudioResponse(BaseModel): |
|
|
"""Audio processing response model""" |
|
|
success: bool = Field(..., description="Whether the request was successful") |
|
|
transcription: str = Field(..., description="Transcribed text from audio") |
|
|
response: str = Field(..., description="The medical response") |
|
|
detected_language: str = Field(..., description="Detected audio language") |
|
|
conversation_id: str = Field(..., description="Conversation identifier") |
|
|
context_used: List[str] = Field(default_factory=list, description="Medical contexts used") |
|
|
processing_time: float = Field(..., description="Response time in seconds") |
|
|
audio_duration: Optional[float] = Field(None, description="Audio duration in seconds") |
|
|
|
|
|
class Config: |
|
|
schema_extra = { |
|
|
"example": { |
|
|
"success": True, |
|
|
"transcription": "What are the symptoms of malaria?", |
|
|
"response": "Malaria symptoms include high fever, chills...", |
|
|
"detected_language": "en", |
|
|
"conversation_id": "conv_456", |
|
|
"context_used": ["Malaria diagnosis"], |
|
|
"processing_time": 3.2, |
|
|
"audio_duration": 4.5 |
|
|
} |
|
|
} |
|
|
|
|
|
class FeedbackRequest(BaseModel): |
|
|
"""Feedback request model""" |
|
|
conversation_id: str = Field(..., description="Conversation ID") |
|
|
rating: int = Field(..., description="Rating from 1-5", ge=1, le=5) |
|
|
feedback: Optional[str] = Field(None, description="Optional text feedback", max_length=500) |
|
|
|
|
|
class Config: |
|
|
schema_extra = { |
|
|
"example": { |
|
|
"conversation_id": "conv_123", |
|
|
"rating": 5, |
|
|
"feedback": "Very helpful and accurate medical information" |
|
|
} |
|
|
} |
|
|
|
|
|
class HealthStatus(BaseModel): |
|
|
"""System health status model""" |
|
|
status: str = Field(..., description="Overall system status") |
|
|
models_loaded: bool = Field(..., description="Whether ML models are loaded") |
|
|
audio_available: bool = Field(..., description="Whether audio processing is available") |
|
|
uptime: float = Field(..., description="System uptime in seconds") |
|
|
version: str = Field(..., description="API version") |
|
|
|
|
|
class Config: |
|
|
schema_extra = { |
|
|
"example": { |
|
|
"status": "healthy", |
|
|
"models_loaded": True, |
|
|
"audio_available": True, |
|
|
"uptime": 3600.0, |
|
|
"version": "2.0.0" |
|
|
} |
|
|
} |
|
|
|
|
|
class ErrorResponse(BaseModel): |
|
|
"""Error response model""" |
|
|
success: bool = Field(False, description="Always false for errors") |
|
|
error: str = Field(..., description="Error message") |
|
|
error_code: str = Field(..., description="Error code") |
|
|
conversation_id: Optional[str] = Field(None, description="Conversation ID if available") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_conversation_id() -> str: |
|
|
"""Generate a unique conversation ID""" |
|
|
return f"conv_{uuid.uuid4().hex[:8]}" |
|
|
|
|
|
def validate_models(): |
|
|
"""Check if models are loaded""" |
|
|
if pipeline is None: |
|
|
raise HTTPException( |
|
|
status_code=503, |
|
|
detail="Medical AI models are not loaded yet. Please try again in a moment." |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/", tags=["system"]) |
|
|
async def root(): |
|
|
"""Root endpoint with API information""" |
|
|
return { |
|
|
"message": "π©Ί Medical AI Assistant API", |
|
|
"version": "2.0.0", |
|
|
"status": "running", |
|
|
"docs": "/docs", |
|
|
"redoc": "/redoc", |
|
|
"endpoints": { |
|
|
"medical_consultation": "/medical/ask", |
|
|
"audio_consultation": "/medical/audio", |
|
|
"health_check": "/health", |
|
|
"feedback": "/feedback" |
|
|
} |
|
|
} |
|
|
|
|
|
@app.get("/health", response_model=HealthStatus, tags=["system"]) |
|
|
async def health_check(): |
|
|
""" |
|
|
## System Health Check |
|
|
|
|
|
Returns the current status of the Medical AI system including: |
|
|
- Overall system health |
|
|
- Model loading status |
|
|
- Audio processing availability |
|
|
- System uptime |
|
|
""" |
|
|
global pipeline, whisper_model |
|
|
|
|
|
|
|
|
uptime = time.time() - getattr(health_check, 'start_time', time.time()) |
|
|
if not hasattr(health_check, 'start_time'): |
|
|
health_check.start_time = time.time() |
|
|
|
|
|
return HealthStatus( |
|
|
status="healthy" if pipeline is not None else "loading", |
|
|
models_loaded=pipeline is not None, |
|
|
audio_available=whisper_model is not None, |
|
|
uptime=uptime, |
|
|
version="2.0.0" |
|
|
) |
|
|
|
|
|
@app.post("/medical/ask", response_model=MedicalResponse, tags=["medical"]) |
|
|
async def medical_consultation(request: MedicalQuestion): |
|
|
""" |
|
|
## Text-based Medical Consultation |
|
|
|
|
|
Process a medical question and return expert medical guidance. |
|
|
|
|
|
**Features:** |
|
|
- π Multilingual support (auto-detect or specify language) |
|
|
- π§ AI-powered medical knowledge retrieval |
|
|
- β‘ Fast response generation |
|
|
- π Medical disclaimers included |
|
|
|
|
|
**Supported Languages:** English (en), French (fr), Auto-detect (auto) |
|
|
""" |
|
|
start_time = time.time() |
|
|
validate_models() |
|
|
|
|
|
conversation_id = request.conversation_id or generate_conversation_id() |
|
|
|
|
|
try: |
|
|
logger.info(f"π©Ί Processing medical question: {request.question[:50]}...") |
|
|
|
|
|
|
|
|
result = pipeline.process( |
|
|
question=request.question, |
|
|
user_lang=request.language, |
|
|
conversation_history=[] |
|
|
) |
|
|
|
|
|
processing_time = time.time() - start_time |
|
|
|
|
|
return MedicalResponse( |
|
|
success=True, |
|
|
response=result["response"], |
|
|
detected_language=result["source_lang"], |
|
|
conversation_id=conversation_id, |
|
|
context_used=result.get("context_used", []), |
|
|
processing_time=round(processing_time, 2), |
|
|
confidence=result.get("confidence", "medium") |
|
|
) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"β Error in medical consultation: {str(e)}", exc_info=True) |
|
|
processing_time = time.time() - start_time |
|
|
|
|
|
raise HTTPException( |
|
|
status_code=500, |
|
|
detail={ |
|
|
"success": False, |
|
|
"error": "Internal processing error occurred", |
|
|
"error_code": "MEDICAL_PROCESSING_ERROR", |
|
|
"conversation_id": conversation_id, |
|
|
"processing_time": round(processing_time, 2) |
|
|
} |
|
|
) |
|
|
|
|
|
@app.post("/medical/audio", response_model=AudioResponse, tags=["audio", "medical"]) |
|
|
async def audio_medical_consultation( |
|
|
file: UploadFile = File(..., description="Audio file (WAV, MP3, M4A, etc.)") |
|
|
): |
|
|
""" |
|
|
## Audio-based Medical Consultation |
|
|
|
|
|
Process an audio medical question and return expert medical guidance. |
|
|
|
|
|
**Features:** |
|
|
- π€ Speech-to-text conversion |
|
|
- π Language detection from audio |
|
|
- π§ Medical AI processing of transcribed text |
|
|
- π Full transcription provided |
|
|
|
|
|
**Supported Audio Formats:** WAV, MP3, M4A, FLAC, OGG |
|
|
**Max File Size:** 25MB |
|
|
**Max Duration:** 5 minutes |
|
|
""" |
|
|
start_time = time.time() |
|
|
validate_models() |
|
|
|
|
|
if whisper_model is None: |
|
|
raise HTTPException( |
|
|
status_code=503, |
|
|
detail="Audio processing is currently unavailable" |
|
|
) |
|
|
|
|
|
conversation_id = generate_conversation_id() |
|
|
|
|
|
try: |
|
|
logger.info(f"π€ Processing audio file: {file.filename}") |
|
|
|
|
|
|
|
|
file_bytes = await file.read() |
|
|
|
|
|
|
|
|
from audio_utils import preprocess_audio |
|
|
processed_audio = preprocess_audio(file_bytes) |
|
|
|
|
|
if len(processed_audio) == 0: |
|
|
raise HTTPException( |
|
|
status_code=400, |
|
|
detail="Could not process audio file. Please check the format and try again." |
|
|
) |
|
|
|
|
|
|
|
|
segments, info = whisper_model.transcribe( |
|
|
processed_audio, |
|
|
beam_size=5, |
|
|
language=None, |
|
|
task='transcribe', |
|
|
vad_filter=True |
|
|
) |
|
|
|
|
|
transcription = "".join([seg.text for seg in segments]) |
|
|
detected_language = info.language |
|
|
|
|
|
if not transcription.strip(): |
|
|
raise HTTPException( |
|
|
status_code=400, |
|
|
detail="Could not transcribe audio. Please ensure clear speech and try again." |
|
|
) |
|
|
|
|
|
logger.info(f"π€ Transcription: {transcription[:100]}...") |
|
|
|
|
|
|
|
|
result = pipeline.process( |
|
|
question=transcription, |
|
|
user_lang=detected_language, |
|
|
conversation_history=[] |
|
|
) |
|
|
|
|
|
processing_time = time.time() - start_time |
|
|
|
|
|
return AudioResponse( |
|
|
success=True, |
|
|
transcription=transcription, |
|
|
response=result["response"], |
|
|
detected_language=detected_language, |
|
|
conversation_id=conversation_id, |
|
|
context_used=result.get("context_used", []), |
|
|
processing_time=round(processing_time, 2), |
|
|
audio_duration=len(processed_audio) / 16000 |
|
|
) |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
except Exception as e: |
|
|
logger.error(f"β Error in audio processing: {str(e)}", exc_info=True) |
|
|
processing_time = time.time() - start_time |
|
|
|
|
|
raise HTTPException( |
|
|
status_code=500, |
|
|
detail={ |
|
|
"success": False, |
|
|
"error": "Audio processing error occurred", |
|
|
"error_code": "AUDIO_PROCESSING_ERROR", |
|
|
"conversation_id": conversation_id, |
|
|
"processing_time": round(processing_time, 2) |
|
|
} |
|
|
) |
|
|
|
|
|
@app.post("/feedback", tags=["feedback"]) |
|
|
async def submit_feedback(request: FeedbackRequest): |
|
|
""" |
|
|
## Submit User Feedback |
|
|
|
|
|
Submit feedback about a medical consultation to help improve the service. |
|
|
|
|
|
**Rating Scale:** |
|
|
- 1: Very Poor |
|
|
- 2: Poor |
|
|
- 3: Average |
|
|
- 4: Good |
|
|
- 5: Excellent |
|
|
""" |
|
|
try: |
|
|
logger.info(f"π Feedback received - ID: {request.conversation_id}, Rating: {request.rating}") |
|
|
|
|
|
|
|
|
|
|
|
feedback_data = { |
|
|
"conversation_id": request.conversation_id, |
|
|
"rating": request.rating, |
|
|
"feedback": request.feedback, |
|
|
"timestamp": time.time() |
|
|
} |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"message": "Thank you for your feedback! This helps us improve our medical AI service.", |
|
|
"feedback_id": f"fb_{uuid.uuid4().hex[:8]}" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"β Error processing feedback: {str(e)}") |
|
|
raise HTTPException( |
|
|
status_code=500, |
|
|
detail="Error processing feedback" |
|
|
) |
|
|
|
|
|
@app.get("/medical/specialties", tags=["medical"]) |
|
|
async def get_medical_specialties(): |
|
|
""" |
|
|
## Get Supported Medical Specialties |
|
|
|
|
|
Returns a list of medical specialties and conditions supported by the AI. |
|
|
""" |
|
|
return { |
|
|
"specialties": [ |
|
|
{ |
|
|
"name": "Primary Care", |
|
|
"description": "General medical consultations and health guidance", |
|
|
"conditions": ["General symptoms", "Preventive care", "Health maintenance"] |
|
|
}, |
|
|
{ |
|
|
"name": "Infectious Diseases", |
|
|
"description": "Infectious disease diagnosis and treatment", |
|
|
"conditions": ["Malaria", "Tuberculosis", "HIV/AIDS", "Respiratory infections"] |
|
|
}, |
|
|
{ |
|
|
"name": "Emergency Medicine", |
|
|
"description": "Emergency protocols and urgent care guidance", |
|
|
"conditions": ["Stroke recognition", "Cardiac emergencies", "Trauma assessment"] |
|
|
}, |
|
|
{ |
|
|
"name": "Chronic Disease Management", |
|
|
"description": "Management of chronic conditions", |
|
|
"conditions": ["Diabetes", "Hypertension", "Gastritis"] |
|
|
} |
|
|
], |
|
|
"languages_supported": ["English", "French", "Auto-detect"], |
|
|
"disclaimer": "This AI provides educational information only. Always consult healthcare professionals for medical advice." |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(404) |
|
|
async def not_found_handler(request, exc): |
|
|
return JSONResponse( |
|
|
status_code=404, |
|
|
content={ |
|
|
"success": False, |
|
|
"error": "Endpoint not found", |
|
|
"error_code": "NOT_FOUND", |
|
|
"available_endpoints": [ |
|
|
"/docs - API Documentation", |
|
|
"/medical/ask - Text consultation", |
|
|
"/medical/audio - Audio consultation", |
|
|
"/health - System status", |
|
|
"/feedback - Submit feedback" |
|
|
] |
|
|
} |
|
|
) |
|
|
|
|
|
@app.exception_handler(422) |
|
|
async def validation_exception_handler(request, exc): |
|
|
return JSONResponse( |
|
|
status_code=422, |
|
|
content={ |
|
|
"success": False, |
|
|
"error": "Invalid request data", |
|
|
"error_code": "VALIDATION_ERROR", |
|
|
"details": exc.errors() |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|