Ali2206 commited on
Commit
c0b6a0b
·
verified ·
1 Parent(s): eea533f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -163
app.py CHANGED
@@ -11,8 +11,9 @@ import shutil
11
  import re
12
  import psutil
13
  import subprocess
14
- import multiprocessing
15
- from functools import partial
 
16
  import time
17
 
18
  # Persistent directory
@@ -33,6 +34,7 @@ os.environ["TRANSFORMERS_CACHE"] = model_cache_dir
33
  os.environ["VLLM_CACHE_DIR"] = vllm_cache_dir
34
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
35
  os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
 
36
 
37
  current_dir = os.path.dirname(os.path.abspath(__file__))
38
  src_path = os.path.abspath(os.path.join(current_dir, "src"))
@@ -40,6 +42,9 @@ sys.path.insert(0, src_path)
40
 
41
  from txagent.txagent import TxAgent
42
 
 
 
 
43
  def sanitize_utf8(text: str) -> str:
44
  return text.encode("utf-8", "ignore").decode("utf-8")
45
 
@@ -47,58 +52,46 @@ def file_hash(path: str) -> str:
47
  with open(path, "rb") as f:
48
  return hashlib.md5(f.read()).hexdigest()
49
 
50
- def extract_page_range(file_path: str, start_page: int, end_page: int) -> str:
51
- """Extract text from a range of PDF pages."""
52
- try:
53
- text_chunks = []
54
- with pdfplumber.open(file_path) as pdf:
55
- for page in pdf.pages[start_page:end_page]:
56
- page_text = page.extract_text() or ""
57
- text_chunks.append(f"=== Page {start_page + pdf.pages.index(page) + 1} ===\n{page_text.strip()}")
58
- return "\n\n".join(text_chunks)
59
- except Exception:
60
- return ""
61
-
62
  def extract_all_pages(file_path: str, progress_callback=None) -> str:
63
- """Extract text from all pages of a PDF using parallel processing."""
64
  try:
65
  with pdfplumber.open(file_path) as pdf:
66
  total_pages = len(pdf.pages)
67
-
68
- if total_pages == 0:
69
- return ""
70
-
71
- # Use 6 processes (adjust based on CPU cores)
72
- num_processes = min(6, multiprocessing.cpu_count())
73
- pages_per_process = max(1, total_pages // num_processes)
74
-
75
- # Create page ranges for parallel processing
76
- ranges = [(i * pages_per_process, min((i + 1) * pages_per_process, total_pages))
77
- for i in range(num_processes)]
78
- if ranges[-1][1] != total_pages:
79
- ranges[-1] = (ranges[-1][0], total_pages)
80
-
81
- # Process page ranges in parallel
82
- with multiprocessing.Pool(processes=num_processes) as pool:
83
- extract_func = partial(extract_page_range, file_path)
84
  results = []
85
- for idx, result in enumerate(pool.starmap(extract_func, ranges)):
86
- results.append(result)
 
 
 
 
 
 
 
 
 
 
 
87
  if progress_callback:
88
- processed_pages = min((idx + 1) * pages_per_process, total_pages)
89
- progress_callback(processed_pages, total_pages)
90
-
91
- return "\n\n".join(filter(None, results))
92
  except Exception as e:
93
  return f"PDF processing error: {str(e)}"
94
 
95
  def convert_file_to_json(file_path: str, file_type: str, progress_callback=None) -> str:
96
  try:
97
- h = file_hash(file_path)
98
- cache_path = os.path.join(file_cache_dir, f"{h}.json")
99
- if os.path.exists(cache_path):
100
- with open(cache_path, "r", encoding="utf-8") as f:
101
- return f.read()
102
 
103
  if file_type == "pdf":
104
  text = extract_all_pages(file_path, progress_callback)
@@ -109,16 +102,13 @@ def convert_file_to_json(file_path: str, file_type: str, progress_callback=None)
109
  content = df.fillna("").astype(str).values.tolist()
110
  result = json.dumps({"filename": os.path.basename(file_path), "rows": content})
111
  elif file_type in ["xls", "xlsx"]:
112
- try:
113
- df = pd.read_excel(file_path, engine="openpyxl", header=None, dtype=str)
114
- except Exception:
115
- df = pd.read_excel(file_path, engine="xlrd", header=None, dtype=str)
116
  content = df.fillna("").astype(str).values.tolist()
117
  result = json.dumps({"filename": os.path.basename(file_path), "rows": content})
118
  else:
119
  result = json.dumps({"error": f"Unsupported file type: {file_type}"})
120
- with open(cache_path, "w", encoding="utf-8") as f:
121
- f.write(result)
122
  return result
123
  except Exception as e:
124
  return json.dumps({"error": f"Error processing {os.path.basename(file_path)}: {str(e)}"})
@@ -139,24 +129,18 @@ def log_system_usage(tag=""):
139
  print(f"[{tag}] GPU/CPU monitor failed: {e}")
140
 
141
  def clean_response(text: str) -> str:
142
- """Clean TxAgent response to group findings under tool-derived headings."""
143
  text = sanitize_utf8(text)
144
- # Remove tool call artifacts, None, and reasoning
145
  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)
146
- # Remove extra whitespace and non-markdown content
147
  text = re.sub(r"\n{3,}", "\n\n", text)
148
- text = re.sub(r"[^\n#\-\*\w\s\.\,\:\(\)]+", "", text) # Keep markdown-relevant characters
149
-
150
- # Define tool-to-heading mapping
151
  tool_to_heading = {
152
  "get_abuse_info_by_drug_name": "Drugs",
153
  "get_dependence_info_by_drug_name": "Drugs",
154
  "get_abuse_types_and_related_adverse_reactions_and_controlled_substance_status_by_drug_name": "Drugs",
155
  "get_info_for_patients_by_drug_name": "Drugs",
156
- # Add other tools from new_tool.json if applicable
157
  }
158
-
159
- # Parse sections and findings
160
  sections = {}
161
  current_section = None
162
  current_tool = None
@@ -165,22 +149,18 @@ def clean_response(text: str) -> str:
165
  line = line.strip()
166
  if not line:
167
  continue
168
- # Detect tool tag
169
  tool_match = re.match(r"\[TOOL:\s*(\w+)\]", line)
170
  if tool_match:
171
  current_tool = tool_match.group(1)
172
  continue
173
- # Detect section heading
174
  section_match = re.match(r"###\s*(Missed Diagnoses|Medication Conflicts|Incomplete Assessments|Urgent Follow-up)", line)
175
  if section_match:
176
  current_section = section_match.group(1)
177
  if current_section not in sections:
178
  sections[current_section] = []
179
  continue
180
- # Detect finding
181
  finding_match = re.match(r"-\s*.+", line)
182
  if finding_match and current_section and not re.match(r"-\s*No issues identified", line):
183
- # Assign to tool-derived heading if tool is specified
184
  if current_tool and current_tool in tool_to_heading:
185
  heading = tool_to_heading[current_tool]
186
  if heading not in sections:
@@ -188,17 +168,14 @@ def clean_response(text: str) -> str:
188
  sections[heading].append(line)
189
  else:
190
  sections[current_section].append(line)
191
-
192
- # Combine non-empty sections
193
  cleaned = []
194
  for heading, findings in sections.items():
195
- if findings: # Only include sections with findings
196
  cleaned.append(f"### {heading}\n" + "\n".join(findings))
197
-
198
  text = "\n\n".join(cleaned).strip()
199
- if not text:
200
- text = "" # Return empty string if no valid findings
201
- return text
202
 
203
  def init_agent():
204
  print("🔁 Initializing model...")
@@ -213,13 +190,20 @@ def init_agent():
213
  rag_model_name="mims-harvard/ToolRAG-T1-GTE-Qwen2-1.5B",
214
  tool_files_dict={"new_tool": target_tool_path},
215
  force_finish=True,
216
- enable_checker=True,
217
  step_rag_num=4,
218
  seed=100,
219
  additional_default_tools=[],
 
220
  )
221
- agent.init_model()
222
- log_system_usage("After Load")
 
 
 
 
 
 
223
  print("✅ Agent Ready")
224
  return agent
225
 
@@ -231,25 +215,25 @@ def create_ui(agent):
231
  msg_input = gr.Textbox(placeholder="Ask about potential oversights...", show_label=False)
232
  send_btn = gr.Button("Analyze", variant="primary")
233
  download_output = gr.File(label="Download Full Report")
 
234
 
235
- def analyze(message: str, history: List[dict], files: List):
236
- history.append({"role": "user", "content": message})
237
- history.append({"role": "assistant", "content": "⏳ Extracting text from files..."})
238
- yield history, None
 
 
 
 
 
 
239
 
240
  extracted = ""
241
  file_hash_value = ""
242
  if files:
243
- # Progress callback for extraction
244
- total_pages = 0
245
- processed_pages = 0
246
  def update_extraction_progress(current, total):
247
- nonlocal processed_pages, total_pages
248
- processed_pages = current
249
- total_pages = total
250
- animation = ["🌀", "🔄", "⚙️", "🔃"][(int(time.time() * 2) % 4)]
251
- history[-1] = {"role": "assistant", "content": f"Extracting text... {animation} Page {processed_pages}/{total_pages}"}
252
- return history, None
253
 
254
  with ThreadPoolExecutor(max_workers=6) as executor:
255
  futures = [executor.submit(convert_file_to_json, f.name, f.name.split(".")[-1].lower(), update_extraction_progress) for f in files]
@@ -257,107 +241,67 @@ def create_ui(agent):
257
  extracted = "\n".join(results)
258
  file_hash_value = file_hash(files[0].name) if files else ""
259
 
260
- history.pop() # Remove extraction message
261
  history.append({"role": "assistant", "content": "✅ Text extraction complete."})
262
- yield history, None
263
 
264
- # Split extracted text into chunks of ~6,000 characters
265
  chunk_size = 6000
266
  chunks = [extracted[i:i + chunk_size] for i in range(0, len(extracted), chunk_size)]
267
  combined_response = ""
268
-
269
- prompt_template = """
270
- You are a medical analysis assistant. Analyze the following patient record excerpt for clinical oversights and provide a concise, evidence-based summary in markdown format. Group findings under appropriate headings based on the tool used (e.g., drug-related findings under 'Drugs'). For each finding, include:
271
- - Clinical context (why the issue was missed or relevant details from the record).
272
- - Potential risks if unaddressed (e.g., disease progression, adverse events).
273
- - Actionable recommendations (e.g., tests, referrals, medication adjustments).
274
- Output ONLY the markdown-formatted findings, with bullet points under each heading. Precede each finding with a tool tag (e.g., [TOOL: get_abuse_info_by_drug_name]) to indicate the tool used. Do NOT include reasoning, tool calls, or intermediate steps. If no issues are found for a tool or category, state "No issues identified" for that section. Ensure the output is specific to the provided text and avoids generic responses.
275
-
276
- Example Output:
277
- ### Drugs
278
- [TOOL: get_abuse_info_by_drug_name]
279
- - Opioid use disorder not addressed. Missed due to lack of screening. Risks: overdose. Recommend: addiction specialist referral.
280
- ### Missed Diagnoses
281
- - Elevated BP noted without diagnosis. Missed due to inconsistent visits. Risks: stroke. Recommend: BP monitoring, antihypertensives.
282
- ### Incomplete Assessments
283
- - Chest pain not evaluated. Time constraints likely cause. Risks: cardiac issues. Recommend: ECG, stress test.
284
- ### Urgent Follow-up
285
- - Abnormal creatinine not addressed. Delayed lab review. Risks: renal failure. Recommend: nephrology referral.
286
-
287
- Patient Record Excerpt (Chunk {0} of {1}):
288
- {chunk}
289
- """
290
 
291
  try:
292
- # Process each chunk and stream results in real-time
293
- for chunk_idx, chunk in enumerate(chunks, 1):
294
- # Update UI with chunk progress
295
- animation = ["🔍", "📊", "🧠", "🔎"][(int(time.time() * 2) % 4)]
296
- history.append({"role": "assistant", "content": f"Analyzing records... {animation} Chunk {chunk_idx}/{len(chunks)}"})
297
- yield history, None
298
-
299
- prompt = prompt_template.format(chunk_idx, len(chunks), chunk=chunk[:4000]) # Truncate to avoid token limits
300
- chunk_response = ""
301
- for chunk_output in agent.run_gradio_chat(
302
- message=prompt,
303
- history=[],
304
- temperature=0.2,
305
- max_new_tokens=1024,
306
- max_token=4096,
307
- call_agent=False,
308
- conversation=[],
309
- ):
310
- if chunk_output is None:
311
- continue
312
- if isinstance(chunk_output, list):
313
- for m in chunk_output:
314
- if hasattr(m, 'content') and m.content:
315
- cleaned = clean_response(m.content)
316
  if cleaned and re.search(r"###\s*\w+", cleaned):
317
  chunk_response += cleaned + "\n\n"
318
- # Update UI with partial response
319
- if history[-1]["content"].startswith("Analyzing"):
320
- history[-1] = {"role": "assistant", "content": f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response.strip()}"}
321
- else:
322
- history[-1]["content"] = f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response.strip()}"
323
- yield history, None
324
- elif isinstance(chunk_output, str) and chunk_output.strip():
325
- cleaned = clean_response(chunk_output)
326
- if cleaned and re.search(r"###\s*\w+", cleaned):
327
- chunk_response += cleaned + "\n\n"
328
- # Update UI with partial response
329
- if history[-1]["content"].startswith("Analyzing"):
330
- history[-1] = {"role": "assistant", "content": f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response.strip()}"}
331
- else:
332
- history[-1]["content"] = f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response.strip()}"
333
- yield history, None
334
-
335
- # Append completed chunk response to combined response
336
- if chunk_response:
337
- combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
338
- else:
339
- combined_response += f"--- Analysis for Chunk {chunk_idx} ---\nNo oversights identified for this chunk.\n\n"
340
-
341
- # Finalize UI with complete response
342
  if combined_response.strip() and not all("No oversights identified" in chunk for chunk in combined_response.split("--- Analysis for Chunk")):
343
  history[-1]["content"] = combined_response.strip()
344
  else:
345
  history.append({"role": "assistant", "content": "No oversights identified in the provided records."})
346
 
347
- # Generate report file
348
  report_path = os.path.join(report_dir, f"{file_hash_value}_report.txt") if file_hash_value else None
349
  if report_path:
350
  with open(report_path, "w", encoding="utf-8") as f:
351
  f.write(combined_response)
352
- yield history, report_path if report_path and os.path.exists(report_path) else None
353
 
354
  except Exception as e:
355
  print("🚨 ERROR:", e)
356
  history.append({"role": "assistant", "content": f"❌ Error occurred: {str(e)}"})
357
- yield history, None
358
 
359
- send_btn.click(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output])
360
- msg_input.submit(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output])
361
  return demo
362
 
363
  if __name__ == "__main__":
 
11
  import re
12
  import psutil
13
  import subprocess
14
+ import threading
15
+ import torch
16
+ from diskcache import Cache
17
  import time
18
 
19
  # Persistent directory
 
34
  os.environ["VLLM_CACHE_DIR"] = vllm_cache_dir
35
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
36
  os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
37
+ os.environ["OMP_NUM_THREADS"] = str(os.cpu_count() // 2) # Optimize CPU threading
38
 
39
  current_dir = os.path.dirname(os.path.abspath(__file__))
40
  src_path = os.path.abspath(os.path.join(current_dir, "src"))
 
42
 
43
  from txagent.txagent import TxAgent
44
 
45
+ # Initialize cache with 10GB limit
46
+ cache = Cache(file_cache_dir, size_limit=10 * 1024**3)
47
+
48
  def sanitize_utf8(text: str) -> str:
49
  return text.encode("utf-8", "ignore").decode("utf-8")
50
 
 
52
  with open(path, "rb") as f:
53
  return hashlib.md5(f.read()).hexdigest()
54
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  def extract_all_pages(file_path: str, progress_callback=None) -> str:
 
56
  try:
57
  with pdfplumber.open(file_path) as pdf:
58
  total_pages = len(pdf.pages)
59
+ if total_pages == 0:
60
+ return ""
61
+
62
+ batch_size = 10 # Process 10 pages per thread
63
+ batches = [(i, min(i + batch_size, total_pages)) for i in range(0, total_pages, batch_size)]
64
+ text_chunks = [""] * total_pages # Pre-allocate for page order
65
+ processed_pages = 0
66
+
67
+ def extract_batch(start: int, end: int) -> List[tuple]:
 
 
 
 
 
 
 
 
68
  results = []
69
+ with pdfplumber.open(file_path) as pdf: # Reopen per thread
70
+ for page in pdf.pages[start:end]:
71
+ page_num = start + pdf.pages.index(page)
72
+ page_text = page.extract_text() or ""
73
+ results.append((page_num, f"=== Page {page_num + 1} ===\n{page_text.strip()}"))
74
+ return results
75
+
76
+ with ThreadPoolExecutor(max_workers=min(6, os.cpu_count())) as executor:
77
+ futures = [executor.submit(extract_batch, start, end) for start, end in batches]
78
+ for future in as_completed(futures):
79
+ for page_num, text in future.result():
80
+ text_chunks[page_num] = text
81
+ processed_pages += batch_size
82
  if progress_callback:
83
+ progress_callback(min(processed_pages, total_pages), total_pages)
84
+
85
+ return "\n\n".join(filter(None, text_chunks))
 
86
  except Exception as e:
87
  return f"PDF processing error: {str(e)}"
88
 
89
  def convert_file_to_json(file_path: str, file_type: str, progress_callback=None) -> str:
90
  try:
91
+ file_h = file_hash(file_path)
92
+ cache_key = f"{file_h}_{file_type}"
93
+ if cache_key in cache:
94
+ return cache[cache_key]
 
95
 
96
  if file_type == "pdf":
97
  text = extract_all_pages(file_path, progress_callback)
 
102
  content = df.fillna("").astype(str).values.tolist()
103
  result = json.dumps({"filename": os.path.basename(file_path), "rows": content})
104
  elif file_type in ["xls", "xlsx"]:
105
+ df = pd.read_excel(file_path, engine="openpyxl", header=None, dtype=str)
 
 
 
106
  content = df.fillna("").astype(str).values.tolist()
107
  result = json.dumps({"filename": os.path.basename(file_path), "rows": content})
108
  else:
109
  result = json.dumps({"error": f"Unsupported file type: {file_type}"})
110
+
111
+ cache[cache_key] = result
112
  return result
113
  except Exception as e:
114
  return json.dumps({"error": f"Error processing {os.path.basename(file_path)}: {str(e)}"})
 
129
  print(f"[{tag}] GPU/CPU monitor failed: {e}")
130
 
131
  def clean_response(text: str) -> str:
 
132
  text = sanitize_utf8(text)
 
133
  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)
 
134
  text = re.sub(r"\n{3,}", "\n\n", text)
135
+ text = re.sub(r"[^\n#\-\*\w\s\.\,\:\(\)]+", "", text)
136
+
 
137
  tool_to_heading = {
138
  "get_abuse_info_by_drug_name": "Drugs",
139
  "get_dependence_info_by_drug_name": "Drugs",
140
  "get_abuse_types_and_related_adverse_reactions_and_controlled_substance_status_by_drug_name": "Drugs",
141
  "get_info_for_patients_by_drug_name": "Drugs",
 
142
  }
143
+
 
144
  sections = {}
145
  current_section = None
146
  current_tool = None
 
149
  line = line.strip()
150
  if not line:
151
  continue
 
152
  tool_match = re.match(r"\[TOOL:\s*(\w+)\]", line)
153
  if tool_match:
154
  current_tool = tool_match.group(1)
155
  continue
 
156
  section_match = re.match(r"###\s*(Missed Diagnoses|Medication Conflicts|Incomplete Assessments|Urgent Follow-up)", line)
157
  if section_match:
158
  current_section = section_match.group(1)
159
  if current_section not in sections:
160
  sections[current_section] = []
161
  continue
 
162
  finding_match = re.match(r"-\s*.+", line)
163
  if finding_match and current_section and not re.match(r"-\s*No issues identified", line):
 
164
  if current_tool and current_tool in tool_to_heading:
165
  heading = tool_to_heading[current_tool]
166
  if heading not in sections:
 
168
  sections[heading].append(line)
169
  else:
170
  sections[current_section].append(line)
171
+
 
172
  cleaned = []
173
  for heading, findings in sections.items():
174
+ if findings:
175
  cleaned.append(f"### {heading}\n" + "\n".join(findings))
176
+
177
  text = "\n\n".join(cleaned).strip()
178
+ return text if text else ""
 
 
179
 
180
  def init_agent():
181
  print("🔁 Initializing model...")
 
190
  rag_model_name="mims-harvard/ToolRAG-T1-GTE-Qwen2-1.5B",
191
  tool_files_dict={"new_tool": target_tool_path},
192
  force_finish=True,
193
+ enable_checker=False, # Disable checker for speed
194
  step_rag_num=4,
195
  seed=100,
196
  additional_default_tools=[],
197
+ dtype=torch.float16, # Enable mixed precision
198
  )
199
+
200
+ def preload_models():
201
+ agent.init_model()
202
+ log_system_usage("After Load")
203
+
204
+ preload_thread = threading.Thread(target=preload_models)
205
+ preload_thread.start()
206
+ preload_thread.join()
207
  print("✅ Agent Ready")
208
  return agent
209
 
 
215
  msg_input = gr.Textbox(placeholder="Ask about potential oversights...", show_label=False)
216
  send_btn = gr.Button("Analyze", variant="primary")
217
  download_output = gr.File(label="Download Full Report")
218
+ progress_bar = gr.Progress()
219
 
220
+ prompt_template = """
221
+ Analyze the patient record excerpt for clinical oversights. Provide a concise, evidence-based summary in markdown with findings grouped under tool-derived headings (e.g., 'Drugs'). For each finding, include clinical context, risks, and recommendations. Precede findings with a tool tag (e.g., [TOOL: get_abuse_info_by_drug_name]). Output only markdown bullet points under headings. If no issues, state "No issues identified".
222
+
223
+ Patient Record Excerpt (Chunk {0} of {1}):
224
+ {chunk}
225
+ """
226
+
227
+ def analyze(message: str, history: List[dict], files: List, progress=gr.Progress()):
228
+ history.append({"role": "user mesage": "user", "content": message})
229
+ yield history, None, None
230
 
231
  extracted = ""
232
  file_hash_value = ""
233
  if files:
 
 
 
234
  def update_extraction_progress(current, total):
235
+ progress(current / total, desc=f"Extracting text... Page {current}/{total}")
236
+ return history, None, None
 
 
 
 
237
 
238
  with ThreadPoolExecutor(max_workers=6) as executor:
239
  futures = [executor.submit(convert_file_to_json, f.name, f.name.split(".")[-1].lower(), update_extraction_progress) for f in files]
 
241
  extracted = "\n".join(results)
242
  file_hash_value = file_hash(files[0].name) if files else ""
243
 
 
244
  history.append({"role": "assistant", "content": "✅ Text extraction complete."})
245
+ yield history, None, None
246
 
 
247
  chunk_size = 6000
248
  chunks = [extracted[i:i + chunk_size] for i in range(0, len(extracted), chunk_size)]
249
  combined_response = ""
250
+ batch_size = 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
  try:
253
+ for batch_idx in range(0, len(chunks), batch_size):
254
+ batch_chunks = chunks[batch_idx:batch_idx + batch_size]
255
+ batch_prompts = [prompt_template.format(i + 1, len(chunks), chunk=chunk[:4000]) for i, chunk in enumerate(batch_chunks)]
256
+ batch_responses = []
257
+
258
+ progress((batch_idx + 1) / len(chunks), desc=f"Analyzing chunks {batch_idx + 1}-{min(batch_idx + batch_size, len(chunks))}/{len(chunks)}")
259
+
260
+ with ThreadPoolExecutor(max_workers=len(batch_chunks)) as executor:
261
+ futures = [executor.submit(agent.run_gradio_chat, prompt, [], 0.2, 512, 2048, False, []) for prompt in batch_prompts]
262
+ for future in as_completed(futures):
263
+ chunk_response = ""
264
+ for chunk_output in future.result():
265
+ if chunk_output is None:
266
+ continue
267
+ if isinstance(chunk_output, list):
268
+ for m in chunk_output:
269
+ if hasattr(m, 'content') and m.content:
270
+ cleaned = clean_response(m.content)
271
+ if cleaned and re.search(r"###\s*\w+", cleaned):
272
+ chunk_response += cleaned + "\n\n"
273
+ elif isinstance(chunk_output, str) and chunk_output.strip():
274
+ cleaned = clean_response(chunk_output)
 
 
275
  if cleaned and re.search(r"###\s*\w+", cleaned):
276
  chunk_response += cleaned + "\n\n"
277
+ batch_responses.append(chunk_response)
278
+
279
+ for chunk_idx, chunk_response in enumerate(batch_responses, batch_idx + 1):
280
+ if chunk_response:
281
+ combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
282
+ else:
283
+ combined_response += f"--- Analysis for Chunk {chunk_idx} ---\nNo oversights identified for this chunk.\n\n"
284
+ history[-1] = {"role": "assistant", "content": combined_response.strip()}
285
+ yield history, None, None
286
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  if combined_response.strip() and not all("No oversights identified" in chunk for chunk in combined_response.split("--- Analysis for Chunk")):
288
  history[-1]["content"] = combined_response.strip()
289
  else:
290
  history.append({"role": "assistant", "content": "No oversights identified in the provided records."})
291
 
 
292
  report_path = os.path.join(report_dir, f"{file_hash_value}_report.txt") if file_hash_value else None
293
  if report_path:
294
  with open(report_path, "w", encoding="utf-8") as f:
295
  f.write(combined_response)
296
+ yield history, report_path if report_path and os.path.exists(report_path) else None, None
297
 
298
  except Exception as e:
299
  print("🚨 ERROR:", e)
300
  history.append({"role": "assistant", "content": f"❌ Error occurred: {str(e)}"})
301
+ yield history, None, None
302
 
303
+ send_btn.click(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output, progress_bar])
304
+ msg_input.submit(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output, progress_bar])
305
  return demo
306
 
307
  if __name__ == "__main__":