Spaces:
Running
Running
Upload 19 files
Browse files- app.py +233 -66
- backend/__pycache__/rag_engine.cpython-314.pyc +0 -0
- backend/chat_endpoint.py +53 -53
- backend/chat_router.py +58 -58
- backend/chat_textgen.py +58 -58
- backend/endpoint_client.py +26 -26
- backend/pdf_utils.py +21 -21
- backend/rag_engine.py +53 -53
- backend/soap_generator.py +15 -15
- pages/1_Diagnostics.py +19 -19
- pages/1_How_It_Works.py +8 -0
- pages/2_What_Can_Be_Done.py +243 -0
- pages/3_How_Doctors_Benefit.py +241 -0
- requirements.txt +7 -7
- style.css +28 -28
- utils/constants.py +13 -13
- utils/helpers.py +11 -11
- utils/persona.py +9 -9
app.py
CHANGED
|
@@ -1,66 +1,233 @@
|
|
| 1 |
-
import os, json, streamlit as st
|
| 2 |
-
from backend.rag_engine import get_embedder, get_chroma, retrieve, seed_index
|
| 3 |
-
from backend.soap_generator import compose_soap
|
| 4 |
-
from backend.pdf_utils import generate_pdf
|
| 5 |
-
from backend.chat_textgen import chat
|
| 6 |
-
from utils.constants import DOCS_DIR, RETRIEVAL_K_DEFAULT
|
| 7 |
-
|
| 8 |
-
st.set_page_config(
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
st.
|
| 25 |
-
st.
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
for
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
st.markdown("
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, json, streamlit as st
|
| 2 |
+
from backend.rag_engine import get_embedder, get_chroma, retrieve, seed_index
|
| 3 |
+
from backend.soap_generator import compose_soap
|
| 4 |
+
from backend.pdf_utils import generate_pdf
|
| 5 |
+
from backend.chat_textgen import chat
|
| 6 |
+
from utils.constants import DOCS_DIR, RETRIEVAL_K_DEFAULT
|
| 7 |
+
|
| 8 |
+
st.set_page_config(
|
| 9 |
+
page_title="MediAssist — Clinical Decision Support",
|
| 10 |
+
page_icon="🩺",
|
| 11 |
+
layout="wide",
|
| 12 |
+
initial_sidebar_state="expanded"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
@st.cache_resource(show_spinner=False)
|
| 16 |
+
def _embedder():
|
| 17 |
+
return get_embedder()
|
| 18 |
+
|
| 19 |
+
@st.cache_resource(show_spinner=False)
|
| 20 |
+
def _col():
|
| 21 |
+
return get_chroma()[1]
|
| 22 |
+
|
| 23 |
+
# Sidebar Configuration
|
| 24 |
+
with st.sidebar:
|
| 25 |
+
st.markdown("## ⚙️ Configuration")
|
| 26 |
+
|
| 27 |
+
with st.expander("RAG Index Management", expanded=False):
|
| 28 |
+
if st.button("🔄 Seed / Refresh RAG Index"):
|
| 29 |
+
with st.spinner("Indexing medical guidelines..."):
|
| 30 |
+
try:
|
| 31 |
+
n = seed_index(_col(), _embedder(), DOCS_DIR)
|
| 32 |
+
st.success(f"✅ Indexed {n} chunks from {DOCS_DIR}")
|
| 33 |
+
except Exception as e:
|
| 34 |
+
st.error(f"❌ Indexing failed: {str(e)}")
|
| 35 |
+
st.caption("Upload .txt/.md files to `data/guidelines/<specialty>/` then reseed.")
|
| 36 |
+
|
| 37 |
+
st.divider()
|
| 38 |
+
st.markdown("### About")
|
| 39 |
+
st.info(
|
| 40 |
+
"**MediAssist v15** combines clinical guidelines with AI to provide "
|
| 41 |
+
"evidence-based decision support for healthcare professionals."
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
with st.expander("Features", expanded=False):
|
| 45 |
+
st.markdown("""
|
| 46 |
+
- 📚 RAG-based guideline retrieval
|
| 47 |
+
- 🤖 SOAP note generation
|
| 48 |
+
- 💬 Context-aware AI chat
|
| 49 |
+
- 📄 PDF report generation
|
| 50 |
+
""")
|
| 51 |
+
|
| 52 |
+
# Main Content
|
| 53 |
+
st.title("🩺 MediAssist — Clinical Decision Support System")
|
| 54 |
+
st.markdown(
|
| 55 |
+
"AI-powered clinical guidelines with **RAG** • **SOAP** • **Chat** • **PDF Reports**"
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Main Input Section
|
| 59 |
+
st.markdown("### 📝 Patient Information")
|
| 60 |
+
narrative = st.text_area(
|
| 61 |
+
"Patient Narrative",
|
| 62 |
+
height=120,
|
| 63 |
+
placeholder="e.g., 32-year-old female with 10 days of period delay, nausea, mild cramps. No fever. Medical history: PCOS.",
|
| 64 |
+
help="Describe the patient's presenting complaint and relevant history"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
col1, col2 = st.columns([3, 1])
|
| 68 |
+
with col1:
|
| 69 |
+
k = st.slider(
|
| 70 |
+
"Number of guidelines to retrieve",
|
| 71 |
+
min_value=1,
|
| 72 |
+
max_value=10,
|
| 73 |
+
value=RETRIEVAL_K_DEFAULT,
|
| 74 |
+
help="More results = more comprehensive but potentially noisy"
|
| 75 |
+
)
|
| 76 |
+
with col2:
|
| 77 |
+
st.metric("Retrieval K", k)
|
| 78 |
+
|
| 79 |
+
st.divider()
|
| 80 |
+
|
| 81 |
+
# Create Tabs for Different Functions
|
| 82 |
+
tab1, tab2, tab3, tab4 = st.tabs(["📊 SOAP Note", "💬 AI Chat", "📄 PDF Report", "📚 Guidelines"])
|
| 83 |
+
|
| 84 |
+
# Tab 1: SOAP Note Generation
|
| 85 |
+
with tab1:
|
| 86 |
+
st.markdown("### Generate SOAP Note with Clinical Evidence")
|
| 87 |
+
|
| 88 |
+
col_soap1, col_soap2 = st.columns(2)
|
| 89 |
+
|
| 90 |
+
if st.button("🧾 Generate SOAP Note", key="soap_button"):
|
| 91 |
+
if not narrative.strip():
|
| 92 |
+
st.warning("⚠️ Please enter patient narrative first.")
|
| 93 |
+
else:
|
| 94 |
+
with st.spinner("Retrieving relevant guidelines..."):
|
| 95 |
+
try:
|
| 96 |
+
items = retrieve(_col(), _embedder(), narrative, k=k)
|
| 97 |
+
soap = compose_soap(narrative, items)
|
| 98 |
+
|
| 99 |
+
with col_soap1:
|
| 100 |
+
st.subheader("SOAP Note (JSON)")
|
| 101 |
+
st.code(json.dumps(soap, indent=2), language="json")
|
| 102 |
+
|
| 103 |
+
# Copy button helper
|
| 104 |
+
st.caption("💡 Tip: Use the copy button in the code block to copy the JSON")
|
| 105 |
+
|
| 106 |
+
with col_soap2:
|
| 107 |
+
st.subheader(f"📚 Citations ({len(items)} sources)")
|
| 108 |
+
if not items:
|
| 109 |
+
st.info("No relevant guidelines found.")
|
| 110 |
+
else:
|
| 111 |
+
for i, it in enumerate(items, 1):
|
| 112 |
+
with st.container(border=True):
|
| 113 |
+
st.markdown(f"**Source {i}: {it.get('title', 'Unknown')}**")
|
| 114 |
+
st.caption(f"📖 {it.get('source', 'N/A')}")
|
| 115 |
+
st.markdown(f"> {it.get('text', '')[:350]}...")
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
st.error(f"❌ Error generating SOAP note: {str(e)}")
|
| 119 |
+
|
| 120 |
+
# Tab 2: AI Chat
|
| 121 |
+
with tab2:
|
| 122 |
+
st.markdown("### Conversational AI Assistant")
|
| 123 |
+
|
| 124 |
+
col_chat1, col_chat2 = st.columns([1, 1])
|
| 125 |
+
|
| 126 |
+
with col_chat1:
|
| 127 |
+
mode = st.radio(
|
| 128 |
+
"Chat Mode",
|
| 129 |
+
["Patient-facing explanation", "Doctor-facing analysis"],
|
| 130 |
+
help="Choose audience for AI response"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
if st.button("💬 Start Chat Session", key="chat_button"):
|
| 134 |
+
if not narrative.strip():
|
| 135 |
+
st.warning("⚠️ Please enter patient narrative first.")
|
| 136 |
+
else:
|
| 137 |
+
with st.spinner("AI is thinking..."):
|
| 138 |
+
try:
|
| 139 |
+
reply = chat(
|
| 140 |
+
narrative,
|
| 141 |
+
mode="patient" if "Patient" in mode else "doctor"
|
| 142 |
+
)
|
| 143 |
+
st.markdown("### AI Response")
|
| 144 |
+
st.markdown(reply)
|
| 145 |
+
except Exception as e:
|
| 146 |
+
st.error(f"❌ Chat error: {str(e)}")
|
| 147 |
+
|
| 148 |
+
# Tab 3: PDF Report
|
| 149 |
+
with tab3:
|
| 150 |
+
st.markdown("### Generate Clinical PDF Report")
|
| 151 |
+
|
| 152 |
+
col_pdf1, col_pdf2 = st.columns([2, 1])
|
| 153 |
+
|
| 154 |
+
with col_pdf1:
|
| 155 |
+
ai_summary = st.text_area(
|
| 156 |
+
"Doctor-Reviewed Summary",
|
| 157 |
+
height=100,
|
| 158 |
+
placeholder="Enter clinical summary, assessment, and plan...",
|
| 159 |
+
help="This will be included in the PDF report"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
with col_pdf2:
|
| 163 |
+
report_name = st.text_input(
|
| 164 |
+
"Report Filename",
|
| 165 |
+
value="MediAssist_Report",
|
| 166 |
+
help="PDF will be saved as [filename].pdf"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
if st.button("📄 Generate PDF Report", key="pdf_button"):
|
| 170 |
+
if not narrative.strip():
|
| 171 |
+
st.warning("⚠️ Please enter patient narrative first.")
|
| 172 |
+
else:
|
| 173 |
+
with st.spinner("Generating PDF..."):
|
| 174 |
+
try:
|
| 175 |
+
items = retrieve(_col(), _embedder(), narrative, k=3)
|
| 176 |
+
soap = compose_soap(narrative, items)
|
| 177 |
+
pdf_path = f"{report_name}.pdf"
|
| 178 |
+
|
| 179 |
+
generate_pdf(
|
| 180 |
+
pdf_path,
|
| 181 |
+
"MediAssist — Clinical Report",
|
| 182 |
+
soap,
|
| 183 |
+
ai_summary
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
with open(pdf_path, "rb") as pdf_file:
|
| 187 |
+
st.download_button(
|
| 188 |
+
label="⬇️ Download PDF Report",
|
| 189 |
+
data=pdf_file,
|
| 190 |
+
file_name=pdf_path,
|
| 191 |
+
mime="application/pdf",
|
| 192 |
+
key="pdf_download"
|
| 193 |
+
)
|
| 194 |
+
st.success("✅ PDF generated successfully!")
|
| 195 |
+
|
| 196 |
+
except Exception as e:
|
| 197 |
+
st.error(f"❌ PDF generation error: {str(e)}")
|
| 198 |
+
|
| 199 |
+
# Tab 4: Guidelines Browse
|
| 200 |
+
with tab4:
|
| 201 |
+
st.markdown("### Browse Medical Guidelines")
|
| 202 |
+
|
| 203 |
+
if st.button("📚 Load Available Guidelines", key="guidelines_button"):
|
| 204 |
+
with st.spinner("Loading guidelines..."):
|
| 205 |
+
try:
|
| 206 |
+
if os.path.exists(DOCS_DIR):
|
| 207 |
+
guidelines = []
|
| 208 |
+
for root, dirs, files in os.walk(DOCS_DIR):
|
| 209 |
+
for file in files:
|
| 210 |
+
if file.endswith(('.txt', '.md')):
|
| 211 |
+
guidelines.append(os.path.join(root, file))
|
| 212 |
+
|
| 213 |
+
if guidelines:
|
| 214 |
+
st.info(f"Found {len(guidelines)} guidelines")
|
| 215 |
+
for guideline in sorted(guidelines)[:10]: # Show first 10
|
| 216 |
+
st.caption(f"📄 {os.path.basename(guideline)}")
|
| 217 |
+
else:
|
| 218 |
+
st.warning("No guidelines found. Upload files to `data/guidelines/`")
|
| 219 |
+
else:
|
| 220 |
+
st.error(f"Guidelines directory not found: {DOCS_DIR}")
|
| 221 |
+
except Exception as e:
|
| 222 |
+
st.error(f"❌ Error loading guidelines: {str(e)}")
|
| 223 |
+
|
| 224 |
+
st.divider()
|
| 225 |
+
|
| 226 |
+
# Footer
|
| 227 |
+
st.markdown("""
|
| 228 |
+
---
|
| 229 |
+
**Disclaimer:** MediAssist is a decision support tool and does not replace professional clinical judgment.
|
| 230 |
+
Always consult with qualified healthcare professionals for medical decisions.
|
| 231 |
+
|
| 232 |
+
Made with ❤️ by the MediAssist Team | Deployed on 🤗 Hugging Face Spaces
|
| 233 |
+
""")
|
backend/__pycache__/rag_engine.cpython-314.pyc
ADDED
|
Binary file (4.57 kB). View file
|
|
|
backend/chat_endpoint.py
CHANGED
|
@@ -1,53 +1,53 @@
|
|
| 1 |
-
import os, requests, json, time
|
| 2 |
-
from utils.constants import CHAT_ENDPOINT_DEFAULT, REQUEST_TIMEOUT_SECONDS_DEFAULT, RETRIES_DEFAULT, BACKOFF_SECONDS_DEFAULT
|
| 3 |
-
from utils.persona import AI_GYNO_PERSONA_V2
|
| 4 |
-
|
| 5 |
-
def active_chat_endpoint():
|
| 6 |
-
return os.getenv("HF_CHAT_ENDPOINT") or os.getenv("CHAT_ENDPOINT") or CHAT_ENDPOINT_DEFAULT
|
| 7 |
-
|
| 8 |
-
def _headers():
|
| 9 |
-
tok = os.getenv("HF_API_TOKEN")
|
| 10 |
-
return {"Authorization": f"Bearer {tok}","Content-Type":"application/json"} if tok else {}
|
| 11 |
-
|
| 12 |
-
def chat(user_message: str, mode: str = "patient"):
|
| 13 |
-
url = active_chat_endpoint()
|
| 14 |
-
headers = _headers()
|
| 15 |
-
if not headers:
|
| 16 |
-
return "⚠ Add HF_API_TOKEN in Settings → Secrets."
|
| 17 |
-
|
| 18 |
-
system = AI_GYNO_PERSONA_V2 + ("\nUse simple, supportive language." if mode=='patient' else "\nProvide differentials, initial workup, and red flags.")
|
| 19 |
-
prompt = f"""{system}
|
| 20 |
-
|
| 21 |
-
Patient narrative:
|
| 22 |
-
{user_message}
|
| 23 |
-
|
| 24 |
-
Assistant:"""
|
| 25 |
-
|
| 26 |
-
payload = {
|
| 27 |
-
"inputs": prompt,
|
| 28 |
-
"parameters": {"max_new_tokens": 400, "temperature": 0.2, "return_full_text": False}
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
for attempt in range(1, RETRIES_DEFAULT+1):
|
| 32 |
-
try:
|
| 33 |
-
r = requests.post(url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT_SECONDS_DEFAULT)
|
| 34 |
-
text = r.text
|
| 35 |
-
try:
|
| 36 |
-
data = r.json()
|
| 37 |
-
if isinstance(data, list) and data and "generated_text" in data[0]:
|
| 38 |
-
return data[0]["generated_text"]
|
| 39 |
-
if isinstance(data, dict) and "generated_text" in data:
|
| 40 |
-
return data["generated_text"]
|
| 41 |
-
if isinstance(data, dict) and "outputs" in data and isinstance(data["outputs"], list) and data["outputs"]:
|
| 42 |
-
gt = data["outputs"][0].get("generated_text")
|
| 43 |
-
if gt: return gt
|
| 44 |
-
return json.dumps(data)[:1500]
|
| 45 |
-
except Exception:
|
| 46 |
-
if "loading" in text.lower():
|
| 47 |
-
time.sleep(BACKOFF_SECONDS_DEFAULT * attempt); continue
|
| 48 |
-
if r.status_code == 404:
|
| 49 |
-
return "❌ 404 from router. Check model path: /hf-inference/text-generation/<MODEL>"
|
| 50 |
-
return f"⚠ Non-JSON response:\n{text[:1000]}"
|
| 51 |
-
except Exception as e:
|
| 52 |
-
time.sleep(BACKOFF_SECONDS_DEFAULT * attempt)
|
| 53 |
-
return "❌ Endpoint unavailable after retries."
|
|
|
|
| 1 |
+
import os, requests, json, time
|
| 2 |
+
from utils.constants import CHAT_ENDPOINT_DEFAULT, REQUEST_TIMEOUT_SECONDS_DEFAULT, RETRIES_DEFAULT, BACKOFF_SECONDS_DEFAULT
|
| 3 |
+
from utils.persona import AI_GYNO_PERSONA_V2
|
| 4 |
+
|
| 5 |
+
def active_chat_endpoint():
|
| 6 |
+
return os.getenv("HF_CHAT_ENDPOINT") or os.getenv("CHAT_ENDPOINT") or CHAT_ENDPOINT_DEFAULT
|
| 7 |
+
|
| 8 |
+
def _headers():
|
| 9 |
+
tok = os.getenv("HF_API_TOKEN")
|
| 10 |
+
return {"Authorization": f"Bearer {tok}","Content-Type":"application/json"} if tok else {}
|
| 11 |
+
|
| 12 |
+
def chat(user_message: str, mode: str = "patient"):
|
| 13 |
+
url = active_chat_endpoint()
|
| 14 |
+
headers = _headers()
|
| 15 |
+
if not headers:
|
| 16 |
+
return "⚠ Add HF_API_TOKEN in Settings → Secrets."
|
| 17 |
+
|
| 18 |
+
system = AI_GYNO_PERSONA_V2 + ("\nUse simple, supportive language." if mode=='patient' else "\nProvide differentials, initial workup, and red flags.")
|
| 19 |
+
prompt = f"""{system}
|
| 20 |
+
|
| 21 |
+
Patient narrative:
|
| 22 |
+
{user_message}
|
| 23 |
+
|
| 24 |
+
Assistant:"""
|
| 25 |
+
|
| 26 |
+
payload = {
|
| 27 |
+
"inputs": prompt,
|
| 28 |
+
"parameters": {"max_new_tokens": 400, "temperature": 0.2, "return_full_text": False}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
for attempt in range(1, RETRIES_DEFAULT+1):
|
| 32 |
+
try:
|
| 33 |
+
r = requests.post(url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT_SECONDS_DEFAULT)
|
| 34 |
+
text = r.text
|
| 35 |
+
try:
|
| 36 |
+
data = r.json()
|
| 37 |
+
if isinstance(data, list) and data and "generated_text" in data[0]:
|
| 38 |
+
return data[0]["generated_text"]
|
| 39 |
+
if isinstance(data, dict) and "generated_text" in data:
|
| 40 |
+
return data["generated_text"]
|
| 41 |
+
if isinstance(data, dict) and "outputs" in data and isinstance(data["outputs"], list) and data["outputs"]:
|
| 42 |
+
gt = data["outputs"][0].get("generated_text")
|
| 43 |
+
if gt: return gt
|
| 44 |
+
return json.dumps(data)[:1500]
|
| 45 |
+
except Exception:
|
| 46 |
+
if "loading" in text.lower():
|
| 47 |
+
time.sleep(BACKOFF_SECONDS_DEFAULT * attempt); continue
|
| 48 |
+
if r.status_code == 404:
|
| 49 |
+
return "❌ 404 from router. Check model path: /hf-inference/text-generation/<MODEL>"
|
| 50 |
+
return f"⚠ Non-JSON response:\n{text[:1000]}"
|
| 51 |
+
except Exception as e:
|
| 52 |
+
time.sleep(BACKOFF_SECONDS_DEFAULT * attempt)
|
| 53 |
+
return "❌ Endpoint unavailable after retries."
|
backend/chat_router.py
CHANGED
|
@@ -1,58 +1,58 @@
|
|
| 1 |
-
# -------------------------------
|
| 2 |
-
# 👇 HuggingFace OpenAI-Compatible Client
|
| 3 |
-
# -------------------------------
|
| 4 |
-
from openai import OpenAI
|
| 5 |
-
import os
|
| 6 |
-
from utils.constants import (
|
| 7 |
-
ROUTER_CHAT_URL,
|
| 8 |
-
ROUTER_MODEL,
|
| 9 |
-
REQUEST_TIMEOUT_SECONDS_DEFAULT,
|
| 10 |
-
)
|
| 11 |
-
from utils.persona import AI_GYNO_PERSONA_V3
|
| 12 |
-
|
| 13 |
-
# Force correct model name
|
| 14 |
-
ROUTER_MODEL = "meta-llama/Llama-3.1-8B-Instruct"
|
| 15 |
-
|
| 16 |
-
# HF Token
|
| 17 |
-
token = os.getenv("HF_API_TOKEN") or os.getenv("HF_TOKEN")
|
| 18 |
-
|
| 19 |
-
# HF Router client
|
| 20 |
-
client = OpenAI(
|
| 21 |
-
base_url="https://router.huggingface.co/v1",
|
| 22 |
-
api_key=token,
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def chat(user_message: str, mode: str = "patient") -> str:
|
| 27 |
-
"""
|
| 28 |
-
Uses HuggingFace Router with OpenAI SDK to get chat completions.
|
| 29 |
-
"""
|
| 30 |
-
|
| 31 |
-
if not token:
|
| 32 |
-
return "⚠ Set HF_API_TOKEN or HF_TOKEN in your environment."
|
| 33 |
-
|
| 34 |
-
# Patient-friendly language vs Clinical doctor mode
|
| 35 |
-
style = (
|
| 36 |
-
"Use simple, reassuring language."
|
| 37 |
-
if mode == "patient"
|
| 38 |
-
else "Use concise clinical phrasing with differentials and next steps."
|
| 39 |
-
)
|
| 40 |
-
|
| 41 |
-
system_prompt = AI_GYNO_PERSONA_V3 + f"\nMode: {mode}. {style}"
|
| 42 |
-
|
| 43 |
-
try:
|
| 44 |
-
completion = client.chat.completions.create(
|
| 45 |
-
model=ROUTER_MODEL,
|
| 46 |
-
messages=[
|
| 47 |
-
{"role": "system", "content": system_prompt},
|
| 48 |
-
{"role": "user", "content": user_message},
|
| 49 |
-
],
|
| 50 |
-
max_tokens=400,
|
| 51 |
-
temperature=0.2,
|
| 52 |
-
timeout=REQUEST_TIMEOUT_SECONDS_DEFAULT,
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
return completion.choices[0].message.content.strip()
|
| 56 |
-
|
| 57 |
-
except Exception as e:
|
| 58 |
-
return f"❌ Error: {str(e)}"
|
|
|
|
| 1 |
+
# -------------------------------
|
| 2 |
+
# 👇 HuggingFace OpenAI-Compatible Client
|
| 3 |
+
# -------------------------------
|
| 4 |
+
from openai import OpenAI
|
| 5 |
+
import os
|
| 6 |
+
from utils.constants import (
|
| 7 |
+
ROUTER_CHAT_URL,
|
| 8 |
+
ROUTER_MODEL,
|
| 9 |
+
REQUEST_TIMEOUT_SECONDS_DEFAULT,
|
| 10 |
+
)
|
| 11 |
+
from utils.persona import AI_GYNO_PERSONA_V3
|
| 12 |
+
|
| 13 |
+
# Force correct model name
|
| 14 |
+
ROUTER_MODEL = "meta-llama/Llama-3.1-8B-Instruct"
|
| 15 |
+
|
| 16 |
+
# HF Token
|
| 17 |
+
token = os.getenv("HF_API_TOKEN") or os.getenv("HF_TOKEN")
|
| 18 |
+
|
| 19 |
+
# HF Router client
|
| 20 |
+
client = OpenAI(
|
| 21 |
+
base_url="https://router.huggingface.co/v1",
|
| 22 |
+
api_key=token,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def chat(user_message: str, mode: str = "patient") -> str:
|
| 27 |
+
"""
|
| 28 |
+
Uses HuggingFace Router with OpenAI SDK to get chat completions.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
if not token:
|
| 32 |
+
return "⚠ Set HF_API_TOKEN or HF_TOKEN in your environment."
|
| 33 |
+
|
| 34 |
+
# Patient-friendly language vs Clinical doctor mode
|
| 35 |
+
style = (
|
| 36 |
+
"Use simple, reassuring language."
|
| 37 |
+
if mode == "patient"
|
| 38 |
+
else "Use concise clinical phrasing with differentials and next steps."
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
system_prompt = AI_GYNO_PERSONA_V3 + f"\nMode: {mode}. {style}"
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
completion = client.chat.completions.create(
|
| 45 |
+
model=ROUTER_MODEL,
|
| 46 |
+
messages=[
|
| 47 |
+
{"role": "system", "content": system_prompt},
|
| 48 |
+
{"role": "user", "content": user_message},
|
| 49 |
+
],
|
| 50 |
+
max_tokens=400,
|
| 51 |
+
temperature=0.2,
|
| 52 |
+
timeout=REQUEST_TIMEOUT_SECONDS_DEFAULT,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
return completion.choices[0].message.content.strip()
|
| 56 |
+
|
| 57 |
+
except Exception as e:
|
| 58 |
+
return f"❌ Error: {str(e)}"
|
backend/chat_textgen.py
CHANGED
|
@@ -1,58 +1,58 @@
|
|
| 1 |
-
import os, requests, json, time
|
| 2 |
-
from utils.constants import ROUTER_TEXTGEN_URL, REQUEST_TIMEOUT_SECONDS_DEFAULT, RETRIES_DEFAULT, BACKOFF_SECONDS_DEFAULT
|
| 3 |
-
from utils.persona import AI_GYNO_PERSONA_V4
|
| 4 |
-
|
| 5 |
-
def _headers():
|
| 6 |
-
tok = os.getenv("HF_API_TOKEN") or os.getenv("HF_TOKEN")
|
| 7 |
-
return {"Authorization": f"Bearer {tok}", "Content-Type": "application/json"} if tok else {}
|
| 8 |
-
|
| 9 |
-
def chat(user_message: str, mode: str = "patient") -> str:
|
| 10 |
-
url = os.getenv("HF_CHAT_ENDPOINT") or ROUTER_TEXTGEN_URL
|
| 11 |
-
headers = _headers()
|
| 12 |
-
if not headers:
|
| 13 |
-
return "⚠ Set HF_API_TOKEN (or HF_TOKEN) in Secrets."
|
| 14 |
-
|
| 15 |
-
style = "Explain simply and reassure." if mode=="patient" else "Use clinical phrasing, list differentials and next steps."
|
| 16 |
-
prompt = f"""{AI_GYNO_PERSONA_V4}
|
| 17 |
-
|
| 18 |
-
Patient message:
|
| 19 |
-
{user_message}
|
| 20 |
-
|
| 21 |
-
Mode: {mode}
|
| 22 |
-
Instructions:
|
| 23 |
-
- {style}
|
| 24 |
-
- Provide likely causes, initial tests, red-flags (2-5), and 2-3 next steps.
|
| 25 |
-
- If missing key info, ask 2 follow-up questions.
|
| 26 |
-
- Keep response concise (<= 250 words).
|
| 27 |
-
"""
|
| 28 |
-
|
| 29 |
-
payload = {
|
| 30 |
-
"inputs": prompt,
|
| 31 |
-
"parameters": {"max_new_tokens": 400, "temperature": 0.2, "return_full_text": False}
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
for attempt in range(1, RETRIES_DEFAULT+1):
|
| 35 |
-
try:
|
| 36 |
-
r = requests.post(url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT_SECONDS_DEFAULT)
|
| 37 |
-
txt = r.text
|
| 38 |
-
# detect HTML / loading
|
| 39 |
-
if "<html" in txt.lower():
|
| 40 |
-
if "loading" in txt.lower():
|
| 41 |
-
time.sleep(BACKOFF_SECONDS_DEFAULT * attempt); continue
|
| 42 |
-
return f"⚠ Non-JSON HTML response: {txt[:500]}"
|
| 43 |
-
try:
|
| 44 |
-
data = r.json()
|
| 45 |
-
except Exception:
|
| 46 |
-
return txt[:2000]
|
| 47 |
-
# Text generation style
|
| 48 |
-
if isinstance(data, list) and data and "generated_text" in data[0]:
|
| 49 |
-
return data[0]["generated_text"]
|
| 50 |
-
if isinstance(data, dict) and "generated_text" in data:
|
| 51 |
-
return data["generated_text"]
|
| 52 |
-
# Some routers return {'text': '...'} or similar
|
| 53 |
-
if isinstance(data, dict) and "text" in data:
|
| 54 |
-
return data["text"]
|
| 55 |
-
return "⚠ Unexpected response: " + json.dumps(data)[:1200]
|
| 56 |
-
except Exception as e:
|
| 57 |
-
time.sleep(BACKOFF_SECONDS_DEFAULT * attempt)
|
| 58 |
-
return "❌ Endpoint unreachable after retries."
|
|
|
|
| 1 |
+
import os, requests, json, time
|
| 2 |
+
from utils.constants import ROUTER_TEXTGEN_URL, REQUEST_TIMEOUT_SECONDS_DEFAULT, RETRIES_DEFAULT, BACKOFF_SECONDS_DEFAULT
|
| 3 |
+
from utils.persona import AI_GYNO_PERSONA_V4
|
| 4 |
+
|
| 5 |
+
def _headers():
|
| 6 |
+
tok = os.getenv("HF_API_TOKEN") or os.getenv("HF_TOKEN")
|
| 7 |
+
return {"Authorization": f"Bearer {tok}", "Content-Type": "application/json"} if tok else {}
|
| 8 |
+
|
| 9 |
+
def chat(user_message: str, mode: str = "patient") -> str:
|
| 10 |
+
url = os.getenv("HF_CHAT_ENDPOINT") or ROUTER_TEXTGEN_URL
|
| 11 |
+
headers = _headers()
|
| 12 |
+
if not headers:
|
| 13 |
+
return "⚠ Set HF_API_TOKEN (or HF_TOKEN) in Secrets."
|
| 14 |
+
|
| 15 |
+
style = "Explain simply and reassure." if mode=="patient" else "Use clinical phrasing, list differentials and next steps."
|
| 16 |
+
prompt = f"""{AI_GYNO_PERSONA_V4}
|
| 17 |
+
|
| 18 |
+
Patient message:
|
| 19 |
+
{user_message}
|
| 20 |
+
|
| 21 |
+
Mode: {mode}
|
| 22 |
+
Instructions:
|
| 23 |
+
- {style}
|
| 24 |
+
- Provide likely causes, initial tests, red-flags (2-5), and 2-3 next steps.
|
| 25 |
+
- If missing key info, ask 2 follow-up questions.
|
| 26 |
+
- Keep response concise (<= 250 words).
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
payload = {
|
| 30 |
+
"inputs": prompt,
|
| 31 |
+
"parameters": {"max_new_tokens": 400, "temperature": 0.2, "return_full_text": False}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
for attempt in range(1, RETRIES_DEFAULT+1):
|
| 35 |
+
try:
|
| 36 |
+
r = requests.post(url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT_SECONDS_DEFAULT)
|
| 37 |
+
txt = r.text
|
| 38 |
+
# detect HTML / loading
|
| 39 |
+
if "<html" in txt.lower():
|
| 40 |
+
if "loading" in txt.lower():
|
| 41 |
+
time.sleep(BACKOFF_SECONDS_DEFAULT * attempt); continue
|
| 42 |
+
return f"⚠ Non-JSON HTML response: {txt[:500]}"
|
| 43 |
+
try:
|
| 44 |
+
data = r.json()
|
| 45 |
+
except Exception:
|
| 46 |
+
return txt[:2000]
|
| 47 |
+
# Text generation style
|
| 48 |
+
if isinstance(data, list) and data and "generated_text" in data[0]:
|
| 49 |
+
return data[0]["generated_text"]
|
| 50 |
+
if isinstance(data, dict) and "generated_text" in data:
|
| 51 |
+
return data["generated_text"]
|
| 52 |
+
# Some routers return {'text': '...'} or similar
|
| 53 |
+
if isinstance(data, dict) and "text" in data:
|
| 54 |
+
return data["text"]
|
| 55 |
+
return "⚠ Unexpected response: " + json.dumps(data)[:1200]
|
| 56 |
+
except Exception as e:
|
| 57 |
+
time.sleep(BACKOFF_SECONDS_DEFAULT * attempt)
|
| 58 |
+
return "❌ Endpoint unreachable after retries."
|
backend/endpoint_client.py
CHANGED
|
@@ -1,26 +1,26 @@
|
|
| 1 |
-
|
| 2 |
-
import os,time,requests,json
|
| 3 |
-
from utils.constants import CHAT_ENDPOINT,MAX_RETRIES_DEFAULT,RETRY_BACKOFF_SECONDS_DEFAULT,REQUEST_TIMEOUT_SECONDS_DEFAULT
|
| 4 |
-
|
| 5 |
-
def call_endpoint(prompt,endpoint=None,token=None,max_retries=None,backoff=None,timeout=None,logs=None):
|
| 6 |
-
url=endpoint or os.getenv("CHAT_ENDPOINT") or CHAT_ENDPOINT
|
| 7 |
-
tok=token or os.getenv("HF_API_TOKEN")
|
| 8 |
-
if not tok:return "❌ Missing HF_API_TOKEN.",{}
|
| 9 |
-
mr=max_retries or MAX_RETRIES_DEFAULT
|
| 10 |
-
bf=backoff or RETRY_BACKOFF_SECONDS_DEFAULT
|
| 11 |
-
to=timeout or REQUEST_TIMEOUT_SECONDS_DEFAULT
|
| 12 |
-
|
| 13 |
-
h={"Authorization":f"Bearer {tok}","Content-Type":"application/json"}
|
| 14 |
-
for a in range(1,mr+1):
|
| 15 |
-
try:
|
| 16 |
-
r=requests.post(url,headers=h,json={"inputs":prompt},timeout=to)
|
| 17 |
-
try:data=r.json()
|
| 18 |
-
except:return "⚠️ Non-JSON:\n"+r.text,{}
|
| 19 |
-
if isinstance(data,list) and data and "generated_text" in data[0]:
|
| 20 |
-
return data[0]["generated_text"],{}
|
| 21 |
-
if isinstance(data,dict) and "generated_text" in data:
|
| 22 |
-
return data["generated_text"],{}
|
| 23 |
-
return "⚠️ Unexpected:"+json.dumps(data)[:500],{}
|
| 24 |
-
except:
|
| 25 |
-
time.sleep(bf*a)
|
| 26 |
-
return "❌ Endpoint unavailable",{}
|
|
|
|
| 1 |
+
|
| 2 |
+
import os,time,requests,json
|
| 3 |
+
from utils.constants import CHAT_ENDPOINT,MAX_RETRIES_DEFAULT,RETRY_BACKOFF_SECONDS_DEFAULT,REQUEST_TIMEOUT_SECONDS_DEFAULT
|
| 4 |
+
|
| 5 |
+
def call_endpoint(prompt,endpoint=None,token=None,max_retries=None,backoff=None,timeout=None,logs=None):
|
| 6 |
+
url=endpoint or os.getenv("CHAT_ENDPOINT") or CHAT_ENDPOINT
|
| 7 |
+
tok=token or os.getenv("HF_API_TOKEN")
|
| 8 |
+
if not tok:return "❌ Missing HF_API_TOKEN.",{}
|
| 9 |
+
mr=max_retries or MAX_RETRIES_DEFAULT
|
| 10 |
+
bf=backoff or RETRY_BACKOFF_SECONDS_DEFAULT
|
| 11 |
+
to=timeout or REQUEST_TIMEOUT_SECONDS_DEFAULT
|
| 12 |
+
|
| 13 |
+
h={"Authorization":f"Bearer {tok}","Content-Type":"application/json"}
|
| 14 |
+
for a in range(1,mr+1):
|
| 15 |
+
try:
|
| 16 |
+
r=requests.post(url,headers=h,json={"inputs":prompt},timeout=to)
|
| 17 |
+
try:data=r.json()
|
| 18 |
+
except:return "⚠️ Non-JSON:\n"+r.text,{}
|
| 19 |
+
if isinstance(data,list) and data and "generated_text" in data[0]:
|
| 20 |
+
return data[0]["generated_text"],{}
|
| 21 |
+
if isinstance(data,dict) and "generated_text" in data:
|
| 22 |
+
return data["generated_text"],{}
|
| 23 |
+
return "⚠️ Unexpected:"+json.dumps(data)[:500],{}
|
| 24 |
+
except:
|
| 25 |
+
time.sleep(bf*a)
|
| 26 |
+
return "❌ Endpoint unavailable",{}
|
backend/pdf_utils.py
CHANGED
|
@@ -1,21 +1,21 @@
|
|
| 1 |
-
from reportlab.lib.pagesizes import A4
|
| 2 |
-
from reportlab.pdfgen import canvas
|
| 3 |
-
from reportlab.lib.units import mm
|
| 4 |
-
def generate_pdf(path, title, soap, summary):
|
| 5 |
-
c=canvas.Canvas(path,pagesize=A4)
|
| 6 |
-
W,H=A4; x=20*mm; y=H-25*mm
|
| 7 |
-
def p(label,txt):
|
| 8 |
-
nonlocal y
|
| 9 |
-
c.setFont("Helvetica-Bold",12); c.drawString(x,y,label); y-=6*mm
|
| 10 |
-
c.setFont("Helvetica",10)
|
| 11 |
-
for line in txt.split("\n"):
|
| 12 |
-
c.drawString(x,y,line); y-=5*mm
|
| 13 |
-
y-=3*mm
|
| 14 |
-
p("Title",title)
|
| 15 |
-
p("Subjective",soap.get("subjective",""))
|
| 16 |
-
p("Objective",soap.get("objective",""))
|
| 17 |
-
p("Assessment","\n".join(soap.get("assessment",[])))
|
| 18 |
-
p("Plan","\n".join(soap.get("plan",[])))
|
| 19 |
-
p("Red Flags","\n".join(soap.get("red_flags",[])))
|
| 20 |
-
p("Summary",summary or "")
|
| 21 |
-
c.showPage(); c.save()
|
|
|
|
| 1 |
+
from reportlab.lib.pagesizes import A4
|
| 2 |
+
from reportlab.pdfgen import canvas
|
| 3 |
+
from reportlab.lib.units import mm
|
| 4 |
+
def generate_pdf(path, title, soap, summary):
|
| 5 |
+
c=canvas.Canvas(path,pagesize=A4)
|
| 6 |
+
W,H=A4; x=20*mm; y=H-25*mm
|
| 7 |
+
def p(label,txt):
|
| 8 |
+
nonlocal y
|
| 9 |
+
c.setFont("Helvetica-Bold",12); c.drawString(x,y,label); y-=6*mm
|
| 10 |
+
c.setFont("Helvetica",10)
|
| 11 |
+
for line in txt.split("\n"):
|
| 12 |
+
c.drawString(x,y,line); y-=5*mm
|
| 13 |
+
y-=3*mm
|
| 14 |
+
p("Title",title)
|
| 15 |
+
p("Subjective",soap.get("subjective",""))
|
| 16 |
+
p("Objective",soap.get("objective",""))
|
| 17 |
+
p("Assessment","\n".join(soap.get("assessment",[])))
|
| 18 |
+
p("Plan","\n".join(soap.get("plan",[])))
|
| 19 |
+
p("Red Flags","\n".join(soap.get("red_flags",[])))
|
| 20 |
+
p("Summary",summary or "")
|
| 21 |
+
c.showPage(); c.save()
|
backend/rag_engine.py
CHANGED
|
@@ -1,53 +1,53 @@
|
|
| 1 |
-
import os, glob
|
| 2 |
-
import chromadb
|
| 3 |
-
from typing import List
|
| 4 |
-
from sentence_transformers import SentenceTransformer, models
|
| 5 |
-
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 6 |
-
from utils.constants import CHROMA_DIR, DOCS_DIR, COLLECTION, EMB_MODEL_NAME
|
| 7 |
-
from utils.helpers import to_safe_items
|
| 8 |
-
|
| 9 |
-
def get_embedder():
|
| 10 |
-
word = models.Transformer(EMB_MODEL_NAME)
|
| 11 |
-
pooling = models.Pooling(word.get_word_embedding_dimension())
|
| 12 |
-
return SentenceTransformer(modules=[word, pooling])
|
| 13 |
-
|
| 14 |
-
def get_chroma():
|
| 15 |
-
client = chromadb.PersistentClient(path=CHROMA_DIR)
|
| 16 |
-
col = client.get_or_create_collection(COLLECTION, metadata={"hnsw:space":"cosine"})
|
| 17 |
-
return client, col
|
| 18 |
-
|
| 19 |
-
def embed_texts(model, texts: List[str]):
|
| 20 |
-
return model.encode(texts, convert_to_numpy=True).tolist()
|
| 21 |
-
|
| 22 |
-
def seed_index(col, model, root_folder: str) -> int:
|
| 23 |
-
splitter = RecursiveCharacterTextSplitter(chunk_size=1100, chunk_overlap=150)
|
| 24 |
-
paths = []
|
| 25 |
-
for ext in ("**/*.txt","**/*.md"):
|
| 26 |
-
paths += glob.glob(os.path.join(root_folder, ext), recursive=True)
|
| 27 |
-
ids, docs, metas = [], [], []
|
| 28 |
-
for p in sorted(paths):
|
| 29 |
-
title = os.path.splitext(os.path.basename(p))[0]
|
| 30 |
-
with open(p, "r", encoding="utf-8") as f:
|
| 31 |
-
txt = f.read()
|
| 32 |
-
# unique id uses relative path from DOCS_DIR to avoid duplicates across specialties
|
| 33 |
-
rel = os.path.relpath(p, root_folder).replace(os.sep, "_")
|
| 34 |
-
chunks = splitter.split_text(txt)
|
| 35 |
-
for i, ch in enumerate(chunks):
|
| 36 |
-
ids.append(f"{rel}-{i}")
|
| 37 |
-
docs.append(ch)
|
| 38 |
-
metas.append({"title": title, "source": p})
|
| 39 |
-
if not docs:
|
| 40 |
-
return 0
|
| 41 |
-
embs = embed_texts(model, docs)
|
| 42 |
-
try:
|
| 43 |
-
col.add(ids=ids, documents=docs, metadatas=metas, embeddings=embs)
|
| 44 |
-
except Exception:
|
| 45 |
-
try: col.delete(ids=ids)
|
| 46 |
-
except Exception: pass
|
| 47 |
-
col.add(ids=ids, documents=docs, metadatas=metas, embeddings=embs)
|
| 48 |
-
return len(docs)
|
| 49 |
-
|
| 50 |
-
def retrieve(col, model, query: str, k: int = 5):
|
| 51 |
-
q_emb = embed_texts(model, [query])[0]
|
| 52 |
-
res = col.query(query_embeddings=[q_emb], n_results=k, include=["documents","metadatas","distances"])
|
| 53 |
-
return to_safe_items(res)
|
|
|
|
| 1 |
+
import os, glob
|
| 2 |
+
import chromadb
|
| 3 |
+
from typing import List
|
| 4 |
+
from sentence_transformers import SentenceTransformer, models
|
| 5 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 6 |
+
from utils.constants import CHROMA_DIR, DOCS_DIR, COLLECTION, EMB_MODEL_NAME
|
| 7 |
+
from utils.helpers import to_safe_items
|
| 8 |
+
|
| 9 |
+
def get_embedder():
|
| 10 |
+
word = models.Transformer(EMB_MODEL_NAME)
|
| 11 |
+
pooling = models.Pooling(word.get_word_embedding_dimension())
|
| 12 |
+
return SentenceTransformer(modules=[word, pooling])
|
| 13 |
+
|
| 14 |
+
def get_chroma():
|
| 15 |
+
client = chromadb.PersistentClient(path=CHROMA_DIR)
|
| 16 |
+
col = client.get_or_create_collection(COLLECTION, metadata={"hnsw:space":"cosine"})
|
| 17 |
+
return client, col
|
| 18 |
+
|
| 19 |
+
def embed_texts(model, texts: List[str]):
|
| 20 |
+
return model.encode(texts, convert_to_numpy=True).tolist()
|
| 21 |
+
|
| 22 |
+
def seed_index(col, model, root_folder: str) -> int:
|
| 23 |
+
splitter = RecursiveCharacterTextSplitter(chunk_size=1100, chunk_overlap=150)
|
| 24 |
+
paths = []
|
| 25 |
+
for ext in ("**/*.txt","**/*.md"):
|
| 26 |
+
paths += glob.glob(os.path.join(root_folder, ext), recursive=True)
|
| 27 |
+
ids, docs, metas = [], [], []
|
| 28 |
+
for p in sorted(paths):
|
| 29 |
+
title = os.path.splitext(os.path.basename(p))[0]
|
| 30 |
+
with open(p, "r", encoding="utf-8") as f:
|
| 31 |
+
txt = f.read()
|
| 32 |
+
# unique id uses relative path from DOCS_DIR to avoid duplicates across specialties
|
| 33 |
+
rel = os.path.relpath(p, root_folder).replace(os.sep, "_")
|
| 34 |
+
chunks = splitter.split_text(txt)
|
| 35 |
+
for i, ch in enumerate(chunks):
|
| 36 |
+
ids.append(f"{rel}-{i}")
|
| 37 |
+
docs.append(ch)
|
| 38 |
+
metas.append({"title": title, "source": p})
|
| 39 |
+
if not docs:
|
| 40 |
+
return 0
|
| 41 |
+
embs = embed_texts(model, docs)
|
| 42 |
+
try:
|
| 43 |
+
col.add(ids=ids, documents=docs, metadatas=metas, embeddings=embs)
|
| 44 |
+
except Exception:
|
| 45 |
+
try: col.delete(ids=ids)
|
| 46 |
+
except Exception: pass
|
| 47 |
+
col.add(ids=ids, documents=docs, metadatas=metas, embeddings=embs)
|
| 48 |
+
return len(docs)
|
| 49 |
+
|
| 50 |
+
def retrieve(col, model, query: str, k: int = 5):
|
| 51 |
+
q_emb = embed_texts(model, [query])[0]
|
| 52 |
+
res = col.query(query_embeddings=[q_emb], n_results=k, include=["documents","metadatas","distances"])
|
| 53 |
+
return to_safe_items(res)
|
backend/soap_generator.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
-
from typing import List, Dict
|
| 2 |
-
COMMON_RED_FLAGS = ["Severe pain","Syncope","Heavy bleeding","Chest pain/SOB","Persistent fever"]
|
| 3 |
-
def compose_soap(narrative: str, retrieved: List[Dict]) -> Dict:
|
| 4 |
-
text = (narrative or "").lower()
|
| 5 |
-
assessment, plan = [], ["Safety-net advice.","Follow-up in 3–7 days."]
|
| 6 |
-
if any(k in text for k in ["bleed","period","aub","spotting"]):
|
| 7 |
-
assessment.append("Abnormal uterine bleeding — consider structural vs hormonal causes.")
|
| 8 |
-
plan.append("Pregnancy test if indicated; CBC; consider pelvic ultrasound.")
|
| 9 |
-
if any(k in text for k in ["pelvic pain","cramp","lower abdominal pain"]):
|
| 10 |
-
assessment.append("Pelvic pain — consider dysmenorrhea, ovarian cyst, endometriosis, or infection.")
|
| 11 |
-
plan.append("Trial NSAIDs; pelvic exam and ultrasound if persistent or severe.")
|
| 12 |
-
if not assessment:
|
| 13 |
-
assessment.append("Non-specific gynae symptoms — conservative management and targeted testing.")
|
| 14 |
-
citations = [it.get("title","") for it in retrieved]
|
| 15 |
-
return {"subjective": narrative,"objective":"Vitals stable","assessment":assessment,"plan":plan,"red_flags":COMMON_RED_FLAGS,"follow_up":"3–7 days","citations":citations}
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
COMMON_RED_FLAGS = ["Severe pain","Syncope","Heavy bleeding","Chest pain/SOB","Persistent fever"]
|
| 3 |
+
def compose_soap(narrative: str, retrieved: List[Dict]) -> Dict:
|
| 4 |
+
text = (narrative or "").lower()
|
| 5 |
+
assessment, plan = [], ["Safety-net advice.","Follow-up in 3–7 days."]
|
| 6 |
+
if any(k in text for k in ["bleed","period","aub","spotting"]):
|
| 7 |
+
assessment.append("Abnormal uterine bleeding — consider structural vs hormonal causes.")
|
| 8 |
+
plan.append("Pregnancy test if indicated; CBC; consider pelvic ultrasound.")
|
| 9 |
+
if any(k in text for k in ["pelvic pain","cramp","lower abdominal pain"]):
|
| 10 |
+
assessment.append("Pelvic pain — consider dysmenorrhea, ovarian cyst, endometriosis, or infection.")
|
| 11 |
+
plan.append("Trial NSAIDs; pelvic exam and ultrasound if persistent or severe.")
|
| 12 |
+
if not assessment:
|
| 13 |
+
assessment.append("Non-specific gynae symptoms — conservative management and targeted testing.")
|
| 14 |
+
citations = [it.get("title","") for it in retrieved]
|
| 15 |
+
return {"subjective": narrative,"objective":"Vitals stable","assessment":assessment,"plan":plan,"red_flags":COMMON_RED_FLAGS,"follow_up":"3–7 days","citations":citations}
|
pages/1_Diagnostics.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
-
import streamlit as st, os, requests, json
|
| 2 |
-
st.set_page_config(page_title="Router Diagnostics", page_icon="🧪", layout="wide")
|
| 3 |
-
st.title("🔎 Router Text-Generation Diagnostics")
|
| 4 |
-
endpoint = st.text_input("Endpoint", os.getenv("HF_CHAT_ENDPOINT") or "https://router.huggingface.co/openai/gpt-oss-120b")
|
| 5 |
-
token = st.text_input("HF API Token (optional; else use env)", os.getenv("HF_API_TOKEN") or os.getenv("HF_TOKEN") or "", type="password")
|
| 6 |
-
prompt = st.text_area("Prompt", "ping")
|
| 7 |
-
if st.button("Ping"):
|
| 8 |
-
try:
|
| 9 |
-
r = requests.post(endpoint, headers={"Authorization": f"Bearer {token or os.getenv('HF_API_TOKEN') or os.getenv('HF_TOKEN')}","Content-Type":"application/json"}, json={"inputs": prompt, "parameters": {"max_new_tokens": 64}}, timeout=60)
|
| 10 |
-
st.subheader("Status / headers")
|
| 11 |
-
st.code(f"{r.status_code}\n{dict(r.headers)}")
|
| 12 |
-
try:
|
| 13 |
-
st.subheader("JSON")
|
| 14 |
-
st.json(r.json())
|
| 15 |
-
except Exception:
|
| 16 |
-
st.subheader("Raw text")
|
| 17 |
-
st.code(r.text[:2000])
|
| 18 |
-
except Exception as e:
|
| 19 |
-
st.error(e)
|
|
|
|
| 1 |
+
import streamlit as st, os, requests, json
|
| 2 |
+
st.set_page_config(page_title="Router Diagnostics", page_icon="🧪", layout="wide")
|
| 3 |
+
st.title("🔎 Router Text-Generation Diagnostics")
|
| 4 |
+
endpoint = st.text_input("Endpoint", os.getenv("HF_CHAT_ENDPOINT") or "https://router.huggingface.co/openai/gpt-oss-120b")
|
| 5 |
+
token = st.text_input("HF API Token (optional; else use env)", os.getenv("HF_API_TOKEN") or os.getenv("HF_TOKEN") or "", type="password")
|
| 6 |
+
prompt = st.text_area("Prompt", "ping")
|
| 7 |
+
if st.button("Ping"):
|
| 8 |
+
try:
|
| 9 |
+
r = requests.post(endpoint, headers={"Authorization": f"Bearer {token or os.getenv('HF_API_TOKEN') or os.getenv('HF_TOKEN')}","Content-Type":"application/json"}, json={"inputs": prompt, "parameters": {"max_new_tokens": 64}}, timeout=60)
|
| 10 |
+
st.subheader("Status / headers")
|
| 11 |
+
st.code(f"{r.status_code}\n{dict(r.headers)}")
|
| 12 |
+
try:
|
| 13 |
+
st.subheader("JSON")
|
| 14 |
+
st.json(r.json())
|
| 15 |
+
except Exception:
|
| 16 |
+
st.subheader("Raw text")
|
| 17 |
+
st.code(r.text[:2000])
|
| 18 |
+
except Exception as e:
|
| 19 |
+
st.error(e)
|
pages/1_How_It_Works.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
st.set_page_config(page_title="How MedicalAI Works", page_icon="🧠", layout="wide")
|
| 3 |
+
st.title("🧠 How MedicalAI Works — RAG + ClinicalBERT + Llama 3.2")
|
| 4 |
+
st.markdown("""**Pipeline**
|
| 5 |
+
1) ClinicalBERT creates embeddings of guideline chunks and the patient's message.
|
| 6 |
+
2) ChromaDB retrieves the most similar clinical chunks.
|
| 7 |
+
3) The retrieved context is provided to an instruction LLM (**Llama-3.2-1B-Instruct**) to generate a safe, empathetic, explainable reply.
|
| 8 |
+
4) We also compose a structured SOAP draft (offline rules) and show citations for transparency.""")
|
pages/2_What_Can_Be_Done.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
st.set_page_config(page_title="What MedicalAI Can Do", page_icon="🚀", layout="wide")
|
| 4 |
+
|
| 5 |
+
st.title("🚀 What MedicalAI Can Do — Capabilities, Use-Cases & Roadmap")
|
| 6 |
+
st.caption("A deep exploration of the present capabilities of the MedicalAI PoC and everything it can evolve into.")
|
| 7 |
+
|
| 8 |
+
# INTRO
|
| 9 |
+
st.markdown("""
|
| 10 |
+
The MedicalAI system combines **ClinicalBERT**, **RAG retrieval**, **Llama-3.2 chat reasoning**, and a **doctor-friendly UI**.
|
| 11 |
+
This PoC already demonstrates several powerful functions.
|
| 12 |
+
But more importantly, it opens the pathway to a **scalable, real-world clinical assistant**.
|
| 13 |
+
|
| 14 |
+
Below is a detailed breakdown of **what MedicalAI can do today** and **what it can become tomorrow**.
|
| 15 |
+
""")
|
| 16 |
+
|
| 17 |
+
# SECTION 1 — TODAY
|
| 18 |
+
st.markdown("""
|
| 19 |
+
---
|
| 20 |
+
## 🎯 1. What MedicalAI Can Do Today (PoC Capabilities)
|
| 21 |
+
|
| 22 |
+
Despite being lightweight and CPU-friendly, the system already performs a wide range of clinical support functions:
|
| 23 |
+
|
| 24 |
+
### ✅ 1.1 Understand patient symptoms in natural language
|
| 25 |
+
Users can type:
|
| 26 |
+
- “My period is 10 days late”
|
| 27 |
+
- “Lower abdominal pain for 3 days”
|
| 28 |
+
- “Irregular cycles after stopping OCP”
|
| 29 |
+
|
| 30 |
+
The system understands:
|
| 31 |
+
- Menstrual irregularities
|
| 32 |
+
- Early pregnancy symptoms
|
| 33 |
+
- Pain character
|
| 34 |
+
- Common gynae patterns
|
| 35 |
+
|
| 36 |
+
This is possible due to **ClinicalBERT embeddings** (medical comprehension).
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
### ✅ 1.2 Retrieve relevant clinical guidelines (RAG)
|
| 41 |
+
The system searches through >100 structured guideline files and retrieves information such as:
|
| 42 |
+
- Standard diagnostic pathways
|
| 43 |
+
- Red-flag symptoms
|
| 44 |
+
- Differentials
|
| 45 |
+
- Investigation recommendations
|
| 46 |
+
- Management steps
|
| 47 |
+
|
| 48 |
+
RAG ensures:
|
| 49 |
+
- Fewer hallucinations
|
| 50 |
+
- More transparency
|
| 51 |
+
- Clinically anchored responses
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
### ✅ 1.3 Generate a structured SOAP OPD note
|
| 56 |
+
The system automatically creates:
|
| 57 |
+
- **S**ubjective: Patient complaint
|
| 58 |
+
- **O**bjective: Basic PoC objective section
|
| 59 |
+
- **A**ssessment: Potential causes
|
| 60 |
+
- **P**lan: Next steps, tests, management lines
|
| 61 |
+
|
| 62 |
+
SOAP notes are heavily used by doctors in OPD documentation.
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
### ✅ 1.4 Chat like a medical assistant (Llama 3.2)
|
| 67 |
+
The assistant can respond conversationally:
|
| 68 |
+
- Empathetic tone
|
| 69 |
+
- Clinically safe
|
| 70 |
+
- Asks follow-up questions
|
| 71 |
+
- Mentions red flags
|
| 72 |
+
- Provides reasoning
|
| 73 |
+
|
| 74 |
+
Example output:
|
| 75 |
+
> “A delayed period can be due to pregnancy, stress, or hormonal imbalance.
|
| 76 |
+
> To guide you better, I need to know:
|
| 77 |
+
> – Any spotting?
|
| 78 |
+
> – Nausea?
|
| 79 |
+
> – Recent stress or change in routine?”
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
### ✅ 1.5 Provide citations and explainability
|
| 84 |
+
Every output shows:
|
| 85 |
+
- Document name
|
| 86 |
+
- Source file location
|
| 87 |
+
- Extracted text
|
| 88 |
+
- Why it was selected
|
| 89 |
+
|
| 90 |
+
This builds **trust**, especially for doctors.
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
### ✅ 1.6 Works fully on HuggingFace Spaces (no GPU)
|
| 95 |
+
The system uses:
|
| 96 |
+
- CPU-friendly ClinicalBERT embeddings
|
| 97 |
+
- CPU-friendly Llama-3.2-1B model
|
| 98 |
+
|
| 99 |
+
It can run:
|
| 100 |
+
- On free-tier HuggingFace
|
| 101 |
+
- On low-cost servers
|
| 102 |
+
- On local devices (laptop/Raspberry Pi-class hardware)
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
### 🎉 Summary of current capabilities
|
| 107 |
+
- 🧠 Clinical understanding
|
| 108 |
+
- 📚 Evidence-grounded retrieval
|
| 109 |
+
- 💬 Safe medical conversation
|
| 110 |
+
- 📝 Automatic documentation
|
| 111 |
+
- 🔍 Citations for transparency
|
| 112 |
+
- 🌐 Works entirely offline/CPU
|
| 113 |
+
""")
|
| 114 |
+
|
| 115 |
+
# SECTION 2 — FUTURE
|
| 116 |
+
st.markdown("""
|
| 117 |
+
---
|
| 118 |
+
## 🚀 2. What MedicalAI Can Do Tomorrow (Full Product Vision)
|
| 119 |
+
|
| 120 |
+
This section outlines what MedicalAI can become with time, data, and investment.
|
| 121 |
+
|
| 122 |
+
### 🔮 2.1 Fully AI-assisted OPD workflow
|
| 123 |
+
- Auto-capture symptoms
|
| 124 |
+
- Auto-order relevant basic labs
|
| 125 |
+
- Auto-fill case sheets
|
| 126 |
+
- Auto-generate discharge notes
|
| 127 |
+
- Auto-create follow-up reminders
|
| 128 |
+
|
| 129 |
+
This reduces **OPD processing time by 40–60%**.
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
### 🔮 2.2 Integration with EMR/EHR systems
|
| 134 |
+
Automatic syncing with:
|
| 135 |
+
- Vitals
|
| 136 |
+
- Ultrasound reports
|
| 137 |
+
- Blood test results
|
| 138 |
+
- Past OPD notes
|
| 139 |
+
- Medication history
|
| 140 |
+
|
| 141 |
+
This allows **end-to-end clinical automation**.
|
| 142 |
+
|
| 143 |
+
---
|
| 144 |
+
|
| 145 |
+
### 🔮 2.3 Doctor dashboard for insights
|
| 146 |
+
A visual analytics dashboard showing:
|
| 147 |
+
- Symptom trends
|
| 148 |
+
- High-risk cases
|
| 149 |
+
- Follow-up compliance
|
| 150 |
+
- Diagnosis distribution
|
| 151 |
+
- Prescription patterns
|
| 152 |
+
|
| 153 |
+
This is valuable for:
|
| 154 |
+
- Clinics
|
| 155 |
+
- Hospitals
|
| 156 |
+
- Corporate chains
|
| 157 |
+
- Research teams
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
### 🔮 2.4 Telemedicine triage assistant
|
| 162 |
+
MedicalAI can pre-screen patients **before** they meet the doctor.
|
| 163 |
+
|
| 164 |
+
It can classify urgency into:
|
| 165 |
+
- **🔴 High risk** (act immediately)
|
| 166 |
+
- **🟠 Moderate** (consult same day)
|
| 167 |
+
- **🟢 Routine** (can wait)
|
| 168 |
+
|
| 169 |
+
This reduces doctor load dramatically.
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
### 🔮 2.5 Multilingual support
|
| 174 |
+
- Hindi
|
| 175 |
+
- Bengali
|
| 176 |
+
- Tamil
|
| 177 |
+
- Marathi
|
| 178 |
+
- Urdu
|
| 179 |
+
|
| 180 |
+
This allows massive adoption across India.
|
| 181 |
+
|
| 182 |
+
---
|
| 183 |
+
|
| 184 |
+
### 🔮 2.6 Continual learning with feedback loops
|
| 185 |
+
Doctors can “approve” or “adjust” suggestions.
|
| 186 |
+
Model adapts over time:
|
| 187 |
+
- More accurate
|
| 188 |
+
- More domain-specialized
|
| 189 |
+
- Safer
|
| 190 |
+
- Personalized to clinic flow
|
| 191 |
+
|
| 192 |
+
---
|
| 193 |
+
|
| 194 |
+
### 🔮 2.7 Integration with medical imaging
|
| 195 |
+
Future versions can support:
|
| 196 |
+
- Ultrasound interpretation assistance
|
| 197 |
+
- Endometrial thickness analysis
|
| 198 |
+
- Follicle monitoring
|
| 199 |
+
- Ovarian cyst classification
|
| 200 |
+
|
| 201 |
+
---
|
| 202 |
+
|
| 203 |
+
### 🔮 2.8 Medication guidance with safety filters
|
| 204 |
+
- Drug interactions
|
| 205 |
+
- Pregnancy-safe medications
|
| 206 |
+
- Lactation-safe medications
|
| 207 |
+
- Dosage ranges
|
| 208 |
+
- Contraindications
|
| 209 |
+
|
| 210 |
+
And warnings for:
|
| 211 |
+
- Renal impairment
|
| 212 |
+
- Liver issues
|
| 213 |
+
- Cardiac comorbidities
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
### 🔮 2.9 Printable PDFs and secure sharing
|
| 218 |
+
One-click:
|
| 219 |
+
- OPD print
|
| 220 |
+
- Detailed visit notes
|
| 221 |
+
- Patient education sheets
|
| 222 |
+
|
| 223 |
+
---
|
| 224 |
+
|
| 225 |
+
### 🔮 2.10 HIPAA / NDHM compliant deployment
|
| 226 |
+
MedicalAI can be upgraded to handle:
|
| 227 |
+
- Secure data storage
|
| 228 |
+
- Patient consent
|
| 229 |
+
- Audit trails
|
| 230 |
+
- NDHM-compliant APIs
|
| 231 |
+
|
| 232 |
+
---
|
| 233 |
+
|
| 234 |
+
# Summary — What Can Be Done
|
| 235 |
+
MedicalAI can evolve into:
|
| 236 |
+
- A **virtual junior resident doctor**
|
| 237 |
+
- A **clinical documentation engine**
|
| 238 |
+
- An **AI-powered OPD assistant**
|
| 239 |
+
- A **scalable multi-speciality medical AI platform**
|
| 240 |
+
|
| 241 |
+
""")
|
| 242 |
+
|
| 243 |
+
st.success("This page explains the complete potential of MedicalAI—both what it does today and what it can grow into.")
|
pages/3_How_Doctors_Benefit.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
st.set_page_config(page_title="How Doctors Benefit", page_icon="🩺", layout="wide")
|
| 4 |
+
|
| 5 |
+
st.title("🩺 How Doctors Benefit — Productivity, Safety & Patient Experience")
|
| 6 |
+
st.caption("A detailed explanation of how MedicalAI improves the daily workflow, efficiency, and outcomes for clinicians.")
|
| 7 |
+
|
| 8 |
+
# INTRO
|
| 9 |
+
st.markdown("""
|
| 10 |
+
Gynaecologists face enormous workload pressures:
|
| 11 |
+
- High OPD volume
|
| 12 |
+
- Limited consultation time
|
| 13 |
+
- Manual documentation burden
|
| 14 |
+
- Patients with incomplete histories
|
| 15 |
+
- Rising complexity of guidelines
|
| 16 |
+
- Medicolegal stress
|
| 17 |
+
|
| 18 |
+
MedicalAI acts as a **clinical co-pilot** — supporting doctors before, during, and after each consultation.
|
| 19 |
+
|
| 20 |
+
Here is a deep look into the tangible benefits for doctors.
|
| 21 |
+
""")
|
| 22 |
+
|
| 23 |
+
# SECTION 1 — TIME SAVING
|
| 24 |
+
st.markdown("""
|
| 25 |
+
---
|
| 26 |
+
## ⏱️ 1. Time Savings & OPD Efficiency
|
| 27 |
+
|
| 28 |
+
OPD doctors typically get **3–7 minutes per patient**.
|
| 29 |
+
|
| 30 |
+
MedicalAI helps by:
|
| 31 |
+
- Summarizing patient complaints automatically
|
| 32 |
+
- Suggesting initial assessments
|
| 33 |
+
- Preparing SOAP notes in advance
|
| 34 |
+
- Flagging missing information
|
| 35 |
+
- Generating follow-up plans
|
| 36 |
+
|
| 37 |
+
### Real-time Examples
|
| 38 |
+
**Before AI:**
|
| 39 |
+
Doctor must manually ask, interpret, document.
|
| 40 |
+
|
| 41 |
+
**With MedicalAI:**
|
| 42 |
+
- Patient narration understood instantly
|
| 43 |
+
- AI suggests follow-up questions
|
| 44 |
+
- Doctor gets structured summary
|
| 45 |
+
- Less writing, more interaction
|
| 46 |
+
|
| 47 |
+
This can reduce OPD time per patient by **30–50%**.
|
| 48 |
+
""")
|
| 49 |
+
|
| 50 |
+
# SECTION 2 — SAFETY
|
| 51 |
+
st.markdown("""
|
| 52 |
+
---
|
| 53 |
+
## 🛡️ 2. Increased Clinical Safety & Fewer Missed Red Flags
|
| 54 |
+
|
| 55 |
+
Doctors sometimes miss early warnings because:
|
| 56 |
+
- OPD is crowded
|
| 57 |
+
- Patient gives incomplete history
|
| 58 |
+
- Notes get rushed
|
| 59 |
+
- Time pressure reduces depth of questioning
|
| 60 |
+
|
| 61 |
+
MedicalAI always checks for:
|
| 62 |
+
- Severe unilateral pelvic pain
|
| 63 |
+
- Syncope
|
| 64 |
+
- Heavy bleeding
|
| 65 |
+
- Fever
|
| 66 |
+
- Severe nausea/vomiting
|
| 67 |
+
- Pregnancy concerns
|
| 68 |
+
|
| 69 |
+
If any red flag is even *hinted*, AI prompts the doctor.
|
| 70 |
+
This acts as a **real-time safety net**.
|
| 71 |
+
|
| 72 |
+
Hospitals and clinics benefit from reduced medicolegal risk.
|
| 73 |
+
""")
|
| 74 |
+
|
| 75 |
+
# SECTION 3 — CONSISTENCY
|
| 76 |
+
st.markdown("""
|
| 77 |
+
---
|
| 78 |
+
## 📋 3. Consistent, Standardized Decision Support
|
| 79 |
+
|
| 80 |
+
Doctors vary in:
|
| 81 |
+
- Style
|
| 82 |
+
- Depth of questioning
|
| 83 |
+
- Documentation
|
| 84 |
+
- Clinical approach
|
| 85 |
+
|
| 86 |
+
MedicalAI helps maintain **consistency**:
|
| 87 |
+
- Same baseline guidelines
|
| 88 |
+
- Same red flag detection
|
| 89 |
+
- Same structured SOAP output
|
| 90 |
+
- Same citation-backed reasoning
|
| 91 |
+
|
| 92 |
+
This is especially valuable for:
|
| 93 |
+
- Multi-doctor clinics
|
| 94 |
+
- Hospital chains
|
| 95 |
+
- Franchise clinics
|
| 96 |
+
- Telemedicine centers
|
| 97 |
+
|
| 98 |
+
Standardization improves clinical quality.
|
| 99 |
+
""")
|
| 100 |
+
|
| 101 |
+
# SECTION 4 — DOCUMENTATION
|
| 102 |
+
st.markdown("""
|
| 103 |
+
---
|
| 104 |
+
## 📝 4. Automatic, High-Quality Documentation
|
| 105 |
+
|
| 106 |
+
Documentation is often the **most painful** part of OPD consultations.
|
| 107 |
+
|
| 108 |
+
MedicalAI:
|
| 109 |
+
- Writes SOAP notes
|
| 110 |
+
- Summarizes patient complaints
|
| 111 |
+
- Adds context
|
| 112 |
+
- Highlights key symptoms
|
| 113 |
+
- Prepares follow-up recommendations
|
| 114 |
+
|
| 115 |
+
The doctor only needs to review and approve.
|
| 116 |
+
|
| 117 |
+
This reduces:
|
| 118 |
+
- End-of-day admin burden
|
| 119 |
+
- Fatigue
|
| 120 |
+
- Errors
|
| 121 |
+
- Missed details
|
| 122 |
+
|
| 123 |
+
And increases:
|
| 124 |
+
- Report clarity
|
| 125 |
+
- Readability
|
| 126 |
+
- Legal protection
|
| 127 |
+
""")
|
| 128 |
+
|
| 129 |
+
# SECTION 5 — PATIENT EXPERIENCE
|
| 130 |
+
st.markdown("""
|
| 131 |
+
---
|
| 132 |
+
## 😊 5. Better Patient Experience
|
| 133 |
+
|
| 134 |
+
Patients feel more satisfied when:
|
| 135 |
+
- Doctor listens actively
|
| 136 |
+
- Doctor explains clearly
|
| 137 |
+
- There is confidence in reasoning
|
| 138 |
+
- Red flags and educational points are provided
|
| 139 |
+
|
| 140 |
+
MedicalAI supports this by:
|
| 141 |
+
- Handling routine questioning
|
| 142 |
+
- Giving structured answers
|
| 143 |
+
- Highlighting important points the doctor can reinforce
|
| 144 |
+
- Reducing rushed interactions
|
| 145 |
+
|
| 146 |
+
Patients perceive the doctor as:
|
| 147 |
+
- More attentive
|
| 148 |
+
- More thorough
|
| 149 |
+
- More knowledgeable
|
| 150 |
+
- More caring
|
| 151 |
+
""")
|
| 152 |
+
|
| 153 |
+
# SECTION 6 — MULTILINGUAL
|
| 154 |
+
st.markdown("""
|
| 155 |
+
---
|
| 156 |
+
## 🌐 6. Local Language Support (Future)
|
| 157 |
+
|
| 158 |
+
Doctors across India deal with:
|
| 159 |
+
- Hindi
|
| 160 |
+
- Bengali
|
| 161 |
+
- Punjabi
|
| 162 |
+
- Tamil
|
| 163 |
+
- Telugu
|
| 164 |
+
- Marathi
|
| 165 |
+
- Odia
|
| 166 |
+
|
| 167 |
+
Future versions of MedicalAI will support:
|
| 168 |
+
- Local language patient input
|
| 169 |
+
- Local language output
|
| 170 |
+
- Regional terminology
|
| 171 |
+
|
| 172 |
+
This gives massive adoption potential.
|
| 173 |
+
""")
|
| 174 |
+
|
| 175 |
+
# SECTION 7 — SCALABILITY
|
| 176 |
+
st.markdown("""
|
| 177 |
+
---
|
| 178 |
+
## 📈 7. Scale for Clinics, Chains & Hospitals
|
| 179 |
+
|
| 180 |
+
MedicalAI can scale to:
|
| 181 |
+
- Solo clinicians
|
| 182 |
+
- Small nursing homes
|
| 183 |
+
- Large speciality clinics
|
| 184 |
+
- Hospital OPDs
|
| 185 |
+
- Telemedicine providers
|
| 186 |
+
- Corporate health chains
|
| 187 |
+
|
| 188 |
+
Because it runs on:
|
| 189 |
+
- CPU
|
| 190 |
+
- Low-cost servers
|
| 191 |
+
- HuggingFace Spaces
|
| 192 |
+
- On-premise deployments
|
| 193 |
+
|
| 194 |
+
It can serve **10 to 10,000 doctors** with the same backend architecture.
|
| 195 |
+
""")
|
| 196 |
+
|
| 197 |
+
# SECTION 8 — DECISION SUPPORT
|
| 198 |
+
st.markdown("""
|
| 199 |
+
---
|
| 200 |
+
## 🧠 8. Clinical Decision Support (Future Capabilities)
|
| 201 |
+
|
| 202 |
+
MedicalAI can evolve into a **full CDS engine**, providing:
|
| 203 |
+
- Differential diagnosis suggestions
|
| 204 |
+
- Risk scoring
|
| 205 |
+
- Lab selection guidance
|
| 206 |
+
- Follow-up interval intelligence
|
| 207 |
+
- Investigations checklist
|
| 208 |
+
- Medication screening for pregnancy/lactation safety
|
| 209 |
+
|
| 210 |
+
All evidence-backed.
|
| 211 |
+
|
| 212 |
+
This improves:
|
| 213 |
+
- Diagnostic accuracy
|
| 214 |
+
- Patient outcomes
|
| 215 |
+
- Treatment consistency
|
| 216 |
+
""")
|
| 217 |
+
|
| 218 |
+
# SECTION 9 — DOCTOR MORALE
|
| 219 |
+
st.markdown("""
|
| 220 |
+
---
|
| 221 |
+
## ❤️ 9. Reduced Burnout & Better Work-Life Balance
|
| 222 |
+
|
| 223 |
+
Documentation, repetitive explanations, and heavy OPD load contribute significantly to burnout.
|
| 224 |
+
|
| 225 |
+
MedicalAI:
|
| 226 |
+
- Automates repetitive tasks
|
| 227 |
+
- Reduces cognitive load
|
| 228 |
+
- Prevents decision fatigue
|
| 229 |
+
- Acts as a smart junior resident
|
| 230 |
+
- Saves hours of admin time
|
| 231 |
+
|
| 232 |
+
Doctors can spend more time with:
|
| 233 |
+
- Family
|
| 234 |
+
- Research
|
| 235 |
+
- Complex cases
|
| 236 |
+
- Personal well-being
|
| 237 |
+
""")
|
| 238 |
+
|
| 239 |
+
# END
|
| 240 |
+
st.markdown("---")
|
| 241 |
+
st.success("This page explains in depth how MedicalAI improves doctor workflow, efficiency, safety, and patient satisfaction.")
|
requirements.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
-
streamlit
|
| 2 |
-
chromadb
|
| 3 |
-
sentence-transformers
|
| 4 |
-
langchain-text-splitters
|
| 5 |
-
numpy
|
| 6 |
-
requests
|
| 7 |
-
reportlab
|
|
|
|
| 1 |
+
streamlit
|
| 2 |
+
chromadb
|
| 3 |
+
sentence-transformers
|
| 4 |
+
langchain-text-splitters
|
| 5 |
+
numpy
|
| 6 |
+
requests
|
| 7 |
+
reportlab
|
style.css
CHANGED
|
@@ -1,28 +1,28 @@
|
|
| 1 |
-
body {
|
| 2 |
-
padding: 2rem;
|
| 3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
h1 {
|
| 7 |
-
font-size: 16px;
|
| 8 |
-
margin-top: 0;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
p {
|
| 12 |
-
color: rgb(107, 114, 128);
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
-
}
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
padding: 2rem;
|
| 3 |
+
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
h1 {
|
| 7 |
+
font-size: 16px;
|
| 8 |
+
margin-top: 0;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
p {
|
| 12 |
+
color: rgb(107, 114, 128);
|
| 13 |
+
font-size: 15px;
|
| 14 |
+
margin-bottom: 10px;
|
| 15 |
+
margin-top: 5px;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
.card {
|
| 19 |
+
max-width: 620px;
|
| 20 |
+
margin: 0 auto;
|
| 21 |
+
padding: 16px;
|
| 22 |
+
border: 1px solid lightgray;
|
| 23 |
+
border-radius: 16px;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.card p:last-child {
|
| 27 |
+
margin-bottom: 0;
|
| 28 |
+
}
|
utils/constants.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
CHROMA_DIR = "./data/chroma"
|
| 2 |
-
DOCS_DIR = "./data/guidelines"
|
| 3 |
-
COLLECTION = "med_guidelines_multispeciality_v15"
|
| 4 |
-
EMB_MODEL_NAME = "medicalai/ClinicalBERT"
|
| 5 |
-
RETRIEVAL_K_DEFAULT = 5
|
| 6 |
-
|
| 7 |
-
# Router Text-Generation endpoint (using HF Router)
|
| 8 |
-
# Default: openai/gpt-oss-120b (text-generation route)
|
| 9 |
-
ROUTER_TEXTGEN_URL = "https://router.huggingface.co/openai/gpt-oss-120b"
|
| 10 |
-
|
| 11 |
-
REQUEST_TIMEOUT_SECONDS_DEFAULT = 60
|
| 12 |
-
RETRIES_DEFAULT = 6
|
| 13 |
-
BACKOFF_SECONDS_DEFAULT = 2
|
|
|
|
| 1 |
+
CHROMA_DIR = "./data/chroma"
|
| 2 |
+
DOCS_DIR = "./data/guidelines"
|
| 3 |
+
COLLECTION = "med_guidelines_multispeciality_v15"
|
| 4 |
+
EMB_MODEL_NAME = "medicalai/ClinicalBERT"
|
| 5 |
+
RETRIEVAL_K_DEFAULT = 5
|
| 6 |
+
|
| 7 |
+
# Router Text-Generation endpoint (using HF Router)
|
| 8 |
+
# Default: openai/gpt-oss-120b (text-generation route)
|
| 9 |
+
ROUTER_TEXTGEN_URL = "https://router.huggingface.co/openai/gpt-oss-120b"
|
| 10 |
+
|
| 11 |
+
REQUEST_TIMEOUT_SECONDS_DEFAULT = 60
|
| 12 |
+
RETRIES_DEFAULT = 6
|
| 13 |
+
BACKOFF_SECONDS_DEFAULT = 2
|
utils/helpers.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
from typing import List, Dict
|
| 2 |
-
def to_safe_items(res) -> List[Dict]:
|
| 3 |
-
items = []
|
| 4 |
-
if res and res.get("ids"):
|
| 5 |
-
for i in range(len(res["ids"][0])):
|
| 6 |
-
items.append({
|
| 7 |
-
"text": res["documents"][0][i],
|
| 8 |
-
"title": res["metadatas"][0][i].get("title","(untitled)"),
|
| 9 |
-
"source": res["metadatas"][0][i].get("source","")
|
| 10 |
-
})
|
| 11 |
-
return items
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
def to_safe_items(res) -> List[Dict]:
|
| 3 |
+
items = []
|
| 4 |
+
if res and res.get("ids"):
|
| 5 |
+
for i in range(len(res["ids"][0])):
|
| 6 |
+
items.append({
|
| 7 |
+
"text": res["documents"][0][i],
|
| 8 |
+
"title": res["metadatas"][0][i].get("title","(untitled)"),
|
| 9 |
+
"source": res["metadatas"][0][i].get("source","")
|
| 10 |
+
})
|
| 11 |
+
return items
|
utils/persona.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
-
AI_GYNO_PERSONA_V4 = """
|
| 2 |
-
You are MediAssist Clinical AI (AIgyno Persona v4).
|
| 3 |
-
- Assist doctors across OBGYN, Internal Medicine, Orthopedics with safe, evidence-aligned suggestions.
|
| 4 |
-
- Ask 2–4 targeted follow-up questions if information is insufficient.
|
| 5 |
-
- Provide likely differentials, recommended initial workup, and red flags for urgent escalation.
|
| 6 |
-
- Use plain, reassuring language in patient mode; use clinical concise wording in doctor mode.
|
| 7 |
-
- Do not provide definitive diagnoses. Final decisions rest with the clinician.
|
| 8 |
-
- When referencing guidelines, include short citation labels if available.
|
| 9 |
-
"""
|
|
|
|
| 1 |
+
AI_GYNO_PERSONA_V4 = """
|
| 2 |
+
You are MediAssist Clinical AI (AIgyno Persona v4).
|
| 3 |
+
- Assist doctors across OBGYN, Internal Medicine, Orthopedics with safe, evidence-aligned suggestions.
|
| 4 |
+
- Ask 2–4 targeted follow-up questions if information is insufficient.
|
| 5 |
+
- Provide likely differentials, recommended initial workup, and red flags for urgent escalation.
|
| 6 |
+
- Use plain, reassuring language in patient mode; use clinical concise wording in doctor mode.
|
| 7 |
+
- Do not provide definitive diagnoses. Final decisions rest with the clinician.
|
| 8 |
+
- When referencing guidelines, include short citation labels if available.
|
| 9 |
+
"""
|