medpanel_api / app.py
Yogeshwarirj's picture
Update app.py
5f199d9 verified
# app.py
# This is the entry point for HuggingFace Spaces β€” HF looks for app.py automatically.
# All the actual AI logic lives in medpanel.py. This file just wraps it in a UI.
import json
import gradio as gr
from PIL import Image
from medpanel import run_medpanel
def format_report(report):
# takes the orchestrator's raw output and turns it into something
# a human can actually read β€” with sections, bullet points, escalation flag, etc.
# nothing came back at all
if not report:
return "⚠️ No report generated."
# sometimes the model skips JSON entirely and just writes prose
# that's fine β€” just show it as-is
if isinstance(report, str):
return report
# raw_response means safe_json() couldn't parse the output
# usually happens when the model hits max_tokens mid-JSON
if "raw_response" in report:
raw = report['raw_response']
if not raw or not raw.strip():
# this is the really bad case β€” model returned nothing at all
return "⚠️ Model returned an empty response. Please try running again."
return f"πŸ“‹ REPORT\n\n{raw}"
# happy path β€” structured JSON came back, format it nicely
lines = []
primary = report.get("primary_diagnosis", "Unknown")
lines.append(f"πŸ”¬ PRIMARY DIAGNOSIS\n{primary}\n")
# list out the differentials so the clinician can see what else was considered
differentials = report.get("differential_diagnoses", [])
if differentials:
lines.append("πŸ“‹ DIFFERENTIAL DIAGNOSES")
for i, d in enumerate(differentials, 1):
lines.append(f" {i}. {d}")
lines.append("")
# agreement score tells you how much the agents agreed
# low score = agents disagreed a lot = probably escalate
score = report.get("panel_agreement_score", "N/A")
lines.append(f"🀝 PANEL AGREEMENT SCORE\n{score}/100\n")
# red flags are things the Devil's Advocate or other agents flagged as dangerous
# these should never be ignored even if the primary diagnosis looks clean
red_flags = report.get("red_flags", [])
if red_flags:
lines.append("🚨 RED FLAGS")
for flag in red_flags:
lines.append(f" β€’ {flag}")
lines.append("")
# concrete next steps β€” tests to order, referrals to make, etc.
next_steps = report.get("recommended_next_steps", [])
if next_steps:
lines.append("πŸ“Œ RECOMMENDED NEXT STEPS")
for step in next_steps:
lines.append(f" β€’ {step}")
lines.append("")
# escalation is the most important output
# red means a real doctor needs to see this now β€” don't ignore it
escalate = report.get("escalate_to_human", False)
reason = report.get("escalation_reason", "Not required")
icon = "πŸ”΄" if escalate else "🟒"
lines.append(f"{icon} ESCALATION TO HUMAN DOCTOR")
lines.append(f" {'REQUIRED' if escalate else 'Not required'}: {reason}\n")
# plain English summary written for the patient, not the clinician
summary = report.get("patient_summary", "")
if summary:
lines.append(f"πŸ’¬ PATIENT SUMMARY\n{summary}")
result = "\n".join(lines)
# last resort β€” if somehow everything above produced nothing, dump raw JSON
# better to show something ugly than a blank screen
if not result.strip():
return json.dumps(report, indent=2)
return result
def analyze(image, clinical_notes):
# this is what runs when the user clicks "Run MedPanel Analysis"
# calls the full 5-agent pipeline and formats the result for display
# don't even bother running if there are no notes
if not clinical_notes or clinical_notes.strip() == "":
return (
"⚠️ Please enter clinical notes before submitting.",
"No trace available."
)
# convert numpy array from Gradio image component to PIL
# medpanel.py expects a PIL Image or None β€” not numpy
pil_image = None
if image is not None:
pil_image = Image.fromarray(image) if not isinstance(image, Image.Image) else image
try:
results = run_medpanel(pil_image, clinical_notes)
final_report = results.get("final_report", {})
trace = results.get("panel_trace", [])
report_text = format_report(final_report)
# agent trace goes in the second tab
# shows raw JSON from each agent β€” good for debugging and for judges
trace_text = json.dumps(trace, indent=2, default=str)
# one more safety net β€” if format_report returned empty for some reason
if not report_text or report_text.strip() == "":
report_text = json.dumps(final_report, indent=2, default=str)
return report_text, trace_text
except Exception as e:
# show the full traceback in the UI so we can debug without digging through logs
import traceback
error_details = traceback.format_exc()
return f"❌ Error: {str(e)}\n\n{error_details}", "Error occurred."
# pre-filled example cases so judges and users can try the system immediately
# picked these because they're the cases where the Devil's Advocate matters most
examples = [
[
None,
"65-year-old male. Persistent cough for 3 months, night sweats, weight loss of 8kg. "
"Recent travel to high TB-prevalence region. Low-grade fever. No prior TB history."
],
[
None,
"45-year-old female. Sudden chest pain radiating to left arm, shortness of breath. "
"History of hypertension and high cholesterol. Smoker for 20 years. ECG changes noted."
],
[
None,
"32-year-old male. Severe headache, photophobia, neck stiffness, fever 39.5Β°C. "
"Petechial rash on lower limbs. Symptoms started 12 hours ago."
]
]
# ── UI ───────────────────────────────────────────────────────────────
# kept it simple β€” two columns, input on the left, output on the right
# tabbed output so you can see the final report or dig into the agent trace
with gr.Blocks(
title="MedPanel β€” Multi-Agent AI Diagnostic System",
theme=gr.themes.Soft(primary_hue="blue")
) as demo:
gr.Markdown("""
# πŸ₯ MedPanel
### Multi-Agent AI Clinical Decision Support System
Built with **Google MedGemma** (HAI-DEF) | Four specialized agents + PubMed RAG
> ⚠️ **Disclaimer:** This system is for research and demonstration purposes only.
> It is not a substitute for professional medical advice, diagnosis, or treatment.
""")
with gr.Row():
# left side β€” image upload + clinical notes
with gr.Column(scale=1):
gr.Markdown("### πŸ“₯ Input")
image_input = gr.Image(
label="Medical Image (X-ray, CT scan β€” optional)",
type="numpy"
)
notes_input = gr.Textbox(
label="Clinical Notes",
placeholder="Enter patient symptoms, history, vitals...\n\nExample: 65yo male, 3-month cough, night sweats, weight loss, recent travel to TB-endemic region.",
lines=8
)
submit_btn = gr.Button(
"πŸ”¬ Run MedPanel Analysis",
variant="primary",
size="lg"
)
# right side β€” final report + raw agent trace
with gr.Column(scale=1):
gr.Markdown("### πŸ“€ Results")
with gr.Tabs():
with gr.TabItem("πŸ“‹ Final Report"):
report_output = gr.Textbox(
label="MedPanel Diagnostic Report",
lines=20,
interactive=False
)
with gr.TabItem("πŸ” Agent Trace"):
# raw JSON from each agent
# useful for understanding the reasoning and proving all 5 agents ran
gr.Markdown("Raw output from each agent β€” useful for understanding the reasoning.")
trace_output = gr.Textbox(
label="Panel Trace (JSON)",
lines=20,
interactive=False
)
submit_btn.click(
fn=analyze,
inputs=[image_input, notes_input],
outputs=[report_output, trace_output]
)
gr.Markdown("### πŸ’‘ Try These Example Cases")
gr.Examples(
examples=examples,
inputs=[image_input, notes_input],
label="Click any example to pre-fill the inputs"
)
gr.Markdown("""
---
### 🧠 How MedPanel Works
| Agent | Role |
|-------|------|
| 🩻 Radiologist | Analyzes the medical image for visual findings |
| 🩺 Internist | Reviews clinical notes and builds a differential |
| πŸ“š Evidence Reviewer | Fetches relevant PubMed literature via RAG |
| 😈 Devil's Advocate | Challenges findings and catches missed diagnoses |
| 🎯 Orchestrator | Synthesizes all inputs into the final report |
""")
# ── Launch ────────────────────────────────────────────────────────────
# server_name="0.0.0.0" is required for HuggingFace Spaces
# without it the app binds to localhost and HF can't reach it from outside
if __name__ == "__main__":
demo.queue(
max_size=5, # don't let too many requests pile up
default_concurrency_limit=1 # one at a time β€” model can't handle parallel inference
).launch(
server_name="0.0.0.0",
server_port=7860,
show_error=True # show errors in the UI, not just buried in logs
)