""" OTC Drug Recommender Chatbot using Meditron-7B (Hugging Face Inference API) Uses epfl-llm/meditron-7b for medical reasoning and chat via API. Integrates with otc_dataset.json for safe OTC drug recommendations. """ import streamlit as st st.set_page_config(page_title="OTC MedGuide", page_icon="💊", layout="wide", initial_sidebar_state="expanded") import pandas as pd import json import os from difflib import SequenceMatcher from typing import List, Dict, Optional from datetime import datetime import requests import re # --------------------------------------------------------------------------- # Symptom Recommendation Rules # --------------------------------------------------------------------------- def get_otc_recommendations(symptom_query: str) -> Dict: """ Rule-based OTC recommendations for common symptoms. Returns a dictionary with keys: condition, symptoms, drugs, safety, special, advice, doctor. This is a conservative rule-based mapping intended as a first-pass recommender. """ # normalize and score against category keywords to robustly detect synonyms q = re.sub(r"[^a-z0-9\s]", " ", symptom_query.lower()) query_lower = q def contains(word: str) -> bool: return re.search(r"\b" + re.escape(word) + r"\b", q) is not None category_keywords = { "headache": [ "headache", "migraine", "tension headache", "cluster headache", "sinus headache", "head pain", "menstrual pain", "toothache", "pressure in head", "head pounding" ], "fever": ["fever", "high temperature", "febrile", "temperature", "chills", "sweats", "feverish"], "cold": [ "cold", "common cold", "flu", "influenza", "runny nose", "sneezing", "nasal congestion", "stuffy", "postnasal drip", "sore throat", "pharyngitis", "body aches", "myalgia", "chills" ], "cough": ["cough", "coughing", "dry cough", "wet cough", "productive cough", "chesty cough", "whooping"], "sore_throat": ["sore throat", "throat pain", "throat ache", "throat irritation", "tonsillitis", "scratchy throat"], "allergy": [ "allergy", "allergies", "allergic", "allergic rhinitis", "hay fever", "itchy eyes", "watery eyes", "sneezing", "nasal itch", "nasal discharge", "swelling", "facial swelling", "hives", "urticaria", "allergic reaction" ], "indigestion": ["indigestion", "heartburn", "acid reflux", "gerd", "stomach burn", "acid indigestion", "upset stomach", "bloating", "belching", "early satiety", "stomach pain", "stomach ache", "stomach"], "diarrhea": ["diarrhea", "loose stools", "watery stools", "dysentery", "flux", "stools", "urgent bowel movements"], "constipation": ["constipation", "hard stools", "infrequent stools", "constipated", "straining", "incomplete evacuation"], "muscle": ["muscle pain", "myalgia", "joint pain", "sprain", "strain", "muscle ache", "arthritis", "soreness"], "motion": ["motion sickness", "travel sickness", "car sickness", "sea sickness", "air sickness", "nausea", "dizziness", "vertigo", "vomiting", "mal de debarquement", "dizzy", "vomit"], "skin": [ "skin", "rash", "itch", "itching", "hives", "hive", "urticaria", "eczema", "psoriasis", "acne", "fungal", "ringworm", "athlete foot", "dermatitis", "blister", "wheal", "contact dermatitis", "rashiness" ], "vitamins": [ "vitamin", "vitamins", "multivitamin", "supplement", "supplements", "supps", "vitamin c", "vitamin d", "vitamin b12", "folic acid", "folate", "iron", "calcium", "zinc", "magnesium", "omega", "omega 3", "fish oil", "cod liver oil", "probiotic", "probiotics", "herbal", "echinacea", "turmeric", "glucosamine", "chondroitin", "garlic supplement", "supplementation", "deficiency" ] } # score categories scores = {k: sum(1 for kw in kws if contains(kw)) for k, kws in category_keywords.items()} best_cat = max(scores.items(), key=lambda x: x[1])[0] if any(v > 0 for v in scores.values()) else None # return based on best detected category (keeps original conservative recommendations) if best_cat == "headache": return { "condition": "Headache or mild pain", "symptoms": "Head pain, muscle aches, toothache, menstrual pain", "drugs": [ {"name": "Paracetamol (Acetaminophen)", "brands": "Panadol, Uphamol, Tylenol", "use": "Pain and fever relief", "warnings": "Avoid overdose; liver damage risk", "preg_age": "Safe for most ages; consult for pregnancy", "interactions": "Avoid with alcohol", "dosage_form": "Tablet, Liquid / Oral"}, {"name": "Ibuprofen", "brands": "Advil, Nurofen", "use": "Pain and inflammation relief", "warnings": "May cause stomach upset; GI bleeding risk; avoid combining with other NSAIDs", "preg_age": "Avoid in 3rd trimester; consult doctor", "interactions": "Avoid with aspirin, blood thinners", "dosage_form": "Tablet, Capsule / Oral"}, {"name": "Aspirin", "brands": "Bayer, Generic aspirin", "use": "Pain and fever relief (less preferred for routine headache due to bleeding risk)", "warnings": "Not for children under 16; GI irritation and bleeding; avoid if has ulcer history", "preg_age": "Avoid in pregnancy; not for children", "interactions": "Avoid with blood thinners", "dosage_form": "Tablet / Oral"} ], "safety": "Avoid overdose (Especially with paracetamol); caution in gastric problems (ibuprofen/aspirin).", "special": "Paracetamol is generally safer for children and those with stomach issues when used correctly.", "advice": "Drink water and rest in a quiet environment.", "doctor": "Seek medical advice if headache is severe, new or different, persistent >3 days, occurs with fever >39°C, stiff neck, confusion, weakness, or after head injury." } if best_cat == "fever": return { "condition": "Fever", "symptoms": "Elevated body temperature, chills", "drugs": [ {"name": "Paracetamol (Acetaminophen)", "brands": "Panadol, Uphamol, Tylenol", "use": "Pain and fever relief", "warnings": "Avoid overdose; liver damage risk", "preg_age": "Safe for most ages; consult for pregnancy", "interactions": "Avoid with alcohol", "dosage_form": "Tablet, Liquid / Oral"}, {"name": "Ibuprofen", "brands": "Advil, Nurofen", "use": "Pain and inflammation relief", "warnings": "May cause stomach upset; GI bleeding risk; avoid combining with other NSAIDs", "preg_age": "Avoid in 3rd trimester; consult doctor", "interactions": "Avoid with aspirin, blood thinners", "dosage_form": "Tablet, Capsule / Oral"}, ], "safety": "Use the lowest effective dose and space doses per label. Maintain hydration.", "special": "Safe for most adults and children when dosed correctly. Alternating paracetamol and ibuprofen should only be done if a healthcare professional advises it.", "advice": "Stay hydrated and rest.", "doctor": "Seek care if fever lasts >3 days, exceeds 39°C, occurs with confusion, chest pain, severe headache, breathing difficulty, or in infants/immune-compromised individuals." } if best_cat == "cold": return { "condition": "Common cold", "symptoms": "Runny nose, sneezing, nasal congestion, sore throat", "drugs": [ {"name": "Loratadine", "brands": "Claritin", "use": "Allergy-related runny nose and sneezing (non-drowsy)", "warnings": "Generally well tolerated; mild headache possible", "preg_age": "Safe for most; consult for pregnancy", "interactions": "None significant", "dosage_form": "Tablet / Oral"}, {"name": "Cetirizine", "brands": "Zyrtec, Generic cetirizine", "use": "Allergy relief for runny nose, sneezing, itchy eyes", "warnings": "May cause drowsiness; caution with alcohol or sedatives", "preg_age": "Safe for most; consult for pregnancy", "interactions": "Avoid with alcohol and CNS depressants (additive sedation)", "dosage_form": "Tablet / Oral"}, {"name": "Pseudoephedrine", "brands": "Clarinase (with loratadine), Sudafed (availability varies)", "use": "Nasal congestion and sinus pressure relief", "warnings": "May increase blood pressure/heart rate; insomnia and anxiety possible; avoid near bedtime", "preg_age": "Consult doctor if pregnant", "interactions": "Avoid with MAO inhibitors stimulants, some antihypertensives", "dosage_form": "Tablet / Oral"}, {"name": "Combination cold products", "brands": "Panadol Cold & Flu, Clarinase (combo), DayQuil/NyQuil", "use": "Multi-symptom relief (may include paracetamol, decongestant, antihistamine, cough suppressant)", "warnings": "Check labels to avoid duplicate paracetamol; decongestants may raise BP; sedating antihistamines can impair alertness", "preg_age": "Consult doctor", "interactions": "Check all ingredients", "dosage_form": "Liquid, Capsule / Oral"} ], "safety": "Some antihistamines cause drowsiness; avoid alcohol. Decongestants can raise blood pressure and cause insomnia.", "special": "Safe for most adults; consult for children. For hypertension or heart disease, prefer saline sprays and avoid oral decongestants unless advised.", "advice": "Rest, drink fluids, use saline nasal spray.", "doctor": "See a doctor if symptoms worsen or last >10 days, or if you develop high fever, severe sinus pain, ear pain, chest pain, or shortness of breath." } if best_cat == "cough": return { "condition": "Cough", "symptoms": "Dry or productive cough", "drugs": [ {"name": "Dextromethorphan", "brands": "Robitussin DM, Vicks Formula 44, Tussidex", "use": "Dry cough suppression", "warnings": "Avoid in children under 6; misuse at high doses can cause serious side effects", "preg_age": "Consult doctor if pregnant or breastfeeding", "interactions": "Avoid with MAO inhibitors, SSRIs, or other serotonergic drugs (risk of serotonin syndrome)", "dosage_form": "Syrup, Tablet / Oral"}, {"name": "Guaifenesin", "brands": "Mucinex, Robitussin Chesty Cough, Actifed Expectorant", "use": "Productive cough (help loosen phlegm)", "warnings": "Drink plenty of water to enhance effectiveness; mild GI upset possible", "preg_age": "Generally safe; consult doctor if pregnant or breastfeeding", "interactions": "None significant", "dosage_form": "Tablet, Liquid / Oral"}, {"name": "Cough lozenges", "brands": "Halls, Strepsils, Fishermans Friend", "use": "Soothes throat and and reduces cough reflex", "warnings": "Use as directed; excessive use may cause stomach upset", "preg_age": "Safe for most ages", "interactions": "None significant", "dosage_form": "Lozenge / Oral"} ], "safety": "Avoid cough suppressants in young children unless advised. Expectorants are safe with hydration.", "special": "Dextromethorphan is for dry cough; guaifenesin is for wet/productive cough.", "advice": "Stay hydrated, use humidifier.", "doctor": "See a doctor if cough is severe, bloody, associated with chest pain, or lasts >3 weeks." } if best_cat == "sore_throat": return { "condition": "Sore throat", "symptoms": "Throat pain, difficulty swallowing", "drugs": [ {"name": "Throat lozenges", "brands": "Cepacol, Strepsils, Halls", "use": "Pain relief and soothing of throat irritation", "warnings": "Use as directed; excessive use may cause mild stomach upset", "preg_age": "Safe for most ages", "interactions": "None significant", "dosage_form": "Lozenge / Oral"}, {"name": "Benzocaine sprays", "brands": "Chloraseptic, Cepacol Spray", "use": "Local anesthetic for throat pain", "warnings": "Avoid if allergic to benzocaine; rare risk of methemoglobinemia", "preg_age": "Consult doctor if pregnant", "interactions": "None significant", "dosage_form": "Spray / Topical (throat)"}, {"name": "Paracetamol (Acetaminophen)", "brands": "Panadol, Uphamol, Tylenol", "use": "Pain and fever relief", "warnings": "Avoid overdose; liver damage risk", "preg_age": "Safe for most ages; consult for pregnancy", "interactions": "Avoid with alcohol", "dosage_form": "Tablet, Liquid / Oral"} ], "safety": "Use lozenges and sprays as directed; avoid benzocaine if allergic.", "special": "Safe for most ages; pediatric versions available.", "advice": "Gargle with warm salt water, stay hydrated.", "doctor": "See a doctor if sore throat is severe, lasts >1 week, or is accompanied by high fever or difficulty breathing." } if best_cat == "allergy": # If the query explicitly mentions hives, return a focused hives/urticaria recommendation if contains('hives') or contains('hive') or contains('urticaria'): return { "condition": "Hives (Urticaria)", "symptoms": "Raised itchy welts, may come and go, can be triggered by allergens or irritants", "drugs": [ {"name": "Cetirizine", "brands": "Zyrtec", "use": "Non-drowsy oral antihistamine for hives/allergic itching", "warnings": "May cause mild drowsiness in some", "preg_age": "Consult in pregnancy", "interactions": "Avoid alcohol", "dosage_form": "Tablet / Oral"}, {"name": "Loratadine", "brands": "Claritin", "use": "Non-drowsy oral antihistamine for itch relief", "warnings": "Generally well tolerated", "preg_age": "Consult in pregnancy", "interactions": "Few", "dosage_form": "Tablet / Oral"}, {"name": "Diphenhydramine (oral)", "brands": "Benadryl", "use": "Sedating antihistamine useful at night for severe itch", "warnings": "Causes drowsiness; avoid driving; not for long-term use", "preg_age": "Use with caution; consult provider", "interactions": "Alcohol and other sedatives", "dosage_form": "Tablet / Oral"}, {"name": "Topical calamine lotion", "brands": "Calamine", "use": "Soothes local itch and irritation", "warnings": "Avoid open broken skin", "preg_age": "Generally safe", "interactions": "None", "dosage_form": "Lotion / Topical"} ], "safety": "Seek emergency care for signs of anaphylaxis (difficulty breathing, swelling of face/lips/tongue, lightheadedness).", "special": "If hives are recurrent or persistent >6 weeks, see a clinician; consider allergy evaluation.", "advice": "Non-drowsy antihistamines are first-line; use sedating antihistamines at night if itch prevents sleep; avoid triggers if known.", "doctor": "Seek urgent care for breathing difficulty, facial swelling, or suspected anaphylaxis. See your doctor for persistent or recurrent hives." } return { "condition": "Allergies (Allergic rhinitis)", "symptoms": "Sneezing, itchy eyes, runny nose", "drugs": [ {"name": "Cetirizine", "brands": "Zyrtec, Reactine, Generic cetirizine", "use": "Allergy relief for runny nose, sneezing, itchy eyes", "warnings": "May cause drowsiness; caution with alcohol or sedatives", "preg_age": "Safe for most; consult for pregnancy", "interactions": "Avoid with alcohol and CNS depressants (additive sedation)", "dosage_form": "Tablet / Oral"}, {"name": "Loratadine", "brands": "Claritin", "use": "Allergy-related runny nose and sneezing (non-drowsy)", "warnings": "Generally well tolerated; mild headache possible", "preg_age": "Safe for most; consult for pregnancy", "interactions": "None significant", "dosage_form": "Tablet / Oral"}, {"name": "Saline nasal spray", "brands": "Sterimar, Ocean, Simply Saline", "use": "Moisturizes nasal passages; reduces dryness and congestion", "warnings": "Safe for regular use; avoid contaminating nozzle", "preg_age": "Safe for all ages", "interactions": "None", "dosage_form": "Spray / Nasal"} ], "safety": "Non-drowsy antihistamines (loratadine) preferred during the day; cetirizine may cause mild sedation.", "special": "Saline sprays are safe for all ages and pregnancy; useful adjunct to antihistamines.", "advice": "Avoid allergens, keep windows closed during pollen season, use air purifier.", "doctor": "See a doctor if symptoms are severe, chronic, or associated with asthma or breathing difficulty." } if best_cat == "indigestion": return { "condition": "Indigestion or heartburn", "symptoms": "Burning chest sensation, bloating", "drugs": [ {"name": "Antacids", "brands": "Gaviscon, Rennie, Tums", "use": "Neutralizes stomach acid for quick relief", "warnings": "May cause constipation (calcium/aluminum) or diarrhea (magnesium); avoid overuse", "preg_age": "Safe for most; consult for pregnancy", "interactions": "May affect absorption of other drugs (e.g., thyroid meds, antibiotics)", "dosage_form": "Tablet, Liquid / Oral"}, {"name": "Famotidine", "brands": "Pepcid, Generic famotidine", "use": "Reduces stomach acid production (H2 blocker)", "warnings": "Long-term use needs monitoring; adjust dose in kidney impairment", "preg_age": "Consult doctor if pregnant or breastfeeding", "interactions": "Few significabt interactions", "dosage_form": "Tablet / Oral"} ], "safety": "Take as directed; avoid long-term use without advice.", "special": "Safe for most; consult during pregnancy; Antacids provide quick relief; famotidine provides longer-lasting acid suppression.", "advice": "Eat smaller meals, avoid spicy/fatty foods, elevate head when sleeping.", "doctor": "See a doctor if symptoms are frequent or severe, or associated with weight loss, vomiting blood, or black stools." } # Stomach pain / abdominal ache if best_cat == "stomach": return { "condition": "Stomach pain or abdominal ache", "symptoms": "Stomach ache, abdominal cramp, belly pain, indigestion", "drugs": [ {"name": "Antacids", "brands": "Gaviscon, Rennie, Tums", "use": "Quick relief for acid-related stomach pain/heartburn", "warnings": "Avoid overuse; may affect absorption of other meds", "preg_age": "Consult in pregnancy", "interactions": "May affect absorption of other drugs", "dosage_form": "Tablet, Liquid / Oral"}, {"name": "Simethicone", "brands": "Gas-X, Mylicon", "use": "Relieves trapped gas and bloating", "warnings": "Generally well tolerated", "preg_age": "Consult in pregnancy", "interactions": "None significant", "dosage_form": "Tablet, Liquid / Oral"}, {"name": "Paracetamol (Acetaminophen)", "brands": "Panadol, Tylenol", "use": "Pain relief for mild abdominal discomfort", "warnings": "Avoid overdose; liver risk", "preg_age": "Use as directed; consult in pregnancy", "interactions": "Alcohol", "dosage_form": "Tablet, Liquid / Oral"} ], "safety": "Seek urgent care for severe, sudden, or worsening abdominal pain, fever, bloody stools, or persistent vomiting.", "special": "If pain is localized, severe, or associated with fever, seek emergency care.", "advice": "Rest, use heat, try bland foods; stay hydrated. If gas-related, consider simethicone and gentle walking.", "doctor": "See a doctor for severe, persistent, or unexplained abdominal pain, or if you have signs of internal bleeding or high fever." } # Diarrhea elif any(word in query_lower for word in ["diarrhea", "loose stools", "watery stools", "frequent bowel movements", "dysentery", "flux"]): return { "condition": "Diarrhea", "symptoms": "Frequent loose stools", "drugs": [ {"name": "Oral Rehydration Salts (ORS)", "brands": "Pedialyte, Hydralyte, ORS packets", "use": "Fluid and electrolyte replacement; prevents dehydration", "warnings": "Essential for hydration; mix according to instructions; avoid overly concentrated solutions", "preg_age": "Safe for all ages", "interactions": "None", "dosage_form": "Powder, Liquid / Oral"}, {"name": "Loperamide", "brands": "Imodium, Diacure", "use": "Reduces bowel movements; provides symptomatic relief", "warnings": "Avoid if diarrhea is bloody, associated with high fever, or suspected bacterial infection", "preg_age": "Consult doctor if pregnant or breastfeeding", "interactions": "Few significant; caution with other CNS depressants", "dosage_form": "Tablet, Capsule / Oral"} ], "safety": "ORS is first-line to prevent dehydration. Loperamide is for adults with non-infectious diarrhea only.", "special": "Children should primarily use ORS; avoid loperamide unless specifically advised.", "advice": "Stay hydrated, eat bland foods, avoid dairy and greasy food", "doctor": "Seek medical care if diarrhea lasts >2 days, is bloody, accompanied by fever, or causes severe dehydration." } # Constipation elif any(word in query_lower for word in ["constipation", "hard stools", "infrequent stools", "difficulty passing stools", "constipated", "bowel movement difficulty"]): return { "condition": "Constipation", "symptoms": "Hard or infrequent stools", "drugs": [ {"name": "Psyllium", "brands": "Metamucil, Fybogel", "use": "Bulk-forming fiber supplement; softens stool and increases frequency", "warnings": "Drink plenty of water to avoid obstruction; may cause bloating", "preg_age": "Safe for most; consult for pregnancy", "interactions": "May affect absorption of oral medications (space dosing by 2 hours)", "dosage_form": "Powder / Oral"}, {"name": "Polyethylene glycol (PEG 3350)", "brands": "MiraLAX, Forlax", "use": "Osmotic laxative; draws water into stool", "warnings": "Increase fluid intake; prolonged use may cause electrolyte imbalance", "preg_age": "Generally safe; consult for pregnancy", "interactions": "Few significant interactions", "dosage_form": "Powder / Oral"} ], "safety": "Increase fiber and water intake; avoid chronic laxative use without medical advice.", "special": "Safe for most adults; pediatric dosing requires medical supervision; consult for chronic issues.", "advice": "Exercise regularly, eat high-fiber foods (fruits, vegetables, whole grains), maintain hydration.", "doctor": "See a doctor if constipation is chronic, associated with severe pain, blood in stool, or unexplained weight loss." } # Muscle & Joint Pain elif any(word in query_lower for word in ["muscle pain", "joint pain", "sprain", "strain", "muscle ache", "joint ache", "arthritis pain", "muscle sprain"]): return { "condition": "Muscle or joint pain", "symptoms": "Sprains, strains, aches", "drugs": [ {"name": "Ibuprofen", "brands": "Advil, Nurofen, Brufen", "use": "Pain and inflammation relief", "warnings": "May cause stomach upset; GI bleeding risk; avoid combining with other NSAIDs", "preg_age": "Avoid in 3rd trimester; consult doctor", "interactions": "Avoid with aspirin, blood thinners", "dosage_form": "Tablet, Capsule / Oral"}, {"name": "Topical diclofenac", "brands": "Voltaren Gel, Flector", "use": "Local pain relief for joints and muscles", "warnings": "For external use only; avoid broken skin; wash hands after application", "preg_age": "Consult doctor if pregnant", "interactions": "Minimal systemic absorption; fewer interactions than oral NSAIDs", "dosage_form": "Gel / Topical"} ], "safety": "Oral NSAIDs for systemic pain; topical NSAIDs for localized pain with fewer systemic risks.", "special": "Safe for adults; consult for children; Topical diclofenac is safer for those with stomach issues compared to oral NSAIDs.", "advice": "Rest, ice, compression, elevation (RICE); gentle stretching once acute pain subsides.", "doctor": "See a doctor for severe pain, swelling, inability to bear weight, or suspected fracture." } # Motion Sickness elif any(word in query_lower for word in ["motion sickness", "nausea", "dizziness", "vertigo", "travel sickness", "car sickness", "vomiting", "paleness", "sea sickness"]): return { "condition": "Motion sickness", "symptoms": "Nausea, dizziness", "drugs": [ {"name": "Meclizine", "brands": "Bonine, Bonamine", "use": "Prevents nausea and dizziness", "warnings": "May cause drowsiness; avoid driving or operating machinery", "preg_age": "Consult doctor if pregnant", "interactions": "Avoid with alcohol", "dosage_form": "Tablet / Oral"}, {"name": "Dimenhydrinate", "brands": "Dramamine, Travelmin", "use": "Motion sickness relief", "warnings": "May cause drowsiness, dry mouth, blurred vision", "preg_age": "Consult doctor if pregnant", "interactions": "Avoid with alcohol, sedatives or other CNS depressants", "dosage_form": "Tablet / Oral"} ], "safety": "Take 30 to 60 minutes before travel; may cause drowsiness.", "special": "Safe for most adults; pediatric dosing requires medical supervision.", "advice": "Look at the horizon, avoid reading, sit in front seat, ensure good ventilation.", "doctor": "See a doctor if symptoms persist despite medication or are associated with severe vertigo." } # Eye and Ear care elif any(word in query_lower for word in ["eye", "eyes", "ear", "earache", "ear pain", "earwax", "red eye", "conjunctivitis", "dry eye", "itchy eyes"]): return { "condition": "Eye or ear care", "symptoms": "Red, dry, itchy or irritated eyes; ear discomfort or excess earwax", "drugs": [ {"name": "Lubricant eye drops (artificial tears)", "brands": "Systane, Refresh", "use": "Relieves dry, irritated eyes", "warnings": "Avoid contamination of nozzle; discard if cloudy", "preg_age": "Generally safe", "interactions": "None significant", "dosage_form": "Eye drops / Ophthalmic"}, {"name": "Antihistamine eye drops", "brands": "Zaditor (ketotifen)", "use": "Relieves itchy, allergic eyes", "warnings": "May cause transient stinging", "preg_age": "Consult in pregnancy", "interactions": "None significant", "dosage_form": "Eye drops / Ophthalmic"}, {"name": "Earwax softening drops (carbamide peroxide)", "brands": "Debrox, Murine", "use": "Helps soften and remove earwax", "warnings": "Do not use if eardrum perforation suspected or ear pain with discharge", "preg_age": "Consult if pregnant", "interactions": "None", "dosage_form": "Ear drops / Otic"} ], "safety": "Do not insert objects into the ear; if severe pain, fever, discharge, or hearing loss, seek medical care.", "special": "For suspected infection (redness, discharge, severe pain), see a clinician rather than self-treating.", "advice": "Use lubricant drops for dryness; use earwax softeners per label; avoid water exposure if ear problems persist.", "doctor": "See a healthcare professional for eye pain, vision changes, severe redness, discharge, or persistent ear pain/hearing loss." } # Skin Problems elif any(word in query_lower for word in ["skin", "rash", "itch", "itching", "itchy", "eczema", "psoriasis", "acne", "fungal", "ringworm", "athlete foot", "dry skin", "irritation", "dermatitis", "rashes"]): return { "condition": "Skin problems", "symptoms": "Rash, itching, irritation, eczema, acne, fungal infections", "drugs": [ {"name": "Hydrocortisone cream", "brands": "Cortizone-10, Lanacort, Dermasone", "use": "Itch and inflammation relief", "warnings": "For external use only; avoid prolonged use; do not apply to infected skin without medical advice", "preg_age": "Safe for most ages; consult for pregnancy", "interactions": "None significant", "dosage_form": "Cream / Topical"}, {"name": "Clotrimazole", "brands": "Canesten, Lotrimin, Mycelex", "use": "Fungal infection treatment (athletes foot, ringworm, yeast infections)", "warnings": "For external use only; avoid eyes and mucous membrane", "preg_age": "Consult doctor if pregnant", "interactions": "None significant", "dosage_form": "Cream / Topical"}, {"name": "Benzoyl peroxide", "brands": "Clearasil, Oxy, Brevoxyl", "use": "Acne treatment", "warnings": "May cause dryness or irritation; start low strength", "preg_age": "Consult doctor if pregnant", "interactions": "Avoid with vitamin A", "dosage_form": "Gel, Cream / Topical"}, {"name": "Salicylic acid", "brands": "Stridex, Clearasil", "use": "Acne and wart removal", "warnings": "May cause dryness; avoid sensitive skin", "preg_age": "Consult doctor if pregnant", "interactions": "None significant", "dosage_form": "Pad, Gel / Topical"}, {"name": "Antifungal powder", "brands": "Zeasorb, Tinactin, Daktarin Powder", "use": "Fungal infections (athlete's foot, jock itch)", "warnings": "For external use; avoid broken skin", "preg_age": "Consult doctor if pregnant", "interactions": "None significant", "dosage_form": "Powder / Topical"} ], "safety": "Apply topicals only to affected areas; avoid eyes and broken skin unless directed.", "special": "Most topicals are safe for adults and children; patch test for sensitivity.", "advice": "Keep skin clean and dry; avoid scratching; use fragrance-free moisturizers.", "doctor": "See a doctor if rash spreads, worsens, or doesn't improve in 1-2 weeks. Seek immediate care for severe allergic reactions." } # Vitamins & Supplements elif any(word in query_lower for word in ["vitamin", "vitamins", "multivitamin", "supplement", "supplements", "supps", "vitamin c", "vitamin d", "vitamin b12", "folic acid", "iron", "calcium", "zinc", "magnesium", "omega", "omega-3", "fish oil", "probiotic", "probiotics", "herbal", "herbs", "immune", "deficiency"]): return { "condition": "Vitamins & dietary supplements", "symptoms": "Nutritional support, immune support, deficiency replacement", "drugs": [ {"name": "Multivitamin", "brands": "Various multivitamin brands", "use": "General nutritional insurance", "warnings": "Not a replacement for a balanced diet; check for excess of fat-soluble vitamins", "preg_age": "Choose pregnancy-specific prenatal vitamins when pregnant", "interactions": "May interact with levothyroxine and some antibiotics; separate dosing", "dosage_form": "Tablet, Capsule"}, {"name": "Vitamin D (cholecalciferol)", "brands": "Various", "use": "Bone health, deficiency replacement", "warnings": "High doses can cause hypercalcemia", "preg_age": "Safe per dosing guidance; consult for pregnancy", "interactions": "Calcium supplements, certain diuretics", "dosage_form": "Tablet, Liquid"}, {"name": "Vitamin C (ascorbic acid)", "brands": "Various", "use": "Immune support, antioxidant", "warnings": "High doses may cause GI upset and kidney stones in predisposed", "preg_age": "Generally safe", "interactions": "May affect certain lab tests", "dosage_form": "Tablet, Chewable"}, {"name": "Zinc", "brands": "Various", "use": "Short-course immune support for colds", "warnings": "Avoid long-term high doses; may cause nausea", "preg_age": "Consult in pregnancy", "interactions": "May interfere with copper absorption", "dosage_form": "Tablet"}, {"name": "Iron (ferrous sulfate)", "brands": "Various", "use": "Iron deficiency replacement", "warnings": "GI upset; overdose risk in children; take with vitamin C to enhance absorption", "preg_age": "Use in documented deficiency or per provider", "interactions": "Avoid with antacids, calcium", "dosage_form": "Tablet"}, {"name": "Probiotic supplements", "brands": "Various", "use": "Support gut health after antibiotics or for some diarrhea types", "warnings": "Use with caution in immunocompromised patients", "preg_age": "Consult for pregnancy", "interactions": "Generally low", "dosage_form": "Capsule, Sachet"}, {"name": "Omega-3 / Fish oil", "brands": "Various", "use": "Cardiovascular and triglyceride support", "warnings": "High doses may increase bleeding risk", "preg_age": "Usually safe; choose purified products", "interactions": "Anticoagulants", "dosage_form": "Capsule"} ], "safety": "Supplements are not tightly regulated; choose reputable brands and avoid megadoses unless directed by a clinician.", "special": "Check blood levels and clinical indications before starting replacement doses (especially iron and vitamin D).", "advice": "Discuss with your healthcare provider before starting supplements, particularly if on medications or with chronic conditions.", "doctor": "Seek medical advice for suspected deficiency, unusual symptoms after starting supplements, or before starting high-dose therapy." } # Default return { "condition": "General symptom", "symptoms": "Various mild symptoms", "drugs": [{"name": "Paracetamol (Acetaminophen)", "brands": "Panadol", "use": "Pain and fever relief"}], "safety": "Use as directed on label.", "special": "Consult for specific conditions.", "advice": "Rest and stay hydrated.", "doctor": "See a doctor if symptoms worsen or persist." } def meditron_reasoning(symptom_query: str, token: Optional[str]) -> Dict: # ----------------------------------------------------------------------- # Model Reasoning (Hugging Face Inference API) # ----------------------------------------------------------------------- """ Use Meditron to determine appropriate drug classes/types for a symptom before dataset lookup. Returns a dict: {"classes": [...], "exclude": [...], "explanation": "..."}. If no token is provided, returns empty classes and an explanation. """ system_prompt = ( "You are a medical expert. Given a user symptom or question, list the appropriate drug classes or generic names that are medically recommended as first-line OTC options. " "Explicitly list any drug types or forms that should be excluded (e.g., topical, eye drops, etc.) for this symptom. " "Respond in JSON with keys: classes (list of generic names or classes), exclude (list of forms/types to avoid), and explanation (short rationale). " "Do NOT recommend prescription drugs." ) prompt = f"System: {system_prompt}\nUser: {symptom_query}\nAssistant:" if not token: return {"classes": [], "exclude": [], "explanation": "No token provided."} headers = {"Authorization": f"Bearer {token}"} HF_INFERENCE_URL = "https://api-inference.huggingface.co/models/epfl-llm/meditron-7b" payload = { "inputs": prompt, "parameters": { "max_new_tokens": 256, "temperature": 0.3, "top_p": 0.95, }, "options": {"wait_for_model": True}, } try: resp = requests.post(HF_INFERENCE_URL, headers=headers, json=payload, timeout=60) if not resp.ok: return {"classes": [], "exclude": [], "explanation": "Model error."} data = resp.json() # Try to extract JSON from model output import re, json as pyjson text = "" if isinstance(data, list) and data: d0 = data[0] if isinstance(d0, dict) and "generated_text" in d0: text = d0["generated_text"] elif isinstance(data, dict) and "generated_text" in data: text = data["generated_text"] # Extract JSON block match = re.search(r'\{.*\}', text, re.DOTALL) if match: return pyjson.loads(match.group(0)) # Fallback: try to parse whole text return pyjson.loads(text) except Exception as e: return {"classes": [], "exclude": [], "explanation": f"Parse error: {e}"} MODEL_ID = "epfl-llm/meditron-7b" OTC_DATASET_PATH = "otc_dataset.json" MAX_NEW_TOKENS = 512 TEMPERATURE = 0.6 TOP_P = 0.95 @st.cache_resource(show_spinner="Loading Meditron model...") def load_meditron_model(): """ Local model loader. Import `transformers` and `torch` only when the model is requested to avoid import-time failures (for example, DLL issues on some Windows environments). Returns a `(tokenizer, model)` pair or `(None, None)` on failure. """ try: # Import transformers and torch here to avoid heavy imports at module import time from transformers import AutoTokenizer, AutoModelForCausalLM import torch tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) model = AutoModelForCausalLM.from_pretrained( MODEL_ID, torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32, device_map="auto" ) return tokenizer, model except Exception as e: st.error(f"Failed to load model: {e}") return None, None SAFETY_DISCLAIMER = """ ⚠️ **Important Medical Disclaimer:** This chatbot provides **general informational guidance only** and does **not constitute medical advice**. - It is not a substitute for professional medical diagnosis, treatment, or consultation - Do not rely on this chatbot for medical emergencies - Seek immediate medical attention for severe, persistent, or worsening symptoms - Consult a qualified healthcare professional before using any medication, if pregnant, breastfeeding, elderly, have chronic conditions, or are taking other medicines - Always read and follow the medication label and package instructions """ # Map to 8 main categories as specified CATEGORY_KEYWORDS: Dict[str, List[str]] = { "pain_relief_fever": [ "pain", "ache", "fever", "headache", "migraine", "soreness", "cramp", "muscle", "joint", "arthritis", "backache", "toothache", "menstrual pain", "period pain", "inflammation", "swelling", "redness", "tenderness" ], "cold_flu_allergy": [ "cold", "common cold", "flu", "influenza", "allergy", "allergies", "allergic", "sneeze", "sneezing", "cough", "congestion", "runny nose", "stuffy", "sinus", "hay fever", "watery eyes", "itchy eyes", "hayfever", "sore throat", "body aches", "myalgia", "chills", "postnasal drip" ], "digestive_health": [ "diarrhea", "loose stools", "watery stools", "constipation", "nausea", "vomit", "vomiting", "stomach", "gas", "bloating", "upset stomach", "indigestion", "heartburn", "acid reflux", "gerd", "laxative", "stool softener", "abdominal cramp", "cramps", "spasm", "belching", "flatulence" ], "heartburn_reflux": ["heartburn", "acid", "reflux", "gerd", "indigestion", "burning", "sour taste", "belching"], "skincare_topicals": [ "rash", "itch", "itching", "hives", "hive", "urticaria", "skin", "eczema", "psoriasis", "acne", "fungal", "ringworm", "athlete foot", "dermatitis", "contact dermatitis", "blister", "wheal", "weeping", "burn", "cut", "scrape", "rashiness" ], "eye_ear_care": [ "eye", "eyes", "dry eye", "dry eyes", "red eye", "redness", "itchy eyes", "watery eyes", "discharge", "blurry vision", "sore eyes", "foreign body sensation", "sensitivity to light", "eye pain", "tearing", "ear", "ears", "earache", "ear pain", "earwax", "ear discharge", "hearing loss", "tinnitus", "blocked ear" ], "vitamins_supplements": [ "vitamin", "vitamins", "multivitamin", "supplement", "supplements", "supps", "vitamin a", "vitamin b", "vitamin b1", "vitamin b2", "vitamin b3", "vitamin b6", "vitamin b12", "vitamin c", "vitamin d", "vitamin e", "vitamin k", "folic acid", "folate", "iron", "calcium", "zinc", "magnesium", "omega", "omega-3", "fish oil", "cod liver oil", "probiotic", "probiotics", "herbal", "herb", "echinacea", "garlic supplement", "turmeric", "curcumin", "glucosamine", "chondroitin", "immune supplement", "multimineral", "supplementation", "deficiency" ], "motion_misc": ["motion sickness", "travel sickness", "car sickness", "sea sickness", "nausea", "dizziness", "vertigo", "vomiting"], "miscellaneous": ["smoking", "sleep", "insomnia", "sleep aid", "antiemetic", "hydration", "antiseptic", "astringent", "oral anesthetic"], } # Map dataset categories to the 8 main categories CATEGORY_GROUPS: Dict[str, List[str]] = { "pain_relief_fever": [ "pain/fever", "pain/inflammation", "topical nsaid", "topical analgesic", "topical anesthetic", "analgesic", ], "cold_flu_allergy": [ "decongestant", "nasal spray", "cough suppressant", "expectorant", "antihistamine", ], "digestive_health": [ "antidiarrheal", "antiflatulent", "adsorbent", "fiber laxative", "osmotic laxative", "stimulant laxative", "stool softener", "laxative/antacid", ], "heartburn_reflux": [ "antacid", "h2 blocker", "proton pump inhibitor", "ppi", ], "skincare_topicals": [ "steroid cream", "skin protectant", "acne treatment", "antifungal", "antibiotic ointment", "skin soothing", ], "eye_ear_care": [ "eye lubricant", "eye decongestant", "eye antihistamine", "eye drops", "eye ointment", "ear drops", ], "vitamins_supplements": [ "supplement", ], "miscellaneous": [ "smoking cessation", "sleep aid", "antiemetic", "hydration", "antiseptic", "astringent", "oral anesthetic", ], } # --- Keyword lists (centralized) --- GREETINGS = ["hi", "hello", "hey", "greetings", "good morning", "good afternoon", "good evening"] VAGUE_PHRASES = ["sick", "not well", "unwell", "bad", "ill", "don't feel good", "not feeling good", "feeling sick", "not feeling well", "help", "not good", "feeling off"] BRIEF_SYMPTOM_KEYWORDS = [ "pain", "fever", "headache", "migraine", "cough", "cold", "flu", "stomach", "stomach ache", "stomach pain", "abdominal pain", "nausea", "vomit", "vomiting", "diarrhea", "constipation", "rash", "itch", "itching", "hives", "hive", "urticaria", "skin", "acne", "eczema", "psoriasis", "sore", "throat", "sore throat", "ache", "dizzy", "lightheaded", "dizziness", "vertigo", "motion sickness", "travel sickness", "car sickness", "sea sickness", "eye", "eyes", "ear", "earache", "ear pain", "earwax", "vitamin", "vitamins", "supplement", "supplements", "zinc", "iron", "calcium", "magnesium", "vitamin d", "vitamin c", "probiotic", "probiotics", "omega", "omega-3", "ibuprofen", "acetaminophen", "tylenol", "advil", "nurofen", "vs", "compare" ] DOSAGE_KEYWORDS = [ "dosage", "dose", "how much", "how many", "how often", "when to take", "how to take", "how much to take", "how often to take", "tablet", "mg", "milligram", "take", "taking" ] COMPARISON_KEYWORDS = [ "compare", "vs", "versus", "difference", "which is better", "which one", "better", "options", "choices", "safe for", "which to use", "which to choose", "recommend", "works best", "which to use" ] SYMPTOM_KEYWORDS = [ "pain", "fever", "headache", "migraine", "tension headache", "sinus headache", "cough", "cold", "flu", "stomach", "stomach ache", "abdominal pain", "nausea", "vomit", "vomiting", "diarrhea", "constipation", "rash", "itch", "itching", "eczema", "psoriasis", "acne", "fungal", "ringworm", "sore throat", "pharyngitis", "throat", "allergy", "congestion", "runny nose", "sneeze", "sinus", "hay fever", "sore", "ache", "cramp", "muscle", "myalgia", "joint", "arthritis", "backache", "heartburn", "acid", "reflux", "gerd", "indigestion", "burning", "bloating", "gas", "belching", "flatulence", "dry eye", "dry eyes", "red eye", "redness", "discharge", "blurry vision", "foreign body", "eye pain", "tearing", "ear", "earache", "earwax", "ear discharge", "hearing loss", "tinnitus", "blocked ear", "conjunctivitis", "irritation", "hives", "urticaria", "swelling", "facial swelling", "angioedema", "vitamin", "supplement", "immune", "deficiency", "calcium", "iron", "zinc", "magnesium", "probiotic", "probiotics", "smoking", "sleep", "insomnia", "motion sickness", "dizziness", "lightheaded", "vertigo" ] def normalize_category(text: str) -> str: return "".join(ch for ch in text.lower() if ch.isalnum()) SUBJECTIVE_KEYWORDS = [ "which is better", "which one", "better", "works best", "best option", "preferred", "recommend", "vs", "versus", "compare", "difference", "choices", "options", "which to use" ] @st.cache_data(show_spinner=False) def load_otc_dataset(path: str = OTC_DATASET_PATH) -> List[Dict]: if not os.path.exists(path): st.warning(f"Dataset not found: {path}") return [] try: with open(path, "r", encoding="utf-8") as f: return json.load(f) except Exception as e: st.error(f"Failed to load dataset: {e}") return [] def extract_searchable_text(drug: Dict) -> str: """ Build a searchable text blob from a dataset drug entry. Combines generic name, brand names, category, common uses, synonyms, dosage forms and routes into a single lowercased string used for similarity matching. """ fields = [ drug.get("generic_name", ""), drug.get("brand_names", []), drug.get("category", ""), drug.get("common_use", []), drug.get("synonyms", []), drug.get("dosage_form", []), drug.get("route_of_administration", []), ] text_parts = [] for f in fields: if isinstance(f, list): text_parts.extend([str(x) for x in f]) elif f: text_parts.append(str(f)) return " ".join(text_parts).lower() def tokenize(text: str) -> List[str]: return [t for t in "".join(ch if ch.isalnum() else " " for ch in text.lower()).split() if t] def similarity_score(a: str, b: str) -> float: """Simple similarity using SequenceMatcher (0.0-1.0).""" return SequenceMatcher(None, (a or "").lower(), (b or "").lower()).ratio() def retrieve_relevant_drugs(query: str, dataset: List[Dict], top_k: int = 5) -> List[Dict]: """Return top_k dataset entries most similar to the query.""" if not dataset or not query: return [] query = query.lower() scored: List[tuple] = [] for d in dataset: searchable = extract_searchable_text(d) extra_fields = [ str(d.get("pregnancy_flags", "")), ", ".join(d.get("interactions", [])), ] if any(extra_fields): searchable += " " + " ".join(extra_fields) seq = similarity_score(query, searchable) scored.append((seq, d)) scored.sort(key=lambda x: x[0], reverse=True) return [d for _, d in scored[:top_k]] def format_drugs_for_context(drugs: List[Dict]) -> str: if not drugs: return "No specific OTC drugs found in database." lines = ["**Related OTC Drugs in Database:**\n"] for i, drug in enumerate(drugs, 1): name = drug.get("generic_name", "Unknown") brand = ", ".join(drug.get("brand_names", [])) or "N/A" use = drug.get("common_use", "N/A") warnings = "; ".join(drug.get("warnings", [])) or "None listed" lines.append(f"{i}. **{name}** ({brand})") lines.append(f" - Uses: {use}") lines.append(f" - Warnings: {warnings}") return "\n".join(lines) def get_dosage_info(drug: Dict) -> str: """Get general dosage guidance for common OTC drugs""" name = drug.get("generic_name", "").lower() category = drug.get("category", "").lower() # Common OTC dosage guidelines (general, always recommend checking label) dosage_guide = { "acetaminophen": "Adults: 500-1000 mg every 4-6 hours as needed; max 4000 mg/day (some guidelines recommend ≤3000 mg/day for chronic use). Children: 10-15 mg/kg every 4-6 hours; max 5 doses in 24 hours.", "ibuprofen": "Adults: 200-400 mg every 4-6 hours as needed; max OTC dose 1200 mg/day (prescription max 3200 mg/day). Children: 5-10 mg/kg every 6-8 hours; max 40 mg/kg/day.", "naproxen": "Adults: 220 mg every 8-12 hours; max OTC dose 660 mg/day. Take with food to reduce stomach upset. Not recommended for children under 12 unless directed.", "aspirin": "Adults: 325-650 mg every 4-6 hours for pain/fever; max 4000 mg/day. Low-dose (81 mg once daily) for heart protection. Avoid in children/teens with viral illness (Reyes syndrome risk).", "diphenhydramine": "Adults: 25-50 mg every 4-6 hours; max 300 mg/day. Children: 1 mg/kg every 6 hours (max 4 doses/day); avoid in children <6 years for sleep.", "loratadine": "Adults and children ≥6 years: 10 mg once daily. Children 2-5 years: 5 mg once daily. Non-drowsy option.", "pseudoephedrine": "Adults: 30-60 mg every 4-6 hours; max 240 mg/day. Children 6-12 years: 30 mg every 4-6 hours; max 120 mg/day. Avoid in children <6 years unless directed.", "dextromethorphan": "Adults: 10-20 mg every 4 hours, or 30 mg every 6-8 hours; max 120 mg/day. Children 6-12 years: 5-10 mg every 4 hours; max 60 mg/day. Avoid in children <4 years.", "loperamide": "Adults: 4 mg initially, then 2 mg after each loose stool; max 8 mg/day (OTC) or 16 mg/day (Rx). Children: dosing varies by age — check label carefully.", "omeprazole": "Adults: 20 mg once daily before breakfast; OTC use limited to 14 days for frequent heartburn. Not recommended for children <18 years unless prescribed.", "famotidine": "Adults: 10–20 mg twice daily, or 20 mg at bedtime; max 40 mg/day OTC. Children: dosing varies by weight — check label.", } for key, info in dosage_guide.items(): if key in name: return info # Category-based general guidance if "antacid" in category: return "Adults: 1-2 tablets as needed, usually after meals or at bedtime. Follow label instructions." if "laxative" in category: return "Follow label instructions. Start with lowest dose. Increase fluid intake." if "topical" in category or "cream" in category or "ointment" in category: return "Apply thin layer to affected area 1-3 times daily as directed. Avoid broken skin unless label specifies." if "eye" in category or "drops" in category: return "Usually 1-2 drops in affected eye(s) 2-4 times daily. Follow label instructions." if "supplement" in category: return "Follow label instructions. Usually taken with food. Do not exceed recommended daily amount." return "Please check the product label for specific dosing instructions. Dosage varies by age, weight, and formulation." def render_table(drugs: List[Dict], include_explanation: bool = True) -> str: if not drugs: return "No specific OTC drugs found in database." response = "" if include_explanation: if len(drugs) > 1: response += "Here are some OTC options that may help:\n\n" else: response += "Here's an OTC option that may help:\n\n" table = ("\n| OTC Drug | Brand Names | Use / Indications | Warnings / Safety | Pregnancy / Age Considerations | Interactions | Dosage Form & Route |\n" "| --- | --- | --- | --- | --- | --- | --- |\n") for drug in drugs: name = drug.get("generic_name", "Unknown").capitalize() brand = ", ".join(drug.get("brand_names", [])) or "N/A" use = ", ".join(drug.get("common_use", [])) if isinstance(drug.get("common_use", []), list) else drug.get("common_use", "N/A") warnings = "; ".join(drug.get("warnings", [])) or "None listed" preg_age = f"Pregnancy: {drug.get('pregnancy_flags', 'N/A')}; Age: {drug.get('age_flags', 'N/A')}" interactions = ", ".join(drug.get("interactions", [])) or "None listed" dosage_form = f"{', '.join(drug.get('dosage_form', []))} / {', '.join(drug.get('route_of_administration', []))}" table += f"| {name} | {brand} | {use} | {warnings} | {preg_age} | {interactions} | {dosage_form} |\n" response += table # Append dosage disclaimer from all drugs (unique only) disclaimers = set([drug.get("dosage_disclaimer", "") for drug in drugs if drug.get("dosage_disclaimer")]) for disclaimer in disclaimers: response += f"\n\n{disclaimer}" if include_explanation and len(drugs) > 1: response += "\n\n💡 **Tip:** Consider your specific needs—some options may be gentler on the stomach, while others work better for inflammation. Always read the label and follow dosing instructions.\n" return response def generate_local_response(prompt: str) -> str: """Generate response using local Meditron model.""" tokenizer, model = load_meditron_model() if not tokenizer or not model: return "[ERROR] Model not loaded." try: inputs = tokenizer(prompt, return_tensors="pt").to(model.device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=MAX_NEW_TOKENS, temperature=TEMPERATURE, top_p=TOP_P, do_sample=True, pad_token_id=tokenizer.eos_token_id ) generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True) # Remove the prompt from the response if generated_text.startswith(prompt): response = generated_text[len(prompt):].strip() else: response = generated_text.strip() return response except Exception as e: st.error(f"[EXCEPTION] Error during local generation: {e}") return f"[ERROR] Exception during local generation: {e}" def fallback_response(question: str, drugs: List[Dict]) -> str: q = question.lower().strip() # Greetings greetings = ["hi", "hello", "hey", "greetings", "good morning", "good afternoon", "good evening"] for word in greetings: if re.search(r"\b" + re.escape(word) + r"\b", q): return "Hello! 👋 I'm here to help you find the right OTC medications for your needs. You can ask me about symptoms, medications, dosages, or comparisons. What can I help you with today?\n\n**This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.**" # Vague or general sickness vague_phrases = ["sick", "not well", "unwell", "bad", "ill", "don't feel good", "not feeling good", "feeling sick", "not feeling well"] if len(q) < 8 or any(re.search(r"\b" + re.escape(phrase) + r"\b", q) for phrase in vague_phrases): return "I'm here to help! 😊 Could you tell me more about your symptoms? For example:\n- Headache or fever\n- Cold or allergy symptoms\n- Stomach issues\n- Skin problems\n\nThis helps me recommend the best OTC options for you.\n\n**This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.**" # Dosage questions dosage_keywords = ["dosage", "dose", "how much", "how many", "how often", "when to take", "how to take", "how much to take", "how often to take"] if any(kw in q for kw in dosage_keywords): # Try to find the drug mentioned for drug in drugs: all_names = [drug.get("generic_name", "").lower()] + [b.lower() for b in drug.get("brand_names", [])] + [s.lower() for s in drug.get("synonyms", [])] if any(n in q for n in all_names): dosage_info = get_dosage_info(drug) name_display = drug.get("generic_name", "Unknown").capitalize() brand_display = ", ".join(drug.get("brand_names", [])) response = f"**Dosage for {name_display}**" if brand_display: response += f" ({brand_display})" response += f":\n\n{dosage_info}\n\n" response += "⚠️ **Important:** Always read the product label for specific instructions, as formulations and strengths vary. Consult a healthcare provider for children, elderly, or if you have medical conditions.\n\n" response += "**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" return response # If no specific drug found but we have context drugs if drugs: response = "Here's dosage information for the options we discussed:\n\n" for drug in drugs[:3]: name = drug.get("generic_name", "Unknown").capitalize() dosage_info = get_dosage_info(drug) response += f"**{name}**: {dosage_info}\n\n" response += "⚠️ Always check the product label for specific dosing instructions.\n\n" response += "**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" return response else: return "I'd be happy to help with dosage information! Could you tell me which medication you're asking about?\n\n**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" # Comparison questions compare_words = ["compare", "vs", "versus", "difference", "which is better", "which one", "better", "options", "choices", "safe for"] subjective = any(k in q for k in SUBJECTIVE_KEYWORDS) if subjective and drugs: response = render_table(drugs, include_explanation=True) # Add helpful comparison notes if len(drugs) >= 2: drug_names = [d.get("generic_name", "").lower() for d in drugs] if "acetaminophen" in drug_names and "ibuprofen" in drug_names: response += "\n💡 **Quick comparison:** Acetaminophen is gentler on the stomach and good for fever. Ibuprofen helps with inflammation and may be better for muscle aches. Both are effective for headaches—choose based on your preferences and any medical conditions.\n" elif "loratadine" in drug_names or "cetirizine" in drug_names: response += "\n💡 **Quick comparison:** These are non-drowsy antihistamines, great for daytime allergy relief. Loratadine is often preferred for minimal side effects.\n" response += "\n**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" return response if any(w in q for w in compare_words) or len(drugs) > 1: if drugs: return render_table(drugs, include_explanation=True) else: return "I couldn't find specific OTC options for your question. Could you provide more details about what you're looking for?" # Side effects/warnings if "side effect" in q or "adverse" in q or "warning" in q or "safe" in q: for drug in drugs: all_names = [drug.get("generic_name", "").lower()] + [b.lower() for b in drug.get("brand_names", [])] + [s.lower() for s in drug.get("synonyms", [])] if any(n in q for n in all_names): warnings = drug.get("warnings", []) contraindications = drug.get("contraindications", []) name_display = drug.get("generic_name", "Unknown").capitalize() response = f"**Safety information for {name_display}:**\n\n" if warnings: response += "**Warnings:**\n" for w in warnings: response += f"- {w}\n" if contraindications: response += "\n**Contraindications (do not use if):**\n" for c in contraindications: response += f"- {c}\n" response += "\n**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" return response return "I'd be happy to help with safety information! Could you tell me which medication you're asking about?\n\n**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" # Default fallback if drugs: if len(drugs) > 1: return render_table(drugs, include_explanation=True) last_drugs = st.session_state.get("last_relevant_drugs_extra", []) if last_drugs and len(last_drugs) > 1: return render_table(last_drugs[:2], include_explanation=True) return render_table(drugs, include_explanation=True) else: return "I couldn't find specific OTC drugs for your question. Could you try rephrasing or providing more details about your symptoms or what you're looking for?" # --------------------------------------------------------------------------- # Intent Detection # --------------------------------------------------------------------------- def detect_intent_and_entities(question: str, dataset: List[Dict]) -> Dict: """ Returns a dict with keys: - intent: 'symptom', 'comparison', 'dosage', 'drug_lookup', 'knowledge', 'clarification', 'greeting' - drugs_mentioned: list of drugs from dataset that match - vague: bool (if input is vague/general) """ q = question.lower().strip() # Greeting for word in GREETINGS: if re.search(r"\b" + re.escape(word) + r"\b", q): return {"intent": "greeting", "drugs_mentioned": [], "vague": False} # Early drug-name detection (so short queries like 'tylenol' or 'vitamin d' are recognized) drugs_mentioned = [] for drug in dataset: all_names = [drug.get("generic_name", "").lower()] + [b.lower() for b in drug.get("brand_names", [])] + [s.lower() for s in drug.get("synonyms", [])] if any(n and re.search(r"\b" + re.escape(n) + r"\b", q) for n in all_names): drugs_mentioned.append(drug) # Vague/general illness: treat as clarification only when input is truly vague. # Recognize short single-word queries as valid if they match symptom keywords, dataset drug names, or category keywords. if any(re.search(r"\b" + re.escape(phrase) + r"\b", q) for phrase in VAGUE_PHRASES): if not (any(re.search(r"\b" + re.escape(k) + r"\b", q) for k in BRIEF_SYMPTOM_KEYWORDS) or drugs_mentioned): return {"intent": "clarification", "drugs_mentioned": [], "vague": True} # For very short inputs, use token-based checks and broaden accepted keyword sources tokens = re.findall(r"\w+", q) is_short_input = len(tokens) <= 2 and len(q) < 20 if is_short_input: # flatten global category keywords for more coverage flat_cat_keywords = [] for kws in CATEGORY_KEYWORDS.values(): flat_cat_keywords.extend(kws) matches_brief = any(re.search(r"\b" + re.escape(k) + r"\b", q) for k in BRIEF_SYMPTOM_KEYWORDS) matches_symptom = any(re.search(r"\b" + re.escape(k) + r"\b", q) for k in SYMPTOM_KEYWORDS) matches_category = any(re.search(r"\b" + re.escape(k) + r"\b", q) for k in flat_cat_keywords) if not (matches_brief or matches_symptom or matches_category or drugs_mentioned): return {"intent": "clarification", "drugs_mentioned": [], "vague": True} # Drug lookup (we already attempted an early pass above) # If specific intent keywords present, prefer those even when a drug name is mentioned if any(re.search(r"\b" + re.escape(k) + r"\b", q) for k in COMPARISON_KEYWORDS): return {"intent": "comparison", "drugs_mentioned": drugs_mentioned, "vague": False} if any(re.search(r"\b" + re.escape(kw) + r"\b", q) for kw in DOSAGE_KEYWORDS): return {"intent": "dosage", "drugs_mentioned": drugs_mentioned, "vague": False} # If the user mentioned both a drug and a symptom (e.g., 'tylenol for headache'), prefer the symptom intent if drugs_mentioned and any(re.search(r"\b" + re.escape(k) + r"\b", q) for k in SYMPTOM_KEYWORDS): return {"intent": "symptom", "drugs_mentioned": drugs_mentioned, "vague": False} if drugs_mentioned: return {"intent": "drug_lookup", "drugs_mentioned": drugs_mentioned, "vague": False} if any(re.search(r"\b" + re.escape(k) + r"\b", q) for k in SYMPTOM_KEYWORDS): return {"intent": "symptom", "drugs_mentioned": drugs_mentioned, "vague": False} # Knowledge/mechanism return {"intent": "knowledge", "drugs_mentioned": drugs_mentioned, "vague": False} # --------------------------------------------------------------------------- # Response Generation # --------------------------------------------------------------------------- def generate_response(user_question: str, dataset: List[Dict], token: Optional[str]) -> str: intent_info = detect_intent_and_entities(user_question, dataset) intent = intent_info["intent"] drugs_mentioned = intent_info["drugs_mentioned"] vague = intent_info["vague"] # Greeting if intent == "greeting": return "Hello! 👋 I'm here to help you find the right OTC medications for your needs. You can ask me about symptoms, medications, dosages, or comparisons. What can I help you with today?\n\n**This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.**" # Symptom clarification fallback if intent == "clarification": # For vague inputs like "I'm sick", avoid guessing or suggesting specific drugs. # Instead, ask a concise clarifying question so the user can provide # symptom details that lead to accurate recommendations. return ( "I'm sorry you're not feeling well — could you tell me the main symptom you're experiencing?\n" "For example: headache, fever, cough, sore throat, rash, stomach upset, or skin irritation." "\n\nThis helps me recommend appropriate OTC options.\n\n**This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.**" ) # Symptom-based recommendations if intent == "symptom": rec = get_otc_recommendations(user_question) response = f"**For {rec['condition']}:**\n\n" response += f"{rec.get('symptoms', '')}\n\n" # Format drugs for table # For vitamins/supplements, try to match real dataset entries first if "vitamin" in rec.get('condition', '').lower() or "supplement" in rec.get('condition', '').lower(): matched = retrieve_relevant_drugs(user_question, dataset, top_k=5) if matched: response += "Here are matching supplements from the OTC database:\n\n" response += render_table(matched, include_explanation=True) else: drugs_for_table = [] for drug in rec.get('drugs', []): drugs_for_table.append({ "generic_name": drug['name'], "brand_names": drug['brands'].split(", "), "common_use": [drug['use']], "warnings": [drug['warnings']], "pregnancy_flags": drug['preg_age'], "age_flags": "Adult & Child", "interactions": drug['interactions'].split(", "), "dosage_form": drug['dosage_form'].split(" / ")[0].split(", "), "route_of_administration": [drug['dosage_form'].split(" / ")[1]] }) response += render_table(drugs_for_table, include_explanation=True) else: drugs_for_table = [] for drug in rec.get('drugs', []): drugs_for_table.append({ "generic_name": drug['name'], "brand_names": drug['brands'].split(", "), "common_use": [drug['use']], "warnings": [drug['warnings']], "pregnancy_flags": drug['preg_age'], "age_flags": "Adult & Child", "interactions": drug['interactions'].split(", "), "dosage_form": drug['dosage_form'].split(" / ")[0].split(", "), "route_of_administration": [drug['dosage_form'].split(" / ")[1]] }) response += render_table(drugs_for_table, include_explanation=True) response += f"\n⚠️ {rec.get('safety', '')}\n" if rec.get('special'): response += f"\n**Special considerations:** {rec['special']}\n" if rec.get('advice'): response += f"\n**Home care advice:** {rec['advice']}\n" response += f"\n**When to see a doctor:** {rec.get('doctor', '')}\n\n" response += "**This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.**" # If user asked for keypoints/summary, prepend a short summary if any(k in user_question.lower() for k in ["keypoints", "key points", "summary", "summarize"]): kp = [] for d in rec.get('drugs', []): kp.append(f"{d.get('name')}: {d.get('use')}") keypoints_text = "**Key points:**\n- " + "\n- ".join(kp) + "\n\n" return keypoints_text + response return response # Direct OTC drug lookup (strict: skip symptom logic, show only that drug) if intent == "drug_lookup" and drugs_mentioned: # Only show the matched drug(s), do not add unrelated drugs return render_table(drugs_mentioned, include_explanation=True) # Dosage questions if intent == "dosage": # Try to find the drug mentioned for drug in dataset: all_names = [drug.get("generic_name", "").lower()] + [b.lower() for b in drug.get("brand_names", [])] + [s.lower() for s in drug.get("synonyms", [])] if any(n and n in user_question.lower() for n in all_names): dosage_info = get_dosage_info(drug) name_display = drug.get("generic_name", "Unknown").capitalize() brand_display = ", ".join(drug.get("brand_names", [])) response = f"**Dosage for {name_display}**" if brand_display: response += f" ({brand_display})" response += f":\n\n{dosage_info}\n\n" response += "⚠️ **Important:** Always read the product label for specific instructions, as formulations and strengths vary. Consult a healthcare provider for children, elderly, or if you have medical conditions.\n\n" response += "**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" return response # If no specific drug found, use model knowledge for general OTC dosage guidance relevant_drugs = retrieve_relevant_drugs(user_question, dataset, top_k=3) if relevant_drugs: response = "Here's dosage information for the options we discussed:\n\n" for drug in relevant_drugs: name = drug.get("generic_name", "Unknown").capitalize() dosage_info = get_dosage_info(drug) response += f"**{name}**: {dosage_info}\n\n" response += "⚠️ Always check the product label for specific dosing instructions.\n\n" response += "**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" return response # If still nothing, provide a safe, generic fallback return "Most OTC medications have specific dosing instructions on the package. Please let me know the name of the medication, and I can provide more detailed dosage guidance.\n\n**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" # Comparison questions if intent == "comparison": # If the user mentioned specific drugs, use them for the comparison if drugs_mentioned and len(drugs_mentioned) >= 2: relevant_drugs = drugs_mentioned else: relevant_drugs = retrieve_relevant_drugs(user_question, dataset, top_k=5) if relevant_drugs and len(relevant_drugs) >= 2: response = render_table(relevant_drugs, include_explanation=True) # Add helpful comparison notes drug_names = [d.get("generic_name", "").lower() for d in relevant_drugs] if "acetaminophen" in drug_names and "ibuprofen" in drug_names: response += "\n💡 **Quick comparison:** Acetaminophen is gentler on the stomach and good for fever. Ibuprofen helps with inflammation and may be better for muscle aches. Both are effective for headaches—choose based on your preferences and any medical conditions.\n" elif "loratadine" in drug_names or "cetirizine" in drug_names: response += "\n💡 **Quick comparison:** These are non-drowsy antihistamines, great for daytime allergy relief. Loratadine is often preferred for minimal side effects.\n" else: response += "\n💡 **Comparison:** Each option has unique benefits and risks. Consider your symptoms, medical history, and preferences.\n" response += "\n**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" return response if relevant_drugs and len(relevant_drugs) == 1: response = render_table(relevant_drugs, include_explanation=True) response += "\n💡 Only one relevant OTC drug was found for your comparison. Please specify the other drug or option you want to compare for a more detailed answer.\n" response += "\n**This is general information, not medical advice. Consult a healthcare professional if unsure or if symptoms worsen or persist.**" return response # No good matches return "I'm here to help! 😊 Could you tell me more about your symptoms or list the specific drugs you want to compare?\n\nThis helps me recommend the best OTC options for you.\n\n**This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.**" # Knowledge/mechanism if intent == "knowledge": system_prompt = ( "You are Meditron, a friendly, expert medical assistant. Answer the user's question using your own medical knowledge in detail, clearly, and understandably. " "Do NOT use or reference any external dataset. Provide a comprehensive explanation covering all relevant aspects. " "Do NOT use a table. Be thorough and educational in your response. " "Always end with: 'This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.'" ) prompt = f"System: {system_prompt}\nUser: {user_question}\nAssistant:" answer = generate_local_response(prompt) if answer and len(answer) > 10 and not answer.startswith("[ERROR]"): if "this is general information" not in answer.lower(): answer += "\n\n**This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.**" return answer.strip() return "I'd be happy to help with your medical question! However, I'm having trouble generating a response right now. Could you rephrase your question or provide more details?\n\n**This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.**" # Fallback return "I'm not sure how to help with that. Could you provide more details or clarify your question?\n\n**This is general information, not medical advice. Consult a healthcare professional if symptoms worsen or persist.**" def detect_question_type(question: str) -> str: """Compatibility wrapper: return intent string for older tests.""" try: info = detect_intent_and_entities(question, load_otc_dataset() or []) return info.get("intent", "knowledge") except Exception: return "knowledge" def meditron_reasoning_local(symptom_query: str) -> Dict: """Compatibility wrapper around `meditron_reasoning` that uses env token if available.""" token = os.environ.get("HF_TOKEN") return meditron_reasoning(symptom_query, token) # --------------------------------------------------------------------------- # Streamlit UI Helpers & Main # --------------------------------------------------------------------------- def init_session_state(): if "conversation" not in st.session_state: st.session_state.conversation = [] if "dataset" not in st.session_state: st.session_state.dataset = load_otc_dataset() if "HF_TOKEN_OVERRIDE" not in st.session_state: st.session_state.HF_TOKEN_OVERRIDE = None if "last_processed_input" not in st.session_state: st.session_state.last_processed_input = None def main(): init_session_state() with st.sidebar: st.header("⚙️ Settings & Info") st.subheader("Hugging Face Token") token_override = st.text_input("HF token (session only)", type="password", help="Temporary session token; not saved") if token_override: st.session_state["HF_TOKEN_OVERRIDE"] = token_override st.success("Session HF token set") token = st.session_state.get("HF_TOKEN_OVERRIDE") or os.environ.get("HF_TOKEN") if token: st.info("HF token available") else: st.warning("No HF token; using fallback responses") st.markdown("---") st.subheader("📌 Model Info") st.write(f"**Model:** `{MODEL_ID}` (local inference)") st.write(f"**Max Tokens:** {MAX_NEW_TOKENS}") st.markdown("---") if st.checkbox("📘 User Guide", value=False): st.write(""" **How to use this chatbot:** ⚠️ **Important:** Please **press the Send button twice** after entering your message to get a response. **You can ask about:** - **Symptoms / common illnesses** (e.g., headache, fever, sore throat, cough) - **OTC drug recommendations** - **Dosage of the medicines** - **Comparisons between OTC drugs** - **Specific OTC drugs** (from the database) **Tips:** - Keep questions short, simple and clear - Mention key symptoms if possible - This chatbot focuses on **over-the-counter (OTC) medicines only** ⚠️ This chatbot provides general information only and is **not medical advice**. """) st.markdown("---") if st.checkbox("⚠️ Medical Disclaimer", value=False): st.write(SAFETY_DISCLAIMER) st.markdown("---") if st.checkbox("📊 Chat History", value=False): st.subheader("Statistics") total_msgs = len(st.session_state.conversation) st.metric("Total Messages", total_msgs) if total_msgs > 0: st.subheader("Recent Messages") recent = st.session_state.conversation[-10:] for msg in recent: role_icon = "👤" if msg["role"] == "user" else "🤖" text_preview = msg["text"][:50] + "..." if len(msg["text"]) > 50 else msg["text"] st.write(f"{role_icon} {text_preview}") st.markdown("---") if st.checkbox("📋 View OTC Database", value=False): if st.session_state.dataset: df = pd.DataFrame([ { "Generic Name": d.get("generic_name", "N/A"), "Brand": ", ".join(d.get("brand_names", [])), "Category": d.get("category", "N/A"), "Common Use": d.get("common_use", "N/A"), } for d in st.session_state.dataset ]) st.dataframe(df, use_container_width=True) else: st.warning("Dataset not loaded") st.markdown("---") if st.button("🗑️ Clear Chat", use_container_width=True): st.session_state.conversation.clear() st.session_state["last_relevant_drugs"] = [] st.session_state["last_relevant_drugs_extra"] = [] st.session_state["last_question"] = None st.session_state["last_symptom"] = None st.success("Chat cleared!") st.title("💊 OTC MedGuide") st.write("Ask me anything about over-the-counter medications and I'll help!") st.warning(SAFETY_DISCLAIMER) st.markdown("### 💬 Conversation") for msg in st.session_state.conversation: if msg["role"] == "user": st.markdown(f"**👤 You:** {msg['text']}") else: st.markdown(f"**🤖 Bot:** {msg['text']}") st.markdown("---") st.markdown("### ✉️ Your Message") # Input at the bottom, single send with st.form("chat_form", clear_on_submit=True): user_input = st.text_input( "Ask about symptoms or OTC drugs:", placeholder="e.g., What's good for a headache?", key="user_input", ) send_button = st.form_submit_button("Send", use_container_width=True) # Process form submission outside the form block to prevent re-submission if send_button: if user_input and user_input.strip(): question = user_input.strip() # Check if we've already processed this exact question in the last message should_process = True if st.session_state.conversation: last_msg = st.session_state.conversation[-1] if last_msg.get("role") == "user" and last_msg.get("text") == question: should_process = False if should_process: st.session_state.conversation.append({"role": "user", "text": question, "timestamp": datetime.now().isoformat()}) with st.spinner("Thinking..."): # Multi-turn context: use last symptom and last drugs if follow-up import re last_symptom = st.session_state.get("last_symptom") last_drugs = st.session_state.get("last_relevant_drugs", []) # If the question is vague or a follow-up, try to reuse context vague_phrases = ["sick", "not well", "unwell", "bad", "ill", "don't feel good", "not feeling good", "feeling sick", "not feeling well"] is_vague = len(question) < 8 or any(phrase in question.lower() for phrase in vague_phrases) if is_vague and last_symptom: # Rephrase question with last symptom question_for_retrieval = last_symptom else: question_for_retrieval = question relevant_drugs = retrieve_relevant_drugs(question_for_retrieval, st.session_state.dataset, top_k=5) token = st.session_state.get("HF_TOKEN_OVERRIDE") or os.environ.get("HF_TOKEN") response = generate_response(question, st.session_state.dataset, token) # Save last relevant drugs to support follow-up questions st.session_state["last_relevant_drugs"] = relevant_drugs st.session_state["last_relevant_drugs_extra"] = relevant_drugs st.session_state["last_question"] = question # Store last symptom if detected symptom_match = re.search(r'(headache|fever|sore throat|allergy|cough|pain|cold|flu|stomach|rash|itch|skin|eye|ear|sleep|motion sickness|vomiting|diarrhea|constipation|nausea)', question, re.IGNORECASE) if symptom_match: st.session_state["last_symptom"] = symptom_match.group(1).lower() st.session_state.conversation.append({"role": "bot", "text": response, "timestamp": datetime.now().isoformat()}) if __name__ == "__main__": main()