""" 🩸 LeukemiaScope Agentic - Multi-Agent Blood Cell Analysis HuggingFace Spaces deployment with multi-step patient flow and PDF export """ import os import sys import gradio as gr from PIL import Image from datetime import datetime # Add current dir to path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from graph.workflow import run_analysis from tools.medgemma_predictor import get_predictor from tools.pdf_generator import generate_pdf_report # Global state current_patient = {} def get_progress_html(active_step=1): """Generate progress bar HTML with the given step highlighted""" def step_style(n): if n == active_step: return "background: rgba(255,255,255,0.95); color: #dc2626;" elif n < active_step: return "background: rgba(255,255,255,0.5); color: #dc2626;" else: return "background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.7);" def label_style(n): if n == active_step: return "color: white; font-weight: 600;" elif n < active_step: return "color: rgba(255,255,255,0.7);" else: return "color: rgba(255,255,255,0.5);" def line_style(n): if n < active_step: return "background: rgba(255,255,255,0.6);" else: return "background: rgba(255,255,255,0.3);" labels = {1: "Patient Info", 2: "Upload Image", 3: "Report"} return f"""

🩸 LeukemiaScope

1
{labels[1]}
2
{labels[2]}
3
{labels[3]}
""" def validate_patient_info(name, dob, gender): """Validate patient information before proceeding""" if not name or len(name.strip()) < 2: return False, "Please enter patient name (at least 2 characters)" return True, "" def save_patient_info(name, dob, gender): """Save patient info and move to next step""" global current_patient is_valid, error_msg = validate_patient_info(name, dob, gender) if not is_valid: return ( gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), f"❌ {error_msg}", get_progress_html(1) ) current_patient = { "name": name.strip(), "dob": dob, "gender": gender, "id": f"LS-{datetime.now().strftime('%Y%m%d%H%M%S')}" } return ( gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), f"✅ Patient registered: {name}", get_progress_html(2) ) def go_back_to_step1(): """Go back to patient info step""" return ( gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), get_progress_html(1) ) def analyze_image_workflow(image): """Run the agentic workflow on uploaded image""" global current_patient if image is None: return ( gr.update(visible=False), gr.update(visible=False), "❌ Please upload an image", "", "", None, get_progress_html(2) ) # Convert to PIL if needed if not isinstance(image, Image.Image): image = Image.fromarray(image).convert("RGB") else: image = image.convert("RGB") try: # Run the agentic workflow result = run_analysis( image=image, patient_id=current_patient.get("id", "Anonymous"), patient_context=f"Name: {current_patient.get('name')}, DOB: {current_patient.get('dob')}, Gender: {current_patient.get('gender')}" ) result["patient_name"] = current_patient.get("name", "") result["patient_dob"] = current_patient.get("dob", "") result["patient_gender"] = current_patient.get("gender", "") result["patient_id"] = current_patient.get("id", "") # Generate enhanced report from agents.report_generator import generate_report report = generate_report( patient_name=result["patient_name"], patient_dob=result["patient_dob"], patient_gender=result["patient_gender"], classification=result.get("classification", "Unknown"), confidence=result.get("confidence", 0.0), clinical_advice=result.get("clinical_advice"), next_steps=result.get("next_steps"), severity=result.get("severity"), patient_id=result["patient_id"] ) # Generate PDF pdf_path = generate_pdf_report( patient_name=result["patient_name"], patient_dob=result["patient_dob"], patient_id=result["patient_id"], classification=result.get("classification", "Unknown"), confidence=result.get("confidence", 0.0), clinical_advice=result.get("clinical_advice"), next_steps=result.get("next_steps"), severity=result.get("severity"), patient_gender=result.get("patient_gender") ) # Workflow trace classification = result.get("classification", "Unknown") confidence = result.get("confidence", 0.0) trace = f""" ### Workflow Execution | Step | Agent | Status | |------|-------|--------| | 1 | 🔬 Image Analyzer | ✅ Complete | | 2 | 🩺 Clinical Advisor | {"✅ Complete" if result.get("clinical_complete") else "⏭️ Skipped"} | | 3 | 📋 Report Generator | ✅ Complete | **Classification**: {classification} ({confidence:.1%} confidence) """ return ( gr.update(visible=False), gr.update(visible=True), "✅ Analysis complete!", report, trace, pdf_path, get_progress_html(3) ) except Exception as e: import traceback traceback.print_exc() return ( gr.update(visible=True), gr.update(visible=False), f"❌ Error: {str(e)}", "", "", None, get_progress_html(2) ) def start_new_analysis(): """Reset and start a new analysis""" global current_patient current_patient = {} return ( gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), "", "", "Not specified", None, "", "", None, get_progress_html(1) ) # ==================== Build Gradio UI ==================== custom_css = """ .gradio-container { max-width: 1000px !important; } .gradio-container .gap { gap: 4px !important; } .step-header { background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%); color: white; padding: 15px 20px; border-radius: 10px; margin-bottom: 20px; } /* Dark mode: force all HTML content to keep readable text on their light backgrounds */ .dark .ls-card { color: #1e293b !important; } .dark .ls-card * { color: inherit !important; } .dark .ls-card h1, .dark .ls-card h2, .dark .ls-card h3, .dark .ls-card h4 { color: inherit !important; } .dark .ls-card code { color: #1e40af !important; background: rgba(0,0,0,0.06) !important; } /* Header stays white text */ .dark .ls-header, .dark .ls-header * { color: white !important; } /* Footer */ .dark .ls-footer { background: #1e293b !important; } .dark .ls-footer p { color: #94a3b8 !important; } .dark .ls-footer code { color: #60a5fa !important; } """ custom_css += """ /* Spinner animation */ @keyframes ls-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes ls-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .ls-spinner { width: 48px; height: 48px; border: 4px solid rgba(220,38,38,0.2); border-top-color: #dc2626; border-radius: 50%; animation: ls-spin 1s linear infinite; } .ls-pulse { animation: ls-pulse 2s ease-in-out infinite; } """ def accept_disclaimer(): """Hide disclaimer and show the main app""" return gr.update(visible=False), gr.update(visible=True) with gr.Blocks( title="LeukemiaScope - AI Blood Cell Analysis", theme=gr.themes.Soft(), css=custom_css ) as demo: # ==================== DISCLAIMER POPUP ==================== with gr.Group(visible=True) as disclaimer_section: gr.HTML("""
🩸

LeukemiaScope

AI-Powered Blood Cell Analysis with Multi-Agent Workflow

MedGemma 1.5 4B LangGraph Agents Gemini 3 Flash
⚕️

Medical Disclaimer

LeukemiaScope is an AI research prototype developed for the MedGemma Impact Challenge 2026. It is designed to assist — not replace — trained medical professionals in screening blood cell images for signs of Acute Lymphoblastic Leukemia (ALL).

⚠️ This is NOT a certified medical device. All results require confirmation through standard laboratory procedures (CBC, bone marrow biopsy, flow cytometry) by a qualified hematologist or oncologist. Do not make clinical decisions based solely on this tool.

🔗 Agentic AI Workflow

Your image is processed through a 3-step intelligent pipeline built with LangGraph. Each agent specializes in a specific task and passes its findings to the next:

1
🔬 Image Analyzer

Fine-tuned MedGemma 1.5 4B with LoRA classifies cells as Normal or Leukemia.

2
🩺 Clinical Advisor

If leukemia is detected, Gemini 3 Flash generates clinical advice and risk assessment.

3
📋 Report Generator

Compiles a structured HTML report with patient data, results, and downloadable PDF.

📷 Supported Image Types

The model was trained on the Kaggle Leukemia Classification Dataset (ALL-IDB). For best results, upload images similar to the examples below:

""") # Example images using Gradio components with gr.Row(): with gr.Column(): gr.HTML("""

✅ Normal Blood Cell

Healthy lymphocyte — round, well-defined

""") gr.Image( value="examples/normal_cell.png", label="Normal Cell Example", show_label=False, height=220, interactive=False ) with gr.Column(): gr.HTML("""

⚠️ Leukemia Blast Cell

Abnormal blast — irregular, large nucleus

""") gr.Image( value="examples/leukemia_cell.png", label="Leukemia Cell Example", show_label=False, height=220, interactive=False ) gr.HTML("""
✔️ Dark background ✔️ Single cell crops ✔️ Wright/Giemsa stain ✔️ JPEG or PNG ✔️ Clear, in-focus
""") accept_btn = gr.Button( "✅ I Understand & Accept — Proceed to App", variant="primary", size="lg" ) # ==================== MAIN APP (hidden until disclaimer accepted) ==================== with gr.Group(visible=False) as main_app: # Dynamic progress bar progress_bar = gr.HTML(value=get_progress_html(1)) status_msg = gr.Markdown("") # Step 1: Patient Information with gr.Group(visible=True) as step1: gr.Markdown("## 📋 Step 1: Patient Information") gr.Markdown("Please enter the patient details before proceeding with the analysis.") with gr.Row(): with gr.Column(): patient_name = gr.Textbox(label="Full Name *", placeholder="Enter patient's full name", max_lines=1) patient_dob = gr.Textbox(label="Date of Birth", placeholder="YYYY-MM-DD", max_lines=1) patient_gender = gr.Dropdown(label="Gender", choices=["Not specified", "Male", "Female", "Other"], value="Not specified") gr.HTML("""

🔒 Privacy Notice: Your personal information is used solely to generate this screening report. No data is stored, retained, or shared — all information is discarded after the session ends.

""") privacy_consent = gr.Checkbox(label="I acknowledge that my information will not be stored and consent to proceed.", value=False) next_btn_1 = gr.Button("Continue to Image Upload →", variant="primary", size="lg", interactive=False) # Step 2: Image Upload with gr.Group(visible=False) as step2: gr.Markdown("## 📷 Step 2: Upload Blood Cell Image") gr.Markdown("Upload a microscopy image of blood cells for analysis.") with gr.Row(): with gr.Column(): image_input = gr.Image(label="Blood Cell Image", type="pil", height=350) with gr.Row(): back_btn = gr.Button("← Back", size="lg") analyze_btn = gr.Button("🔬 Analyze Image", variant="primary", size="lg") # Processing overlay (hidden by default) with gr.Group(visible=False) as processing_overlay: gr.HTML("""

🔬 Analyzing Blood Cell Image...

Multi-agent workflow in progress. This may take 15–30 seconds.

🔬 Agent 1: Image Analyzer running MedGemma...
🩺 Agent 2: Clinical Advisor (pending)
📋 Agent 3: Report Generator (pending)
""") # Step 3: Results with gr.Group(visible=False) as step3: gr.HTML("""

✅ Analysis Complete — Report Ready

""") # Action buttons — prominent at top with gr.Row(): pdf_download = gr.File(label="📥 Download PDF Report", interactive=False) with gr.Row(): new_analysis_btn = gr.Button("🔄 Start New Analysis", variant="primary", size="lg") # Workflow trace (collapsible) with gr.Accordion("📡 Workflow Execution Trace", open=False): trace_output = gr.Markdown(label="Workflow Trace") # Full-width report gr.HTML("""

📄 Full Medical Report

""") report_output = gr.HTML(label="Medical Report") # Footer (inside main_app so it hides with disclaimer) gr.HTML(""" """) # ==================== Event Handlers ==================== # Disclaimer accept accept_btn.click(accept_disclaimer, [], [disclaimer_section, main_app]) # Privacy consent toggles Continue button privacy_consent.change( fn=lambda checked: gr.update(interactive=checked), inputs=[privacy_consent], outputs=[next_btn_1] ) # Step 1 -> Step 2 next_btn_1.click(save_patient_info, [patient_name, patient_dob, patient_gender], [step1, step2, step3, status_msg, progress_bar]) back_btn.click(go_back_to_step1, [], [step1, step2, step3, progress_bar]) # Analyze: chain events — show overlay first, then run analysis def show_processing(): return gr.update(visible=False), gr.update(visible=True), get_progress_html(2) def run_and_finish(image): # Run the actual analysis result = analyze_image_workflow(image) # result is (step2_vis, step3_vis, status, report, trace, pdf, progress) # We also need to hide the processing overlay return ( result[0], # step2 result[1], # step3 gr.update(visible=False), # hide processing overlay result[2], # status_msg result[3], # report_output result[4], # trace_output result[5], # pdf_download result[6], # progress_bar ) analyze_btn.click( show_processing, [], [step2, processing_overlay, progress_bar] ).then( run_and_finish, [image_input], [step2, step3, processing_overlay, status_msg, report_output, trace_output, pdf_download, progress_bar] ) new_analysis_btn.click(start_new_analysis, [], [step1, step2, step3, patient_name, patient_dob, patient_gender, image_input, report_output, trace_output, pdf_download, progress_bar]) # Pre-load model at startup print("=" * 60) print("🩸 LeukemiaScope - Agentic AI Workflow") print("=" * 60) print("📥 Pre-loading MedGemma model...") predictor = get_predictor() predictor.load() print("✅ Model loaded! Launching app...") # Launch for HuggingFace Spaces demo.launch(server_name="0.0.0.0", server_port=7860)