Spaces:
Running on Zero
Running on Zero
Add the 5 questions to the profile questionnaire that Dr. Tariman suggested
#2
by stevafernandes - opened
app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
MyeAI (Myeloma AI)
|
| 3 |
===========================================================
|
| 4 |
-
Takes a participant's answers to the
|
| 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
|
| 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
|
| 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
|
| 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
|
| 244 |
|
| 245 |
|
| 246 |
def build_answers_summary(answers):
|
| 247 |
-
"""The
|
| 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
|
| 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
|
| 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
|
| 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 (
|
| 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
|
| 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 |
-
#
|
| 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
|
| 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
|
| 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,
|