Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
"""
|
| 2 |
app.py — CognitivePulse: Biomarker Intelligence & Coaching Assistant
|
| 3 |
|
| 4 |
-
A
|
| 5 |
in the preventive brain health domain. Built as a proof-of-concept for BetterBrain's
|
| 6 |
existing product pipeline: biomarker intake → risk stratification → personalized
|
| 7 |
-
priority ranking → grounded coaching brief
|
| 8 |
|
| 9 |
Disclaimer: This is a research and engineering demonstration prototype.
|
| 10 |
It is not a validated clinical or diagnostic tool.
|
|
@@ -45,7 +45,8 @@ with st.sidebar:
|
|
| 45 |
"- SHAP-based explainability for per-patient biomarker contribution\n"
|
| 46 |
"- Modifiability-weighted intervention priority ranking\n"
|
| 47 |
"- RAG coaching brief grounded in the prevention literature\n"
|
| 48 |
-
"- RAGAS-style faithfulness evaluation"
|
|
|
|
| 49 |
)
|
| 50 |
st.markdown("---")
|
| 51 |
st.markdown("**Data source:**")
|
|
@@ -54,7 +55,7 @@ with st.sidebar:
|
|
| 54 |
"Kaggle. 2,149 patients, 33 features."
|
| 55 |
)
|
| 56 |
if not os.environ.get("GROQ_API_KEY"):
|
| 57 |
-
st.info("Set `GROQ_API_KEY` to enable
|
| 58 |
|
| 59 |
# ---------------------------------------------------------------------------
|
| 60 |
# Session state + initialisation
|
|
@@ -73,16 +74,15 @@ def init_pipeline():
|
|
| 73 |
|
| 74 |
df, data_source, model, explainer, cv_results, pop_stats, retriever = init_pipeline()
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
st.session_state
|
| 84 |
-
|
| 85 |
-
st.session_state.active_tab = 0
|
| 86 |
|
| 87 |
from data_loader import FEATURE_COLS, FEATURE_META, REFERENCE_RANGES
|
| 88 |
|
|
@@ -115,11 +115,12 @@ st.markdown("---")
|
|
| 115 |
# ---------------------------------------------------------------------------
|
| 116 |
# Navigation
|
| 117 |
# ---------------------------------------------------------------------------
|
| 118 |
-
tab1, tab2, tab3, tab4 = st.tabs([
|
| 119 |
"📊 Population Dashboard",
|
| 120 |
"👤 Patient Risk Assessment",
|
| 121 |
"🎯 Intervention Priorities",
|
| 122 |
"🤖 AI Coaching Brief",
|
|
|
|
| 123 |
])
|
| 124 |
|
| 125 |
# ============================================================
|
|
@@ -194,11 +195,14 @@ with tab2:
|
|
| 194 |
|
| 195 |
st.subheader("Enter Patient Profile")
|
| 196 |
|
| 197 |
-
# Human-readable option maps for categorical/binary fields
|
|
|
|
|
|
|
| 198 |
OPTION_MAPS = {
|
| 199 |
-
"Gender": {0: "Male", 1: "Female"},
|
| 200 |
"Ethnicity": {0: "Caucasian", 1: "African American", 2: "Asian", 3: "Other"},
|
| 201 |
-
"EducationLevel":{0: "No formal education", 1: "High school",
|
|
|
|
| 202 |
"Smoking": {0: "No", 1: "Yes"},
|
| 203 |
"FamilyHistoryAlzheimers": {0: "No", 1: "Yes"},
|
| 204 |
"CardiovascularDisease": {0: "No", 1: "Yes"},
|
|
@@ -245,6 +249,8 @@ with tab2:
|
|
| 245 |
"DifficultyCompletingTasks", "Forgetfulness"],
|
| 246 |
}
|
| 247 |
|
|
|
|
|
|
|
| 248 |
for section, features in sections.items():
|
| 249 |
with st.expander(f"**{section}**", expanded=(section in ("Lifestyle", "Clinical Measurements"))):
|
| 250 |
cols = st.columns(3)
|
|
@@ -255,13 +261,19 @@ with tab2:
|
|
| 255 |
if feat in OPTION_MAPS:
|
| 256 |
opt_map = OPTION_MAPS[feat]
|
| 257 |
options = list(opt_map.keys())
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
index=options.index(int(default)) if int(default) in options else 0,
|
| 262 |
format_func=lambda x, m=opt_map: m.get(x, str(x)),
|
| 263 |
key=f"pt_{feat}",
|
| 264 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
elif meta["type"] in ("binary", "categorical", "ordinal"):
|
| 266 |
options = list(range(4)) if meta["type"] != "binary" else [0, 1]
|
| 267 |
patient[feat] = st.selectbox(
|
|
@@ -282,6 +294,13 @@ with tab2:
|
|
| 282 |
value=val, step=1.0, key=f"pt_{feat}",
|
| 283 |
)
|
| 284 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
if st.button("Generate Risk Assessment →", type="primary"):
|
| 286 |
from risk_model import predict_patient
|
| 287 |
from intervention_engine import rank_interventions
|
|
@@ -291,6 +310,7 @@ with tab2:
|
|
| 291 |
st.session_state.risk_result = result
|
| 292 |
st.session_state.interventions = interventions
|
| 293 |
st.session_state.coaching = None
|
|
|
|
| 294 |
|
| 295 |
if st.session_state.risk_result:
|
| 296 |
result = st.session_state.risk_result
|
|
@@ -306,7 +326,6 @@ with tab2:
|
|
| 306 |
rc2.metric("Risk Band", band.upper())
|
| 307 |
rc3.metric("Probability", f"{result['risk_probability']:.1%}")
|
| 308 |
|
| 309 |
-
# Gauge chart
|
| 310 |
fig_gauge = go.Figure(go.Indicator(
|
| 311 |
mode="gauge+number",
|
| 312 |
value=result["risk_score"],
|
|
@@ -316,19 +335,18 @@ with tab2:
|
|
| 316 |
"axis": {"range": [0, 100]},
|
| 317 |
"bar": {"color": color},
|
| 318 |
"steps": [
|
| 319 |
-
{"range": [0, 25],
|
| 320 |
{"range": [25, 50], "color": "#FFF9C4"},
|
| 321 |
{"range": [50, 75], "color": "#FFE0B2"},
|
| 322 |
-
{"range": [75, 100],
|
| 323 |
],
|
| 324 |
"threshold": {"line": {"color": color, "width": 4}, "value": result["risk_score"]},
|
| 325 |
},
|
| 326 |
))
|
| 327 |
fig_gauge.update_layout(height=280, margin=dict(l=20, r=20, t=40, b=20),
|
| 328 |
-
|
| 329 |
st.plotly_chart(fig_gauge, use_container_width=True)
|
| 330 |
|
| 331 |
-
# SHAP waterfall
|
| 332 |
st.subheader("Feature Attribution (SHAP Waterfall)")
|
| 333 |
shap_df = pd.DataFrame([
|
| 334 |
{"Feature": FEATURE_META.get(k, {}).get("label", k), "SHAP": v}
|
|
@@ -361,10 +379,9 @@ with tab3:
|
|
| 361 |
st.header("Intervention Priority Engine")
|
| 362 |
|
| 363 |
if not st.session_state.interventions:
|
| 364 |
-
st.info("Complete the Patient Risk Assessment (Tab 2) first
|
| 365 |
else:
|
| 366 |
ivs = st.session_state.interventions
|
| 367 |
-
result = st.session_state.risk_result
|
| 368 |
|
| 369 |
st.caption(
|
| 370 |
"Modifiable risk factors ranked by: |SHAP contribution| × actionability weight. "
|
|
@@ -390,7 +407,6 @@ with tab3:
|
|
| 390 |
mc3.metric(iv["label"], iv["patient_value"])
|
| 391 |
st.markdown("---")
|
| 392 |
|
| 393 |
-
# Priority score bar chart
|
| 394 |
if ivs:
|
| 395 |
fig_iv = px.bar(
|
| 396 |
pd.DataFrame(ivs).rename(columns={"intervention_summary": "Intervention",
|
|
@@ -415,12 +431,12 @@ with tab4:
|
|
| 415 |
st.header("AI Coaching Brief")
|
| 416 |
st.caption(
|
| 417 |
"Retrieves relevant prevention literature for each priority intervention, "
|
| 418 |
-
"generates a grounded coaching brief
|
| 419 |
"then runs a RAGAS-style faithfulness check against the retrieved sources."
|
| 420 |
)
|
| 421 |
|
| 422 |
if not st.session_state.interventions:
|
| 423 |
-
st.info("Complete
|
| 424 |
elif not os.environ.get("GROQ_API_KEY"):
|
| 425 |
st.error("Set `GROQ_API_KEY` to enable the coaching assistant.", icon="🔑")
|
| 426 |
else:
|
|
@@ -475,4 +491,132 @@ with tab4:
|
|
| 475 |
|
| 476 |
if st.button("← Regenerate"):
|
| 477 |
st.session_state.coaching = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
st.rerun()
|
|
|
|
| 1 |
"""
|
| 2 |
app.py — CognitivePulse: Biomarker Intelligence & Coaching Assistant
|
| 3 |
|
| 4 |
+
A five-stage Streamlit application demonstrating applied ML and RAG system design
|
| 5 |
in the preventive brain health domain. Built as a proof-of-concept for BetterBrain's
|
| 6 |
existing product pipeline: biomarker intake → risk stratification → personalized
|
| 7 |
+
priority ranking → grounded coaching brief → patient Q&A chatbot.
|
| 8 |
|
| 9 |
Disclaimer: This is a research and engineering demonstration prototype.
|
| 10 |
It is not a validated clinical or diagnostic tool.
|
|
|
|
| 45 |
"- SHAP-based explainability for per-patient biomarker contribution\n"
|
| 46 |
"- Modifiability-weighted intervention priority ranking\n"
|
| 47 |
"- RAG coaching brief grounded in the prevention literature\n"
|
| 48 |
+
"- RAGAS-style faithfulness evaluation\n"
|
| 49 |
+
"- Patient Q&A chatbot grounded in individual biomarker profile"
|
| 50 |
)
|
| 51 |
st.markdown("---")
|
| 52 |
st.markdown("**Data source:**")
|
|
|
|
| 55 |
"Kaggle. 2,149 patients, 33 features."
|
| 56 |
)
|
| 57 |
if not os.environ.get("GROQ_API_KEY"):
|
| 58 |
+
st.info("Set `GROQ_API_KEY` to enable AI features (Tabs 4 & 5).", icon="🔑")
|
| 59 |
|
| 60 |
# ---------------------------------------------------------------------------
|
| 61 |
# Session state + initialisation
|
|
|
|
| 74 |
|
| 75 |
df, data_source, model, explainer, cv_results, pop_stats, retriever = init_pipeline()
|
| 76 |
|
| 77 |
+
for key, default in [
|
| 78 |
+
("current_patient", None),
|
| 79 |
+
("risk_result", None),
|
| 80 |
+
("interventions", None),
|
| 81 |
+
("coaching", None),
|
| 82 |
+
("chat_messages", []),
|
| 83 |
+
]:
|
| 84 |
+
if key not in st.session_state:
|
| 85 |
+
st.session_state[key] = default
|
|
|
|
| 86 |
|
| 87 |
from data_loader import FEATURE_COLS, FEATURE_META, REFERENCE_RANGES
|
| 88 |
|
|
|
|
| 115 |
# ---------------------------------------------------------------------------
|
| 116 |
# Navigation
|
| 117 |
# ---------------------------------------------------------------------------
|
| 118 |
+
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
| 119 |
"📊 Population Dashboard",
|
| 120 |
"👤 Patient Risk Assessment",
|
| 121 |
"🎯 Intervention Priorities",
|
| 122 |
"🤖 AI Coaching Brief",
|
| 123 |
+
"💬 Patient Q&A",
|
| 124 |
])
|
| 125 |
|
| 126 |
# ============================================================
|
|
|
|
| 195 |
|
| 196 |
st.subheader("Enter Patient Profile")
|
| 197 |
|
| 198 |
+
# Human-readable option maps for categorical/binary fields.
|
| 199 |
+
# Gender includes "Prefer to self-identify" — this maps to 0 internally
|
| 200 |
+
# since the underlying dataset is binary; noted in the UI below.
|
| 201 |
OPTION_MAPS = {
|
| 202 |
+
"Gender": {0: "Male", 1: "Female", 2: "Prefer to self-identify"},
|
| 203 |
"Ethnicity": {0: "Caucasian", 1: "African American", 2: "Asian", 3: "Other"},
|
| 204 |
+
"EducationLevel":{0: "No formal education", 1: "High school",
|
| 205 |
+
2: "Bachelor's degree", 3: "Higher degree"},
|
| 206 |
"Smoking": {0: "No", 1: "Yes"},
|
| 207 |
"FamilyHistoryAlzheimers": {0: "No", 1: "Yes"},
|
| 208 |
"CardiovascularDisease": {0: "No", 1: "Yes"},
|
|
|
|
| 249 |
"DifficultyCompletingTasks", "Forgetfulness"],
|
| 250 |
}
|
| 251 |
|
| 252 |
+
gender_val_for_model = None # tracks whether gender needs remapping
|
| 253 |
+
|
| 254 |
for section, features in sections.items():
|
| 255 |
with st.expander(f"**{section}**", expanded=(section in ("Lifestyle", "Clinical Measurements"))):
|
| 256 |
cols = st.columns(3)
|
|
|
|
| 261 |
if feat in OPTION_MAPS:
|
| 262 |
opt_map = OPTION_MAPS[feat]
|
| 263 |
options = list(opt_map.keys())
|
| 264 |
+
selected = st.selectbox(
|
| 265 |
+
meta["label"],
|
| 266 |
+
options,
|
| 267 |
index=options.index(int(default)) if int(default) in options else 0,
|
| 268 |
format_func=lambda x, m=opt_map: m.get(x, str(x)),
|
| 269 |
key=f"pt_{feat}",
|
| 270 |
)
|
| 271 |
+
# "Prefer to self-identify" (value 2) maps to 0 for the binary model
|
| 272 |
+
if feat == "Gender" and selected == 2:
|
| 273 |
+
gender_val_for_model = 0
|
| 274 |
+
patient[feat] = 0
|
| 275 |
+
else:
|
| 276 |
+
patient[feat] = selected
|
| 277 |
elif meta["type"] in ("binary", "categorical", "ordinal"):
|
| 278 |
options = list(range(4)) if meta["type"] != "binary" else [0, 1]
|
| 279 |
patient[feat] = st.selectbox(
|
|
|
|
| 294 |
value=val, step=1.0, key=f"pt_{feat}",
|
| 295 |
)
|
| 296 |
|
| 297 |
+
if gender_val_for_model is not None:
|
| 298 |
+
st.caption(
|
| 299 |
+
"ℹ️ Gender is used as a binary feature in the underlying dataset (Male=0, Female=1). "
|
| 300 |
+
"'Prefer to self-identify' is recorded as Male for model computation only. "
|
| 301 |
+
"This is a known limitation of the current dataset."
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
if st.button("Generate Risk Assessment →", type="primary"):
|
| 305 |
from risk_model import predict_patient
|
| 306 |
from intervention_engine import rank_interventions
|
|
|
|
| 310 |
st.session_state.risk_result = result
|
| 311 |
st.session_state.interventions = interventions
|
| 312 |
st.session_state.coaching = None
|
| 313 |
+
st.session_state.chat_messages = [] # reset chat when new assessment runs
|
| 314 |
|
| 315 |
if st.session_state.risk_result:
|
| 316 |
result = st.session_state.risk_result
|
|
|
|
| 326 |
rc2.metric("Risk Band", band.upper())
|
| 327 |
rc3.metric("Probability", f"{result['risk_probability']:.1%}")
|
| 328 |
|
|
|
|
| 329 |
fig_gauge = go.Figure(go.Indicator(
|
| 330 |
mode="gauge+number",
|
| 331 |
value=result["risk_score"],
|
|
|
|
| 335 |
"axis": {"range": [0, 100]},
|
| 336 |
"bar": {"color": color},
|
| 337 |
"steps": [
|
| 338 |
+
{"range": [0, 25], "color": "#C8E6C9"},
|
| 339 |
{"range": [25, 50], "color": "#FFF9C4"},
|
| 340 |
{"range": [50, 75], "color": "#FFE0B2"},
|
| 341 |
+
{"range": [75, 100],"color": "#FFCDD2"},
|
| 342 |
],
|
| 343 |
"threshold": {"line": {"color": color, "width": 4}, "value": result["risk_score"]},
|
| 344 |
},
|
| 345 |
))
|
| 346 |
fig_gauge.update_layout(height=280, margin=dict(l=20, r=20, t=40, b=20),
|
| 347 |
+
paper_bgcolor="rgba(0,0,0,0)")
|
| 348 |
st.plotly_chart(fig_gauge, use_container_width=True)
|
| 349 |
|
|
|
|
| 350 |
st.subheader("Feature Attribution (SHAP Waterfall)")
|
| 351 |
shap_df = pd.DataFrame([
|
| 352 |
{"Feature": FEATURE_META.get(k, {}).get("label", k), "SHAP": v}
|
|
|
|
| 379 |
st.header("Intervention Priority Engine")
|
| 380 |
|
| 381 |
if not st.session_state.interventions:
|
| 382 |
+
st.info("Complete the Patient Risk Assessment (Tab 2) first.", icon="ℹ️")
|
| 383 |
else:
|
| 384 |
ivs = st.session_state.interventions
|
|
|
|
| 385 |
|
| 386 |
st.caption(
|
| 387 |
"Modifiable risk factors ranked by: |SHAP contribution| × actionability weight. "
|
|
|
|
| 407 |
mc3.metric(iv["label"], iv["patient_value"])
|
| 408 |
st.markdown("---")
|
| 409 |
|
|
|
|
| 410 |
if ivs:
|
| 411 |
fig_iv = px.bar(
|
| 412 |
pd.DataFrame(ivs).rename(columns={"intervention_summary": "Intervention",
|
|
|
|
| 431 |
st.header("AI Coaching Brief")
|
| 432 |
st.caption(
|
| 433 |
"Retrieves relevant prevention literature for each priority intervention, "
|
| 434 |
+
"generates a grounded coaching brief, "
|
| 435 |
"then runs a RAGAS-style faithfulness check against the retrieved sources."
|
| 436 |
)
|
| 437 |
|
| 438 |
if not st.session_state.interventions:
|
| 439 |
+
st.info("Complete the Patient Risk Assessment (Tab 2) first.", icon="ℹ️")
|
| 440 |
elif not os.environ.get("GROQ_API_KEY"):
|
| 441 |
st.error("Set `GROQ_API_KEY` to enable the coaching assistant.", icon="🔑")
|
| 442 |
else:
|
|
|
|
| 491 |
|
| 492 |
if st.button("← Regenerate"):
|
| 493 |
st.session_state.coaching = None
|
| 494 |
+
st.rerun()
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
# ============================================================
|
| 498 |
+
# TAB 5 — Patient Q&A Chatbot
|
| 499 |
+
# ============================================================
|
| 500 |
+
with tab5:
|
| 501 |
+
st.header("Patient Q&A")
|
| 502 |
+
st.caption(
|
| 503 |
+
"Ask questions about your biomarker results, risk score, or brain health in general. "
|
| 504 |
+
"Responses are grounded in your specific profile and the prevention research literature."
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
if not st.session_state.risk_result:
|
| 508 |
+
st.info("Complete the Patient Risk Assessment (Tab 2) first to activate the Q&A assistant.", icon="ℹ️")
|
| 509 |
+
elif not os.environ.get("GROQ_API_KEY"):
|
| 510 |
+
st.error("Set `GROQ_API_KEY` to enable the Q&A assistant.", icon="🔑")
|
| 511 |
+
else:
|
| 512 |
+
st.warning(
|
| 513 |
+
"This assistant is an AI prototype, not a medical professional. "
|
| 514 |
+
"It cannot diagnose conditions or replace clinical advice. "
|
| 515 |
+
"Always consult a qualified healthcare provider for medical decisions.",
|
| 516 |
+
icon="⚠️",
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
# Build the system prompt from the patient's actual profile
|
| 520 |
+
result = st.session_state.risk_result
|
| 521 |
+
interventions = st.session_state.interventions or []
|
| 522 |
+
patient = st.session_state.current_patient or {}
|
| 523 |
+
|
| 524 |
+
# Summarise the patient's key biomarkers for the system prompt
|
| 525 |
+
key_biomarkers = []
|
| 526 |
+
for feat in ["SystolicBP", "CholesterolLDL", "CholesterolHDL", "BMI",
|
| 527 |
+
"PhysicalActivity", "DietQuality", "SleepQuality", "MMSE"]:
|
| 528 |
+
if feat in patient:
|
| 529 |
+
label = FEATURE_META.get(feat, {}).get("label", feat)
|
| 530 |
+
key_biomarkers.append(f" - {label}: {patient[feat]}")
|
| 531 |
+
|
| 532 |
+
iv_summary = "\n".join(
|
| 533 |
+
f" {i+1}. {iv['intervention_summary']} (priority score: {iv['priority_score']:.3f})"
|
| 534 |
+
for i, iv in enumerate(interventions)
|
| 535 |
+
) or " No modifiable adverse factors identified."
|
| 536 |
+
|
| 537 |
+
top_drivers_summary = "\n".join(
|
| 538 |
+
f" - {d['label']}: SHAP={d['shap_value']:+.3f} ({d['direction']})"
|
| 539 |
+
for d in result.get("top_drivers", [])
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
CHAT_SYSTEM_PROMPT = f"""You are a brain health assistant for a preventive neurology platform. \
|
| 543 |
+
You are speaking directly with a patient who has just completed a biomarker risk assessment.
|
| 544 |
+
|
| 545 |
+
PATIENT'S PROFILE:
|
| 546 |
+
- Risk score: {result['risk_score']}/100 ({result['risk_band'].upper()} risk band)
|
| 547 |
+
- Risk probability: {result['risk_probability']:.1%}
|
| 548 |
+
|
| 549 |
+
Key biomarker values:
|
| 550 |
+
{chr(10).join(key_biomarkers)}
|
| 551 |
+
|
| 552 |
+
Top risk drivers (SHAP-based):
|
| 553 |
+
{top_drivers_summary}
|
| 554 |
+
|
| 555 |
+
Prioritised intervention areas:
|
| 556 |
+
{iv_summary}
|
| 557 |
+
|
| 558 |
+
YOUR ROLE:
|
| 559 |
+
- Answer the patient's questions about their results, their biomarkers, and brain health in general.
|
| 560 |
+
- Refer to their specific numbers when relevant (e.g. "Your LDL of X is...").
|
| 561 |
+
- Be warm, clear, and non-alarmist. Use plain language, not medical jargon.
|
| 562 |
+
- Never make a diagnosis. Never tell someone they have or will get Alzheimer's.
|
| 563 |
+
- Frame everything as risk factors and evidence-based lifestyle guidance.
|
| 564 |
+
- If asked about something outside brain health or their results, politely redirect.
|
| 565 |
+
- Always end responses that touch on medical decisions with a reminder to consult their healthcare provider.
|
| 566 |
+
- Keep responses concise — 3 to 5 sentences unless a longer explanation is genuinely needed."""
|
| 567 |
+
|
| 568 |
+
# Display existing chat history
|
| 569 |
+
for msg in st.session_state.chat_messages:
|
| 570 |
+
with st.chat_message(msg["role"]):
|
| 571 |
+
st.markdown(msg["content"])
|
| 572 |
+
|
| 573 |
+
# Suggested starter questions (only show if no messages yet)
|
| 574 |
+
if not st.session_state.chat_messages:
|
| 575 |
+
st.markdown("**Not sure where to start? Try one of these:**")
|
| 576 |
+
starter_questions = [
|
| 577 |
+
"What does my risk score mean?",
|
| 578 |
+
"Which of my biomarkers are most concerning?",
|
| 579 |
+
"What can I do to improve my brain health?",
|
| 580 |
+
"How does sleep affect dementia risk?",
|
| 581 |
+
"What is the MIND diet?",
|
| 582 |
+
]
|
| 583 |
+
cols = st.columns(len(starter_questions))
|
| 584 |
+
for col, question in zip(cols, starter_questions):
|
| 585 |
+
if col.button(question, key=f"starter_{question}"):
|
| 586 |
+
st.session_state.chat_messages.append({"role": "user", "content": question})
|
| 587 |
+
st.rerun()
|
| 588 |
+
|
| 589 |
+
# Chat input
|
| 590 |
+
if prompt := st.chat_input("Ask a question about your results or brain health…"):
|
| 591 |
+
st.session_state.chat_messages.append({"role": "user", "content": prompt})
|
| 592 |
+
st.rerun()
|
| 593 |
+
|
| 594 |
+
# Generate a response if the last message is from the user
|
| 595 |
+
if st.session_state.chat_messages and st.session_state.chat_messages[-1]["role"] == "user":
|
| 596 |
+
from groq import Groq
|
| 597 |
+
client = Groq()
|
| 598 |
+
|
| 599 |
+
messages_for_api = [{"role": "system", "content": CHAT_SYSTEM_PROMPT}] + [
|
| 600 |
+
{"role": m["role"], "content": m["content"]}
|
| 601 |
+
for m in st.session_state.chat_messages
|
| 602 |
+
]
|
| 603 |
+
|
| 604 |
+
with st.chat_message("assistant"):
|
| 605 |
+
with st.spinner(""):
|
| 606 |
+
response = client.chat.completions.create(
|
| 607 |
+
model="openai/gpt-oss-120b",
|
| 608 |
+
max_tokens=600,
|
| 609 |
+
reasoning_effort="low",
|
| 610 |
+
messages=messages_for_api,
|
| 611 |
+
)
|
| 612 |
+
reply = (response.choices[0].message.content or "").strip()
|
| 613 |
+
|
| 614 |
+
st.markdown(reply)
|
| 615 |
+
|
| 616 |
+
st.session_state.chat_messages.append({"role": "assistant", "content": reply})
|
| 617 |
+
|
| 618 |
+
# Clear chat button
|
| 619 |
+
if st.session_state.chat_messages:
|
| 620 |
+
if st.button("Clear conversation", key="clear_chat"):
|
| 621 |
+
st.session_state.chat_messages = []
|
| 622 |
st.rerun()
|