rajkhanke commited on
Commit
e380a5c
·
verified ·
1 Parent(s): 7e1a033

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +239 -209
app.py CHANGED
@@ -66,19 +66,21 @@ class Patient(db.Model):
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
 
@@ -113,23 +115,17 @@ if GENAI_API_KEY:
113
  logger.info("Gemini API configured successfully.")
114
 
115
  generation_config = {
116
- "temperature": 0.8,
117
  "top_p": 0.9,
118
  "top_k": 30,
119
- "max_output_tokens": 4096,
120
  }
121
 
 
122
  model = genai.GenerativeModel(
123
- model_name="gemini-1.5-flash-latest",
124
  generation_config=generation_config,
125
- # Optionally add safety settings
126
- # safety_settings=[
127
- # {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
128
- # {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
129
- # {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
130
- # {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
131
- # {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"}, # Duplicate, remove
132
- # ]
133
  )
134
  logger.info(f"Using Gemini model: {model.model_name}")
135
 
@@ -150,22 +146,18 @@ def extract_text_from_pdf(pdf_file):
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]"
@@ -193,58 +185,40 @@ def extract_care_plan_format(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
-
233
- logger.info("No sections or potential headers found in PDF.")
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.")
241
  return None
242
 
243
- format_template = ""
244
- for section_title in unique_headers:
245
- format_template += f"{section_title.strip()}:\n- [Details]\n" # Use a simple list format placeholder
246
- logger.info(f"Extracted sections: {unique_headers}")
247
- return format_template if format_template.strip() else None
 
 
 
 
 
 
 
 
 
248
 
249
 
250
  # --- OPTIMIZED determine_patient_status FUNCTION ---
@@ -276,8 +250,9 @@ def determine_patient_status(original_plan, updated_plan, feedback):
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.
@@ -293,7 +268,8 @@ def determine_patient_status(original_plan, updated_plan, feedback):
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.
@@ -309,14 +285,30 @@ def determine_patient_status(original_plan, updated_plan, feedback):
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
 
@@ -324,33 +316,35 @@ def determine_patient_status(original_plan, updated_plan, feedback):
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).")
@@ -490,18 +484,26 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
490
  continue
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))
507
  else:
@@ -510,7 +512,8 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
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))
@@ -564,20 +567,38 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
564
  # Replace common list bullet formats with WhatsApp bullet
565
  formatted_plan = formatted_plan.replace('- ', '• ').replace('* ', '• ')
566
 
567
- # Attempt to bold section headers - look for lines ending with a colon or period, possibly followed by whitespace
 
568
  formatted_plan_lines = []
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)
 
 
 
581
  formatted_plan = '\n'.join(formatted_plan_lines)
582
 
583
 
@@ -589,10 +610,16 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
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,
@@ -621,25 +648,30 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
621
 
622
  @app.route('/')
623
  def index():
624
- role = session.get('role', 'patient')
 
 
 
 
 
 
625
  # If doctor role is in session but user visits '/', redirect to doctor dashboard
626
  if role == 'doctor':
 
627
  return redirect(url_for('doctor_dashboard'))
628
- # Otherwise, render patient index
 
629
  return render_template('index.html')
630
 
631
 
632
  @app.route('/switch_role', methods=['POST'])
633
  def switch_role():
634
- role = request.form.get('role')
 
635
  if role in ['patient', 'doctor']:
636
  session['role'] = role
637
  logger.info(f"Role switched to: {role}")
638
- # Redirect to the appropriate page after switching
639
- if role == 'doctor':
640
- return redirect(url_for('doctor_dashboard'))
641
- else:
642
- return redirect(url_for('index')) # Redirect to patient home
643
  logger.warning(f"Invalid role switch attempted: {role}")
644
  return jsonify({'success': False, 'error': 'Invalid role'}), 400
645
 
@@ -649,7 +681,8 @@ def doctor_dashboard():
649
  # Ensure user is marked as doctor in session when accessing this page directly
650
  if session.get('role') != 'doctor':
651
  session['role'] = 'doctor'
652
- logger.info("Accessed doctor dashboard, setting role to doctor.")
 
653
  return render_template('doctor_dashboard.html')
654
 
655
 
@@ -681,7 +714,6 @@ def submit_feedback():
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
685
 
686
  if 'care_plan_pdf' in request.files:
687
  pdf_file = request.files['care_plan_pdf']
@@ -694,25 +726,35 @@ def submit_feedback():
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:
702
- care_plan_format = extract_care_plan_format(care_plan_text)
703
- logger.info(f"Extracted text length: {len(care_plan_text)}. Format found: {care_plan_format is not None}")
704
  else:
705
  logger.info("No PDF file uploaded or file is empty.")
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
 
712
-
713
  generated_plan_text = "" # This will store the AI-generated or fallback plan
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':
@@ -725,82 +767,67 @@ def submit_feedback():
725
  f"- Gender: {gender}\n\n"
726
  "ASSESSMENT:\n"
727
  f"- Emergency symptoms reported: {feedback}. Immediate medical attention required.\n\n"
728
- "EMERGENCY ACTION PLAN:\n"
729
- "- *Call emergency services immediately* at your local emergency number (e.g., 104/108/109/112).\n"
730
- "- Do not delay seeking medical help. If possible, have someone stay with the patient.\n"
731
- "- If conscious, help the patient into a comfortable position (e.g., upright for breathing difficulties, on back with legs elevated for shock).\n"
732
- "- Do not give food or drink until evaluated by medical professionals.\n"
733
- "- Prepare relevant medical history, medication list, and previous care plan if available for emergency responders.\n"
734
- "- Follow *all* instructions from emergency responders.\n\n"
735
- "RED FLAGS / WHEN TO SEEK HELP:\n"
736
- "- *Any* worsening of reported emergency symptoms requires urgent re-evaluation by medical professionals.\n"
737
- "- Do not attempt to manage severe symptoms at home once emergency signs are present.\n\n"
738
- "FOLLOW-UP:\n"
739
- "- Immediate hospitalization or urgent medical evaluation is necessary.\n"
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
748
- # Define or get the format template
749
- if not care_plan_format or not care_plan_format.strip():
750
- logger.info("Using default care plan format as extraction failed or returned empty.")
751
- care_plan_format = """
752
- PATIENT INFORMATION:
753
- - Name: [Patient Name]
754
- - Age: [Age]
755
- - Gender: [Gender] (no other parameter other than these three should be there , not even contact number i want only three Name)
756
- ASSESSMENT:
757
- - [Summary of patient's current condition based on feedback and previous plan]
758
- DAILY CARE PLAN:
759
- Morning:
760
- - [Morning activities/medications/checks]
761
- Afternoon:
762
- - [Afternoon activities/medications/checks]
763
- Night:
764
- - [Night activities/medications/sleep instructions/checks]
765
- MEDICATIONS:
766
- - [List of medications, dosage, frequency, and time, including PRN (as needed) instructions] {hey please give it by general sense dont response like i am not physician etc , give , if you dont know still appropriate medicates pelase give)}
767
- - DIET AND HYDRATION
768
- - Physical activity and exercise
769
- SYMPTOM MANAGEMENT:
770
- - [Short instructions for managing specific symptoms (e.g., pain, nausea, shortness of breath), non-pharmacological interventions]
771
- """ # Enhanced default format
772
 
 
773
  prompt = f"""
774
- You are a helpful and highly structured AI assistant generating updated patient care plans.
775
- Your task is to create a NEW, comprehensive, and well-structured daily care plan based on the provided patient's personal information, their current symptoms/feedback, and their previous care plan (if available).
776
- Patient Information:
 
 
777
  Name: {name}
778
  Age: {age if age is not None else 'N/A'}
779
  Gender: {gender}
780
- Patient Feedback/Symptoms Update:
 
781
  {feedback}
782
- Previous Care Plan Details (if available):
 
783
  {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."}
784
- Instructions:
785
- 1. Generate the updated care plan strictly using the exact following format template.
786
- 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.
787
- 3. Prioritize addressing the issues raised in the patient feedback.
788
- 4. Be specific, actionable, and realistic in your recommendations (e.g., specify *what* activities, *when* to take medications and *what" medications and diet to follow).
789
- 5. Ensure the language is clear, empathetic, and easy for a patient or caregiver to understand. Avoid overly technical jargon where possible.
790
- 7. Do NOT include any introductory phrases (like "Here is the updated plan:", "Based on your feedback...") or concluding sentences outside the plan structure. Provide ONLY the structured content that fits within the template.
791
- 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.
792
- 9. Ensure the plan is medically sound and reflects standard care principles. also give specific medications rexomemndations based on patient symptoms (ex : paracetamol for fever etc)
793
- 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.
794
- 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
795
- 12. kindly, do not produce emergency contact section drop it okay,also in any section dont give like not provided, not known,need to be added like stufff, cosnider youself super intellgient and real physician
796
- Care Plan Format Template (hey only give in text format do not use tbales okay ,a lso ensure that plan is short and no section great than 4 sentences okay):
797
- {care_plan_format}
798
- also ensure that do not give the follwong section :
799
- Emergency Contacts:
800
- Primary Caregiver: (Not provided)
801
- Doctor's Name: (Not provided)
802
- Hospital Contact: (Not provided) (it is useless abd vague okay and also in any section do not say like doctor or caretaker will provide)
 
 
803
  """
 
 
804
  logger.info("Sending prompt to AI model...")
805
 
806
  try:
@@ -809,7 +836,7 @@ Hospital Contact: (Not provided) (it is useless abd vague okay and also in any s
809
  generated_plan_text = response.text.strip()
810
 
811
  # Remove markdown code block formatting if present
812
- if generated_plan_text.startswith('```') and generated_plan_text.endswith('```'):
813
  # Find the first newline after ``` to potentially strip language name
814
  first_newline_after_code = generated_plan_text.find('\n')
815
  if first_newline_after_code != -1:
@@ -834,6 +861,7 @@ Hospital Contact: (Not provided) (it is useless abd vague okay and also in any s
834
  # Re-determine the final status using the generated plan as well.
835
  # This is important because the AI might infer severity the keyword matching missed initially,
836
  # or the generated plan text itself might contain explicit strong status indicators.
 
837
  final_status_to_save = determine_patient_status(care_plan_text, generated_plan_text, feedback)
838
  logger.info(f"Final status determined after AI generation: {final_status_to_save}")
839
 
@@ -872,6 +900,7 @@ Hospital Contact: (Not provided) (it is useless abd vague okay and also in any s
872
 
873
 
874
  # Create and store patient record in the database
 
875
  new_patient = Patient(
876
  name=name,
877
  age=age,
@@ -890,14 +919,14 @@ Hospital Contact: (Not provided) (it is useless abd vague okay and also in any s
890
 
891
  # Generate PDF for downloading using the stored data
892
  # Note: We pass the patient object directly to the PDF generator for simplicity
893
- # and to include feedback in the PDF
894
  pdf_buffer = generate_care_plan_pdf(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
895
  pdf_buffer.seek(0) # Ensure buffer is at the start before base64 encoding
896
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
897
  logger.info("PDF generated and base64 encoded.")
898
 
899
  # Send care plan via WhatsApp (using the final saved data)
900
- # Note: We pass the patient object directly to the WhatsApp function
901
  whatsapp_sent, whatsapp_message = send_whatsapp_care_plan(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
902
  logger.info(f"WhatsApp message attempt sent: {whatsapp_sent}, message: {whatsapp_message}")
903
 
@@ -910,7 +939,8 @@ Hospital Contact: (Not provided) (it is useless abd vague okay and also in any s
910
  'status': new_patient.status, # Return the final determined status
911
  'whatsapp_sent': whatsapp_sent,
912
  'whatsapp_message': whatsapp_message,
913
- 'ai_error': not ai_enabled or (ai_error_message is not None) # Indicate if AI was not enabled or failed
 
914
  })
915
 
916
  except Exception as e:
@@ -1013,6 +1043,7 @@ def download_pdf(patient_id):
1013
  return "Patient data not found.", 404
1014
 
1015
  # Pass the full patient dict to PDF generator for feedback inclusion
 
1016
  pdf_buffer = generate_care_plan_pdf(
1017
  patient.to_dict(),
1018
  patient.updated_plan,
@@ -1039,9 +1070,6 @@ def download_pdf(patient_id):
1039
  @app.route('/get_emergency_notifications')
1040
  def get_emergency_notifications():
1041
  # Only include patients whose status is 'emergency'
1042
- # Exclude temporary 'emergency' status from new submissions before AI runs,
1043
- # perhaps only include statuses confirmed by AI or manual save?
1044
- # For simplicity now, just filter by final status == 'emergency' in DB.
1045
  emergency_patients_query = Patient.query.filter_by(status='emergency').order_by(Patient.timestamp.desc())
1046
 
1047
  # Only return basic info needed for the alert, not full patient details
@@ -1109,13 +1137,15 @@ def delete_patient(patient_id):
1109
  if __name__ == '__main__':
1110
  # Create database tables if they don't exist within the application context
1111
  with app.app_context():
1112
- # Check if tables exist before creating to avoid errors on subsequent runs
1113
- inspector = db.inspect(db.engine)
1114
- if not inspector.has_table("patient"): # Check for at least one model's table
1115
- logger.info("Database tables not found, creating.")
1116
- db.create_all()
1117
- else:
1118
- logger.info("Database tables already exist.")
 
 
1119
 
1120
  # Use a more robust development server like Waitress or Gunicorn in production
1121
  # For development, debug=True is fine
 
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
+ # Use a flag to prevent running on every request
70
+ if not getattr(app, '_tables_created', False):
71
  with app.app_context():
72
  # Check if at least one table exists (e.g., the Patient table)
 
73
  inspector = db.inspect(db.engine)
74
  if not inspector.has_table("patient"):
75
  logger.info("Creating database tables.")
76
  db.create_all()
77
+ else:
78
+ logger.info("Database tables already exist.")
79
+ app._tables_created = True # Set flag
80
 
81
 
82
+ upload_base = os.getenv('UPLOAD_DIR', '/tmp')
83
+ upload_folder = os.path.join(upload_base, 'uploads', 'pdfs') # Use a sub-directory like 'uploads'
84
 
85
  app.config['UPLOAD_FOLDER'] = upload_folder
86
 
 
115
  logger.info("Gemini API configured successfully.")
116
 
117
  generation_config = {
118
+ "temperature": 0.7, # Slightly lower temp for potentially more stable output
119
  "top_p": 0.9,
120
  "top_k": 30,
121
+ "max_output_tokens": 4096, # Ensure enough tokens, but AI should be concise
122
  }
123
 
124
+ # Use a model suitable for instructions and structured output
125
  model = genai.GenerativeModel(
126
+ model_name="gemini-1.5-flash-latest", # Or gemini-1.0-pro
127
  generation_config=generation_config,
128
+ # safety_settings can be added here if needed, but ensure they don't block valid medical info
 
 
 
 
 
 
 
129
  )
130
  logger.info(f"Using Gemini model: {model.model_name}")
131
 
 
146
  text = ""
147
  if pdf_reader.is_encrypted:
148
  try:
149
+ # Attempt decryption - Note: Robust decryption requires knowing the password.
150
+ # This is a basic attempt and may fail.
151
+ # In production, you might need to handle encrypted PDFs differently.
 
152
  try:
153
+ pdf_reader.decrypt('') # Try with empty password first
154
  except PyPDF2.errors.PasswordError:
 
155
  logger.warning("PDF is encrypted and requires a password.")
156
  return "[PDF Content Unavailable: File is encrypted and requires a password]"
157
  except Exception as dec_e:
 
158
  logger.error(f"Error during PDF decryption attempt: {dec_e}")
159
  return "[PDF Content Unavailable: Error decrypting file]"
160
 
 
161
  except Exception: # Catch any other unexpected error during decryption check
162
  logger.error("Unexpected error during PDF decryption check.")
163
  return "[PDF Content Unavailable: Unexpected error with encrypted file]"
 
185
  logger.info("No valid PDF text available to extract format.")
186
  return None
187
 
188
+ # Look for lines that seem like headers followed by a colon, period, or are just standalone capitalized lines
189
+ # Refined Pattern: Start of line, followed by one or more uppercase letters or spaces/words, ending with colon/period or followed by newline/indentation.
190
+ # Let's stick to the colon/period heuristic first as it's more reliable for structured docs.
191
+ # Pattern: Start of line ^, followed by one or more word characters (\w+) or spaces (\s), potentially including lowercase letters now `[A-Za-z\s]`, ending with a colon `:` or period `.` and then potentially space or end of line `$`.
192
+ # Add lookahead for newlines/spaces to handle headers that are the last line or followed by blank space.
193
+ potential_headers = re.findall(r'^[A-Z][A-Za-z\s]*[:.]?(?:\s|$)', pdf_text, re.MULTILINE)
194
+ # Further filter to exclude things that are likely sentence fragments or short lines
195
+ potential_headers = [h.strip() for h in potential_headers if h.strip() and len(h.strip().split()) > 1 and len(h.strip()) > 5] # Filter short/single word lines
196
 
197
  if not potential_headers:
198
+ logger.info("No potential headers found in PDF text.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  return None
200
 
201
+ # Use a set to get unique headers, remove trailing colon/period, strip, and sort
202
+ unique_headers = sorted(list(set([re.sub(r'[:.\s]*$', '', h).strip() for h in potential_headers if re.sub(r'[:.\s]*$', '', h).strip()])))
203
 
204
  if not unique_headers:
205
  logger.info("Extracted headers are empty after cleaning.")
206
  return None
207
 
208
+ # Construct a format template string
209
+ # We don't need to represent the *exact* sub-structure with [Details] anymore,
210
+ # just the main sections. The AI will be instructed to follow a specific *required* structure.
211
+ # This extraction is mostly for informing the AI about *potential* sections present in the original.
212
+ # However, the user wants a *fixed* template structure regardless of the PDF.
213
+ # Let's *ignore* the extracted format for the AI prompt and rely solely on the hardcoded one.
214
+ # The extracted text itself is what's important from the PDF, not its format template.
215
+ # So, we can simplify this function or remove it if the AI strictly uses a hardcoded template.
216
+
217
+ # Given the user wants a FIXED template in the AI output, this extraction function's
218
+ # primary use becomes less critical for *generating* the new plan's format,
219
+ # but it *does* provide context about the original plan's content organization.
220
+ # Let's keep it simple, just return the extracted text. The prompt handles the structure.
221
+ return None # We will rely on the hardcoded structure in the prompt
222
 
223
 
224
  # --- OPTIMIZED determine_patient_status FUNCTION ---
 
250
  "signs of shock", "severe dehydration symptoms", "acute change in mental status",
251
  "unstable vitals", "rapidly worsening symptoms", "can't breathe", "chest tight severe",
252
  "severe difficulty swallowing suddenly", "new onset paralysis", "severe burns",
253
+ "major trauma", "suspected poisoning", "overdose", "suicidal thoughts active",
254
+ "unstable", "critically ill", "life-threatening", "no pulse", "low oxygen saturation severe",
255
+ "actively bleeding heavily" # Added
256
  ]
257
 
258
  # Deteriorating Keywords: Indicate condition is worsening, not improving as expected, or new concerning symptoms. Requires prompt medical review.
 
268
  "tired all the time", "much weaker", "feeling noticeably worse", "consistent high blood pressure",
269
  "uncontrolled blood sugar levels", "increased swelling", "persistent cough getting worse",
270
  "unexplained weight loss significant", "gradual decline", "unmanaged symptoms",
271
+ "not resolving", "changes in condition", "difficulty performing daily tasks",
272
+ "labored breathing", "pain score increasing", "difficulty managing symptoms", "less responsive" # Added
273
  ]
274
 
275
  # Improvement Keywords: Indicate condition is getting better, symptoms are resolving, or goals are being met.
 
285
  "more energy", "walking further", "blood pressure normal range",
286
  "blood sugar stable", "swelling reduced", "easier breathing", "cough improving",
287
  "weight gain healthy", "feeling like myself again", "in remission", "managing well at home",
288
+ "tolerating well", "no issues reported", "feeling good", "symptoms gone", "pain score decreasing",
289
+ "increased mobility", "feeling more independent" # Added
290
  ]
291
 
292
  # Helper to check if any keyword is found in the text
293
  def check_keywords(text, keywords):
294
  # Create a regex pattern for whole words, handling potential special characters in keywords
295
+ # Ensure keywords match whole words or are at word boundaries
296
  pattern = r'\b(?:' + '|'.join(re.escape(kw) for kw in keywords) + r')\b'
297
+ # Also check for phrases that might not be whole words at the start/end, but contain spaces
298
+ # e.g., "severe chest pain" might be part of "patient reports severe chest pain"
299
+ # A simple join with | might miss this if not careful about word boundaries.
300
+ # Let's rely on the \b boundaries which should work for multi-word phrases too if surrounded by non-word chars or boundaries.
301
+ # Alternative: simple `in` check might be less precise but catches more variations.
302
+ # Let's use the regex but ensure it's robust. Adding spaces around keywords in pattern? No, \b is better.
303
+ # Let's also include a fallback simple 'in' check for keywords with spaces that might be tricky.
304
+ for kw in keywords:
305
+ if re.search(r'\b' + re.escape(kw) + r'\b', text):
306
+ return True
307
+ # Fallback check for phrases that might not strictly align with \b everywhere
308
+ # This is less precise, but can catch things like "patient reports severe chest pain".
309
+ # Let's stick to the more precise regex for now, as loose matching can lead to false positives.
310
+ return False
311
+
312
 
313
  # --- Classification Logic ---
314
 
 
316
  combined_initial_text_lower = feedback_lower + " " + (original_plan_lower if original_plan_lower else "")
317
  combined_initial_text_lower = re.sub(r'\s+', ' ', combined_initial_text_lower).strip() # Clean up spaces
318
 
319
+ # Combine feedback and *updated* plan text for final assessment
320
+ combined_final_text_lower = feedback_lower + " " + (updated_plan_lower if updated_plan_lower else "")
321
+ combined_final_text_lower = re.sub(r'\s+', ' ', combined_final_text_lower).strip() # Clean up spaces
 
 
 
322
 
 
 
 
 
 
 
323
 
324
  # 1. Check for EMERGENCY status (Highest Priority)
325
+ # Check the final combined text (feedback + updated plan) for emergency keywords.
326
+ # This ensures if the AI generates text indicating emergency, it's caught.
327
+ is_emergency = check_keywords(combined_final_text_lower, emergency_keywords)
328
 
329
+ if is_emergency:
330
+ logger.info("Status determined: EMERGENCY (keyword found in feedback/final plan).")
 
 
 
 
 
331
  return "emergency"
332
 
333
+ # 2. Check for DETERIORATING status (Second Priority)
334
+ # Check the final combined text for deteriorating keywords (only if not emergency)
335
+ is_deteriorating = check_keywords(combined_final_text_lower, deteriorating_keywords)
336
+ if is_deteriorating:
337
+ # Add a check to see if improving keywords are also present - prioritize improving if both are there?
338
+ # Or assume deteriorating overrides improving? Let's assume deteriorating is higher priority.
339
+ logger.info("Status determined: DETERIORATING (keyword found in feedback/final plan).")
340
+ return "deteriorating"
341
+
342
+ # 3. Check for IMPROVING status (Third Priority)
343
+ # Check the final combined text for improving keywords (only if not emergency or deteriorating)
344
+ is_improving = check_keywords(combined_final_text_lower, improvement_keywords)
345
+ if is_improving:
346
+ logger.info("Status determined: IMPROVING (keyword found in feedback/final plan).")
347
+ return "improving"
348
 
349
  # 4. Default to STABLE if no specific status keywords are found
350
  logger.info("Status determined: STABLE (no specific status keywords found).")
 
484
  continue
485
 
486
  # Check for section headers (e.g., "MEDICATIONS:", "ASSESSMENT:")
487
+ # Look for lines starting with one or more uppercase words followed by a colon or period,
488
+ # or lines that are entirely uppercase words (might be headers without colon).
489
+ # Pattern: ^ Start of line, ([A-Z][A-Za-z\s]*): Captures words/spaces starting with Capital.
490
+ # [:. ]? Optional colon/period/space. ($|\n) Followed by end of line or newline (accounts for empty lines after header).
491
+ # Added check for lines that are ALL CAPS and maybe don't have a colon/period.
492
+ header_match = re.match(r'^([A-Z][A-Za-z\s]*)\s*[:.]?$', stripped_line) # Looks for headers ending in optional colon/period/space
493
+ all_caps_match = re.match(r'^[A-Z\s]+$', stripped_line) # Looks for lines that are all caps
494
+
495
+ if header_match or (all_caps_match and len(stripped_line.split()) > 1): # Consider all-caps lines with >1 word headers
496
+ # Clean up header text - remove trailing colon/period/whitespace
497
  header_text = re.sub(r'[:.\s]*$', '', stripped_line).strip()
498
+ if header_text: # Only add if header text isn't empty after cleaning
499
+ story.append(Spacer(1, 8)) # Add space before a new section
500
+ story.append(Paragraph(header_text + ":", heading_style)) # Use heading style for sections
501
  # Check for list items (starting with -, *, •)
502
  elif stripped_line.startswith('-') or stripped_line.startswith('*') or stripped_line.startswith('•'):
503
  # Remove the bullet character and any leading space/tab
504
+ bullet_text = re.sub(r'^[-*•][ \t]*', '', line).strip() # Use original line to preserve indentation? No, strip is better.
505
  if bullet_text:
506
+ # Replace newline with <br/> for ReportLab within bullet text
507
  formatted_bullet_text = bullet_text.replace('\n', '<br/>')
508
  story.append(Paragraph(f"• {formatted_bullet_text}", bullet_style))
509
  else:
 
512
  else:
513
  # Handle regular paragraph text
514
  normal_line_content = line.strip().replace('\n', '<br/>')
515
+ if normal_line_content: # Only add if not empty after cleaning
516
+ story.append(Paragraph(normal_line_content, normal_style))
517
 
518
 
519
  story.append(Spacer(1, 20))
 
567
  # Replace common list bullet formats with WhatsApp bullet
568
  formatted_plan = formatted_plan.replace('- ', '• ').replace('* ', '• ')
569
 
570
+ # Attempt to bold section headers - look for lines ending with a colon or period, potentially followed by whitespace
571
+ # Use the same pattern as in PDF generation for consistency
572
  formatted_plan_lines = []
573
+ lines = formatted_plan.split('\n')
574
+ for i, line in enumerate(lines):
575
  stripped_line = line.strip()
576
+ if not stripped_line:
577
+ formatted_plan_lines.append("") # Keep empty lines for spacing
578
+ continue
579
+
580
+ # Check for lines that look like headers
581
+ header_match = re.match(r'^([A-Z][A-Za-z\s]*)\s*[:.]?$', stripped_line)
582
+ all_caps_match = re.match(r'^[A-Z\s]+$', stripped_line)
583
+
584
+ if header_match or (all_caps_match and len(stripped_line.split()) > 1):
585
+ # Clean up header text - remove trailing colon/period/whitespace
586
  header_text = re.sub(r'[:.\s]*$', '', stripped_line).strip()
587
+ if header_text:
588
+ formatted_plan_lines.append(f"*{header_text.upper()}:*") # Bold and capitalize for clarity
589
+ else:
590
+ formatted_plan_lines.append(line) # Add original line if cleaning results in empty
591
+ # Check for bullet points
592
+ elif stripped_line.startswith('•'):
593
+ # Ensure consistent bullet point and formatting
594
+ # If the AI uses "-", replace it earlier. Here, just format the text after "•"
595
+ bullet_content = re.sub(r'^•[ \t]*', '', line).strip() # Get text after bullet
596
+ formatted_plan_lines.append(f"• {bullet_content}") # Add WhatsApp bullet
597
  else:
598
+ # Handle regular paragraph text
599
+ formatted_plan_lines.append(line) # Add as is
600
+
601
+
602
  formatted_plan = '\n'.join(formatted_plan_lines)
603
 
604
 
 
610
  # Add feedback if available
611
  feedback_text = patient_info.get('feedback')
612
  if feedback_text and feedback_text.strip(): # Check if feedback is not empty
613
+ # Simple formatting for feedback in WhatsApp
614
  message += f"*Latest Feedback:*\n{feedback_text.strip()}\n\n"
615
 
616
  message += f"*Care Plan Details:*\n{formatted_plan}"
617
 
618
+ # Basic check for message length (Twilio has limits, WhatsApp is large but not infinite)
619
+ if len(message) > 4000: # Arbitrary limit, adjust as needed
620
+ logger.warning(f"WhatsApp message for ID {patient_info.get('id')} is very long ({len(message)} chars). May be truncated or fail.")
621
+ # Consider truncating or sending in multiple parts if needed for production
622
+
623
  logger.info(f"Attempting to send WhatsApp message to {TWILIO_TO}...")
624
  message_sent = twilio_client.messages.create(
625
  from_=TWILIO_FROM,
 
648
 
649
  @app.route('/')
650
  def index():
651
+ # Default role if not set
652
+ if 'role' not in session:
653
+ session['role'] = 'patient'
654
+ logger.info("Session role not set, defaulting to 'patient'.")
655
+
656
+ role = session.get('role')
657
+
658
  # If doctor role is in session but user visits '/', redirect to doctor dashboard
659
  if role == 'doctor':
660
+ logger.info("Role is 'doctor', redirecting to doctor dashboard.")
661
  return redirect(url_for('doctor_dashboard'))
662
+
663
+ logger.info("Rendering patient index page.")
664
  return render_template('index.html')
665
 
666
 
667
  @app.route('/switch_role', methods=['POST'])
668
  def switch_role():
669
+ data = request.get_json()
670
+ role = data.get('role')
671
  if role in ['patient', 'doctor']:
672
  session['role'] = role
673
  logger.info(f"Role switched to: {role}")
674
+ return jsonify({'success': True, 'role': role})
 
 
 
 
675
  logger.warning(f"Invalid role switch attempted: {role}")
676
  return jsonify({'success': False, 'error': 'Invalid role'}), 400
677
 
 
681
  # Ensure user is marked as doctor in session when accessing this page directly
682
  if session.get('role') != 'doctor':
683
  session['role'] = 'doctor'
684
+ logger.info("Accessed doctor dashboard directly, setting role to doctor.")
685
+ logger.info("Rendering doctor dashboard page.")
686
  return render_template('doctor_dashboard.html')
687
 
688
 
 
714
  age = None # Store as None if not provided
715
 
716
  care_plan_text = "" # This will store the extracted text from PDF
 
717
 
718
  if 'care_plan_pdf' in request.files:
719
  pdf_file = request.files['care_plan_pdf']
 
726
  logger.info(f"Processing uploaded PDF: {pdf_file.filename}")
727
  care_plan_text = extract_text_from_pdf(pdf_file)
728
 
729
+ # If extraction resulted in an error message, the variable will contain it.
730
+ # This error message will be passed to the AI as "Previous Care Plan Details".
731
+ logger.info(f"Extracted text length: {len(care_plan_text)}. Begins with: {care_plan_text[:100]}...")
 
 
 
 
732
  else:
733
  logger.info("No PDF file uploaded or file is empty.")
734
 
735
+ # Determine the initial status based on feedback and original plan text
736
+ # Pass "" for updated_plan initially
737
  initial_status = determine_patient_status(care_plan_text, "", feedback)
738
  logger.info(f"Initial status determined based on feedback/original plan: {initial_status}")
739
 
 
740
  generated_plan_text = "" # This will store the AI-generated or fallback plan
741
  final_status_to_save = initial_status # Start with initial status
742
 
743
+ # --- Define the NEW, FIXED Care Plan Structure for the AI Output ---
744
+ # Removed 'Emergency Contacts' and 'Red Flags' sections, added 'PHYSICAL ACTIVITY AND EXERCISE'
745
+ required_care_plan_structure = """
746
+ PATIENT INFORMATION:
747
+ ASSESSMENT:
748
+ DAILY CARE PLAN:
749
+ Morning:
750
+ Afternoon:
751
+ Night:
752
+ MEDICATIONS:
753
+ DIET AND HYDRATION:
754
+ PHYSICAL ACTIVITY AND EXERCISE:
755
+ SYMPTOM MANAGEMENT:
756
+ """
757
+
758
  # Only generate AI plan if AI is enabled AND status isn't immediate emergency based on feedback
759
  # If feedback triggers "emergency", the generated_plan_text is a fixed emergency plan.
760
  if final_status_to_save == 'emergency':
 
767
  f"- Gender: {gender}\n\n"
768
  "ASSESSMENT:\n"
769
  f"- Emergency symptoms reported: {feedback}. Immediate medical attention required.\n\n"
770
+ "DAILY CARE PLAN:\n" # Using the new structure headings
771
+ "Morning:\n"
772
+ "- Seek immediate medical evaluation.\n"
773
+ "Afternoon:\n"
774
+ "- Follow instructions from emergency services.\n"
775
+ "Night:\n"
776
+ "- Remain under medical supervision if advised.\n\n"
777
+ "MEDICATIONS:\n"
778
+ "- Follow *only* instructions from emergency medical personnel regarding medications.\n\n"
779
+ "DIET AND HYDRATION:\n"
780
+ "- Do not consume food or drink unless advised by medical professionals.\n\n"
781
+ "PHYSICAL ACTIVITY AND EXERCISE:\n"
782
+ "- Rest completely and avoid any physical activity until evaluated by medical professionals.\n\n"
783
+ "SYMPTOM MANAGEMENT:\n"
784
+ "- Focus on seeking professional help. Do not attempt home management of emergency symptoms.\n"
785
  )
 
786
  # Status remains 'emergency', which was already set as final_status_to_save.
787
 
 
788
  elif ai_enabled: # AI is enabled and initial status is not emergency
789
+ logger.info("AI is enabled and status is not emergency. Generating plan via AI.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
790
 
791
+ # --- MODIFIED AND OPTIMIZED AI PROMPT ---
792
  prompt = f"""
793
+ You are an expert AI assistant specialized in generating concise, structured patient care plans.
794
+
795
+ Based on the following Patient Information, Patient Feedback, and Previous Care Plan (if available), generate a NEW care plan.
796
+
797
+ --- Patient Information ---
798
  Name: {name}
799
  Age: {age if age is not None else 'N/A'}
800
  Gender: {gender}
801
+
802
+ --- Patient Feedback/Symptoms Update ---
803
  {feedback}
804
+
805
+ --- Previous Care Plan Details (if available) ---
806
  {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."}
807
+
808
+ --- Instructions for Generating the New Care Plan ---
809
+ 1. Generate the care plan using ONLY the exact section headings and sub-sections listed in the "Required Care Plan Structure" below. Maintain the order.
810
+ 2. Fill in the details for each section based on the provided information. Prioritize addressing the latest feedback while incorporating relevant, SAFE, and appropriate details from the previous plan.
811
+ 3. Be specific, actionable, and tailored to the patient's situation as described.
812
+ 4. Keep the content concise. Aim for a few sentences or bullet points per sub-section (e.g., Morning, Afternoon, Night) and per main section (e.g., ASSESSMENT, MEDICATIONS, DIET, EXERCISE, SYMPTOM MANAGEMENT). Do NOT exceed 4 sentences/points per section/sub-section if possible, but prioritize clarity and necessary instructions.
813
+ 5. Regarding MEDICATIONS:
814
+ - If the previous plan listed specific prescribed medications, include them with dosage and timing if possible.
815
+ - If specific medications are not available in the previous plan, list the *types* of medications commonly used for the patient's condition (if implied by the feedback/previous plan, e.g., "Blood pressure medication", "Pain relief").
816
+ - You MAY mention common, over-the-counter examples for specific symptoms if the feedback implies them (e.g., "Paracetamol for fever or mild pain"), but *always* emphasize consulting a doctor for specific prescriptions and dosage. Avoid recommending prescription drugs you are not explicitly told about.
817
+ - *Crucially, do NOT make up prescription drug names or dosages.*
818
+ - *Crucially, do NOT use phrases like "Medications to be determined by physician", "Doctor will provide", "Not provided", "(Unknown)", etc.* List what is known or infer likely categories.
819
+ 6. For PHYSICAL ACTIVITY AND EXERCISE, provide concrete examples or recommendations suitable for someone with the patient's condition and current status (e.g., "Gentle walking 15 mins daily", "Light stretching exercises", "Avoid strenuous activity"). Tailor this to the patient's feedback (e.g., if deteriorating, recommend rest).
820
+ 7. Do NOT include sections like "Red Flags", "Emergency Contacts", "Contact Number", "Doctor's Name", "Hospital Contact", or "Primary Caregiver".
821
+ 8. Do NOT use any vague placeholders like "(To be added)", "(Not provided)", or "(Unknown)" anywhere in the plan. If information is truly unavailable and you cannot infer a reasonable default (like suggesting a type of medication), omit the specific detail rather than using a placeholder.
822
+ 9. Do NOT include any introductory phrases (e.g., "Here is the plan", "Based on your feedback") or concluding remarks outside the plan structure. Provide ONLY the structured content.
823
+ 10. Ensure the plan's tone and content are appropriate for the patient's determined status (Deteriorating, Improving, Stable). For deteriorating status, emphasize caution and monitoring. For improving, suggest gradual progression if appropriate.
824
+ 11. Respond ONLY with the text that follows the "Required Care Plan Structure".
825
+
826
+ --- Required Care Plan Structure (Use ONLY these exact sections and sub-sections in this order) ---
827
+ {required_care_plan_structure}
828
  """
829
+ # --- END OF MODIFIED AI PROMPT ---
830
+
831
  logger.info("Sending prompt to AI model...")
832
 
833
  try:
 
836
  generated_plan_text = response.text.strip()
837
 
838
  # Remove markdown code block formatting if present
839
+ if generated_plan_text.startswith('```'):
840
  # Find the first newline after ``` to potentially strip language name
841
  first_newline_after_code = generated_plan_text.find('\n')
842
  if first_newline_after_code != -1:
 
861
  # Re-determine the final status using the generated plan as well.
862
  # This is important because the AI might infer severity the keyword matching missed initially,
863
  # or the generated plan text itself might contain explicit strong status indicators.
864
+ # Pass original_plan_text again for context if needed by the status function, but updated_plan_text is key here.
865
  final_status_to_save = determine_patient_status(care_plan_text, generated_plan_text, feedback)
866
  logger.info(f"Final status determined after AI generation: {final_status_to_save}")
867
 
 
900
 
901
 
902
  # Create and store patient record in the database
903
+ # Ensure original_plan is stored even if it contains error messages from PDF extraction
904
  new_patient = Patient(
905
  name=name,
906
  age=age,
 
919
 
920
  # Generate PDF for downloading using the stored data
921
  # Note: We pass the patient object directly to the PDF generator for simplicity
922
+ # and to include feedback in the PDF. PDF generator reads updated_plan from the object.
923
  pdf_buffer = generate_care_plan_pdf(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
924
  pdf_buffer.seek(0) # Ensure buffer is at the start before base64 encoding
925
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
926
  logger.info("PDF generated and base64 encoded.")
927
 
928
  # Send care plan via WhatsApp (using the final saved data)
929
+ # Note: We pass the patient object directly to the WhatsApp function.
930
  whatsapp_sent, whatsapp_message = send_whatsapp_care_plan(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
931
  logger.info(f"WhatsApp message attempt sent: {whatsapp_sent}, message: {whatsapp_message}")
932
 
 
939
  'status': new_patient.status, # Return the final determined status
940
  'whatsapp_sent': whatsapp_sent,
941
  'whatsapp_message': whatsapp_message,
942
+ 'ai_error': not ai_enabled or (ai_error_message is not None), # Indicate if AI was not enabled or failed
943
+ 'ai_error_message': ai_error_message # Include the specific error message if applicable
944
  })
945
 
946
  except Exception as e:
 
1043
  return "Patient data not found.", 404
1044
 
1045
  # Pass the full patient dict to PDF generator for feedback inclusion
1046
+ # PDF generator reads patient.updated_plan and patient.status
1047
  pdf_buffer = generate_care_plan_pdf(
1048
  patient.to_dict(),
1049
  patient.updated_plan,
 
1070
  @app.route('/get_emergency_notifications')
1071
  def get_emergency_notifications():
1072
  # Only include patients whose status is 'emergency'
 
 
 
1073
  emergency_patients_query = Patient.query.filter_by(status='emergency').order_by(Patient.timestamp.desc())
1074
 
1075
  # Only return basic info needed for the alert, not full patient details
 
1137
  if __name__ == '__main__':
1138
  # Create database tables if they don't exist within the application context
1139
  with app.app_context():
1140
+ # Use the flag to avoid re-creating tables on every `run` if hot-reloading
1141
+ if not getattr(app, '_tables_created', False):
1142
+ inspector = db.inspect(db.engine)
1143
+ if not inspector.has_table("patient"): # Check for at least one model's table
1144
+ logger.info("Database tables not found, creating.")
1145
+ db.create_all()
1146
+ else:
1147
+ logger.info("Database tables already exist.")
1148
+ app._tables_created = True # Set flag after creation/check
1149
 
1150
  # Use a more robust development server like Waitress or Gunicorn in production
1151
  # For development, debug=True is fine