Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -15,6 +15,7 @@ from reportlab.pdfgen import canvas
|
|
| 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') # Explicitly set instance_path to a writable location
|
| 20 |
|
|
@@ -73,9 +74,9 @@ os.makedirs(upload_folder, exist_ok=True)
|
|
| 73 |
|
| 74 |
# Twilio Configuration
|
| 75 |
# IMPORTANT: Use environment variables for sensitive information like SID and Auth Token
|
| 76 |
-
ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'AC490e071f8d01bf0df2f03d086c788d87')
|
| 77 |
-
AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '224b23b950ad5a4052aba15893fdf083')
|
| 78 |
-
TWILIO_FROM = os.getenv('TWILIO_FROM_NUMBER', 'whatsapp:+14155238886')
|
| 79 |
TWILIO_TO = os.getenv('TWILIO_TO_NUMBER', 'whatsapp:+917559355282') # Hardcoded number as requested
|
| 80 |
|
| 81 |
# Initialize Twilio client
|
|
@@ -93,7 +94,7 @@ else:
|
|
| 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") #
|
| 97 |
model = None
|
| 98 |
if GENAI_API_KEY:
|
| 99 |
try:
|
|
@@ -108,7 +109,7 @@ if GENAI_API_KEY:
|
|
| 108 |
}
|
| 109 |
|
| 110 |
model = genai.GenerativeModel(
|
| 111 |
-
model_name="gemini-1.5-flash-latest",
|
| 112 |
generation_config=generation_config,
|
| 113 |
)
|
| 114 |
print(f"Using Gemini model: {model.model_name}")
|
|
@@ -128,10 +129,12 @@ def extract_text_from_pdf(pdf_file):
|
|
| 128 |
text = ""
|
| 129 |
if pdf_reader.is_encrypted:
|
| 130 |
try:
|
|
|
|
| 131 |
pdf_reader.decrypt('')
|
| 132 |
-
except Exception
|
| 133 |
-
|
| 134 |
-
|
|
|
|
| 135 |
|
| 136 |
for page in pdf_reader.pages:
|
| 137 |
page_text = page.extract_text()
|
|
@@ -145,28 +148,38 @@ def extract_text_from_pdf(pdf_file):
|
|
| 145 |
|
| 146 |
def extract_care_plan_format(pdf_text):
|
| 147 |
"""Extract a general format template from PDF text by identifying common section headers."""
|
| 148 |
-
if not pdf_text or "[No readable text found" in pdf_text or "[Error extracting PDF text" in pdf_text:
|
| 149 |
return None
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
-
if not
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
| 162 |
return format_template if format_template.strip() else None
|
|
|
|
| 163 |
print("No sections or potential headers found in PDF.")
|
| 164 |
return None
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
format_template = ""
|
| 167 |
-
for section_title
|
| 168 |
-
format_template += f"{section_title.strip()}:\n- [Details]\n"
|
| 169 |
-
print(f"Extracted sections: {
|
| 170 |
return format_template if format_template.strip() else None
|
| 171 |
|
| 172 |
|
|
@@ -175,7 +188,7 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 175 |
|
| 176 |
feedback_lower = feedback.lower()
|
| 177 |
original_plan_lower = original_plan.lower() if original_plan else ""
|
| 178 |
-
updated_plan_lower = updated_plan.lower()
|
| 179 |
|
| 180 |
emergency_keywords = [
|
| 181 |
"severe chest pain", "heart attack", "sudden shortness of breath",
|
|
@@ -187,7 +200,9 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 187 |
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion",
|
| 188 |
"suffocating", "not breathing", "blue lips", "blue face", "cardiac arrest",
|
| 189 |
"high fever", "signs of shock", "severe dehydration", "acute change",
|
| 190 |
-
"unstable vitals", "rapidly worsening", "can't breathe", "chest tight"
|
|
|
|
|
|
|
| 191 |
]
|
| 192 |
|
| 193 |
deteriorating_keywords = [
|
|
@@ -199,7 +214,9 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 199 |
"increased symptoms", "more severe", "progressing", "progressive", "complicated",
|
| 200 |
"adverse change", "unstable", "needs attention", "spike in", "significant increase",
|
| 201 |
"new symptoms", "trouble with", "reduced appetite", "difficulty sleeping",
|
| 202 |
-
"tired all the time", "much weaker", "feeling worse"
|
|
|
|
|
|
|
| 203 |
]
|
| 204 |
|
| 205 |
improvement_keywords = [
|
|
@@ -210,9 +227,12 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 210 |
"normalized", "normal range", "responding well", "responding positively",
|
| 211 |
"effective treatment", "successful treatment", "managed well", "under control",
|
| 212 |
"symptoms decreased", "feeling stronger", "better sleep", "increased appetite",
|
| 213 |
-
"pain decreased", "more energy", "walking further"
|
|
|
|
|
|
|
| 214 |
]
|
| 215 |
|
|
|
|
| 216 |
if any(keyword in feedback_lower for keyword in emergency_keywords):
|
| 217 |
return "emergency"
|
| 218 |
if any(keyword in feedback_lower for keyword in deteriorating_keywords):
|
|
@@ -220,14 +240,29 @@ def determine_patient_status(original_plan, updated_plan, feedback):
|
|
| 220 |
if any(keyword in feedback_lower for keyword in improvement_keywords):
|
| 221 |
return "improving"
|
| 222 |
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
if any(keyword in updated_plan_lower for keyword in emergency_keywords):
|
| 225 |
-
|
|
|
|
|
|
|
| 226 |
if any(keyword in updated_plan_lower for keyword in deteriorating_keywords):
|
| 227 |
-
return "deteriorating"
|
| 228 |
if any(keyword in updated_plan_lower for keyword in improvement_keywords):
|
| 229 |
-
return "improving"
|
|
|
|
| 230 |
|
|
|
|
| 231 |
return "stable"
|
| 232 |
|
| 233 |
|
|
@@ -241,30 +276,45 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 241 |
|
| 242 |
styles = getSampleStyleSheet()
|
| 243 |
|
|
|
|
| 244 |
title_style = ParagraphStyle(
|
| 245 |
'Title',
|
| 246 |
parent=styles['Heading1'],
|
| 247 |
-
fontSize=22, alignment=
|
|
|
|
| 248 |
)
|
| 249 |
|
| 250 |
heading_style = ParagraphStyle(
|
| 251 |
'Heading',
|
| 252 |
parent=styles['Heading2'],
|
| 253 |
-
fontSize=15, spaceAfter=8, spaceBefore=18, textColor=colors.HexColor("#1cc88a")
|
|
|
|
| 254 |
)
|
| 255 |
|
| 256 |
normal_style = ParagraphStyle(
|
| 257 |
'Normal',
|
| 258 |
parent=styles['Normal'],
|
| 259 |
-
fontSize=11, spaceAfter=6, leading=14, textColor=colors.HexColor("#5a5c69")
|
|
|
|
| 260 |
)
|
| 261 |
|
| 262 |
bullet_style = ParagraphStyle(
|
| 263 |
'Bullet',
|
| 264 |
parent=styles['Normal'],
|
| 265 |
-
fontSize=11, spaceAfter=3, leftIndent=20, leading=14, bulletIndent=10, textColor=colors.HexColor("#5a5c69")
|
|
|
|
| 266 |
)
|
| 267 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
status_colors = {
|
| 269 |
'emergency': colors.HexColor("#e74a3b"),
|
| 270 |
'deteriorating': colors.HexColor("#f6c23e"),
|
|
@@ -274,11 +324,11 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 274 |
}
|
| 275 |
|
| 276 |
status_text_styles = {
|
| 277 |
-
'emergency': ParagraphStyle('StatusEmergency', parent=styles['Heading2'], fontSize=16, spaceBefore=10, spaceAfter=15, textColor=status_colors['emergency'], alignment=
|
| 278 |
-
'deteriorating': ParagraphStyle('StatusDeteriorating', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['deteriorating'], alignment=
|
| 279 |
-
'improving': ParagraphStyle('StatusImproving', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['improving'], alignment=
|
| 280 |
-
'stable': ParagraphStyle('StatusStable', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['stable'], alignment=
|
| 281 |
-
'unknown': ParagraphStyle('StatusUnknown', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=colors.black, alignment=
|
| 282 |
}
|
| 283 |
|
| 284 |
story = []
|
|
@@ -286,10 +336,10 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 286 |
story.append(Paragraph("Patient Care Plan", title_style))
|
| 287 |
|
| 288 |
status_map_text = {
|
| 289 |
-
'emergency': "EMERGENCY - IMMEDIATE ACTION REQUIRED",
|
| 290 |
-
'deteriorating': "HIGH RISK - Condition Deteriorating",
|
| 291 |
-
'improving': "LOW RISK - Condition Improving",
|
| 292 |
-
'stable': "STABLE - Maintain Current Care",
|
| 293 |
'unknown': "Status: Unknown"
|
| 294 |
}
|
| 295 |
current_status_text = status_map_text.get(status, 'unknown')
|
|
@@ -304,11 +354,13 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 304 |
[Paragraph("<b>Generated Date:</b>", normal_style), Paragraph(datetime.now().strftime("%Y-%m-%d %H:%M"), normal_style)]
|
| 305 |
]
|
| 306 |
|
| 307 |
-
|
|
|
|
| 308 |
patient_table.setStyle(TableStyle([
|
| 309 |
('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#f2f2f2")),
|
| 310 |
-
('TEXTCOLOR', (0, 0), (
|
| 311 |
-
('ALIGN', (0, 0), (
|
|
|
|
| 312 |
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 313 |
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")),
|
| 314 |
('BOX', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")),
|
|
@@ -321,31 +373,48 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 321 |
story.append(patient_table)
|
| 322 |
story.append(Spacer(1, 25))
|
| 323 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
story.append(Paragraph("Care Plan Details:", heading_style))
|
| 325 |
story.append(Spacer(1, 10))
|
| 326 |
|
|
|
|
| 327 |
lines = care_plan_text.strip().split('\n')
|
| 328 |
for line in lines:
|
| 329 |
stripped_line = line.strip()
|
| 330 |
if not stripped_line:
|
| 331 |
continue
|
| 332 |
|
| 333 |
-
|
|
|
|
|
|
|
| 334 |
if header_match:
|
| 335 |
-
story.append(Spacer(1, 8))
|
| 336 |
-
story.append(Paragraph(stripped_line, heading_style))
|
|
|
|
| 337 |
elif stripped_line.startswith('-') or stripped_line.startswith('*') or stripped_line.startswith('•'):
|
| 338 |
-
|
|
|
|
| 339 |
if bullet_text:
|
| 340 |
-
|
|
|
|
| 341 |
else:
|
|
|
|
| 342 |
story.append(Paragraph("• ", bullet_style))
|
| 343 |
else:
|
| 344 |
-
|
|
|
|
| 345 |
|
| 346 |
story.append(Spacer(1, 20))
|
| 347 |
-
footer_style = ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, alignment=
|
| 348 |
-
story.append(Paragraph(f"Generated by Patient Care Management System on {datetime.now().strftime('%Y-%m-%d')}", footer_style))
|
| 349 |
|
| 350 |
try:
|
| 351 |
doc.build(story)
|
|
@@ -354,6 +423,8 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
|
| 354 |
return buffer
|
| 355 |
except Exception as e:
|
| 356 |
print(f"Error building PDF: {e}")
|
|
|
|
|
|
|
| 357 |
error_buffer = io.BytesIO()
|
| 358 |
c = canvas.Canvas(error_buffer, pagesize=letter)
|
| 359 |
c.drawString(100, 750, "Error Generating Care Plan PDF")
|
|
@@ -367,7 +438,11 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 367 |
"""Send care plan via WhatsApp using Twilio with improved formatting"""
|
| 368 |
if not twilio_client:
|
| 369 |
print("Twilio client not configured. Cannot send WhatsApp message.")
|
| 370 |
-
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
|
| 372 |
try:
|
| 373 |
status_emoji = {
|
|
@@ -378,15 +453,34 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 378 |
'unknown': "⚪ Status: Unknown"
|
| 379 |
}
|
| 380 |
|
|
|
|
| 381 |
formatted_plan = care_plan_text.strip()
|
|
|
|
| 382 |
formatted_plan = re.sub(r'\n{2,}', '\n\n', formatted_plan)
|
|
|
|
| 383 |
formatted_plan = formatted_plan.replace('- ', '• ').replace('* ', '• ')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
message = f"*Care Plan Update*\n\n"
|
| 386 |
message += f"*Patient Name:* {patient_info.get('name', 'N/A')}\n"
|
| 387 |
message += f"*Age:* {patient_info.get('age', 'N/A')}\n"
|
| 388 |
message += f"*Gender:* {patient_info.get('gender', 'N/A')}\n"
|
| 389 |
message += f"*Status:* {status_emoji.get(status, 'Unknown')}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
message += f"*Care Plan Details:*\n{formatted_plan}"
|
| 391 |
|
| 392 |
print(f"Attempting to send WhatsApp message to {TWILIO_TO}...")
|
|
@@ -396,17 +490,19 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
|
| 396 |
to=TWILIO_TO
|
| 397 |
)
|
| 398 |
print(f"WhatsApp message sent, SID: {message_sent.sid}")
|
| 399 |
-
return True
|
| 400 |
except Exception as e:
|
| 401 |
print(f"Error sending WhatsApp message: {e}")
|
| 402 |
-
return False
|
| 403 |
|
| 404 |
|
| 405 |
@app.route('/')
|
| 406 |
def index():
|
| 407 |
role = session.get('role', 'patient')
|
|
|
|
| 408 |
if role == 'doctor':
|
| 409 |
return redirect(url_for('doctor_dashboard'))
|
|
|
|
| 410 |
return render_template('index.html')
|
| 411 |
|
| 412 |
|
|
@@ -415,12 +511,20 @@ def switch_role():
|
|
| 415 |
role = request.form.get('role')
|
| 416 |
if role in ['patient', 'doctor']:
|
| 417 |
session['role'] = role
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
return jsonify({'success': False, 'error': 'Invalid role'}), 400
|
| 420 |
|
| 421 |
|
| 422 |
@app.route('/doctor_dashboard')
|
| 423 |
def doctor_dashboard():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
return render_template('doctor_dashboard.html')
|
| 425 |
|
| 426 |
|
|
@@ -433,18 +537,21 @@ def submit_feedback():
|
|
| 433 |
name = request.form.get('name', 'Unnamed Patient')
|
| 434 |
age = request.form.get('age')
|
| 435 |
gender = request.form.get('gender', 'N/A')
|
| 436 |
-
feedback = request.form.get('feedback', '')
|
| 437 |
|
| 438 |
if not name or not feedback:
|
| 439 |
return jsonify({'success': False, 'error': 'Patient Name and Feedback are required.'}), 400
|
|
|
|
|
|
|
| 440 |
if age:
|
| 441 |
try:
|
| 442 |
age = int(age)
|
| 443 |
if age <= 0: raise ValueError("Age must be positive")
|
|
|
|
| 444 |
except ValueError:
|
| 445 |
return jsonify({'success': False, 'error': 'Invalid Age provided.'}), 400
|
| 446 |
else:
|
| 447 |
-
age = None
|
| 448 |
|
| 449 |
care_plan_text = ""
|
| 450 |
care_plan_format = None
|
|
@@ -452,16 +559,26 @@ def submit_feedback():
|
|
| 452 |
if 'care_plan_pdf' in request.files:
|
| 453 |
pdf_file = request.files['care_plan_pdf']
|
| 454 |
if pdf_file and pdf_file.filename != '':
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
care_plan_text = extract_text_from_pdf(pdf_file)
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
else:
|
| 460 |
print("No PDF file uploaded or file is empty.")
|
| 461 |
|
| 462 |
|
|
|
|
| 463 |
if not care_plan_format or not care_plan_format.strip():
|
| 464 |
-
print("Using default care plan format.")
|
| 465 |
care_plan_format = """
|
| 466 |
PATIENT INFORMATION:
|
| 467 |
- Name: [Patient Name]
|
|
@@ -471,41 +588,43 @@ ASSESSMENT:
|
|
| 471 |
- [Summary of patient's current condition based on feedback and previous plan]
|
| 472 |
DAILY CARE PLAN:
|
| 473 |
Morning:
|
| 474 |
-
- [Morning activities/medications]
|
| 475 |
Afternoon:
|
| 476 |
-
- [Afternoon activities/medications]
|
| 477 |
Evening:
|
| 478 |
-
- [Evening activities/medications]
|
| 479 |
Night:
|
| 480 |
-
- [Night activities/medications/sleep instructions]
|
| 481 |
MEDICATIONS:
|
| 482 |
-
- [List of medications, dosage, frequency, and time]
|
| 483 |
DIET AND HYDRATION:
|
| 484 |
-
- [
|
| 485 |
PHYSICAL ACTIVITY/EXERCISE:
|
| 486 |
-
- [Recommended physical activities, duration, frequency]
|
| 487 |
SYMPTOM MANAGEMENT:
|
| 488 |
-
- [
|
| 489 |
RED FLAGS / WHEN TO SEEK HELP:
|
| 490 |
-
- [Clear instructions on symptoms requiring immediate medical attention (Emergency)]
|
| 491 |
-
- [Instructions on symptoms requiring contact with doctor/clinic (Urgent but not Emergency)]
|
| 492 |
ADDITIONAL RECOMMENDATIONS:
|
| 493 |
-
- [Other relevant advice, e.g., rest, monitoring
|
| 494 |
FOLLOW-UP:
|
| 495 |
-
- [Details about next appointment or
|
| 496 |
-
"""
|
| 497 |
-
else:
|
| 498 |
-
print("Using extracted care plan format.")
|
| 499 |
|
|
|
|
| 500 |
initial_status = determine_patient_status(care_plan_text, "", feedback)
|
| 501 |
|
| 502 |
|
| 503 |
generated_plan_text = ""
|
| 504 |
-
status = initial_status
|
| 505 |
|
|
|
|
|
|
|
| 506 |
if status == 'emergency':
|
| 507 |
print("Emergency status detected from feedback. Generating emergency plan.")
|
| 508 |
generated_plan_text = (
|
|
|
|
| 509 |
"PATIENT INFORMATION:\n"
|
| 510 |
f"- Name: {name}\n"
|
| 511 |
f"- Age: {age if age is not None else 'N/A'}\n"
|
|
@@ -513,39 +632,52 @@ FOLLOW-UP:
|
|
| 513 |
"ASSESSMENT:\n"
|
| 514 |
f"- Emergency symptoms reported: {feedback}. Immediate medical attention required.\n\n"
|
| 515 |
"EMERGENCY ACTION PLAN:\n"
|
| 516 |
-
"- Call emergency services immediately at
|
| 517 |
-
"- Do not delay seeking medical help.\n"
|
| 518 |
-
"- If conscious,
|
| 519 |
-
"- Do not
|
| 520 |
-
"-
|
|
|
|
| 521 |
"RED FLAGS / WHEN TO SEEK HELP:\n"
|
| 522 |
-
"- *Any* worsening of reported emergency symptoms requires urgent
|
|
|
|
| 523 |
"FOLLOW-UP:\n"
|
| 524 |
"- Immediate hospitalization or urgent medical evaluation is necessary.\n"
|
| 525 |
-
"- Inform your primary physician as soon as medically stable.\n"
|
| 526 |
-
"-
|
| 527 |
)
|
|
|
|
|
|
|
|
|
|
| 528 |
|
| 529 |
-
else:
|
| 530 |
prompt = f"""
|
| 531 |
-
You are a helpful assistant generating updated patient care plans.
|
| 532 |
-
|
|
|
|
| 533 |
Patient Information:
|
| 534 |
Name: {name}
|
| 535 |
Age: {age if age is not None else 'N/A'}
|
| 536 |
Gender: {gender}
|
|
|
|
| 537 |
Patient Feedback/Symptoms Update:
|
| 538 |
{feedback}
|
|
|
|
| 539 |
Previous Care Plan Details (if available):
|
| 540 |
-
{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."}
|
|
|
|
| 541 |
Instructions:
|
| 542 |
-
1. Generate the updated care plan using the exact following format template.
|
| 543 |
-
2.
|
| 544 |
-
3.
|
| 545 |
-
4.
|
| 546 |
-
5.
|
| 547 |
-
6.
|
| 548 |
-
7.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
Care Plan Format Template:
|
| 550 |
{care_plan_format}
|
| 551 |
"""
|
|
@@ -555,29 +687,41 @@ Care Plan Format Template:
|
|
| 555 |
response = model.generate_content(prompt)
|
| 556 |
generated_plan_text = response.text.strip()
|
| 557 |
|
|
|
|
| 558 |
if generated_plan_text.startswith('```') and generated_plan_text.endswith('```'):
|
| 559 |
generated_plan_text = generated_plan_text[3:-3].strip()
|
| 560 |
-
if
|
|
|
|
| 561 |
generated_plan_text = generated_plan_text[4:].strip()
|
|
|
|
|
|
|
|
|
|
| 562 |
|
| 563 |
print(f"AI Response received. Length: {len(generated_plan_text)}")
|
| 564 |
|
| 565 |
-
status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
|
| 567 |
except Exception as ai_error:
|
| 568 |
print(f"Error generating content from AI: {ai_error}")
|
|
|
|
| 569 |
generated_plan_text = f"[Error generating updated plan from AI: {ai_error}]\n\n"
|
| 570 |
-
if care_plan_text and "[No readable text found" not in care_plan_text and "[Error extracting PDF text" not in care_plan_text:
|
| 571 |
-
generated_plan_text += "
|
| 572 |
-
|
|
|
|
| 573 |
else:
|
| 574 |
generated_plan_text += "No previous plan available."
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
# Even if AI fails, we still create a record with the error/fallback plan
|
| 578 |
-
# and the determined status (e.g., 'emergency' if feedback indicated).
|
| 579 |
-
# Continue to store the patient record below.
|
| 580 |
|
|
|
|
| 581 |
|
| 582 |
# Create and store patient record in the database
|
| 583 |
new_patient = Patient(
|
|
@@ -585,46 +729,39 @@ Care Plan Format Template:
|
|
| 585 |
age=age,
|
| 586 |
gender=gender,
|
| 587 |
feedback=feedback,
|
| 588 |
-
original_plan=care_plan_text,
|
| 589 |
-
updated_plan=
|
| 590 |
-
status=
|
| 591 |
timestamp=datetime.utcnow()
|
| 592 |
)
|
| 593 |
db.session.add(new_patient)
|
| 594 |
db.session.commit()
|
| 595 |
patient_id = new_patient.id
|
| 596 |
|
| 597 |
-
print(f"Patient {patient_id} added to DB with status: {
|
| 598 |
|
| 599 |
# Generate PDF for downloading using the stored data
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
'gender': gender
|
| 604 |
-
}
|
| 605 |
-
pdf_buffer = generate_care_plan_pdf(patient_info_for_pdf, generated_plan_text, status)
|
| 606 |
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
|
| 607 |
print("PDF generated and base64 encoded.")
|
| 608 |
|
| 609 |
-
# Send care plan via WhatsApp
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
'gender': gender
|
| 614 |
-
}
|
| 615 |
-
whatsapp_sent = send_whatsapp_care_plan(patient_info_for_whatsapp, generated_plan_text, status)
|
| 616 |
-
print(f"WhatsApp message attempt sent: {whatsapp_sent}")
|
| 617 |
|
| 618 |
-
# Return success response
|
| 619 |
return jsonify({
|
| 620 |
'success': True, # Indicate that patient record was created/updated
|
| 621 |
-
'updated_plan':
|
| 622 |
'pdf_data': pdf_base64,
|
| 623 |
'patient_id': patient_id,
|
| 624 |
-
'status': status,
|
| 625 |
'whatsapp_sent': whatsapp_sent,
|
| 626 |
-
'
|
| 627 |
-
|
| 628 |
})
|
| 629 |
|
| 630 |
except Exception as e:
|
|
@@ -637,6 +774,86 @@ Care Plan Format Template:
|
|
| 637 |
'error': f'An unexpected server error occurred: {str(e)}'
|
| 638 |
}), 500
|
| 639 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
|
| 641 |
@app.route('/download_pdf/<patient_id>')
|
| 642 |
def download_pdf(patient_id):
|
|
@@ -649,13 +866,9 @@ def download_pdf(patient_id):
|
|
| 649 |
print(f"Patient ID {patient_id} not found in database for download.")
|
| 650 |
return "Patient data not found.", 404
|
| 651 |
|
| 652 |
-
|
| 653 |
-
'name': patient.name,
|
| 654 |
-
'age': patient.age,
|
| 655 |
-
'gender': patient.gender
|
| 656 |
-
}
|
| 657 |
pdf_buffer = generate_care_plan_pdf(
|
| 658 |
-
|
| 659 |
patient.updated_plan,
|
| 660 |
patient.status
|
| 661 |
)
|
|
@@ -681,9 +894,11 @@ def download_pdf(patient_id):
|
|
| 681 |
|
| 682 |
@app.route('/get_emergency_notifications')
|
| 683 |
def get_emergency_notifications():
|
|
|
|
| 684 |
emergency_patients_query = Patient.query.filter_by(status='emergency').order_by(Patient.timestamp.desc())
|
| 685 |
|
| 686 |
-
|
|
|
|
| 687 |
|
| 688 |
return jsonify({
|
| 689 |
'success': True,
|
|
@@ -694,6 +909,7 @@ def get_emergency_notifications():
|
|
| 694 |
@app.route('/get_patients')
|
| 695 |
def get_patients():
|
| 696 |
all_patients_query = Patient.query.order_by(Patient.timestamp.desc())
|
|
|
|
| 697 |
patients_list = [p.to_dict() for p in all_patients_query.all()]
|
| 698 |
return jsonify({
|
| 699 |
'success': True,
|
|
@@ -711,12 +927,13 @@ def get_patient(patient_id):
|
|
| 711 |
return jsonify({'success': False, 'error': 'Patient not found.'}), 404
|
| 712 |
|
| 713 |
print(f"Found patient {patient_id}: {patient.name}")
|
|
|
|
| 714 |
return jsonify({
|
| 715 |
'success': True,
|
| 716 |
'patient': patient.to_dict()
|
| 717 |
})
|
| 718 |
|
| 719 |
-
#
|
| 720 |
@app.route('/delete_patient/<patient_id>', methods=['DELETE'])
|
| 721 |
def delete_patient(patient_id):
|
| 722 |
print(f"Delete requested for patient ID: {patient_id}")
|
|
@@ -744,7 +961,9 @@ def delete_patient(patient_id):
|
|
| 744 |
|
| 745 |
|
| 746 |
if __name__ == '__main__':
|
|
|
|
| 747 |
with app.app_context():
|
| 748 |
db.create_all()
|
| 749 |
# Use a more robust development server like Waitress or Gunicorn in production
|
|
|
|
| 750 |
app.run(debug=True)
|
|
|
|
| 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 |
+
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
|
| 19 |
|
| 20 |
app = Flask(__name__, instance_path='/tmp') # Explicitly set instance_path to a writable location
|
| 21 |
|
|
|
|
| 74 |
|
| 75 |
# Twilio Configuration
|
| 76 |
# IMPORTANT: Use environment variables for sensitive information like SID and Auth Token
|
| 77 |
+
ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'AC490e071f8d01bf0df2f03d086c788d87') # Replace with your SID
|
| 78 |
+
AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '224b23b950ad5a4052aba15893fdf083') # Replace with your Auth Token
|
| 79 |
+
TWILIO_FROM = os.getenv('TWILIO_FROM_NUMBER', 'whatsapp:+14155238886') # Replace with your Twilio Sandbox or purchased number
|
| 80 |
TWILIO_TO = os.getenv('TWILIO_TO_NUMBER', 'whatsapp:+917559355282') # Hardcoded number as requested
|
| 81 |
|
| 82 |
# Initialize Twilio client
|
|
|
|
| 94 |
|
| 95 |
# Gemini API Configuration
|
| 96 |
# IMPORTANT: Use environment variables for sensitive information like API keys
|
| 97 |
+
GENAI_API_KEY = os.getenv('GENAI_API_KEY', "AIzaSyD54ejbjVIVa-F3aD_Urnp8m1EFLUGR__I") # Replace with your actual API Key
|
| 98 |
model = None
|
| 99 |
if GENAI_API_KEY:
|
| 100 |
try:
|
|
|
|
| 109 |
}
|
| 110 |
|
| 111 |
model = genai.GenerativeModel(
|
| 112 |
+
model_name="gemini-1.5-flash-latest", # Consider gemini-1.5-pro-latest for potentially better quality
|
| 113 |
generation_config=generation_config,
|
| 114 |
)
|
| 115 |
print(f"Using Gemini model: {model.model_name}")
|
|
|
|
| 129 |
text = ""
|
| 130 |
if pdf_reader.is_encrypted:
|
| 131 |
try:
|
| 132 |
+
# Attempt decryption with a blank password first
|
| 133 |
pdf_reader.decrypt('')
|
| 134 |
+
except Exception:
|
| 135 |
+
# If blank password fails, the PDF is likely password protected
|
| 136 |
+
print("PDF is encrypted and requires a password.")
|
| 137 |
+
return "[PDF Content Unavailable: File is encrypted and requires a password]"
|
| 138 |
|
| 139 |
for page in pdf_reader.pages:
|
| 140 |
page_text = page.extract_text()
|
|
|
|
| 148 |
|
| 149 |
def extract_care_plan_format(pdf_text):
|
| 150 |
"""Extract a general format template from PDF text by identifying common section headers."""
|
| 151 |
+
if not pdf_text or "[No readable text found" in pdf_text or "[Error extracting PDF text" in pdf_text or "[PDF Content Unavailable" in pdf_text:
|
| 152 |
return None
|
| 153 |
|
| 154 |
+
# Look for lines that seem like headers followed by a colon, possibly with content below
|
| 155 |
+
# Pattern: Start of line, followed by one or more uppercase letters or spaces, ending with a colon.
|
| 156 |
+
# Non-capturing group `(?:...)` ensures we match the pattern but don't store the content part separately.
|
| 157 |
+
# Use word boundaries \b to avoid matching things like "MEDICATION:" within a sentence.
|
| 158 |
+
potential_headers = re.findall(r'^\b([A-Z][A-Z\s]*)\b[ \t]*:', pdf_text, re.MULTILINE)
|
| 159 |
+
|
| 160 |
+
if not potential_headers:
|
| 161 |
+
# Fallback: Look for lines that start with a capital letter and seem like standalone headers
|
| 162 |
+
# (e.g., "Patient Information", "Assessment.")
|
| 163 |
+
fallback_headers = re.findall(r'^[A-Z][A-Za-z\s,]*[.:]?$', pdf_text, re.MULTILINE)
|
| 164 |
+
if fallback_headers:
|
| 165 |
+
print(f"Extracted potential headers (fallback): {list(set(fallback_headers))}")
|
| 166 |
+
format_template = "\n".join([f"{header.strip()}:" for header in sorted(list(set(fallback_headers))) if header.strip()]) # Sort for consistency
|
| 167 |
return format_template if format_template.strip() else None
|
| 168 |
+
|
| 169 |
print("No sections or potential headers found in PDF.")
|
| 170 |
return None
|
| 171 |
|
| 172 |
+
# Use a set to get unique headers and sort them for consistency
|
| 173 |
+
unique_headers = sorted(list(set([h.strip() for h in potential_headers if h.strip()])))
|
| 174 |
+
|
| 175 |
+
if not unique_headers:
|
| 176 |
+
print("Extracted headers are empty after cleaning.")
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
format_template = ""
|
| 180 |
+
for section_title in unique_headers:
|
| 181 |
+
format_template += f"{section_title.strip()}:\n- [Details]\n" # Use a simple list format placeholder
|
| 182 |
+
print(f"Extracted sections: {unique_headers}")
|
| 183 |
return format_template if format_template.strip() else None
|
| 184 |
|
| 185 |
|
|
|
|
| 188 |
|
| 189 |
feedback_lower = feedback.lower()
|
| 190 |
original_plan_lower = original_plan.lower() if original_plan else ""
|
| 191 |
+
updated_plan_lower = updated_plan.lower() if updated_plan else "" # Ensure it's not None
|
| 192 |
|
| 193 |
emergency_keywords = [
|
| 194 |
"severe chest pain", "heart attack", "sudden shortness of breath",
|
|
|
|
| 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 |
+
"severe difficulty swallowing", "new onset paralysis", "severe burns",
|
| 205 |
+
"major trauma", "poisoning", "overdose", "suicidal thoughts" # Added some more critical terms
|
| 206 |
]
|
| 207 |
|
| 208 |
deteriorating_keywords = [
|
|
|
|
| 214 |
"increased symptoms", "more severe", "progressing", "progressive", "complicated",
|
| 215 |
"adverse change", "unstable", "needs attention", "spike in", "significant increase",
|
| 216 |
"new symptoms", "trouble with", "reduced appetite", "difficulty sleeping",
|
| 217 |
+
"tired all the time", "much weaker", "feeling worse", "consistent high blood pressure",
|
| 218 |
+
"uncontrolled blood sugar", "increased swelling", "difficulty walking",
|
| 219 |
+
"persistent cough", "unexplained weight loss" # Added more worsening signs
|
| 220 |
]
|
| 221 |
|
| 222 |
improvement_keywords = [
|
|
|
|
| 227 |
"normalized", "normal range", "responding well", "responding positively",
|
| 228 |
"effective treatment", "successful treatment", "managed well", "under control",
|
| 229 |
"symptoms decreased", "feeling stronger", "better sleep", "increased appetite",
|
| 230 |
+
"pain decreased", "more energy", "walking further", "blood pressure normal",
|
| 231 |
+
"blood sugar stable", "swelling reduced", "easier breathing", "cough improving",
|
| 232 |
+
"weight gain", "feeling like myself again" # Added more positive signs
|
| 233 |
]
|
| 234 |
|
| 235 |
+
# Check feedback first as it's the most direct update from the patient
|
| 236 |
if any(keyword in feedback_lower for keyword in emergency_keywords):
|
| 237 |
return "emergency"
|
| 238 |
if any(keyword in feedback_lower for keyword in deteriorating_keywords):
|
|
|
|
| 240 |
if any(keyword in feedback_lower for keyword in improvement_keywords):
|
| 241 |
return "improving"
|
| 242 |
|
| 243 |
+
# If feedback wasn't definitive, look at the *change* in plan
|
| 244 |
+
# Compare original plan (if available) vs updated plan keywords
|
| 245 |
+
# This part is tricky with AI generation; sometimes the plan *describes* worsening symptoms
|
| 246 |
+
# while still being a 'stable' plan on *how to manage* it.
|
| 247 |
+
# Let's prioritize keywords in the feedback. Only if feedback is neutral,
|
| 248 |
+
# or if the *updated plan itself strongly mandates* a status change (e.g., it says "Monitor closely due to worsening..."),
|
| 249 |
+
# we might infer status from the updated plan *content* keywords.
|
| 250 |
+
# However, for this version, sticking primarily to feedback-driven status is safer.
|
| 251 |
+
# The AI *should* generate content reflecting the feedback anyway.
|
| 252 |
+
# We'll keep the check on the updated plan as a secondary indicator if feedback is neutral.
|
| 253 |
+
|
| 254 |
+
if updated_plan_lower and updated_plan_lower != original_plan_lower:
|
| 255 |
if any(keyword in updated_plan_lower for keyword in emergency_keywords):
|
| 256 |
+
# This is less likely to be a *status* derived from the plan itself
|
| 257 |
+
# unless the plan is *instructing* emergency actions based on the feedback
|
| 258 |
+
return "emergency" # Re-confirm based on AI output if needed
|
| 259 |
if any(keyword in updated_plan_lower for keyword in deteriorating_keywords):
|
| 260 |
+
return "deteriorating" # Re-confirm based on AI output if needed
|
| 261 |
if any(keyword in updated_plan_lower for keyword in improvement_keywords):
|
| 262 |
+
return "improving" # Re-confirm based on AI output if needed
|
| 263 |
+
|
| 264 |
|
| 265 |
+
# Default if no specific keywords found
|
| 266 |
return "stable"
|
| 267 |
|
| 268 |
|
|
|
|
| 276 |
|
| 277 |
styles = getSampleStyleSheet()
|
| 278 |
|
| 279 |
+
# Custom Styles
|
| 280 |
title_style = ParagraphStyle(
|
| 281 |
'Title',
|
| 282 |
parent=styles['Heading1'],
|
| 283 |
+
fontSize=22, alignment=TA_CENTER, spaceAfter=25, textColor=colors.HexColor("#4e73df"),
|
| 284 |
+
fontName='Helvetica-Bold' # Use bold font
|
| 285 |
)
|
| 286 |
|
| 287 |
heading_style = ParagraphStyle(
|
| 288 |
'Heading',
|
| 289 |
parent=styles['Heading2'],
|
| 290 |
+
fontSize=15, spaceAfter=8, spaceBefore=18, textColor=colors.HexColor("#1cc88a"),
|
| 291 |
+
fontName='Helvetica-Bold' # Use bold font
|
| 292 |
)
|
| 293 |
|
| 294 |
normal_style = ParagraphStyle(
|
| 295 |
'Normal',
|
| 296 |
parent=styles['Normal'],
|
| 297 |
+
fontSize=11, spaceAfter=6, leading=14, textColor=colors.HexColor("#5a5c69"),
|
| 298 |
+
alignment=TA_JUSTIFY # Justify text
|
| 299 |
)
|
| 300 |
|
| 301 |
bullet_style = ParagraphStyle(
|
| 302 |
'Bullet',
|
| 303 |
parent=styles['Normal'],
|
| 304 |
+
fontSize=11, spaceAfter=3, leftIndent=20, leading=14, bulletIndent=10, textColor=colors.HexColor("#5a5c69"),
|
| 305 |
+
alignment=TA_JUSTIFY # Justify text
|
| 306 |
)
|
| 307 |
|
| 308 |
+
feedback_style = ParagraphStyle(
|
| 309 |
+
'Feedback',
|
| 310 |
+
parent=normal_style,
|
| 311 |
+
spaceBefore=10, spaceAfter=10, backColor=colors.HexColor("#f8f9fc"),
|
| 312 |
+
borderWidth=0.5, borderColor=colors.HexColor("#dee2e6"), borderPadding=6,
|
| 313 |
+
borderRadius=5,
|
| 314 |
+
textColor=colors.HexColor("#212529") # Darker text for readability on light background
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
status_colors = {
|
| 319 |
'emergency': colors.HexColor("#e74a3b"),
|
| 320 |
'deteriorating': colors.HexColor("#f6c23e"),
|
|
|
|
| 324 |
}
|
| 325 |
|
| 326 |
status_text_styles = {
|
| 327 |
+
'emergency': ParagraphStyle('StatusEmergency', parent=styles['Heading2'], fontSize=16, spaceBefore=10, spaceAfter=15, textColor=status_colors['emergency'], alignment=TA_CENTER, fontName='Helvetica-Bold'),
|
| 328 |
+
'deteriorating': ParagraphStyle('StatusDeteriorating', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['deteriorating'], alignment=TA_CENTER, fontName='Helvetica-Bold'),
|
| 329 |
+
'improving': ParagraphStyle('StatusImproving', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['improving'], alignment=TA_CENTER, fontName='Helvetica-Bold'),
|
| 330 |
+
'stable': ParagraphStyle('StatusStable', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['stable'], alignment=TA_CENTER, fontName='Helvetica-Bold'),
|
| 331 |
+
'unknown': ParagraphStyle('StatusUnknown', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=colors.black, alignment=TA_CENTER, fontName='Helvetica-Bold'),
|
| 332 |
}
|
| 333 |
|
| 334 |
story = []
|
|
|
|
| 336 |
story.append(Paragraph("Patient Care Plan", title_style))
|
| 337 |
|
| 338 |
status_map_text = {
|
| 339 |
+
'emergency': "🚨 EMERGENCY - IMMEDIATE ACTION REQUIRED 🚨",
|
| 340 |
+
'deteriorating': "⚠️ HIGH RISK - Condition Deteriorating ⚠️",
|
| 341 |
+
'improving': "✅ LOW RISK - Condition Improving ✅",
|
| 342 |
+
'stable': "🟦 STABLE - Maintain Current Care 🟦",
|
| 343 |
'unknown': "Status: Unknown"
|
| 344 |
}
|
| 345 |
current_status_text = status_map_text.get(status, 'unknown')
|
|
|
|
| 354 |
[Paragraph("<b>Generated Date:</b>", normal_style), Paragraph(datetime.now().strftime("%Y-%m-%d %H:%M"), normal_style)]
|
| 355 |
]
|
| 356 |
|
| 357 |
+
# Using a Table for patient info
|
| 358 |
+
patient_table = Table(patient_data, colWidths=[doc.width/3, doc.width/3*2]) # Divide width into 1/3 and 2/3
|
| 359 |
patient_table.setStyle(TableStyle([
|
| 360 |
('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#f2f2f2")),
|
| 361 |
+
('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
|
| 362 |
+
('ALIGN', (0, 0), (0, -1), 'LEFT'), # Left align headers
|
| 363 |
+
('ALIGN', (1, 0), (-1, -1), 'LEFT'), # Left align details
|
| 364 |
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 365 |
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")),
|
| 366 |
('BOX', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")),
|
|
|
|
| 373 |
story.append(patient_table)
|
| 374 |
story.append(Spacer(1, 25))
|
| 375 |
|
| 376 |
+
# Add Feedback Section (if available)
|
| 377 |
+
if patient_info.get('feedback'):
|
| 378 |
+
story.append(Paragraph("Patient Feedback:", heading_style))
|
| 379 |
+
story.append(Spacer(1, 5))
|
| 380 |
+
# Using Paragraph with the feedback_style to format the text
|
| 381 |
+
story.append(Paragraph(patient_info['feedback'].replace('\n', '<br/>'), feedback_style)) # Replace newline with <br/> for ReportLab
|
| 382 |
+
story.append(Spacer(1, 15))
|
| 383 |
+
|
| 384 |
+
|
| 385 |
story.append(Paragraph("Care Plan Details:", heading_style))
|
| 386 |
story.append(Spacer(1, 10))
|
| 387 |
|
| 388 |
+
# Process care plan text line by line
|
| 389 |
lines = care_plan_text.strip().split('\n')
|
| 390 |
for line in lines:
|
| 391 |
stripped_line = line.strip()
|
| 392 |
if not stripped_line:
|
| 393 |
continue
|
| 394 |
|
| 395 |
+
# Check for section headers (e.g., "MEDICATIONS:", "ASSESSMENT:")
|
| 396 |
+
# Look for lines starting with one or more uppercase words followed by a colon
|
| 397 |
+
header_match = re.match(r'^([A-Z][A-Z\s]*)\b[ \t]*:', stripped_line)
|
| 398 |
if header_match:
|
| 399 |
+
story.append(Spacer(1, 8)) # Add space before a new section
|
| 400 |
+
story.append(Paragraph(stripped_line, heading_style)) # Use heading style for sections
|
| 401 |
+
# Check for list items (starting with -, *, •)
|
| 402 |
elif stripped_line.startswith('-') or stripped_line.startswith('*') or stripped_line.startswith('•'):
|
| 403 |
+
# Remove the bullet character and any leading space/tab
|
| 404 |
+
bullet_text = re.sub(r'^[-*•][ \t]*', '', line).strip()
|
| 405 |
if bullet_text:
|
| 406 |
+
# Use the bullet style, replace newline with <br/> for formatted text
|
| 407 |
+
story.append(Paragraph(f"• {bullet_text.replace('\n', '<br/>')}", bullet_style))
|
| 408 |
else:
|
| 409 |
+
# Handle cases with just a bullet point on a line
|
| 410 |
story.append(Paragraph("• ", bullet_style))
|
| 411 |
else:
|
| 412 |
+
# Handle regular paragraph text
|
| 413 |
+
story.append(Paragraph(line.strip().replace('\n', '<br/>'), normal_style)) # Use normal style, replace newline
|
| 414 |
|
| 415 |
story.append(Spacer(1, 20))
|
| 416 |
+
footer_style = ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, alignment=TA_CENTER, textColor=colors.grey)
|
| 417 |
+
story.append(Paragraph(f"Generated by Patient Care Management System on {datetime.now().strftime('%Y-%m-%d %H:%M')}", footer_style))
|
| 418 |
|
| 419 |
try:
|
| 420 |
doc.build(story)
|
|
|
|
| 423 |
return buffer
|
| 424 |
except Exception as e:
|
| 425 |
print(f"Error building PDF: {e}")
|
| 426 |
+
import traceback
|
| 427 |
+
traceback.print_exc()
|
| 428 |
error_buffer = io.BytesIO()
|
| 429 |
c = canvas.Canvas(error_buffer, pagesize=letter)
|
| 430 |
c.drawString(100, 750, "Error Generating Care Plan PDF")
|
|
|
|
| 438 |
"""Send care plan via WhatsApp using Twilio with improved formatting"""
|
| 439 |
if not twilio_client:
|
| 440 |
print("Twilio client not configured. Cannot send WhatsApp message.")
|
| 441 |
+
return False, "Twilio client not configured."
|
| 442 |
+
|
| 443 |
+
if not TWILIO_TO or not TWILIO_FROM:
|
| 444 |
+
print("Twilio TO or FROM number not set.")
|
| 445 |
+
return False, "Twilio TO or FROM number not configured."
|
| 446 |
|
| 447 |
try:
|
| 448 |
status_emoji = {
|
|
|
|
| 453 |
'unknown': "⚪ Status: Unknown"
|
| 454 |
}
|
| 455 |
|
| 456 |
+
# Clean and format the plan text for WhatsApp
|
| 457 |
formatted_plan = care_plan_text.strip()
|
| 458 |
+
# Replace multiple newlines with double newline for paragraphs
|
| 459 |
formatted_plan = re.sub(r'\n{2,}', '\n\n', formatted_plan)
|
| 460 |
+
# Replace common list bullet formats with WhatsApp bullet
|
| 461 |
formatted_plan = formatted_plan.replace('- ', '• ').replace('* ', '• ')
|
| 462 |
+
# Basic attempt to bold section headers - look for lines ending with a colon followed by a newline
|
| 463 |
+
# or a line starting with uppercase word(s) ending in a colon.
|
| 464 |
+
formatted_plan_lines = []
|
| 465 |
+
for line in formatted_plan.split('\n'):
|
| 466 |
+
stripped_line = line.strip()
|
| 467 |
+
if re.match(r'^[A-Z][A-Z\s]*\b[ \t]*:(\s|$)', stripped_line):
|
| 468 |
+
formatted_plan_lines.append(f"*{stripped_line}*")
|
| 469 |
+
else:
|
| 470 |
+
formatted_plan_lines.append(line)
|
| 471 |
+
formatted_plan = '\n'.join(formatted_plan_lines)
|
| 472 |
+
|
| 473 |
|
| 474 |
message = f"*Care Plan Update*\n\n"
|
| 475 |
message += f"*Patient Name:* {patient_info.get('name', 'N/A')}\n"
|
| 476 |
message += f"*Age:* {patient_info.get('age', 'N/A')}\n"
|
| 477 |
message += f"*Gender:* {patient_info.get('gender', 'N/A')}\n"
|
| 478 |
message += f"*Status:* {status_emoji.get(status, 'Unknown')}\n\n"
|
| 479 |
+
# Add feedback if available
|
| 480 |
+
feedback_text = patient_info.get('feedback')
|
| 481 |
+
if feedback_text:
|
| 482 |
+
message += f"*Latest Feedback:*\n{feedback_text.strip()}\n\n"
|
| 483 |
+
|
| 484 |
message += f"*Care Plan Details:*\n{formatted_plan}"
|
| 485 |
|
| 486 |
print(f"Attempting to send WhatsApp message to {TWILIO_TO}...")
|
|
|
|
| 490 |
to=TWILIO_TO
|
| 491 |
)
|
| 492 |
print(f"WhatsApp message sent, SID: {message_sent.sid}")
|
| 493 |
+
return True, "WhatsApp message sent successfully."
|
| 494 |
except Exception as e:
|
| 495 |
print(f"Error sending WhatsApp message: {e}")
|
| 496 |
+
return False, f"Error sending WhatsApp: {e}"
|
| 497 |
|
| 498 |
|
| 499 |
@app.route('/')
|
| 500 |
def index():
|
| 501 |
role = session.get('role', 'patient')
|
| 502 |
+
# If doctor role is in session but user visits '/', redirect to doctor dashboard
|
| 503 |
if role == 'doctor':
|
| 504 |
return redirect(url_for('doctor_dashboard'))
|
| 505 |
+
# Otherwise, render patient index
|
| 506 |
return render_template('index.html')
|
| 507 |
|
| 508 |
|
|
|
|
| 511 |
role = request.form.get('role')
|
| 512 |
if role in ['patient', 'doctor']:
|
| 513 |
session['role'] = role
|
| 514 |
+
# Redirect to the appropriate page after switching
|
| 515 |
+
if role == 'doctor':
|
| 516 |
+
return redirect(url_for('doctor_dashboard'))
|
| 517 |
+
else:
|
| 518 |
+
return redirect(url_for('index')) # Redirect to patient home
|
| 519 |
return jsonify({'success': False, 'error': 'Invalid role'}), 400
|
| 520 |
|
| 521 |
|
| 522 |
@app.route('/doctor_dashboard')
|
| 523 |
def doctor_dashboard():
|
| 524 |
+
# Ensure user is marked as doctor in session when accessing this page directly
|
| 525 |
+
# This handles cases where someone might type the URL directly
|
| 526 |
+
if session.get('role') != 'doctor':
|
| 527 |
+
session['role'] = 'doctor'
|
| 528 |
return render_template('doctor_dashboard.html')
|
| 529 |
|
| 530 |
|
|
|
|
| 537 |
name = request.form.get('name', 'Unnamed Patient')
|
| 538 |
age = request.form.get('age')
|
| 539 |
gender = request.form.get('gender', 'N/A')
|
| 540 |
+
feedback = request.form.get('feedback', '').strip() # Strip whitespace
|
| 541 |
|
| 542 |
if not name or not feedback:
|
| 543 |
return jsonify({'success': False, 'error': 'Patient Name and Feedback are required.'}), 400
|
| 544 |
+
|
| 545 |
+
# Basic sanitization/validation for age
|
| 546 |
if age:
|
| 547 |
try:
|
| 548 |
age = int(age)
|
| 549 |
if age <= 0: raise ValueError("Age must be positive")
|
| 550 |
+
if age > 200: raise ValueError("Age seems unreasonably high") # Sanity check
|
| 551 |
except ValueError:
|
| 552 |
return jsonify({'success': False, 'error': 'Invalid Age provided.'}), 400
|
| 553 |
else:
|
| 554 |
+
age = None # Store as None if not provided
|
| 555 |
|
| 556 |
care_plan_text = ""
|
| 557 |
care_plan_format = None
|
|
|
|
| 559 |
if 'care_plan_pdf' in request.files:
|
| 560 |
pdf_file = request.files['care_plan_pdf']
|
| 561 |
if pdf_file and pdf_file.filename != '':
|
| 562 |
+
# Check file extension
|
| 563 |
+
if not pdf_file.filename.lower().endswith('.pdf'):
|
| 564 |
+
return jsonify({'success': False, 'error': 'Invalid file type. Only PDF files are allowed.'}), 400
|
| 565 |
+
|
| 566 |
care_plan_text = extract_text_from_pdf(pdf_file)
|
| 567 |
+
|
| 568 |
+
# If extraction resulted in an error message, set format to None
|
| 569 |
+
if "[Error extracting PDF text" in care_plan_text or "[No readable text found" in care_plan_text or "[PDF Content Unavailable" in care_plan_text:
|
| 570 |
+
care_plan_format = None
|
| 571 |
+
print(f"PDF text extraction failed or empty: {care_plan_text}")
|
| 572 |
+
else:
|
| 573 |
+
care_plan_format = extract_care_plan_format(care_plan_text)
|
| 574 |
+
print(f"Extracted text length: {len(care_plan_text)}. Format found: {care_plan_format is not None}")
|
| 575 |
else:
|
| 576 |
print("No PDF file uploaded or file is empty.")
|
| 577 |
|
| 578 |
|
| 579 |
+
# Define or get the format template
|
| 580 |
if not care_plan_format or not care_plan_format.strip():
|
| 581 |
+
print("Using default care plan format as extraction failed or returned empty.")
|
| 582 |
care_plan_format = """
|
| 583 |
PATIENT INFORMATION:
|
| 584 |
- Name: [Patient Name]
|
|
|
|
| 588 |
- [Summary of patient's current condition based on feedback and previous plan]
|
| 589 |
DAILY CARE PLAN:
|
| 590 |
Morning:
|
| 591 |
+
- [Morning activities/medications/checks]
|
| 592 |
Afternoon:
|
| 593 |
+
- [Afternoon activities/medications/checks]
|
| 594 |
Evening:
|
| 595 |
+
- [Evening activities/medications/checks]
|
| 596 |
Night:
|
| 597 |
+
- [Night activities/medications/sleep instructions/checks]
|
| 598 |
MEDICATIONS:
|
| 599 |
+
- [List of medications, dosage, frequency, and time, including PRN (as needed) instructions]
|
| 600 |
DIET AND HYDRATION:
|
| 601 |
+
- [Specific dietary recommendations (e.g., low sodium, diabetic), hydration goals]
|
| 602 |
PHYSICAL ACTIVITY/EXERCISE:
|
| 603 |
+
- [Recommended physical activities, duration, frequency, limitations, and progression]
|
| 604 |
SYMPTOM MANAGEMENT:
|
| 605 |
+
- [Detailed instructions for managing specific symptoms (e.g., pain, nausea, shortness of breath), non-pharmacological interventions]
|
| 606 |
RED FLAGS / WHEN TO SEEK HELP:
|
| 607 |
+
- [Clear, actionable instructions on symptoms requiring immediate medical attention (Emergency - call 911/local emergency, go to ER)]
|
| 608 |
+
- [Instructions on symptoms requiring contact with doctor/clinic (Urgent but not Emergency - call office, visit clinic)]
|
| 609 |
ADDITIONAL RECOMMENDATIONS:
|
| 610 |
+
- [Other relevant advice, e.g., rest, monitoring specific vital signs, wound care instructions, mental health support, caregiver tips]
|
| 611 |
FOLLOW-UP:
|
| 612 |
+
- [Details about next scheduled appointment, or criteria for scheduling follow-up (e.g., "call if symptoms worsen significantly")]
|
| 613 |
+
""" # Enhanced default format
|
|
|
|
|
|
|
| 614 |
|
| 615 |
+
# Determine initial status based primarily on feedback
|
| 616 |
initial_status = determine_patient_status(care_plan_text, "", feedback)
|
| 617 |
|
| 618 |
|
| 619 |
generated_plan_text = ""
|
| 620 |
+
status = initial_status # Start with feedback-determined status
|
| 621 |
|
| 622 |
+
# Only generate AI plan if status isn't immediate emergency based on feedback
|
| 623 |
+
# If feedback triggers "emergency", the generated_plan_text is a fixed emergency plan.
|
| 624 |
if status == 'emergency':
|
| 625 |
print("Emergency status detected from feedback. Generating emergency plan.")
|
| 626 |
generated_plan_text = (
|
| 627 |
+
"🚨 *EMERGENCY - IMMEDIATE ACTION REQUIRED* 🚨\n\n"
|
| 628 |
"PATIENT INFORMATION:\n"
|
| 629 |
f"- Name: {name}\n"
|
| 630 |
f"- Age: {age if age is not None else 'N/A'}\n"
|
|
|
|
| 632 |
"ASSESSMENT:\n"
|
| 633 |
f"- Emergency symptoms reported: {feedback}. Immediate medical attention required.\n\n"
|
| 634 |
"EMERGENCY ACTION PLAN:\n"
|
| 635 |
+
"- *Call emergency services immediately* at your local emergency number (e.g., 104/108/109/112).\n"
|
| 636 |
+
"- Do not delay seeking medical help. If possible, have someone stay with the patient.\n"
|
| 637 |
+
"- If conscious, help the patient into a comfortable position (e.g., upright for breathing difficulties, on back with legs elevated for shock).\n"
|
| 638 |
+
"- Do not give food or drink until evaluated by medical professionals.\n"
|
| 639 |
+
"- Prepare relevant medical history, medication list, and previous care plan if available for emergency responders.\n"
|
| 640 |
+
"- Follow *all* instructions from emergency responders.\n\n"
|
| 641 |
"RED FLAGS / WHEN TO SEEK HELP:\n"
|
| 642 |
+
"- *Any* worsening of reported emergency symptoms requires urgent re-evaluation by medical professionals.\n"
|
| 643 |
+
"- Do not attempt to manage severe symptoms at home once emergency signs are present.\n\n"
|
| 644 |
"FOLLOW-UP:\n"
|
| 645 |
"- Immediate hospitalization or urgent medical evaluation is necessary.\n"
|
| 646 |
+
"- Inform your primary physician/care team as soon as medically stable.\n"
|
| 647 |
+
"- A new care plan will be developed after the emergency situation is resolved and evaluated by medical professionals.\n"
|
| 648 |
)
|
| 649 |
+
# For emergency, the generated plan text *is* the final plan to save/send.
|
| 650 |
+
final_plan_to_save = generated_plan_text
|
| 651 |
+
final_status_to_save = 'emergency' # Explicitly keep emergency status
|
| 652 |
|
| 653 |
+
else: # Status is not emergency based on feedback, proceed with AI generation
|
| 654 |
prompt = f"""
|
| 655 |
+
You are a helpful and highly structured AI assistant generating updated patient care plans.
|
| 656 |
+
Your task is to create a NEW, comprehensive, and well-structured daily care plan based on the provided patient's personal information, their current symptoms/feedback, and their previous care plan (if available).
|
| 657 |
+
|
| 658 |
Patient Information:
|
| 659 |
Name: {name}
|
| 660 |
Age: {age if age is not None else 'N/A'}
|
| 661 |
Gender: {gender}
|
| 662 |
+
|
| 663 |
Patient Feedback/Symptoms Update:
|
| 664 |
{feedback}
|
| 665 |
+
|
| 666 |
Previous Care Plan Details (if available):
|
| 667 |
+
{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 and "[PDF Content Unavailable" not in care_plan_text else "No previous care plan provided or could not be read."}
|
| 668 |
+
|
| 669 |
Instructions:
|
| 670 |
+
1. Generate the updated care plan strictly using the exact following format template.
|
| 671 |
+
2. Populate each section of the template based on the patient's information, their *latest feedback/symptoms*, and integrate relevant, SAFE, and appropriate elements from the previous plan if they are still applicable and helpful given the feedback.
|
| 672 |
+
3. Prioritize addressing the issues raised in the patient feedback.
|
| 673 |
+
4. Be specific, actionable, and realistic in your recommendations (e.g., specify *what* activities, *when* to take medications).
|
| 674 |
+
5. Ensure the language is clear, empathetic, and easy for a patient or caregiver to understand. Avoid overly technical jargon where possible.
|
| 675 |
+
6. Include clear instructions in the "RED FLAGS / WHEN TO SEEK HELP" section, distinguishing between immediate emergency (e.g., call 911/local emergency) and urgent but non-emergency situations (e.g., call doctor's office).
|
| 676 |
+
7. Do NOT include any introductory phrases (like "Here is the updated plan:", "Based on your feedback...") or concluding sentences outside the plan structure. Provide ONLY the structured content that fits within the template.
|
| 677 |
+
8. If the previous care plan was unavailable or unreadable, create the plan based solely on the patient information and feedback, still following the template.
|
| 678 |
+
9. Ensure the plan is medically sound and reflects standard care principles. If feedback indicates a significant change or potential issue, the ASSESSMENT and subsequent sections should clearly address this and recommend appropriate actions (like contacting their doctor for a re-evaluation, even if it's not an immediate emergency).
|
| 679 |
+
10. If the feedback indicates significant improvement, the plan should reflect this (e.g., adjusting activity levels up, noting successful symptom management) while still including monitoring and red flags.
|
| 680 |
+
|
| 681 |
Care Plan Format Template:
|
| 682 |
{care_plan_format}
|
| 683 |
"""
|
|
|
|
| 687 |
response = model.generate_content(prompt)
|
| 688 |
generated_plan_text = response.text.strip()
|
| 689 |
|
| 690 |
+
# Remove markdown code block formatting if present
|
| 691 |
if generated_plan_text.startswith('```') and generated_plan_text.endswith('```'):
|
| 692 |
generated_plan_text = generated_plan_text[3:-3].strip()
|
| 693 |
+
# Remove language identifier if present (e.g., 'text', 'markdown')
|
| 694 |
+
if generated_plan_text.lower().startswith('text'):
|
| 695 |
generated_plan_text = generated_plan_text[4:].strip()
|
| 696 |
+
elif generated_plan_text.lower().startswith('markdown'):
|
| 697 |
+
generated_plan_text = generated_plan_text[8:].strip()
|
| 698 |
+
|
| 699 |
|
| 700 |
print(f"AI Response received. Length: {len(generated_plan_text)}")
|
| 701 |
|
| 702 |
+
# Re-evaluate status based on generated plan content in conjunction with feedback
|
| 703 |
+
# This is a secondary check. The primary status is from feedback.
|
| 704 |
+
# We only update status here if the *generated plan* explicitly uses keywords
|
| 705 |
+
# that might contradict or strongly reinforce the initial feedback-based status.
|
| 706 |
+
# However, to avoid AI hallucination changing status, let's stick to feedback-driven status for submission for now.
|
| 707 |
+
# The UI can highlight AI warnings if the plan mentions critical things the feedback didn't.
|
| 708 |
+
# For simplicity, the status saved to the DB will be the one determined *before* AI generation based on feedback.
|
| 709 |
+
final_plan_to_save = generated_plan_text
|
| 710 |
+
final_status_to_save = status # Use the status determined before AI call
|
| 711 |
|
| 712 |
except Exception as ai_error:
|
| 713 |
print(f"Error generating content from AI: {ai_error}")
|
| 714 |
+
# If AI fails, construct an error message plan
|
| 715 |
generated_plan_text = f"[Error generating updated plan from AI: {ai_error}]\n\n"
|
| 716 |
+
if care_plan_text and "[No readable text found" not in care_plan_text and "[Error extracting PDF text" not in care_plan_text and "[PDF Content Unavailable" not in care_plan_text:
|
| 717 |
+
generated_plan_text += "Falling back to original plan if available:\n\n" + care_plan_text
|
| 718 |
+
# If falling back to original, status should reflect original plan/feedback, not just AI failure
|
| 719 |
+
final_status_to_save = determine_patient_status(care_plan_text, care_plan_text, feedback)
|
| 720 |
else:
|
| 721 |
generated_plan_text += "No previous plan available."
|
| 722 |
+
final_status_to_save = status # Keep the feedback-determined status
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
|
| 724 |
+
final_plan_to_save = generated_plan_text
|
| 725 |
|
| 726 |
# Create and store patient record in the database
|
| 727 |
new_patient = Patient(
|
|
|
|
| 729 |
age=age,
|
| 730 |
gender=gender,
|
| 731 |
feedback=feedback,
|
| 732 |
+
original_plan=care_plan_text, # Store extracted text, could be error message
|
| 733 |
+
updated_plan=final_plan_to_save, # Store generated/fallback plan text
|
| 734 |
+
status=final_status_to_save, # Store determined status
|
| 735 |
timestamp=datetime.utcnow()
|
| 736 |
)
|
| 737 |
db.session.add(new_patient)
|
| 738 |
db.session.commit()
|
| 739 |
patient_id = new_patient.id
|
| 740 |
|
| 741 |
+
print(f"Patient {patient_id} added to DB with status: {final_status_to_save}.")
|
| 742 |
|
| 743 |
# Generate PDF for downloading using the stored data
|
| 744 |
+
# Note: We pass the patient object directly to the PDF generator for simplicity
|
| 745 |
+
# and to include feedback in the PDF
|
| 746 |
+
pdf_buffer = generate_care_plan_pdf(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
|
|
|
|
|
|
|
|
|
|
| 747 |
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
|
| 748 |
print("PDF generated and base64 encoded.")
|
| 749 |
|
| 750 |
+
# Send care plan via WhatsApp (using the final saved data)
|
| 751 |
+
# Note: We pass the patient object directly to the WhatsApp function
|
| 752 |
+
whatsapp_sent, whatsapp_message = send_whatsapp_care_plan(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
|
| 753 |
+
print(f"WhatsApp message attempt sent: {whatsapp_sent}, message: {whatsapp_message}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
|
| 755 |
+
# Return success response, include relevant data
|
| 756 |
return jsonify({
|
| 757 |
'success': True, # Indicate that patient record was created/updated
|
| 758 |
+
'updated_plan': new_patient.updated_plan, # Return the final saved plan
|
| 759 |
'pdf_data': pdf_base64,
|
| 760 |
'patient_id': patient_id,
|
| 761 |
+
'status': new_patient.status, # Return the final determined status
|
| 762 |
'whatsapp_sent': whatsapp_sent,
|
| 763 |
+
'whatsapp_message': whatsapp_message,
|
| 764 |
+
'ai_error': (not model) or ("Error generating updated plan" in new_patient.updated_plan) # Indicate if AI failed or was skipped
|
| 765 |
})
|
| 766 |
|
| 767 |
except Exception as e:
|
|
|
|
| 774 |
'error': f'An unexpected server error occurred: {str(e)}'
|
| 775 |
}), 500
|
| 776 |
|
| 777 |
+
# --- New routes for Doctor Dashboard actions ---
|
| 778 |
+
|
| 779 |
+
@app.route('/update_care_plan/<patient_id>', methods=['PUT'])
|
| 780 |
+
def update_care_plan(patient_id):
|
| 781 |
+
"""Endpoint for doctor to save edited care plan text."""
|
| 782 |
+
try:
|
| 783 |
+
data = request.get_json()
|
| 784 |
+
if not data or 'updated_plan' not in data:
|
| 785 |
+
return jsonify({'success': False, 'error': 'Invalid data provided.'}), 400
|
| 786 |
+
|
| 787 |
+
updated_plan_text = data['updated_plan'].strip()
|
| 788 |
+
|
| 789 |
+
patient = Patient.query.get(patient_id)
|
| 790 |
+
|
| 791 |
+
if not patient:
|
| 792 |
+
print(f"Patient ID {patient_id} not found for update.")
|
| 793 |
+
return jsonify({'success': False, 'error': 'Patient not found.'}), 404
|
| 794 |
+
|
| 795 |
+
# Optional: Re-determine status based on the manually updated plan + existing feedback/original?
|
| 796 |
+
# For simplicity, we'll keep the existing status unless explicitly changed via another mechanism.
|
| 797 |
+
# If doctor edits to add critical info, the status should ideally be manually updated too.
|
| 798 |
+
# But for this request, just saving the text is sufficient.
|
| 799 |
+
# patient.status = determine_patient_status(patient.original_plan, updated_plan_text, patient.feedback) # This would make saving potentially change status
|
| 800 |
+
|
| 801 |
+
patient.updated_plan = updated_plan_text
|
| 802 |
+
db.session.commit()
|
| 803 |
+
print(f"Updated plan saved for patient ID: {patient_id}")
|
| 804 |
+
|
| 805 |
+
return jsonify({
|
| 806 |
+
'success': True,
|
| 807 |
+
'message': 'Care plan saved successfully.',
|
| 808 |
+
'patient_id': patient_id,
|
| 809 |
+
# Optionally return the updated status if determined above
|
| 810 |
+
# 'status': patient.status
|
| 811 |
+
})
|
| 812 |
+
|
| 813 |
+
except Exception as e:
|
| 814 |
+
print(f"Error updating care plan for ID {patient_id}: {str(e)}")
|
| 815 |
+
import traceback
|
| 816 |
+
traceback.print_exc()
|
| 817 |
+
db.session.rollback()
|
| 818 |
+
return jsonify({'success': False, 'error': f'An error occurred while saving the plan: {str(e)}'}), 500
|
| 819 |
+
|
| 820 |
+
|
| 821 |
+
@app.route('/send_whatsapp_doctor/<patient_id>', methods=['POST'])
|
| 822 |
+
def send_whatsapp_doctor(patient_id):
|
| 823 |
+
"""Endpoint for doctor to send WhatsApp message for a patient."""
|
| 824 |
+
try:
|
| 825 |
+
patient = Patient.query.get(patient_id)
|
| 826 |
+
|
| 827 |
+
if not patient:
|
| 828 |
+
print(f"Patient ID {patient_id} not found for WhatsApp send.")
|
| 829 |
+
return jsonify({'success': False, 'error': 'Patient not found.'}), 404
|
| 830 |
+
|
| 831 |
+
# Use the latest data from the database for the message
|
| 832 |
+
patient_info_for_whatsapp = patient.to_dict() # Gets name, age, gender, feedback etc.
|
| 833 |
+
care_plan_text_to_send = patient.updated_plan
|
| 834 |
+
status_to_send = patient.status
|
| 835 |
+
|
| 836 |
+
whatsapp_sent, whatsapp_message = send_whatsapp_care_plan(
|
| 837 |
+
patient_info_for_whatsapp,
|
| 838 |
+
care_plan_text_to_send,
|
| 839 |
+
status_to_send
|
| 840 |
+
)
|
| 841 |
+
|
| 842 |
+
if whatsapp_sent:
|
| 843 |
+
print(f"WhatsApp sent successfully for patient ID: {patient_id}")
|
| 844 |
+
return jsonify({'success': True, 'message': whatsapp_message})
|
| 845 |
+
else:
|
| 846 |
+
print(f"WhatsApp failed for patient ID: {patient_id} - {whatsapp_message}")
|
| 847 |
+
return jsonify({'success': False, 'error': whatsapp_message}), 500 # Use 500 for server-side error
|
| 848 |
+
|
| 849 |
+
except Exception as e:
|
| 850 |
+
print(f"Error triggering WhatsApp send for ID {patient_id}: {str(e)}")
|
| 851 |
+
import traceback
|
| 852 |
+
traceback.print_exc()
|
| 853 |
+
return jsonify({'success': False, 'error': f'An error occurred while sending WhatsApp: {str(e)}'}), 500
|
| 854 |
+
|
| 855 |
+
|
| 856 |
+
# --- Existing routes remain below ---
|
| 857 |
|
| 858 |
@app.route('/download_pdf/<patient_id>')
|
| 859 |
def download_pdf(patient_id):
|
|
|
|
| 866 |
print(f"Patient ID {patient_id} not found in database for download.")
|
| 867 |
return "Patient data not found.", 404
|
| 868 |
|
| 869 |
+
# Pass the full patient dict to PDF generator for feedback inclusion
|
|
|
|
|
|
|
|
|
|
|
|
|
| 870 |
pdf_buffer = generate_care_plan_pdf(
|
| 871 |
+
patient.to_dict(),
|
| 872 |
patient.updated_plan,
|
| 873 |
patient.status
|
| 874 |
)
|
|
|
|
| 894 |
|
| 895 |
@app.route('/get_emergency_notifications')
|
| 896 |
def get_emergency_notifications():
|
| 897 |
+
# Only include patients whose status is 'emergency'
|
| 898 |
emergency_patients_query = Patient.query.filter_by(status='emergency').order_by(Patient.timestamp.desc())
|
| 899 |
|
| 900 |
+
# Only return basic info needed for the alert, not full patient details
|
| 901 |
+
notifications = [{'id': p.id, 'name': p.name, 'status': p.status} for p in emergency_patients_query.all()]
|
| 902 |
|
| 903 |
return jsonify({
|
| 904 |
'success': True,
|
|
|
|
| 909 |
@app.route('/get_patients')
|
| 910 |
def get_patients():
|
| 911 |
all_patients_query = Patient.query.order_by(Patient.timestamp.desc())
|
| 912 |
+
# Include status in the dictionary representation
|
| 913 |
patients_list = [p.to_dict() for p in all_patients_query.all()]
|
| 914 |
return jsonify({
|
| 915 |
'success': True,
|
|
|
|
| 927 |
return jsonify({'success': False, 'error': 'Patient not found.'}), 404
|
| 928 |
|
| 929 |
print(f"Found patient {patient_id}: {patient.name}")
|
| 930 |
+
# Return the full patient data including original and updated plans
|
| 931 |
return jsonify({
|
| 932 |
'success': True,
|
| 933 |
'patient': patient.to_dict()
|
| 934 |
})
|
| 935 |
|
| 936 |
+
# Route to handle patient deletion
|
| 937 |
@app.route('/delete_patient/<patient_id>', methods=['DELETE'])
|
| 938 |
def delete_patient(patient_id):
|
| 939 |
print(f"Delete requested for patient ID: {patient_id}")
|
|
|
|
| 961 |
|
| 962 |
|
| 963 |
if __name__ == '__main__':
|
| 964 |
+
# Create database tables if they don't exist within the application context
|
| 965 |
with app.app_context():
|
| 966 |
db.create_all()
|
| 967 |
# Use a more robust development server like Waitress or Gunicorn in production
|
| 968 |
+
# For development, debug=True is fine
|
| 969 |
app.run(debug=True)
|