Spaces:
Running on Zero
Running on Zero
File size: 25,376 Bytes
dd4d624 d97ee23 d19f25f dd4d624 d19f25f dd4d624 d19f25f dd4d624 7f8678c d19f25f 7f8678c dd4d624 d19f25f dd4d624 d0bd74c d19f25f dd4d624 d19f25f 9586c06 d19f25f dd4d624 d19f25f dd4d624 f8a1e2a d19f25f f8a1e2a d19f25f f8a1e2a d0bd74c dd4d624 d19f25f dd4d624 d19f25f dd4d624 d19f25f dd4d624 d19f25f dd4d624 d19f25f dd4d624 d19f25f 3124adf dd4d624 d19f25f dd4d624 f8a1e2a dd4d624 f8a1e2a dd4d624 f8a1e2a dd4d624 f8a1e2a dd4d624 d19f25f dd4d624 d19f25f dd4d624 d19f25f dd4d624 d0bd74c f8a1e2a d0bd74c dd4d624 d97ee23 d0bd74c d97ee23 d0bd74c dd4d624 f8a1e2a dd4d624 f8a1e2a dd4d624 f8a1e2a dd4d624 d19f25f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 | """
MyeAI (Myeloma AI)
===========================================================
Takes a participant's answers to the 16 Shared Decision Making questions
and runs an adaptive follow-up conversation using Google's Gemini API.
GROUNDING:
General informational questions from the user are answered STRICTLY from the
documents in the uploaded ZIP corpus (Archive_2.zip) using retrieval-augmented
generation. At startup the app extracts text from every PDF/PNG in the corpus,
embeds it with Gemini embeddings, and builds a searchable index. Each user
question retrieves the most relevant passages, and Gemini is instructed to
answer only from those passages (or say the answer isn't in the documents).
Deploy on Hugging Face Spaces (SDK: Gradio).
- Upload Archive_2.zip to the Space repo root (or set KB_ZIP / KB_DIR).
- Set your Gemini key as a Space Secret named GEMINI_API_KEY.
- Recommended packages.txt: poppler-utils, tesseract-ocr (see notes at bottom).
"""
import os
import json
# ----------------------------------------------------------------------
# ZeroGPU compatibility shim (harmless on CPU basic; skipped if not present).
# Satisfies "No @spaces.GPU function detected during startup" if the Space is
# on ZeroGPU. This app is API-only and does not use a GPU — CPU basic is fine.
# ----------------------------------------------------------------------
try:
import spaces
@spaces.GPU
def _zerogpu_warmup():
return None
except Exception:
pass
from google import genai
from google.genai import types
import gradio as gr
from knowledge_base import KnowledgeBase
# ----------------------------------------------------------------------
# The 16 profile questions (verbatim from the intake form)
# ----------------------------------------------------------------------
QUESTIONS = [
"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",
"It is important for me to understand my treatment options for relapsed or refractory myeloma",
"I trust my healthcare team very much, that's why I leave it up to them to recommend therapy that is right for me",
"I live alone with no caregiver",
"I have strong social support and a network that can help with my treatment appointments",
"I will do whatever it takes to so I can be cured, cancer-free, kill all the myeloma, or at least be in remission and live a long life",
"I would like to take an aggressive approach to treat my myeloma",
"I am willing to endure as many side effects as possible to control my myeloma",
"I prefer to receive treatment in an outpatient setting",
"I prefer to take medications at home",
"I prefer to take the least possible amount of pills to control my cancer",
"Quality of life is more important to me than quantity of life",
"Clinical drug trial participation is of interest to me",
"My out-of-pocket cost of treatment is important to me",
"I prefer to continue an active lifestyle during my myeloma treatment",
"I worry about how my treatment will affect future treatment options",
]
# Short tags used for the summary view
QUESTION_TAGS = [
"Shared decision-making",
"Wants to understand options",
"Defers to care team",
"Lives alone / no caregiver",
"Strong social support",
"Goal: cure / long life",
"Wants aggressive approach",
"Tolerant of side effects",
"Prefers outpatient",
"Prefers meds at home",
"Prefers fewer pills",
"Quality over quantity",
"Interested in clinical trials",
"Out-of-pocket cost matters",
"Wants active lifestyle",
"Worries about future options",
]
MODEL = "gemini-2.5-flash"
MAX_FOLLOWUPS = 8 # cap the adaptive conversation
# The conversation always opens with this fixed question from the patient.
FIRST_QUESTION = "Using my profile, what are the best treatment options for my myeloma?"
# ----------------------------------------------------------------------
# Knowledge base — built once at startup.
# ----------------------------------------------------------------------
KB = KnowledgeBase()
_KB_BUILD_ATTEMPTED = False
def ensure_kb(client):
"""Build the KB on first successful client. Returns (ready, message)."""
global _KB_BUILD_ATTEMPTED
if KB.ready:
return True, ""
if _KB_BUILD_ATTEMPTED and KB.error:
return False, KB.error
_KB_BUILD_ATTEMPTED = True
KB.build(client)
if KB.ready:
return True, ""
return False, (KB.error or "Knowledge base could not be initialized.")
# ----------------------------------------------------------------------
# Prompt building
# ----------------------------------------------------------------------
def build_profile_text(answers):
"""answers: list of 'Yes'/'No' aligned to QUESTIONS."""
lines = []
for q, tag, a in zip(QUESTIONS, QUESTION_TAGS, answers):
lines.append(f"- [{a.upper()}] ({tag}) {q}")
return "\n".join(lines)
def detect_tensions(answers):
"""Flag notable patterns/contradictions worth probing. Pure logic, no API."""
a = {i: (answers[i].strip().lower() == "yes") for i in range(len(answers))}
flags = []
if (a.get(5) or a.get(6) or a.get(7)) and a.get(11):
flags.append("Wants an aggressive/curative approach but also values quality of life over quantity. Worth clarifying how they weigh these when they conflict.")
if a.get(7) and (a.get(14) or a.get(11)):
flags.append("Willing to endure many side effects, yet wants to stay active / prioritizes quality of life. Probe acceptable side-effect threshold.")
if a.get(3) and not a.get(4):
flags.append("Lives alone with no caregiver and limited social support. Treatment logistics and safety monitoring need exploration.")
if a.get(3) and a.get(9):
flags.append("Lives alone but prefers taking medications at home. Explore support for safe self-administration and side-effect monitoring.")
if a.get(2) and (a.get(0) or a.get(1)):
flags.append("Says they leave decisions to the care team, yet also wants to collaborate / understand options. Clarify how involved they actually want to be.")
if a.get(13) and a.get(12):
flags.append("Cost matters and they're open to clinical trials — trials may reduce drug cost; worth surfacing.")
if a.get(10) and (a.get(6) or a.get(7)):
flags.append("Prefers the fewest pills possible but wants an aggressive approach. Explore tolerance for treatment intensity vs convenience.")
if a.get(8) and a.get(3):
flags.append("Prefers outpatient treatment but lives alone — explore transport and post-visit support.")
return flags
def system_instruction():
return (
"You are a warm, plain-spoken health navigator helping a multiple myeloma patient "
"(relapsed or refractory) prepare for a shared decision-making conversation with their "
"care team. You are NOT a doctor and you never give medical advice, diagnoses, dosing, or "
"treatment recommendations. Your job is to (a) ask thoughtful follow-up questions that help "
"the patient clarify their own values, priorities, constraints, and concerns, and (b) answer "
"the patient's general informational questions using ONLY the reference documents provided to "
"you in the RETRIEVED CONTEXT for that turn.\n\n"
"STRICT GROUNDING RULES (most important):\n"
"A. For any general/informational question the patient asks (e.g. 'what is CAR T-cell "
"therapy?', 'what are the side effects of stem cell transplant?', 'what does relapsed mean?'), "
"you MUST base your answer solely on the text inside the RETRIEVED CONTEXT block for that turn. "
"Do NOT use outside knowledge, and do NOT add facts that are not present in that context.\n"
"B. If the RETRIEVED CONTEXT does not contain enough information to answer, say so plainly: "
"\"I couldn't find that in the reference documents provided for this tool. Please ask your care "
"team.\" Do not guess or fill gaps from general knowledge.\n"
"C. When you use information from the context, attribute it naturally to the document "
"TITLE(S) shown in each passage's [Document: ...] label — use that title, not a number, and "
"never say \"Source 1/2/3\" (e.g. \"According to the NCCN Guideline ...\" or \"The CAR T cell "
"Therapy — International Myeloma Foundation page notes ...\"). Use the title exactly as shown; "
"do not invent or embellish document names.\n"
"D. Never invent drug names, doses, statistics, or study results that are not in the context.\n\n"
"CONVERSATION RULES:\n"
"1. When you ask a follow-up question, ask exactly ONE question per turn, 1-3 sentences, "
"conversational and jargon-free.\n"
"2. Base follow-up questions on the patient's profile and previous answers. Prioritize the "
"FLAGGED TENSIONS provided — gently explore apparent contradictions without judgment.\n"
"3. Do not repeat questions already asked. Build on what they say.\n"
"4. Never recommend or rank treatments. If the patient asks for medical advice or 'what should "
"I do', kindly redirect them to their care team, then (if appropriate) continue with a follow-up "
"question.\n"
"5. Tone: empathetic, respectful, never alarming.\n"
"6. TREATMENT-OPTIONS QUESTION: When the patient asks what the best treatment options are for "
"their myeloma (e.g. 'Using my profile, what are the best treatment options for my myeloma?'), "
"combine TWO sources: their 16 yes/no profile answers AND the RETRIEVED CONTEXT documents. Do "
"this:\n"
" - Open with one warm sentence.\n"
" - Walk through how THEIR specific stated priorities map to the kinds of treatment approaches "
"described in the RETRIEVED CONTEXT for relapsed/refractory myeloma. Tie each point explicitly "
"back to their answers (for example: a preference for medications at home and fewest pills points "
"toward asking about convenient outpatient or oral regimens; openness to clinical trials means "
"trials are worth raising; willingness to endure side effects and wanting an aggressive approach "
"points toward asking about more intensive options; prioritizing quality of life points toward "
"regimens that protect daily functioning; living alone with limited support points toward "
"logistics and monitoring).\n"
" - Only describe treatment approaches that actually appear in the RETRIEVED CONTEXT. Do NOT "
"invent clinical details, drug names, dosing, or anything not supported by the context or implied "
"by their answers. Do NOT rank, prescribe, or state which option is medically best. Frame "
"everything as 'based on what you told us and these reference materials, here are options and "
"questions to raise with your care team.'\n"
" - End by reminding them their care team must confirm what is medically appropriate, then ask "
"your first single follow-up question drawn from their profile and the flagged tensions.\n"
"7. When you judge that you have enough to summarize their priorities (or after several "
"exchanges), instead of asking another question, output a final summary. Begin that final "
"message with the exact token <<SUMMARY>> on its own line, then 4-7 short bullet points "
"capturing the patient's key priorities, constraints, and questions to raise with their care team."
)
# ----------------------------------------------------------------------
# Gemini client helpers
# ----------------------------------------------------------------------
def get_client(user_key):
key = (user_key or "").strip() or os.environ.get("GEMINI_API_KEY", "").strip()
if not key:
return None, "No API key found. Set GEMINI_API_KEY as a Space secret."
try:
client = genai.Client(api_key=key)
return client, None
except Exception as e:
return None, f"Could not initialize Gemini client: {e}"
def to_gemini_contents(history):
"""history: list of {'role': 'user'|'assistant', 'content': str}
Returns Gemini Content list (assistant -> 'model')."""
contents = []
for m in history:
role = "model" if m["role"] == "assistant" else "user"
contents.append(types.Content(role=role, parts=[types.Part(text=m["content"])]))
return contents
def _latest_user_query(convo):
for m in reversed(convo):
if m["role"] == "user":
return m["content"]
return ""
def gemini_reply(client, convo, n_followups):
"""convo: internal history list. Retrieves KB context for the latest user
turn and injects it, then asks Gemini to answer grounded in that context."""
# Retrieve relevant passages for the most recent user message.
context_block = ""
query = _latest_user_query(convo)
if KB.ready and query.strip():
try:
retrieved = KB.retrieve(client, query)
if retrieved:
context_block = KB.context_block(retrieved)
except Exception:
context_block = ""
contents = to_gemini_contents(convo)
# Build the per-turn system instruction with the retrieved context appended.
sys = system_instruction()
if context_block:
sys += (
"\n\n==================== RETRIEVED CONTEXT (reference documents) ====================\n"
"The following passages were retrieved from the uploaded reference documents for THIS "
"turn. Answer general/informational questions using ONLY these passages. If they do not "
"contain the answer, say you couldn't find it in the reference documents.\n\n"
f"{context_block}\n"
"================================================================================\n"
)
else:
sys += (
"\n\n[No reference passages were retrieved for this turn. If the patient asked a general "
"informational question, tell them you couldn't find it in the reference documents and "
"suggest they ask their care team. You may still ask a values-clarifying follow-up "
"question or work with their profile.]\n"
)
if n_followups >= MAX_FOLLOWUPS - 1:
sys += "\n\nYou have asked enough questions. Provide the final <<SUMMARY>> now."
cfg = types.GenerateContentConfig(
system_instruction=sys,
temperature=0.3, # lower temp -> stays closer to the sources
# gemini-2.5-flash has "thinking" on by default, and those thinking
# tokens are drawn from max_output_tokens. That could exhaust the budget
# and truncate the visible answer mid-sentence (e.g. the long treatment-
# options response). Disable thinking (budget=0) so the whole budget goes
# to the answer, and raise the ceiling for extra headroom.
max_output_tokens=8192,
thinking_config=types.ThinkingConfig(thinking_budget=0),
)
resp = client.models.generate_content(model=MODEL, contents=contents, config=cfg)
return (resp.text or "").strip()
# ----------------------------------------------------------------------
# Gradio app
# ----------------------------------------------------------------------
def start_chat(api_key, *radio_values):
answers = [v if v in ("Yes", "No") else None for v in radio_values]
if any(v is None for v in answers):
missing = [i + 1 for i, v in enumerate(answers) if v is None]
gr.Warning(f"Please answer all 16 questions. Missing: {missing}")
return (gr.update(), [], [], 0, gr.update(visible=True), gr.update(visible=False))
client, err = get_client(api_key)
if err:
gr.Warning(err)
return (gr.update(), [], [], 0, gr.update(visible=True), gr.update(visible=False))
# Build the knowledge base on first use (may take a bit on cold start).
ready, msg = ensure_kb(client)
if not ready:
gr.Warning(f"Reference documents unavailable: {msg}")
# We still allow the conversation, but answers will note missing docs.
profile = build_profile_text(answers)
flags = detect_tensions(answers)
flag_text = "\n".join(f"- {f}" for f in flags) if flags else "- (No obvious contradictions; explore their highest-stakes priorities.)"
seed = (
"Here is the patient's completed profile (16 yes/no answers):\n\n"
f"{profile}\n\n"
"FLAGGED TENSIONS / PRIORITIES TO EXPLORE:\n"
f"{flag_text}\n\n"
"Acknowledge in one short sentence that you have their profile. Do NOT ask a question yet "
"and do NOT list treatment options yet. Simply invite them to ask their question."
)
convo = [{"role": "user", "content": seed}]
try:
reply = gemini_reply(client, convo, 0)
except Exception as e:
gr.Warning(f"Gemini error: {e}")
return (gr.update(), [], [], 0, gr.update(visible=True), gr.update(visible=False))
convo.append({"role": "assistant", "content": reply})
chat_display = [{"role": "assistant", "content": reply}]
return (
chat_display,
chat_display,
convo,
0,
gr.update(visible=False),
gr.update(visible=True),
)
def respond(user_msg, chat_display, convo, n_followups, api_key):
user_msg = (user_msg or "").strip()
if not user_msg:
return chat_display, chat_display, convo, n_followups, ""
client, err = get_client(api_key)
if err:
gr.Warning(err)
return chat_display, chat_display, convo, n_followups, user_msg
ensure_kb(client) # no-op if already built
convo = convo + [{"role": "user", "content": user_msg}]
chat_display = chat_display + [{"role": "user", "content": user_msg}]
try:
reply = gemini_reply(client, convo, n_followups)
except Exception as e:
gr.Warning(f"Gemini error: {e}")
return chat_display, chat_display, convo, n_followups, ""
convo = convo + [{"role": "assistant", "content": reply}]
display_reply = reply
if "<<SUMMARY>>" in reply:
display_reply = reply.replace("<<SUMMARY>>", "").strip()
display_reply = "**Your Priorities Summary**\n\n" + display_reply + (
"\n\n_This summary reflects what you shared. Please bring it to your care team. "
"It is not medical advice._"
)
chat_display = chat_display + [{"role": "assistant", "content": display_reply}]
return chat_display, chat_display, convo, n_followups + 1, ""
def reset_all():
radio_resets = [gr.update(value=None) for _ in QUESTIONS]
return (
[],
[],
[],
0,
gr.update(visible=True),
gr.update(visible=False),
*radio_resets,
)
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
.gradio-container, .gradio-container * {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif !important;
}
.gradio-container {
background: #ffffff !important;
color: #111111 !important;
max-width: 880px !important;
margin: 0 auto !important;
}
body, .gradio-container .prose, .gradio-container p,
.gradio-container span, .gradio-container label {
color: #111111 !important;
}
#app-header {
border-bottom: 1px solid #e6e6e6;
padding: 4px 0 18px 0;
margin-bottom: 6px;
}
#app-title {
font-size: 1.7rem;
font-weight: 700;
color: #111111;
letter-spacing: -0.01em;
margin: 0;
}
.q-card {
border: 1px solid #e2e2e2 !important;
border-radius: 8px !important;
padding: 16px 18px !important;
margin-bottom: 12px !important;
background: #ffffff !important;
box-shadow: 0 1px 2px rgba(0,0,0,0.04) !important;
}
.q-card label, .q-card span {
color: #111111 !important;
font-weight: 500 !important;
}
.gradio-container input[type="radio"] + span,
.gradio-container .wrap label {
color: #111111 !important;
}
button.primary, .gradio-container button.primary {
background: #7a0c2e !important;
color: #ffffff !important;
border: none !important;
border-radius: 6px !important;
font-weight: 600 !important;
}
button.primary:hover, .gradio-container button.primary:hover {
background: #5f0a24 !important;
}
button.secondary, .gradio-container button.secondary {
background: #ffffff !important;
color: #111111 !important;
border: 1px solid #cfcfcf !important;
border-radius: 6px !important;
font-weight: 600 !important;
}
.gradio-container .chatbot, .gradio-container [class*="chatbot"] {
background: #ffffff !important;
border: 1px solid #e2e2e2 !important;
border-radius: 8px !important;
}
.gradio-container .message.bot, .gradio-container .message.user {
color: #111111 !important;
}
.gradio-container textarea, .gradio-container input[type="text"],
.gradio-container input[type="password"] {
background: #ffffff !important;
color: #111111 !important;
border: 1px solid #cfcfcf !important;
border-radius: 6px !important;
}
.gradio-container .label-wrap, .gradio-container details summary {
color: #111111 !important;
}
.sample-q {
background: #f7f7f7 !important;
border: 1px solid #e2e2e2 !important;
border-left: 3px solid #7a0c2e !important;
border-radius: 6px !important;
padding: 10px 14px !important;
margin: 12px 0 4px 0 !important;
}
.sample-q-label {
display: block;
font-size: 0.85rem;
font-weight: 600;
color: #5a5a5a !important;
margin-bottom: 4px;
}
.sample-q-text {
display: block;
font-size: 0.95rem;
color: #111111 !important;
background: #ffffff !important;
border: 1px solid #e2e2e2 !important;
border-radius: 4px !important;
padding: 6px 10px !important;
font-family: 'Inter', sans-serif !important;
}
footer {visibility: hidden;}
"""
with gr.Blocks(title="MyeAI (Myeloma AI)", css=CSS,
theme=gr.themes.Base(
primary_hue=gr.themes.colors.red,
neutral_hue=gr.themes.colors.gray,
font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
)) as demo:
with gr.Column(elem_id="app-header"):
gr.HTML(
"<h1 id='app-title'>MyeAI (Myeloma AI)</h1>"
)
api_key = gr.State("")
convo_state = gr.State([])
display_state = gr.State([])
followups = gr.State(0)
with gr.Group(visible=True) as intake_panel:
gr.Markdown("**Answer all 16 questions, then click Start Follow-up.**")
radios = []
for i, q in enumerate(QUESTIONS):
with gr.Row(elem_classes="q-card"):
r = gr.Radio(["Yes", "No"], label=f"{i+1}. {q}")
radios.append(r)
start_btn = gr.Button("Start Follow-up", variant="primary")
with gr.Group(visible=False) as chat_panel:
chatbot = gr.Chatbot(label="Follow-up Conversation", type="messages", height=460)
gr.HTML(
"<div class='sample-q'>"
"<span class='sample-q-label'>Sample question — copy and paste it into the box below to begin:</span>"
f"<code class='sample-q-text'>{FIRST_QUESTION}</code>"
"</div>"
)
with gr.Row():
msg = gr.Textbox(placeholder="Type or paste your question here...", show_label=False, scale=8)
send_btn = gr.Button("Send", variant="primary", scale=1)
restart_btn = gr.Button("Start Over")
start_btn.click(
start_chat,
inputs=[api_key] + radios,
outputs=[chatbot, display_state, convo_state, followups, intake_panel, chat_panel],
)
send_btn.click(
respond,
inputs=[msg, display_state, convo_state, followups, api_key],
outputs=[chatbot, display_state, convo_state, followups, msg],
)
msg.submit(
respond,
inputs=[msg, display_state, convo_state, followups, api_key],
outputs=[chatbot, display_state, convo_state, followups, msg],
)
restart_btn.click(
reset_all,
inputs=None,
outputs=[chatbot, display_state, convo_state, followups, intake_panel, chat_panel] + radios,
)
if __name__ == "__main__":
demo.launch()
# ----------------------------------------------------------------------
# HUGGING FACE SPACE SETUP NOTES
# ----------------------------------------------------------------------
# 1) Files in the Space repo:
# app.py
# knowledge_base.py
# Archive_2.zip <- the uploaded corpus (repo root)
# requirements.txt <- google-genai, gradio, numpy, pypdf, pdf2image, pytesseract, pillow
# packages.txt <- poppler-utils, tesseract-ocr (system deps for PDF text + OCR)
# 2) Space Secret:
# GEMINI_API_KEY = <your key>
# 3) Hardware: CPU basic is sufficient (no GPU needed).
# 4) On first launch the app extracts + embeds the corpus once and caches the
# index to _kb_index.npz, so subsequent restarts are fast.
|