rajkhanke commited on
Commit
cb23d25
·
verified ·
1 Parent(s): 31c4836

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +185 -197
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 # 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, # Slightly reduced temp for more stable output
107
- "top_p": 0.9, # Adjusted top_p
108
- "top_k": 30, # Adjusted top_k
109
- "max_output_tokens": 4096, # Reduced token limit slightly if needed, or increase if plans are long
110
  }
111
 
112
  model = genai.GenerativeModel(
113
- model_name="gemini-1.5-flash-latest", # Recommended model alias
114
  generation_config=generation_config,
115
  )
116
- # Test model generation (optional)
117
- # model.generate_content("Hello")
118
  print(f"Using Gemini model: {model.model_name}")
119
 
120
  except Exception as e:
121
  print(f"Error configuring Gemini API or loading model: {e}")
122
- model = None # Handle cases where API configuration fails
123
  else:
124
  print("GENAI_API_KEY environment variable not set. AI generation disabled.")
125
 
@@ -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('') # Attempt decryption with empty password
138
  except Exception as e:
139
  print(f"PDF is encrypted and cannot be decrypted: {e}")
140
- return "[PDF Content Unavailable: File is encrypted]" # Return a message indicating failure
141
 
142
  for page in pdf_reader.pages:
143
  page_text = page.extract_text()
144
  if page_text:
145
- text += page_text + "\n" # Add newline between pages
146
- return text.strip() or "[No readable text found in PDF]" # Return placeholder if no text
147
  except Exception as e:
148
  print(f"Error extracting PDF text: {e}")
149
- return f"[Error extracting PDF text: {e}]" # Include error in placeholder
150
 
151
 
152
  def extract_care_plan_format(pdf_text):
153
  """Extract a general format template from PDF text by identifying common section headers."""
154
  if not pdf_text or "[No readable text found" in pdf_text or "[Error extracting PDF text" in pdf_text:
155
- return None # Cannot extract format from empty or error text
156
 
157
- # Look for lines that seem like headers (e.g., capitalized words ending with :, followed by content)
158
- # This regex is an attempt to capture common patterns but might not be perfect for all PDFs.
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 # No recognizable format found
176
 
177
  format_template = ""
178
  for section_title, _ in sections:
179
- format_template += f"{section_title.strip()}:\n- [Details]\n" # Use a placeholder bullet structure
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) # Add margins
258
 
259
  styles = getSampleStyleSheet()
260
 
261
- # Create custom styles
262
  title_style = ParagraphStyle(
263
  'Title',
264
  parent=styles['Heading1'],
265
- fontSize=22, # Slightly larger title
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, # Slightly larger headings
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"), # Danger color
303
- 'deteriorating': colors.HexColor("#f6c23e"), # Warning color
304
- 'improving': colors.HexColor("#1cc88a"), # Success color
305
- 'stable': colors.HexColor("#36b9cc") # Info color
 
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)], # Ensure age is string for Paragraph
341
  [Paragraph("<b>Gender:</b>", normal_style), Paragraph(patient_info.get('gender', 'N/A'), normal_style)],
342
  [Paragraph("<b>Generated Date:</b>", normal_style), Paragraph(datetime.now().strftime("%Y-%m-%d %H:%M"), normal_style)]
343
  ]
344
 
345
- patient_table = Table(patient_data, colWidths=[150, 360]) # Adjusted column widths
346
  patient_table.setStyle(TableStyle([
347
- ('BACKGROUND', (0, 0), (0, -1), colors.HexColor("#f2f2f2")), # Lighter grey
348
  ('TEXTCOLOR', (0, 0), (0, -1), colors.black),
349
  ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
350
  ('VALIGN', (0, 0), (-1, -1), 'TOP'),
351
- ('INNERGRID', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")), # Lighter grid
352
  ('BOX', (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")),
353
  ('LEFTPADDING', (0, 0), (-1, -1), 6),
354
  ('RIGHTPADDING', (0, 0), (-1, -1), 6),
355
- ('TOPPADDING', (0, 0), (-1, -1), 6), # Increased padding
356
- ('BOTTOMPADDING', (0, 0), (-1, -1), 6), # Increased padding
357
  ]))
358
 
359
  story.append(patient_table)
360
- story.append(Spacer(1, 25)) # More space after table
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 # Skip empty lines
374
 
375
- # Check for potential section headers (e.g., "SECTION TITLE:")
376
  header_match = re.match(r'^([A-Z][A-Z\s]*):$', stripped_line)
377
  if header_match:
378
- story.append(Spacer(1, 8)) # Space before new section
379
  story.append(Paragraph(stripped_line, heading_style))
380
- # Check for bullet points (handles -, *, •)
381
  elif stripped_line.startswith('-') or stripped_line.startswith('*') or stripped_line.startswith('•'):
382
- # Clean the bullet point text
383
- bullet_text = re.sub(r'^[-*•]\s*', '', line).strip() # Use original line to preserve initial indentation
384
  if bullet_text:
385
  story.append(Paragraph(f"• {bullet_text}", bullet_style))
386
- else: # Handle empty bullet points
387
  story.append(Paragraph("• ", bullet_style))
388
  else:
389
- # Treat as normal paragraph content
390
- story.append(Paragraph(line, normal_style)) # Use original line to preserve indentation/spacing within paragraphs
391
 
392
-
393
- # Add footer or timestamp if desired
394
  story.append(Spacer(1, 20))
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}" # Include the full plan text
448
 
449
  print(f"Attempting to send WhatsApp message to {TWILIO_TO}...")
450
- # Send WhatsApp message using hardcoded number
451
  message_sent = twilio_client.messages.create(
452
  from_=TWILIO_FROM,
453
  body=message,
@@ -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') # Get as string initially
501
  gender = request.form.get('gender', 'N/A')
502
  feedback = request.form.get('feedback', '')
503
 
504
- # Validate required fields
505
  if not name or not feedback:
506
  return jsonify({'success': False, 'error': 'Patient Name and Feedback are required.'}), 400
507
  if age:
508
  try:
509
- age = int(age) # Convert age to integer
510
  if age <= 0: raise ValueError("Age must be positive")
511
  except ValueError:
512
  return jsonify({'success': False, 'error': 'Invalid Age provided.'}), 400
513
  else:
514
- age = None # Store as None if not provided or invalid
515
 
516
  care_plan_text = ""
517
  care_plan_format = None
518
 
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
- # Determine status BEFORE calling AI, especially for emergency
571
- initial_status = determine_patient_status(care_plan_text, "", feedback) # Compare against empty updated plan initially
572
-
573
 
574
  generated_plan_text = ""
575
- status = initial_status # Start with status determined from feedback
576
 
577
  if status == 'emergency':
578
  print("Emergency status detected from feedback. Generating emergency plan.")
579
- # If emergency symptoms are detected in feedback, generate emergency instructions
580
  generated_plan_text = (
581
  "PATIENT INFORMATION:\n"
582
  f"- Name: {name}\n"
@@ -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() # Get the text and strip leading/trailing whitespace
634
 
635
- # Clean up common AI formatting issues (e.g., extra ```)
636
  if generated_plan_text.startswith('```') and generated_plan_text.endswith('```'):
637
  generated_plan_text = generated_plan_text[3:-3].strip()
638
- if generated_plan_text.startswith('text'): # Handle ```text...```
639
  generated_plan_text = generated_plan_text[4:].strip()
640
 
641
-
642
  print(f"AI Response received. Length: {len(generated_plan_text)}")
643
- # print(f"AI Response (first 200 chars):\n---\n{generated_plan_text[:200]}\n---") # Print truncated response
644
-
645
 
646
- # Re-determine patient status based on feedback AND the generated updated plan
 
647
  status = determine_patient_status(care_plan_text, generated_plan_text, feedback)
648
 
 
649
  except Exception as ai_error:
650
  print(f"Error generating content from AI: {ai_error}")
651
- # Fallback: Use initial status and provide an error message in the plan
652
  generated_plan_text = f"[Error generating updated plan from AI: {ai_error}]\n\n"
653
- if care_plan_text and "[No readable text found" not in care_plan_text:
654
  generated_plan_text += "Showing original plan due to AI error:\n\n" + care_plan_text
655
- status = determine_patient_status(care_plan_text, care_plan_text, feedback) # Re-evaluate status based on original plan + feedback
 
 
656
  else:
657
  generated_plan_text += "No previous plan available."
658
- status = initial_status # Keep the initial status if no original plan
659
-
660
- return jsonify({
661
- 'success': False,
662
- 'error': f'Error generating care plan: {ai_error}',
663
- 'fallback_plan': generated_plan_text, # Optionally send fallback plan
664
- 'patient_info': {'name': name, 'age': age, 'gender': gender},
665
- 'status': status,
666
- 'original_plan': care_plan_text # Include original plan if any
667
- }), 500 # Return 500 if AI fails, but provide some data
668
 
 
 
 
669
 
670
- # --- Actions after plan generation (Emergency or AI) ---
671
 
672
  # Create and store patient record in the database
673
  new_patient = Patient(
@@ -675,18 +589,17 @@ Care Plan Format Template:
675
  age=age,
676
  gender=gender,
677
  feedback=feedback,
678
- original_plan=care_plan_text, # Store original text
679
- updated_plan=generated_plan_text, # Store the generated plan text
680
  status=status,
681
- timestamp=datetime.utcnow() # Use UTC time
682
  )
683
  db.session.add(new_patient)
684
  db.session.commit()
685
- patient_id = new_patient.id # Get the generated ID
686
 
687
  print(f"Patient {patient_id} added to DB with status: {status}.")
688
 
689
-
690
  # Generate PDF for downloading using the stored data
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, # Send back the generated text
712
- 'pdf_data': pdf_base64, # Send base64 data for preview
713
- 'patient_id': patient_id, # Send the generated patient ID for download
714
  'status': status,
715
- 'whatsapp_sent': whatsapp_sent
 
 
716
  })
717
 
718
  except Exception as e:
719
- # Catch any other unexpected errors
720
  print(f"An unexpected error occurred during submission: {str(e)}")
721
- # Log the traceback for debugging
722
  import traceback
723
  traceback.print_exc()
724
- # Rollback database session in case of error
725
  db.session.rollback()
726
  return jsonify({
727
  'success': False,
@@ -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) # Ensure buffer is at the start
758
 
759
- # Sanitize patient name for filename
760
  safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '', patient.name or 'patient').lower()
761
- download_name = f"care_plan_{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" # Add timestamp to filename
762
 
763
  print(f"Serving PDF for {patient_id} as {download_name}")
764
  return send_file(
@@ -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() # Return dictionary representation
824
  })
825
 
826
- # Helper to serve PDF file directly (used by doctor dashboard PDF viewer)
827
- # Note: This route is potentially less secure than storing files on disk and serving them
828
- # or generating on demand. But for a demo, it works with the base64 data flow.
829
- # A better approach for large PDFs or production would be saving the PDF server-side
830
- # and serving a URL that points to the saved file, or using a dedicated PDF viewer lib.
831
- # For this fix, let's stick to serving the *generated* PDF data via download link,
832
- # and rely on the browser's embedding for the patient view preview on initial submit.
833
- # Doctor dashboard will just show the text content in tabs.
834
 
835
- # Removed the direct PDF view route as it's problematic.
836
- # The doctor dashboard will display the text content in the tabs.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
 
838
 
839
  if __name__ == '__main__':
840
- # Use a more robust development server like Waitress or Gunicorn in production
841
- # Ensure database tables are created before running the app
842
  with app.app_context():
843
  db.create_all()
844
- app.run(debug=True) # Explicitly set port
 
 
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)