Spaces:
Runtime error
Runtime error
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 2 |
+
from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
|
| 3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
import google.generativeai as genai
|
| 7 |
+
from gtts import gTTS
|
| 8 |
+
import speech_recognition as sr
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import tempfile
|
| 12 |
+
import subprocess
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
import firebase_admin
|
| 15 |
+
from firebase_admin import credentials, firestore
|
| 16 |
+
|
| 17 |
+
# Load environment variables
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
# ==================== ENVIRONMENT VALIDATION ====================
|
| 21 |
+
def validate_environment():
|
| 22 |
+
"""Validate required environment variables"""
|
| 23 |
+
required_vars = ["GEMINI_API_KEY", "FIREBASE_CREDENTIALS"]
|
| 24 |
+
missing = [var for var in required_vars if not os.getenv(var)]
|
| 25 |
+
if missing:
|
| 26 |
+
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
|
| 27 |
+
|
| 28 |
+
validate_environment()
|
| 29 |
+
|
| 30 |
+
# ==================== INITIALIZE SERVICES ====================
|
| 31 |
+
# Initialize Gemini client (NO HARDCODED KEY)
|
| 32 |
+
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
|
| 33 |
+
|
| 34 |
+
# Initialize Firebase
|
| 35 |
+
firebase_creds = os.getenv("FIREBASE_CREDENTIALS")
|
| 36 |
+
cred_dict = json.loads(firebase_creds)
|
| 37 |
+
|
| 38 |
+
if not firebase_admin._apps:
|
| 39 |
+
cred = credentials.Certificate(cred_dict)
|
| 40 |
+
firebase_admin.initialize_app(cred)
|
| 41 |
+
|
| 42 |
+
db = firestore.client()
|
| 43 |
+
|
| 44 |
+
# Initialize FastAPI
|
| 45 |
+
app = FastAPI(title="Dr. HealBot - Medical Consultation API")
|
| 46 |
+
|
| 47 |
+
# CORS
|
| 48 |
+
app.add_middleware(
|
| 49 |
+
CORSMiddleware,
|
| 50 |
+
allow_origins=["*"],
|
| 51 |
+
allow_credentials=True,
|
| 52 |
+
allow_methods=["*"],
|
| 53 |
+
allow_headers=["*"],
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# ==================== MODELS ====================
|
| 57 |
+
class ChatRequest(BaseModel):
|
| 58 |
+
message: str
|
| 59 |
+
user_id: str
|
| 60 |
+
language: str = "auto"
|
| 61 |
+
|
| 62 |
+
class PatientData(BaseModel):
|
| 63 |
+
name: str
|
| 64 |
+
patient_profile: dict
|
| 65 |
+
lab_test_results: dict
|
| 66 |
+
|
| 67 |
+
class TTSRequest(BaseModel):
|
| 68 |
+
text: str
|
| 69 |
+
language_code: str = "en"
|
| 70 |
+
|
| 71 |
+
# ==================== SYSTEM PROMPT ====================
|
| 72 |
+
DOCTOR_SYSTEM_PROMPT = """
|
| 73 |
+
You are Dr. HealBot, a calm, knowledgeable, and empathetic virtual doctor.
|
| 74 |
+
|
| 75 |
+
GOAL:
|
| 76 |
+
Hold a natural, focused conversation with the patient to understand their health issue through a series of questions (ONE AT A TIME) before providing comprehensive guidance.
|
| 77 |
+
|
| 78 |
+
PATIENT HISTORY (IMPORTANT):
|
| 79 |
+
The following medical profile belongs to the current patient:
|
| 80 |
+
{patient_summary}
|
| 81 |
+
|
| 82 |
+
RULES FOR PATIENT HISTORY:
|
| 83 |
+
- ALWAYS use patient history in your reasoning (chronic diseases, medications, allergies, surgeries, recent labs, lifestyle).
|
| 84 |
+
- NEVER ignore relevant risks or medication interactions.
|
| 85 |
+
- TAILOR all advice (possible causes, medication safety, red flags) based on the patient's medical profile.
|
| 86 |
+
- Keep references to history natural and brief—only if medically relevant.
|
| 87 |
+
|
| 88 |
+
⚠️ CRITICAL: ASK ONLY ONE QUESTION AT A TIME - This makes the conversation natural and not overwhelming.
|
| 89 |
+
|
| 90 |
+
RESTRICTIONS:
|
| 91 |
+
- ONLY provide information related to medical, health, or wellness topics.
|
| 92 |
+
- If asked anything non-medical, politely decline:
|
| 93 |
+
"I'm a medical consultation assistant and can only help with health or medical-related concerns."
|
| 94 |
+
|
| 95 |
+
CONVERSATION FLOW:
|
| 96 |
+
|
| 97 |
+
**PHASE 1: INFORMATION GATHERING (Conversational)**
|
| 98 |
+
When a patient first mentions a symptom or health concern:
|
| 99 |
+
- Acknowledge their concern warmly (1-2 sentences)
|
| 100 |
+
- Ask ONLY ONE relevant follow-up question
|
| 101 |
+
- Keep it conversational - like a real doctor's consultation
|
| 102 |
+
- DO NOT give the final detailed response yet
|
| 103 |
+
- DO NOT ask multiple questions at once
|
| 104 |
+
|
| 105 |
+
Examples of good single questions:
|
| 106 |
+
- "How long have you been experiencing this fever?"
|
| 107 |
+
- "Have you taken your temperature? If yes, what was the reading?"
|
| 108 |
+
- "Are you experiencing any other symptoms along with the fever?"
|
| 109 |
+
- "Have you taken any medication for this yet?"
|
| 110 |
+
|
| 111 |
+
**PHASE 2: CONTINUED CONVERSATION**
|
| 112 |
+
- Continue asking clarifying questions ONE AT A TIME until you have enough information
|
| 113 |
+
- Typical consultations need 2-3 exchanges before final assessment
|
| 114 |
+
- Each response should have: brief acknowledgment + ONE question
|
| 115 |
+
- Consider asking about: onset, duration, severity, location, triggers, relieving factors
|
| 116 |
+
- Factor in patient's medical history when asking questions
|
| 117 |
+
- Never overwhelm the patient with multiple questions at once
|
| 118 |
+
|
| 119 |
+
**PHASE 3: FINAL COMPREHENSIVE RESPONSE**
|
| 120 |
+
Only provide the detailed response format AFTER you have gathered sufficient information through conversation.
|
| 121 |
+
|
| 122 |
+
📋 Based on what you've told me...
|
| 123 |
+
[Brief summary of patient's symptoms, plus any relevant history factors]
|
| 124 |
+
|
| 125 |
+
🔍 Possible Causes (Preliminary)
|
| 126 |
+
- 1–2 possible explanations using soft language ("It could be…", "This might be…")
|
| 127 |
+
- Include disclaimer that this is not a confirmed diagnosis
|
| 128 |
+
- NOTE: Adjust based on patient's history (conditions, meds, allergies)
|
| 129 |
+
|
| 130 |
+
💊 Medication Advice (Safe & OTC)
|
| 131 |
+
- Suggest only widely available OTC medicines
|
| 132 |
+
- ENSURE medication is safe given the patient's:
|
| 133 |
+
- allergies
|
| 134 |
+
- chronic illnesses
|
| 135 |
+
- current medications
|
| 136 |
+
- Use disclaimers:
|
| 137 |
+
"Use only if you have no allergies to this medication."
|
| 138 |
+
"Follow packaging instructions or consult a doctor for exact dosing."
|
| 139 |
+
|
| 140 |
+
💡 Lifestyle & Home Care Tips
|
| 141 |
+
- 2–3 simple, practical suggestions
|
| 142 |
+
|
| 143 |
+
⚠️ When to See a Real Doctor
|
| 144 |
+
- Warning signs adjusted to the patient's underlying medical risks
|
| 145 |
+
|
| 146 |
+
📅 Follow-Up Advice
|
| 147 |
+
- One short recommendation about monitoring symptoms or follow-up timing
|
| 148 |
+
|
| 149 |
+
**HOW TO DECIDE WHEN TO GIVE FINAL RESPONSE:**
|
| 150 |
+
Give the detailed final response when you have:
|
| 151 |
+
✅ Duration of symptoms
|
| 152 |
+
✅ Severity level
|
| 153 |
+
✅ Main accompanying symptoms
|
| 154 |
+
✅ Any relevant patient history considerations
|
| 155 |
+
✅ Patient has answered at least 2-3 of your questions
|
| 156 |
+
|
| 157 |
+
If patient explicitly asks for immediate advice or says "just tell me what to do", you can provide the final response earlier.
|
| 158 |
+
|
| 159 |
+
CONVERSATION MODES:
|
| 160 |
+
|
| 161 |
+
1. **Doctor Mode** (for symptoms/health issues):
|
| 162 |
+
- Start with conversational questions
|
| 163 |
+
- Gather information progressively
|
| 164 |
+
- Only provide structured final response after sufficient information
|
| 165 |
+
|
| 166 |
+
2. **Instructor Mode** (for general medical questions):
|
| 167 |
+
- If patient asks "What is diabetes?" or "How does aspirin work?" - provide direct educational answer
|
| 168 |
+
- Give clear, educational explanations
|
| 169 |
+
- Use short paragraphs or bullet points
|
| 170 |
+
- No need for lengthy information gathering
|
| 171 |
+
|
| 172 |
+
TONE & STYLE:
|
| 173 |
+
- Warm, calm, professional—like a caring doctor in a consultation
|
| 174 |
+
- Conversational and natural in early exchanges
|
| 175 |
+
- Clear, empathetic, no jargon
|
| 176 |
+
- Show you're listening by referencing what they've told you
|
| 177 |
+
- Never give definitive diagnoses; always use soft language
|
| 178 |
+
|
| 179 |
+
IMPORTANT:
|
| 180 |
+
- This is preliminary guidance, not a substitute for professional care.
|
| 181 |
+
- Never provide non-medical information.
|
| 182 |
+
- Be conversational first, comprehensive later.
|
| 183 |
+
- response has No Emoji or No emojis No smileys No flags No pictographs
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
# ==================== HELPER FUNCTIONS ====================
|
| 187 |
+
def generate_patient_summary(patient_data: dict) -> str:
|
| 188 |
+
"""Generate a comprehensive summary of patient's medical profile and lab results"""
|
| 189 |
+
if not patient_data:
|
| 190 |
+
return ""
|
| 191 |
+
|
| 192 |
+
summary = "\n🏥 **PATIENT MEDICAL PROFILE**\n"
|
| 193 |
+
|
| 194 |
+
# Patient Profile Section
|
| 195 |
+
if "patient_profile" in patient_data:
|
| 196 |
+
profile = patient_data["patient_profile"]
|
| 197 |
+
|
| 198 |
+
# Critical Medical Info
|
| 199 |
+
if "critical_medical_info" in profile:
|
| 200 |
+
cmi = profile["critical_medical_info"]
|
| 201 |
+
summary += "\n📌 **Critical Medical Information:**\n"
|
| 202 |
+
summary += f"- Major Conditions: {cmi.get('major_conditions', 'None')}\n"
|
| 203 |
+
summary += f"- Current Medications: {cmi.get('current_medications', 'None')}\n"
|
| 204 |
+
summary += f"- Allergies: {cmi.get('allergies', 'None')}\n"
|
| 205 |
+
if cmi.get('past_surgeries_or_treatments') and cmi['past_surgeries_or_treatments'] != 'None':
|
| 206 |
+
summary += f"- Past Surgeries: {cmi.get('past_surgeries_or_treatments')}\n"
|
| 207 |
+
|
| 208 |
+
# Vital Risk Factors
|
| 209 |
+
if "vital_risk_factors" in profile:
|
| 210 |
+
vrf = profile["vital_risk_factors"]
|
| 211 |
+
summary += "\n⚠️ **Risk Factors:**\n"
|
| 212 |
+
if vrf.get('smoking_status') and 'smok' in vrf['smoking_status'].lower():
|
| 213 |
+
summary += f"- Smoking: {vrf.get('smoking_status')}\n"
|
| 214 |
+
if vrf.get('blood_pressure_issue') and vrf['blood_pressure_issue'] != 'No':
|
| 215 |
+
summary += f"- Blood Pressure: {vrf.get('blood_pressure_issue')}\n"
|
| 216 |
+
if vrf.get('cholesterol_issue') and vrf['cholesterol_issue'] != 'No':
|
| 217 |
+
summary += f"- Cholesterol: {vrf.get('cholesterol_issue')}\n"
|
| 218 |
+
if vrf.get('diabetes_status') and 'diabetes' in vrf['diabetes_status'].lower():
|
| 219 |
+
summary += f"- Diabetes: {vrf.get('diabetes_status')}\n"
|
| 220 |
+
if vrf.get('family_history_major_disease'):
|
| 221 |
+
summary += f"- Family History: {vrf.get('family_history_major_disease')}\n"
|
| 222 |
+
|
| 223 |
+
# Organ Health Summary
|
| 224 |
+
if "organ_health_summary" in profile:
|
| 225 |
+
ohs = profile["organ_health_summary"]
|
| 226 |
+
issues = []
|
| 227 |
+
if ohs.get('heart_health') and 'normal' not in ohs['heart_health'].lower():
|
| 228 |
+
issues.append(f"Heart: {ohs['heart_health']}")
|
| 229 |
+
if ohs.get('kidney_health') and 'no' not in ohs['kidney_health'].lower():
|
| 230 |
+
issues.append(f"Kidney: {ohs['kidney_health']}")
|
| 231 |
+
if ohs.get('liver_health') and 'normal' not in ohs['liver_health'].lower() and 'no' not in ohs['liver_health'].lower():
|
| 232 |
+
issues.append(f"Liver: {ohs['liver_health']}")
|
| 233 |
+
if ohs.get('gut_health') and 'normal' not in ohs['gut_health'].lower():
|
| 234 |
+
issues.append(f"Gut: {ohs['gut_health']}")
|
| 235 |
+
|
| 236 |
+
if issues:
|
| 237 |
+
summary += "\n🫀 **Organ Health Concerns:**\n"
|
| 238 |
+
for issue in issues:
|
| 239 |
+
summary += f"- {issue}\n"
|
| 240 |
+
|
| 241 |
+
# Mental & Sleep Health
|
| 242 |
+
if "mental_sleep_health" in profile:
|
| 243 |
+
msh = profile["mental_sleep_health"]
|
| 244 |
+
summary += "\n🧠 **Mental & Sleep Health:**\n"
|
| 245 |
+
summary += f"- Mental Status: {msh.get('mental_health_status', 'Not specified')}\n"
|
| 246 |
+
if msh.get('mental_conditions'):
|
| 247 |
+
summary += f"- Mental Conditions: {msh.get('mental_conditions')}\n"
|
| 248 |
+
summary += f"- Sleep: {msh.get('sleep_hours', 'Not specified')} per night"
|
| 249 |
+
if msh.get('sleep_problems'):
|
| 250 |
+
summary += f" ({msh.get('sleep_problems')})\n"
|
| 251 |
+
else:
|
| 252 |
+
summary += "\n"
|
| 253 |
+
|
| 254 |
+
# Lifestyle
|
| 255 |
+
if "lifestyle" in profile:
|
| 256 |
+
ls = profile["lifestyle"]
|
| 257 |
+
summary += "\n🏃 **Lifestyle:**\n"
|
| 258 |
+
summary += f"- Activity: {ls.get('physical_activity_level', 'Not specified')}\n"
|
| 259 |
+
summary += f"- Diet: {ls.get('diet_type', 'Not specified')}\n"
|
| 260 |
+
|
| 261 |
+
# Lab Test Results Section
|
| 262 |
+
if "lab_test_results" in patient_data:
|
| 263 |
+
lab_results = patient_data["lab_test_results"]
|
| 264 |
+
abnormal_results = []
|
| 265 |
+
|
| 266 |
+
# Check each test category for abnormal results
|
| 267 |
+
for test_category, tests in lab_results.items():
|
| 268 |
+
if isinstance(tests, dict):
|
| 269 |
+
for test_name, result in tests.items():
|
| 270 |
+
if result and isinstance(result, str):
|
| 271 |
+
result_lower = result.lower()
|
| 272 |
+
if any(word in result_lower for word in ['high', 'low', 'elevated', 'borderline']):
|
| 273 |
+
abnormal_results.append(f"{test_name.replace('_', ' ').title()}: {result}")
|
| 274 |
+
|
| 275 |
+
if abnormal_results:
|
| 276 |
+
summary += "\n🔬 **Key Lab Results (Abnormal):**\n"
|
| 277 |
+
for result in abnormal_results[:10]:
|
| 278 |
+
summary += f"- {result}\n"
|
| 279 |
+
|
| 280 |
+
# Health Goals
|
| 281 |
+
if "patient_profile" in patient_data and "primary_health_goals" in patient_data["patient_profile"]:
|
| 282 |
+
goals = patient_data["patient_profile"]["primary_health_goals"]
|
| 283 |
+
summary += f"\n🎯 **Health Goals:** {goals}\n"
|
| 284 |
+
|
| 285 |
+
return summary
|
| 286 |
+
|
| 287 |
+
def save_patient_data(user_id: str, data: dict):
|
| 288 |
+
"""Save patient data to Firebase Firestore"""
|
| 289 |
+
data["last_updated"] = datetime.now().isoformat()
|
| 290 |
+
db.collection("patients").document(user_id).set(data)
|
| 291 |
+
|
| 292 |
+
def load_patient_data(user_id: str) -> dict:
|
| 293 |
+
"""Load patient data from Firebase Firestore"""
|
| 294 |
+
doc = db.collection("patients").document(user_id).get()
|
| 295 |
+
if doc.exists:
|
| 296 |
+
return doc.to_dict()
|
| 297 |
+
return None
|
| 298 |
+
|
| 299 |
+
def save_chat_history(user_id: str, messages: list):
|
| 300 |
+
db.collection("chat_history").document(user_id).set({
|
| 301 |
+
"messages": messages,
|
| 302 |
+
"last_updated": datetime.now().isoformat()
|
| 303 |
+
})
|
| 304 |
+
|
| 305 |
+
def load_chat_history(user_id: str) -> list:
|
| 306 |
+
doc = db.collection("chat_history").document(user_id).get()
|
| 307 |
+
if doc.exists:
|
| 308 |
+
return doc.to_dict().get("messages", [])
|
| 309 |
+
return []
|
| 310 |
+
|
| 311 |
+
def delete_chat_history(user_id: str):
|
| 312 |
+
db.collection("chat_history").document(user_id).delete()
|
| 313 |
+
|
| 314 |
+
import re
|
| 315 |
+
|
| 316 |
+
def remove_emojis(text: str) -> str:
|
| 317 |
+
"""
|
| 318 |
+
Remove all emojis from a string.
|
| 319 |
+
"""
|
| 320 |
+
emoji_pattern = re.compile(
|
| 321 |
+
"["
|
| 322 |
+
"\U0001F600-\U0001F64F" # emoticons
|
| 323 |
+
"\U0001F300-\U0001F5FF" # symbols & pictographs
|
| 324 |
+
"\U0001F680-\U0001F6FF" # transport & map symbols
|
| 325 |
+
"\U0001F1E0-\U0001F1FF" # flags
|
| 326 |
+
"\U00002700-\U000027BF" # Dingbats
|
| 327 |
+
"\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
|
| 328 |
+
"\U00002600-\U000026FF" # Misc symbols
|
| 329 |
+
"\U00002B00-\U00002BFF" # Misc symbols & arrows
|
| 330 |
+
"]+", flags=re.UNICODE
|
| 331 |
+
)
|
| 332 |
+
return emoji_pattern.sub(r'', text)
|
| 333 |
+
import markdown
|
| 334 |
+
|
| 335 |
+
def generate_patient_summary_html(patient_data: dict) -> str:
|
| 336 |
+
"""
|
| 337 |
+
Generate patient summary as HTML instead of Markdown.
|
| 338 |
+
"""
|
| 339 |
+
md_summary = generate_patient_summary(patient_data)
|
| 340 |
+
html_summary = markdown.markdown(md_summary)
|
| 341 |
+
return html_summary
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
# ==================== ROOT ENDPOINT ====================
|
| 345 |
+
@app.get("/", response_class=HTMLResponse)
|
| 346 |
+
async def root():
|
| 347 |
+
"""Root endpoint - tries to serve index.html, falls back to JSON"""
|
| 348 |
+
try:
|
| 349 |
+
with open("index.html", "r", encoding="utf-8") as f:
|
| 350 |
+
return f.read()
|
| 351 |
+
except FileNotFoundError:
|
| 352 |
+
return JSONResponse({
|
| 353 |
+
"status": "healthy",
|
| 354 |
+
"service": "Dr. HealBot API",
|
| 355 |
+
"version": "1.0.0",
|
| 356 |
+
"endpoints": {
|
| 357 |
+
"chat": "/chat",
|
| 358 |
+
"tts": "/tts",
|
| 359 |
+
"stt": "/stt",
|
| 360 |
+
"patient_data": "/patient-data/{user_id}",
|
| 361 |
+
"chat_history": "/chat-history/{user_id}",
|
| 362 |
+
"patient_summary": "/patient-summary/{user_id}"
|
| 363 |
+
}
|
| 364 |
+
})
|
| 365 |
+
|
| 366 |
+
@app.get("/ping")
|
| 367 |
+
async def ping():
|
| 368 |
+
return {"message": "pong"}
|
| 369 |
+
|
| 370 |
+
# ==================== CHAT ENDPOINT ====================
|
| 371 |
+
@app.post("/chat")
|
| 372 |
+
async def chat(request: ChatRequest):
|
| 373 |
+
"""
|
| 374 |
+
Chat endpoint that:
|
| 375 |
+
- Loads patient data and chat history
|
| 376 |
+
- Updates patient data if new symptoms are reported
|
| 377 |
+
- Sends patient summary + chat history + current message to Gemini
|
| 378 |
+
- Returns structured, history-aware medical response
|
| 379 |
+
"""
|
| 380 |
+
try:
|
| 381 |
+
user_id = request.user_id
|
| 382 |
+
user_message = request.message.strip()
|
| 383 |
+
|
| 384 |
+
# Load patient data & chat history
|
| 385 |
+
patient_data = load_patient_data(user_id) or {}
|
| 386 |
+
chat_history = load_chat_history(user_id)
|
| 387 |
+
|
| 388 |
+
# Update patient data with new symptom info
|
| 389 |
+
if "new_symptoms" not in patient_data:
|
| 390 |
+
patient_data["new_symptoms"] = []
|
| 391 |
+
|
| 392 |
+
# Simple heuristic: if message contains key symptoms, store it
|
| 393 |
+
symptom_keywords = ["fever", "cough", "headache", "ache", "pain", "rash", "vomit", "nausea"]
|
| 394 |
+
if any(word in user_message.lower() for word in symptom_keywords):
|
| 395 |
+
patient_data["new_symptoms"].append(user_message)
|
| 396 |
+
save_patient_data(user_id, patient_data)
|
| 397 |
+
|
| 398 |
+
# Generate patient summary
|
| 399 |
+
persistent_summary = generate_patient_summary(patient_data) if patient_data else "No patient history available."
|
| 400 |
+
|
| 401 |
+
# Prepare messages for Gemini (convert to single prompt format)
|
| 402 |
+
system_context = f"""
|
| 403 |
+
{DOCTOR_SYSTEM_PROMPT}
|
| 404 |
+
|
| 405 |
+
You MUST always consider the following patient medical data when responding:
|
| 406 |
+
|
| 407 |
+
{persistent_summary}
|
| 408 |
+
|
| 409 |
+
Instructions:
|
| 410 |
+
1. **Conversational Stage**:
|
| 411 |
+
- Start by acknowledging the patient's symptoms warmly.
|
| 412 |
+
- Ask **only one question at a time** to clarify their condition.
|
| 413 |
+
- Wait for the patient's answer before asking the next question.
|
| 414 |
+
- Limit clarifying questions to **3–4 total**, but ask them sequentially, not all at once.
|
| 415 |
+
- Example:
|
| 416 |
+
- "I'm sorry you're feeling unwell. How long have you had this fever?"
|
| 417 |
+
- Wait for response, then: "Are you experiencing any chills or body aches?"
|
| 418 |
+
- And so on.
|
| 419 |
+
2. **Structured Guidance Stage**:
|
| 420 |
+
- Only after 3–4 clarifying questions, provide the structured advice in the FINAL RESPONSE FORMAT.
|
| 421 |
+
|
| 422 |
+
- Always factor in patient history (conditions, medications, allergies, labs).
|
| 423 |
+
- Keep tone warm, empathetic, professional.
|
| 424 |
+
- Never give definitive diagnoses; always use soft language.
|
| 425 |
+
"""
|
| 426 |
+
|
| 427 |
+
# Build conversation prompt
|
| 428 |
+
conversation_prompt = system_context + "\n\n=== CONVERSATION HISTORY ===\n"
|
| 429 |
+
|
| 430 |
+
# Add previous chat history
|
| 431 |
+
for msg in chat_history:
|
| 432 |
+
role = "Patient" if msg["role"] == "user" else "Dr. HealBot"
|
| 433 |
+
conversation_prompt += f"\n{role}: {msg['content']}\n"
|
| 434 |
+
|
| 435 |
+
# Add current user message
|
| 436 |
+
conversation_prompt += f"\nPatient: {user_message}\n\nDr. HealBot:"
|
| 437 |
+
|
| 438 |
+
# Call Gemini API
|
| 439 |
+
model = genai.GenerativeModel('gemini-2.5-flash')
|
| 440 |
+
response = model.generate_content(
|
| 441 |
+
conversation_prompt,
|
| 442 |
+
generation_config=genai.types.GenerationConfig(
|
| 443 |
+
temperature=0.7,
|
| 444 |
+
max_output_tokens=1024,
|
| 445 |
+
)
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
reply_text = response.text.strip()
|
| 449 |
+
|
| 450 |
+
# Update chat history
|
| 451 |
+
chat_history.append({"role": "user", "content": user_message})
|
| 452 |
+
chat_history.append({"role": "assistant", "content": reply_text})
|
| 453 |
+
save_chat_history(user_id, chat_history)
|
| 454 |
+
|
| 455 |
+
return JSONResponse({
|
| 456 |
+
"reply": reply_text,
|
| 457 |
+
"user_id": user_id,
|
| 458 |
+
"message_count": len(chat_history)
|
| 459 |
+
})
|
| 460 |
+
|
| 461 |
+
except Exception as e:
|
| 462 |
+
print(f"Error in /chat: {str(e)}")
|
| 463 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 464 |
+
|
| 465 |
+
# ==================== CHAT HISTORY ENDPOINTS ====================
|
| 466 |
+
@app.get("/chat-history/{user_id}")
|
| 467 |
+
async def get_chat_history(user_id: str):
|
| 468 |
+
"""Get chat history for a user"""
|
| 469 |
+
try:
|
| 470 |
+
history = load_chat_history(user_id)
|
| 471 |
+
return JSONResponse({
|
| 472 |
+
"user_id": user_id,
|
| 473 |
+
"chat_history": history,
|
| 474 |
+
"message_count": len(history)
|
| 475 |
+
})
|
| 476 |
+
except Exception as e:
|
| 477 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 478 |
+
|
| 479 |
+
@app.delete("/chat-history/{user_id}")
|
| 480 |
+
async def clear_chat_history(user_id: str):
|
| 481 |
+
"""Clear chat history for a user"""
|
| 482 |
+
try:
|
| 483 |
+
delete_chat_history(user_id)
|
| 484 |
+
return JSONResponse({"message": "Chat history cleared", "user_id": user_id})
|
| 485 |
+
except Exception as e:
|
| 486 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 487 |
+
|
| 488 |
+
# ==================== PATIENT DATA ENDPOINTS ====================
|
| 489 |
+
@app.post("/patient-data/{user_id}")
|
| 490 |
+
async def save_patient(user_id: str, data: PatientData):
|
| 491 |
+
"""Save patient profile and lab test results"""
|
| 492 |
+
try:
|
| 493 |
+
patient_info = {
|
| 494 |
+
"name": data.name,
|
| 495 |
+
"patient_profile": data.patient_profile,
|
| 496 |
+
"lab_test_results": data.lab_test_results,
|
| 497 |
+
"last_updated": datetime.now().isoformat()
|
| 498 |
+
}
|
| 499 |
+
save_patient_data(user_id, patient_info)
|
| 500 |
+
return JSONResponse({
|
| 501 |
+
"message": "Patient data saved successfully",
|
| 502 |
+
"user_id": user_id
|
| 503 |
+
})
|
| 504 |
+
except Exception as e:
|
| 505 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 506 |
+
|
| 507 |
+
@app.get("/patient-data/{user_id}")
|
| 508 |
+
async def get_patient(user_id: str):
|
| 509 |
+
"""Get patient data"""
|
| 510 |
+
try:
|
| 511 |
+
data = load_patient_data(user_id)
|
| 512 |
+
if data:
|
| 513 |
+
return JSONResponse(data)
|
| 514 |
+
return JSONResponse({"message": "No patient data found"}, status_code=404)
|
| 515 |
+
except Exception as e:
|
| 516 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 517 |
+
|
| 518 |
+
@app.get("/patient-summary/{user_id}")
|
| 519 |
+
async def get_patient_summary(user_id: str, format: str = "markdown"):
|
| 520 |
+
"""
|
| 521 |
+
Get formatted summary of patient's medical profile and lab results.
|
| 522 |
+
Supports Markdown (default) or HTML output.
|
| 523 |
+
"""
|
| 524 |
+
try:
|
| 525 |
+
data = load_patient_data(user_id)
|
| 526 |
+
if not data:
|
| 527 |
+
return JSONResponse({"summary": "No patient data available"})
|
| 528 |
+
|
| 529 |
+
if format.lower() == "html":
|
| 530 |
+
summary = generate_patient_summary_html(data)
|
| 531 |
+
else:
|
| 532 |
+
summary = generate_patient_summary(data)
|
| 533 |
+
|
| 534 |
+
return JSONResponse({"summary": summary, "raw_data": data})
|
| 535 |
+
except Exception as e:
|
| 536 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
# ==================== TTS ENDPOINT ====================
|
| 540 |
+
@app.post("/tts")
|
| 541 |
+
async def text_to_speech(req: TTSRequest):
|
| 542 |
+
try:
|
| 543 |
+
# Remove emojis from the input text
|
| 544 |
+
clean_text = remove_emojis(req.text)
|
| 545 |
+
|
| 546 |
+
tmp_mp3 = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
|
| 547 |
+
tts = gTTS(text=clean_text, lang=req.language_code)
|
| 548 |
+
tts.save(tmp_mp3.name)
|
| 549 |
+
|
| 550 |
+
tmp_wav = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
|
| 551 |
+
subprocess.run(
|
| 552 |
+
["ffmpeg", "-y", "-i", tmp_mp3.name, "-ar", "44100", "-ac", "2", tmp_wav.name],
|
| 553 |
+
stdout=subprocess.DEVNULL,
|
| 554 |
+
stderr=subprocess.DEVNULL
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
# Delete temporary mp3
|
| 558 |
+
os.remove(tmp_mp3.name)
|
| 559 |
+
|
| 560 |
+
return FileResponse(tmp_wav.name, media_type="audio/wav", filename="speech.wav")
|
| 561 |
+
except Exception as e:
|
| 562 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
# ==================== STT ENDPOINT ====================
|
| 566 |
+
# Initialize speech recognizer
|
| 567 |
+
recognizer = sr.Recognizer()
|
| 568 |
+
|
| 569 |
+
@app.post("/stt")
|
| 570 |
+
async def speech_to_text(file: UploadFile = File(...)):
|
| 571 |
+
try:
|
| 572 |
+
# Save uploaded file temporarily
|
| 573 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
| 574 |
+
tmp.write(await file.read())
|
| 575 |
+
tmp_path = tmp.name
|
| 576 |
+
|
| 577 |
+
# Use speech_recognition library with Google's free API
|
| 578 |
+
with sr.AudioFile(tmp_path) as source:
|
| 579 |
+
audio_data = recognizer.record(source)
|
| 580 |
+
# Use Google Speech Recognition (free, no API key needed)
|
| 581 |
+
transcript = recognizer.recognize_google(audio_data)
|
| 582 |
+
|
| 583 |
+
# Clean up temp file
|
| 584 |
+
os.remove(tmp_path)
|
| 585 |
+
|
| 586 |
+
return JSONResponse({"transcript": transcript})
|
| 587 |
+
except sr.UnknownValueError:
|
| 588 |
+
raise HTTPException(status_code=400, detail="Could not understand audio")
|
| 589 |
+
except sr.RequestError as e:
|
| 590 |
+
raise HTTPException(status_code=500, detail=f"Speech recognition service error: {str(e)}")
|
| 591 |
+
except Exception as e:
|
| 592 |
+
print(f"Error in STT: {str(e)}")
|
| 593 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 594 |
+
|