lyimo commited on
Commit
fb83103
·
verified ·
1 Parent(s): d9a0a81

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +279 -311
app.py CHANGED
@@ -1,5 +1,4 @@
1
  import os
2
- # import base64 # No longer needed for Gemini vision part
3
  import gradio as gr
4
  import pandas as pd
5
  from groq import Groq
@@ -9,45 +8,48 @@ import datetime
9
  import re
10
  import google.generativeai as genai # Added for Gemini
11
  from google.generativeai import types # Added for Gemini
 
12
 
13
  # Initialize Groq client (for chat/assessment)
14
- groq_client = Groq(
15
- api_key=os.environ.get("GROQ_API_KEY")
16
- )
 
 
 
17
 
18
  # NOTE: Gemini client initialization will happen inside the analyze_ecg_image function
19
  # Ensure GEMINI_API_KEY is set in your environment variables
20
 
21
- # Function to encode images to base64 - REMOVED as not needed for Gemini bytes input
22
- # def encode_image(image):
23
- # buffered = io.BytesIO()
24
- # image.save(buffered, format="JPEG")
25
- # return base64.b64encode(buffered.getvalue()).decode('utf-8')
26
-
27
- # Process patient history file (Unchanged)
28
  def process_patient_history(file):
29
  if file is None:
30
  return ""
31
 
32
  try:
33
  # Check file extension
34
- file_ext = os.path.splitext(file.name)[1].lower()
 
35
 
36
  if file_ext == '.txt':
37
  # Read text file
38
- # Gradio File object might need .name for path, let's assume it provides a file-like object
39
- with open(file.name, 'r', encoding='utf-8') as f:
40
  content = f.read()
41
- # If file is already an IO object (depends on Gradio version/usage)
42
- # content = file.read().decode('utf-8')
43
  return content
44
 
45
  elif file_ext in ['.csv', '.xlsx', '.xls']:
46
  # Read spreadsheet file
47
  if file_ext == '.csv':
48
- df = pd.read_csv(file.name)
49
  else:
50
- df = pd.read_excel(file.name)
 
 
 
 
 
 
 
51
 
52
  # Convert dataframe to formatted string
53
  formatted_data = "PATIENT INFORMATION:\n\n"
@@ -56,16 +58,22 @@ def process_patient_history(file):
56
  for column in df.columns:
57
  # Handle potential missing values gracefully
58
  value = df.iloc[0].get(column, 'N/A')
59
- formatted_data += f"{column}: {value}\n"
 
60
  else:
61
  formatted_data += "Spreadsheet is empty or format is not recognized correctly."
62
 
63
  return formatted_data
64
 
65
  else:
66
- return "Unsupported file format. Please upload a .txt, .csv, or .xlsx file."
67
 
 
 
 
 
68
  except Exception as e:
 
69
  return f"Error processing patient history file: {str(e)}"
70
 
71
 
@@ -80,11 +88,22 @@ def analyze_ecg_image(image, vision_model="gemini-2.0-flash-exp"):
80
  # Ensure image is PIL Image
81
  if not isinstance(image, Image.Image):
82
  try:
83
- image = Image.open(image)
 
 
 
 
 
 
 
 
 
84
  except Exception as e:
 
85
  return f"<strong style='color:red'>Error opening image: {str(e)}</strong>"
86
 
87
  # --- Gemini Specific Part ---
 
88
  try:
89
  # Get Gemini API Key
90
  gemini_api_key = os.environ.get("GEMINI_API_KEY")
@@ -92,18 +111,22 @@ def analyze_ecg_image(image, vision_model="gemini-2.0-flash-exp"):
92
  return "<strong style='color:red'>GEMINI_API_KEY environment variable not set.</strong>"
93
 
94
  # Initialize Gemini client
95
- # Using configure for simplicity if preferred, or Client() directly
96
- # genai.configure(api_key=gemini_api_key)
97
- # model = genai.GenerativeModel(model_name=vision_model)
98
  gemini_client = genai.Client(api_key=gemini_api_key)
99
 
100
-
101
  # Convert PIL image to bytes (JPEG format)
102
  buffered = io.BytesIO()
103
  # Ensure image is in RGB format if it's RGBA or P which might cause issues
 
104
  if image.mode in ('RGBA', 'P'):
105
  image = image.convert('RGB')
106
- image.save(buffered, format="JPEG")
 
 
 
 
 
 
107
  image_bytes = buffered.getvalue()
108
 
109
  # Get current timestamp (computer time)
@@ -113,52 +136,61 @@ def analyze_ecg_image(image, vision_model="gemini-2.0-flash-exp"):
113
  vision_prompt = f"""Analyze this ECG image carefully. You are a cardiologist analyzing an electrocardiogram (ECG).
114
 
115
  Extract and report all visible parameters, including but not limited to:
116
- 1. Heart rate
117
- 2. PR interval
118
- 3. QRS duration
119
- 4. QT/QTc interval
120
- 5. P wave morphology
121
- 6. ST segment changes
122
- 7. T wave morphology
123
- 8. Rhythm classification
124
- 9. Specific patterns (if any)
 
125
 
126
- Report exact numerical values where visible. Format your response using HTML list elements for better readability.
127
 
128
- If certain measurements aren't visible in the image, indicate that they cannot be determined.
129
 
130
- If you notice any abnormalities or concerning patterns, highlight them clearly but avoid making definitive diagnoses.
131
 
132
- Format your response like this:
133
  <h3>ECG Report</h3>
134
  <ul>
135
- <li><strong>Date and Time:</strong> {timestamp}</li>
136
- <li><strong>Heart Rate:</strong> [value] bpm</li>
137
- <li><strong>PR Interval:</strong> [value] ms</li>
138
- <!-- Other measurements -->
 
 
 
 
 
 
 
139
  </ul>
140
 
141
- <h3>Additional Observations</h3>
142
  <ul>
143
- <li>Observation 1</li>
144
- <li>Observation 2</li>
 
145
  </ul>
146
 
147
- <h3>Conclusion</h3>
148
- <p>[Your conclusion text]</p>
149
 
150
  Important formatting instructions:
151
- - Use HTML elements for structure (<ul>, <li>, <strong>, <h3>, etc.)
152
- - Do not use asterisks (**) for emphasis - use proper HTML formatting instead
153
- - For any urgent findings, use <span style="color:red"> to highlight them
154
  """
155
 
156
  # Generate content using Gemini
157
  response = gemini_client.models.generate_content(
158
  model=vision_model,
159
  contents=[vision_prompt,
160
- types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg")]
161
- # Add safety_settings if needed:
162
  # safety_settings=[
163
  # {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
164
  # {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
@@ -169,174 +201,141 @@ def analyze_ecg_image(image, vision_model="gemini-2.0-flash-exp"):
169
 
170
  # Handle potential blocks or errors in response
171
  if not response.candidates:
172
- # Check finish_reason if available, e.g., safety settings block
173
- finish_reason = getattr(response, 'prompt_feedback', None)
174
- if finish_reason and getattr(finish_reason, 'block_reason', None):
175
- return f"<strong style='color:red'>Analysis blocked due to: {finish_reason.block_reason}. Check safety settings or content.</strong>"
176
  else:
177
- return "<strong style='color:red'>No content generated by the model. The request might have been blocked or failed.</strong>"
 
 
178
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- # Assuming the first candidate has the content
181
- ecg_analysis = response.text # Or response.candidates[0].content.parts[0].text
182
 
183
  # --- End of Gemini Specific Part ---
184
 
185
- # Process the response to convert any remaining ** to HTML tags
186
- ecg_analysis = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', ecg_analysis)
 
 
 
187
 
188
- # Make sure all headers are properly formatted (with complete pattern)
189
- ecg_analysis = re.sub(r'^(#+)\s+(.+)$', r'<h3>\2</h3>', ecg_analysis, flags=re.MULTILINE)
190
-
191
- # If there's no HTML formatting at all, wrap in basic structure
192
- if not re.search(r'<[^>]+>', ecg_analysis):
193
- lines = ecg_analysis.split('\n')
194
- formatted_html = "<h3>ECG Report</h3>\n<ul>\n"
195
-
196
- for line in lines:
197
- if line.strip():
198
- # Try to identify key-value pairs
199
- match = re.match(r'^([^:]+):\s*(.+)$', line.strip())
200
- if match:
201
- key, value = match.groups()
202
- formatted_html += f" <li><strong>{key.strip()}:</strong> {value.strip()}</li>\n"
203
- else:
204
- formatted_html += f" <li>{line.strip()}</li>\n"
205
-
206
- formatted_html += "</ul>"
207
- ecg_analysis = formatted_html
208
 
209
  return ecg_analysis
210
 
211
  except Exception as e:
212
  # Catch potential API errors or other issues
213
- import traceback
214
- print(traceback.format_exc()) # Print full traceback to console for debugging
215
- return f"<strong style='color:red'>Error analyzing ECG image with Gemini:</strong> {str(e)}"
 
 
 
216
 
217
 
218
- # Generate medical assessment based on ECG readings and patient history (Unchanged - uses Groq)
219
- def generate_assessment(ecg_analysis, patient_history=None, chat_model="llama-3.1-70b-versatile"): # Adjusted default model slightly if needed
220
- # Fixed model
221
- chat_model = "llama-3.1-70b-versatile" # Or keep llama-3.3 if available/preferred
 
 
 
 
222
 
223
  if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
224
- return "<strong style='color:red'>Please analyze an ECG image first.</strong>"
225
 
226
  # Get current timestamp
227
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
228
 
229
- # Construct prompt based on available information
230
- if patient_history and patient_history.strip():
231
- prompt = f"""You are a highly trained cardiologist assistant. Based on the ECG analysis below and the patient's history, provide a comprehensive assessment of the patient's cardiac status. Indicate clearly if there are any concerning findings that require immediate medical attention.
232
-
233
- ECG ANALYSIS:
234
- {ecg_analysis}
235
-
236
- PATIENT HISTORY:
237
- {patient_history}
238
-
239
- TIMESTAMP: {timestamp}
240
-
241
- Format your assessment using HTML elements for readability:
242
-
243
- <h3>Summary of Findings</h3>
244
- <ul>
245
- <li>Finding 1</li>
246
- <li>Finding 2</li>
247
- <!-- etc. -->
248
- </ul>
249
-
250
- <h3>Key Abnormalities</h3>
251
- <ul>
252
- <li>Abnormality 1</li>
253
- <li>Abnormality 2 (if any)</li>
254
- <!-- etc. -->
255
- </ul>
256
-
257
- <h3>Potential Clinical Implications</h3>
258
- <ul>
259
- <li>Implication 1</li>
260
- <li>Implication 2</li>
261
- <!-- etc. -->
262
- </ul>
263
 
264
- <h3>Recommendation</h3>
265
- <ul>
266
- <li>Recommendation 1 (include urgency level)</li>
267
- <li>Recommendation 2 (if any)</li>
268
- <!-- etc. -->
269
- </ul>
270
-
271
- <h3>Differential Considerations</h3>
272
- <ul>
273
- <li>Differential 1</li>
274
- <li>Differential 2</li>
275
- <!-- etc. -->
276
- </ul>
277
 
278
- Important formatting instructions:
279
- - Use HTML elements for structure (<ul>, <li>, <strong>, <h3>, etc.)
280
- - Do not use asterisks (**) for emphasis - use proper HTML formatting instead
281
- - For any urgent findings, use <span style="color:red"> to highlight them
282
- """
283
  else:
284
- prompt = f"""You are a highly trained cardiologist assistant. Based on the ECG analysis below, provide a comprehensive assessment of the patient's cardiac status. Indicate clearly if there are any concerning findings that require immediate medical attention.
285
 
286
- ECG ANALYSIS:
287
- {ecg_analysis}
288
 
289
- TIMESTAMP: {timestamp}
 
290
 
291
- Format your assessment using HTML elements for readability:
292
-
293
- <h3>Summary of Findings</h3>
294
  <ul>
295
- <li>Finding 1</li>
296
- <li>Finding 2</li>
297
  <!-- etc. -->
298
  </ul>
299
 
300
- <h3>Key Abnormalities</h3>
301
  <ul>
302
- <li>Abnormality 1</li>
303
- <li>Abnormality 2 (if any)</li>
304
- <!-- etc. -->
305
  </ul>
306
 
307
  <h3>Potential Clinical Implications</h3>
308
  <ul>
309
- <li>Implication 1</li>
310
- <li>Implication 2</li>
311
  <!-- etc. -->
312
  </ul>
313
 
314
- <h3>Recommendation</h3>
315
  <ul>
316
- <li>Recommendation 1 (include urgency level)</li>
317
- <li>Recommendation 2 (if any)</li>
318
  <!-- etc. -->
319
  </ul>
320
 
321
- <h3>Differential Considerations</h3>
322
  <ul>
323
- <li>Differential 1</li>
324
- <li>Differential 2</li>
325
  <!-- etc. -->
326
  </ul>
327
 
328
- Important formatting instructions:
329
- - Use HTML elements for structure (<ul>, <li>, <strong>, <h3>, etc.)
330
- - Do not use asterisks (**) for emphasis - use proper HTML formatting instead
331
- - For any urgent findings, use <span style="color:red"> to highlight them
332
- """
 
 
 
333
 
334
  try:
335
  assessment_completion = groq_client.chat.completions.create(
336
  messages=[
337
  {
338
  "role": "system",
339
- "content": "You are a medical AI assistant specialized in cardiology. Provide accurate, clinically relevant interpretations of ECG data. If there are concerning findings that might indicate a medical emergency, clearly highlight them. Avoid definitive diagnoses but provide reasoned medical assessments based on the data provided."
340
  },
341
  {
342
  "role": "user",
@@ -345,83 +344,47 @@ Important formatting instructions:
345
  ],
346
  model=chat_model,
347
  temperature=0.2,
348
- max_tokens=2048, # Note: Groq uses max_tokens
 
 
349
  )
350
 
351
  assessment_text = assessment_completion.choices[0].message.content
352
 
353
- # Process the response to convert any remaining ** to HTML tags
354
- assessment_text = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', assessment_text)
355
-
356
- # Make sure all headers are properly formatted (with complete pattern)
357
- assessment_text = re.sub(r'^(#+)\s+(.+)$', r'<h3>\2</h3>', assessment_text, flags=re.MULTILINE)
358
-
359
- # If there's no HTML formatting at all, wrap in basic structure (Improved fallback)
360
- if not re.search(r'<[^>]+>', assessment_text):
361
- sections = [
362
- "Summary of Findings",
363
- "Key Abnormalities",
364
- "Potential Clinical Implications",
365
- "Recommendation",
366
- "Differential Considerations"
367
- ]
368
- formatted_html = ""
369
- lines = assessment_text.split('\n')
370
- current_section_content = []
371
- current_section_title = ""
372
-
373
- def format_section(title, content_lines):
374
- html = f"<h3>{title}</h3>\n<ul>\n"
375
- for line in content_lines:
376
- if line.strip():
377
- html += f" <li>{line.strip()}</li>\n"
378
- html += "</ul>\n"
379
- return html
380
-
381
- for line in lines:
382
- line_stripped = line.strip()
383
- if not line_stripped:
384
- continue
385
-
386
- is_header = False
387
- for section_title in sections:
388
- # Check if line looks like a header (case-insensitive)
389
- if section_title.lower() in line_stripped.lower() and len(line_stripped) < len(section_title) + 10:
390
- if current_section_title and current_section_content:
391
- formatted_html += format_section(current_section_title, current_section_content)
392
- current_section_title = section_title # Use the canonical title
393
- current_section_content = []
394
- is_header = True
395
- break
396
-
397
- if not is_header:
398
- # If no section started yet, assume it's summary
399
- if not current_section_title:
400
- current_section_title = "Summary of Findings"
401
- current_section_content.append(line_stripped)
402
-
403
- # Add the last section
404
- if current_section_title and current_section_content:
405
- formatted_html += format_section(current_section_title, current_section_content)
406
-
407
- assessment_text = formatted_html if formatted_html else f"<p>{assessment_text.replace('\n', '<br>')}</p>" # Basic wrap if structure fails
408
 
409
  return assessment_text
410
 
411
  except Exception as e:
412
- import traceback
413
- print(traceback.format_exc()) # Print full traceback
414
- return f"<strong style='color:red'>Error generating assessment:</strong> {str(e)}"
 
415
 
 
 
 
 
 
 
 
416
 
417
- # Doctor's chat interaction with the model about the patient (Unchanged - uses Groq)
418
- def doctor_chat(message, chat_history, ecg_analysis, patient_history, assessment, chat_model="llama-3.1-70b-versatile"): # Adjusted default model slightly if needed
419
  # Fixed model
420
- chat_model = "llama-3.1-70b-versatile" # Or keep llama-3.3 if available/preferred
421
 
422
  # Check if ECG analysis exists and is not an error message
423
  if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
424
- # Prepend error message to history instead of returning it directly
425
  chat_history.append((message, "<strong style='color:red'>Cannot start chat. Please analyze a valid ECG image first.</strong>"))
426
  return "", chat_history # Clear input, update history
427
 
@@ -431,36 +394,42 @@ def doctor_chat(message, chat_history, ecg_analysis, patient_history, assessment
431
  # Get current timestamp
432
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
433
 
 
 
 
 
 
434
  # Prepare chat context
435
  context = f"""CURRENT TIMESTAMP: {timestamp}
436
 
437
- === BEGIN CONTEXT ===
438
  PATIENT HISTORY:
439
- {patient_history if patient_history and patient_history.strip() else "No patient history provided."}
440
 
441
- ECG ANALYSIS:
442
- {ecg_analysis}
443
 
444
- MEDICAL ASSESSMENT:
445
- {assessment if assessment and not assessment.startswith("<strong style='color:red'>") else "No assessment generated yet or assessment failed."}
446
- === END CONTEXT ===
447
 
448
- Based *only* on the context provided above, answer the doctor's questions concisely. If the information is not in the context, state that.
449
  """
450
 
451
  # Construct full chat history for context
452
  messages = [
453
  {
454
  "role": "system",
455
- "content": f"You are a medical AI assistant specialized in cardiology, conversing with a doctor. Your knowledge is strictly limited to the patient information provided in the context below. Do not invent information or access external knowledge.\n\n{context}"
456
  }
457
  ]
458
 
459
- # Add chat history to the context (limited to last 5 exchanges to avoid token limits)
460
- for user_msg, assistant_msg in chat_history[-5:]:
 
461
  messages.append({"role": "user", "content": user_msg})
462
  # Avoid adding error messages from assistant back into context
463
- if not assistant_msg.startswith("<strong style='color:red'>"):
464
  messages.append({"role": "assistant", "content": assistant_msg})
465
 
466
  # Add the current message
@@ -470,31 +439,30 @@ Based *only* on the context provided above, answer the doctor's questions concis
470
  chat_completion = groq_client.chat.completions.create(
471
  messages=messages,
472
  model=chat_model,
473
- temperature=0.3,
474
- max_tokens=1024, # Note: Groq uses max_tokens
475
  )
476
 
477
  response = chat_completion.choices[0].message.content
478
 
479
- # Process any remaining asterisks to HTML tags in the response
480
- response = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', response)
 
481
 
482
  chat_history.append((message, response))
483
  return "", chat_history # Clear input message box
484
  except Exception as e:
485
- import traceback
486
- print(traceback.format_exc()) # Print full traceback
487
- error_message = f"<strong style='color:red'>Error in chat:</strong> {str(e)}"
488
  chat_history.append((message, error_message))
489
  return "", chat_history # Clear input message box
490
 
491
  # Create Gradio interface
492
  with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as app:
493
- # Session state to store data (Removed - use outputs directly or manage state differently if needed)
494
- # ecg_analysis_state = gr.State("") # Not strictly necessary if passing outputs directly
495
 
496
  gr.Markdown("# 🫀 Cardiac ECG Analysis System")
497
- gr.Markdown("Upload an ECG image and optional patient history to get an automated analysis and assessment.")
498
 
499
  with gr.Tabs():
500
  with gr.TabItem("💻 Main Interface"):
@@ -502,10 +470,9 @@ with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as a
502
  with gr.Column(scale=1):
503
  # Input components
504
  with gr.Group():
505
- gr.Markdown("### 📊 ECG Image")
506
- ecg_image = gr.Image(type="pil", label="Upload ECG Image")
507
- # Display UPDATED fixed model info for vision
508
- gr.Markdown("**Vision Model:** gemini-2.0-flash-exp")
509
  analyze_button = gr.Button("Analyze ECG Image", variant="primary")
510
 
511
  with gr.Group():
@@ -513,19 +480,18 @@ with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as a
513
  patient_history_text = gr.Textbox(
514
  lines=8,
515
  label="Patient History (Manual Entry or Loaded from File)",
516
- placeholder="Enter patient's medical history, age, sex, symptoms, medications, etc. OR upload a file below and click Load."
517
  )
518
  patient_history_file = gr.File(
519
- label="Upload Patient History File (Optional, .txt, .csv, or .xlsx)",
520
  file_types=[".txt", ".csv", ".xlsx", ".xls"]
521
  )
522
  load_history_button = gr.Button("Load Patient History from File")
523
 
524
  with gr.Group():
525
- gr.Markdown("### 🧠 Assessment Settings")
526
- # Display fixed model info for chat/assessment
527
- # Make sure this model name matches the one used in generate_assessment/doctor_chat
528
- gr.Markdown("**Chat/Assessment Model:** llama-3.1-70b-versatile")
529
  assess_button = gr.Button("Generate Assessment", variant="primary")
530
 
531
  with gr.Column(scale=1):
@@ -538,91 +504,93 @@ with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as a
538
  gr.Markdown("### 📝 Medical Assessment")
539
  assessment_output = gr.HTML(label="Assessment", elem_id="assessment-output")
540
 
541
- gr.Markdown("## 👨‍⚕️ Doctor's Consultation")
542
- gr.Markdown("Ask follow-up questions about the patient's ECG results and medical condition.")
 
543
 
544
  with gr.Group():
545
- chatbot = gr.Chatbot(label="Consultation", height=400, bubble_full_width=False)
 
 
 
 
 
546
  with gr.Row():
547
  message = gr.Textbox(
548
- lines=2,
549
- label="Doctor's Question",
550
- placeholder="Ask a question about this patient's cardiac status...",
551
  scale=4,
552
- show_label=False,
553
- container=False, # Makes textbox slimmer vertically
554
  )
555
  chat_button = gr.Button("Send", scale=1, variant="primary")
556
 
557
- with gr.TabItem("ℹ️ Instructions"):
558
  gr.Markdown("""
559
  ## How to Use This Application
560
 
561
- ### Step 1: Upload and Analyze ECG
562
- 1. Upload an ECG image using the file uploader in the "Main Interface" tab.
563
- 2. Click **Analyze ECG Image**. The system will use Gemini Vision to interpret the image.
564
- 3. Wait for the "ECG Analysis Results" to appear.
565
-
566
- ### Step 2: Add Patient Information (Optional)
567
- * Enter patient history directly into the "Patient History" text box, OR
568
- * Upload a patient history file (.txt, .csv, or .xlsx) and click **Load Patient History from File**. The content will populate the text box.
569
-
570
- ### Step 3: Generate Assessment
571
- * Once the ECG analysis is complete, click **Generate Assessment**. The system will use a Llama model (via Groq) combining the ECG analysis and patient history (if provided).
572
- * Wait for the "Medical Assessment" to appear.
573
-
574
- ### Step 4: Consultation
575
- * Use the chat interface at the bottom to ask follow-up questions.
576
- * Type your question and click **Send** or press Enter.
577
- * The AI (Llama via Groq) will consider the ECG analysis, patient history, and the generated assessment in its responses.
578
-
579
- ### Important Notes
580
- * **API Keys:** Ensure `GEMINI_API_KEY` and `GROQ_API_KEY` environment variables are set correctly before running the application.
581
- * **Purpose:** This tool is designed to assist healthcare professionals and is NOT a substitute for professional clinical judgment or diagnosis.
582
- * **Validation:** Always validate AI-generated medical interpretations with qualified medical expertise.
583
- * **Privacy:** Ensure compliance with patient data privacy regulations (e.g., HIPAA) when using this tool. Do not upload identifiable patient information if not permitted.
584
  """)
585
 
586
  # --- Event Handlers ---
587
 
588
- # Chain analysis button click to update output
589
  analyze_button.click(
590
- analyze_ecg_image,
591
  inputs=[ecg_image],
592
  outputs=ecg_analysis_output
593
  )
594
 
595
- # Load history file content into the textbox
596
  load_history_button.click(
597
- process_patient_history,
598
  inputs=[patient_history_file],
599
  outputs=[patient_history_text]
600
  )
601
 
602
- # Generate assessment based on ECG output and history textbox
603
  assess_button.click(
604
- generate_assessment,
605
  inputs=[ecg_analysis_output, patient_history_text],
606
  outputs=assessment_output
607
  )
608
 
609
- # Handle chat interactions
610
  chat_button.click(
611
- doctor_chat,
612
  inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
613
- outputs=[message, chatbot] # Clear message input, update chatbot history
614
  )
615
 
616
- # Also trigger chat on Enter key press in the message textbox
617
  message.submit(
618
- doctor_chat,
619
  inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
620
- outputs=[message, chatbot] # Clear message input, update chatbot history
621
  )
622
 
623
 
624
  # Launch the app
625
  if __name__ == "__main__":
626
- # For development, you might want debug=True
627
- # app.launch(debug=True)
628
- app.launch()
 
 
 
 
1
  import os
 
2
  import gradio as gr
3
  import pandas as pd
4
  from groq import Groq
 
8
  import re
9
  import google.generativeai as genai # Added for Gemini
10
  from google.generativeai import types # Added for Gemini
11
+ import traceback # For detailed error logging
12
 
13
  # Initialize Groq client (for chat/assessment)
14
+ # Ensure GROQ_API_KEY is set in your environment variables
15
+ try:
16
+ groq_client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
17
+ except Exception as e:
18
+ print(f"Error initializing Groq client: {e}")
19
+ groq_client = None # Set to None to handle initialization errors gracefully
20
 
21
  # NOTE: Gemini client initialization will happen inside the analyze_ecg_image function
22
  # Ensure GEMINI_API_KEY is set in your environment variables
23
 
24
+ # Process patient history file
 
 
 
 
 
 
25
  def process_patient_history(file):
26
  if file is None:
27
  return ""
28
 
29
  try:
30
  # Check file extension
31
+ file_path = file.name # Gradio File object usually has a .name attribute with the path
32
+ file_ext = os.path.splitext(file_path)[1].lower()
33
 
34
  if file_ext == '.txt':
35
  # Read text file
36
+ with open(file_path, 'r', encoding='utf-8') as f:
 
37
  content = f.read()
 
 
38
  return content
39
 
40
  elif file_ext in ['.csv', '.xlsx', '.xls']:
41
  # Read spreadsheet file
42
  if file_ext == '.csv':
43
+ df = pd.read_csv(file_path)
44
  else:
45
+ # Specify engine for newer xlsx files if needed
46
+ try:
47
+ df = pd.read_excel(file_path)
48
+ except ImportError:
49
+ return "Error: `openpyxl` needed for .xlsx files. Install with `pip install openpyxl`"
50
+ except Exception as e_excel:
51
+ return f"Error reading Excel file: {e_excel}"
52
+
53
 
54
  # Convert dataframe to formatted string
55
  formatted_data = "PATIENT INFORMATION:\n\n"
 
58
  for column in df.columns:
59
  # Handle potential missing values gracefully
60
  value = df.iloc[0].get(column, 'N/A')
61
+ # Convert value to string to avoid potential type issues
62
+ formatted_data += f"{column}: {str(value)}\n"
63
  else:
64
  formatted_data += "Spreadsheet is empty or format is not recognized correctly."
65
 
66
  return formatted_data
67
 
68
  else:
69
+ return f"Unsupported file format ({file_ext}). Please upload a .txt, .csv, or .xlsx file."
70
 
71
+ except AttributeError:
72
+ return "Error: Could not get file path from Gradio File object. Ensure a file was uploaded."
73
+ except FileNotFoundError:
74
+ return f"Error: File not found at path: {file_path}"
75
  except Exception as e:
76
+ print(f"Error processing patient history file:\n{traceback.format_exc()}")
77
  return f"Error processing patient history file: {str(e)}"
78
 
79
 
 
88
  # Ensure image is PIL Image
89
  if not isinstance(image, Image.Image):
90
  try:
91
+ # If 'image' is a path (from older Gradio versions or specific setups)
92
+ if isinstance(image, str) and os.path.exists(image):
93
+ image = Image.open(image)
94
+ # If 'image' is file-like object from Gradio upload
95
+ elif hasattr(image, 'name'):
96
+ image = Image.open(image.name)
97
+ else:
98
+ # Assume it might be bytes or needs loading differently
99
+ # This part might need adjustment depending on how Gradio passes the image
100
+ return f"<strong style='color:red'>Unrecognized image input format: {type(image)}</strong>"
101
  except Exception as e:
102
+ print(f"Error opening image:\n{traceback.format_exc()}")
103
  return f"<strong style='color:red'>Error opening image: {str(e)}</strong>"
104
 
105
  # --- Gemini Specific Part ---
106
+ gemini_client = None # Initialize to None
107
  try:
108
  # Get Gemini API Key
109
  gemini_api_key = os.environ.get("GEMINI_API_KEY")
 
111
  return "<strong style='color:red'>GEMINI_API_KEY environment variable not set.</strong>"
112
 
113
  # Initialize Gemini client
114
+ # Use Client() for explicit initialization per request (safer for concurrent use)
 
 
115
  gemini_client = genai.Client(api_key=gemini_api_key)
116
 
 
117
  # Convert PIL image to bytes (JPEG format)
118
  buffered = io.BytesIO()
119
  # Ensure image is in RGB format if it's RGBA or P which might cause issues
120
+ img_format = "JPEG"
121
  if image.mode in ('RGBA', 'P'):
122
  image = image.convert('RGB')
123
+ # Handle potential transparency issues for PNG -> JPEG conversion
124
+ elif image.mode == 'LA':
125
+ image = image.convert('RGB') # Convert Luminance Alpha to RGB
126
+ # Check format if needed
127
+ # if image.format == 'PNG': img_format = 'PNG' # Gemini might prefer JPEG? Test this.
128
+
129
+ image.save(buffered, format=img_format)
130
  image_bytes = buffered.getvalue()
131
 
132
  # Get current timestamp (computer time)
 
136
  vision_prompt = f"""Analyze this ECG image carefully. You are a cardiologist analyzing an electrocardiogram (ECG).
137
 
138
  Extract and report all visible parameters, including but not limited to:
139
+ 1. Heart rate (bpm)
140
+ 2. PR interval (ms)
141
+ 3. QRS duration (ms)
142
+ 4. QT/QTc interval (ms)
143
+ 5. P wave morphology (e.g., upright in lead II, biphasic, absent)
144
+ 6. ST segment changes (e.g., elevation, depression, location)
145
+ 7. T wave morphology (e.g., upright, inverted, peaked, flattened)
146
+ 8. Rhythm classification (e.g., Sinus Rhythm, Atrial Fibrillation, etc.)
147
+ 9. Axis deviation (if determinable)
148
+ 10. Specific patterns (e.g., Bundle Branch Block, LVH criteria, WPW)
149
 
150
+ Report exact numerical values where visible. If a range is shown, report the range. Format your response using HTML list elements for better readability.
151
 
152
+ If certain measurements aren't clearly visible or determinable from the provided image quality, explicitly state that they cannot be determined.
153
 
154
+ If you notice any abnormalities or concerning patterns, highlight them clearly but avoid making definitive diagnoses. State observations neutrally.
155
 
156
+ Format your response strictly like this example:
157
  <h3>ECG Report</h3>
158
  <ul>
159
+ <li><strong>Analysis Time:</strong> {timestamp}</li>
160
+ <li><strong>Heart Rate:</strong> [value] bpm (or 'Not determinable')</li>
161
+ <li><strong>Rhythm:</strong> [description] (or 'Not determinable')</li>
162
+ <li><strong>PR Interval:</strong> [value] ms (or 'Not determinable')</li>
163
+ <li><strong>QRS Duration:</strong> [value] ms (or 'Not determinable')</li>
164
+ <li><strong>QT/QTc Interval:</strong> [value]/[value] ms (or 'Not determinable')</li>
165
+ <li><strong>Axis:</strong> [description] (or 'Not determinable')</li>
166
+ <li><strong>P Waves:</strong> [description]</li>
167
+ <li><strong>ST Segment:</strong> [description]</li>
168
+ <li><strong>T Waves:</strong> [description]</li>
169
+ <!-- Add other relevant findings as list items -->
170
  </ul>
171
 
172
+ <h3>Key Observations / Potential Abnormalities</h3>
173
  <ul>
174
+ <li>[Observation 1, e.g., Possible ST elevation in leads V1-V3]</li>
175
+ <li>[Observation 2, e.g., Inverted T waves in lead III]</li>
176
+ <!-- List significant observations, use <span style="color:red"> for critical findings -->
177
  </ul>
178
 
179
+ <h3>Impression</h3>
180
+ <p>[Provide a brief summary impression based *only* on the visible findings, e.g., "Sinus tachycardia with possible anterior ST changes." Avoid definitive diagnosis.]</p>
181
 
182
  Important formatting instructions:
183
+ - Use EXACTLY the HTML structure shown (<h3>, <ul>, <li>, <strong>, <p>).
184
+ - Do NOT use markdown like asterisks (**) or hashtags (#). Use HTML tags for formatting.
185
+ - For potentially urgent findings in the 'Key Observations' list, wrap the description in <span style="color:red">text</span>.
186
  """
187
 
188
  # Generate content using Gemini
189
  response = gemini_client.models.generate_content(
190
  model=vision_model,
191
  contents=[vision_prompt,
192
+ types.Part.from_bytes(data=image_bytes, mime_type=f"image/{img_format.lower()}")]
193
+ # Add safety_settings if needed (BLOCK_NONE allows more content but use carefully):
194
  # safety_settings=[
195
  # {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
196
  # {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
 
201
 
202
  # Handle potential blocks or errors in response
203
  if not response.candidates:
204
+ feedback = getattr(response, 'prompt_feedback', None)
205
+ block_reason = getattr(feedback, 'block_reason', None) if feedback else None
206
+ if block_reason:
207
+ return f"<strong style='color:red'>Analysis blocked by safety filter: {block_reason}. Consider adjusting safety settings or image content if appropriate.</strong>"
208
  else:
209
+ # Log the full response for debugging if possible
210
+ print(f"Gemini Response Error: No candidates returned. Full response: {response}")
211
+ return "<strong style='color:red'>No content generated by the model. The request might have been blocked, timed out, or failed unexpectedly. Check logs.</strong>"
212
 
213
+ # Accessing response text - check structure based on library version
214
+ try:
215
+ # Preferred way for recent versions
216
+ ecg_analysis = response.text
217
+ except ValueError:
218
+ # Fallback for potential older structures or errors
219
+ if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
220
+ ecg_analysis = "".join(part.text for part in response.candidates[0].content.parts if hasattr(part, 'text'))
221
+ else:
222
+ print(f"Gemini Response Error: Could not extract text. Response structure: {response}")
223
+ return "<strong style='color:red'>Error processing model response. Unexpected format. Check logs.</strong>"
224
 
 
 
225
 
226
  # --- End of Gemini Specific Part ---
227
 
228
+ # Basic post-processing (Gemini should follow HTML instructions, but just in case)
229
+ # Remove potential markdown that might slip through
230
+ ecg_analysis = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', ecg_analysis)
231
+ ecg_analysis = re.sub(r'^\s*#+\s+(.*?)\s*$', r'<h3>\1</h3>', ecg_analysis, flags=re.MULTILINE)
232
+ ecg_analysis = re.sub(r'^\s*[\*-]\s+(.*?)\s*$', r'<li>\1</li>', ecg_analysis, flags=re.MULTILINE) # Convert markdown lists
233
 
234
+ # Simple check if the response looks somewhat like the requested HTML structure
235
+ if not ("<h3>" in ecg_analysis and "<ul>" in ecg_analysis):
236
+ print(f"Warning: Gemini response might not be in the expected HTML format:\n{ecg_analysis[:500]}...")
237
+ # Optionally wrap in basic tags if completely unformatted
238
+ # ecg_analysis = f"<h3>ECG Analysis (Raw Output)</h3><p>{ecg_analysis.replace('\n', '<br>')}</p>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  return ecg_analysis
241
 
242
  except Exception as e:
243
  # Catch potential API errors or other issues
244
+ print(f"Error during Gemini ECG analysis:\n{traceback.format_exc()}")
245
+ error_type = type(e).__name__
246
+ return f"<strong style='color:red'>Error analyzing ECG image with Gemini ({error_type}):</strong> {str(e)}"
247
+ finally:
248
+ # Clean up client resource if necessary, though Client() might not require explicit closing
249
+ pass
250
 
251
 
252
+ # Generate medical assessment based on ECG readings and patient history (Uses Groq)
253
+ def generate_assessment(ecg_analysis, patient_history=None, chat_model="llama3-70b-8192"): # Using a common Groq model
254
+ # Check Groq client initialization
255
+ if groq_client is None:
256
+ return "<strong style='color:red'>Groq client not initialized. Check API Key and startup logs.</strong>"
257
+
258
+ # Use a known available and capable Groq model
259
+ chat_model = "llama3-70b-8192" # Or "mixtral-8x7b-32768" or another available one
260
 
261
  if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
262
+ return "<strong style='color:red'>Cannot generate assessment. Please analyze a valid ECG image first.</strong>"
263
 
264
  # Get current timestamp
265
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
266
 
267
+ # Clean up potential HTML issues in the input ECG analysis for the prompt
268
+ # This helps prevent confusing the assessment model
269
+ clean_ecg_analysis = re.sub('<[^>]+>', '', ecg_analysis) # Strip HTML tags for the prompt context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
+ # Construct prompt based on available information
272
+ prompt_parts = [
273
+ "You are a highly trained cardiologist assistant AI. Your task is to synthesize information from an ECG analysis and patient history (if provided) into a clinical assessment for a reviewing physician.",
274
+ "Focus on integrating the findings and suggesting potential implications and recommendations.",
275
+ "Format your response strictly using the specified HTML structure.",
276
+ "\nECG ANALYSIS SUMMARY (Provided):\n" + clean_ecg_analysis, # Use cleaned text
277
+ ]
 
 
 
 
 
 
278
 
279
+ if patient_history and patient_history.strip():
280
+ prompt_parts.append("\nPATIENT HISTORY (Provided):\n" + patient_history)
 
 
 
281
  else:
282
+ prompt_parts.append("\nPATIENT HISTORY: Not provided.")
283
 
284
+ prompt_parts.append(f"\nASSESSMENT TIMESTAMP: {timestamp}")
 
285
 
286
+ prompt_parts.append("""
287
+ Format your assessment using ONLY the following HTML structure:
288
 
289
+ <h3>Summary of Integrated Findings</h3>
 
 
290
  <ul>
291
+ <li>[Combine key ECG findings with relevant patient history points, e.g., "ECG shows sinus tachycardia in the context of reported palpitations."]</li>
292
+ <li>[Finding 2]</li>
293
  <!-- etc. -->
294
  </ul>
295
 
296
+ <h3>Key Abnormalities and Concerns</h3>
297
  <ul>
298
+ <li>[List specific significant abnormalities from the ECG, potentially contextualized by history, e.g., "ST elevation in anterior leads concerning for possible ischemia."]</li>
299
+ <li>[Abnormality 2 (if any)]</li>
300
+ <!-- Use <span style="color:red"> for urgent/critical concerns -->
301
  </ul>
302
 
303
  <h3>Potential Clinical Implications</h3>
304
  <ul>
305
+ <li>[Suggest possible underlying conditions or risks based on findings, e.g., "Findings could be consistent with acute coronary syndrome."]</li>
306
+ <li>[Implication 2]</li>
307
  <!-- etc. -->
308
  </ul>
309
 
310
+ <h3>Recommendations for Physician Review</h3>
311
  <ul>
312
+ <li>[Suggest next steps or urgency, e.g., "<span style="color:red">Urgent clinical correlation and comparison with previous ECGs recommended.</span>"]</li>
313
+ <li>[Recommendation 2 (e.g., Consider further cardiac workup like troponins, echocardiogram if clinically indicated.)]</li>
314
  <!-- etc. -->
315
  </ul>
316
 
317
+ <h3>Differential Considerations (Optional)</h3>
318
  <ul>
319
+ <li>[List possible alternative explanations for the findings, if applicable.]</li>
320
+ <li>[Differential 2]</li>
321
  <!-- etc. -->
322
  </ul>
323
 
324
+ Important Instructions:
325
+ - Adhere strictly to the HTML format (<h3>, <ul>, <li>, <strong>, <p>, <span style="color:red">).
326
+ - Do NOT use markdown formatting (**, #, - ).
327
+ - Base your assessment ONLY on the provided ECG analysis and patient history.
328
+ - Do NOT make a definitive diagnosis. Phrase conclusions as possibilities or suggestions for the physician.
329
+ - If the ECG analysis indicated 'Not determinable' for key parameters, acknowledge this limitation.
330
+ """)
331
+ prompt = "\n".join(prompt_parts)
332
 
333
  try:
334
  assessment_completion = groq_client.chat.completions.create(
335
  messages=[
336
  {
337
  "role": "system",
338
+ "content": "You are a medical AI assistant specialized in cardiology. Generate a structured clinical assessment based on the provided ECG and patient data, formatted in HTML for physician review. Highlight urgent findings appropriately. Avoid definitive diagnoses."
339
  },
340
  {
341
  "role": "user",
 
344
  ],
345
  model=chat_model,
346
  temperature=0.2,
347
+ max_tokens=2048, # Adjust as needed
348
+ # top_p=0.9, # Optional parameter tuning
349
+ # stop=None, # Optional stop sequences
350
  )
351
 
352
  assessment_text = assessment_completion.choices[0].message.content
353
 
354
+ # Basic post-processing (Groq should follow HTML instructions, but good to have fallbacks)
355
+ assessment_text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', assessment_text)
356
+ assessment_text = re.sub(r'^\s*#+\s+(.*?)\s*$', r'<h3>\1</h3>', assessment_text, flags=re.MULTILINE)
357
+
358
+ # *** CORRECTED FALLBACK LOGIC ***
359
+ # Check if the response seems to contain the expected HTML structure
360
+ if not ("<h3>" in assessment_text and "<ul>" in assessment_text):
361
+ print(f"Warning: Groq assessment response might not be in the expected HTML format:\n{assessment_text[:500]}...")
362
+ # Fallback: Wrap the raw text in a paragraph tag with line breaks
363
+ processed_text = assessment_text.replace('\n', '<br>')
364
+ assessment_text = f"<h3>Assessment (Raw Output)</h3><p>{processed_text}</p>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
  return assessment_text
367
 
368
  except Exception as e:
369
+ print(f"Error during Groq assessment generation:\n{traceback.format_exc()}")
370
+ error_type = type(e).__name__
371
+ return f"<strong style='color:red'>Error generating assessment with Groq ({error_type}):</strong> {str(e)}"
372
+
373
 
374
+ # Doctor's chat interaction with the model about the patient (Uses Groq)
375
+ def doctor_chat(message, chat_history, ecg_analysis, patient_history, assessment, chat_model="llama3-70b-8192"):
376
+ # Check Groq client initialization
377
+ if groq_client is None:
378
+ # Prepend error message to history instead of returning it directly
379
+ chat_history.append((message, "<strong style='color:red'>Cannot start chat. Groq client not initialized. Check API Key.</strong>"))
380
+ return "", chat_history # Clear input, update history
381
 
 
 
382
  # Fixed model
383
+ chat_model = "llama3-70b-8192" # Consistent with assessment
384
 
385
  # Check if ECG analysis exists and is not an error message
386
  if not ecg_analysis or ecg_analysis.startswith("<strong style='color:red'>"):
387
+ # Prepend error message to history
388
  chat_history.append((message, "<strong style='color:red'>Cannot start chat. Please analyze a valid ECG image first.</strong>"))
389
  return "", chat_history # Clear input, update history
390
 
 
394
  # Get current timestamp
395
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
396
 
397
+ # Clean inputs for context
398
+ clean_ecg = re.sub('<[^>]+>', '', ecg_analysis)
399
+ clean_assessment = re.sub('<[^>]+>', '', assessment) if assessment and not assessment.startswith("<strong style='color:red'>") else "Assessment not available or failed."
400
+ clean_history = patient_history if patient_history and patient_history.strip() else "No patient history provided."
401
+
402
  # Prepare chat context
403
  context = f"""CURRENT TIMESTAMP: {timestamp}
404
 
405
+ === BEGIN PATIENT CONTEXT ===
406
  PATIENT HISTORY:
407
+ {clean_history}
408
 
409
+ ECG ANALYSIS SUMMARY:
410
+ {clean_ecg}
411
 
412
+ GENERATED ASSESSMENT SUMMARY:
413
+ {clean_assessment}
414
+ === END PATIENT CONTEXT ===
415
 
416
+ Based *only* on the patient context provided above, answer the doctor's questions concisely and professionally. If the information needed to answer is not in the context, explicitly state that. Do not invent information or access external knowledge.
417
  """
418
 
419
  # Construct full chat history for context
420
  messages = [
421
  {
422
  "role": "system",
423
+ "content": f"You are a specialized cardiology AI assistant conversing with a doctor. Your knowledge is strictly limited to the patient information provided in the context below. Answer questions based *only* on this context.\n\n{context}"
424
  }
425
  ]
426
 
427
+ # Add chat history to the context (limited number of turns to manage token count)
428
+ history_limit = 5 # Number of past user/assistant pairs
429
+ for user_msg, assistant_msg in chat_history[-history_limit:]:
430
  messages.append({"role": "user", "content": user_msg})
431
  # Avoid adding error messages from assistant back into context
432
+ if isinstance(assistant_msg, str) and not assistant_msg.startswith("<strong style='color:red'>"):
433
  messages.append({"role": "assistant", "content": assistant_msg})
434
 
435
  # Add the current message
 
439
  chat_completion = groq_client.chat.completions.create(
440
  messages=messages,
441
  model=chat_model,
442
+ temperature=0.3, # Slightly higher for more conversational answers, but still factual
443
+ max_tokens=1024, # Adjust as needed
444
  )
445
 
446
  response = chat_completion.choices[0].message.content
447
 
448
+ # Basic post-processing for the chat response (less critical for HTML here)
449
+ response = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', response) # Convert bold markdown
450
+ response = response.replace('\n', '<br>') # Ensure line breaks are rendered in HTML chatbot
451
 
452
  chat_history.append((message, response))
453
  return "", chat_history # Clear input message box
454
  except Exception as e:
455
+ print(f"Error during Groq chat:\n{traceback.format_exc()}")
456
+ error_type = type(e).__name__
457
+ error_message = f"<strong style='color:red'>Error in chat ({error_type}):</strong> {str(e)}"
458
  chat_history.append((message, error_message))
459
  return "", chat_history # Clear input message box
460
 
461
  # Create Gradio interface
462
  with gr.Blocks(title="Cardiac ECG Analysis System", theme=gr.themes.Soft()) as app:
 
 
463
 
464
  gr.Markdown("# 🫀 Cardiac ECG Analysis System")
465
+ gr.Markdown("Upload an ECG image and optional patient history for AI-assisted analysis, assessment, and consultation.")
466
 
467
  with gr.Tabs():
468
  with gr.TabItem("💻 Main Interface"):
 
470
  with gr.Column(scale=1):
471
  # Input components
472
  with gr.Group():
473
+ gr.Markdown("### 📊 ECG Image Upload")
474
+ ecg_image = gr.Image(type="pil", label="Upload ECG Image", height=300)
475
+ gr.Markdown("**Vision Model:** `gemini-2.0-flash-exp` (via Google AI)")
 
476
  analyze_button = gr.Button("Analyze ECG Image", variant="primary")
477
 
478
  with gr.Group():
 
480
  patient_history_text = gr.Textbox(
481
  lines=8,
482
  label="Patient History (Manual Entry or Loaded from File)",
483
+ placeholder="Enter relevant patient details (age, sex, symptoms, meds, conditions) OR upload a file (.txt, .csv, .xlsx) and click Load."
484
  )
485
  patient_history_file = gr.File(
486
+ label="Upload Patient History File (Optional)",
487
  file_types=[".txt", ".csv", ".xlsx", ".xls"]
488
  )
489
  load_history_button = gr.Button("Load Patient History from File")
490
 
491
  with gr.Group():
492
+ gr.Markdown("### 🧠 Generate Assessment")
493
+ # Ensure this model name matches the one used in generate_assessment/doctor_chat
494
+ gr.Markdown("**Assessment/Chat Model:** `llama3-70b-8192` (via Groq)")
 
495
  assess_button = gr.Button("Generate Assessment", variant="primary")
496
 
497
  with gr.Column(scale=1):
 
504
  gr.Markdown("### 📝 Medical Assessment")
505
  assessment_output = gr.HTML(label="Assessment", elem_id="assessment-output")
506
 
507
+ gr.Markdown("---") # Separator
508
+ gr.Markdown("## 👨‍⚕️ Doctor's Consultation Chat")
509
+ gr.Markdown("Ask follow-up questions based on the analysis and assessment above.")
510
 
511
  with gr.Group():
512
+ chatbot = gr.Chatbot(
513
+ label="Consultation Log",
514
+ height=450,
515
+ bubble_full_width=False,
516
+ show_label=False # Label provided by Markdown above
517
+ )
518
  with gr.Row():
519
  message = gr.Textbox(
520
+ label="Your Question", # Added label for clarity
521
+ placeholder="Type your question here and press Enter or click Send...",
 
522
  scale=4,
523
+ show_label=False, # Hide label visually if desired, but keep for accessibility
524
+ container=False,
525
  )
526
  chat_button = gr.Button("Send", scale=1, variant="primary")
527
 
528
+ with gr.TabItem("ℹ️ Instructions & Disclaimer"):
529
  gr.Markdown("""
530
  ## How to Use This Application
531
 
532
+ 1. **Upload ECG:** Go to the "Main Interface" tab. Upload an ECG image using the designated area.
533
+ 2. **Analyze ECG:** Click the **Analyze ECG Image** button. Wait for the results to appear in the "ECG Analysis Results" box. This uses the Gemini model.
534
+ 3. **Add Patient History (Optional):**
535
+ * Type relevant details directly into the "Patient History" text box.
536
+ * OR, upload a `.txt`, `.csv`, or `.xlsx` file containing patient info and click **Load Patient History from File**. The content will load into the text box.
537
+ 4. **Generate Assessment:** Click the **Generate Assessment** button. The system will combine the ECG analysis and patient history (if provided) to generate a structured assessment using a Llama model via Groq. Results appear in the "Medical Assessment" box.
538
+ 5. **Consult:** Use the chat interface at the bottom to ask follow-up questions. Type your question and click **Send** or press Enter. The chat uses the Llama model via Groq and considers the context provided above it.
539
+
540
+ ---
541
+ ## Important Disclaimer
542
+
543
+ * **Not a Medical Device:** This tool is for informational and educational purposes only. It is **NOT** a certified medical device and should **NOT** be used for primary diagnosis, treatment decisions, or emergency situations.
544
+ * **AI Limitations:** AI models can make mistakes, misinterpret images, or generate inaccurate information. The outputs are not a substitute for professional medical expertise.
545
+ * **Professional Judgment Required:** All outputs must be critically reviewed and verified by a qualified healthcare professional in the context of the patient's overall clinical picture. Do not rely solely on the AI's interpretation.
546
+ * **Data Privacy:** Ensure you comply with all applicable data privacy regulations (e.g., HIPAA) when using this tool. Avoid uploading identifiable patient information unless you have explicit consent and are operating within a secure, compliant environment. The developers of this interface are not responsible for data breaches resulting from user uploads.
547
+ * **API Keys:** Securely manage your `GEMINI_API_KEY` and `GROQ_API_KEY`. Do not expose them publicly.
548
+ * **No Liability:** Use this tool at your own risk. The creators assume no liability for any decisions made based on its output.
 
 
 
 
 
 
549
  """)
550
 
551
  # --- Event Handlers ---
552
 
553
+ # Analyze Button: Input ECG Image -> Output ECG Analysis HTML
554
  analyze_button.click(
555
+ fn=analyze_ecg_image,
556
  inputs=[ecg_image],
557
  outputs=ecg_analysis_output
558
  )
559
 
560
+ # Load History Button: Input File -> Output Text into History Textbox
561
  load_history_button.click(
562
+ fn=process_patient_history,
563
  inputs=[patient_history_file],
564
  outputs=[patient_history_text]
565
  )
566
 
567
+ # Assess Button: Input ECG Analysis HTML, History Text -> Output Assessment HTML
568
  assess_button.click(
569
+ fn=generate_assessment,
570
  inputs=[ecg_analysis_output, patient_history_text],
571
  outputs=assessment_output
572
  )
573
 
574
+ # Chat Send Button: Input Message, Chat History, Context -> Output Cleared Message, Updated Chat History
575
  chat_button.click(
576
+ fn=doctor_chat,
577
  inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
578
+ outputs=[message, chatbot]
579
  )
580
 
581
+ # Chat Textbox Submit (Enter): Input Message, Chat History, Context -> Output Cleared Message, Updated Chat History
582
  message.submit(
583
+ fn=doctor_chat,
584
  inputs=[message, chatbot, ecg_analysis_output, patient_history_text, assessment_output],
585
+ outputs=[message, chatbot]
586
  )
587
 
588
 
589
  # Launch the app
590
  if __name__ == "__main__":
591
+ print("===== Application Startup =====")
592
+ print(f"Attempting to launch Gradio app at {datetime.datetime.now()}")
593
+ # Add share=True for a temporary public link (use with caution regarding data privacy)
594
+ # Add debug=True for more detailed error output during development
595
+ app.launch()
596
+ # app.launch(share=True, debug=True)