ryanDing26 commited on
Commit
cf55491
·
1 Parent(s): fbb09e2
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .env
2
+ __pycache__
app.py CHANGED
@@ -1,915 +1,184 @@
1
- # import os
2
- # import re
3
- # import shutil
4
- # import traceback
5
- # import gradio as gr
6
- # from pathlib import Path
7
- # from histopath.agent import A1
8
- # from dotenv import load_dotenv
9
-
10
- # # Load environment variables
11
- # load_dotenv()
12
-
13
- # # Get passcode from environment
14
- # PASSCODE = os.getenv("GRADIO_PASSWORD")
15
-
16
- # # Initialize agent (will be created after passcode validation)
17
- # agent = None
18
-
19
-
20
- # def check_for_output_files():
21
- # """Check for all files in the output directory and return their paths."""
22
- # output_dir = Path("./output")
23
- # if not output_dir.exists():
24
- # return [], []
25
-
26
- # image_extensions = {".png", ".jpg", ".jpeg", ".svg", ".tif", ".tiff"}
27
- # data_extensions = {".csv", ".txt", ".json", ".npy"}
28
-
29
- # images = []
30
- # data_files = []
31
-
32
- # for file in output_dir.iterdir():
33
- # if file.is_file():
34
- # if file.suffix.lower() in image_extensions:
35
- # images.append(str(file))
36
- # elif file.suffix.lower() in data_extensions:
37
- # data_files.append(str(file))
38
-
39
- # return images, data_files
40
-
41
-
42
- # def preview_uploaded_file(uploaded_file):
43
- # """Preview the uploaded file - show image or file info."""
44
- # if uploaded_file is None:
45
- # return None, None, "No file uploaded"
46
-
47
- # file_path = Path(uploaded_file.name)
48
- # file_ext = file_path.suffix.lower()
49
-
50
- # image_extensions = {".png", ".jpg", ".jpeg", ".svg", ".tif", ".tiff", ".svs"}
51
-
52
- # if file_ext in image_extensions:
53
- # # Show image preview
54
- # return uploaded_file.name, None, f"📷 Previewing: {file_path.name}"
55
- # else:
56
- # # Show file info
57
- # file_size = Path(uploaded_file.name).stat().st_size / 1024 # KB
58
- # return None, uploaded_file.name, f"📄 File: {file_path.name} ({file_size:.1f} KB)"
59
-
60
-
61
- # def parse_agent_output(output):
62
- # """Parse agent output to extract code blocks, observations, and regular text."""
63
- # # Strip out the message divider bars
64
- # output = re.sub(r'={30,}\s*(Human|Ai)\s+Message\s*={30,}', '', output)
65
- # output = output.strip()
66
-
67
- # parsed = {
68
- # "type": "text",
69
- # "content": output,
70
- # "code": None,
71
- # "observation": None,
72
- # "thinking": None
73
- # }
74
-
75
- # # Check for code execution block
76
- # execute_match = re.search(r'<execute>(.*?)</execute>', output, re.DOTALL)
77
- # if execute_match:
78
- # parsed["type"] = "code"
79
- # parsed["code"] = execute_match.group(1).strip()
80
- # # Extract text before the code block (thinking/explanation)
81
- # text_before = output[:execute_match.start()].strip()
82
- # # Remove any think tags but keep the content
83
- # text_before = re.sub(r'<think>(.*?)</think>', r'\1', text_before, flags=re.DOTALL)
84
- # text_before = re.sub(r'={30,}.*?={30,}', '', text_before).strip()
85
- # parsed["thinking"] = text_before if text_before else None
86
- # return parsed
87
-
88
- # # Check for observation block
89
- # observation_match = re.search(r'<observation>(.*?)</observation>', output, re.DOTALL)
90
- # if observation_match:
91
- # parsed["type"] = "observation"
92
- # parsed["observation"] = observation_match.group(1).strip()
93
- # # Extract text before observation if any
94
- # text_before = output[:observation_match.start()].strip()
95
- # text_before = re.sub(r'<think>(.*?)</think>', r'\1', text_before, flags=re.DOTALL)
96
- # text_before = re.sub(r'={30,}.*?={30,}', '', text_before).strip()
97
- # parsed["thinking"] = text_before if text_before else None
98
- # return parsed
99
-
100
- # # Check for solution block
101
- # solution_match = re.search(r'<solution>(.*?)</solution>', output, re.DOTALL)
102
- # if solution_match:
103
- # parsed["type"] = "solution"
104
- # parsed["content"] = solution_match.group(1).strip()
105
- # # Get thinking before solution
106
- # text_before = output[:solution_match.start()].strip()
107
- # text_before = re.sub(r'<think>(.*?)</think>', r'\1', text_before, flags=re.DOTALL)
108
- # text_before = re.sub(r'={30,}.*?={30,}', '', text_before).strip()
109
- # parsed["thinking"] = text_before if text_before else None
110
- # return parsed
111
-
112
- # # Clean up any remaining tags for display
113
- # cleaned = re.sub(r'<think>(.*?)</think>', r'\1', output, flags=re.DOTALL)
114
- # cleaned = re.sub(r'={30,}.*?={30,}', '', cleaned).strip()
115
- # parsed["content"] = cleaned
116
-
117
- # return parsed
118
-
119
-
120
- # def format_message_for_display(parsed_output):
121
- # """Format parsed output into a readable message for the chatbot."""
122
- # msg_parts = []
123
-
124
- # # Add thinking/explanation text first if present
125
- # if parsed_output.get("thinking"):
126
- # msg_parts.append(parsed_output["thinking"])
127
-
128
- # if parsed_output["type"] == "code":
129
- # # Add separator if there was thinking text
130
- # if parsed_output.get("thinking"):
131
- # msg_parts.append("\n---\n")
132
-
133
- # msg_parts.append("### 💻 Executing Code\n")
134
- # msg_parts.append(f"```python\n{parsed_output['code']}\n```")
135
-
136
- # elif parsed_output["type"] == "observation":
137
- # # Add separator if there was thinking text
138
- # if parsed_output.get("thinking"):
139
- # msg_parts.append("\n---\n")
140
-
141
- # msg_parts.append("### 📊 Observation\n")
142
- # msg_parts.append(f"```\n{parsed_output['observation']}\n```")
143
-
144
- # elif parsed_output["type"] == "solution":
145
- # # Add separator if there was thinking text
146
- # if parsed_output.get("thinking"):
147
- # msg_parts.append("\n---\n")
148
-
149
- # msg_parts.append("### ✅ Solution\n")
150
- # msg_parts.append(parsed_output['content'])
151
-
152
- # else:
153
- # # For regular text, just add the content if thinking wasn't already set
154
- # if not parsed_output.get("thinking"):
155
- # msg_parts.append(parsed_output["content"])
156
-
157
- # return "\n\n".join(msg_parts)
158
-
159
-
160
- # def process_agent_response(prompt, uploaded_file, chatbot_history):
161
- # """Process the agent response and update chatbot - AGGRESSIVE FIX: Minimal yields."""
162
- # global agent
163
-
164
- # if agent is None:
165
- # chatbot_history.append({
166
- # "role": "assistant",
167
- # "content": "⚠️ Please enter the passcode first to initialize the agent."
168
- # })
169
- # yield chatbot_history, None, None, None, None, "⚠️ Agent not initialized"
170
- # return
171
-
172
- # if not prompt.strip() and uploaded_file is None:
173
- # chatbot_history.append({
174
- # "role": "assistant",
175
- # "content": "⚠️ Please provide a prompt or upload a file."
176
- # })
177
- # yield chatbot_history, None, None, None, None, "⚠️ No input provided"
178
- # return
179
-
180
- # # Handle file upload
181
- # file_path = None
182
- # file_info = ""
183
- # if uploaded_file is not None:
184
- # try:
185
- # # Create data directory if it doesn't exist
186
- # data_dir = Path("./data")
187
- # data_dir.mkdir(exist_ok=True)
188
-
189
- # # Copy uploaded file to data directory
190
- # file_name = Path(uploaded_file.name).name
191
- # file_path = data_dir / file_name
192
- # shutil.copy(uploaded_file.name, file_path)
193
-
194
- # file_info = f"\n\n📎 **Uploaded file:** `{file_path}`\n"
195
-
196
- # # Augment prompt with file path
197
- # if prompt.strip():
198
- # prompt = f"{prompt}\n\nUploaded file path: {file_path}"
199
- # else:
200
- # prompt = f"I have uploaded a file at: {file_path}. Please analyze it."
201
-
202
- # except Exception as e:
203
- # error_msg = f"❌ Error handling file upload: {str(e)}"
204
- # chatbot_history.append({
205
- # "role": "assistant",
206
- # "content": error_msg
207
- # })
208
- # yield chatbot_history, None, None, None, None, error_msg
209
- # return
210
-
211
- # # Add user message to chat
212
- # user_message = prompt if not file_info else f"{prompt}{file_info}"
213
- # chatbot_history.append({"role": "user", "content": user_message})
214
-
215
- # # CRITICAL FIX: Only yield once at the start to show user message
216
- # yield chatbot_history, None, None, None, None, "🔄 Processing..."
217
-
218
- # try:
219
- # # CRITICAL FIX: Collect ALL steps without yielding
220
- # step_count = 0
221
- # collected_outputs = []
222
-
223
- # for step in agent.go_stream(prompt):
224
- # step_count += 1
225
- # output = step.get("output", "")
226
-
227
- # if output:
228
- # collected_outputs.append(output)
229
-
230
- # # CRITICAL FIX: Process ALL collected outputs at once
231
- # for output in collected_outputs:
232
- # parsed = parse_agent_output(output)
233
- # formatted_message = format_message_for_display(parsed)
234
-
235
- # # Update or append to chatbot history
236
- # if chatbot_history and chatbot_history[-1]["role"] == "assistant":
237
- # # Update the last assistant message
238
- # chatbot_history[-1]["content"] = formatted_message
239
- # else:
240
- # # Add new assistant message
241
- # chatbot_history.append({
242
- # "role": "assistant",
243
- # "content": formatted_message
244
- # })
245
-
246
- # # CRITICAL FIX: Check files only ONCE after all processing
247
- # images, data = check_for_output_files()
248
-
249
- # status_msg = f"✅ Complete ({step_count} steps)"
250
- # if images:
251
- # status_msg += f" | {len(images)} image(s)"
252
- # if data:
253
- # status_msg += f" | {len(data)} data file(s)"
254
-
255
- # # CRITICAL FIX: Final single yield with all results
256
- # yield chatbot_history, images, data, None, None, status_msg
257
-
258
- # except Exception as e:
259
- # error_trace = traceback.format_exc()
260
- # error_msg = f"❌ **Error:** {str(e)}\n\n<details>\n<summary>Stack Trace</summary>\n\n```\n{error_trace}\n```\n</details>"
261
-
262
- # chatbot_history.append({
263
- # "role": "assistant",
264
- # "content": error_msg
265
- # })
266
-
267
- # yield chatbot_history, None, None, None, None, f"❌ Error: {str(e)}"
268
-
269
-
270
- # def clear_chat():
271
- # """Clear the chat history and outputs."""
272
- # return [], None, None, None, None, "Ready"
273
-
274
-
275
- # def validate_passcode(input_passcode):
276
- # """Validate the passcode and initialize the agent."""
277
- # global agent
278
-
279
- # if input_passcode == PASSCODE:
280
- # try:
281
- # # Initialize the agent
282
- # agent = A1(
283
- # path="./data",
284
- # llm="claude-sonnet-4-20250514",
285
- # use_tool_retriever=True,
286
- # timeout_seconds=600
287
- # )
288
- # return (
289
- # gr.update(visible=False), # Hide passcode section
290
- # gr.update(visible=True), # Show main interface
291
- # "✅ Access granted! Agent initialized successfully."
292
- # )
293
- # except Exception as e:
294
- # error_msg = f"❌ Failed to initialize agent: {str(e)}"
295
- # return (
296
- # gr.update(visible=True),
297
- # gr.update(visible=False),
298
- # error_msg
299
- # )
300
- # else:
301
- # return (
302
- # gr.update(visible=True),
303
- # gr.update(visible=False),
304
- # "❌ Invalid passcode. Please try again."
305
- # )
306
- # # batched streaming instead
307
- # def process_agent_response_batched(prompt, uploaded_file, chatbot_history, batch_size=5):
308
- # """Process agent response with BATCHED updates (every N steps)."""
309
- # global agent
310
-
311
- # if agent is None:
312
- # chatbot_history.append({
313
- # "role": "assistant",
314
- # "content": "⚠️ Please enter the passcode first to initialize the agent."
315
- # })
316
- # yield chatbot_history, None, None, None, None, "⚠️ Agent not initialized"
317
- # return
318
-
319
- # if not prompt.strip() and uploaded_file is None:
320
- # chatbot_history.append({
321
- # "role": "assistant",
322
- # "content": "⚠️ Please provide a prompt or upload a file."
323
- # })
324
- # yield chatbot_history, None, None, None, None, "⚠️ No input provided"
325
- # return
326
-
327
- # # Handle file upload
328
- # file_path = None
329
- # file_info = ""
330
- # if uploaded_file is not None:
331
- # try:
332
- # data_dir = Path("./data")
333
- # data_dir.mkdir(exist_ok=True)
334
-
335
- # file_name = Path(uploaded_file.name).name
336
- # file_path = data_dir / file_name
337
- # shutil.copy(uploaded_file.name, file_path)
338
-
339
- # file_info = f"\n\n📎 **Uploaded file:** `{file_path}`\n"
340
-
341
- # if prompt.strip():
342
- # prompt = f"{prompt}\n\nUploaded file path: {file_path}"
343
- # else:
344
- # prompt = f"I have uploaded a file at: {file_path}. Please analyze it."
345
-
346
- # except Exception as e:
347
- # error_msg = f"❌ Error handling file upload: {str(e)}"
348
- # chatbot_history.append({
349
- # "role": "assistant",
350
- # "content": error_msg
351
- # })
352
- # yield chatbot_history, None, None, None, None, error_msg
353
- # return
354
-
355
- # # Add user message to chat
356
- # user_message = prompt if not file_info else f"{prompt}{file_info}"
357
- # chatbot_history.append({"role": "user", "content": user_message})
358
- # yield chatbot_history, None, None, None, None, "🔄 Processing..."
359
-
360
- # try:
361
- # # Stream with batching
362
- # step_count = 0
363
- # batch_count = 0
364
-
365
- # for step in agent.go_stream(prompt):
366
- # step_count += 1
367
- # output = step.get("output", "")
368
-
369
- # if output:
370
- # parsed = parse_agent_output(output)
371
- # formatted_message = format_message_for_display(parsed)
372
-
373
- # # Update chatbot history
374
- # if chatbot_history and chatbot_history[-1]["role"] == "assistant":
375
- # chatbot_history[-1]["content"] = formatted_message
376
- # else:
377
- # chatbot_history.append({
378
- # "role": "assistant",
379
- # "content": formatted_message
380
- # })
381
-
382
- # # Only yield every batch_size steps
383
- # if step_count % batch_size == 0:
384
- # batch_count += 1
385
- # yield chatbot_history, None, None, None, None, f"🔄 Step {step_count}..."
386
-
387
- # # Final yield with files
388
- # images, data = check_for_output_files()
389
-
390
- # status_msg = f"✅ Complete ({step_count} steps)"
391
- # if images:
392
- # status_msg += f" | {len(images)} image(s)"
393
- # if data:
394
- # status_msg += f" | {len(data)} data file(s)"
395
-
396
- # yield chatbot_history, images, data, None, None, status_msg
397
-
398
- # except Exception as e:
399
- # error_trace = traceback.format_exc()
400
- # error_msg = f"❌ **Error:** {str(e)}\n\n<details>\n<summary>Stack Trace</summary>\n\n```\n{error_trace}\n```\n</details>"
401
-
402
- # chatbot_history.append({
403
- # "role": "assistant",
404
- # "content": error_msg
405
- # })
406
-
407
- # yield chatbot_history, None, None, None, None, f"❌ Error: {str(e)}"
408
-
409
-
410
- # # Custom theme
411
- # custom_theme = gr.themes.Soft(
412
- # primary_hue="indigo",
413
- # secondary_hue="purple",
414
- # neutral_hue="slate",
415
- # font=["Inter", "system-ui", "sans-serif"],
416
- # text_size="md",
417
- # ).set(
418
- # button_primary_background_fill="*primary_500",
419
- # button_primary_background_fill_hover="*primary_600",
420
- # block_label_text_weight="600",
421
- # block_title_text_weight="600",
422
- # )
423
-
424
- # with gr.Blocks(title="HistoPath Agent", theme=custom_theme, css="""
425
- # .gradio-container {
426
- # max-width: 100% !important;
427
- # }
428
- # .main-header {
429
- # text-align: center;
430
- # padding: 1.5rem 0;
431
- # background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
432
- # color: white;
433
- # border-radius: 8px;
434
- # margin-bottom: 1.5rem;
435
- # }
436
- # .main-header h1 {
437
- # margin: 0;
438
- # font-size: 2.2rem;
439
- # font-weight: 700;
440
- # }
441
- # .main-header p {
442
- # margin: 0.5rem 0 0 0;
443
- # opacity: 0.95;
444
- # font-size: 1.1rem;
445
- # }
446
- # .file-upload-box .wrap {
447
- # min-width: 0 !important;
448
- # }
449
- # .file-upload-box .file-name {
450
- # word-break: break-word !important;
451
- # white-space: normal !important;
452
- # overflow-wrap: break-word !important;
453
- # }
454
- # .tab-nav {
455
- # margin-bottom: 0.5rem;
456
- # }
457
- # /* Better styling for code and observation blocks */
458
- # .message.bot pre {
459
- # background-color: #f6f8fa !important;
460
- # border: 1px solid #d0d7de !important;
461
- # border-radius: 6px !important;
462
- # padding: 12px !important;
463
- # margin: 8px 0 !important;
464
- # }
465
- # .message.bot h3 {
466
- # margin-top: 12px !important;
467
- # margin-bottom: 8px !important;
468
- # font-weight: 600 !important;
469
- # }
470
- # .message.bot hr {
471
- # border: none !important;
472
- # border-top: 2px solid #e1e4e8 !important;
473
- # margin: 16px 0 !important;
474
- # }
475
- # """) as demo:
476
-
477
- # # Header
478
- # gr.HTML("""
479
- # <div class="main-header">
480
- # <h1>🔬 HistoPath Agent</h1>
481
- # <p>AI-Powered Histopathology Analysis Assistant</p>
482
- # </div>
483
- # """)
484
-
485
- # # Passcode section
486
- # with gr.Group(visible=True) as passcode_section:
487
- # gr.Markdown("### 🔐 Authentication Required")
488
-
489
- # with gr.Row():
490
- # passcode_input = gr.Textbox(
491
- # label="Passcode",
492
- # type="password",
493
- # placeholder="Enter your passcode...",
494
- # scale=3
495
- # )
496
- # passcode_btn = gr.Button("🔓 Unlock", variant="primary", scale=1, size="lg")
497
-
498
- # passcode_status = gr.Textbox(
499
- # label="Status",
500
- # interactive=False,
501
- # lines=2
502
- # )
503
-
504
- # # Main interface (hidden initially)
505
- # with gr.Group(visible=False) as main_interface:
506
- # with gr.Row(equal_height=True):
507
- # # Left column - Chat interface
508
- # with gr.Column(scale=3):
509
- # chatbot = gr.Chatbot(
510
- # label="💬 Conversation",
511
- # type="messages",
512
- # height=550,
513
- # show_label=True,
514
- # render_markdown=True,
515
- # )
516
-
517
- # # Input area
518
- # with gr.Row():
519
- # with gr.Column(scale=7):
520
- # prompt_input = gr.Textbox(
521
- # label="Your Query",
522
- # placeholder="E.g., 'Caption the uploaded whole slide image' or 'Segment cells using instanseg model'",
523
- # lines=2,
524
- # max_lines=5,
525
- # show_label=False,
526
- # )
527
- # with gr.Column(scale=3):
528
- # file_upload = gr.File(
529
- # label="📎 Upload File",
530
- # file_types=[".svs", ".png", ".jpg", ".jpeg", ".tif", ".tiff", ".csv", ".txt", ".json", ".npy"],
531
- # height=75,
532
- # elem_classes="file-upload-box",
533
- # )
534
-
535
- # with gr.Row():
536
- # submit_btn = gr.Button("🚀 Submit", variant="primary", scale=3, size="lg")
537
- # clear_btn = gr.Button("🗑️ Clear", scale=1, size="lg", variant="secondary")
538
-
539
- # status_text = gr.Textbox(
540
- # label="Status",
541
- # interactive=False,
542
- # value="Ready",
543
- # show_label=False,
544
- # container=False,
545
- # )
546
-
547
- # # Right column - Outputs
548
- # with gr.Column(scale=2):
549
- # with gr.Tabs():
550
- # with gr.Tab("📥 Input"):
551
- # with gr.Column():
552
- # input_image_preview = gr.Image(
553
- # label="Input Image",
554
- # height=400,
555
- # show_label=False,
556
- # container=True,
557
- # )
558
- # input_file_preview = gr.File(
559
- # label="Input File",
560
- # interactive=False,
561
- # height=100,
562
- # show_label=False,
563
- # container=True,
564
- # )
565
- # input_status = gr.Textbox(
566
- # value="Upload a file to preview",
567
- # show_label=False,
568
- # interactive=False,
569
- # container=False,
570
- # )
571
-
572
- # with gr.Tab("🖼️ Images"):
573
- # output_gallery = gr.Gallery(
574
- # label="Generated Visualizations",
575
- # columns=1,
576
- # height=600,
577
- # object_fit="contain",
578
- # show_label=False,
579
- # show_download_button=True,
580
- # )
581
-
582
- # with gr.Tab("📄 Data"):
583
- # data_files = gr.File(
584
- # label="Generated Data Files",
585
- # file_count="multiple",
586
- # interactive=False,
587
- # height=600,
588
- # show_label=False,
589
- # )
590
-
591
- # # Event handlers
592
- # passcode_btn.click(
593
- # fn=validate_passcode,
594
- # inputs=[passcode_input],
595
- # outputs=[passcode_section, main_interface, passcode_status]
596
- # )
597
-
598
- # # File upload preview
599
- # file_upload.change(
600
- # fn=preview_uploaded_file,
601
- # inputs=[file_upload],
602
- # outputs=[input_image_preview, input_file_preview, input_status]
603
- # )
604
-
605
- # submit_btn.click(
606
- # fn=process_agent_response,
607
- # inputs=[prompt_input, file_upload, chatbot],
608
- # outputs=[chatbot, output_gallery, data_files, input_image_preview, input_file_preview, status_text]
609
- # )
610
-
611
- # clear_btn.click(
612
- # fn=clear_chat,
613
- # outputs=[chatbot, output_gallery, data_files, input_image_preview, input_file_preview, status_text]
614
- # )
615
-
616
- # # Allow enter key to submit
617
- # prompt_input.submit(
618
- # fn=process_agent_response,
619
- # inputs=[prompt_input, file_upload, chatbot],
620
- # outputs=[chatbot, output_gallery, data_files, input_image_preview, input_file_preview, status_text]
621
- # )
622
-
623
-
624
- # if __name__ == "__main__":
625
- # # Create necessary directories
626
- # Path("./data").mkdir(exist_ok=True)
627
- # Path("./output").mkdir(exist_ok=True)
628
-
629
- # print("=" * 60)
630
- # print("🔬 HistoPath Agent - Gradio Interface")
631
- # print("=" * 60)
632
- # print("Starting server...")
633
- # print("=" * 60)
634
-
635
- # # Launch the app
636
- # demo.launch(show_api=False)
637
  import os
638
  import re
639
  import shutil
640
  import traceback
641
  import gradio as gr
642
  from pathlib import Path
 
643
  from histopath.agent import A1
644
  from dotenv import load_dotenv
645
- from typing import List, Dict, Any, Optional, Tuple
646
 
647
- # Load environment variables
648
- load_dotenv()
 
649
 
650
- # Get passcode from environment
651
- PASSCODE = os.getenv("GRADIO_PASSWORD")
652
-
653
- # Initialize agent (will be created after passcode validation)
654
  agent = None
655
 
656
 
657
  def check_for_output_files():
658
- """Check for all files in the output directory and return their paths."""
659
  output_dir = Path("./output")
660
  if not output_dir.exists():
661
  return [], []
662
 
663
- image_extensions = {".png", ".jpg", ".jpeg", ".svg", ".tif", ".tiff"}
664
- data_extensions = {".csv", ".txt", ".json", ".npy"}
665
-
666
- images = []
667
- data_files = []
668
 
669
- for file in output_dir.iterdir():
670
- if file.is_file():
671
- if file.suffix.lower() in image_extensions:
672
- images.append(str(file))
673
- elif file.suffix.lower() in data_extensions:
674
- data_files.append(str(file))
675
-
676
- return images, data_files
677
 
678
 
679
- def preview_uploaded_file(uploaded_file):
680
- """Preview the uploaded file - show image or file info."""
681
- if uploaded_file is None:
682
- return None, None, "No file uploaded"
683
-
684
- file_path = Path(uploaded_file.name)
685
- file_ext = file_path.suffix.lower()
686
 
687
- image_extensions = {".png", ".jpg", ".jpeg", ".svg", ".tif", ".tiff", ".svs"}
688
-
689
- if file_ext in image_extensions:
690
- # Show image preview
691
- return uploaded_file.name, None, f"📷 Previewing: {file_path.name}"
692
  else:
693
- # Show file info
694
- file_size = Path(uploaded_file.name).stat().st_size / 1024 # KB
695
- return None, uploaded_file.name, f"📄 File: {file_path.name} ({file_size:.1f} KB)"
696
-
697
-
698
- def parse_agent_output(output):
699
- """Parse agent output to extract code blocks, observations, and regular text."""
700
- # Strip out the message divider bars
701
- output = re.sub(r'={30,}\s*(Human|Ai)\s+Message\s*={30,}', '', output)
702
- output = output.strip()
703
-
704
- parsed = {
705
- "type": "text",
706
- "content": output,
707
- "code": None,
708
- "observation": None,
709
- "thinking": None
710
- }
711
-
712
- # Check for code execution block
713
- execute_match = re.search(r'<execute>(.*?)</execute>', output, re.DOTALL)
714
- if execute_match:
715
- parsed["type"] = "code"
716
- parsed["code"] = execute_match.group(1).strip()
717
- # Extract text before the code block (thinking/explanation)
718
- text_before = output[:execute_match.start()].strip()
719
- # Remove any think tags but keep the content
720
- text_before = re.sub(r'<think>(.*?)</think>', r'\1', text_before, flags=re.DOTALL)
721
- text_before = re.sub(r'={30,}.*?={30,}', '', text_before).strip()
722
- parsed["thinking"] = text_before if text_before else None
723
- return parsed
724
-
725
- # Check for observation block
726
- observation_match = re.search(r'<observation>(.*?)</observation>', output, re.DOTALL)
727
- if observation_match:
728
- parsed["type"] = "observation"
729
- parsed["observation"] = observation_match.group(1).strip()
730
- # Extract text before observation if any
731
- text_before = output[:observation_match.start()].strip()
732
- text_before = re.sub(r'<think>(.*?)</think>', r'\1', text_before, flags=re.DOTALL)
733
- text_before = re.sub(r'={30,}.*?={30,}', '', text_before).strip()
734
- parsed["thinking"] = text_before if text_before else None
735
- return parsed
736
-
737
- # Check for solution block
738
- solution_match = re.search(r'<solution>(.*?)</solution>', output, re.DOTALL)
739
- if solution_match:
740
- parsed["type"] = "solution"
741
- parsed["content"] = solution_match.group(1).strip()
742
- # Get thinking before solution
743
- text_before = output[:solution_match.start()].strip()
744
- text_before = re.sub(r'<think>(.*?)</think>', r'\1', text_before, flags=re.DOTALL)
745
- text_before = re.sub(r'={30,}.*?={30,}', '', text_before).strip()
746
- parsed["thinking"] = text_before if text_before else None
747
- return parsed
748
-
749
- # Clean up any remaining tags for display
750
- cleaned = re.sub(r'<think>(.*?)</think>', r'\1', output, flags=re.DOTALL)
751
- cleaned = re.sub(r'={30,}.*?={30,}', '', cleaned).strip()
752
- parsed["content"] = cleaned
753
-
754
- return parsed
755
-
756
-
757
- def format_message_for_display(parsed_output):
758
- """Format parsed output into a readable message for the chatbot."""
759
- msg_parts = []
760
-
761
- # Add thinking/explanation text first if present
762
- if parsed_output.get("thinking"):
763
- msg_parts.append(parsed_output["thinking"])
764
-
765
- if parsed_output["type"] == "code":
766
- # Add separator if there was thinking text
767
- if parsed_output.get("thinking"):
768
- msg_parts.append("\n---\n")
769
-
770
- msg_parts.append("### 💻 Executing Code\n")
771
- msg_parts.append(f"```python\n{parsed_output['code']}\n```")
772
-
773
- elif parsed_output["type"] == "observation":
774
- # Add separator if there was thinking text
775
- if parsed_output.get("thinking"):
776
- msg_parts.append("\n---\n")
777
-
778
- msg_parts.append("### 📊 Observation\n")
779
- msg_parts.append(f"```\n{parsed_output['observation']}\n```")
780
-
781
- elif parsed_output["type"] == "solution":
782
- # Add separator if there was thinking text
783
- if parsed_output.get("thinking"):
784
- msg_parts.append("\n---\n")
785
-
786
- msg_parts.append("### ✅ Solution\n")
787
- msg_parts.append(parsed_output['content'])
788
-
789
  else:
790
- # For regular text, just add the content if thinking wasn't already set
791
- if not parsed_output.get("thinking"):
792
- msg_parts.append(parsed_output["content"])
793
 
794
- return "\n\n".join(msg_parts)
795
 
796
 
797
- # CRITICAL FIX: Wrap the main processing function to simplify type signatures
798
- def process_agent_response_wrapper(prompt: str, uploaded_file, chatbot_history: List[Dict[str, str]]):
799
- """
800
- Wrapper function with simplified type hints to avoid Gradio API introspection issues.
801
-
802
- Args:
803
- prompt: User query string
804
- uploaded_file: Uploaded file object (or None)
805
- chatbot_history: List of message dictionaries
806
-
807
- Returns:
808
- Tuple of (chatbot_history, images, data_files, preview_image, preview_file, status)
809
- """
810
  global agent
811
 
 
 
 
 
 
812
  if agent is None:
813
- chatbot_history.append({
814
- "role": "assistant",
815
- "content": "⚠️ Please enter the passcode first to initialize the agent."
816
- })
817
- yield chatbot_history, None, None, None, None, "⚠️ Agent not initialized"
818
  return
819
 
820
- if not prompt.strip() and uploaded_file is None:
821
- chatbot_history.append({
822
- "role": "assistant",
823
- "content": "⚠️ Please provide a prompt or upload a file."
824
- })
825
- yield chatbot_history, None, None, None, None, "⚠️ No input provided"
826
  return
827
 
828
- # Handle file upload
829
- file_path = None
830
- file_info = ""
831
- if uploaded_file is not None:
832
  try:
833
- # Create data directory if it doesn't exist
834
- data_dir = Path("./data")
835
- data_dir.mkdir(exist_ok=True)
836
-
837
- # Copy uploaded file to data directory
838
- file_name = Path(uploaded_file.name).name
839
- file_path = data_dir / file_name
840
- shutil.copy(uploaded_file.name, file_path)
841
-
842
- file_info = f"\n\n📎 **Uploaded file:** `{file_path}`\n"
843
-
844
- # Augment prompt with file path
845
- if prompt.strip():
846
- prompt = f"{prompt}\n\nUploaded file path: {file_path}"
847
- else:
848
- prompt = f"I have uploaded a file at: {file_path}. Please analyze it."
849
-
850
  except Exception as e:
851
- error_msg = f"❌ Error handling file upload: {str(e)}"
852
- chatbot_history.append({
853
- "role": "assistant",
854
- "content": error_msg
855
- })
856
- yield chatbot_history, None, None, None, None, error_msg
857
  return
858
 
859
- # Add user message to chat
860
- user_message = prompt if not file_info else f"{prompt}{file_info}"
861
- chatbot_history.append({"role": "user", "content": user_message})
862
-
863
- # CRITICAL FIX: Only yield once at the start to show user message
864
- yield chatbot_history, None, None, None, None, "🔄 Processing..."
865
 
 
866
  try:
867
- # CRITICAL FIX: Collect ALL steps without yielding
868
- step_count = 0
869
- collected_outputs = []
870
-
871
  for step in agent.go_stream(prompt):
872
- step_count += 1
873
- output = step.get("output", "")
874
- collected_outputs.append(output)
875
 
876
- # Now process all collected outputs
877
- for output in collected_outputs:
878
- if not output or output.strip() == "":
879
  continue
880
-
881
- parsed = parse_agent_output(output)
882
- formatted_msg = format_message_for_display(parsed)
883
-
884
- if formatted_msg and formatted_msg.strip():
885
- chatbot_history.append({
886
- "role": "assistant",
887
- "content": formatted_msg
888
- })
889
 
890
- # CRITICAL: Single final yield with all results
891
- images, data = check_for_output_files()
892
- final_status = f"✅ Complete! Processed {step_count} steps"
893
-
894
- yield chatbot_history, images, data, None, None, final_status
895
 
896
  except Exception as e:
897
- error_trace = traceback.format_exc()
898
- error_msg = f" **Error occurred:**\n\n```\n{str(e)}\n\n{error_trace}\n```"
899
- chatbot_history.append({
900
- "role": "assistant",
901
- "content": error_msg
902
- })
903
- yield chatbot_history, None, None, None, None, f"❌ Error: {str(e)}"
904
 
905
 
906
- def validate_passcode(input_passcode: str) -> Tuple:
907
- """Validate the passcode and initialize the agent."""
908
  global agent
909
 
910
- if input_passcode == PASSCODE:
911
  try:
912
- # Initialize the agent
913
  agent = A1(
914
  path="./data",
915
  llm="claude-sonnet-4-20250514",
@@ -917,264 +186,71 @@ def validate_passcode(input_passcode: str) -> Tuple:
917
  use_tool_retriever=True,
918
  timeout_seconds=600
919
  )
920
-
921
- return (
922
- gr.update(visible=False), # Hide passcode section
923
- gr.update(visible=True), # Show main interface
924
- "✅ Authentication successful! Agent initialized."
925
- )
926
  except Exception as e:
927
- return (
928
- gr.update(visible=True), # Keep passcode section visible
929
- gr.update(visible=False), # Keep main interface hidden
930
- f"❌ Error initializing agent: {str(e)}"
931
- )
932
  else:
933
- return (
934
- gr.update(visible=True), # Keep passcode section visible
935
- gr.update(visible=False), # Keep main interface hidden
936
- "❌ Invalid passcode. Please try again."
937
- )
938
 
939
 
940
- def clear_chat():
941
- """Clear the chat and reset outputs."""
942
- # Clean output directory
943
- output_dir = Path("./output")
944
- if output_dir.exists():
945
- for file in output_dir.iterdir():
946
- if file.is_file():
947
- file.unlink()
948
-
949
- return [], None, None, None, None, "Chat cleared"
950
-
951
 
952
- # Custom theme
953
- custom_theme = gr.themes.Soft(
954
- primary_hue="indigo",
955
- secondary_hue="purple",
956
- neutral_hue="slate",
957
- font=["Inter", "system-ui", "sans-serif"],
958
- text_size="md",
959
- ).set(
960
- button_primary_background_fill="*primary_500",
961
- button_primary_background_fill_hover="*primary_600",
962
- block_label_text_weight="600",
963
- block_title_text_weight="600",
964
- )
965
 
966
- with gr.Blocks(title="HistoPath Agent", theme=custom_theme, css="""
967
- .gradio-container {
968
- max-width: 100% !important;
969
- }
970
- .main-header {
971
- text-align: center;
972
- padding: 1.5rem 0;
973
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
974
- color: white;
975
- border-radius: 8px;
976
- margin-bottom: 1.5rem;
977
- }
978
- .main-header h1 {
979
- margin: 0;
980
- font-size: 2.2rem;
981
- font-weight: 700;
982
- }
983
- .main-header p {
984
- margin: 0.5rem 0 0 0;
985
- opacity: 0.95;
986
- font-size: 1.1rem;
987
- }
988
- .file-upload-box .wrap {
989
- min-width: 0 !important;
990
- }
991
- .file-upload-box .file-name {
992
- word-break: break-word !important;
993
- white-space: normal !important;
994
- overflow-wrap: break-word !important;
995
- }
996
- .tab-nav {
997
- margin-bottom: 0.5rem;
998
- }
999
- /* Better styling for code and observation blocks */
1000
- .message.bot pre {
1001
- background-color: #f6f8fa !important;
1002
- border: 1px solid #d0d7de !important;
1003
- border-radius: 6px !important;
1004
- padding: 12px !important;
1005
- margin: 8px 0 !important;
1006
- }
1007
- .message.bot h3 {
1008
- margin-top: 12px !important;
1009
- margin-bottom: 8px !important;
1010
- font-weight: 600 !important;
1011
- }
1012
- .message.bot hr {
1013
- border: none !important;
1014
- border-top: 2px solid #e1e4e8 !important;
1015
- margin: 16px 0 !important;
1016
- }
1017
- """) as demo:
1018
 
1019
- # Header
1020
- gr.HTML("""
1021
- <div class="main-header">
1022
- <h1>🔬 HistoPath Agent</h1>
1023
- <p>AI-Powered Histopathology Analysis Assistant</p>
1024
- </div>
1025
- """)
1026
-
1027
- # Passcode section
1028
- with gr.Group(visible=True) as passcode_section:
1029
- gr.Markdown("### 🔐 Authentication Required")
1030
-
1031
  with gr.Row():
1032
- passcode_input = gr.Textbox(
1033
- label="Passcode",
1034
- type="password",
1035
- placeholder="Enter your passcode...",
1036
- scale=3
1037
- )
1038
- passcode_btn = gr.Button("🔓 Unlock", variant="primary", scale=1, size="lg")
1039
-
1040
- passcode_status = gr.Textbox(
1041
- label="Status",
1042
- interactive=False,
1043
- lines=2
1044
- )
1045
 
1046
- # Main interface (hidden initially)
1047
- with gr.Group(visible=False) as main_interface:
1048
- with gr.Row(equal_height=True):
1049
- # Left column - Chat interface
1050
  with gr.Column(scale=3):
1051
- chatbot = gr.Chatbot(
1052
- label="💬 Conversation",
1053
- type="messages",
1054
- height=550,
1055
- show_label=True,
1056
- render_markdown=True,
1057
- )
1058
-
1059
- # Input area
1060
  with gr.Row():
1061
- with gr.Column(scale=7):
1062
- prompt_input = gr.Textbox(
1063
- label="Your Query",
1064
- placeholder="E.g., 'Caption the uploaded whole slide image' or 'Segment cells using instanseg model'",
1065
- lines=2,
1066
- max_lines=5,
1067
- show_label=False,
1068
- )
1069
- with gr.Column(scale=3):
1070
- file_upload = gr.File(
1071
- label="📎 Upload File",
1072
- file_types=[".svs", ".png", ".jpg", ".jpeg", ".tif", ".tiff", ".csv", ".txt", ".json", ".npy"],
1073
- height=75,
1074
- elem_classes="file-upload-box",
1075
- )
1076
-
1077
  with gr.Row():
1078
- submit_btn = gr.Button("🚀 Submit", variant="primary", scale=3, size="lg")
1079
- clear_btn = gr.Button("🗑️ Clear", scale=1, size="lg", variant="secondary")
1080
-
1081
- status_text = gr.Textbox(
1082
- label="Status",
1083
- interactive=False,
1084
- value="Ready",
1085
- show_label=False,
1086
- container=False,
1087
- )
1088
 
1089
- # Right column - Outputs
1090
  with gr.Column(scale=2):
1091
  with gr.Tabs():
1092
- with gr.Tab("📥 Input"):
1093
- with gr.Column():
1094
- input_image_preview = gr.Image(
1095
- label="Input Image",
1096
- height=400,
1097
- show_label=False,
1098
- container=True,
1099
- )
1100
- input_file_preview = gr.File(
1101
- label="Input File",
1102
- interactive=False,
1103
- height=100,
1104
- show_label=False,
1105
- container=True,
1106
- )
1107
- input_status = gr.Textbox(
1108
- value="Upload a file to preview",
1109
- show_label=False,
1110
- interactive=False,
1111
- container=False,
1112
- )
1113
-
1114
- with gr.Tab("🖼️ Images"):
1115
- output_gallery = gr.Gallery(
1116
- label="Generated Visualizations",
1117
- columns=1,
1118
- height=600,
1119
- object_fit="contain",
1120
- show_label=False,
1121
- show_download_button=True,
1122
- )
1123
-
1124
- with gr.Tab("📄 Data"):
1125
- data_files = gr.File(
1126
- label="Generated Data Files",
1127
- file_count="multiple",
1128
- interactive=False,
1129
- height=600,
1130
- show_label=False,
1131
- )
1132
-
1133
- # Event handlers
1134
- passcode_btn.click(
1135
- fn=validate_passcode,
1136
- inputs=[passcode_input],
1137
- outputs=[passcode_section, main_interface, passcode_status]
1138
- )
1139
-
1140
- # File upload preview
1141
- file_upload.change(
1142
- fn=preview_uploaded_file,
1143
- inputs=[file_upload],
1144
- outputs=[input_image_preview, input_file_preview, input_status]
1145
- )
1146
-
1147
- submit_btn.click(
1148
- fn=process_agent_response_wrapper,
1149
- inputs=[prompt_input, file_upload, chatbot],
1150
- outputs=[chatbot, output_gallery, data_files, input_image_preview, input_file_preview, status_text]
1151
- )
1152
-
1153
- clear_btn.click(
1154
- fn=clear_chat,
1155
- outputs=[chatbot, output_gallery, data_files, input_image_preview, input_file_preview, status_text]
1156
- )
1157
-
1158
- # Allow enter key to submit
1159
- prompt_input.submit(
1160
- fn=process_agent_response_wrapper,
1161
- inputs=[prompt_input, file_upload, chatbot],
1162
- outputs=[chatbot, output_gallery, data_files, input_image_preview, input_file_preview, status_text]
1163
- )
1164
 
1165
 
1166
  if __name__ == "__main__":
1167
- # Create necessary directories
1168
  Path("./data").mkdir(exist_ok=True)
1169
  Path("./output").mkdir(exist_ok=True)
1170
 
1171
- print("=" * 60)
1172
- print("🔬 HistoPath Agent - Gradio Interface")
1173
- print("=" * 60)
1174
- print("Starting server...")
1175
- print("=" * 60)
1176
 
1177
- # Launch the app with API documentation completely disabled
1178
- demo.launch(
1179
- show_api=False,
1180
- )
 
1
+ """
2
+ SIMPLIFIED VERSION - No monkey patching, just clean type hints
3
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import os
5
  import re
6
  import shutil
7
  import traceback
8
  import gradio as gr
9
  from pathlib import Path
10
+ from typing import List, Dict, Any, Tuple, Optional
11
  from histopath.agent import A1
12
  from dotenv import load_dotenv
 
13
 
14
+ # Load environment
15
+ if os.path.exists(".env"):
16
+ load_dotenv()
17
 
18
+ PASSCODE = os.getenv("GRADIO_PASSWORD", "TESTING")
 
 
 
19
  agent = None
20
 
21
 
22
  def check_for_output_files():
23
+ """Check output directory for files."""
24
  output_dir = Path("./output")
25
  if not output_dir.exists():
26
  return [], []
27
 
28
+ images = [str(f) for f in output_dir.glob("*") if f.suffix.lower() in {".png", ".jpg", ".jpeg", ".svg", ".tif", ".tiff"}]
29
+ data = [str(f) for f in output_dir.glob("*") if f.suffix.lower() in {".csv", ".txt", ".json", ".npy"}]
 
 
 
30
 
31
+ return images, data
 
 
 
 
 
 
 
32
 
33
 
34
+ def preview_file(file):
35
+ """Preview uploaded file."""
36
+ if file is None:
37
+ return None, None, "No file"
 
 
 
38
 
39
+ path = Path(file.name)
40
+ if path.suffix.lower() in {".png", ".jpg", ".jpeg", ".svg", ".tif", ".tiff", ".svs"}:
41
+ return file.name, None, f"Preview: {path.name}"
 
 
42
  else:
43
+ size = path.stat().st_size / 1024
44
+ return None, file.name, f"File: {path.name} ({size:.1f} KB)"
45
+
46
+
47
+ def parse_output(text):
48
+ """Parse agent output."""
49
+ text = re.sub(r'={30,}.*?={30,}', '', text, flags=re.DOTALL).strip()
50
+
51
+ result = {"type": "text", "content": text, "code": None, "obs": None, "think": None}
52
+
53
+ code_match = re.search(r'<execute>(.*?)</execute>', text, re.DOTALL)
54
+ if code_match:
55
+ result["type"] = "code"
56
+ result["code"] = code_match.group(1).strip()
57
+ before = text[:code_match.start()].strip()
58
+ before = re.sub(r'<think>(.*?)</think>', r'\1', before, flags=re.DOTALL)
59
+ result["think"] = before or None
60
+ return result
61
+
62
+ obs_match = re.search(r'<observation>(.*?)</observation>', text, re.DOTALL)
63
+ if obs_match:
64
+ result["type"] = "obs"
65
+ result["obs"] = obs_match.group(1).strip()
66
+ before = text[:obs_match.start()].strip()
67
+ before = re.sub(r'<think>(.*?)</think>', r'\1', before, flags=re.DOTALL)
68
+ result["think"] = before or None
69
+ return result
70
+
71
+ sol_match = re.search(r'<solution>(.*?)</solution>', text, re.DOTALL)
72
+ if sol_match:
73
+ result["type"] = "solution"
74
+ result["content"] = sol_match.group(1).strip()
75
+ before = text[:sol_match.start()].strip()
76
+ before = re.sub(r'<think>(.*?)</think>', r'\1', before, flags=re.DOTALL)
77
+ result["think"] = before or None
78
+ return result
79
+
80
+ result["content"] = re.sub(r'<think>(.*?)</think>', r'\1', text, flags=re.DOTALL)
81
+ return result
82
+
83
+
84
+ def format_display(parsed):
85
+ """Format for display."""
86
+ parts = []
87
+
88
+ if parsed.get("think"):
89
+ parts.append(parsed["think"])
90
+
91
+ if parsed["type"] == "code":
92
+ if parsed.get("think"):
93
+ parts.append("\n---\n")
94
+ parts.append("### 💻 Code\n")
95
+ parts.append(f"```python\n{parsed['code']}\n```")
96
+ elif parsed["type"] == "obs":
97
+ if parsed.get("think"):
98
+ parts.append("\n---\n")
99
+ parts.append("### 📊 Output\n")
100
+ parts.append(f"```\n{parsed['obs']}\n```")
101
+ elif parsed["type"] == "solution":
102
+ if parsed.get("think"):
103
+ parts.append("\n---\n")
104
+ parts.append("### ✅ Solution\n")
105
+ parts.append(parsed['content'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  else:
107
+ if not parsed.get("think"):
108
+ parts.append(parsed["content"])
 
109
 
110
+ return "\n\n".join(parts)
111
 
112
 
113
+ # CRITICAL: Simple types only
114
+ def process_query(prompt: str, file: Any, history: List[Dict[str, str]]):
115
+ """Process user query - SIMPLE TYPES ONLY."""
 
 
 
 
 
 
 
 
 
 
116
  global agent
117
 
118
+ # Initialize history if None
119
+ if history is None:
120
+ history = []
121
+
122
+ # Check agent
123
  if agent is None:
124
+ history.append({"role": "assistant", "content": "⚠️ Enter passcode first"})
125
+ yield history, None, None, None, None, "Not initialized"
 
 
 
126
  return
127
 
128
+ # Check input
129
+ if not prompt.strip() and file is None:
130
+ history.append({"role": "assistant", "content": "⚠️ Provide prompt or file"})
131
+ yield history, None, None, None, None, "No input"
 
 
132
  return
133
 
134
+ # Handle file
135
+ if file is not None:
 
 
136
  try:
137
+ Path("./data").mkdir(exist_ok=True)
138
+ fname = Path(file.name).name
139
+ fpath = Path("./data") / fname
140
+ shutil.copy(file.name, fpath)
141
+ prompt = f"{prompt}\n\nFile: {fpath}" if prompt.strip() else f"File at: {fpath}"
 
 
 
 
 
 
 
 
 
 
 
 
142
  except Exception as e:
143
+ history.append({"role": "assistant", "content": f"❌ File error: {e}"})
144
+ yield history, None, None, None, None, str(e)
 
 
 
 
145
  return
146
 
147
+ # Add user message
148
+ history.append({"role": "user", "content": prompt})
149
+ yield history, None, None, None, None, "Processing..."
 
 
 
150
 
151
+ # Run agent
152
  try:
153
+ outputs = []
 
 
 
154
  for step in agent.go_stream(prompt):
155
+ outputs.append(step.get("output", ""))
 
 
156
 
157
+ # Format outputs
158
+ for out in outputs:
159
+ if not out.strip():
160
  continue
161
+ parsed = parse_output(out)
162
+ msg = format_display(parsed)
163
+ if msg.strip():
164
+ history.append({"role": "assistant", "content": msg})
 
 
 
 
 
165
 
166
+ # Get results
167
+ imgs, data = check_for_output_files()
168
+ yield history, imgs, data, None, None, f"✅ Done ({len(outputs)} steps)"
 
 
169
 
170
  except Exception as e:
171
+ err = f"❌ Error:\n```\n{traceback.format_exc()}\n```"
172
+ history.append({"role": "assistant", "content": err})
173
+ yield history, None, None, None, None, str(e)
 
 
 
 
174
 
175
 
176
+ def validate_pass(pwd: str):
177
+ """Validate passcode."""
178
  global agent
179
 
180
+ if pwd == PASSCODE:
181
  try:
 
182
  agent = A1(
183
  path="./data",
184
  llm="claude-sonnet-4-20250514",
 
186
  use_tool_retriever=True,
187
  timeout_seconds=600
188
  )
189
+ return gr.update(visible=False), gr.update(visible=True), "✅ Authenticated"
 
 
 
 
 
190
  except Exception as e:
191
+ return gr.update(visible=True), gr.update(visible=False), f"❌ Init error: {e}"
 
 
 
 
192
  else:
193
+ return gr.update(visible=True), gr.update(visible=False), "❌ Invalid passcode"
 
 
 
 
194
 
195
 
196
+ def clear_all():
197
+ """Clear everything."""
198
+ out_dir = Path("./output")
199
+ if out_dir.exists():
200
+ for f in out_dir.iterdir():
201
+ if f.is_file():
202
+ f.unlink()
203
+ return [], None, None, None, None, "Cleared"
 
 
 
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
+ # UI
207
+ with gr.Blocks(title="HistoPath") as demo:
208
+ gr.HTML("<h1 style='text-align:center'>🔬 HistoPath Agent</h1>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
+ with gr.Group(visible=True) as pass_section:
211
+ gr.Markdown("### 🔐 Enter Passcode")
 
 
 
 
 
 
 
 
 
 
212
  with gr.Row():
213
+ pass_input = gr.Textbox(label="Passcode", type="password", scale=3)
214
+ pass_btn = gr.Button("Unlock", variant="primary", scale=1)
215
+ pass_status = gr.Textbox(label="Status", interactive=False)
 
 
 
 
 
 
 
 
 
 
216
 
217
+ with gr.Group(visible=False) as main_section:
218
+ with gr.Row():
 
 
219
  with gr.Column(scale=3):
220
+ chat = gr.Chatbot(type="messages", height=500)
 
 
 
 
 
 
 
 
221
  with gr.Row():
222
+ msg = gr.Textbox(label="Query", placeholder="Enter query...", scale=4)
223
+ upload = gr.File(label="Upload", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  with gr.Row():
225
+ send = gr.Button("Send", variant="primary", scale=2)
226
+ clear = gr.Button("Clear", scale=1)
227
+ status = gr.Textbox(value="Ready", interactive=False, show_label=False)
 
 
 
 
 
 
 
228
 
 
229
  with gr.Column(scale=2):
230
  with gr.Tabs():
231
+ with gr.Tab("Input"):
232
+ in_img = gr.Image(height=300)
233
+ in_file = gr.File(interactive=False)
234
+ in_stat = gr.Textbox(value="No file", interactive=False, show_label=False)
235
+ with gr.Tab("Images"):
236
+ out_imgs = gr.Gallery(height=500)
237
+ with gr.Tab("Data"):
238
+ out_data = gr.File(file_count="multiple", interactive=False)
239
+
240
+ # Events
241
+ pass_btn.click(validate_pass, [pass_input], [pass_section, main_section, pass_status])
242
+ upload.change(preview_file, [upload], [in_img, in_file, in_stat])
243
+ send.click(process_query, [msg, upload, chat], [chat, out_imgs, out_data, in_img, in_file, status])
244
+ clear.click(clear_all, None, [chat, out_imgs, out_data, in_img, in_file, status])
245
+ msg.submit(process_query, [msg, upload, chat], [chat, out_imgs, out_data, in_img, in_file, status])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
 
248
  if __name__ == "__main__":
 
249
  Path("./data").mkdir(exist_ok=True)
250
  Path("./output").mkdir(exist_ok=True)
251
 
252
+ print("=" * 50)
253
+ print("🔬 HistoPath Agent - Simplified")
254
+ print("=" * 50)
 
 
255
 
256
+ demo.launch(show_api=False, show_error=True)
 
 
 
histopath/tool/__pycache__/pathology.cpython-311.pyc CHANGED
Binary files a/histopath/tool/__pycache__/pathology.cpython-311.pyc and b/histopath/tool/__pycache__/pathology.cpython-311.pyc differ
 
test_gradio.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MINIMAL TEST VERSION - Diagnose what's wrong
3
+ Run this first to see if basic Gradio works
4
+ """
5
+ import gradio as gr
6
+ from typing import List, Dict
7
+
8
+ def simple_echo(message: str, history: List[Dict[str, str]]):
9
+ """Simplest possible function."""
10
+ history = history or []
11
+ history.append({"role": "user", "content": message})
12
+ history.append({"role": "assistant", "content": f"Echo: {message}"})
13
+ return history
14
+
15
+ with gr.Blocks() as demo:
16
+ gr.Markdown("# Minimal Test")
17
+ chatbot = gr.Chatbot(type="messages")
18
+ msg = gr.Textbox(label="Message")
19
+ btn = gr.Button("Send")
20
+
21
+ btn.click(simple_echo, [msg, chatbot], [chatbot])
22
+
23
+ if __name__ == "__main__":
24
+ print("Testing minimal Gradio setup...")
25
+ demo.launch(show_api=False)