Spaces:
Runtime error
Runtime error
| """ | |
| MamaCare — Maternal Health Risk Advisor (Part 3) | |
| patient's numbers ──▶ ML pipeline ──▶ risk level ──┐ | |
| │ ├──▶ LLM ──▶ warm, personalised advice | |
| └───────────────────────────────────────────┘ | |
| Run locally: python app.py → open the printed http://127.0.0.1:7860 link | |
| Needs: maternal_risk_pipeline.joblib next to this file, and GROQ_API_KEY | |
| in a .env file (locally) or in the host's Secrets (when deployed). | |
| """ | |
| import os | |
| import gradio as gr | |
| import joblib | |
| import pandas as pd | |
| import spaces | |
| from dotenv import load_dotenv | |
| from openai import OpenAI | |
| # ---- 1. LLM client (same provider setup as the Part 2 notebook) ------------- | |
| BASE_URL = "https://api.groq.com/openai/v1" | |
| MODEL = "llama-3.3-70b-versatile" | |
| KEY_NAME = "GROQ_API_KEY" | |
| load_dotenv() # reads .env locally; on Hugging Face Spaces the secret is already in the environment | |
| api_key = os.environ.get(KEY_NAME) | |
| if not api_key: | |
| raise RuntimeError( | |
| f"{KEY_NAME} not found. Locally: put it in a .env file next to app.py " | |
| f"({KEY_NAME}=gsk_...). On Hugging Face Spaces: add it under " | |
| "Settings -> Variables and secrets." | |
| ) | |
| client = OpenAI(base_url=BASE_URL, api_key=api_key) | |
| # ---- 2. Trained ML pipeline (loaded ONCE at startup) ------------------------ | |
| FEATURES = ['Age', 'SystolicBP', 'DiastolicBP', 'BS', 'BodyTemp', 'HeartRate'] | |
| predictor = joblib.load('maternal_risk_pipeline.joblib') | |
| # ---- 3. System prompt (unchanged from the Part 2 notebook) ------------------ | |
| HEALTH_ADVISOR_SYSTEM_PROMPT = """ | |
| You are MamaCare Assistant, a warm and supportive maternal health advisor for pregnant | |
| women in Nigeria. You receive a patient's health measurements together with a risk level | |
| (low risk, mid risk, or high risk) predicted by a trained machine-learning model. Your job | |
| is to explain what the numbers mean and give practical, encouraging advice. | |
| ## Your rules | |
| - Be warm, respectful and reassuring. Use simple everyday English; avoid medical jargon. | |
| - ALWAYS personalise: compare each of the patient's numbers to the normal ranges below and | |
| say clearly what looks fine, what is high, what is low, and what to increase or reduce. | |
| - You are NOT a doctor. Never diagnose, never prescribe or name medicines, never tell a | |
| patient to stop treatment. Every response must encourage antenatal care with a qualified | |
| health provider. | |
| - If the risk level is HIGH RISK, the very first line of your response must urge the | |
| patient to see a doctor or visit a health facility promptly. | |
| - Keep the whole response under about 350 words. | |
| ## Normal reference ranges (pregnancy) | |
| - Systolic blood pressure: 90-120 mmHg (140+ is high) | |
| - Diastolic blood pressure: 60-80 mmHg (90+ is high) | |
| - Blood sugar (BS): about 6-7 mmol/L; 8+ suggests high blood sugar | |
| - Body temperature: around 98.6 F (100.4+ suggests fever) | |
| - Resting heart rate: 60-100 bpm (mild increase is normal in pregnancy) | |
| ## Advice bank by risk level | |
| ### HIGH RISK | |
| - See a doctor, nurse or the nearest health facility promptly - do not wait for the next | |
| routine visit. High BP or high blood sugar in pregnancy needs professional monitoring. | |
| - Diet: reduce salt (less seasoning cubes, salty snacks, smoked/dried fish); if blood | |
| sugar is high, cut sugary drinks, sweets and reduce large portions of white rice, white | |
| bread and other refined carbohydrates. Eat more vegetables (ugu, spinach), beans, | |
| unripe plantain, whole grains, fish and eggs. Drink plenty of water. | |
| - Activity: gentle movement only (short walks); avoid strenuous exercise and heavy | |
| lifting; rest often, lying on the left side improves blood flow to the baby. | |
| - Monitor: BP and blood sugar as often as the clinic advises; keep a written record. | |
| - DANGER SIGNS - go to a hospital immediately: severe headache, blurred vision, swelling | |
| of face or hands, severe belly pain, vaginal bleeding, fits/convulsions, or the baby | |
| moving much less than usual. | |
| ### MID RISK | |
| - Book an antenatal check-up soon (within the coming days, not months) so a professional | |
| can review the numbers. | |
| - Diet: balanced meals - vegetables, beans, eggs, fish, fruits; limit salt, sugary drinks, | |
| fried foods and instant noodles. Small regular meals help steady blood sugar. | |
| - Activity: moderate exercise like a 30-minute walk most days, if the clinic agrees; | |
| regular sleep (7-8 hours); reduce stress where possible. | |
| - Monitor: check BP (and blood sugar if advised) regularly - many pharmacies and clinics | |
| can do this cheaply; watch for the same danger signs listed above. | |
| ### LOW RISK | |
| - Encourage her: the numbers look healthy - keep up the good habits. | |
| - Continue routine antenatal visits and take supplements (like folic acid or iron) exactly | |
| as the clinic advises. | |
| - Diet: keep eating balanced meals with vegetables, fruits, beans, fish and whole grains; | |
| stay well hydrated. | |
| - Activity: stay gently active (walking, light chores), rest well, and avoid alcohol and | |
| smoking completely. | |
| - Still report any danger sign immediately, even with a low-risk result. | |
| ## Response format (use these exact sections) | |
| 1. **Hello** - one warm sentence greeting (use her age respectfully, never repeat the raw table). | |
| 2. **What your numbers say** - short bullet per measurement: fine / high / low and why it matters. | |
| 3. **Food & drink** - what to eat more of, what to reduce, personalised to her numbers. | |
| 4. **Activity & rest** - exercise and rest advice for her risk level. | |
| 5. **Warning signs** - the danger signs that mean "go to hospital now". | |
| 6. **Next step** - the single most important action (for high risk: see a doctor promptly). | |
| End with one short encouraging sentence. | |
| """ | |
| # ---- 4. Risk → visual style lookup, used only by the frontend --------------- | |
| RISK_STYLES = { | |
| "low risk": {"emoji": "🟢", "label": "Low Risk", "fg": "#15803d", "bg": "#dcfce7", "border": "#86efac"}, | |
| "mid risk": {"emoji": "🟡", "label": "Mid Risk", "fg": "#b45309", "bg": "#fef3c7", "border": "#fcd34d"}, | |
| "high risk": {"emoji": "🔴", "label": "High Risk", "fg": "#b91c1c", "bg": "#fee2e2", "border": "#fca5a5"}, | |
| } | |
| def risk_badge_html(risk_level: str) -> str: | |
| style = RISK_STYLES.get(risk_level, {"emoji": "⚪", "label": risk_level.title(), "fg": "#374151", "bg": "#f3f4f6", "border": "#d1d5db"}) | |
| return f""" | |
| <div class="risk-badge" style="background:{style['bg']}; color:{style['fg']}; border-color:{style['border']};"> | |
| <span class="risk-badge-emoji">{style['emoji']}</span> | |
| <span>{style['label']}</span> | |
| </div> | |
| """ | |
| WELCOME_HTML = """ | |
| <div class="risk-badge risk-badge-placeholder"> | |
| <span class="risk-badge-emoji">🩺</span> | |
| <span>Awaiting patient data</span> | |
| </div> | |
| """ | |
| # ---- 5. Core function: ML prediction + LLM advice --------------------------- | |
| # no GPU work happens here; this only satisfies HF's free ZeroGPU hardware check | |
| def advise_patient(age, systolic_bp, diastolic_bp, bs, body_temp, heart_rate): | |
| """Predict the risk level with the ML pipeline, then ask the LLM for advice.""" | |
| values = [age, systolic_bp, diastolic_bp, bs, body_temp, heart_rate] | |
| if any(v is None for v in values): | |
| raise gr.Error("Please fill in all six measurements before submitting.") | |
| patient = pd.DataFrame([values], columns=FEATURES) | |
| risk_level = predictor.predict(patient)[0] | |
| try: | |
| proba = predictor.predict_proba(patient)[0] | |
| confidences = dict(zip(predictor.classes_, proba)) | |
| except AttributeError: | |
| confidences = {risk_level: 1.0} | |
| user_message = f"""Here is a patient. Please advise her. | |
| Age: {age} years | |
| Systolic BP: {systolic_bp} mmHg | |
| Diastolic BP: {diastolic_bp} mmHg | |
| Blood sugar (BS): {bs} mmol/L | |
| Body temperature: {body_temp} F | |
| Heart rate: {heart_rate} bpm | |
| Risk level predicted by our screening model: {risk_level.upper()}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": HEALTH_ADVISOR_SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_message}, | |
| ], | |
| temperature=0.4, | |
| ) | |
| except Exception as exc: | |
| raise gr.Error( | |
| f"The LLM call failed: {exc}. " | |
| "(401 = bad/expired API key; 429 = free-tier rate limit - wait a minute.)" | |
| ) | |
| return risk_badge_html(risk_level), confidences, response.choices[0].message.content | |
| def reset_form(): | |
| return 25, 110, 70, 6.5, 98, 72, WELCOME_HTML, None, "" | |
| # ---- 6. Look & feel ----------------------------------------------------- | |
| THEME = gr.themes.Soft( | |
| primary_hue="teal", | |
| secondary_hue="rose", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont("Nunito"), "ui-sans-serif", "system-ui", "sans-serif"], | |
| ) | |
| CUSTOM_CSS = """ | |
| .gradio-container { | |
| max-width: 1100px !important; | |
| margin: auto !important; | |
| } | |
| .app-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 18px; | |
| padding: 22px 28px; | |
| margin-bottom: 8px; | |
| border-radius: 18px; | |
| background: linear-gradient(120deg, #0f766e 0%, #14b8a6 55%, #fb7185 100%); | |
| box-shadow: 0 8px 24px rgba(15, 118, 110, 0.25); | |
| } | |
| .app-header-emoji { font-size: 2.6rem; line-height: 1; } | |
| .app-header h1 { margin: 0; color: #ffffff; font-size: 1.6rem; font-weight: 800; } | |
| .app-header p { margin: 2px 0 0; color: #f0fdfa; opacity: 0.95; font-size: 0.95rem; } | |
| .mc-panel { | |
| border-radius: 18px !important; | |
| padding: 18px !important; | |
| box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06); | |
| } | |
| .risk-badge { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| font-size: 1.15rem; | |
| font-weight: 800; | |
| padding: 16px; | |
| border: 2px solid; | |
| border-radius: 14px; | |
| margin-bottom: 12px; | |
| } | |
| .risk-badge-emoji { font-size: 1.4rem; } | |
| .risk-badge-placeholder { opacity: 0.6; border-style: dashed; } | |
| .mc-footer { | |
| text-align: center; | |
| font-size: 0.82rem; | |
| color: #6b7280; | |
| padding: 14px 8px 4px; | |
| } | |
| .dark .mc-panel { background: #1f2937 !important; } | |
| .dark .mc-footer { color: #9ca3af; } | |
| """ | |
| # ---- 7. Gradio interface (dashboard layout) ---------------------------------------------------- | |
| with gr.Blocks(title="MamaCare — Maternal Health Risk Advisor") as app: | |
| gr.HTML( | |
| """ | |
| <div class="app-header"> | |
| <div class="app-header-emoji">🤰</div> | |
| <div> | |
| <h1>MamaCare</h1> | |
| <p>ML-powered maternal health risk screening + AI-personalised advice</p> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Accordion("ℹ️ How this works", open=False): | |
| gr.Markdown( | |
| "Enter the patient's six measurements and click **Analyze**. A trained " | |
| "machine-learning model (Gradient Boosting) predicts a risk level, then an " | |
| "LLM turns the numbers and the prediction into warm, practical advice.\n\n" | |
| "*3MTT student educational tool only — it never replaces antenatal care by a " | |
| "qualified health provider.*" | |
| ) | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=5, elem_classes="mc-panel"): | |
| gr.Markdown("### 📋 Patient Vitals") | |
| with gr.Row(): | |
| age = gr.Number(label="Age (years)", value=25) | |
| heart_rate = gr.Number(label="Heart rate (bpm)", value=72) | |
| with gr.Row(): | |
| systolic_bp = gr.Number(label="Systolic BP (mmHg)", value=110) | |
| diastolic_bp = gr.Number(label="Diastolic BP (mmHg)", value=70) | |
| with gr.Row(): | |
| bs = gr.Number(label="Blood sugar (mmol/L)", value=6.5) | |
| body_temp = gr.Number(label="Body temperature (F)", value=98) | |
| with gr.Row(): | |
| reset_btn = gr.Button("↺ Reset") | |
| submit_btn = gr.Button("🩺 Analyze & Get Advice", variant="primary") | |
| gr.Examples( | |
| examples=[ | |
| [35, 140, 95, 13.0, 98, 82], | |
| [25, 110, 70, 6.5, 98, 72], | |
| ], | |
| inputs=[age, systolic_bp, diastolic_bp, bs, body_temp, heart_rate], | |
| label="Try a demo patient", | |
| ) | |
| with gr.Column(scale=6, elem_classes="mc-panel"): | |
| gr.Markdown("### 🩺 Assessment") | |
| risk_output = gr.HTML(value=WELCOME_HTML) | |
| confidence_output = gr.Label(label="Model confidence", num_top_classes=3) | |
| advice_output = gr.Markdown(label="Personalised advice", container=True) | |
| gr.HTML( | |
| '<div class="mc-footer">Built for 3MTT · Not a diagnostic device · ' | |
| 'Always seek care from a qualified health provider.</div>' | |
| ) | |
| submit_btn.click( | |
| fn=advise_patient, | |
| inputs=[age, systolic_bp, diastolic_bp, bs, body_temp, heart_rate], | |
| outputs=[risk_output, confidence_output, advice_output], | |
| ) | |
| reset_btn.click( | |
| fn=reset_form, | |
| inputs=None, | |
| outputs=[age, systolic_bp, diastolic_bp, bs, body_temp, heart_rate, | |
| risk_output, confidence_output, advice_output], | |
| ) | |
| if __name__ == "__main__": | |
| app.launch(theme=THEME, css=CUSTOM_CSS) | |