rajkhanke commited on
Commit
148dccf
·
verified ·
1 Parent(s): eab47dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +144 -173
app.py CHANGED
@@ -64,22 +64,21 @@ class Patient(db.Model):
64
  # Create database tables if they don't exist
65
  @app.before_request
66
  def create_tables():
67
- # Ensure this runs only once per application start
68
- # Use a flag to prevent repeated checks/creations on every request
69
- if not getattr(app, '_tables_created', False):
70
  with app.app_context():
 
 
71
  inspector = db.inspect(db.engine)
72
- if not inspector.has_table("patient"): # Check for at least one table
73
  logger.info("Creating database tables.")
74
  db.create_all()
75
- app._tables_created = True # Set flag
76
- else:
77
- logger.info("Database tables already exist.")
78
- app._tables_created = True # Set flag even if they exist
79
 
80
 
81
- upload_base = os.getenv('UPLOAD_DIR', '/tmp') # Changed base to /tmp, safer default
82
- upload_folder = os.path.join(upload_base, 'uploads', 'pdfs') # Added 'uploads' subdirectory
83
 
84
  app.config['UPLOAD_FOLDER'] = upload_folder
85
 
@@ -93,9 +92,6 @@ AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN')
93
  TWILIO_FROM = os.getenv('TWILIO_FROM_NUMBER')
94
  TWILIO_TO = os.getenv('TWILIO_TO_NUMBER') # Hardcoded number as requested, consider making this configurable per patient
95
 
96
- # Define WhatsApp message length limit (slightly below the official 4096 for safety)
97
- WHATSAPP_MAX_CHARS = 4000
98
-
99
  twilio_client = None
100
  if ACCOUNT_SID and AUTH_TOKEN and TWILIO_FROM and TWILIO_TO:
101
  try:
@@ -154,18 +150,23 @@ def extract_text_from_pdf(pdf_file):
154
  text = ""
155
  if pdf_reader.is_encrypted:
156
  try:
157
- # Attempt decryption with common/empty passwords - very basic
 
 
 
158
  try:
159
  pdf_reader.decrypt('') # Try with empty password
160
- logger.warning("PDF is encrypted but decrypted successfully with empty password.")
161
  except PyPDF2.errors.PasswordError:
 
162
  logger.warning("PDF is encrypted and requires a password.")
163
  return "[PDF Content Unavailable: File is encrypted and requires a password]"
164
  except Exception as dec_e:
 
165
  logger.error(f"Error during PDF decryption attempt: {dec_e}")
166
  return "[PDF Content Unavailable: Error decrypting file]"
167
 
168
- except Exception: # Catch any other unexpected error
 
169
  logger.error("Unexpected error during PDF decryption check.")
170
  return "[PDF Content Unavailable: Unexpected error with encrypted file]"
171
 
@@ -178,15 +179,9 @@ def extract_text_from_pdf(pdf_file):
178
  text += page_text + "\n"
179
  except Exception as page_e:
180
  logger.error(f"Error extracting text from page {page_num + 1}: {page_e}")
181
- text += f"[Error extracting page {page_num + 1}]\n" # Add placeholder
182
-
183
-
184
- extracted = text.strip()
185
- if not extracted:
186
- logger.info("PDF text extraction resulted in empty string.")
187
- return "[No readable text found in PDF]"
188
- return extracted
189
 
 
190
  except Exception as e:
191
  logger.error(f"Error extracting PDF text: {e}", exc_info=True)
192
  return f"[Error extracting PDF text: {e}]"
@@ -194,34 +189,44 @@ def extract_text_from_pdf(pdf_file):
194
 
195
  def extract_care_plan_format(pdf_text):
196
  """Extract a general format template from PDF text by identifying common section headers."""
197
- # Define unreadable indicators
198
- unreadable_indicators = ["[No readable text found", "[Error extracting PDF text", "[PDF Content Unavailable"]
199
-
200
- if not pdf_text or any(indicator in pdf_text for indicator in unreadable_indicators):
201
  logger.info("No valid PDF text available to extract format.")
202
  return None
203
 
204
- # Pattern to find potential headers: Start of line, contains uppercase letters/spaces, ends with colon or period,
205
- # possibly followed by whitespace only.
206
- # \b ensures we match whole words at boundaries
207
- # [:.] accounts for headers ending in colon or period
208
- # (?:[\s\r\n]|$) accounts for whitespace or end of string after colon/period
209
- potential_headers = re.findall(r'^\s*([A-Z][A-Z\s]*)\b[ \t]*[:.]?[\s\r\n]*$', pdf_text, re.MULTILINE)
210
 
211
  if not potential_headers:
212
  # Fallback: Look for lines that start with a capital letter and seem like standalone headers
213
- # Ensure it doesn't look like a sentence (e.g., short phrase, starts with Cap, ends maybe with space or newline).
214
- # Simple heuristic: Starts with Capital, followed by 3+ letters/spaces, ends before another capital or end of line.
215
- # This is less reliable than the colon/period method.
216
- fallback_headers_loose = re.findall(r'^[A-Z][A-Za-z\s]{3,}(?=\s[A-Z]|\s*$)', pdf_text, re.MULTILINE)
 
 
 
 
 
 
 
 
 
 
 
217
  fallback_headers_loose = [h.strip() for h in fallback_headers_loose if h.strip()]
218
  if fallback_headers_loose:
219
- # Filter out potential sentences - check length, word count, maybe look for typical header words
220
- fallback_headers_loose = [h for h in fallback_headers_loose if len(h) > 5 and len(h.split()) < 7 and not h.endswith('.')] # Example filter
 
 
221
  if fallback_headers_loose:
222
  logger.info(f"Extracted potential headers (fallback - loose): {list(set(fallback_headers_loose))}")
223
  unique_headers = sorted(list(set([h.strip() for h in fallback_headers_loose if h.strip()])))
224
- format_template = "\n".join([f"{header.strip()}:" for header in unique_headers if header.strip()])
225
  return format_template if format_template.strip() else None
226
 
227
 
@@ -229,8 +234,7 @@ def extract_care_plan_format(pdf_text):
229
  return None
230
 
231
  # Use a set to get unique headers and sort them for consistency
232
- # Clean up headers: remove trailing colons/periods and strip
233
- unique_headers = sorted(list(set([re.sub(r'[:.\s]*$', '', h).strip() for h in potential_headers if h.strip()])))
234
 
235
  if not unique_headers:
236
  logger.info("Extracted headers are empty after cleaning.")
@@ -243,9 +247,10 @@ def extract_care_plan_format(pdf_text):
243
  return format_template if format_template.strip() else None
244
 
245
 
 
246
  def determine_patient_status(original_plan, updated_plan, feedback):
247
  """
248
- Determine patient status based on feedback, original plan, and the final updated plan text.
249
  Prioritizes emergency > deteriorating > improving. Checks feedback/original first,
250
  then checks updated plan for emergency confirmation. Uses refined keyword matching.
251
  """
@@ -253,10 +258,11 @@ def determine_patient_status(original_plan, updated_plan, feedback):
253
 
254
  feedback_lower = feedback.lower() if feedback else ""
255
  original_plan_lower = original_plan.lower() if original_plan else ""
256
- updated_plan_lower = updated_plan.lower() if updated_plan else ""
257
 
258
 
259
  # Define robust keyword lists
 
260
  emergency_keywords = [
261
  "severe chest pain", "heart attack", "sudden shortness of breath",
262
  "difficulty breathing severely", "loss of consciousness", "unresponsive",
@@ -270,11 +276,11 @@ def determine_patient_status(original_plan, updated_plan, feedback):
270
  "signs of shock", "severe dehydration symptoms", "acute change in mental status",
271
  "unstable vitals", "rapidly worsening symptoms", "can't breathe", "chest tight severe",
272
  "severe difficulty swallowing suddenly", "new onset paralysis", "severe burns",
273
- "major trauma", "suspected poisoning", "overdose", "suicidal thoughts active",
274
- "unstable", "critically ill", "life-threatening", "no pulse", "low oxygen saturation severe",
275
- "uncontrolled high fever", "septic shock", "signs of major infection" # Added a few more
276
  ]
277
 
 
278
  deteriorating_keywords = [
279
  "worsening", "increased pain", "not improving", "deteriorating",
280
  "getting worse", "more frequent symptoms", "elevated", "higher",
@@ -287,10 +293,10 @@ def determine_patient_status(original_plan, updated_plan, feedback):
287
  "tired all the time", "much weaker", "feeling noticeably worse", "consistent high blood pressure",
288
  "uncontrolled blood sugar levels", "increased swelling", "persistent cough getting worse",
289
  "unexplained weight loss significant", "gradual decline", "unmanaged symptoms",
290
- "not resolving", "changes in condition", "difficulty performing daily tasks",
291
- "pain getting worse", "more tired", "less active", "increased falls", "poorly controlled" # Added a few more
292
  ]
293
 
 
294
  improvement_keywords = [
295
  "improving", "better today", "reduced pain", "lower", "less frequent",
296
  "healing well", "recovery on track", "making progress", "stable condition",
@@ -303,37 +309,49 @@ def determine_patient_status(original_plan, updated_plan, feedback):
303
  "more energy", "walking further", "blood pressure normal range",
304
  "blood sugar stable", "swelling reduced", "easier breathing", "cough improving",
305
  "weight gain healthy", "feeling like myself again", "in remission", "managing well at home",
306
- "tolerating well", "no issues reported", "feeling good", "symptoms gone",
307
- "activity increased", "pain well managed", "sleeping better", "appetite improved" # Added a few more
308
  ]
309
 
310
- # Helper to check if any keyword is found in the text (using word boundaries)
311
  def check_keywords(text, keywords):
312
- if not text: return False
313
  pattern = r'\b(?:' + '|'.join(re.escape(kw) for kw in keywords) + r')\b'
314
  return re.search(pattern, text) is not None
315
 
316
  # --- Classification Logic ---
317
 
318
- # 1. Check for EMERGENCY status (Highest Priority)
319
- # Check combined initial text (feedback + original plan) AND the final generated plan text
320
- combined_text_lower = feedback_lower + " " + (original_plan_lower if original_plan_lower else "") + " " + (updated_plan_lower if updated_plan_lower else "")
321
- combined_text_lower = re.sub(r'\s+', ' ', combined_text_lower).strip() # Clean up spaces
322
-
323
- if check_keywords(combined_text_lower, emergency_keywords):
324
- logger.info("Status determined: EMERGENCY (keyword found).")
325
- return "emergency"
326
 
327
  # 2. Check for DETERIORATING status (Second Priority)
328
- if check_keywords(combined_text_lower, deteriorating_keywords):
329
- logger.info("Status determined: DETERIORATING (keyword found).")
 
 
330
  return "deteriorating"
331
 
332
- # 3. Check for IMPROVING status (Third Priority)
333
- if check_keywords(combined_text_lower, improvement_keywords):
334
- logger.info("Status determined: IMPROVING (keyword found).")
 
 
335
  return "improving"
336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  # 4. Default to STABLE if no specific status keywords are found
338
  logger.info("Status determined: STABLE (no specific status keywords found).")
339
  return "stable"
@@ -473,21 +491,16 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
473
 
474
  # Check for section headers (e.g., "MEDICATIONS:", "ASSESSMENT:")
475
  # Look for lines starting with one or more uppercase words followed by a colon or period
476
- header_match = re.match(r'^\s*([A-Z][A-Z\s]*)\b[ \t]*[:.]?[\s\r\n]*$', line) # Use original line to preserve leading spaces if intentional?
477
  if header_match:
478
  # Remove trailing colon or period for cleaner heading
479
  header_text = re.sub(r'[:.\s]*$', '', stripped_line).strip()
480
- if header_text: # Ensure there's actual text before adding a heading
481
- story.append(Spacer(1, 8)) # Add space before a new section
482
- story.append(Paragraph(header_text + ":", heading_style)) # Use heading style for sections
483
- # else: # If line was just ":" or ".", treat as empty and skip or handle as plain text?
484
- # pass # Skipping lines that are just punctuation seems reasonable
485
-
486
- # Check for list items (starting with -, *, •) - only if NOT matched as a header
487
- # Use original line to preserve leading spaces/tabs for bullet indent
488
- elif line.lstrip().startswith(('-', '*', '•')): # Use lstrip to check for bullet at start ignoring leading space
489
- # Remove the bullet character and any leading space/tab after it
490
- bullet_text = re.sub(r'^[\s-]*[-*•][ \t]*', '', line).strip() # More robust removal including space/tab before bullet
491
  if bullet_text:
492
  formatted_bullet_text = bullet_text.replace('\n', '<br/>')
493
  story.append(Paragraph(f"• {formatted_bullet_text}", bullet_style))
@@ -495,11 +508,9 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
495
  # Handle cases with just a bullet point on a line
496
  story.append(Paragraph("• ", bullet_style))
497
  else:
498
- # Handle regular paragraph text that wasn't a header or bullet
499
- # Only add if the stripped line has content
500
- if stripped_line:
501
- normal_line_content = line.strip().replace('\n', '<br/>') # Use stripped line for content
502
- story.append(Paragraph(normal_line_content, normal_style))
503
 
504
 
505
  story.append(Spacer(1, 20))
@@ -513,31 +524,29 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
513
  return buffer
514
  except Exception as e:
515
  logger.error(f"Error building PDF: {e}", exc_info=True)
516
- # Fallback: create a simple error PDF
517
  error_buffer = io.BytesIO()
518
  c = canvas.Canvas(error_buffer, pagesize=letter)
519
  c.drawString(100, 750, "Error Generating Care Plan PDF")
520
  c.drawString(100, 735, f"Details: {str(e)}")
521
- c.drawString(100, 720, "Please check server logs for more information.")
522
  c.save()
523
  error_buffer.seek(0)
524
  return error_buffer
525
 
526
 
527
  def send_whatsapp_care_plan(patient_info, care_plan_text, status):
528
- """Send care plan via WhatsApp using Twilio with improved formatting and length check"""
529
  if not twilio_client:
530
  logger.warning("Twilio client not configured. Cannot send WhatsApp message.")
531
- return False, "Twilio client not configured. Check server settings."
532
 
533
  if not TWILIO_TO or not TWILIO_FROM:
534
  logger.warning("Twilio TO or FROM number not set.")
535
- return False, "Twilio TO or FROM number not configured. Check server settings."
536
 
537
  # Basic check for empty plan text before sending
538
  if not care_plan_text or not care_plan_text.strip():
539
  logger.warning("Care plan text is empty. Cannot send WhatsApp message.")
540
- return False, "Care plan text is empty. No plan to send."
541
 
542
  try:
543
  status_emoji = {
@@ -560,14 +569,12 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
560
  for line in formatted_plan.split('\n'):
561
  stripped_line = line.strip()
562
  # Check for lines that look like headers (starts with capital, ends with colon/period, possibly followed by whitespace)
563
- # Added ? after [:. ] to handle cases where header doesn't have punctuation
564
- header_match = re.match(r'^[A-Z][A-Za-z\s]*\b[ \t]*[:.]?$', stripped_line)
565
- if header_match:
566
- # Remove trailing colon or period before bolding
567
  header_text = re.sub(r'[:.\s]*$', '', stripped_line).strip()
568
  if header_text: # Only bold if there's actual text before the colon/period
569
- formatted_plan_lines.append(f"*{header_text}:*") # Add colon back after bolding
570
- else: # Handle cases like just ":" or "." on a line
571
  formatted_plan_lines.append(line)
572
  else:
573
  formatted_plan_lines.append(line)
@@ -581,20 +588,12 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
581
  message += f"*Status:* {status_emoji.get(status, 'Unknown')}\n\n"
582
  # Add feedback if available
583
  feedback_text = patient_info.get('feedback')
584
- if feedback_text and feedback_text.strip():
585
  message += f"*Latest Feedback:*\n{feedback_text.strip()}\n\n"
586
 
587
  message += f"*Care Plan Details:*\n{formatted_plan}"
588
 
589
- # --- WhatsApp Message Length Check ---
590
- if len(message) > WHATSAPP_MAX_CHARS:
591
- error_msg = f"WhatsApp message exceeds the maximum length ({WHATSAPP_MAX_CHARS} characters). The full care plan text is too long to send via WhatsApp. Please download the PDF instead."
592
- logger.warning(f"WhatsApp message too long for patient {patient_info.get('name', 'N/A')} (ID: {patient_info.get('id', 'N/A')}). Length: {len(message)}. Max: {WHATSAPP_MAX_CHARS}")
593
- return False, error_msg
594
- # --- End Length Check ---
595
-
596
-
597
- logger.info(f"Attempting to send WhatsApp message to {TWILIO_TO} (length: {len(message)})...")
598
  message_sent = twilio_client.messages.create(
599
  from_=TWILIO_FROM,
600
  body=message,
@@ -606,13 +605,16 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
606
  logger.error(f"Error sending WhatsApp message: {e}", exc_info=True)
607
  # Provide more specific Twilio error details if available
608
  twilio_error_message = str(e)
 
609
  if hasattr(e, 'status_code') and hasattr(e, 'text'):
610
  try:
611
  error_details = json.loads(e.text)
612
  if 'message' in error_details:
613
  twilio_error_message = f"Twilio API error: {error_details['message']} (Code: {error_details.get('code')})"
614
- except (json.JSONDecodeError, Exception):
615
- pass
 
 
616
 
617
  return False, f"Error sending WhatsApp: {twilio_error_message}"
618
 
@@ -655,8 +657,6 @@ def doctor_dashboard():
655
  def submit_feedback():
656
  ai_enabled = bool(model) # Check if model is initialized
657
  ai_error_message = None # Initialize error message
658
- whatsapp_sent = False
659
- whatsapp_message = "WhatsApp sending skipped or not attempted yet." # Default message
660
 
661
  try:
662
  name = request.form.get('name', 'Unnamed Patient')
@@ -673,12 +673,12 @@ def submit_feedback():
673
  try:
674
  age = int(age)
675
  if age <= 0: raise ValueError("Age must be positive")
676
- if age > 150: raise ValueError("Age seems unreasonably high")
677
  except ValueError:
678
  logger.warning(f"Submission failed: Invalid Age provided: {age}")
679
  return jsonify({'success': False, 'error': 'Invalid Age provided.'}), 400
680
  else:
681
- age = None
682
 
683
  care_plan_text = "" # This will store the extracted text from PDF
684
  care_plan_format = None # This will store the detected format
@@ -692,13 +692,10 @@ def submit_feedback():
692
  return jsonify({'success': False, 'error': 'Invalid file type. Only PDF files are allowed.'}), 400
693
 
694
  logger.info(f"Processing uploaded PDF: {pdf_file.filename}")
695
- # Reset the file pointer just in case, although seek(0) is in extract_text_from_pdf
696
- # pdf_file.seek(0) # Redundant due to seek(0) in extract_text_from_pdf
697
  care_plan_text = extract_text_from_pdf(pdf_file)
698
 
699
  # If extraction resulted in an error message, set format to None
700
- unreadable_indicators = ["[No readable text found", "[Error extracting PDF text", "[PDF Content Unavailable"]
701
- if any(indicator in care_plan_text for indicator in unreadable_indicators):
702
  care_plan_format = None
703
  logger.warning(f"PDF text extraction failed or empty: {care_plan_text}")
704
  else:
@@ -709,7 +706,6 @@ def submit_feedback():
709
 
710
  # Determine the initial status based on feedback and original plan
711
  # Pass "" for updated_plan initially, as it hasn't been generated yet for status check
712
- # Using care_plan_text for original_plan part of status check
713
  initial_status = determine_patient_status(care_plan_text, "", feedback)
714
  logger.info(f"Initial status determined based on feedback/original plan: {initial_status}")
715
 
@@ -718,6 +714,7 @@ def submit_feedback():
718
  final_status_to_save = initial_status # Start with initial status
719
 
720
  # Only generate AI plan if AI is enabled AND status isn't immediate emergency based on feedback
 
721
  if final_status_to_save == 'emergency':
722
  logger.info("Emergency status detected. Generating fixed emergency plan.")
723
  generated_plan_text = (
@@ -743,7 +740,8 @@ def submit_feedback():
743
  "- Inform your primary physician/care team as soon as medically stable.\n"
744
  "- A new care plan will be developed after the emergency situation is resolved and evaluated by medical professionals.\n"
745
  )
746
- # For emergency, the status is already 'emergency' and the generated plan is fixed.
 
747
 
748
 
749
  elif ai_enabled: # AI is enabled and initial status is not emergency
@@ -762,8 +760,6 @@ Morning:
762
  - [Morning activities/medications/checks]
763
  Afternoon:
764
  - [Afternoon activities/medications/checks]
765
- Evening:
766
- - [Evening activities/medications/checks]
767
  Night:
768
  - [Night activities/medications/sleep instructions/checks]
769
  MEDICATIONS:
@@ -774,10 +770,6 @@ PHYSICAL ACTIVITY/EXERCISE:
774
  - [Recommended physical activities, duration, frequency, limitations, and progression]
775
  SYMPTOM MANAGEMENT:
776
  - [Detailed instructions for managing specific symptoms (e.g., pain, nausea, shortness of breath), non-pharmacological interventions]
777
- ADDITIONAL RECOMMENDATIONS:
778
- - [Other relevant advice, e.g., rest, monitoring specific vital signs, wound care instructions, mental health support, caregiver tips]
779
- FOLLOW-UP:
780
- - [Details about next scheduled appointment, or criteria for scheduling follow-up (e.g., "call if symptoms worsen significantly")]
781
  """ # Enhanced default format
782
 
783
  prompt = f"""
@@ -789,8 +781,8 @@ Age: {age if age is not None else 'N/A'}
789
  Gender: {gender}
790
  Patient Feedback/Symptoms Update:
791
  {feedback}
792
- Previous Care Plan Details (if available and readable):
793
- {care_plan_text if care_plan_text and not any(indicator in care_plan_text for indicator in unreadable_indicators) else "No previous care plan provided or could not be read."}
794
  Instructions:
795
  1. Generate the updated care plan strictly using the exact following format template.
796
  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.
@@ -801,7 +793,7 @@ Instructions:
801
  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.
802
  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).
803
  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.
804
- 11. Review the feedback and previous plan carefully to determine the most likely current STATUS (e.g., Emergency, Deteriorating, Improving, Stable) and ensure the plan content aligns with that status, especially the RED FLAGS section. The determined status will be saved alongside the plan.
805
  Care Plan Format Template:
806
  {care_plan_format}
807
  """
@@ -809,6 +801,7 @@ Care Plan Format Template:
809
 
810
  try:
811
  response = model.generate_content(prompt)
 
812
  generated_plan_text = response.text.strip()
813
 
814
  # Remove markdown code block formatting if present
@@ -835,6 +828,8 @@ Care Plan Format Template:
835
  logger.info(f"AI Response received. Length: {len(generated_plan_text)}")
836
 
837
  # Re-determine the final status using the generated plan as well.
 
 
838
  final_status_to_save = determine_patient_status(care_plan_text, generated_plan_text, feedback)
839
  logger.info(f"Final status determined after AI generation: {final_status_to_save}")
840
 
@@ -843,11 +838,10 @@ Care Plan Format Template:
843
  logger.error(f"Error generating content from AI: {ai_error}", exc_info=True)
844
  # If AI fails, construct an error message plan
845
  generated_plan_text = f"[Error generating updated plan from AI: {ai_error}]\n\n"
846
- unreadable_indicators = ["[No readable text found", "[Error extracting PDF text", "[PDF Content Unavailable"]
847
- if care_plan_text and not any(indicator in care_plan_text for indicator in unreadable_indicators):
848
  generated_plan_text += "Falling back to original plan if available:\n\n" + care_plan_text
849
  # If falling back to original, status should reflect original plan/feedback
850
- final_status_to_save = determine_patient_status(care_plan_text, care_plan_text, feedback) # Use original plan text for status check if AI failed
851
  ai_error_message = f"AI generation failed. Showing original plan if available. Error: {ai_error}"
852
  else:
853
  generated_plan_text += "No previous plan available."
@@ -860,8 +854,7 @@ Care Plan Format Template:
860
  else: # AI is not enabled and status is not emergency
861
  logger.warning("AI generation is disabled.")
862
  generated_plan_text = f"[AI generation is currently disabled.]\n\n"
863
- unreadable_indicators = ["[No readable text found", "[Error extracting PDF text", "[PDF Content Unavailable"]
864
- if care_plan_text and not any(indicator in care_plan_text for indicator in unreadable_indicators):
865
  generated_plan_text += "Showing original plan if available:\n\n" + care_plan_text
866
  # Status based on original plan/feedback
867
  final_status_to_save = determine_patient_status(care_plan_text, care_plan_text, feedback)
@@ -892,16 +885,17 @@ Care Plan Format Template:
892
  logger.info(f"Patient {patient_id} added to DB with status: {final_status_to_save}.")
893
 
894
  # Generate PDF for downloading using the stored data
 
 
895
  pdf_buffer = generate_care_plan_pdf(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
896
  pdf_buffer.seek(0) # Ensure buffer is at the start before base64 encoding
897
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
898
  logger.info("PDF generated and base64 encoded.")
899
 
900
  # Send care plan via WhatsApp (using the final saved data)
901
- # This call will now handle the length check internally
902
  whatsapp_sent, whatsapp_message = send_whatsapp_care_plan(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
903
- logger.info(f"WhatsApp message attempt finished: {whatsapp_sent}, message: {whatsapp_message}")
904
-
905
 
906
  # Return success response, include relevant data
907
  return jsonify({
@@ -910,8 +904,8 @@ Care Plan Format Template:
910
  'pdf_data': pdf_base64,
911
  'patient_id': patient_id,
912
  'status': new_patient.status, # Return the final determined status
913
- 'whatsapp_sent': whatsapp_sent, # Indicates if the message was *attempted* and passed length check
914
- 'whatsapp_message': whatsapp_message, # Contains success or error message
915
  'ai_error': not ai_enabled or (ai_error_message is not None) # Indicate if AI was not enabled or failed
916
  })
917
 
@@ -942,7 +936,9 @@ def update_care_plan(patient_id):
942
  logger.warning(f"Update failed for ID {patient_id}: Patient not found.")
943
  return jsonify({'success': False, 'error': 'Patient not found.'}), 404
944
 
945
- # Re-determine status based on the manually updated plan + existing feedback/original
 
 
946
  patient.status = determine_patient_status(patient.original_plan, updated_plan_text, patient.feedback)
947
 
948
  patient.updated_plan = updated_plan_text
@@ -979,7 +975,6 @@ def send_whatsapp_doctor(patient_id):
979
  care_plan_text_to_send = patient.updated_plan
980
  status_to_send = patient.status
981
 
982
- # This call now includes the length check
983
  whatsapp_sent, whatsapp_message = send_whatsapp_care_plan(
984
  patient_info_for_whatsapp,
985
  care_plan_text_to_send,
@@ -990,18 +985,18 @@ def send_whatsapp_doctor(patient_id):
990
  logger.info(f"WhatsApp sent successfully for patient ID: {patient_id}")
991
  return jsonify({'success': True, 'message': whatsapp_message})
992
  else:
993
- logger.warning(f"WhatsApp failed (likely too long) for patient ID: {patient_id} - {whatsapp_message}")
994
- # Return a 400 error specifically for the length issue or other client-side fixable problems
995
- # Use a more descriptive error message for the user
996
- return jsonify({'success': False, 'error': whatsapp_message}), 400 # Changed status to 400
997
 
998
  except Exception as e:
999
  logger.error(f"Error triggering WhatsApp send for ID {patient_id}: {str(e)}", exc_info=True)
1000
- # Use 500 for unexpected server errors
1001
- return jsonify({'success': False, 'error': f'An unexpected error occurred while sending WhatsApp: {str(e)}'}), 500
1002
 
1003
 
1004
  # --- Existing routes remain below ---
 
1005
  @app.route('/download_pdf/<patient_id>')
1006
  def download_pdf(patient_id):
1007
  logger.info(f"Download requested for patient ID: {patient_id}")
@@ -1020,30 +1015,8 @@ def download_pdf(patient_id):
1020
  patient.status
1021
  )
1022
 
1023
- # Check if the buffer contains the error message text (simple check)
1024
- # Using latin-1 and ignore for decoding potentially non-UTF8 PDF content
1025
- buffer_content = pdf_buffer.getvalue().decode('latin-1', errors='ignore')
1026
- if "Error Generating Care Plan PDF" in buffer_content:
1027
- logger.error(f"PDF generation failed for ID {patient_id}, returning error PDF.")
1028
- # --- FIX START ---
1029
- # Extract the re.sub call out of the f-string expression
1030
- safe_name_error = re.sub(r'[^a-zA-Z0-9_\-]', '', patient.name or 'patient').lower()
1031
- # Use the variable in the f-string
1032
- download_name_error = f"care_plan_{safe_name_error}_error.pdf"
1033
- # --- FIX END ---
1034
-
1035
- # Send the error PDF, but indicate it's an error
1036
- return send_file(
1037
- io.BytesIO(pdf_buffer.getvalue()), # Need to create a new buffer from the value
1038
- as_attachment=True,
1039
- download_name=download_name_error, # Use the corrected variable here
1040
- mimetype='application/pdf'
1041
- )
1042
-
1043
 
1044
- pdf_buffer.seek(0) # Reset buffer position after checking content
1045
-
1046
- # This part was already correct
1047
  safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '', patient.name or 'patient').lower()
1048
  download_name = f"care_plan_{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
1049
 
@@ -1056,14 +1029,15 @@ def download_pdf(patient_id):
1056
  )
1057
  except Exception as e:
1058
  logger.error(f"PDF Download Error for ID {patient_id}: {str(e)}", exc_info=True)
1059
- # Fallback error response if even generating the basic error PDF fails
1060
  return f"Error generating PDF for download: {str(e)}", 500
1061
-
1062
 
1063
 
1064
  @app.route('/get_emergency_notifications')
1065
  def get_emergency_notifications():
1066
  # Only include patients whose status is 'emergency'
 
 
 
1067
  emergency_patients_query = Patient.query.filter_by(status='emergency').order_by(Patient.timestamp.desc())
1068
 
1069
  # Only return basic info needed for the alert, not full patient details
@@ -1136,11 +1110,8 @@ if __name__ == '__main__':
1136
  if not inspector.has_table("patient"): # Check for at least one model's table
1137
  logger.info("Database tables not found, creating.")
1138
  db.create_all()
1139
- app._tables_created = True # Set flag after creation
1140
  else:
1141
  logger.info("Database tables already exist.")
1142
- app._tables_created = True # Set flag if they exist
1143
-
1144
 
1145
  # Use a more robust development server like Waitress or Gunicorn in production
1146
  # For development, debug=True is fine
 
64
  # Create database tables if they don't exist
65
  @app.before_request
66
  def create_tables():
67
+ # Ensure this runs only once per request or use a flag
68
+ # Simple approach for development: check if tables exist
69
+ if not app.config.get('_tables_created', False):
70
  with app.app_context():
71
+ # Check if at least one table exists (e.g., the Patient table)
72
+ # This check is basic and might not be sufficient for complex setups
73
  inspector = db.inspect(db.engine)
74
+ if not inspector.has_table("patient"):
75
  logger.info("Creating database tables.")
76
  db.create_all()
77
+ app.config['_tables_created'] = True # Set flag
 
 
 
78
 
79
 
80
+ upload_base = os.getenv('UPLOAD_DIR', '/tmp/uploads')
81
+ upload_folder = os.path.join(upload_base, 'pdfs')
82
 
83
  app.config['UPLOAD_FOLDER'] = upload_folder
84
 
 
92
  TWILIO_FROM = os.getenv('TWILIO_FROM_NUMBER')
93
  TWILIO_TO = os.getenv('TWILIO_TO_NUMBER') # Hardcoded number as requested, consider making this configurable per patient
94
 
 
 
 
95
  twilio_client = None
96
  if ACCOUNT_SID and AUTH_TOKEN and TWILIO_FROM and TWILIO_TO:
97
  try:
 
150
  text = ""
151
  if pdf_reader.is_encrypted:
152
  try:
153
+ # Attempt decryption - PyPDF2 v3+ might not need password for simple cases
154
+ # or might fail differently. Explicitly try common/empty passwords.
155
+ # Note: Robust decryption requires knowing the password, which is not handled here.
156
+ # This attempt is basic.
157
  try:
158
  pdf_reader.decrypt('') # Try with empty password
 
159
  except PyPDF2.errors.PasswordError:
160
+ # If empty password fails, and it's truly password protected
161
  logger.warning("PDF is encrypted and requires a password.")
162
  return "[PDF Content Unavailable: File is encrypted and requires a password]"
163
  except Exception as dec_e:
164
+ # Handle other potential decryption errors
165
  logger.error(f"Error during PDF decryption attempt: {dec_e}")
166
  return "[PDF Content Unavailable: Error decrypting file]"
167
 
168
+
169
+ except Exception: # Catch any other unexpected error during decryption check
170
  logger.error("Unexpected error during PDF decryption check.")
171
  return "[PDF Content Unavailable: Unexpected error with encrypted file]"
172
 
 
179
  text += page_text + "\n"
180
  except Exception as page_e:
181
  logger.error(f"Error extracting text from page {page_num + 1}: {page_e}")
182
+ text += f"[Error extracting page {page_num + 1}]\n" # Add placeholder for failed pages
 
 
 
 
 
 
 
183
 
184
+ return text.strip() or "[No readable text found in PDF]"
185
  except Exception as e:
186
  logger.error(f"Error extracting PDF text: {e}", exc_info=True)
187
  return f"[Error extracting PDF text: {e}]"
 
189
 
190
  def extract_care_plan_format(pdf_text):
191
  """Extract a general format template from PDF text by identifying common section headers."""
192
+ 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:
 
 
 
193
  logger.info("No valid PDF text available to extract format.")
194
  return None
195
 
196
+ # Look for lines that seem like headers followed by a colon, possibly with content below
197
+ # Pattern: Start of line, followed by one or more uppercase letters or spaces, ending with a colon.
198
+ # Use word boundaries \b to avoid matching things like "MEDICATION:" within a sentence.
199
+ # Refined pattern: Allow optional space before colon, and optional space/newline after colon before content starts.
200
+ # It also accounts for the possibility that a header is the last thing in the text.
201
+ potential_headers = re.findall(r'^\b([A-Z][A-Z\s]*)\b[ \t]*:(?:[\s\r\n]|$)', pdf_text, re.MULTILINE)
202
 
203
  if not potential_headers:
204
  # Fallback: Look for lines that start with a capital letter and seem like standalone headers
205
+ # (e.g., "Patient Information", "Assessment.")
206
+ # Ensure it doesn't look like a sentence.
207
+ # Add more constraints: must end with colon or period, or be followed by a newline and indented text?
208
+ # Simple fallback: Starts with Capital, contains mostly letters/spaces, ends with colon or period.
209
+ fallback_headers_strict = re.findall(r'^[A-Z][A-Za-z\s]*[:.][ \t]*$', pdf_text, re.MULTILINE)
210
+
211
+ if fallback_headers_strict:
212
+ logger.info(f"Extracted potential headers (fallback - strict): {list(set(fallback_headers_strict))}")
213
+ # Remove ending colon/period and strip whitespace
214
+ unique_headers = sorted(list(set([re.sub(r'[:.\s]*$', '', h).strip() for h in fallback_headers_strict if re.sub(r'[:.\s]*$', '', h).strip()])))
215
+ format_template = "\n".join([f"{header.strip()}:" for header in unique_headers if header.strip()]) # Sort for consistency
216
+ return format_template if format_template.strip() else None
217
+
218
+ # Less strict fallback: Starts with Capital, seems like a short phrase line
219
+ fallback_headers_loose = re.findall(r'^[A-Z][A-Za-z\s]{3,}[ \t]*$', pdf_text, re.MULTILINE)
220
  fallback_headers_loose = [h.strip() for h in fallback_headers_loose if h.strip()]
221
  if fallback_headers_loose:
222
+ # Further filter to remove things that look like sentence beginnings
223
+ # Check if the line is followed by a line starting with a bullet or indentation? Too complex.
224
+ # Simple filter: check length and word count.
225
+ fallback_headers_loose = [h for h in fallback_headers_loose if len(h) > 5 and len(h.split()) < 6] # Example filter
226
  if fallback_headers_loose:
227
  logger.info(f"Extracted potential headers (fallback - loose): {list(set(fallback_headers_loose))}")
228
  unique_headers = sorted(list(set([h.strip() for h in fallback_headers_loose if h.strip()])))
229
+ format_template = "\n".join([f"{header.strip()}:" for header in unique_headers if header.strip()]) # Sort for consistency
230
  return format_template if format_template.strip() else None
231
 
232
 
 
234
  return None
235
 
236
  # Use a set to get unique headers and sort them for consistency
237
+ unique_headers = sorted(list(set([h.strip() for h in potential_headers if h.strip()])))
 
238
 
239
  if not unique_headers:
240
  logger.info("Extracted headers are empty after cleaning.")
 
247
  return format_template if format_template.strip() else None
248
 
249
 
250
+ # --- OPTIMIZED determine_patient_status FUNCTION ---
251
  def determine_patient_status(original_plan, updated_plan, feedback):
252
  """
253
+ Determine patient status based on feedback, original plan, and the *final* updated plan text.
254
  Prioritizes emergency > deteriorating > improving. Checks feedback/original first,
255
  then checks updated plan for emergency confirmation. Uses refined keyword matching.
256
  """
 
258
 
259
  feedback_lower = feedback.lower() if feedback else ""
260
  original_plan_lower = original_plan.lower() if original_plan else ""
261
+ updated_plan_lower = updated_plan.lower() if updated_plan else "" # Include updated plan lower
262
 
263
 
264
  # Define robust keyword lists
265
+ # Emergency Keywords: Indicate immediate threat to life or limb, requires urgent medical intervention.
266
  emergency_keywords = [
267
  "severe chest pain", "heart attack", "sudden shortness of breath",
268
  "difficulty breathing severely", "loss of consciousness", "unresponsive",
 
276
  "signs of shock", "severe dehydration symptoms", "acute change in mental status",
277
  "unstable vitals", "rapidly worsening symptoms", "can't breathe", "chest tight severe",
278
  "severe difficulty swallowing suddenly", "new onset paralysis", "severe burns",
279
+ "major trauma", "suspected poisoning", "overdose", "suicidal thoughts active", # Added qualifiers
280
+ "unstable", "critically ill", "life-threatening", "no pulse", "low oxygen saturation severe"
 
281
  ]
282
 
283
+ # Deteriorating Keywords: Indicate condition is worsening, not improving as expected, or new concerning symptoms. Requires prompt medical review.
284
  deteriorating_keywords = [
285
  "worsening", "increased pain", "not improving", "deteriorating",
286
  "getting worse", "more frequent symptoms", "elevated", "higher",
 
293
  "tired all the time", "much weaker", "feeling noticeably worse", "consistent high blood pressure",
294
  "uncontrolled blood sugar levels", "increased swelling", "persistent cough getting worse",
295
  "unexplained weight loss significant", "gradual decline", "unmanaged symptoms",
296
+ "not resolving", "changes in condition", "difficulty performing daily tasks" # Added more
 
297
  ]
298
 
299
+ # Improvement Keywords: Indicate condition is getting better, symptoms are resolving, or goals are being met.
300
  improvement_keywords = [
301
  "improving", "better today", "reduced pain", "lower", "less frequent",
302
  "healing well", "recovery on track", "making progress", "stable condition",
 
309
  "more energy", "walking further", "blood pressure normal range",
310
  "blood sugar stable", "swelling reduced", "easier breathing", "cough improving",
311
  "weight gain healthy", "feeling like myself again", "in remission", "managing well at home",
312
+ "tolerating well", "no issues reported", "feeling good", "symptoms gone" # Added more positive
 
313
  ]
314
 
315
+ # Helper to check if any keyword is found in the text
316
  def check_keywords(text, keywords):
317
+ # Create a regex pattern for whole words, handling potential special characters in keywords
318
  pattern = r'\b(?:' + '|'.join(re.escape(kw) for kw in keywords) + r')\b'
319
  return re.search(pattern, text) is not None
320
 
321
  # --- Classification Logic ---
322
 
323
+ # Combine feedback and original plan text for initial assessment
324
+ combined_initial_text_lower = feedback_lower + " " + (original_plan_lower if original_plan_lower else "")
325
+ combined_initial_text_lower = re.sub(r'\s+', ' ', combined_initial_text_lower).strip() # Clean up spaces
 
 
 
 
 
326
 
327
  # 2. Check for DETERIORATING status (Second Priority)
328
+ # Check combined initial text for deteriorating keywords (only if not emergency)
329
+ is_deteriorating_initial = check_keywords(combined_initial_text_lower, deteriorating_keywords)
330
+ if is_deteriorating_initial:
331
+ logger.info("Status determined: DETERIORATING (keyword found in feedback/original).")
332
  return "deteriorating"
333
 
334
+ # 3. Check for IMPROVING status (Third Priority)
335
+ # Check combined initial text for improving keywords (only if not emergency or deteriorating)
336
+ is_improving_initial = check_keywords(combined_initial_text_lower, improvement_keywords)
337
+ if is_improving_initial:
338
+ logger.info("Status determined: IMPROVING (keyword found in feedback/original).")
339
  return "improving"
340
 
341
+ # 1. Check for EMERGENCY status (Highest Priority)
342
+ # Check combined initial text for emergency keywords
343
+ is_emergency_initial = check_keywords(combined_initial_text_lower, emergency_keywords)
344
+
345
+ # Also, check if the *final generated plan* explicitly contains strong emergency keywords.
346
+ # This helps catch cases where the AI correctly inferred emergency from subtle cues
347
+ # or context, even if the patient's feedback wasn't explicitly critical *using the exact keywords*.
348
+ is_emergency_final_plan = check_keywords(updated_plan_lower, emergency_keywords)
349
+
350
+ if is_emergency_initial:
351
+ logger.info("Status determined: EMERGENCY (keyword found in feedback/original or final plan).")
352
+ return "emergency"
353
+
354
+
355
  # 4. Default to STABLE if no specific status keywords are found
356
  logger.info("Status determined: STABLE (no specific status keywords found).")
357
  return "stable"
 
491
 
492
  # Check for section headers (e.g., "MEDICATIONS:", "ASSESSMENT:")
493
  # Look for lines starting with one or more uppercase words followed by a colon or period
494
+ header_match = re.match(r'^([A-Z][A-Z\s]*)\b[ \t]*[:.](\s|$)', stripped_line) # Added period check
495
  if header_match:
496
  # Remove trailing colon or period for cleaner heading
497
  header_text = re.sub(r'[:.\s]*$', '', stripped_line).strip()
498
+ story.append(Spacer(1, 8)) # Add space before a new section
499
+ story.append(Paragraph(header_text + ":", heading_style)) # Use heading style for sections
500
+ # Check for list items (starting with -, *, •)
501
+ elif stripped_line.startswith('-') or stripped_line.startswith('*') or stripped_line.startswith('•'):
502
+ # Remove the bullet character and any leading space/tab
503
+ bullet_text = re.sub(r'^[-*•][ \t]*', '', line).strip()
 
 
 
 
 
504
  if bullet_text:
505
  formatted_bullet_text = bullet_text.replace('\n', '<br/>')
506
  story.append(Paragraph(f"• {formatted_bullet_text}", bullet_style))
 
508
  # Handle cases with just a bullet point on a line
509
  story.append(Paragraph("• ", bullet_style))
510
  else:
511
+ # Handle regular paragraph text
512
+ normal_line_content = line.strip().replace('\n', '<br/>')
513
+ story.append(Paragraph(normal_line_content, normal_style))
 
 
514
 
515
 
516
  story.append(Spacer(1, 20))
 
524
  return buffer
525
  except Exception as e:
526
  logger.error(f"Error building PDF: {e}", exc_info=True)
 
527
  error_buffer = io.BytesIO()
528
  c = canvas.Canvas(error_buffer, pagesize=letter)
529
  c.drawString(100, 750, "Error Generating Care Plan PDF")
530
  c.drawString(100, 735, f"Details: {str(e)}")
 
531
  c.save()
532
  error_buffer.seek(0)
533
  return error_buffer
534
 
535
 
536
  def send_whatsapp_care_plan(patient_info, care_plan_text, status):
537
+ """Send care plan via WhatsApp using Twilio with improved formatting"""
538
  if not twilio_client:
539
  logger.warning("Twilio client not configured. Cannot send WhatsApp message.")
540
+ return False, "Twilio client not configured."
541
 
542
  if not TWILIO_TO or not TWILIO_FROM:
543
  logger.warning("Twilio TO or FROM number not set.")
544
+ return False, "Twilio TO or FROM number not configured."
545
 
546
  # Basic check for empty plan text before sending
547
  if not care_plan_text or not care_plan_text.strip():
548
  logger.warning("Care plan text is empty. Cannot send WhatsApp message.")
549
+ return False, "Care plan text is empty."
550
 
551
  try:
552
  status_emoji = {
 
569
  for line in formatted_plan.split('\n'):
570
  stripped_line = line.strip()
571
  # Check for lines that look like headers (starts with capital, ends with colon/period, possibly followed by whitespace)
572
+ if re.match(r'^[A-Z][A-Z\s]*\b[ \t]*[:.](\s|$)', stripped_line + '\n'): # Add newline for robust match
573
+ # Remove trailing colon/period before bolding
 
 
574
  header_text = re.sub(r'[:.\s]*$', '', stripped_line).strip()
575
  if header_text: # Only bold if there's actual text before the colon/period
576
+ formatted_plan_lines.append(f"*{header_text}:*")
577
+ else: # Handle cases like just ":" on a line
578
  formatted_plan_lines.append(line)
579
  else:
580
  formatted_plan_lines.append(line)
 
588
  message += f"*Status:* {status_emoji.get(status, 'Unknown')}\n\n"
589
  # Add feedback if available
590
  feedback_text = patient_info.get('feedback')
591
+ if feedback_text and feedback_text.strip(): # Check if feedback is not empty
592
  message += f"*Latest Feedback:*\n{feedback_text.strip()}\n\n"
593
 
594
  message += f"*Care Plan Details:*\n{formatted_plan}"
595
 
596
+ logger.info(f"Attempting to send WhatsApp message to {TWILIO_TO}...")
 
 
 
 
 
 
 
 
597
  message_sent = twilio_client.messages.create(
598
  from_=TWILIO_FROM,
599
  body=message,
 
605
  logger.error(f"Error sending WhatsApp message: {e}", exc_info=True)
606
  # Provide more specific Twilio error details if available
607
  twilio_error_message = str(e)
608
+ # Attempt to parse Twilio API error response
609
  if hasattr(e, 'status_code') and hasattr(e, 'text'):
610
  try:
611
  error_details = json.loads(e.text)
612
  if 'message' in error_details:
613
  twilio_error_message = f"Twilio API error: {error_details['message']} (Code: {error_details.get('code')})"
614
+ except json.JSONDecodeError:
615
+ pass # Fallback to generic error if JSON parsing fails
616
+ except Exception:
617
+ pass # Catch other potential exceptions during parsing/formatting
618
 
619
  return False, f"Error sending WhatsApp: {twilio_error_message}"
620
 
 
657
  def submit_feedback():
658
  ai_enabled = bool(model) # Check if model is initialized
659
  ai_error_message = None # Initialize error message
 
 
660
 
661
  try:
662
  name = request.form.get('name', 'Unnamed Patient')
 
673
  try:
674
  age = int(age)
675
  if age <= 0: raise ValueError("Age must be positive")
676
+ if age > 150: raise ValueError("Age seems unreasonably high") # More realistic sanity check
677
  except ValueError:
678
  logger.warning(f"Submission failed: Invalid Age provided: {age}")
679
  return jsonify({'success': False, 'error': 'Invalid Age provided.'}), 400
680
  else:
681
+ age = None # Store as None if not provided
682
 
683
  care_plan_text = "" # This will store the extracted text from PDF
684
  care_plan_format = None # This will store the detected format
 
692
  return jsonify({'success': False, 'error': 'Invalid file type. Only PDF files are allowed.'}), 400
693
 
694
  logger.info(f"Processing uploaded PDF: {pdf_file.filename}")
 
 
695
  care_plan_text = extract_text_from_pdf(pdf_file)
696
 
697
  # If extraction resulted in an error message, set format to None
698
+ 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:
 
699
  care_plan_format = None
700
  logger.warning(f"PDF text extraction failed or empty: {care_plan_text}")
701
  else:
 
706
 
707
  # Determine the initial status based on feedback and original plan
708
  # Pass "" for updated_plan initially, as it hasn't been generated yet for status check
 
709
  initial_status = determine_patient_status(care_plan_text, "", feedback)
710
  logger.info(f"Initial status determined based on feedback/original plan: {initial_status}")
711
 
 
714
  final_status_to_save = initial_status # Start with initial status
715
 
716
  # Only generate AI plan if AI is enabled AND status isn't immediate emergency based on feedback
717
+ # If feedback triggers "emergency", the generated_plan_text is a fixed emergency plan.
718
  if final_status_to_save == 'emergency':
719
  logger.info("Emergency status detected. Generating fixed emergency plan.")
720
  generated_plan_text = (
 
740
  "- Inform your primary physician/care team as soon as medically stable.\n"
741
  "- A new care plan will be developed after the emergency situation is resolved and evaluated by medical professionals.\n"
742
  )
743
+ # For emergency, the generated plan text *is* the final plan to save/send.
744
+ # Status remains 'emergency', which was already set as final_status_to_save.
745
 
746
 
747
  elif ai_enabled: # AI is enabled and initial status is not emergency
 
760
  - [Morning activities/medications/checks]
761
  Afternoon:
762
  - [Afternoon activities/medications/checks]
 
 
763
  Night:
764
  - [Night activities/medications/sleep instructions/checks]
765
  MEDICATIONS:
 
770
  - [Recommended physical activities, duration, frequency, limitations, and progression]
771
  SYMPTOM MANAGEMENT:
772
  - [Detailed instructions for managing specific symptoms (e.g., pain, nausea, shortness of breath), non-pharmacological interventions]
 
 
 
 
773
  """ # Enhanced default format
774
 
775
  prompt = f"""
 
781
  Gender: {gender}
782
  Patient Feedback/Symptoms Update:
783
  {feedback}
784
+ Previous Care Plan Details (if available):
785
+ {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."}
786
  Instructions:
787
  1. Generate the updated care plan strictly using the exact following format template.
788
  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.
 
793
  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.
794
  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).
795
  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.
796
+ 11. Review the feedback and previous plan carefully to determine the most likely current STATUS (e.g., Emergency, Deteriorating, Improving, Stable) and ensure the plan content aligns with that status, especially the RED FLAGS section. ensure plan does not exist whatsapp message character limit
797
  Care Plan Format Template:
798
  {care_plan_format}
799
  """
 
801
 
802
  try:
803
  response = model.generate_content(prompt)
804
+ # Access text attribute
805
  generated_plan_text = response.text.strip()
806
 
807
  # Remove markdown code block formatting if present
 
828
  logger.info(f"AI Response received. Length: {len(generated_plan_text)}")
829
 
830
  # Re-determine the final status using the generated plan as well.
831
+ # This is important because the AI might infer severity the keyword matching missed initially,
832
+ # or the generated plan text itself might contain explicit strong status indicators.
833
  final_status_to_save = determine_patient_status(care_plan_text, generated_plan_text, feedback)
834
  logger.info(f"Final status determined after AI generation: {final_status_to_save}")
835
 
 
838
  logger.error(f"Error generating content from AI: {ai_error}", exc_info=True)
839
  # If AI fails, construct an error message plan
840
  generated_plan_text = f"[Error generating updated plan from AI: {ai_error}]\n\n"
841
+ 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:
 
842
  generated_plan_text += "Falling back to original plan if available:\n\n" + care_plan_text
843
  # If falling back to original, status should reflect original plan/feedback
844
+ final_status_to_save = determine_patient_status(care_plan_text, care_plan_text, feedback) # Use original plan for status check if AI failed
845
  ai_error_message = f"AI generation failed. Showing original plan if available. Error: {ai_error}"
846
  else:
847
  generated_plan_text += "No previous plan available."
 
854
  else: # AI is not enabled and status is not emergency
855
  logger.warning("AI generation is disabled.")
856
  generated_plan_text = f"[AI generation is currently disabled.]\n\n"
857
+ 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:
 
858
  generated_plan_text += "Showing original plan if available:\n\n" + care_plan_text
859
  # Status based on original plan/feedback
860
  final_status_to_save = determine_patient_status(care_plan_text, care_plan_text, feedback)
 
885
  logger.info(f"Patient {patient_id} added to DB with status: {final_status_to_save}.")
886
 
887
  # Generate PDF for downloading using the stored data
888
+ # Note: We pass the patient object directly to the PDF generator for simplicity
889
+ # and to include feedback in the PDF
890
  pdf_buffer = generate_care_plan_pdf(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
891
  pdf_buffer.seek(0) # Ensure buffer is at the start before base64 encoding
892
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
893
  logger.info("PDF generated and base64 encoded.")
894
 
895
  # Send care plan via WhatsApp (using the final saved data)
896
+ # Note: We pass the patient object directly to the WhatsApp function
897
  whatsapp_sent, whatsapp_message = send_whatsapp_care_plan(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
898
+ logger.info(f"WhatsApp message attempt sent: {whatsapp_sent}, message: {whatsapp_message}")
 
899
 
900
  # Return success response, include relevant data
901
  return jsonify({
 
904
  'pdf_data': pdf_base64,
905
  'patient_id': patient_id,
906
  'status': new_patient.status, # Return the final determined status
907
+ 'whatsapp_sent': whatsapp_sent,
908
+ 'whatsapp_message': whatsapp_message,
909
  'ai_error': not ai_enabled or (ai_error_message is not None) # Indicate if AI was not enabled or failed
910
  })
911
 
 
936
  logger.warning(f"Update failed for ID {patient_id}: Patient not found.")
937
  return jsonify({'success': False, 'error': 'Patient not found.'}), 404
938
 
939
+ # Re-determine status based on the manually updated plan + existing feedback/original?
940
+ # Yes, this makes sense. If the doctor edits the plan, it should potentially change the status indication
941
+ # if their edits include stronger language about severity or improvement.
942
  patient.status = determine_patient_status(patient.original_plan, updated_plan_text, patient.feedback)
943
 
944
  patient.updated_plan = updated_plan_text
 
975
  care_plan_text_to_send = patient.updated_plan
976
  status_to_send = patient.status
977
 
 
978
  whatsapp_sent, whatsapp_message = send_whatsapp_care_plan(
979
  patient_info_for_whatsapp,
980
  care_plan_text_to_send,
 
985
  logger.info(f"WhatsApp sent successfully for patient ID: {patient_id}")
986
  return jsonify({'success': True, 'message': whatsapp_message})
987
  else:
988
+ logger.error(f"WhatsApp failed for patient ID: {patient_id} - {whatsapp_message}")
989
+ # Use 500 for server-side error only if it's a backend issue, otherwise 400 maybe?
990
+ # Let's stick to 500 for general failure to send via backend service.
991
+ return jsonify({'success': False, 'error': whatsapp_message}), 500
992
 
993
  except Exception as e:
994
  logger.error(f"Error triggering WhatsApp send for ID {patient_id}: {str(e)}", exc_info=True)
995
+ return jsonify({'success': False, 'error': f'An error occurred while sending WhatsApp: {str(e)}'}), 500
 
996
 
997
 
998
  # --- Existing routes remain below ---
999
+
1000
  @app.route('/download_pdf/<patient_id>')
1001
  def download_pdf(patient_id):
1002
  logger.info(f"Download requested for patient ID: {patient_id}")
 
1015
  patient.status
1016
  )
1017
 
1018
+ pdf_buffer.seek(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1019
 
 
 
 
1020
  safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '', patient.name or 'patient').lower()
1021
  download_name = f"care_plan_{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
1022
 
 
1029
  )
1030
  except Exception as e:
1031
  logger.error(f"PDF Download Error for ID {patient_id}: {str(e)}", exc_info=True)
 
1032
  return f"Error generating PDF for download: {str(e)}", 500
 
1033
 
1034
 
1035
  @app.route('/get_emergency_notifications')
1036
  def get_emergency_notifications():
1037
  # Only include patients whose status is 'emergency'
1038
+ # Exclude temporary 'emergency' status from new submissions before AI runs,
1039
+ # perhaps only include statuses confirmed by AI or manual save?
1040
+ # For simplicity now, just filter by final status == 'emergency' in DB.
1041
  emergency_patients_query = Patient.query.filter_by(status='emergency').order_by(Patient.timestamp.desc())
1042
 
1043
  # Only return basic info needed for the alert, not full patient details
 
1110
  if not inspector.has_table("patient"): # Check for at least one model's table
1111
  logger.info("Database tables not found, creating.")
1112
  db.create_all()
 
1113
  else:
1114
  logger.info("Database tables already exist.")
 
 
1115
 
1116
  # Use a more robust development server like Waitress or Gunicorn in production
1117
  # For development, debug=True is fine