Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_file
|
|
|
|
| 2 |
import google.generativeai as genai
|
| 3 |
import PyPDF2
|
| 4 |
import os
|
|
@@ -20,6 +21,47 @@ app = Flask(__name__)
|
|
| 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', '/tmp/uploads')
|
| 24 |
upload_folder = os.path.join(upload_base, 'pdfs')
|
| 25 |
|
|
@@ -36,54 +78,51 @@ 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 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
# Gemini API Configuration
|
| 47 |
# IMPORTANT: Use environment variables for sensitive information like API keys
|
| 48 |
GENAI_API_KEY = os.getenv('GENAI_API_KEY', "AIzaSyD54ejbjVIVa-F3aD_Urnp8m1EFLUGR__I") # CHANGE THIS KEY
|
| 49 |
-
try:
|
| 50 |
-
genai.configure(api_key=GENAI_API_KEY)
|
| 51 |
-
# Simple test to check connectivity
|
| 52 |
-
list(genai.list_models())
|
| 53 |
-
print("Gemini API configured successfully.")
|
| 54 |
-
except Exception as e:
|
| 55 |
-
print(f"Error configuring Gemini API: {e}")
|
| 56 |
-
genai = None # Handle cases where API configuration fails
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
generation_config = {
|
| 60 |
-
"temperature": 0.8, # Slightly reduced temp for more stable output
|
| 61 |
-
"top_p": 0.9, # Adjusted top_p
|
| 62 |
-
"top_k": 30, # Adjusted top_k
|
| 63 |
-
"max_output_tokens": 4096, # Reduced token limit slightly if needed, or increase if plans are long
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
# Use a more capable model if available and affordable
|
| 67 |
-
# "gemini-1.5-pro" or "gemini-1.5-flash" are better choices than gemini-2.0-flash if 2.0 means an older preview
|
| 68 |
-
# Let's assume gemini-2.0-flash is a valid, accessible model alias
|
| 69 |
model = None
|
| 70 |
-
if
|
| 71 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
model = genai.GenerativeModel(
|
| 73 |
model_name="gemini-1.5-flash-latest", # Recommended model alias
|
| 74 |
generation_config=generation_config,
|
| 75 |
)
|
| 76 |
-
# Test model generation
|
| 77 |
# model.generate_content("Hello")
|
| 78 |
print(f"Using Gemini model: {model.model_name}")
|
| 79 |
-
except Exception as e:
|
| 80 |
-
print(f"Error initializing Gemini model: {e}")
|
| 81 |
-
model = None # Handle cases where model initialization fails
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
# Patient database (in-memory for demo - WILL BE RESET ON SERVER RESTART)
|
| 85 |
-
# For persistence, replace this with a database (SQLite, PostgreSQL, etc.)
|
| 86 |
-
patients_db = {}
|
| 87 |
|
| 88 |
def extract_text_from_pdf(pdf_file):
|
| 89 |
"""Extract text from uploaded PDF file"""
|
|
@@ -92,19 +131,28 @@ def extract_text_from_pdf(pdf_file):
|
|
| 92 |
pdf_file.seek(0)
|
| 93 |
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
| 94 |
text = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
for page in pdf_reader.pages:
|
| 96 |
page_text = page.extract_text()
|
| 97 |
if page_text:
|
| 98 |
text += page_text + "\n" # Add newline between pages
|
| 99 |
-
return text.strip()
|
| 100 |
except Exception as e:
|
| 101 |
print(f"Error extracting PDF text: {e}")
|
| 102 |
-
return ""
|
|
|
|
| 103 |
|
| 104 |
def extract_care_plan_format(pdf_text):
|
| 105 |
"""Extract a general format template from PDF text by identifying common section headers."""
|
| 106 |
-
if not pdf_text:
|
| 107 |
-
return None
|
| 108 |
|
| 109 |
# Look for lines that seem like headers (e.g., capitalized words ending with :, followed by content)
|
| 110 |
# This regex is an attempt to capture common patterns but might not be perfect for all PDFs.
|
|
@@ -123,7 +171,7 @@ def extract_care_plan_format(pdf_text):
|
|
| 123 |
format_template = "\n".join([f"{header.strip()}:" for header in set(potential_headers) if header.strip()]) + "\n"
|
| 124 |
print(f"Extracted potential headers (fallback): {list(set(potential_headers))}")
|
| 125 |
return format_template if format_template.strip() else None
|
| 126 |
-
print("No sections or potential headers found.")
|
| 127 |
return None # No recognizable format found
|
| 128 |
|
| 129 |
format_template = ""
|
|
@@ -141,6 +189,7 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 141 |
updated_plan_lower = updated_plan.lower()
|
| 142 |
|
| 143 |
# Comprehensive keywords indicating condition worsening or emergency
|
|
|
|
| 144 |
emergency_keywords = [
|
| 145 |
"severe chest pain", "heart attack", "sudden shortness of breath",
|
| 146 |
"difficulty breathing", "loss of consciousness", "extreme pain",
|
|
@@ -151,7 +200,7 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 151 |
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion",
|
| 152 |
"suffocating", "not breathing", "blue lips", "blue face", "cardiac arrest",
|
| 153 |
"high fever", "signs of shock", "severe dehydration", "acute change",
|
| 154 |
-
"unstable vitals", "rapidly worsening"
|
| 155 |
]
|
| 156 |
|
| 157 |
deteriorating_keywords = [
|
|
@@ -162,7 +211,8 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 162 |
"weakening", "relapse", "recurrence", "regressing", "not responding",
|
| 163 |
"increased symptoms", "more severe", "progressing", "progressive", "complicated",
|
| 164 |
"adverse change", "unstable", "needs attention", "spike in", "significant increase",
|
| 165 |
-
"new symptoms", "trouble with", "reduced appetite", "difficulty sleeping"
|
|
|
|
| 166 |
]
|
| 167 |
|
| 168 |
improvement_keywords = [
|
|
@@ -172,36 +222,26 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 172 |
"improved", "enhancement", "advancement", "resolving", "resolved", "recovering",
|
| 173 |
"normalized", "normal range", "responding well", "responding positively",
|
| 174 |
"effective treatment", "successful treatment", "managed well", "under control",
|
| 175 |
-
"symptoms decreased", "feeling stronger", "better sleep", "increased appetite"
|
|
|
|
| 176 |
]
|
| 177 |
|
| 178 |
-
# Check
|
| 179 |
if any(keyword in feedback_lower for keyword in emergency_keywords):
|
| 180 |
return "emergency"
|
| 181 |
-
# Then check updated plan, as AI might interpret feedback and include emergency terms
|
| 182 |
-
if any(keyword in updated_plan_lower for keyword in emergency_keywords):
|
| 183 |
-
return "emergency"
|
| 184 |
-
|
| 185 |
-
# Check for deteriorating keywords
|
| 186 |
if any(keyword in feedback_lower for keyword in deteriorating_keywords):
|
| 187 |
return "deteriorating"
|
| 188 |
-
# Check updated plan for deteriorating indicators
|
| 189 |
-
if any(keyword in updated_plan_lower for keyword in deteriorating_keywords):
|
| 190 |
-
return "deteriorating"
|
| 191 |
-
|
| 192 |
-
# Check for improvement keywords
|
| 193 |
if any(keyword in feedback_lower for keyword in improvement_keywords):
|
| 194 |
return "improving"
|
| 195 |
-
# Check updated plan for improvement indicators
|
| 196 |
-
if any(keyword in updated_plan_lower for keyword in improvement_keywords):
|
| 197 |
-
return "improving"
|
| 198 |
-
|
| 199 |
-
# Compare original vs updated plan length/content as a weak signal (optional, can be removed if unreliable)
|
| 200 |
-
# if len(updated_plan) < len(original_plan) * 0.8 and any(k in updated_plan_lower for k in improvement_keywords):
|
| 201 |
-
# return "improving"
|
| 202 |
-
# if len(updated_plan) > len(original_plan) * 1.2 and any(k in updated_plan_lower for k in deteriorating_keywords):
|
| 203 |
-
# return "deteriorating"
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
# Default to stable if no clear indicators of change are found
|
| 207 |
return "stable"
|
|
@@ -297,19 +337,19 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 297 |
# Patient Information Table
|
| 298 |
patient_data = [
|
| 299 |
[Paragraph("<b>Patient Name:</b>", normal_style), Paragraph(patient_info.get('name', 'N/A'), normal_style)],
|
| 300 |
-
[Paragraph("<b>Age:</b>", normal_style), Paragraph(patient_info.get('age', 'N/A'), normal_style)],
|
| 301 |
[Paragraph("<b>Gender:</b>", normal_style), Paragraph(patient_info.get('gender', 'N/A'), normal_style)],
|
| 302 |
[Paragraph("<b>Generated Date:</b>", normal_style), Paragraph(datetime.now().strftime("%Y-%m-%d %H:%M"), normal_style)]
|
| 303 |
]
|
| 304 |
|
| 305 |
patient_table = Table(patient_data, colWidths=[150, 360]) # Adjusted column widths
|
| 306 |
patient_table.setStyle(TableStyle([
|
| 307 |
-
('BACKGROUND', (0, 0), (0, -1), colors.
|
| 308 |
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
|
| 309 |
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 310 |
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 311 |
-
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.
|
| 312 |
-
('BOX', (0, 0), (-1, -1), 0.25, colors.
|
| 313 |
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
| 314 |
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
| 315 |
('TOPPADDING', (0, 0), (-1, -1), 6), # Increased padding
|
|
@@ -324,88 +364,31 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 324 |
story.append(Spacer(1, 10))
|
| 325 |
|
| 326 |
# Process care plan text by lines or sections for better formatting
|
|
|
|
|
|
|
| 327 |
lines = care_plan_text.strip().split('\n')
|
| 328 |
-
current_section = []
|
| 329 |
-
is_bullet_section = False
|
| 330 |
-
|
| 331 |
-
def add_section_to_story(section_lines, is_bullet):
|
| 332 |
-
if not section_lines:
|
| 333 |
-
return
|
| 334 |
-
text = "\n".join(section_lines).strip()
|
| 335 |
-
if not text:
|
| 336 |
-
return
|
| 337 |
-
|
| 338 |
-
# Attempt to identify potential sub-headings within content
|
| 339 |
-
sub_sections = re.split(r'\n(?=[A-Z][A-Z\s]*:)', text)
|
| 340 |
-
|
| 341 |
-
for i, sub_text in enumerate(sub_sections):
|
| 342 |
-
if i > 0: # Add space before subsequent sections
|
| 343 |
-
story.append(Spacer(1, 8))
|
| 344 |
-
|
| 345 |
-
# Check if the sub_text starts with a potential sub-heading
|
| 346 |
-
header_match = re.match(r'([A-Z][A-Z\s]*):', sub_text)
|
| 347 |
-
if header_match:
|
| 348 |
-
header_title = header_match.group(1).strip()
|
| 349 |
-
content_after_header = sub_text[header_match.end():].strip()
|
| 350 |
-
story.append(Paragraph(header_title + ":", heading_style)) # Use heading style for sub-sections too
|
| 351 |
-
|
| 352 |
-
# Process content after the potential header
|
| 353 |
-
content_lines = content_after_header.split('\n')
|
| 354 |
-
sub_is_bullet = any(re.match(r'^\s*[-•*]', line) for line in content_lines)
|
| 355 |
-
|
| 356 |
-
if sub_is_bullet:
|
| 357 |
-
sub_bullet_points = re.findall(r'^\s*[-•*]\s*(.*?)$', content_after_header, re.MULTILINE)
|
| 358 |
-
if sub_bullet_points:
|
| 359 |
-
for point in sub_bullet_points:
|
| 360 |
-
if point.strip():
|
| 361 |
-
story.append(Paragraph(f"• {point.strip()}", bullet_style))
|
| 362 |
-
else: # Fallback if regex misses some bullets
|
| 363 |
-
for line in content_lines:
|
| 364 |
-
cleaned_line = re.sub(r'^\s*[-•*]\s*', '', line).strip()
|
| 365 |
-
if cleaned_line:
|
| 366 |
-
story.append(Paragraph(f"• {cleaned_line}", bullet_style))
|
| 367 |
-
|
| 368 |
-
else: # Not a bulleted sub-section
|
| 369 |
-
cleaned_content = content_after_header.strip()
|
| 370 |
-
if cleaned_content:
|
| 371 |
-
story.append(Paragraph(cleaned_content, normal_style))
|
| 372 |
-
|
| 373 |
-
else: # If no sub-heading found at the start, treat as a single block of content
|
| 374 |
-
if is_bullet:
|
| 375 |
-
bullet_points = re.findall(r'^\s*[-•*]\s*(.*?)$', text, re.MULTILINE)
|
| 376 |
-
if bullet_points:
|
| 377 |
-
for point in bullet_points:
|
| 378 |
-
if point.strip():
|
| 379 |
-
story.append(Paragraph(f"• {point.strip()}", bullet_style))
|
| 380 |
-
else: # Fallback if regex misses some bullets
|
| 381 |
-
for line in section_lines:
|
| 382 |
-
cleaned_line = re.sub(r'^\s*[-•*]\s*', '', line).strip()
|
| 383 |
-
if cleaned_line:
|
| 384 |
-
story.append(Paragraph(f"• {cleaned_line}", bullet_style))
|
| 385 |
-
else:
|
| 386 |
-
story.append(Paragraph(text, normal_style))
|
| 387 |
-
|
| 388 |
for line in lines:
|
| 389 |
stripped_line = line.strip()
|
| 390 |
-
|
| 391 |
-
|
| 392 |
|
|
|
|
|
|
|
| 393 |
if header_match:
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
|
|
|
| 403 |
else:
|
| 404 |
-
#
|
| 405 |
-
|
| 406 |
|
| 407 |
-
# Add the last section
|
| 408 |
-
add_section_to_story(current_section, is_bullet_section)
|
| 409 |
|
| 410 |
# Add footer or timestamp if desired
|
| 411 |
story.append(Spacer(1, 20))
|
|
@@ -414,15 +397,27 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 414 |
|
| 415 |
|
| 416 |
# Build the PDF
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
|
| 421 |
|
| 422 |
def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
| 423 |
"""Send care plan via WhatsApp using Twilio with improved formatting"""
|
| 424 |
if not twilio_client:
|
| 425 |
-
print("Twilio client not
|
| 426 |
return False
|
| 427 |
|
| 428 |
try:
|
|
@@ -431,28 +426,39 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 431 |
'emergency': "🚨 EMERGENCY",
|
| 432 |
'deteriorating': "⚠️ HIGH RISK",
|
| 433 |
'improving': "✅ IMPROVING",
|
| 434 |
-
'stable': "🟦 STABLE"
|
|
|
|
| 435 |
}
|
| 436 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
# Format message for WhatsApp including patient details and the full plan text
|
| 438 |
message = f"*Care Plan Update*\n\n"
|
| 439 |
message += f"*Patient Name:* {patient_info.get('name', 'N/A')}\n"
|
| 440 |
message += f"*Age:* {patient_info.get('age', 'N/A')}\n"
|
| 441 |
message += f"*Gender:* {patient_info.get('gender', 'N/A')}\n"
|
| 442 |
message += f"*Status:* {status_emoji.get(status, 'Unknown')}\n\n"
|
| 443 |
-
message += f"*Care Plan Details:*\n{
|
| 444 |
|
|
|
|
| 445 |
# Send WhatsApp message using hardcoded number
|
| 446 |
-
|
| 447 |
from_=TWILIO_FROM,
|
| 448 |
body=message,
|
| 449 |
to=TWILIO_TO
|
| 450 |
)
|
| 451 |
-
print(f"WhatsApp message sent, SID: {
|
| 452 |
return True
|
| 453 |
except Exception as e:
|
| 454 |
print(f"Error sending WhatsApp message: {e}")
|
| 455 |
# Twilio error handling can be more specific (e.g., check e.status)
|
|
|
|
| 456 |
return False
|
| 457 |
|
| 458 |
|
|
@@ -483,34 +489,30 @@ def doctor_dashboard():
|
|
| 483 |
return render_template('doctor_dashboard.html')
|
| 484 |
|
| 485 |
|
| 486 |
-
# This route is not strictly needed anymore as patient details are loaded via AJAX on doctor_dashboard
|
| 487 |
-
# Keeping it for backward compatibility or direct linking if desired, but AJAX is preferred.
|
| 488 |
-
@app.route('/patient_details/<patient_id>')
|
| 489 |
-
def patient_details(patient_id):
|
| 490 |
-
print(f"Attempting to load patient details page for ID: {patient_id}")
|
| 491 |
-
print(f"Current patients_db keys: {patients_db.keys()}")
|
| 492 |
-
patient = patients_db.get(patient_id)
|
| 493 |
-
if not patient:
|
| 494 |
-
print(f"Patient ID {patient_id} not found in patients_db.")
|
| 495 |
-
# Redirect to dashboard, perhaps with an error message
|
| 496 |
-
return redirect(url_for('doctor_dashboard', error="Patient not found"))
|
| 497 |
-
# Render the dashboard template, passing the specific patient ID
|
| 498 |
-
# The JS on doctor_dashboard.html will detect this ID in the URL and load the details
|
| 499 |
-
return render_template('doctor_dashboard.html', selected_patient_id=patient_id)
|
| 500 |
-
|
| 501 |
-
|
| 502 |
@app.route('/submit_feedback', methods=['POST'])
|
| 503 |
def submit_feedback():
|
| 504 |
if not model:
|
| 505 |
-
return jsonify({'success': False, 'error': 'AI model not initialized. Please check API key.'}), 500
|
| 506 |
|
| 507 |
try:
|
| 508 |
# Get patient information from form
|
| 509 |
name = request.form.get('name', 'Unnamed Patient')
|
| 510 |
-
age = request.form.get('age'
|
| 511 |
gender = request.form.get('gender', 'N/A')
|
| 512 |
feedback = request.form.get('feedback', '')
|
| 513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
care_plan_text = ""
|
| 515 |
care_plan_format = None
|
| 516 |
|
|
@@ -520,9 +522,12 @@ def submit_feedback():
|
|
| 520 |
if pdf_file and pdf_file.filename != '':
|
| 521 |
# Read the PDF content from the file stream
|
| 522 |
care_plan_text = extract_text_from_pdf(pdf_file)
|
| 523 |
-
if care_plan_text:
|
| 524 |
care_plan_format = extract_care_plan_format(care_plan_text)
|
| 525 |
print(f"Extracted text length: {len(care_plan_text)}. Format found: {care_plan_format is not None}")
|
|
|
|
|
|
|
|
|
|
| 526 |
|
| 527 |
# If no format is found in the PDF, use a default format
|
| 528 |
if not care_plan_format or not care_plan_format.strip():
|
|
@@ -532,10 +537,8 @@ PATIENT INFORMATION:
|
|
| 532 |
- Name: [Patient Name]
|
| 533 |
- Age: [Age]
|
| 534 |
- Gender: [Gender]
|
| 535 |
-
|
| 536 |
ASSESSMENT:
|
| 537 |
- [Summary of patient's current condition based on feedback and previous plan]
|
| 538 |
-
|
| 539 |
DAILY CARE PLAN:
|
| 540 |
Morning:
|
| 541 |
- [Morning activities/medications]
|
|
@@ -545,60 +548,39 @@ Evening:
|
|
| 545 |
- [Evening activities/medications]
|
| 546 |
Night:
|
| 547 |
- [Night activities/medications/sleep instructions]
|
| 548 |
-
|
| 549 |
MEDICATIONS:
|
| 550 |
- [List of medications, dosage, frequency, and time]
|
| 551 |
-
|
| 552 |
DIET AND HYDRATION:
|
| 553 |
- [Dietary recommendations and hydration goals]
|
| 554 |
-
|
| 555 |
PHYSICAL ACTIVITY/EXERCISE:
|
| 556 |
- [Recommended physical activities, duration, frequency]
|
| 557 |
-
|
| 558 |
SYMPTOM MANAGEMENT:
|
| 559 |
- [Instructions for managing specific symptoms, including what to do if they worsen]
|
| 560 |
-
|
| 561 |
RED FLAGS / WHEN TO SEEK HELP:
|
| 562 |
- [Clear instructions on symptoms requiring immediate medical attention (Emergency)]
|
| 563 |
- [Instructions on symptoms requiring contact with doctor/clinic (Urgent but not Emergency)]
|
| 564 |
-
|
| 565 |
ADDITIONAL RECOMMENDATIONS:
|
| 566 |
- [Other relevant advice, e.g., rest, monitoring, specific precautions]
|
| 567 |
-
|
| 568 |
FOLLOW-UP:
|
| 569 |
- [Details about next appointment or when to contact healthcare provider]
|
| 570 |
"""
|
| 571 |
else:
|
| 572 |
print("Using extracted care plan format.")
|
| 573 |
|
| 574 |
-
#
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
"dizziness", "loss of consciousness", "extreme pain",
|
| 578 |
-
"sudden weakness", "confusion", "slurred speech", "severe headache",
|
| 579 |
-
"difficulty breathing", "severe shortness of breath", "wheezing",
|
| 580 |
-
"severe abdominal pain", "persistent vomiting", "uncontrolled bleeding",
|
| 581 |
-
"severe allergic reaction", "anaphylaxis", "immediate medical attention",
|
| 582 |
-
"emergency", "call 911", "urgent care", "hospital", "critical", "ambulance",
|
| 583 |
-
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion",
|
| 584 |
-
"suffocating", "not breathing", "blue lips", "blue face", "cardiac arrest",
|
| 585 |
-
"high fever", "signs of shock", "severe dehydration", "acute change",
|
| 586 |
-
"unstable vitals", "rapidly worsening"
|
| 587 |
-
]
|
| 588 |
-
|
| 589 |
-
feedback_lower = feedback.lower()
|
| 590 |
-
is_emergency_feedback = any(keyword in feedback_lower for keyword in emergency_keywords_check)
|
| 591 |
|
| 592 |
generated_plan_text = ""
|
| 593 |
-
status =
|
| 594 |
|
| 595 |
-
if
|
| 596 |
-
print("Emergency
|
| 597 |
# If emergency symptoms are detected in feedback, generate emergency instructions
|
| 598 |
generated_plan_text = (
|
| 599 |
"PATIENT INFORMATION:\n"
|
| 600 |
f"- Name: {name}\n"
|
| 601 |
-
f"- Age: {age}\n"
|
| 602 |
f"- Gender: {gender}\n\n"
|
| 603 |
"ASSESSMENT:\n"
|
| 604 |
f"- Emergency symptoms reported: {feedback}. Immediate medical attention required.\n\n"
|
|
@@ -615,7 +597,7 @@ FOLLOW-UP:
|
|
| 615 |
"- Inform your primary physician as soon as medically stable.\n"
|
| 616 |
"- Review and update care plan only after emergency situation is resolved and evaluated by medical professionals.\n"
|
| 617 |
)
|
| 618 |
-
|
| 619 |
|
| 620 |
else:
|
| 621 |
# Prepare a prompt for Gemini AI for a perfect, attractively formatted day care plan.
|
|
@@ -623,18 +605,14 @@ FOLLOW-UP:
|
|
| 623 |
prompt = f"""
|
| 624 |
You are a helpful assistant generating updated patient care plans.
|
| 625 |
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.
|
| 626 |
-
|
| 627 |
Patient Information:
|
| 628 |
Name: {name}
|
| 629 |
-
Age: {age}
|
| 630 |
Gender: {gender}
|
| 631 |
-
|
| 632 |
Patient Feedback/Symptoms Update:
|
| 633 |
{feedback}
|
| 634 |
-
|
| 635 |
Previous Care Plan Details (if available):
|
| 636 |
-
{care_plan_text if care_plan_text else "No previous care plan provided."}
|
| 637 |
-
|
| 638 |
Instructions:
|
| 639 |
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.
|
| 640 |
2. Be specific and actionable in your recommendations.
|
|
@@ -642,46 +620,71 @@ Instructions:
|
|
| 642 |
4. Include realistic times for medications if applicable (e.g., "Take 1 tablet in the morning with food").
|
| 643 |
5. Highlight important instructions or warnings, especially regarding symptom management and when to seek help.
|
| 644 |
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.
|
| 645 |
-
|
| 646 |
Care Plan Format Template:
|
| 647 |
{care_plan_format}
|
| 648 |
"""
|
| 649 |
print("Sending prompt to AI model...")
|
| 650 |
-
print(f"Prompt:\n---\n{prompt}\n---")
|
| 651 |
|
| 652 |
# Get response from Gemini AI
|
| 653 |
try:
|
| 654 |
response = model.generate_content(prompt)
|
| 655 |
generated_plan_text = response.text.strip() # Get the text and strip leading/trailing whitespace
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
print(f"AI Response received. Length: {len(generated_plan_text)}")
|
| 657 |
-
print(f"AI Response:\n---\n{generated_plan_text}\n---")
|
|
|
|
| 658 |
|
| 659 |
-
#
|
| 660 |
status = determine_patient_status(care_plan_text, generated_plan_text, feedback)
|
| 661 |
|
| 662 |
except Exception as ai_error:
|
| 663 |
print(f"Error generating content from AI: {ai_error}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
return jsonify({
|
| 665 |
'success': False,
|
| 666 |
-
'error': f'Error generating care plan: {ai_error}'
|
| 667 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
|
| 669 |
# --- Actions after plan generation (Emergency or AI) ---
|
| 670 |
|
| 671 |
-
#
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
|
|
|
|
|
|
| 685 |
|
| 686 |
|
| 687 |
# Generate PDF for downloading using the stored data
|
|
@@ -706,53 +709,56 @@ Care Plan Format Template:
|
|
| 706 |
return jsonify({
|
| 707 |
'success': True,
|
| 708 |
'updated_plan': generated_plan_text, # Send back the generated text
|
| 709 |
-
'pdf_data': pdf_base64,
|
| 710 |
-
'patient_id': patient_id, # Send the generated patient ID
|
| 711 |
'status': status,
|
| 712 |
'whatsapp_sent': whatsapp_sent
|
| 713 |
})
|
| 714 |
|
| 715 |
except Exception as e:
|
| 716 |
-
|
|
|
|
| 717 |
# Log the traceback for debugging
|
| 718 |
import traceback
|
| 719 |
traceback.print_exc()
|
|
|
|
|
|
|
| 720 |
return jsonify({
|
| 721 |
'success': False,
|
| 722 |
-
'error': f'An unexpected error occurred: {str(e)}'
|
| 723 |
}), 500
|
| 724 |
|
| 725 |
|
| 726 |
@app.route('/download_pdf/<patient_id>')
|
| 727 |
def download_pdf(patient_id):
|
| 728 |
print(f"Download requested for patient ID: {patient_id}")
|
| 729 |
-
print(f"Current patients_db keys: {patients_db.keys()}")
|
| 730 |
|
| 731 |
try:
|
| 732 |
-
patient
|
|
|
|
| 733 |
|
| 734 |
if not patient:
|
| 735 |
-
print(f"Patient ID {patient_id} not found in
|
| 736 |
-
return "Patient data not found
|
| 737 |
|
| 738 |
# Use the patient's stored information to regenerate the PDF content
|
| 739 |
patient_info_for_pdf = {
|
| 740 |
-
'name': patient.
|
| 741 |
-
'age': patient.
|
| 742 |
-
'gender': patient.
|
| 743 |
}
|
| 744 |
-
# Use the already determined status and updated plan text
|
| 745 |
pdf_buffer = generate_care_plan_pdf(
|
| 746 |
patient_info_for_pdf,
|
| 747 |
-
patient
|
| 748 |
-
patient
|
| 749 |
)
|
| 750 |
|
| 751 |
pdf_buffer.seek(0) # Ensure buffer is at the start
|
| 752 |
|
| 753 |
# Sanitize patient name for filename
|
| 754 |
-
safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '', patient.
|
| 755 |
-
download_name = f"care_plan_{safe_name}_{datetime.now().strftime('%Y%m%
|
| 756 |
|
| 757 |
print(f"Serving PDF for {patient_id} as {download_name}")
|
| 758 |
return send_file(
|
|
@@ -765,28 +771,15 @@ def download_pdf(patient_id):
|
|
| 765 |
print(f"PDF Download Error for ID {patient_id}: {str(e)}")
|
| 766 |
import traceback
|
| 767 |
traceback.print_exc()
|
| 768 |
-
return f"Error generating PDF: {str(e)}", 500
|
| 769 |
|
| 770 |
|
| 771 |
@app.route('/get_emergency_notifications')
|
| 772 |
def get_emergency_notifications():
|
| 773 |
-
# Filter patients with emergency status
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
emergency_patients.sort(key=lambda x: datetime.strptime(x['timestamp'], "%Y-%m-%d %H:%M:%S"), reverse=True)
|
| 778 |
-
|
| 779 |
-
notifications = []
|
| 780 |
-
for patient in emergency_patients:
|
| 781 |
-
notifications.append({
|
| 782 |
-
'patient_id': patient['id'],
|
| 783 |
-
'patient_name': patient.get('name', 'N/A'),
|
| 784 |
-
'patient_age': patient.get('age', 'N/A'),
|
| 785 |
-
'patient_gender': patient.get('gender', 'N/A'),
|
| 786 |
-
'patient_feedback': patient.get('feedback', 'No feedback recorded.'),
|
| 787 |
-
'timestamp': patient.get('timestamp', 'N/A'),
|
| 788 |
-
'status': patient.get('status', 'unknown')
|
| 789 |
-
})
|
| 790 |
|
| 791 |
return jsonify({
|
| 792 |
'success': True,
|
|
@@ -796,36 +789,56 @@ def get_emergency_notifications():
|
|
| 796 |
|
| 797 |
@app.route('/get_patients')
|
| 798 |
def get_patients():
|
| 799 |
-
# Return all patients for the doctor dashboard
|
| 800 |
-
#
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
|
| 805 |
return jsonify({
|
| 806 |
'success': True,
|
| 807 |
-
'patients':
|
| 808 |
})
|
| 809 |
|
| 810 |
|
| 811 |
@app.route('/get_patient/<patient_id>')
|
| 812 |
def get_patient(patient_id):
|
| 813 |
print(f"API request for patient ID: {patient_id}")
|
| 814 |
-
print(f"Current patients_db keys: {patients_db.keys()}")
|
| 815 |
|
| 816 |
-
patient
|
|
|
|
| 817 |
|
| 818 |
if not patient:
|
| 819 |
-
print(f"Patient ID {patient_id} not found in
|
| 820 |
-
return jsonify({'success': False, 'error': 'Patient not found
|
| 821 |
|
| 822 |
-
print(f"Found patient {patient_id}: {patient.
|
| 823 |
return jsonify({
|
| 824 |
'success': True,
|
| 825 |
-
'patient': patient
|
| 826 |
})
|
| 827 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
|
| 829 |
if __name__ == '__main__':
|
| 830 |
# Use a more robust development server like Waitress or Gunicorn in production
|
| 831 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_file
|
| 2 |
+
from flask_sqlalchemy import SQLAlchemy # Import SQLAlchemy
|
| 3 |
import google.generativeai as genai
|
| 4 |
import PyPDF2
|
| 5 |
import os
|
|
|
|
| 21 |
# Use a strong, unique secret key
|
| 22 |
app.secret_key = os.getenv('SECRET_KEY', '688ed745a74bdd7ac238f5b50f4104fb87d6774b8b0a4e06e7e18ac5ed0fa31c') # CHANGE THIS IN PRODUCTION
|
| 23 |
|
| 24 |
+
# Database Configuration
|
| 25 |
+
# Use SQLite for simplicity
|
| 26 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///patients.db')
|
| 27 |
+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
| 28 |
+
|
| 29 |
+
db = SQLAlchemy(app)
|
| 30 |
+
|
| 31 |
+
# Define the Patient Model
|
| 32 |
+
class Patient(db.Model):
|
| 33 |
+
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
| 34 |
+
name = db.Column(db.String(100), nullable=False)
|
| 35 |
+
age = db.Column(db.Integer)
|
| 36 |
+
gender = db.Column(db.String(50))
|
| 37 |
+
feedback = db.Column(db.Text, nullable=False)
|
| 38 |
+
original_plan = db.Column(db.Text) # Store text from uploaded PDF
|
| 39 |
+
updated_plan = db.Column(db.Text, nullable=False) # Store generated plan text
|
| 40 |
+
status = db.Column(db.String(50), default='stable')
|
| 41 |
+
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
| 42 |
+
|
| 43 |
+
def __repr__(self):
|
| 44 |
+
return f"<Patient {self.name} ({self.id})>"
|
| 45 |
+
|
| 46 |
+
def to_dict(self):
|
| 47 |
+
return {
|
| 48 |
+
'id': self.id,
|
| 49 |
+
'name': self.name,
|
| 50 |
+
'age': self.age,
|
| 51 |
+
'gender': self.gender,
|
| 52 |
+
'feedback': self.feedback,
|
| 53 |
+
'original_plan': self.original_plan,
|
| 54 |
+
'updated_plan': self.updated_plan,
|
| 55 |
+
'status': self.status,
|
| 56 |
+
'timestamp': self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp else None
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
# Create database tables if they don't exist
|
| 60 |
+
@app.before_request
|
| 61 |
+
def create_tables():
|
| 62 |
+
db.create_all()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
upload_base = os.getenv('UPLOAD_DIR', '/tmp/uploads')
|
| 66 |
upload_folder = os.path.join(upload_base, 'pdfs')
|
| 67 |
|
|
|
|
| 78 |
TWILIO_TO = os.getenv('TWILIO_TO_NUMBER', 'whatsapp:+917559355282') # Hardcoded number as requested
|
| 79 |
|
| 80 |
# Initialize Twilio client
|
| 81 |
+
twilio_client = None
|
| 82 |
+
if ACCOUNT_SID and AUTH_TOKEN and TWILIO_FROM and TWILIO_TO:
|
| 83 |
+
try:
|
| 84 |
+
twilio_client = Client(ACCOUNT_SID, AUTH_TOKEN)
|
| 85 |
+
# Basic check to ensure credentials are valid (optional but good)
|
| 86 |
+
# twilio_client.api.accounts.get(ACCOUNT_SID)
|
| 87 |
+
print("Twilio client initialized successfully.")
|
| 88 |
+
except Exception as e:
|
| 89 |
+
print(f"Error initializing Twilio client (check SID/Token/Network): {e}")
|
| 90 |
+
else:
|
| 91 |
+
print("Twilio environment variables not set. WhatsApp sending disabled.")
|
| 92 |
+
|
| 93 |
|
| 94 |
# Gemini API Configuration
|
| 95 |
# IMPORTANT: Use environment variables for sensitive information like API keys
|
| 96 |
GENAI_API_KEY = os.getenv('GENAI_API_KEY', "AIzaSyD54ejbjVIVa-F3aD_Urnp8m1EFLUGR__I") # CHANGE THIS KEY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
model = None
|
| 98 |
+
if GENAI_API_KEY:
|
| 99 |
try:
|
| 100 |
+
genai.configure(api_key=GENAI_API_KEY)
|
| 101 |
+
# Simple test to check connectivity
|
| 102 |
+
# list(genai.list_models()) # Can take time, maybe skip in prod startup
|
| 103 |
+
print("Gemini API configured successfully.")
|
| 104 |
+
|
| 105 |
+
generation_config = {
|
| 106 |
+
"temperature": 0.8, # Slightly reduced temp for more stable output
|
| 107 |
+
"top_p": 0.9, # Adjusted top_p
|
| 108 |
+
"top_k": 30, # Adjusted top_k
|
| 109 |
+
"max_output_tokens": 4096, # Reduced token limit slightly if needed, or increase if plans are long
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
model = genai.GenerativeModel(
|
| 113 |
model_name="gemini-1.5-flash-latest", # Recommended model alias
|
| 114 |
generation_config=generation_config,
|
| 115 |
)
|
| 116 |
+
# Test model generation (optional)
|
| 117 |
# model.generate_content("Hello")
|
| 118 |
print(f"Using Gemini model: {model.model_name}")
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"Error configuring Gemini API or loading model: {e}")
|
| 122 |
+
model = None # Handle cases where API configuration fails
|
| 123 |
+
else:
|
| 124 |
+
print("GENAI_API_KEY environment variable not set. AI generation disabled.")
|
| 125 |
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
def extract_text_from_pdf(pdf_file):
|
| 128 |
"""Extract text from uploaded PDF file"""
|
|
|
|
| 131 |
pdf_file.seek(0)
|
| 132 |
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
| 133 |
text = ""
|
| 134 |
+
# Handle potential decryption errors
|
| 135 |
+
if pdf_reader.is_encrypted:
|
| 136 |
+
try:
|
| 137 |
+
pdf_reader.decrypt('') # Attempt decryption with empty password
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"PDF is encrypted and cannot be decrypted: {e}")
|
| 140 |
+
return "[PDF Content Unavailable: File is encrypted]" # Return a message indicating failure
|
| 141 |
+
|
| 142 |
for page in pdf_reader.pages:
|
| 143 |
page_text = page.extract_text()
|
| 144 |
if page_text:
|
| 145 |
text += page_text + "\n" # Add newline between pages
|
| 146 |
+
return text.strip() or "[No readable text found in PDF]" # Return placeholder if no text
|
| 147 |
except Exception as e:
|
| 148 |
print(f"Error extracting PDF text: {e}")
|
| 149 |
+
return f"[Error extracting PDF text: {e}]" # Include error in placeholder
|
| 150 |
+
|
| 151 |
|
| 152 |
def extract_care_plan_format(pdf_text):
|
| 153 |
"""Extract a general format template from PDF text by identifying common section headers."""
|
| 154 |
+
if not pdf_text or "[No readable text found" in pdf_text or "[Error extracting PDF text" in pdf_text:
|
| 155 |
+
return None # Cannot extract format from empty or error text
|
| 156 |
|
| 157 |
# Look for lines that seem like headers (e.g., capitalized words ending with :, followed by content)
|
| 158 |
# This regex is an attempt to capture common patterns but might not be perfect for all PDFs.
|
|
|
|
| 171 |
format_template = "\n".join([f"{header.strip()}:" for header in set(potential_headers) if header.strip()]) + "\n"
|
| 172 |
print(f"Extracted potential headers (fallback): {list(set(potential_headers))}")
|
| 173 |
return format_template if format_template.strip() else None
|
| 174 |
+
print("No sections or potential headers found in PDF.")
|
| 175 |
return None # No recognizable format found
|
| 176 |
|
| 177 |
format_template = ""
|
|
|
|
| 189 |
updated_plan_lower = updated_plan.lower()
|
| 190 |
|
| 191 |
# Comprehensive keywords indicating condition worsening or emergency
|
| 192 |
+
# Prioritize feedback keywords
|
| 193 |
emergency_keywords = [
|
| 194 |
"severe chest pain", "heart attack", "sudden shortness of breath",
|
| 195 |
"difficulty breathing", "loss of consciousness", "extreme pain",
|
|
|
|
| 200 |
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion",
|
| 201 |
"suffocating", "not breathing", "blue lips", "blue face", "cardiac arrest",
|
| 202 |
"high fever", "signs of shock", "severe dehydration", "acute change",
|
| 203 |
+
"unstable vitals", "rapidly worsening", "can't breathe", "chest tight"
|
| 204 |
]
|
| 205 |
|
| 206 |
deteriorating_keywords = [
|
|
|
|
| 211 |
"weakening", "relapse", "recurrence", "regressing", "not responding",
|
| 212 |
"increased symptoms", "more severe", "progressing", "progressive", "complicated",
|
| 213 |
"adverse change", "unstable", "needs attention", "spike in", "significant increase",
|
| 214 |
+
"new symptoms", "trouble with", "reduced appetite", "difficulty sleeping",
|
| 215 |
+
"tired all the time", "much weaker", "feeling worse"
|
| 216 |
]
|
| 217 |
|
| 218 |
improvement_keywords = [
|
|
|
|
| 222 |
"improved", "enhancement", "advancement", "resolving", "resolved", "recovering",
|
| 223 |
"normalized", "normal range", "responding well", "responding positively",
|
| 224 |
"effective treatment", "successful treatment", "managed well", "under control",
|
| 225 |
+
"symptoms decreased", "feeling stronger", "better sleep", "increased appetite",
|
| 226 |
+
"pain decreased", "more energy", "walking further"
|
| 227 |
]
|
| 228 |
|
| 229 |
+
# Check feedback first
|
| 230 |
if any(keyword in feedback_lower for keyword in emergency_keywords):
|
| 231 |
return "emergency"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
if any(keyword in feedback_lower for keyword in deteriorating_keywords):
|
| 233 |
return "deteriorating"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
if any(keyword in feedback_lower for keyword in improvement_keywords):
|
| 235 |
return "improving"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
+
# Fallback: If no strong indicator in feedback, analyze the updated plan if it exists and differs
|
| 238 |
+
if updated_plan_lower != original_plan_lower:
|
| 239 |
+
if any(keyword in updated_plan_lower for keyword in emergency_keywords):
|
| 240 |
+
return "emergency"
|
| 241 |
+
if any(keyword in updated_plan_lower for keyword in deteriorating_keywords):
|
| 242 |
+
return "deteriorating"
|
| 243 |
+
if any(keyword in updated_plan_lower for keyword in improvement_keywords):
|
| 244 |
+
return "improving"
|
| 245 |
|
| 246 |
# Default to stable if no clear indicators of change are found
|
| 247 |
return "stable"
|
|
|
|
| 337 |
# Patient Information Table
|
| 338 |
patient_data = [
|
| 339 |
[Paragraph("<b>Patient Name:</b>", normal_style), Paragraph(patient_info.get('name', 'N/A'), normal_style)],
|
| 340 |
+
[Paragraph("<b>Age:</b>", normal_style), Paragraph(str(patient_info.get('age', 'N/A')), normal_style)], # Ensure age is string for Paragraph
|
| 341 |
[Paragraph("<b>Gender:</b>", normal_style), Paragraph(patient_info.get('gender', 'N/A'), normal_style)],
|
| 342 |
[Paragraph("<b>Generated Date:</b>", normal_style), Paragraph(datetime.now().strftime("%Y-%m-%d %H:%M"), normal_style)]
|
| 343 |
]
|
| 344 |
|
| 345 |
patient_table = Table(patient_data, colWidths=[150, 360]) # Adjusted column widths
|
| 346 |
patient_table.setStyle(TableStyle([
|
| 347 |
+
('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#f2f2f2")), # Lighter grey
|
| 348 |
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
|
| 349 |
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 350 |
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 351 |
+
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")), # Lighter grid
|
| 352 |
+
('BOX', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")),
|
| 353 |
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
| 354 |
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
| 355 |
('TOPPADDING', (0, 0), (-1, -1), 6), # Increased padding
|
|
|
|
| 364 |
story.append(Spacer(1, 10))
|
| 365 |
|
| 366 |
# Process care plan text by lines or sections for better formatting
|
| 367 |
+
# This logic attempts to render text, sections, and bullet points reasonably.
|
| 368 |
+
# Complex formatting in the AI output might require more sophisticated parsing.
|
| 369 |
lines = care_plan_text.strip().split('\n')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
for line in lines:
|
| 371 |
stripped_line = line.strip()
|
| 372 |
+
if not stripped_line:
|
| 373 |
+
continue # Skip empty lines
|
| 374 |
|
| 375 |
+
# Check for potential section headers (e.g., "SECTION TITLE:")
|
| 376 |
+
header_match = re.match(r'^([A-Z][A-Z\s]*):$', stripped_line)
|
| 377 |
if header_match:
|
| 378 |
+
story.append(Spacer(1, 8)) # Space before new section
|
| 379 |
+
story.append(Paragraph(stripped_line, heading_style))
|
| 380 |
+
# Check for bullet points (handles -, *, •)
|
| 381 |
+
elif stripped_line.startswith('-') or stripped_line.startswith('*') or stripped_line.startswith('•'):
|
| 382 |
+
# Clean the bullet point text
|
| 383 |
+
bullet_text = re.sub(r'^[-*•]\s*', '', line).strip() # Use original line to preserve initial indentation
|
| 384 |
+
if bullet_text:
|
| 385 |
+
story.append(Paragraph(f"• {bullet_text}", bullet_style))
|
| 386 |
+
else: # Handle empty bullet points
|
| 387 |
+
story.append(Paragraph("• ", bullet_style))
|
| 388 |
else:
|
| 389 |
+
# Treat as normal paragraph content
|
| 390 |
+
story.append(Paragraph(line, normal_style)) # Use original line to preserve indentation/spacing within paragraphs
|
| 391 |
|
|
|
|
|
|
|
| 392 |
|
| 393 |
# Add footer or timestamp if desired
|
| 394 |
story.append(Spacer(1, 20))
|
|
|
|
| 397 |
|
| 398 |
|
| 399 |
# Build the PDF
|
| 400 |
+
try:
|
| 401 |
+
doc.build(story)
|
| 402 |
+
buffer.seek(0)
|
| 403 |
+
print("PDF generated successfully.")
|
| 404 |
+
return buffer
|
| 405 |
+
except Exception as e:
|
| 406 |
+
print(f"Error building PDF: {e}")
|
| 407 |
+
# Attempt to create a minimal error PDF
|
| 408 |
+
error_buffer = io.BytesIO()
|
| 409 |
+
c = canvas.Canvas(error_buffer, pagesize=letter)
|
| 410 |
+
c.drawString(100, 750, "Error Generating Care Plan PDF")
|
| 411 |
+
c.drawString(100, 735, f"Details: {str(e)}")
|
| 412 |
+
c.save()
|
| 413 |
+
error_buffer.seek(0)
|
| 414 |
+
return error_buffer
|
| 415 |
|
| 416 |
|
| 417 |
def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
| 418 |
"""Send care plan via WhatsApp using Twilio with improved formatting"""
|
| 419 |
if not twilio_client:
|
| 420 |
+
print("Twilio client not configured. Cannot send WhatsApp message.")
|
| 421 |
return False
|
| 422 |
|
| 423 |
try:
|
|
|
|
| 426 |
'emergency': "🚨 EMERGENCY",
|
| 427 |
'deteriorating': "⚠️ HIGH RISK",
|
| 428 |
'improving': "✅ IMPROVING",
|
| 429 |
+
'stable': "🟦 STABLE",
|
| 430 |
+
'unknown': "⚪ Status: Unknown"
|
| 431 |
}
|
| 432 |
|
| 433 |
+
# Basic sanitization for WhatsApp (avoid excessive newlines, etc.)
|
| 434 |
+
formatted_plan = care_plan_text.strip()
|
| 435 |
+
# Replace multiple newlines with fewer for cleaner WhatsApp message
|
| 436 |
+
formatted_plan = re.sub(r'\n{2,}', '\n\n', formatted_plan)
|
| 437 |
+
# Attempt to make bullet points more visible
|
| 438 |
+
formatted_plan = formatted_plan.replace('- ', '• ').replace('* ', '• ')
|
| 439 |
+
|
| 440 |
+
|
| 441 |
# Format message for WhatsApp including patient details and the full plan text
|
| 442 |
message = f"*Care Plan Update*\n\n"
|
| 443 |
message += f"*Patient Name:* {patient_info.get('name', 'N/A')}\n"
|
| 444 |
message += f"*Age:* {patient_info.get('age', 'N/A')}\n"
|
| 445 |
message += f"*Gender:* {patient_info.get('gender', 'N/A')}\n"
|
| 446 |
message += f"*Status:* {status_emoji.get(status, 'Unknown')}\n\n"
|
| 447 |
+
message += f"*Care Plan Details:*\n{formatted_plan}" # Include the full plan text
|
| 448 |
|
| 449 |
+
print(f"Attempting to send WhatsApp message to {TWILIO_TO}...")
|
| 450 |
# Send WhatsApp message using hardcoded number
|
| 451 |
+
message_sent = twilio_client.messages.create(
|
| 452 |
from_=TWILIO_FROM,
|
| 453 |
body=message,
|
| 454 |
to=TWILIO_TO
|
| 455 |
)
|
| 456 |
+
print(f"WhatsApp message sent, SID: {message_sent.sid}")
|
| 457 |
return True
|
| 458 |
except Exception as e:
|
| 459 |
print(f"Error sending WhatsApp message: {e}")
|
| 460 |
# Twilio error handling can be more specific (e.g., check e.status)
|
| 461 |
+
# Example: If the 'To' number is not registered for WhatsApp
|
| 462 |
return False
|
| 463 |
|
| 464 |
|
|
|
|
| 489 |
return render_template('doctor_dashboard.html')
|
| 490 |
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
@app.route('/submit_feedback', methods=['POST'])
|
| 493 |
def submit_feedback():
|
| 494 |
if not model:
|
| 495 |
+
return jsonify({'success': False, 'error': 'AI model not initialized. Please check API key or server logs.'}), 500
|
| 496 |
|
| 497 |
try:
|
| 498 |
# Get patient information from form
|
| 499 |
name = request.form.get('name', 'Unnamed Patient')
|
| 500 |
+
age = request.form.get('age') # Get as string initially
|
| 501 |
gender = request.form.get('gender', 'N/A')
|
| 502 |
feedback = request.form.get('feedback', '')
|
| 503 |
|
| 504 |
+
# Validate required fields
|
| 505 |
+
if not name or not feedback:
|
| 506 |
+
return jsonify({'success': False, 'error': 'Patient Name and Feedback are required.'}), 400
|
| 507 |
+
if age:
|
| 508 |
+
try:
|
| 509 |
+
age = int(age) # Convert age to integer
|
| 510 |
+
if age <= 0: raise ValueError("Age must be positive")
|
| 511 |
+
except ValueError:
|
| 512 |
+
return jsonify({'success': False, 'error': 'Invalid Age provided.'}), 400
|
| 513 |
+
else:
|
| 514 |
+
age = None # Store as None if not provided or invalid
|
| 515 |
+
|
| 516 |
care_plan_text = ""
|
| 517 |
care_plan_format = None
|
| 518 |
|
|
|
|
| 522 |
if pdf_file and pdf_file.filename != '':
|
| 523 |
# Read the PDF content from the file stream
|
| 524 |
care_plan_text = extract_text_from_pdf(pdf_file)
|
| 525 |
+
if care_plan_text and "[No readable text found" not in care_plan_text and "[Error extracting PDF text" not in care_plan_text:
|
| 526 |
care_plan_format = extract_care_plan_format(care_plan_text)
|
| 527 |
print(f"Extracted text length: {len(care_plan_text)}. Format found: {care_plan_format is not None}")
|
| 528 |
+
else:
|
| 529 |
+
print("No PDF file uploaded or file is empty.")
|
| 530 |
+
|
| 531 |
|
| 532 |
# If no format is found in the PDF, use a default format
|
| 533 |
if not care_plan_format or not care_plan_format.strip():
|
|
|
|
| 537 |
- Name: [Patient Name]
|
| 538 |
- Age: [Age]
|
| 539 |
- Gender: [Gender]
|
|
|
|
| 540 |
ASSESSMENT:
|
| 541 |
- [Summary of patient's current condition based on feedback and previous plan]
|
|
|
|
| 542 |
DAILY CARE PLAN:
|
| 543 |
Morning:
|
| 544 |
- [Morning activities/medications]
|
|
|
|
| 548 |
- [Evening activities/medications]
|
| 549 |
Night:
|
| 550 |
- [Night activities/medications/sleep instructions]
|
|
|
|
| 551 |
MEDICATIONS:
|
| 552 |
- [List of medications, dosage, frequency, and time]
|
|
|
|
| 553 |
DIET AND HYDRATION:
|
| 554 |
- [Dietary recommendations and hydration goals]
|
|
|
|
| 555 |
PHYSICAL ACTIVITY/EXERCISE:
|
| 556 |
- [Recommended physical activities, duration, frequency]
|
|
|
|
| 557 |
SYMPTOM MANAGEMENT:
|
| 558 |
- [Instructions for managing specific symptoms, including what to do if they worsen]
|
|
|
|
| 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 |
ADDITIONAL RECOMMENDATIONS:
|
| 563 |
- [Other relevant advice, e.g., rest, monitoring, specific precautions]
|
|
|
|
| 564 |
FOLLOW-UP:
|
| 565 |
- [Details about next appointment or when to contact healthcare provider]
|
| 566 |
"""
|
| 567 |
else:
|
| 568 |
print("Using extracted care plan format.")
|
| 569 |
|
| 570 |
+
# Determine status BEFORE calling AI, especially for emergency
|
| 571 |
+
initial_status = determine_patient_status(care_plan_text, "", feedback) # Compare against empty updated plan initially
|
| 572 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
|
| 574 |
generated_plan_text = ""
|
| 575 |
+
status = initial_status # Start with status determined from feedback
|
| 576 |
|
| 577 |
+
if status == 'emergency':
|
| 578 |
+
print("Emergency status detected from feedback. Generating emergency plan.")
|
| 579 |
# If emergency symptoms are detected in feedback, generate emergency instructions
|
| 580 |
generated_plan_text = (
|
| 581 |
"PATIENT INFORMATION:\n"
|
| 582 |
f"- Name: {name}\n"
|
| 583 |
+
f"- Age: {age if age is not None else 'N/A'}\n"
|
| 584 |
f"- Gender: {gender}\n\n"
|
| 585 |
"ASSESSMENT:\n"
|
| 586 |
f"- Emergency symptoms reported: {feedback}. Immediate medical attention required.\n\n"
|
|
|
|
| 597 |
"- Inform your primary physician as soon as medically stable.\n"
|
| 598 |
"- Review and update care plan only after emergency situation is resolved and evaluated by medical professionals.\n"
|
| 599 |
)
|
| 600 |
+
# Status remains 'emergency'
|
| 601 |
|
| 602 |
else:
|
| 603 |
# Prepare a prompt for Gemini AI for a perfect, attractively formatted day care plan.
|
|
|
|
| 605 |
prompt = f"""
|
| 606 |
You are a helpful assistant generating updated patient care plans.
|
| 607 |
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.
|
|
|
|
| 608 |
Patient Information:
|
| 609 |
Name: {name}
|
| 610 |
+
Age: {age if age is not None else 'N/A'}
|
| 611 |
Gender: {gender}
|
|
|
|
| 612 |
Patient Feedback/Symptoms Update:
|
| 613 |
{feedback}
|
|
|
|
| 614 |
Previous Care Plan Details (if available):
|
| 615 |
+
{care_plan_text if care_plan_text and "[No readable text found" not in care_plan_text else "No previous care plan provided."}
|
|
|
|
| 616 |
Instructions:
|
| 617 |
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.
|
| 618 |
2. Be specific and actionable in your recommendations.
|
|
|
|
| 620 |
4. Include realistic times for medications if applicable (e.g., "Take 1 tablet in the morning with food").
|
| 621 |
5. Highlight important instructions or warnings, especially regarding symptom management and when to seek help.
|
| 622 |
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.
|
| 623 |
+
7. If the previous care plan text indicates it was unreadable or had errors, acknowledge that and generate a new plan based primarily on the feedback and patient info, using the format template.
|
| 624 |
Care Plan Format Template:
|
| 625 |
{care_plan_format}
|
| 626 |
"""
|
| 627 |
print("Sending prompt to AI model...")
|
| 628 |
+
# print(f"Prompt:\n---\n{prompt}\n---") # Avoid printing large prompts in logs
|
| 629 |
|
| 630 |
# Get response from Gemini AI
|
| 631 |
try:
|
| 632 |
response = model.generate_content(prompt)
|
| 633 |
generated_plan_text = response.text.strip() # Get the text and strip leading/trailing whitespace
|
| 634 |
+
|
| 635 |
+
# Clean up common AI formatting issues (e.g., extra ```)
|
| 636 |
+
if generated_plan_text.startswith('```') and generated_plan_text.endswith('```'):
|
| 637 |
+
generated_plan_text = generated_plan_text[3:-3].strip()
|
| 638 |
+
if generated_plan_text.startswith('text'): # Handle ```text...```
|
| 639 |
+
generated_plan_text = generated_plan_text[4:].strip()
|
| 640 |
+
|
| 641 |
+
|
| 642 |
print(f"AI Response received. Length: {len(generated_plan_text)}")
|
| 643 |
+
# print(f"AI Response (first 200 chars):\n---\n{generated_plan_text[:200]}\n---") # Print truncated response
|
| 644 |
+
|
| 645 |
|
| 646 |
+
# Re-determine patient status based on feedback AND the generated updated plan
|
| 647 |
status = determine_patient_status(care_plan_text, generated_plan_text, feedback)
|
| 648 |
|
| 649 |
except Exception as ai_error:
|
| 650 |
print(f"Error generating content from AI: {ai_error}")
|
| 651 |
+
# Fallback: Use initial status and provide an error message in the plan
|
| 652 |
+
generated_plan_text = f"[Error generating updated plan from AI: {ai_error}]\n\n"
|
| 653 |
+
if care_plan_text and "[No readable text found" not in care_plan_text:
|
| 654 |
+
generated_plan_text += "Showing original plan due to AI error:\n\n" + care_plan_text
|
| 655 |
+
status = determine_patient_status(care_plan_text, care_plan_text, feedback) # Re-evaluate status based on original plan + feedback
|
| 656 |
+
else:
|
| 657 |
+
generated_plan_text += "No previous plan available."
|
| 658 |
+
status = initial_status # Keep the initial status if no original plan
|
| 659 |
+
|
| 660 |
return jsonify({
|
| 661 |
'success': False,
|
| 662 |
+
'error': f'Error generating care plan: {ai_error}',
|
| 663 |
+
'fallback_plan': generated_plan_text, # Optionally send fallback plan
|
| 664 |
+
'patient_info': {'name': name, 'age': age, 'gender': gender},
|
| 665 |
+
'status': status,
|
| 666 |
+
'original_plan': care_plan_text # Include original plan if any
|
| 667 |
+
}), 500 # Return 500 if AI fails, but provide some data
|
| 668 |
+
|
| 669 |
|
| 670 |
# --- Actions after plan generation (Emergency or AI) ---
|
| 671 |
|
| 672 |
+
# Create and store patient record in the database
|
| 673 |
+
new_patient = Patient(
|
| 674 |
+
name=name,
|
| 675 |
+
age=age,
|
| 676 |
+
gender=gender,
|
| 677 |
+
feedback=feedback,
|
| 678 |
+
original_plan=care_plan_text, # Store original text
|
| 679 |
+
updated_plan=generated_plan_text, # Store the generated plan text
|
| 680 |
+
status=status,
|
| 681 |
+
timestamp=datetime.utcnow() # Use UTC time
|
| 682 |
+
)
|
| 683 |
+
db.session.add(new_patient)
|
| 684 |
+
db.session.commit()
|
| 685 |
+
patient_id = new_patient.id # Get the generated ID
|
| 686 |
+
|
| 687 |
+
print(f"Patient {patient_id} added to DB with status: {status}.")
|
| 688 |
|
| 689 |
|
| 690 |
# Generate PDF for downloading using the stored data
|
|
|
|
| 709 |
return jsonify({
|
| 710 |
'success': True,
|
| 711 |
'updated_plan': generated_plan_text, # Send back the generated text
|
| 712 |
+
'pdf_data': pdf_base64, # Send base64 data for preview
|
| 713 |
+
'patient_id': patient_id, # Send the generated patient ID for download
|
| 714 |
'status': status,
|
| 715 |
'whatsapp_sent': whatsapp_sent
|
| 716 |
})
|
| 717 |
|
| 718 |
except Exception as e:
|
| 719 |
+
# Catch any other unexpected errors
|
| 720 |
+
print(f"An unexpected error occurred during submission: {str(e)}")
|
| 721 |
# Log the traceback for debugging
|
| 722 |
import traceback
|
| 723 |
traceback.print_exc()
|
| 724 |
+
# Rollback database session in case of error
|
| 725 |
+
db.session.rollback()
|
| 726 |
return jsonify({
|
| 727 |
'success': False,
|
| 728 |
+
'error': f'An unexpected server error occurred: {str(e)}'
|
| 729 |
}), 500
|
| 730 |
|
| 731 |
|
| 732 |
@app.route('/download_pdf/<patient_id>')
|
| 733 |
def download_pdf(patient_id):
|
| 734 |
print(f"Download requested for patient ID: {patient_id}")
|
|
|
|
| 735 |
|
| 736 |
try:
|
| 737 |
+
# Fetch patient from the database
|
| 738 |
+
patient = Patient.query.get(patient_id)
|
| 739 |
|
| 740 |
if not patient:
|
| 741 |
+
print(f"Patient ID {patient_id} not found in database for download.")
|
| 742 |
+
return "Patient data not found.", 404
|
| 743 |
|
| 744 |
# Use the patient's stored information to regenerate the PDF content
|
| 745 |
patient_info_for_pdf = {
|
| 746 |
+
'name': patient.name,
|
| 747 |
+
'age': patient.age,
|
| 748 |
+
'gender': patient.gender
|
| 749 |
}
|
| 750 |
+
# Use the already determined status and updated plan text from the DB record
|
| 751 |
pdf_buffer = generate_care_plan_pdf(
|
| 752 |
patient_info_for_pdf,
|
| 753 |
+
patient.updated_plan, # Use the stored updated plan
|
| 754 |
+
patient.status # Use the stored status
|
| 755 |
)
|
| 756 |
|
| 757 |
pdf_buffer.seek(0) # Ensure buffer is at the start
|
| 758 |
|
| 759 |
# Sanitize patient name for filename
|
| 760 |
+
safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '', patient.name or 'patient').lower()
|
| 761 |
+
download_name = f"care_plan_{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" # Add timestamp to filename
|
| 762 |
|
| 763 |
print(f"Serving PDF for {patient_id} as {download_name}")
|
| 764 |
return send_file(
|
|
|
|
| 771 |
print(f"PDF Download Error for ID {patient_id}: {str(e)}")
|
| 772 |
import traceback
|
| 773 |
traceback.print_exc()
|
| 774 |
+
return f"Error generating PDF for download: {str(e)}", 500
|
| 775 |
|
| 776 |
|
| 777 |
@app.route('/get_emergency_notifications')
|
| 778 |
def get_emergency_notifications():
|
| 779 |
+
# Filter patients with emergency status directly from the database
|
| 780 |
+
emergency_patients_query = Patient.query.filter_by(status='emergency').order_by(Patient.timestamp.desc())
|
| 781 |
+
|
| 782 |
+
notifications = [p.to_dict() for p in emergency_patients_query.all()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
|
| 784 |
return jsonify({
|
| 785 |
'success': True,
|
|
|
|
| 789 |
|
| 790 |
@app.route('/get_patients')
|
| 791 |
def get_patients():
|
| 792 |
+
# Return all patients for the doctor dashboard from the database
|
| 793 |
+
# Order by timestamp, newest first
|
| 794 |
+
all_patients_query = Patient.query.order_by(Patient.timestamp.desc())
|
| 795 |
+
|
| 796 |
+
# Convert SQLAlchemy objects to dictionaries
|
| 797 |
+
patients_list = [p.to_dict() for p in all_patients_query.all()]
|
| 798 |
+
|
| 799 |
+
# Manual sorting by status priority for display if needed, but JS handles this
|
| 800 |
+
# Or you can add complex ordering to the query if using PostgreSQL etc.
|
| 801 |
+
# For simplicity, JS handles sorting after getting the timestamp-ordered list.
|
| 802 |
|
| 803 |
return jsonify({
|
| 804 |
'success': True,
|
| 805 |
+
'patients': patients_list
|
| 806 |
})
|
| 807 |
|
| 808 |
|
| 809 |
@app.route('/get_patient/<patient_id>')
|
| 810 |
def get_patient(patient_id):
|
| 811 |
print(f"API request for patient ID: {patient_id}")
|
|
|
|
| 812 |
|
| 813 |
+
# Fetch patient from the database
|
| 814 |
+
patient = Patient.query.get(patient_id)
|
| 815 |
|
| 816 |
if not patient:
|
| 817 |
+
print(f"Patient ID {patient_id} not found in database for API request.")
|
| 818 |
+
return jsonify({'success': False, 'error': 'Patient not found.'}), 404
|
| 819 |
|
| 820 |
+
print(f"Found patient {patient_id}: {patient.name}")
|
| 821 |
return jsonify({
|
| 822 |
'success': True,
|
| 823 |
+
'patient': patient.to_dict() # Return dictionary representation
|
| 824 |
})
|
| 825 |
|
| 826 |
+
# Helper to serve PDF file directly (used by doctor dashboard PDF viewer)
|
| 827 |
+
# Note: This route is potentially less secure than storing files on disk and serving them
|
| 828 |
+
# or generating on demand. But for a demo, it works with the base64 data flow.
|
| 829 |
+
# A better approach for large PDFs or production would be saving the PDF server-side
|
| 830 |
+
# and serving a URL that points to the saved file, or using a dedicated PDF viewer lib.
|
| 831 |
+
# For this fix, let's stick to serving the *generated* PDF data via download link,
|
| 832 |
+
# and rely on the browser's embedding for the patient view preview on initial submit.
|
| 833 |
+
# Doctor dashboard will just show the text content in tabs.
|
| 834 |
+
|
| 835 |
+
# Removed the direct PDF view route as it's problematic.
|
| 836 |
+
# The doctor dashboard will display the text content in the tabs.
|
| 837 |
+
|
| 838 |
|
| 839 |
if __name__ == '__main__':
|
| 840 |
# Use a more robust development server like Waitress or Gunicorn in production
|
| 841 |
+
# Ensure database tables are created before running the app
|
| 842 |
+
with app.app_context():
|
| 843 |
+
db.create_all()
|
| 844 |
+
app.run(debug=True) # Explicitly set port
|