"""
🩸 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"""
"""
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("""
⚕️
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:
Fine-tuned MedGemma 1.5 4B with LoRA classifies cells as Normal or Leukemia.
If leukemia is detected, Gemini 3 Flash generates clinical advice and risk assessment.
Compiles a structured HTML report with patient data, results, and downloadable PDF.
""")
# 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)