Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,81 +14,74 @@ from reportlab.pdfgen import canvas
|
|
| 14 |
from reportlab.lib import colors
|
| 15 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 16 |
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
| 17 |
-
import traceback # Import traceback for better error logging
|
| 18 |
|
| 19 |
app = Flask(__name__)
|
| 20 |
|
| 21 |
# Use a strong, unique secret key
|
| 22 |
app.secret_key = os.getenv('SECRET_KEY', '688ed745a74bdd7ac238f5b50f4104fb87d6774b8b0a4e06e7e18ac5ed0fa31c') # CHANGE THIS IN PRODUCTION
|
| 23 |
|
| 24 |
-
# Configure upload folder (not strictly used for persistent storage in this demo)
|
| 25 |
upload_base = os.getenv('UPLOAD_DIR', './uploads')
|
| 26 |
upload_folder = os.path.join(upload_base, 'pdfs') # Changed folder name for clarity
|
| 27 |
|
| 28 |
app.config['UPLOAD_FOLDER'] = upload_folder
|
| 29 |
-
os.makedirs(upload_folder, exist_ok=True)
|
| 30 |
|
| 31 |
# Twilio Configuration
|
| 32 |
# IMPORTANT: Use environment variables for sensitive information like SID and Auth Token
|
| 33 |
ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'AC490e071f8d01bf0df2f03d086c788d87')
|
| 34 |
AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '224b23b950ad5a4052aba15893fdf083')
|
| 35 |
-
TWILIO_FROM = os.getenv('TWILIO_FROM_NUMBER', 'whatsapp:+14155238886')
|
| 36 |
TWILIO_TO = os.getenv('TWILIO_TO_NUMBER', 'whatsapp:+917559355282') # Hardcoded number as requested
|
| 37 |
|
| 38 |
# Initialize Twilio client
|
| 39 |
-
twilio_client = None
|
| 40 |
try:
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
# Optional: Verify account SID is valid (can raise error if not)
|
| 44 |
-
# twilio_client.api.account.fetch()
|
| 45 |
-
print("Twilio client initialized successfully.")
|
| 46 |
-
else:
|
| 47 |
-
print("TWILIO_ACCOUNT_SID or TWILIO_AUTH_TOKEN not set. WhatsApp sending disabled.")
|
| 48 |
except Exception as e:
|
| 49 |
print(f"Error initializing Twilio client: {e}")
|
| 50 |
-
|
| 51 |
-
twilio_client = None # Ensure client is None if initialization fails
|
| 52 |
-
|
| 53 |
|
| 54 |
# Gemini API Configuration
|
| 55 |
# IMPORTANT: Use environment variables for sensitive information like API keys
|
| 56 |
-
GENAI_API_KEY = os.getenv('GENAI_API_KEY', "AIzaSyD54ejbjVIVa-F3aD_Urnp8m1EFLUGR__I") # CHANGE THIS KEY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
model = None
|
| 58 |
-
if
|
| 59 |
try:
|
| 60 |
-
genai.configure(api_key=GENAI_API_KEY)
|
| 61 |
-
# Simple test to check connectivity (optional, can slow down startup)
|
| 62 |
-
# list(genai.list_models())
|
| 63 |
-
print("Gemini API configured successfully.")
|
| 64 |
-
|
| 65 |
-
generation_config = {
|
| 66 |
-
"temperature": 0.8,
|
| 67 |
-
"top_p": 0.9,
|
| 68 |
-
"top_k": 30,
|
| 69 |
-
"max_output_tokens": 4096,
|
| 70 |
-
}
|
| 71 |
-
# Using a recommended stable model alias
|
| 72 |
model = genai.GenerativeModel(
|
| 73 |
-
model_name="gemini-1.5-flash-latest",
|
| 74 |
generation_config=generation_config,
|
| 75 |
)
|
| 76 |
-
#
|
| 77 |
# model.generate_content("Hello")
|
| 78 |
print(f"Using Gemini model: {model.model_name}")
|
| 79 |
-
|
| 80 |
except Exception as e:
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
model = None # Ensure model is None if configuration or initialization fails
|
| 84 |
-
else:
|
| 85 |
-
print("GENAI_API_KEY not set. AI generation disabled.")
|
| 86 |
|
| 87 |
|
| 88 |
-
# Patient database (in-memory for
|
| 89 |
-
#
|
| 90 |
patients_db = {}
|
| 91 |
-
print("Patient database initialized (in-memory). Data will be lost on server restart.")
|
| 92 |
|
| 93 |
def extract_text_from_pdf(pdf_file):
|
| 94 |
"""Extract text from uploaded PDF file"""
|
|
@@ -104,37 +97,38 @@ def extract_text_from_pdf(pdf_file):
|
|
| 104 |
return text.strip()
|
| 105 |
except Exception as e:
|
| 106 |
print(f"Error extracting PDF text: {e}")
|
| 107 |
-
traceback.print_exc()
|
| 108 |
return ""
|
| 109 |
|
| 110 |
def extract_care_plan_format(pdf_text):
|
| 111 |
"""Extract a general format template from PDF text by identifying common section headers."""
|
| 112 |
if not pdf_text:
|
| 113 |
-
print("No PDF text provided for format extraction.")
|
| 114 |
return None
|
| 115 |
|
| 116 |
-
#
|
| 117 |
-
#
|
| 118 |
-
# followed by a colon,
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
if not
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
return None # No recognizable format found
|
| 125 |
|
| 126 |
-
# Use a set to get unique headers, clean them up
|
| 127 |
-
cleaned_headers = sorted(list(set([h.strip(' :.\n') for h in potential_headers if h.strip()])))
|
| 128 |
-
|
| 129 |
-
if not cleaned_headers:
|
| 130 |
-
print("Cleaned headers list is empty.")
|
| 131 |
-
return None
|
| 132 |
-
|
| 133 |
format_template = ""
|
| 134 |
-
for
|
| 135 |
-
format_template += f"{
|
| 136 |
-
print(f"Extracted
|
| 137 |
-
return format_template
|
| 138 |
|
| 139 |
|
| 140 |
def determine_patient_status(original_plan, updated_plan, feedback):
|
|
@@ -145,75 +139,69 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 145 |
updated_plan_lower = updated_plan.lower()
|
| 146 |
|
| 147 |
# Comprehensive keywords indicating condition worsening or emergency
|
| 148 |
-
# Added more specific medical terms
|
| 149 |
emergency_keywords = [
|
| 150 |
"severe chest pain", "heart attack", "sudden shortness of breath",
|
| 151 |
-
"difficulty breathing", "
|
| 152 |
-
"
|
| 153 |
-
"
|
| 154 |
-
"
|
| 155 |
-
"
|
| 156 |
-
"
|
| 157 |
-
"
|
| 158 |
-
"
|
| 159 |
-
"
|
| 160 |
-
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion", "status epilepticus",
|
| 161 |
-
"not breathing", "apnea", "blue lips", "cyanosis", "blue face", "cardiac arrest", "no pulse",
|
| 162 |
-
"high fever", "fever over 104f", "signs of shock", "low blood pressure", "rapid heart rate",
|
| 163 |
-
"severe dehydration", "unable to keep fluids down", "acute change", "rapidly worsening",
|
| 164 |
-
"unstable vitals", "chest pressure", "radiating pain", "vision loss", "sudden vision change"
|
| 165 |
]
|
| 166 |
|
| 167 |
deteriorating_keywords = [
|
| 168 |
-
"worsening", "increased pain", "
|
| 169 |
-
"elevated", "higher", "
|
| 170 |
-
"decline", "decreased function", "less able", "more difficult", "worse",
|
| 171 |
-
"
|
| 172 |
-
"
|
| 173 |
-
"
|
| 174 |
-
"
|
| 175 |
-
"
|
| 176 |
-
"swelling increasing", "redness spreading", "discharge increasing", "uncontrolled", "lab values changing negatively"
|
| 177 |
]
|
| 178 |
|
| 179 |
improvement_keywords = [
|
| 180 |
-
"improving", "better", "
|
| 181 |
-
"
|
| 182 |
-
"well-controlled", "
|
| 183 |
-
"improved", "enhancement", "advancement", "resolving", "resolved", "
|
| 184 |
-
"normalized", "normal range", "responding
|
| 185 |
-
"
|
| 186 |
-
"
|
| 187 |
-
"more energy", "easier to breathe"
|
| 188 |
]
|
| 189 |
|
| 190 |
-
# Check for emergency keywords first (highest priority)
|
| 191 |
-
# Prioritize feedback first, then the generated plan text
|
| 192 |
if any(keyword in feedback_lower for keyword in emergency_keywords):
|
| 193 |
-
print("Emergency keywords found in feedback.")
|
| 194 |
return "emergency"
|
|
|
|
| 195 |
if any(keyword in updated_plan_lower for keyword in emergency_keywords):
|
| 196 |
-
print("Emergency keywords found in updated plan.")
|
| 197 |
return "emergency"
|
| 198 |
|
| 199 |
# Check for deteriorating keywords
|
| 200 |
if any(keyword in feedback_lower for keyword in deteriorating_keywords):
|
| 201 |
-
print("Deteriorating keywords found in feedback.")
|
| 202 |
return "deteriorating"
|
|
|
|
| 203 |
if any(keyword in updated_plan_lower for keyword in deteriorating_keywords):
|
| 204 |
-
print("Deteriorating keywords found in updated plan.")
|
| 205 |
return "deteriorating"
|
| 206 |
|
| 207 |
# Check for improvement keywords
|
| 208 |
if any(keyword in feedback_lower for keyword in improvement_keywords):
|
| 209 |
-
print("Improvement keywords found in feedback.")
|
| 210 |
return "improving"
|
|
|
|
| 211 |
if any(keyword in updated_plan_lower for keyword in improvement_keywords):
|
| 212 |
-
print("Improvement keywords found in updated plan.")
|
| 213 |
return "improving"
|
| 214 |
|
| 215 |
-
#
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
return "stable"
|
| 218 |
|
| 219 |
|
|
@@ -235,8 +223,7 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 235 |
fontSize=22, # Slightly larger title
|
| 236 |
alignment=1, # Center alignment
|
| 237 |
spaceAfter=25, # More space after title
|
| 238 |
-
textColor=colors.HexColor("#4e73df")
|
| 239 |
-
fontName='Helvetica-Bold'
|
| 240 |
)
|
| 241 |
|
| 242 |
heading_style = ParagraphStyle(
|
|
@@ -245,8 +232,7 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 245 |
fontSize=15, # Slightly larger headings
|
| 246 |
spaceAfter=8, # Less space after heading for tighter look
|
| 247 |
spaceBefore=18, # More space before heading
|
| 248 |
-
textColor=colors.HexColor("#1cc88a")
|
| 249 |
-
fontName='Helvetica-Bold'
|
| 250 |
)
|
| 251 |
|
| 252 |
normal_style = ParagraphStyle(
|
|
@@ -255,8 +241,7 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 255 |
fontSize=11,
|
| 256 |
spaceAfter=6, # More space after paragraphs
|
| 257 |
leading=14,
|
| 258 |
-
textColor=colors.HexColor("#5a5c69")
|
| 259 |
-
fontName='Helvetica'
|
| 260 |
)
|
| 261 |
|
| 262 |
bullet_style = ParagraphStyle(
|
|
@@ -267,9 +252,7 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 267 |
leftIndent=20,
|
| 268 |
leading=14,
|
| 269 |
bulletIndent=10, # Space between bullet and text
|
| 270 |
-
textColor=colors.HexColor("#5a5c69")
|
| 271 |
-
fontName='Helvetica',
|
| 272 |
-
bulletText='•' # Explicitly set bullet character
|
| 273 |
)
|
| 274 |
|
| 275 |
# Status color mapping
|
|
@@ -277,8 +260,7 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 277 |
'emergency': colors.HexColor("#e74a3b"), # Danger color
|
| 278 |
'deteriorating': colors.HexColor("#f6c23e"), # Warning color
|
| 279 |
'improving': colors.HexColor("#1cc88a"), # Success color
|
| 280 |
-
'stable': colors.HexColor("#36b9cc")
|
| 281 |
-
'unknown': colors.HexColor("#5a5c69") # Dark grey color
|
| 282 |
}
|
| 283 |
|
| 284 |
status_text_styles = {
|
|
@@ -286,7 +268,7 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 286 |
'deteriorating': ParagraphStyle('StatusDeteriorating', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['deteriorating'], alignment=1, fontName='Helvetica-Bold'),
|
| 287 |
'improving': ParagraphStyle('StatusImproving', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['improving'], alignment=1, fontName='Helvetica-Bold'),
|
| 288 |
'stable': ParagraphStyle('StatusStable', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['stable'], alignment=1, fontName='Helvetica-Bold'),
|
| 289 |
-
'unknown': ParagraphStyle('StatusUnknown', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=
|
| 290 |
}
|
| 291 |
|
| 292 |
|
|
@@ -339,58 +321,94 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 339 |
story.append(Paragraph("Care Plan Details:", heading_style))
|
| 340 |
story.append(Spacer(1, 10))
|
| 341 |
|
| 342 |
-
# Process care plan text by lines
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
if not
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
#
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
#
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
else:
|
| 375 |
-
#
|
| 376 |
-
|
| 377 |
-
if content_text:
|
| 378 |
-
# Also check if this block contains bullets, format accordingly
|
| 379 |
-
bullet_points = re.findall(r'^\s*[-•*]\s*(.*?)$', content_text, re.MULTILINE)
|
| 380 |
-
if bullet_points:
|
| 381 |
-
for point in bullet_points:
|
| 382 |
-
if point.strip():
|
| 383 |
-
story.append(Paragraph(f"• {point.strip()}", bullet_style))
|
| 384 |
-
else:
|
| 385 |
-
story.append(Paragraph(content_text, normal_style))
|
| 386 |
-
|
| 387 |
-
story.append(Spacer(1, 12)) # Space between blocks
|
| 388 |
|
|
|
|
|
|
|
| 389 |
|
| 390 |
# Add footer or timestamp if desired
|
| 391 |
story.append(Spacer(1, 20))
|
| 392 |
-
footer_style = ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, alignment=1, textColor=colors.grey
|
| 393 |
-
story.append(Paragraph(f"Generated by Patient Care Management System on {datetime.now().strftime('%Y-%m-%d
|
| 394 |
|
| 395 |
|
| 396 |
# Build the PDF
|
|
@@ -404,9 +422,6 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 404 |
if not twilio_client:
|
| 405 |
print("Twilio client not initialized. Cannot send WhatsApp message.")
|
| 406 |
return False
|
| 407 |
-
if not TWILIO_FROM or not TWILIO_TO:
|
| 408 |
-
print("TWILIO_FROM or TWILIO_TO number not set. Cannot send WhatsApp message.")
|
| 409 |
-
return False
|
| 410 |
|
| 411 |
try:
|
| 412 |
# Status emoji mapping
|
|
@@ -414,8 +429,7 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 414 |
'emergency': "🚨 EMERGENCY",
|
| 415 |
'deteriorating': "⚠️ HIGH RISK",
|
| 416 |
'improving': "✅ IMPROVING",
|
| 417 |
-
'stable': "🟦 STABLE"
|
| 418 |
-
'unknown': "❓ STATUS UNKNOWN"
|
| 419 |
}
|
| 420 |
|
| 421 |
# Format message for WhatsApp including patient details and the full plan text
|
|
@@ -423,27 +437,20 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 423 |
message += f"*Patient Name:* {patient_info.get('name', 'N/A')}\n"
|
| 424 |
message += f"*Age:* {patient_info.get('age', 'N/A')}\n"
|
| 425 |
message += f"*Gender:* {patient_info.get('gender', 'N/A')}\n"
|
| 426 |
-
message += f"*Status:* {status_emoji.get(status, '
|
| 427 |
-
message += f"*Care Plan Details:*\n
|
| 428 |
|
| 429 |
-
# Send WhatsApp message using
|
| 430 |
message = twilio_client.messages.create(
|
| 431 |
from_=TWILIO_FROM,
|
| 432 |
body=message,
|
| 433 |
to=TWILIO_TO
|
| 434 |
)
|
| 435 |
-
print(f"WhatsApp message sent
|
| 436 |
return True
|
| 437 |
except Exception as e:
|
| 438 |
print(f"Error sending WhatsApp message: {e}")
|
| 439 |
-
|
| 440 |
-
# Log specific Twilio error details if available
|
| 441 |
-
if hasattr(e, 'status'):
|
| 442 |
-
print(f"Twilio API Error Status: {e.status}")
|
| 443 |
-
if hasattr(e, 'code'):
|
| 444 |
-
print(f"Twilio API Error Code: {e.code}")
|
| 445 |
-
if hasattr(e, 'msg'):
|
| 446 |
-
print(f"Twilio API Error Message: {e.msg}")
|
| 447 |
return False
|
| 448 |
|
| 449 |
|
|
@@ -463,50 +470,44 @@ def switch_role():
|
|
| 463 |
role = request.form.get('role')
|
| 464 |
if role in ['patient', 'doctor']:
|
| 465 |
session['role'] = role
|
| 466 |
-
|
| 467 |
-
if role == 'doctor':
|
| 468 |
-
return jsonify({'success': True, 'role': role, 'redirect': url_for('doctor_dashboard')})
|
| 469 |
-
else: # patient
|
| 470 |
-
return jsonify({'success': True, 'role': role, 'redirect': url_for('index')})
|
| 471 |
-
|
| 472 |
return jsonify({'success': False, 'error': 'Invalid role'}), 400
|
| 473 |
|
| 474 |
|
| 475 |
@app.route('/doctor_dashboard')
|
| 476 |
def doctor_dashboard():
|
| 477 |
# This route will render the dashboard template
|
| 478 |
-
# The patient list
|
| 479 |
return render_template('doctor_dashboard.html')
|
| 480 |
|
| 481 |
|
| 482 |
-
# This route is not strictly needed
|
| 483 |
-
#
|
| 484 |
-
# Keeping it as a redirect target for potentially old links or direct access attempts
|
| 485 |
@app.route('/patient_details/<patient_id>')
|
| 486 |
-
def
|
| 487 |
-
print(f"
|
| 488 |
-
|
| 489 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
|
| 492 |
@app.route('/submit_feedback', methods=['POST'])
|
| 493 |
def submit_feedback():
|
| 494 |
if not model:
|
| 495 |
-
|
| 496 |
-
return jsonify({'success': False, 'error': 'AI model not initialized. Please check API key and configuration.'}), 500
|
| 497 |
|
| 498 |
try:
|
| 499 |
# Get patient information from form
|
| 500 |
-
name = request.form.get('name', 'Unnamed Patient')
|
| 501 |
-
age = request.form.get('age', 'N/A')
|
| 502 |
-
gender = request.form.get('gender', 'N/A')
|
| 503 |
-
feedback = request.form.get('feedback', '')
|
| 504 |
-
|
| 505 |
-
if not feedback:
|
| 506 |
-
return jsonify({'success': False, 'error': 'Feedback is required.'}), 400
|
| 507 |
-
if not name or not age or gender == 'N/A':
|
| 508 |
-
return jsonify({'success': False, 'error': 'Patient Name, Age, and Gender are required.'}), 400
|
| 509 |
-
|
| 510 |
|
| 511 |
care_plan_text = ""
|
| 512 |
care_plan_format = None
|
|
@@ -515,18 +516,13 @@ def submit_feedback():
|
|
| 515 |
if 'care_plan_pdf' in request.files:
|
| 516 |
pdf_file = request.files['care_plan_pdf']
|
| 517 |
if pdf_file and pdf_file.filename != '':
|
| 518 |
-
|
| 519 |
care_plan_text = extract_text_from_pdf(pdf_file)
|
| 520 |
if care_plan_text:
|
| 521 |
care_plan_format = extract_care_plan_format(care_plan_text)
|
| 522 |
-
print(f"Extracted text length: {len(care_plan_text)
|
| 523 |
-
if not care_plan_text:
|
| 524 |
-
print("PDF text extraction failed.")
|
| 525 |
-
# Optionally return an error or proceed without the PDF content
|
| 526 |
-
# For now, we'll proceed, treating it like no PDF was uploaded.
|
| 527 |
|
| 528 |
-
|
| 529 |
-
# If no format is found in the PDF, use a robust default format
|
| 530 |
if not care_plan_format or not care_plan_format.strip():
|
| 531 |
print("Using default care plan format.")
|
| 532 |
care_plan_format = """
|
|
@@ -549,23 +545,23 @@ Night:
|
|
| 549 |
- [Night activities/medications/sleep instructions]
|
| 550 |
|
| 551 |
MEDICATIONS:
|
| 552 |
-
- [List of medications, dosage, frequency, and time
|
| 553 |
|
| 554 |
DIET AND HYDRATION:
|
| 555 |
- [Dietary recommendations and hydration goals]
|
| 556 |
|
| 557 |
PHYSICAL ACTIVITY/EXERCISE:
|
| 558 |
-
- [Recommended physical activities, duration, frequency
|
| 559 |
|
| 560 |
SYMPTOM MANAGEMENT:
|
| 561 |
- [Instructions for managing specific symptoms, including what to do if they worsen]
|
| 562 |
|
| 563 |
RED FLAGS / WHEN TO SEEK HELP:
|
| 564 |
-
- [Clear instructions on symptoms requiring
|
| 565 |
-
- [Instructions on symptoms requiring
|
| 566 |
|
| 567 |
ADDITIONAL RECOMMENDATIONS:
|
| 568 |
-
- [Other relevant advice, e.g., rest, monitoring, specific precautions
|
| 569 |
|
| 570 |
FOLLOW-UP:
|
| 571 |
- [Details about next appointment or when to contact healthcare provider]
|
|
@@ -573,14 +569,29 @@ FOLLOW-UP:
|
|
| 573 |
else:
|
| 574 |
print("Using extracted care plan format.")
|
| 575 |
|
| 576 |
-
#
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
|
| 579 |
generated_plan_text = ""
|
| 580 |
-
status =
|
| 581 |
|
| 582 |
-
if
|
| 583 |
-
print("Emergency
|
| 584 |
# If emergency symptoms are detected in feedback, generate emergency instructions
|
| 585 |
generated_plan_text = (
|
| 586 |
"PATIENT INFORMATION:\n"
|
|
@@ -588,26 +599,25 @@ FOLLOW-UP:
|
|
| 588 |
f"- Age: {age}\n"
|
| 589 |
f"- Gender: {gender}\n\n"
|
| 590 |
"ASSESSMENT:\n"
|
| 591 |
-
f"- Emergency symptoms reported
|
| 592 |
"EMERGENCY ACTION PLAN:\n"
|
| 593 |
-
"- Call emergency services immediately at 104/108/109/112 or your local emergency number
|
| 594 |
-
"- Do not delay seeking medical help
|
| 595 |
-
"- If conscious, try to remain calm
|
| 596 |
-
"- If patient is unconscious or not breathing, begin CPR if trained and instructed by emergency services.\n"
|
| 597 |
"- Do not eat or drink anything until evaluated by medical professionals.\n"
|
| 598 |
-
"-
|
| 599 |
"RED FLAGS / WHEN TO SEEK HELP:\n"
|
| 600 |
-
"- *Any* worsening of reported
|
| 601 |
"FOLLOW-UP:\n"
|
| 602 |
"- Immediate hospitalization or urgent medical evaluation is necessary.\n"
|
| 603 |
-
"- Inform your primary physician
|
| 604 |
-
"- Review and update
|
| 605 |
)
|
| 606 |
-
#
|
| 607 |
|
| 608 |
else:
|
| 609 |
-
|
| 610 |
-
#
|
| 611 |
prompt = f"""
|
| 612 |
You are a helpful assistant generating updated patient care plans.
|
| 613 |
Based on the patient's personal information, their current symptoms/feedback, and their previous care plan, create a NEW, comprehensive, and well-structured daily care plan.
|
|
@@ -620,41 +630,38 @@ Gender: {gender}
|
|
| 620 |
Patient Feedback/Symptoms Update:
|
| 621 |
{feedback}
|
| 622 |
|
| 623 |
-
Previous Care Plan Details (if available
|
| 624 |
-
{care_plan_text if care_plan_text else "No previous care plan provided.
|
| 625 |
|
| 626 |
Instructions:
|
| 627 |
-
1. Generate the updated care plan using
|
| 628 |
-
2. Be specific
|
| 629 |
-
3. Ensure the language is clear and easy to understand
|
| 630 |
-
4. Include realistic times for medications if applicable.
|
| 631 |
-
5. Highlight important instructions or warnings, especially regarding symptom management and when to seek help
|
| 632 |
-
6. Do NOT include any introductory or concluding sentences outside the plan
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
Care Plan Format Guideline (Adapt structure as needed, cover these topics):
|
| 636 |
{care_plan_format}
|
| 637 |
"""
|
| 638 |
print("Sending prompt to AI model...")
|
| 639 |
-
|
| 640 |
|
| 641 |
# Get response from Gemini AI
|
| 642 |
try:
|
| 643 |
response = model.generate_content(prompt)
|
| 644 |
generated_plan_text = response.text.strip() # Get the text and strip leading/trailing whitespace
|
| 645 |
print(f"AI Response received. Length: {len(generated_plan_text)}")
|
| 646 |
-
|
| 647 |
|
| 648 |
# Determine patient status based on AI's output and feedback
|
| 649 |
status = determine_patient_status(care_plan_text, generated_plan_text, feedback)
|
| 650 |
-
print(f"Determined status after AI generation: {status}")
|
| 651 |
|
| 652 |
except Exception as ai_error:
|
| 653 |
print(f"Error generating content from AI: {ai_error}")
|
| 654 |
-
traceback.print_exc()
|
| 655 |
return jsonify({
|
| 656 |
'success': False,
|
| 657 |
-
'error': f'Error generating care plan
|
| 658 |
}), 500
|
| 659 |
|
| 660 |
# --- Actions after plan generation (Emergency or AI) ---
|
|
@@ -670,9 +677,9 @@ Care Plan Format Guideline (Adapt structure as needed, cover these topics):
|
|
| 670 |
'original_plan': care_plan_text, # Store original text from PDF
|
| 671 |
'updated_plan': generated_plan_text, # Store the generated plan text
|
| 672 |
'status': status,
|
| 673 |
-
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 674 |
}
|
| 675 |
-
print(f"Patient {patient_id} added to in-memory DB with status: {status}. Current DB keys: {
|
| 676 |
|
| 677 |
|
| 678 |
# Generate PDF for downloading using the stored data
|
|
@@ -681,16 +688,11 @@ Care Plan Format Guideline (Adapt structure as needed, cover these topics):
|
|
| 681 |
'age': age,
|
| 682 |
'gender': gender
|
| 683 |
}
|
| 684 |
-
|
| 685 |
-
pdf_buffer = generate_care_plan_pdf(
|
| 686 |
-
patient_info_for_pdf,
|
| 687 |
-
generated_plan_text,
|
| 688 |
-
status
|
| 689 |
-
)
|
| 690 |
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
|
| 691 |
-
print("PDF generated and base64 encoded
|
| 692 |
|
| 693 |
-
# Send care plan via WhatsApp
|
| 694 |
patient_info_for_whatsapp = {
|
| 695 |
'name': name,
|
| 696 |
'age': age,
|
|
@@ -702,36 +704,34 @@ Care Plan Format Guideline (Adapt structure as needed, cover these topics):
|
|
| 702 |
return jsonify({
|
| 703 |
'success': True,
|
| 704 |
'updated_plan': generated_plan_text, # Send back the generated text
|
| 705 |
-
'pdf_data': pdf_base64,
|
| 706 |
'patient_id': patient_id, # Send the generated patient ID
|
| 707 |
'status': status,
|
| 708 |
'whatsapp_sent': whatsapp_sent
|
| 709 |
})
|
| 710 |
|
| 711 |
except Exception as e:
|
| 712 |
-
print(f"An unexpected error occurred
|
|
|
|
|
|
|
| 713 |
traceback.print_exc()
|
| 714 |
return jsonify({
|
| 715 |
'success': False,
|
| 716 |
-
'error': f'An unexpected
|
| 717 |
}), 500
|
| 718 |
|
| 719 |
|
| 720 |
@app.route('/download_pdf/<patient_id>')
|
| 721 |
def download_pdf(patient_id):
|
| 722 |
print(f"Download requested for patient ID: {patient_id}")
|
| 723 |
-
|
| 724 |
|
| 725 |
try:
|
| 726 |
patient = patients_db.get(patient_id)
|
| 727 |
|
| 728 |
if not patient:
|
| 729 |
print(f"Patient ID {patient_id} not found in patients_db for download.")
|
| 730 |
-
|
| 731 |
-
# For this demo, we just report it's not found in the current state
|
| 732 |
-
return "Patient data not found (it may have been cleared due to server restart). Please regenerate the plan.", 404
|
| 733 |
-
|
| 734 |
-
print(f"Found patient {patient_id} for download: {patient.get('name')}")
|
| 735 |
|
| 736 |
# Use the patient's stored information to regenerate the PDF content
|
| 737 |
patient_info_for_pdf = {
|
|
@@ -742,16 +742,15 @@ def download_pdf(patient_id):
|
|
| 742 |
# Use the already determined status and updated plan text
|
| 743 |
pdf_buffer = generate_care_plan_pdf(
|
| 744 |
patient_info_for_pdf,
|
| 745 |
-
patient
|
| 746 |
-
patient
|
| 747 |
)
|
| 748 |
|
| 749 |
pdf_buffer.seek(0) # Ensure buffer is at the start
|
| 750 |
|
| 751 |
# Sanitize patient name for filename
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
download_name = f"care_plan_{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
| 755 |
|
| 756 |
print(f"Serving PDF for {patient_id} as {download_name}")
|
| 757 |
return send_file(
|
|
@@ -762,6 +761,7 @@ def download_pdf(patient_id):
|
|
| 762 |
)
|
| 763 |
except Exception as e:
|
| 764 |
print(f"PDF Download Error for ID {patient_id}: {str(e)}")
|
|
|
|
| 765 |
traceback.print_exc()
|
| 766 |
return f"Error generating PDF: {str(e)}", 500
|
| 767 |
|
|
@@ -772,13 +772,7 @@ def get_emergency_notifications():
|
|
| 772 |
emergency_patients = [p for p in patients_db.values() if p.get('status') == 'emergency']
|
| 773 |
|
| 774 |
# Sort by timestamp, newest first
|
| 775 |
-
|
| 776 |
-
emergency_patients.sort(key=lambda x: datetime.strptime(x.get('timestamp', '1900-01-01 00:00:00'), "%Y-%m-%d %H:%M:%S"), reverse=True)
|
| 777 |
-
except ValueError:
|
| 778 |
-
print("Error sorting emergency patients by timestamp. Using unsorted list.")
|
| 779 |
-
# If timestamp format is unexpected, use unsorted list or fall back to another key
|
| 780 |
-
pass
|
| 781 |
-
|
| 782 |
|
| 783 |
notifications = []
|
| 784 |
for patient in emergency_patients:
|
|
@@ -801,21 +795,10 @@ def get_emergency_notifications():
|
|
| 801 |
@app.route('/get_patients')
|
| 802 |
def get_patients():
|
| 803 |
# Return all patients for the doctor dashboard
|
| 804 |
-
# Sort by
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
sorted_patients = sorted(patients_db.values(),
|
| 809 |
-
key=lambda x: (status_priority.get(x.get('status', 'unknown'), 4),
|
| 810 |
-
datetime.strptime(x.get('timestamp', '1900-01-01 00:00:00'), "%Y-%m-%d %H:%M:%S")),
|
| 811 |
-
reverse=True) # Reverse timestamp sort for newest first
|
| 812 |
-
except ValueError:
|
| 813 |
-
print("Error sorting patients by timestamp/status. Using unsorted list.")
|
| 814 |
-
sorted_patients = list(patients_db.values()) # Fallback to unsorted
|
| 815 |
-
|
| 816 |
-
# Reverse the status priority sort so 0 (Emergency) comes first
|
| 817 |
-
sorted_patients = sorted(sorted_patients, key=lambda x: status_priority.get(x.get('status', 'unknown'), 4))
|
| 818 |
-
|
| 819 |
|
| 820 |
return jsonify({
|
| 821 |
'success': True,
|
|
@@ -826,16 +809,15 @@ def get_patients():
|
|
| 826 |
@app.route('/get_patient/<patient_id>')
|
| 827 |
def get_patient(patient_id):
|
| 828 |
print(f"API request for patient ID: {patient_id}")
|
| 829 |
-
|
| 830 |
|
| 831 |
patient = patients_db.get(patient_id)
|
| 832 |
|
| 833 |
if not patient:
|
| 834 |
print(f"Patient ID {patient_id} not found in patients_db for API request.")
|
| 835 |
-
return jsonify({'success': False, 'error': 'Patient not found or data cleared
|
| 836 |
|
| 837 |
-
print(f"Found patient {patient_id}
|
| 838 |
-
# Return the full patient object
|
| 839 |
return jsonify({
|
| 840 |
'success': True,
|
| 841 |
'patient': patient
|
|
@@ -844,5 +826,4 @@ def get_patient(patient_id):
|
|
| 844 |
|
| 845 |
if __name__ == '__main__':
|
| 846 |
# Use a more robust development server like Waitress or Gunicorn in production
|
| 847 |
-
# For development, debug=True is fine
|
| 848 |
app.run(debug=True, port=5000) # Explicitly set port
|
|
|
|
| 14 |
from reportlab.lib import colors
|
| 15 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 16 |
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
|
|
|
| 17 |
|
| 18 |
app = Flask(__name__)
|
| 19 |
|
| 20 |
# Use a strong, unique secret key
|
| 21 |
app.secret_key = os.getenv('SECRET_KEY', '688ed745a74bdd7ac238f5b50f4104fb87d6774b8b0a4e06e7e18ac5ed0fa31c') # CHANGE THIS IN PRODUCTION
|
| 22 |
|
|
|
|
| 23 |
upload_base = os.getenv('UPLOAD_DIR', './uploads')
|
| 24 |
upload_folder = os.path.join(upload_base, 'pdfs') # Changed folder name for clarity
|
| 25 |
|
| 26 |
app.config['UPLOAD_FOLDER'] = upload_folder
|
| 27 |
+
os.makedirs(upload_folder, exist_ok=True)
|
| 28 |
|
| 29 |
# Twilio Configuration
|
| 30 |
# IMPORTANT: Use environment variables for sensitive information like SID and Auth Token
|
| 31 |
ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'AC490e071f8d01bf0df2f03d086c788d87')
|
| 32 |
AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '224b23b950ad5a4052aba15893fdf083')
|
| 33 |
+
TWILIO_FROM = os.getenv('TWILIO_FROM_NUMBER', 'whatsapp:+14155238886')
|
| 34 |
TWILIO_TO = os.getenv('TWILIO_TO_NUMBER', 'whatsapp:+917559355282') # Hardcoded number as requested
|
| 35 |
|
| 36 |
# Initialize Twilio client
|
|
|
|
| 37 |
try:
|
| 38 |
+
twilio_client = Client(ACCOUNT_SID, AUTH_TOKEN)
|
| 39 |
+
print("Twilio client initialized successfully.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
except Exception as e:
|
| 41 |
print(f"Error initializing Twilio client: {e}")
|
| 42 |
+
twilio_client = None # Handle cases where Twilio initialization fails
|
|
|
|
|
|
|
| 43 |
|
| 44 |
# Gemini API Configuration
|
| 45 |
# IMPORTANT: Use environment variables for sensitive information like API keys
|
| 46 |
+
GENAI_API_KEY = os.getenv('GENAI_API_KEY', "AIzaSyD54ejbjVIVa-F3aD_Urnp8m1EFLUGR__I") # CHANGE THIS KEY
|
| 47 |
+
try:
|
| 48 |
+
genai.configure(api_key=GENAI_API_KEY)
|
| 49 |
+
# Simple test to check connectivity
|
| 50 |
+
list(genai.list_models())
|
| 51 |
+
print("Gemini API configured successfully.")
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"Error configuring Gemini API: {e}")
|
| 54 |
+
genai = None # Handle cases where API configuration fails
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
generation_config = {
|
| 58 |
+
"temperature": 0.8, # Slightly reduced temp for more stable output
|
| 59 |
+
"top_p": 0.9, # Adjusted top_p
|
| 60 |
+
"top_k": 30, # Adjusted top_k
|
| 61 |
+
"max_output_tokens": 4096, # Reduced token limit slightly if needed, or increase if plans are long
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# Use a more capable model if available and affordable
|
| 65 |
+
# "gemini-1.5-pro" or "gemini-1.5-flash" are better choices than gemini-2.0-flash if 2.0 means an older preview
|
| 66 |
+
# Let's assume gemini-2.0-flash is a valid, accessible model alias
|
| 67 |
model = None
|
| 68 |
+
if genai:
|
| 69 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
model = genai.GenerativeModel(
|
| 71 |
+
model_name="gemini-1.5-flash-latest", # Recommended model alias
|
| 72 |
generation_config=generation_config,
|
| 73 |
)
|
| 74 |
+
# Test model generation
|
| 75 |
# model.generate_content("Hello")
|
| 76 |
print(f"Using Gemini model: {model.model_name}")
|
|
|
|
| 77 |
except Exception as e:
|
| 78 |
+
print(f"Error initializing Gemini model: {e}")
|
| 79 |
+
model = None # Handle cases where model initialization fails
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
|
| 82 |
+
# Patient database (in-memory for demo - WILL BE RESET ON SERVER RESTART)
|
| 83 |
+
# For persistence, replace this with a database (SQLite, PostgreSQL, etc.)
|
| 84 |
patients_db = {}
|
|
|
|
| 85 |
|
| 86 |
def extract_text_from_pdf(pdf_file):
|
| 87 |
"""Extract text from uploaded PDF file"""
|
|
|
|
| 97 |
return text.strip()
|
| 98 |
except Exception as e:
|
| 99 |
print(f"Error extracting PDF text: {e}")
|
|
|
|
| 100 |
return ""
|
| 101 |
|
| 102 |
def extract_care_plan_format(pdf_text):
|
| 103 |
"""Extract a general format template from PDF text by identifying common section headers."""
|
| 104 |
if not pdf_text:
|
|
|
|
| 105 |
return None
|
| 106 |
|
| 107 |
+
# Look for lines that seem like headers (e.g., capitalized words ending with :, followed by content)
|
| 108 |
+
# This regex is an attempt to capture common patterns but might not be perfect for all PDFs.
|
| 109 |
+
# It looks for uppercase words (potentially with spaces), followed by a colon, then captures subsequent lines until another similar pattern or end.
|
| 110 |
+
sections = re.findall(
|
| 111 |
+
r'^([A-Z][A-Z\s]*):(?:\s*\n)?((?:(?!\n[A-Z][A-Z\s]*:).*\n?)*)',
|
| 112 |
+
pdf_text,
|
| 113 |
+
re.MULTILINE | re.DOTALL
|
| 114 |
+
)
|
| 115 |
|
| 116 |
+
if not sections:
|
| 117 |
+
# Fallback: If strict section matching fails, try to find prominent lines as potential headers
|
| 118 |
+
potential_headers = re.findall(r'^[A-Z][A-Za-z\s,]*[.:]?$', pdf_text, re.MULTILINE)
|
| 119 |
+
if potential_headers:
|
| 120 |
+
# Just list the potential headers as a basic format guess
|
| 121 |
+
format_template = "\n".join([f"{header.strip()}:" for header in set(potential_headers) if header.strip()]) + "\n"
|
| 122 |
+
print(f"Extracted potential headers (fallback): {list(set(potential_headers))}")
|
| 123 |
+
return format_template if format_template.strip() else None
|
| 124 |
+
print("No sections or potential headers found.")
|
| 125 |
return None # No recognizable format found
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
format_template = ""
|
| 128 |
+
for section_title, _ in sections:
|
| 129 |
+
format_template += f"{section_title.strip()}:\n- [Details]\n" # Use a placeholder bullet structure
|
| 130 |
+
print(f"Extracted sections: {[s[0].strip() for s in sections]}")
|
| 131 |
+
return format_template if format_template.strip() else None
|
| 132 |
|
| 133 |
|
| 134 |
def determine_patient_status(original_plan, updated_plan, feedback):
|
|
|
|
| 139 |
updated_plan_lower = updated_plan.lower()
|
| 140 |
|
| 141 |
# Comprehensive keywords indicating condition worsening or emergency
|
|
|
|
| 142 |
emergency_keywords = [
|
| 143 |
"severe chest pain", "heart attack", "sudden shortness of breath",
|
| 144 |
+
"difficulty breathing", "loss of consciousness", "extreme pain",
|
| 145 |
+
"sudden weakness", "confusion", "slurred speech", "severe headache",
|
| 146 |
+
"severe abdominal pain", "persistent vomiting", "uncontrolled bleeding",
|
| 147 |
+
"severe allergic reaction", "anaphylaxis", "immediate medical attention",
|
| 148 |
+
"emergency", "call 911", "urgent care", "hospital", "critical", "ambulance",
|
| 149 |
+
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion",
|
| 150 |
+
"suffocating", "not breathing", "blue lips", "blue face", "cardiac arrest",
|
| 151 |
+
"high fever", "signs of shock", "severe dehydration", "acute change",
|
| 152 |
+
"unstable vitals", "rapidly worsening"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
]
|
| 154 |
|
| 155 |
deteriorating_keywords = [
|
| 156 |
+
"worsening", "increased pain", "not improving", "deteriorating",
|
| 157 |
+
"elevated", "higher", "more frequent", "concerning", "monitor closely",
|
| 158 |
+
"decline", "decreased function", "less able", "more difficult", "worse",
|
| 159 |
+
"getting worse", "aggravated", "intensified", "escalating", "degrading",
|
| 160 |
+
"weakening", "relapse", "recurrence", "regressing", "not responding",
|
| 161 |
+
"increased symptoms", "more severe", "progressing", "progressive", "complicated",
|
| 162 |
+
"adverse change", "unstable", "needs attention", "spike in", "significant increase",
|
| 163 |
+
"new symptoms", "trouble with", "reduced appetite", "difficulty sleeping"
|
|
|
|
| 164 |
]
|
| 165 |
|
| 166 |
improvement_keywords = [
|
| 167 |
+
"improving", "better", "reduced", "lower", "less pain", "increased function",
|
| 168 |
+
"healing", "recovery", "progress", "stable", "maintained", "consistent",
|
| 169 |
+
"well-controlled", "responsive", "good progress", "getting better", "positive",
|
| 170 |
+
"improved", "enhancement", "advancement", "resolving", "resolved", "recovering",
|
| 171 |
+
"normalized", "normal range", "responding well", "responding positively",
|
| 172 |
+
"effective treatment", "successful treatment", "managed well", "under control",
|
| 173 |
+
"symptoms decreased", "feeling stronger", "better sleep", "increased appetite"
|
|
|
|
| 174 |
]
|
| 175 |
|
| 176 |
+
# Check for emergency keywords first (highest priority), primarily in feedback as it's direct input
|
|
|
|
| 177 |
if any(keyword in feedback_lower for keyword in emergency_keywords):
|
|
|
|
| 178 |
return "emergency"
|
| 179 |
+
# Then check updated plan, as AI might interpret feedback and include emergency terms
|
| 180 |
if any(keyword in updated_plan_lower for keyword in emergency_keywords):
|
|
|
|
| 181 |
return "emergency"
|
| 182 |
|
| 183 |
# Check for deteriorating keywords
|
| 184 |
if any(keyword in feedback_lower for keyword in deteriorating_keywords):
|
|
|
|
| 185 |
return "deteriorating"
|
| 186 |
+
# Check updated plan for deteriorating indicators
|
| 187 |
if any(keyword in updated_plan_lower for keyword in deteriorating_keywords):
|
|
|
|
| 188 |
return "deteriorating"
|
| 189 |
|
| 190 |
# Check for improvement keywords
|
| 191 |
if any(keyword in feedback_lower for keyword in improvement_keywords):
|
|
|
|
| 192 |
return "improving"
|
| 193 |
+
# Check updated plan for improvement indicators
|
| 194 |
if any(keyword in updated_plan_lower for keyword in improvement_keywords):
|
|
|
|
| 195 |
return "improving"
|
| 196 |
|
| 197 |
+
# Compare original vs updated plan length/content as a weak signal (optional, can be removed if unreliable)
|
| 198 |
+
# if len(updated_plan) < len(original_plan) * 0.8 and any(k in updated_plan_lower for k in improvement_keywords):
|
| 199 |
+
# return "improving"
|
| 200 |
+
# if len(updated_plan) > len(original_plan) * 1.2 and any(k in updated_plan_lower for k in deteriorating_keywords):
|
| 201 |
+
# return "deteriorating"
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# Default to stable if no clear indicators of change are found
|
| 205 |
return "stable"
|
| 206 |
|
| 207 |
|
|
|
|
| 223 |
fontSize=22, # Slightly larger title
|
| 224 |
alignment=1, # Center alignment
|
| 225 |
spaceAfter=25, # More space after title
|
| 226 |
+
textColor=colors.HexColor("#4e73df") # Primary color
|
|
|
|
| 227 |
)
|
| 228 |
|
| 229 |
heading_style = ParagraphStyle(
|
|
|
|
| 232 |
fontSize=15, # Slightly larger headings
|
| 233 |
spaceAfter=8, # Less space after heading for tighter look
|
| 234 |
spaceBefore=18, # More space before heading
|
| 235 |
+
textColor=colors.HexColor("#1cc88a") # Secondary color
|
|
|
|
| 236 |
)
|
| 237 |
|
| 238 |
normal_style = ParagraphStyle(
|
|
|
|
| 241 |
fontSize=11,
|
| 242 |
spaceAfter=6, # More space after paragraphs
|
| 243 |
leading=14,
|
| 244 |
+
textColor=colors.HexColor("#5a5c69") # Dark color
|
|
|
|
| 245 |
)
|
| 246 |
|
| 247 |
bullet_style = ParagraphStyle(
|
|
|
|
| 252 |
leftIndent=20,
|
| 253 |
leading=14,
|
| 254 |
bulletIndent=10, # Space between bullet and text
|
| 255 |
+
textColor=colors.HexColor("#5a5c69") # Dark color
|
|
|
|
|
|
|
| 256 |
)
|
| 257 |
|
| 258 |
# Status color mapping
|
|
|
|
| 260 |
'emergency': colors.HexColor("#e74a3b"), # Danger color
|
| 261 |
'deteriorating': colors.HexColor("#f6c23e"), # Warning color
|
| 262 |
'improving': colors.HexColor("#1cc88a"), # Success color
|
| 263 |
+
'stable': colors.HexColor("#36b9cc") # Info color
|
|
|
|
| 264 |
}
|
| 265 |
|
| 266 |
status_text_styles = {
|
|
|
|
| 268 |
'deteriorating': ParagraphStyle('StatusDeteriorating', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['deteriorating'], alignment=1, fontName='Helvetica-Bold'),
|
| 269 |
'improving': ParagraphStyle('StatusImproving', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['improving'], alignment=1, fontName='Helvetica-Bold'),
|
| 270 |
'stable': ParagraphStyle('StatusStable', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['stable'], alignment=1, fontName='Helvetica-Bold'),
|
| 271 |
+
'unknown': ParagraphStyle('StatusUnknown', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=colors.black, alignment=1, fontName='Helvetica-Bold'),
|
| 272 |
}
|
| 273 |
|
| 274 |
|
|
|
|
| 321 |
story.append(Paragraph("Care Plan Details:", heading_style))
|
| 322 |
story.append(Spacer(1, 10))
|
| 323 |
|
| 324 |
+
# Process care plan text by lines or sections for better formatting
|
| 325 |
+
lines = care_plan_text.strip().split('\n')
|
| 326 |
+
current_section = []
|
| 327 |
+
is_bullet_section = False
|
| 328 |
+
|
| 329 |
+
def add_section_to_story(section_lines, is_bullet):
|
| 330 |
+
if not section_lines:
|
| 331 |
+
return
|
| 332 |
+
text = "\n".join(section_lines).strip()
|
| 333 |
+
if not text:
|
| 334 |
+
return
|
| 335 |
+
|
| 336 |
+
# Attempt to identify potential sub-headings within content
|
| 337 |
+
sub_sections = re.split(r'\n(?=[A-Z][A-Z\s]*:)', text)
|
| 338 |
+
|
| 339 |
+
for i, sub_text in enumerate(sub_sections):
|
| 340 |
+
if i > 0: # Add space before subsequent sections
|
| 341 |
+
story.append(Spacer(1, 8))
|
| 342 |
+
|
| 343 |
+
# Check if the sub_text starts with a potential sub-heading
|
| 344 |
+
header_match = re.match(r'([A-Z][A-Z\s]*):', sub_text)
|
| 345 |
+
if header_match:
|
| 346 |
+
header_title = header_match.group(1).strip()
|
| 347 |
+
content_after_header = sub_text[header_match.end():].strip()
|
| 348 |
+
story.append(Paragraph(header_title + ":", heading_style)) # Use heading style for sub-sections too
|
| 349 |
+
|
| 350 |
+
# Process content after the potential header
|
| 351 |
+
content_lines = content_after_header.split('\n')
|
| 352 |
+
sub_is_bullet = any(re.match(r'^\s*[-•*]', line) for line in content_lines)
|
| 353 |
+
|
| 354 |
+
if sub_is_bullet:
|
| 355 |
+
sub_bullet_points = re.findall(r'^\s*[-•*]\s*(.*?)$', content_after_header, re.MULTILINE)
|
| 356 |
+
if sub_bullet_points:
|
| 357 |
+
for point in sub_bullet_points:
|
| 358 |
+
if point.strip():
|
| 359 |
+
story.append(Paragraph(f"• {point.strip()}", bullet_style))
|
| 360 |
+
else: # Fallback if regex misses some bullets
|
| 361 |
+
for line in content_lines:
|
| 362 |
+
cleaned_line = re.sub(r'^\s*[-•*]\s*', '', line).strip()
|
| 363 |
+
if cleaned_line:
|
| 364 |
+
story.append(Paragraph(f"• {cleaned_line}", bullet_style))
|
| 365 |
+
|
| 366 |
+
else: # Not a bulleted sub-section
|
| 367 |
+
cleaned_content = content_after_header.strip()
|
| 368 |
+
if cleaned_content:
|
| 369 |
+
story.append(Paragraph(cleaned_content, normal_style))
|
| 370 |
+
|
| 371 |
+
else: # If no sub-heading found at the start, treat as a single block of content
|
| 372 |
+
if is_bullet:
|
| 373 |
+
bullet_points = re.findall(r'^\s*[-•*]\s*(.*?)$', text, re.MULTILINE)
|
| 374 |
+
if bullet_points:
|
| 375 |
+
for point in bullet_points:
|
| 376 |
+
if point.strip():
|
| 377 |
+
story.append(Paragraph(f"• {point.strip()}", bullet_style))
|
| 378 |
+
else: # Fallback if regex misses some bullets
|
| 379 |
+
for line in section_lines:
|
| 380 |
+
cleaned_line = re.sub(r'^\s*[-•*]\s*', '', line).strip()
|
| 381 |
+
if cleaned_line:
|
| 382 |
+
story.append(Paragraph(f"• {cleaned_line}", bullet_style))
|
| 383 |
+
else:
|
| 384 |
+
story.append(Paragraph(text, normal_style))
|
| 385 |
+
|
| 386 |
+
for line in lines:
|
| 387 |
+
stripped_line = line.strip()
|
| 388 |
+
# Check if line is a potential section header
|
| 389 |
+
header_match = re.match(r'^([A-Z][A-Z\s]*):$', stripped_line)
|
| 390 |
+
|
| 391 |
+
if header_match:
|
| 392 |
+
# Add previous section before starting a new one
|
| 393 |
+
add_section_to_story(current_section, is_bullet_section)
|
| 394 |
+
story.append(Paragraph(stripped_line, heading_style)) # Add the new header
|
| 395 |
+
current_section = [] # Reset current section content
|
| 396 |
+
is_bullet_section = False # Assume new section is not bulleted initially
|
| 397 |
+
elif stripped_line.startswith('-') or stripped_line.startswith('•') or stripped_line.startswith('*'):
|
| 398 |
+
# Line is a bullet point, add it to the current section content
|
| 399 |
+
current_section.append(line)
|
| 400 |
+
is_bullet_section = True # Mark the current section as potentially bulleted
|
| 401 |
else:
|
| 402 |
+
# Line is regular content, add it to the current section
|
| 403 |
+
current_section.append(line)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
+
# Add the last section
|
| 406 |
+
add_section_to_story(current_section, is_bullet_section)
|
| 407 |
|
| 408 |
# Add footer or timestamp if desired
|
| 409 |
story.append(Spacer(1, 20))
|
| 410 |
+
footer_style = ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, alignment=1, textColor=colors.grey)
|
| 411 |
+
story.append(Paragraph(f"Generated by Patient Care Management System on {datetime.now().strftime('%Y-%m-%d')}", footer_style))
|
| 412 |
|
| 413 |
|
| 414 |
# Build the PDF
|
|
|
|
| 422 |
if not twilio_client:
|
| 423 |
print("Twilio client not initialized. Cannot send WhatsApp message.")
|
| 424 |
return False
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
try:
|
| 427 |
# Status emoji mapping
|
|
|
|
| 429 |
'emergency': "🚨 EMERGENCY",
|
| 430 |
'deteriorating': "⚠️ HIGH RISK",
|
| 431 |
'improving': "✅ IMPROVING",
|
| 432 |
+
'stable': "🟦 STABLE"
|
|
|
|
| 433 |
}
|
| 434 |
|
| 435 |
# Format message for WhatsApp including patient details and the full plan text
|
|
|
|
| 437 |
message += f"*Patient Name:* {patient_info.get('name', 'N/A')}\n"
|
| 438 |
message += f"*Age:* {patient_info.get('age', 'N/A')}\n"
|
| 439 |
message += f"*Gender:* {patient_info.get('gender', 'N/A')}\n"
|
| 440 |
+
message += f"*Status:* {status_emoji.get(status, 'Unknown')}\n\n"
|
| 441 |
+
message += f"*Care Plan Details:*\n{care_plan_text.strip()}" # Include the full plan text
|
| 442 |
|
| 443 |
+
# Send WhatsApp message using hardcoded number
|
| 444 |
message = twilio_client.messages.create(
|
| 445 |
from_=TWILIO_FROM,
|
| 446 |
body=message,
|
| 447 |
to=TWILIO_TO
|
| 448 |
)
|
| 449 |
+
print(f"WhatsApp message sent, SID: {message.sid}")
|
| 450 |
return True
|
| 451 |
except Exception as e:
|
| 452 |
print(f"Error sending WhatsApp message: {e}")
|
| 453 |
+
# Twilio error handling can be more specific (e.g., check e.status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
return False
|
| 455 |
|
| 456 |
|
|
|
|
| 470 |
role = request.form.get('role')
|
| 471 |
if role in ['patient', 'doctor']:
|
| 472 |
session['role'] = role
|
| 473 |
+
return jsonify({'success': True, 'role': role})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
return jsonify({'success': False, 'error': 'Invalid role'}), 400
|
| 475 |
|
| 476 |
|
| 477 |
@app.route('/doctor_dashboard')
|
| 478 |
def doctor_dashboard():
|
| 479 |
# This route will render the dashboard template
|
| 480 |
+
# The patient list will be loaded via AJAX by the JavaScript
|
| 481 |
return render_template('doctor_dashboard.html')
|
| 482 |
|
| 483 |
|
| 484 |
+
# This route is not strictly needed anymore as patient details are loaded via AJAX on doctor_dashboard
|
| 485 |
+
# Keeping it for backward compatibility or direct linking if desired, but AJAX is preferred.
|
|
|
|
| 486 |
@app.route('/patient_details/<patient_id>')
|
| 487 |
+
def patient_details(patient_id):
|
| 488 |
+
print(f"Attempting to load patient details page for ID: {patient_id}")
|
| 489 |
+
print(f"Current patients_db keys: {patients_db.keys()}")
|
| 490 |
+
patient = patients_db.get(patient_id)
|
| 491 |
+
if not patient:
|
| 492 |
+
print(f"Patient ID {patient_id} not found in patients_db.")
|
| 493 |
+
# Redirect to dashboard, perhaps with an error message
|
| 494 |
+
return redirect(url_for('doctor_dashboard', error="Patient not found"))
|
| 495 |
+
# Render the dashboard template, passing the specific patient ID
|
| 496 |
+
# The JS on doctor_dashboard.html will detect this ID in the URL and load the details
|
| 497 |
+
return render_template('doctor_dashboard.html', selected_patient_id=patient_id)
|
| 498 |
|
| 499 |
|
| 500 |
@app.route('/submit_feedback', methods=['POST'])
|
| 501 |
def submit_feedback():
|
| 502 |
if not model:
|
| 503 |
+
return jsonify({'success': False, 'error': 'AI model not initialized. Please check API key.'}), 500
|
|
|
|
| 504 |
|
| 505 |
try:
|
| 506 |
# Get patient information from form
|
| 507 |
+
name = request.form.get('name', 'Unnamed Patient')
|
| 508 |
+
age = request.form.get('age', 'N/A')
|
| 509 |
+
gender = request.form.get('gender', 'N/A')
|
| 510 |
+
feedback = request.form.get('feedback', '')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
|
| 512 |
care_plan_text = ""
|
| 513 |
care_plan_format = None
|
|
|
|
| 516 |
if 'care_plan_pdf' in request.files:
|
| 517 |
pdf_file = request.files['care_plan_pdf']
|
| 518 |
if pdf_file and pdf_file.filename != '':
|
| 519 |
+
# Read the PDF content from the file stream
|
| 520 |
care_plan_text = extract_text_from_pdf(pdf_file)
|
| 521 |
if care_plan_text:
|
| 522 |
care_plan_format = extract_care_plan_format(care_plan_text)
|
| 523 |
+
print(f"Extracted text length: {len(care_plan_text)}. Format found: {care_plan_format is not None}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
|
| 525 |
+
# If no format is found in the PDF, use a default format
|
|
|
|
| 526 |
if not care_plan_format or not care_plan_format.strip():
|
| 527 |
print("Using default care plan format.")
|
| 528 |
care_plan_format = """
|
|
|
|
| 545 |
- [Night activities/medications/sleep instructions]
|
| 546 |
|
| 547 |
MEDICATIONS:
|
| 548 |
+
- [List of medications, dosage, frequency, and time]
|
| 549 |
|
| 550 |
DIET AND HYDRATION:
|
| 551 |
- [Dietary recommendations and hydration goals]
|
| 552 |
|
| 553 |
PHYSICAL ACTIVITY/EXERCISE:
|
| 554 |
+
- [Recommended physical activities, duration, frequency]
|
| 555 |
|
| 556 |
SYMPTOM MANAGEMENT:
|
| 557 |
- [Instructions for managing specific symptoms, including what to do if they worsen]
|
| 558 |
|
| 559 |
RED FLAGS / WHEN TO SEEK HELP:
|
| 560 |
+
- [Clear instructions on symptoms requiring immediate medical attention (Emergency)]
|
| 561 |
+
- [Instructions on symptoms requiring contact with doctor/clinic (Urgent but not Emergency)]
|
| 562 |
|
| 563 |
ADDITIONAL RECOMMENDATIONS:
|
| 564 |
+
- [Other relevant advice, e.g., rest, monitoring, specific precautions]
|
| 565 |
|
| 566 |
FOLLOW-UP:
|
| 567 |
- [Details about next appointment or when to contact healthcare provider]
|
|
|
|
| 569 |
else:
|
| 570 |
print("Using extracted care plan format.")
|
| 571 |
|
| 572 |
+
# Check for emergency symptoms FIRST in feedback
|
| 573 |
+
emergency_keywords_check = [
|
| 574 |
+
"severe chest pain", "heart attack", "shortness of breath",
|
| 575 |
+
"dizziness", "loss of consciousness", "extreme pain",
|
| 576 |
+
"sudden weakness", "confusion", "slurred speech", "severe headache",
|
| 577 |
+
"difficulty breathing", "severe shortness of breath", "wheezing",
|
| 578 |
+
"severe abdominal pain", "persistent vomiting", "uncontrolled bleeding",
|
| 579 |
+
"severe allergic reaction", "anaphylaxis", "immediate medical attention",
|
| 580 |
+
"emergency", "call 911", "urgent care", "hospital", "critical", "ambulance",
|
| 581 |
+
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion",
|
| 582 |
+
"suffocating", "not breathing", "blue lips", "blue face", "cardiac arrest",
|
| 583 |
+
"high fever", "signs of shock", "severe dehydration", "acute change",
|
| 584 |
+
"unstable vitals", "rapidly worsening"
|
| 585 |
+
]
|
| 586 |
+
|
| 587 |
+
feedback_lower = feedback.lower()
|
| 588 |
+
is_emergency_feedback = any(keyword in feedback_lower for keyword in emergency_keywords_check)
|
| 589 |
|
| 590 |
generated_plan_text = ""
|
| 591 |
+
status = 'stable' # Default status
|
| 592 |
|
| 593 |
+
if is_emergency_feedback:
|
| 594 |
+
print("Emergency keywords detected in feedback.")
|
| 595 |
# If emergency symptoms are detected in feedback, generate emergency instructions
|
| 596 |
generated_plan_text = (
|
| 597 |
"PATIENT INFORMATION:\n"
|
|
|
|
| 599 |
f"- Age: {age}\n"
|
| 600 |
f"- Gender: {gender}\n\n"
|
| 601 |
"ASSESSMENT:\n"
|
| 602 |
+
f"- Emergency symptoms reported: {feedback}. Immediate medical attention required.\n\n"
|
| 603 |
"EMERGENCY ACTION PLAN:\n"
|
| 604 |
+
"- Call emergency services immediately at 104/108/109/112 or your local emergency number.\n"
|
| 605 |
+
"- Do not delay seeking medical help.\n"
|
| 606 |
+
"- If conscious, try to remain calm.\n"
|
|
|
|
| 607 |
"- Do not eat or drink anything until evaluated by medical professionals.\n"
|
| 608 |
+
"- Follow instructions from emergency responders.\n\n"
|
| 609 |
"RED FLAGS / WHEN TO SEEK HELP:\n"
|
| 610 |
+
"- *Any* worsening of reported emergency symptoms requires urgent escalation.\n\n"
|
| 611 |
"FOLLOW-UP:\n"
|
| 612 |
"- Immediate hospitalization or urgent medical evaluation is necessary.\n"
|
| 613 |
+
"- Inform your primary physician as soon as medically stable.\n"
|
| 614 |
+
"- Review and update care plan only after emergency situation is resolved and evaluated by medical professionals.\n"
|
| 615 |
)
|
| 616 |
+
status = 'emergency' # Set status to emergency
|
| 617 |
|
| 618 |
else:
|
| 619 |
+
# Prepare a prompt for Gemini AI for a perfect, attractively formatted day care plan.
|
| 620 |
+
# Include patient info directly in the prompt for context
|
| 621 |
prompt = f"""
|
| 622 |
You are a helpful assistant generating updated patient care plans.
|
| 623 |
Based on the patient's personal information, their current symptoms/feedback, and their previous care plan, create a NEW, comprehensive, and well-structured daily care plan.
|
|
|
|
| 630 |
Patient Feedback/Symptoms Update:
|
| 631 |
{feedback}
|
| 632 |
|
| 633 |
+
Previous Care Plan Details (if available):
|
| 634 |
+
{care_plan_text if care_plan_text else "No previous care plan provided."}
|
| 635 |
|
| 636 |
Instructions:
|
| 637 |
+
1. Generate the updated care plan using the exact following format template. Fill in the details based on the patient's information, feedback, and integrate relevant elements from the previous plan if helpful.
|
| 638 |
+
2. Be specific and actionable in your recommendations.
|
| 639 |
+
3. Ensure the language is clear and easy to understand.
|
| 640 |
+
4. Include realistic times for medications if applicable (e.g., "Take 1 tablet in the morning with food").
|
| 641 |
+
5. Highlight important instructions or warnings, especially regarding symptom management and when to seek help.
|
| 642 |
+
6. Do NOT include any introductory or concluding sentences outside the plan format itself (like "Here is the updated plan:" or "Let me know if you need further assistance."). Provide ONLY the structured plan content.
|
| 643 |
+
|
| 644 |
+
Care Plan Format Template:
|
|
|
|
| 645 |
{care_plan_format}
|
| 646 |
"""
|
| 647 |
print("Sending prompt to AI model...")
|
| 648 |
+
print(f"Prompt:\n---\n{prompt}\n---")
|
| 649 |
|
| 650 |
# Get response from Gemini AI
|
| 651 |
try:
|
| 652 |
response = model.generate_content(prompt)
|
| 653 |
generated_plan_text = response.text.strip() # Get the text and strip leading/trailing whitespace
|
| 654 |
print(f"AI Response received. Length: {len(generated_plan_text)}")
|
| 655 |
+
print(f"AI Response:\n---\n{generated_plan_text}\n---")
|
| 656 |
|
| 657 |
# Determine patient status based on AI's output and feedback
|
| 658 |
status = determine_patient_status(care_plan_text, generated_plan_text, feedback)
|
|
|
|
| 659 |
|
| 660 |
except Exception as ai_error:
|
| 661 |
print(f"Error generating content from AI: {ai_error}")
|
|
|
|
| 662 |
return jsonify({
|
| 663 |
'success': False,
|
| 664 |
+
'error': f'Error generating care plan: {ai_error}'
|
| 665 |
}), 500
|
| 666 |
|
| 667 |
# --- Actions after plan generation (Emergency or AI) ---
|
|
|
|
| 677 |
'original_plan': care_plan_text, # Store original text from PDF
|
| 678 |
'updated_plan': generated_plan_text, # Store the generated plan text
|
| 679 |
'status': status,
|
| 680 |
+
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 681 |
}
|
| 682 |
+
print(f"Patient {patient_id} added to in-memory DB with status: {status}. Current DB keys: {patients_db.keys()}")
|
| 683 |
|
| 684 |
|
| 685 |
# Generate PDF for downloading using the stored data
|
|
|
|
| 688 |
'age': age,
|
| 689 |
'gender': gender
|
| 690 |
}
|
| 691 |
+
pdf_buffer = generate_care_plan_pdf(patient_info_for_pdf, generated_plan_text, status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
|
| 693 |
+
print("PDF generated and base64 encoded.")
|
| 694 |
|
| 695 |
+
# Send care plan via WhatsApp
|
| 696 |
patient_info_for_whatsapp = {
|
| 697 |
'name': name,
|
| 698 |
'age': age,
|
|
|
|
| 704 |
return jsonify({
|
| 705 |
'success': True,
|
| 706 |
'updated_plan': generated_plan_text, # Send back the generated text
|
| 707 |
+
'pdf_data': pdf_base64,
|
| 708 |
'patient_id': patient_id, # Send the generated patient ID
|
| 709 |
'status': status,
|
| 710 |
'whatsapp_sent': whatsapp_sent
|
| 711 |
})
|
| 712 |
|
| 713 |
except Exception as e:
|
| 714 |
+
print(f"An unexpected error occurred: {str(e)}")
|
| 715 |
+
# Log the traceback for debugging
|
| 716 |
+
import traceback
|
| 717 |
traceback.print_exc()
|
| 718 |
return jsonify({
|
| 719 |
'success': False,
|
| 720 |
+
'error': f'An unexpected error occurred: {str(e)}'
|
| 721 |
}), 500
|
| 722 |
|
| 723 |
|
| 724 |
@app.route('/download_pdf/<patient_id>')
|
| 725 |
def download_pdf(patient_id):
|
| 726 |
print(f"Download requested for patient ID: {patient_id}")
|
| 727 |
+
print(f"Current patients_db keys: {patients_db.keys()}")
|
| 728 |
|
| 729 |
try:
|
| 730 |
patient = patients_db.get(patient_id)
|
| 731 |
|
| 732 |
if not patient:
|
| 733 |
print(f"Patient ID {patient_id} not found in patients_db for download.")
|
| 734 |
+
return "Patient data not found (it may have been cleared). Please regenerate the plan.", 404
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
|
| 736 |
# Use the patient's stored information to regenerate the PDF content
|
| 737 |
patient_info_for_pdf = {
|
|
|
|
| 742 |
# Use the already determined status and updated plan text
|
| 743 |
pdf_buffer = generate_care_plan_pdf(
|
| 744 |
patient_info_for_pdf,
|
| 745 |
+
patient['updated_plan'],
|
| 746 |
+
patient['status']
|
| 747 |
)
|
| 748 |
|
| 749 |
pdf_buffer.seek(0) # Ensure buffer is at the start
|
| 750 |
|
| 751 |
# Sanitize patient name for filename
|
| 752 |
+
safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '', patient.get('name', 'patient')).lower()
|
| 753 |
+
download_name = f"care_plan_{safe_name}_{datetime.now().strftime('%Y%m%d')}.pdf"
|
|
|
|
| 754 |
|
| 755 |
print(f"Serving PDF for {patient_id} as {download_name}")
|
| 756 |
return send_file(
|
|
|
|
| 761 |
)
|
| 762 |
except Exception as e:
|
| 763 |
print(f"PDF Download Error for ID {patient_id}: {str(e)}")
|
| 764 |
+
import traceback
|
| 765 |
traceback.print_exc()
|
| 766 |
return f"Error generating PDF: {str(e)}", 500
|
| 767 |
|
|
|
|
| 772 |
emergency_patients = [p for p in patients_db.values() if p.get('status') == 'emergency']
|
| 773 |
|
| 774 |
# Sort by timestamp, newest first
|
| 775 |
+
emergency_patients.sort(key=lambda x: datetime.strptime(x['timestamp'], "%Y-%m-%d %H:%M:%S"), reverse=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
|
| 777 |
notifications = []
|
| 778 |
for patient in emergency_patients:
|
|
|
|
| 795 |
@app.route('/get_patients')
|
| 796 |
def get_patients():
|
| 797 |
# Return all patients for the doctor dashboard
|
| 798 |
+
# Sort by timestamp, newest first by default, or add sorting logic here
|
| 799 |
+
sorted_patients = sorted(patients_db.values(),
|
| 800 |
+
key=lambda x: datetime.strptime(x.get('timestamp', '1900-01-01 00:00:00'), "%Y-%m-%d %H:%M:%S"),
|
| 801 |
+
reverse=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
|
| 803 |
return jsonify({
|
| 804 |
'success': True,
|
|
|
|
| 809 |
@app.route('/get_patient/<patient_id>')
|
| 810 |
def get_patient(patient_id):
|
| 811 |
print(f"API request for patient ID: {patient_id}")
|
| 812 |
+
print(f"Current patients_db keys: {patients_db.keys()}")
|
| 813 |
|
| 814 |
patient = patients_db.get(patient_id)
|
| 815 |
|
| 816 |
if not patient:
|
| 817 |
print(f"Patient ID {patient_id} not found in patients_db for API request.")
|
| 818 |
+
return jsonify({'success': False, 'error': 'Patient not found or data cleared.'}), 404
|
| 819 |
|
| 820 |
+
print(f"Found patient {patient_id}: {patient.get('name')}")
|
|
|
|
| 821 |
return jsonify({
|
| 822 |
'success': True,
|
| 823 |
'patient': patient
|
|
|
|
| 826 |
|
| 827 |
if __name__ == '__main__':
|
| 828 |
# Use a more robust development server like Waitress or Gunicorn in production
|
|
|
|
| 829 |
app.run(debug=True, port=5000) # Explicitly set port
|