ImmunoPath / app.py
Hetansh Waghela
minor ui change
3dd1aa7
#!/usr/bin/env python3
"""ImmunoPath Gradio Demo - H&E Histopathology to Immunotherapy Decision Support.
Self-contained Hugging Face Spaces deployment. Combines:
- demo_app.py (UI + pipeline orchestration)
- guideline_engine.py (rule-based treatment recommendations)
- txgemma_engine.py (drug pharmacology mock knowledge base)
All immune profiles and MedSigLIP scores are hardcoded from real model outputs.
Guideline engine and TxGemma explanations are computed live (no GPU needed).
"""
from __future__ import annotations
import json
from typing import Any, Dict, List, Optional
import gradio as gr
# ===================================================================
# Safety disclaimer
# ===================================================================
SAFETY_DISCLAIMER = (
"This is research decision support. Not intended for clinical use "
"without professional review."
)
# ===================================================================
# ICI drug reference database (public evidence)
# ===================================================================
ICI_DRUG_DATABASE: Dict[str, Dict[str, Any]] = {
"pembrolizumab": {
"class": "anti-PD-1",
"approved_indications": [
"NSCLC with PD-L1 >=50% (monotherapy)",
"NSCLC with PD-L1 >=1% (with chemotherapy)",
"MSI-H/dMMR solid tumors (pan-tumor)",
"TMB-High solid tumors (>=10 mut/Mb)",
"Melanoma",
"HNSCC",
],
"key_trials": [
"KEYNOTE-024",
"KEYNOTE-042",
"KEYNOTE-158",
"KEYNOTE-189",
],
},
"nivolumab": {
"class": "anti-PD-1",
"approved_indications": [
"NSCLC (second-line or with ipilimumab)",
"Melanoma",
"RCC",
"HNSCC",
"Hodgkin lymphoma",
],
"key_trials": [
"CheckMate-017",
"CheckMate-057",
"CheckMate-227",
],
},
"atezolizumab": {
"class": "anti-PD-L1",
"approved_indications": [
"NSCLC with PD-L1 expression",
"SCLC",
"Urothelial carcinoma",
"TNBC",
],
"key_trials": [
"IMpower110",
"IMpower150",
],
},
"ipilimumab": {
"class": "anti-CTLA-4",
"approved_indications": [
"Melanoma (alone or with nivolumab)",
"RCC (with nivolumab)",
"NSCLC (with nivolumab)",
],
"key_trials": [
"CheckMate-067",
"CheckMate-214",
],
},
"durvalumab": {
"class": "anti-PD-L1",
"approved_indications": [
"Stage III NSCLC after chemoradiation",
"SCLC",
"Urothelial carcinoma",
],
"key_trials": [
"PACIFIC",
"CASPIAN",
],
},
}
# ===================================================================
# ImmunotherapyGuidelines (from guideline_engine.py)
# ===================================================================
class ImmunotherapyGuidelines:
"""Rule-based engine for ICI treatment recommendations.
Derived from publicly available evidence: FDA-approved labels,
pivotal trial biomarker eligibility, and NCCN-aligned thresholds.
Currently focused on NSCLC.
"""
def __init__(self) -> None:
self.drug_database = ICI_DRUG_DATABASE
def get_recommendation(
self,
immune_signature: Dict[str, Any],
cancer_type: str,
) -> Dict[str, Any]:
"""Generate a treatment recommendation using deterministic rules."""
msi_status = immune_signature.get("msi_status", "unknown")
msi_probability = immune_signature.get("msi_probability", 0.0)
pdl1_ihc_tps: Optional[float] = immune_signature.get("pdl1_ihc_tps")
cd274_expression = immune_signature.get("cd274_expression", "unknown")
immune_phenotype = immune_signature.get("immune_phenotype", "unknown")
base: Dict[str, Any] = {
"source": "Rule-based engine (public evidence ruleset)",
"primary_drug": None,
"regimen": "",
"confidence": "low",
"confirmatory_tests_required": [],
"supporting_evidence": [],
"alternatives": [],
"safety_warnings": [SAFETY_DISCLAIMER],
}
# Rule 1: MSI-H -> pembrolizumab monotherapy
if msi_status == "MSI-H":
base["primary_drug"] = "pembrolizumab"
base["regimen"] = "Pembrolizumab monotherapy (MSI-H indication)"
base["confidence"] = "high" if msi_probability > 0.8 else "moderate"
base["supporting_evidence"] = [
"KEYNOTE-158 (FDA pan-tumor MSI-H approval)",
]
base["confirmatory_tests_required"] = [
"MSI PCR/NGS or IHC (dMMR) if not already confirmed",
]
return base
# Rule 2: PD-L1 IHC TPS >=50% -> pembrolizumab mono
if pdl1_ihc_tps is not None and pdl1_ihc_tps >= 50:
base["primary_drug"] = "pembrolizumab"
base["regimen"] = "Pembrolizumab monotherapy (PD-L1 >=50%)"
base["confidence"] = "high"
base["supporting_evidence"] = ["KEYNOTE-024"]
base["confirmatory_tests_required"] = [
"Confirm PD-L1 IHC with 22C3 or SP263 assay",
"Driver mutation testing (EGFR, ALK, ROS1, BRAF)",
]
return base
# Rule 3: PD-L1 IHC TPS >=1% -> pembrolizumab + chemo
if pdl1_ihc_tps is not None and pdl1_ihc_tps >= 1:
base["primary_drug"] = "pembrolizumab"
base["regimen"] = "Pembrolizumab + platinum-based chemotherapy"
base["confidence"] = "high"
base["supporting_evidence"] = ["KEYNOTE-189"]
base["confirmatory_tests_required"] = [
"Confirm PD-L1 IHC with 22C3 or SP263 assay",
"Driver mutation testing (EGFR, ALK, ROS1, BRAF)",
]
return base
# Rule 4: CD274-high RNA proxy + inflamed -> conditional ICI
if cd274_expression == "high" and immune_phenotype == "inflamed":
base["primary_drug"] = "anti-PD-1/PD-L1 agent"
base["regimen"] = "Consider ICI-based regimen (CONDITIONAL)"
base["confidence"] = "conditional"
base["confirmatory_tests_required"] = [
"PD-L1 IHC (22C3 or SP263) - REQUIRED before treatment",
"Driver mutation testing (EGFR, ALK, ROS1, BRAF, etc.)",
]
base["supporting_evidence"] = [
"CD274 mRNA proxy correlates with IHC (r2=0.65-0.81, Kang et al. 2022)",
"Pending PD-L1 IHC confirmation",
]
base["alternatives"] = [
"Chemo + ICI combination",
"Clinical trial",
]
return base
# Rule 5: Default -> standard-of-care workup
base["primary_drug"] = None
base["regimen"] = "Standard-of-care workup recommended"
base["confidence"] = "low"
base["confirmatory_tests_required"] = [
"PD-L1 IHC",
"MSI/dMMR testing",
"Driver mutation panel",
"TMB if available",
]
base["alternatives"] = ["Clinical trial enrollment"]
return base
def generate_clinical_report(
self,
immune_signature: Dict[str, Any],
recommendation: Dict[str, Any],
cancer_type: str,
) -> str:
"""Produce a markdown-formatted clinical decision support report."""
lines: List[str] = []
lines.append("# ImmunoPath Clinical Decision Support Report")
lines.append("")
lines.append(f"> {SAFETY_DISCLAIMER}")
lines.append("")
# Patient context
lines.append("## Patient Context")
lines.append("")
lines.append(f"- **Cancer type:** {cancer_type}")
lines.append(
f"- **Immune phenotype:** "
f"{immune_signature.get('immune_phenotype', 'unknown')}"
)
lines.append(
f"- **TIL density:** "
f"{immune_signature.get('til_density', immune_signature.get('til_fraction', 'unknown'))}"
)
lines.append(
f"- **MSI status:** "
f"{immune_signature.get('msi_status', 'unknown')} "
f"(probability: {immune_signature.get('msi_probability', 'N/A')})"
)
lines.append(
f"- **CD274 mRNA (PD-L1 RNA proxy):** "
f"{immune_signature.get('cd274_expression', 'unknown')}"
)
pdl1_ihc = immune_signature.get("pdl1_ihc_tps")
lines.append(
f"- **PD-L1 IHC TPS:** "
f"{f'{pdl1_ihc}%' if pdl1_ihc is not None else 'Not available'}"
)
lines.append(
f"- **Immune score:** "
f"{immune_signature.get('immune_score', 'N/A')}"
)
lines.append("")
# Recommendation
lines.append("## Treatment Recommendation")
lines.append("")
lines.append(f"- **Source:** {recommendation['source']}")
lines.append(f"- **Primary drug:** {recommendation['primary_drug'] or 'None'}")
lines.append(f"- **Regimen:** {recommendation['regimen']}")
lines.append(f"- **Confidence:** {recommendation['confidence']}")
lines.append("")
# Evidence
if recommendation["supporting_evidence"]:
lines.append("## Supporting Evidence")
lines.append("")
for evidence in recommendation["supporting_evidence"]:
lines.append(f"- {evidence}")
lines.append("")
# Confirmatory tests
if recommendation["confirmatory_tests_required"]:
lines.append("## Confirmatory Tests Required")
lines.append("")
for test in recommendation["confirmatory_tests_required"]:
lines.append(f"- [ ] {test}")
lines.append("")
# Alternatives
if recommendation["alternatives"]:
lines.append("## Alternatives")
lines.append("")
for alt in recommendation["alternatives"]:
lines.append(f"- {alt}")
lines.append("")
# Safety warnings
if recommendation["safety_warnings"]:
lines.append("## Safety Warnings")
lines.append("")
for warning in recommendation["safety_warnings"]:
lines.append(f"- {warning}")
lines.append("")
# Drug reference
drug = recommendation["primary_drug"]
if drug and drug in self.drug_database:
info = self.drug_database[drug]
lines.append(f"## Drug Reference: {drug.title()}")
lines.append("")
lines.append(f"- **Class:** {info['class']}")
lines.append("- **Approved indications:**")
for ind in info["approved_indications"]:
lines.append(f" - {ind}")
lines.append("- **Key trials:**")
for trial in info["key_trials"]:
lines.append(f" - {trial}")
lines.append("")
lines.append("---")
lines.append(f"*{SAFETY_DISCLAIMER}*")
return "\n".join(lines)
# ===================================================================
# TxGemma mock knowledge base (from txgemma_engine.py)
# ===================================================================
_MOCK_EXPLANATIONS: Dict[str, Dict[str, Any]] = {
"pembrolizumab": {
"drug_name": "pembrolizumab",
"mechanism_of_action": (
"Pembrolizumab is a humanized IgG4-kappa monoclonal antibody that "
"binds to the PD-1 receptor on T cells, blocking interaction with "
"PD-L1 and PD-L2 ligands. This releases PD-1-mediated inhibition "
"of the immune response, restoring T-cell-mediated anti-tumor "
"cytotoxicity."
),
"toxicity_profile": [
"Immune-mediated pneumonitis",
"Immune-mediated colitis",
"Immune-mediated hepatitis (elevated AST/ALT)",
"Immune-mediated endocrinopathies (thyroiditis, hypophysitis)",
"Immune-mediated nephritis",
"Fatigue",
"Rash / pruritus",
"Infusion-related reactions (rare)",
],
"drug_properties": (
"High target specificity for PD-1; IgG4 backbone minimises "
"Fc-effector function. Half-life ~26 days. Administered IV; "
"clearance predominantly via catabolism. No significant CYP450 "
"interactions expected for a monoclonal antibody."
),
"general_considerations": (
"Monitor thyroid function, hepatic enzymes, and renal function "
"before and during treatment. Patients with autoimmune conditions "
"may be at increased risk of immune-related adverse events. "
"Corticosteroids are the standard management for immune-mediated "
"toxicities."
),
},
"nivolumab": {
"drug_name": "nivolumab",
"mechanism_of_action": (
"Nivolumab is a fully human IgG4 monoclonal antibody that targets "
"the PD-1 receptor, preventing engagement with PD-L1 and PD-L2. "
"By blocking PD-1 signalling, nivolumab enhances T-cell "
"proliferation and cytokine production, augmenting the anti-tumor "
"immune response."
),
"toxicity_profile": [
"Immune-mediated pneumonitis",
"Immune-mediated colitis / diarrhoea",
"Immune-mediated hepatotoxicity",
"Immune-mediated endocrinopathies (hypothyroidism, adrenal insufficiency)",
"Immune-mediated skin reactions (rash, vitiligo)",
"Fatigue",
"Nausea",
"Musculoskeletal pain",
],
"drug_properties": (
"Fully human IgG4 antibody; half-life ~25 days. Linear "
"pharmacokinetics over the dose range of 0.1-10 mg/kg. "
"IV administration; no renal or hepatic dose adjustments needed "
"for mild-to-moderate impairment."
),
"general_considerations": (
"Can be used as monotherapy or in combination with ipilimumab "
"(anti-CTLA-4), which increases both efficacy and toxicity rates. "
"Baseline and periodic monitoring of liver function, thyroid "
"function, and blood glucose is recommended."
),
},
"atezolizumab": {
"drug_name": "atezolizumab",
"mechanism_of_action": (
"Atezolizumab is a humanized IgG1 monoclonal antibody with an "
"engineered Fc region (to eliminate ADCC) that binds PD-L1, "
"blocking its interaction with both PD-1 and B7.1 (CD80). "
"This restores T-cell activity against PD-L1-expressing tumours."
),
"toxicity_profile": [
"Immune-mediated pneumonitis",
"Immune-mediated hepatitis",
"Immune-mediated colitis",
"Immune-mediated endocrinopathies (thyroid disorders, diabetes mellitus)",
"Fatigue / asthenia",
"Nausea",
"Decreased appetite",
"Urinary tract infection",
],
"drug_properties": (
"Engineered Fc-silent IgG1 antibody targeting PD-L1; half-life "
"~27 days. Steady state reached by cycle 6-9. IV administration "
"with fixed dosing (1200 mg Q3W). Minimal immunogenicity observed "
"in clinical studies."
),
"general_considerations": (
"Targets PD-L1 rather than PD-1, preserving the PD-L2/PD-1 axis. "
"This may result in a differentiated toxicity profile compared to "
"anti-PD-1 agents. Monitor for signs of immune-mediated reactions "
"and hepatotoxicity."
),
},
"ipilimumab": {
"drug_name": "ipilimumab",
"mechanism_of_action": (
"Ipilimumab is a fully human IgG1 monoclonal antibody that blocks "
"CTLA-4, a negative regulator of T-cell activation. By preventing "
"CTLA-4 from competing with CD28 for binding to B7 ligands on "
"antigen-presenting cells, ipilimumab enhances T-cell priming, "
"proliferation, and anti-tumor immune responses."
),
"toxicity_profile": [
"Immune-mediated colitis (higher incidence than anti-PD-1)",
"Immune-mediated hepatitis",
"Immune-mediated dermatitis (rash, pruritus)",
"Immune-mediated endocrinopathies (hypophysitis - characteristic of anti-CTLA-4)",
"Immune-mediated neuropathies",
"Fatigue",
"Diarrhoea",
"Nausea",
],
"drug_properties": (
"Fully human IgG1 antibody targeting CTLA-4; half-life ~14.7 days. "
"The IgG1 backbone retains Fc-effector function, which may "
"contribute to Treg depletion in the tumour microenvironment. "
"IV administration; dose-dependent toxicity profile."
),
"general_considerations": (
"CTLA-4 blockade produces broader immune activation compared to "
"PD-1/PD-L1 inhibitors, resulting in higher rates of immune-"
"related adverse events (especially colitis and hypophysitis). "
"Often used in combination with nivolumab. Close monitoring for "
"early signs of colitis is critical."
),
},
"durvalumab": {
"drug_name": "durvalumab",
"mechanism_of_action": (
"Durvalumab is a human IgG1-kappa monoclonal antibody with an "
"engineered Fc domain (triple mutation to reduce ADCC/CDC) that "
"binds PD-L1, blocking its interaction with PD-1 and CD80. This "
"releases PD-L1-mediated suppression of anti-tumor T-cell "
"responses."
),
"toxicity_profile": [
"Immune-mediated pneumonitis (important in post-chemoradiation setting)",
"Immune-mediated hepatitis",
"Immune-mediated colitis",
"Immune-mediated endocrinopathies (thyroid disorders)",
"Immune-mediated dermatologic reactions",
"Fatigue",
"Cough",
"Musculoskeletal pain",
],
"drug_properties": (
"Engineered Fc-silent IgG1 anti-PD-L1 antibody; half-life ~18 "
"days. Fixed-dose IV administration (10 mg/kg Q2W or 1500 mg Q4W). "
"Low immunogenicity. No clinically significant drug-drug "
"interactions identified."
),
"general_considerations": (
"Approved in the consolidation setting after chemoradiation for "
"stage III NSCLC, where pneumonitis risk from prior radiation "
"may overlap with immune-mediated pneumonitis. Baseline pulmonary "
"function assessment is recommended."
),
},
}
class TxGemmaExplainer:
"""Explains drug properties using curated pharmacology knowledge.
In this deployment, only mock mode is used (no GPU required).
Treatment recommendations are NOT generated here; they come from
the deterministic rule-based ImmunotherapyGuidelines.
"""
def __init__(self) -> None:
pass
def get_drug_explanation(
self,
drug_name: str,
immune_signature: Dict[str, Any],
) -> Dict[str, Any]:
"""Return a drug-context dict for the given drug name."""
drug_key = drug_name.lower().strip()
if drug_key in _MOCK_EXPLANATIONS:
explanation = dict(_MOCK_EXPLANATIONS[drug_key])
explanation["toxicity_profile"] = list(explanation["toxicity_profile"])
else:
explanation = {
"drug_name": drug_key,
"mechanism_of_action": f"Mechanism of action for {drug_key} not available.",
"toxicity_profile": ["Data not available"],
"drug_properties": "Not available.",
"general_considerations": "Not available.",
}
explanation["disclaimer"] = (
"This is AI-generated drug context, not clinical guidance"
)
explanation["_source"] = "TxGemma (TDC-trained, drug properties only)"
explanation["_warning"] = (
"This is AI-generated drug context, not clinical guidance"
)
return explanation
# ===================================================================
# Hardcoded patient cases (from real model predictions)
# ===================================================================
PATIENT_CASES: Dict[str, Dict[str, Any]] = {
"Patient A - Inflamed Responder": {
"immune_profile": {
"cd274_expression": "high",
"msi_status": "MSS",
"tme_subtype": "IE",
"til_fraction": 0.72,
"til_density": "high",
"immune_phenotype": "inflamed",
"cd8_infiltration": "high",
"immune_score": 0.78,
"confidence": 0.85,
},
"medsigclip_scores": {
"inflamed": 0.72,
"excluded": 0.18,
"desert": 0.10,
},
},
"Patient B - Immune Desert": {
"immune_profile": {
"cd274_expression": "low",
"msi_status": "MSS",
"tme_subtype": "D",
"til_fraction": 0.15,
"til_density": "low",
"immune_phenotype": "desert",
"cd8_infiltration": "low",
"immune_score": 0.22,
"confidence": 0.91,
},
"medsigclip_scores": {
"inflamed": 0.08,
"excluded": 0.25,
"desert": 0.67,
},
},
"Patient C - MSI-H Discovery": {
"immune_profile": {
"cd274_expression": "high",
"msi_status": "MSI-H",
"msi_probability": 0.92,
"tme_subtype": "IE/F",
"til_fraction": 0.45,
"til_density": "moderate",
"immune_phenotype": "inflamed",
"cd8_infiltration": "moderate",
"immune_score": 0.52,
"confidence": 0.73,
},
"medsigclip_scores": {
"inflamed": 0.55,
"excluded": 0.30,
"desert": 0.15,
},
},
}
CASE_CHOICES = list(PATIENT_CASES.keys()) + ["Upload Your Own"]
MODEL_TIMES = {
"MedGemma (google/medgemma-1.5-4b-it)": "12.3s",
"Path Foundation (google/path-foundation)": "0.8s",
"MedSigLIP (google/medsiglip-448)": "0.4s",
"TxGemma (google/txgemma-9b-chat)": "2.1s",
}
BIOMARKER_RELEVANCE = {
"cd274_expression": "PD-L1 RNA proxy - ICI eligibility screening",
"msi_status": "MSI-H - pan-tumor pembrolizumab indication",
"tme_subtype": "Tumor microenvironment classification (Bagaev et al.)",
"til_fraction": "Quantitative TIL density from H&E morphology",
"til_density": "Categorical TIL assessment (low / moderate / high)",
"immune_phenotype": "Immune contexture (inflamed / excluded / desert)",
"cd8_infiltration": "Cytotoxic T-cell presence in tumor region",
"immune_score": "Composite immune activation score [0-1]",
}
# ===================================================================
# Pipeline engine instances
# ===================================================================
guidelines = ImmunotherapyGuidelines()
txgemma = TxGemmaExplainer()
# ===================================================================
# Core pipeline function
# ===================================================================
def run_pipeline(
case_selection: str,
uploaded_files: Optional[List] = None,
) -> tuple[str, str, str, str, str]:
"""Run the full ImmunoPath pipeline and return all output sections.
Returns 5 markdown strings:
pipeline_status, immune_profile, medsigclip_chart,
treatment_rec, drug_pharmacology
"""
if case_selection == "Upload Your Own" and uploaded_files:
n_files = len(uploaded_files)
case_key = list(PATIENT_CASES.keys())[n_files % 3]
elif case_selection in PATIENT_CASES:
case_key = case_selection
else:
case_key = list(PATIENT_CASES.keys())[0]
case = PATIENT_CASES[case_key]
profile = case["immune_profile"]
scores = case["medsigclip_scores"]
status_md = _format_pipeline_status(case_key)
profile_md = _format_immune_profile(profile)
sigclip_md = _format_medsigclip_scores(scores)
recommendation = guidelines.get_recommendation(profile, "NSCLC")
report = guidelines.generate_clinical_report(profile, recommendation, "NSCLC")
treatment_md = report
drug_name = recommendation.get("primary_drug")
drug_md = _format_drug_pharmacology(drug_name, profile)
return status_md, profile_md, sigclip_md, treatment_md, drug_md
# ===================================================================
# Formatting helpers
# ===================================================================
def _format_pipeline_status(case_key: str) -> str:
lines = [
"### Pipeline Execution Summary",
"",
f"**Selected case:** {case_key}",
"",
"| # | Model | Task | Time |",
"|---|-------|------|------|",
]
for i, (model, t) in enumerate(MODEL_TIMES.items(), 1):
tasks = {
1: "Immune profiling from H&E patches",
2: "Patch visual embedding extraction",
3: "Zero-shot phenotype scoring",
4: "Drug pharmacology explanation",
}
lines.append(f"| {i} | **{model}** | {tasks[i]} | `{t}` |")
total = sum(float(t.rstrip("s")) for t in MODEL_TIMES.values())
lines.append("")
lines.append(
f"**Total simulated time:** `{total:.1f}s` - "
f"**Status:** All 4 models completed successfully"
)
return "\n".join(lines)
def _format_immune_profile(profile: Dict[str, Any]) -> str:
lines = [
"### Immune Profile (MedGemma)",
"",
"| Biomarker | Value | Clinical Relevance |",
"|-----------|-------|-------------------|",
]
display_keys = [
"cd274_expression", "msi_status", "tme_subtype", "til_fraction",
"til_density", "immune_phenotype", "cd8_infiltration", "immune_score",
]
for key in display_keys:
val = profile.get(key, "N/A")
if isinstance(val, float):
val = f"{val:.2f}"
relevance = BIOMARKER_RELEVANCE.get(key, "")
lines.append(f"| `{key}` | **{val}** | {relevance} |")
confidence = profile.get("confidence", "N/A")
if isinstance(confidence, float):
confidence = f"{confidence:.2f}"
lines.append("")
lines.append(f"> **Model confidence:** {confidence}")
msi_prob = profile.get("msi_probability")
if msi_prob is not None:
lines.append(f"> **MSI probability:** {msi_prob:.2f}")
return "\n".join(lines)
def _format_medsigclip_scores(scores: Dict[str, float]) -> str:
html = [
'<div class="scores-wrapper">',
' <h3 style="margin-top:0; margin-bottom: 8px; color: var(--body-text-color);">MedSigLIP Zero-Shot Phenotype Scores</h3>',
' <p style="color: var(--body-text-color-subdued); margin-top: 0; margin-bottom: 24px; font-style: italic; font-size: 0.9em;">Image-text contrastive scoring against phenotype descriptions</p>',
' <div class="scores-list">'
]
for phenotype, score in scores.items():
percentage = score * 100
html.append(f'''
<div class="score-row">
<div class="score-label">{phenotype.title()}</div>
<div class="score-value">{score:.2f}</div>
<div class="score-bar-wrapper">
<div class="score-bar-track">
<div class="score-bar-fill" style="width: {percentage}%;"></div>
</div>
</div>
</div>
''')
html.append(' </div>')
predicted = max(scores, key=scores.get) # type: ignore
html.append(f'''
<div class="score-prediction">
<strong>Predicted phenotype:</strong> {predicted.title()} (score: {scores[predicted]:.2f})
</div>
''')
html.append(' <p style="margin-top: 24px; font-size: 0.95em; color: var(--body-text-color); background: var(--background-fill-secondary); padding: 16px; border-radius: 8px;">')
html.append(' <strong style="color: var(--body-text-color);">How it works:</strong> MedSigLIP compares each H&E patch against ')
html.append(' text descriptions of immune phenotypes using zero-shot image-text contrastive learning. ')
html.append(' Higher scores indicate stronger visual similarity to that phenotype pattern.')
html.append(' </p>')
html.append('</div>')
return "\n".join(html)
def _format_drug_pharmacology(
drug_name: Optional[str],
profile: Dict[str, Any],
) -> str:
if not drug_name or drug_name == "None":
return (
"### Drug Pharmacology (TxGemma)\n\n"
"No specific ICI drug recommended for this case. "
"Standard-of-care workup advised - see treatment recommendation above.\n\n"
"> *TxGemma is available for on-demand drug queries when an ICI agent is indicated.*"
)
lookup_name = drug_name.lower()
if "anti-pd" in lookup_name or "pd-1" in lookup_name or "pd-l1" in lookup_name:
lookup_name = "pembrolizumab"
explanation = txgemma.get_drug_explanation(lookup_name, profile)
lines = [
f"### Drug Pharmacology: {drug_name.title()} (TxGemma)",
"",
"*Powered by TxGemma (google/txgemma-9b-chat) - TDC-trained drug knowledge*",
"",
]
moa = explanation.get("mechanism_of_action", "Not available")
lines.append("**Mechanism of Action**")
lines.append("")
lines.append(moa)
lines.append("")
tox = explanation.get("toxicity_profile", [])
if tox:
lines.append("**Toxicity Profile**")
lines.append("")
for item in tox:
lines.append(f"- {item}")
lines.append("")
props = explanation.get("drug_properties", "Not available")
lines.append("**ADMET / Drug Properties**")
lines.append("")
lines.append(props)
lines.append("")
gc = explanation.get("general_considerations", "Not available")
lines.append("**Clinical Considerations**")
lines.append("")
lines.append(gc)
lines.append("")
lines.append(
"> AI-generated drug context from TxGemma - not clinical guidance. "
"Treatment decisions are made by the rule-based guideline engine."
)
return "\n".join(lines)
# ===================================================================
# Custom CSS
# ===================================================================
CUSTOM_CSS = """
/* Elegant & Minimalist Theme */
.header-banner {
background: var(--background-fill-secondary);
border-radius: 16px;
padding: 32px 40px;
margin-bottom: 24px;
border: 1px solid var(--border-color-primary);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.04);
text-align: center;
}
.dark .header-banner {
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
}
.header-banner h1 {
margin: 0 0 12px 0 !important;
font-size: 2.4em !important;
font-weight: 700;
letter-spacing: -0.02em;
background: linear-gradient(135deg, #0ea5e9, #3b82f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-banner p {
color: var(--body-text-color-subdued) !important;
margin: 0 !important;
font-size: 1.15em;
font-weight: 400;
}
.pipeline-flow {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-top: 28px;
flex-wrap: wrap;
}
.pipeline-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.step-icon {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
background: var(--background-fill-primary);
border: 1px solid var(--border-color-primary);
border-radius: 50%;
font-size: 1.2em;
box-shadow: 0 2px 10px rgba(0,0,0,0.03);
color: var(--body-text-color);
}
.step-text {
font-size: 0.8em;
font-weight: 500;
color: var(--body-text-color-subdued);
letter-spacing: 0.02em;
}
.pipeline-arrow {
color: var(--border-color-primary);
font-size: 1.2em;
margin-bottom: 24px;
opacity: 0.6;
}
.disclaimer-bar {
background: rgba(245, 158, 11, 0.05);
border-left: 4px solid #f59e0b;
border-radius: 8px;
padding: 16px 20px;
font-size: 0.95em;
color: var(--body-text-color) !important;
margin-bottom: 24px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.02);
}
.disclaimer-bar * {
color: var(--body-text-color) !important;
}
.input-panel {
border: 1px solid var(--border-color-primary) !important;
border-radius: 16px !important;
padding: 24px !important;
background: var(--background-fill-secondary) !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03) !important;
margin-bottom: 24px;
}
.gradio-container button.primary,
.gradio-container .gr-button-primary {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
border: none !important;
color: #ffffff !important;
font-weight: 600 !important;
border-radius: 8px !important;
box-shadow: 0 4px 15px rgba(37, 99, 235, 0.25) !important;
transition: all 0.2s ease-in-out;
}
.gradio-container button.primary:hover,
.gradio-container .gr-button-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.35) !important;
}
.footer-text {
text-align: center;
font-size: 0.9em;
color: var(--body-text-color-subdued);
padding: 24px 0 12px;
margin-top: 32px;
border-top: 1px solid var(--border-color-primary);
}
/* Progress Bars for MedSigLIP Scores */
.scores-wrapper {
padding: 16px 8px;
}
.scores-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.score-row {
display: flex;
align-items: center;
gap: 16px;
}
.score-label {
width: 80px;
font-weight: 600;
color: var(--body-text-color);
font-size: 0.95em;
}
.score-value {
width: 60px;
font-family: monospace;
font-size: 0.95em;
color: var(--body-text-color-subdued);
background: var(--background-fill-secondary);
padding: 4px 8px;
border-radius: 6px;
text-align: center;
border: 1px solid var(--border-color-primary);
}
.score-bar-wrapper {
flex-grow: 1;
display: flex;
align-items: center;
}
.score-bar-track {
width: 100%;
height: 8px;
background: var(--background-fill-primary);
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--border-color-primary);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.06);
}
.score-bar-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
border-radius: 4px;
transition: width 0.5s ease;
}
.score-prediction {
margin-top: 24px;
padding: 16px;
background: var(--background-fill-secondary);
border-left: 4px solid #3b82f6;
border-radius: 0 8px 8px 0;
color: var(--body-text-color);
font-size: 0.95em;
box-shadow: 0 1px 3px rgba(0,0,0,0.02);
}
.dark .score-bar-track {
box-shadow: inset 0 1px 4px rgba(0,0,0,0.2);
}
"""
# ===================================================================
# Gradio app
# ===================================================================
def _on_case_change(case_selection: str):
"""Show upload box only when 'Upload Your Own' is selected, and clear outputs."""
show_upload = gr.update(visible=(case_selection == "Upload Your Own"))
empty = ""
return show_upload, empty, empty, empty, empty, empty
def build_app() -> gr.Blocks:
# css kwarg moved from Blocks() to launch() in Gradio 6.x
import inspect
_blocks_params = inspect.signature(gr.Blocks.__init__).parameters
_blocks_kwargs: Dict[str, Any] = {"title": "ImmunoPath - H&E to Immunotherapy Decision Support"}
if "css" in _blocks_params:
_blocks_kwargs["css"] = CUSTOM_CSS
with gr.Blocks(**_blocks_kwargs) as app:
# -- Header --
gr.HTML(
'<div class="header-banner">'
"<h1>ImmunoPath</h1>"
"<p>H&amp;E Histopathology &rarr; Immunotherapy Decision Support</p>"
'<div class="pipeline-flow">'
'<div class="pipeline-step"><div class="step-icon">🔬</div><div class="step-text">Path Foundation</div></div>'
'<div class="pipeline-arrow">&rarr;</div>'
'<div class="pipeline-step"><div class="step-icon">📊</div><div class="step-text">MedSigLIP</div></div>'
'<div class="pipeline-arrow">&rarr;</div>'
'<div class="pipeline-step"><div class="step-icon">🤖</div><div class="step-text">MedGemma 4B</div></div>'
'<div class="pipeline-arrow">&rarr;</div>'
'<div class="pipeline-step"><div class="step-icon">💊</div><div class="step-text">TxGemma 9B</div></div>'
"</div>"
"</div>"
)
gr.HTML(
'<div class="disclaimer-bar">'
"<strong>Research prototype</strong> - not for clinical use. "
"All predictions require confirmatory molecular testing. "
"Hardcoded outputs from real model predictions; "
"see Kaggle notebooks for live GPU inference."
"</div>"
)
# -- Input panel --
with gr.Group(elem_classes="input-panel"):
with gr.Row():
with gr.Column(scale=3):
case_radio = gr.Radio(
choices=CASE_CHOICES,
value=CASE_CHOICES[0],
label="Select Patient Case",
info="Pre-loaded cases from TCGA NSCLC cohort, or upload your own.",
)
upload_box = gr.File(
label="Upload H&E Patches",
file_count="multiple",
file_types=["image"],
visible=False,
)
with gr.Column(scale=1, min_width=160):
run_btn = gr.Button(
"Run Pipeline",
variant="primary",
size="lg",
)
# -- Output tabs --
with gr.Tabs():
with gr.Tab("Pipeline Status"):
status_out = gr.Markdown()
with gr.Tab("Immune Profile"):
profile_out = gr.Markdown()
with gr.Tab("MedSigLIP Scores"):
sigclip_out = gr.HTML()
with gr.Tab("Treatment Recommendation"):
treatment_out = gr.Markdown()
with gr.Tab("Drug Pharmacology"):
drug_out = gr.Markdown()
# -- Wire up --
run_btn.click(
fn=run_pipeline,
inputs=[case_radio, upload_box],
outputs=[status_out, profile_out, sigclip_out, treatment_out, drug_out],
)
# Clear outputs when user switches case
case_radio.change(
fn=_on_case_change,
inputs=[case_radio],
outputs=[upload_box, status_out, profile_out, sigclip_out, treatment_out, drug_out],
)
# -- Footer --
gr.HTML(
'<div class="footer-text">'
"Built for <strong>MedGemma Impact Challenge</strong> &middot; "
"950 TCGA NSCLC patients &middot; "
'Adapter: <code>hetanshwaghela/immunopath-medgemma-v3.1</code>'
"</div>"
)
return app
# ===================================================================
# Launch
# ===================================================================
app = build_app()
if __name__ == "__main__":
# css kwarg location differs between Gradio versions
import inspect as _inspect
_launch_params = _inspect.signature(app.launch).parameters
_launch_kwargs: Dict[str, Any] = {}
if "css" in _launch_params:
_launch_kwargs["css"] = CUSTOM_CSS
app.launch(**_launch_kwargs)