Spaces:
Sleeping
Sleeping
| # 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 | |
| ) |