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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -130
app.py CHANGED
@@ -28,6 +28,7 @@ app = Flask(__name__, instance_path='/tmp')
28
  app.secret_key = os.getenv('SECRET_KEY', '688ed745a74bdd7ac238f5b50f4104fb87d6774b8b0a4e06e7e18ac5ed0fa31c') # CHANGE THIS IN PRODUCTION
29
 
30
  # Database Configuration
 
31
  app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:////tmp/patients.db')
32
  app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
33
 
@@ -61,31 +62,20 @@ class Patient(db.Model):
61
  'timestamp': self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp else None
62
  }
63
 
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
- # 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
 
87
- # Ensure the folder exists at runtime
 
 
88
  os.makedirs(upload_folder, exist_ok=True)
 
89
 
90
 
91
  # Twilio Configuration
@@ -179,49 +169,18 @@ def extract_text_from_pdf(pdf_file):
179
  return f"[Error extracting PDF text: {e}]"
180
 
181
 
 
 
 
182
  def extract_care_plan_format(pdf_text):
183
  """Extract a general format template from PDF text by identifying common section headers."""
184
- 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:
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 ---
225
  def determine_patient_status(original_plan, updated_plan, feedback):
226
  """
227
  Determine patient status based on feedback, original plan, and the *final* updated plan text.
@@ -291,25 +250,12 @@ def determine_patient_status(original_plan, updated_plan, feedback):
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
 
315
  # Combine feedback and original plan text for initial assessment
@@ -323,7 +269,6 @@ def determine_patient_status(original_plan, updated_plan, feedback):
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:
@@ -334,8 +279,6 @@ def determine_patient_status(original_plan, updated_plan, feedback):
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
 
@@ -351,6 +294,7 @@ def determine_patient_status(original_plan, updated_plan, feedback):
351
  return "stable"
352
 
353
 
 
354
  def generate_care_plan_pdf(patient_info, care_plan_text, status):
355
  """Generate a PDF of the care plan with improved styling"""
356
  buffer = io.BytesIO()
@@ -486,33 +430,26 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
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:
510
- # Handle cases with just a bullet point on a line
511
  story.append(Paragraph("• ", bullet_style))
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
 
@@ -536,6 +473,7 @@ def generate_care_plan_pdf(patient_info, care_plan_text, status):
536
  return error_buffer
537
 
538
 
 
539
  def send_whatsapp_care_plan(patient_info, care_plan_text, status):
540
  """Send care plan via WhatsApp using Twilio with improved formatting"""
541
  if not twilio_client:
@@ -567,11 +505,10 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
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
@@ -582,7 +519,6 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
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
@@ -590,8 +526,6 @@ def send_whatsapp_care_plan(patient_info, care_plan_text, status):
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:
@@ -785,10 +719,11 @@ SYMPTOM MANAGEMENT:
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
 
@@ -821,13 +756,14 @@ Gender: {gender}
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:
@@ -837,21 +773,17 @@ Gender: {gender}
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:
843
- # Check if there's a language name before the newline
844
- potential_lang = generated_plan_text[3:first_newline_after_code].strip()
845
- if re.match(r'^[a-zA-Z0-9]+$', potential_lang): # Simple check for language name
846
- generated_plan_text = generated_plan_text[first_newline_after_code:].strip()
847
- else:
848
- # No language name or unexpected format, just strip ```
849
- generated_plan_text = generated_plan_text[3:].strip()
850
  else:
851
- # Handle case where ``` is on the last line without newline
852
- generated_plan_text = generated_plan_text[3:].strip()
853
 
854
- # Strip ending ```
855
  if generated_plan_text.endswith('```'):
856
  generated_plan_text = generated_plan_text[:-3].strip()
857
 
@@ -859,9 +791,6 @@ Gender: {gender}
859
  logger.info(f"AI Response received. Length: {len(generated_plan_text)}")
860
 
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
 
@@ -918,15 +847,12 @@ Gender: {gender}
918
  logger.info(f"Patient {patient_id} added to DB with status: {final_status_to_save}.")
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
 
@@ -951,7 +877,7 @@ Gender: {gender}
951
  'error': f'An unexpected server error occurred: {str(e)}'
952
  }), 500
953
 
954
- # --- New routes for Doctor Dashboard actions ---
955
 
956
  @app.route('/update_care_plan/<patient_id>', methods=['PUT'])
957
  def update_care_plan(patient_id):
@@ -970,9 +896,7 @@ def update_care_plan(patient_id):
970
  logger.warning(f"Update failed for ID {patient_id}: Patient not found.")
971
  return jsonify({'success': False, 'error': 'Patient not found.'}), 404
972
 
973
- # Re-determine status based on the manually updated plan + existing feedback/original?
974
- # Yes, this makes sense. If the doctor edits the plan, it should potentially change the status indication
975
- # if their edits include stronger language about severity or improvement.
976
  patient.status = determine_patient_status(patient.original_plan, updated_plan_text, patient.feedback)
977
 
978
  patient.updated_plan = updated_plan_text
@@ -1020,8 +944,6 @@ def send_whatsapp_doctor(patient_id):
1020
  return jsonify({'success': True, 'message': whatsapp_message})
1021
  else:
1022
  logger.error(f"WhatsApp failed for patient ID: {patient_id} - {whatsapp_message}")
1023
- # Use 500 for server-side error only if it's a backend issue, otherwise 400 maybe?
1024
- # Let's stick to 500 for general failure to send via backend service.
1025
  return jsonify({'success': False, 'error': whatsapp_message}), 500
1026
 
1027
  except Exception as e:
@@ -1136,16 +1058,14 @@ def delete_patient(patient_id):
1136
 
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
 
28
  app.secret_key = os.getenv('SECRET_KEY', '688ed745a74bdd7ac238f5b50f4104fb87d6774b8b0a4e06e7e18ac5ed0fa31c') # CHANGE THIS IN PRODUCTION
29
 
30
  # Database Configuration
31
+ # Revert to original DB path if different
32
  app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:////tmp/patients.db')
33
  app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
34
 
 
62
  'timestamp': self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp else None
63
  }
64
 
65
+ # Database table creation will be handled in if __name__ == '__main__': block below
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
 
68
  upload_base = os.getenv('UPLOAD_DIR', '/tmp')
69
+ # Revert upload folder path to the original structure to avoid PermissionError
70
+ upload_folder = os.path.join(upload_base, 'pdfs')
71
 
72
  app.config['UPLOAD_FOLDER'] = upload_folder
73
 
74
+ # Ensure the folder exists at runtime - This line caused the error with the wrong path
75
+ # It should work now that the path is /tmp/pdfs, assuming /tmp is writable.
76
+ # If /tmp itself is not writable, you'll need to configure UPLOAD_DIR env var.
77
  os.makedirs(upload_folder, exist_ok=True)
78
+ logger.info(f"Upload folder path set to: {upload_folder}")
79
 
80
 
81
  # Twilio Configuration
 
169
  return f"[Error extracting PDF text: {e}]"
170
 
171
 
172
+ # Reverting extract_care_plan_format as it's not used for AI structure anymore
173
+ # The AI prompt uses a fixed structure. The extracted text is passed as context.
174
+ # Keeping a simplified version in case it was used elsewhere, but it's not critical for the prompt.
175
  def extract_care_plan_format(pdf_text):
176
  """Extract a general format template from PDF text by identifying common section headers."""
177
+ # Simplified: The AI uses a fixed template. This function might just return None
178
+ # or the raw text if needed for some other purpose (it wasn't clearly used elsewhere).
179
+ # Let's return None as the AI prompt defines the structure.
180
+ return None
181
+
182
+
183
+ # --- OPTIMIZED determine_patient_status FUNCTION (Keeping this as it's needed) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  def determine_patient_status(original_plan, updated_plan, feedback):
185
  """
186
  Determine patient status based on feedback, original plan, and the *final* updated plan text.
 
250
 
251
  # Helper to check if any keyword is found in the text
252
  def check_keywords(text, keywords):
253
+ # Use regex with word boundaries for precision
 
 
 
 
 
 
 
 
 
254
  for kw in keywords:
255
  if re.search(r'\b' + re.escape(kw) + r'\b', text):
256
  return True
 
 
 
257
  return False
258
 
 
259
  # --- Classification Logic ---
260
 
261
  # Combine feedback and original plan text for initial assessment
 
269
 
270
  # 1. Check for EMERGENCY status (Highest Priority)
271
  # Check the final combined text (feedback + updated plan) for emergency keywords.
 
272
  is_emergency = check_keywords(combined_final_text_lower, emergency_keywords)
273
 
274
  if is_emergency:
 
279
  # Check the final combined text for deteriorating keywords (only if not emergency)
280
  is_deteriorating = check_keywords(combined_final_text_lower, deteriorating_keywords)
281
  if is_deteriorating:
 
 
282
  logger.info("Status determined: DETERIORATING (keyword found in feedback/final plan).")
283
  return "deteriorating"
284
 
 
294
  return "stable"
295
 
296
 
297
+ # --- generate_care_plan_pdf function (Keeping improvements) ---
298
  def generate_care_plan_pdf(patient_info, care_plan_text, status):
299
  """Generate a PDF of the care plan with improved styling"""
300
  buffer = io.BytesIO()
 
430
  # Check for section headers (e.g., "MEDICATIONS:", "ASSESSMENT:")
431
  # Look for lines starting with one or more uppercase words followed by a colon or period,
432
  # or lines that are entirely uppercase words (might be headers without colon).
433
+ header_match = re.match(r'^([A-Z][A-Za-z\s]*)\s*[:.]?$', stripped_line)
434
+ all_caps_match = re.match(r'^[A-Z\s]+$', stripped_line)
435
+
436
+ if header_match or (all_caps_match and len(stripped_line.split()) > 1):
 
 
 
 
437
  header_text = re.sub(r'[:.\s]*$', '', stripped_line).strip()
438
+ if header_text:
439
  story.append(Spacer(1, 8)) # Add space before a new section
440
  story.append(Paragraph(header_text + ":", heading_style)) # Use heading style for sections
441
  # Check for list items (starting with -, *, •)
442
  elif stripped_line.startswith('-') or stripped_line.startswith('*') or stripped_line.startswith('•'):
443
+ bullet_text = re.sub(r'^[-*•][ \t]*', '', line).strip()
 
444
  if bullet_text:
 
445
  formatted_bullet_text = bullet_text.replace('\n', '<br/>')
446
  story.append(Paragraph(f"• {formatted_bullet_text}", bullet_style))
447
  else:
 
448
  story.append(Paragraph("• ", bullet_style))
449
  else:
450
  # Handle regular paragraph text
451
  normal_line_content = line.strip().replace('\n', '<br/>')
452
+ if normal_line_content:
453
  story.append(Paragraph(normal_line_content, normal_style))
454
 
455
 
 
473
  return error_buffer
474
 
475
 
476
+ # --- send_whatsapp_care_plan function (Keeping improvements) ---
477
  def send_whatsapp_care_plan(patient_info, care_plan_text, status):
478
  """Send care plan via WhatsApp using Twilio with improved formatting"""
479
  if not twilio_client:
 
505
  # Replace common list bullet formats with WhatsApp bullet
506
  formatted_plan = formatted_plan.replace('- ', '• ').replace('* ', '• ')
507
 
508
+ # Attempt to bold section headers
 
509
  formatted_plan_lines = []
510
  lines = formatted_plan.split('\n')
511
+ for line in lines:
512
  stripped_line = line.strip()
513
  if not stripped_line:
514
  formatted_plan_lines.append("") # Keep empty lines for spacing
 
519
  all_caps_match = re.match(r'^[A-Z\s]+$', stripped_line)
520
 
521
  if header_match or (all_caps_match and len(stripped_line.split()) > 1):
 
522
  header_text = re.sub(r'[:.\s]*$', '', stripped_line).strip()
523
  if header_text:
524
  formatted_plan_lines.append(f"*{header_text.upper()}:*") # Bold and capitalize for clarity
 
526
  formatted_plan_lines.append(line) # Add original line if cleaning results in empty
527
  # Check for bullet points
528
  elif stripped_line.startswith('•'):
 
 
529
  bullet_content = re.sub(r'^•[ \t]*', '', line).strip() # Get text after bullet
530
  formatted_plan_lines.append(f"• {bullet_content}") # Add WhatsApp bullet
531
  else:
 
719
  )
720
  # Status remains 'emergency', which was already set as final_status_to_save.
721
 
722
+
723
  elif ai_enabled: # AI is enabled and initial status is not emergency
724
  logger.info("AI is enabled and status is not emergency. Generating plan via AI.")
725
 
726
+ # --- MODIFIED AND OPTIMIZED AI PROMPT (Keeping this from previous) ---
727
  prompt = f"""
728
  You are an expert AI assistant specialized in generating concise, structured patient care plans.
729
 
 
756
  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.
757
  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.
758
  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.
759
+ 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.
760
 
761
  --- Required Care Plan Structure (Use ONLY these exact sections and sub-sections in this order) ---
762
  {required_care_plan_structure}
763
  """
764
  # --- END OF MODIFIED AI PROMPT ---
765
 
766
+
767
  logger.info("Sending prompt to AI model...")
768
 
769
  try:
 
773
 
774
  # Remove markdown code block formatting if present
775
  if generated_plan_text.startswith('```'):
 
776
  first_newline_after_code = generated_plan_text.find('\n')
777
  if first_newline_after_code != -1:
778
+ # Check if there's a language name before the newline
779
+ potential_lang = generated_plan_text[3:first_newline_after_code].strip()
780
+ if re.match(r'^[a-zA-Z0-9]+$', potential_lang): # Simple check for language name
781
+ generated_plan_text = generated_plan_text[first_newline_after_code:].strip()
782
+ else:
783
+ generated_plan_text = generated_plan_text[3:].strip()
 
784
  else:
785
+ generated_plan_text = generated_plan_text[3:].strip()
 
786
 
 
787
  if generated_plan_text.endswith('```'):
788
  generated_plan_text = generated_plan_text[:-3].strip()
789
 
 
791
  logger.info(f"AI Response received. Length: {len(generated_plan_text)}")
792
 
793
  # Re-determine the final status using the generated plan as well.
 
 
 
794
  final_status_to_save = determine_patient_status(care_plan_text, generated_plan_text, feedback)
795
  logger.info(f"Final status determined after AI generation: {final_status_to_save}")
796
 
 
847
  logger.info(f"Patient {patient_id} added to DB with status: {final_status_to_save}.")
848
 
849
  # Generate PDF for downloading using the stored data
 
 
850
  pdf_buffer = generate_care_plan_pdf(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
851
  pdf_buffer.seek(0) # Ensure buffer is at the start before base64 encoding
852
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
853
  logger.info("PDF generated and base64 encoded.")
854
 
855
  # Send care plan via WhatsApp (using the final saved data)
 
856
  whatsapp_sent, whatsapp_message = send_whatsapp_care_plan(new_patient.to_dict(), new_patient.updated_plan, new_patient.status)
857
  logger.info(f"WhatsApp message attempt sent: {whatsapp_sent}, message: {whatsapp_message}")
858
 
 
877
  'error': f'An unexpected server error occurred: {str(e)}'
878
  }), 500
879
 
880
+ # --- New routes for Doctor Dashboard actions (Keeping these) ---
881
 
882
  @app.route('/update_care_plan/<patient_id>', methods=['PUT'])
883
  def update_care_plan(patient_id):
 
896
  logger.warning(f"Update failed for ID {patient_id}: Patient not found.")
897
  return jsonify({'success': False, 'error': 'Patient not found.'}), 404
898
 
899
+ # Re-determine status based on the manually updated plan + existing feedback/original
 
 
900
  patient.status = determine_patient_status(patient.original_plan, updated_plan_text, patient.feedback)
901
 
902
  patient.updated_plan = updated_plan_text
 
944
  return jsonify({'success': True, 'message': whatsapp_message})
945
  else:
946
  logger.error(f"WhatsApp failed for patient ID: {patient_id} - {whatsapp_message}")
 
 
947
  return jsonify({'success': False, 'error': whatsapp_message}), 500
948
 
949
  except Exception as e:
 
1058
 
1059
  if __name__ == '__main__':
1060
  # Create database tables if they don't exist within the application context
1061
+ # This is the standard place for create_all() for simpler apps
1062
  with app.app_context():
1063
+ inspector = db.inspect(db.engine)
1064
+ if not inspector.has_table("patient"): # Check for at least one model's table
1065
+ logger.info("Database tables not found, creating.")
1066
+ db.create_all()
1067
+ else:
1068
+ logger.info("Database tables already exist.")
 
 
 
1069
 
1070
  # Use a more robust development server like Waitress or Gunicorn in production
1071
  # For development, debug=True is fine