Ali2206 commited on
Commit
a71a831
·
verified ·
1 Parent(s): fcebf54

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +335 -792
app.py CHANGED
@@ -4,7 +4,7 @@ import pandas as pd
4
  import pdfplumber
5
  import json
6
  import gradio as gr
7
- from typing import List, Dict, Generator, Any, Optional
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
  import hashlib
10
  import shutil
@@ -15,858 +15,401 @@ import logging
15
  import torch
16
  import gc
17
  from diskcache import Cache
 
18
  from transformers import AutoTokenizer
19
- from pathlib import Path
20
 
21
- # ==================== CONFIGURATION ====================
22
  logging.basicConfig(level=logging.INFO)
23
  logger = logging.getLogger(__name__)
24
 
25
- # Directory Setup
26
- BASE_DIR = Path("/data/hf_cache")
27
- DIRECTORIES = {
28
- "models": BASE_DIR / "txagent_models",
29
- "tools": BASE_DIR / "tool_cache",
30
- "cache": BASE_DIR / "cache",
31
- "reports": BASE_DIR / "reports",
32
- "vllm": BASE_DIR / "vllm_cache"
33
- }
34
-
35
- for dir_path in DIRECTORIES.values():
36
- dir_path.mkdir(parents=True, exist_ok=True)
37
-
38
- # Environment Configuration
39
- os.environ.update({
40
- "HF_HOME": str(DIRECTORIES["models"]),
41
- "TRANSFORMERS_CACHE": str(DIRECTORIES["models"]),
42
- "VLLM_CACHE_DIR": str(DIRECTORIES["vllm"]),
43
- "TOKENIZERS_PARALLELISM": "false",
44
- "CUDA_LAUNCH_BLOCKING": "1"
45
- })
46
-
47
- # Add src path for txagent
48
  current_dir = os.path.dirname(os.path.abspath(__file__))
49
  src_path = os.path.abspath(os.path.join(current_dir, "src"))
50
  sys.path.insert(0, src_path)
51
 
52
  from txagent.txagent import TxAgent
53
 
54
- # ==================== CORE COMPONENTS ====================
55
- class FileProcessor:
56
- """Handles all file processing operations"""
57
-
58
- @staticmethod
59
- def extract_pdf_content(file_path: str) -> str:
60
- """Extract text from PDF with parallel processing"""
61
- try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  with pdfplumber.open(file_path) as pdf:
63
- total_pages = len(pdf.pages)
64
- if not total_pages:
65
- return ""
66
-
67
- def process_batch(start: int, end: int) -> List[tuple]:
68
- results = []
69
- with pdfplumber.open(file_path) as pdf:
70
- for page in pdf.pages[start:end]:
71
- page_num = start + pdf.pages.index(page)
72
- text = page.extract_text() or ""
73
- results.append((page_num, f"=== Page {page_num + 1} ===\n{text.strip()}"))
74
- return results
75
-
76
- batch_size = min(10, total_pages)
77
- batches = [(i, min(i + batch_size, total_pages)) for i in range(0, total_pages, batch_size)]
78
- text_chunks = [""] * total_pages
79
-
80
- with ThreadPoolExecutor(max_workers=min(6, os.cpu_count() or 4)) as executor:
81
- futures = [executor.submit(process_batch, start, end) for start, end in batches]
82
- for future in as_completed(futures):
83
- for page_num, text in future.result():
84
- text_chunks[page_num] = text
85
-
86
- return "\n\n".join(filter(None, text_chunks))
87
- except Exception as e:
88
- logger.error(f"PDF extraction failed: {e}")
89
- return f"PDF processing error: {str(e)}"
90
-
91
- @staticmethod
92
- def process_tabular_data(file_path: str, file_type: str) -> List[Dict]:
93
- """Process Excel or CSV files"""
94
  try:
95
- if file_type == "csv":
96
- chunks = pd.read_csv(
97
- file_path,
98
- header=None,
99
- dtype=str,
100
- encoding_errors='replace',
101
- on_bad_lines='skip',
102
- chunksize=10000
103
- )
104
- df = pd.concat(chunks) if chunks else pd.DataFrame()
105
- else: # Excel
106
- try:
107
- df = pd.read_excel(file_path, engine='openpyxl', header=None, dtype=str)
108
- except:
109
- df = pd.read_excel(file_path, engine='xlrd', header=None, dtype=str)
110
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  return [{
112
  "filename": os.path.basename(file_path),
113
- "rows": df.where(pd.notnull(df), "").astype(str).values.tolist(),
114
- "type": file_type
 
115
  }]
116
- except Exception as e:
117
- logger.error(f"{file_type.upper()} processing failed: {e}")
118
- return [{"error": f"{file_type.upper()} processing error: {str(e)}"}]
119
-
120
- @classmethod
121
- def handle_upload(cls, file_path: str, file_type: str) -> List[Dict]:
122
- """Route file processing based on type"""
123
- processor_map = {
124
- "pdf": cls.extract_pdf_content,
125
- "xls": lambda x: cls.process_tabular_data(x, "excel"),
126
- "xlsx": lambda x: cls.process_tabular_data(x, "excel"),
127
- "csv": lambda x: cls.process_tabular_data(x, "csv")
128
- }
129
-
130
- if file_type not in processor_map:
131
  return [{"error": f"Unsupported file type: {file_type}"}]
132
-
133
- try:
134
- result = processor_map[file_type](file_path)
135
- if file_type == "pdf":
136
- return [{
137
- "filename": os.path.basename(file_path),
138
- "content": result,
139
- "type": "pdf"
140
- }]
141
- return result
142
- except Exception as e:
143
- logger.error(f"File processing failed: {e}")
144
- return [{"error": f"File processing error: {str(e)}"}]
145
-
146
- class TextAnalyzer:
147
- """Handles text processing and analysis"""
148
-
149
- def __init__(self):
150
- self.tokenizer = AutoTokenizer.from_pretrained("mims-harvard/TxAgent-T1-Llama-3.1-8B")
151
- self.cache = Cache(DIRECTORIES["cache"], size_limit=10*1024**3)
152
-
153
- def chunk_content(self, text: str, max_tokens: int = 1800) -> List[str]:
154
- """Split text into token-limited chunks"""
155
- tokens = self.tokenizer.encode(text)
156
- return [
157
- self.tokenizer.decode(tokens[i:i+max_tokens])
158
- for i in range(0, len(tokens), max_tokens)
159
- ]
160
-
161
- def clean_output(self, text: str) -> str:
162
- """Clean and format model response"""
163
- text = text.encode("utf-8", "ignore").decode("utf-8")
164
- text = re.sub(
165
- r"\[.*?\]|\bNone\b|To analyze the patient record excerpt.*?medications\."
166
- r"|Since the previous attempts.*?\.|I need to.*?medications\."
167
- r"|Retrieving tools.*?\.", "", text, flags=re.DOTALL
168
  )
169
-
170
- diagnoses = []
171
- in_section = False
172
-
173
- for line in text.splitlines():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  line = line.strip()
175
  if not line:
176
  continue
177
  if re.match(r"###\s*Missed Diagnoses", line):
178
- in_section = True
179
  continue
180
  if re.match(r"###\s*(Medication Conflicts|Incomplete Assessments|Urgent Follow-up)", line):
181
- in_section = False
182
  continue
183
- if in_section and re.match(r"-\s*.+", line):
184
  diagnosis = re.sub(r"^\-\s*", "", line).strip()
185
  if diagnosis and not re.match(r"No issues identified", diagnosis, re.IGNORECASE):
186
  diagnoses.append(diagnosis)
187
-
188
- return " ".join(diagnoses) if diagnoses else ""
189
-
190
- def generate_summary(self, analysis: str) -> str:
191
- """Create concise clinical summary"""
192
- findings = []
193
- for chunk in analysis.split("--- Analysis for Chunk"):
194
- chunk = chunk.strip()
195
- if not chunk or "No oversights identified" in chunk:
196
- continue
197
-
198
- in_section = False
199
- for line in chunk.splitlines():
200
- line = line.strip()
201
- if not line:
202
- continue
203
- if re.match(r"###\s*Missed Diagnoses", line):
204
- in_section = True
205
- continue
206
- if re.match(r"###\s*(Medication Conflicts|Incomplete Assessments|Urgent Follow-up)", line):
207
- in_section = False
208
- continue
209
- if in_section and re.match(r"-\s*.+", line):
210
- finding = re.sub(r"^\-\s*", "", line).strip()
211
- if finding and not re.match(r"No issues identified", finding, re.IGNORECASE):
212
- findings.append(finding)
213
-
214
- unique_findings = list(dict.fromkeys(findings))
215
-
216
- if not unique_findings:
217
- return "No clinical concerns identified in the provided records."
218
-
219
- if len(unique_findings) > 1:
220
- summary = "Potential concerns include: " + ", ".join(unique_findings[:-1])
221
- summary += f", and {unique_findings[-1]}"
222
- else:
223
- summary = "Potential concern identified: " + unique_findings[0]
224
-
225
- return summary + ". Recommend urgent clinical review."
226
 
227
- class ClinicalAgent:
228
- """Main application controller"""
229
-
230
- def __init__(self):
231
- self.agent = self._init_agent()
232
- self.file_processor = FileProcessor()
233
- self.text_analyzer = TextAnalyzer()
234
-
235
- def _init_agent(self) -> Any:
236
- """Initialize the AI agent"""
237
- logger.info("Initializing clinical agent...")
238
- self._log_system_status("pre-init")
239
-
240
- tool_path = DIRECTORIES["tools"] / "new_tool.json"
241
- if not tool_path.exists():
242
- default_tools = Path("data/new_tool.json")
243
- if default_tools.exists():
244
- shutil.copy(default_tools, tool_path)
245
-
246
- agent = TxAgent(
247
- model_name="mims-harvard/TxAgent-T1-Llama-3.1-8B",
248
- rag_model_name="mims-harvard/ToolRAG-T1-GTE-Qwen2-1.5B",
249
- tool_files_dict={"new_tool": str(tool_path)},
250
- force_finish=True,
251
- enable_checker=False,
252
- step_rag_num=4,
253
- seed=100,
254
- additional_default_tools=[],
255
- )
256
- agent.init_model()
257
-
258
- self._log_system_status("post-init")
259
- logger.info("Clinical agent ready")
260
- return agent
261
 
262
- def _log_system_status(self, phase: str) -> None:
263
- """Log system resource utilization"""
264
- try:
265
- cpu = psutil.cpu_percent(interval=1)
266
- mem = psutil.virtual_memory()
267
- logger.info(f"[{phase}] CPU: {cpu:.1f}% | RAM: {mem.used//(1024**2)}MB/{mem.total//(1024**2)}MB")
268
-
269
- gpu_info = subprocess.run(
270
- ["nvidia-smi", "--query-gpu=memory.used,memory.total,utilization.gpu",
271
- "--format=csv,nounits,noheader"],
272
- capture_output=True, text=True
273
- )
274
- if gpu_info.returncode == 0:
275
- used, total, util = gpu_info.stdout.strip().split(", ")
276
- logger.info(f"[{phase}] GPU: {used}MB/{total}MB | Util: {util}%")
277
- except Exception as e:
278
- logger.error(f"Resource monitoring failed: {e}")
279
-
280
- def process_stream(self, prompt: str, history: List[Dict]) -> Generator[Dict, None, None]:
281
- """Stream the agent's responses"""
282
- full_response = ""
283
- for chunk in self.agent.run_gradio_chat(prompt, [], 0.2, 512, 2048, False, []):
284
- if not chunk:
285
- continue
286
-
287
- if isinstance(chunk, list):
288
- for msg in chunk:
289
- if hasattr(msg, 'content') and msg.content:
290
- cleaned = self.text_analyzer.clean_output(msg.content)
291
- if cleaned:
292
- full_response += cleaned + " "
293
- yield {"role": "assistant", "content": full_response}
294
- elif isinstance(chunk, str) and chunk.strip():
295
- cleaned = self.text_analyzer.clean_output(chunk)
296
- if cleaned:
297
- full_response += cleaned + " "
298
- yield {"role": "assistant", "content": full_response}
299
 
300
- def analyze_records(self, message: str, history: List[Dict], files: List) -> Generator[tuple, None, None]:
301
- """Main analysis workflow"""
302
- outputs = {
303
- "chatbot": history.copy(),
304
- "download_output": None,
305
- "final_summary": "",
306
- "progress": {"value": "Initializing...", "visible": True}
307
- }
308
- yield (outputs["chatbot"], outputs["download_output"], outputs["final_summary"], outputs["progress"])
309
-
310
- try:
311
- # Add user message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  history.append({"role": "user", "content": message})
313
- outputs["chatbot"] = history
314
- yield (outputs["chatbot"], outputs["download_output"], outputs["final_summary"], outputs["progress"])
315
-
316
- # Process files
317
  extracted = []
318
- file_hash = ""
319
 
320
  if files:
 
321
  with ThreadPoolExecutor(max_workers=4) as executor:
322
  futures = []
323
  for f in files:
324
- file_type = Path(f.name).suffix[1:].lower()
325
  futures.append(executor.submit(
326
- self.file_processor.handle_upload,
327
- f.name,
328
  file_type
329
  ))
330
 
331
- for i, future in enumerate(as_completed(futures), 1):
332
  try:
333
  extracted.extend(future.result())
334
- outputs["progress"] = self._format_progress(i, len(files), "Processing files")
335
- yield (outputs["chatbot"], outputs["download_output"], outputs["final_summary"], outputs["progress"])
336
  except Exception as e:
337
- logger.error(f"File processing failed: {e}")
338
- extracted.append({"error": str(e)})
339
-
340
- if files and os.path.exists(files[0].name):
341
- file_hash = hashlib.md5(open(files[0].name, "rb").read()).hexdigest()
342
-
343
- history.append({"role": "assistant", "content": "✅ Files processed successfully"})
344
- outputs.update({
345
- "chatbot": history,
346
- "progress": self._format_progress(len(files), len(files), "Files processed")
347
- })
348
- yield (outputs["chatbot"], outputs["download_output"], outputs["final_summary"], outputs["progress"])
349
-
350
- # Analyze content
351
- text_content = "\n".join(json.dumps(item) for item in extracted)
352
- chunks = self.text_analyzer.chunk_content(text_content)
353
- full_analysis = ""
354
-
355
- for idx, chunk in enumerate(chunks, 1):
356
- prompt = f"""
357
- Analyze this clinical documentation for potential missed diagnoses. Provide:
358
- 1. Specific clinical findings with references (e.g., "Elevated BP (160/95) on page 3")
359
- 2. Their clinical significance
360
- 3. Urgency of review
361
- Use concise, continuous prose without bullet points. If no concerns, state "No missed diagnoses identified."
362
-
363
- Document Excerpt (Part {idx}/{len(chunks)}):
364
- {chunk[:1750]}
365
- """
366
- history.append({"role": "assistant", "content": ""})
367
- outputs.update({
368
- "chatbot": history,
369
- "progress": self._format_progress(idx, len(chunks), "Analyzing")
370
- })
371
- yield (outputs["chatbot"], outputs["download_output"], outputs["final_summary"], outputs["progress"])
372
-
373
- # Stream analysis
374
- chunk_response = ""
375
- for update in self.process_stream(prompt, history):
376
- history[-1] = update
377
- chunk_response = update["content"]
378
- outputs.update({
379
- "chatbot": history,
380
- "progress": self._format_progress(idx, len(chunks), "Analyzing")
381
- })
382
- yield (outputs["chatbot"], outputs["download_output"], outputs["final_summary"], outputs["progress"])
383
 
384
- full_analysis += f"--- Analysis Part {idx} ---\n{chunk_response}\n"
385
- torch.cuda.empty_cache()
386
- gc.collect()
387
 
388
- # Final outputs
389
- summary = self.text_analyzer.generate_summary(full_analysis)
390
- report_path = DIRECTORIES["reports"] / f"{file_hash}_report.txt" if file_hash else None
391
 
392
- if report_path:
393
- with open(report_path, "w", encoding="utf-8") as f:
394
- f.write(full_analysis + "\n\nSUMMARY:\n" + summary)
 
395
 
396
- outputs.update({
397
- "download_output": str(report_path) if report_path and report_path.exists() else None,
398
- "final_summary": summary,
399
- "progress": {"visible": False}
400
- })
401
- yield (outputs["chatbot"], outputs["download_output"], outputs["final_summary"], outputs["progress"])
402
-
403
- except Exception as e:
404
- logger.error(f"Analysis failed: {e}")
405
- history.append({"role": "assistant", "content": f"❌ Analysis error: {str(e)}"})
406
- outputs.update({
407
- "chatbot": history,
408
- "final_summary": f"Error: {str(e)}",
409
- "progress": {"visible": False}
410
- })
411
- yield (outputs["chatbot"], outputs["download_output"], outputs["final_summary"], outputs["progress"])
412
-
413
- def _format_progress(self, current: int, total: int, stage: str = "") -> Dict[str, Any]:
414
- """Format progress update for UI"""
415
- status = f"{stage} - {current}/{total}" if stage else f"{current}/{total}"
416
- return {"value": status, "visible": True, "label": f"Progress: {status}"}
417
-
418
- def create_interface(self) -> gr.Blocks:
419
- """Build the Gradio interface"""
420
- css = """
421
- /* ==================== BASE STYLES ==================== */
422
- :root {
423
- --primary-color: #4f46e5;
424
- --primary-dark: #4338ca;
425
- --border-radius: 8px;
426
- --transition: all 0.3s ease;
427
- --shadow: 0 4px 12px rgba(0,0,0,0.1);
428
- --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
429
- --background: #ffffff;
430
- --text-color: #1e293b;
431
- --chat-bg: #f8fafc;
432
- --message-bg: #e2e8f0;
433
- --panel-bg: rgba(248, 250, 252, 0.9);
434
- --panel-dark-bg: rgba(30, 41, 59, 0.9);
435
- }
436
-
437
- [data-theme="dark"] {
438
- --background: #1e2a44;
439
- --text-color: #f1f5f9;
440
- --chat-bg: #2d3b55;
441
- --message-bg: #475569;
442
- --panel-bg: var(--panel-dark-bg);
443
- }
444
-
445
- body, .gradio-container {
446
- font-family: var(--font-family);
447
- background: var(--background);
448
- color: var(--text-color);
449
- margin: 0;
450
- padding: 0;
451
- transition: var(--transition);
452
- }
453
-
454
- /* ==================== LAYOUT ==================== */
455
- .gradio-container {
456
- max-width: 1200px;
457
- margin: 0 auto;
458
- padding: 1.5rem;
459
- display: flex;
460
- flex-direction: column;
461
- gap: 1.5rem;
462
- }
463
-
464
- .chat-container {
465
- background: var(--chat-bg);
466
- border-radius: var(--border-radius);
467
- border: 1px solid #e2e8f0;
468
- padding: 1.5rem;
469
- min-height: 50vh;
470
- max-height: 80vh;
471
- overflow-y: auto;
472
- box-shadow: var(--shadow);
473
- margin-bottom: 4rem;
474
- }
475
-
476
- .summary-panel {
477
- background: var(--panel-bg);
478
- border-left: 4px solid var(--primary-color);
479
- padding: 1rem;
480
- border-radius: var(--border-radius);
481
- margin-bottom: 1rem;
482
- box-shadow: var(--shadow);
483
- backdrop-filter: blur(8px);
484
- }
485
-
486
- .upload-area {
487
- border: 2px dashed #cbd5e1;
488
- border-radius: var(--border-radius);
489
- padding: 1.5rem;
490
- margin: 0.75rem 0;
491
- transition: var(--transition);
492
- }
493
-
494
- .upload-area:hover {
495
- border-color: var(--primary-color);
496
- background: rgba(79, 70, 229, 0.05);
497
- }
498
-
499
- /* ==================== COMPONENTS ==================== */
500
- .chat__message {
501
- margin: 0.75rem 0;
502
- padding: 0.75rem 1rem;
503
- border-radius: var(--border-radius);
504
- max-width: 85%;
505
- transition: var(--transition);
506
- background: var(--message-bg);
507
- border: 1px solid rgba(0,0,0,0.05);
508
- animation: messageFade 0.3s ease;
509
- }
510
-
511
- .chat__message:hover {
512
- transform: translateY(-2px);
513
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
514
- }
515
-
516
- .chat__message.user {
517
- background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
518
- color: white;
519
- margin-left: auto;
520
- }
521
-
522
- .chat__message.assistant {
523
- background: var(--message-bg);
524
- color: var(--text-color);
525
- }
526
-
527
- .input-container {
528
- display: flex;
529
- align-items: center;
530
- gap: 0.75rem;
531
- background: var(--chat-bg);
532
- padding: 0.75rem 1rem;
533
- border-radius: 1.5rem;
534
- box-shadow: var(--shadow);
535
- position: sticky;
536
- bottom: 1rem;
537
- z-index: 10;
538
- }
539
-
540
- .input__textbox {
541
- flex-grow: 1;
542
- border: none;
543
- background: transparent;
544
- color: var(--text-color);
545
- outline: none;
546
- font-size: 1rem;
547
- }
548
-
549
- .input__textbox:focus {
550
- border-bottom: 2px solid var(--primary-color);
551
- }
552
-
553
- .submit-btn {
554
- background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
555
- color: white;
556
- border: none;
557
- border-radius: 1rem;
558
- padding: 0.5rem 1.25rem;
559
- font-size: 0.9rem;
560
- transition: var(--transition);
561
- }
562
-
563
- .submit-btn:hover {
564
- transform: scale(1.05);
565
- }
566
-
567
- .submit-btn:active {
568
- animation: glow 0.3s ease;
569
- }
570
-
571
- .tooltip {
572
- position: relative;
573
- }
574
-
575
- .tooltip:hover::after {
576
- content: attr(data-tip);
577
- position: absolute;
578
- top: -2.5rem;
579
- left: 50%;
580
- transform: translateX(-50%);
581
- background: #1e293b;
582
- color: white;
583
- padding: 0.4rem 0.8rem;
584
- border-radius: 0.4rem;
585
- font-size: 0.85rem;
586
- max-width: 200px;
587
- white-space: normal;
588
- text-align: center;
589
- z-index: 1000;
590
- animation: fadeIn 0.3s ease;
591
- }
592
-
593
- .progress-tracker {
594
- position: relative;
595
- padding: 0.5rem;
596
- background: var(--message-bg);
597
- border-radius: var(--border-radius);
598
- margin-top: 0.75rem;
599
- overflow: hidden;
600
- }
601
-
602
- .progress-tracker::before {
603
- content: '';
604
- position: absolute;
605
- top: 0;
606
- left: 0;
607
- height: 100%;
608
- width: 0;
609
- background: linear-gradient(to right, var(--primary-color), var(--primary-dark));
610
- opacity: 0.3;
611
- animation: progress 2s ease-in-out infinite;
612
- }
613
-
614
- /* ==================== ANIMATIONS ==================== */
615
- @keyframes glow {
616
- 0%, 100% { transform: scale(1); opacity: 1; }
617
- 50% { transform: scale(1.1); opacity: 0.8; }
618
- }
619
-
620
- @keyframes fadeIn {
621
- from { opacity: 0; }
622
- to { opacity: 1; }
623
- }
624
-
625
- @keyframes messageFade {
626
- from { opacity: 0; transform: translateY(10px) scale(0.95); }
627
- to { opacity: 1; transform: translateY(0) scale(1); }
628
- }
629
-
630
- @keyframes progress {
631
- 0% { width: 0; }
632
- 50% { width: 60%; }
633
- 100% { width: 0; }
634
- }
635
-
636
- /* ==================== THEMES ==================== */
637
- [data-theme="dark"] .chat-container {
638
- border-color: #475569;
639
- }
640
-
641
- [data-theme="dark"] .upload-area {
642
- border-color: #64748b;
643
- }
644
-
645
- [data-theme="dark"] .upload-area:hover {
646
- background: rgba(79, 70, 229, 0.1);
647
- }
648
-
649
- [data-theme="dark"] .summary-panel {
650
- border-left-color: #818cf8;
651
- }
652
-
653
- /* ==================== MEDIA QUERIES ==================== */
654
- @media (max-width: 768px) {
655
- .gradio-container {
656
- padding: 1rem;
657
- }
658
-
659
- .chat-container {
660
- min-height: 40vh;
661
- max-height: 70vh;
662
- margin-bottom: 3.5rem;
663
- }
664
-
665
- .summary-panel {
666
- padding: 0.75rem;
667
- }
668
-
669
- .upload-area {
670
- padding: 1rem;
671
- }
672
-
673
- .input-container {
674
- gap: 0.5rem;
675
- padding: 0.5rem;
676
- }
677
-
678
- .submit-btn {
679
- padding: 0.4rem 1rem;
680
- }
681
- }
682
-
683
- @media (max-width: 480px) {
684
- .chat-container {
685
- padding: 1rem;
686
- margin-bottom: 3rem;
687
- }
688
-
689
- .input-container {
690
- flex-direction: column;
691
- padding: 0.5rem;
692
- }
693
-
694
- .input__textbox {
695
- font-size: 0.9rem;
696
- }
697
-
698
- .submit-btn {
699
- width: 100%;
700
- padding: 0.5rem;
701
- font-size: 0.85rem;
702
- }
703
-
704
- .chat__message {
705
- max-width: 90%;
706
- padding: 0.5rem 0.75rem;
707
- }
708
-
709
- .tooltip:hover::after {
710
- top: auto;
711
- bottom: -2.5rem;
712
- max-width: 80vw;
713
- }
714
- }
715
- """
716
-
717
- js = """
718
- function applyTheme(theme) {
719
- document.documentElement.setAttribute('data-theme', theme);
720
- localStorage.setItem('theme', theme);
721
- }
722
-
723
- document.addEventListener('DOMContentLoaded', () => {
724
- const savedTheme = localStorage.getItem('theme') || 'light';
725
- applyTheme(savedTheme);
726
- });
727
- """
728
-
729
- with gr.Blocks(
730
- theme=gr.themes.Soft(
731
- primary_hue="indigo",
732
- secondary_hue="blue",
733
- neutral_hue="slate"
734
- ),
735
- title="Clinical Oversight Assistant",
736
- css=css,
737
- js=js
738
- ) as app:
739
- # Header
740
- gr.Markdown("""
741
- <div style='text-align: center; margin-bottom: 24px;'>
742
- <h1 style='color: var(--primary-color); margin-bottom: 8px;'>🩺 Clinical Oversight Assistant</h1>
743
- <p style='color: #64748b;'>
744
- AI-powered analysis for identifying potential missed diagnoses in patient records
745
- </p>
746
- </div>
747
- """)
748
-
749
- with gr.Row(equal_height=False):
750
- # Main Chat Panel
751
- with gr.Column(scale=3):
752
- gr.Markdown(
753
- "<div class='tooltip' data-tip='View conversation history'>**Clinical Analysis Conversation**</div>"
754
- )
755
- chatbot = gr.Chatbot(
756
- label="",
757
- height=650,
758
- show_copy_button=True,
759
- avatar_images=(
760
- "assets/user.png",
761
- "assets/assistant.png"
762
- ) if Path("assets/user.png").exists() else None,
763
- bubble_full_width=False,
764
- type="messages",
765
- elem_classes=["chat-container"]
766
- )
767
-
768
- # Results Panel
769
- with gr.Column(scale=1):
770
- with gr.Group():
771
- gr.Markdown(
772
- "<div class='tooltip' data-tip='Summary of findings'>**Clinical Summary**</div>"
773
- )
774
- final_summary = gr.Markdown(
775
- "<div class='tooltip' data-tip='Analysis results'>Analysis results will appear here...</div>",
776
- elem_classes=["summary-panel"]
777
  )
 
 
778
 
779
- with gr.Group():
780
- gr.Markdown(
781
- "<div class='tooltip' data-tip='Download report'>**Report Export**</div>"
782
- )
783
- download_output = gr.File(
784
- label="Download Full Analysis",
785
- visible=False,
786
- interactive=False
787
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788
 
789
- # Input Section
790
- with gr.Row():
791
- file_upload = gr.File(
792
- file_types=[".pdf", ".csv", ".xls", ".xlsx"],
793
- file_count="multiple",
794
- label="Upload Patient Records",
795
- elem_classes=["upload-area"],
796
- elem_id="file-upload"
797
- )
798
-
799
- with gr.Row(elem_classes=["input-container"]):
800
- user_input = gr.Textbox(
801
- placeholder="Enter your clinical query or analysis request...",
802
- show_label=False,
803
- container=False,
804
- scale=7,
805
- autofocus=True,
806
- elem_classes=["input__textbox"],
807
- elem_id="user-input"
808
- )
809
- submit_btn = gr.Button(
810
- "Analyze",
811
- variant="primary",
812
- scale=1,
813
- min_width=120,
814
- elem_classes=["submit-btn"],
815
- elem_id="submit-btn"
816
- )
817
-
818
- # Hidden progress tracker
819
- progress_tracker = gr.Textbox(
820
- label="Analysis Progress",
821
- visible=False,
822
- interactive=False,
823
- elem_classes=["progress-tracker"],
824
- elem_id="progress-tracker"
825
- )
826
-
827
- # Event handlers
828
- submit_btn.click(
829
- self.analyze_records,
830
- inputs=[user_input, chatbot, file_upload],
831
- outputs=[chatbot, download_output, final_summary, progress_tracker],
832
- show_progress="hidden"
833
- )
834
-
835
- user_input.submit(
836
- self.analyze_records,
837
- inputs=[user_input, chatbot, file_upload],
838
- outputs=[chatbot, download_output, final_summary, progress_tracker],
839
- show_progress="hidden"
840
- )
841
-
842
- app.load(
843
- lambda: [[], None, "<div class='tooltip' data-tip='Analysis results'>Analysis results will appear here...</div>", "", None, {"visible": False}],
844
- outputs=[chatbot, download_output, final_summary, user_input, file_upload, progress_tracker],
845
- queue=False
846
- )
847
 
848
- return app
 
 
849
 
850
- # ==================== APPLICATION ENTRY POINT ====================
851
  if __name__ == "__main__":
852
  try:
853
- logger.info("Launching Clinical Oversight Assistant...")
854
- clinical_app = ClinicalAgent()
855
- interface = clinical_app.create_interface()
856
-
857
- interface.queue(
858
- api_open=False,
859
- max_size=20
860
- ).launch(
861
  server_name="0.0.0.0",
862
  server_port=7860,
863
  show_error=True,
864
- allowed_paths=[str(DIRECTORIES["reports"])],
865
  share=False
866
  )
867
- except Exception as e:
868
- logger.error(f"Application failed to start: {e}")
869
- raise
870
  finally:
871
  if torch.distributed.is_initialized():
872
  torch.distributed.destroy_process_group()
 
4
  import pdfplumber
5
  import json
6
  import gradio as gr
7
+ from typing import List, Dict, Optional, Generator
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
  import hashlib
10
  import shutil
 
15
  import torch
16
  import gc
17
  from diskcache import Cache
18
+ import time
19
  from transformers import AutoTokenizer
 
20
 
21
+ # Configure logging
22
  logging.basicConfig(level=logging.INFO)
23
  logger = logging.getLogger(__name__)
24
 
25
+ # Persistent directory
26
+ persistent_dir = "/data/hf_cache"
27
+ os.makedirs(persistent_dir, exist_ok=True)
28
+
29
+ model_cache_dir = os.path.join(persistent_dir, "txagent_models")
30
+ tool_cache_dir = os.path.join(persistent_dir, "tool_cache")
31
+ file_cache_dir = os.path.join(persistent_dir, "cache")
32
+ report_dir = os.path.join(persistent_dir, "reports")
33
+ vllm_cache_dir = os.path.join(persistent_dir, "vllm_cache")
34
+
35
+ for directory in [model_cache_dir, tool_cache_dir, file_cache_dir, report_dir, vllm_cache_dir]:
36
+ os.makedirs(directory, exist_ok=True)
37
+
38
+ os.environ["HF_HOME"] = model_cache_dir
39
+ os.environ["TRANSFORMERS_CACHE"] = model_cache_dir
40
+ os.environ["VLLM_CACHE_DIR"] = vllm_cache_dir
41
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
42
+ os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
43
+
 
 
 
 
44
  current_dir = os.path.dirname(os.path.abspath(__file__))
45
  src_path = os.path.abspath(os.path.join(current_dir, "src"))
46
  sys.path.insert(0, src_path)
47
 
48
  from txagent.txagent import TxAgent
49
 
50
+ # Initialize cache with 10GB limit
51
+ cache = Cache(file_cache_dir, size_limit=10 * 1024**3)
52
+
53
+ # Initialize tokenizer for precise chunking
54
+ tokenizer = AutoTokenizer.from_pretrained("mims-harvard/TxAgent-T1-Llama-3.1-8B")
55
+
56
+ def sanitize_utf8(text: str) -> str:
57
+ return text.encode("utf-8", "ignore").decode("utf-8")
58
+
59
+ def file_hash(path: str) -> str:
60
+ with open(path, "rb") as f:
61
+ return hashlib.md5(f.read()).hexdigest()
62
+
63
+ def extract_all_pages(file_path: str, progress_callback=None) -> str:
64
+ try:
65
+ with pdfplumber.open(file_path) as pdf:
66
+ total_pages = len(pdf.pages)
67
+ if total_pages == 0:
68
+ return ""
69
+
70
+ batch_size = 10
71
+ batches = [(i, min(i + batch_size, total_pages)) for i in range(0, total_pages, batch_size)]
72
+ text_chunks = [""] * total_pages
73
+ processed_pages = 0
74
+
75
+ def extract_batch(start: int, end: int) -> List[tuple]:
76
+ results = []
77
  with pdfplumber.open(file_path) as pdf:
78
+ for page in pdf.pages[start:end]:
79
+ page_num = start + pdf.pages.index(page)
80
+ page_text = page.extract_text() or ""
81
+ results.append((page_num, f"=== Page {page_num + 1} ===\n{page_text.strip()}"))
82
+ return results
83
+
84
+ with ThreadPoolExecutor(max_workers=6) as executor:
85
+ futures = [executor.submit(extract_batch, start, end) for start, end in batches]
86
+ for future in as_completed(futures):
87
+ for page_num, text in future.result():
88
+ text_chunks[page_num] = text
89
+ processed_pages += batch_size
90
+ if progress_callback:
91
+ progress_callback(min(processed_pages, total_pages), total_pages)
92
+
93
+ return "\n\n".join(filter(None, text_chunks))
94
+ except Exception as e:
95
+ logger.error("PDF processing error: %s", e)
96
+ return f"PDF processing error: {str(e)}"
97
+
98
+ def excel_to_json(file_path: str) -> List[Dict]:
99
+ """Convert Excel file to JSON with optimized processing"""
100
+ try:
101
+ # First try with openpyxl (faster for xlsx)
 
 
 
 
 
 
 
102
  try:
103
+ df = pd.read_excel(file_path, engine='openpyxl', header=None, dtype=str)
104
+ except Exception:
105
+ # Fall back to xlrd if needed
106
+ df = pd.read_excel(file_path, engine='xlrd', header=None, dtype=str)
107
+
108
+ # Convert to list of lists with null handling
109
+ content = df.where(pd.notnull(df), "").astype(str).values.tolist()
110
+
111
+ return [{
112
+ "filename": os.path.basename(file_path),
113
+ "rows": content,
114
+ "type": "excel"
115
+ }]
116
+ except Exception as e:
117
+ logger.error(f"Error processing Excel file: {e}")
118
+ return [{"error": f"Error processing Excel file: {str(e)}"}]
119
+
120
+ def csv_to_json(file_path: str) -> List[Dict]:
121
+ """Convert CSV file to JSON with optimized processing"""
122
+ try:
123
+ # Read CSV in chunks if large
124
+ chunks = []
125
+ for chunk in pd.read_csv(
126
+ file_path,
127
+ header=None,
128
+ dtype=str,
129
+ encoding_errors='replace',
130
+ on_bad_lines='skip',
131
+ chunksize=10000
132
+ ):
133
+ chunks.append(chunk)
134
+
135
+ df = pd.concat(chunks) if chunks else pd.DataFrame()
136
+ content = df.where(pd.notnull(df), "").astype(str).values.tolist()
137
+
138
+ return [{
139
+ "filename": os.path.basename(file_path),
140
+ "rows": content,
141
+ "type": "csv"
142
+ }]
143
+ except Exception as e:
144
+ logger.error(f"Error processing CSV file: {e}")
145
+ return [{"error": f"Error processing CSV file: {str(e)}"}]
146
+
147
+ def process_file(file_path: str, file_type: str) -> List[Dict]:
148
+ """Process file based on type and return JSON data"""
149
+ try:
150
+ if file_type == "pdf":
151
+ text = extract_all_pages(file_path)
152
  return [{
153
  "filename": os.path.basename(file_path),
154
+ "content": text,
155
+ "status": "initial",
156
+ "type": "pdf"
157
  }]
158
+ elif file_type in ["xls", "xlsx"]:
159
+ return excel_to_json(file_path)
160
+ elif file_type == "csv":
161
+ return csv_to_json(file_path)
162
+ else:
 
 
 
 
 
 
 
 
 
 
163
  return [{"error": f"Unsupported file type: {file_type}"}]
164
+ except Exception as e:
165
+ logger.error("Error processing %s: %s", os.path.basename(file_path), e)
166
+ return [{"error": f"Error processing {os.path.basename(file_path)}: {str(e)}"}]
167
+
168
+ def tokenize_and_chunk(text: str, max_tokens: int = 1800) -> List[str]:
169
+ """Split text into chunks based on token count"""
170
+ tokens = tokenizer.encode(text)
171
+ chunks = []
172
+ for i in range(0, len(tokens), max_tokens):
173
+ chunk_tokens = tokens[i:i + max_tokens]
174
+ chunks.append(tokenizer.decode(chunk_tokens))
175
+ return chunks
176
+
177
+ def log_system_usage(tag=""):
178
+ try:
179
+ cpu = psutil.cpu_percent(interval=1)
180
+ mem = psutil.virtual_memory()
181
+ logger.info("[%s] CPU: %.1f%% | RAM: %dMB / %dMB", tag, cpu, mem.used // (1024**2), mem.total // (1024**2))
182
+ result = subprocess.run(
183
+ ["nvidia-smi", "--query-gpu=memory.used,memory.total,utilization.gpu", "--format=csv,nounits,noheader"],
184
+ capture_output=True, text=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  )
186
+ if result.returncode == 0:
187
+ used, total, util = result.stdout.strip().split(", ")
188
+ logger.info("[%s] GPU: %sMB / %sMB | Utilization: %s%%", tag, used, total, util)
189
+ except Exception as e:
190
+ logger.error("[%s] GPU/CPU monitor failed: %s", tag, e)
191
+
192
+ def clean_response(text: str) -> str:
193
+ text = sanitize_utf8(text)
194
+ text = re.sub(r"\[.*?\]|\bNone\b|To analyze the patient record excerpt.*?medications\.|Since the previous attempts.*?\.|I need to.*?medications\.|Retrieving tools.*?\.", "", text, flags=re.DOTALL)
195
+ diagnoses = []
196
+ lines = text.splitlines()
197
+ in_diagnoses_section = False
198
+ for line in lines:
199
+ line = line.strip()
200
+ if not line:
201
+ continue
202
+ if re.match(r"###\s*Missed Diagnoses", line):
203
+ in_diagnoses_section = True
204
+ continue
205
+ if re.match(r"###\s*(Medication Conflicts|Incomplete Assessments|Urgent Follow-up)", line):
206
+ in_diagnoses_section = False
207
+ continue
208
+ if in_diagnoses_section and re.match(r"-\s*.+", line):
209
+ diagnosis = re.sub(r"^\-\s*", "", line).strip()
210
+ if diagnosis and not re.match(r"No issues identified", diagnosis, re.IGNORECASE):
211
+ diagnoses.append(diagnosis)
212
+ text = " ".join(diagnoses)
213
+ text = re.sub(r"\s+", " ", text).strip()
214
+ text = re.sub(r"[^\w\s\.\,\(\)\-]", "", text)
215
+ return text if text else ""
216
+
217
+ def summarize_findings(combined_response: str) -> str:
218
+ chunks = combined_response.split("--- Analysis for Chunk")
219
+ diagnoses = []
220
+ for chunk in chunks:
221
+ chunk = chunk.strip()
222
+ if not chunk or "No oversights identified" in chunk:
223
+ continue
224
+ lines = chunk.splitlines()
225
+ in_diagnoses_section = False
226
+ for line in lines:
227
  line = line.strip()
228
  if not line:
229
  continue
230
  if re.match(r"###\s*Missed Diagnoses", line):
231
+ in_diagnoses_section = True
232
  continue
233
  if re.match(r"###\s*(Medication Conflicts|Incomplete Assessments|Urgent Follow-up)", line):
234
+ in_diagnoses_section = False
235
  continue
236
+ if in_diagnoses_section and re.match(r"-\s*.+", line):
237
  diagnosis = re.sub(r"^\-\s*", "", line).strip()
238
  if diagnosis and not re.match(r"No issues identified", diagnosis, re.IGNORECASE):
239
  diagnoses.append(diagnosis)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ seen = set()
242
+ unique_diagnoses = [d for d in diagnoses if not (d in seen or seen.add(d))]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ if not unique_diagnoses:
245
+ return "No missed diagnoses were identified in the provided records."
246
+
247
+ summary = "Missed diagnoses include " + ", ".join(unique_diagnoses[:-1])
248
+ if len(unique_diagnoses) > 1:
249
+ summary += f", and {unique_diagnoses[-1]}"
250
+ elif len(unique_diagnoses) == 1:
251
+ summary = "Missed diagnoses include " + unique_diagnoses[0]
252
+ summary += ", all of which require urgent clinical review to prevent potential adverse outcomes."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ return summary.strip()
255
+
256
+ def init_agent():
257
+ logger.info("Initializing model...")
258
+ log_system_usage("Before Load")
259
+ default_tool_path = os.path.abspath("data/new_tool.json")
260
+ target_tool_path = os.path.join(tool_cache_dir, "new_tool.json")
261
+ if not os.path.exists(target_tool_path):
262
+ shutil.copy(default_tool_path, target_tool_path)
263
+
264
+ agent = TxAgent(
265
+ model_name="mims-harvard/TxAgent-T1-Llama-3.1-8B",
266
+ rag_model_name="mims-harvard/ToolRAG-T1-GTE-Qwen2-1.5B",
267
+ tool_files_dict={"new_tool": target_tool_path},
268
+ force_finish=True,
269
+ enable_checker=False,
270
+ step_rag_num=4,
271
+ seed=100,
272
+ additional_default_tools=[],
273
+ )
274
+ agent.init_model()
275
+ log_system_usage("After Load")
276
+ logger.info("Agent Ready")
277
+ return agent
278
+
279
+ def create_ui(agent):
280
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
281
+ gr.Markdown("<h1 style='text-align: center;'>🩺 Clinical Oversight Assistant</h1>")
282
+ chatbot = gr.Chatbot(label="Detailed Analysis", height=600, type="messages")
283
+ final_summary = gr.Markdown(label="Summary of Missed Diagnoses")
284
+ file_upload = gr.File(file_types=[".pdf", ".csv", ".xls", ".xlsx"], file_count="multiple")
285
+ msg_input = gr.Textbox(placeholder="Ask about potential oversights...", show_label=False)
286
+ send_btn = gr.Button("Analyze", variant="primary")
287
+ download_output = gr.File(label="Download Full Report")
288
+ progress_bar = gr.Progress()
289
+
290
+ prompt_template = """
291
+ Analyze the patient record excerpt for missed diagnoses only. Provide a concise, evidence-based summary as a single paragraph without headings or bullet points. Include specific clinical findings (e.g., 'elevated blood pressure (160/95) on page 10'), their potential implications (e.g., 'may indicate untreated hypertension'), and a recommendation for urgent review. Do not include other oversight categories like medication conflicts. If no missed diagnoses are found, state 'No missed diagnoses identified' in a single sentence.
292
+ Patient Record Excerpt (Chunk {0} of {1}):
293
+ {chunk}
294
+ """
295
+
296
+ def analyze(message: str, history: List[dict], files: List, progress=gr.Progress()):
297
  history.append({"role": "user", "content": message})
298
+ yield history, None, ""
299
+
 
 
300
  extracted = []
301
+ file_hash_value = ""
302
 
303
  if files:
304
+ # Process files in parallel
305
  with ThreadPoolExecutor(max_workers=4) as executor:
306
  futures = []
307
  for f in files:
308
+ file_type = f.name.split(".")[-1].lower()
309
  futures.append(executor.submit(
310
+ process_file,
311
+ f.name,
312
  file_type
313
  ))
314
 
315
+ for future in as_completed(futures):
316
  try:
317
  extracted.extend(future.result())
 
 
318
  except Exception as e:
319
+ logger.error(f"File processing error: {e}")
320
+ extracted.append({"error": f"Error processing file: {str(e)}"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
+ file_hash_value = file_hash(files[0].name) if files else ""
323
+ history.append({"role": "assistant", "content": "✅ File processing complete"})
324
+ yield history, None, ""
325
 
326
+ # Convert extracted data to JSON text
327
+ text_content = "\n".join(json.dumps(item) for item in extracted)
 
328
 
329
+ # Tokenize and chunk the content properly
330
+ chunks = tokenize_and_chunk(text_content)
331
+ combined_response = ""
332
+ batch_size = 2 # Reduced batch size to prevent token overflow
333
 
334
+ try:
335
+ for batch_idx in range(0, len(chunks), batch_size):
336
+ batch_chunks = chunks[batch_idx:batch_idx + batch_size]
337
+ batch_prompts = [
338
+ prompt_template.format(
339
+ batch_idx + i + 1,
340
+ len(chunks),
341
+ chunk=chunk[:1800] # Conservative chunk size
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  )
343
+ for i, chunk in enumerate(batch_chunks)
344
+ ]
345
 
346
+ progress((batch_idx) / len(chunks),
347
+ desc=f"Analyzing batch {(batch_idx // batch_size) + 1}/{(len(chunks) + batch_size - 1) // batch_size}")
348
+
349
+ # Process batch in parallel
350
+ with ThreadPoolExecutor(max_workers=len(batch_prompts)) as executor:
351
+ future_to_prompt = {
352
+ executor.submit(
353
+ agent.run_gradio_chat,
354
+ prompt, [], 0.2, 512, 2048, False, []
355
+ ): prompt
356
+ for prompt in batch_prompts
357
+ }
358
+
359
+ for future in as_completed(future_to_prompt):
360
+ chunk_response = ""
361
+ for chunk_output in future.result():
362
+ if chunk_output is None:
363
+ continue
364
+ if isinstance(chunk_output, list):
365
+ for m in chunk_output:
366
+ if hasattr(m, 'content') and m.content:
367
+ cleaned = clean_response(m.content)
368
+ if cleaned:
369
+ chunk_response += cleaned + " "
370
+ elif isinstance(chunk_output, str) and chunk_output.strip():
371
+ cleaned = clean_response(chunk_output)
372
+ if cleaned:
373
+ chunk_response += cleaned + " "
374
+
375
+ combined_response += f"--- Analysis for Chunk {batch_idx + 1} ---\n{chunk_response.strip()}\n"
376
+ history[-1] = {"role": "assistant", "content": combined_response.strip()}
377
+ yield history, None, ""
378
+
379
+ # Clean up memory
380
+ torch.cuda.empty_cache()
381
+ gc.collect()
382
+
383
+ # Generate final summary
384
+ summary = summarize_findings(combined_response)
385
+ report_path = os.path.join(report_dir, f"{file_hash_value}_report.txt") if file_hash_value else None
386
+ if report_path:
387
+ with open(report_path, "w", encoding="utf-8") as f:
388
+ f.write(combined_response + "\n\n" + summary)
389
+
390
+ yield history, report_path if report_path and os.path.exists(report_path) else None, summary
391
 
392
+ except Exception as e:
393
+ logger.error("Analysis error: %s", e)
394
+ history.append({"role": "assistant", "content": f"❌ Error occurred: {str(e)}"})
395
+ yield history, None, f"Error occurred during analysis: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
 
397
+ send_btn.click(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output, final_summary])
398
+ msg_input.submit(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output, final_summary])
399
+ return demo
400
 
 
401
  if __name__ == "__main__":
402
  try:
403
+ logger.info("Launching app...")
404
+ agent = init_agent()
405
+ demo = create_ui(agent)
406
+ demo.queue(api_open=False).launch(
 
 
 
 
407
  server_name="0.0.0.0",
408
  server_port=7860,
409
  show_error=True,
410
+ allowed_paths=[report_dir],
411
  share=False
412
  )
 
 
 
413
  finally:
414
  if torch.distributed.is_initialized():
415
  torch.distributed.destroy_process_group()