Spaces:
Sleeping
Sleeping
Add Gradio app with all 4 pipeline modules
Browse files
app.py
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PCOS Health AI β Interactive Demo Space
|
| 3 |
+
========================================
|
| 4 |
+
A Gradio-powered demo showcasing all components of the PCOS Health AI pipeline:
|
| 5 |
+
Tab 1: π‘οΈ Safety Guardrails β Test medical safety checks interactively
|
| 6 |
+
Tab 2: π RAG Pipeline β Query the PCOS medical knowledge base
|
| 7 |
+
Tab 3: π Dataset Generator β Preview synthetic PCOS training data
|
| 8 |
+
Tab 4: βοΈ Training Config β View LoRA fine-tuning presets
|
| 9 |
+
|
| 10 |
+
Repository: https://huggingface.co/gthor7/pcos-health-ai-pipeline
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import re
|
| 14 |
+
import json
|
| 15 |
+
import random
|
| 16 |
+
import hashlib
|
| 17 |
+
import logging
|
| 18 |
+
import numpy as np
|
| 19 |
+
from typing import Dict, List, Optional, Tuple
|
| 20 |
+
from enum import Enum
|
| 21 |
+
from dataclasses import dataclass
|
| 22 |
+
|
| 23 |
+
import gradio as gr
|
| 24 |
+
|
| 25 |
+
logging.basicConfig(level=logging.INFO)
|
| 26 |
+
logger = logging.getLogger(__name__)
|
| 27 |
+
|
| 28 |
+
random.seed(42)
|
| 29 |
+
np.random.seed(42)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
# EMBEDDED: Safety Guardrails (from safety_guardrails.py)
|
| 34 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
+
|
| 36 |
+
SYSTEM_PROMPT = """You are a compassionate, evidence-based PCOS health assistant.
|
| 37 |
+
STRICT BOUNDARIES: Never prescribe medications. Never diagnose. Always recommend consulting an endocrinologist or OB/GYN."""
|
| 38 |
+
|
| 39 |
+
PROHIBITED_MEDICATIONS = [
|
| 40 |
+
"metformin", "spironolactone", "oral contraceptive", "birth control pill",
|
| 41 |
+
"clomiphene", "clomid", "letrozole", "femara", "gonadotropin",
|
| 42 |
+
"progesterone", "provera", "medroxyprogesterone", "drospirenone",
|
| 43 |
+
"cyproterone", "diane-35", "yaz", "yasmin", "finasteride",
|
| 44 |
+
"eflornithine", "vaniqa", "pioglitazone", "rosiglitazone",
|
| 45 |
+
"inositol", "n-acetylcysteine", "nac", "glucophage",
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
EMERGENCY_SYMPTOMS = [
|
| 49 |
+
"suicidal", "self-harm", "severe pain", "heavy bleeding",
|
| 50 |
+
"chest pain", "shortness of breath", "fainting", "seizure",
|
| 51 |
+
"sudden vision loss", "severe headache", "allergic reaction",
|
| 52 |
+
"anaphylaxis", "pregnancy complication", "ectopic",
|
| 53 |
+
"ovarian torsion", "severe abdominal pain",
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
DIET_CULTURE_FLAGS = [
|
| 57 |
+
"keto cure", "carnivore cure", "detox", "cleanse", "juice fast",
|
| 58 |
+
"intermittent fasting cure", "supplement cure", "miracle",
|
| 59 |
+
"guaranteed weight loss", "rapid weight loss", "diet pill",
|
| 60 |
+
"fat burner", "metabolism booster",
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def check_safety(user_message: str) -> Tuple[str, str, List[str]]:
|
| 65 |
+
"""Check user input through safety guardrails. Returns (level, response, rules)."""
|
| 66 |
+
triggered = []
|
| 67 |
+
level = "β
SAFE"
|
| 68 |
+
response = None
|
| 69 |
+
text = user_message.lower()
|
| 70 |
+
|
| 71 |
+
# Emergency check
|
| 72 |
+
for symptom in EMERGENCY_SYMPTOMS:
|
| 73 |
+
if symptom in text:
|
| 74 |
+
triggered.append(f"π¨ EMERGENCY: '{symptom}' detected")
|
| 75 |
+
level = "π¨ EMERGENCY"
|
| 76 |
+
|
| 77 |
+
if level == "π¨ EMERGENCY":
|
| 78 |
+
response = (
|
| 79 |
+
"π¨ **The symptoms you're describing need immediate medical attention.**\n\n"
|
| 80 |
+
"Please:\n"
|
| 81 |
+
"- Contact your healthcare provider or go to the nearest ER\n"
|
| 82 |
+
"- If having thoughts of self-harm, call **988 Suicide & Crisis Lifeline**\n"
|
| 83 |
+
"- Do not wait β your safety is the top priority\n\n"
|
| 84 |
+
"I'm here for you, but this needs professional help right away."
|
| 85 |
+
)
|
| 86 |
+
return level, response, triggered
|
| 87 |
+
|
| 88 |
+
# Medication/prescription check
|
| 89 |
+
prescription_words = ["prescribe", "prescription", "what dose", "how much should i take",
|
| 90 |
+
"can you prescribe", "increase my dose", "change my medication"]
|
| 91 |
+
if any(pw in text for pw in prescription_words):
|
| 92 |
+
triggered.append("π PRESCRIPTION_REQUEST detected")
|
| 93 |
+
level = "π REFERRAL NEEDED"
|
| 94 |
+
|
| 95 |
+
dosage_pattern = re.compile(r'\b(\d+\s*(?:mg|mcg|ml|units?|tablets?|pills?|capsules?))\b', re.IGNORECASE)
|
| 96 |
+
if dosage_pattern.search(user_message):
|
| 97 |
+
for med in PROHIBITED_MEDICATIONS:
|
| 98 |
+
if med in text:
|
| 99 |
+
triggered.append(f"π MEDICATION+DOSAGE: '{med}' with dosage")
|
| 100 |
+
level = "π REFERRAL NEEDED"
|
| 101 |
+
|
| 102 |
+
if level == "π REFERRAL NEEDED":
|
| 103 |
+
response = (
|
| 104 |
+
"βοΈ **This is a question for your healthcare team.**\n\n"
|
| 105 |
+
"I'm not able to prescribe, recommend specific medications, or suggest dosage changes. "
|
| 106 |
+
"Please discuss this with your **endocrinologist or OB/GYN** who knows your complete medical history.\n\n"
|
| 107 |
+
"I *can* help you understand PCOS symptoms, track patterns, and prepare questions for your next appointment."
|
| 108 |
+
)
|
| 109 |
+
return level, response, triggered
|
| 110 |
+
|
| 111 |
+
# Diet culture check
|
| 112 |
+
for term in DIET_CULTURE_FLAGS:
|
| 113 |
+
if term in text:
|
| 114 |
+
triggered.append(f"β οΈ DIET_CULTURE: '{term}' detected")
|
| 115 |
+
level = "β οΈ WARNING"
|
| 116 |
+
|
| 117 |
+
if level == "β οΈ WARNING":
|
| 118 |
+
response = (
|
| 119 |
+
"βΉοΈ **Let me share what the evidence actually says...**\n\n"
|
| 120 |
+
"No single diet or supplement has been proven to 'cure' PCOS. What research consistently "
|
| 121 |
+
"supports is focusing on:\n"
|
| 122 |
+
"- **Blood sugar stability** through balanced meals (protein + fiber + healthy fats)\n"
|
| 123 |
+
"- **Regular physical activity** (150 min/week moderate exercise)\n"
|
| 124 |
+
"- **Adequate sleep** (7-9 hours)\n"
|
| 125 |
+
"- **Stress management**\n\n"
|
| 126 |
+
"The goal is **metabolic health**, not following a specific named diet.\n\n"
|
| 127 |
+
"βοΈ A registered dietitian specializing in PCOS can create a sustainable, personalized plan."
|
| 128 |
+
)
|
| 129 |
+
return level, response, triggered
|
| 130 |
+
|
| 131 |
+
# Safe β provide educational response
|
| 132 |
+
if not triggered:
|
| 133 |
+
triggered.append("β
No safety rules triggered")
|
| 134 |
+
response = (
|
| 135 |
+
"β
**This query is safe to process.**\n\n"
|
| 136 |
+
"In a full deployment, this would be passed to the PCOS Health LLM + RAG pipeline "
|
| 137 |
+
"to generate an empathetic, evidence-based response grounded in clinical guidelines.\n\n"
|
| 138 |
+
"The model would:\n"
|
| 139 |
+
"1. Retrieve relevant medical documents from the knowledge base\n"
|
| 140 |
+
"2. Generate a response with source citations\n"
|
| 141 |
+
"3. Run output safety checks before delivering\n"
|
| 142 |
+
"4. Include a medical disclaimer"
|
| 143 |
+
)
|
| 144 |
+
return level, response, triggered
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def safety_chat(message, history):
|
| 148 |
+
"""Chatbot handler for safety guardrails tab."""
|
| 149 |
+
if not message.strip():
|
| 150 |
+
return "", history
|
| 151 |
+
|
| 152 |
+
level, response, rules = check_safety(message)
|
| 153 |
+
|
| 154 |
+
# Format rules as a details block
|
| 155 |
+
rules_text = "\n".join(f"- {r}" for r in rules)
|
| 156 |
+
full_response = (
|
| 157 |
+
f"**Safety Classification: {level}**\n\n"
|
| 158 |
+
f"---\n\n"
|
| 159 |
+
f"{response}\n\n"
|
| 160 |
+
f"---\n"
|
| 161 |
+
f"<details><summary>π Triggered Rules ({len(rules)})</summary>\n\n"
|
| 162 |
+
f"{rules_text}\n"
|
| 163 |
+
f"</details>"
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
history.append({"role": "user", "content": message})
|
| 167 |
+
history.append({"role": "assistant", "content": full_response})
|
| 168 |
+
return "", history
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def test_output_safety(model_output: str) -> str:
|
| 172 |
+
"""Check if a model's output would pass safety guardrails."""
|
| 173 |
+
if not model_output.strip():
|
| 174 |
+
return "Enter a model output to check."
|
| 175 |
+
|
| 176 |
+
triggered = []
|
| 177 |
+
level = "β
SAFE"
|
| 178 |
+
text_lower = model_output.lower()
|
| 179 |
+
|
| 180 |
+
# Prescription check
|
| 181 |
+
prescription_pattern = re.compile(
|
| 182 |
+
r'\b(you\s+should\s+take|start\s+taking|increase\s+(?:your\s+)?dose|'
|
| 183 |
+
r'decrease\s+(?:your\s+)?dose|recommended?\s+dose|take\s+\d+\s*mg|stop\s+taking)\b',
|
| 184 |
+
re.IGNORECASE
|
| 185 |
+
)
|
| 186 |
+
dosage_pattern = re.compile(r'\b(\d+\s*(?:mg|mcg|ml|units?|tablets?|pills?))\b', re.IGNORECASE)
|
| 187 |
+
|
| 188 |
+
if prescription_pattern.search(model_output):
|
| 189 |
+
for med in PROHIBITED_MEDICATIONS:
|
| 190 |
+
if med in text_lower:
|
| 191 |
+
triggered.append(f"π« OUTPUT_PRESCRIPTION: model prescribed '{med}'")
|
| 192 |
+
level = "π« BLOCKED"
|
| 193 |
+
|
| 194 |
+
if dosage_pattern.search(model_output):
|
| 195 |
+
for med in PROHIBITED_MEDICATIONS:
|
| 196 |
+
if med in text_lower:
|
| 197 |
+
triggered.append(f"π« OUTPUT_DOSAGE: model specified dosage for '{med}'")
|
| 198 |
+
level = "π« BLOCKED"
|
| 199 |
+
|
| 200 |
+
# Diagnosis check
|
| 201 |
+
diag_patterns = [
|
| 202 |
+
r'\byou (?:have|definitely have|clearly have)\b.*\b(?:pcos|diabetes|syndrome)\b',
|
| 203 |
+
r'\bI (?:can )?diagnose\b', r'\byour diagnosis is\b',
|
| 204 |
+
]
|
| 205 |
+
for pat in diag_patterns:
|
| 206 |
+
if re.search(pat, model_output, re.IGNORECASE):
|
| 207 |
+
triggered.append("β οΈ OUTPUT_DIAGNOSIS: definitive diagnosis language")
|
| 208 |
+
if level == "β
SAFE":
|
| 209 |
+
level = "β οΈ WARNING"
|
| 210 |
+
|
| 211 |
+
# Disclaimer check
|
| 212 |
+
has_disclaimer = any(m in text_lower for m in [
|
| 213 |
+
"healthcare provider", "consult your doctor", "medical advice",
|
| 214 |
+
"endocrinologist", "ob/gyn", "healthcare team", "βοΈ", "specialist"
|
| 215 |
+
])
|
| 216 |
+
if not has_disclaimer:
|
| 217 |
+
triggered.append("βΉοΈ OUTPUT_NO_DISCLAIMER: missing safety disclaimer")
|
| 218 |
+
|
| 219 |
+
if not triggered:
|
| 220 |
+
triggered.append("β
All checks passed")
|
| 221 |
+
|
| 222 |
+
rules_text = "\n".join(f"- {r}" for r in triggered)
|
| 223 |
+
|
| 224 |
+
if level == "π« BLOCKED":
|
| 225 |
+
replacement = (
|
| 226 |
+
"**π« This output would be BLOCKED and replaced with:**\n\n"
|
| 227 |
+
"> I appreciate your question, but medication decisions β including starting, stopping, "
|
| 228 |
+
"or adjusting dosages β must be made by your healthcare provider who knows your complete "
|
| 229 |
+
"medical history.\n>\n> βοΈ Please discuss this with your endocrinologist or OB/GYN."
|
| 230 |
+
)
|
| 231 |
+
return f"## Output Safety: {level}\n\n{replacement}\n\n### Rules Triggered:\n{rules_text}"
|
| 232 |
+
|
| 233 |
+
return f"## Output Safety: {level}\n\n{'β
Output would be delivered to user.' if level == 'β
SAFE' else 'β οΈ Output would be delivered with added disclaimers.'}\n\n{'βοΈ *Missing disclaimer β one would be auto-appended.*' if not has_disclaimer else ''}\n\n### Rules Triggered:\n{rules_text}"
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 237 |
+
# EMBEDDED: RAG Pipeline (from rag_pipeline.py β lightweight, no ChromaDB)
|
| 238 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 239 |
+
|
| 240 |
+
KNOWLEDGE_BASE = {
|
| 241 |
+
"diagnosis": {
|
| 242 |
+
"title": "PCOS Diagnosis β Endocrine Society Guidelines",
|
| 243 |
+
"source": "Endocrine Society Clinical Practice Guidelines, 2013 (reaffirmed 2023)",
|
| 244 |
+
"content": (
|
| 245 |
+
"Diagnosis requires β₯2 of 3 Rotterdam criteria: (1) Oligo-anovulation (cycles >35 days, "
|
| 246 |
+
"<8 cycles/year, or amenorrhea >3 months), (2) Hyperandrogenism β clinical (hirsutism F-G β₯4-6, "
|
| 247 |
+
"acne, alopecia) or biochemical (elevated testosterone, DHEA-S), (3) Polycystic ovarian morphology "
|
| 248 |
+
"(β₯12 follicles 2-9mm per ovary OR volume >10 mL). Must exclude thyroid dysfunction, CAH, "
|
| 249 |
+
"Cushing's, hyperprolactinemia. Four phenotypes: A (full classic), B (no PCO), C (ovulatory), D (non-hyperandrogenic)."
|
| 250 |
+
),
|
| 251 |
+
},
|
| 252 |
+
"management": {
|
| 253 |
+
"title": "PCOS Management β ACOG Practice Bulletin No. 194",
|
| 254 |
+
"source": "ACOG, Practice Bulletin No. 194 (2018)",
|
| 255 |
+
"content": (
|
| 256 |
+
"First-line: lifestyle modification for ALL patients (150 min/week exercise, balanced nutrition, "
|
| 257 |
+
"behavioral support). Menstrual regulation: combined hormonal contraceptives (first-line). "
|
| 258 |
+
"Hyperandrogenism: CHCs first-line, anti-androgen add-on. Insulin resistance: Metformin (second-line, "
|
| 259 |
+
"for metabolic indications). Fertility: Letrozole (first-line, higher live birth than clomiphene 27.5% vs 19.1%), "
|
| 260 |
+
"Clomiphene (second-line), Gonadotropins (third-line), IVF (treatment-resistant). "
|
| 261 |
+
"Screen: OGTT at diagnosis then every 1-3 years, lipids, BP, depression/anxiety."
|
| 262 |
+
),
|
| 263 |
+
},
|
| 264 |
+
"insulin_resistance": {
|
| 265 |
+
"title": "Insulin Resistance in PCOS β Pathophysiology",
|
| 266 |
+
"source": "Diamanti-Kandarakis & Dunaif, Endocrine Reviews 2012",
|
| 267 |
+
"content": (
|
| 268 |
+
"50-70% of PCOS patients have insulin resistance independent of BMI. Selective insulin resistance: "
|
| 269 |
+
"metabolic pathways resistant but steroidogenic pathways remain sensitive β hyperinsulinemia drives "
|
| 270 |
+
"ovarian androgen production. Consequences: impaired glucose tolerance (30-40%), T2D risk 5-10x higher, "
|
| 271 |
+
"NAFLD (30-40%), CVD risk factors. Assessment: HOMA-IR, OGTT (gold standard), HbA1c. "
|
| 272 |
+
"Management: lifestyle improves sensitivity 30-40%, exercise (aerobic + resistance), metformin, inositol (emerging)."
|
| 273 |
+
),
|
| 274 |
+
},
|
| 275 |
+
"hyperandrogenism": {
|
| 276 |
+
"title": "Hyperandrogenism in PCOS",
|
| 277 |
+
"source": "Endocrine Society Guidelines; Azziz R et al., JCEM 2009",
|
| 278 |
+
"content": (
|
| 279 |
+
"Hirsutism (60-70%): excess terminal hair, Ferriman-Gallwey score β₯4-6. Acne (15-30%): adult-onset, "
|
| 280 |
+
"cystic, jawline distribution. Alopecia (5-10%): diffuse thinning, Ludwig scale. "
|
| 281 |
+
"Biochemical: elevated total/free testosterone (most sensitive), DHEA-S, low SHBG. "
|
| 282 |
+
"Pathophysiology: ovarian theca cell overproduction driven by LH hypersecretion + hyperinsulinemia, "
|
| 283 |
+
"reduced SHBG β more bioavailable testosterone. Adrenal contribution ~20-30%."
|
| 284 |
+
),
|
| 285 |
+
},
|
| 286 |
+
"mental_health": {
|
| 287 |
+
"title": "Mental Health and PCOS",
|
| 288 |
+
"source": "Cooney LG et al., Fertility & Sterility 2017",
|
| 289 |
+
"content": (
|
| 290 |
+
"Depression: 28-64% (3-8x general population). Anxiety: 34-57%. Eating disorders: 2-3x higher. "
|
| 291 |
+
"Mechanisms: androgen fluctuations affect serotonin/GABA, insulin resistance impacts brain glucose, "
|
| 292 |
+
"chronic symptom burden, visible symptoms, fertility concerns. Screen with PHQ-9, GAD-7 at diagnosis. "
|
| 293 |
+
"Management: CBT (strongest evidence), lifestyle intervention, pharmacotherapy for moderate-severe, "
|
| 294 |
+
"PCOS support groups. Mental health directly impacts treatment adherence and metabolic outcomes."
|
| 295 |
+
),
|
| 296 |
+
},
|
| 297 |
+
"fertility": {
|
| 298 |
+
"title": "Fertility and PCOS",
|
| 299 |
+
"source": "ACOG; Legro RS et al., NEJM 2014",
|
| 300 |
+
"content": (
|
| 301 |
+
"PCOS is most common cause of anovulatory infertility (70-80%). Most can conceive with management. "
|
| 302 |
+
"Treatment ladder: (1) Lifestyle β 5-10% weight reduction restores ovulation in 30-50%, "
|
| 303 |
+
"(2) Letrozole β FIRST-LINE, higher live birth rates (NEJM 2014), (3) Clomiphene β 60-80% ovulation rate, "
|
| 304 |
+
"(4) Gonadotropins β low-dose step-up, OHSS risk, (5) Ovarian drilling, (6) IVF β GnRH antagonist preferred. "
|
| 305 |
+
"OPKs may give false positives in PCOS (elevated LH); BBT is more reliable for confirming ovulation."
|
| 306 |
+
),
|
| 307 |
+
},
|
| 308 |
+
"lifestyle": {
|
| 309 |
+
"title": "Lifestyle Management in PCOS β Evidence Base",
|
| 310 |
+
"source": "Teede HJ et al., Int J Women's Health 2023",
|
| 311 |
+
"content": (
|
| 312 |
+
"Exercise: β₯150 min/week moderate OR 75 min vigorous. Resistance training 2-3x/week improves insulin "
|
| 313 |
+
"sensitivity 25-30%. Benefits occur independent of weight loss. Nutrition: no single diet proven superior. "
|
| 314 |
+
"Evidence supports: adequate protein (1.2-1.6 g/kg), fiber 25-30g/day, whole grains, omega-3s, "
|
| 315 |
+
"limiting added sugars. Mediterranean pattern has most evidence. Keto/IF: insufficient PCOS-specific evidence. "
|
| 316 |
+
"Sleep: 7-9 hours (sleep apnea risk elevated). Stress: chronic stress worsens insulin resistance. "
|
| 317 |
+
"Weight: 5-10% loss improves markers, but PCOS makes loss harder β focus on behaviors not scale."
|
| 318 |
+
),
|
| 319 |
+
},
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def rag_query(query: str) -> Tuple[str, str, str]:
|
| 324 |
+
"""Simple keyword-based RAG retrieval (no vector DB needed for demo)."""
|
| 325 |
+
if not query.strip():
|
| 326 |
+
return "Enter a question about PCOS.", "", ""
|
| 327 |
+
|
| 328 |
+
query_lower = query.lower()
|
| 329 |
+
|
| 330 |
+
# Score each document by keyword overlap
|
| 331 |
+
scores = {}
|
| 332 |
+
for key, doc in KNOWLEDGE_BASE.items():
|
| 333 |
+
text = (doc["title"] + " " + doc["content"]).lower()
|
| 334 |
+
score = sum(1 for word in query_lower.split() if word in text and len(word) > 3)
|
| 335 |
+
# Boost for category keyword matches
|
| 336 |
+
category_keywords = {
|
| 337 |
+
"diagnosis": ["diagnos", "rotterdam", "criteria", "phenotype", "test"],
|
| 338 |
+
"management": ["treat", "manage", "medication", "therapy", "screen"],
|
| 339 |
+
"insulin_resistance": ["insulin", "resistance", "glucose", "diabetes", "metabolic", "a1c"],
|
| 340 |
+
"hyperandrogenism": ["androgen", "hirsutism", "acne", "hair", "testosterone", "shbg"],
|
| 341 |
+
"mental_health": ["depress", "anxiety", "mental", "mood", "emotion", "feel", "sad"],
|
| 342 |
+
"fertility": ["fertil", "pregnant", "conceive", "ovulat", "ivf", "letrozole", "baby"],
|
| 343 |
+
"lifestyle": ["diet", "exercise", "nutrition", "weight", "sleep", "keto", "food", "stress"],
|
| 344 |
+
}
|
| 345 |
+
for kw in category_keywords.get(key, []):
|
| 346 |
+
if kw in query_lower:
|
| 347 |
+
score += 3
|
| 348 |
+
scores[key] = score
|
| 349 |
+
|
| 350 |
+
# Get top 3
|
| 351 |
+
top_keys = sorted(scores, key=scores.get, reverse=True)[:3]
|
| 352 |
+
top_docs = [(KNOWLEDGE_BASE[k], scores[k]) for k in top_keys if scores[k] > 0]
|
| 353 |
+
|
| 354 |
+
if not top_docs:
|
| 355 |
+
top_docs = [(KNOWLEDGE_BASE["management"], 1)] # Fallback
|
| 356 |
+
|
| 357 |
+
# Format retrieved context
|
| 358 |
+
context_parts = []
|
| 359 |
+
for i, (doc, score) in enumerate(top_docs, 1):
|
| 360 |
+
context_parts.append(
|
| 361 |
+
f"**[{i}] {doc['title']}**\n"
|
| 362 |
+
f"*Source: {doc['source']}*\n\n"
|
| 363 |
+
f"{doc['content']}"
|
| 364 |
+
)
|
| 365 |
+
context = "\n\n---\n\n".join(context_parts)
|
| 366 |
+
|
| 367 |
+
# Format sources
|
| 368 |
+
sources = "\n".join(
|
| 369 |
+
f"- **{doc['title']}** ({doc['source']}) β relevance: {score}"
|
| 370 |
+
for doc, score in top_docs
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
# Format the augmented prompt that would go to the LLM
|
| 374 |
+
augmented_prompt = (
|
| 375 |
+
f"**System**: {SYSTEM_PROMPT}\n\n"
|
| 376 |
+
f"**Retrieved Context**:\n{context}\n\n"
|
| 377 |
+
f"**User Question**: {query}\n\n"
|
| 378 |
+
f"**Instructions**: Provide empathetic, evidence-based response. Cite sources. "
|
| 379 |
+
f"Never prescribe. Include safety disclaimer."
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
return context, sources, augmented_prompt
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 386 |
+
# EMBEDDED: Dataset Generator (from dataset_pipeline.py β lightweight)
|
| 387 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 388 |
+
|
| 389 |
+
PHENOTYPES = [
|
| 390 |
+
{"name": "Classic PCOS (Full)", "hyperandrogenism": True, "oligo_anovulation": True},
|
| 391 |
+
{"name": "Classic PCOS (No PCO)", "hyperandrogenism": True, "oligo_anovulation": True},
|
| 392 |
+
{"name": "Ovulatory PCOS", "hyperandrogenism": True, "oligo_anovulation": False},
|
| 393 |
+
{"name": "Non-hyperandrogenic PCOS", "hyperandrogenism": False, "oligo_anovulation": True},
|
| 394 |
+
]
|
| 395 |
+
|
| 396 |
+
CLINICAL_QUESTIONS = [
|
| 397 |
+
"What exactly is PCOS and why does it happen?",
|
| 398 |
+
"How is PCOS diagnosed? What are the Rotterdam criteria?",
|
| 399 |
+
"Why are my periods so irregular with PCOS?",
|
| 400 |
+
"Can I get pregnant with PCOS?",
|
| 401 |
+
"What does insulin resistance have to do with PCOS?",
|
| 402 |
+
"What diet is best for PCOS?",
|
| 403 |
+
"I feel so frustrated dealing with PCOS every day.",
|
| 404 |
+
"Does PCOS affect mental health?",
|
| 405 |
+
"Should I cut out all carbs?",
|
| 406 |
+
"I hate how my body looks because of PCOS.",
|
| 407 |
+
]
|
| 408 |
+
|
| 409 |
+
EMPATHETIC_SCENARIOS = [
|
| 410 |
+
("I just got diagnosed with PCOS and feel overwhelmed.",
|
| 411 |
+
"First, take a breath. It's completely natural to feel overwhelmed right now. PCOS is manageable, and you're not alone β about 1 in 10 women have it. A good first step is making a list of symptoms and questions for your doctor. βοΈ Schedule a follow-up to create a personalized plan."),
|
| 412 |
+
("I've been trying to conceive for over a year and losing hope.",
|
| 413 |
+
"I'm so sorry you're going through this. PCOS-related fertility challenges are among the most treatable causes of difficulty conceiving. Many people with PCOS have successful pregnancies. βοΈ I'd encourage speaking with a reproductive endocrinologist."),
|
| 414 |
+
("The facial hair and acne make me feel so unfeminine.",
|
| 415 |
+
"What you're feeling is so understandable. These are medical symptoms caused by hormones, not reflections of who you are. A dermatologist experienced with PCOS can be an amazing ally. Your worth and femininity are not defined by these symptoms. π"),
|
| 416 |
+
("My doctor told me to just lose weight. I've tried everything.",
|
| 417 |
+
"That must have been frustrating. PCOS itself makes weight management significantly harder due to insulin resistance and hormonal factors β this is a medical reality, not a willpower issue. Focus on metabolic health markers, not the scale. βοΈ Consider a PCOS-specialized endocrinologist."),
|
| 418 |
+
("I read online that keto will cure my PCOS.",
|
| 419 |
+
"No single diet has been proven to 'cure' PCOS. Some evidence supports reducing refined carbs for insulin resistance, but strict keto is often unsustainable and can worsen hormonal stress. Focus on blood sugar stability: adequate protein, fiber-rich foods, healthy fats. βοΈ See a PCOS-specialized dietitian."),
|
| 420 |
+
]
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def generate_sample_data(n_samples: int = 10) -> list:
|
| 424 |
+
"""Generate sample PCOS training data for preview."""
|
| 425 |
+
samples = []
|
| 426 |
+
|
| 427 |
+
for i in range(min(n_samples, 30)):
|
| 428 |
+
category = random.choice(["tracking_log", "clinical_qa", "empathetic"])
|
| 429 |
+
|
| 430 |
+
if category == "tracking_log":
|
| 431 |
+
phenotype = random.choice(PHENOTYPES)
|
| 432 |
+
age = random.randint(18, 42)
|
| 433 |
+
bmi = round(random.triangular(18.5, 42.0, 28.0), 1)
|
| 434 |
+
cycle = random.choice([random.randint(35, 90), -1]) if phenotype["oligo_anovulation"] else random.randint(25, 35)
|
| 435 |
+
bbt = round(random.uniform(96.8, 98.2), 1)
|
| 436 |
+
symptoms = []
|
| 437 |
+
if phenotype["hyperandrogenism"]:
|
| 438 |
+
symptoms.append(random.choice(["Acne: moderate on jawline", "Hirsutism: chin (F-G: 12)", "Hair loss: diffuse thinning"]))
|
| 439 |
+
symptoms.append(random.choice(["Fatigue: moderate, afternoon crash", "Mood: anxious", "Bloating: mild, daily"]))
|
| 440 |
+
|
| 441 |
+
user_msg = (
|
| 442 |
+
f"Daily log: Age {age}, BMI {bmi}. "
|
| 443 |
+
f"Cycle: {'amenorrhea (>6mo)' if cycle < 0 else f'{cycle} days'}. "
|
| 444 |
+
f"BBT: {bbt}Β°F. Symptoms: {'; '.join(symptoms)}."
|
| 445 |
+
)
|
| 446 |
+
asst_msg = (
|
| 447 |
+
f"Thank you for tracking! π Your BBT of {bbt}Β°F {'without clear thermal shift suggests anovulatory cycle β common in PCOS' if bbt < 97.5 else 'shows a pattern developing β keep tracking'}. "
|
| 448 |
+
f"{'Amenorrhea >6mo: discuss with your OB/GYN for endometrial health.' if cycle < 0 else f'Cycles of {cycle} days are common with PCOS.'} "
|
| 449 |
+
f"βοΈ Discuss changes with your healthcare team."
|
| 450 |
+
)
|
| 451 |
+
samples.append([f"π‘οΈ Tracking Log", phenotype["name"], user_msg[:200], asst_msg[:200]])
|
| 452 |
+
|
| 453 |
+
elif category == "clinical_qa":
|
| 454 |
+
q = random.choice(CLINICAL_QUESTIONS)
|
| 455 |
+
prefix = random.choice([f"I'm {random.randint(18, 42)} with PCOS. ", ""])
|
| 456 |
+
samples.append([
|
| 457 |
+
"π Clinical Q&A",
|
| 458 |
+
"Guideline-grounded",
|
| 459 |
+
(prefix + q)[:200],
|
| 460 |
+
"Evidence-based response citing Endocrine Society / ACOG guidelines..."[:200],
|
| 461 |
+
])
|
| 462 |
+
|
| 463 |
+
else:
|
| 464 |
+
scenario = random.choice(EMPATHETIC_SCENARIOS)
|
| 465 |
+
samples.append([
|
| 466 |
+
"π Empathetic",
|
| 467 |
+
"Emotional support",
|
| 468 |
+
scenario[0][:200],
|
| 469 |
+
scenario[1][:200],
|
| 470 |
+
])
|
| 471 |
+
|
| 472 |
+
return samples
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 476 |
+
# EMBEDDED: Training Config (from lora_finetuning.py)
|
| 477 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 478 |
+
|
| 479 |
+
def get_training_config(preset: str) -> str:
|
| 480 |
+
"""Return training configuration as formatted markdown."""
|
| 481 |
+
configs = {
|
| 482 |
+
"Quick Test": {
|
| 483 |
+
"Base Model": "BioMistral/BioMistral-7B",
|
| 484 |
+
"LoRA Rank (r)": 8, "LoRA Alpha": 16, "LoRA Dropout": 0.05,
|
| 485 |
+
"Target Modules": "all-linear", "Epochs": 1,
|
| 486 |
+
"Batch Size": "1 Γ 4 grad_accum = 4 effective",
|
| 487 |
+
"Learning Rate": "1e-4", "Scheduler": "cosine",
|
| 488 |
+
"Max Seq Length": 512, "Quantization": "4-bit NF4 (QLoRA)",
|
| 489 |
+
"Optimizer": "paged_adamw_32bit", "Hardware": "a10g-small (24GB GPU)",
|
| 490 |
+
"Estimated Time": "~30 minutes",
|
| 491 |
+
"Paper Reference": "BioMistral (arXiv:2402.10373)",
|
| 492 |
+
},
|
| 493 |
+
"Standard": {
|
| 494 |
+
"Base Model": "BioMistral/BioMistral-7B",
|
| 495 |
+
"LoRA Rank (r)": 16, "LoRA Alpha": 32, "LoRA Dropout": 0.05,
|
| 496 |
+
"Target Modules": "all-linear", "Epochs": 3,
|
| 497 |
+
"Batch Size": "2 Γ 8 grad_accum = 16 effective",
|
| 498 |
+
"Learning Rate": "1e-4", "Scheduler": "cosine",
|
| 499 |
+
"Max Seq Length": 2048, "Quantization": "4-bit NF4 (QLoRA)",
|
| 500 |
+
"Optimizer": "paged_adamw_32bit", "Hardware": "a10g-large (24GB GPU)",
|
| 501 |
+
"Estimated Time": "~3-4 hours",
|
| 502 |
+
"Paper Reference": "Med42 (arXiv:2404.14779) + BioMistral (arXiv:2402.10373)",
|
| 503 |
+
},
|
| 504 |
+
"High Quality": {
|
| 505 |
+
"Base Model": "meta-llama/Meta-Llama-3.1-8B-Instruct",
|
| 506 |
+
"LoRA Rank (r)": 64, "LoRA Alpha": 128, "LoRA Dropout": 0.05,
|
| 507 |
+
"Target Modules": "all-linear", "Epochs": 5,
|
| 508 |
+
"Batch Size": "2 Γ 16 grad_accum = 32 effective",
|
| 509 |
+
"Learning Rate": "5e-5", "Scheduler": "cosine",
|
| 510 |
+
"Max Seq Length": 4096, "Quantization": "4-bit NF4 (QLoRA)",
|
| 511 |
+
"Optimizer": "paged_adamw_32bit", "Hardware": "a100-large (80GB GPU)",
|
| 512 |
+
"Estimated Time": "~6-8 hours",
|
| 513 |
+
"Paper Reference": "Med42 (arXiv:2404.14779)",
|
| 514 |
+
},
|
| 515 |
+
"Maximum (Full FT Parity)": {
|
| 516 |
+
"Base Model": "meta-llama/Meta-Llama-3.1-8B-Instruct",
|
| 517 |
+
"LoRA Rank (r)": 256, "LoRA Alpha": 16, "LoRA Dropout": 0.05,
|
| 518 |
+
"Target Modules": "all-linear", "Epochs": 8,
|
| 519 |
+
"Batch Size": "2 Γ 16 grad_accum = 32 effective",
|
| 520 |
+
"Learning Rate": "2e-4", "Scheduler": "cosine",
|
| 521 |
+
"Max Seq Length": 4096, "Quantization": "4-bit NF4 (QLoRA)",
|
| 522 |
+
"Optimizer": "paged_adamw_32bit", "Hardware": "a100-large (80GB GPU)",
|
| 523 |
+
"Estimated Time": "~12-16 hours",
|
| 524 |
+
"Paper Reference": "LoRA Without Regret (2025, Schulman et al.)",
|
| 525 |
+
},
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
cfg = configs.get(preset, configs["Standard"])
|
| 529 |
+
|
| 530 |
+
rows = "\n".join(f"| **{k}** | {v} |" for k, v in cfg.items())
|
| 531 |
+
md = (
|
| 532 |
+
f"## π― Preset: {preset}\n\n"
|
| 533 |
+
f"| Parameter | Value |\n|---|---|\n{rows}\n\n"
|
| 534 |
+
f"### Key Design Decisions\n\n"
|
| 535 |
+
f"- **`target_modules='all-linear'`**: Per Med42 paper, applying LoRA to ALL linear layers "
|
| 536 |
+
f"(not just attention Q/K/V/O) significantly outperforms attention-only LoRA.\n"
|
| 537 |
+
f"- **Learning rate 10x higher than full FT**: Standard practice for LoRA β the adapter weights "
|
| 538 |
+
f"need stronger gradients since only a small fraction of parameters are updated.\n"
|
| 539 |
+
f"- **Cosine schedule**: Both Med42 and BioMistral validate cosine > linear for LoRA training.\n"
|
| 540 |
+
f"- **QLoRA (4-bit NF4)**: Reduces VRAM from ~14GB (fp16) to ~5GB for 7B models, enabling "
|
| 541 |
+
f"training on consumer GPUs.\n\n"
|
| 542 |
+
f"### Mobile Export\n\n"
|
| 543 |
+
f"After training:\n"
|
| 544 |
+
f"- **GGUF**: `convert-hf-to-gguf.py β Q4_K_M` (~5GB for mobile)\n"
|
| 545 |
+
f"- **AWQ**: `group_size=128, w_bit=4` (~4.68GB for mobile)\n"
|
| 546 |
+
)
|
| 547 |
+
return md
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 551 |
+
# GRADIO APP
|
| 552 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 553 |
+
|
| 554 |
+
EXAMPLE_SAFE = [
|
| 555 |
+
"What are common symptoms of PCOS?",
|
| 556 |
+
"How does PCOS affect my menstrual cycle?",
|
| 557 |
+
"Can you explain what insulin resistance means?",
|
| 558 |
+
]
|
| 559 |
+
EXAMPLE_REFERRAL = [
|
| 560 |
+
"What dose of metformin should I take?",
|
| 561 |
+
"Can you prescribe me birth control for PCOS?",
|
| 562 |
+
"Should I increase my spironolactone to 200mg?",
|
| 563 |
+
]
|
| 564 |
+
EXAMPLE_EMERGENCY = [
|
| 565 |
+
"I'm having severe abdominal pain and heavy bleeding",
|
| 566 |
+
"I've been having suicidal thoughts because of my PCOS",
|
| 567 |
+
]
|
| 568 |
+
EXAMPLE_DIET = [
|
| 569 |
+
"Will a keto cure my PCOS?",
|
| 570 |
+
"What detox should I do for hormonal balance?",
|
| 571 |
+
]
|
| 572 |
+
|
| 573 |
+
CSS = """
|
| 574 |
+
.header-text { text-align: center; }
|
| 575 |
+
.disclaimer { background-color: #fff3cd; padding: 12px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 10px 0; }
|
| 576 |
+
"""
|
| 577 |
+
|
| 578 |
+
with gr.Blocks(title="PCOS Health AI Demo") as demo:
|
| 579 |
+
|
| 580 |
+
gr.Markdown(
|
| 581 |
+
"""
|
| 582 |
+
# π©Ί PCOS Health AI β Interactive Pipeline Demo
|
| 583 |
+
|
| 584 |
+
**A production-grade AI pipeline for hormonal health tracking focused on Polycystic Ovary Syndrome (PCOS).**
|
| 585 |
+
|
| 586 |
+
Explore all 4 components: safety guardrails, RAG knowledge retrieval, dataset generation, and training configuration.
|
| 587 |
+
|
| 588 |
+
π¦ [View Full Code Repository](https://huggingface.co/gthor7/pcos-health-ai-pipeline)
|
| 589 |
+
""",
|
| 590 |
+
elem_classes="header-text",
|
| 591 |
+
)
|
| 592 |
+
|
| 593 |
+
gr.Markdown(
|
| 594 |
+
"β οΈ **Disclaimer**: This is a research demo, NOT a medical device. Never use AI for medical decisions. "
|
| 595 |
+
"Always consult a licensed healthcare professional.",
|
| 596 |
+
elem_classes="disclaimer",
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
# ββ Tab 1: Safety Guardrails ββββββββββββββββββββββββββββββββββββββββββββ
|
| 600 |
+
with gr.Tab("π‘οΈ Safety Guardrails"):
|
| 601 |
+
gr.Markdown(
|
| 602 |
+
"### Test the 3-layer medical safety system\n"
|
| 603 |
+
"Type any health question and see how the guardrails classify it. "
|
| 604 |
+
"Try medication requests, emergency symptoms, or diet culture claims."
|
| 605 |
+
)
|
| 606 |
+
|
| 607 |
+
with gr.Row():
|
| 608 |
+
with gr.Column(scale=3):
|
| 609 |
+
chatbot = gr.Chatbot(
|
| 610 |
+
height=450,
|
| 611 |
+
buttons=["copy", "copy_all"],
|
| 612 |
+
placeholder="Try asking about PCOS symptoms, medications, or emergencies...",
|
| 613 |
+
)
|
| 614 |
+
with gr.Row():
|
| 615 |
+
msg_input = gr.Textbox(
|
| 616 |
+
label="Your message",
|
| 617 |
+
placeholder="e.g., 'What dose of metformin should I take?'",
|
| 618 |
+
scale=5,
|
| 619 |
+
show_label=False,
|
| 620 |
+
)
|
| 621 |
+
send_btn = gr.Button("Send", variant="primary", scale=1)
|
| 622 |
+
clear_btn = gr.ClearButton([msg_input, chatbot], value="ποΈ Clear Chat")
|
| 623 |
+
|
| 624 |
+
with gr.Column(scale=2):
|
| 625 |
+
gr.Markdown("#### π§ͺ Try These Examples")
|
| 626 |
+
gr.Markdown("**β
Safe queries:**")
|
| 627 |
+
for ex in EXAMPLE_SAFE:
|
| 628 |
+
gr.Button(ex, size="sm").click(
|
| 629 |
+
lambda x=ex: (x, ), outputs=[msg_input]
|
| 630 |
+
)
|
| 631 |
+
gr.Markdown("**π Medication (β Referral):**")
|
| 632 |
+
for ex in EXAMPLE_REFERRAL:
|
| 633 |
+
gr.Button(ex, size="sm").click(
|
| 634 |
+
lambda x=ex: (x, ), outputs=[msg_input]
|
| 635 |
+
)
|
| 636 |
+
gr.Markdown("**π¨ Emergency:**")
|
| 637 |
+
for ex in EXAMPLE_EMERGENCY:
|
| 638 |
+
gr.Button(ex, size="sm").click(
|
| 639 |
+
lambda x=ex: (x, ), outputs=[msg_input]
|
| 640 |
+
)
|
| 641 |
+
gr.Markdown("**β οΈ Diet culture:**")
|
| 642 |
+
for ex in EXAMPLE_DIET:
|
| 643 |
+
gr.Button(ex, size="sm").click(
|
| 644 |
+
lambda x=ex: (x, ), outputs=[msg_input]
|
| 645 |
+
)
|
| 646 |
+
|
| 647 |
+
msg_input.submit(safety_chat, [msg_input, chatbot], [msg_input, chatbot])
|
| 648 |
+
send_btn.click(safety_chat, [msg_input, chatbot], [msg_input, chatbot])
|
| 649 |
+
|
| 650 |
+
gr.Markdown("---\n### π Output Safety Checker\nPaste a model's response to check if it would pass output guardrails:")
|
| 651 |
+
with gr.Row():
|
| 652 |
+
output_input = gr.Textbox(
|
| 653 |
+
label="Model output to check",
|
| 654 |
+
placeholder="e.g., 'You should take 500mg of metformin twice daily.'",
|
| 655 |
+
lines=3,
|
| 656 |
+
)
|
| 657 |
+
output_result = gr.Markdown()
|
| 658 |
+
output_btn = gr.Button("Check Output Safety", variant="secondary")
|
| 659 |
+
output_btn.click(test_output_safety, inputs=output_input, outputs=output_result)
|
| 660 |
+
|
| 661 |
+
# ββ Tab 2: RAG Pipeline βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 662 |
+
with gr.Tab("π RAG Knowledge Base"):
|
| 663 |
+
gr.Markdown(
|
| 664 |
+
"### Query the PCOS Medical Knowledge Base\n"
|
| 665 |
+
"Ask any PCOS question and see which clinical guidelines are retrieved, "
|
| 666 |
+
"along with the augmented prompt that would be sent to the LLM."
|
| 667 |
+
)
|
| 668 |
+
|
| 669 |
+
rag_input = gr.Textbox(
|
| 670 |
+
label="Your PCOS question",
|
| 671 |
+
placeholder="e.g., 'What are the Rotterdam criteria for diagnosing PCOS?'",
|
| 672 |
+
lines=2,
|
| 673 |
+
)
|
| 674 |
+
rag_btn = gr.Button("π Retrieve & Build Prompt", variant="primary")
|
| 675 |
+
|
| 676 |
+
gr.Markdown("#### Example Queries")
|
| 677 |
+
with gr.Row():
|
| 678 |
+
for q in ["How is PCOS diagnosed?", "Does PCOS affect fertility?",
|
| 679 |
+
"What diet is best for PCOS?", "Why does PCOS cause depression?",
|
| 680 |
+
"What is insulin resistance in PCOS?"]:
|
| 681 |
+
gr.Button(q, size="sm").click(lambda x=q: x, outputs=[rag_input])
|
| 682 |
+
|
| 683 |
+
with gr.Row():
|
| 684 |
+
rag_context = gr.Markdown(label="Retrieved Documents")
|
| 685 |
+
with gr.Accordion("π Sources & Relevance Scores", open=False):
|
| 686 |
+
rag_sources = gr.Markdown()
|
| 687 |
+
with gr.Accordion("π Full Augmented Prompt (sent to LLM)", open=False):
|
| 688 |
+
rag_prompt = gr.Markdown()
|
| 689 |
+
|
| 690 |
+
rag_btn.click(rag_query, inputs=rag_input, outputs=[rag_context, rag_sources, rag_prompt])
|
| 691 |
+
|
| 692 |
+
# ββ Tab 3: Dataset Generator ββββββββββββββββββββββββββββββββββββββββββββ
|
| 693 |
+
with gr.Tab("π Dataset Preview"):
|
| 694 |
+
gr.Markdown(
|
| 695 |
+
"### Synthetic PCOS Training Data\n"
|
| 696 |
+
"Preview the 3 types of synthetic training data generated by the pipeline:\n"
|
| 697 |
+
"- π‘οΈ **Tracking Logs** β Daily menstrual/BBT/symptom logs with AI interpretations\n"
|
| 698 |
+
"- π **Clinical Q&A** β Guideline-grounded questions and answers\n"
|
| 699 |
+
"- π **Empathetic Conversations** β Emotional support exchanges\n\n"
|
| 700 |
+
"**Plus 5 HuggingFace datasets** (350K+ examples total) preprocessed into ChatML format."
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
with gr.Row():
|
| 704 |
+
n_slider = gr.Slider(minimum=5, maximum=30, value=10, step=1, label="Number of samples")
|
| 705 |
+
gen_btn = gr.Button("π² Generate Samples", variant="primary")
|
| 706 |
+
|
| 707 |
+
dataset_table = gr.Dataframe(
|
| 708 |
+
headers=["Type", "Subtype", "User Message (preview)", "Assistant Response (preview)"],
|
| 709 |
+
datatype=["str", "str", "str", "str"],
|
| 710 |
+
wrap=True,
|
| 711 |
+
interactive=False,
|
| 712 |
+
)
|
| 713 |
+
gen_btn.click(generate_sample_data, inputs=n_slider, outputs=dataset_table)
|
| 714 |
+
|
| 715 |
+
gr.Markdown(
|
| 716 |
+
"### π¦ Hub Datasets Used\n\n"
|
| 717 |
+
"| # | Dataset | Size | Purpose |\n"
|
| 718 |
+
"|---|---------|------|---------|\n"
|
| 719 |
+
"| 1 | [lavita/ChatDoctor-HealthCareMagic-100k](https://huggingface.co/datasets/lavita/ChatDoctor-HealthCareMagic-100k) | 100K | Real patient-doctor Q&A |\n"
|
| 720 |
+
"| 2 | [ruslanmv/ai-medical-chatbot](https://huggingface.co/datasets/ruslanmv/ai-medical-chatbot) | 250K+ | High-volume medical dialogues |\n"
|
| 721 |
+
"| 3 | [medalpaca/medical_meadow_wikidoc_patient_information](https://huggingface.co/datasets/medalpaca/medical_meadow_wikidoc_patient_information) | 5.5K | Clinical knowledge (plain language) |\n"
|
| 722 |
+
"| 4 | [Amod/mental_health_counseling_conversations](https://huggingface.co/datasets/Amod/mental_health_counseling_conversations) | 3.5K | Empathetic counseling |\n"
|
| 723 |
+
"| 5 | [Malikeh1375/medical-question-answering-datasets](https://huggingface.co/datasets/Malikeh1375/medical-question-answering-datasets) | 152MB | Unified 46-dataset bundle |"
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
# ββ Tab 4: Training Config ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 727 |
+
with gr.Tab("βοΈ Training Config"):
|
| 728 |
+
gr.Markdown(
|
| 729 |
+
"### LoRA Fine-Tuning Presets\n"
|
| 730 |
+
"Select a training preset to view the full configuration. "
|
| 731 |
+
"All hyperparameters are grounded in published medical LLM papers."
|
| 732 |
+
)
|
| 733 |
+
|
| 734 |
+
preset_selector = gr.Radio(
|
| 735 |
+
choices=["Quick Test", "Standard", "High Quality", "Maximum (Full FT Parity)"],
|
| 736 |
+
value="Standard",
|
| 737 |
+
label="Select Preset",
|
| 738 |
+
)
|
| 739 |
+
config_display = gr.Markdown()
|
| 740 |
+
preset_selector.change(get_training_config, inputs=preset_selector, outputs=config_display)
|
| 741 |
+
|
| 742 |
+
# Load default
|
| 743 |
+
demo.load(get_training_config, inputs=preset_selector, outputs=config_display)
|
| 744 |
+
|
| 745 |
+
gr.Markdown(
|
| 746 |
+
"### ποΈ Architecture Overview\n\n"
|
| 747 |
+
"```\n"
|
| 748 |
+
"βββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ\n"
|
| 749 |
+
"β User Input βββββΆβ Llama Guard βββββΆβ PCOS LLM β\n"
|
| 750 |
+
"β β β (Input Safety) β β + RAG Pipeline β\n"
|
| 751 |
+
"βββββββββββββββ ββββββββββββββββββββ ββββββββββ¬βββββββββ\n"
|
| 752 |
+
" β\n"
|
| 753 |
+
" ββββββββββββββββββββ ββββββββββΌβββββββββ\n"
|
| 754 |
+
" β NeMo Guardrails βββββ Llama Guard β\n"
|
| 755 |
+
" β (Topic Control) β β (Output Safety) β\n"
|
| 756 |
+
" ββββββββββββ¬ββββββββ βββββββββββββββββββ\n"
|
| 757 |
+
" β\n"
|
| 758 |
+
" ββββββββββββΌββββββββ\n"
|
| 759 |
+
" β Safe Response β\n"
|
| 760 |
+
" β + Disclaimer β\n"
|
| 761 |
+
" οΏ½οΏ½οΏ½βββββββββββββββββββ\n"
|
| 762 |
+
"```\n\n"
|
| 763 |
+
"### π Paper References\n\n"
|
| 764 |
+
"| Paper | Key Contribution |\n"
|
| 765 |
+
"|-------|------------------|\n"
|
| 766 |
+
"| Med42 (arXiv:2404.14779) | LoRA all-linear targets outperform attention-only |\n"
|
| 767 |
+
"| BioMistral (arXiv:2402.10373) | Medical pre-training on PMC, r=16, dropout=0.05 |\n"
|
| 768 |
+
"| LoRA Without Regret (2025) | r=256 matches full fine-tuning at 67% compute |\n"
|
| 769 |
+
"| ChatDoctor (arXiv:2303.14070) | 100K patient-doctor conversation dataset |\n"
|
| 770 |
+
"| SoulChat (arXiv:2311.00273) | Empathetic health conversation patterns |\n"
|
| 771 |
+
"| Llama Guard (arXiv:2312.06674) | Input/output safety classification, F1=0.945 |\n"
|
| 772 |
+
"| NeMo Guardrails (arXiv:2310.10501) | Programmable safety rails via Colang |"
|
| 773 |
+
)
|
| 774 |
+
|
| 775 |
+
gr.Markdown(
|
| 776 |
+
"---\n"
|
| 777 |
+
"**Built by [gthor7](https://huggingface.co/gthor7)** | "
|
| 778 |
+
"[π¦ Full Code Repository](https://huggingface.co/gthor7/pcos-health-ai-pipeline) | "
|
| 779 |
+
"π©Ί *This is a research demo β not medical advice.*"
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
if __name__ == "__main__":
|
| 784 |
+
demo.launch(theme=gr.themes.Soft(), css=CSS)
|