"""
Medication Safety Checker
Streamlit app β Hugging Face Spaces (free CPU tier)
"""
import streamlit as st
from safety.high_alert import (get_all_drugs as ha_all, get_categories as ha_cats,
search_drugs as ha_search, get_by_category, is_high_alert,
HIGH_ALERT_DRUGS)
from safety.lasa import (get_all_pairs, search_lasa, get_critical_pairs,
get_high_risk_pairs, RISK_COLOURS as LASA_RISK_COLOURS,
TYPE_ICONS)
from safety.interactions import (check_all_interactions, check_interactions_local,
SEVERITY_COLOURS, SEVERITY_ICONS)
from safety.dose_checker import (search_drug, check_dose, get_drug_names, DOSE_DATABASE)
# ---------------------------------------------------------------------------
# Page config
# ---------------------------------------------------------------------------
st.set_page_config(
page_title="Medication Safety Checker β Student Nurses",
page_icon="π",
layout="wide",
initial_sidebar_state="expanded",
)
# ---------------------------------------------------------------------------
# CSS
# ---------------------------------------------------------------------------
st.markdown("""
""", unsafe_allow_html=True)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def risk_badge(risk: str) -> str:
cls = f"badge-{risk.lower()}"
return f'{risk}'
def severity_badge(sev: str) -> str:
icon = SEVERITY_ICONS.get(sev.lower(), "βͺ")
cls = f"badge-{sev.lower()}" if sev.lower() in ("critical","high","moderate","low","minor") else "badge-moderate"
return f'{icon} {sev.upper()}'
# ---------------------------------------------------------------------------
# Header
# ---------------------------------------------------------------------------
st.title("π Medication Safety Checker")
st.caption(
"ISMP High-Alert Medications Β· Drug Interactions Β· Safe Dose Ranges Β· "
"LASA Drug Warnings Β· Five Rights Checklist Β· For educational purposes only"
)
# ---------------------------------------------------------------------------
# Sidebar
# ---------------------------------------------------------------------------
with st.sidebar:
st.markdown("## π Medication Safety")
st.divider()
st.markdown("""
**Quick Reference**
- π΄ CRITICAL β extreme caution / specialty setting
- π HIGH β significant error potential
- π‘ MODERATE β important precautions
- π’ LOW β standard care
**ISMP High-Alert Categories**
- Anticoagulants
- Concentrated Electrolytes
- Insulin (all types)
- Opioids
- Neuromuscular Blocking Agents
- Antineoplastics
""")
st.divider()
st.markdown("""
**β οΈ Disclaimer**
This tool is for **educational purposes only**. Always consult current clinical guidelines, pharmacy resources, and your institution's protocols before administering any medication.
""")
st.divider()
st.markdown(
"Part of the [Nursing Citizen Development](https://huggingface.co/NurseCitizenDeveloper) suite"
)
# ---------------------------------------------------------------------------
# Tabs
# ---------------------------------------------------------------------------
tab_ha, tab_inter, tab_dose, tab_lasa, tab_5rights = st.tabs([
"π High-Alert Meds",
"β‘ Drug Interactions",
"π Dose Safety Check",
"π€ LASA Warnings",
"β
Five Rights",
])
# ========================= HIGH-ALERT MEDICATIONS ===========================
with tab_ha:
st.subheader("π ISMP High-Alert Medication Reference")
st.caption("Based on the Institute for Safe Medication Practices (ISMP) 2023 High-Alert Medications list.")
col_search, col_cat = st.columns([2, 2])
with col_search:
ha_query = st.text_input("π Search drug name", placeholder="e.g. heparin, insulin, morphine")
with col_cat:
categories = ["All categories"] + ha_cats()
sel_cat = st.selectbox("Filter by category", categories)
st.divider()
# Determine which drugs to show
if ha_query.strip():
drugs = ha_search(ha_query)
if not drugs:
st.warning(f"No high-alert medications found matching '{ha_query}'. Try generic or brand name.")
else:
st.success(f"Found {len(drugs)} high-alert medication(s) matching '{ha_query}'")
elif sel_cat != "All categories":
drugs = get_by_category(sel_cat)
else:
drugs = ha_all()
if not ha_query.strip() and sel_cat == "All categories":
st.info("π‘ Search for a drug above or filter by category to view high-alert details. "
f"Database contains **{len(drugs)} high-alert medications** across {len(ha_cats())} categories.")
# Summary table
for cat in ha_cats():
cat_drugs = get_by_category(cat)
names = ", ".join(d["name"] for d in cat_drugs)
st.markdown(f"**{cat}** β {names}")
else:
for drug in drugs:
rl = drug["risk_level"]
banner_class = "critical-banner" if rl == "CRITICAL" else "high-alert-banner"
icon = "π΄" if rl == "CRITICAL" else "π "
with st.expander(
f"{icon} {drug['name']} β {drug['category']}",
expanded=len(drugs) == 1
):
col1, col2 = st.columns([3, 1])
with col1:
st.markdown(f"**Generic:** {drug['generic'].title()}")
st.markdown(f"**Category:** {drug['category']} Β· **ISMP Scope:** {drug['ismp_class']}")
with col2:
st.markdown(
f'
{risk_badge(rl)}
',
unsafe_allow_html=True
)
# Warnings
st.markdown("**β οΈ Key Safety Warnings:**")
for w in drug["warnings"]:
st.markdown(
f'β οΈ {w}
',
unsafe_allow_html=True
)
col_a, col_b = st.columns(2)
with col_a:
st.markdown("**π©Ί Nursing Considerations:**")
for n in drug["nursing_considerations"]:
st.markdown(f"β’ {n}")
with col_b:
st.markdown("**π Monitoring Parameters:**")
for m in drug["monitoring"]:
st.markdown(f"β’ {m}")
if drug.get("antidote"):
st.markdown(
f'π Antidote / Reversal: {drug["antidote"]}
',
unsafe_allow_html=True
)
if drug.get("lasa_risk"):
st.markdown(
f'π€ LASA Risk: May be confused with: {", ".join(drug["lasa_risk"])}
',
unsafe_allow_html=True
)
st.markdown(f"**Routes:** {', '.join(drug['route'])}")
# ========================= DRUG INTERACTIONS ================================
with tab_inter:
st.subheader("β‘ Drug Interaction Checker")
st.caption("Enter 2β5 medications to check for clinically significant interactions. Uses NIH RxNorm API + curated clinical database.")
st.markdown("**Enter medication names (one per line):**")
drug_input = st.text_area(
"Medications",
placeholder="e.g.\nwarfarin\naspirin\namiodarone",
height=140,
label_visibility="collapsed",
)
col_check, col_clear = st.columns([3, 1])
with col_check:
check_btn = st.button("β‘ Check Interactions", type="primary", use_container_width=True)
with col_clear:
if st.button("π Clear", use_container_width=True):
st.rerun()
st.divider()
# Quick common combinations
st.markdown("**π‘ Quick check β common combinations:**")
q_cols = st.columns(4)
quick_combos = [
("Warfarin + Aspirin", ["warfarin", "aspirin"]),
("Morphine + Lorazepam", ["morphine", "lorazepam"]),
("Metformin + Contrast", ["metformin", "iodinated contrast"]),
("SSRI + Tramadol", ["sertraline", "tramadol"]),
]
for i, (label, combo) in enumerate(quick_combos):
if q_cols[i].button(label, use_container_width=True, key=f"quick_{i}"):
drug_input = "\n".join(combo)
check_btn = True
if check_btn and drug_input.strip():
drug_list = [d.strip() for d in drug_input.strip().splitlines() if d.strip()]
drug_list = list(dict.fromkeys(drug_list)) # deduplicate
if len(drug_list) < 2:
st.error("Please enter at least 2 medications.")
elif len(drug_list) > 5:
st.warning("Maximum 5 drugs per check. Only the first 5 will be checked.")
drug_list = drug_list[:5]
else:
st.markdown(f"**Checking interactions between:** {', '.join(d.title() for d in drug_list)}")
with st.spinner("Checking interactionsβ¦"):
# Local check always; API check if available
local_results = check_interactions_local(drug_list)
api_results = []
from safety.interactions import get_rxcui, check_interactions_api
rxcuis = []
for name in drug_list:
rxcui = get_rxcui(name)
if rxcui:
rxcuis.append(rxcui)
if len(rxcuis) >= 2:
api_results = check_interactions_api(rxcuis)
# Merge
all_results = []
seen = set()
for r in (api_results + local_results):
pair = frozenset([r["drug1"].lower(), r["drug2"].lower()])
if pair not in seen:
all_results.append(r)
seen.add(pair)
severity_order = {"critical": 0, "high": 1, "moderate": 2, "minor": 3, "low": 4, "unknown": 5}
all_results.sort(key=lambda x: severity_order.get(x.get("severity", "unknown").lower(), 5))
if not all_results:
st.markdown(
'β
No significant interactions found in the checked database for these medications. '
'Always verify with a pharmacist or clinical drug reference for comprehensive checking.
',
unsafe_allow_html=True
)
else:
n_critical = sum(1 for r in all_results if r.get("severity","").lower() == "critical")
n_high = sum(1 for r in all_results if r.get("severity","").lower() == "high")
n_moderate = sum(1 for r in all_results if r.get("severity","").lower() == "moderate")
summary_parts = []
if n_critical: summary_parts.append(f"π΄ {n_critical} CRITICAL")
if n_high: summary_parts.append(f"π {n_high} HIGH")
if n_moderate: summary_parts.append(f"π‘ {n_moderate} MODERATE")
if summary_parts:
st.markdown(
f'Found {len(all_results)} interaction(s): {" Β· ".join(summary_parts)}
',
unsafe_allow_html=True
)
for r in all_results:
sev = r.get("severity", "unknown")
icon = SEVERITY_ICONS.get(sev.lower(), "βͺ")
with st.expander(
f"{icon} {r['drug1'].title()} β {r['drug2'].title()} β {sev.upper()}",
expanded=(sev.lower() in ("critical", "high"))
):
st.markdown(
f''
f'{severity_badge(sev)} {r["drug1"].title()} β {r["drug2"].title()}'
f'
',
unsafe_allow_html=True
)
st.markdown(f"**Description:** {r['description']}")
if r.get("management"):
st.markdown(
f'π©Ί Management: {r["management"]}
',
unsafe_allow_html=True
)
if r.get("mechanism"):
st.markdown(f"**Mechanism:** {r['mechanism']}")
st.caption(f"Source: {r.get('source', 'Clinical Database')}")
# ========================= DOSE SAFETY CHECK ================================
with tab_dose:
st.subheader("π Safe Dose Verification")
st.caption("Verify ordered doses against standard adult therapeutic ranges. Always check weight-based dosing.")
col_drug, col_dose, col_wt = st.columns([3, 2, 2])
with col_drug:
drug_names_list = [""] + get_drug_names()
sel_drug = st.selectbox("Medication", drug_names_list,
format_func=lambda x: "Select or type a medicationβ¦" if x == "" else x)
or_type = st.text_input("Or type drug name", placeholder="e.g. digoxin")
with col_dose:
ordered_dose = st.number_input("Ordered dose", min_value=0.0, step=0.5, format="%.2f")
with col_wt:
patient_weight = st.number_input("Patient weight (kg) β optional", min_value=0.0, step=1.0, format="%.1f")
if st.button("π Check Dose", type="primary", use_container_width=True):
drug_to_check = or_type.strip() if or_type.strip() else sel_drug.strip()
if not drug_to_check:
st.error("Please select or enter a medication name.")
elif ordered_dose <= 0:
st.error("Please enter a valid ordered dose > 0.")
else:
wt = patient_weight if patient_weight > 0 else None
result = check_dose(drug_to_check, ordered_dose, wt)
if result["status"] == "not_found":
st.warning(result["message"])
else:
drug = result["drug"]
st.markdown(f"### {drug['generic'].title()} ({drug['brand']})")
st.markdown(f"**Category:** {drug['category']} Β· **Route:** {drug['route']} Β· **Unit:** {drug['unit']}")
# Range visual
pct = min(ordered_dose / drug["max_dose"], 1.0)
c1, c2, c3 = st.columns(3)
c1.metric("Minimum", f"{drug['min_dose']} {drug['unit']}")
c2.metric("Ordered", f"{ordered_dose} {drug['unit']}")
c3.metric("Maximum", f"{drug['max_dose']} {drug['unit']}")
st.progress(pct)
# Flags
for flag in result.get("flags", []):
if "β
" in flag:
st.markdown(f'{flag}
', unsafe_allow_html=True)
elif "π΄" in flag:
st.markdown(f'{flag}
', unsafe_allow_html=True)
elif "β οΈ" in flag:
st.markdown(f'{flag}
', unsafe_allow_html=True)
elif "βοΈ" in flag:
st.markdown(f'{flag}
', unsafe_allow_html=True)
else:
st.markdown(f'{flag}
', unsafe_allow_html=True)
# Frequency and nursing notes
st.markdown(f"**Typical frequency:** {drug['frequency']}")
if drug.get("therapeutic_range"):
st.markdown(f"**Therapeutic range:** {drug['therapeutic_range']}")
st.divider()
st.markdown("**π©Ί Nursing Notes:**")
for note in drug.get("nursing_notes", []):
st.markdown(f"β’ {note}")
st.divider()
st.caption(f"π Dose database covers {len(DOSE_DATABASE)} common medications. Always verify against current clinical guidelines and your institution's protocols.")
# ========================= LASA WARNINGS ====================================
with tab_lasa:
st.subheader("π€ Look-Alike / Sound-Alike (LASA) Drug Warnings")
st.caption("Based on ISMP LASA list and Joint Commission National Patient Safety Goals.")
col_ls, col_lt = st.columns([2, 2])
with col_ls:
lasa_query = st.text_input("π Search drug name", placeholder="e.g. morphine, insulin, warfarin", key="lasa_search")
with col_lt:
lasa_type_filter = st.selectbox(
"Filter by type",
["All types", "look-alike", "sound-alike", "both"],
format_func=lambda x: {"All types": "All LASA types", "look-alike": "ποΈ Look-Alike",
"sound-alike": "π Sound-Alike", "both": "ποΈπ Both"}[x],
)
lasa_risk_filter = st.radio(
"Risk level",
["All", "CRITICAL + HIGH only", "CRITICAL only"],
horizontal=True
)
st.divider()
# Get pairs
all_pairs = get_all_pairs()
if lasa_query.strip():
pairs = search_lasa(lasa_query)
elif lasa_risk_filter == "CRITICAL only":
pairs = get_critical_pairs()
elif lasa_risk_filter == "CRITICAL + HIGH only":
pairs = get_high_risk_pairs()
else:
pairs = all_pairs
if lasa_type_filter != "All types":
pairs = [p for p in pairs if p["type"] == lasa_type_filter or
(lasa_type_filter != "both" and p["type"] == "both")]
st.markdown(f"**Showing {len(pairs)} LASA pair(s)**")
if not pairs:
st.info("No LASA pairs found matching your filters.")
else:
for pair in pairs:
risk = pair["risk"]
icon = TYPE_ICONS.get(pair["type"], "π€")
risk_icon = {"CRITICAL": "π΄", "HIGH": "π ", "MODERATE": "π‘", "LOW": "π’"}.get(risk, "βͺ")
with st.expander(
f"{risk_icon} {pair['drug_a']} β {pair['drug_b']} {icon}",
expanded=(risk == "CRITICAL")
):
col_a, col_b = st.columns([3, 1])
with col_a:
st.markdown(f"**Drug A:** {pair['drug_a']}")
st.markdown(f"**Drug B:** {pair['drug_b']}")
with col_b:
st.markdown(
f''
f'{risk_badge(risk)}
{icon} {pair["type"].title()}'
f'
',
unsafe_allow_html=True
)
banner = "critical-banner" if risk == "CRITICAL" else "high-alert-banner" if risk == "HIGH" else "warn-banner"
st.markdown(
f'β οΈ Clinical Risk: {pair["clinical_note"]}
',
unsafe_allow_html=True
)
st.markdown(
f'β
Prevention Strategy: {pair["strategy"]}
',
unsafe_allow_html=True
)
if pair.get("tags"):
tags = " Β· ".join(f"`{t}`" for t in pair["tags"])
st.markdown(f"**Tags:** {tags}")
# ========================= FIVE RIGHTS CHECKLIST ===========================
with tab_5rights:
st.subheader("β
Medication Administration: 9 Rights Checklist")
st.caption("The Five Rights (+ 4 additional) of safe medication administration. Use this as a pre-administration checklist.")
if "rights_state" not in st.session_state:
st.session_state.rights_state = {}
drug_name_input = st.text_input("π Medication being prepared:", placeholder="e.g. Metoprolol 50 mg PO")
st.divider()
RIGHTS = [
{
"right": "1οΈβ£ Right PATIENT",
"description": "Verify patient identity using TWO identifiers (name + DOB, or name + MRN)",
"checks": [
"Asked the patient to state their full name",
"Verified date of birth or MRN",
"Checked wristband against MAR",
"Confirmed allergies",
],
},
{
"right": "2οΈβ£ Right DRUG",
"description": "Confirm the correct medication name β generic AND brand",
"checks": [
"Compared drug name on MAR to medication label",
"Checked for LASA look-alikes",
"Verified correct formulation (IR vs XR, concentration)",
"Checked if medication is on ISMP high-alert list",
],
},
{
"right": "3οΈβ£ Right DOSE",
"description": "Verify the dose is correct and within safe therapeutic range",
"checks": [
"Calculated dose independently",
"Verified dose against standard range",
"Performed weight-based calculation if applicable",
"Second nurse verification completed (if required by policy)",
],
},
{
"right": "4οΈβ£ Right ROUTE",
"description": "Confirm the ordered route is appropriate and safe",
"checks": [
"Verified route on MAR (oral, IV, SubQ, IM, etc.)",
"Confirmed IV access is patent and suitable for drug",
"Confirmed oral route is appropriate (no dysphagia/NPO)",
"Verified compatibility if adding to existing IV",
],
},
{
"right": "5οΈβ£ Right TIME",
"description": "Administer the medication at the correct time",
"checks": [
"Confirmed time on MAR matches current administration window",
"Verified last dose time (avoid double dosing)",
"Confirmed patient has not already received the dose",
"For time-critical medications (antibiotics, insulin): on time",
],
},
{
"right": "6οΈβ£ Right DOCUMENTATION",
"description": "Document immediately after β never before β administration",
"checks": [
"MAR signed immediately after administration",
"Noted time, route, site (for injections), patient response",
"Documented any refused or withheld doses with reason",
"PRN medications: documented reason for administration",
],
},
{
"right": "7οΈβ£ Right REASON",
"description": "Understand WHY this medication is being given",
"checks": [
"Know the indication for this medication",
"Can explain purpose to patient if asked",
"Verified indication matches the patient's current diagnosis",
"Questioned any orders that don't match clinical presentation",
],
},
{
"right": "8οΈβ£ Right RESPONSE",
"description": "Evaluate the patient's response to the medication",
"checks": [
"Assessed baseline vital signs and relevant parameters before administration",
"Documented pre-administration assessment",
"Plan for post-administration re-assessment",
"Know expected therapeutic effect timeline",
],
},
{
"right": "9οΈβ£ Right to REFUSE",
"description": "Patient has the right to refuse any medication",
"checks": [
"Patient has been informed about the medication and its purpose",
"Patient understands the consequences of refusal",
"Refusal documented and provider notified",
"Patient's autonomy and right to decide is respected",
],
},
]
all_checked = True
total_checks = 0
completed_checks = 0
for right_item in RIGHTS:
right_key = right_item["right"]
with st.expander(right_key, expanded=True):
st.caption(right_item["description"])
for ci, check_text in enumerate(right_item["checks"]):
key = f"right_{right_key}_{ci}"
current = st.session_state.rights_state.get(key, False)
checked = st.checkbox(check_text, value=current, key=key)
st.session_state.rights_state[key] = checked
total_checks += 1
if checked:
completed_checks += 1
else:
all_checked = False
st.divider()
pct_done = round((completed_checks / total_checks) * 100) if total_checks > 0 else 0
st.progress(pct_done / 100)
st.markdown(f"**{completed_checks} / {total_checks} checks completed ({pct_done}%)**")
if all_checked:
drug_label = f" β {drug_name_input}" if drug_name_input else ""
st.markdown(
f'β
All 9 Rights verified{drug_label}. '
f'Safe to administer. Document immediately after administration.
',
unsafe_allow_html=True
)
elif pct_done >= 75:
st.markdown(
'π‘ Almost complete β review remaining unchecked items before administering.
',
unsafe_allow_html=True
)
else:
st.markdown(
'β οΈ Complete all verification checks before administering medication.
',
unsafe_allow_html=True
)
col_reset, _ = st.columns([1, 3])
with col_reset:
if st.button("π Reset Checklist"):
st.session_state.rights_state = {}
st.rerun()
# ---------------------------------------------------------------------------
# Footer
# ---------------------------------------------------------------------------
st.divider()
st.caption(
"Based on ISMP High-Alert Medications list (2023), Joint Commission NPSG, and standard clinical references. "
"Drug interaction data supplemented by NIH RxNorm API. "
"For educational purposes only β always follow your institution's protocols and consult a pharmacist."
)