Rajan Sharma commited on
Commit
f6cdc91
·
verified ·
1 Parent(s): ed780b7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +87 -83
app.py CHANGED
@@ -47,13 +47,12 @@ from huggingface_hub import login
47
 
48
  from safety import safety_filter, refusal_reply
49
  from retriever import init_retriever, retrieve_context
50
- from decision_math import compute_operational_numbers # fixed import name
51
  from prompt_templates import build_system_preamble
52
  from upload_ingest import extract_text_from_files
53
  from session_rag import SessionRAG
54
- from mdsi_analysis import capacity_projection, cost_estimate, outcomes_summary
55
 
56
- # NEW: dynamic data plumbing
57
  from data_registry import DataRegistry
58
  from schema_mapper import map_concepts, build_phase1_questions
59
  from auto_metrics import build_data_findings_markdown
@@ -68,23 +67,21 @@ USE_HOSTED_COHERE = bool(COHERE_API_KEY and _HAS_COHERE)
68
  # Larger output budget for Phase 2
69
  MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "2048"))
70
 
71
- # ---------- System Master (Phase 2) ----------
72
  SYSTEM_MASTER = """
73
  SYSTEM ROLE
74
- You are ClarityOps, a medical analytics system that interacts only via this chat.
75
  Absolute rules:
76
  - Use ONLY information provided in this conversation (scenario text + uploaded files + user answers).
77
  - Never invent data. If something required is missing after clarifications, write the literal token: INSUFFICIENT_DATA.
78
- - Produce clear calculations (show multipliers and totals), follow medical units, and keep privacy safeguards (aggregate; suppress cohorts <10).
79
- Formatting hard rules for Phase 2:
80
- - Start with the header: “Structured Analysis”
81
- - Follow this section order:
82
- 1. Prioritization
83
- 2. Capacity
84
- 3. Cost
85
- 4. Clinical Benefits
86
- 5. ClarityOps Top 3 Recommendations
87
- - End with a brief “Provenance” mapping outputs to scenario text, uploaded files, and answers.
88
  """.strip()
89
 
90
  # ---------- Helpers ----------
@@ -122,10 +119,21 @@ def _sanitize_text(s: str) -> str:
122
  return re2.sub(r'[\p{C}--[\n\t]]+', '', s)
123
 
124
  def is_scenario_triggered(text: str, uploaded_files_paths) -> bool:
 
125
  t = (text or "").lower()
126
- has_keyword = "scenario" in t
 
 
 
 
 
 
 
127
  has_files = bool(uploaded_files_paths)
128
- return has_keyword or has_files
 
 
 
129
 
130
  # ---------- Cohere first ----------
131
  def cohere_chat(message, history):
@@ -206,39 +214,20 @@ def local_generate(model, tokenizer, input_ids, max_new_tokens=MAX_NEW_TOKENS):
206
 
207
  # ---------- Snapshot & retrieval ----------
208
  def _load_snapshot(path=SNAPSHOT_PATH):
 
209
  try:
210
  with open(path, "r", encoding="utf-8") as f:
211
  return json.load(f)
212
  except Exception:
213
- return {
214
- "timestamp": None, "beds_total": 400, "staffed_ratio": 1.0, "occupied_pct": 0.97,
215
- "ed_census": 62, "ed_admits_waiting": 19, "avg_ed_wait_hours": 8,
216
- "discharge_ready_today": 11, "discharge_barriers": {"allied_health": 7, "placement": 4},
217
- "rn_shortfall": {"med_ward_A": 1, "med_ward_B": 1},
218
- "forecast_admits_next_24h": {"respiratory": 14, "other": 9},
219
- "isolation_needs_waiting": {"contact": 3, "airborne": 1}, "telemetry_needed_waiting": 5
220
- }
221
 
222
  init_retriever()
223
  _session_rag = SessionRAG()
224
 
225
- # ---------- Executive pre-compute (MDSi block) ----------
226
- def _mdsi_block():
227
- base_capacity = capacity_projection(18, 48, 6)
228
- cons_capacity = capacity_projection(12, 48, 6)
229
- opt_capacity = capacity_projection(24, 48, 6)
230
- cost_1200 = cost_estimate(1200, 74.0, 75000.0)
231
- outcomes = outcomes_summary()
232
- return json.dumps({
233
- "capacity_projection": {"conservative": cons_capacity, "base": base_capacity, "optimistic": opt_capacity},
234
- "cost_for_1200": cost_1200,
235
- "outcomes_summary": outcomes
236
- }, indent=2)
237
-
238
  # NEW: session-scoped data registry
239
  _data_registry = DataRegistry()
240
 
241
- # ---------- Core chat logic (auto scenario, dynamic Phase 1) ----------
242
  def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answers=False):
243
  try:
244
  log_event("user_message", None, {"sizes": {"chars": len(user_msg or "")}})
@@ -249,10 +238,10 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
249
  return history + [(user_msg, ans)], awaiting_answers
250
 
251
  if is_identity_query(safe_in, history):
252
- ans = "I am ClarityOps, your strategic decision making AI partner."
253
  return history + [(user_msg, ans)], awaiting_answers
254
 
255
- # 1) Ingest uploads into RAG AND DataRegistry (files alone can trigger Scenario Mode)
256
  artifacts = []
257
  if uploaded_files_paths:
258
  ing = extract_text_from_files(uploaded_files_paths)
@@ -269,7 +258,7 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
269
  "chunks": len(chunks), "artifacts": len(artifacts), "tables": len(_data_registry.names())
270
  })
271
 
272
- # quick helper
273
  if re.search(r"\b(columns?|headers?)\b", (safe_in or "").lower()):
274
  cols = _session_rag.get_latest_csv_columns()
275
  if cols:
@@ -302,12 +291,12 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
302
  })
303
  return history + [(user_msg, safe_out)], awaiting_answers
304
 
305
- # ---------- Scenario Mode ----------
306
  # 3) Build dynamic concept mapping from scenario + data
307
  mapping = map_concepts(safe_in, _data_registry)
308
 
309
  if not awaiting_answers:
310
- # PHASE 1: ask only for missing/ambiguous
311
  phase1 = build_phase1_questions(scenario_text=safe_in, registry=_data_registry, mapping=mapping)
312
  phase1 = _sanitize_text(phase1)
313
  log_event("assistant_reply", None, {
@@ -318,56 +307,52 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
318
  })
319
  return history + [(user_msg, phase1)], True
320
 
321
- # PHASE 2: compute data findings in Python, then let LLM write the narrative
322
  data_findings_md, missing_keys = build_data_findings_markdown(_data_registry, mapping)
323
 
324
- # If critical missing items remain, surface INSUFFICIENT_DATA context to the model + ask for the rest
325
- insuff_note = ""
326
  if missing_keys:
327
- insuff_note = (
328
- "\n\nUncomputable (still missing columns/defs): "
329
  + ", ".join(sorted(set(missing_keys)))
330
- + ". If any of these are essential to the requested outputs, write INSUFFICIENT_DATA where appropriate."
331
  )
332
 
333
- # Preamble context (snapshot + policy)
334
- session_snips = "\n---\n".join(_session_rag.retrieve(
335
- "diabetes screening Indigenous Métis mobile program cost throughput outcomes logistics",
336
- k=6
337
- ))
 
338
  snapshot = _load_snapshot()
339
- policy_context = retrieve_context(
340
- "mobile diabetes screening Indigenous community outreach cultural safety data governance outcomes"
341
- )
342
- computed = compute_operational_numbers(snapshot)
343
 
344
- user_lower = (safe_in or "").lower()
345
- mdsi_extra = ""
346
- if any(k in user_lower for k in ["diabetes", "mdsi", "mobile screening"]):
347
- mdsi_extra = _mdsi_block()
348
-
349
- # Build artifact + table summary for the prompt
350
  registry_summary = _data_registry.summarize_for_prompt()
351
- artifact_block = "Uploaded Data Files (tables):\n" + registry_summary
352
 
353
  scenario_block = safe_in if len((safe_in or "")) > 0 else ""
354
  system_preamble = build_system_preamble(
355
  snapshot=snapshot,
356
  policy_context=policy_context,
357
- computed_numbers=computed,
358
- scenario_text=scenario_block + f"\n\n{artifact_block}\n\n{data_findings_md}" + (f"\n\nExecutive Pre-Computed Blocks:\n{mdsi_extra}" if mdsi_extra else "") + insuff_note,
359
  session_snips=session_snips
360
  )
361
 
362
  directive = (
363
- "\n\n[INSTRUCTION TO MODEL]\n"
364
- "Produce **Phase 2** now: begin with 'Structured Analysis' and follow the exact section order "
365
- "(Prioritization, Capacity, Cost, Clinical Benefits, ClarityOps Top 3 Recommendations). "
366
- "Use the **Python-computed tables** in the context as ground truth; when something is truly missing, write INSUFFICIENT_DATA. "
367
- "Show calculations, units, and add a brief Provenance.\n"
368
  )
369
 
370
- augmented_user = SYSTEM_MASTER + "\n\n" + system_preamble + "\n\nUser scenario & answers:\n" + safe_in + directive
371
 
372
  out = cohere_chat(augmented_user, history)
373
  if not out:
@@ -402,10 +387,28 @@ def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answe
402
  pass
403
  return history + [(user_msg, err)], awaiting_answers
404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  # ---------- Theme & CSS ----------
406
  theme = gr.themes.Soft(primary_hue="teal", neutral_hue="slate", radius_size=gr.themes.sizes.radius_lg)
407
  custom_css = """
408
- :root { --brand-bg: #0f172a; --brand-accent: #0d9488; --brand-text: #0f172a; --brand-text-light: #ffffff; } /* bg same as chat for integrated look */
409
  html, body, .gradio-container { height: 100vh; }
410
  .gradio-container { background: var(--brand-bg); display: flex; flex-direction: column; }
411
 
@@ -433,33 +436,33 @@ textarea, input, .gr-input { border-radius: 12px !important; }
433
 
434
  # ---------- UI ----------
435
  with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
436
- # --- HERO (initial Google-like screen) ---
437
  with gr.Column(elem_id="hero-wrap", visible=True) as hero_wrap:
438
  with gr.Column(elem_id="hero"):
439
- gr.HTML("<h2>What can I assist with?</h2>")
440
  with gr.Row(elem_classes="search-row"):
441
  hero_msg = gr.Textbox(
442
- placeholder="Ask anything (type 'scenario' and/or attach files for Scenario Mode)…",
443
  show_label=False,
444
  lines=1,
445
  elem_classes="hero-box"
446
  )
447
  hero_send = gr.Button("➤", scale=0, elem_id="hero-send")
448
- gr.Markdown('<div class="hint">Scenario Mode triggers when you type the word <b>scenario</b> or upload files. Phase&nbsp;1 asks dynamic clarifications; Phase&nbsp;2 returns a structured analysis.</div>')
449
 
450
  # --- MAIN APP (hidden until first message) ---
451
  with gr.Column(elem_id="chat-container", visible=False) as app_wrap:
452
  chat = gr.Chatbot(label="", show_label=False, height="80vh")
453
  with gr.Row():
454
  uploads = gr.Files(
455
- label="Upload docs/images (PDF, DOCX, CSV, PNG, JPG)",
456
  file_types=["file"], file_count="multiple", height=68
457
  )
458
  with gr.Row(elem_id="chat-input-row"):
459
  msg = gr.Textbox(
460
  label="",
461
  show_label=False,
462
- placeholder="Continue here. Paste scenario details (include the word 'scenario' to trigger), add files above.",
463
  scale=10,
464
  elem_id="chat-msg",
465
  lines=1,
@@ -529,8 +532,9 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
529
  concurrency_limit=2, queue=True)
530
 
531
  def _on_clear():
532
- # Also clear the in-memory data registry for a fresh scenario
533
  _data_registry.clear()
 
534
  return (
535
  [], "", [], False,
536
  gr.update(visible=True),
@@ -542,7 +546,7 @@ with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
542
 
543
  if __name__ == "__main__":
544
  port = int(os.environ.get("PORT", "7860"))
545
- demo.launch(server_name="0.0.0.0", server_port=port, show_api=False, max_threads=8)
546
 
547
 
548
 
 
47
 
48
  from safety import safety_filter, refusal_reply
49
  from retriever import init_retriever, retrieve_context
50
+ from decision_math import compute_operational_numbers
51
  from prompt_templates import build_system_preamble
52
  from upload_ingest import extract_text_from_files
53
  from session_rag import SessionRAG
 
54
 
55
+ # NEW: dynamic data analysis framework
56
  from data_registry import DataRegistry
57
  from schema_mapper import map_concepts, build_phase1_questions
58
  from auto_metrics import build_data_findings_markdown
 
67
  # Larger output budget for Phase 2
68
  MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "2048"))
69
 
70
+ # ---------- Generic System Prompt ----------
71
  SYSTEM_MASTER = """
72
  SYSTEM ROLE
73
+ You are an AI analytical system that provides data-driven insights for any scenario.
74
  Absolute rules:
75
  - Use ONLY information provided in this conversation (scenario text + uploaded files + user answers).
76
  - Never invent data. If something required is missing after clarifications, write the literal token: INSUFFICIENT_DATA.
77
+ - Provide clear analysis with calculations, evidence, and reasoning.
78
+ - Maintain privacy safeguards (aggregate data; suppress small cohorts <10).
79
+ - Adapt your analysis approach to the specific scenario and data provided.
80
+
81
+ Formatting rules for structured analysis:
82
+ - Start with the header: "Structured Analysis"
83
+ - Organize analysis into logical sections based on the scenario requirements
84
+ - End with concrete recommendations and a brief "Provenance" mapping outputs to scenario text, uploaded files, and answers.
 
 
85
  """.strip()
86
 
87
  # ---------- Helpers ----------
 
119
  return re2.sub(r'[\p{C}--[\n\t]]+', '', s)
120
 
121
  def is_scenario_triggered(text: str, uploaded_files_paths) -> bool:
122
+ """Detect if this should be treated as a scenario analysis request."""
123
  t = (text or "").lower()
124
+
125
+ # Scenario keywords
126
+ scenario_keywords = [
127
+ "scenario", "analysis", "analyze", "assess", "evaluate", "recommendation",
128
+ "strategy", "plan", "solution", "decision", "priority", "allocate", "resource"
129
+ ]
130
+
131
+ has_keyword = any(keyword in t for keyword in scenario_keywords)
132
  has_files = bool(uploaded_files_paths)
133
+
134
+ # If files are uploaded, assume scenario mode
135
+ # If certain analytical keywords are present, assume scenario mode
136
+ return has_files or has_keyword
137
 
138
  # ---------- Cohere first ----------
139
  def cohere_chat(message, history):
 
214
 
215
  # ---------- Snapshot & retrieval ----------
216
  def _load_snapshot(path=SNAPSHOT_PATH):
217
+ """Load operational snapshot if available."""
218
  try:
219
  with open(path, "r", encoding="utf-8") as f:
220
  return json.load(f)
221
  except Exception:
222
+ return {} # Return empty dict if no snapshot available
 
 
 
 
 
 
 
223
 
224
  init_retriever()
225
  _session_rag = SessionRAG()
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  # NEW: session-scoped data registry
228
  _data_registry = DataRegistry()
229
 
230
+ # ---------- Core chat logic (generic scenario handling) ----------
231
  def clarityops_reply(user_msg, history, tz, uploaded_files_paths, awaiting_answers=False):
232
  try:
233
  log_event("user_message", None, {"sizes": {"chars": len(user_msg or "")}})
 
238
  return history + [(user_msg, ans)], awaiting_answers
239
 
240
  if is_identity_query(safe_in, history):
241
+ ans = "I am an AI analytical system designed to help you analyze scenarios and make data-driven decisions."
242
  return history + [(user_msg, ans)], awaiting_answers
243
 
244
+ # 1) Ingest uploads into RAG AND DataRegistry
245
  artifacts = []
246
  if uploaded_files_paths:
247
  ing = extract_text_from_files(uploaded_files_paths)
 
258
  "chunks": len(chunks), "artifacts": len(artifacts), "tables": len(_data_registry.names())
259
  })
260
 
261
+ # Quick helper for column inspection
262
  if re.search(r"\b(columns?|headers?)\b", (safe_in or "").lower()):
263
  cols = _session_rag.get_latest_csv_columns()
264
  if cols:
 
291
  })
292
  return history + [(user_msg, safe_out)], awaiting_answers
293
 
294
+ # ---------- Generic Scenario Analysis Mode ----------
295
  # 3) Build dynamic concept mapping from scenario + data
296
  mapping = map_concepts(safe_in, _data_registry)
297
 
298
  if not awaiting_answers:
299
+ # PHASE 1: ask for missing/ambiguous information
300
  phase1 = build_phase1_questions(scenario_text=safe_in, registry=_data_registry, mapping=mapping)
301
  phase1 = _sanitize_text(phase1)
302
  log_event("assistant_reply", None, {
 
307
  })
308
  return history + [(user_msg, phase1)], True
309
 
310
+ # PHASE 2: compute data analysis and generate structured response
311
  data_findings_md, missing_keys = build_data_findings_markdown(_data_registry, mapping)
312
 
313
+ # Build context for analysis
314
+ insufficient_data_note = ""
315
  if missing_keys:
316
+ insufficient_data_note = (
317
+ "\n\nData limitations: Missing or uncomputable: "
318
  + ", ".join(sorted(set(missing_keys)))
319
+ + ". Where these are essential to analysis, write INSUFFICIENT_DATA."
320
  )
321
 
322
+ # Get relevant context from uploaded documents
323
+ # Extract key terms from scenario to improve retrieval
324
+ scenario_terms = _extract_key_terms_from_scenario(safe_in)
325
+ session_snips = "\n---\n".join(_session_rag.retrieve(scenario_terms, k=6))
326
+
327
+ # Load any available operational data
328
  snapshot = _load_snapshot()
329
+ computed_numbers = compute_operational_numbers(snapshot) if snapshot else {}
330
+
331
+ # Get general policy/context if available
332
+ policy_context = retrieve_context(scenario_terms)
333
 
334
+ # Build comprehensive data summary for analysis
 
 
 
 
 
335
  registry_summary = _data_registry.summarize_for_prompt()
336
+ artifact_block = "Uploaded Data Files:\n" + registry_summary if registry_summary else "No data files uploaded."
337
 
338
  scenario_block = safe_in if len((safe_in or "")) > 0 else ""
339
  system_preamble = build_system_preamble(
340
  snapshot=snapshot,
341
  policy_context=policy_context,
342
+ computed_numbers=computed_numbers,
343
+ scenario_text=scenario_block + f"\n\n{artifact_block}\n\n{data_findings_md}" + insufficient_data_note,
344
  session_snips=session_snips
345
  )
346
 
347
  directive = (
348
+ "\n\n[ANALYSIS INSTRUCTION]\n"
349
+ "Provide a structured analysis appropriate to this scenario. Begin with 'Structured Analysis' and "
350
+ "organize your response into logical sections based on what the scenario requires. Use the data "
351
+ "provided as ground truth. When information is missing, write INSUFFICIENT_DATA. Show your reasoning "
352
+ "and calculations. End with concrete recommendations and a brief Provenance section.\n"
353
  )
354
 
355
+ augmented_user = SYSTEM_MASTER + "\n\n" + system_preamble + "\n\nScenario and context:\n" + safe_in + directive
356
 
357
  out = cohere_chat(augmented_user, history)
358
  if not out:
 
387
  pass
388
  return history + [(user_msg, err)], awaiting_answers
389
 
390
+ def _extract_key_terms_from_scenario(scenario_text: str) -> str:
391
+ """Extract key terms from scenario text for better context retrieval."""
392
+ if not scenario_text:
393
+ return ""
394
+
395
+ # Simple extraction of important words (remove common stop words)
396
+ stop_words = {
397
+ 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
398
+ 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did',
399
+ 'a', 'an', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they'
400
+ }
401
+
402
+ words = re.findall(r'\b[a-zA-Z]{3,}\b', scenario_text.lower())
403
+ key_terms = [word for word in words if word not in stop_words]
404
+
405
+ # Return first 10-15 key terms
406
+ return ' '.join(key_terms[:15])
407
+
408
  # ---------- Theme & CSS ----------
409
  theme = gr.themes.Soft(primary_hue="teal", neutral_hue="slate", radius_size=gr.themes.sizes.radius_lg)
410
  custom_css = """
411
+ :root { --brand-bg: #0f172a; --brand-accent: #0d9488; --brand-text: #0f172a; --brand-text-light: #ffffff; }
412
  html, body, .gradio-container { height: 100vh; }
413
  .gradio-container { background: var(--brand-bg); display: flex; flex-direction: column; }
414
 
 
436
 
437
  # ---------- UI ----------
438
  with gr.Blocks(theme=theme, css=custom_css, analytics_enabled=False) as demo:
439
+ # --- HERO (initial screen) ---
440
  with gr.Column(elem_id="hero-wrap", visible=True) as hero_wrap:
441
  with gr.Column(elem_id="hero"):
442
+ gr.HTML("<h2>What scenario can I help you analyze?</h2>")
443
  with gr.Row(elem_classes="search-row"):
444
  hero_msg = gr.Textbox(
445
+ placeholder="Describe your scenario or ask any question (upload files for data analysis)…",
446
  show_label=False,
447
  lines=1,
448
  elem_classes="hero-box"
449
  )
450
  hero_send = gr.Button("➤", scale=0, elem_id="hero-send")
451
+ gr.Markdown('<div class="hint">Upload files and describe your scenario for comprehensive analysis. The system will ask clarifying questions, then provide structured insights.</div>')
452
 
453
  # --- MAIN APP (hidden until first message) ---
454
  with gr.Column(elem_id="chat-container", visible=False) as app_wrap:
455
  chat = gr.Chatbot(label="", show_label=False, height="80vh")
456
  with gr.Row():
457
  uploads = gr.Files(
458
+ label="Upload data files (PDF, DOCX, CSV, PNG, JPG)",
459
  file_types=["file"], file_count="multiple", height=68
460
  )
461
  with gr.Row(elem_id="chat-input-row"):
462
  msg = gr.Textbox(
463
  label="",
464
  show_label=False,
465
+ placeholder="Continue the conversation. Provide additional details or answer clarifying questions.",
466
  scale=10,
467
  elem_id="chat-msg",
468
  lines=1,
 
532
  concurrency_limit=2, queue=True)
533
 
534
  def _on_clear():
535
+ # Clear the in-memory data registry for a fresh scenario
536
  _data_registry.clear()
537
+ _session_rag.clear() # Also clear RAG session if available
538
  return (
539
  [], "", [], False,
540
  gr.update(visible=True),
 
546
 
547
  if __name__ == "__main__":
548
  port = int(os.environ.get("PORT", "7860"))
549
+ demo.launch(server_name="0.0.0.0", server_port=port, show_api=False, max_threads=40)ds=8)
550
 
551
 
552