""" 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." )