Rajan Sharma commited on
Commit
b9b3e60
·
verified ·
1 Parent(s): d312515

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +58 -102
app.py CHANGED
@@ -1,10 +1,8 @@
1
- import os, re, json, traceback
2
  from functools import lru_cache
3
 
4
  import gradio as gr
5
  import torch
6
-
7
- # NEW: robust control-char sanitizer (requires `regex` package)
8
  import regex as re2 # pip install regex
9
 
10
  from settings import SNAPSHOT_PATH, PERSIST_CONTENT
@@ -12,29 +10,26 @@ from audit_log import log_event, hash_summary
12
  from privacy import redact_text
13
 
14
  # ---------- Environment / cache (Spaces-safe, writable) ----------
15
- # Hugging Face caches
16
- os.environ.setdefault("HF_HOME", "/data/.cache/huggingface")
17
- os.environ.setdefault("HF_HUB_CACHE", "/data/.cache/huggingface/hub")
18
- os.environ.setdefault("TRANSFORMERS_CACHE", "/data/.cache/huggingface/transformers")
19
-
20
- # SentenceTransformers cache (used by retriever.py)
21
- os.environ.setdefault("SENTENCE_TRANSFORMERS_HOME", "/data/.cache/sentence-transformers")
22
-
23
- # Gradio temp/cache
24
- os.environ.setdefault("GRADIO_TEMP_DIR", "/data/gradio")
25
- os.environ.setdefault("GRADIO_CACHE_DIR", "/data/gradio")
26
-
27
- # Disable experimental xet transport; use stable transfer
 
 
 
28
  os.environ.setdefault("HF_HUB_ENABLE_XET", "0")
29
  os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
30
 
31
- for p in [
32
- "/data/.cache/huggingface",
33
- "/data/.cache/huggingface/hub",
34
- "/data/.cache/huggingface/transformers",
35
- "/data/.cache/sentence-transformers",
36
- "/data/gradio",
37
- ]:
38
  try:
39
  os.makedirs(p, exist_ok=True)
40
  except Exception:
@@ -114,15 +109,9 @@ def pick_dtype_and_map():
114
 
115
  def is_identity_query(message, history):
116
  patterns = [
117
- r"\bwho\s+are\s+you\b",
118
- r"\bwhat\s+are\s+you\b",
119
- r"\bwhat\s+is\s+your\s+name\b",
120
- r"\bwho\s+is\s+this\b",
121
- r"\bidentify\s+yourself\b",
122
- r"\btell\s+me\s+about\s+yourself\b",
123
- r"\bdescribe\s+yourself\b",
124
- r"\band\s+you\s*\?\b",
125
- r"\byour\s+name\b",
126
  r"\bwho\s+am\s+i\s+chatting\s+with\b",
127
  ]
128
  def match(t): return any(re.search(p, (t or "").strip().lower()) for p in patterns)
@@ -139,13 +128,13 @@ def _iter_user_assistant(history):
139
  a = item[1] if len(item) > 1 else ""
140
  yield u, a
141
 
 
 
 
 
 
142
  def _history_to_prompt(message, history):
143
- """
144
- Build a simple chat-style prompt INCLUDING the System Master preamble.
145
- """
146
- parts = []
147
- # system master always first
148
- parts.append(f"System: {SYSTEM_MASTER}")
149
  for u, a in _iter_user_assistant(history):
150
  if u: parts.append(f"User: {u}")
151
  if a: parts.append(f"Assistant: {a}")
@@ -153,20 +142,11 @@ def _history_to_prompt(message, history):
153
  parts.append("Assistant:")
154
  return "\n".join(parts)
155
 
156
- def _sanitize_text(s: str) -> str:
157
- """
158
- Strip control characters (except newline/tab) to avoid garbled UI output.
159
- """
160
- if not isinstance(s, str):
161
- return s
162
- return re2.sub(r'[\p{C}--[\n\t]]+', '', s)
163
-
164
- # ---------- Cohere (default path) ----------
165
  def cohere_chat(message, history):
166
  if not USE_HOSTED_COHERE:
167
  return None
168
  try:
169
- # Create client on demand to avoid init errors in some environments
170
  client = cohere.Client(api_key=COHERE_API_KEY)
171
  prompt = _history_to_prompt(message, history)
172
  resp = client.chat(
@@ -182,7 +162,7 @@ def cohere_chat(message, history):
182
  except Exception:
183
  return None
184
 
185
- # ---------- Local model (accelerate-safe fallback) ----------
186
  @lru_cache(maxsize=1)
187
  def load_local_model():
188
  if not HF_TOKEN:
@@ -192,16 +172,19 @@ def load_local_model():
192
  tok = AutoTokenizer.from_pretrained(
193
  MODEL_ID, token=HF_TOKEN, use_fast=True, model_max_length=8192,
194
  padding_side="left", trust_remote_code=True,
 
195
  )
196
  try:
197
  mdl = AutoModelForCausalLM.from_pretrained(
198
  MODEL_ID, token=HF_TOKEN, device_map=device_map,
199
  low_cpu_mem_usage=True, torch_dtype=dtype, trust_remote_code=True,
 
200
  )
201
  except Exception:
202
  mdl = AutoModelForCausalLM.from_pretrained(
203
  MODEL_ID, token=HF_TOKEN,
204
  low_cpu_mem_usage=True, torch_dtype=dtype, trust_remote_code=True,
 
205
  )
206
  mdl.to("cuda" if torch.cuda.is_available() else "cpu")
207
  if mdl.config.eos_token_id is None and tok.eos_token_id is not None:
@@ -209,9 +192,7 @@ def load_local_model():
209
  return mdl, tok
210
 
211
  def build_inputs(tokenizer, message, history):
212
- msgs = []
213
- # Always inject system master into the chat template, if supported
214
- msgs.append({"role": "system", "content": SYSTEM_MASTER})
215
  for u, a in _iter_user_assistant(history):
216
  if u: msgs.append({"role": "user", "content": u})
217
  if a: msgs.append({"role": "assistant", "content": a})
@@ -233,7 +214,7 @@ def local_generate(model, tokenizer, input_ids, max_new_tokens=MAX_NEW_TOKENS):
233
  gen_only = out[0, input_ids.shape[-1]:]
234
  return tokenizer.decode(gen_only, skip_special_tokens=True).strip()
235
 
236
- # ---------- Snapshot loader ----------
237
  def _load_snapshot(path=SNAPSHOT_PATH):
238
  try:
239
  with open(path, "r", encoding="utf-8") as f:
@@ -248,11 +229,9 @@ def _load_snapshot(path=SNAPSHOT_PATH):
248
  "isolation_needs_waiting": {"contact": 3, "airborne": 1}, "telemetry_needed_waiting": 5
249
  }
250
 
251
- # ---------- Init retrieval engines ----------
252
  init_retriever()
253
- _session_rag = SessionRAG() # in-memory; supports artifacts (CSV columns)
254
 
255
- # ---------- Executive pre-compute (MDSi block) ----------
256
  def _mdsi_block():
257
  base_capacity = capacity_projection(18, 48, 6)
258
  cons_capacity = capacity_projection(12, 48, 6)
@@ -265,12 +244,12 @@ def _mdsi_block():
265
  "outcomes_summary": outcomes
266
  }, indent=2)
267
 
268
- # ---------- Core chat logic with two-phase behavior ----------
269
  def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answers=False):
270
  """
271
  awaiting_answers:
272
- - False: Phase 1 mode -> generate clarification questions and WAIT
273
- - True: Phase 2 mode -> consume clarifications and produce structured analysis
274
  """
275
  try:
276
  log_event("user_message", None, {"sizes": {"chars": len(user_msg or "")}})
@@ -286,23 +265,7 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
286
  ans = "I am ClarityOps, your strategic decision making AI partner."
287
  return history + [(user_msg, ans)], awaiting_answers
288
 
289
- # Debug slash command: /diag
290
- if (safe_in or "").strip().lower().startswith("/diag"):
291
- try:
292
- chunk_count = len(getattr(_session_rag, "texts", []) or [])
293
- cols = _session_rag.get_latest_csv_columns()
294
- sample = _session_rag.retrieve("the", k=2)
295
- msg = [
296
- f"Chunks in session: {chunk_count}",
297
- f"Latest CSV columns: {', '.join(cols) if cols else '<none>'}",
298
- "Sample retrieved snippets:",
299
- *(sample or ["<no snippets>"])
300
- ]
301
- return history + [(user_msg, "\n\n".join(msg))], awaiting_answers
302
- except Exception as e:
303
- return history + [(user_msg, f"Diag error: {e}")], awaiting_answers
304
-
305
- # Ingest uploads: returns chunks + artifacts
306
  if uploaded_files_paths:
307
  ing = extract_text_from_files(uploaded_files_paths)
308
  chunks = ing.get("chunks", []) if isinstance(ing, dict) else (ing or [])
@@ -313,26 +276,24 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
313
  _session_rag.register_artifacts(artifacts)
314
  log_event("uploads_added", None, {"chunks": len(chunks), "artifacts": len(artifacts)})
315
 
316
- # Deterministic CSV "columns/headers" handler
317
  if re.search(r"\b(columns?|headers?)\b", (safe_in or "").lower()):
318
  cols = _session_rag.get_latest_csv_columns()
319
  if cols:
320
  return history + [(user_msg, "Here are the column names from your most recent CSV upload:\n\n- " + "\n- ".join(cols))], awaiting_answers
321
 
322
- # Retrieve from session uploads (text chunks)
323
  session_snips = "\n---\n".join(_session_rag.retrieve(
324
- "diabetes screening Indigenous Métis mobile program cost throughput outcomes logistics bed flow staffing discharge forecast",
325
  k=6
326
  ))
327
 
328
- # Load daily snapshot + policies + computed ops numbers
329
  snapshot = _load_snapshot()
330
  policy_context = retrieve_context(
331
- "mobile diabetes screening Indigenous community outreach logistics referral pathways cultural safety data governance cost effectiveness outcomes bed management discharge acceleration ambulance offload"
332
  )
333
  computed = compute_operational_numbers(snapshot)
334
 
335
- # Exec scenario detect (MDSi)
336
  user_lower = (safe_in or "").lower()
337
  mdsi_extra = _mdsi_block() if ("diabetes" in user_lower or "mdsi" in user_lower or "mobile screening" in user_lower) else ""
338
 
@@ -345,7 +306,7 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
345
  session_snips=session_snips
346
  )
347
 
348
- # Phase-specific instruction appended to the user content
349
  if not awaiting_answers:
350
  phase_directive = (
351
  "\n\n[INSTRUCTION TO MODEL]\n"
@@ -362,16 +323,14 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
362
 
363
  augmented_user = SYSTEM_MASTER + "\n\n" + system_preamble + "\n\nUser message:\n" + safe_in + phase_directive
364
 
365
- # Cohere first
366
  out = cohere_chat(augmented_user, history)
367
-
368
- # Fallback to local HF model if Cohere not set or failed
369
  if not out:
370
  model, tokenizer = load_local_model()
371
  inputs = build_inputs(tokenizer, augmented_user, history)
372
  out = local_generate(model, tokenizer, inputs, max_new_tokens=MAX_NEW_TOKENS)
373
 
374
- # Tidy echoes and sanitize
375
  if isinstance(out, str):
376
  for tag in ("Assistant:", "System:", "User:"):
377
  if out.startswith(tag):
@@ -383,16 +342,14 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
383
  if blocked_out:
384
  safe_out = refusal_reply(reason_out)
385
 
386
- # Decide next state:
387
- # If we just asked clarifications, set awaiting_answers=True.
388
- # If we just produced structured analysis, set awaiting_answers=False.
389
  new_awaiting = awaiting_answers
390
- if not awaiting_answers and "clarification questions" in safe_out.lower():
 
391
  new_awaiting = True
392
- elif awaiting_answers and "structured analysis" in safe_out.lower():
393
  new_awaiting = False
394
 
395
- # Audit (content-free fingerprints)
396
  log_event("assistant_reply", None, {
397
  **hash_summary("prompt", augmented_user if not PERSIST_CONTENT else ""),
398
  **hash_summary("reply", safe_out if not PERSIST_CONTENT else ""),
@@ -400,6 +357,7 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
400
  })
401
 
402
  return history + [(user_msg, safe_out)], new_awaiting
 
403
  except Exception as e:
404
  err = f"Error: {e}"
405
  try:
@@ -438,7 +396,7 @@ textarea, input, .gr-input { border-radius: 12px !important; }
438
  #chat-container { position: relative; }
439
  """
440
 
441
- # ---------- UI (single window; uploads at bottom) ----------
442
  with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
443
  gr.Markdown("# ClarityOps Augmented Decision AI")
444
 
@@ -466,7 +424,7 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
466
 
467
  state_history = gr.State(value=[])
468
  state_uploaded = gr.State(value=[])
469
- state_awaiting = gr.State(value=False) # False = Phase 1 next; True = awaiting answers for Phase 2
470
 
471
  def _store_uploads(files, current):
472
  paths = []
@@ -477,19 +435,18 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
477
  uploads.change(fn=_store_uploads, inputs=[uploads, state_uploaded], outputs=state_uploaded)
478
 
479
  def _on_send(user_msg, history, up_paths, awaiting):
480
- # Hide handshake on first interaction by returning a class change
481
  hide_overlay_js = gr.update(value='<div id="handshake-overlay" class="hidden"></div>')
482
  try:
483
  if not user_msg or not user_msg.strip():
484
  return history, "", history, awaiting, hide_overlay_js
485
- new_history, new_awaiting = clarityops_reply(user_msg.strip(), history or [], None, up_paths or [], awaiting_answers=awaiting)
 
 
486
  return new_history, "", new_history, new_awaiting, hide_overlay_js
487
  except Exception as e:
488
  err = f"Error: {e}"
489
- try:
490
- traceback.print_exc()
491
- except Exception:
492
- pass
493
  new_hist = (history or []) + [(user_msg or "", err)]
494
  return new_hist, "", new_hist, awaiting, hide_overlay_js
495
 
@@ -502,7 +459,6 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
502
  concurrency_limit=2, queue=True)
503
 
504
  def _on_clear():
505
- # Reset everything, show handshake again
506
  return [], "", [], False, '<div id="handshake-overlay">ClarityOps loaded. Paste your scenario and attach files. I’ll ask up to 5 clarifications, then produce the structured analysis</div>'
507
 
508
  clear.click(_on_clear, None, [chat, msg, state_history, state_awaiting, handshake])
 
1
+ import os, re, json, traceback, pathlib
2
  from functools import lru_cache
3
 
4
  import gradio as gr
5
  import torch
 
 
6
  import regex as re2 # pip install regex
7
 
8
  from settings import SNAPSHOT_PATH, PERSIST_CONTENT
 
10
  from privacy import redact_text
11
 
12
  # ---------- Environment / cache (Spaces-safe, writable) ----------
13
+ HOME = pathlib.Path.home() # /home/user on Spaces
14
+ HF_HOME = str(HOME / ".cache" / "huggingface")
15
+ HF_HUB_CACHE = str(HOME / ".cache" / "huggingface" / "hub")
16
+ HF_TRANSFORMERS = str(HOME / ".cache" / "huggingface" / "transformers")
17
+ ST_HOME = str(HOME / ".cache" / "sentence-transformers")
18
+ GRADIO_TMP = str(HOME / "app" / "gradio") # you can switch to "/tmp/gradio" if preferred
19
+ GRADIO_CACHE = GRADIO_TMP
20
+
21
+ os.environ.setdefault("HF_HOME", HF_HOME)
22
+ os.environ.setdefault("HF_HUB_CACHE", HF_HUB_CACHE)
23
+ os.environ.setdefault("TRANSFORMERS_CACHE", HF_TRANSFORMERS) # deprecated warning is harmless
24
+ os.environ.setdefault("SENTENCE_TRANSFORMERS_HOME", ST_HOME)
25
+ os.environ.setdefault("GRADIO_TEMP_DIR", GRADIO_TMP)
26
+ os.environ.setdefault("GRADIO_CACHE_DIR", GRADIO_CACHE)
27
+
28
+ # Disable experimental xet; prefer stable transfer
29
  os.environ.setdefault("HF_HUB_ENABLE_XET", "0")
30
  os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
31
 
32
+ for p in [HF_HOME, HF_HUB_CACHE, HF_TRANSFORMERS, ST_HOME, GRADIO_TMP, GRADIO_CACHE]:
 
 
 
 
 
 
33
  try:
34
  os.makedirs(p, exist_ok=True)
35
  except Exception:
 
109
 
110
  def is_identity_query(message, history):
111
  patterns = [
112
+ r"\bwho\s+are\s+you\b", r"\bwhat\s+are\s+you\b", r"\bwhat\s+is\s+your\s+name\b",
113
+ r"\bwho\s+is\s+this\b", r"\bidentify\s+yourself\b", r"\btell\s+me\s+about\s+yourself\b",
114
+ r"\bdescribe\s+yourself\b", r"\band\s+you\s*\?\b", r"\byour\s+name\b",
 
 
 
 
 
 
115
  r"\bwho\s+am\s+i\s+chatting\s+with\b",
116
  ]
117
  def match(t): return any(re.search(p, (t or "").strip().lower()) for p in patterns)
 
128
  a = item[1] if len(item) > 1 else ""
129
  yield u, a
130
 
131
+ def _sanitize_text(s: str) -> str:
132
+ if not isinstance(s, str):
133
+ return s
134
+ return re2.sub(r'[\p{C}--[\n\t]]+', '', s)
135
+
136
  def _history_to_prompt(message, history):
137
+ parts = [f"System: {SYSTEM_MASTER}"]
 
 
 
 
 
138
  for u, a in _iter_user_assistant(history):
139
  if u: parts.append(f"User: {u}")
140
  if a: parts.append(f"Assistant: {a}")
 
142
  parts.append("Assistant:")
143
  return "\n".join(parts)
144
 
145
+ # ---------- Cohere first ----------
 
 
 
 
 
 
 
 
146
  def cohere_chat(message, history):
147
  if not USE_HOSTED_COHERE:
148
  return None
149
  try:
 
150
  client = cohere.Client(api_key=COHERE_API_KEY)
151
  prompt = _history_to_prompt(message, history)
152
  resp = client.chat(
 
162
  except Exception:
163
  return None
164
 
165
+ # ---------- Local model (HF) ----------
166
  @lru_cache(maxsize=1)
167
  def load_local_model():
168
  if not HF_TOKEN:
 
172
  tok = AutoTokenizer.from_pretrained(
173
  MODEL_ID, token=HF_TOKEN, use_fast=True, model_max_length=8192,
174
  padding_side="left", trust_remote_code=True,
175
+ cache_dir=os.environ.get("TRANSFORMERS_CACHE")
176
  )
177
  try:
178
  mdl = AutoModelForCausalLM.from_pretrained(
179
  MODEL_ID, token=HF_TOKEN, device_map=device_map,
180
  low_cpu_mem_usage=True, torch_dtype=dtype, trust_remote_code=True,
181
+ cache_dir=os.environ.get("TRANSFORMERS_CACHE")
182
  )
183
  except Exception:
184
  mdl = AutoModelForCausalLM.from_pretrained(
185
  MODEL_ID, token=HF_TOKEN,
186
  low_cpu_mem_usage=True, torch_dtype=dtype, trust_remote_code=True,
187
+ cache_dir=os.environ.get("TRANSFORMERS_CACHE")
188
  )
189
  mdl.to("cuda" if torch.cuda.is_available() else "cpu")
190
  if mdl.config.eos_token_id is None and tok.eos_token_id is not None:
 
192
  return mdl, tok
193
 
194
  def build_inputs(tokenizer, message, history):
195
+ msgs = [{"role": "system", "content": SYSTEM_MASTER}]
 
 
196
  for u, a in _iter_user_assistant(history):
197
  if u: msgs.append({"role": "user", "content": u})
198
  if a: msgs.append({"role": "assistant", "content": a})
 
214
  gen_only = out[0, input_ids.shape[-1]:]
215
  return tokenizer.decode(gen_only, skip_special_tokens=True).strip()
216
 
217
+ # ---------- Snapshot, retriever, RAG ----------
218
  def _load_snapshot(path=SNAPSHOT_PATH):
219
  try:
220
  with open(path, "r", encoding="utf-8") as f:
 
229
  "isolation_needs_waiting": {"contact": 3, "airborne": 1}, "telemetry_needed_waiting": 5
230
  }
231
 
 
232
  init_retriever()
233
+ _session_rag = SessionRAG()
234
 
 
235
  def _mdsi_block():
236
  base_capacity = capacity_projection(18, 48, 6)
237
  cons_capacity = capacity_projection(12, 48, 6)
 
244
  "outcomes_summary": outcomes
245
  }, indent=2)
246
 
247
+ # ---------- Core chat logic (two-phase) ----------
248
  def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answers=False):
249
  """
250
  awaiting_answers:
251
+ - False: Phase 1 -> generate clarification questions and WAIT
252
+ - True: Phase 2 -> consume clarifications and produce structured analysis
253
  """
254
  try:
255
  log_event("user_message", None, {"sizes": {"chars": len(user_msg or "")}})
 
265
  ans = "I am ClarityOps, your strategic decision making AI partner."
266
  return history + [(user_msg, ans)], awaiting_answers
267
 
268
+ # Ingest uploads (text + artifacts like CSV headers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  if uploaded_files_paths:
270
  ing = extract_text_from_files(uploaded_files_paths)
271
  chunks = ing.get("chunks", []) if isinstance(ing, dict) else (ing or [])
 
276
  _session_rag.register_artifacts(artifacts)
277
  log_event("uploads_added", None, {"chunks": len(chunks), "artifacts": len(artifacts)})
278
 
279
+ # Columns helper
280
  if re.search(r"\b(columns?|headers?)\b", (safe_in or "").lower()):
281
  cols = _session_rag.get_latest_csv_columns()
282
  if cols:
283
  return history + [(user_msg, "Here are the column names from your most recent CSV upload:\n\n- " + "\n- ".join(cols))], awaiting_answers
284
 
285
+ # Session retrieval to enrich the system preamble
286
  session_snips = "\n---\n".join(_session_rag.retrieve(
287
+ "diabetes screening Indigenous Métis mobile program cost throughput outcomes logistics",
288
  k=6
289
  ))
290
 
 
291
  snapshot = _load_snapshot()
292
  policy_context = retrieve_context(
293
+ "mobile diabetes screening Indigenous community outreach cultural safety data governance outcomes"
294
  )
295
  computed = compute_operational_numbers(snapshot)
296
 
 
297
  user_lower = (safe_in or "").lower()
298
  mdsi_extra = _mdsi_block() if ("diabetes" in user_lower or "mdsi" in user_lower or "mobile screening" in user_lower) else ""
299
 
 
306
  session_snips=session_snips
307
  )
308
 
309
+ # Phase directive
310
  if not awaiting_answers:
311
  phase_directive = (
312
  "\n\n[INSTRUCTION TO MODEL]\n"
 
323
 
324
  augmented_user = SYSTEM_MASTER + "\n\n" + system_preamble + "\n\nUser message:\n" + safe_in + phase_directive
325
 
326
+ # Call LLM
327
  out = cohere_chat(augmented_user, history)
 
 
328
  if not out:
329
  model, tokenizer = load_local_model()
330
  inputs = build_inputs(tokenizer, augmented_user, history)
331
  out = local_generate(model, tokenizer, inputs, max_new_tokens=MAX_NEW_TOKENS)
332
 
333
+ # Clean + sanitize
334
  if isinstance(out, str):
335
  for tag in ("Assistant:", "System:", "User:"):
336
  if out.startswith(tag):
 
342
  if blocked_out:
343
  safe_out = refusal_reply(reason_out)
344
 
345
+ # Flip phase state based on headers
 
 
346
  new_awaiting = awaiting_answers
347
+ low = safe_out.lower()
348
+ if not awaiting_answers and "clarification questions" in low:
349
  new_awaiting = True
350
+ elif awaiting_answers and "structured analysis" in low:
351
  new_awaiting = False
352
 
 
353
  log_event("assistant_reply", None, {
354
  **hash_summary("prompt", augmented_user if not PERSIST_CONTENT else ""),
355
  **hash_summary("reply", safe_out if not PERSIST_CONTENT else ""),
 
357
  })
358
 
359
  return history + [(user_msg, safe_out)], new_awaiting
360
+
361
  except Exception as e:
362
  err = f"Error: {e}"
363
  try:
 
396
  #chat-container { position: relative; }
397
  """
398
 
399
+ # ---------- UI ----------
400
  with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
401
  gr.Markdown("# ClarityOps Augmented Decision AI")
402
 
 
424
 
425
  state_history = gr.State(value=[])
426
  state_uploaded = gr.State(value=[])
427
+ state_awaiting = gr.State(value=False) # False -> Phase 1 next; True -> awaiting answers for Phase 2
428
 
429
  def _store_uploads(files, current):
430
  paths = []
 
435
  uploads.change(fn=_store_uploads, inputs=[uploads, state_uploaded], outputs=state_uploaded)
436
 
437
  def _on_send(user_msg, history, up_paths, awaiting):
 
438
  hide_overlay_js = gr.update(value='<div id="handshake-overlay" class="hidden"></div>')
439
  try:
440
  if not user_msg or not user_msg.strip():
441
  return history, "", history, awaiting, hide_overlay_js
442
+ new_history, new_awaiting = clarityops_reply(
443
+ user_msg.strip(), history or [], None, up_paths or [], awaiting_answers=awaiting
444
+ )
445
  return new_history, "", new_history, new_awaiting, hide_overlay_js
446
  except Exception as e:
447
  err = f"Error: {e}"
448
+ try: traceback.print_exc()
449
+ except Exception: pass
 
 
450
  new_hist = (history or []) + [(user_msg or "", err)]
451
  return new_hist, "", new_hist, awaiting, hide_overlay_js
452
 
 
459
  concurrency_limit=2, queue=True)
460
 
461
  def _on_clear():
 
462
  return [], "", [], False, '<div id="handshake-overlay">ClarityOps loaded. Paste your scenario and attach files. I’ll ask up to 5 clarifications, then produce the structured analysis</div>'
463
 
464
  clear.click(_on_clear, None, [chat, msg, state_history, state_awaiting, handshake])