Add the 5 questions to the profile questionnaire that Dr. Tariman suggested

#2
Files changed (1) hide show
  1. app.py +23 -14
app.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
  MyeAI (Myeloma AI)
3
  ===========================================================
4
- Takes a participant's answers to the 16 Shared Decision Making questions
5
  and runs an adaptive follow-up conversation using Google's Gemini API.
6
 
7
  GROUNDING:
@@ -46,7 +46,7 @@ from supabase_store import (SupabaseStore, normalize_email, is_valid_email,
46
  user_ids_match, CONFLICT)
47
 
48
  # ----------------------------------------------------------------------
49
- # The 16 profile questions (verbatim from the intake form)
50
  # ----------------------------------------------------------------------
51
  QUESTIONS = [
52
  "I prefer my healthcare team (hematologist, nurse practitioner, or physician assistant), and I collaborate in deciding which treatment for relapsed or refractory myeloma is best for me",
@@ -65,6 +65,10 @@ QUESTIONS = [
65
  "My out-of-pocket cost of treatment is important to me",
66
  "I prefer to continue an active lifestyle during my myeloma treatment",
67
  "I worry about how my treatment will affect future treatment options",
 
 
 
 
68
  ]
69
 
70
  # Short tags used for the summary view
@@ -85,6 +89,11 @@ QUESTION_TAGS = [
85
  "Out-of-pocket cost matters",
86
  "Wants active lifestyle",
87
  "Worries about future options",
 
 
 
 
 
88
  ]
89
 
90
  MODEL = "gemini-2.5-flash"
@@ -116,14 +125,14 @@ def ensure_kb(client):
116
 
117
  # ----------------------------------------------------------------------
118
  # Supabase persistence — every participant is identified by their email.
119
- # Their 16 answers and their whole conversation are written to Supabase as
120
  # they go, and restored automatically when they sign back in.
121
  # ----------------------------------------------------------------------
122
  STORE = SupabaseStore()
123
 
124
 
125
  def _coerce_answers(value):
126
- """Normalize a stored answers list into exactly 16 'Yes'/'No'/None values."""
127
  out = [None] * len(QUESTIONS)
128
  if isinstance(value, list):
129
  for i, v in enumerate(value[: len(QUESTIONS)]):
@@ -240,11 +249,11 @@ def _user_badge(user_id):
240
  return f"User ID<br><strong>{html.escape(user_id)}</strong>"
241
 
242
 
243
- ANSWERS_SUMMARY_HEADING = "**Your responses to the 16 questions**"
244
 
245
 
246
  def build_answers_summary(answers):
247
- """The 16 questions and this participant's answers, shown as the opening
248
  message of the chat so they can see the profile the conversation is built
249
  on without leaving the conversation."""
250
  lines = [ANSWERS_SUMMARY_HEADING, ""]
@@ -393,7 +402,7 @@ def system_instruction():
393
  "5. Tone: empathetic, respectful, never alarming.\n"
394
  "6. TREATMENT-OPTIONS QUESTION: When the patient asks what the best treatment options are for "
395
  "their myeloma (e.g. 'Using my profile, what are the best treatment options for my myeloma?'), "
396
- "combine TWO sources: their 16 yes/no profile answers AND the source excerpts. "
397
  "Keep the whole reply under 120 words — one opening line, at most 3 bullets, one closing "
398
  "line, one question:\n"
399
  " - One warm opening line.\n"
@@ -655,7 +664,7 @@ def sign_in(user_id_raw, email_raw, uid_attempts):
655
 
656
  if chat_display and convo:
657
  # A conversation saved before the opening summary existed has none, so
658
- # add it here: every chat should begin with the 16 questions.
659
  if (any(a is not None for a in answers)
660
  and not chat_display[0]["content"].startswith(ANSWERS_SUMMARY_HEADING)):
661
  chat_display = [{"role": "assistant",
@@ -702,7 +711,7 @@ def start_chat(api_key, email, can_save, revision, chat_display, convo,
702
  answers = [v if v in ("Yes", "No") else None for v in radio_values]
703
  if any(v is None for v in answers):
704
  missing = [i + 1 for i, v in enumerate(answers) if v is None]
705
- gr.Warning(f"Please answer all 16 questions. Missing: {missing}")
706
  # Nothing has been written yet, so a rejected submission must change
707
  # nothing: hand the existing conversation state straight back.
708
  return (gr.update(), chat_display, convo, n_followups, revision,
@@ -734,7 +743,7 @@ def start_chat(api_key, email, can_save, revision, chat_display, convo,
734
  flag_text = "\n".join(f"- {f}" for f in flags) if flags else "- (No obvious contradictions; explore their highest-stakes priorities.)"
735
 
736
  seed = (
737
- "Here is the patient's completed profile (16 yes/no answers):\n\n"
738
  f"{profile}\n\n"
739
  "FLAGGED TENSIONS / PRIORITIES TO EXPLORE:\n"
740
  f"{flag_text}\n\n"
@@ -1074,7 +1083,7 @@ with gr.Blocks(title="MyeAI (Myeloma AI)", css=CSS,
1074
  email_btn = gr.Button("Continue", variant="primary")
1075
 
1076
  with gr.Group(visible=False) as intake_panel:
1077
- gr.Markdown("**Answer all 16 questions, then click Start Follow-up.**")
1078
  radios = []
1079
  for i, q in enumerate(QUESTIONS):
1080
  with gr.Row(elem_classes="q-card"):
@@ -1089,7 +1098,7 @@ with gr.Blocks(title="MyeAI (Myeloma AI)", css=CSS,
1089
  # paste elsewhere (e.g. a note for their care team).
1090
  show_copy_button=True,
1091
  # Without this, consecutive assistant messages (the
1092
- # 16-answer summary + the greeting) merge into one
1093
  # bubble sharing one copy button, so the summary
1094
  # could not be copied on its own.
1095
  group_consecutive_messages=False)
@@ -1107,7 +1116,7 @@ with gr.Blocks(title="MyeAI (Myeloma AI)", css=CSS,
1107
  gr.Markdown(
1108
  "**Are you sure you want to start over?**\n\n"
1109
  "This erases your current chat history and begins a new session. "
1110
- "You will answer the 16 questions again."
1111
  )
1112
  with gr.Row():
1113
  confirm_reset_btn = gr.Button("Yes, erase and start over",
@@ -1132,7 +1141,7 @@ with gr.Blocks(title="MyeAI (Myeloma AI)", css=CSS,
1132
  # Save the questionnaire as it is filled in, so a half-finished intake
1133
  # survives a closed tab.
1134
  # `.input` rather than `.change`: fires only on a real click, so restoring
1135
- # or clearing the form does not stampede 16 writes back at Supabase.
1136
  for r in radios:
1137
  r.input(
1138
  save_partial_answers,
 
1
  """
2
  MyeAI (Myeloma AI)
3
  ===========================================================
4
+ Takes a participant's answers to the 21 Shared Decision Making questions
5
  and runs an adaptive follow-up conversation using Google's Gemini API.
6
 
7
  GROUNDING:
 
46
  user_ids_match, CONFLICT)
47
 
48
  # ----------------------------------------------------------------------
49
+ # The 21 profile questions (verbatim from the intake form)
50
  # ----------------------------------------------------------------------
51
  QUESTIONS = [
52
  "I prefer my healthcare team (hematologist, nurse practitioner, or physician assistant), and I collaborate in deciding which treatment for relapsed or refractory myeloma is best for me",
 
65
  "My out-of-pocket cost of treatment is important to me",
66
  "I prefer to continue an active lifestyle during my myeloma treatment",
67
  "I worry about how my treatment will affect future treatment options",
68
+ "I have received an autologous stem cell transplant",
69
+ "I have received CAR-T therapy",
70
+ "This is my first relapse from myeloma",
71
+ "I am refractory to proteasome inhibitors or PIs and immunomodulatory drugs or IMiDs",
72
  ]
73
 
74
  # Short tags used for the summary view
 
89
  "Out-of-pocket cost matters",
90
  "Wants active lifestyle",
91
  "Worries about future options",
92
+ "Prior autologous stem cell transplant",
93
+ "Prior CAR-T therapy",
94
+ "Prior bispecific therapy",
95
+ "First myeloma relapse",
96
+ "Refractory to PI and IMiD",
97
  ]
98
 
99
  MODEL = "gemini-2.5-flash"
 
125
 
126
  # ----------------------------------------------------------------------
127
  # Supabase persistence — every participant is identified by their email.
128
+ # Their 21 answers and their whole conversation are written to Supabase as
129
  # they go, and restored automatically when they sign back in.
130
  # ----------------------------------------------------------------------
131
  STORE = SupabaseStore()
132
 
133
 
134
  def _coerce_answers(value):
135
+ """Normalize a stored answers list into exactly 21 'Yes'/'No'/None values."""
136
  out = [None] * len(QUESTIONS)
137
  if isinstance(value, list):
138
  for i, v in enumerate(value[: len(QUESTIONS)]):
 
249
  return f"User ID<br><strong>{html.escape(user_id)}</strong>"
250
 
251
 
252
+ ANSWERS_SUMMARY_HEADING = "**Your responses to the 21 questions**"
253
 
254
 
255
  def build_answers_summary(answers):
256
+ """The 21 questions and this participant's answers, shown as the opening
257
  message of the chat so they can see the profile the conversation is built
258
  on without leaving the conversation."""
259
  lines = [ANSWERS_SUMMARY_HEADING, ""]
 
402
  "5. Tone: empathetic, respectful, never alarming.\n"
403
  "6. TREATMENT-OPTIONS QUESTION: When the patient asks what the best treatment options are for "
404
  "their myeloma (e.g. 'Using my profile, what are the best treatment options for my myeloma?'), "
405
+ "combine TWO sources: their 21 yes/no profile answers AND the source excerpts. "
406
  "Keep the whole reply under 120 words — one opening line, at most 3 bullets, one closing "
407
  "line, one question:\n"
408
  " - One warm opening line.\n"
 
664
 
665
  if chat_display and convo:
666
  # A conversation saved before the opening summary existed has none, so
667
+ # add it here: every chat should begin with the 21 questions.
668
  if (any(a is not None for a in answers)
669
  and not chat_display[0]["content"].startswith(ANSWERS_SUMMARY_HEADING)):
670
  chat_display = [{"role": "assistant",
 
711
  answers = [v if v in ("Yes", "No") else None for v in radio_values]
712
  if any(v is None for v in answers):
713
  missing = [i + 1 for i, v in enumerate(answers) if v is None]
714
+ gr.Warning(f"Please answer all 21 questions. Missing: {missing}")
715
  # Nothing has been written yet, so a rejected submission must change
716
  # nothing: hand the existing conversation state straight back.
717
  return (gr.update(), chat_display, convo, n_followups, revision,
 
743
  flag_text = "\n".join(f"- {f}" for f in flags) if flags else "- (No obvious contradictions; explore their highest-stakes priorities.)"
744
 
745
  seed = (
746
+ "Here is the patient's completed profile (21 yes/no answers):\n\n"
747
  f"{profile}\n\n"
748
  "FLAGGED TENSIONS / PRIORITIES TO EXPLORE:\n"
749
  f"{flag_text}\n\n"
 
1083
  email_btn = gr.Button("Continue", variant="primary")
1084
 
1085
  with gr.Group(visible=False) as intake_panel:
1086
+ gr.Markdown("**Answer all 21 questions, then click Start Follow-up.**")
1087
  radios = []
1088
  for i, q in enumerate(QUESTIONS):
1089
  with gr.Row(elem_classes="q-card"):
 
1098
  # paste elsewhere (e.g. a note for their care team).
1099
  show_copy_button=True,
1100
  # Without this, consecutive assistant messages (the
1101
+ # 21-answer summary + the greeting) merge into one
1102
  # bubble sharing one copy button, so the summary
1103
  # could not be copied on its own.
1104
  group_consecutive_messages=False)
 
1116
  gr.Markdown(
1117
  "**Are you sure you want to start over?**\n\n"
1118
  "This erases your current chat history and begins a new session. "
1119
+ "You will answer the 21 questions again."
1120
  )
1121
  with gr.Row():
1122
  confirm_reset_btn = gr.Button("Yes, erase and start over",
 
1141
  # Save the questionnaire as it is filled in, so a half-finished intake
1142
  # survives a closed tab.
1143
  # `.input` rather than `.change`: fires only on a real click, so restoring
1144
+ # or clearing the form does not stampede 21 writes back at Supabase.
1145
  for r in radios:
1146
  r.input(
1147
  save_partial_answers,