Ali2206 commited on
Commit
a8cd932
Β·
verified Β·
1 Parent(s): 80bc091

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +161 -106
app.py CHANGED
@@ -11,9 +11,8 @@ import shutil
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,7 +33,6 @@ os.environ["TRANSFORMERS_CACHE"] = model_cache_dir
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,9 +40,6 @@ sys.path.insert(0, src_path)
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,46 +47,58 @@ def file_hash(path: str) -> str:
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,13 +109,16 @@ def convert_file_to_json(file_path: str, file_type: str, progress_callback=None)
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,18 +139,24 @@ def log_system_usage(tag=""):
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,18 +165,22 @@ def clean_response(text: str) -> str:
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,14 +188,17 @@ def clean_response(text: str) -> str:
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,20 +213,13 @@ def init_agent():
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,24 +231,25 @@ def create_ui(agent):
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
- Patient Record Excerpt (Chunk {0} of {1}):
223
- {chunk}
224
- """
225
-
226
- def analyze(message: str, history: List[dict], files: List, progress=gr.Progress()):
227
- history.append({"role": "user mesage": "user", "content": message})
228
- yield history, None, None
229
 
230
  extracted = ""
231
  file_hash_value = ""
232
  if files:
 
 
 
233
  def update_extraction_progress(current, total):
234
- progress(current / total, desc=f"Extracting text... Page {current}/{total}")
235
- return history, None, None
 
 
 
 
236
 
237
  with ThreadPoolExecutor(max_workers=6) as executor:
238
  futures = [executor.submit(convert_file_to_json, f.name, f.name.split(".")[-1].lower(), update_extraction_progress) for f in files]
@@ -240,67 +257,105 @@ Patient Record Excerpt (Chunk {0} of {1}):
240
  extracted = "\n".join(results)
241
  file_hash_value = file_hash(files[0].name) if files else ""
242
 
 
243
  history.append({"role": "assistant", "content": "βœ… Text extraction complete."})
244
- yield history, None, None
245
 
 
246
  chunk_size = 6000
247
  chunks = [extracted[i:i + chunk_size] for i in range(0, len(extracted), chunk_size)]
248
  combined_response = ""
249
- batch_size = 2
250
-
251
- try:
252
- for batch_idx in range(0, len(chunks), batch_size):
253
- batch_chunks = chunks[batch_idx:batch_idx + batch_size]
254
- batch_prompts = [prompt_template.format(i + 1, len(chunks), chunk=chunk[:4000]) for i, chunk in enumerate(batch_chunks)]
255
- batch_responses = []
256
 
257
- progress((batch_idx + 1) / len(chunks), desc=f"Analyzing chunks {batch_idx + 1}-{min(batch_idx + batch_size, len(chunks))}/{len(chunks)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
- with ThreadPoolExecutor(max_workers=len(batch_chunks)) as executor:
260
- futures = [executor.submit(agent.run_gradio_chat, prompt, [], 0.2, 512, 2048, False, []) for prompt in batch_prompts]
261
- for future in as_completed(futures):
262
- chunk_response = ""
263
- for chunk_output in future.result():
264
- if chunk_output is None:
265
- continue
266
- if isinstance(chunk_output, list):
267
- for m in chunk_output:
268
- if hasattr(m, 'content') and m.content:
269
- cleaned = clean_response(m.content)
270
- if cleaned and re.search(r"###\s*\w+", cleaned):
271
- chunk_response += cleaned + "\n\n"
272
- elif isinstance(chunk_output, str) and chunk_output.strip():
273
- cleaned = clean_response(chunk_output)
 
 
 
 
 
 
 
 
 
 
274
  if cleaned and re.search(r"###\s*\w+", cleaned):
275
  chunk_response += cleaned + "\n\n"
276
- batch_responses.append(chunk_response)
277
-
278
- for chunk_idx, chunk_response in enumerate(batch_responses, batch_idx + 1):
279
- if chunk_response:
280
- combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
281
- else:
282
- combined_response += f"--- Analysis for Chunk {chunk_idx} ---\nNo oversights identified for this chunk.\n\n"
283
- history[-1] = {"role": "assistant", "content": combined_response.strip()}
284
- yield history, None, None
285
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  if combined_response.strip() and not all("No oversights identified" in chunk for chunk in combined_response.split("--- Analysis for Chunk")):
287
  history[-1]["content"] = combined_response.strip()
288
  else:
289
  history.append({"role": "assistant", "content": "No oversights identified in the provided records."})
290
 
 
291
  report_path = os.path.join(report_dir, f"{file_hash_value}_report.txt") if file_hash_value else None
292
  if report_path:
293
  with open(report_path, "w", encoding="utf-8") as f:
294
  f.write(combined_response)
295
- yield history, report_path if report_path and os.path.exists(report_path) else None, None
296
 
297
  except Exception as e:
298
  print("🚨 ERROR:", e)
299
  history.append({"role": "assistant", "content": f"❌ Error occurred: {str(e)}"})
300
- yield history, None, None
301
 
302
- send_btn.click(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output, progress_bar])
303
- msg_input.submit(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output, progress_bar])
304
  return demo
305
 
306
  if __name__ == "__main__":
 
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
  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
 
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
  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
  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
  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
  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
  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
  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
  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
  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
+ Example Output:
276
+ ### Drugs
277
+ [TOOL: get_abuse_info_by_drug_name]
278
+ - Opioid use disorder not addressed. Missed due to lack of screening. Risks: overdose. Recommend: addiction specialist referral.
279
+ ### Missed Diagnoses
280
+ - Elevated BP noted without diagnosis. Missed due to inconsistent visits. Risks: stroke. Recommend: BP monitoring, antihypertensives.
281
+ ### Incomplete Assessments
282
+ - Chest pain not evaluated. Time constraints likely cause. Risks: cardiac issues. Recommend: ECG, stress test.
283
+ ### Urgent Follow-up
284
+ - Abnormal creatinine not addressed. Delayed lab review. Risks: renal failure. Recommend: nephrology referral.
285
+ Patient Record Excerpt (Chunk {0} of {1}):
286
+ {chunk}
287
+ """
288
 
289
+ try:
290
+ # Process each chunk and stream results in real-time
291
+ for chunk_idx, chunk in enumerate(chunks, 1):
292
+ # Update UI with chunk progress
293
+ animation = ["πŸ”", "πŸ“Š", "🧠", "πŸ”Ž"][(int(time.time() * 2) % 4)]
294
+ history.append({"role": "assistant", "content": f"Analyzing records... {animation} Chunk {chunk_idx}/{len(chunks)}"})
295
+ yield history, None
296
+
297
+ prompt = prompt_template.format(chunk_idx, len(chunks), chunk=chunk[:4000]) # Truncate to avoid token limits
298
+ chunk_response = ""
299
+ for chunk_output in agent.run_gradio_chat(
300
+ message=prompt,
301
+ history=[],
302
+ temperature=0.2,
303
+ max_new_tokens=1024,
304
+ max_token=4096,
305
+ call_agent=False,
306
+ conversation=[],
307
+ ):
308
+ if chunk_output is None:
309
+ continue
310
+ if isinstance(chunk_output, list):
311
+ for m in chunk_output:
312
+ if hasattr(m, 'content') and m.content:
313
+ cleaned = clean_response(m.content)
314
  if cleaned and re.search(r"###\s*\w+", cleaned):
315
  chunk_response += cleaned + "\n\n"
316
+ # Update UI with partial response
317
+ if history[-1]["content"].startswith("Analyzing"):
318
+ history[-1] = {"role": "assistant", "content": f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response.strip()}"}
319
+ else:
320
+ history[-1]["content"] = f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response.strip()}"
321
+ yield history, None
322
+ elif isinstance(chunk_output, str) and chunk_output.strip():
323
+ cleaned = clean_response(chunk_output)
324
+ if cleaned and re.search(r"###\s*\w+", cleaned):
325
+ chunk_response += cleaned + "\n\n"
326
+ # Update UI with partial response
327
+ if history[-1]["content"].startswith("Analyzing"):
328
+ history[-1] = {"role": "assistant", "content": f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response.strip()}"}
329
+ else:
330
+ history[-1]["content"] = f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response.strip()}"
331
+ yield history, None
332
+
333
+ # Append completed chunk response to combined response
334
+ if chunk_response:
335
+ combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
336
+ else:
337
+ combined_response += f"--- Analysis for Chunk {chunk_idx} ---\nNo oversights identified for this chunk.\n\n"
338
+
339
+ # Finalize UI with complete response
340
  if combined_response.strip() and not all("No oversights identified" in chunk for chunk in combined_response.split("--- Analysis for Chunk")):
341
  history[-1]["content"] = combined_response.strip()
342
  else:
343
  history.append({"role": "assistant", "content": "No oversights identified in the provided records."})
344
 
345
+ # Generate report file
346
  report_path = os.path.join(report_dir, f"{file_hash_value}_report.txt") if file_hash_value else None
347
  if report_path:
348
  with open(report_path, "w", encoding="utf-8") as f:
349
  f.write(combined_response)
350
+ yield history, report_path if report_path and os.path.exists(report_path) else None
351
 
352
  except Exception as e:
353
  print("🚨 ERROR:", e)
354
  history.append({"role": "assistant", "content": f"❌ Error occurred: {str(e)}"})
355
+ yield history, None
356
 
357
+ send_btn.click(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output])
358
+ msg_input.submit(analyze, inputs=[msg_input, gr.State([]), file_upload], outputs=[chatbot, download_output])
359
  return demo
360
 
361
  if __name__ == "__main__":