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