File size: 7,379 Bytes
19d6abb
 
 
 
 
 
 
 
 
3973c7a
 
19d6abb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3973c7a
19d6abb
 
3973c7a
19d6abb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3973c7a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import sys
import os
from pathlib import Path

# Add parent directory to sys.path to allow imports from root level modules
sys.path.insert(0, str(Path(__file__).parent.parent))

from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from typing import List
from agents.orchestrator import run_presage_pipeline
import logging
from datetime import datetime

# Setup logging for debugging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(
    title="PRESAGE Multi-Agent AI Health Prediction System",
    version="1.0.0",
    description="Backend analysis engine utilizing parallel medical agents and ML inference."
)

# Enable CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Define the Pydantic data schema for incoming API requests to ensure strict validation
class PatientInputSchema(BaseModel):
    age: int = Field(..., ge=1, le=120, description="Age of the patient")
    sex: str = Field(..., description="Biological sex (male/female/other)")
    blood_pressure_systolic: int = Field(..., ge=50, le=250, description="Systolic blood pressure in mmHg")
    blood_pressure_diastolic: int = Field(..., ge=30, le=150, description="Diastolic blood pressure in mmHg")
    blood_sugar_fasting: float = Field(..., ge=40.0, le=500.0, description="Fasting blood sugar in mg/dL")
    cholesterol_total: float = Field(..., ge=80.0, le=500.0, description="Total cholesterol in mg/dL")
    hdl_cholesterol: float = Field(..., ge=10.0, le=150.0, description="HDL cholesterol in mg/dL")
    bmi: float = Field(..., ge=10.0, le=60.0, description="Body Mass Index")
    sleep_hours: float = Field(..., ge=0.0, le=24.0, description="Average sleep hours per night")
    stress_level: str = Field(..., description="Perceived stress level (low/medium/high)")
    smoking: bool = Field(..., description="True if current smoker, False otherwise")
    alcohol: str = Field(..., description="Alcohol consumption tier (none/moderate/heavy)")
    exercise_days_per_week: int = Field(..., ge=0, le=7, description="Number of exercise days per week")
    iron_level: float = Field(..., ge=5.0, le=300.0, description="Serum iron level in mcg/dL")
    cortisol: float = Field(..., ge=1.0, le=60.0, description="Cortisol level in mcg/dL")
    family_history: List[str] = Field(default=[], description="List of conditions present in family history")

    class Config:
        json_schema_extra = {
            "example": {
                "age": 45,
                "sex": "male",
                "blood_pressure_systolic": 135,
                "blood_pressure_diastolic": 85,
                "blood_sugar_fasting": 105.5,
                "cholesterol_total": 210.0,
                "hdl_cholesterol": 38.0,
                "bmi": 26.4,
                "sleep_hours": 5.5,
                "stress_level": "high",
                "smoking": True,
                "alcohol": "moderate",
                "exercise_days_per_week": 1,
                "iron_level": 75.0,
                "cortisol": 22.1,
                "family_history": ["diabetes", "hypertension"]
            }
        }

@app.get("/health")
def health_check():
    """Returns the operational status of the PRESAGE backend engine."""
    return {"status": "healthy", "engine": "PRESAGE Multi-Agent Pipeline"}

@app.post("/analyze")
def analyze_patient_profile(patient_data: PatientInputSchema):
    """
    Accepts a validated patient profile, routes it through the multi-agent execution pipeline,
    and returns a comprehensive risk evaluation and customized 90-day intervention strategy.
    """
    try:
        logger.info("Starting patient analysis pipeline...")
        # Convert the validated Pydantic object directly into a standard Python dict
        raw_input_dict = patient_data.model_dump()
        
        # Execute the pipeline via our orchestrator
        logger.info("Executing orchestrator pipeline...")
        pipeline_output = run_presage_pipeline(raw_input_dict)
        logger.info("Pipeline completed successfully")
        
        return pipeline_output
    except Exception as e:
        logger.error(f"Pipeline error: {str(e)}", exc_info=True)
        raise HTTPException(status_code=500, detail=f"Pipeline Processing Error: {str(e)}")

@app.post("/upload-image")
async def upload_medical_image(file: UploadFile = File(...), patient_id: str = None):
    """
    Accepts medical images (X-rays, lab reports, etc.) and stores them for future analysis.
    Supports: JPEG, PNG, PDF, and common medical image formats.
    """
    try:
        # Create uploads directory if it doesn't exist
        upload_dir = os.path.join(os.path.dirname(__file__), "..", "data", "patient_images")
        os.makedirs(upload_dir, exist_ok=True)
        
        # Validate file type
        allowed_extensions = {'jpg', 'jpeg', 'png', 'pdf', 'dicom', 'dcm', 'gif'}
        file_ext = file.filename.split('.')[-1].lower() if file.filename else ""
        
        if file_ext not in allowed_extensions:
            raise HTTPException(
                status_code=400, 
                detail=f"Invalid file type: {file_ext}. Allowed: {', '.join(allowed_extensions)}"
            )
        
        # Validate file size (max 50MB)
        max_size = 50 * 1024 * 1024
        file_content = await file.read()
        
        if len(file_content) > max_size:
            raise HTTPException(
                status_code=413,
                detail=f"File too large. Maximum size: 50MB, Received: {len(file_content) / (1024*1024):.2f}MB"
            )
        
        # Generate unique filename with timestamp
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        patient_prefix = f"{patient_id}_" if patient_id else ""
        filename = f"{patient_prefix}{timestamp}_{file.filename}"
        filepath = os.path.join(upload_dir, filename)
        
        # Save file
        with open(filepath, "wb") as f:
            f.write(file_content)
        
        logger.info(f"Medical image uploaded: {filename} ({len(file_content) / 1024:.2f}KB)")
        
        return {
            "status": "success",
            "message": "Medical image uploaded successfully",
            "filename": filename,
            "filepath": filepath,
            "size_kb": len(file_content) / 1024,
            "patient_id": patient_id
        }
    
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Image upload error: {str(e)}", exc_info=True)
        raise HTTPException(status_code=500, detail=f"Image upload failed: {str(e)}")

# Serve static files from the Next.js export
# Mount the static files at the root
# We use a helper to serve index.html for the root and other routes
out_path = Path(__file__).parent.parent / "client" / "out"

if out_path.exists():
    app.mount("/", StaticFiles(directory=str(out_path), html=True), name="static")

@app.exception_handler(404)
async def custom_404_handler(request, __):
    if out_path.exists():
        return FileResponse(str(out_path / "index.html"))
    return HTTPException(status_code=404, detail="Not Found")