Seth0330 commited on
Commit
601bf70
·
verified ·
1 Parent(s): 77420f5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -136
app.py CHANGED
@@ -4,11 +4,11 @@ import csv
4
  import re
5
  import base64
6
  import textwrap
 
7
  import requests
8
  import streamlit as st
9
  from PIL import Image
10
  from audio_recorder_streamlit import audio_recorder
11
- from huggingface_hub import InferenceClient
12
 
13
  # =========================
14
  # BASIC CONFIG
@@ -23,33 +23,23 @@ st.set_page_config(
23
  # --- Keys / endpoints ---
24
 
25
  OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
26
-
27
- # HF_TOKEN is the new standard name; fall back to HF_API_TOKEN if needed
28
- HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HF_API_TOKEN")
29
-
30
  if not OPENROUTER_API_KEY:
31
  st.warning("Set OPENROUTER_API_KEY in your Space secrets (OpenRouter) to enable AI features.")
32
  st.stop()
33
 
 
 
34
  OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
35
 
36
  # IMPORTANT: set these to valid OpenRouter model slugs from your OpenRouter dashboard.
37
- VISION_MODEL = os.getenv("VISION_MODEL", "nvidia/nemotron-nano-12b-v2-vl:free") # example; update to real slug if different
38
- REASONING_MODEL = os.getenv("REASONING_MODEL", "nvidia/nemotron-nano-12b-v2-vl:free") # or another instruct-capable model
39
-
40
- # Whisper model to use on Hugging Face Inference
41
- HF_WHISPER_MODEL = os.getenv("HF_WHISPER_MODEL", "openai/whisper-small")
42
 
43
- # Create HF Inference client if token available
44
- hf_asr_client = None
45
- if HF_TOKEN:
46
- try:
47
- hf_asr_client = InferenceClient(
48
- provider="hf-inference",
49
- api_key=HF_TOKEN,
50
- )
51
- except Exception:
52
- hf_asr_client = None # fail soft; app still runs without ASR
53
 
54
  DPD_BASE_URL = "https://health-products.canada.ca/api/drug/drugproduct"
55
  RECALLS_FEED_URL = os.getenv(
@@ -250,6 +240,11 @@ def load_city_options(path: str = "canadacities.csv"):
250
 
251
 
252
  CITY_OPTIONS = load_city_options()
 
 
 
 
 
253
 
254
 
255
  def split_city_label(label: str):
@@ -419,7 +414,7 @@ def tool_hqontario_context_city(city_label: str) -> str:
419
  return ""
420
 
421
  # =========================
422
- # DPD & RECALLS HELPERS
423
  # =========================
424
 
425
  def tool_lookup_drug_products(text: str) -> str:
@@ -505,16 +500,11 @@ def tool_get_wait_times_awareness() -> str:
505
  # =========================
506
 
507
  def call_openrouter_chat(model: str, messages, temperature: float = 0.3):
508
- """
509
- Helper for OpenRouter's /chat/completions.
510
- Returns a readable error message if the response is not valid JSON.
511
- """
512
  headers = {
513
  "Authorization": f"Bearer {OPENROUTER_API_KEY}",
514
  "Content-Type": "application/json",
515
  }
516
 
517
- # Optional attribution if you configure APP_URL + origins in OpenRouter
518
  app_url = os.getenv("APP_URL", "").strip()
519
  if app_url:
520
  headers["HTTP-Referer"] = app_url
@@ -527,35 +517,25 @@ def call_openrouter_chat(model: str, messages, temperature: float = 0.3):
527
  }
528
 
529
  try:
530
- r = requests.post(OPENROUTER_URL, headers=headers, json=payload, timeout=60)
531
-
532
  if r.status_code != 200:
533
  text_snippet = r.text[:300].replace("\n", " ")
534
  return f"(Model call error: {r.status_code} — {text_snippet})"
535
-
536
- try:
537
- data = r.json()
538
- except ValueError:
539
- text_snippet = r.text[:300].replace("\n", " ")
540
- return f"(Model call error: Non-JSON response — {text_snippet})"
541
-
542
  choices = data.get("choices")
543
- if not choices or "message" not in choices[0] or "content" not in choices[0]["message"]:
544
- return f"(Model call error: Unexpected response format — {data})"
545
-
546
- return choices[0]["message"]["content"].strip()
547
-
548
  except Exception as e:
549
  return f"(Model call unavailable: {e})"
550
 
551
  # =========================
552
- # VISION (Nemotron via OpenRouter)
553
  # =========================
554
 
555
  def call_vision_summarizer(image_bytes: bytes) -> str:
556
- """
557
- Structured, cautious but detailed visual assessment via vision-capable model on OpenRouter.
558
- """
559
  if not image_bytes:
560
  return ""
561
  b64 = base64.b64encode(image_bytes).decode("utf-8")
@@ -598,47 +578,84 @@ Write as if supporting a separate triage system, not directly reassuring or diag
598
  return call_openrouter_chat(VISION_MODEL, messages, temperature=0.2)
599
 
600
  # =========================
601
- # ASR (Whisper via HF InferenceClient)
602
  # =========================
603
 
604
  def call_asr(audio_source) -> str:
605
  """
606
- Uses Hugging Face InferenceClient for Whisper.
607
- Returns transcript text on success, or "" on any failure.
608
- Never surfaces raw HTTP errors to the user.
 
609
  """
610
- if not audio_source or hf_asr_client is None:
611
  return ""
612
 
613
- # Normalize to bytes
 
 
 
 
614
  if isinstance(audio_source, bytes):
615
  audio_bytes = audio_source
616
  else:
617
  audio_bytes = audio_source.read()
618
 
 
 
 
 
 
 
619
  try:
620
- # automatic_speech_recognition accepts bytes directly
621
- result = hf_asr_client.automatic_speech_recognition(
622
- audio_bytes,
623
- model=HF_WHISPER_MODEL,
 
624
  )
 
 
 
 
625
 
626
- # result can be:
627
- # - dict with "text"
628
- # - string
629
- if isinstance(result, dict):
630
- text_val = result.get("text") or result.get("generated_text") or ""
631
- return (text_val or "").strip()
632
- elif isinstance(result, str):
633
- return result.strip()
634
- else:
635
- return ""
636
- except Exception:
637
- # Fail silently; caller will prompt user to type instead
638
  return ""
639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  # =========================
641
- # REASONING AGENT (Nemotron via OpenRouter)
642
  # =========================
643
 
644
  def call_reasoning_agent(
@@ -674,8 +691,11 @@ HARD SAFETY RULES
674
  3. Do NOT claim to provide real-time wait times.
675
  4. Do NOT contradict emergency advice from standard red-flag criteria.
676
  5. DO NOT invent or guess facility names.
677
- - You may ONLY list specific facilities that appear literally in facilities_context or hqontario_context.
678
- - If facilities_context reports none found, direct users to Google Maps / official tools without naming specific clinics.
 
 
 
679
  6. Keep the whole answer under about 350 words.
680
  7. Use clear, neutral, plain language that a non-clinician can understand.
681
 
@@ -683,90 +703,110 @@ IMAGE USE (VERY IMPORTANT)
683
  --------------------------
684
  - If a vision_summary is provided, you MUST treat it as a primary source of information.
685
  - Cross-check the user narrative against the vision_summary.
686
- - If the image description suggests more serious concern (e.g., black tissue, deep open wound, marked swelling,
687
- spreading redness, thick pus, obvious deformity), escalate the triage tier even if the user's text sounds mild.
688
- - If there is a conflict between what is described and what is seen, favour caution and explain that visually
689
- it appears safer to seek a higher level of assessment.
 
690
  - You still MUST NOT assign a diagnosis or name specific diseases based on the image.
691
 
692
  TRIAGE LOGIC (INTERNAL)
693
  -----------------------
694
  You must internally choose EXACTLY ONE primary pathway, but DO NOT show letters or labels to the user.
695
 
696
- (1) "No current action needed (you appear well, continue normal care)"
697
- - Use when the user clearly reports feeling well or "perfect", with no symptoms or concerns.
698
- - Provide light wellness reminders.
699
- - Phrase as "based on what you've shared, you appear well." This is NOT a diagnosis.
700
-
701
- (2) "Home care reasonable (self-care + monitor)"
702
- - For mild, short-lived, non-progressive symptoms that do not limit normal activities.
703
- - Provide simple, safe self-care steps.
704
- - Clearly state when to escalate to pharmacist/clinic/Telehealth.
705
-
706
- (3) "Pharmacist / Walk-in clinic / Family doctor or Telehealth recommended"
707
- - For persistent, recurrent, or moderate symptoms, or diagnostic uncertainty without clear emergency.
708
- - Explain why an in-person or phone assessment is appropriate.
709
-
710
- (4) "Go to Emergency / Call 911 now"
711
- - For clear red-flag features:
712
- - severe or rapidly worsening pain,
713
- - major trauma,
714
- - uncontrolled bleeding,
715
- - chest pain or trouble breathing,
716
- - stroke-like symptoms,
717
- - signs of sepsis or systemic illness,
718
- - significant numbness/weakness,
719
- - concerning post-surgical changes,
720
- - or visually severe findings from the image.
721
- - Be direct and unambiguous.
 
 
 
 
 
 
 
722
 
723
  OUTPUT FORMAT (WHAT USER SEES)
724
  ------------------------------
725
  1. **Primary Recommended Pathway**
726
  - One brief sentence stating the recommendation WITHOUT internal labels.
727
 
728
- 2. Then include sections as appropriate:
729
 
730
  If you chose "No current action needed":
731
- - **Home Care / Wellness**
732
- - **When to seek medical advice**
733
- - **When this is an Emergency (ER/911)**
734
- - **Local & Official Resources**
735
- - **Important Disclaimer**
736
 
737
  If you chose "Home care reasonable (self-care + monitor)":
738
- - **Home Care**
739
- - **When to see a Pharmacist / Clinic / Telehealth**
740
- - **When this is an Emergency (ER/911)**
741
- - **Local & Official Resources**
742
- - **Important Disclaimer**
743
 
744
  If you chose "Pharmacist / Walk-in clinic / Family doctor or Telehealth recommended":
745
- - **When to see a Pharmacist / Clinic / Telehealth**
746
- - **When this is an Emergency (ER/911)**
747
- - **Local & Official Resources**
748
- - **Important Disclaimer**
749
 
750
  If you chose "Go to Emergency / Call 911 now":
751
- - **When this is an Emergency (ER/911)**
752
- - **Local & Official Resources**
753
- - **Important Disclaimer**
754
- - Do NOT include pure-home-care reassurance in this case.
755
 
756
  LOCAL & OFFICIAL RESOURCES
757
  --------------------------
758
- - If facilities_context lists facilities, summarize them as bullet points:
759
- - name, city/address, phone (if present), hours (if present), URL.
760
- - If none are listed, say so and direct user to Google Maps / provincial tools.
761
- - Optionally remind of:
762
- - Health Canada Drug Product Database / Recalls site,
763
- - CIHI/provincial dashboards for general (non-real-time) wait information.
 
 
 
 
 
 
 
 
 
 
 
 
764
 
765
  TONE
766
  ----
767
- - Calm, supportive, neutral.
768
- - Never alarmist, never dismissive.
769
- - Always remind users this is informational only and not a diagnosis.
770
  """
771
 
772
  user_message = f"""
@@ -806,7 +846,7 @@ HQ Ontario info:
806
  return call_openrouter_chat(REASONING_MODEL, messages, temperature=0.3)
807
 
808
  # =========================
809
- # STATE & NAV HELPERS
810
  # =========================
811
 
812
  if "step" not in st.session_state:
@@ -840,7 +880,7 @@ def render_steps():
840
  )
841
 
842
  # =========================
843
- # APP HEADER
844
  # =========================
845
 
846
  st.markdown(
@@ -857,7 +897,7 @@ st.markdown(
857
  render_steps()
858
 
859
  # =========================
860
- # STEP 1: CITY + CAMERA
861
  # =========================
862
 
863
  if st.session_state.step == 1:
@@ -909,7 +949,7 @@ if st.session_state.step == 1:
909
  st.markdown("</div>", unsafe_allow_html=True)
910
 
911
  # =========================
912
- # STEP 2: VOICE OR TEXT (WITH AUTO-FILL)
913
  # =========================
914
 
915
  elif st.session_state.step == 2:
@@ -936,14 +976,11 @@ elif st.session_state.step == 2:
936
 
937
  if audio_bytes:
938
  st.session_state.audio_bytes = audio_bytes
939
- # Only re-transcribe if this is a new recording
940
  if st.session_state.last_audio != audio_bytes:
941
  st.success("Voice note captured. Transcribing...")
942
  transcript = call_asr(audio_bytes)
943
  if transcript:
944
- st.session_state.user_text = transcript.strip()
945
- else:
946
- st.warning("Couldn't transcribe that recording. Please type a brief description.")
947
  st.session_state.last_audio = audio_bytes
948
 
949
  user_text = st.text_area(
@@ -1014,7 +1051,7 @@ elif st.session_state.step == 2:
1014
  st.markdown("</div>", unsafe_allow_html=True)
1015
 
1016
  # =========================
1017
- # STEP 3: RESULT SCREEN
1018
  # =========================
1019
 
1020
  elif st.session_state.step == 3:
 
4
  import re
5
  import base64
6
  import textwrap
7
+
8
  import requests
9
  import streamlit as st
10
  from PIL import Image
11
  from audio_recorder_streamlit import audio_recorder
 
12
 
13
  # =========================
14
  # BASIC CONFIG
 
23
  # --- Keys / endpoints ---
24
 
25
  OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
 
 
 
 
26
  if not OPENROUTER_API_KEY:
27
  st.warning("Set OPENROUTER_API_KEY in your Space secrets (OpenRouter) to enable AI features.")
28
  st.stop()
29
 
30
+ HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HF_API_TOKEN") # for Whisper via HF router
31
+
32
  OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
33
 
34
  # IMPORTANT: set these to valid OpenRouter model slugs from your OpenRouter dashboard.
35
+ VISION_MODEL = os.getenv("VISION_MODEL", "nvidia/nemotron-nano-12b-v2-vl:free") # example; update to actual slug if needed
36
+ REASONING_MODEL = os.getenv("REASONING_MODEL", "nvidia/nemotron-nano-12b-v2-vl:free") # or another instruct model
 
 
 
37
 
38
+ # Whisper via HF Inference router (remote, no local load)
39
+ HF_WHISPER_URL = os.getenv(
40
+ "HF_WHISPER_URL",
41
+ "https://router.huggingface.co/hf-inference/models/openai/whisper-large-v3",
42
+ )
 
 
 
 
 
43
 
44
  DPD_BASE_URL = "https://health-products.canada.ca/api/drug/drugproduct"
45
  RECALLS_FEED_URL = os.getenv(
 
240
 
241
 
242
  CITY_OPTIONS = load_city_options()
243
+ if not CITY_OPTIONS:
244
+ st.warning(
245
+ "Could not load cities from canadacities.csv. "
246
+ "Autocomplete list will be empty until that file is available."
247
+ )
248
 
249
 
250
  def split_city_label(label: str):
 
414
  return ""
415
 
416
  # =========================
417
+ # DPD & RECALLS
418
  # =========================
419
 
420
  def tool_lookup_drug_products(text: str) -> str:
 
500
  # =========================
501
 
502
  def call_openrouter_chat(model: str, messages, temperature: float = 0.3):
 
 
 
 
503
  headers = {
504
  "Authorization": f"Bearer {OPENROUTER_API_KEY}",
505
  "Content-Type": "application/json",
506
  }
507
 
 
508
  app_url = os.getenv("APP_URL", "").strip()
509
  if app_url:
510
  headers["HTTP-Referer"] = app_url
 
517
  }
518
 
519
  try:
520
+ r = requests.post(OPENROUTER_URL, headers=headers, json=payload, timeout=90)
 
521
  if r.status_code != 200:
522
  text_snippet = r.text[:300].replace("\n", " ")
523
  return f"(Model call error: {r.status_code} — {text_snippet})"
524
+ data = r.json()
 
 
 
 
 
 
525
  choices = data.get("choices")
526
+ if not choices:
527
+ return "(Model call error: empty response.)"
528
+ msg = choices[0].get("message", {})
529
+ content = msg.get("content", "")
530
+ return content.strip() if isinstance(content, str) else str(content)
531
  except Exception as e:
532
  return f"(Model call unavailable: {e})"
533
 
534
  # =========================
535
+ # VISION SUMMARIZER (Nemotron via OpenRouter)
536
  # =========================
537
 
538
  def call_vision_summarizer(image_bytes: bytes) -> str:
 
 
 
539
  if not image_bytes:
540
  return ""
541
  b64 = base64.b64encode(image_bytes).decode("utf-8")
 
578
  return call_openrouter_chat(VISION_MODEL, messages, temperature=0.2)
579
 
580
  # =========================
581
+ # ASR VIA HF ROUTER (Whisper Large v3)
582
  # =========================
583
 
584
  def call_asr(audio_source) -> str:
585
  """
586
+ Use Hugging Face router + openai/whisper-large-v3 for transcription.
587
+ - No local model load.
588
+ - Returns transcript or "".
589
+ - Shows friendly warnings, never raw HTTP/JSON in the text area.
590
  """
591
+ if not audio_source:
592
  return ""
593
 
594
+ if not HF_TOKEN:
595
+ st.warning("Voice transcription is not configured. Please type your description.")
596
+ return ""
597
+
598
+ # Normalize to bytes from audio_recorder_streamlit
599
  if isinstance(audio_source, bytes):
600
  audio_bytes = audio_source
601
  else:
602
  audio_bytes = audio_source.read()
603
 
604
+ headers = {
605
+ "Authorization": f"Bearer {HF_TOKEN}",
606
+ # audio_recorder_streamlit sends wav; this is fine for Whisper
607
+ "Content-Type": "audio/wav",
608
+ }
609
+
610
  try:
611
+ resp = requests.post(
612
+ HF_WHISPER_URL,
613
+ headers=headers,
614
+ data=audio_bytes,
615
+ timeout=120,
616
  )
617
+ except Exception as e:
618
+ print(f"Whisper HF router request error: {e}")
619
+ st.warning("Voice transcription is temporarily unavailable. Please type your description.")
620
+ return ""
621
 
622
+ if resp.status_code != 200:
623
+ # Log server-side, keep UI clean
624
+ print(f"Whisper HF router HTTP {resp.status_code}: {resp.text[:300]}")
625
+ st.warning("We couldn't transcribe that recording. Please type or edit your description.")
626
+ return ""
627
+
628
+ try:
629
+ data = resp.json()
630
+ except Exception as e:
631
+ print(f"Whisper HF router JSON error: {e}, body: {resp.text[:300]}")
632
+ st.warning("We couldn't transcribe that recording. Please type or edit your description.")
 
633
  return ""
634
 
635
+ # HF router responses for Whisper typically include "text" or similar.
636
+ text_val = ""
637
+
638
+ if isinstance(data, dict):
639
+ # Common schema: {"text": "..."} or {"generated_text": "..."} or error
640
+ if "error" in data:
641
+ print(f"Whisper HF router error field: {data['error']}")
642
+ text_val = data.get("text") or data.get("generated_text") or ""
643
+ elif isinstance(data, list) and data:
644
+ # Sometimes list of segments or outputs
645
+ first = data[0]
646
+ if isinstance(first, dict):
647
+ text_val = first.get("text") or first.get("generated_text") or ""
648
+ elif isinstance(first, str):
649
+ text_val = first
650
+
651
+ text_val = (text_val or "").strip()
652
+
653
+ if not text_val:
654
+ st.warning("We couldn't clearly understand that recording. Please check or type your description.")
655
+ return text_val
656
+
657
  # =========================
658
+ # REASONING AGENT
659
  # =========================
660
 
661
  def call_reasoning_agent(
 
691
  3. Do NOT claim to provide real-time wait times.
692
  4. Do NOT contradict emergency advice from standard red-flag criteria.
693
  5. DO NOT invent or guess facility names.
694
+ - You may ONLY list specific hospitals/clinics/ERs that appear LITERALLY in:
695
+ - facilities_context, or
696
+ - hqontario_context.
697
+ - If facilities_context says no specific local facilities were found, you MUST NOT make up examples.
698
+ Instead, direct users to Google Maps or official provincial "find a clinic/ER" tools.
699
  6. Keep the whole answer under about 350 words.
700
  7. Use clear, neutral, plain language that a non-clinician can understand.
701
 
 
703
  --------------------------
704
  - If a vision_summary is provided, you MUST treat it as a primary source of information.
705
  - Cross-check the user narrative against the vision_summary.
706
+ - If the image description suggests more serious concern (e.g., black tissue, deep open wound,
707
+ marked swelling, spreading redness, thick pus, obvious deformity), escalate the triage tier
708
+ even if the user's text sounds mild.
709
+ - If there is a conflict between what is described and what is seen, favour caution and explain that
710
+ visually it appears safer to seek a higher level of assessment.
711
  - You still MUST NOT assign a diagnosis or name specific diseases based on the image.
712
 
713
  TRIAGE LOGIC (INTERNAL)
714
  -----------------------
715
  You must internally choose EXACTLY ONE primary pathway, but DO NOT show letters or labels to the user.
716
 
717
+ (1) "No current action needed (you appear well, continue normal care)":
718
+ - Use when the user clearly reports feeling well or "perfect", with no symptoms or concerns.
719
+ - You may gently remind them of general wellness habits.
720
+ - Phrase as "based on what you've shared, you appear well"; this is NOT a diagnosis.
721
+
722
+ (2) "Home care reasonable (self-care + monitor)":
723
+ - Use when symptoms are:
724
+ - mild,
725
+ - short-lived,
726
+ - non-progressive,
727
+ - and NOT limiting normal activities.
728
+ - Provide clear, safe self-care suggestions and "watch for these changes" rules.
729
+
730
+ (3) "Pharmacist / Walk-in clinic / Family doctor or Telehealth recommended":
731
+ - Use when:
732
+ - symptoms are persistent or recurrent,
733
+ - moderate in intensity,
734
+ - affecting function,
735
+ - or there is diagnostic uncertainty but NOT clear emergency.
736
+ - Explain why an in-person/phone assessment is appropriate.
737
+
738
+ (4) "Go to Emergency / Call 911 now":
739
+ - Use when there are red-flag features:
740
+ - severe or rapidly worsening pain,
741
+ - major trauma,
742
+ - uncontrolled bleeding,
743
+ - chest pain, trouble breathing,
744
+ - stroke signs,
745
+ - signs of sepsis or systemic illness,
746
+ - significant numbness/weakness,
747
+ - concerning changes after surgery or known serious conditions,
748
+ - or visually very concerning findings on the image.
749
+ - Be direct and clear.
750
 
751
  OUTPUT FORMAT (WHAT USER SEES)
752
  ------------------------------
753
  1. **Primary Recommended Pathway**
754
  - One brief sentence stating the recommendation WITHOUT internal labels.
755
 
756
+ 2. Conditional sections depending on your chosen pathway:
757
 
758
  If you chose "No current action needed":
759
+ - **Home Care / Wellness**
760
+ - **When to seek medical advice**
761
+ - **When this is an Emergency (ER/911)**
762
+ - **Local & Official Resources**
763
+ - **Important Disclaimer**
764
 
765
  If you chose "Home care reasonable (self-care + monitor)":
766
+ - **Home Care**
767
+ - **When to see a Pharmacist / Clinic / Telehealth**
768
+ - **When this is an Emergency (ER/911)**
769
+ - **Local & Official Resources**
770
+ - **Important Disclaimer**
771
 
772
  If you chose "Pharmacist / Walk-in clinic / Family doctor or Telehealth recommended":
773
+ - **When to see a Pharmacist / Clinic / Telehealth**
774
+ - **When this is an Emergency (ER/911)**
775
+ - **Local & Official Resources**
776
+ - **Important Disclaimer**
777
 
778
  If you chose "Go to Emergency / Call 911 now":
779
+ - **When this is an Emergency (ER/911)**
780
+ - **Local & Official Resources**
781
+ - **Important Disclaimer**
782
+ - Do NOT include home-care reassurance.
783
 
784
  LOCAL & OFFICIAL RESOURCES
785
  --------------------------
786
+ - If facilities_context lists facilities:
787
+ - Summarize them as bullet points:
788
+ - name,
789
+ - city or address,
790
+ - phone (if present),
791
+ - hours/“open now” (if present),
792
+ - link/URL.
793
+ - Do NOT alter names or add new ones.
794
+ - If facilities_context indicates that no specific facilities were found:
795
+ - Say that directly.
796
+ - Instruct users to:
797
+ - use Google Maps with their city,
798
+ - or use official provincial "find a clinic/ER" / Telehealth tools.
799
+ - Do NOT fabricate facility names.
800
+ - Include hqontario_context when relevant (for Ontario users).
801
+ - Optionally remind users:
802
+ - Health Canada Drug Product Database and Recalls site for medication/product safety.
803
+ - CIHI/provincial dashboards for general (non-real-time) wait-time information.
804
 
805
  TONE
806
  ----
807
+ - Calm, supportive, non-alarming.
808
+ - No legalese walls of text; keep it tight and readable.
809
+ - Always remind: this is informational, not a diagnosis; if worried, they should seek real care.
810
  """
811
 
812
  user_message = f"""
 
846
  return call_openrouter_chat(REASONING_MODEL, messages, temperature=0.3)
847
 
848
  # =========================
849
+ # STATE & NAV
850
  # =========================
851
 
852
  if "step" not in st.session_state:
 
880
  )
881
 
882
  # =========================
883
+ # HEADER
884
  # =========================
885
 
886
  st.markdown(
 
897
  render_steps()
898
 
899
  # =========================
900
+ # STEP 1
901
  # =========================
902
 
903
  if st.session_state.step == 1:
 
949
  st.markdown("</div>", unsafe_allow_html=True)
950
 
951
  # =========================
952
+ # STEP 2
953
  # =========================
954
 
955
  elif st.session_state.step == 2:
 
976
 
977
  if audio_bytes:
978
  st.session_state.audio_bytes = audio_bytes
 
979
  if st.session_state.last_audio != audio_bytes:
980
  st.success("Voice note captured. Transcribing...")
981
  transcript = call_asr(audio_bytes)
982
  if transcript:
983
+ st.session_state.user_text = transcript
 
 
984
  st.session_state.last_audio = audio_bytes
985
 
986
  user_text = st.text_area(
 
1051
  st.markdown("</div>", unsafe_allow_html=True)
1052
 
1053
  # =========================
1054
+ # STEP 3
1055
  # =========================
1056
 
1057
  elif st.session_state.step == 3: