Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -17,506 +17,656 @@ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, Tabl
|
|
| 17 |
|
| 18 |
app = Flask(__name__)
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
upload_base = os.getenv('UPLOAD_DIR', './uploads')
|
| 22 |
-
upload_folder = os.path.join(upload_base, '
|
| 23 |
|
| 24 |
app.config['UPLOAD_FOLDER'] = upload_folder
|
| 25 |
os.makedirs(upload_folder, exist_ok=True)
|
| 26 |
|
| 27 |
# Twilio Configuration
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
# Initialize Twilio client
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
# Gemini API Configuration
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
generation_config = {
|
| 40 |
-
"temperature":
|
| 41 |
-
"top_p": 0.
|
| 42 |
-
"top_k":
|
| 43 |
-
"max_output_tokens":
|
| 44 |
}
|
| 45 |
|
| 46 |
-
model
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
# Patient database (in-memory for demo, use a real database in production)
|
| 52 |
-
patients_db = {}
|
| 53 |
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
def extract_text_from_pdf(pdf_file):
|
| 56 |
"""Extract text from uploaded PDF file"""
|
| 57 |
try:
|
|
|
|
|
|
|
| 58 |
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
| 59 |
text = ""
|
| 60 |
for page in pdf_reader.pages:
|
| 61 |
-
|
|
|
|
|
|
|
| 62 |
return text.strip()
|
| 63 |
except Exception as e:
|
| 64 |
print(f"Error extracting PDF text: {e}")
|
| 65 |
return ""
|
| 66 |
|
| 67 |
-
|
| 68 |
def extract_care_plan_format(pdf_text):
|
| 69 |
-
"""Extract
|
| 70 |
-
|
| 71 |
-
# Find sections and their content in the PDF using regex
|
| 72 |
-
sections = re.findall(
|
| 73 |
-
r'([A-Z][A-Z\s]+)[:|\n]((?:(?!\n[A-Z][A-Z\s]+[:|\n]).)*)',
|
| 74 |
-
pdf_text,
|
| 75 |
-
re.DOTALL
|
| 76 |
-
)
|
| 77 |
-
|
| 78 |
-
if sections:
|
| 79 |
-
format_template = ""
|
| 80 |
-
for section, content in sections:
|
| 81 |
-
format_template += f"{section.strip()}:\n"
|
| 82 |
-
# Extract bullet points if they exist and remove any asterisks
|
| 83 |
-
bullets = re.findall(r'[-•*]\s*(.*?)(?=[-•*]|\n|$)', content)
|
| 84 |
-
if bullets:
|
| 85 |
-
for bullet in bullets:
|
| 86 |
-
format_template += f"- {bullet.strip()}\n"
|
| 87 |
-
format_template += "\n"
|
| 88 |
-
return format_template
|
| 89 |
-
return None
|
| 90 |
-
except Exception as e:
|
| 91 |
-
print(f"Error extracting format: {e}")
|
| 92 |
return None
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
def determine_patient_status(original_plan, updated_plan, feedback):
|
| 96 |
"""Determine patient status based on care plan comparison and feedback"""
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
emergency_keywords = [
|
| 99 |
-
"severe chest pain", "heart attack", "shortness of breath",
|
| 100 |
-
"
|
| 101 |
"sudden weakness", "confusion", "slurred speech", "severe headache",
|
| 102 |
-
"difficulty breathing", "severe shortness of breath", "wheezing",
|
| 103 |
"severe abdominal pain", "persistent vomiting", "uncontrolled bleeding",
|
| 104 |
"severe allergic reaction", "anaphylaxis", "immediate medical attention",
|
| 105 |
"emergency", "call 911", "urgent care", "hospital", "critical", "ambulance",
|
| 106 |
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion",
|
| 107 |
-
"suffocating", "not breathing", "blue lips", "blue face", "cardiac arrest"
|
|
|
|
|
|
|
| 108 |
]
|
| 109 |
-
|
| 110 |
deteriorating_keywords = [
|
| 111 |
"worsening", "increased pain", "not improving", "deteriorating",
|
| 112 |
"elevated", "higher", "more frequent", "concerning", "monitor closely",
|
| 113 |
"decline", "decreased function", "less able", "more difficult", "worse",
|
| 114 |
"getting worse", "aggravated", "intensified", "escalating", "degrading",
|
| 115 |
-
"
|
| 116 |
"increased symptoms", "more severe", "progressing", "progressive", "complicated",
|
| 117 |
-
"
|
|
|
|
| 118 |
]
|
| 119 |
-
|
| 120 |
improvement_keywords = [
|
| 121 |
"improving", "better", "reduced", "lower", "less pain", "increased function",
|
| 122 |
"healing", "recovery", "progress", "stable", "maintained", "consistent",
|
| 123 |
"well-controlled", "responsive", "good progress", "getting better", "positive",
|
| 124 |
"improved", "enhancement", "advancement", "resolving", "resolved", "recovering",
|
| 125 |
"normalized", "normal range", "responding well", "responding positively",
|
| 126 |
-
"effective treatment", "successful treatment", "managed well", "under control"
|
|
|
|
| 127 |
]
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
updated_plan_lower = updated_plan.lower()
|
| 131 |
-
feedback_lower = feedback.lower()
|
| 132 |
-
|
| 133 |
-
# Check for emergency keywords first - higher priority in feedback
|
| 134 |
if any(keyword in feedback_lower for keyword in emergency_keywords):
|
| 135 |
return "emergency"
|
| 136 |
-
|
| 137 |
-
# Check for emergency keywords in updated plan
|
| 138 |
if any(keyword in updated_plan_lower for keyword in emergency_keywords):
|
| 139 |
return "emergency"
|
| 140 |
-
|
| 141 |
# Check for deteriorating keywords
|
| 142 |
if any(keyword in feedback_lower for keyword in deteriorating_keywords):
|
| 143 |
return "deteriorating"
|
| 144 |
-
|
| 145 |
-
# Further check for deteriorating indicators in updated plan
|
| 146 |
if any(keyword in updated_plan_lower for keyword in deteriorating_keywords):
|
| 147 |
return "deteriorating"
|
| 148 |
-
|
| 149 |
# Check for improvement keywords
|
| 150 |
if any(keyword in feedback_lower for keyword in improvement_keywords):
|
| 151 |
return "improving"
|
| 152 |
-
|
| 153 |
-
# Further check for improvement indicators in updated plan
|
| 154 |
if any(keyword in updated_plan_lower for keyword in improvement_keywords):
|
| 155 |
return "improving"
|
| 156 |
-
|
| 157 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
return "stable"
|
| 159 |
|
| 160 |
|
| 161 |
def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
| 162 |
"""Generate a PDF of the care plan with improved styling"""
|
| 163 |
buffer = io.BytesIO()
|
| 164 |
-
|
| 165 |
# Create a PDF document
|
| 166 |
-
doc = SimpleDocTemplate(buffer, pagesize=letter
|
|
|
|
|
|
|
|
|
|
| 167 |
styles = getSampleStyleSheet()
|
| 168 |
-
|
| 169 |
# Create custom styles
|
| 170 |
title_style = ParagraphStyle(
|
| 171 |
'Title',
|
| 172 |
parent=styles['Heading1'],
|
| 173 |
-
fontSize=
|
| 174 |
alignment=1, # Center alignment
|
| 175 |
-
spaceAfter=
|
| 176 |
-
textColor=colors.
|
| 177 |
)
|
| 178 |
-
|
| 179 |
heading_style = ParagraphStyle(
|
| 180 |
'Heading',
|
| 181 |
parent=styles['Heading2'],
|
| 182 |
-
fontSize=
|
| 183 |
-
spaceAfter=
|
| 184 |
-
spaceBefore=
|
| 185 |
-
textColor=colors.
|
| 186 |
)
|
| 187 |
-
|
| 188 |
normal_style = ParagraphStyle(
|
| 189 |
'Normal',
|
| 190 |
parent=styles['Normal'],
|
| 191 |
fontSize=11,
|
| 192 |
-
spaceAfter=
|
| 193 |
-
leading=14
|
|
|
|
| 194 |
)
|
| 195 |
-
|
| 196 |
bullet_style = ParagraphStyle(
|
| 197 |
'Bullet',
|
| 198 |
parent=styles['Normal'],
|
| 199 |
fontSize=11,
|
| 200 |
spaceAfter=3,
|
| 201 |
leftIndent=20,
|
| 202 |
-
leading=14
|
|
|
|
|
|
|
| 203 |
)
|
| 204 |
-
|
| 205 |
# Status color mapping
|
| 206 |
status_colors = {
|
| 207 |
-
'emergency': colors.
|
| 208 |
-
'deteriorating': colors.
|
| 209 |
-
'improving': colors.
|
| 210 |
-
'stable': colors.
|
| 211 |
}
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
# Build the document content
|
| 223 |
story = []
|
| 224 |
-
|
| 225 |
# Title
|
| 226 |
story.append(Paragraph("Patient Care Plan", title_style))
|
| 227 |
-
|
| 228 |
-
|
| 229 |
# Status indicator
|
| 230 |
-
|
| 231 |
'emergency': "EMERGENCY - IMMEDIATE ACTION REQUIRED",
|
| 232 |
'deteriorating': "HIGH RISK - Condition Deteriorating",
|
| 233 |
'improving': "LOW RISK - Condition Improving",
|
| 234 |
-
'stable': "STABLE - Maintain Current Care"
|
|
|
|
| 235 |
}
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
| 240 |
# Patient Information Table
|
| 241 |
patient_data = [
|
| 242 |
-
["Patient Name
|
| 243 |
-
["Age
|
| 244 |
-
["Gender
|
| 245 |
-
["Generated Date
|
| 246 |
]
|
| 247 |
-
|
| 248 |
-
patient_table = Table(patient_data, colWidths=[
|
| 249 |
patient_table.setStyle(TableStyle([
|
| 250 |
-
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
| 251 |
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
|
| 252 |
-
('
|
| 253 |
-
('
|
|
|
|
|
|
|
|
|
|
| 254 |
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
| 255 |
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
| 256 |
-
('TOPPADDING', (0, 0), (-1, -1),
|
| 257 |
-
('BOTTOMPADDING', (0, 0), (-1, -1),
|
| 258 |
]))
|
| 259 |
-
|
| 260 |
story.append(patient_table)
|
| 261 |
-
story.append(Spacer(1,
|
| 262 |
-
|
| 263 |
# Care Plan Content
|
| 264 |
story.append(Paragraph("Care Plan Details:", heading_style))
|
| 265 |
story.append(Spacer(1, 10))
|
| 266 |
-
|
| 267 |
-
# Process care plan text by sections
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
# Build the PDF
|
| 293 |
doc.build(story)
|
| 294 |
buffer.seek(0)
|
| 295 |
return buffer
|
| 296 |
|
| 297 |
|
| 298 |
-
def send_whatsapp_care_plan(
|
| 299 |
"""Send care plan via WhatsApp using Twilio with improved formatting"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
try:
|
| 301 |
-
# Status emoji
|
| 302 |
status_emoji = {
|
| 303 |
'emergency': "🚨 EMERGENCY",
|
| 304 |
'deteriorating': "⚠️ HIGH RISK",
|
| 305 |
'improving': "✅ IMPROVING",
|
| 306 |
'stable': "🟦 STABLE"
|
| 307 |
}
|
| 308 |
-
|
| 309 |
-
# Format message for WhatsApp
|
| 310 |
-
message = f"*Care Plan Update
|
| 311 |
-
message += f"*
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
# If no sections found, add the entire text
|
| 318 |
-
message += care_plan_text
|
| 319 |
-
else:
|
| 320 |
-
for section_title, section_content in sections:
|
| 321 |
-
message += f"*{section_title}:*\n"
|
| 322 |
-
|
| 323 |
-
# Process bullet points if they exist
|
| 324 |
-
bullet_points = re.findall(r'[-•]\s*(.*?)(?=[-•]|\n|$)', section_content.strip())
|
| 325 |
-
|
| 326 |
-
if bullet_points:
|
| 327 |
-
for point in bullet_points:
|
| 328 |
-
if point.strip():
|
| 329 |
-
message += f"• {point.strip()}\n"
|
| 330 |
-
else:
|
| 331 |
-
# If no bullet points, add the content as a paragraph
|
| 332 |
-
cleaned_content = section_content.strip()
|
| 333 |
-
if cleaned_content:
|
| 334 |
-
message += f"{cleaned_content}\n"
|
| 335 |
-
|
| 336 |
-
message += "\n"
|
| 337 |
-
|
| 338 |
# Send WhatsApp message using hardcoded number
|
| 339 |
-
twilio_client.messages.create(
|
| 340 |
from_=TWILIO_FROM,
|
| 341 |
body=message,
|
| 342 |
to=TWILIO_TO
|
| 343 |
)
|
|
|
|
| 344 |
return True
|
| 345 |
except Exception as e:
|
| 346 |
print(f"Error sending WhatsApp message: {e}")
|
|
|
|
| 347 |
return False
|
| 348 |
|
| 349 |
|
| 350 |
@app.route('/')
|
| 351 |
def index():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
return render_template('index.html')
|
| 353 |
|
| 354 |
|
| 355 |
@app.route('/switch_role', methods=['POST'])
|
| 356 |
def switch_role():
|
| 357 |
role = request.form.get('role')
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
| 360 |
|
| 361 |
|
| 362 |
@app.route('/doctor_dashboard')
|
| 363 |
def doctor_dashboard():
|
|
|
|
|
|
|
| 364 |
return render_template('doctor_dashboard.html')
|
| 365 |
|
| 366 |
|
|
|
|
|
|
|
| 367 |
@app.route('/patient_details/<patient_id>')
|
| 368 |
def patient_details(patient_id):
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
|
| 374 |
|
| 375 |
@app.route('/submit_feedback', methods=['POST'])
|
| 376 |
def submit_feedback():
|
|
|
|
|
|
|
|
|
|
| 377 |
try:
|
| 378 |
-
# Get patient information
|
| 379 |
-
name = request.form.get('name', '')
|
| 380 |
-
age = request.form.get('age', '')
|
| 381 |
-
gender = request.form.get('gender', '')
|
| 382 |
feedback = request.form.get('feedback', '')
|
| 383 |
-
|
| 384 |
care_plan_text = ""
|
| 385 |
care_plan_format = None
|
| 386 |
-
|
|
|
|
| 387 |
if 'care_plan_pdf' in request.files:
|
| 388 |
pdf_file = request.files['care_plan_pdf']
|
| 389 |
-
if pdf_file.filename != '':
|
|
|
|
| 390 |
care_plan_text = extract_text_from_pdf(pdf_file)
|
| 391 |
-
|
| 392 |
-
|
|
|
|
|
|
|
| 393 |
# If no format is found in the PDF, use a default format
|
| 394 |
-
if not care_plan_format:
|
|
|
|
| 395 |
care_plan_format = """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
ASSESSMENT:
|
| 397 |
-
- [
|
|
|
|
| 398 |
DAILY CARE PLAN:
|
| 399 |
Morning:
|
| 400 |
-
- [Morning activities]
|
| 401 |
Afternoon:
|
| 402 |
-
- [Afternoon activities]
|
| 403 |
Evening:
|
| 404 |
-
- [Evening activities]
|
|
|
|
|
|
|
|
|
|
| 405 |
MEDICATIONS:
|
| 406 |
-
- [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
ADDITIONAL RECOMMENDATIONS:
|
| 408 |
-
- [
|
|
|
|
| 409 |
FOLLOW-UP:
|
| 410 |
-
- [
|
| 411 |
"""
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
|
|
|
| 416 |
"severe chest pain", "heart attack", "shortness of breath",
|
| 417 |
"dizziness", "loss of consciousness", "extreme pain",
|
| 418 |
-
|
| 419 |
-
# Neurological
|
| 420 |
"sudden weakness", "confusion", "slurred speech", "severe headache",
|
| 421 |
-
|
| 422 |
-
# Respiratory
|
| 423 |
-
"difficulty breathing", "severe shortness of breath", "wheezing", "respiratory distress",
|
| 424 |
-
|
| 425 |
-
# Gastrointestinal
|
| 426 |
"severe abdominal pain", "persistent vomiting", "uncontrolled bleeding",
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
"
|
|
|
|
|
|
|
|
|
|
| 430 |
]
|
| 431 |
-
|
| 432 |
feedback_lower = feedback.lower()
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
"ASSESSMENT:\n"
|
| 439 |
-
"- Emergency symptoms
|
| 440 |
"EMERGENCY ACTION PLAN:\n"
|
| 441 |
-
"- Call emergency services immediately at 104/108/109/112.\n"
|
| 442 |
"- Do not delay seeking medical help.\n"
|
| 443 |
-
"-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
"FOLLOW-UP:\n"
|
| 445 |
-
"- Immediate hospitalization
|
| 446 |
-
"-
|
|
|
|
| 447 |
)
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
'original_plan': care_plan_text,
|
| 458 |
-
'updated_plan': emergency_message,
|
| 459 |
-
'status': 'emergency',
|
| 460 |
-
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 461 |
-
}
|
| 462 |
-
|
| 463 |
-
# Generate PDF for emergency instructions
|
| 464 |
-
patient_info = {
|
| 465 |
-
'name': name,
|
| 466 |
-
'age': age,
|
| 467 |
-
'gender': gender
|
| 468 |
-
}
|
| 469 |
-
pdf_buffer = generate_care_plan_pdf(patient_info, emergency_message, 'emergency')
|
| 470 |
-
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
|
| 471 |
-
|
| 472 |
-
# Send emergency WhatsApp message
|
| 473 |
-
send_whatsapp_care_plan(name, emergency_message, 'emergency')
|
| 474 |
-
|
| 475 |
-
return jsonify({
|
| 476 |
-
'success': True,
|
| 477 |
-
'updated_plan': emergency_message,
|
| 478 |
-
'patient_id': patient_id,
|
| 479 |
-
'status': 'emergency',
|
| 480 |
-
'pdf_data': pdf_base64
|
| 481 |
-
})
|
| 482 |
-
|
| 483 |
-
# Prepare a prompt for Gemini AI for a perfect, attractively formatted day care plan.
|
| 484 |
-
prompt = f"""
|
| 485 |
Patient Information:
|
| 486 |
Name: {name}
|
| 487 |
Age: {age}
|
| 488 |
Gender: {gender}
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
{care_plan_format}
|
| 494 |
-
Please ensure the following:
|
| 495 |
-
- The response is well-structured with clear section headings.
|
| 496 |
-
- Use bullet points for list items.
|
| 497 |
-
- Format the text attractively and professionally.
|
| 498 |
-
- Be specific with recommendations based on the patient's feedback.
|
| 499 |
-
- Include specific times for medications if applicable.
|
| 500 |
-
- Provide clear follow-up instructions.
|
| 501 |
"""
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
patient_id = str(uuid.uuid4())
|
| 521 |
patients_db[patient_id] = {
|
| 522 |
'id': patient_id,
|
|
@@ -524,77 +674,118 @@ Please ensure the following:
|
|
| 524 |
'age': age,
|
| 525 |
'gender': gender,
|
| 526 |
'feedback': feedback,
|
| 527 |
-
'original_plan': care_plan_text,
|
| 528 |
-
'updated_plan':
|
| 529 |
'status': status,
|
| 530 |
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 531 |
}
|
| 532 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
# Send care plan via WhatsApp
|
| 534 |
-
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
return jsonify({
|
| 537 |
'success': True,
|
| 538 |
-
'updated_plan':
|
| 539 |
'pdf_data': pdf_base64,
|
| 540 |
-
'patient_id': patient_id,
|
| 541 |
'status': status,
|
| 542 |
'whatsapp_sent': whatsapp_sent
|
| 543 |
})
|
| 544 |
-
|
| 545 |
except Exception as e:
|
| 546 |
-
print(f"
|
|
|
|
|
|
|
|
|
|
| 547 |
return jsonify({
|
| 548 |
'success': False,
|
| 549 |
-
'error': str(e)
|
| 550 |
-
})
|
| 551 |
|
| 552 |
|
| 553 |
@app.route('/download_pdf/<patient_id>')
|
| 554 |
def download_pdf(patient_id):
|
|
|
|
|
|
|
|
|
|
| 555 |
try:
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
'
|
| 565 |
-
'
|
| 566 |
-
|
| 567 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
pdf_buffer.seek(0) # Ensure buffer is at the start
|
| 569 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
return send_file(
|
| 571 |
pdf_buffer,
|
| 572 |
as_attachment=True,
|
| 573 |
-
download_name=
|
| 574 |
mimetype='application/pdf'
|
| 575 |
)
|
| 576 |
except Exception as e:
|
| 577 |
-
print(f"PDF Download Error: {str(e)}")
|
|
|
|
|
|
|
| 578 |
return f"Error generating PDF: {str(e)}", 500
|
| 579 |
|
| 580 |
|
| 581 |
@app.route('/get_emergency_notifications')
|
| 582 |
def get_emergency_notifications():
|
| 583 |
# Filter patients with emergency status
|
| 584 |
-
emergency_patients = [p for p in patients_db.values() if p
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
| 586 |
notifications = []
|
| 587 |
for patient in emergency_patients:
|
| 588 |
notifications.append({
|
| 589 |
'patient_id': patient['id'],
|
| 590 |
-
'patient_name': patient
|
| 591 |
-
'patient_age': patient
|
| 592 |
-
'patient_gender': patient
|
| 593 |
-
'patient_feedback': patient
|
| 594 |
-
'timestamp': patient
|
| 595 |
-
'status': patient
|
| 596 |
})
|
| 597 |
-
|
| 598 |
return jsonify({
|
| 599 |
'success': True,
|
| 600 |
'notifications': notifications
|
|
@@ -604,22 +795,35 @@ def get_emergency_notifications():
|
|
| 604 |
@app.route('/get_patients')
|
| 605 |
def get_patients():
|
| 606 |
# Return all patients for the doctor dashboard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
return jsonify({
|
| 608 |
'success': True,
|
| 609 |
-
'patients': list(
|
| 610 |
})
|
| 611 |
|
| 612 |
|
| 613 |
@app.route('/get_patient/<patient_id>')
|
| 614 |
def get_patient(patient_id):
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
return jsonify({
|
| 619 |
'success': True,
|
| 620 |
-
'patient':
|
| 621 |
})
|
| 622 |
|
| 623 |
|
| 624 |
if __name__ == '__main__':
|
| 625 |
-
|
|
|
|
|
|
| 17 |
|
| 18 |
app = Flask(__name__)
|
| 19 |
|
| 20 |
+
# Use a strong, unique secret key
|
| 21 |
+
app.secret_key = os.getenv('SECRET_KEY', '688ed745a74bdd7ac238f5b50f4104fb87d6774b8b0a4e06e7e18ac5ed0fa31c') # CHANGE THIS IN PRODUCTION
|
| 22 |
+
|
| 23 |
upload_base = os.getenv('UPLOAD_DIR', './uploads')
|
| 24 |
+
upload_folder = os.path.join(upload_base, 'pdfs') # Changed folder name for clarity
|
| 25 |
|
| 26 |
app.config['UPLOAD_FOLDER'] = upload_folder
|
| 27 |
os.makedirs(upload_folder, exist_ok=True)
|
| 28 |
|
| 29 |
# Twilio Configuration
|
| 30 |
+
# IMPORTANT: Use environment variables for sensitive information like SID and Auth Token
|
| 31 |
+
ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'AC490e071f8d01bf0df2f03d086c788d87')
|
| 32 |
+
AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '224b23b950ad5a4052aba15893fdf083')
|
| 33 |
+
TWILIO_FROM = os.getenv('TWILIO_FROM_NUMBER', 'whatsapp:+14155238886')
|
| 34 |
+
TWILIO_TO = os.getenv('TWILIO_TO_NUMBER', 'whatsapp:+917559355282') # Hardcoded number as requested
|
| 35 |
|
| 36 |
# Initialize Twilio client
|
| 37 |
+
try:
|
| 38 |
+
twilio_client = Client(ACCOUNT_SID, AUTH_TOKEN)
|
| 39 |
+
print("Twilio client initialized successfully.")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"Error initializing Twilio client: {e}")
|
| 42 |
+
twilio_client = None # Handle cases where Twilio initialization fails
|
| 43 |
|
| 44 |
# Gemini API Configuration
|
| 45 |
+
# IMPORTANT: Use environment variables for sensitive information like API keys
|
| 46 |
+
GENAI_API_KEY = os.getenv('GENAI_API_KEY', "AIzaSyD54ejbjVIVa-F3aD_Urnp8m1EFLUGR__I") # CHANGE THIS KEY
|
| 47 |
+
try:
|
| 48 |
+
genai.configure(api_key=GENAI_API_KEY)
|
| 49 |
+
# Simple test to check connectivity
|
| 50 |
+
list(genai.list_models())
|
| 51 |
+
print("Gemini API configured successfully.")
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"Error configuring Gemini API: {e}")
|
| 54 |
+
genai = None # Handle cases where API configuration fails
|
| 55 |
+
|
| 56 |
|
| 57 |
generation_config = {
|
| 58 |
+
"temperature": 0.8, # Slightly reduced temp for more stable output
|
| 59 |
+
"top_p": 0.9, # Adjusted top_p
|
| 60 |
+
"top_k": 30, # Adjusted top_k
|
| 61 |
+
"max_output_tokens": 4096, # Reduced token limit slightly if needed, or increase if plans are long
|
| 62 |
}
|
| 63 |
|
| 64 |
+
# Use a more capable model if available and affordable
|
| 65 |
+
# "gemini-1.5-pro" or "gemini-1.5-flash" are better choices than gemini-2.0-flash if 2.0 means an older preview
|
| 66 |
+
# Let's assume gemini-2.0-flash is a valid, accessible model alias
|
| 67 |
+
model = None
|
| 68 |
+
if genai:
|
| 69 |
+
try:
|
| 70 |
+
model = genai.GenerativeModel(
|
| 71 |
+
model_name="gemini-1.5-flash-latest", # Recommended model alias
|
| 72 |
+
generation_config=generation_config,
|
| 73 |
+
)
|
| 74 |
+
# Test model generation
|
| 75 |
+
# model.generate_content("Hello")
|
| 76 |
+
print(f"Using Gemini model: {model.model_name}")
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"Error initializing Gemini model: {e}")
|
| 79 |
+
model = None # Handle cases where model initialization fails
|
| 80 |
|
|
|
|
|
|
|
| 81 |
|
| 82 |
+
# Patient database (in-memory for demo - WILL BE RESET ON SERVER RESTART)
|
| 83 |
+
# For persistence, replace this with a database (SQLite, PostgreSQL, etc.)
|
| 84 |
+
patients_db = {}
|
| 85 |
|
| 86 |
def extract_text_from_pdf(pdf_file):
|
| 87 |
"""Extract text from uploaded PDF file"""
|
| 88 |
try:
|
| 89 |
+
# Ensure file pointer is at the beginning
|
| 90 |
+
pdf_file.seek(0)
|
| 91 |
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
| 92 |
text = ""
|
| 93 |
for page in pdf_reader.pages:
|
| 94 |
+
page_text = page.extract_text()
|
| 95 |
+
if page_text:
|
| 96 |
+
text += page_text + "\n" # Add newline between pages
|
| 97 |
return text.strip()
|
| 98 |
except Exception as e:
|
| 99 |
print(f"Error extracting PDF text: {e}")
|
| 100 |
return ""
|
| 101 |
|
|
|
|
| 102 |
def extract_care_plan_format(pdf_text):
|
| 103 |
+
"""Extract a general format template from PDF text by identifying common section headers."""
|
| 104 |
+
if not pdf_text:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
return None
|
| 106 |
|
| 107 |
+
# Look for lines that seem like headers (e.g., capitalized words ending with :, followed by content)
|
| 108 |
+
# This regex is an attempt to capture common patterns but might not be perfect for all PDFs.
|
| 109 |
+
# It looks for uppercase words (potentially with spaces), followed by a colon, then captures subsequent lines until another similar pattern or end.
|
| 110 |
+
sections = re.findall(
|
| 111 |
+
r'^([A-Z][A-Z\s]*):(?:\s*\n)?((?:(?!\n[A-Z][A-Z\s]*:).*\n?)*)',
|
| 112 |
+
pdf_text,
|
| 113 |
+
re.MULTILINE | re.DOTALL
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
if not sections:
|
| 117 |
+
# Fallback: If strict section matching fails, try to find prominent lines as potential headers
|
| 118 |
+
potential_headers = re.findall(r'^[A-Z][A-Za-z\s,]*[.:]?$', pdf_text, re.MULTILINE)
|
| 119 |
+
if potential_headers:
|
| 120 |
+
# Just list the potential headers as a basic format guess
|
| 121 |
+
format_template = "\n".join([f"{header.strip()}:" for header in set(potential_headers) if header.strip()]) + "\n"
|
| 122 |
+
print(f"Extracted potential headers (fallback): {list(set(potential_headers))}")
|
| 123 |
+
return format_template if format_template.strip() else None
|
| 124 |
+
print("No sections or potential headers found.")
|
| 125 |
+
return None # No recognizable format found
|
| 126 |
+
|
| 127 |
+
format_template = ""
|
| 128 |
+
for section_title, _ in sections:
|
| 129 |
+
format_template += f"{section_title.strip()}:\n- [Details]\n" # Use a placeholder bullet structure
|
| 130 |
+
print(f"Extracted sections: {[s[0].strip() for s in sections]}")
|
| 131 |
+
return format_template if format_template.strip() else None
|
| 132 |
+
|
| 133 |
|
| 134 |
def determine_patient_status(original_plan, updated_plan, feedback):
|
| 135 |
"""Determine patient status based on care plan comparison and feedback"""
|
| 136 |
+
|
| 137 |
+
feedback_lower = feedback.lower()
|
| 138 |
+
original_plan_lower = original_plan.lower() if original_plan else ""
|
| 139 |
+
updated_plan_lower = updated_plan.lower()
|
| 140 |
+
|
| 141 |
+
# Comprehensive keywords indicating condition worsening or emergency
|
| 142 |
emergency_keywords = [
|
| 143 |
+
"severe chest pain", "heart attack", "sudden shortness of breath",
|
| 144 |
+
"difficulty breathing", "loss of consciousness", "extreme pain",
|
| 145 |
"sudden weakness", "confusion", "slurred speech", "severe headache",
|
|
|
|
| 146 |
"severe abdominal pain", "persistent vomiting", "uncontrolled bleeding",
|
| 147 |
"severe allergic reaction", "anaphylaxis", "immediate medical attention",
|
| 148 |
"emergency", "call 911", "urgent care", "hospital", "critical", "ambulance",
|
| 149 |
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion",
|
| 150 |
+
"suffocating", "not breathing", "blue lips", "blue face", "cardiac arrest",
|
| 151 |
+
"high fever", "signs of shock", "severe dehydration", "acute change",
|
| 152 |
+
"unstable vitals", "rapidly worsening"
|
| 153 |
]
|
| 154 |
+
|
| 155 |
deteriorating_keywords = [
|
| 156 |
"worsening", "increased pain", "not improving", "deteriorating",
|
| 157 |
"elevated", "higher", "more frequent", "concerning", "monitor closely",
|
| 158 |
"decline", "decreased function", "less able", "more difficult", "worse",
|
| 159 |
"getting worse", "aggravated", "intensified", "escalating", "degrading",
|
| 160 |
+
"weakening", "relapse", "recurrence", "regressing", "not responding",
|
| 161 |
"increased symptoms", "more severe", "progressing", "progressive", "complicated",
|
| 162 |
+
"adverse change", "unstable", "needs attention", "spike in", "significant increase",
|
| 163 |
+
"new symptoms", "trouble with", "reduced appetite", "difficulty sleeping"
|
| 164 |
]
|
| 165 |
+
|
| 166 |
improvement_keywords = [
|
| 167 |
"improving", "better", "reduced", "lower", "less pain", "increased function",
|
| 168 |
"healing", "recovery", "progress", "stable", "maintained", "consistent",
|
| 169 |
"well-controlled", "responsive", "good progress", "getting better", "positive",
|
| 170 |
"improved", "enhancement", "advancement", "resolving", "resolved", "recovering",
|
| 171 |
"normalized", "normal range", "responding well", "responding positively",
|
| 172 |
+
"effective treatment", "successful treatment", "managed well", "under control",
|
| 173 |
+
"symptoms decreased", "feeling stronger", "better sleep", "increased appetite"
|
| 174 |
]
|
| 175 |
+
|
| 176 |
+
# Check for emergency keywords first (highest priority), primarily in feedback as it's direct input
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
if any(keyword in feedback_lower for keyword in emergency_keywords):
|
| 178 |
return "emergency"
|
| 179 |
+
# Then check updated plan, as AI might interpret feedback and include emergency terms
|
|
|
|
| 180 |
if any(keyword in updated_plan_lower for keyword in emergency_keywords):
|
| 181 |
return "emergency"
|
| 182 |
+
|
| 183 |
# Check for deteriorating keywords
|
| 184 |
if any(keyword in feedback_lower for keyword in deteriorating_keywords):
|
| 185 |
return "deteriorating"
|
| 186 |
+
# Check updated plan for deteriorating indicators
|
|
|
|
| 187 |
if any(keyword in updated_plan_lower for keyword in deteriorating_keywords):
|
| 188 |
return "deteriorating"
|
| 189 |
+
|
| 190 |
# Check for improvement keywords
|
| 191 |
if any(keyword in feedback_lower for keyword in improvement_keywords):
|
| 192 |
return "improving"
|
| 193 |
+
# Check updated plan for improvement indicators
|
|
|
|
| 194 |
if any(keyword in updated_plan_lower for keyword in improvement_keywords):
|
| 195 |
return "improving"
|
| 196 |
+
|
| 197 |
+
# Compare original vs updated plan length/content as a weak signal (optional, can be removed if unreliable)
|
| 198 |
+
# if len(updated_plan) < len(original_plan) * 0.8 and any(k in updated_plan_lower for k in improvement_keywords):
|
| 199 |
+
# return "improving"
|
| 200 |
+
# if len(updated_plan) > len(original_plan) * 1.2 and any(k in updated_plan_lower for k in deteriorating_keywords):
|
| 201 |
+
# return "deteriorating"
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# Default to stable if no clear indicators of change are found
|
| 205 |
return "stable"
|
| 206 |
|
| 207 |
|
| 208 |
def generate_care_plan_pdf(patient_info, care_plan_text, status):
|
| 209 |
"""Generate a PDF of the care plan with improved styling"""
|
| 210 |
buffer = io.BytesIO()
|
| 211 |
+
|
| 212 |
# Create a PDF document
|
| 213 |
+
doc = SimpleDocTemplate(buffer, pagesize=letter,
|
| 214 |
+
leftMargin=72, rightMargin=72,
|
| 215 |
+
topMargin=72, bottomMargin=72) # Add margins
|
| 216 |
+
|
| 217 |
styles = getSampleStyleSheet()
|
| 218 |
+
|
| 219 |
# Create custom styles
|
| 220 |
title_style = ParagraphStyle(
|
| 221 |
'Title',
|
| 222 |
parent=styles['Heading1'],
|
| 223 |
+
fontSize=22, # Slightly larger title
|
| 224 |
alignment=1, # Center alignment
|
| 225 |
+
spaceAfter=25, # More space after title
|
| 226 |
+
textColor=colors.HexColor("#4e73df") # Primary color
|
| 227 |
)
|
| 228 |
+
|
| 229 |
heading_style = ParagraphStyle(
|
| 230 |
'Heading',
|
| 231 |
parent=styles['Heading2'],
|
| 232 |
+
fontSize=15, # Slightly larger headings
|
| 233 |
+
spaceAfter=8, # Less space after heading for tighter look
|
| 234 |
+
spaceBefore=18, # More space before heading
|
| 235 |
+
textColor=colors.HexColor("#1cc88a") # Secondary color
|
| 236 |
)
|
| 237 |
+
|
| 238 |
normal_style = ParagraphStyle(
|
| 239 |
'Normal',
|
| 240 |
parent=styles['Normal'],
|
| 241 |
fontSize=11,
|
| 242 |
+
spaceAfter=6, # More space after paragraphs
|
| 243 |
+
leading=14,
|
| 244 |
+
textColor=colors.HexColor("#5a5c69") # Dark color
|
| 245 |
)
|
| 246 |
+
|
| 247 |
bullet_style = ParagraphStyle(
|
| 248 |
'Bullet',
|
| 249 |
parent=styles['Normal'],
|
| 250 |
fontSize=11,
|
| 251 |
spaceAfter=3,
|
| 252 |
leftIndent=20,
|
| 253 |
+
leading=14,
|
| 254 |
+
bulletIndent=10, # Space between bullet and text
|
| 255 |
+
textColor=colors.HexColor("#5a5c69") # Dark color
|
| 256 |
)
|
| 257 |
+
|
| 258 |
# Status color mapping
|
| 259 |
status_colors = {
|
| 260 |
+
'emergency': colors.HexColor("#e74a3b"), # Danger color
|
| 261 |
+
'deteriorating': colors.HexColor("#f6c23e"), # Warning color
|
| 262 |
+
'improving': colors.HexColor("#1cc88a"), # Success color
|
| 263 |
+
'stable': colors.HexColor("#36b9cc") # Info color
|
| 264 |
}
|
| 265 |
+
|
| 266 |
+
status_text_styles = {
|
| 267 |
+
'emergency': ParagraphStyle('StatusEmergency', parent=styles['Heading2'], fontSize=16, spaceBefore=10, spaceAfter=15, textColor=status_colors['emergency'], alignment=1, fontName='Helvetica-Bold'),
|
| 268 |
+
'deteriorating': ParagraphStyle('StatusDeteriorating', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['deteriorating'], alignment=1, fontName='Helvetica-Bold'),
|
| 269 |
+
'improving': ParagraphStyle('StatusImproving', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['improving'], alignment=1, fontName='Helvetica-Bold'),
|
| 270 |
+
'stable': ParagraphStyle('StatusStable', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=status_colors['stable'], alignment=1, fontName='Helvetica-Bold'),
|
| 271 |
+
'unknown': ParagraphStyle('StatusUnknown', parent=styles['Heading2'], fontSize=15, spaceBefore=10, spaceAfter=15, textColor=colors.black, alignment=1, fontName='Helvetica-Bold'),
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
|
| 275 |
# Build the document content
|
| 276 |
story = []
|
| 277 |
+
|
| 278 |
# Title
|
| 279 |
story.append(Paragraph("Patient Care Plan", title_style))
|
| 280 |
+
|
|
|
|
| 281 |
# Status indicator
|
| 282 |
+
status_map_text = {
|
| 283 |
'emergency': "EMERGENCY - IMMEDIATE ACTION REQUIRED",
|
| 284 |
'deteriorating': "HIGH RISK - Condition Deteriorating",
|
| 285 |
'improving': "LOW RISK - Condition Improving",
|
| 286 |
+
'stable': "STABLE - Maintain Current Care",
|
| 287 |
+
'unknown': "Status: Unknown"
|
| 288 |
}
|
| 289 |
+
current_status_text = status_map_text.get(status, 'unknown')
|
| 290 |
+
current_status_style = status_text_styles.get(status, status_text_styles['unknown'])
|
| 291 |
+
|
| 292 |
+
story.append(Paragraph(current_status_text, current_status_style))
|
| 293 |
+
|
| 294 |
+
|
| 295 |
# Patient Information Table
|
| 296 |
patient_data = [
|
| 297 |
+
[Paragraph("<b>Patient Name:</b>", normal_style), Paragraph(patient_info.get('name', 'N/A'), normal_style)],
|
| 298 |
+
[Paragraph("<b>Age:</b>", normal_style), Paragraph(patient_info.get('age', 'N/A'), normal_style)],
|
| 299 |
+
[Paragraph("<b>Gender:</b>", normal_style), Paragraph(patient_info.get('gender', 'N/A'), normal_style)],
|
| 300 |
+
[Paragraph("<b>Generated Date:</b>", normal_style), Paragraph(datetime.now().strftime("%Y-%m-%d %H:%M"), normal_style)]
|
| 301 |
]
|
| 302 |
+
|
| 303 |
+
patient_table = Table(patient_data, colWidths=[150, 360]) # Adjusted column widths
|
| 304 |
patient_table.setStyle(TableStyle([
|
|
|
|
| 305 |
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
|
| 306 |
+
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
|
| 307 |
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 308 |
+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 309 |
+
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.grey), # Lighter grid
|
| 310 |
+
('BOX', (0, 0), (-1, -1), 0.25, colors.grey),
|
| 311 |
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
| 312 |
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
| 313 |
+
('TOPPADDING', (0, 0), (-1, -1), 6), # Increased padding
|
| 314 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 6), # Increased padding
|
| 315 |
]))
|
| 316 |
+
|
| 317 |
story.append(patient_table)
|
| 318 |
+
story.append(Spacer(1, 25)) # More space after table
|
| 319 |
+
|
| 320 |
# Care Plan Content
|
| 321 |
story.append(Paragraph("Care Plan Details:", heading_style))
|
| 322 |
story.append(Spacer(1, 10))
|
| 323 |
+
|
| 324 |
+
# Process care plan text by lines or sections for better formatting
|
| 325 |
+
lines = care_plan_text.strip().split('\n')
|
| 326 |
+
current_section = []
|
| 327 |
+
is_bullet_section = False
|
| 328 |
+
|
| 329 |
+
def add_section_to_story(section_lines, is_bullet):
|
| 330 |
+
if not section_lines:
|
| 331 |
+
return
|
| 332 |
+
text = "\n".join(section_lines).strip()
|
| 333 |
+
if not text:
|
| 334 |
+
return
|
| 335 |
+
|
| 336 |
+
# Attempt to identify potential sub-headings within content
|
| 337 |
+
sub_sections = re.split(r'\n(?=[A-Z][A-Z\s]*:)', text)
|
| 338 |
+
|
| 339 |
+
for i, sub_text in enumerate(sub_sections):
|
| 340 |
+
if i > 0: # Add space before subsequent sections
|
| 341 |
+
story.append(Spacer(1, 8))
|
| 342 |
+
|
| 343 |
+
# Check if the sub_text starts with a potential sub-heading
|
| 344 |
+
header_match = re.match(r'([A-Z][A-Z\s]*):', sub_text)
|
| 345 |
+
if header_match:
|
| 346 |
+
header_title = header_match.group(1).strip()
|
| 347 |
+
content_after_header = sub_text[header_match.end():].strip()
|
| 348 |
+
story.append(Paragraph(header_title + ":", heading_style)) # Use heading style for sub-sections too
|
| 349 |
+
|
| 350 |
+
# Process content after the potential header
|
| 351 |
+
content_lines = content_after_header.split('\n')
|
| 352 |
+
sub_is_bullet = any(re.match(r'^\s*[-•*]', line) for line in content_lines)
|
| 353 |
+
|
| 354 |
+
if sub_is_bullet:
|
| 355 |
+
sub_bullet_points = re.findall(r'^\s*[-•*]\s*(.*?)$', content_after_header, re.MULTILINE)
|
| 356 |
+
if sub_bullet_points:
|
| 357 |
+
for point in sub_bullet_points:
|
| 358 |
+
if point.strip():
|
| 359 |
+
story.append(Paragraph(f"• {point.strip()}", bullet_style))
|
| 360 |
+
else: # Fallback if regex misses some bullets
|
| 361 |
+
for line in content_lines:
|
| 362 |
+
cleaned_line = re.sub(r'^\s*[-•*]\s*', '', line).strip()
|
| 363 |
+
if cleaned_line:
|
| 364 |
+
story.append(Paragraph(f"• {cleaned_line}", bullet_style))
|
| 365 |
+
|
| 366 |
+
else: # Not a bulleted sub-section
|
| 367 |
+
cleaned_content = content_after_header.strip()
|
| 368 |
+
if cleaned_content:
|
| 369 |
+
story.append(Paragraph(cleaned_content, normal_style))
|
| 370 |
+
|
| 371 |
+
else: # If no sub-heading found at the start, treat as a single block of content
|
| 372 |
+
if is_bullet:
|
| 373 |
+
bullet_points = re.findall(r'^\s*[-•*]\s*(.*?)$', text, re.MULTILINE)
|
| 374 |
+
if bullet_points:
|
| 375 |
+
for point in bullet_points:
|
| 376 |
+
if point.strip():
|
| 377 |
+
story.append(Paragraph(f"• {point.strip()}", bullet_style))
|
| 378 |
+
else: # Fallback if regex misses some bullets
|
| 379 |
+
for line in section_lines:
|
| 380 |
+
cleaned_line = re.sub(r'^\s*[-•*]\s*', '', line).strip()
|
| 381 |
+
if cleaned_line:
|
| 382 |
+
story.append(Paragraph(f"• {cleaned_line}", bullet_style))
|
| 383 |
+
else:
|
| 384 |
+
story.append(Paragraph(text, normal_style))
|
| 385 |
+
|
| 386 |
+
for line in lines:
|
| 387 |
+
stripped_line = line.strip()
|
| 388 |
+
# Check if line is a potential section header
|
| 389 |
+
header_match = re.match(r'^([A-Z][A-Z\s]*):$', stripped_line)
|
| 390 |
+
|
| 391 |
+
if header_match:
|
| 392 |
+
# Add previous section before starting a new one
|
| 393 |
+
add_section_to_story(current_section, is_bullet_section)
|
| 394 |
+
story.append(Paragraph(stripped_line, heading_style)) # Add the new header
|
| 395 |
+
current_section = [] # Reset current section content
|
| 396 |
+
is_bullet_section = False # Assume new section is not bulleted initially
|
| 397 |
+
elif stripped_line.startswith('-') or stripped_line.startswith('•') or stripped_line.startswith('*'):
|
| 398 |
+
# Line is a bullet point, add it to the current section content
|
| 399 |
+
current_section.append(line)
|
| 400 |
+
is_bullet_section = True # Mark the current section as potentially bulleted
|
| 401 |
+
else:
|
| 402 |
+
# Line is regular content, add it to the current section
|
| 403 |
+
current_section.append(line)
|
| 404 |
+
|
| 405 |
+
# Add the last section
|
| 406 |
+
add_section_to_story(current_section, is_bullet_section)
|
| 407 |
+
|
| 408 |
+
# Add footer or timestamp if desired
|
| 409 |
+
story.append(Spacer(1, 20))
|
| 410 |
+
footer_style = ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, alignment=1, textColor=colors.grey)
|
| 411 |
+
story.append(Paragraph(f"Generated by Patient Care Management System on {datetime.now().strftime('%Y-%m-%d')}", footer_style))
|
| 412 |
+
|
| 413 |
+
|
| 414 |
# Build the PDF
|
| 415 |
doc.build(story)
|
| 416 |
buffer.seek(0)
|
| 417 |
return buffer
|
| 418 |
|
| 419 |
|
| 420 |
+
def send_whatsapp_care_plan(patient_info, care_plan_text, status):
|
| 421 |
"""Send care plan via WhatsApp using Twilio with improved formatting"""
|
| 422 |
+
if not twilio_client:
|
| 423 |
+
print("Twilio client not initialized. Cannot send WhatsApp message.")
|
| 424 |
+
return False
|
| 425 |
+
|
| 426 |
try:
|
| 427 |
+
# Status emoji mapping
|
| 428 |
status_emoji = {
|
| 429 |
'emergency': "🚨 EMERGENCY",
|
| 430 |
'deteriorating': "⚠️ HIGH RISK",
|
| 431 |
'improving': "✅ IMPROVING",
|
| 432 |
'stable': "🟦 STABLE"
|
| 433 |
}
|
| 434 |
+
|
| 435 |
+
# Format message for WhatsApp including patient details and the full plan text
|
| 436 |
+
message = f"*Care Plan Update*\n\n"
|
| 437 |
+
message += f"*Patient Name:* {patient_info.get('name', 'N/A')}\n"
|
| 438 |
+
message += f"*Age:* {patient_info.get('age', 'N/A')}\n"
|
| 439 |
+
message += f"*Gender:* {patient_info.get('gender', 'N/A')}\n"
|
| 440 |
+
message += f"*Status:* {status_emoji.get(status, 'Unknown')}\n\n"
|
| 441 |
+
message += f"*Care Plan Details:*\n{care_plan_text.strip()}" # Include the full plan text
|
| 442 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
# Send WhatsApp message using hardcoded number
|
| 444 |
+
message = twilio_client.messages.create(
|
| 445 |
from_=TWILIO_FROM,
|
| 446 |
body=message,
|
| 447 |
to=TWILIO_TO
|
| 448 |
)
|
| 449 |
+
print(f"WhatsApp message sent, SID: {message.sid}")
|
| 450 |
return True
|
| 451 |
except Exception as e:
|
| 452 |
print(f"Error sending WhatsApp message: {e}")
|
| 453 |
+
# Twilio error handling can be more specific (e.g., check e.status)
|
| 454 |
return False
|
| 455 |
|
| 456 |
|
| 457 |
@app.route('/')
|
| 458 |
def index():
|
| 459 |
+
# Determine current role from session or default to patient
|
| 460 |
+
role = session.get('role', 'patient')
|
| 461 |
+
if role == 'doctor':
|
| 462 |
+
# If role is doctor, redirect to doctor dashboard
|
| 463 |
+
return redirect(url_for('doctor_dashboard'))
|
| 464 |
+
# Otherwise, render the patient index page
|
| 465 |
return render_template('index.html')
|
| 466 |
|
| 467 |
|
| 468 |
@app.route('/switch_role', methods=['POST'])
|
| 469 |
def switch_role():
|
| 470 |
role = request.form.get('role')
|
| 471 |
+
if role in ['patient', 'doctor']:
|
| 472 |
+
session['role'] = role
|
| 473 |
+
return jsonify({'success': True, 'role': role})
|
| 474 |
+
return jsonify({'success': False, 'error': 'Invalid role'}), 400
|
| 475 |
|
| 476 |
|
| 477 |
@app.route('/doctor_dashboard')
|
| 478 |
def doctor_dashboard():
|
| 479 |
+
# This route will render the dashboard template
|
| 480 |
+
# The patient list will be loaded via AJAX by the JavaScript
|
| 481 |
return render_template('doctor_dashboard.html')
|
| 482 |
|
| 483 |
|
| 484 |
+
# This route is not strictly needed anymore as patient details are loaded via AJAX on doctor_dashboard
|
| 485 |
+
# Keeping it for backward compatibility or direct linking if desired, but AJAX is preferred.
|
| 486 |
@app.route('/patient_details/<patient_id>')
|
| 487 |
def patient_details(patient_id):
|
| 488 |
+
print(f"Attempting to load patient details page for ID: {patient_id}")
|
| 489 |
+
print(f"Current patients_db keys: {patients_db.keys()}")
|
| 490 |
+
patient = patients_db.get(patient_id)
|
| 491 |
+
if not patient:
|
| 492 |
+
print(f"Patient ID {patient_id} not found in patients_db.")
|
| 493 |
+
# Redirect to dashboard, perhaps with an error message
|
| 494 |
+
return redirect(url_for('doctor_dashboard', error="Patient not found"))
|
| 495 |
+
# Render the dashboard template, passing the specific patient ID
|
| 496 |
+
# The JS on doctor_dashboard.html will detect this ID in the URL and load the details
|
| 497 |
+
return render_template('doctor_dashboard.html', selected_patient_id=patient_id)
|
| 498 |
|
| 499 |
|
| 500 |
@app.route('/submit_feedback', methods=['POST'])
|
| 501 |
def submit_feedback():
|
| 502 |
+
if not model:
|
| 503 |
+
return jsonify({'success': False, 'error': 'AI model not initialized. Please check API key.'}), 500
|
| 504 |
+
|
| 505 |
try:
|
| 506 |
+
# Get patient information from form
|
| 507 |
+
name = request.form.get('name', 'Unnamed Patient')
|
| 508 |
+
age = request.form.get('age', 'N/A')
|
| 509 |
+
gender = request.form.get('gender', 'N/A')
|
| 510 |
feedback = request.form.get('feedback', '')
|
| 511 |
+
|
| 512 |
care_plan_text = ""
|
| 513 |
care_plan_format = None
|
| 514 |
+
|
| 515 |
+
# Handle PDF file upload
|
| 516 |
if 'care_plan_pdf' in request.files:
|
| 517 |
pdf_file = request.files['care_plan_pdf']
|
| 518 |
+
if pdf_file and pdf_file.filename != '':
|
| 519 |
+
# Read the PDF content from the file stream
|
| 520 |
care_plan_text = extract_text_from_pdf(pdf_file)
|
| 521 |
+
if care_plan_text:
|
| 522 |
+
care_plan_format = extract_care_plan_format(care_plan_text)
|
| 523 |
+
print(f"Extracted text length: {len(care_plan_text)}. Format found: {care_plan_format is not None}")
|
| 524 |
+
|
| 525 |
# If no format is found in the PDF, use a default format
|
| 526 |
+
if not care_plan_format or not care_plan_format.strip():
|
| 527 |
+
print("Using default care plan format.")
|
| 528 |
care_plan_format = """
|
| 529 |
+
PATIENT INFORMATION:
|
| 530 |
+
- Name: [Patient Name]
|
| 531 |
+
- Age: [Age]
|
| 532 |
+
- Gender: [Gender]
|
| 533 |
+
|
| 534 |
ASSESSMENT:
|
| 535 |
+
- [Summary of patient's current condition based on feedback and previous plan]
|
| 536 |
+
|
| 537 |
DAILY CARE PLAN:
|
| 538 |
Morning:
|
| 539 |
+
- [Morning activities/medications]
|
| 540 |
Afternoon:
|
| 541 |
+
- [Afternoon activities/medications]
|
| 542 |
Evening:
|
| 543 |
+
- [Evening activities/medications]
|
| 544 |
+
Night:
|
| 545 |
+
- [Night activities/medications/sleep instructions]
|
| 546 |
+
|
| 547 |
MEDICATIONS:
|
| 548 |
+
- [List of medications, dosage, frequency, and time]
|
| 549 |
+
|
| 550 |
+
DIET AND HYDRATION:
|
| 551 |
+
- [Dietary recommendations and hydration goals]
|
| 552 |
+
|
| 553 |
+
PHYSICAL ACTIVITY/EXERCISE:
|
| 554 |
+
- [Recommended physical activities, duration, frequency]
|
| 555 |
+
|
| 556 |
+
SYMPTOM MANAGEMENT:
|
| 557 |
+
- [Instructions for managing specific symptoms, including what to do if they worsen]
|
| 558 |
+
|
| 559 |
+
RED FLAGS / WHEN TO SEEK HELP:
|
| 560 |
+
- [Clear instructions on symptoms requiring immediate medical attention (Emergency)]
|
| 561 |
+
- [Instructions on symptoms requiring contact with doctor/clinic (Urgent but not Emergency)]
|
| 562 |
+
|
| 563 |
ADDITIONAL RECOMMENDATIONS:
|
| 564 |
+
- [Other relevant advice, e.g., rest, monitoring, specific precautions]
|
| 565 |
+
|
| 566 |
FOLLOW-UP:
|
| 567 |
+
- [Details about next appointment or when to contact healthcare provider]
|
| 568 |
"""
|
| 569 |
+
else:
|
| 570 |
+
print("Using extracted care plan format.")
|
| 571 |
+
|
| 572 |
+
# Check for emergency symptoms FIRST in feedback
|
| 573 |
+
emergency_keywords_check = [
|
| 574 |
"severe chest pain", "heart attack", "shortness of breath",
|
| 575 |
"dizziness", "loss of consciousness", "extreme pain",
|
|
|
|
|
|
|
| 576 |
"sudden weakness", "confusion", "slurred speech", "severe headache",
|
| 577 |
+
"difficulty breathing", "severe shortness of breath", "wheezing",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
"severe abdominal pain", "persistent vomiting", "uncontrolled bleeding",
|
| 579 |
+
"severe allergic reaction", "anaphylaxis", "immediate medical attention",
|
| 580 |
+
"emergency", "call 911", "urgent care", "hospital", "critical", "ambulance",
|
| 581 |
+
"collapsed", "unconscious", "unresponsive", "stroke", "seizure", "convulsion",
|
| 582 |
+
"suffocating", "not breathing", "blue lips", "blue face", "cardiac arrest",
|
| 583 |
+
"high fever", "signs of shock", "severe dehydration", "acute change",
|
| 584 |
+
"unstable vitals", "rapidly worsening"
|
| 585 |
]
|
| 586 |
+
|
| 587 |
feedback_lower = feedback.lower()
|
| 588 |
+
is_emergency_feedback = any(keyword in feedback_lower for keyword in emergency_keywords_check)
|
| 589 |
+
|
| 590 |
+
generated_plan_text = ""
|
| 591 |
+
status = 'stable' # Default status
|
| 592 |
+
|
| 593 |
+
if is_emergency_feedback:
|
| 594 |
+
print("Emergency keywords detected in feedback.")
|
| 595 |
+
# If emergency symptoms are detected in feedback, generate emergency instructions
|
| 596 |
+
generated_plan_text = (
|
| 597 |
+
"PATIENT INFORMATION:\n"
|
| 598 |
+
f"- Name: {name}\n"
|
| 599 |
+
f"- Age: {age}\n"
|
| 600 |
+
f"- Gender: {gender}\n\n"
|
| 601 |
"ASSESSMENT:\n"
|
| 602 |
+
f"- Emergency symptoms reported: {feedback}. Immediate medical attention required.\n\n"
|
| 603 |
"EMERGENCY ACTION PLAN:\n"
|
| 604 |
+
"- Call emergency services immediately at 104/108/109/112 or your local emergency number.\n"
|
| 605 |
"- Do not delay seeking medical help.\n"
|
| 606 |
+
"- If conscious, try to remain calm.\n"
|
| 607 |
+
"- Do not eat or drink anything until evaluated by medical professionals.\n"
|
| 608 |
+
"- Follow instructions from emergency responders.\n\n"
|
| 609 |
+
"RED FLAGS / WHEN TO SEEK HELP:\n"
|
| 610 |
+
"- *Any* worsening of reported emergency symptoms requires urgent escalation.\n\n"
|
| 611 |
"FOLLOW-UP:\n"
|
| 612 |
+
"- Immediate hospitalization or urgent medical evaluation is necessary.\n"
|
| 613 |
+
"- Inform your primary physician as soon as medically stable.\n"
|
| 614 |
+
"- Review and update care plan only after emergency situation is resolved and evaluated by medical professionals.\n"
|
| 615 |
)
|
| 616 |
+
status = 'emergency' # Set status to emergency
|
| 617 |
+
|
| 618 |
+
else:
|
| 619 |
+
# Prepare a prompt for Gemini AI for a perfect, attractively formatted day care plan.
|
| 620 |
+
# Include patient info directly in the prompt for context
|
| 621 |
+
prompt = f"""
|
| 622 |
+
You are a helpful assistant generating updated patient care plans.
|
| 623 |
+
Based on the patient's personal information, their current symptoms/feedback, and their previous care plan, create a NEW, comprehensive, and well-structured daily care plan.
|
| 624 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
Patient Information:
|
| 626 |
Name: {name}
|
| 627 |
Age: {age}
|
| 628 |
Gender: {gender}
|
| 629 |
+
|
| 630 |
+
Patient Feedback/Symptoms Update:
|
| 631 |
+
{feedback}
|
| 632 |
+
|
| 633 |
+
Previous Care Plan Details (if available):
|
| 634 |
+
{care_plan_text if care_plan_text else "No previous care plan provided."}
|
| 635 |
+
|
| 636 |
+
Instructions:
|
| 637 |
+
1. Generate the updated care plan using the exact following format template. Fill in the details based on the patient's information, feedback, and integrate relevant elements from the previous plan if helpful.
|
| 638 |
+
2. Be specific and actionable in your recommendations.
|
| 639 |
+
3. Ensure the language is clear and easy to understand.
|
| 640 |
+
4. Include realistic times for medications if applicable (e.g., "Take 1 tablet in the morning with food").
|
| 641 |
+
5. Highlight important instructions or warnings, especially regarding symptom management and when to seek help.
|
| 642 |
+
6. Do NOT include any introductory or concluding sentences outside the plan format itself (like "Here is the updated plan:" or "Let me know if you need further assistance."). Provide ONLY the structured plan content.
|
| 643 |
+
|
| 644 |
+
Care Plan Format Template:
|
| 645 |
{care_plan_format}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
"""
|
| 647 |
+
print("Sending prompt to AI model...")
|
| 648 |
+
print(f"Prompt:\n---\n{prompt}\n---")
|
| 649 |
+
|
| 650 |
+
# Get response from Gemini AI
|
| 651 |
+
try:
|
| 652 |
+
response = model.generate_content(prompt)
|
| 653 |
+
generated_plan_text = response.text.strip() # Get the text and strip leading/trailing whitespace
|
| 654 |
+
print(f"AI Response received. Length: {len(generated_plan_text)}")
|
| 655 |
+
print(f"AI Response:\n---\n{generated_plan_text}\n---")
|
| 656 |
+
|
| 657 |
+
# Determine patient status based on AI's output and feedback
|
| 658 |
+
status = determine_patient_status(care_plan_text, generated_plan_text, feedback)
|
| 659 |
+
|
| 660 |
+
except Exception as ai_error:
|
| 661 |
+
print(f"Error generating content from AI: {ai_error}")
|
| 662 |
+
return jsonify({
|
| 663 |
+
'success': False,
|
| 664 |
+
'error': f'Error generating care plan: {ai_error}'
|
| 665 |
+
}), 500
|
| 666 |
+
|
| 667 |
+
# --- Actions after plan generation (Emergency or AI) ---
|
| 668 |
+
|
| 669 |
+
# Store patient record (in-memory DB)
|
| 670 |
patient_id = str(uuid.uuid4())
|
| 671 |
patients_db[patient_id] = {
|
| 672 |
'id': patient_id,
|
|
|
|
| 674 |
'age': age,
|
| 675 |
'gender': gender,
|
| 676 |
'feedback': feedback,
|
| 677 |
+
'original_plan': care_plan_text, # Store original text from PDF
|
| 678 |
+
'updated_plan': generated_plan_text, # Store the generated plan text
|
| 679 |
'status': status,
|
| 680 |
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 681 |
}
|
| 682 |
+
print(f"Patient {patient_id} added to in-memory DB with status: {status}. Current DB keys: {patients_db.keys()}")
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
# Generate PDF for downloading using the stored data
|
| 686 |
+
patient_info_for_pdf = {
|
| 687 |
+
'name': name,
|
| 688 |
+
'age': age,
|
| 689 |
+
'gender': gender
|
| 690 |
+
}
|
| 691 |
+
pdf_buffer = generate_care_plan_pdf(patient_info_for_pdf, generated_plan_text, status)
|
| 692 |
+
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode('utf-8')
|
| 693 |
+
print("PDF generated and base64 encoded.")
|
| 694 |
+
|
| 695 |
# Send care plan via WhatsApp
|
| 696 |
+
patient_info_for_whatsapp = {
|
| 697 |
+
'name': name,
|
| 698 |
+
'age': age,
|
| 699 |
+
'gender': gender
|
| 700 |
+
}
|
| 701 |
+
whatsapp_sent = send_whatsapp_care_plan(patient_info_for_whatsapp, generated_plan_text, status)
|
| 702 |
+
print(f"WhatsApp message attempt sent: {whatsapp_sent}")
|
| 703 |
+
|
| 704 |
return jsonify({
|
| 705 |
'success': True,
|
| 706 |
+
'updated_plan': generated_plan_text, # Send back the generated text
|
| 707 |
'pdf_data': pdf_base64,
|
| 708 |
+
'patient_id': patient_id, # Send the generated patient ID
|
| 709 |
'status': status,
|
| 710 |
'whatsapp_sent': whatsapp_sent
|
| 711 |
})
|
| 712 |
+
|
| 713 |
except Exception as e:
|
| 714 |
+
print(f"An unexpected error occurred: {str(e)}")
|
| 715 |
+
# Log the traceback for debugging
|
| 716 |
+
import traceback
|
| 717 |
+
traceback.print_exc()
|
| 718 |
return jsonify({
|
| 719 |
'success': False,
|
| 720 |
+
'error': f'An unexpected error occurred: {str(e)}'
|
| 721 |
+
}), 500
|
| 722 |
|
| 723 |
|
| 724 |
@app.route('/download_pdf/<patient_id>')
|
| 725 |
def download_pdf(patient_id):
|
| 726 |
+
print(f"Download requested for patient ID: {patient_id}")
|
| 727 |
+
print(f"Current patients_db keys: {patients_db.keys()}")
|
| 728 |
+
|
| 729 |
try:
|
| 730 |
+
patient = patients_db.get(patient_id)
|
| 731 |
+
|
| 732 |
+
if not patient:
|
| 733 |
+
print(f"Patient ID {patient_id} not found in patients_db for download.")
|
| 734 |
+
return "Patient data not found (it may have been cleared). Please regenerate the plan.", 404
|
| 735 |
+
|
| 736 |
+
# Use the patient's stored information to regenerate the PDF content
|
| 737 |
+
patient_info_for_pdf = {
|
| 738 |
+
'name': patient.get('name', 'N/A'),
|
| 739 |
+
'age': patient.get('age', 'N/A'),
|
| 740 |
+
'gender': patient.get('gender', 'N/A')
|
| 741 |
+
}
|
| 742 |
+
# Use the already determined status and updated plan text
|
| 743 |
+
pdf_buffer = generate_care_plan_pdf(
|
| 744 |
+
patient_info_for_pdf,
|
| 745 |
+
patient['updated_plan'],
|
| 746 |
+
patient['status']
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
pdf_buffer.seek(0) # Ensure buffer is at the start
|
| 750 |
+
|
| 751 |
+
# Sanitize patient name for filename
|
| 752 |
+
safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '', patient.get('name', 'patient')).lower()
|
| 753 |
+
download_name = f"care_plan_{safe_name}_{datetime.now().strftime('%Y%m%d')}.pdf"
|
| 754 |
+
|
| 755 |
+
print(f"Serving PDF for {patient_id} as {download_name}")
|
| 756 |
return send_file(
|
| 757 |
pdf_buffer,
|
| 758 |
as_attachment=True,
|
| 759 |
+
download_name=download_name,
|
| 760 |
mimetype='application/pdf'
|
| 761 |
)
|
| 762 |
except Exception as e:
|
| 763 |
+
print(f"PDF Download Error for ID {patient_id}: {str(e)}")
|
| 764 |
+
import traceback
|
| 765 |
+
traceback.print_exc()
|
| 766 |
return f"Error generating PDF: {str(e)}", 500
|
| 767 |
|
| 768 |
|
| 769 |
@app.route('/get_emergency_notifications')
|
| 770 |
def get_emergency_notifications():
|
| 771 |
# Filter patients with emergency status
|
| 772 |
+
emergency_patients = [p for p in patients_db.values() if p.get('status') == 'emergency']
|
| 773 |
+
|
| 774 |
+
# Sort by timestamp, newest first
|
| 775 |
+
emergency_patients.sort(key=lambda x: datetime.strptime(x['timestamp'], "%Y-%m-%d %H:%M:%S"), reverse=True)
|
| 776 |
+
|
| 777 |
notifications = []
|
| 778 |
for patient in emergency_patients:
|
| 779 |
notifications.append({
|
| 780 |
'patient_id': patient['id'],
|
| 781 |
+
'patient_name': patient.get('name', 'N/A'),
|
| 782 |
+
'patient_age': patient.get('age', 'N/A'),
|
| 783 |
+
'patient_gender': patient.get('gender', 'N/A'),
|
| 784 |
+
'patient_feedback': patient.get('feedback', 'No feedback recorded.'),
|
| 785 |
+
'timestamp': patient.get('timestamp', 'N/A'),
|
| 786 |
+
'status': patient.get('status', 'unknown')
|
| 787 |
})
|
| 788 |
+
|
| 789 |
return jsonify({
|
| 790 |
'success': True,
|
| 791 |
'notifications': notifications
|
|
|
|
| 795 |
@app.route('/get_patients')
|
| 796 |
def get_patients():
|
| 797 |
# Return all patients for the doctor dashboard
|
| 798 |
+
# Sort by timestamp, newest first by default, or add sorting logic here
|
| 799 |
+
sorted_patients = sorted(patients_db.values(),
|
| 800 |
+
key=lambda x: datetime.strptime(x.get('timestamp', '1900-01-01 00:00:00'), "%Y-%m-%d %H:%M:%S"),
|
| 801 |
+
reverse=True)
|
| 802 |
+
|
| 803 |
return jsonify({
|
| 804 |
'success': True,
|
| 805 |
+
'patients': list(sorted_patients)
|
| 806 |
})
|
| 807 |
|
| 808 |
|
| 809 |
@app.route('/get_patient/<patient_id>')
|
| 810 |
def get_patient(patient_id):
|
| 811 |
+
print(f"API request for patient ID: {patient_id}")
|
| 812 |
+
print(f"Current patients_db keys: {patients_db.keys()}")
|
| 813 |
+
|
| 814 |
+
patient = patients_db.get(patient_id)
|
| 815 |
+
|
| 816 |
+
if not patient:
|
| 817 |
+
print(f"Patient ID {patient_id} not found in patients_db for API request.")
|
| 818 |
+
return jsonify({'success': False, 'error': 'Patient not found or data cleared.'}), 404
|
| 819 |
+
|
| 820 |
+
print(f"Found patient {patient_id}: {patient.get('name')}")
|
| 821 |
return jsonify({
|
| 822 |
'success': True,
|
| 823 |
+
'patient': patient
|
| 824 |
})
|
| 825 |
|
| 826 |
|
| 827 |
if __name__ == '__main__':
|
| 828 |
+
# Use a more robust development server like Waitress or Gunicorn in production
|
| 829 |
+
app.run(debug=True, port=5000) # Explicitly set port
|