Seth0330 commited on
Commit
acea432
·
verified ·
1 Parent(s): 9feb77c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +121 -88
app.py CHANGED
@@ -7,8 +7,7 @@ import textwrap
7
  import requests
8
  import streamlit as st
9
  from PIL import Image
10
- from openai import OpenAI
11
- from audio_recorder_streamlit import audio_recorder # mic button
12
 
13
  # =========================
14
  # BASIC CONFIG
@@ -20,16 +19,28 @@ st.set_page_config(
20
  layout="centered"
21
  )
22
 
23
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
24
- if not OPENAI_API_KEY:
25
- st.warning("Set OPENAI_API_KEY in your Space secrets to enable AI features.")
 
 
 
 
26
  st.stop()
27
 
28
- client = OpenAI(api_key=OPENAI_API_KEY)
 
 
 
 
29
 
30
- VISION_MODEL = "gpt-4.1-mini" # image-capable model
31
- REASONING_MODEL = "gpt-4o" # reasoning / instructions
32
- ASR_MODEL = "whisper-1" # transcription
 
 
 
 
33
 
34
  DPD_BASE_URL = "https://health-products.canada.ca/api/drug/drugproduct"
35
  RECALLS_FEED_URL = os.getenv(
@@ -75,7 +86,6 @@ st.markdown(
75
  justify-content: center;
76
  gap: 8px;
77
  margin-bottom: 0.8rem;
78
- /* kill any weird pill styles */
79
  background: transparent !important;
80
  box-shadow: none !important;
81
  padding: 0 !important;
@@ -160,7 +170,7 @@ def shorten(text, max_chars=260):
160
  def extract_phone(text: str) -> str:
161
  if not text:
162
  return ""
163
- m = re.search(r"(\(?\d{3}\)?[\\s\\-\\.]?\\d{3}[\\s\\-\\.]?\\d{4})", text)
164
  return m.group(1) if m else ""
165
 
166
 
@@ -486,13 +496,41 @@ def tool_get_wait_times_awareness() -> str:
486
 
487
 
488
  # =========================
489
- # OPENAI HELPERS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  # =========================
491
 
492
  def call_vision_summarizer(image_bytes: bytes) -> str:
493
  """
494
- Structured, cautious but detailed visual assessment.
495
- Treats the image as a primary signal, without diagnosing.
496
  """
497
  if not image_bytes:
498
  return ""
@@ -514,59 +552,74 @@ Describe clearly and systematically:
514
  6. Any visible discharge, bleeding, or crusting.
515
  7. Any objects/devices/dressings in view (e.g., ring, bandage, splint).
516
  8. Any visually concerning features that might suggest higher risk
517
- (e.g., blackened tissue, rapidly spreading redness pattern, deep open wound, thick pus) — describe ONLY what is seen.
518
 
519
  Keep it about 120–180 words.
520
  Write as if supporting a separate triage system, not directly reassuring or diagnosing the patient.
521
  """
522
 
523
- try:
524
- resp = client.chat.completions.create(
525
- model=VISION_MODEL,
526
- messages=[{
527
- "role": "user",
528
- "content": [
529
- {"type": "text", "text": prompt},
530
- {
531
- "type": "image_url",
532
- "image_url": {"url": f"data:image/jpeg;base64,{b64}"},
533
  },
534
- ],
535
- }],
536
- temperature=0.2,
537
- )
538
- return resp.choices[0].message.content.strip()
539
- except Exception as e:
540
- return f"(Vision analysis unavailable: {e})"
541
 
 
 
 
 
 
 
542
 
543
  def call_asr(audio_source) -> str:
544
  """
545
- Accepts:
 
546
  - bytes from audio_recorder_streamlit, or
547
- - a file-like object (if extended in future).
548
  """
549
- if not audio_source:
550
  return ""
551
- try:
552
- if isinstance(audio_source, bytes):
553
- bio = io.BytesIO(audio_source)
554
- bio.name = "voice.wav"
555
- else:
556
- audio_bytes = audio_source.read()
557
- bio = io.BytesIO(audio_bytes)
558
- bio.name = getattr(audio_source, "name", "voice.wav")
559
 
560
- result = client.audio.transcriptions.create(
561
- model=ASR_MODEL,
562
- file=bio
563
- )
564
- txt = getattr(result, "text", None)
565
- return txt.strip() if isinstance(txt, str) else str(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  except Exception as e:
567
  return f"(Transcription unavailable: {e})"
568
 
569
 
 
 
 
 
570
  def call_reasoning_agent(
571
  narrative: str,
572
  vision_summary: str = "",
@@ -621,23 +674,17 @@ You must internally choose EXACTLY ONE primary pathway, but DO NOT show letters
621
 
622
  (1) "No current action needed (you appear well, continue normal care)"
623
  - Use when the user clearly reports feeling well or "perfect", with no symptoms or concerns.
624
- - Example: "I feel healthy and really perfect and fresh."
625
  - Provide light wellness reminders.
626
- - This is NOT a diagnosis; phrase as "based on what you've shared, you appear well."
627
 
628
  (2) "Home care reasonable (self-care + monitor)"
629
- - For mild, short-lived, non-progressive symptoms that do not limit normal activities
630
- (e.g., light headache, small bruise, mild soreness, mild cold symptoms without red flags).
631
  - Provide simple, safe self-care steps.
632
  - Clearly state when to escalate to pharmacist/clinic/Telehealth.
633
- - Do NOT automatically push these to doctor/ER unless they persist, worsen, or match red flags.
634
 
635
  (3) "Pharmacist / Walk-in clinic / Family doctor or Telehealth recommended"
636
- - For persistent, recurrent, or moderate symptoms,
637
- or when there is diagnostic uncertainty but no clear emergency.
638
  - Explain why an in-person or phone assessment is appropriate.
639
- - Suggest pharmacist for minor medication/symptom questions.
640
- - Suggest walk-in/clinic/family doctor/Telehealth when a physical exam/history is needed.
641
 
642
  (4) "Go to Emergency / Call 911 now"
643
  - For clear red-flag features:
@@ -648,21 +695,16 @@ You must internally choose EXACTLY ONE primary pathway, but DO NOT show letters
648
  - stroke-like symptoms,
649
  - signs of sepsis or systemic illness,
650
  - significant numbness/weakness,
651
- - concerning changes after surgery,
652
- - or visually severe findings in the image.
653
  - Be direct and unambiguous.
654
 
655
  OUTPUT FORMAT (WHAT USER SEES)
656
  ------------------------------
657
  1. **Primary Recommended Pathway**
658
  - One brief sentence stating the recommendation WITHOUT internal labels.
659
- Examples:
660
- - "No current action needed (you appear well, continue normal care)."
661
- - "Home care is reasonable with self-care and monitoring."
662
- - "An in-person assessment with a walk-in clinic, your family doctor, or Telehealth is recommended."
663
- - "You should go to an emergency department or call 911 now."
664
 
665
- 2. Then include sections as appropriate for the chosen pathway:
666
 
667
  If you chose "No current action needed":
668
  - **Home Care / Wellness**
@@ -688,14 +730,14 @@ If you chose "Go to Emergency / Call 911 now":
688
  - **When this is an Emergency (ER/911)**
689
  - **Local & Official Resources**
690
  - **Important Disclaimer**
691
- - Do NOT include home-care-only reassurance in this case.
692
 
693
  LOCAL & OFFICIAL RESOURCES
694
  --------------------------
695
  - If facilities_context lists facilities, summarize them as bullet points:
696
- - name, city/address, phone (if present), hours (if present), and URL.
697
  - If none are listed, say so and direct user to Google Maps / provincial tools.
698
- - Optionally remind:
699
  - Health Canada Drug Product Database / Recalls site,
700
  - CIHI/provincial dashboards for general (non-real-time) wait information.
701
 
@@ -735,21 +777,12 @@ HQ Ontario info:
735
  {hqontario_context or "(not applicable)"}
736
  """
737
 
738
- try:
739
- resp = client.chat.completions.create(
740
- model=REASONING_MODEL,
741
- temperature=0.3,
742
- messages=[
743
- {"role": "system", "content": system_prompt},
744
- {"role": "user", "content": user_message},
745
- ],
746
- )
747
- return resp.choices[0].message.content.strip()
748
- except Exception as e:
749
- return (
750
- f"(Reasoning agent unavailable: {e})\n\n"
751
- "Please contact a licensed provider, Telehealth, or emergency services as appropriate."
752
- )
753
 
754
 
755
  # =========================
@@ -809,11 +842,11 @@ render_steps()
809
  if st.session_state.step == 1:
810
  st.markdown('<div class="card">', unsafe_allow_html=True)
811
  st.markdown(
812
- '<div class="summary-title">Step 1 · Where are you, and what area is this?</div>',
813
  unsafe_allow_html=True,
814
  )
815
  st.markdown(
816
- '<div class="label-soft">Select your city and (optionally) take a clear photo of the area you are concerned about.</div>',
817
  unsafe_allow_html=True,
818
  )
819
 
@@ -865,7 +898,7 @@ elif st.session_state.step == 2:
865
  unsafe_allow_html=True,
866
  )
867
  st.markdown(
868
- '<div class="label-soft">Use the mic to describe your concern, or type instead. A clear story helps the AI match what it sees in the photo.</div>',
869
  unsafe_allow_html=True,
870
  )
871
 
@@ -891,7 +924,7 @@ elif st.session_state.step == 2:
891
  st.session_state.user_text = user_text
892
 
893
  st.markdown(
894
- '<div class="label-soft">When ready, get your one recommended pathway.</div>',
895
  unsafe_allow_html=True,
896
  )
897
 
 
7
  import requests
8
  import streamlit as st
9
  from PIL import Image
10
+ from audio_recorder_streamlit import audio_recorder
 
11
 
12
  # =========================
13
  # BASIC CONFIG
 
19
  layout="centered"
20
  )
21
 
22
+ # --- Keys / endpoints ---
23
+
24
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
25
+ HF_API_TOKEN = os.getenv("HF_API_TOKEN")
26
+
27
+ if not OPENROUTER_API_KEY:
28
+ st.warning("Set OPENROUTER_API_KEY in your Space secrets (OpenRouter) to enable AI features.")
29
  st.stop()
30
 
31
+ if not HF_API_TOKEN:
32
+ st.warning("Set HF_API_TOKEN in your Space secrets (Hugging Face Inference) to enable speech-to-text.")
33
+ # we don't stop; app still works without voice
34
+
35
+ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
36
 
37
+ # IMPORTANT: put the exact OpenRouter model IDs here.
38
+ # Check https://openrouter.ai/models for the correct slugs.
39
+ VISION_MODEL = os.getenv("VISION_MODEL", "nvidia/nemotron-nano-12b-vl") # <-- adjust if needed
40
+ REASONING_MODEL = os.getenv("REASONING_MODEL", "nvidia/nemotron-nano-12b-vl") # <-- or another Nemotron/Instruct model
41
+
42
+ HF_WHISPER_MODEL = os.getenv("HF_WHISPER_MODEL", "openai/whisper-large-v3")
43
+ HF_WHISPER_URL = f"https://api-inference.huggingface.co/models/{HF_WHISPER_MODEL}"
44
 
45
  DPD_BASE_URL = "https://health-products.canada.ca/api/drug/drugproduct"
46
  RECALLS_FEED_URL = os.getenv(
 
86
  justify-content: center;
87
  gap: 8px;
88
  margin-bottom: 0.8rem;
 
89
  background: transparent !important;
90
  box-shadow: none !important;
91
  padding: 0 !important;
 
170
  def extract_phone(text: str) -> str:
171
  if not text:
172
  return ""
173
+ m = re.search(r"(\(?\d{3}\)?[\s\-\.]?\d{3}[\s\-\.]?\d{4})", text)
174
  return m.group(1) if m else ""
175
 
176
 
 
496
 
497
 
498
  # =========================
499
+ # OPENROUTER HELPER
500
+ # =========================
501
+
502
+ def call_openrouter_chat(model: str, messages, temperature: float = 0.3):
503
+ """
504
+ Generic helper for OpenRouter chat/completions API with OpenAI-format messages.
505
+ """
506
+ headers = {
507
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
508
+ "Content-Type": "application/json",
509
+ # Optional but recommended by OpenRouter:
510
+ "HTTP-Referer": "https://seth0330-save.hf.space",
511
+ "X-Title": "CareCall AI (Canada)",
512
+ }
513
+ payload = {
514
+ "model": model,
515
+ "messages": messages,
516
+ "temperature": temperature,
517
+ }
518
+ try:
519
+ r = requests.post(OPENROUTER_URL, headers=headers, json=payload, timeout=60)
520
+ r.raise_for_status()
521
+ data = r.json()
522
+ return data["choices"][0]["message"]["content"].strip()
523
+ except Exception as e:
524
+ return f"(Model call unavailable: {e})"
525
+
526
+
527
+ # =========================
528
+ # VISION (Nemotron via OpenRouter)
529
  # =========================
530
 
531
  def call_vision_summarizer(image_bytes: bytes) -> str:
532
  """
533
+ Structured, cautious but detailed visual assessment via vision-capable model on OpenRouter.
 
534
  """
535
  if not image_bytes:
536
  return ""
 
552
  6. Any visible discharge, bleeding, or crusting.
553
  7. Any objects/devices/dressings in view (e.g., ring, bandage, splint).
554
  8. Any visually concerning features that might suggest higher risk
555
+ (e.g., blackened tissue, spreading redness, deep open wound, thick pus) — describe ONLY what is seen.
556
 
557
  Keep it about 120–180 words.
558
  Write as if supporting a separate triage system, not directly reassuring or diagnosing the patient.
559
  """
560
 
561
+ messages = [
562
+ {
563
+ "role": "user",
564
+ "content": [
565
+ {"type": "text", "text": prompt},
566
+ {
567
+ "type": "image_url",
568
+ "image_url": {
569
+ "url": f"data:image/jpeg;base64,{b64}"
 
570
  },
571
+ },
572
+ ],
573
+ }
574
+ ]
 
 
 
575
 
576
+ return call_openrouter_chat(VISION_MODEL, messages, temperature=0.2)
577
+
578
+
579
+ # =========================
580
+ # ASR (Whisper via HF Inference)
581
+ # =========================
582
 
583
  def call_asr(audio_source) -> str:
584
  """
585
+ Uses Hugging Face Inference API for Whisper.
586
+ Expects:
587
  - bytes from audio_recorder_streamlit, or
588
+ - file-like object.
589
  """
590
+ if not audio_source or not HF_API_TOKEN:
591
  return ""
 
 
 
 
 
 
 
 
592
 
593
+ # Normalize to bytes
594
+ if isinstance(audio_source, bytes):
595
+ audio_bytes = audio_source
596
+ else:
597
+ audio_bytes = audio_source.read()
598
+
599
+ headers = {
600
+ "Authorization": f"Bearer {HF_API_TOKEN}",
601
+ "Content-Type": "audio/wav", # works for most; HF autodetects
602
+ }
603
+
604
+ try:
605
+ resp = requests.post(HF_WHISPER_URL, headers=headers, data=audio_bytes, timeout=120)
606
+ resp.raise_for_status()
607
+ data = resp.json()
608
+ # ASR pipeline returns {"text": "..."}
609
+ if isinstance(data, dict) and "text" in data:
610
+ return data["text"].strip()
611
+ # Some models may return list-style
612
+ if isinstance(data, list) and data and isinstance(data[0], dict):
613
+ return (data[0].get("text") or data[0].get("generated_text") or "").strip()
614
+ return ""
615
  except Exception as e:
616
  return f"(Transcription unavailable: {e})"
617
 
618
 
619
+ # =========================
620
+ # REASONING AGENT (Nemotron via OpenRouter)
621
+ # =========================
622
+
623
  def call_reasoning_agent(
624
  narrative: str,
625
  vision_summary: str = "",
 
674
 
675
  (1) "No current action needed (you appear well, continue normal care)"
676
  - Use when the user clearly reports feeling well or "perfect", with no symptoms or concerns.
 
677
  - Provide light wellness reminders.
678
+ - Phrase as "based on what you've shared, you appear well." This is NOT a diagnosis.
679
 
680
  (2) "Home care reasonable (self-care + monitor)"
681
+ - For mild, short-lived, non-progressive symptoms that do not limit normal activities.
 
682
  - Provide simple, safe self-care steps.
683
  - Clearly state when to escalate to pharmacist/clinic/Telehealth.
 
684
 
685
  (3) "Pharmacist / Walk-in clinic / Family doctor or Telehealth recommended"
686
+ - For persistent, recurrent, or moderate symptoms, or diagnostic uncertainty without clear emergency.
 
687
  - Explain why an in-person or phone assessment is appropriate.
 
 
688
 
689
  (4) "Go to Emergency / Call 911 now"
690
  - For clear red-flag features:
 
695
  - stroke-like symptoms,
696
  - signs of sepsis or systemic illness,
697
  - significant numbness/weakness,
698
+ - concerning post-surgical changes,
699
+ - or visually severe findings from the image.
700
  - Be direct and unambiguous.
701
 
702
  OUTPUT FORMAT (WHAT USER SEES)
703
  ------------------------------
704
  1. **Primary Recommended Pathway**
705
  - One brief sentence stating the recommendation WITHOUT internal labels.
 
 
 
 
 
706
 
707
+ 2. Then include sections as appropriate:
708
 
709
  If you chose "No current action needed":
710
  - **Home Care / Wellness**
 
730
  - **When this is an Emergency (ER/911)**
731
  - **Local & Official Resources**
732
  - **Important Disclaimer**
733
+ - Do NOT include pure-home-care reassurance in this case.
734
 
735
  LOCAL & OFFICIAL RESOURCES
736
  --------------------------
737
  - If facilities_context lists facilities, summarize them as bullet points:
738
+ - name, city/address, phone (if present), hours (if present), URL.
739
  - If none are listed, say so and direct user to Google Maps / provincial tools.
740
+ - Optionally remind of:
741
  - Health Canada Drug Product Database / Recalls site,
742
  - CIHI/provincial dashboards for general (non-real-time) wait information.
743
 
 
777
  {hqontario_context or "(not applicable)"}
778
  """
779
 
780
+ messages = [
781
+ {"role": "system", "content": system_prompt},
782
+ {"role": "user", "content": user_message},
783
+ ]
784
+
785
+ return call_openrouter_chat(REASONING_MODEL, messages, temperature=0.3)
 
 
 
 
 
 
 
 
 
786
 
787
 
788
  # =========================
 
842
  if st.session_state.step == 1:
843
  st.markdown('<div class="card">', unsafe_allow_html=True)
844
  st.markdown(
845
+ '<div class="summary-title">Step 1 · Where & what are we looking at?</div>',
846
  unsafe_allow_html=True,
847
  )
848
  st.markdown(
849
+ '<div class="label-soft">Select your city and (optionally) take a clear photo of the area you are worried about.</div>',
850
  unsafe_allow_html=True,
851
  )
852
 
 
898
  unsafe_allow_html=True,
899
  )
900
  st.markdown(
901
+ '<div class="label-soft">Use the mic to describe your concern, or type instead. A clear story helps match what we see in the photo.</div>',
902
  unsafe_allow_html=True,
903
  )
904
 
 
924
  st.session_state.user_text = user_text
925
 
926
  st.markdown(
927
+ '<div class="label-soft">When you are ready, get your one recommended pathway.</div>',
928
  unsafe_allow_html=True,
929
  )
930