Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_file
|
| 2 |
-
from flask_sqlalchemy import SQLAlchemy
|
| 3 |
import google.generativeai as genai
|
| 4 |
import PyPDF2
|
| 5 |
import os
|
|
@@ -16,13 +16,15 @@ from reportlab.lib import colors
|
|
| 16 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 17 |
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
| 18 |
|
|
|
|
| 19 |
app = Flask(__name__, instance_path='/tmp')
|
| 20 |
|
| 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 |
|
|
@@ -53,6 +55,7 @@ class Patient(db.Model):
|
|
| 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 |
|
|
@@ -62,64 +65,56 @@ 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 |
-
|
| 68 |
app.config['UPLOAD_FOLDER'] = upload_folder
|
| 69 |
-
|
| 70 |
-
# Ensure the folder exists at runtime
|
| 71 |
os.makedirs(upload_folder, exist_ok=True)
|
| 72 |
|
|
|
|
| 73 |
# Twilio Configuration
|
| 74 |
-
# IMPORTANT: Use environment variables for sensitive information like SID and Auth Token
|
| 75 |
ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'AC490e071f8d01bf0df2f03d086c788d87')
|
| 76 |
AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '224b23b950ad5a4052aba15893fdf083')
|
| 77 |
TWILIO_FROM = os.getenv('TWILIO_FROM_NUMBER', 'whatsapp:+14155238886')
|
| 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,
|
| 107 |
-
"top_p": 0.9,
|
| 108 |
-
"top_k": 30,
|
| 109 |
-
"max_output_tokens": 4096,
|
| 110 |
}
|
| 111 |
|
| 112 |
model = genai.GenerativeModel(
|
| 113 |
-
model_name="gemini-1.5-flash-latest",
|
| 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
|
| 123 |
else:
|
| 124 |
print("GENAI_API_KEY environment variable not set. AI generation disabled.")
|
| 125 |
|
|
@@ -127,36 +122,31 @@ else:
|
|
| 127 |
def extract_text_from_pdf(pdf_file):
|
| 128 |
"""Extract text from uploaded PDF file"""
|
| 129 |
try:
|
| 130 |
-
# Ensure file pointer is at the beginning
|
| 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('')
|
| 138 |
except Exception as e:
|
| 139 |
print(f"PDF is encrypted and cannot be decrypted: {e}")
|
| 140 |
-
return "[PDF Content Unavailable: File is encrypted]"
|
| 141 |
|
| 142 |
for page in pdf_reader.pages:
|
| 143 |
page_text = page.extract_text()
|
| 144 |
if page_text:
|
| 145 |
-
text += page_text + "\n"
|
| 146 |
-
return text.strip() or "[No readable text found in PDF]"
|
| 147 |
except Exception as e:
|
| 148 |
print(f"Error extracting PDF text: {e}")
|
| 149 |
-
return f"[Error extracting PDF text: {e}]"
|
| 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
|
| 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.
|
| 159 |
-
# It looks for uppercase words (potentially with spaces), followed by a colon, then captures subsequent lines until another similar pattern or end.
|
| 160 |
sections = re.findall(
|
| 161 |
r'^([A-Z][A-Z\s]*):(?:\s*\n)?((?:(?!\n[A-Z][A-Z\s]*:).*\n?)*)',
|
| 162 |
pdf_text,
|
|
@@ -164,19 +154,17 @@ def extract_care_plan_format(pdf_text):
|
|
| 164 |
)
|
| 165 |
|
| 166 |
if not sections:
|
| 167 |
-
# Fallback: If strict section matching fails, try to find prominent lines as potential headers
|
| 168 |
potential_headers = re.findall(r'^[A-Z][A-Za-z\s,]*[.:]?$', pdf_text, re.MULTILINE)
|
| 169 |
if potential_headers:
|
| 170 |
-
# Just list the potential headers as a basic format guess
|
| 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
|
| 176 |
|
| 177 |
format_template = ""
|
| 178 |
for section_title, _ in sections:
|
| 179 |
-
format_template += f"{section_title.strip()}:\n- [Details]\n"
|
| 180 |
print(f"Extracted sections: {[s[0].strip() for s in sections]}")
|
| 181 |
return format_template if format_template.strip() else None
|
| 182 |
|
|
@@ -188,8 +176,6 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 188 |
original_plan_lower = original_plan.lower() if original_plan else ""
|
| 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",
|
|
@@ -226,7 +212,6 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 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):
|
|
@@ -234,7 +219,6 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 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"
|
|
@@ -243,7 +227,6 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 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"
|
| 248 |
|
| 249 |
|
|
@@ -251,58 +234,42 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 251 |
"""Generate a PDF of the care plan with improved styling"""
|
| 252 |
buffer = io.BytesIO()
|
| 253 |
|
| 254 |
-
# Create a PDF document
|
| 255 |
doc = SimpleDocTemplate(buffer, pagesize=letter,
|
| 256 |
leftMargin=72, rightMargin=72,
|
| 257 |
-
topMargin=72, bottomMargin=72)
|
| 258 |
|
| 259 |
styles = getSampleStyleSheet()
|
| 260 |
|
| 261 |
-
# Create custom styles
|
| 262 |
title_style = ParagraphStyle(
|
| 263 |
'Title',
|
| 264 |
parent=styles['Heading1'],
|
| 265 |
-
fontSize=22,
|
| 266 |
-
alignment=1, # Center alignment
|
| 267 |
-
spaceAfter=25, # More space after title
|
| 268 |
-
textColor=colors.HexColor("#4e73df") # Primary color
|
| 269 |
)
|
| 270 |
|
| 271 |
heading_style = ParagraphStyle(
|
| 272 |
'Heading',
|
| 273 |
parent=styles['Heading2'],
|
| 274 |
-
fontSize=15,
|
| 275 |
-
spaceAfter=8, # Less space after heading for tighter look
|
| 276 |
-
spaceBefore=18, # More space before heading
|
| 277 |
-
textColor=colors.HexColor("#1cc88a") # Secondary color
|
| 278 |
)
|
| 279 |
|
| 280 |
normal_style = ParagraphStyle(
|
| 281 |
'Normal',
|
| 282 |
parent=styles['Normal'],
|
| 283 |
-
fontSize=11,
|
| 284 |
-
spaceAfter=6, # More space after paragraphs
|
| 285 |
-
leading=14,
|
| 286 |
-
textColor=colors.HexColor("#5a5c69") # Dark color
|
| 287 |
)
|
| 288 |
|
| 289 |
bullet_style = ParagraphStyle(
|
| 290 |
'Bullet',
|
| 291 |
parent=styles['Normal'],
|
| 292 |
-
fontSize=11,
|
| 293 |
-
spaceAfter=3,
|
| 294 |
-
leftIndent=20,
|
| 295 |
-
leading=14,
|
| 296 |
-
bulletIndent=10, # Space between bullet and text
|
| 297 |
-
textColor=colors.HexColor("#5a5c69") # Dark color
|
| 298 |
)
|
| 299 |
|
| 300 |
-
# Status color mapping
|
| 301 |
status_colors = {
|
| 302 |
-
'emergency': colors.HexColor("#e74a3b"),
|
| 303 |
-
'deteriorating': colors.HexColor("#f6c23e"),
|
| 304 |
-
'improving': colors.HexColor("#1cc88a"),
|
| 305 |
-
'stable': colors.HexColor("#36b9cc")
|
|
|
|
| 306 |
}
|
| 307 |
|
| 308 |
status_text_styles = {
|
|
@@ -313,14 +280,10 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 313 |
'unknown': ParagraphStyle('StatusUnknown', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=colors.black, alignment=1, fontName='Helvetica-Bold'),
|
| 314 |
}
|
| 315 |
|
| 316 |
-
|
| 317 |
-
# Build the document content
|
| 318 |
story = []
|
| 319 |
|
| 320 |
-
# Title
|
| 321 |
story.append(Paragraph("Patient Care Plan", title_style))
|
| 322 |
|
| 323 |
-
# Status indicator
|
| 324 |
status_map_text = {
|
| 325 |
'emergency': "EMERGENCY - IMMEDIATE ACTION REQUIRED",
|
| 326 |
'deteriorating': "HIGH RISK - Condition Deteriorating",
|
|
@@ -333,70 +296,56 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 333 |
|
| 334 |
story.append(Paragraph(current_status_text, current_status_style))
|
| 335 |
|
| 336 |
-
|
| 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)],
|
| 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])
|
| 346 |
patient_table.setStyle(TableStyle([
|
| 347 |
-
('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#f2f2f2")),
|
| 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")),
|
| 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),
|
| 356 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
| 357 |
]))
|
| 358 |
|
| 359 |
story.append(patient_table)
|
| 360 |
-
story.append(Spacer(1, 25))
|
| 361 |
|
| 362 |
-
# Care Plan Content
|
| 363 |
story.append(Paragraph("Care Plan Details:", heading_style))
|
| 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
|
| 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))
|
| 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 |
-
|
| 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:
|
| 387 |
story.append(Paragraph("• ", bullet_style))
|
| 388 |
else:
|
| 389 |
-
|
| 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))
|
| 395 |
footer_style = ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, alignment=1, textColor=colors.grey)
|
| 396 |
story.append(Paragraph(f"Generated by Patient Care Management System on {datetime.now().strftime('%Y-%m-%d')}", footer_style))
|
| 397 |
|
| 398 |
-
|
| 399 |
-
# Build the PDF
|
| 400 |
try:
|
| 401 |
doc.build(story)
|
| 402 |
buffer.seek(0)
|
|
@@ -404,7 +353,6 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 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")
|
|
@@ -421,7 +369,6 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 421 |
return False
|
| 422 |
|
| 423 |
try:
|
| 424 |
-
# Status emoji mapping
|
| 425 |
status_emoji = {
|
| 426 |
'emergency': "🚨 EMERGENCY",
|
| 427 |
'deteriorating': "⚠️ HIGH RISK",
|
|
@@ -430,24 +377,18 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 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}"
|
| 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,
|
|
@@ -457,19 +398,14 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 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 |
|
| 465 |
@app.route('/')
|
| 466 |
def index():
|
| 467 |
-
# Determine current role from session or default to patient
|
| 468 |
role = session.get('role', 'patient')
|
| 469 |
if role == 'doctor':
|
| 470 |
-
# If role is doctor, redirect to doctor dashboard
|
| 471 |
return redirect(url_for('doctor_dashboard'))
|
| 472 |
-
# Otherwise, render the patient index page
|
| 473 |
return render_template('index.html')
|
| 474 |
|
| 475 |
|
|
@@ -484,8 +420,6 @@ def switch_role():
|
|
| 484 |
|
| 485 |
@app.route('/doctor_dashboard')
|
| 486 |
def doctor_dashboard():
|
| 487 |
-
# This route will render the dashboard template
|
| 488 |
-
# The patient list will be loaded via AJAX by the JavaScript
|
| 489 |
return render_template('doctor_dashboard.html')
|
| 490 |
|
| 491 |
|
|
@@ -495,32 +429,28 @@ def submit_feedback():
|
|
| 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')
|
| 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)
|
| 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
|
| 515 |
|
| 516 |
care_plan_text = ""
|
| 517 |
care_plan_format = None
|
| 518 |
|
| 519 |
-
# Handle PDF file upload
|
| 520 |
if 'care_plan_pdf' in request.files:
|
| 521 |
pdf_file = request.files['care_plan_pdf']
|
| 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)
|
|
@@ -529,7 +459,6 @@ def submit_feedback():
|
|
| 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():
|
| 534 |
print("Using default care plan format.")
|
| 535 |
care_plan_format = """
|
|
@@ -567,16 +496,13 @@ FOLLOW-UP:
|
|
| 567 |
else:
|
| 568 |
print("Using extracted care plan format.")
|
| 569 |
|
| 570 |
-
|
| 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
|
| 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"
|
|
@@ -597,11 +523,8 @@ FOLLOW-UP:
|
|
| 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.
|
| 604 |
-
# Include patient info directly in the prompt for context
|
| 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.
|
|
@@ -612,7 +535,7 @@ 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.
|
|
@@ -625,49 +548,40 @@ 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()
|
| 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'):
|
| 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 |
-
#
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 656 |
else:
|
| 657 |
generated_plan_text += "No previous plan available."
|
| 658 |
-
|
| 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(
|
|
@@ -675,18 +589,17 @@ Care Plan Format Template:
|
|
| 675 |
age=age,
|
| 676 |
gender=gender,
|
| 677 |
feedback=feedback,
|
| 678 |
-
original_plan=care_plan_text,
|
| 679 |
-
updated_plan=generated_plan_text,
|
| 680 |
status=status,
|
| 681 |
-
timestamp=datetime.utcnow()
|
| 682 |
)
|
| 683 |
db.session.add(new_patient)
|
| 684 |
db.session.commit()
|
| 685 |
-
patient_id = new_patient.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
|
| 691 |
patient_info_for_pdf = {
|
| 692 |
'name': name,
|
|
@@ -706,22 +619,22 @@ Care Plan Format Template:
|
|
| 706 |
whatsapp_sent = send_whatsapp_care_plan(patient_info_for_whatsapp, generated_plan_text, status)
|
| 707 |
print(f"WhatsApp message attempt sent: {whatsapp_sent}")
|
| 708 |
|
|
|
|
| 709 |
return jsonify({
|
| 710 |
'success': True,
|
| 711 |
-
'updated_plan': generated_plan_text,
|
| 712 |
-
'pdf_data': pdf_base64, #
|
| 713 |
-
'patient_id': patient_id,
|
| 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,
|
|
@@ -734,31 +647,27 @@ 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)
|
| 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"
|
| 762 |
|
| 763 |
print(f"Serving PDF for {patient_id} as {download_name}")
|
| 764 |
return send_file(
|
|
@@ -776,7 +685,6 @@ def download_pdf(patient_id):
|
|
| 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()]
|
|
@@ -789,17 +697,8 @@ def get_emergency_notifications():
|
|
| 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
|
|
@@ -809,8 +708,6 @@ def get_patients():
|
|
| 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:
|
|
@@ -820,25 +717,116 @@ def get_patient(patient_id):
|
|
| 820 |
print(f"Found patient {patient_id}: {patient.name}")
|
| 821 |
return jsonify({
|
| 822 |
'success': True,
|
| 823 |
-
'patient': patient.to_dict()
|
| 824 |
})
|
| 825 |
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 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 |
-
|
| 836 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
| 1 |
from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_file
|
| 2 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 3 |
import google.generativeai as genai
|
| 4 |
import PyPDF2
|
| 5 |
import os
|
|
|
|
| 16 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 17 |
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
| 18 |
|
| 19 |
+
# Explicitly set instance_path for writable storage
|
| 20 |
app = Flask(__name__, instance_path='/tmp')
|
| 21 |
|
| 22 |
# Use a strong, unique secret key
|
| 23 |
app.secret_key = os.getenv('SECRET_KEY', '688ed745a74bdd7ac238f5b50f4104fb87d6774b8b0a4e06e7e18ac5ed0fa31c') # CHANGE THIS IN PRODUCTION
|
| 24 |
|
| 25 |
# Database Configuration
|
| 26 |
+
# Use SQLite for simplicity, store in /tmp (instance_path)
|
| 27 |
+
# Flask-SQLAlchemy interprets 'sqlite:///patients.db' relative to instance_path if specified
|
| 28 |
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///patients.db')
|
| 29 |
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
| 30 |
|
|
|
|
| 55 |
'original_plan': self.original_plan,
|
| 56 |
'updated_plan': self.updated_plan,
|
| 57 |
'status': self.status,
|
| 58 |
+
# Format timestamp as string for JSON response
|
| 59 |
'timestamp': self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp else None
|
| 60 |
}
|
| 61 |
|
|
|
|
| 65 |
db.create_all()
|
| 66 |
|
| 67 |
|
| 68 |
+
# Upload folder configuration (used by patient view for initial PDF processing if needed,
|
| 69 |
+
# but not for long-term storage - data is in DB text fields)
|
| 70 |
upload_base = os.getenv('UPLOAD_DIR', '/tmp/uploads')
|
| 71 |
upload_folder = os.path.join(upload_base, 'pdfs')
|
|
|
|
| 72 |
app.config['UPLOAD_FOLDER'] = upload_folder
|
|
|
|
|
|
|
| 73 |
os.makedirs(upload_folder, exist_ok=True)
|
| 74 |
|
| 75 |
+
|
| 76 |
# Twilio Configuration
|
|
|
|
| 77 |
ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'AC490e071f8d01bf0df2f03d086c788d87')
|
| 78 |
AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '224b23b950ad5a4052aba15893fdf083')
|
| 79 |
TWILIO_FROM = os.getenv('TWILIO_FROM_NUMBER', 'whatsapp:+14155238886')
|
| 80 |
TWILIO_TO = os.getenv('TWILIO_TO_NUMBER', 'whatsapp:+917559355282') # Hardcoded number as requested
|
| 81 |
|
|
|
|
| 82 |
twilio_client = None
|
| 83 |
if ACCOUNT_SID and AUTH_TOKEN and TWILIO_FROM and TWILIO_TO:
|
| 84 |
try:
|
| 85 |
twilio_client = Client(ACCOUNT_SID, AUTH_TOKEN)
|
|
|
|
|
|
|
| 86 |
print("Twilio client initialized successfully.")
|
| 87 |
except Exception as e:
|
| 88 |
print(f"Error initializing Twilio client (check SID/Token/Network): {e}")
|
| 89 |
+
twilio_client = None
|
| 90 |
else:
|
| 91 |
print("Twilio environment variables not set. WhatsApp sending disabled.")
|
| 92 |
|
| 93 |
|
| 94 |
# Gemini API Configuration
|
|
|
|
| 95 |
GENAI_API_KEY = os.getenv('GENAI_API_KEY', "AIzaSyD54ejbjVIVa-F3aD_Urnp8m1EFLUGR__I") # CHANGE THIS KEY
|
| 96 |
model = None
|
| 97 |
if GENAI_API_KEY:
|
| 98 |
try:
|
| 99 |
genai.configure(api_key=GENAI_API_KEY)
|
|
|
|
|
|
|
| 100 |
print("Gemini API configured successfully.")
|
| 101 |
|
| 102 |
generation_config = {
|
| 103 |
+
"temperature": 0.8,
|
| 104 |
+
"top_p": 0.9,
|
| 105 |
+
"top_k": 30,
|
| 106 |
+
"max_output_tokens": 4096,
|
| 107 |
}
|
| 108 |
|
| 109 |
model = genai.GenerativeModel(
|
| 110 |
+
model_name="gemini-1.5-flash-latest",
|
| 111 |
generation_config=generation_config,
|
| 112 |
)
|
|
|
|
|
|
|
| 113 |
print(f"Using Gemini model: {model.model_name}")
|
| 114 |
|
| 115 |
except Exception as e:
|
| 116 |
print(f"Error configuring Gemini API or loading model: {e}")
|
| 117 |
+
model = None
|
| 118 |
else:
|
| 119 |
print("GENAI_API_KEY environment variable not set. AI generation disabled.")
|
| 120 |
|
|
|
|
| 122 |
def extract_text_from_pdf(pdf_file):
|
| 123 |
"""Extract text from uploaded PDF file"""
|
| 124 |
try:
|
|
|
|
| 125 |
pdf_file.seek(0)
|
| 126 |
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
| 127 |
text = ""
|
|
|
|
| 128 |
if pdf_reader.is_encrypted:
|
| 129 |
try:
|
| 130 |
+
pdf_reader.decrypt('')
|
| 131 |
except Exception as e:
|
| 132 |
print(f"PDF is encrypted and cannot be decrypted: {e}")
|
| 133 |
+
return "[PDF Content Unavailable: File is encrypted]"
|
| 134 |
|
| 135 |
for page in pdf_reader.pages:
|
| 136 |
page_text = page.extract_text()
|
| 137 |
if page_text:
|
| 138 |
+
text += page_text + "\n"
|
| 139 |
+
return text.strip() or "[No readable text found in PDF]"
|
| 140 |
except Exception as e:
|
| 141 |
print(f"Error extracting PDF text: {e}")
|
| 142 |
+
return f"[Error extracting PDF text: {e}]"
|
| 143 |
|
| 144 |
|
| 145 |
def extract_care_plan_format(pdf_text):
|
| 146 |
"""Extract a general format template from PDF text by identifying common section headers."""
|
| 147 |
if not pdf_text or "[No readable text found" in pdf_text or "[Error extracting PDF text" in pdf_text:
|
| 148 |
+
return None
|
| 149 |
|
|
|
|
|
|
|
|
|
|
| 150 |
sections = re.findall(
|
| 151 |
r'^([A-Z][A-Z\s]*):(?:\s*\n)?((?:(?!\n[A-Z][A-Z\s]*:).*\n?)*)',
|
| 152 |
pdf_text,
|
|
|
|
| 154 |
)
|
| 155 |
|
| 156 |
if not sections:
|
|
|
|
| 157 |
potential_headers = re.findall(r'^[A-Z][A-Za-z\s,]*[.:]?$', pdf_text, re.MULTILINE)
|
| 158 |
if potential_headers:
|
|
|
|
| 159 |
format_template = "\n".join([f"{header.strip()}:" for header in set(potential_headers) if header.strip()]) + "\n"
|
| 160 |
print(f"Extracted potential headers (fallback): {list(set(potential_headers))}")
|
| 161 |
return format_template if format_template.strip() else None
|
| 162 |
print("No sections or potential headers found in PDF.")
|
| 163 |
+
return None
|
| 164 |
|
| 165 |
format_template = ""
|
| 166 |
for section_title, _ in sections:
|
| 167 |
+
format_template += f"{section_title.strip()}:\n- [Details]\n"
|
| 168 |
print(f"Extracted sections: {[s[0].strip() for s in sections]}")
|
| 169 |
return format_template if format_template.strip() else None
|
| 170 |
|
|
|
|
| 176 |
original_plan_lower = original_plan.lower() if original_plan else ""
|
| 177 |
updated_plan_lower = updated_plan.lower()
|
| 178 |
|
|
|
|
|
|
|
| 179 |
emergency_keywords = [
|
| 180 |
"severe chest pain", "heart attack", "sudden shortness of breath",
|
| 181 |
"difficulty breathing", "loss of consciousness", "extreme pain",
|
|
|
|
| 212 |
"pain decreased", "more energy", "walking further"
|
| 213 |
]
|
| 214 |
|
|
|
|
| 215 |
if any(keyword in feedback_lower for keyword in emergency_keywords):
|
| 216 |
return "emergency"
|
| 217 |
if any(keyword in feedback_lower for keyword in deteriorating_keywords):
|
|
|
|
| 219 |
if any(keyword in feedback_lower for keyword in improvement_keywords):
|
| 220 |
return "improving"
|
| 221 |
|
|
|
|
| 222 |
if updated_plan_lower != original_plan_lower:
|
| 223 |
if any(keyword in updated_plan_lower for keyword in emergency_keywords):
|
| 224 |
return "emergency"
|
|
|
|
| 227 |
if any(keyword in updated_plan_lower for keyword in improvement_keywords):
|
| 228 |
return "improving"
|
| 229 |
|
|
|
|
| 230 |
return "stable"
|
| 231 |
|
| 232 |
|
|
|
|
| 234 |
"""Generate a PDF of the care plan with improved styling"""
|
| 235 |
buffer = io.BytesIO()
|
| 236 |
|
|
|
|
| 237 |
doc = SimpleDocTemplate(buffer, pagesize=letter,
|
| 238 |
leftMargin=72, rightMargin=72,
|
| 239 |
+
topMargin=72, bottomMargin=72)
|
| 240 |
|
| 241 |
styles = getSampleStyleSheet()
|
| 242 |
|
|
|
|
| 243 |
title_style = ParagraphStyle(
|
| 244 |
'Title',
|
| 245 |
parent=styles['Heading1'],
|
| 246 |
+
fontSize=22, alignment=1, spaceAfter=25, textColor=colors.HexColor("#4e73df")
|
|
|
|
|
|
|
|
|
|
| 247 |
)
|
| 248 |
|
| 249 |
heading_style = ParagraphStyle(
|
| 250 |
'Heading',
|
| 251 |
parent=styles['Heading2'],
|
| 252 |
+
fontSize=15, spaceAfter=8, spaceBefore=18, textColor=colors.HexColor("#1cc88a")
|
|
|
|
|
|
|
|
|
|
| 253 |
)
|
| 254 |
|
| 255 |
normal_style = ParagraphStyle(
|
| 256 |
'Normal',
|
| 257 |
parent=styles['Normal'],
|
| 258 |
+
fontSize=11, spaceAfter=6, leading=14, textColor=colors.HexColor("#5a5c69")
|
|
|
|
|
|
|
|
|
|
| 259 |
)
|
| 260 |
|
| 261 |
bullet_style = ParagraphStyle(
|
| 262 |
'Bullet',
|
| 263 |
parent=styles['Normal'],
|
| 264 |
+
fontSize=11, spaceAfter=3, leftIndent=20, leading=14, bulletIndent=10, textColor=colors.HexColor("#5a5c69")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
)
|
| 266 |
|
|
|
|
| 267 |
status_colors = {
|
| 268 |
+
'emergency': colors.HexColor("#e74a3b"),
|
| 269 |
+
'deteriorating': colors.HexColor("#f6c23e"),
|
| 270 |
+
'improving': colors.HexColor("#1cc88a"),
|
| 271 |
+
'stable': colors.HexColor("#36b9cc"),
|
| 272 |
+
'unknown': colors.black
|
| 273 |
}
|
| 274 |
|
| 275 |
status_text_styles = {
|
|
|
|
| 280 |
'unknown': ParagraphStyle('StatusUnknown', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=colors.black, alignment=1, fontName='Helvetica-Bold'),
|
| 281 |
}
|
| 282 |
|
|
|
|
|
|
|
| 283 |
story = []
|
| 284 |
|
|
|
|
| 285 |
story.append(Paragraph("Patient Care Plan", title_style))
|
| 286 |
|
|
|
|
| 287 |
status_map_text = {
|
| 288 |
'emergency': "EMERGENCY - IMMEDIATE ACTION REQUIRED",
|
| 289 |
'deteriorating': "HIGH RISK - Condition Deteriorating",
|
|
|
|
| 296 |
|
| 297 |
story.append(Paragraph(current_status_text, current_status_style))
|
| 298 |
|
|
|
|
|
|
|
| 299 |
patient_data = [
|
| 300 |
[Paragraph("<b>Patient Name:</b>", normal_style), Paragraph(patient_info.get('name', 'N/A'), normal_style)],
|
| 301 |
+
[Paragraph("<b>Age:</b>", normal_style), Paragraph(str(patient_info.get('age', 'N/A')), normal_style)],
|
| 302 |
[Paragraph("<b>Gender:</b>", normal_style), Paragraph(patient_info.get('gender', 'N/A'), normal_style)],
|
| 303 |
[Paragraph("<b>Generated Date:</b>", normal_style), Paragraph(datetime.now().strftime("%Y-%m-%d %H:%M"), normal_style)]
|
| 304 |
]
|
| 305 |
|
| 306 |
+
patient_table = Table(patient_data, colWidths=[150, 360])
|
| 307 |
patient_table.setStyle(TableStyle([
|
| 308 |
+
('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#f2f2f2")),
|
| 309 |
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
|
| 310 |
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 311 |
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 312 |
+
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")),
|
| 313 |
('BOX', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")),
|
| 314 |
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
| 315 |
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
| 316 |
+
('TOPPADDING', (0, 0), (-1, -1), 6),
|
| 317 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
| 318 |
]))
|
| 319 |
|
| 320 |
story.append(patient_table)
|
| 321 |
+
story.append(Spacer(1, 25))
|
| 322 |
|
|
|
|
| 323 |
story.append(Paragraph("Care Plan Details:", heading_style))
|
| 324 |
story.append(Spacer(1, 10))
|
| 325 |
|
|
|
|
|
|
|
|
|
|
| 326 |
lines = care_plan_text.strip().split('\n')
|
| 327 |
for line in lines:
|
| 328 |
stripped_line = line.strip()
|
| 329 |
if not stripped_line:
|
| 330 |
+
continue
|
| 331 |
|
|
|
|
| 332 |
header_match = re.match(r'^([A-Z][A-Z\s]*):$', stripped_line)
|
| 333 |
if header_match:
|
| 334 |
+
story.append(Spacer(1, 8))
|
| 335 |
story.append(Paragraph(stripped_line, heading_style))
|
|
|
|
| 336 |
elif stripped_line.startswith('-') or stripped_line.startswith('*') or stripped_line.startswith('•'):
|
| 337 |
+
bullet_text = re.sub(r'^[-*•]\s*', '', line).strip()
|
|
|
|
| 338 |
if bullet_text:
|
| 339 |
story.append(Paragraph(f"• {bullet_text}", bullet_style))
|
| 340 |
+
else:
|
| 341 |
story.append(Paragraph("• ", bullet_style))
|
| 342 |
else:
|
| 343 |
+
story.append(Paragraph(line, normal_style))
|
|
|
|
| 344 |
|
|
|
|
|
|
|
| 345 |
story.append(Spacer(1, 20))
|
| 346 |
footer_style = ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, alignment=1, textColor=colors.grey)
|
| 347 |
story.append(Paragraph(f"Generated by Patient Care Management System on {datetime.now().strftime('%Y-%m-%d')}", footer_style))
|
| 348 |
|
|
|
|
|
|
|
| 349 |
try:
|
| 350 |
doc.build(story)
|
| 351 |
buffer.seek(0)
|
|
|
|
| 353 |
return buffer
|
| 354 |
except Exception as e:
|
| 355 |
print(f"Error building PDF: {e}")
|
|
|
|
| 356 |
error_buffer = io.BytesIO()
|
| 357 |
c = canvas.Canvas(error_buffer, pagesize=letter)
|
| 358 |
c.drawString(100, 750, "Error Generating Care Plan PDF")
|
|
|
|
| 369 |
return False
|
| 370 |
|
| 371 |
try:
|
|
|
|
| 372 |
status_emoji = {
|
| 373 |
'emergency': "🚨 EMERGENCY",
|
| 374 |
'deteriorating': "⚠️ HIGH RISK",
|
|
|
|
| 377 |
'unknown': "⚪ Status: Unknown"
|
| 378 |
}
|
| 379 |
|
|
|
|
| 380 |
formatted_plan = care_plan_text.strip()
|
|
|
|
| 381 |
formatted_plan = re.sub(r'\n{2,}', '\n\n', formatted_plan)
|
|
|
|
| 382 |
formatted_plan = formatted_plan.replace('- ', '• ').replace('* ', '• ')
|
| 383 |
|
|
|
|
|
|
|
| 384 |
message = f"*Care Plan Update*\n\n"
|
| 385 |
message += f"*Patient Name:* {patient_info.get('name', 'N/A')}\n"
|
| 386 |
+
message += f"*Age:* {patient_info.get('age', 'N/A')}\n" # Ensure age is string
|
| 387 |
message += f"*Gender:* {patient_info.get('gender', 'N/A')}\n"
|
| 388 |
message += f"*Status:* {status_emoji.get(status, 'Unknown')}\n\n"
|
| 389 |
+
message += f"*Care Plan Details:*\n{formatted_plan}"
|
| 390 |
|
| 391 |
print(f"Attempting to send WhatsApp message to {TWILIO_TO}...")
|
|
|
|
| 392 |
message_sent = twilio_client.messages.create(
|
| 393 |
from_=TWILIO_FROM,
|
| 394 |
body=message,
|
|
|
|
| 398 |
return True
|
| 399 |
except Exception as e:
|
| 400 |
print(f"Error sending WhatsApp message: {e}")
|
|
|
|
|
|
|
| 401 |
return False
|
| 402 |
|
| 403 |
|
| 404 |
@app.route('/')
|
| 405 |
def index():
|
|
|
|
| 406 |
role = session.get('role', 'patient')
|
| 407 |
if role == 'doctor':
|
|
|
|
| 408 |
return redirect(url_for('doctor_dashboard'))
|
|
|
|
| 409 |
return render_template('index.html')
|
| 410 |
|
| 411 |
|
|
|
|
| 420 |
|
| 421 |
@app.route('/doctor_dashboard')
|
| 422 |
def doctor_dashboard():
|
|
|
|
|
|
|
| 423 |
return render_template('doctor_dashboard.html')
|
| 424 |
|
| 425 |
|
|
|
|
| 429 |
return jsonify({'success': False, 'error': 'AI model not initialized. Please check API key or server logs.'}), 500
|
| 430 |
|
| 431 |
try:
|
|
|
|
| 432 |
name = request.form.get('name', 'Unnamed Patient')
|
| 433 |
+
age = request.form.get('age')
|
| 434 |
gender = request.form.get('gender', 'N/A')
|
| 435 |
feedback = request.form.get('feedback', '')
|
| 436 |
|
|
|
|
| 437 |
if not name or not feedback:
|
| 438 |
return jsonify({'success': False, 'error': 'Patient Name and Feedback are required.'}), 400
|
| 439 |
if age:
|
| 440 |
try:
|
| 441 |
+
age = int(age)
|
| 442 |
if age <= 0: raise ValueError("Age must be positive")
|
| 443 |
except ValueError:
|
| 444 |
return jsonify({'success': False, 'error': 'Invalid Age provided.'}), 400
|
| 445 |
else:
|
| 446 |
+
age = None
|
| 447 |
|
| 448 |
care_plan_text = ""
|
| 449 |
care_plan_format = None
|
| 450 |
|
|
|
|
| 451 |
if 'care_plan_pdf' in request.files:
|
| 452 |
pdf_file = request.files['care_plan_pdf']
|
| 453 |
if pdf_file and pdf_file.filename != '':
|
|
|
|
| 454 |
care_plan_text = extract_text_from_pdf(pdf_file)
|
| 455 |
if care_plan_text and "[No readable text found" not in care_plan_text and "[Error extracting PDF text" not in care_plan_text:
|
| 456 |
care_plan_format = extract_care_plan_format(care_plan_text)
|
|
|
|
| 459 |
print("No PDF file uploaded or file is empty.")
|
| 460 |
|
| 461 |
|
|
|
|
| 462 |
if not care_plan_format or not care_plan_format.strip():
|
| 463 |
print("Using default care plan format.")
|
| 464 |
care_plan_format = """
|
|
|
|
| 496 |
else:
|
| 497 |
print("Using extracted care plan format.")
|
| 498 |
|
| 499 |
+
initial_status = determine_patient_status(care_plan_text, "", feedback)
|
|
|
|
|
|
|
| 500 |
|
| 501 |
generated_plan_text = ""
|
| 502 |
+
status = initial_status
|
| 503 |
|
| 504 |
if status == 'emergency':
|
| 505 |
print("Emergency status detected from feedback. Generating emergency plan.")
|
|
|
|
| 506 |
generated_plan_text = (
|
| 507 |
"PATIENT INFORMATION:\n"
|
| 508 |
f"- Name: {name}\n"
|
|
|
|
| 523 |
"- Inform your primary physician as soon as medically stable.\n"
|
| 524 |
"- Review and update care plan only after emergency situation is resolved and evaluated by medical professionals.\n"
|
| 525 |
)
|
|
|
|
| 526 |
|
| 527 |
else:
|
|
|
|
|
|
|
| 528 |
prompt = f"""
|
| 529 |
You are a helpful assistant generating updated patient care plans.
|
| 530 |
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.
|
|
|
|
| 535 |
Patient Feedback/Symptoms Update:
|
| 536 |
{feedback}
|
| 537 |
Previous Care Plan Details (if available):
|
| 538 |
+
{care_plan_text if care_plan_text and "[No readable text found" not in care_plan_text and "[Error extracting PDF text" not in care_plan_text else "No previous care plan provided."}
|
| 539 |
Instructions:
|
| 540 |
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.
|
| 541 |
2. Be specific and actionable in your recommendations.
|
|
|
|
| 548 |
{care_plan_format}
|
| 549 |
"""
|
| 550 |
print("Sending prompt to AI model...")
|
|
|
|
| 551 |
|
|
|
|
| 552 |
try:
|
| 553 |
response = model.generate_content(prompt)
|
| 554 |
+
generated_plan_text = response.text.strip()
|
| 555 |
|
|
|
|
| 556 |
if generated_plan_text.startswith('```') and generated_plan_text.endswith('```'):
|
| 557 |
generated_plan_text = generated_plan_text[3:-3].strip()
|
| 558 |
+
if generated_plan_text.startswith('text'):
|
| 559 |
generated_plan_text = generated_plan_text[4:].strip()
|
| 560 |
|
|
|
|
| 561 |
print(f"AI Response received. Length: {len(generated_plan_text)}")
|
|
|
|
|
|
|
| 562 |
|
| 563 |
+
# Recalculate status based on the *generated* updated plan as well as feedback
|
| 564 |
+
# Pass original plan text so determine_patient_status can compare
|
| 565 |
status = determine_patient_status(care_plan_text, generated_plan_text, feedback)
|
| 566 |
|
| 567 |
+
|
| 568 |
except Exception as ai_error:
|
| 569 |
print(f"Error generating content from AI: {ai_error}")
|
|
|
|
| 570 |
generated_plan_text = f"[Error generating updated plan from AI: {ai_error}]\n\n"
|
| 571 |
+
if care_plan_text and "[No readable text found" not in care_plan_text and "[Error extracting PDF text" not in care_plan_text:
|
| 572 |
generated_plan_text += "Showing original plan due to AI error:\n\n" + care_plan_text
|
| 573 |
+
# If AI failed, the "updated plan" is the original one + error message.
|
| 574 |
+
# Recalculate status based on this fallback updated plan and feedback.
|
| 575 |
+
status = determine_patient_status(care_plan_text, generated_plan_text, feedback)
|
| 576 |
else:
|
| 577 |
generated_plan_text += "No previous plan available."
|
| 578 |
+
# If no previous plan and AI failed, keep the initial status from feedback
|
| 579 |
+
status = initial_status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
|
| 581 |
+
# Even if AI fails, we still create a record with the error/fallback plan
|
| 582 |
+
# and the determined status (e.g., 'emergency' if feedback indicated).
|
| 583 |
+
# Continue to store the patient record below.
|
| 584 |
|
|
|
|
| 585 |
|
| 586 |
# Create and store patient record in the database
|
| 587 |
new_patient = Patient(
|
|
|
|
| 589 |
age=age,
|
| 590 |
gender=gender,
|
| 591 |
feedback=feedback,
|
| 592 |
+
original_plan=care_plan_text,
|
| 593 |
+
updated_plan=generated_plan_text,
|
| 594 |
status=status,
|
| 595 |
+
timestamp=datetime.utcnow()
|
| 596 |
)
|
| 597 |
db.session.add(new_patient)
|
| 598 |
db.session.commit()
|
| 599 |
+
patient_id = new_patient.id
|
| 600 |
|
| 601 |
print(f"Patient {patient_id} added to DB with status: {status}.")
|
| 602 |
|
|
|
|
| 603 |
# Generate PDF for downloading using the stored data
|
| 604 |
patient_info_for_pdf = {
|
| 605 |
'name': name,
|
|
|
|
| 619 |
whatsapp_sent = send_whatsapp_care_plan(patient_info_for_whatsapp, generated_plan_text, status)
|
| 620 |
print(f"WhatsApp message attempt sent: {whatsapp_sent}")
|
| 621 |
|
| 622 |
+
# Return success response (indicates record creation/update) even if AI failed, but include error message
|
| 623 |
return jsonify({
|
| 624 |
'success': True,
|
| 625 |
+
'updated_plan': generated_plan_text,
|
| 626 |
+
'pdf_data': pdf_base64, # Include PDF data for patient preview
|
| 627 |
+
'patient_id': patient_id,
|
| 628 |
'status': status,
|
| 629 |
+
'whatsapp_sent': whatsapp_sent,
|
| 630 |
+
'ai_error': not model # Indicate if AI was configured
|
| 631 |
+
or ("Error generating updated plan" in generated_plan_text) # Check if error message was inserted
|
| 632 |
})
|
| 633 |
|
| 634 |
except Exception as e:
|
|
|
|
| 635 |
print(f"An unexpected error occurred during submission: {str(e)}")
|
|
|
|
| 636 |
import traceback
|
| 637 |
traceback.print_exc()
|
|
|
|
| 638 |
db.session.rollback()
|
| 639 |
return jsonify({
|
| 640 |
'success': False,
|
|
|
|
| 647 |
print(f"Download requested for patient ID: {patient_id}")
|
| 648 |
|
| 649 |
try:
|
|
|
|
| 650 |
patient = Patient.query.get(patient_id)
|
| 651 |
|
| 652 |
if not patient:
|
| 653 |
print(f"Patient ID {patient_id} not found in database for download.")
|
| 654 |
return "Patient data not found.", 404
|
| 655 |
|
|
|
|
| 656 |
patient_info_for_pdf = {
|
| 657 |
'name': patient.name,
|
| 658 |
'age': patient.age,
|
| 659 |
'gender': patient.gender
|
| 660 |
}
|
|
|
|
| 661 |
pdf_buffer = generate_care_plan_pdf(
|
| 662 |
patient_info_for_pdf,
|
| 663 |
patient.updated_plan, # Use the stored updated plan
|
| 664 |
patient.status # Use the stored status
|
| 665 |
)
|
| 666 |
|
| 667 |
+
pdf_buffer.seek(0)
|
| 668 |
|
|
|
|
| 669 |
safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '', patient.name or 'patient').lower()
|
| 670 |
+
download_name = f"care_plan_{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
| 671 |
|
| 672 |
print(f"Serving PDF for {patient_id} as {download_name}")
|
| 673 |
return send_file(
|
|
|
|
| 685 |
|
| 686 |
@app.route('/get_emergency_notifications')
|
| 687 |
def get_emergency_notifications():
|
|
|
|
| 688 |
emergency_patients_query = Patient.query.filter_by(status='emergency').order_by(Patient.timestamp.desc())
|
| 689 |
|
| 690 |
notifications = [p.to_dict() for p in emergency_patients_query.all()]
|
|
|
|
| 697 |
|
| 698 |
@app.route('/get_patients')
|
| 699 |
def get_patients():
|
|
|
|
|
|
|
| 700 |
all_patients_query = Patient.query.order_by(Patient.timestamp.desc())
|
|
|
|
|
|
|
| 701 |
patients_list = [p.to_dict() for p in all_patients_query.all()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
return jsonify({
|
| 703 |
'success': True,
|
| 704 |
'patients': patients_list
|
|
|
|
| 708 |
@app.route('/get_patient/<patient_id>')
|
| 709 |
def get_patient(patient_id):
|
| 710 |
print(f"API request for patient ID: {patient_id}")
|
|
|
|
|
|
|
| 711 |
patient = Patient.query.get(patient_id)
|
| 712 |
|
| 713 |
if not patient:
|
|
|
|
| 717 |
print(f"Found patient {patient_id}: {patient.name}")
|
| 718 |
return jsonify({
|
| 719 |
'success': True,
|
| 720 |
+
'patient': patient.to_dict()
|
| 721 |
})
|
| 722 |
|
| 723 |
+
@app.route('/delete_patient/<patient_id>', methods=['DELETE'])
|
| 724 |
+
def delete_patient(patient_id):
|
| 725 |
+
print(f"Delete requested for patient ID: {patient_id}")
|
| 726 |
+
try:
|
| 727 |
+
patient = Patient.query.get(patient_id)
|
|
|
|
|
|
|
|
|
|
| 728 |
|
| 729 |
+
if not patient:
|
| 730 |
+
print(f"Patient ID {patient_id} not found for deletion.")
|
| 731 |
+
return jsonify({'success': False, 'error': 'Patient not found.'}), 404
|
| 732 |
+
|
| 733 |
+
db.session.delete(patient)
|
| 734 |
+
db.session.commit()
|
| 735 |
+
print(f"Patient ID {patient_id} deleted successfully.")
|
| 736 |
+
|
| 737 |
+
return jsonify({'success': True, 'message': 'Patient deleted successfully.'})
|
| 738 |
+
|
| 739 |
+
except Exception as e:
|
| 740 |
+
print(f"Error deleting patient ID {patient_id}: {str(e)}")
|
| 741 |
+
import traceback
|
| 742 |
+
traceback.print_exc()
|
| 743 |
+
db.session.rollback()
|
| 744 |
+
return jsonify({'success': False, 'error': f'An error occurred during deletion: {str(e)}'}), 500
|
| 745 |
+
|
| 746 |
+
# New route to save edited care plan
|
| 747 |
+
@app.route('/save_care_plan/<patient_id>', methods=['PUT'])
|
| 748 |
+
def save_care_plan(patient_id):
|
| 749 |
+
print(f"Save requested for patient ID: {patient_id}")
|
| 750 |
+
try:
|
| 751 |
+
# Get the edited plan text from the request body
|
| 752 |
+
data = request.get_json()
|
| 753 |
+
if not data or 'updated_plan' not in data:
|
| 754 |
+
return jsonify({'success': False, 'error': 'No updated_plan data provided.'}), 400
|
| 755 |
+
|
| 756 |
+
new_updated_plan = data['updated_plan']
|
| 757 |
+
if not new_updated_plan.strip():
|
| 758 |
+
return jsonify({'success': False, 'error': 'Care plan cannot be empty.'}), 400
|
| 759 |
+
|
| 760 |
+
|
| 761 |
+
# Find the patient by ID
|
| 762 |
+
patient = Patient.query.get(patient_id)
|
| 763 |
+
|
| 764 |
+
if not patient:
|
| 765 |
+
print(f"Patient ID {patient_id} not found for saving care plan.")
|
| 766 |
+
return jsonify({'success': False, 'error': 'Patient not found.'}), 404
|
| 767 |
+
|
| 768 |
+
# Update the care plan text and timestamp
|
| 769 |
+
patient.updated_plan = new_updated_plan
|
| 770 |
+
patient.timestamp = datetime.utcnow() # Update timestamp when plan is manually saved
|
| 771 |
+
|
| 772 |
+
# Optional: Re-evaluate status based on manual edits + original feedback/plan?
|
| 773 |
+
# For simplicity, we'll keep the status as is unless manually changed later,
|
| 774 |
+
# or you could add logic here:
|
| 775 |
+
# patient.status = determine_patient_status(patient.original_plan, patient.updated_plan, patient.feedback)
|
| 776 |
+
# Or prompt doctor to set status manually on save
|
| 777 |
+
|
| 778 |
+
db.session.commit()
|
| 779 |
+
print(f"Care plan for patient ID {patient_id} saved successfully.")
|
| 780 |
+
|
| 781 |
+
return jsonify({'success': True, 'message': 'Care plan saved successfully.', 'new_timestamp': patient.timestamp.strftime("%Y-%m-%d %H:%M:%S")})
|
| 782 |
+
|
| 783 |
+
except Exception as e:
|
| 784 |
+
print(f"Error saving care plan for patient ID {patient_id}: {str(e)}")
|
| 785 |
+
import traceback
|
| 786 |
+
traceback.print_exc()
|
| 787 |
+
db.session.rollback()
|
| 788 |
+
return jsonify({'success': False, 'error': f'An error occurred during saving: {str(e)}'}), 500
|
| 789 |
+
|
| 790 |
+
# New route to resend WhatsApp message
|
| 791 |
+
@app.route('/resend_whatsapp/<patient_id>', methods=['POST'])
|
| 792 |
+
def resend_whatsapp(patient_id):
|
| 793 |
+
print(f"WhatsApp resend requested for patient ID: {patient_id}")
|
| 794 |
+
|
| 795 |
+
if not twilio_client:
|
| 796 |
+
return jsonify({'success': False, 'error': 'WhatsApp service not configured on the server.'}), 500
|
| 797 |
+
|
| 798 |
+
try:
|
| 799 |
+
# Find the patient by ID
|
| 800 |
+
patient = Patient.query.get(patient_id)
|
| 801 |
+
|
| 802 |
+
if not patient:
|
| 803 |
+
print(f"Patient ID {patient_id} not found for WhatsApp resend.")
|
| 804 |
+
return jsonify({'success': False, 'error': 'Patient not found.'}), 404
|
| 805 |
+
|
| 806 |
+
# Use the patient's current data (including potentially edited updated_plan)
|
| 807 |
+
patient_info = {
|
| 808 |
+
'name': patient.name,
|
| 809 |
+
'age': patient.age,
|
| 810 |
+
'gender': patient.gender
|
| 811 |
+
}
|
| 812 |
+
whatsapp_sent = send_whatsapp_care_plan(patient_info, patient.updated_plan, patient.status)
|
| 813 |
+
|
| 814 |
+
if whatsapp_sent:
|
| 815 |
+
print(f"WhatsApp message resent successfully for patient ID {patient_id}.")
|
| 816 |
+
return jsonify({'success': True, 'message': 'WhatsApp message sent successfully.', 'whatsapp_sent': True})
|
| 817 |
+
else:
|
| 818 |
+
print(f"Failed to resend WhatsApp message for patient ID {patient_id}.")
|
| 819 |
+
return jsonify({'success': True, 'message': 'Failed to send WhatsApp message.', 'whatsapp_sent': False}) # Return success=True but whatsapp_sent=False
|
| 820 |
+
|
| 821 |
+
except Exception as e:
|
| 822 |
+
print(f"Error resending WhatsApp for patient ID {patient_id}: {str(e)}")
|
| 823 |
+
import traceback
|
| 824 |
+
traceback.print_exc()
|
| 825 |
+
return jsonify({'success': False, 'error': f'An error occurred during WhatsApp resend: {str(e)}'}), 500
|
| 826 |
|
| 827 |
|
| 828 |
if __name__ == '__main__':
|
|
|
|
|
|
|
| 829 |
with app.app_context():
|
| 830 |
db.create_all()
|
| 831 |
+
# Use a more robust development server like Waitress or Gunicorn in production
|
| 832 |
+
app.run(debug=True)
|