Ali2206 commited on
Commit
f394b25
·
verified ·
1 Parent(s): ba63eca

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +542 -95
app.py CHANGED
@@ -1,112 +1,559 @@
1
- # Update the Chatbot component in create_ui() to use the new message format:
2
- chatbot = gr.Chatbot(
3
- label="Analysis Conversation",
4
- height=600,
5
- show_copy_button=True,
6
- avatar_images=(
7
- "assets/user.png",
8
- "assets/assistant.png"
9
- ) if os.path.exists("assets/user.png") else None,
10
- render=False,
11
- bubble_full_width=False,
12
- type="messages" # Add this line to use the new format
13
- )
14
-
15
- # Update the analyze function to properly return all outputs:
16
- def analyze(message: str, history: List[dict], files: List) -> Generator[Dict[str, Any], None, None]:
17
- # Initialize all outputs
18
- outputs = {
19
- "chatbot": history.copy(),
20
- "download_output": None,
21
- "final_summary": "",
22
- "progress_text": {"value": "Starting analysis...", "visible": True}
23
- }
24
- yield outputs # First yield with all outputs
25
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  try:
27
- # Add user message to history
28
- history.append({"role": "user", "content": message})
29
- outputs["chatbot"] = history
30
- yield outputs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- extracted = []
33
- file_hash_value = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- if files:
36
- # Process files in parallel
37
- with ThreadPoolExecutor(max_workers=4) as executor:
38
- futures = []
39
- for f in files:
40
- file_type = f.name.split(".")[-1].lower()
41
- futures.append(executor.submit(process_file, f.name, file_type))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- for i, future in enumerate(as_completed(futures), 1):
44
- try:
45
- extracted.extend(future.result())
46
- outputs["progress_text"] = update_progress(i, len(files), "Processing files")
47
- yield outputs
48
- except Exception as e:
49
- logger.error(f"File processing error: {e}")
50
- extracted.append({"error": f"Error processing file: {str(e)}"})
 
 
 
51
 
52
- file_hash_value = file_hash(files[0].name) if files else ""
53
- history.append({"role": "assistant", "content": "✅ File processing complete"})
54
  outputs.update({
55
- "chatbot": history,
56
- "progress_text": update_progress(len(files), len(files), "Files processed")
 
57
  })
58
  yield outputs
59
 
60
- # Process content and generate responses
61
- text_content = "\n".join(json.dumps(item) for item in extracted)
62
- chunks = tokenize_and_chunk(text_content)
63
- combined_response = ""
64
-
65
- for chunk_idx, chunk in enumerate(chunks, 1):
66
- prompt = f"""Analyze this patient record for missed diagnoses...""" # Your prompt here
67
-
68
- history.append({"role": "assistant", "content": ""})
69
  outputs.update({
70
  "chatbot": history,
71
- "progress_text": update_progress(chunk_idx, len(chunks), "Analyzing")
 
72
  })
73
  yield outputs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- # Process response stream
76
- chunk_response = ""
77
- for update in process_response_stream(prompt, history):
78
- history[-1] = update
79
- chunk_response = update["content"]
80
- outputs.update({
81
- "chatbot": history,
82
- "progress_text": update_progress(chunk_idx, len(chunks), "Analyzing")
83
- })
84
- yield outputs
85
 
86
- combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
87
- torch.cuda.empty_cache()
88
- gc.collect()
89
-
90
- # Final outputs
91
- summary = summarize_findings(combined_response)
92
- report_path = os.path.join(report_dir, f"{file_hash_value}_report.txt") if file_hash_value else None
93
- if report_path:
94
- with open(report_path, "w", encoding="utf-8") as f:
95
- f.write(combined_response + "\n\n" + summary)
96
-
97
- outputs.update({
98
- "download_output": report_path if report_path else None,
99
- "final_summary": summary,
100
- "progress_text": {"visible": False}
101
- })
102
- yield outputs
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  except Exception as e:
105
- logger.error("Analysis error: %s", e)
106
- history.append({"role": "assistant", "content": f"❌ Error: {str(e)}"})
107
- outputs.update({
108
- "chatbot": history,
109
- "final_summary": f"Error occurred: {str(e)}",
110
- "progress_text": {"visible": False}
111
- })
112
- yield outputs
 
1
+ import sys
2
+ import os
3
+ import pandas as pd
4
+ import pdfplumber
5
+ import json
6
+ import gradio as gr
7
+ from typing import List, Dict, Generator, Any
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ import hashlib
10
+ import shutil
11
+ import re
12
+ import psutil
13
+ import subprocess
14
+ import logging
15
+ import torch
16
+ import gc
17
+ from diskcache import Cache
18
+ from transformers import AutoTokenizer
19
+
20
+ # ==================== CONFIGURATION ====================
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Setup directories
26
+ PERSISTENT_DIR = "/data/hf_cache"
27
+ DIRECTORIES = {
28
+ "models": os.path.join(PERSISTENT_DIR, "txagent_models"),
29
+ "tools": os.path.join(PERSISTENT_DIR, "tool_cache"),
30
+ "cache": os.path.join(PERSISTENT_DIR, "cache"),
31
+ "reports": os.path.join(PERSISTENT_DIR, "reports"),
32
+ "vllm": os.path.join(PERSISTENT_DIR, "vllm_cache")
33
+ }
34
+
35
+ # Create directories
36
+ for dir_path in DIRECTORIES.values():
37
+ os.makedirs(dir_path, exist_ok=True)
38
+
39
+ # Environment variables
40
+ os.environ.update({
41
+ "HF_HOME": DIRECTORIES["models"],
42
+ "TRANSFORMERS_CACHE": DIRECTORIES["models"],
43
+ "VLLM_CACHE_DIR": DIRECTORIES["vllm"],
44
+ "TOKENIZERS_PARALLELISM": "false",
45
+ "CUDA_LAUNCH_BLOCKING": "1"
46
+ })
47
+
48
+ # ==================== UTILITY FUNCTIONS ====================
49
+ def sanitize_text(text: str) -> str:
50
+ """Clean and sanitize text input"""
51
+ return text.encode("utf-8", "ignore").decode("utf-8")
52
+
53
+ def get_file_hash(file_path: str) -> str:
54
+ """Generate MD5 hash of file content"""
55
+ with open(file_path, "rb") as f:
56
+ return hashlib.md5(f.read()).hexdigest()
57
+
58
+ def log_system_resources(tag: str = "") -> None:
59
+ """Log system resource usage"""
60
  try:
61
+ cpu = psutil.cpu_percent(interval=1)
62
+ mem = psutil.virtual_memory()
63
+ logger.info(f"[{tag}] CPU: {cpu:.1f}% | RAM: {mem.used//(1024**2)}MB/{mem.total//(1024**2)}MB")
64
+
65
+ gpu_info = subprocess.run(
66
+ ["nvidia-smi", "--query-gpu=memory.used,memory.total,utilization.gpu",
67
+ "--format=csv,nounits,noheader"],
68
+ capture_output=True, text=True
69
+ )
70
+ if gpu_info.returncode == 0:
71
+ used, total, util = gpu_info.stdout.strip().split(", ")
72
+ logger.info(f"[{tag}] GPU: {used}MB/{total}MB | Util: {util}%")
73
+ except Exception as e:
74
+ logger.error(f"[{tag}] Resource monitoring failed: {e}")
75
+
76
+ # ==================== FILE PROCESSING ====================
77
+ class FileProcessor:
78
+ @staticmethod
79
+ def extract_pdf_text(file_path: str) -> str:
80
+ """Extract text from PDF with parallel processing"""
81
+ try:
82
+ with pdfplumber.open(file_path) as pdf:
83
+ total_pages = len(pdf.pages)
84
+ if not total_pages:
85
+ return ""
86
+
87
+ def process_page_range(start: int, end: int) -> List[tuple]:
88
+ results = []
89
+ with pdfplumber.open(file_path) as pdf:
90
+ for page in pdf.pages[start:end]:
91
+ page_num = start + pdf.pages.index(page)
92
+ text = page.extract_text() or ""
93
+ results.append((page_num, f"=== Page {page_num + 1} ===\n{text.strip()}"))
94
+ return results
95
+
96
+ batch_size = 10
97
+ batches = [(i, min(i+batch_size, total_pages)) for i in range(0, total_pages, batch_size)]
98
+ text_chunks = [""] * total_pages
99
+
100
+ with ThreadPoolExecutor(max_workers=6) as executor:
101
+ futures = [executor.submit(process_page_range, start, end) for start, end in batches]
102
+ for future in as_completed(futures):
103
+ for page_num, text in future.result():
104
+ text_chunks[page_num] = text
105
+
106
+ return "\n\n".join(filter(None, text_chunks))
107
+ except Exception as e:
108
+ logger.error(f"PDF processing error: {e}")
109
+ return f"PDF processing error: {str(e)}"
110
+
111
+ @staticmethod
112
+ def excel_to_data(file_path: str) -> List[Dict]:
113
+ """Convert Excel file to structured data"""
114
+ try:
115
+ df = pd.read_excel(file_path, engine='openpyxl', header=None, dtype=str)
116
+ content = df.where(pd.notnull(df), "").astype(str).values.tolist()
117
+ return [{"filename": os.path.basename(file_path), "rows": content, "type": "excel"}]
118
+ except Exception as e:
119
+ logger.error(f"Excel processing error: {e}")
120
+ return [{"error": f"Excel processing error: {str(e)}"}]
121
+
122
+ @staticmethod
123
+ def csv_to_data(file_path: str) -> List[Dict]:
124
+ """Convert CSV file to structured data"""
125
+ try:
126
+ chunks = []
127
+ for chunk in pd.read_csv(
128
+ file_path, header=None, dtype=str,
129
+ encoding_errors='replace', on_bad_lines='skip', chunksize=10000
130
+ ):
131
+ chunks.append(chunk)
132
+
133
+ df = pd.concat(chunks) if chunks else pd.DataFrame()
134
+ content = df.where(pd.notnull(df), "").astype(str).values.tolist()
135
+ return [{"filename": os.path.basename(file_path), "rows": content, "type": "csv"}]
136
+ except Exception as e:
137
+ logger.error(f"CSV processing error: {e}")
138
+ return [{"error": f"CSV processing error: {str(e)}"}]
139
+
140
+ @classmethod
141
+ def process_file(cls, file_path: str, file_type: str) -> List[Dict]:
142
+ """Route file processing based on type"""
143
+ processors = {
144
+ "pdf": cls.extract_pdf_text,
145
+ "xls": cls.excel_to_data,
146
+ "xlsx": cls.excel_to_data,
147
+ "csv": cls.csv_to_data
148
+ }
149
+
150
+ if file_type not in processors:
151
+ return [{"error": f"Unsupported file type: {file_type}"}]
152
 
153
+ try:
154
+ result = processors[file_type](file_path)
155
+ if file_type == "pdf":
156
+ return [{
157
+ "filename": os.path.basename(file_path),
158
+ "content": result,
159
+ "status": "initial",
160
+ "type": "pdf"
161
+ }]
162
+ return result
163
+ except Exception as e:
164
+ logger.error(f"Error processing {file_type} file: {e}")
165
+ return [{"error": f"Error processing file: {str(e)}"}]
166
+
167
+ # ==================== TEXT PROCESSING ====================
168
+ class TextProcessor:
169
+ def __init__(self):
170
+ self.tokenizer = AutoTokenizer.from_pretrained("mims-harvard/TxAgent-T1-Llama-3.1-8B")
171
+ self.cache = Cache(DIRECTORIES["cache"], size_limit=10*1024**3)
172
+
173
+ def chunk_text(self, text: str, max_tokens: int = 1800) -> List[str]:
174
+ """Split text into token-limited chunks"""
175
+ tokens = self.tokenizer.encode(text)
176
+ return [
177
+ self.tokenizer.decode(tokens[i:i+max_tokens])
178
+ for i in range(0, len(tokens), max_tokens)
179
+ ]
180
+
181
+ def clean_response(self, text: str) -> str:
182
+ """Clean and format model response"""
183
+ text = sanitize_text(text)
184
+ text = re.sub(
185
+ r"\[.*?\]|\bNone\b|To analyze the patient record excerpt.*?medications\."
186
+ r"|Since the previous attempts.*?\.|I need to.*?medications\."
187
+ r"|Retrieving tools.*?\.", "", text, flags=re.DOTALL
188
+ )
189
+
190
+ diagnoses = []
191
+ in_diagnoses = False
192
+
193
+ for line in text.splitlines():
194
+ line = line.strip()
195
+ if not line:
196
+ continue
197
+ if re.match(r"###\s*Missed Diagnoses", line):
198
+ in_diagnoses = True
199
+ continue
200
+ if re.match(r"###\s*(Medication Conflicts|Incomplete Assessments|Urgent Follow-up)", line):
201
+ in_diagnoses = False
202
+ continue
203
+ if in_diagnoses and re.match(r"-\s*.+", line):
204
+ diagnosis = re.sub(r"^\-\s*", "", line).strip()
205
+ if diagnosis and not re.match(r"No issues identified", diagnosis, re.IGNORECASE):
206
+ diagnoses.append(diagnosis)
207
+
208
+ return " ".join(diagnoses) if diagnoses else ""
209
+
210
+ def summarize_results(self, analysis: str) -> str:
211
+ """Generate concise summary from full analysis"""
212
+ chunks = analysis.split("--- Analysis for Chunk")
213
+ diagnoses = []
214
+
215
+ for chunk in chunks:
216
+ chunk = chunk.strip()
217
+ if not chunk or "No oversights identified" in chunk:
218
+ continue
219
+
220
+ in_diagnoses = False
221
+ for line in chunk.splitlines():
222
+ line = line.strip()
223
+ if not line:
224
+ continue
225
+ if re.match(r"###\s*Missed Diagnoses", line):
226
+ in_diagnoses = True
227
+ continue
228
+ if re.match(r"###\s*(Medication Conflicts|Incomplete Assessments|Urgent Follow-up)", line):
229
+ in_diagnoses = False
230
+ continue
231
+ if in_diagnoses and re.match(r"-\s*.+", line):
232
+ diagnosis = re.sub(r"^\-\s*", "", line).strip()
233
+ if diagnosis and not re.match(r"No issues identified", diagnosis, re.IGNORECASE):
234
+ diagnoses.append(diagnosis)
235
+
236
+ unique_diagnoses = list(dict.fromkeys(diagnoses)) # Remove duplicates
237
+
238
+ if not unique_diagnoses:
239
+ return "No missed diagnoses were identified in the provided records."
240
+
241
+ if len(unique_diagnoses) > 1:
242
+ summary = "Missed diagnoses include " + ", ".join(unique_diagnoses[:-1])
243
+ summary += f", and {unique_diagnoses[-1]}"
244
+ else:
245
+ summary = "Missed diagnoses include " + unique_diagnoses[0]
246
+
247
+ return summary + ", all requiring urgent clinical review."
248
+
249
+ # ==================== CORE APPLICATION ====================
250
+ class ClinicalOversightApp:
251
+ def __init__(self):
252
+ self.agent = self._initialize_agent()
253
+ self.text_processor = TextProcessor()
254
+ self.file_processor = FileProcessor()
255
+
256
+ def _initialize_agent(self):
257
+ """Initialize the TxAgent with proper configuration"""
258
+ logger.info("Initializing AI model...")
259
+ log_system_resources("Before Load")
260
+
261
+ tool_path = os.path.join(DIRECTORIES["tools"], "new_tool.json")
262
+ if not os.path.exists(tool_path):
263
+ default_tools = os.path.abspath("data/new_tool.json")
264
+ shutil.copy(default_tools, tool_path)
265
+
266
+ agent = TxAgent(
267
+ model_name="mims-harvard/TxAgent-T1-Llama-3.1-8B",
268
+ rag_model_name="mims-harvard/ToolRAG-T1-GTE-Qwen2-1.5B",
269
+ tool_files_dict={"new_tool": tool_path},
270
+ force_finish=True,
271
+ enable_checker=False,
272
+ step_rag_num=4,
273
+ seed=100,
274
+ additional_default_tools=[],
275
+ )
276
+ agent.init_model()
277
+
278
+ log_system_resources("After Load")
279
+ logger.info("AI Agent Ready")
280
+ return agent
281
+
282
+ def process_response_stream(self, prompt: str, history: List[dict]) -> Generator[dict, None, None]:
283
+ """Stream the agent's response with proper formatting"""
284
+ full_response = ""
285
+ for chunk in self.agent.run_gradio_chat(prompt, [], 0.2, 512, 2048, False, []):
286
+ if not chunk:
287
+ continue
288
+
289
+ if isinstance(chunk, list):
290
+ for message in chunk:
291
+ if hasattr(message, 'content') and message.content:
292
+ cleaned = self.text_processor.clean_response(message.content)
293
+ if cleaned:
294
+ full_response += cleaned + " "
295
+ yield {"role": "assistant", "content": full_response}
296
+ elif isinstance(chunk, str) and chunk.strip():
297
+ cleaned = self.text_processor.clean_response(chunk)
298
+ if cleaned:
299
+ full_response += cleaned + " "
300
+ yield {"role": "assistant", "content": full_response}
301
+
302
+ def analyze(self, message: str, history: List[dict], files: List) -> Generator[Dict[str, Any], None, None]:
303
+ """Main analysis pipeline with proper output formatting"""
304
+ # Initialize all output components
305
+ outputs = {
306
+ "chatbot": history.copy(),
307
+ "download_output": None,
308
+ "final_summary": "",
309
+ "progress_text": {"value": "Starting analysis...", "visible": True}
310
+ }
311
+ yield outputs
312
 
313
+ try:
314
+ # Add user message to history
315
+ history.append({"role": "user", "content": message})
316
+ outputs["chatbot"] = history
317
+ yield outputs
318
+
319
+ # Process uploaded files
320
+ extracted = []
321
+ file_hash_value = ""
322
+
323
+ if files:
324
+ with ThreadPoolExecutor(max_workers=4) as executor:
325
+ futures = []
326
+ for f in files:
327
+ file_type = f.name.split(".")[-1].lower()
328
+ futures.append(executor.submit(self.file_processor.process_file, f.name, file_type))
329
+
330
+ for i, future in enumerate(as_completed(futures), 1):
331
+ try:
332
+ extracted.extend(future.result())
333
+ outputs["progress_text"] = self._update_progress(i, len(files), "Processing files")
334
+ yield outputs
335
+ except Exception as e:
336
+ logger.error(f"File processing error: {e}")
337
+ extracted.append({"error": f"Error processing file: {str(e)}"})
338
+
339
+ file_hash_value = get_file_hash(files[0].name) if files else ""
340
+ history.append({"role": "assistant", "content": "✅ File processing complete"})
341
+ outputs.update({
342
+ "chatbot": history,
343
+ "progress_text": self._update_progress(len(files), len(files), "Files processed")
344
+ })
345
+ yield outputs
346
+
347
+ # Analyze content
348
+ text_content = "\n".join(json.dumps(item) for item in extracted)
349
+ chunks = self.text_processor.chunk_text(text_content)
350
+ combined_response = ""
351
+
352
+ for chunk_idx, chunk in enumerate(chunks, 1):
353
+ prompt = f"""
354
+ Analyze this patient record for missed diagnoses. Provide a concise, evidence-based summary
355
+ as a single paragraph without headings or bullet points. Include specific clinical findings
356
+ with their potential implications and urgent review recommendations. If no missed diagnoses
357
+ are found, state 'No missed diagnoses identified'.
358
+
359
+ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
360
+ {chunk[:1800]}
361
+ """
362
+ history.append({"role": "assistant", "content": ""})
363
+ outputs.update({
364
+ "chatbot": history,
365
+ "progress_text": self._update_progress(chunk_idx, len(chunks), "Analyzing")
366
+ })
367
+ yield outputs
368
+
369
+ # Stream response
370
+ chunk_response = ""
371
+ for update in self.process_response_stream(prompt, history):
372
+ history[-1] = update
373
+ chunk_response = update["content"]
374
+ outputs.update({
375
+ "chatbot": history,
376
+ "progress_text": self._update_progress(chunk_idx, len(chunks), "Analyzing")
377
+ })
378
+ yield outputs
379
 
380
+ combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
381
+ torch.cuda.empty_cache()
382
+ gc.collect()
383
+
384
+ # Generate final outputs
385
+ summary = self.text_processor.summarize_results(combined_response)
386
+ report_path = os.path.join(DIRECTORIES["reports"], f"{file_hash_value}_report.txt") if file_hash_value else None
387
+
388
+ if report_path:
389
+ with open(report_path, "w", encoding="utf-8") as f:
390
+ f.write(combined_response + "\n\n" + summary)
391
 
 
 
392
  outputs.update({
393
+ "download_output": report_path if report_path else None,
394
+ "final_summary": summary,
395
+ "progress_text": {"visible": False}
396
  })
397
  yield outputs
398
 
399
+ except Exception as e:
400
+ logger.error(f"Analysis error: {e}")
401
+ history.append({"role": "assistant", "content": f"❌ Error: {str(e)}"})
 
 
 
 
 
 
402
  outputs.update({
403
  "chatbot": history,
404
+ "final_summary": f"Error occurred: {str(e)}",
405
+ "progress_text": {"visible": False}
406
  })
407
  yield outputs
408
+
409
+ def _update_progress(self, current: int, total: int, stage: str = "") -> Dict[str, Any]:
410
+ """Format progress update for UI"""
411
+ progress = f"{stage} - {current}/{total}" if stage else f"{current}/{total}"
412
+ return {"value": progress, "visible": True, "label": f"Progress: {progress}"}
413
+
414
+ def create_interface(self):
415
+ """Create Gradio interface with improved layout"""
416
+ with gr.Blocks(
417
+ theme=gr.themes.Soft(
418
+ primary_hue="indigo",
419
+ secondary_hue="blue",
420
+ neutral_hue="slate"
421
+ ),
422
+ title="Clinical Oversight Assistant",
423
+ css="""
424
+ .diagnosis-summary {
425
+ border-left: 4px solid #4f46e5;
426
+ padding: 12px;
427
+ background: #f8fafc;
428
+ border-radius: 4px;
429
+ }
430
+ .file-upload {
431
+ border: 2px dashed #cbd5e1;
432
+ border-radius: 8px;
433
+ padding: 20px;
434
+ }
435
+ """
436
+ ) as app:
437
+ # Header Section
438
+ gr.Markdown("""
439
+ <div style='text-align: center; margin-bottom: 20px;'>
440
+ <h1 style='color: #4f46e5;'>🩺 Clinical Oversight Assistant</h1>
441
+ <p style='color: #64748b;'>
442
+ AI-powered analysis of patient records for potential missed diagnoses
443
+ </p>
444
+ </div>
445
+ """)
446
+
447
+ with gr.Row(equal_height=False):
448
+ # Main Chat Column
449
+ with gr.Column(scale=3):
450
+ chatbot = gr.Chatbot(
451
+ label="Clinical Analysis",
452
+ height=600,
453
+ show_copy_button=True,
454
+ avatar_images=(
455
+ "assets/user.png",
456
+ "assets/assistant.png"
457
+ ) if os.path.exists("assets/user.png") else None,
458
+ bubble_full_width=False,
459
+ type="messages",
460
+ elem_classes=["chat-container"]
461
+ )
462
+
463
+ # Results Column
464
+ with gr.Column(scale=1):
465
+ with gr.Group():
466
+ gr.Markdown("### 📝 Summary of Findings")
467
+ final_summary = gr.Markdown(
468
+ "Analysis results will appear here...",
469
+ elem_classes=["diagnosis-summary"]
470
+ )
471
+
472
+ with gr.Group():
473
+ gr.Markdown("### 📂 Report Download")
474
+ download_output = gr.File(
475
+ label="Full Report",
476
+ visible=False,
477
+ interactive=False
478
+ )
479
+
480
+ # Input Section
481
+ with gr.Row():
482
+ file_upload = gr.File(
483
+ file_types=[".pdf", ".csv", ".xls", ".xlsx"],
484
+ file_count="multiple",
485
+ label="Upload Patient Records",
486
+ elem_classes=["file-upload"]
487
+ )
488
+
489
+ # Interaction Section
490
+ with gr.Row():
491
+ msg_input = gr.Textbox(
492
+ placeholder="Ask about potential oversights or upload files...",
493
+ show_label=False,
494
+ container=False,
495
+ scale=7,
496
+ autofocus=True
497
+ )
498
+ send_btn = gr.Button(
499
+ "Analyze",
500
+ variant="primary",
501
+ scale=1,
502
+ min_width=100
503
+ )
504
+
505
+ # Progress Indicator
506
+ progress_text = gr.Textbox(
507
+ label="Progress Status",
508
+ visible=False,
509
+ interactive=False
510
+ )
511
+
512
+ # Event Handlers
513
+ send_btn.click(
514
+ self.analyze,
515
+ inputs=[msg_input, chatbot, file_upload],
516
+ outputs=[chatbot, download_output, final_summary, progress_text],
517
+ show_progress="hidden"
518
+ )
519
 
520
+ msg_input.submit(
521
+ self.analyze,
522
+ inputs=[msg_input, chatbot, file_upload],
523
+ outputs=[chatbot, download_output, final_summary, progress_text],
524
+ show_progress="hidden"
525
+ )
 
 
 
 
526
 
527
+ app.load(
528
+ lambda: [
529
+ [], None, "", "", None, {"visible": False}
530
+ ],
531
+ outputs=[chatbot, download_output, final_summary, msg_input, file_upload, progress_text],
532
+ queue=False
533
+ )
534
+
535
+ return app
 
 
 
 
 
 
 
 
536
 
537
+ # ==================== APPLICATION ENTRY POINT ====================
538
+ if __name__ == "__main__":
539
+ try:
540
+ logger.info("Starting Clinical Oversight Assistant...")
541
+ app = ClinicalOversightApp()
542
+ interface = app.create_interface()
543
+
544
+ interface.queue(
545
+ api_open=False,
546
+ max_size=20
547
+ ).launch(
548
+ server_name="0.0.0.0",
549
+ server_port=7860,
550
+ show_error=True,
551
+ allowed_paths=[DIRECTORIES["reports"]],
552
+ share=False
553
+ )
554
  except Exception as e:
555
+ logger.error(f"Application failed to start: {e}")
556
+ raise
557
+ finally:
558
+ if torch.distributed.is_initialized():
559
+ torch.distributed.destroy_process_group()