Seth0330 commited on
Commit
4302ebd
·
verified ·
1 Parent(s): 9d4a26b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +198 -201
app.py CHANGED
@@ -26,10 +26,11 @@ if not OPENAI_API_KEY:
26
 
27
  client = OpenAI(api_key=OPENAI_API_KEY)
28
 
29
- VISION_MODEL = "gpt-4.1-mini" # image-capable
30
- REASONING_MODEL = "gpt-4.1-mini" # reasoning
31
- ASR_MODEL = "whisper-1" # transcription
32
 
 
33
  DPD_BASE_URL = "https://health-products.canada.ca/api/drug/drugproduct"
34
  RECALLS_FEED_URL = os.getenv(
35
  "RECALLS_FEED_URL",
@@ -37,13 +38,14 @@ RECALLS_FEED_URL = os.getenv(
37
  )
38
  WAIT_TIMES_INFO_URL = "https://www.cihi.ca/en/topics/access-and-wait-times"
39
 
 
40
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
41
  GOOGLE_CSE_ID = os.getenv("GOOGLE_CSE_ID")
42
 
43
- # if you later add your own wait-time microservice:
44
  WAIT_TIMES_API_BASE = os.getenv("WAIT_TIMES_API_BASE", "").rstrip("/")
45
 
46
- USER_AGENT = "carecall-ai-google-only/1.0"
47
 
48
  POSTAL_PREFIX_TO_PROVINCE = {
49
  "A": "Newfoundland and Labrador",
@@ -67,7 +69,7 @@ POSTAL_PREFIX_TO_PROVINCE = {
67
  }
68
 
69
  # =========================
70
- # UTILITIES
71
  # =========================
72
 
73
  def safe_get_json(url, params=None, method="GET", data=None, timeout=8):
@@ -105,7 +107,6 @@ def infer_province_from_postal(postal_code: str):
105
  def extract_phone(text: str) -> str:
106
  if not text:
107
  return ""
108
- # basic North American phone pattern
109
  m = re.search(r"(\(?\d{3}\)?[\s\-\.]?\d{3}[\s\-\.]?\d{4})", text)
110
  return m.group(1) if m else ""
111
 
@@ -113,7 +114,6 @@ def extract_phone(text: str) -> str:
113
  def extract_hours(text: str) -> str:
114
  if not text:
115
  return ""
116
- # very rough; just grab a window like "8 AM-2 PM" / "9am–5pm"
117
  m = re.search(
118
  r"(\d{1,2}\s?(?:AM|PM|am|pm)\s?[–\-]\s?\d{1,2}\s?(?:AM|PM|am|pm))",
119
  text
@@ -122,7 +122,7 @@ def extract_hours(text: str) -> str:
122
 
123
 
124
  # =========================
125
- # POSTAL -> CITY (canadacities.csv)
126
  # =========================
127
 
128
  def load_postal_city_map(path: str = "canadacities.csv"):
@@ -136,14 +136,13 @@ def load_postal_city_map(path: str = "canadacities.csv"):
136
  header = next(reader, None)
137
  if not header:
138
  return mapping
139
-
140
  lower = [h.strip().lower() for h in header]
141
 
142
- def find_col(candidates):
143
- for idx, name in enumerate(lower):
144
- for cand in candidates:
145
- if cand in name:
146
- return idx
147
  return None
148
 
149
  pc_idx = find_col(["postal", "pcode", "fsa"])
@@ -156,22 +155,22 @@ def load_postal_city_map(path: str = "canadacities.csv"):
156
  for row in reader:
157
  if not row or len(row) <= pc_idx:
158
  continue
159
- raw_pc = row[pc_idx].strip().upper().replace(" ", "")
160
- if not raw_pc:
161
  continue
162
- prefix = raw_pc[:3]
163
  city = (
164
  row[city_idx].strip()
165
- if (city_idx is not None and len(row) > city_idx)
166
  else ""
167
  )
168
  prov = (
169
  row[prov_idx].strip()
170
- if (prov_idx is not None and len(row) > prov_idx)
171
  else ""
172
  )
173
- if prefix and (city or prov):
174
- mapping.setdefault(prefix, (city, prov))
175
  except Exception:
176
  return mapping
177
 
@@ -207,28 +206,38 @@ def google_cse_search(query: str, num: int = 5):
207
  return safe_get_json(url, params=params)
208
 
209
 
210
- def facilities_from_cse(items, city_filter=None):
211
- """
212
- Parse CSE items into structured facilities.
213
- Only keep results clearly related to the target city/FSA if city_filter is provided.
214
- """
215
- facilities = []
216
- city_lower = (city_filter or "").lower() if city_filter else None
 
 
 
 
 
 
 
217
 
218
- for it in items:
 
 
219
  title = (it.get("title") or "").strip()
220
  link = (it.get("link") or "").strip()
221
  snippet = (it.get("snippet") or it.get("htmlSnippet") or "").strip()
222
-
223
  if not title:
224
  continue
225
 
226
- blob = (title + " " + snippet).lower()
227
- if city_lower and city_lower not in blob:
228
- # Require mention of city name if we know it, to avoid far-away hospitals
 
 
229
  continue
230
 
231
- phone = extract_phone(title + " " + snippet)
232
  hours = extract_hours(snippet)
233
  info = shorten(snippet, 200)
234
 
@@ -237,122 +246,123 @@ def facilities_from_cse(items, city_filter=None):
237
  "info": info,
238
  "phone": phone,
239
  "hours": hours,
240
- "url": link
 
241
  })
242
 
243
- return facilities
 
244
 
245
 
246
  def tool_list_facilities_google(postal_code: str) -> str:
247
  """
248
- Use ONLY Google Custom Search to list nearby:
249
- - Emergency departments / hospitals
250
- - Walk-in / urgent care / family practice
251
- Based on city inferred from canadacities.csv (if available).
252
- Returns markdown listing with name, info/address snippet, phone, hours, url.
 
 
 
253
  """
254
  if not postal_code:
255
  return "No postal code provided; cannot list local facilities."
256
-
257
  if not GOOGLE_API_KEY or not GOOGLE_CSE_ID:
258
  return (
259
  "Google Custom Search is not configured (set GOOGLE_API_KEY and GOOGLE_CSE_ID) "
260
  "so facility listing is unavailable."
261
  )
262
 
263
- city, prov = get_city_from_postal(postal_code)
264
- pc_clean = postal_code.strip().upper().replace(" ", "")
265
- if city:
266
- loc = f"{city}, {prov or 'Canada'}"
267
- city_filter = city # require this name in results
268
- else:
269
- loc = f"{pc_clean} Canada"
270
- city_filter = None
271
-
272
- # --- Emergency / hospital search ---
273
- er_query = f"emergency department hospital near {loc}"
274
- er_data = google_cse_search(er_query, num=8)
275
- er_facilities = facilities_from_cse(er_data.get("items", []), city_filter) if er_data else []
276
-
277
- # --- Walk-in / clinic search ---
278
- clinic_query = f"walk-in clinic OR urgent care OR family practice near {loc}"
279
- clinic_data = google_cse_search(clinic_query, num=8)
280
- clinic_facilities = facilities_from_cse(clinic_data.get("items", []), city_filter) if clinic_data else []
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
  lines = []
283
 
284
- if er_facilities:
285
- lines.append(f"Nearby Emergency Departments / Hospitals (via Google CSE around {loc}):")
286
- for f in er_facilities[:5]:
287
- entry = f"- {f['name']}"
288
  if f["info"]:
289
- entry += f" | Info: {f['info']}"
290
  if f["phone"]:
291
- entry += f" | Phone: {f['phone']}"
292
  if f["hours"]:
293
- entry += f" | Hours (from listing): {f['hours']}"
294
  if f["url"]:
295
- entry += f" | {f['url']}"
296
- lines.append(entry)
297
- lines.append("") # spacer
298
-
299
- if clinic_facilities:
300
- lines.append(f"Nearby Walk-in / Urgent Care / Clinic options (via Google CSE around {loc}):")
301
- for f in clinic_facilities[:8]:
302
- entry = f"- {f['name']}"
303
  if f["info"]:
304
- entry += f" | Info: {f['info']}"
305
  if f["phone"]:
306
- entry += f" | Phone: {f['phone']}"
307
  if f["hours"]:
308
- entry += f" | Hours (from listing): {f['hours']}"
309
  if f["url"]:
310
- entry += f" | {f['url']}"
311
- lines.append(entry)
312
 
313
  if not lines:
314
  return (
315
- f"No strong facility results could be parsed for {loc}. "
316
- "Please use official provincial 'find a clinic/ER' tools or maps."
317
  )
318
 
319
  lines.append(
320
- "Note: Details (distance, exact hours, availability) can change. "
321
- "Users should always confirm on the facility or provincial website."
322
  )
323
  return "\n".join(lines)
324
 
325
 
326
  def tool_search_wait_times_google(postal_code: str) -> str:
327
- """
328
- Supplemental: links about wait-time / access info near user.
329
- """
330
- if not postal_code:
331
- return "No postal code; wait-time links not generated."
332
- if not GOOGLE_API_KEY or not GOOGLE_CSE_ID:
333
- return (
334
- "Google CSE not configured; wait-time links unavailable. "
335
- "Use provincial / hospital websites directly."
336
- )
337
-
338
- city, prov = get_city_from_postal(postal_code)
339
  pc_clean = postal_code.strip().upper().replace(" ", "")
340
- if city:
341
- loc = f"{city}, {prov or 'Canada'}"
342
- else:
343
- loc = f"{pc_clean} Canada"
344
 
345
  data = google_cse_search(
346
  query=f"emergency department wait times {loc}",
347
  num=5
348
  )
349
  if not data or "items" not in data:
350
- return (
351
- f"No specific wait-time dashboards found for {loc}. "
352
- "Check provincial or hospital websites directly."
353
- )
354
 
355
- lines = [f"Wait-time / access info links for {loc} (verify sources):"]
356
  for it in data["items"][:5]:
357
  title = it.get("title", "Result")
358
  link = it.get("link", "")
@@ -366,7 +376,7 @@ def tool_hqontario_context(postal_code: str) -> str:
366
  if province != "Ontario":
367
  return ""
368
  return (
369
- "For Ontario ED locations and typical wait times near your postal code, use Ontario Health's official tool: "
370
  "https://www.hqontario.ca/System-Performance/Time-Spent-in-Emergency-Departments "
371
  "(select 'Area by Postal Code')."
372
  )
@@ -375,27 +385,30 @@ def tool_hqontario_context(postal_code: str) -> str:
375
  def tool_region_context(postal_code: str) -> str:
376
  province = infer_province_from_postal(postal_code)
377
  if not postal_code:
378
- return "No postal code provided. Use Canada-wide guidance and mention provincial resources."
379
  if not province:
380
  return (
381
- f"Postal code '{postal_code}' could not be mapped reliably. "
382
- "Use Canada-wide guidance and suggest checking your provincial health ministry and Telehealth services."
383
  )
384
- telehealth_note = (
385
- f"In {province}, users can contact the provincial Telehealth/811-style nurse line for real-time advice."
386
  )
387
- wait_note = (
388
- "Average wait-time indicators (not real-time) may be available from CIHI and provincial dashboards: "
389
- f"{WAIT_TIMES_INFO_URL}"
390
  )
391
- return f"User postal code '{postal_code}' mapped to {province}.\n{telehealth_note}\n{wait_note}"
 
392
 
 
 
 
393
 
394
  def tool_lookup_drug_products(text: str) -> str:
395
  if not text:
396
  return "No text provided for medication lookup."
397
 
398
- # naive: capitalized tokens as candidate brand names
399
  tokens = []
400
  for tok in text.split():
401
  clean = "".join(ch for ch in tok if ch.isalnum())
@@ -403,7 +416,7 @@ def tool_lookup_drug_products(text: str) -> str:
403
  tokens.append(clean)
404
  candidates = sorted(set(tokens))[:10]
405
  if not candidates:
406
- return "No likely medication names detected for Drug Product Database lookup."
407
 
408
  found = []
409
  for name in candidates[:5]:
@@ -411,11 +424,8 @@ def tool_lookup_drug_products(text: str) -> str:
411
  data = safe_get_json(DPD_BASE_URL, params=params)
412
  if not data:
413
  continue
414
- uniq = set()
415
- for item in data[:3]:
416
- b = item.get("brand_name") or item.get("brandname") or name
417
- if b:
418
- uniq.add(b)
419
  if uniq:
420
  found.append(
421
  f"- Health Canada DPD has entries related to '{name}' "
@@ -444,7 +454,7 @@ def tool_get_recent_recalls_snippet() -> str:
444
 
445
  if not items:
446
  return (
447
- "Unable to load recalls. See the official Government of Canada Recalls and Safety Alerts website."
448
  )
449
 
450
  lines = ["Recent recalls & safety alerts (snapshot):"]
@@ -459,22 +469,22 @@ def tool_get_recent_recalls_snippet() -> str:
459
  category = item.get("category") or item.get("type") or ""
460
  lines.append(f"- {shorten(title)} ({category}, {date})")
461
  lines.append(
462
- "For full details, visit the official Recalls and Safety Alerts portal."
463
  )
464
  return "\n".join(lines)
465
 
466
 
467
  def tool_get_wait_times_awareness() -> str:
468
  return textwrap.dedent(f"""
469
- Use wait-time information conceptually:
470
- - CIHI and some provinces publish average wait-time dashboards (not real-time guarantees).
471
- - For severe symptoms or concerning changes, recommend ER/911 regardless of posted averages.
472
  Reference: {WAIT_TIMES_INFO_URL}
473
  """).strip()
474
 
475
 
476
  # =========================
477
- # OPENAI HELPERS
478
  # =========================
479
 
480
  def call_vision_summarizer(image_bytes: bytes) -> str:
@@ -483,9 +493,8 @@ def call_vision_summarizer(image_bytes: bytes) -> str:
483
  b64 = base64.b64encode(image_bytes).decode("utf-8")
484
  prompt = (
485
  "You are a cautious Canadian health-information assistant.\n"
486
- "Describe ONLY what is visible in neutral terms (e.g., colour, swelling, broken skin).\n"
487
- "Do NOT diagnose or prescribe.\n"
488
- "Keep to about 80-100 words."
489
  )
490
  try:
491
  resp = client.chat.completions.create(
@@ -539,71 +548,65 @@ def call_reasoning_agent(
539
  system_prompt = """
540
  You are CareCall AI, an agentic Canadian health information assistant.
541
 
542
- Inputs provided:
543
- - User narrative and image summary.
544
- - Postal code and inferred region.
545
- - Facilities_context: hospitals / ERs / clinics from Google CSE (already filtered by city/postal).
546
- - Wait_links_context: links to wait-time or access info.
547
- - HQOntario_context when applicable.
548
- - Drug Product Database context.
549
- - Recalls context.
550
- - General wait-time awareness context.
551
 
552
  Hard rules:
553
- - DO NOT give a diagnosis.
554
- - DO NOT prescribe or specify dosing.
555
- - DO NOT invent real-time wait times.
556
- - DO NOT invent facility names: you may ONLY list specific hospitals/clinics that appear inside facilities_context,
557
  hqontario_context, or wait_links_context.
558
- - Keep response under ~350 words.
559
- - Be clear, neutral, and practical.
560
 
561
- Decide EXACTLY ONE primary pathway:
562
  A) "Home care likely reasonable (monitor closely)"
563
  B) "Pharmacist / Walk-in clinic / Family doctor recommended"
564
  C) "Call provincial Telehealth / nurse line for guidance"
565
  D) "Go to Emergency / Call 911 now"
566
 
567
- Formatting rules:
568
 
569
- 1. Start with:
570
-
571
- **Primary Recommended Pathway**
572
- - <one of A/B/C/D> (one short reason)
573
 
574
  2. Conditional sections:
575
 
576
- - If A (Home care):
577
- Then include, in order:
578
- **Home Care**
579
- **When to see a Pharmacist / Clinic / Telehealth**
580
- **When this is an Emergency (ER/911)**
581
- **Local & Official Resources**
582
- **Important Disclaimer**
583
 
584
  - If B or C:
585
- Then include:
586
- **When to see a Pharmacist / Clinic / Telehealth** (this is the primary plan)
587
- **When this is an Emergency (ER/911)**
588
- **Local & Official Resources**
589
- **Important Disclaimer**
590
- Do NOT include a standalone **Home Care** block.
591
 
592
  - If D:
593
- Then include ONLY:
594
- **When this is an Emergency (ER/911)**
595
- **Local & Official Resources**
596
- **Important Disclaimer**
597
- No Home Care / Walk-in sections.
598
-
599
- 3. Local & Official Resources:
600
- - Summarize facilities_context: list some nearby ERs/hospitals and walk-in/urgent care (names, phones, hours/info) as given.
601
- - Include HQOntario_context when present.
602
- - Optionally mention key wait_links_context resources.
603
- - Mention Health Canada / PHAC / Recalls / Drug Product Database generically.
604
- - If facilities_context is weak, say so and advise using official find-a-clinic/ER tools or maps.
605
-
606
- Use only broad, non-diagnostic language (e.g., "possible infection risk", "soft tissue or nail injury").
607
  """
608
 
609
  user_prompt = f"""
@@ -662,33 +665,30 @@ Follow the system instructions exactly.
662
  # =========================
663
 
664
  st.title("CareCall AI (Canada)")
665
- st.caption("Vision + Voice + Postal → Google-based local facilitiesOne clear pathway (not medical diagnosis)")
666
 
667
  st.markdown(
668
  """
669
  This tool:
670
  - Takes an image, voice note, and/or typed description.
671
- - Uses your **Canadian postal code** to:
672
- - infer your city & province (via an internal table),
673
- - look up nearby ERs/hospitals and walk-in clinics using **Google Custom Search** only,
674
- - (optionally) link to official wait-time dashboards (no fake live waits).
675
- - Combines this with Canadian public data references and returns **one** clear pathway:
676
- home care, clinic/Telehealth, or ER — plus relevant safety guidance.
677
-
678
- All outputs are **informational only** and **not a diagnosis**.
679
  """
680
  )
681
 
682
- # Postal code
683
  postal_code = st.text_input(
684
- "Canadian postal code (e.g., M5V 2T6).",
685
  max_chars=7,
686
  )
687
 
688
- # Image
689
  st.markdown("### Image (optional)")
690
  image_file = st.file_uploader(
691
- "Upload a photo (JPG/PNG). Avoid highly identifying content.",
692
  type=["jpg", "jpeg", "png"],
693
  accept_multiple_files=False,
694
  )
@@ -704,7 +704,6 @@ if image_file is not None:
704
  st.warning(f"Could not read that image: {e}")
705
  image_bytes = None
706
 
707
- # Audio
708
  st.markdown("### Voice note (optional)")
709
  audio_file = st.file_uploader(
710
  "Upload a short voice note (wav/mp3/m4a).",
@@ -712,17 +711,15 @@ audio_file = st.file_uploader(
712
  accept_multiple_files=False,
713
  )
714
 
715
- # Text
716
  st.markdown("### Description")
717
  user_text = st.text_area(
718
  "Describe what is happening:",
719
- placeholder='Example: "Big toenail is sore for 3 days, a bit red, no fever, I can walk okay but it hurts in shoes."',
720
  height=120,
 
721
  )
722
 
723
- # Run
724
  if st.button("Run CareCall Agent"):
725
- with st.spinner("Analyzing and building your CareCall summary..."):
726
 
727
  vision_summary = call_vision_summarizer(image_bytes) if image_bytes else ""
728
  voice_text = call_asr(audio_file) if audio_file is not None else ""
@@ -740,14 +737,14 @@ if st.button("Run CareCall Agent"):
740
  wait_awareness_context = tool_get_wait_times_awareness()
741
 
742
  if postal_code.strip():
743
- region_context = tool_region_context(postal_code)
744
  facilities_context = tool_list_facilities_google(postal_code)
 
745
  wait_links_context = tool_search_wait_times_google(postal_code)
746
  hqontario_context = tool_hqontario_context(postal_code)
747
  else:
748
- region_context = "No postal code provided. Use Canada-wide guidance."
749
  facilities_context = "No postal code; cannot list local facilities."
750
- wait_links_context = "No postal code; no local wait-time links."
 
751
  hqontario_context = ""
752
 
753
  final_answer = call_reasoning_agent(
 
26
 
27
  client = OpenAI(api_key=OPENAI_API_KEY)
28
 
29
+ VISION_MODEL = "gpt-4.1-mini"
30
+ REASONING_MODEL = "gpt-4.1-mini"
31
+ ASR_MODEL = "whisper-1"
32
 
33
+ # Health Canada & recalls (for safety snippets)
34
  DPD_BASE_URL = "https://health-products.canada.ca/api/drug/drugproduct"
35
  RECALLS_FEED_URL = os.getenv(
36
  "RECALLS_FEED_URL",
 
38
  )
39
  WAIT_TIMES_INFO_URL = "https://www.cihi.ca/en/topics/access-and-wait-times"
40
 
41
+ # Google Custom Search keys (MUST be set)
42
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
43
  GOOGLE_CSE_ID = os.getenv("GOOGLE_CSE_ID")
44
 
45
+ # Optional: your own wait-time microservice (not required)
46
  WAIT_TIMES_API_BASE = os.getenv("WAIT_TIMES_API_BASE", "").rstrip("/")
47
 
48
+ USER_AGENT = "carecall-ai-google-local/1.2"
49
 
50
  POSTAL_PREFIX_TO_PROVINCE = {
51
  "A": "Newfoundland and Labrador",
 
69
  }
70
 
71
  # =========================
72
+ # HELPERS
73
  # =========================
74
 
75
  def safe_get_json(url, params=None, method="GET", data=None, timeout=8):
 
107
  def extract_phone(text: str) -> str:
108
  if not text:
109
  return ""
 
110
  m = re.search(r"(\(?\d{3}\)?[\s\-\.]?\d{3}[\s\-\.]?\d{4})", text)
111
  return m.group(1) if m else ""
112
 
 
114
  def extract_hours(text: str) -> str:
115
  if not text:
116
  return ""
 
117
  m = re.search(
118
  r"(\d{1,2}\s?(?:AM|PM|am|pm)\s?[–\-]\s?\d{1,2}\s?(?:AM|PM|am|pm))",
119
  text
 
122
 
123
 
124
  # =========================
125
+ # POSTAL -> CITY via canadacities.csv
126
  # =========================
127
 
128
  def load_postal_city_map(path: str = "canadacities.csv"):
 
136
  header = next(reader, None)
137
  if not header:
138
  return mapping
 
139
  lower = [h.strip().lower() for h in header]
140
 
141
+ def find_col(cands):
142
+ for i, name in enumerate(lower):
143
+ for c in cands:
144
+ if c in name:
145
+ return i
146
  return None
147
 
148
  pc_idx = find_col(["postal", "pcode", "fsa"])
 
155
  for row in reader:
156
  if not row or len(row) <= pc_idx:
157
  continue
158
+ raw = row[pc_idx].strip().upper().replace(" ", "")
159
+ if not raw:
160
  continue
161
+ fsa = raw[:3]
162
  city = (
163
  row[city_idx].strip()
164
+ if city_idx is not None and len(row) > city_idx
165
  else ""
166
  )
167
  prov = (
168
  row[prov_idx].strip()
169
+ if prov_idx is not None and len(row) > prov_idx
170
  else ""
171
  )
172
+ if fsa and (city or prov):
173
+ mapping.setdefault(fsa, (city, prov))
174
  except Exception:
175
  return mapping
176
 
 
206
  return safe_get_json(url, params=params)
207
 
208
 
209
+ def facility_local_score(blob: str, city: str, prov: str, fsa: str, postal: str) -> int:
210
+ """Heuristic scoring: prioritize truly local results, drop far-away hospitals."""
211
+ b = blob.lower()
212
+ score = 0
213
+ if postal and postal.replace(" ", "").lower() in b.replace(" ", ""):
214
+ score += 4
215
+ if fsa and fsa.lower() in b:
216
+ score += 3
217
+ if city and city.lower() in b:
218
+ score += 3
219
+ if prov and prov.lower() in b:
220
+ score += 1
221
+ return score
222
+
223
 
224
+ def facilities_from_cse(items, city, prov, fsa, postal, min_score=3, max_results=8):
225
+ facilities = []
226
+ for it in items or []:
227
  title = (it.get("title") or "").strip()
228
  link = (it.get("link") or "").strip()
229
  snippet = (it.get("snippet") or it.get("htmlSnippet") or "").strip()
 
230
  if not title:
231
  continue
232
 
233
+ blob = f"{title} {snippet}"
234
+ score = facility_local_score(blob, city, prov, fsa, postal)
235
+
236
+ # Keep only results that look genuinely local
237
+ if score < min_score:
238
  continue
239
 
240
+ phone = extract_phone(blob)
241
  hours = extract_hours(snippet)
242
  info = shorten(snippet, 200)
243
 
 
246
  "info": info,
247
  "phone": phone,
248
  "hours": hours,
249
+ "url": link,
250
+ "score": score,
251
  })
252
 
253
+ facilities = sorted(facilities, key=lambda x: x["score"], reverse=True)
254
+ return facilities[:max_results]
255
 
256
 
257
  def tool_list_facilities_google(postal_code: str) -> str:
258
  """
259
+ Pure Google-based facility listing:
260
+
261
+ - Detect city/province from canadacities.csv.
262
+ - Query Google CSE using postal + city + province.
263
+ - Build two groups:
264
+ 1) Nearby Emergency / Hospital
265
+ 2) Nearby Walk-in / Urgent Care / Family Practice
266
+ - Only include facilities that pass a locality score threshold to avoid far-away sites.
267
  """
268
  if not postal_code:
269
  return "No postal code provided; cannot list local facilities."
 
270
  if not GOOGLE_API_KEY or not GOOGLE_CSE_ID:
271
  return (
272
  "Google Custom Search is not configured (set GOOGLE_API_KEY and GOOGLE_CSE_ID) "
273
  "so facility listing is unavailable."
274
  )
275
 
276
+ pc_clean = postal_code.strip().upper()
277
+ pc_nospace = pc_clean.replace(" ", "")
278
+ fsa = pc_nospace[:3] if len(pc_nospace) >= 3 else ""
279
+ city, prov = get_city_from_postal(pc_clean)
280
+
281
+ loc_label = f"{city}, {prov}" if city else f"{pc_clean}, Canada"
282
+
283
+ # Emergency/hospital search
284
+ er_query = (
285
+ f"emergency department OR hospital near {pc_clean} {city or ''} {prov or ''} Canada"
286
+ )
287
+ er_data = google_cse_search(er_query, num=10)
288
+ er_fac = facilities_from_cse(
289
+ er_data.get("items") if er_data else [],
290
+ city, prov, fsa, pc_nospace,
291
+ min_score=3,
292
+ max_results=6,
293
+ )
294
+
295
+ # Walk-in / clinic search
296
+ clinic_query = (
297
+ f"walk-in clinic OR urgent care OR family practice near {pc_clean} {city or ''} {prov or ''} Canada"
298
+ )
299
+ clinic_data = google_cse_search(clinic_query, num=10)
300
+ clinic_fac = facilities_from_cse(
301
+ clinic_data.get("items") if clinic_data else [],
302
+ city, prov, fsa, pc_nospace,
303
+ min_score=3,
304
+ max_results=10,
305
+ )
306
 
307
  lines = []
308
 
309
+ if er_fac:
310
+ lines.append(f"Nearby Emergency Departments / Hospitals (Google CSE around {loc_label}):")
311
+ for f in er_fac:
312
+ s = f"- {f['name']}"
313
  if f["info"]:
314
+ s += f" | Info: {f['info']}"
315
  if f["phone"]:
316
+ s += f" | Phone: {f['phone']}"
317
  if f["hours"]:
318
+ s += f" | Hours (from listing): {f['hours']}"
319
  if f["url"]:
320
+ s += f" | {f['url']}"
321
+ lines.append(s)
322
+ lines.append("")
323
+
324
+ if clinic_fac:
325
+ lines.append(f"Nearby Walk-in / Urgent Care / Clinic options (Google CSE around {loc_label}):")
326
+ for f in clinic_fac:
327
+ s = f"- {f['name']}"
328
  if f["info"]:
329
+ s += f" | Info: {f['info']}"
330
  if f["phone"]:
331
+ s += f" | Phone: {f['phone']}"
332
  if f["hours"]:
333
+ s += f" | Hours (from listing): {f['hours']}"
334
  if f["url"]:
335
+ s += f" | {f['url']}"
336
+ lines.append(s)
337
 
338
  if not lines:
339
  return (
340
+ f"No strong local facilities parsed for {loc_label}. "
341
+ "Ask users to use official provincial 'find a clinic/ER' tools or maps with their postal code."
342
  )
343
 
344
  lines.append(
345
+ "Note: This list is based on Google Custom Search results filtered by your postal code/city. "
346
+ "Users must confirm details (distance, hours, availability) on the facility or provincial website."
347
  )
348
  return "\n".join(lines)
349
 
350
 
351
  def tool_search_wait_times_google(postal_code: str) -> str:
352
+ if not postal_code or not GOOGLE_API_KEY or not GOOGLE_CSE_ID:
353
+ return ""
 
 
 
 
 
 
 
 
 
 
354
  pc_clean = postal_code.strip().upper().replace(" ", "")
355
+ city, prov = get_city_from_postal(pc_clean)
356
+ loc = f"{city}, {prov}" if city else f"{pc_clean}, Canada"
 
 
357
 
358
  data = google_cse_search(
359
  query=f"emergency department wait times {loc}",
360
  num=5
361
  )
362
  if not data or "items" not in data:
363
+ return ""
 
 
 
364
 
365
+ lines = [f"Wait-time / access info links near {loc}:"]
366
  for it in data["items"][:5]:
367
  title = it.get("title", "Result")
368
  link = it.get("link", "")
 
376
  if province != "Ontario":
377
  return ""
378
  return (
379
+ "For Ontario ED locations and typical wait times near your postal code, use Ontario Health's tool: "
380
  "https://www.hqontario.ca/System-Performance/Time-Spent-in-Emergency-Departments "
381
  "(select 'Area by Postal Code')."
382
  )
 
385
  def tool_region_context(postal_code: str) -> str:
386
  province = infer_province_from_postal(postal_code)
387
  if not postal_code:
388
+ return "No postal code: provide Canada-wide guidance and suggest local provincial resources."
389
  if not province:
390
  return (
391
+ f"Postal code '{postal_code}' not mapped cleanly. "
392
+ "Suggest user check their provincial health ministry and Telehealth services."
393
  )
394
+ tele = (
395
+ f"In {province}, users can contact the provincial Telehealth/811 nurse line for real-time assessment."
396
  )
397
+ wait = (
398
+ "CIHI and some provinces publish average wait-time data (not real-time): "
399
+ f"{WAIT_TIMES_INFO_URL}."
400
  )
401
+ return f"Postal code '{postal_code}' mapped to {province}.\n{tele}\n{wait}"
402
+
403
 
404
+ # =========================
405
+ # DPD & RECALLS (unchanged, safe)
406
+ # =========================
407
 
408
  def tool_lookup_drug_products(text: str) -> str:
409
  if not text:
410
  return "No text provided for medication lookup."
411
 
 
412
  tokens = []
413
  for tok in text.split():
414
  clean = "".join(ch for ch in tok if ch.isalnum())
 
416
  tokens.append(clean)
417
  candidates = sorted(set(tokens))[:10]
418
  if not candidates:
419
+ return "No likely medication names detected for DPD lookup."
420
 
421
  found = []
422
  for name in candidates[:5]:
 
424
  data = safe_get_json(DPD_BASE_URL, params=params)
425
  if not data:
426
  continue
427
+ uniq = { (it.get("brand_name") or it.get("brandname") or name) for it in data[:3] }
428
+ uniq = {u for u in uniq if u}
 
 
 
429
  if uniq:
430
  found.append(
431
  f"- Health Canada DPD has entries related to '{name}' "
 
454
 
455
  if not items:
456
  return (
457
+ "Unable to load recalls. See the Government of Canada Recalls and Safety Alerts website."
458
  )
459
 
460
  lines = ["Recent recalls & safety alerts (snapshot):"]
 
469
  category = item.get("category") or item.get("type") or ""
470
  lines.append(f"- {shorten(title)} ({category}, {date})")
471
  lines.append(
472
+ "For details, see the official Recalls and Safety Alerts portal."
473
  )
474
  return "\n".join(lines)
475
 
476
 
477
  def tool_get_wait_times_awareness() -> str:
478
  return textwrap.dedent(f"""
479
+ Use wait-time information conceptually only:
480
+ - CIHI/provincial dashboards give average waits (not real-time guarantees).
481
+ - Severe or red-flag symptoms should go to ER/911 regardless.
482
  Reference: {WAIT_TIMES_INFO_URL}
483
  """).strip()
484
 
485
 
486
  # =========================
487
+ # OPENAI CALLS
488
  # =========================
489
 
490
  def call_vision_summarizer(image_bytes: bytes) -> str:
 
493
  b64 = base64.b64encode(image_bytes).decode("utf-8")
494
  prompt = (
495
  "You are a cautious Canadian health-information assistant.\n"
496
+ "Describe only visible features (colour, swelling, wounds, etc.) in neutral terms.\n"
497
+ "Do NOT diagnose or prescribe. Keep to 80-100 words."
 
498
  )
499
  try:
500
  resp = client.chat.completions.create(
 
548
  system_prompt = """
549
  You are CareCall AI, an agentic Canadian health information assistant.
550
 
551
+ Use ONLY the provided context:
552
+ - narrative, vision_summary, postal_code
553
+ - dpd_context, recalls_context
554
+ - wait_awareness_context
555
+ - region_context
556
+ - facilities_context (local hospitals/ERs/clinics from Google CSE)
557
+ - wait_links_context
558
+ - hqontario_context
 
559
 
560
  Hard rules:
561
+ - Do NOT give a diagnosis.
562
+ - Do NOT prescribe or state exact medication doses.
563
+ - Do NOT invent real-time wait times.
564
+ - Do NOT invent facilities: you may ONLY name facilities that appear in facilities_context,
565
  hqontario_context, or wait_links_context.
566
+ - Max ~350 words.
567
+ - Use clear, neutral, practical language.
568
 
569
+ Choose EXACTLY ONE primary pathway:
570
  A) "Home care likely reasonable (monitor closely)"
571
  B) "Pharmacist / Walk-in clinic / Family doctor recommended"
572
  C) "Call provincial Telehealth / nurse line for guidance"
573
  D) "Go to Emergency / Call 911 now"
574
 
575
+ Formatting:
576
 
577
+ 1. **Primary Recommended Pathway**
578
+ - One bullet with A/B/C/D and a one-sentence reason.
 
 
579
 
580
  2. Conditional sections:
581
 
582
+ - If A:
583
+ **Home Care**
584
+ **When to see a Pharmacist / Clinic / Telehealth**
585
+ **When this is an Emergency (ER/911)**
586
+ **Local & Official Resources**
587
+ **Important Disclaimer**
 
588
 
589
  - If B or C:
590
+ **When to see a Pharmacist / Clinic / Telehealth**
591
+ **When this is an Emergency (ER/911)**
592
+ **Local & Official Resources**
593
+ **Important Disclaimer**
594
+ (No separate Home Care block.)
 
595
 
596
  - If D:
597
+ **When this is an Emergency (ER/911)**
598
+ **Local & Official Resources**
599
+ **Important Disclaimer**
600
+ (No Home Care / Walk-in sections.)
601
+
602
+ In **Local & Official Resources**:
603
+ - Summarize facilities_context (names, phones, hours/info) as nearby options.
604
+ - Include HQOntario_context if present.
605
+ - Optionally summarize wait_links_context.
606
+ - Mention Health Canada / Recalls / DPD generically.
607
+ - If facilities_context is weak, say so and direct user to official find-a-clinic/ER tools or maps.
608
+
609
+ Use only broad, non-diagnostic descriptions (e.g., "possible infection risk", "soft tissue irritation").
 
610
  """
611
 
612
  user_prompt = f"""
 
665
  # =========================
666
 
667
  st.title("CareCall AI (Canada)")
668
+ st.caption("Vision + Voice + Postal → Google-local clinics & ERs ONE clear pathway (informational only)")
669
 
670
  st.markdown(
671
  """
672
  This tool:
673
  - Takes an image, voice note, and/or typed description.
674
+ - Uses your **Canadian postal code** and internal city table to:
675
+ - query Google Custom Search for nearby emergency departments and walk-in/urgent-care clinics,
676
+ - filter results so only truly local facilities (matching your postal/FSA/city) are listed.
677
+ - Then combines symptom context + public data to suggest **one** primary pathway:
678
+ home care, clinic/Telehealth, or emergency, with clear safety caveats.
679
+
680
+ All output is informational and does **not** replace a clinician.
 
681
  """
682
  )
683
 
 
684
  postal_code = st.text_input(
685
+ "Canadian postal code (e.g., L1T 2R2).",
686
  max_chars=7,
687
  )
688
 
 
689
  st.markdown("### Image (optional)")
690
  image_file = st.file_uploader(
691
+ "Upload a photo (JPG/PNG).",
692
  type=["jpg", "jpeg", "png"],
693
  accept_multiple_files=False,
694
  )
 
704
  st.warning(f"Could not read that image: {e}")
705
  image_bytes = None
706
 
 
707
  st.markdown("### Voice note (optional)")
708
  audio_file = st.file_uploader(
709
  "Upload a short voice note (wav/mp3/m4a).",
 
711
  accept_multiple_files=False,
712
  )
713
 
 
714
  st.markdown("### Description")
715
  user_text = st.text_area(
716
  "Describe what is happening:",
 
717
  height=120,
718
+ placeholder='Example: "Big toenail is sore for 3 days, mild redness, no fever, pain 3/10 when walking."'
719
  )
720
 
 
721
  if st.button("Run CareCall Agent"):
722
+ with st.spinner("Analyzing your inputs and fetching local facilities..."):
723
 
724
  vision_summary = call_vision_summarizer(image_bytes) if image_bytes else ""
725
  voice_text = call_asr(audio_file) if audio_file is not None else ""
 
737
  wait_awareness_context = tool_get_wait_times_awareness()
738
 
739
  if postal_code.strip():
 
740
  facilities_context = tool_list_facilities_google(postal_code)
741
+ region_context = tool_region_context(postal_code)
742
  wait_links_context = tool_search_wait_times_google(postal_code)
743
  hqontario_context = tool_hqontario_context(postal_code)
744
  else:
 
745
  facilities_context = "No postal code; cannot list local facilities."
746
+ region_context = "No postal code; use Canada-wide guidance."
747
+ wait_links_context = ""
748
  hqontario_context = ""
749
 
750
  final_answer = call_reasoning_agent(