rajkhanke commited on
Commit
842e603
·
verified ·
1 Parent(s): 1993645

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +424 -231
app.py CHANGED
@@ -12,10 +12,18 @@ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
12
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, ListItem, ListFlowable
13
  from reportlab.lib.units import inch
14
  from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
 
 
 
 
 
15
 
16
  app = Flask(__name__)
17
 
18
- app.secret_key = '688ed745a74bdd7ac238f5b50f4104fb87d6774b8b0a4e06e7e18ac5ed0fa31c' # Add this for session management
 
 
 
19
  upload_base = os.getenv('UPLOAD_DIR', 'static')
20
  upload_folder = os.path.join(upload_base, 'uploads')
21
  pdf_folder = os.path.join(upload_base, 'pdfs')
@@ -26,7 +34,8 @@ os.makedirs(upload_folder, exist_ok=True)
26
  os.makedirs(pdf_folder, exist_ok=True)
27
 
28
  # Gemini API Configuration
29
- genai.configure(api_key="AIzaSyCizcswP6vlKDMdB3HRAtVi2JbifOpbPvA")
 
30
 
31
  generation_config = {
32
  "temperature": 1,
@@ -35,96 +44,155 @@ generation_config = {
35
  "max_output_tokens": 8192,
36
  }
37
 
38
- model = genai.GenerativeModel(
39
- model_name="gemini-2.0-flash",
40
- generation_config=generation_config,
41
- )
 
 
 
 
 
42
 
43
  # Twilio Configuration
44
- ACCOUNT_SID = 'AC490e071f8d01bf0df2f03d086c788d87'
45
- AUTH_TOKEN = '224b23b950ad5a4052aba15893fdf083'
46
- TWILIO_FROM = 'whatsapp:+14155238886'
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  def extract_text_from_pdf(pdf_file):
 
49
  try:
 
 
50
  pdf_reader = PyPDF2.PdfReader(pdf_file)
51
  text = ""
52
  for page in pdf_reader.pages:
53
- text += page.extract_text()
 
54
  return text.strip()
55
  except Exception as e:
56
- print(f"Error extracting PDF text: {e}")
57
  return ""
58
 
59
  def extract_care_plan_format(pdf_text):
60
- """Extract the care plan format from PDF text"""
 
 
 
 
 
61
  try:
62
- # Find sections and their content in the PDF using regex
 
 
63
  sections = re.findall(
64
- r'([A-Z][A-Z\s]+)[:|\n]((?:(?!\n[A-Z][A-Z\s]+[:|\n]).)*)',
65
  pdf_text,
66
- re.DOTALL
67
  )
68
 
 
69
  if sections:
70
- format_template = ""
71
- for section, content in sections:
72
- format_template += f"{section.strip()}:\n"
73
- # Extract bullet points if they exist and remove any asterisks
74
- bullets = re.findall(r'[-•*]\s*(.*?)(?=[-•*]|\n|$)', content)
75
- if bullets:
76
- for bullet in bullets:
77
- format_template += f"- {bullet.strip()}\n"
78
- format_template += "\n"
79
- return format_template
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  return None
81
  except Exception as e:
82
- print(f"Error extracting format: {e}")
83
  return None
84
 
85
  def create_care_plan_pdf(care_plan_text, patient_name="Patient"):
86
- """Generate a beautifully formatted PDF with the care plan"""
87
  buffer = io.BytesIO()
88
- doc = SimpleDocTemplate(buffer, pagesize=letter)
 
 
89
  styles = getSampleStyleSheet()
90
-
91
  # Create custom styles
92
  styles.add(ParagraphStyle(
93
  name='Title',
94
  parent=styles['Heading1'],
95
- fontSize=18,
 
96
  alignment=TA_CENTER,
97
- spaceAfter=20,
98
  textColor=colors.HexColor('#0d6efd')
99
  ))
100
-
101
  styles.add(ParagraphStyle(
102
  name='SectionHeading',
103
  parent=styles['Heading2'],
104
  fontSize=14,
105
- spaceAfter=6,
106
- spaceBefore=12,
107
- textColor=colors.HexColor('#0a58ca')
 
 
108
  ))
109
-
110
  styles.add(ParagraphStyle(
111
  name='BodyText',
112
  parent=styles['Normal'],
113
  fontSize=11,
 
114
  alignment=TA_JUSTIFY,
115
- spaceBefore=2,
116
- spaceAfter=2
117
  ))
118
-
119
- # Add logo path or use a placeholder
120
- # logo_path = os.path.join("static", "images", "logo.png")
121
-
 
 
 
 
 
 
 
 
 
122
  # Create story elements
123
  story = []
124
-
125
  # Add title
126
  timestamp = datetime.now().strftime("%B %d, %Y")
127
- title = Paragraph(f"UPDATED CARE PLAN<br/><br/>{timestamp}", styles["Title"])
 
128
  story.append(title)
129
  story.append(Spacer(1, 0.25*inch))
130
 
@@ -132,106 +200,149 @@ def create_care_plan_pdf(care_plan_text, patient_name="Patient"):
132
  patient_info = Paragraph(f"<b>Patient:</b> {patient_name}", styles["BodyText"])
133
  story.append(patient_info)
134
  story.append(Spacer(1, 0.15*inch))
135
-
136
  # Process care plan sections
137
- sections = care_plan_text.split("\n\n")
 
 
138
  for section in sections:
139
- if not section.strip():
 
140
  continue
141
-
142
- lines = section.strip().split("\n")
143
  if not lines:
144
  continue
145
-
146
- # Add section heading
147
  heading_line = lines[0].strip()
148
- if ":" in heading_line:
149
- heading = heading_line.split(":", 1)[0].strip()
150
- story.append(Paragraph(heading, styles["SectionHeading"]))
151
-
152
- # Process bullet points and content
153
- content_lines = lines[1:] if len(lines) > 1 else []
154
- bullet_items = []
155
- normal_text = ""
156
-
157
- for line in content_lines:
158
- line = line.strip()
159
- if line.startswith("-") or line.startswith("•"):
160
- bullet_text = line[1:].strip()
161
- bullet_items.append(ListItem(Paragraph(bullet_text, styles["BodyText"])))
162
- else:
163
- normal_text += line + " "
164
-
165
- # Add normal text if present
166
- if normal_text:
167
- story.append(Paragraph(normal_text, styles["BodyText"]))
168
-
169
- # Add bullet points if present
170
- if bullet_items:
171
- bullet_list = ListFlowable(
172
- bullet_items,
173
- bulletType='bullet',
174
- start=None,
175
- bulletFontName='Helvetica',
176
- bulletFontSize=11,
177
- leftIndent=20,
178
- bulletOffsetY=0
179
- )
180
- story.append(bullet_list)
181
-
182
- story.append(Spacer(1, 0.1*inch))
183
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  # Add footer
185
  footer_text = """This care plan has been automatically generated based on your feedback. Please consult with your healthcare provider before making any changes to your current treatment plan."""
186
  footer = Paragraph(f"<i>{footer_text}</i>", styles["BodyText"])
187
- story.append(Spacer(1, 0.25*inch))
188
  story.append(footer)
189
-
190
  # Build PDF
191
- doc.build(story)
192
- buffer.seek(0)
193
- return buffer
 
 
 
 
 
 
194
 
195
- def send_whatsapp_message(message, recipient, pdf_data=None, pdf_name=None):
196
  """
197
  Sends a WhatsApp message using Twilio.
198
- If pdf_data is provided, it attaches the PDF to the message.
 
 
 
 
 
199
  """
 
 
 
 
 
 
 
 
200
  try:
201
- client = Client(ACCOUNT_SID, AUTH_TOKEN)
202
-
203
  # Check if recipient needs WhatsApp prefix
204
  if not recipient.startswith('whatsapp:'):
205
  recipient = f'whatsapp:{recipient}'
206
-
 
207
  message_params = {
208
  'from_': TWILIO_FROM,
209
  'body': message,
210
  'to': recipient
211
  }
212
-
213
- # If we have a PDF to send, attach it
214
- if pdf_data and pdf_name:
215
- # Create a temporary file
216
- temp_pdf_path = os.path.join(app.config['PDF_FOLDER'], pdf_name)
217
- with open(temp_pdf_path, 'wb') as f:
218
- f.write(pdf_data.getvalue())
219
-
220
- # Add media URL (would be your app's URL in production)
221
- media_url = f'https://your-app-domain.com/static/pdfs/{pdf_name}'
222
-
223
- # In development, use a public URL service like ngrok
224
- # or just mention that we'd need a public URL in production
225
- # For now, we'll just send the message without the attachment
226
- message_params['body'] += "\n\nYour care plan PDF has been generated and would normally be attached here. Due to development environment limitations, please view it in the web interface."
227
-
228
- msg = client.messages.create(**message_params)
 
 
 
 
 
 
 
229
  return {
230
- 'success': True,
231
  'sid': msg.sid
232
  }
233
  except Exception as e:
234
- print(f"Error sending WhatsApp message: {e}")
235
  return {
236
  'success': False,
237
  'error': str(e)
@@ -239,195 +350,277 @@ def send_whatsapp_message(message, recipient, pdf_data=None, pdf_name=None):
239
 
240
  @app.route('/')
241
  def index():
242
- return render_template('index.html')
 
 
 
243
 
244
- @app.route('/switch_role', methods=['POST'])
245
- def switch_role():
246
- role = request.form.get('role')
247
- session['role'] = role
248
- return jsonify({'success': True, 'role': role})
249
 
250
  @app.route('/doctor_dashboard')
251
  def doctor_dashboard():
 
 
252
  if session.get('role') != 'doctor':
253
- return redirect('/')
 
254
  return render_template('doctor_dashboard.html')
255
 
 
 
 
 
 
 
 
 
 
 
256
  @app.route('/download_pdf/<filename>')
257
  def download_pdf(filename):
 
258
  try:
 
 
 
 
 
 
259
  return send_file(
260
- os.path.join(app.config['PDF_FOLDER'], filename),
261
  as_attachment=True,
262
- download_name=filename
 
263
  )
264
  except Exception as e:
265
- return jsonify({'success': False, 'error': str(e)})
 
266
 
267
  @app.route('/submit_feedback', methods=['POST'])
268
  def submit_feedback():
 
 
 
 
 
 
 
269
  try:
270
- feedback = request.form.get('feedback', '')
271
- patient_name = request.form.get('patient_name', 'Patient')
272
- patient_phone = request.form.get('patient_phone', '')
273
  care_plan_text = ""
274
  care_plan_format = None
 
 
 
275
 
276
  if 'care_plan_pdf' in request.files:
277
  pdf_file = request.files['care_plan_pdf']
278
- if pdf_file.filename != '':
 
279
  care_plan_text = extract_text_from_pdf(pdf_file)
280
  care_plan_format = extract_care_plan_format(care_plan_text)
 
 
 
 
281
 
282
- # If no format is found in the PDF, use a default format
283
  if not care_plan_format:
 
284
  care_plan_format = """
285
  ASSESSMENT:
286
- [Assessment details]
287
-
288
  DAILY CARE PLAN:
289
  Morning:
290
- - [Morning activities]
291
  Afternoon:
292
- - [Afternoon activities]
293
  Evening:
294
- - [Evening activities]
295
-
296
  MEDICATIONS:
297
- - [Medication details]
298
-
299
  ADDITIONAL RECOMMENDATIONS:
300
- - [Recommendations]
301
-
302
  FOLLOW-UP:
303
- [Follow-up details]
304
  """
305
 
306
  # Define emergency keywords to check for severe symptoms
307
  emergency_keywords = [
308
  # Cardiovascular
309
- "severe chest pain", "heart attack", "shortness of breath",
310
- "dizziness", "loss of consciousness", "extreme pain",
311
-
312
  # Neurological
313
- "sudden weakness", "confusion", "slurred speech", "severe headache",
314
-
315
  # Respiratory
316
  "difficulty breathing", "severe shortness of breath", "wheezing", "respiratory distress",
317
-
318
  # Gastrointestinal
319
- "severe abdominal pain", "persistent vomiting", "uncontrolled bleeding",
320
-
321
  # Others
322
- "severe allergic reaction", "anaphylaxis"
 
323
  ]
324
 
325
  feedback_lower = feedback.lower()
326
  is_emergency = any(keyword in feedback_lower for keyword in emergency_keywords)
327
 
 
 
 
 
 
328
  if is_emergency:
329
- # Store emergency notification
330
- emergency_data = {
331
- 'patient_name': patient_name,
332
- 'patient_phone': patient_phone,
333
- 'patient_feedback': feedback,
334
- 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
335
- 'status': 'urgent'
336
- }
337
-
338
- # In a real application, you would store this in a database
339
- # For now, we'll use a global variable (not recommended for production)
340
- if not hasattr(app, 'emergency_notifications'):
341
- app.emergency_notifications = []
342
- app.emergency_notifications.append(emergency_data)
343
-
344
- # If emergency symptoms are detected, instruct immediate emergency response
345
- emergency_message = (
346
- "Emergency symptoms detected. Please call emergency services immediately at 104/108/109/112."
347
  )
348
-
349
- # Send emergency message to patient if phone number is provided
 
350
  if patient_phone:
351
- send_whatsapp_message(
352
- message=emergency_message,
353
- recipient=patient_phone
354
  )
355
-
356
- return jsonify({
357
- 'success': True,
358
- 'is_emergency': True,
359
- 'updated_plan': emergency_message
360
- })
361
-
362
- # Prepare a prompt for Gemini AI for a perfect, attractively formatted day care plan.
363
- prompt = f"""
364
  Patient Care Plan Update Request:
365
  Patient Name: {patient_name}
366
  Current Symptoms and Feedback: {feedback}
367
- Current Care Plan (extracted from PDF): {care_plan_text}
368
- Based on the patient's feedback and current care plan, please provide an updated, perfect daily care plan in the exact following format:
 
 
 
369
  {care_plan_format}
370
- Please ensure the following:
371
- - The response is well-structured with clear section headings.
372
- - Use bullet points for list items without including any asterisk (*) symbols.
373
- - Format the text attractively and professionally.
374
- - Remove any extraneous symbols.
 
 
 
375
  """
376
- # Get response from Gemini AI
377
- response = model.generate_content(prompt)
378
- # Remove any asterisk symbols from the response text
379
- updated_plan = response.text.replace("*", "")
380
-
381
- # Generate PDF
382
- pdf_buffer = create_care_plan_pdf(updated_plan, patient_name)
383
-
384
- # Save the PDF to file system with a unique name
385
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
386
- safe_name = re.sub(r'[^a-zA-Z0-9]', '_', patient_name)
387
- pdf_filename = f"care_plan_{safe_name}_{timestamp}.pdf"
388
- pdf_path = os.path.join(app.config['PDF_FOLDER'], pdf_filename)
389
-
390
- with open(pdf_path, 'wb') as f:
391
- f.write(pdf_buffer.getvalue())
392
-
393
- # Reset the buffer pointer for potential reuse
394
- pdf_buffer.seek(0)
395
-
396
- # Send WhatsApp message with PDF if phone number is provided
397
- message_result = None
398
- if patient_phone:
399
- message_result = send_whatsapp_message(
400
- message=f"Hello {patient_name}, your updated care plan is ready. Please review it and contact your healthcare provider if you have any questions.",
401
- recipient=patient_phone,
402
- pdf_data=pdf_buffer,
403
- pdf_name=pdf_filename
404
- )
405
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  return jsonify({
407
  'success': True,
408
  'updated_plan': updated_plan,
409
- 'pdf_filename': pdf_filename,
410
- 'message_sent': message_result['success'] if message_result else False,
411
- 'is_emergency': False
 
412
  })
413
 
414
  except Exception as e:
415
  import traceback
416
  traceback.print_exc()
 
417
  return jsonify({
418
  'success': False,
419
- 'error': str(e)
420
- })
 
421
 
422
 
423
- @app.route('/get_emergency_notifications')
424
- def get_emergency_notifications():
 
425
  if session.get('role') != 'doctor':
426
- return jsonify({'success': False, 'error': 'Unauthorized'})
 
 
 
427
 
428
- notifications = getattr(app, 'emergency_notifications', [])
429
- return jsonify({'success': True, 'notifications': notifications})
430
 
431
 
432
  if __name__ == '__main__':
433
- app.run(debug=True)
 
 
 
12
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, ListItem, ListFlowable
13
  from reportlab.lib.units import inch
14
  from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
15
+ import logging # Import logging
16
+
17
+ # Configure logging
18
+ logging.basicConfig(level=logging.INFO)
19
+
20
 
21
  app = Flask(__name__)
22
 
23
+ # Use a more secure way to generate secret key in production
24
+ app.secret_key = os.getenv('SECRET_KEY', '688ed745a74bdd7ac238f5b50f4104fb87d6774b8b0a4e06e7e18ac5ed0fa31c') # Ensure this is strong and secret
25
+
26
+ # Directory configuration
27
  upload_base = os.getenv('UPLOAD_DIR', 'static')
28
  upload_folder = os.path.join(upload_base, 'uploads')
29
  pdf_folder = os.path.join(upload_base, 'pdfs')
 
34
  os.makedirs(pdf_folder, exist_ok=True)
35
 
36
  # Gemini API Configuration
37
+ # Replace with your actual API key, ideally from environment variables
38
+ genai.configure(api_key=os.getenv("GEMINI_API_KEY", "YOUR_GEMINI_API_KEY")) # Replace with your key or env var
39
 
40
  generation_config = {
41
  "temperature": 1,
 
44
  "max_output_tokens": 8192,
45
  }
46
 
47
+ try:
48
+ model = genai.GenerativeModel(
49
+ model_name="gemini-2.0-flash", # Using flash model
50
+ generation_config=generation_config,
51
+ )
52
+ logging.info("Gemini model initialized successfully.")
53
+ except Exception as e:
54
+ logging.error(f"Error initializing Gemini model: {e}")
55
+ model = None # Set model to None if initialization fails
56
 
57
  # Twilio Configuration
58
+ # Replace with your actual Twilio credentials, ideally from environment variables
59
+ ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID", "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") # Replace
60
+ AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN", "your_auth_token") # Replace
61
+ TWILIO_FROM = os.getenv("TWILIO_FROM_NUMBER", "whatsapp:+14155238886") # Replace
62
+
63
+ try:
64
+ twilio_client = Client(ACCOUNT_SID, AUTH_TOKEN)
65
+ logging.info("Twilio client initialized successfully.")
66
+ except Exception as e:
67
+ logging.error(f"Error initializing Twilio client: {e}")
68
+ twilio_client = None # Set client to None if initialization fails
69
+
70
+
71
+ # In-memory storage for update history (replace with database in production)
72
+ app.update_history = []
73
 
74
  def extract_text_from_pdf(pdf_file):
75
+ """Extract text from a PDF file object."""
76
  try:
77
+ # Ensure the file pointer is at the beginning
78
+ pdf_file.seek(0)
79
  pdf_reader = PyPDF2.PdfReader(pdf_file)
80
  text = ""
81
  for page in pdf_reader.pages:
82
+ text += page.extract_text() or "" # Handle pages with no extractable text
83
+ pdf_file.seek(0) # Reset pointer after reading
84
  return text.strip()
85
  except Exception as e:
86
+ logging.error(f"Error extracting PDF text: {e}")
87
  return ""
88
 
89
  def extract_care_plan_format(pdf_text):
90
+ """
91
+ Extract a potential care plan format from PDF text by identifying headings.
92
+ Returns a string representing the discovered structure.
93
+ """
94
+ if not pdf_text:
95
+ return None
96
  try:
97
+ # Look for lines that look like section headings (uppercase words potentially followed by :)
98
+ # And capture the content block following them
99
+ # This regex is an approximation and might need tuning based on actual PDF formats
100
  sections = re.findall(
101
+ r'^([A-Z\s]+(?:[:\n]|$))([\s\S]*?)(?=\n^[A-Z\s]+(?:[:\n]|$)|$)',
102
  pdf_text,
103
+ re.MULTILINE | re.DOTALL
104
  )
105
 
106
+ format_template = ""
107
  if sections:
108
+ for i, (section_heading_match, content) in enumerate(sections):
109
+ # Clean up heading - take the part before : or just the line if no :
110
+ heading = section_heading_match.strip()
111
+ if heading.endswith(':'):
112
+ heading = heading[:-1].strip()
113
+ elif heading.endswith('\n'):
114
+ heading = heading[:-1].strip()
115
+
116
+ if heading:
117
+ format_template += f"{heading.strip()}:\n"
118
+ # Attempt to find bullet points within the content
119
+ bullets = re.findall(r'^[-\*•]\s*(.*?)$', content, re.MULTILINE)
120
+ if bullets:
121
+ for bullet in bullets:
122
+ format_template += f"- {bullet.strip()}\n"
123
+ else:
124
+ # If no obvious bullets, just add a placeholder
125
+ format_template += "[Details]\n"
126
+ format_template += "\n"
127
+
128
+ logging.info("Extracted format from PDF.")
129
+ return format_template.strip() if format_template else None
130
+
131
+ logging.warning("No distinct sections found using regex for format extraction.")
132
  return None
133
  except Exception as e:
134
+ logging.error(f"Error extracting format: {e}")
135
  return None
136
 
137
  def create_care_plan_pdf(care_plan_text, patient_name="Patient"):
138
+ """Generate a beautifully formatted PDF with the care plan."""
139
  buffer = io.BytesIO()
140
+ doc = SimpleDocTemplate(buffer, pagesize=letter,
141
+ rightMargin=72, leftMargin=72,
142
+ topMargin=72, bottomMargin=18) # Adjusted margins
143
  styles = getSampleStyleSheet()
144
+
145
  # Create custom styles
146
  styles.add(ParagraphStyle(
147
  name='Title',
148
  parent=styles['Heading1'],
149
+ fontSize=20, # Slightly larger title
150
+ leading=24,
151
  alignment=TA_CENTER,
152
+ spaceAfter=24, # More space after title
153
  textColor=colors.HexColor('#0d6efd')
154
  ))
155
+
156
  styles.add(ParagraphStyle(
157
  name='SectionHeading',
158
  parent=styles['Heading2'],
159
  fontSize=14,
160
+ leading=16,
161
+ spaceAfter=8, # More space after section heading
162
+ spaceBefore=16, # More space before section heading
163
+ textColor=colors.HexColor('#0a58ca'),
164
+ bold=True # Make headings bold
165
  ))
166
+
167
  styles.add(ParagraphStyle(
168
  name='BodyText',
169
  parent=styles['Normal'],
170
  fontSize=11,
171
+ leading=14, # Line spacing
172
  alignment=TA_JUSTIFY,
173
+ spaceBefore=4,
174
+ spaceAfter=4
175
  ))
176
+
177
+ # Style for list items within the ListFlowable
178
+ styles.add(ParagraphStyle(
179
+ name='ListItemText',
180
+ parent=styles['BodyText'],
181
+ leftIndent=0, # Managed by ListFlowable
182
+ bulletIndent=0, # Managed by ListFlowable
183
+ spaceBefore=0,
184
+ spaceAfter=0,
185
+ leading=14 # Inherit leading
186
+ ))
187
+
188
+
189
  # Create story elements
190
  story = []
191
+
192
  # Add title
193
  timestamp = datetime.now().strftime("%B %d, %Y")
194
+ # Use paragraphs for multi-line elements in title
195
+ title = Paragraph("UPDATED CARE PLAN", styles["Title"])
196
  story.append(title)
197
  story.append(Spacer(1, 0.25*inch))
198
 
 
200
  patient_info = Paragraph(f"<b>Patient:</b> {patient_name}", styles["BodyText"])
201
  story.append(patient_info)
202
  story.append(Spacer(1, 0.15*inch))
203
+
204
  # Process care plan sections
205
+ # Splitting by double newline should generally work if the AI adheres to the format
206
+ sections = care_plan_text.strip().split("\n\n")
207
+
208
  for section in sections:
209
+ section = section.strip()
210
+ if not section:
211
  continue
212
+
213
+ lines = section.split("\n")
214
  if not lines:
215
  continue
216
+
217
+ # Assume the first line is the heading, potentially followed by a colon
218
  heading_line = lines[0].strip()
219
+ heading = heading_line.split(":", 1)[0].strip()
220
+
221
+ if heading: # Only add if a heading was found
222
+ story.append(Paragraph(heading, styles["SectionHeading"]))
223
+
224
+ content_lines = lines[1:] if len(lines) > 1 else []
225
+ bullet_items = []
226
+ normal_paragraphs = []
227
+ current_paragraph = ""
228
+
229
+ # Process content lines, distinguishing between bullets and normal text
230
+ for line in content_lines:
231
+ line = line.strip()
232
+ if not line:
233
+ continue # Skip empty lines within content
234
+
235
+ if line.startswith("-") or line.startswith("•"):
236
+ # If there was collected normal text, add it before the list
237
+ if current_paragraph:
238
+ normal_paragraphs.append(Paragraph(current_paragraph.strip(), styles["BodyText"]))
239
+ current_paragraph = ""
240
+ bullet_text = line[1:].strip()
241
+ # Use ListItem with Paragraph for better formatting control
242
+ bullet_items.append(ListItem(Paragraph(bullet_text, styles["ListItemText"]), leftIndent=20))
243
+ else:
244
+ # Collect normal text into a paragraph
245
+ current_paragraph += line + " "
246
+
247
+ # Add the last collected normal paragraph if it exists
248
+ if current_paragraph:
249
+ normal_paragraphs.append(Paragraph(current_paragraph.strip(), styles["BodyText"]))
250
+
251
+ # Add collected normal paragraphs
252
+ for para in normal_paragraphs:
253
+ story.append(para)
254
+
255
+ # Add bullet points if present
256
+ if bullet_items:
257
+ # ListFlowable handles the bullet rendering and indent
258
+ story.append(ListFlowable(
259
+ bullet_items,
260
+ bulletType='bullet',
261
+ start='disc', # Use disc for bullet point appearance
262
+ leftIndent=20, # Indent the entire list block
263
+ bulletAnchor='start'
264
+ ))
265
+
266
+ # Add spacing after the section content if there was content
267
+ if content_lines:
268
+ story.append(Spacer(1, 0.15*inch)) # Space after section content
269
+
270
  # Add footer
271
  footer_text = """This care plan has been automatically generated based on your feedback. Please consult with your healthcare provider before making any changes to your current treatment plan."""
272
  footer = Paragraph(f"<i>{footer_text}</i>", styles["BodyText"])
273
+ story.append(Spacer(1, 0.35*inch)) # More space before footer
274
  story.append(footer)
275
+
276
  # Build PDF
277
+ try:
278
+ doc.build(story)
279
+ buffer.seek(0)
280
+ logging.info("PDF generated successfully.")
281
+ return buffer
282
+ except Exception as e:
283
+ logging.error(f"Error building PDF: {e}")
284
+ return None
285
+
286
 
287
+ def send_whatsapp_message(message, recipient, pdf_data=None, pdf_filename=None):
288
  """
289
  Sends a WhatsApp message using Twilio.
290
+ If pdf_data is provided, it attempts to attach the PDF to the message.
291
+ NOTE: Attaching media requires the media to be accessible via a public URL.
292
+ In a local development environment, this will likely fail unless you use a service like ngrok
293
+ to expose your Flask app publicly and configure the URL correctly.
294
+ For this demo, the message will be sent, but the PDF attachment part might not work
295
+ unless the URL is valid.
296
  """
297
+ if not twilio_client:
298
+ logging.warning("Twilio client not initialized. Cannot send message.")
299
+ return {'success': False, 'error': 'Twilio client not configured.'}
300
+
301
+ if not recipient:
302
+ logging.warning("Recipient number is empty. Cannot send message.")
303
+ return {'success': False, 'error': 'Recipient number not provided.'}
304
+
305
  try:
 
 
306
  # Check if recipient needs WhatsApp prefix
307
  if not recipient.startswith('whatsapp:'):
308
  recipient = f'whatsapp:{recipient}'
309
+ logging.info(f"Formatted recipient number: {recipient}")
310
+
311
  message_params = {
312
  'from_': TWILIO_FROM,
313
  'body': message,
314
  'to': recipient
315
  }
316
+
317
+ # --- IMPORTANT NOTE ABOUT PDF ATTACHMENT ---
318
+ # To send a PDF via Twilio WhatsApp, the PDF must be hosted
319
+ # somewhere publicly accessible online, and you provide Twilio with that URL.
320
+ # Example: media_url = f'https://your-app-domain.com/static/pdfs/{pdf_filename}'
321
+ # In this local development setup, this URL won't be publicly accessible
322
+ # unless you use a service like ngrok.
323
+ # For this demo, we will skip adding the media_url directly,
324
+ # and instead just mention in the body that the PDF is available.
325
+ # If you set up public hosting (e.g., ngrok), you would add:
326
+ # if pdf_data and pdf_filename:
327
+ # # Assuming 'static/pdfs/' is served publicly at '/static/pdfs/'
328
+ # public_base_url = os.getenv("PUBLIC_APP_URL", "YOUR_PUBLIC_URL") # e.g. "https://abcdef123456.ngrok.io"
329
+ # if public_base_url != "YOUR_PUBLIC_URL":
330
+ # media_url = f'{public_base_url}/static/pdfs/{pdf_filename}'
331
+ # message_params['media_url'] = [media_url] # Twilio expects a list of URLs
332
+ # logging.info(f"Attempting to attach PDF from URL: {media_url}")
333
+ # else:
334
+ # logging.warning("PUBLIC_APP_URL not set. Cannot attach PDF to WhatsApp message.")
335
+ # message_params['body'] += "\n\nYour care plan PDF is available via the web link."
336
+
337
+
338
+ msg = twilio_client.messages.create(**message_params)
339
+ logging.info(f"WhatsApp message sent successfully to {recipient}. SID: {msg.sid}")
340
  return {
341
+ 'success': True,
342
  'sid': msg.sid
343
  }
344
  except Exception as e:
345
+ logging.error(f"Error sending WhatsApp message to {recipient}: {e}")
346
  return {
347
  'success': False,
348
  'error': str(e)
 
350
 
351
  @app.route('/')
352
  def index():
353
+ """Render the patient feedback page."""
354
+ # Default role to patient
355
+ if 'role' not in session:
356
+ session['role'] = 'patient'
357
 
358
+ # Redirect to doctor dashboard if role is doctor
359
+ if session.get('role') == 'doctor':
360
+ return redirect(url_for('doctor_dashboard'))
361
+
362
+ return render_template('index.html')
363
 
364
  @app.route('/doctor_dashboard')
365
  def doctor_dashboard():
366
+ """Render the doctor dashboard."""
367
+ # Redirect to patient page if role is not doctor
368
  if session.get('role') != 'doctor':
369
+ return redirect(url_for('index'))
370
+
371
  return render_template('doctor_dashboard.html')
372
 
373
+ @app.route('/switch_role', methods=['POST'])
374
+ def switch_role():
375
+ """Endpoint to switch user role."""
376
+ role = request.form.get('role')
377
+ if role in ['patient', 'doctor']:
378
+ session['role'] = role
379
+ logging.info(f"User role switched to: {role}")
380
+ return jsonify({'success': True, 'role': role})
381
+ return jsonify({'success': False, 'error': 'Invalid role'}), 400
382
+
383
  @app.route('/download_pdf/<filename>')
384
  def download_pdf(filename):
385
+ """Serve the generated PDF file for download."""
386
  try:
387
+ pdf_path = os.path.join(app.config['PDF_FOLDER'], filename)
388
+ if not os.path.exists(pdf_path):
389
+ logging.warning(f"Attempted download of non-existent file: {filename}")
390
+ return "File not found", 404
391
+
392
+ logging.info(f"Serving PDF for download: {filename}")
393
  return send_file(
394
+ pdf_path,
395
  as_attachment=True,
396
+ download_name=filename,
397
+ mimetype='application/pdf'
398
  )
399
  except Exception as e:
400
+ logging.error(f"Error serving PDF file {filename}: {e}")
401
+ return jsonify({'success': False, 'error': str(e)}), 500
402
 
403
  @app.route('/submit_feedback', methods=['POST'])
404
  def submit_feedback():
405
+ """Handles patient feedback submission, generates/updates care plan."""
406
+ if model is None:
407
+ return jsonify({'success': False, 'error': 'AI model is not initialized.'}), 500
408
+ if twilio_client is None:
409
+ logging.warning("Twilio client not initialized, WhatsApp messages will not be sent.")
410
+ # Allow processing to continue, but indicate failure later
411
+
412
  try:
413
+ patient_name = request.form.get('patient_name', 'Patient').strip()
414
+ patient_phone = request.form.get('patient_phone', '').strip()
415
+ feedback = request.form.get('feedback', '').strip()
416
  care_plan_text = ""
417
  care_plan_format = None
418
+ pdf_filename = None # Initialize PDF filename
419
+
420
+ logging.info(f"Received feedback for {patient_name}. Phone: {patient_phone}. Feedback: '{feedback[:100]}...'")
421
 
422
  if 'care_plan_pdf' in request.files:
423
  pdf_file = request.files['care_plan_pdf']
424
+ if pdf_file and pdf_file.filename != '':
425
+ logging.info(f"Processing uploaded PDF: {pdf_file.filename}")
426
  care_plan_text = extract_text_from_pdf(pdf_file)
427
  care_plan_format = extract_care_plan_format(care_plan_text)
428
+ logging.info(f"Extracted PDF text length: {len(care_plan_text)}")
429
+ if care_plan_format:
430
+ logging.info(f"Extracted format:\n{care_plan_format}")
431
+
432
 
433
+ # Use a default format if extraction failed or no PDF was uploaded
434
  if not care_plan_format:
435
+ logging.warning("No format extracted or PDF missing. Using default format.")
436
  care_plan_format = """
437
  ASSESSMENT:
438
+ - [Brief summary based on feedback]
 
439
  DAILY CARE PLAN:
440
  Morning:
441
+ - [Morning activities/meds]
442
  Afternoon:
443
+ - [Afternoon activities/meds]
444
  Evening:
445
+ - [Evening activities/meds]
 
446
  MEDICATIONS:
447
+ - [Review current list, note changes or adherence issues based on feedback]
 
448
  ADDITIONAL RECOMMENDATIONS:
449
+ - [Suggestions based on feedback, e.g., rest, hydration, specific exercises]
 
450
  FOLLOW-UP:
451
+ - [Suggested next steps, e.g., monitor symptoms, contact doctor if symptoms worsen]
452
  """
453
 
454
  # Define emergency keywords to check for severe symptoms
455
  emergency_keywords = [
456
  # Cardiovascular
457
+ "severe chest pain", "heart attack", "sudden shortness of breath",
458
+ "passing out", "loss of consciousness", "extreme pain", "uncontrolled bleeding",
 
459
  # Neurological
460
+ "sudden weakness", "confusion", "slurred speech", "severe headache", "stroke symptoms",
 
461
  # Respiratory
462
  "difficulty breathing", "severe shortness of breath", "wheezing", "respiratory distress",
 
463
  # Gastrointestinal
464
+ "severe abdominal pain", "persistent vomiting", "vomiting blood", "black stool",
 
465
  # Others
466
+ "severe allergic reaction", "anaphylaxis", "high fever that won't come down", "sudden swelling",
467
+ "can't feel", "paralysis"
468
  ]
469
 
470
  feedback_lower = feedback.lower()
471
  is_emergency = any(keyword in feedback_lower for keyword in emergency_keywords)
472
 
473
+ status = 'normal' # Default status
474
+ updated_plan = ""
475
+ message_sent_status = False
476
+ pdf_download_url = None # Initialize PDF URL
477
+
478
  if is_emergency:
479
+ status = 'urgent'
480
+ updated_plan = (
481
+ "Emergency symptoms detected. Please call emergency services immediately "
482
+ f"at 104/108/109/112. Do not wait for a care plan update."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  )
484
+ logging.warning(f"Emergency detected for {patient_name}. Feedback: '{feedback}'")
485
+
486
+ # Send immediate emergency message to patient if phone number is provided
487
  if patient_phone:
488
+ message_result = send_whatsapp_message(
489
+ message=updated_plan, # Send the emergency instruction message
490
+ recipient=patient_phone # Use the patient's phone number
491
  )
492
+ message_sent_status = message_result.get('success', False)
493
+ logging.info(f"Emergency WhatsApp message result: {message_result}")
494
+
495
+ else:
496
+ # Not an emergency, generate updated plan
497
+ prompt = f"""
 
 
 
498
  Patient Care Plan Update Request:
499
  Patient Name: {patient_name}
500
  Current Symptoms and Feedback: {feedback}
501
+ {f"Current Care Plan (extracted from previous PDF): {care_plan_text}" if care_plan_text else "No previous care plan provided."}
502
+
503
+ Based on the patient's feedback and, if provided, the current care plan, please provide an updated daily care plan.
504
+ Follow this format strictly where applicable, filling in or updating sections based *only* on the provided feedback. If a section is not relevant to the feedback, keep the placeholder or indicate it's unchanged.
505
+
506
  {care_plan_format}
507
+
508
+ Ensure the following:
509
+ - The response is structured exactly with the provided section headings ({', '.join([h.strip() for h in care_plan_format.strip().split(':\n') if h.strip()])}...).
510
+ - Use bullet points (-) for list items where the format shows them.
511
+ - Do not include any asterisk (*) or other symbols not in the format.
512
+ - Provide actionable advice based *only* on the feedback and general health principles (do not invent medical advice).
513
+ - If the feedback indicates symptoms that need professional medical attention but aren't strictly emergency keywords (e.g., persistent mild pain, concerning but non-severe symptoms), include a recommendation to consult their doctor.
514
+ - The tone should be professional and supportive.
515
  """
516
+ logging.info("Sending prompt to Gemini...")
517
+ logging.debug(f"Gemini Prompt:\n{prompt}")
518
+
519
+ try:
520
+ response = model.generate_content(prompt)
521
+ # Remove any asterisk symbols from the response text
522
+ updated_plan = response.text.replace("*", "").strip()
523
+ logging.info("Received response from Gemini.")
524
+ logging.debug(f"Gemini Response:\n{updated_plan}")
525
+
526
+ # Generate PDF for normal updates
527
+ pdf_buffer = create_care_plan_pdf(updated_plan, patient_name)
528
+
529
+ if pdf_buffer:
530
+ # Save the PDF to file system with a unique name
531
+ timestamp_str = datetime.now().strftime("%Y%m%d%H%M%S")
532
+ # Sanitize name for filename
533
+ safe_name = re.sub(r'[^\w\s-]', '', patient_name).strip().replace(' ', '_')
534
+ if not safe_name:
535
+ safe_name = "Patient"
536
+ pdf_filename = f"care_plan_{safe_name}_{timestamp_str}.pdf"
537
+ pdf_path = os.path.join(app.config['PDF_FOLDER'], pdf_filename)
538
+
539
+ with open(pdf_path, 'wb') as f:
540
+ f.write(pdf_buffer.getvalue())
541
+ logging.info(f"PDF saved: {pdf_path}")
542
+
543
+ # Construct download URL (relative path)
544
+ pdf_download_url = url_for('download_pdf', filename=pdf_filename)
545
+ logging.info(f"PDF download URL: {pdf_download_url}")
546
+
547
+ # Reset the buffer pointer for potential reuse (e.g., sending via WhatsApp)
548
+ pdf_buffer.seek(0)
549
+
550
+ # Send WhatsApp message with PDF link/info if phone number is provided
551
+ if patient_phone:
552
+ whatsapp_message_body = (
553
+ f"Hello {patient_name},\n\nYour updated care plan is ready based on your feedback. "
554
+ f"You can view or download it here: [Link Placeholder - See app for actual download] "
555
+ f"\n\nPlease review it and contact your healthcare provider if you have any questions or if your symptoms change."
556
+ )
557
+ # Note: The PDF is not actually attached via media_url in this local demo setup
558
+ # due to public URL requirements. The message body tells the user where to find it.
559
+ message_result = send_whatsapp_message(
560
+ message=whatsapp_message_body,
561
+ recipient=patient_phone
562
+ # media_url would go here if using a public URL service
563
+ )
564
+ message_sent_status = message_result.get('success', False)
565
+ logging.info(f"Normal WhatsApp message result: {message_result}")
566
+ else:
567
+ logging.error("PDF buffer is None after creation attempt.")
568
+ # Handle case where PDF creation failed
569
+ updated_plan += "\n\nNOTE: There was an error generating the PDF version of this plan."
570
+
571
+ except Exception as e:
572
+ logging.error(f"Error during Gemini interaction or PDF generation: {e}")
573
+ updated_plan = f"Error updating care plan based on feedback: {e}"
574
+ status = 'error' # Add an error status if AI/PDF fails
575
+
576
+ # Store the update history (in-memory list)
577
+ app.update_history.append({
578
+ 'patient_name': patient_name,
579
+ 'patient_phone': patient_phone,
580
+ 'feedback': feedback,
581
+ 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
582
+ 'status': status,
583
+ 'updated_plan_text': updated_plan if status == 'normal' else None, # Store text only for normal updates
584
+ 'pdf_filename': pdf_filename, # Store filename for download link
585
+ 'message_sent': message_sent_status,
586
+ 'raw_feedback': feedback # Store original feedback for doctor view
587
+ })
588
+ logging.info(f"Update history recorded. Total entries: {len(app.update_history)}")
589
+
590
+
591
  return jsonify({
592
  'success': True,
593
  'updated_plan': updated_plan,
594
+ 'status': status,
595
+ 'pdf_filename': pdf_filename, # Send filename back
596
+ 'pdf_download_url': pdf_download_url, # Send URL back
597
+ 'message_sent': message_sent_status,
598
  })
599
 
600
  except Exception as e:
601
  import traceback
602
  traceback.print_exc()
603
+ logging.error(f"Critical error in submit_feedback: {e}")
604
  return jsonify({
605
  'success': False,
606
+ 'error': str(e),
607
+ 'status': 'error'
608
+ }), 500
609
 
610
 
611
+ @app.route('/get_update_history')
612
+ def get_update_history():
613
+ """Endpoint for doctor dashboard to fetch update history."""
614
  if session.get('role') != 'doctor':
615
+ return jsonify({'success': False, 'error': 'Unauthorized'}), 403
616
+
617
+ # Return history in reverse chronological order
618
+ sorted_history = sorted(app.update_history, key=lambda x: x['timestamp'], reverse=True)
619
 
620
+ return jsonify({'success': True, 'history': sorted_history})
 
621
 
622
 
623
  if __name__ == '__main__':
624
+ # In production, use a production-ready server like Gunicorn or uWSGI
625
+ # and ensure secret key, API keys, and Twilio credentials are set as environment variables.
626
+ app.run(debug=True) # debug=True should NOT be used in production