Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
#
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 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 |
-
#
|
| 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 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
header_match
|
| 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:
|
| 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 |
-
|
| 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:
|
| 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
|
| 571 |
-
# Use the same pattern as in PDF generation for consistency
|
| 572 |
formatted_plan_lines = []
|
| 573 |
lines = formatted_plan.split('\n')
|
| 574 |
-
for
|
| 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.
|
| 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 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
generated_plan_text = generated_plan_text[3:].strip()
|
| 850 |
else:
|
| 851 |
-
|
| 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 |
-
|
| 1141 |
-
if not
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 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
|