kshamaasuresh commited on
Commit
99fe06b
·
verified ·
1 Parent(s): a71ef07

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +175 -31
app.py CHANGED
@@ -1,10 +1,10 @@
1
  """
2
  app.py — CognitivePulse: Biomarker Intelligence & Coaching Assistant
3
 
4
- A four-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 generation.
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 the coaching assistant (Stage 4).", icon="🔑")
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
- if "current_patient" not in st.session_state:
77
- st.session_state.current_patient = None
78
- if "risk_result" not in st.session_state:
79
- st.session_state.risk_result = None
80
- if "interventions" not in st.session_state:
81
- st.session_state.interventions = None
82
- if "coaching" not in st.session_state:
83
- st.session_state.coaching = None
84
- if "active_tab" not in st.session_state:
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", 2: "Bachelor's degree", 3: "Higher degree"},
 
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
- labels = list(opt_map.values())
259
- patient[feat] = st.selectbox(
260
- meta["label"], options,
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], "color": "#C8E6C9"},
320
  {"range": [25, 50], "color": "#FFF9C4"},
321
  {"range": [50, 75], "color": "#FFE0B2"},
322
- {"range": [75, 100], "color": "#FFCDD2"},
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
- paper_bgcolor="rgba(0,0,0,0)")
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 to generate personalised intervention priorities.", icon="ℹ️")
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 via Groq's inference API, "
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 Tabs 2 and 3 first to generate the coaching brief.", icon="ℹ️")
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()