Update app.py
Browse files
app.py
CHANGED
|
@@ -18,28 +18,25 @@ st.set_page_config(
|
|
| 18 |
)
|
| 19 |
|
| 20 |
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 21 |
-
if not OPENAI_API_KEY:
|
| 22 |
-
st.error("Missing OPENAI_API_KEY. Please set it in your Space secrets.")
|
| 23 |
client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
|
| 24 |
|
| 25 |
-
#
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
ASR_MODEL = "whisper-1" # transcription
|
| 29 |
|
| 30 |
-
# Health Canada Drug Product Database API
|
| 31 |
DPD_BASE_URL = "https://health-products.canada.ca/api/drug/drugproduct"
|
| 32 |
|
| 33 |
-
# Recalls & Safety Alerts JSON feed (
|
| 34 |
RECALLS_FEED_URL = os.getenv(
|
| 35 |
"RECALLS_FEED_URL",
|
| 36 |
"https://recalls-rappels.canada.ca/static-data/items-en.json"
|
| 37 |
)
|
| 38 |
|
| 39 |
-
# CIHI wait-times info (general reference
|
| 40 |
WAIT_TIMES_INFO_URL = "https://www.cihi.ca/en/topics/access-and-wait-times"
|
| 41 |
|
| 42 |
-
# Postal code to province mapping (
|
| 43 |
POSTAL_PREFIX_TO_PROVINCE = {
|
| 44 |
"A": "Newfoundland and Labrador",
|
| 45 |
"B": "Nova Scotia",
|
|
@@ -58,12 +55,12 @@ POSTAL_PREFIX_TO_PROVINCE = {
|
|
| 58 |
"T": "Alberta",
|
| 59 |
"V": "British Columbia",
|
| 60 |
"X": "Northwest Territories / Nunavut",
|
| 61 |
-
"Y": "Yukon"
|
| 62 |
}
|
| 63 |
|
| 64 |
|
| 65 |
# =========================
|
| 66 |
-
# HELPERS
|
| 67 |
# =========================
|
| 68 |
|
| 69 |
def safe_get_json(url, params=None, timeout=6):
|
|
@@ -86,85 +83,63 @@ def shorten(text, max_chars=400):
|
|
| 86 |
|
| 87 |
|
| 88 |
# =========================
|
| 89 |
-
# REGION
|
| 90 |
# =========================
|
| 91 |
|
| 92 |
def infer_province_from_postal(postal_code: str):
|
| 93 |
-
"""
|
| 94 |
-
Very simple FSA-based province inference using the first character.
|
| 95 |
-
"""
|
| 96 |
if not postal_code:
|
| 97 |
return None
|
| 98 |
pc = postal_code.strip().upper().replace(" ", "")
|
| 99 |
if not pc:
|
| 100 |
return None
|
| 101 |
-
|
| 102 |
-
return POSTAL_PREFIX_TO_PROVINCE.get(first)
|
| 103 |
|
| 104 |
|
| 105 |
def tool_region_context(postal_code: str) -> str:
|
| 106 |
-
"""
|
| 107 |
-
Build region-aware context for the agent:
|
| 108 |
-
- Province guess (if possible)
|
| 109 |
-
- Telehealth-style guidance
|
| 110 |
-
- Where to check local wait times / services
|
| 111 |
-
"""
|
| 112 |
province = infer_province_from_postal(postal_code)
|
| 113 |
if not postal_code:
|
| 114 |
-
|
| 115 |
-
return base
|
| 116 |
|
| 117 |
if not province:
|
| 118 |
return (
|
| 119 |
-
f"Postal code '{postal_code}' could not be
|
| 120 |
-
"Provide
|
| 121 |
)
|
| 122 |
|
| 123 |
-
# Generic, safe phrasing without hard-coding all numbers:
|
| 124 |
telehealth_note = (
|
| 125 |
-
f"In {province},
|
| 126 |
-
"and
|
| 127 |
)
|
| 128 |
-
|
| 129 |
wait_note = (
|
| 130 |
-
"
|
| 131 |
-
"
|
| 132 |
-
f"Reference (non-exclusively): {WAIT_TIMES_INFO_URL}"
|
| 133 |
)
|
| 134 |
|
| 135 |
return (
|
| 136 |
-
f"User
|
| 137 |
f"{telehealth_note}\n"
|
| 138 |
f"{wait_note}"
|
| 139 |
)
|
| 140 |
|
| 141 |
|
| 142 |
# =========================
|
| 143 |
-
#
|
| 144 |
# =========================
|
| 145 |
|
| 146 |
def tool_lookup_drug_products(candidates):
|
| 147 |
if not candidates:
|
| 148 |
-
return "No clear medication names detected that require a
|
| 149 |
|
| 150 |
found_lines = []
|
| 151 |
for name in candidates[:5]:
|
| 152 |
-
params = {
|
| 153 |
-
"lang": "en",
|
| 154 |
-
"type": "json",
|
| 155 |
-
"brandname": name
|
| 156 |
-
}
|
| 157 |
data = safe_get_json(DPD_BASE_URL, params=params)
|
| 158 |
if not data:
|
| 159 |
continue
|
| 160 |
|
| 161 |
unique_brands = set()
|
| 162 |
for item in data[:3]:
|
| 163 |
-
b = (
|
| 164 |
-
item.get("brand_name")
|
| 165 |
-
or item.get("brandname")
|
| 166 |
-
or name
|
| 167 |
-
)
|
| 168 |
if b:
|
| 169 |
unique_brands.add(b)
|
| 170 |
|
|
@@ -181,30 +156,37 @@ def tool_lookup_drug_products(candidates):
|
|
| 181 |
)
|
| 182 |
|
| 183 |
found_lines.append(
|
| 184 |
-
"For definitive product information,
|
| 185 |
-
"users must consult the official Health Canada Drug Product Database."
|
| 186 |
)
|
| 187 |
return "\n".join(found_lines)
|
| 188 |
|
| 189 |
|
| 190 |
# =========================
|
| 191 |
-
#
|
| 192 |
# =========================
|
| 193 |
|
| 194 |
def tool_get_recent_recalls_snippet():
|
| 195 |
data = safe_get_json(RECALLS_FEED_URL)
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
return (
|
| 198 |
-
"Unable to
|
| 199 |
"Recalls and Safety Alerts website for up-to-date information."
|
| 200 |
)
|
| 201 |
|
| 202 |
-
|
| 203 |
-
lines = ["Recent recalls and safety alerts (high-level snapshot):"]
|
| 204 |
for item in items:
|
| 205 |
title = (
|
| 206 |
item.get("title")
|
| 207 |
or item.get("english_title")
|
|
|
|
| 208 |
or "Recall / Alert"
|
| 209 |
)
|
| 210 |
date = item.get("date_published") or item.get("date") or ""
|
|
@@ -217,23 +199,23 @@ def tool_get_recent_recalls_snippet():
|
|
| 217 |
|
| 218 |
|
| 219 |
# =========================
|
| 220 |
-
#
|
| 221 |
# =========================
|
| 222 |
|
| 223 |
def tool_get_wait_times_awareness():
|
| 224 |
return textwrap.dedent(f"""
|
| 225 |
-
Use
|
| 226 |
-
- CIHI and several provinces publish
|
| 227 |
-
- These are
|
| 228 |
Guidance:
|
| 229 |
-
- For non-urgent issues: consider
|
| 230 |
-
- For
|
| 231 |
-
|
| 232 |
""").strip()
|
| 233 |
|
| 234 |
|
| 235 |
# =========================
|
| 236 |
-
#
|
| 237 |
# =========================
|
| 238 |
|
| 239 |
def extract_candidate_terms(text: str):
|
|
@@ -248,7 +230,7 @@ def extract_candidate_terms(text: str):
|
|
| 248 |
|
| 249 |
|
| 250 |
# =========================
|
| 251 |
-
# OPENAI
|
| 252 |
# =========================
|
| 253 |
|
| 254 |
def call_vision_summarizer(image_bytes: bytes) -> str:
|
|
@@ -256,14 +238,15 @@ def call_vision_summarizer(image_bytes: bytes) -> str:
|
|
| 256 |
return ""
|
| 257 |
|
| 258 |
b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
|
|
| 259 |
prompt = (
|
| 260 |
"You are a cautious Canadian health-information assistant.\n"
|
| 261 |
-
"
|
| 262 |
-
"- If
|
| 263 |
-
"- If
|
| 264 |
-
"- If
|
| 265 |
-
"- If
|
| 266 |
-
"Never diagnose. Never prescribe.
|
| 267 |
)
|
| 268 |
|
| 269 |
try:
|
|
@@ -339,29 +322,26 @@ def call_reasoning_agent(
|
|
| 339 |
You are CareCall AI, an agentic Canadian health information assistant.
|
| 340 |
|
| 341 |
Rules:
|
| 342 |
-
-
|
| 343 |
-
-
|
| 344 |
-
-
|
| 345 |
-
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
- Always mention that official sources include:
|
| 359 |
-
Health Canada, Public Health Agency of Canada, CIHI, and the user's provincial health services.
|
| 360 |
-
- Max 350 words. Clear, calm, practical.
|
| 361 |
"""
|
| 362 |
|
| 363 |
user_prompt = f"""
|
| 364 |
-
INPUT
|
| 365 |
|
| 366 |
1) User narrative:
|
| 367 |
{narrative}
|
|
@@ -383,17 +363,17 @@ INPUT TO THE AGENT
|
|
| 383 |
|
| 384 |
TASK
|
| 385 |
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
1. Briefly summarize what the user seems
|
| 389 |
-
2.
|
| 390 |
-
3. Provide clear next
|
| 391 |
-
-
|
| 392 |
-
-
|
| 393 |
-
-
|
| 394 |
-
-
|
| 395 |
-
4. If
|
| 396 |
-
5. End with a
|
| 397 |
"""
|
| 398 |
|
| 399 |
try:
|
|
@@ -409,7 +389,7 @@ Using ONLY the above information:
|
|
| 409 |
except Exception as e:
|
| 410 |
return (
|
| 411 |
f"(Reasoning agent unavailable: {e})\n\n"
|
| 412 |
-
"Please contact a licensed provider, Telehealth, or emergency
|
| 413 |
)
|
| 414 |
|
| 415 |
|
|
@@ -420,31 +400,38 @@ Using ONLY the above information:
|
|
| 420 |
st.title("CareCall AI (Canada)")
|
| 421 |
st.caption("Agentic, vision + voice + postal-aware health info companion. Not a doctor. Not a diagnosis.")
|
| 422 |
|
|
|
|
|
|
|
|
|
|
| 423 |
st.markdown(
|
| 424 |
"""
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
"""
|
| 435 |
)
|
| 436 |
|
| 437 |
-
# Postal code
|
| 438 |
st.markdown("### 1. Postal code (optional, Canada only)")
|
| 439 |
postal_code = st.text_input(
|
| 440 |
-
"
|
| 441 |
-
max_chars=7
|
| 442 |
)
|
| 443 |
|
| 444 |
-
# Image upload
|
| 445 |
st.markdown("### 2. Upload an image (optional)")
|
| 446 |
image_file = st.file_uploader(
|
| 447 |
-
"Upload a photo (JPG/PNG). Avoid
|
| 448 |
type=["jpg", "jpeg", "png"],
|
| 449 |
accept_multiple_files=False,
|
| 450 |
)
|
|
@@ -452,8 +439,9 @@ image_file = st.file_uploader(
|
|
| 452 |
image_bytes = None
|
| 453 |
if image_file is not None:
|
| 454 |
try:
|
| 455 |
-
|
| 456 |
-
|
|
|
|
| 457 |
buf = io.BytesIO()
|
| 458 |
img.save(buf, format="JPEG")
|
| 459 |
image_bytes = buf.getvalue()
|
|
@@ -461,7 +449,7 @@ if image_file is not None:
|
|
| 461 |
st.warning(f"Could not read that image: {e}")
|
| 462 |
image_bytes = None
|
| 463 |
|
| 464 |
-
# Audio upload
|
| 465 |
st.markdown("### 3. Add a voice note (optional)")
|
| 466 |
audio_file = st.file_uploader(
|
| 467 |
"Upload a short voice note (wav/mp3/m4a).",
|
|
@@ -469,9 +457,10 @@ audio_file = st.file_uploader(
|
|
| 469 |
accept_multiple_files=False,
|
| 470 |
)
|
| 471 |
|
| 472 |
-
# Text description
|
|
|
|
| 473 |
user_text = st.text_area(
|
| 474 |
-
"
|
| 475 |
placeholder='Example: "Red itchy patch on forearm for 3 days, mild burning, no fever, using a new soap."',
|
| 476 |
height=120,
|
| 477 |
)
|
|
@@ -493,7 +482,7 @@ if st.button("Run CareCall Agent"):
|
|
| 493 |
if audio_file is not None:
|
| 494 |
voice_text = call_asr(audio_file)
|
| 495 |
|
| 496 |
-
#
|
| 497 |
combined_for_terms = " ".join(
|
| 498 |
t for t in [user_text or "", voice_text or "", vision_summary or ""] if t
|
| 499 |
)
|
|
@@ -503,12 +492,14 @@ if st.button("Run CareCall Agent"):
|
|
| 503 |
# Recalls
|
| 504 |
recalls_context = tool_get_recent_recalls_snippet()
|
| 505 |
|
| 506 |
-
# Wait-
|
| 507 |
wait_times_context = tool_get_wait_times_awareness()
|
| 508 |
|
| 509 |
-
# Region context
|
| 510 |
-
region_context =
|
| 511 |
-
|
|
|
|
|
|
|
| 512 |
)
|
| 513 |
|
| 514 |
# Final reasoning
|
|
@@ -528,15 +519,16 @@ if st.button("Run CareCall Agent"):
|
|
| 528 |
st.markdown(
|
| 529 |
"""
|
| 530 |
---
|
| 531 |
-
Critical Safety Notice
|
| 532 |
-
|
| 533 |
-
- This tool does not provide medical diagnoses, prescriptions, or emergency triage.
|
| 534 |
-
- It may be incomplete or incorrect and must not replace a doctor, nurse, pharmacist,
|
| 535 |
-
Telehealth service, or emergency
|
| 536 |
-
- If you have severe or worsening symptoms (
|
| 537 |
-
|
| 538 |
-
call 911 or go to the nearest emergency department immediately.
|
| 539 |
-
- For
|
| 540 |
-
Health Canada, the Public Health Agency of Canada, CIHI,
|
|
|
|
| 541 |
"""
|
| 542 |
)
|
|
|
|
| 18 |
)
|
| 19 |
|
| 20 |
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
|
|
|
|
|
|
| 21 |
client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
|
| 22 |
|
| 23 |
+
VISION_MODEL = "gpt-4.1-mini" # vision-capable OpenAI model
|
| 24 |
+
REASONING_MODEL = "gpt-4.1-mini" # reasoning / agent model
|
| 25 |
+
ASR_MODEL = "whisper-1" # transcription model
|
|
|
|
| 26 |
|
| 27 |
+
# Health Canada Drug Product Database API
|
| 28 |
DPD_BASE_URL = "https://health-products.canada.ca/api/drug/drugproduct"
|
| 29 |
|
| 30 |
+
# Recalls & Safety Alerts JSON feed (example; can override via env if GC changes path)
|
| 31 |
RECALLS_FEED_URL = os.getenv(
|
| 32 |
"RECALLS_FEED_URL",
|
| 33 |
"https://recalls-rappels.canada.ca/static-data/items-en.json"
|
| 34 |
)
|
| 35 |
|
| 36 |
+
# CIHI wait-times info (general reference)
|
| 37 |
WAIT_TIMES_INFO_URL = "https://www.cihi.ca/en/topics/access-and-wait-times"
|
| 38 |
|
| 39 |
+
# Postal code to province mapping (first letter)
|
| 40 |
POSTAL_PREFIX_TO_PROVINCE = {
|
| 41 |
"A": "Newfoundland and Labrador",
|
| 42 |
"B": "Nova Scotia",
|
|
|
|
| 55 |
"T": "Alberta",
|
| 56 |
"V": "British Columbia",
|
| 57 |
"X": "Northwest Territories / Nunavut",
|
| 58 |
+
"Y": "Yukon",
|
| 59 |
}
|
| 60 |
|
| 61 |
|
| 62 |
# =========================
|
| 63 |
+
# GENERIC HELPERS
|
| 64 |
# =========================
|
| 65 |
|
| 66 |
def safe_get_json(url, params=None, timeout=6):
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
# =========================
|
| 86 |
+
# REGION / POSTAL HELPERS
|
| 87 |
# =========================
|
| 88 |
|
| 89 |
def infer_province_from_postal(postal_code: str):
|
|
|
|
|
|
|
|
|
|
| 90 |
if not postal_code:
|
| 91 |
return None
|
| 92 |
pc = postal_code.strip().upper().replace(" ", "")
|
| 93 |
if not pc:
|
| 94 |
return None
|
| 95 |
+
return POSTAL_PREFIX_TO_PROVINCE.get(pc[0])
|
|
|
|
| 96 |
|
| 97 |
|
| 98 |
def tool_region_context(postal_code: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
province = infer_province_from_postal(postal_code)
|
| 100 |
if not postal_code:
|
| 101 |
+
return "No postal code provided. Use general Canada-wide guidance."
|
|
|
|
| 102 |
|
| 103 |
if not province:
|
| 104 |
return (
|
| 105 |
+
f"Postal code '{postal_code}' could not be mapped reliably. "
|
| 106 |
+
"Provide Canada-wide guidance and advise checking local provincial health websites and Telehealth services."
|
| 107 |
)
|
| 108 |
|
|
|
|
| 109 |
telehealth_note = (
|
| 110 |
+
f"In {province}, suggest the official provincial Telehealth / 811-style nurse advice line "
|
| 111 |
+
"and Ministry of Health website for region-specific guidance."
|
| 112 |
)
|
|
|
|
| 113 |
wait_note = (
|
| 114 |
+
"For non-urgent issues, users can check provincial or regional health authority pages and CIHI indicators "
|
| 115 |
+
f"for access and wait-time information. Reference (general): {WAIT_TIMES_INFO_URL}"
|
|
|
|
| 116 |
)
|
| 117 |
|
| 118 |
return (
|
| 119 |
+
f"User postal code: '{postal_code}' mapped to {province}.\n"
|
| 120 |
f"{telehealth_note}\n"
|
| 121 |
f"{wait_note}"
|
| 122 |
)
|
| 123 |
|
| 124 |
|
| 125 |
# =========================
|
| 126 |
+
# DRUG PRODUCT LOOKUP (DPD)
|
| 127 |
# =========================
|
| 128 |
|
| 129 |
def tool_lookup_drug_products(candidates):
|
| 130 |
if not candidates:
|
| 131 |
+
return "No clear medication/product names detected that require a Drug Product Database lookup."
|
| 132 |
|
| 133 |
found_lines = []
|
| 134 |
for name in candidates[:5]:
|
| 135 |
+
params = {"lang": "en", "type": "json", "brandname": name}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
data = safe_get_json(DPD_BASE_URL, params=params)
|
| 137 |
if not data:
|
| 138 |
continue
|
| 139 |
|
| 140 |
unique_brands = set()
|
| 141 |
for item in data[:3]:
|
| 142 |
+
b = item.get("brand_name") or item.get("brandname") or name
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
if b:
|
| 144 |
unique_brands.add(b)
|
| 145 |
|
|
|
|
| 156 |
)
|
| 157 |
|
| 158 |
found_lines.append(
|
| 159 |
+
"For definitive product information, users must consult the official Health Canada Drug Product Database."
|
|
|
|
| 160 |
)
|
| 161 |
return "\n".join(found_lines)
|
| 162 |
|
| 163 |
|
| 164 |
# =========================
|
| 165 |
+
# RECALLS & SAFETY ALERTS
|
| 166 |
# =========================
|
| 167 |
|
| 168 |
def tool_get_recent_recalls_snippet():
|
| 169 |
data = safe_get_json(RECALLS_FEED_URL)
|
| 170 |
+
items = []
|
| 171 |
+
|
| 172 |
+
if isinstance(data, list):
|
| 173 |
+
items = data[:5]
|
| 174 |
+
elif isinstance(data, dict):
|
| 175 |
+
# some feeds use { items: [...] }
|
| 176 |
+
items = (data.get("items") or data.get("results") or [])[:5]
|
| 177 |
+
|
| 178 |
+
if not items:
|
| 179 |
return (
|
| 180 |
+
"Unable to load recent recalls. Direct users to the official Government of Canada "
|
| 181 |
"Recalls and Safety Alerts website for up-to-date information."
|
| 182 |
)
|
| 183 |
|
| 184 |
+
lines = ["Recent recalls & safety alerts (high-level snapshot):"]
|
|
|
|
| 185 |
for item in items:
|
| 186 |
title = (
|
| 187 |
item.get("title")
|
| 188 |
or item.get("english_title")
|
| 189 |
+
or item.get("name")
|
| 190 |
or "Recall / Alert"
|
| 191 |
)
|
| 192 |
date = item.get("date_published") or item.get("date") or ""
|
|
|
|
| 199 |
|
| 200 |
|
| 201 |
# =========================
|
| 202 |
+
# WAIT TIMES AWARENESS (GENERIC)
|
| 203 |
# =========================
|
| 204 |
|
| 205 |
def tool_get_wait_times_awareness():
|
| 206 |
return textwrap.dedent(f"""
|
| 207 |
+
Use Canadian open-data wait-time indicators conceptually:
|
| 208 |
+
- CIHI and several provinces publish average wait-time dashboards.
|
| 209 |
+
- These are not real-time triage.
|
| 210 |
Guidance:
|
| 211 |
+
- For mild/non-urgent issues: consider local clinics or urgent care; check provincial resources.
|
| 212 |
+
- For red-flag or severe symptoms: skip tools and go directly to ER or call 911.
|
| 213 |
+
General reference: {WAIT_TIMES_INFO_URL}
|
| 214 |
""").strip()
|
| 215 |
|
| 216 |
|
| 217 |
# =========================
|
| 218 |
+
# TERM EXTRACTION
|
| 219 |
# =========================
|
| 220 |
|
| 221 |
def extract_candidate_terms(text: str):
|
|
|
|
| 230 |
|
| 231 |
|
| 232 |
# =========================
|
| 233 |
+
# OPENAI CALLS
|
| 234 |
# =========================
|
| 235 |
|
| 236 |
def call_vision_summarizer(image_bytes: bytes) -> str:
|
|
|
|
| 238 |
return ""
|
| 239 |
|
| 240 |
b64 = base64.b64encode(image_bytes).decode("utf-8")
|
| 241 |
+
|
| 242 |
prompt = (
|
| 243 |
"You are a cautious Canadian health-information assistant.\n"
|
| 244 |
+
"Describe only what is visibly observable in neutral terms.\n"
|
| 245 |
+
"- If skin: describe general appearance (location, size, colour, pattern). Do NOT name diseases.\n"
|
| 246 |
+
"- If pill/box: mention visible drug name/strength/markings if legible.\n"
|
| 247 |
+
"- If document: say what type (e.g., lab report, appointment letter) without copying identifiers.\n"
|
| 248 |
+
"- If irrelevant/unclear: say that it is not medically interpretable.\n"
|
| 249 |
+
"Never diagnose. Never prescribe. Limit to about 80-100 words."
|
| 250 |
)
|
| 251 |
|
| 252 |
try:
|
|
|
|
| 322 |
You are CareCall AI, an agentic Canadian health information assistant.
|
| 323 |
|
| 324 |
Rules:
|
| 325 |
+
- Do NOT diagnose.
|
| 326 |
+
- Do NOT prescribe or give dosing.
|
| 327 |
+
- Do NOT claim exact real-time wait times.
|
| 328 |
+
- Use only the information and tool outputs provided.
|
| 329 |
+
- Use open-data-style context (Drug Product DB, recalls, wait-time awareness, region) to improve safety.
|
| 330 |
+
- Map concerns to broad categories only (e.g., irritation vs infection vs allergy vs medication vs admin).
|
| 331 |
+
- Give practical next steps in Canada:
|
| 332 |
+
* self-care when clearly mild,
|
| 333 |
+
* pharmacist / family doctor / walk-in clinic,
|
| 334 |
+
* provincial Telehealth / nurse line,
|
| 335 |
+
* ER / 911 for red-flag symptoms.
|
| 336 |
+
- Red flags (chest pain, breathing trouble, stroke signs, severe bleeding, suicidal thoughts,
|
| 337 |
+
major trauma, high fever with stiff neck, rapidly worsening symptoms, etc.) -> clearly say ER/911.
|
| 338 |
+
- Mention official sources: Health Canada, Public Health Agency of Canada, CIHI,
|
| 339 |
+
and the user's provincial health services.
|
| 340 |
+
- Maximum length: 350 words. Clear and calm.
|
|
|
|
|
|
|
|
|
|
| 341 |
"""
|
| 342 |
|
| 343 |
user_prompt = f"""
|
| 344 |
+
INPUT CONTEXT
|
| 345 |
|
| 346 |
1) User narrative:
|
| 347 |
{narrative}
|
|
|
|
| 363 |
|
| 364 |
TASK
|
| 365 |
|
| 366 |
+
Based only on this information:
|
| 367 |
+
|
| 368 |
+
1. Briefly summarize what the user seems concerned about (neutral; no diagnosis).
|
| 369 |
+
2. Briefly outline broad possible categories (without asserting any specific disease).
|
| 370 |
+
3. Provide clear next steps in bullet form, aligned with Canadian context and region info where available:
|
| 371 |
+
- when self-care might be reasonable,
|
| 372 |
+
- when to speak with a pharmacist, family doctor, or walk-in clinic,
|
| 373 |
+
- when to use provincial Telehealth / nurse lines,
|
| 374 |
+
- when to go to ER or call 911.
|
| 375 |
+
4. If medication/recall context looks relevant, remind them to confirm via official Health Canada resources.
|
| 376 |
+
5. End with a short disclaimer that this is informational only and not medical care.
|
| 377 |
"""
|
| 378 |
|
| 379 |
try:
|
|
|
|
| 389 |
except Exception as e:
|
| 390 |
return (
|
| 391 |
f"(Reasoning agent unavailable: {e})\n\n"
|
| 392 |
+
"Please contact a licensed provider, Telehealth service, or emergency care as appropriate."
|
| 393 |
)
|
| 394 |
|
| 395 |
|
|
|
|
| 400 |
st.title("CareCall AI (Canada)")
|
| 401 |
st.caption("Agentic, vision + voice + postal-aware health info companion. Not a doctor. Not a diagnosis.")
|
| 402 |
|
| 403 |
+
if not OPENAI_API_KEY:
|
| 404 |
+
st.warning("Set OPENAI_API_KEY in your Hugging Face Space secrets to enable the AI backend.")
|
| 405 |
+
|
| 406 |
st.markdown(
|
| 407 |
"""
|
| 408 |
+
**How it works**
|
| 409 |
+
|
| 410 |
+
1. Optionally enter your **Canadian postal code** (to tailor guidance to your province).
|
| 411 |
+
2. Optionally upload a **photo** (rash, pill bottle, letter, etc.).
|
| 412 |
+
3. Optionally upload a **voice note**, and/or type a **short description**.
|
| 413 |
+
4. The agent:
|
| 414 |
+
- analyzes the image with a vision model (safely),
|
| 415 |
+
- transcribes your voice with Whisper,
|
| 416 |
+
- looks up related info via:
|
| 417 |
+
- Health Canada Drug Product Database (high-level),
|
| 418 |
+
- Recalls & Safety Alerts feed (snapshot),
|
| 419 |
+
- CIHI/provincial wait-time context (awareness),
|
| 420 |
+
- then generates structured, non-diagnostic next steps.
|
| 421 |
"""
|
| 422 |
)
|
| 423 |
|
| 424 |
+
# 1) Postal code
|
| 425 |
st.markdown("### 1. Postal code (optional, Canada only)")
|
| 426 |
postal_code = st.text_input(
|
| 427 |
+
"Canadian postal code (e.g., M5V 2T6). Leave blank to keep guidance general.",
|
| 428 |
+
max_chars=7,
|
| 429 |
)
|
| 430 |
|
| 431 |
+
# 2) Image upload
|
| 432 |
st.markdown("### 2. Upload an image (optional)")
|
| 433 |
image_file = st.file_uploader(
|
| 434 |
+
"Upload a photo (JPG/PNG). Avoid very identifying or explicit content.",
|
| 435 |
type=["jpg", "jpeg", "png"],
|
| 436 |
accept_multiple_files=False,
|
| 437 |
)
|
|
|
|
| 439 |
image_bytes = None
|
| 440 |
if image_file is not None:
|
| 441 |
try:
|
| 442 |
+
# Convert to RGB to avoid RGBA -> JPEG error
|
| 443 |
+
img = Image.open(image_file).convert("RGB")
|
| 444 |
+
st.image(img, caption="Image received", use_container_width=True)
|
| 445 |
buf = io.BytesIO()
|
| 446 |
img.save(buf, format="JPEG")
|
| 447 |
image_bytes = buf.getvalue()
|
|
|
|
| 449 |
st.warning(f"Could not read that image: {e}")
|
| 450 |
image_bytes = None
|
| 451 |
|
| 452 |
+
# 3) Audio upload
|
| 453 |
st.markdown("### 3. Add a voice note (optional)")
|
| 454 |
audio_file = st.file_uploader(
|
| 455 |
"Upload a short voice note (wav/mp3/m4a).",
|
|
|
|
| 457 |
accept_multiple_files=False,
|
| 458 |
)
|
| 459 |
|
| 460 |
+
# 4) Text description
|
| 461 |
+
st.markdown("### 4. Or type a short description")
|
| 462 |
user_text = st.text_area(
|
| 463 |
+
"Describe what’s going on:",
|
| 464 |
placeholder='Example: "Red itchy patch on forearm for 3 days, mild burning, no fever, using a new soap."',
|
| 465 |
height=120,
|
| 466 |
)
|
|
|
|
| 482 |
if audio_file is not None:
|
| 483 |
voice_text = call_asr(audio_file)
|
| 484 |
|
| 485 |
+
# Candidate terms (for Drug DB)
|
| 486 |
combined_for_terms = " ".join(
|
| 487 |
t for t in [user_text or "", voice_text or "", vision_summary or ""] if t
|
| 488 |
)
|
|
|
|
| 492 |
# Recalls
|
| 493 |
recalls_context = tool_get_recent_recalls_snippet()
|
| 494 |
|
| 495 |
+
# Wait-times awareness
|
| 496 |
wait_times_context = tool_get_wait_times_awareness()
|
| 497 |
|
| 498 |
+
# Region context
|
| 499 |
+
region_context = (
|
| 500 |
+
tool_region_context(postal_code)
|
| 501 |
+
if postal_code
|
| 502 |
+
else "No postal code provided. Use Canada-wide guidance and suggest provincial resources."
|
| 503 |
)
|
| 504 |
|
| 505 |
# Final reasoning
|
|
|
|
| 519 |
st.markdown(
|
| 520 |
"""
|
| 521 |
---
|
| 522 |
+
**Critical Safety Notice**
|
| 523 |
+
|
| 524 |
+
- This tool does **not** provide medical diagnoses, prescriptions, or emergency triage.
|
| 525 |
+
- It may be incomplete or incorrect and must **not** replace a doctor, nurse, pharmacist,
|
| 526 |
+
Telehealth service, or emergency department.
|
| 527 |
+
- If you have severe or rapidly worsening symptoms (chest pain, trouble breathing, signs of stroke,
|
| 528 |
+
heavy bleeding, suicidal thoughts, major injury, high fever with stiff neck, etc.),
|
| 529 |
+
call **911** or go to the **nearest emergency department** immediately.
|
| 530 |
+
- For trusted information, consult:
|
| 531 |
+
Health Canada, the Public Health Agency of Canada, CIHI,
|
| 532 |
+
and your provincial/territorial health services.
|
| 533 |
"""
|
| 534 |
)
|