Rajan Sharma commited on
Commit
eb04439
·
verified ·
1 Parent(s): 3f8efdd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -107
app.py CHANGED
@@ -61,41 +61,23 @@ USE_HOSTED_COHERE = bool(COHERE_API_KEY and _HAS_COHERE)
61
  # Larger output (Cohere + HF fallback)
62
  MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "2048"))
63
 
64
- # ---------- System Master (two-phase, LLM-only behavior) ----------
65
  SYSTEM_MASTER = """
66
  SYSTEM ROLE (fixed, always on)
67
- You are ClarityOps, a medical analytics system that interacts only via this chat.
68
 
69
  Absolute rules:
70
- - Use ONLY information provided in this conversation (scenario text + uploaded files).
71
  - Never invent data. If something required is missing after clarifications, output the literal token: INSUFFICIENT_DATA.
72
- - Always run in TWO PHASES:
73
- Phase 1: Ask up to 5 concise clarification questions, grouped by category (Prioritization, Capacity, Cost, Clinical, Recommendations). Then STOP and WAIT.
74
- Phase 2: After answers are provided, produce the final structured analysis exactly in the required format.
75
-
76
- Core behavior:
77
- - Read and synthesize any user-uploaded files (e.g., CSV/XLSX/PDF) relevant to the scenario.
78
  - Prefer analytics/longitudinal recommendations (risk targeting, follow-up, clustering) over generic ops advice.
79
- - Show all calculations explicitly for capacity and costs (e.g., “6 teams × 8 clients/day × 60 days = 2,880”).
80
  - Use correct clinical units and plausible ranges.
81
- - Include a brief “Provenance” section mapping each key output to scenario text, files, and/or clarified answers.
82
 
83
  Medical guardrails (always apply):
84
  - Units: BP in mmHg, A1c in %, BMI in kg/m², Total Cholesterol in mmol/L (or as provided), Percentages in %.
85
  - Plausible ranges: A1c 3–20 %, SBP 60–250 mmHg, DBP 30–150 mmHg, BMI 10–70 kg/m², Total Chol 2–12 mmol/L.
86
  - Privacy: avoid PHI; aggregate only; apply small-cell suppression where cohort < 10 (describe at a higher level).
87
  - When data includes mixed or ambiguous indicators, ask to confirm preferred indicators (e.g., obesity/metabolic syndrome vs self-reported diabetes).
88
-
89
- Formatting hard rules:
90
- - Phase 1 output MUST include the header line: “Clarification Questions”
91
- - Phase 2 output MUST include the header line: “Structured Analysis”
92
- - Phase 2 MUST follow this exact section order:
93
- 1. Prioritization
94
- 2. Capacity
95
- 3. Cost
96
- 4. Clinical Benefits
97
- 5. ClarityOps Top 3 Recommendations
98
- (Include a short Provenance block at the end.)
99
  """.strip()
100
 
101
  # ---------- Helpers ----------
@@ -132,8 +114,8 @@ def _sanitize_text(s: str) -> str:
132
  return s
133
  return re2.sub(r'[\p{C}--[\n\t]]+', '', s)
134
 
135
- def _history_to_prompt(message, history):
136
- parts = [f"System: {SYSTEM_MASTER}"]
137
  for u, a in _iter_user_assistant(history):
138
  if u: parts.append(f"User: {u}")
139
  if a: parts.append(f"Assistant: {a}")
@@ -142,12 +124,12 @@ def _history_to_prompt(message, history):
142
  return "\n".join(parts)
143
 
144
  # ---------- Cohere first ----------
145
- def cohere_chat(message, history):
146
  if not USE_HOSTED_COHERE:
147
  return None
148
  try:
149
  client = cohere.Client(api_key=COHERE_API_KEY)
150
- prompt = _history_to_prompt(message, history)
151
  resp = client.chat(
152
  model="command-r7b-12-2024",
153
  message=prompt,
@@ -190,8 +172,8 @@ def load_local_model():
190
  mdl.config.eos_token_id = tok.eos_token_id
191
  return mdl, tok
192
 
193
- def build_inputs(tokenizer, message, history):
194
- msgs = [{"role": "system", "content": SYSTEM_MASTER}]
195
  for u, a in _iter_user_assistant(history):
196
  if u: msgs.append({"role": "user", "content": u})
197
  if a: msgs.append({"role": "assistant", "content": a})
@@ -243,26 +225,63 @@ def _mdsi_block():
243
  "outcomes_summary": outcomes
244
  }, indent=2)
245
 
246
- # ---------- Core chat logic (two-phase) ----------
247
- def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answers=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  """
249
- awaiting_answers:
250
- - False: Phase 1 -> generate clarification questions and WAIT
251
- - True: Phase 2 -> consume clarifications and produce structured analysis
252
  """
253
  try:
254
- log_event("user_message", None, {"sizes": {"chars": len(user_msg or "")}})
255
 
256
  # Safety (input)
257
  safe_in, blocked_in, reason_in = safety_filter(user_msg, mode="input")
258
  if blocked_in:
259
  ans = refusal_reply(reason_in)
260
- return history + [(user_msg, ans)], awaiting_answers
261
 
262
  # Identity short-circuit
263
  if is_identity_query(safe_in, history):
264
  ans = "I am ClarityOps, your strategic decision making AI partner."
265
- return history + [(user_msg, ans)], awaiting_answers
266
 
267
  # Ingest uploads
268
  if uploaded_files_paths:
@@ -279,20 +298,9 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
279
  if re.search(r"\b(columns?|headers?)\b", (safe_in or "").lower()):
280
  cols = _session_rag.get_latest_csv_columns()
281
  if cols:
282
- return history + [(user_msg, "Here are the column names from your most recent CSV upload:\n\n- " + "\n- ".join(cols))], awaiting_answers
283
-
284
- # Pull CSV summaries (NEW) and format a concise block for the prompt
285
- csv_summaries = _session_rag.get_csv_summaries()
286
- csv_block = ""
287
- if csv_summaries:
288
- # Cap to 3 latest digests to keep prompt lean
289
- lines = ["Uploaded Data Summaries (auto-parsed)"]
290
- for s in csv_summaries[:3]:
291
- digest = s.get("digest") or ""
292
- lines.append(f"- {digest}")
293
- csv_block = "\n" + "\n".join(lines)
294
-
295
- # Session retrieval to enrich the system preamble
296
  session_snips = "\n---\n".join(_session_rag.retrieve(
297
  "diabetes screening Indigenous Métis mobile program cost throughput outcomes logistics",
298
  k=6
@@ -308,68 +316,104 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
308
  mdsi_extra = _mdsi_block() if ("diabetes" in user_lower or "mdsi" in user_lower or "mobile screening" in user_lower) else ""
309
 
310
  scenario_block = safe_in if len((safe_in or "")) > 0 else ""
311
- # Inject the CSV summary digests into the system preamble so both phases see them
312
- scenario_plus_csv = scenario_block + (("\n\n" + csv_block) if csv_block else "")
313
-
314
  system_preamble = build_system_preamble(
315
  snapshot=snapshot,
316
  policy_context=policy_context,
317
  computed_numbers=computed,
318
- scenario_text=scenario_plus_csv + (f"\n\nExecutive Pre-Computed Blocks:\n{mdsi_extra}" if mdsi_extra else ""),
319
  session_snips=session_snips
320
  )
321
 
322
- # Phase directive
323
- if not awaiting_answers:
324
- phase_directive = (
325
- "\n\n[INSTRUCTION TO MODEL]\n"
326
- "Produce **Phase 1** only: output a header 'Clarification Questions' and ask up to 5 concise, grouped questions "
327
- "(Prioritization, Capacity, Cost, Clinical, Recommendations). Then STOP and WAIT.\n"
328
- )
329
- else:
330
  phase_directive = (
331
  "\n\n[INSTRUCTION TO MODEL]\n"
332
  "Produce **Phase 2** only: output a header 'Structured Analysis' and follow the exact section order "
333
  "(Prioritization, Capacity, Cost, Clinical Benefits, ClarityOps Top 3 Recommendations). "
334
  "Use uploaded files + the user's latest answers as authoritative. Show calculations, units, and a brief Provenance.\n"
335
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
- augmented_user = SYSTEM_MASTER + "\n\n" + system_preamble + "\n\nUser message:\n" + safe_in + phase_directive
338
-
339
- # Call LLM
340
  out = cohere_chat(augmented_user, history)
341
  if not out:
342
  model, tokenizer = load_local_model()
343
  inputs = build_inputs(tokenizer, augmented_user, history)
344
  out = local_generate(model, tokenizer, inputs, max_new_tokens=MAX_NEW_TOKENS)
345
 
346
- # Clean + sanitize
347
  if isinstance(out, str):
348
  for tag in ("Assistant:", "System:", "User:"):
349
  if out.startswith(tag):
350
  out = out[len(tag):].strip()
351
  out = _sanitize_text(out)
352
 
353
- # Safety (output)
354
  safe_out, blocked_out, reason_out = safety_filter(out, mode="output")
355
  if blocked_out:
356
  safe_out = refusal_reply(reason_out)
357
 
358
- # Flip phase state based on headers
359
- new_awaiting = awaiting_answers
360
- low = safe_out.lower()
361
- if not awaiting_answers and "clarification questions" in low:
362
- new_awaiting = True
363
- elif awaiting_answers and "structured analysis" in low:
364
- new_awaiting = False
365
-
366
  log_event("assistant_reply", None, {
367
  **hash_summary("prompt", augmented_user if not PERSIST_CONTENT else ""),
368
  **hash_summary("reply", safe_out if not PERSIST_CONTENT else ""),
369
- "awaiting_next_phase": new_awaiting
370
  })
371
-
372
- return history + [(user_msg, safe_out)], new_awaiting
373
 
374
  except Exception as e:
375
  err = f"Error: {e}"
@@ -377,7 +421,7 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
377
  traceback.print_exc()
378
  except Exception:
379
  pass
380
- return history + [(user_msg, err)], awaiting_answers
381
 
382
  # ---------- Theme & CSS ----------
383
  theme = gr.themes.Soft(primary_hue="teal", neutral_hue="slate", radius_size=gr.themes.sizes.radius_lg)
@@ -410,13 +454,17 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
410
  gr.HTML("<h2>What can I help with?</h2>")
411
  with gr.Row(elem_classes="search-row"):
412
  hero_msg = gr.Textbox(
413
- placeholder="Ask anything (paste scenarios here; you can attach files after)...",
414
  show_label=False,
415
  lines=1,
416
  elem_classes="hero-box"
417
  )
418
  hero_send = gr.Button("➤", scale=0)
419
- gr.Markdown('<div class="hint">ClarityOps will first ask up to 5 clarifications, then produce a structured analysis.</div>')
 
 
 
 
420
 
421
  # --- MAIN APP (hidden until first message) ---
422
  with gr.Column(elem_id="chat-container", visible=False) as app_wrap:
@@ -430,7 +478,7 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
430
  msg = gr.Textbox(
431
  label="",
432
  show_label=False,
433
- placeholder="Continue here. Paste scenario details, add files below.",
434
  scale=10
435
  )
436
  send = gr.Button("Send", scale=1)
@@ -439,8 +487,9 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
439
  # ---- State
440
  state_history = gr.State(value=[])
441
  state_uploaded = gr.State(value=[])
442
- state_awaiting = gr.State(value=False) # False -> Phase 1 next; True -> awaiting answers for Phase 2
443
 
 
444
  def _store_uploads(files, current):
445
  paths = []
446
  for f in (files or []):
@@ -449,25 +498,27 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
449
 
450
  uploads.change(fn=_store_uploads, inputs=[uploads, state_uploaded], outputs=state_uploaded)
451
 
452
- def _on_send(user_msg, history, up_paths, awaiting):
 
453
  try:
454
  if not user_msg or not user_msg.strip():
455
- return history, "", history, awaiting
456
- new_history, new_awaiting = clarityops_reply(
457
- user_msg.strip(), history or [], None, up_paths or [], awaiting_answers=awaiting
458
  )
459
- return new_history, "", new_history, new_awaiting
460
  except Exception as e:
461
  err = f"Error: {e}"
462
  try: traceback.print_exc()
463
  except Exception: pass
464
  new_hist = (history or []) + [(user_msg or "", err)]
465
- return new_hist, "", new_hist, awaiting
466
 
467
- def _hero_start(user_msg, history, up_paths, awaiting):
468
- chat_o, msg_o, hist_o, await_o = _on_send(user_msg, history, up_paths, awaiting)
 
469
  return (
470
- chat_o, msg_o, hist_o, await_o,
471
  gr.update(visible=False), # hide hero
472
  gr.update(visible=True), # show app
473
  "" # clear hero box
@@ -475,39 +526,36 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
475
 
476
  hero_send.click(
477
  _hero_start,
478
- inputs=[hero_msg, state_history, state_uploaded, state_awaiting],
479
- outputs=[chat, msg, state_history, state_awaiting, hero_wrap, app_wrap, hero_msg],
480
  concurrency_limit=2, queue=True
481
  )
482
  hero_msg.submit(
483
  _hero_start,
484
- inputs=[hero_msg, state_history, state_uploaded, state_awaiting],
485
- outputs=[chat, msg, state_history, state_awaiting, hero_wrap, app_wrap, hero_msg],
486
  concurrency_limit=2, queue=True
487
  )
488
 
489
- send.click(_on_send, inputs=[msg, state_history, state_uploaded, state_awaiting],
490
- outputs=[chat, msg, state_history, state_awaiting],
 
491
  concurrency_limit=2, queue=True)
492
- msg.submit(_on_send, inputs=[msg, state_history, state_uploaded, state_awaiting],
493
- outputs=[chat, msg, state_history, state_awaiting],
494
  concurrency_limit=2, queue=True)
495
 
496
  def _on_clear():
 
497
  return (
498
- [], "", [], False,
499
  gr.update(visible=True), # show hero
500
  gr.update(visible=False), # hide app
501
  "" # clear hero input
502
  )
503
 
504
- clear.click(_on_clear, None, [chat, msg, state_history, state_awaiting, hero_wrap, app_wrap, hero_msg])
505
 
506
  if __name__ == "__main__":
507
  port = int(os.environ.get("PORT", "7860"))
508
  demo.launch(server_name="0.0.0.0", server_port=port, show_api=False, max_threads=8)
509
-
510
-
511
-
512
-
513
-
 
61
  # Larger output (Cohere + HF fallback)
62
  MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "2048"))
63
 
64
+ # ---------- Fixed System Preamble for Medical Guardrails ----------
65
  SYSTEM_MASTER = """
66
  SYSTEM ROLE (fixed, always on)
67
+ You are ClarityOps, a medical analytics assistant.
68
 
69
  Absolute rules:
70
+ - Use ONLY information provided in this conversation (user messages + uploaded files).
71
  - Never invent data. If something required is missing after clarifications, output the literal token: INSUFFICIENT_DATA.
 
 
 
 
 
 
72
  - Prefer analytics/longitudinal recommendations (risk targeting, follow-up, clustering) over generic ops advice.
73
+ - Show all calculations explicitly when computing capacity and cost.
74
  - Use correct clinical units and plausible ranges.
 
75
 
76
  Medical guardrails (always apply):
77
  - Units: BP in mmHg, A1c in %, BMI in kg/m², Total Cholesterol in mmol/L (or as provided), Percentages in %.
78
  - Plausible ranges: A1c 3–20 %, SBP 60–250 mmHg, DBP 30–150 mmHg, BMI 10–70 kg/m², Total Chol 2–12 mmol/L.
79
  - Privacy: avoid PHI; aggregate only; apply small-cell suppression where cohort < 10 (describe at a higher level).
80
  - When data includes mixed or ambiguous indicators, ask to confirm preferred indicators (e.g., obesity/metabolic syndrome vs self-reported diabetes).
 
 
 
 
 
 
 
 
 
 
 
81
  """.strip()
82
 
83
  # ---------- Helpers ----------
 
114
  return s
115
  return re2.sub(r'[\p{C}--[\n\t]]+', '', s)
116
 
117
+ def _history_to_prompt(message, history, system_text):
118
+ parts = [f"System: {system_text}"]
119
  for u, a in _iter_user_assistant(history):
120
  if u: parts.append(f"User: {u}")
121
  if a: parts.append(f"Assistant: {a}")
 
124
  return "\n".join(parts)
125
 
126
  # ---------- Cohere first ----------
127
+ def cohere_chat(message, history, system_text=SYSTEM_MASTER):
128
  if not USE_HOSTED_COHERE:
129
  return None
130
  try:
131
  client = cohere.Client(api_key=COHERE_API_KEY)
132
+ prompt = _history_to_prompt(message, history, system_text)
133
  resp = client.chat(
134
  model="command-r7b-12-2024",
135
  message=prompt,
 
172
  mdl.config.eos_token_id = tok.eos_token_id
173
  return mdl, tok
174
 
175
+ def build_inputs(tokenizer, message, history, system_text=SYSTEM_MASTER):
176
+ msgs = [{"role": "system", "content": system_text}]
177
  for u, a in _iter_user_assistant(history):
178
  if u: msgs.append({"role": "user", "content": u})
179
  if a: msgs.append({"role": "assistant", "content": a})
 
225
  "outcomes_summary": outcomes
226
  }, indent=2)
227
 
228
+ # ---------- Scenario auto-detection ----------
229
+ _SCENARIO_HEADINGS = [
230
+ "context", "background", "scenario", "case study",
231
+ "data inputs", "inputs", "evaluation questions", "questions",
232
+ "recommendations", "deployment strategy", "next steps", "assumptions"
233
+ ]
234
+ _SCENARIO_KEYWORDS = [
235
+ "diabetes", "screening", "metabolic", "prevalence", "capacity",
236
+ "cost", "startup", "ongoing", "clinical", "a1c", "mmhg", "bmi",
237
+ "cholesterol", "settlements", "program", "mobile", "ops", "throughput"
238
+ ]
239
+
240
+ def _looks_like_scenario(text: str, uploaded_paths) -> bool:
241
+ if not text:
242
+ return False
243
+ t = text.strip()
244
+ low = t.lower()
245
+
246
+ # 1) Length + structure signals
247
+ if len(t) >= 450 and any(h in low for h in _SCENARIO_HEADINGS):
248
+ return True
249
+
250
+ # 2) Strong clinical/ops vocabulary density
251
+ kw_hits = sum(1 for k in _SCENARIO_KEYWORDS if k in low)
252
+ if len(t) >= 350 and kw_hits >= 4:
253
+ return True
254
+
255
+ # 3) Table/percent/metrics hints
256
+ if re.search(r"\b\d{2,4}\b", low) and re.search(r"%|\bmmhg\b|\bbmi\b|\ba1c\b", low):
257
+ if len(t) >= 300:
258
+ return True
259
+
260
+ # 4) Files attached (CSV/PDF/DOCX) + domain keywords
261
+ if uploaded_paths and kw_hits >= 2:
262
+ return True
263
+
264
+ return False
265
+
266
+ # ---------- Core chat logic (auto scenario) ----------
267
+ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, mode="chat"):
268
  """
269
+ mode: "chat" (default) or "awaiting_answers"
270
+ Returns: (updated_history, updated_mode)
 
271
  """
272
  try:
273
+ log_event("user_message", None, {"sizes": {"chars": len(user_msg or "")}, "mode": mode})
274
 
275
  # Safety (input)
276
  safe_in, blocked_in, reason_in = safety_filter(user_msg, mode="input")
277
  if blocked_in:
278
  ans = refusal_reply(reason_in)
279
+ return history + [(user_msg, ans)], mode
280
 
281
  # Identity short-circuit
282
  if is_identity_query(safe_in, history):
283
  ans = "I am ClarityOps, your strategic decision making AI partner."
284
+ return history + [(user_msg, ans)], mode
285
 
286
  # Ingest uploads
287
  if uploaded_files_paths:
 
298
  if re.search(r"\b(columns?|headers?)\b", (safe_in or "").lower()):
299
  cols = _session_rag.get_latest_csv_columns()
300
  if cols:
301
+ return history + [(user_msg, "Here are the column names from your most recent CSV upload:\n\n- " + "\n- ".join(cols))], mode
302
+
303
+ # Session retrieval & context
 
 
 
 
 
 
 
 
 
 
 
304
  session_snips = "\n---\n".join(_session_rag.retrieve(
305
  "diabetes screening Indigenous Métis mobile program cost throughput outcomes logistics",
306
  k=6
 
316
  mdsi_extra = _mdsi_block() if ("diabetes" in user_lower or "mdsi" in user_lower or "mobile screening" in user_lower) else ""
317
 
318
  scenario_block = safe_in if len((safe_in or "")) > 0 else ""
 
 
 
319
  system_preamble = build_system_preamble(
320
  snapshot=snapshot,
321
  policy_context=policy_context,
322
  computed_numbers=computed,
323
+ scenario_text=scenario_block + (f"\n\nExecutive Pre-Computed Blocks:\n{mdsi_extra}" if mdsi_extra else ""),
324
  session_snips=session_snips
325
  )
326
 
327
+ # -------- Auto-routing --------
328
+ if mode == "awaiting_answers":
329
+ # Any reply now triggers Phase 2
 
 
 
 
 
330
  phase_directive = (
331
  "\n\n[INSTRUCTION TO MODEL]\n"
332
  "Produce **Phase 2** only: output a header 'Structured Analysis' and follow the exact section order "
333
  "(Prioritization, Capacity, Cost, Clinical Benefits, ClarityOps Top 3 Recommendations). "
334
  "Use uploaded files + the user's latest answers as authoritative. Show calculations, units, and a brief Provenance.\n"
335
  )
336
+ augmented_user = SYSTEM_MASTER + "\n\n" + system_preamble + "\n\nClarification answers from user:\n" + (safe_in or "<none>") + phase_directive
337
+
338
+ out = cohere_chat(augmented_user, history)
339
+ if not out:
340
+ model, tokenizer = load_local_model()
341
+ inputs = build_inputs(tokenizer, augmented_user, history)
342
+ out = local_generate(model, tokenizer, inputs, max_new_tokens=MAX_NEW_TOKENS)
343
+
344
+ if isinstance(out, str):
345
+ for tag in ("Assistant:", "System:", "User:"):
346
+ if out.startswith(tag):
347
+ out = out[len(tag):].strip()
348
+ out = _sanitize_text(out)
349
+
350
+ safe_out, blocked_out, reason_out = safety_filter(out, mode="output")
351
+ if blocked_out:
352
+ safe_out = refusal_reply(reason_out)
353
+
354
+ log_event("assistant_reply", None, {
355
+ **hash_summary("prompt", augmented_user if not PERSIST_CONTENT else ""),
356
+ **hash_summary("reply", safe_out if not PERSIST_CONTENT else ""),
357
+ "awaiting_next_phase": False
358
+ })
359
+ return history + [(user_msg, safe_out)], "chat"
360
+
361
+ # Normal chat — unless it looks like a scenario
362
+ if not _looks_like_scenario(safe_in, uploaded_files_paths):
363
+ normal_user = SYSTEM_MASTER + "\n\n" + system_preamble + "\n\nUser message:\n" + safe_in
364
+ out = cohere_chat(normal_user, history)
365
+ if not out:
366
+ model, tokenizer = load_local_model()
367
+ inputs = build_inputs(tokenizer, normal_user, history)
368
+ out = local_generate(model, tokenizer, inputs, max_new_tokens=MAX_NEW_TOKENS)
369
+
370
+ if isinstance(out, str):
371
+ for tag in ("Assistant:", "System:", "User:"):
372
+ if out.startswith(tag):
373
+ out = out[len(tag):].strip()
374
+ out = _sanitize_text(out)
375
+
376
+ safe_out, blocked_out, reason_out = safety_filter(out, mode="output")
377
+ if blocked_out:
378
+ safe_out = refusal_reply(reason_out)
379
+
380
+ log_event("assistant_reply", None, {
381
+ **hash_summary("prompt", normal_user if not PERSIST_CONTENT else ""),
382
+ **hash_summary("reply", safe_out if not PERSIST_CONTENT else ""),
383
+ "awaiting_next_phase": False
384
+ })
385
+ return history + [(user_msg, safe_out)], "chat"
386
+
387
+ # Scenario detected -> Phase 1
388
+ phase_directive = (
389
+ "\n\n[INSTRUCTION TO MODEL]\n"
390
+ "Produce **Phase 1** only: output a header 'Clarification Questions' and ask up to 5 concise, grouped questions "
391
+ "(Prioritization, Capacity, Cost, Clinical, Recommendations). Then STOP and WAIT.\n"
392
+ )
393
+ augmented_user = SYSTEM_MASTER + "\n\n" + system_preamble + "\n\nUser scenario:\n" + safe_in + phase_directive
394
 
 
 
 
395
  out = cohere_chat(augmented_user, history)
396
  if not out:
397
  model, tokenizer = load_local_model()
398
  inputs = build_inputs(tokenizer, augmented_user, history)
399
  out = local_generate(model, tokenizer, inputs, max_new_tokens=MAX_NEW_TOKENS)
400
 
 
401
  if isinstance(out, str):
402
  for tag in ("Assistant:", "System:", "User:"):
403
  if out.startswith(tag):
404
  out = out[len(tag):].strip()
405
  out = _sanitize_text(out)
406
 
 
407
  safe_out, blocked_out, reason_out = safety_filter(out, mode="output")
408
  if blocked_out:
409
  safe_out = refusal_reply(reason_out)
410
 
 
 
 
 
 
 
 
 
411
  log_event("assistant_reply", None, {
412
  **hash_summary("prompt", augmented_user if not PERSIST_CONTENT else ""),
413
  **hash_summary("reply", safe_out if not PERSIST_CONTENT else ""),
414
+ "awaiting_next_phase": True
415
  })
416
+ return history + [(user_msg, safe_out)], "awaiting_answers"
 
417
 
418
  except Exception as e:
419
  err = f"Error: {e}"
 
421
  traceback.print_exc()
422
  except Exception:
423
  pass
424
+ return history + [(user_msg, err)], mode
425
 
426
  # ---------- Theme & CSS ----------
427
  theme = gr.themes.Soft(primary_hue="teal", neutral_hue="slate", radius_size=gr.themes.sizes.radius_lg)
 
454
  gr.HTML("<h2>What can I help with?</h2>")
455
  with gr.Row(elem_classes="search-row"):
456
  hero_msg = gr.Textbox(
457
+ placeholder="Ask anything paste a scenario (and attach files) to trigger structured analysis.",
458
  show_label=False,
459
  lines=1,
460
  elem_classes="hero-box"
461
  )
462
  hero_send = gr.Button("➤", scale=0)
463
+ gr.Markdown(
464
+ '<div class="hint">Tip: Pasting a structured medical scenario (with sections like '
465
+ '<i>Context, Data Inputs, Evaluation Questions</i>) will auto-trigger clarifications first, '
466
+ 'then the final analysis. CSVs are auto-summarized.</div>'
467
+ )
468
 
469
  # --- MAIN APP (hidden until first message) ---
470
  with gr.Column(elem_id="chat-container", visible=False) as app_wrap:
 
478
  msg = gr.Textbox(
479
  label="",
480
  show_label=False,
481
+ placeholder="Chat freely… Paste a scenario to auto-start clarifications.",
482
  scale=10
483
  )
484
  send = gr.Button("Send", scale=1)
 
487
  # ---- State
488
  state_history = gr.State(value=[])
489
  state_uploaded = gr.State(value=[])
490
+ state_mode = gr.State(value="chat") # "chat" or "awaiting_answers"
491
 
492
+ # ---- Uploads
493
  def _store_uploads(files, current):
494
  paths = []
495
  for f in (files or []):
 
498
 
499
  uploads.change(fn=_store_uploads, inputs=[uploads, state_uploaded], outputs=state_uploaded)
500
 
501
+ # ---- Core send (used by both hero input and chat input)
502
+ def _on_send(user_msg, history, up_paths, mode):
503
  try:
504
  if not user_msg or not user_msg.strip():
505
+ return history, "", history, mode
506
+ new_history, new_mode = clarityops_reply(
507
+ user_msg.strip(), history or [], None, up_paths or [], mode=mode
508
  )
509
+ return new_history, "", new_history, new_mode
510
  except Exception as e:
511
  err = f"Error: {e}"
512
  try: traceback.print_exc()
513
  except Exception: pass
514
  new_hist = (history or []) + [(user_msg or "", err)]
515
+ return new_hist, "", new_hist, mode
516
 
517
+ # ---- Hero -> App transition + first send
518
+ def _hero_start(user_msg, history, up_paths, mode):
519
+ chat_o, msg_o, hist_o, mode_o = _on_send(user_msg, history, up_paths, mode)
520
  return (
521
+ chat_o, msg_o, hist_o, mode_o,
522
  gr.update(visible=False), # hide hero
523
  gr.update(visible=True), # show app
524
  "" # clear hero box
 
526
 
527
  hero_send.click(
528
  _hero_start,
529
+ inputs=[hero_msg, state_history, state_uploaded, state_mode],
530
+ outputs=[chat, msg, state_history, state_mode, hero_wrap, app_wrap, hero_msg],
531
  concurrency_limit=2, queue=True
532
  )
533
  hero_msg.submit(
534
  _hero_start,
535
+ inputs=[hero_msg, state_history, state_uploaded, state_mode],
536
+ outputs=[chat, msg, state_history, state_mode, hero_wrap, app_wrap, hero_msg],
537
  concurrency_limit=2, queue=True
538
  )
539
 
540
+ # ---- Normal chat interactions after hero is gone
541
+ send.click(_on_send, inputs=[msg, state_history, state_uploaded, state_mode],
542
+ outputs=[chat, msg, state_history, state_mode],
543
  concurrency_limit=2, queue=True)
544
+ msg.submit(_on_send, inputs=[msg, state_history, state_uploaded, state_mode],
545
+ outputs=[chat, msg, state_history, state_mode],
546
  concurrency_limit=2, queue=True)
547
 
548
  def _on_clear():
549
+ # reset to fresh hero screen and chat mode
550
  return (
551
+ [], "", [], "chat",
552
  gr.update(visible=True), # show hero
553
  gr.update(visible=False), # hide app
554
  "" # clear hero input
555
  )
556
 
557
+ clear.click(_on_clear, None, [chat, msg, state_history, state_mode, hero_wrap, app_wrap, hero_msg])
558
 
559
  if __name__ == "__main__":
560
  port = int(os.environ.get("PORT", "7860"))
561
  demo.launch(server_name="0.0.0.0", server_port=port, show_api=False, max_threads=8)