import gradio as gr
import json
# ── helpers ──────────────────────────────────────────────────────────────────
def chip(text: str, color: str = "#94a3b8") -> str:
return (
f'{text}'
)
def badge(label: str, bg: str, color: str) -> str:
return (
f'{label}'
)
def acmg_badge(acmg_class: str) -> str:
mapping = {
"Pathogenic": ("rgba(239,68,68,0.35)", "#fca5a5"),
"Likely Pathogenic":("rgba(249,115,22,0.35)", "#fdba74"),
"VUS": ("rgba(234,179,8,0.35)", "#fde047"),
"Likely Benign": ("rgba(34,197,94,0.35)", "#86efac"),
"Benign": ("rgba(16,185,129,0.35)", "#6ee7b7"),
}
bg, color = mapping.get(acmg_class, ("rgba(100,100,120,0.35)", "#cbd5e1"))
return badge(acmg_class, bg, color)
def card_bg(acmg_class: str) -> str:
mapping = {
"Pathogenic": "rgba(239,68,68,0.12)",
"Likely Pathogenic": "rgba(249,115,22,0.12)",
"VUS": "rgba(234,179,8,0.10)",
"Likely Benign": "rgba(34,197,94,0.10)",
"Benign": "rgba(16,185,129,0.10)",
}
return mapping.get(acmg_class, "rgba(30,30,50,0.50)")
def border_color(acmg_class: str) -> str:
mapping = {
"Pathogenic": "rgba(239,68,68,0.55)",
"Likely Pathogenic": "rgba(249,115,22,0.55)",
"VUS": "rgba(234,179,8,0.55)",
"Likely Benign": "rgba(34,197,94,0.55)",
"Benign": "rgba(16,185,129,0.55)",
}
return mapping.get(acmg_class, "rgba(148,163,184,0.30)")
def render_variant_card(
patient_name: str,
gene: str,
variant_notation: str,
chromosome: str,
position: str,
zygosity: str,
acmg_class: str,
criteria_met: list,
population_af: str,
clinical_significance: str,
recommended_action: str,
pathogenicity_reasoning: str = "",
population_assessment: str = "",
) -> str:
criteria_chips = "".join(chip(c, "#a5f3fc") for c in criteria_met)
bg = card_bg(acmg_class)
border = border_color(acmg_class)
pat_section = (
f'
'
f'Patient: {patient_name}
'
if patient_name else ""
)
reasoning_section = (
f''
f'Pathogenicity Reasoning: {pathogenicity_reasoning}
'
if pathogenicity_reasoning else ""
)
pop_section = (
f''
f'Population Assessment: {population_assessment}
'
if population_assessment else ""
)
return f"""
{pat_section}
{gene}
{variant_notation}
{acmg_badge(acmg_class)}
Chromosome: {chromosome}
Position: {position}
Zygosity: {zygosity}
Population AF: {population_af}
ACMG Criteria Met
{criteria_chips}
Clinical Significance
{clinical_significance}
Recommended Action
{recommended_action}
{reasoning_section}
{pop_section}
"""
# ── pre-computed demo cases ───────────────────────────────────────────────────
DEMO_CASES = [
dict(
patient_name="",
gene="BRCA1",
variant_notation="c.5266dupC (p.Gln1756ProfsTer74)",
chromosome="chr17",
position="41,245,466",
zygosity="Heterozygous",
acmg_class="Pathogenic",
criteria_met=["PVS1", "PS3", "PM2", "PP3"],
population_af="0.0001 (gnomAD)",
clinical_significance=(
"Frameshift causing premature stop codon at position 1756. "
"Pathogenic for Hereditary Breast and Ovarian Cancer (HBOC) syndrome. "
"Loss of BRCA1 function disrupts homologous recombination DNA repair."
),
recommended_action=(
"Refer to certified genetic counselor. Discuss prophylactic risk-reduction "
"surgery options (bilateral mastectomy / salpingo-oophorectomy). "
"Cascade testing of first-degree relatives strongly recommended."
),
),
dict(
patient_name="",
gene="CFTR",
variant_notation="c.1521_1523delCTT (p.Phe508del)",
chromosome="chr7",
position="117,548,628",
zygosity="Homozygous",
acmg_class="Pathogenic",
criteria_met=["PVS1", "PS1", "PM3", "PP5"],
population_af="0.0139 (carrier frequency)",
clinical_significance=(
"Most common Cystic Fibrosis-causing variant (≈70% of CF alleles). "
"Homozygous state is consistent with classic Cystic Fibrosis. "
"Causes misfolding and premature degradation of CFTR protein."
),
recommended_action=(
"Diagnose Cystic Fibrosis. Refer to accredited CF Center. "
"Initiate evaluation for CFTR modulator therapy (e.g., Elexacaftor/Tezacaftor/Ivacaftor). "
"Baseline pulmonary function tests, sweat chloride, and pancreatic assessment."
),
),
dict(
patient_name="",
gene="ATM",
variant_notation="c.7271T>G (p.Val2424Gly)",
chromosome="chr11",
position="108,123,551",
zygosity="Heterozygous",
acmg_class="VUS",
criteria_met=["PM1", "PM2", "PP3"],
population_af="0.000089",
clinical_significance=(
"Missense variant located in the kinase domain of ATM. "
"Functional studies remain inconclusive. PM1 supported by critical domain location; "
"however, evidence is insufficient for pathogenic or benign classification at this time."
),
recommended_action=(
"Variant of Uncertain Significance — cannot be used for independent clinical decision-making. "
"Recommend periodic reclassification as new functional data and population studies emerge. "
"Consider segregation studies in affected family members."
),
),
dict(
patient_name="",
gene="MLH1",
variant_notation="c.116+5G>A",
chromosome="chr3",
position="37,034,801",
zygosity="Heterozygous",
acmg_class="Likely Benign",
criteria_met=["BP4", "BP7", "BA1 (partial)"],
population_af="0.0042",
clinical_significance=(
"Intronic splice-region variant. Multiple in silico computational tools "
"(SpliceSiteFinder, MaxEntScan, NNSPLICE) predict no significant impact on splicing. "
"Observed at appreciable frequency in the general population."
),
recommended_action=(
"Likely benign variant. No clinical action required based on this variant alone. "
"Document in medical record. Re-evaluate if new evidence emerges or in context of "
"strong family history of Lynch Syndrome."
),
),
]
def build_demo_html() -> str:
header = """
Pre-Computed Analysis · No API Key Required
Four representative genomic variants illustrating the full ACMG classification spectrum.
"""
cards = "".join(render_variant_card(**c) for c in DEMO_CASES)
return header + cards
# ── GPT-4o-mini classification ────────────────────────────────────────────────
SYSTEM_PROMPT = """You are an expert clinical genomics scientist specializing in ACMG/AMP variant
classification guidelines (2015 and 2019 ClinGen updates). Given a genomic variant and clinical
context, perform a structured classification and return ONLY valid JSON with these keys:
- acmg_class: one of "Pathogenic", "Likely Pathogenic", "VUS", "Likely Benign", "Benign"
- criteria_met: array of ACMG criteria strings (e.g. ["PVS1","PM2","PP3"])
- clinical_significance: 2-4 sentence clinical interpretation
- recommended_action: 2-4 sentence clinical recommendation
- population_assessment: 1-2 sentence assessment of population frequency significance
- pathogenicity_reasoning: 2-3 sentence detailed molecular reasoning
Be precise, evidence-based, and medically accurate."""
def classify_variant(
patient_name, gene, chromosome, position, ref_allele, alt_allele,
zygosity, population_af, indication, panel_type, api_key
):
if not api_key or not api_key.strip():
return "Please enter your OpenAI API key in the field above.
"
if not gene or not gene.strip():
return "Please enter a gene name (e.g. BRCA1, TP53, CFTR).
"
user_message = f"""Classify the following genomic variant:
Gene: {gene.strip()}
Chromosome: {chromosome}
Position: {int(position) if position else 'Unknown'}
Reference Allele: {ref_allele or 'N/A'}
Alt Allele: {alt_allele or 'N/A'}
Zygosity: {zygosity}
Population AF: {population_af:.6f}
Clinical Indication / Phenotype: {indication or 'Not specified'}
Genetic Panel: {panel_type or 'Not specified'}
Apply ACMG/AMP 2015 classification criteria. Return JSON only."""
try:
from openai import OpenAI
client = OpenAI(api_key=api_key.strip())
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.1,
response_format={"type": "json_object"},
)
raw = response.choices[0].message.content
data = json.loads(raw)
except Exception as e:
err = str(e)
return (
f""
f"Error: {err}
"
)
variant_notation = f"{ref_allele or '?'}>{alt_allele or '?'}"
return render_variant_card(
patient_name=patient_name or "",
gene=gene.strip(),
variant_notation=variant_notation,
chromosome=chromosome,
position=str(int(position)) if position else "N/A",
zygosity=zygosity,
acmg_class=data.get("acmg_class", "VUS"),
criteria_met=data.get("criteria_met", []),
population_af=f"{population_af:.6f}",
clinical_significance=data.get("clinical_significance", ""),
recommended_action=data.get("recommended_action", ""),
pathogenicity_reasoning=data.get("pathogenicity_reasoning", ""),
population_assessment=data.get("population_assessment", ""),
)
# ── How It Works content ──────────────────────────────────────────────────────
HOW_IT_WORKS_HTML = """
n8n Workflow Architecture
Step 1
Webhook Trigger
Receives variant data payload (gene, position, zygosity, clinical indication) from external LIMS or EHR system via HTTP POST.
Step 2
Database Enrichment
Queries ClinVar, gnomAD, and ClinGen APIs for prior classification, population frequency data, and functional evidence annotations.
Step 3
GPT-4o-mini ACMG Classifier
Sends enriched variant context to GPT-4o-mini with structured ACMG/AMP prompt. Returns JSON classification with criteria evidence codes and clinical narrative.
Step 4
Report Generation & Delivery
Generates structured clinical report, logs to Google Sheets for audit trail, and routes high-severity Pathogenic findings to clinical team via email/Slack alert.
ACMG/AMP Classification Criteria Reference
| Code |
Strength |
Direction |
Description |
| PVS1 |
Very Strong |
Pathogenic |
Null variant (frameshift, nonsense, splice ±1/2) in gene where LOF is disease mechanism |
| PS1–PS4 |
Strong |
Pathogenic |
Same AA change as established pathogenic; functional studies; de novo; prevalence increase in affected |
| PM1–PM6 |
Moderate |
Pathogenic |
Critical domain; absent in population; cosegregation; in trans with pathogenic; de novo (unconfirmed) |
| PP1–PP5 |
Supporting |
Pathogenic |
Cosegregation; functional evidence; reputable source (ClinVar); in trans with pathogenic; in cis pathogenic |
| BA1 |
Stand-Alone |
Benign |
Allele frequency >5% in population databases (gnomAD, 1000G, ESP) |
| BS1–BS4 |
Strong |
Benign |
Allele frequency greater than expected; benign functional studies; nonsegregation; in trans VUS |
| BP1–BP7 |
Supporting |
Benign |
Missense in gene with only truncating pathogenic; silent with no splice impact; in trans pathogenic; multiple lines benign computational |
CLINICAL DISCLAIMER
This tool is intended for research and educational purposes only. Classifications generated
by AI should not be used as the sole basis for clinical decision-making. All variant
classifications must be reviewed and confirmed by a board-certified clinical geneticist or
molecular pathologist before clinical application.
"""
# ── Gradio UI ─────────────────────────────────────────────────────────────────
CUSTOM_CSS = """
body, .gradio-container {
background: #0f0f1a !important;
font-family: 'Inter', sans-serif !important;
}
.gr-panel, .panel, .block {
background: transparent !important;
border: none !important;
}
.gr-box {
background: rgba(30,30,50,0.5) !important;
border: 1px solid rgba(148,163,184,0.2) !important;
border-radius: 10px !important;
}
label, .gr-input-label {
color: #94a3b8 !important;
font-size: 0.82rem !important;
font-weight: 600 !important;
}
input, textarea, select {
background: rgba(15,15,30,0.8) !important;
color: #e2e8f0 !important;
border: 1px solid rgba(148,163,184,0.25) !important;
border-radius: 8px !important;
}
button.primary {
background: linear-gradient(135deg, rgba(99,102,241,0.8), rgba(139,92,246,0.8)) !important;
color: #f1f5f9 !important;
border: 1px solid rgba(139,92,246,0.5) !important;
border-radius: 10px !important;
font-weight: 700 !important;
}
.tab-nav button {
color: #94a3b8 !important;
background: transparent !important;
}
.tab-nav button.selected {
color: #a5f3fc !important;
border-bottom: 2px solid #a5f3fc !important;
}
"""
HEADER_HTML = """
Genomic Variant Clinical Significance Classifier
ACMG/AMP 2015 guideline-based variant classification powered by GPT-4o-mini
"""
with gr.Blocks(theme=gr.themes.Soft(), css=CUSTOM_CSS, title="Genomic Variant Classifier") as demo:
gr.HTML(HEADER_HTML)
with gr.Tabs():
# ── TAB 1: Live Demo ─────────────────────────────────────────────────
with gr.Tab("Live Demo"):
gr.HTML(build_demo_html())
# ── TAB 2: Classify Variant ──────────────────────────────────────────
with gr.Tab("Classify Variant"):
gr.HTML(
'Enter variant details below and provide your OpenAI API key to run live ACMG classification.
'
)
with gr.Row():
with gr.Column(scale=1):
api_key = gr.Textbox(
type="password",
label="OpenAI API Key",
placeholder="sk-...",
)
patient_name = gr.Textbox(
label="Patient Name / ID (optional)",
placeholder="e.g. Patient_001",
)
gene = gr.Textbox(
label="Gene Symbol",
placeholder="e.g. BRCA1, TP53, CFTR",
)
chromosome = gr.Dropdown(
label="Chromosome",
choices=[
"chr1","chr2","chr3","chr4","chr5","chr6",
"chr7","chr8","chr9","chr10","chr11","chr12",
"chr13","chr14","chr15","chr16","chr17","chr18",
"chr19","chr20","chr21","chr22","chrX","chrY",
],
value="chr17",
)
position = gr.Number(
label="Genomic Position (GRCh38)",
value=41245466,
precision=0,
)
ref_allele = gr.Textbox(
label="Reference Allele",
placeholder="e.g. A, CTTT",
)
alt_allele = gr.Textbox(
label="Alternate Allele",
placeholder="e.g. G, -",
)
zygosity = gr.Radio(
label="Zygosity",
choices=["Heterozygous", "Homozygous"],
value="Heterozygous",
)
population_af = gr.Slider(
minimum=0,
maximum=0.05,
step=0.0001,
value=0.0001,
label="Population Allele Frequency (gnomAD)",
)
indication = gr.Textbox(
label="Clinical Indication / Phenotype",
placeholder="e.g. Hereditary breast/ovarian cancer, Lynch Syndrome",
)
panel_type = gr.Textbox(
label="Genetic Panel / Test Name",
placeholder="e.g. BRCA1/2 Panel, Hereditary Cancer 47-gene",
)
classify_btn = gr.Button("Classify Variant", variant="primary")
with gr.Column(scale=1):
result_html = gr.HTML(
value=(
''
'Classification result will appear here
'
)
)
classify_btn.click(
fn=classify_variant,
inputs=[
patient_name, gene, chromosome, position,
ref_allele, alt_allele, zygosity, population_af,
indication, panel_type, api_key,
],
outputs=result_html,
)
# ── TAB 3: How It Works ──────────────────────────────────────────────
with gr.Tab("How It Works"):
gr.HTML(HOW_IT_WORKS_HTML)
if __name__ == "__main__":
demo.launch()