AI-agent / app.py
fady-50's picture
Upload 273 files
ab436f2 verified
Raw
History Blame Contribute Delete
12.2 kB
import os
import re
import uuid
from pathlib import Path
from typing import Optional
import requests
import uvicorn
from fastapi import Cookie, FastAPI, File, Form, HTTPException, Response, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from agents.agent_decision import (
load_report_from_disk,
medical_agent_instance,
reports_db,
)
from config import Config
config = Config()
app = FastAPI(
title="AI-MD Integrated Multi-Agent Medical API",
version="5.0",
description="One API for medical chat, anatomical-layer triage, image analysis, RAG, voice transcription, and speech.",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
UPLOAD_DIR = Path("uploads/backend")
REPORTS_DIR = Path("uploads/backend/reports")
SKIN_LESION_OUTPUT_DIR = Path("uploads/skin_lesion_output")
SPEECH_DIR = Path("uploads/speech")
for directory in [UPLOAD_DIR, REPORTS_DIR, SKIN_LESION_OUTPUT_DIR, SPEECH_DIR]:
directory.mkdir(parents=True, exist_ok=True)
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "webp"}
ALLOWED_AUDIO_EXTENSIONS = {"webm", "wav", "mp3", "m4a", "ogg"}
_image_agent = None
class StartRequest(BaseModel):
body_part: str
chief_complaint: Optional[str] = None
class DiagnosticRequest(BaseModel):
session_id: str
body_part: str
message: str
class SpeechRequest(BaseModel):
text: str
voice_id: Optional[str] = None
def secure_filename(filename: str) -> str:
filename = os.path.basename(filename or "upload")
filename = re.sub(r"[^A-Za-z0-9_.-]+", "_", filename).strip("._")
return filename or "upload"
def allowed_file(filename: str, allowed: set[str]) -> bool:
return "." in filename and filename.rsplit(".", 1)[1].lower() in allowed
def get_image_agent():
global _image_agent
if _image_agent is None:
from agents.image_analysis_agent import ImageAnalysisAgent
_image_agent = ImageAnalysisAgent(config)
return _image_agent
def save_upload(upload: UploadFile, folder: Path, allowed: set[str], max_mb: int) -> Path:
if not upload.filename or not allowed_file(upload.filename, allowed):
raise HTTPException(status_code=400, detail="Unsupported file type.")
content = upload.file.read()
if not content:
raise HTTPException(status_code=400, detail="Empty uploaded file.")
if len(content) > max_mb * 1024 * 1024:
raise HTTPException(status_code=413, detail=f"File too large. Max: {max_mb}MB.")
filename = f"{uuid.uuid4()}_{secure_filename(upload.filename)}"
path = folder / filename
path.write_bytes(content)
return path
def run_image_analysis(image_path: Path, image_type: str, text: str = "") -> dict:
agent = get_image_agent()
normalized_type = (image_type or "auto").strip().lower()
if normalized_type == "auto":
try:
classifier_result = str(agent.analyze_image(str(image_path)))
except Exception as exc:
classifier_result = f"auto-classification unavailable: {exc}"
lower = classifier_result.lower()
if "xray" in lower or "x-ray" in lower or "chest" in lower:
normalized_type = "chest_xray"
elif "skin" in lower or "lesion" in lower:
normalized_type = "skin_lesion"
else:
return {
"agent": "IMAGE_CLASSIFIER",
"image_type": "auto",
"result": classifier_result,
"medical_note": "Image type was not confidently routed to a specialist image agent.",
}
if normalized_type in {"chest", "xray", "x-ray", "chest_xray"}:
result = agent.classify_chest_xray(str(image_path))
return {
"agent": "CHEST_XRAY_AGENT",
"image_type": "chest_xray",
"result": result,
"medical_note": "AI image output must be reviewed by a qualified clinician.",
}
if normalized_type in {"skin", "lesion", "skin_lesion"}:
result = agent.segment_skin_lesion(str(image_path))
mask_url = None
mask_path = Path(config.medical_cv.skin_lesion_segmentation_output_path)
if mask_path.exists():
mask_url = "/" + str(mask_path).replace("\\", "/")
return {
"agent": "SKIN_LESION_AGENT",
"image_type": "skin_lesion",
"result": result,
"mask_url": mask_url,
"medical_note": "AI image output must be reviewed by a qualified clinician.",
}
raise HTTPException(
status_code=400,
detail="image_type must be auto, chest_xray, or skin_lesion.",
)
@app.get("/")
def root():
return {
"status": "ok",
"service": "AI-MD Integrated Multi-Agent Medical API",
"docs": "/docs",
"endpoints": [
"GET /health",
"POST /api/v1/start",
"POST /api/v1/chat",
"POST /api/v1/image/analyze",
"POST /api/v1/multimodal-chat",
"POST /api/v1/transcribe",
"POST /api/v1/generate-speech",
"GET /api/v1/report/{session_id}",
],
}
@app.get("/health")
def health_check():
return {"status": "healthy"}
@app.post("/api/v1/start")
def start_diagnostic_session(request: StartRequest):
try:
return medical_agent_instance.start_session(
body_part=request.body_part,
chief_complaint=request.chief_complaint,
)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@app.post("/api/v1/chat")
def process_diagnostic_chat(request: DiagnosticRequest):
try:
return medical_agent_instance.process(
session_id=request.session_id,
body_part=request.body_part,
user_message=request.message,
)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@app.get("/api/v1/report/{session_id}")
def get_final_report(session_id: str):
entry = reports_db.get(session_id)
if not entry:
saved = load_report_from_disk(session_id)
if saved:
entry = {"status": "COMPLETED", "report": saved}
reports_db[session_id] = entry
if not entry:
raise HTTPException(status_code=404, detail="Session not found or report is not ready.")
if entry.get("status") == "COMPLETED" or entry.get("report"):
medical_agent_instance.cleanup_session(session_id)
return {
"status": "REPORT",
"session_status": "COMPLETED",
"report": entry.get("report", {}),
}
return entry
@app.post("/api/v1/image/analyze")
async def analyze_image(
image: UploadFile = File(...),
image_type: str = Form("auto"),
text: str = Form(""),
session_id: Optional[str] = Form(None),
body_part: Optional[str] = Form(None),
):
path = save_upload(image, UPLOAD_DIR, ALLOWED_IMAGE_EXTENSIONS, config.api.max_image_upload_size)
try:
image_result = run_image_analysis(path, image_type=image_type, text=text)
diagnostic_update = None
if session_id and body_part:
message = (
f"Image analysis result from {image_result['agent']} "
f"for {image_result['image_type']}: {image_result.get('result')}. "
f"Patient context: {text}"
)
diagnostic_update = medical_agent_instance.process(
session_id=session_id,
body_part=body_part,
user_message=message,
)
return {
"status": "success",
"session_id": session_id,
"image": image_result,
"diagnostic_update": diagnostic_update,
}
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@app.post("/api/v1/upload")
async def upload_image_legacy(
response: Response,
image: UploadFile = File(...),
text: str = Form(""),
image_type: str = Form("auto"),
session_id: Optional[str] = Cookie(None),
body_part: Optional[str] = Form(None),
):
result = await analyze_image(
image=image,
image_type=image_type,
text=text,
session_id=session_id,
body_part=body_part,
)
if result.get("session_id"):
response.set_cookie(key="session_id", value=result["session_id"])
return result
@app.post("/api/v1/multimodal-chat")
async def multimodal_chat(
session_id: str = Form(...),
body_part: str = Form(...),
message: str = Form(""),
image: Optional[UploadFile] = File(None),
image_type: str = Form("auto"),
):
image_result = None
combined_message = message
if image is not None:
path = save_upload(image, UPLOAD_DIR, ALLOWED_IMAGE_EXTENSIONS, config.api.max_image_upload_size)
image_result = run_image_analysis(path, image_type=image_type, text=message)
combined_message = (
f"{message}\n\nAttached image analysis result: "
f"{image_result['agent']} / {image_result['image_type']} -> {image_result.get('result')}"
)
diagnostic_response = medical_agent_instance.process(
session_id=session_id,
body_part=body_part,
user_message=combined_message,
)
return {
"status": "success",
"session_id": session_id,
"image": image_result,
"diagnostic_response": diagnostic_response,
}
@app.post("/api/v1/transcribe")
async def transcribe_audio(audio: UploadFile = File(...)):
if not config.speech.eleven_labs_api_key:
raise HTTPException(status_code=503, detail="ELEVEN_LABS_API_KEY is not configured.")
path = save_upload(audio, SPEECH_DIR, ALLOWED_AUDIO_EXTENSIONS, max_mb=25)
try:
from elevenlabs.client import ElevenLabs
client = ElevenLabs(api_key=config.speech.eleven_labs_api_key)
with path.open("rb") as audio_file:
transcription = client.speech_to_text.convert(
file=audio_file,
model_id="scribe_v1",
tag_audio_events=True,
language_code="eng",
diarize=True,
)
return {"status": "success", "transcript": transcription.text}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@app.post("/api/v1/generate-speech")
async def generate_speech(request: SpeechRequest):
if not config.speech.eleven_labs_api_key:
raise HTTPException(status_code=503, detail="ELEVEN_LABS_API_KEY is not configured.")
voice_id = request.voice_id or config.speech.eleven_labs_voice_id
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream"
headers = {
"Accept": "audio/mpeg",
"Content-Type": "application/json",
"xi-api-key": config.speech.eleven_labs_api_key,
}
payload = {
"text": request.text,
"model_id": "eleven_monolingual_v1",
"voice_settings": {"stability": 0.5, "similarity_boost": 0.5},
}
response = requests.post(url, headers=headers, json=payload, timeout=120)
if response.status_code != 200:
raise HTTPException(status_code=500, detail=response.text)
output_path = SPEECH_DIR / f"{uuid.uuid4()}.mp3"
output_path.write_bytes(response.content)
return FileResponse(path=output_path, media_type="audio/mpeg", filename="generated_speech.mp3")
@app.exception_handler(413)
async def request_entity_too_large(request, exc):
return JSONResponse(
status_code=413,
content={"status": "error", "response": f"File too large. Max: {config.api.max_image_upload_size}MB"},
)
if __name__ == "__main__":
uvicorn.run(app, host=config.api.host, port=config.api.port)