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