Update app.py
Browse files
app.py
CHANGED
@@ -44,10 +44,14 @@ os.environ.update({
|
|
44 |
"TOKENIZERS_PARALLELISM": "false",
|
45 |
"CUDA_LAUNCH_BLOCKING": "1"
|
46 |
})
|
|
|
|
|
47 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
48 |
src_path = os.path.abspath(os.path.join(current_dir, "src"))
|
49 |
sys.path.insert(0, src_path)
|
|
|
50 |
from txagent.txagent import TxAgent
|
|
|
51 |
# ==================== UTILITY FUNCTIONS ====================
|
52 |
def sanitize_text(text: str) -> str:
|
53 |
"""Clean and sanitize text input"""
|
@@ -79,8 +83,12 @@ def log_system_resources(tag: str = "") -> None:
|
|
79 |
# ==================== FILE PROCESSING ====================
|
80 |
class FileProcessor:
|
81 |
@staticmethod
|
82 |
-
def extract_pdf_text(file_path: str) -> str:
|
83 |
-
"""Extract text from PDF with
|
|
|
|
|
|
|
|
|
84 |
try:
|
85 |
with pdfplumber.open(file_path) as pdf:
|
86 |
total_pages = len(pdf.pages)
|
@@ -100,31 +108,43 @@ class FileProcessor:
|
|
100 |
batches = [(i, min(i+batch_size, total_pages)) for i in range(0, total_pages, batch_size)]
|
101 |
text_chunks = [""] * total_pages
|
102 |
|
103 |
-
with ThreadPoolExecutor(max_workers=
|
104 |
futures = [executor.submit(process_page_range, start, end) for start, end in batches]
|
105 |
for future in as_completed(futures):
|
106 |
for page_num, text in future.result():
|
107 |
text_chunks[page_num] = text
|
108 |
|
109 |
-
|
|
|
|
|
110 |
except Exception as e:
|
111 |
logger.error(f"PDF processing error: {e}")
|
112 |
return f"PDF processing error: {str(e)}"
|
113 |
|
114 |
@staticmethod
|
115 |
-
def excel_to_data(file_path: str) -> List[Dict]:
|
116 |
-
"""Convert Excel file to structured data"""
|
|
|
|
|
|
|
|
|
117 |
try:
|
118 |
df = pd.read_excel(file_path, engine='openpyxl', header=None, dtype=str)
|
119 |
content = df.where(pd.notnull(df), "").astype(str).values.tolist()
|
120 |
-
|
|
|
|
|
121 |
except Exception as e:
|
122 |
logger.error(f"Excel processing error: {e}")
|
123 |
return [{"error": f"Excel processing error: {str(e)}"}]
|
124 |
|
125 |
@staticmethod
|
126 |
-
def csv_to_data(file_path: str) -> List[Dict]:
|
127 |
-
"""Convert CSV file to structured data"""
|
|
|
|
|
|
|
|
|
128 |
try:
|
129 |
chunks = []
|
130 |
for chunk in pd.read_csv(
|
@@ -135,13 +155,15 @@ class FileProcessor:
|
|
135 |
|
136 |
df = pd.concat(chunks) if chunks else pd.DataFrame()
|
137 |
content = df.where(pd.notnull(df), "").astype(str).values.tolist()
|
138 |
-
|
|
|
|
|
139 |
except Exception as e:
|
140 |
logger.error(f"CSV processing error: {e}")
|
141 |
return [{"error": f"CSV processing error: {str(e)}"}]
|
142 |
|
143 |
@classmethod
|
144 |
-
def process_file(cls, file_path: str, file_type: str) -> List[Dict]:
|
145 |
"""Route file processing based on type"""
|
146 |
processors = {
|
147 |
"pdf": cls.extract_pdf_text,
|
@@ -154,7 +176,7 @@ class FileProcessor:
|
|
154 |
return [{"error": f"Unsupported file type: {file_type}"}]
|
155 |
|
156 |
try:
|
157 |
-
result = processors[file_type](file_path)
|
158 |
if file_type == "pdf":
|
159 |
return [{
|
160 |
"filename": os.path.basename(file_path),
|
@@ -173,7 +195,7 @@ class TextProcessor:
|
|
173 |
self.tokenizer = AutoTokenizer.from_pretrained("mims-harvard/TxAgent-T1-Llama-3.1-8B")
|
174 |
self.cache = Cache(DIRECTORIES["cache"], size_limit=10*1024**3)
|
175 |
|
176 |
-
def chunk_text(self, text: str, max_tokens: int =
|
177 |
"""Split text into token-limited chunks"""
|
178 |
tokens = self.tokenizer.encode(text)
|
179 |
return [
|
@@ -184,11 +206,7 @@ class TextProcessor:
|
|
184 |
def clean_response(self, text: str) -> str:
|
185 |
"""Clean and format model response"""
|
186 |
text = sanitize_text(text)
|
187 |
-
text = re.sub(
|
188 |
-
r"\[.*?\]|\bNone\b|To analyze the patient record excerpt.*?medications\."
|
189 |
-
r"|Since the previous attempts.*?\.|I need to.*?medications\."
|
190 |
-
r"|Retrieving tools.*?\.", "", text, flags=re.DOTALL
|
191 |
-
)
|
192 |
|
193 |
diagnoses = []
|
194 |
in_diagnoses = False
|
@@ -236,7 +254,7 @@ class TextProcessor:
|
|
236 |
if diagnosis and not re.match(r"No issues identified", diagnosis, re.IGNORECASE):
|
237 |
diagnoses.append(diagnosis)
|
238 |
|
239 |
-
unique_diagnoses = list(dict.fromkeys(diagnoses))
|
240 |
|
241 |
if not unique_diagnoses:
|
242 |
return "No missed diagnoses were identified in the provided records."
|
@@ -302,50 +320,43 @@ class ClinicalOversightApp:
|
|
302 |
full_response += cleaned + " "
|
303 |
yield {"role": "assistant", "content": full_response}
|
304 |
|
305 |
-
def analyze(self, message: str, history: List[dict], files: List) -> Generator[
|
306 |
"""Main analysis pipeline with proper output formatting"""
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
"final_summary": "",
|
312 |
-
"progress_text": {"value": "Starting analysis...", "visible": True}
|
313 |
-
}
|
314 |
-
yield outputs
|
315 |
|
316 |
try:
|
317 |
# Add user message to history
|
318 |
-
|
319 |
-
|
320 |
-
yield outputs
|
321 |
|
322 |
# Process uploaded files
|
|
|
323 |
extracted = []
|
324 |
file_hash_value = ""
|
325 |
|
326 |
if files:
|
327 |
-
with ThreadPoolExecutor(max_workers=
|
328 |
futures = []
|
329 |
for f in files:
|
330 |
file_type = f.name.split(".")[-1].lower()
|
331 |
-
futures.append(executor.submit(self.file_processor.process_file, f.name, file_type))
|
332 |
|
333 |
for i, future in enumerate(as_completed(futures), 1):
|
334 |
try:
|
335 |
extracted.extend(future.result())
|
336 |
-
|
337 |
-
yield
|
338 |
except Exception as e:
|
339 |
logger.error(f"File processing error: {e}")
|
340 |
extracted.append({"error": f"Error processing file: {str(e)}"})
|
341 |
|
342 |
file_hash_value = get_file_hash(files[0].name) if files else ""
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
"progress_text": self._update_progress(len(files), len(files), "Files processed")
|
347 |
-
})
|
348 |
-
yield outputs
|
349 |
|
350 |
# Analyze content
|
351 |
text_content = "\n".join(json.dumps(item) for item in extracted)
|
@@ -360,159 +371,258 @@ with their potential implications and urgent review recommendations. If no misse
|
|
360 |
are found, state 'No missed diagnoses identified'.
|
361 |
|
362 |
Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
363 |
-
{chunk[:
|
364 |
"""
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
"progress_text": self._update_progress(chunk_idx, len(chunks), "Analyzing")
|
369 |
-
})
|
370 |
-
yield outputs
|
371 |
|
372 |
# Stream response
|
373 |
chunk_response = ""
|
374 |
-
for update in self.process_response_stream(prompt,
|
375 |
-
|
376 |
chunk_response = update["content"]
|
377 |
-
|
378 |
-
|
379 |
-
"progress_text": self._update_progress(chunk_idx, len(chunks), "Analyzing")
|
380 |
-
})
|
381 |
-
yield outputs
|
382 |
|
383 |
combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
|
384 |
torch.cuda.empty_cache()
|
385 |
gc.collect()
|
386 |
|
387 |
# Generate final outputs
|
388 |
-
|
389 |
report_path = os.path.join(DIRECTORIES["reports"], f"{file_hash_value}_report.txt") if file_hash_value else None
|
390 |
|
391 |
if report_path:
|
392 |
with open(report_path, "w", encoding="utf-8") as f:
|
393 |
-
f.write(combined_response + "\n\n" +
|
394 |
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
"progress_text": {"visible": False}
|
399 |
-
})
|
400 |
-
yield outputs
|
401 |
|
402 |
except Exception as e:
|
403 |
logger.error(f"Analysis error: {e}")
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
"progress_text": {"visible": False}
|
409 |
-
})
|
410 |
-
yield outputs
|
411 |
|
412 |
def _update_progress(self, current: int, total: int, stage: str = "") -> Dict[str, Any]:
|
413 |
"""Format progress update for UI"""
|
414 |
progress = f"{stage} - {current}/{total}" if stage else f"{current}/{total}"
|
415 |
-
return {"value": progress, "visible": True
|
416 |
|
417 |
def create_interface(self):
|
418 |
-
"""Create Gradio interface with
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
432 |
}
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
padding:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
437 |
}
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
446 |
</p>
|
447 |
</div>
|
448 |
""")
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
type="messages",
|
463 |
-
elem_classes=["chat-container"]
|
464 |
-
)
|
465 |
-
|
466 |
-
# Results Column
|
467 |
-
with gr.Column(scale=1):
|
468 |
-
with gr.Group():
|
469 |
-
gr.Markdown("### 📝 Summary of Findings")
|
470 |
-
final_summary = gr.Markdown(
|
471 |
-
"Analysis results will appear here...",
|
472 |
-
elem_classes=["diagnosis-summary"]
|
473 |
-
)
|
474 |
-
|
475 |
-
with gr.Group():
|
476 |
-
gr.Markdown("### 📂 Report Download")
|
477 |
-
download_output = gr.File(
|
478 |
-
label="Full Report",
|
479 |
-
visible=False,
|
480 |
-
interactive=False
|
481 |
-
)
|
482 |
-
|
483 |
-
# Input Section
|
484 |
with gr.Row():
|
|
|
|
|
|
|
|
|
|
|
485 |
file_upload = gr.File(
|
486 |
file_types=[".pdf", ".csv", ".xls", ".xlsx"],
|
487 |
file_count="multiple",
|
488 |
-
label="Upload Patient Records"
|
489 |
-
elem_classes=["file-upload"]
|
490 |
)
|
491 |
-
|
492 |
-
|
493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
494 |
msg_input = gr.Textbox(
|
495 |
placeholder="Ask about potential oversights or upload files...",
|
496 |
show_label=False,
|
497 |
container=False,
|
498 |
-
|
499 |
autofocus=True
|
500 |
)
|
501 |
send_btn = gr.Button(
|
502 |
"Analyze",
|
503 |
variant="primary",
|
504 |
-
|
505 |
-
min_width=100
|
506 |
)
|
507 |
-
|
508 |
-
# Progress Indicator
|
509 |
progress_text = gr.Textbox(
|
510 |
label="Progress Status",
|
511 |
visible=False,
|
512 |
interactive=False
|
513 |
)
|
514 |
|
515 |
-
# Event Handlers
|
516 |
send_btn.click(
|
517 |
self.analyze,
|
518 |
inputs=[msg_input, chatbot, file_upload],
|
|
|
44 |
"TOKENIZERS_PARALLELISM": "false",
|
45 |
"CUDA_LAUNCH_BLOCKING": "1"
|
46 |
})
|
47 |
+
|
48 |
+
# Add src path for txagent
|
49 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
50 |
src_path = os.path.abspath(os.path.join(current_dir, "src"))
|
51 |
sys.path.insert(0, src_path)
|
52 |
+
|
53 |
from txagent.txagent import TxAgent
|
54 |
+
|
55 |
# ==================== UTILITY FUNCTIONS ====================
|
56 |
def sanitize_text(text: str) -> str:
|
57 |
"""Clean and sanitize text input"""
|
|
|
83 |
# ==================== FILE PROCESSING ====================
|
84 |
class FileProcessor:
|
85 |
@staticmethod
|
86 |
+
def extract_pdf_text(file_path: str, cache: Cache) -> str:
|
87 |
+
"""Extract text from PDF with caching"""
|
88 |
+
cache_key = f"pdf_{get_file_hash(file_path)}"
|
89 |
+
if cache_key in cache:
|
90 |
+
return cache[cache_key]
|
91 |
+
|
92 |
try:
|
93 |
with pdfplumber.open(file_path) as pdf:
|
94 |
total_pages = len(pdf.pages)
|
|
|
108 |
batches = [(i, min(i+batch_size, total_pages)) for i in range(0, total_pages, batch_size)]
|
109 |
text_chunks = [""] * total_pages
|
110 |
|
111 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
112 |
futures = [executor.submit(process_page_range, start, end) for start, end in batches]
|
113 |
for future in as_completed(futures):
|
114 |
for page_num, text in future.result():
|
115 |
text_chunks[page_num] = text
|
116 |
|
117 |
+
result = "\n\n".join(filter(None, text_chunks))
|
118 |
+
cache[cache_key] = result
|
119 |
+
return result
|
120 |
except Exception as e:
|
121 |
logger.error(f"PDF processing error: {e}")
|
122 |
return f"PDF processing error: {str(e)}"
|
123 |
|
124 |
@staticmethod
|
125 |
+
def excel_to_data(file_path: str, cache: Cache) -> List[Dict]:
|
126 |
+
"""Convert Excel file to structured data with caching"""
|
127 |
+
cache_key = f"excel_{get_file_hash(file_path)}"
|
128 |
+
if cache_key in cache:
|
129 |
+
return cache[cache_key]
|
130 |
+
|
131 |
try:
|
132 |
df = pd.read_excel(file_path, engine='openpyxl', header=None, dtype=str)
|
133 |
content = df.where(pd.notnull(df), "").astype(str).values.tolist()
|
134 |
+
result = [{"filename": os.path.basename(file_path), "rows": content, "type": "excel"}]
|
135 |
+
cache[cache_key] = result
|
136 |
+
return result
|
137 |
except Exception as e:
|
138 |
logger.error(f"Excel processing error: {e}")
|
139 |
return [{"error": f"Excel processing error: {str(e)}"}]
|
140 |
|
141 |
@staticmethod
|
142 |
+
def csv_to_data(file_path: str, cache: Cache) -> List[Dict]:
|
143 |
+
"""Convert CSV file to structured data with caching"""
|
144 |
+
cache_key = f"csv_{get_file_hash(file_path)}"
|
145 |
+
if cache_key in cache:
|
146 |
+
return cache[cache_key]
|
147 |
+
|
148 |
try:
|
149 |
chunks = []
|
150 |
for chunk in pd.read_csv(
|
|
|
155 |
|
156 |
df = pd.concat(chunks) if chunks else pd.DataFrame()
|
157 |
content = df.where(pd.notnull(df), "").astype(str).values.tolist()
|
158 |
+
result = [{"filename": os.path.basename(file_path), "rows": content, "type": "csv"}]
|
159 |
+
cache[cache_key] = result
|
160 |
+
return result
|
161 |
except Exception as e:
|
162 |
logger.error(f"CSV processing error: {e}")
|
163 |
return [{"error": f"CSV processing error: {str(e)}"}]
|
164 |
|
165 |
@classmethod
|
166 |
+
def process_file(cls, file_path: str, file_type: str, cache: Cache) -> List[Dict]:
|
167 |
"""Route file processing based on type"""
|
168 |
processors = {
|
169 |
"pdf": cls.extract_pdf_text,
|
|
|
176 |
return [{"error": f"Unsupported file type: {file_type}"}]
|
177 |
|
178 |
try:
|
179 |
+
result = processors[file_type](file_path, cache)
|
180 |
if file_type == "pdf":
|
181 |
return [{
|
182 |
"filename": os.path.basename(file_path),
|
|
|
195 |
self.tokenizer = AutoTokenizer.from_pretrained("mims-harvard/TxAgent-T1-Llama-3.1-8B")
|
196 |
self.cache = Cache(DIRECTORIES["cache"], size_limit=10*1024**3)
|
197 |
|
198 |
+
def chunk_text(self, text: str, max_tokens: int = 1200) -> List[str]:
|
199 |
"""Split text into token-limited chunks"""
|
200 |
tokens = self.tokenizer.encode(text)
|
201 |
return [
|
|
|
206 |
def clean_response(self, text: str) -> str:
|
207 |
"""Clean and format model response"""
|
208 |
text = sanitize_text(text)
|
209 |
+
text = re.sub(r"\[.*?\]|\bNone\b", "", text)
|
|
|
|
|
|
|
|
|
210 |
|
211 |
diagnoses = []
|
212 |
in_diagnoses = False
|
|
|
254 |
if diagnosis and not re.match(r"No issues identified", diagnosis, re.IGNORECASE):
|
255 |
diagnoses.append(diagnosis)
|
256 |
|
257 |
+
unique_diagnoses = list(dict.fromkeys(diagnoses))
|
258 |
|
259 |
if not unique_diagnoses:
|
260 |
return "No missed diagnoses were identified in the provided records."
|
|
|
320 |
full_response += cleaned + " "
|
321 |
yield {"role": "assistant", "content": full_response}
|
322 |
|
323 |
+
def analyze(self, message: str, history: List[dict], files: List) -> Generator[tuple, None, None]:
|
324 |
"""Main analysis pipeline with proper output formatting"""
|
325 |
+
chatbot_output = history.copy()
|
326 |
+
download_output = None
|
327 |
+
final_summary = ""
|
328 |
+
progress_text = {"value": "Starting analysis...", "visible": True}
|
|
|
|
|
|
|
|
|
329 |
|
330 |
try:
|
331 |
# Add user message to history
|
332 |
+
chatbot_output.append({"role": "user", "content": message})
|
333 |
+
yield (chatbot_output, download_output, final_summary, progress_text)
|
|
|
334 |
|
335 |
# Process uploaded files
|
336 |
+
.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 0
|
337 |
extracted = []
|
338 |
file_hash_value = ""
|
339 |
|
340 |
if files:
|
341 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
342 |
futures = []
|
343 |
for f in files:
|
344 |
file_type = f.name.split(".")[-1].lower()
|
345 |
+
futures.append(executor.submit(self.file_processor.process_file, f.name, file_type, self.text_processor.cache))
|
346 |
|
347 |
for i, future in enumerate(as_completed(futures), 1):
|
348 |
try:
|
349 |
extracted.extend(future.result())
|
350 |
+
progress_text = self._update_progress(i, len(files), "Processing files")
|
351 |
+
yield (chatbot_output, download_output, final_summary, progress_text)
|
352 |
except Exception as e:
|
353 |
logger.error(f"File processing error: {e}")
|
354 |
extracted.append({"error": f"Error processing file: {str(e)}"})
|
355 |
|
356 |
file_hash_value = get_file_hash(files[0].name) if files else ""
|
357 |
+
chatbot_output.append({"role": "assistant", "content": "✅ File processing complete"})
|
358 |
+
progress_text = self._update_progress(len(files), len(files), "Files processed")
|
359 |
+
yield (chatbot_output, download_output, final_summary, progress_text)
|
|
|
|
|
|
|
360 |
|
361 |
# Analyze content
|
362 |
text_content = "\n".join(json.dumps(item) for item in extracted)
|
|
|
371 |
are found, state 'No missed diagnoses identified'.
|
372 |
|
373 |
Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
374 |
+
{chunk[:1200]}
|
375 |
"""
|
376 |
+
chatbot_output.append({"role": "assistant", "content": ""})
|
377 |
+
progress_text = self._update_progress(chunk_idx, len(chunks), "Analyzing")
|
378 |
+
yield (chatbot_output, download_output, final_summary, progress_text)
|
|
|
|
|
|
|
379 |
|
380 |
# Stream response
|
381 |
chunk_response = ""
|
382 |
+
for update in self.process_response_stream(prompt, chatbot_output):
|
383 |
+
chatbot_output[-1] = update
|
384 |
chunk_response = update["content"]
|
385 |
+
progress_text = self._update_progress(chunk_idx, len(chunks), "Analyzing")
|
386 |
+
yield (chatbot_output, download_output, final_summary, progress_text)
|
|
|
|
|
|
|
387 |
|
388 |
combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
|
389 |
torch.cuda.empty_cache()
|
390 |
gc.collect()
|
391 |
|
392 |
# Generate final outputs
|
393 |
+
final_summary = self.text_processor.summarize_results(combined_response)
|
394 |
report_path = os.path.join(DIRECTORIES["reports"], f"{file_hash_value}_report.txt") if file_hash_value else None
|
395 |
|
396 |
if report_path:
|
397 |
with open(report_path, "w", encoding="utf-8") as f:
|
398 |
+
f.write(combined_response + "\n\n" + final_summary)
|
399 |
|
400 |
+
download_output = report_path if report_path and os.path.exists(report_path) else None
|
401 |
+
progress_text = {"visible": False}
|
402 |
+
yield (chatbot_output, download_output, final_summary, progress_text)
|
|
|
|
|
|
|
403 |
|
404 |
except Exception as e:
|
405 |
logger.error(f"Analysis error: {e}")
|
406 |
+
chatbot_output.append({"role": "assistant", "content": f"❌ Error: {str(e)}"})
|
407 |
+
final_summary = f"Error occurred: {str(e)}"
|
408 |
+
progress_text = {"visible": False}
|
409 |
+
yield (chatbot_output, download_output, final_summary, progress_text)
|
|
|
|
|
|
|
410 |
|
411 |
def _update_progress(self, current: int, total: int, stage: str = "") -> Dict[str, Any]:
|
412 |
"""Format progress update for UI"""
|
413 |
progress = f"{stage} - {current}/{total}" if stage else f"{current}/{total}"
|
414 |
+
return {"value": progress, "visible": True}
|
415 |
|
416 |
def create_interface(self):
|
417 |
+
"""Create Gradio interface with ChatGPT-like design"""
|
418 |
+
css = """
|
419 |
+
body, .gradio-container {
|
420 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
421 |
+
background: var(--background);
|
422 |
+
color: var(--text-color);
|
423 |
+
}
|
424 |
+
.gradio-container {
|
425 |
+
max-width: 800px;
|
426 |
+
margin: 0 auto;
|
427 |
+
padding: 20px;
|
428 |
+
}
|
429 |
+
.chat-container {
|
430 |
+
background: var(--chat-bg);
|
431 |
+
border-radius: 12px;
|
432 |
+
padding: 20px;
|
433 |
+
height: 80vh;
|
434 |
+
overflow-y: auto;
|
435 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
436 |
+
}
|
437 |
+
.message {
|
438 |
+
margin: 10px 0;
|
439 |
+
padding: 12px 16px;
|
440 |
+
border-radius: 12px;
|
441 |
+
max-width: 80%;
|
442 |
+
transition: all 0.2s ease;
|
443 |
+
}
|
444 |
+
.message.user {
|
445 |
+
background: #007bff;
|
446 |
+
color: white;
|
447 |
+
margin-left: auto;
|
448 |
+
}
|
449 |
+
.message.assistant {
|
450 |
+
background: var(--message-bg);
|
451 |
+
color: var(--text-color);
|
452 |
+
}
|
453 |
+
.input-container {
|
454 |
+
display: flex;
|
455 |
+
align-items: center;
|
456 |
+
margin-top: 20px;
|
457 |
+
background: var(--chat-bg);
|
458 |
+
padding: 10px 20px;
|
459 |
+
border-radius: 25px;
|
460 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
461 |
+
}
|
462 |
+
.input-textbox {
|
463 |
+
flex-grow: 1;
|
464 |
+
border: none;
|
465 |
+
background: transparent;
|
466 |
+
color: var(--text-color);
|
467 |
+
outline: none;
|
468 |
+
}
|
469 |
+
.send-btn {
|
470 |
+
background: #007bff;
|
471 |
+
color: white;
|
472 |
+
border: none;
|
473 |
+
border-radius: 20px;
|
474 |
+
padding: 8px 16px;
|
475 |
+
margin-left: 10px;
|
476 |
+
}
|
477 |
+
.send-btn:hover {
|
478 |
+
background: #0056b3;
|
479 |
+
}
|
480 |
+
.sidebar {
|
481 |
+
background: var(--sidebar-bg);
|
482 |
+
padding: 20px;
|
483 |
+
border-radius: 12px;
|
484 |
+
margin-top: 20px;
|
485 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
486 |
+
}
|
487 |
+
.sidebar-hidden {
|
488 |
+
display: none;
|
489 |
+
}
|
490 |
+
.header {
|
491 |
+
text-align: center;
|
492 |
+
margin-bottom: 20px;
|
493 |
+
}
|
494 |
+
.theme-toggle {
|
495 |
+
position: absolute;
|
496 |
+
top: 20px;
|
497 |
+
right: 20px;
|
498 |
+
background: #007bff;
|
499 |
+
color: white;
|
500 |
+
border: none;
|
501 |
+
border-radius: 20px;
|
502 |
+
padding: 8px 16px;
|
503 |
+
}
|
504 |
+
:root {
|
505 |
+
--background: #ffffff;
|
506 |
+
--text-color: #333333;
|
507 |
+
--chat-bg: #f7f7f8;
|
508 |
+
--message-bg: #e5e5ea;
|
509 |
+
--sidebar-bg: #f1f1f1;
|
510 |
+
}
|
511 |
+
@media (prefers-color-scheme: dark) {
|
512 |
+
:root {
|
513 |
+
--background: #1e2a44;
|
514 |
+
--text-color: #ffffff;
|
515 |
+
--chat-bg: #2d3b55;
|
516 |
+
--message-bg: #3e4c6a;
|
517 |
+
--sidebar-bg: #2a3650;
|
518 |
}
|
519 |
+
}
|
520 |
+
@media (max-width: 600px) {
|
521 |
+
.gradio-container {
|
522 |
+
padding: 10px;
|
523 |
+
}
|
524 |
+
.chat-container {
|
525 |
+
height: 70vh;
|
526 |
+
}
|
527 |
+
.input-container {
|
528 |
+
flex-direction: column;
|
529 |
+
gap: 10px;
|
530 |
+
}
|
531 |
+
.send-btn {
|
532 |
+
width: 100%;
|
533 |
+
margin-left: 0;
|
534 |
}
|
535 |
+
}
|
536 |
+
"""
|
537 |
+
|
538 |
+
js = """
|
539 |
+
function toggleTheme() {
|
540 |
+
const root = document.documentElement;
|
541 |
+
const isDark = root.style.getPropertyValue('--background') === '#1e2a44';
|
542 |
+
root.style.setProperty('--background', isDark ? '#ffffff' : '#1e2a44');
|
543 |
+
root.style.setProperty('--text-color', isDark ? '#333333' : '#ffffff');
|
544 |
+
root.style.setProperty('--chat-bg', isDark ? '#f7f7f8' : '#2d3b55');
|
545 |
+
root.style.setProperty('--message-bg', isDark ? '#e5e5ea' : '#3e4c6a');
|
546 |
+
root.style.setProperty('--sidebar-bg', isDark ? '#f1f1f1' : '#2a3650');
|
547 |
+
localStorage.setItem('theme', isDark ? 'light' : 'dark');
|
548 |
+
}
|
549 |
+
|
550 |
+
function toggleSidebar() {
|
551 |
+
const sidebar = document.querySelector('.sidebar');
|
552 |
+
sidebar.classList.toggle('sidebar-hidden');
|
553 |
+
}
|
554 |
+
|
555 |
+
document.addEventListener('DOMContentLoaded', () => {
|
556 |
+
const savedTheme = localStorage.getItem('theme');
|
557 |
+
if (savedTheme === 'dark') toggleTheme();
|
558 |
+
document.querySelector('.sidebar').classList.add('sidebar-hidden');
|
559 |
+
});
|
560 |
+
"""
|
561 |
+
|
562 |
+
with gr.Blocks(theme=gr.themes.Default(), css=css, js=js, title="Clinical Oversight Assistant") as app:
|
563 |
+
gr.HTML("""
|
564 |
+
<div class='header'>
|
565 |
+
<h1 style='color: var(--text-color);'>🩺 Clinical Oversight Assistant</h1>
|
566 |
+
<p style='color: var(--text-color); opacity: 0.7;'>
|
567 |
+
AI-powered analysis of patient records for missed diagnoses
|
568 |
</p>
|
569 |
</div>
|
570 |
""")
|
571 |
+
gr.Button("Toggle Light/Dark Mode", elem_classes="theme-toggle").click(
|
572 |
+
None, None, None, _js="toggleTheme"
|
573 |
+
)
|
574 |
+
|
575 |
+
with gr.Column(elem_classes="chat-container"):
|
576 |
+
chatbot = gr.Chatbot(
|
577 |
+
label="Clinical Analysis",
|
578 |
+
height="100%",
|
579 |
+
show_copy_button=True,
|
580 |
+
type="messages",
|
581 |
+
elem_classes="chatbot"
|
582 |
+
)
|
583 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
584 |
with gr.Row():
|
585 |
+
gr.Button("Show/Hide Tools", variant="secondary").click(
|
586 |
+
None, None, None, _js="toggleSidebar"
|
587 |
+
)
|
588 |
+
|
589 |
+
with gr.Column(elem_classes="sidebar"):
|
590 |
file_upload = gr.File(
|
591 |
file_types=[".pdf", ".csv", ".xls", ".xlsx"],
|
592 |
file_count="multiple",
|
593 |
+
label="Upload Patient Records"
|
|
|
594 |
)
|
595 |
+
gr.Markdown("### 📝 Summary of Findings")
|
596 |
+
final_summary = gr.Markdown(
|
597 |
+
"Analysis results will appear here..."
|
598 |
+
)
|
599 |
+
gr.Markdown("### 📂 Report Download")
|
600 |
+
download_output = gr.File(
|
601 |
+
label="Full Report",
|
602 |
+
visible=False,
|
603 |
+
interactive=False
|
604 |
+
)
|
605 |
+
|
606 |
+
with gr.Row(elem_classes="input-container"):
|
607 |
msg_input = gr.Textbox(
|
608 |
placeholder="Ask about potential oversights or upload files...",
|
609 |
show_label=False,
|
610 |
container=False,
|
611 |
+
elem_classes="input-textbox",
|
612 |
autofocus=True
|
613 |
)
|
614 |
send_btn = gr.Button(
|
615 |
"Analyze",
|
616 |
variant="primary",
|
617 |
+
elem_classes="send-btn"
|
|
|
618 |
)
|
619 |
+
|
|
|
620 |
progress_text = gr.Textbox(
|
621 |
label="Progress Status",
|
622 |
visible=False,
|
623 |
interactive=False
|
624 |
)
|
625 |
|
|
|
626 |
send_btn.click(
|
627 |
self.analyze,
|
628 |
inputs=[msg_input, chatbot, file_upload],
|