import os import io import csv import re import base64 import textwrap import requests import streamlit as st from PIL import Image from audio_recorder_streamlit import audio_recorder # ========================= # BASIC CONFIG # ========================= st.set_page_config( page_title="CareCall AI (Canada)", page_icon="🩺", layout="centered" ) # --- Keys / endpoints --- OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY") if not OPENROUTER_API_KEY: st.warning("Set OPENROUTER_API_KEY in your Space secrets (OpenRouter) to enable AI features.") st.stop() HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HF_API_TOKEN") # for Whisper via HF router OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" # IMPORTANT: set these to valid OpenRouter model slugs from your OpenRouter dashboard. VISION_MODEL = os.getenv("VISION_MODEL", "nvidia/nemotron-nano-12b-v2-vl:free") # example; update to actual slug if needed REASONING_MODEL = os.getenv("REASONING_MODEL", "nvidia/nemotron-nano-12b-v2-vl:free") # or another instruct model # Whisper via HF Inference router (remote, no local load) HF_WHISPER_URL = os.getenv( "HF_WHISPER_URL", "https://router.huggingface.co/hf-inference/models/openai/whisper-large-v3", ) DPD_BASE_URL = "https://health-products.canada.ca/api/drug/drugproduct" RECALLS_FEED_URL = os.getenv( "RECALLS_FEED_URL", "https://recalls-rappels.canada.ca/static-data/items-en.json" ) WAIT_TIMES_INFO_URL = "https://www.cihi.ca/en/topics/access-and-wait-times" GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY") or os.getenv("GOOGLE_API_KEY") USER_AGENT = "carecall-ai-places-city/1.0" PROVINCES = { "AB": "Alberta", "BC": "British Columbia", "MB": "Manitoba", "NB": "New Brunswick", "NL": "Newfoundland and Labrador", "NS": "Nova Scotia", "NT": "Northwest Territories", "NU": "Nunavut", "ON": "Ontario", "PE": "Prince Edward Island", "QC": "Quebec", "SK": "Saskatchewan", "YT": "Yukon", } # ========================= # GLOBAL STYLE (MOBILE FEEL) # ========================= st.markdown( """ """, unsafe_allow_html=True, ) # ========================= # GENERIC HELPERS # ========================= def safe_get_json(url, params=None, method="GET", data=None, timeout=8): try: headers = {"User-Agent": USER_AGENT} if method == "GET": r = requests.get(url, params=params, headers=headers, timeout=timeout) else: r = requests.post(url, data=data, headers=headers, timeout=timeout) if r.ok: return r.json() except Exception: return None return None def shorten(text, max_chars=260): if not text: return "" t = text.strip() if len(t) <= max_chars: return t return t[: max_chars - 3].rstrip() + "..." def extract_phone(text: str) -> str: if not text: return "" m = re.search(r"(\(?\d{3}\)?[\s\-\.]?\d{3}[\s\-\.]?\d{4})", text) return m.group(1) if m else "" def format_opening_hours(opening_hours: dict) -> str: if not opening_hours: return "" pieces = [] if opening_hours.get("open_now") is True: pieces.append("Open now") elif opening_hours.get("open_now") is False: pieces.append("Currently closed") weekday = opening_hours.get("weekday_text") or [] if weekday: pieces.append("; ".join(weekday[:2])) return " | ".join(pieces) # ========================= # CITY LIST # ========================= def load_city_options(path: str = "canadacities.csv"): options = set() if not os.path.exists(path): return [] try: with open(path, "r", encoding="utf-8") as f: reader = csv.reader(f) header = next(reader, None) if not header: return [] lower = [h.strip().lower() for h in header] def find_col(cands): for i, name in enumerate(lower): for c in cands: if c in name: return i return None city_idx = find_col(["city", "place"]) prov_idx = find_col(["province", "prov", "state"]) if city_idx is None or prov_idx is None: return [] for row in reader: if not row: continue if len(row) <= max(city_idx, prov_idx): continue city = row[city_idx].strip() prov = row[prov_idx].strip() if not city or not prov: continue prov_short = prov.strip() if len(prov_short) > 3: for code, full in PROVINCES.items(): if prov_short.lower().startswith(full.lower()): prov_short = code break label = f"{city}, {prov_short}" options.add(label) except Exception: return [] return sorted(options) CITY_OPTIONS = load_city_options() if not CITY_OPTIONS: st.warning( "Could not load cities from canadacities.csv. " "Autocomplete list will be empty until that file is available." ) def split_city_label(label: str): if not label: return None, None parts = [p.strip() for p in label.split(",")] if len(parts) == 2: return parts[0], parts[1] return label.strip(), None # ========================= # GOOGLE PLACES HELPERS # ========================= def places_text_search(query: str): if not GOOGLE_MAPS_API_KEY: return None url = "https://maps.googleapis.com/maps/api/place/textsearch/json" params = { "key": GOOGLE_MAPS_API_KEY, "query": query, } return safe_get_json(url, params=params) def place_details(place_id: str): if not GOOGLE_MAPS_API_KEY or not place_id: return None url = "https://maps.googleapis.com/maps/api/place/details/json" params = { "key": GOOGLE_MAPS_API_KEY, "place_id": place_id, "fields": "formatted_phone_number,international_phone_number,opening_hours,website,url", } return safe_get_json(url, params=params) def search_places_in_city(city_label: str, main_query: str, max_results: int = 5): if not GOOGLE_MAPS_API_KEY: return [] q = f"{main_query} in {city_label}, Canada" data = places_text_search(q) if not data or "results" not in data: return [] facilities = [] for r in data.get("results", [])[:max_results]: name = r.get("name") address = r.get("formatted_address") place_id = r.get("place_id") if not name: continue phone = "" hours_str = "" maps_url = "" if place_id: det = place_details(place_id) if det and det.get("result"): res = det["result"] phone = ( res.get("formatted_phone_number") or res.get("international_phone_number") or "" ) hours_str = format_opening_hours(res.get("opening_hours") or {}) maps_url = res.get("url") or res.get("website") or "" if not maps_url: maps_url = ( "https://www.google.com/maps/search/?api=1&query=" + requests.utils.quote(name + " " + (address or city_label)) ) facilities.append( { "name": name, "address": address or city_label, "phone": phone, "hours": hours_str, "url": maps_url, } ) return facilities def tool_list_facilities_places(city_label: str) -> str: if not city_label: return "No city selected; cannot list local facilities." if not GOOGLE_MAPS_API_KEY: return ( "Google Maps / Places API key is not configured. " "Set GOOGLE_MAPS_API_KEY (or GOOGLE_API_KEY) to enable local facility listings." ) er_list = search_places_in_city(city_label, "emergency department", max_results=5) clinic_list = search_places_in_city( city_label, "walk-in clinic OR urgent care OR family practice clinic", max_results=8, ) lines = [] if er_list: lines.append(f"Emergency Departments / Hospitals near {city_label}:") for f in er_list: s = f"- {f['name']} β€” {f['address']}" if f["phone"]: s += f" β€” Phone: {f['phone']}" if f["hours"]: s += f" β€” Hours: {f['hours']}" if f["url"]: s += f" β€” {f['url']}" lines.append(s) lines.append("") if clinic_list: lines.append(f"Walk-in / Urgent Care / Family Clinics near {city_label}:") for f in clinic_list: s = f"- {f['name']} β€” {f['address']}" if f["phone"]: s += f" β€” Phone: {f['phone']}" if f["hours"]: s += f" β€” Hours: {f['hours']}" if f["url"]: s += f" β€” {f['url']}" lines.append(s) if not lines: return ( f"No facilities found via Places API for {city_label}. " "Advise user to open Google Maps and search 'emergency department' or 'walk-in clinic' in their city." ) lines.append( "Note: Facility details (distance, hours, availability) can change. " "Users must confirm on the facility or provincial website / Google Maps." ) return "\n".join(lines) def tool_region_context_city(city_label: str) -> str: if not city_label: return "No city selected; provide Canada-wide guidance." city, prov = split_city_label(city_label) if not city: return "City not recognized; use general Canadian guidance." prov_name = None if prov and prov in PROVINCES: prov_name = PROVINCES[prov] elif prov: prov_name = prov if prov_name: tele = f"In {prov_name}, users can contact the provincial Telehealth/811 nurse line (where available) for real-time guidance." else: tele = "Users can contact their provincial/territorial Telehealth or nurse advice line where available." wait = ( f"Average system-level wait-time indicators (not real-time) may be available via CIHI or {prov_name or 'provincial'} dashboards: " f"{WAIT_TIMES_INFO_URL}." ) return f"User location: {city_label}.\n{tele}\n{wait}" def tool_hqontario_context_city(city_label: str) -> str: _, prov = split_city_label(city_label) if prov == "ON": return ( "For Ontario ED locations and typical wait times by region, use Ontario Health's tool: " "https://www.hqontario.ca/System-Performance/Time-Spent-in-Emergency-Departments." ) return "" # ========================= # DPD & RECALLS # ========================= def tool_lookup_drug_products(text: str) -> str: if not text: return "No text provided for medication lookup." tokens = [] for tok in text.split(): clean = "".join(ch for ch in tok if ch.isalnum()) if len(clean) > 3 and clean[0].isupper(): tokens.append(clean) candidates = sorted(set(tokens))[:10] if not candidates: return "No likely medication names detected for Drug Product Database lookup." found = [] for name in candidates[:5]: params = {"lang": "en", "type": "json", "brandname": name} data = safe_get_json(DPD_BASE_URL, params=params) if not data: continue uniq = { (it.get("brand_name") or it.get("brandname") or name) for it in data[:3] } uniq = {u for u in uniq if u} if uniq: found.append( f"- Health Canada DPD has entries related to '{name}' " f"(e.g., {', '.join(sorted(uniq))})." ) if not found: return ( "No strong matches found in the Drug Product Database for detected terms. " "Users should confirm medications directly in the official DPD." ) found.append( "For definitive medication information, consult the official Health Canada Drug Product Database." ) return "\n".join(found) def tool_get_recent_recalls_snippet() -> str: data = safe_get_json(RECALLS_FEED_URL) items = [] if isinstance(data, list): items = data[:5] elif isinstance(data, dict): items = (data.get("items") or data.get("results") or [])[:5] if not items: return ( "Unable to load recalls. See the Government of Canada Recalls and Safety Alerts website." ) lines = ["Recent recalls & safety alerts (snapshot):"] for item in items: title = ( item.get("title") or item.get("english_title") or item.get("name") or "Recall / Alert" ) date = item.get("date_published") or item.get("date") or "" category = item.get("category") or item.get("type") or "" lines.append(f"- {shorten(title)} ({category}, {date})") lines.append("For details, see the official Recalls and Safety Alerts portal.") return "\n".join(lines) def tool_get_wait_times_awareness() -> str: return textwrap.dedent(f""" Use wait-time info conceptually: - CIHI/provincial dashboards show average waits (not real-time guarantees). - Severe or red-flag symptoms should go to ER/911 regardless. Reference: {WAIT_TIMES_INFO_URL} """).strip() # ========================= # OPENROUTER HELPER # ========================= def call_openrouter_chat(model: str, messages, temperature: float = 0.3): headers = { "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json", } app_url = os.getenv("APP_URL", "").strip() if app_url: headers["HTTP-Referer"] = app_url headers["X-Title"] = "CareCall AI (Canada)" payload = { "model": model, "messages": messages, "temperature": float(temperature), } try: r = requests.post(OPENROUTER_URL, headers=headers, json=payload, timeout=90) if r.status_code != 200: text_snippet = r.text[:300].replace("\n", " ") return f"(Model call error: {r.status_code} β€” {text_snippet})" data = r.json() choices = data.get("choices") if not choices: return "(Model call error: empty response.)" msg = choices[0].get("message", {}) content = msg.get("content", "") return content.strip() if isinstance(content, str) else str(content) except Exception as e: return f"(Model call unavailable: {e})" # ========================= # VISION SUMMARIZER (Nemotron via OpenRouter) # ========================= def call_vision_summarizer(image_bytes: bytes) -> str: if not image_bytes: return "" b64 = base64.b64encode(image_bytes).decode("utf-8") prompt = """ You are a cautious Canadian health-information assistant. Your job is to LOOK CLOSELY at the image and produce a detailed, objective visual assessment ONLY. Do not guess internal causes. Do not diagnose. Do not recommend specific drugs. Describe clearly and systematically: 1. Skin/area colour and any colour changes. 2. Swelling or asymmetry (none / mild / moderate / severe). 3. Visible borders or edges (sharp, blurred, irregular). 4. Surface changes (dry, cracked, shiny, blistered, open wound, scab, rash pattern, etc.). 5. Size/extent in plain-language terms (e.g., "covers a small patch", "covers most of the visible toe/hand/area"). 6. Any visible discharge, bleeding, or crusting. 7. Any objects/devices/dressings in view (e.g., ring, bandage, splint). 8. Any visually concerning features that might suggest higher risk (e.g., blackened tissue, spreading redness, deep open wound, thick pus) β€” describe ONLY what is seen. Keep it about 120–180 words. Write as if supporting a separate triage system, not directly reassuring or diagnosing the patient. """ messages = [ { "role": "user", "content": [ {"type": "text", "text": prompt}, { "type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}, }, ], } ] return call_openrouter_chat(VISION_MODEL, messages, temperature=0.2) # ========================= # ASR VIA HF ROUTER (Whisper Large v3) # ========================= def call_asr(audio_source) -> str: """ Use Hugging Face router + openai/whisper-large-v3 for transcription. - No local model load. - Returns transcript or "". - Shows friendly warnings, never raw HTTP/JSON in the text area. """ if not audio_source: return "" if not HF_TOKEN: st.warning("Voice transcription is not configured. Please type your description.") return "" # Normalize to bytes from audio_recorder_streamlit if isinstance(audio_source, bytes): audio_bytes = audio_source else: audio_bytes = audio_source.read() headers = { "Authorization": f"Bearer {HF_TOKEN}", # audio_recorder_streamlit sends wav; this is fine for Whisper "Content-Type": "audio/wav", } try: resp = requests.post( HF_WHISPER_URL, headers=headers, data=audio_bytes, timeout=120, ) except Exception as e: print(f"Whisper HF router request error: {e}") st.warning("Voice transcription is temporarily unavailable. Please type your description.") return "" if resp.status_code != 200: # Log server-side, keep UI clean print(f"Whisper HF router HTTP {resp.status_code}: {resp.text[:300]}") st.warning("We couldn't transcribe that recording. Please type or edit your description.") return "" try: data = resp.json() except Exception as e: print(f"Whisper HF router JSON error: {e}, body: {resp.text[:300]}") st.warning("We couldn't transcribe that recording. Please type or edit your description.") return "" # HF router responses for Whisper typically include "text" or similar. text_val = "" if isinstance(data, dict): # Common schema: {"text": "..."} or {"generated_text": "..."} or error if "error" in data: print(f"Whisper HF router error field: {data['error']}") text_val = data.get("text") or data.get("generated_text") or "" elif isinstance(data, list) and data: # Sometimes list of segments or outputs first = data[0] if isinstance(first, dict): text_val = first.get("text") or first.get("generated_text") or "" elif isinstance(first, str): text_val = first text_val = (text_val or "").strip() if not text_val: st.warning("We couldn't clearly understand that recording. Please check or type your description.") return text_val # ========================= # REASONING AGENT # ========================= def call_reasoning_agent( narrative: str, vision_summary: str = "", city_label: str = "", dpd_context: str = "", recalls_context: str = "", wait_awareness_context: str = "", region_context: str = "", facilities_context: str = "", hqontario_context: str = "", ) -> str: system_prompt = """ You are CareCall AI, an agentic Canadian health information assistant. You MUST base your answer only on the structured context provided: - User narrative and vision_summary - City_label or region info - dpd_context (Health Canada Drug Product Database lookups) - recalls_context (recent recalls & safety alerts) - wait_awareness_context (general notes on wait-time data) - region_context (province / Telehealth context) - facilities_context (local facilities from trusted APIs) - hqontario_context (Ontario-specific ED tool when present) You are an INFORMATIONAL tool, not a clinician. HARD SAFETY RULES ----------------- 1. Do NOT give a medical diagnosis. 2. Do NOT prescribe or specify exact medication doses. 3. Do NOT claim to provide real-time wait times. 4. Do NOT contradict emergency advice from standard red-flag criteria. 5. DO NOT invent or guess facility names. - You may ONLY list specific hospitals/clinics/ERs that appear LITERALLY in: - facilities_context, or - hqontario_context. - If facilities_context says no specific local facilities were found, you MUST NOT make up examples. Instead, direct users to Google Maps or official provincial "find a clinic/ER" tools. 6. Keep the whole answer under about 350 words. 7. Use clear, neutral, plain language that a non-clinician can understand. IMAGE USE (VERY IMPORTANT) -------------------------- - If a vision_summary is provided, you MUST treat it as a primary source of information. - Cross-check the user narrative against the vision_summary. - If the image description suggests more serious concern (e.g., black tissue, deep open wound, marked swelling, spreading redness, thick pus, obvious deformity), escalate the triage tier even if the user's text sounds mild. - If there is a conflict between what is described and what is seen, favour caution and explain that visually it appears safer to seek a higher level of assessment. - You still MUST NOT assign a diagnosis or name specific diseases based on the image. TRIAGE LOGIC (INTERNAL) ----------------------- You must internally choose EXACTLY ONE primary pathway, but DO NOT show letters or labels to the user. (1) "No current action needed (you appear well, continue normal care)": - Use when the user clearly reports feeling well or "perfect", with no symptoms or concerns. - You may gently remind them of general wellness habits. - Phrase as "based on what you've shared, you appear well"; this is NOT a diagnosis. (2) "Home care reasonable (self-care + monitor)": - Use when symptoms are: - mild, - short-lived, - non-progressive, - and NOT limiting normal activities. - Provide clear, safe self-care suggestions and "watch for these changes" rules. (3) "Pharmacist / Walk-in clinic / Family doctor or Telehealth recommended": - Use when: - symptoms are persistent or recurrent, - moderate in intensity, - affecting function, - or there is diagnostic uncertainty but NOT clear emergency. - Explain why an in-person/phone assessment is appropriate. (4) "Go to Emergency / Call 911 now": - Use when there are red-flag features: - severe or rapidly worsening pain, - major trauma, - uncontrolled bleeding, - chest pain, trouble breathing, - stroke signs, - signs of sepsis or systemic illness, - significant numbness/weakness, - concerning changes after surgery or known serious conditions, - or visually very concerning findings on the image. - Be direct and clear. OUTPUT FORMAT (WHAT USER SEES) ------------------------------ 1. **Primary Recommended Pathway** - One brief sentence stating the recommendation WITHOUT internal labels. 2. Conditional sections depending on your chosen pathway: If you chose "No current action needed": - **Home Care / Wellness** - **When to seek medical advice** - **When this is an Emergency (ER/911)** - **Local & Official Resources** - **Important Disclaimer** If you chose "Home care reasonable (self-care + monitor)": - **Home Care** - **When to see a Pharmacist / Clinic / Telehealth** - **When this is an Emergency (ER/911)** - **Local & Official Resources** - **Important Disclaimer** If you chose "Pharmacist / Walk-in clinic / Family doctor or Telehealth recommended": - **When to see a Pharmacist / Clinic / Telehealth** - **When this is an Emergency (ER/911)** - **Local & Official Resources** - **Important Disclaimer** If you chose "Go to Emergency / Call 911 now": - **When this is an Emergency (ER/911)** - **Local & Official Resources** - **Important Disclaimer** - Do NOT include home-care reassurance. LOCAL & OFFICIAL RESOURCES -------------------------- - If facilities_context lists facilities: - Summarize them as bullet points: - name, - city or address, - phone (if present), - hours/β€œopen now” (if present), - link/URL. - Do NOT alter names or add new ones. - If facilities_context indicates that no specific facilities were found: - Say that directly. - Instruct users to: - use Google Maps with their city, - or use official provincial "find a clinic/ER" / Telehealth tools. - Do NOT fabricate facility names. - Include hqontario_context when relevant (for Ontario users). - Optionally remind users: - Health Canada Drug Product Database and Recalls site for medication/product safety. - CIHI/provincial dashboards for general (non-real-time) wait-time information. TONE ---- - Calm, supportive, non-alarming. - No legalese walls of text; keep it tight and readable. - Always remind: this is informational, not a diagnosis; if worried, they should seek real care. """ user_message = f""" User narrative: {narrative or "(none provided)"} Vision summary: {vision_summary or "(none)"} City/region: {city_label or "(not provided)"} DPD context: {dpd_context or "(none)"} Recalls context: {recalls_context or "(none)"} Wait awareness: {wait_awareness_context or "(none)"} Region info: {region_context or "(none)"} Facilities info: {facilities_context or "(none)"} HQ Ontario info: {hqontario_context or "(not applicable)"} """ messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ] return call_openrouter_chat(REASONING_MODEL, messages, temperature=0.3) # ========================= # STATE & NAV # ========================= if "step" not in st.session_state: st.session_state.step = 1 if "city_label" not in st.session_state: st.session_state.city_label = "" if "image_bytes" not in st.session_state: st.session_state.image_bytes = None if "audio_bytes" not in st.session_state: st.session_state.audio_bytes = None if "last_audio" not in st.session_state: st.session_state.last_audio = None if "user_text" not in st.session_state: st.session_state.user_text = "" if "final_answer" not in st.session_state: st.session_state.final_answer = "" def go_to_step(n: int): st.session_state.step = n def render_steps(): dots = [] for i in [1, 2, 3]: cls = "step-dot active" if st.session_state.step == i else "step-dot" dots.append(f'
') st.markdown( f'
{"".join(dots)}
', unsafe_allow_html=True, ) # ========================= # HEADER # ========================= st.markdown( """
CareCall AI (Canada)
Simple 3-step check-in for non-emergency situations. Widely accepted Canadian health guidelines, no CTAS/eCTAS. Not a diagnosis. Not for 911 emergencies. Zero Retention - No info saved.
""", unsafe_allow_html=True, ) render_steps() # ========================= # STEP 1 # ========================= if st.session_state.step == 1: st.markdown('
', unsafe_allow_html=True) st.markdown( '
Step 1 Β· Where & what are we looking at?
', unsafe_allow_html=True, ) st.markdown( '
Select your city and (optionally) take a clear photo of the area you are worried about.
', unsafe_allow_html=True, ) city = st.selectbox( "Your city / town", options=[""] + CITY_OPTIONS, index=0, help="Used only to suggest nearby care options.", ) st.session_state.city_label = city photo = st.camera_input( "Tap to open camera (optional)", label_visibility="visible", ) if photo is not None: try: img = Image.open(photo).convert("RGB") buf = io.BytesIO() img.save(buf, format="JPEG") st.session_state.image_bytes = buf.getvalue() st.success("Photo captured.") except Exception as e: st.warning(f"Could not read that image: {e}") st.session_state.image_bytes = None col1, col2 = st.columns(2) with col1: if st.button("Next", use_container_width=True): go_to_step(2) st.rerun() with col2: if st.button("Skip photo", use_container_width=True): st.session_state.image_bytes = None go_to_step(2) st.rerun() st.markdown("
", unsafe_allow_html=True) # ========================= # STEP 2 # ========================= elif st.session_state.step == 2: st.markdown('
', unsafe_allow_html=True) st.markdown( '
Step 2 Β· Tell us what is happening
', unsafe_allow_html=True, ) st.markdown( '
Use the mic to describe your concern, or type instead. ' 'We auto-fill the text box with your recording so you can review and edit.
', unsafe_allow_html=True, ) st.markdown('
Speak (optional)
', unsafe_allow_html=True) # Mic recorder audio_bytes = audio_recorder( text="Tap to record", recording_color="#ef4444", neutral_color="#e5e7eb", icon_name="microphone", icon_size="1.3x", ) # If new audio, send to Whisper + fill textarea if audio_bytes: st.session_state.audio_bytes = audio_bytes if st.session_state.last_audio != audio_bytes: st.success("Voice note captured. Transcribing...") transcript = call_asr(audio_bytes) if transcript: st.session_state.user_text = transcript st.session_state.last_audio = audio_bytes # Text area (shows transcript or manual input) user_text = st.text_area( "Or type / edit your description here", value=st.session_state.user_text, height=120, placeholder='Example: "Painful big toe for 3 days, mild redness, no fever, can walk but hurts in shoes."', ) st.session_state.user_text = user_text st.markdown( '
When you are ready, tap below to get your one recommended pathway.
', unsafe_allow_html=True, ) # Buttons: defined once col1, col2 = st.columns(2) with col1: back_clicked = st.button( "Back", use_container_width=True, key="step2_back", ) with col2: go_clicked = st.button( "Get my recommendation", use_container_width=True, key="step2_go", ) # Handle Back if back_clicked: go_to_step(1) st.rerun() # Handle Get my recommendation if go_clicked: spinner_placeholder = st.empty() with spinner_placeholder, st.spinner("Analyzing.Might take a minute..."): image_bytes = st.session_state.image_bytes city_label = (st.session_state.city_label or "").strip() vision_summary = call_vision_summarizer(image_bytes) if image_bytes else "" narrative_text = st.session_state.user_text.strip() combined_for_drugs = " ".join( x for x in [narrative_text, vision_summary] if x ) dpd_context = ( tool_lookup_drug_products(combined_for_drugs) if combined_for_drugs else "No medication context." ) recalls_context = tool_get_recent_recalls_snippet() wait_awareness_context = tool_get_wait_times_awareness() if city_label: facilities_context = tool_list_facilities_places(city_label) region_context = tool_region_context_city(city_label) hqontario_context = tool_hqontario_context_city(city_label) else: facilities_context = "No city selected; cannot list local facilities." region_context = "No city selected; use Canada-wide guidance." hqontario_context = "" final_answer = call_reasoning_agent( narrative=narrative_text, vision_summary=vision_summary, city_label=city_label, dpd_context=dpd_context, recalls_context=recalls_context, wait_awareness_context=wait_awareness_context, region_context=region_context, facilities_context=facilities_context, hqontario_context=hqontario_context, ) st.session_state.final_answer = final_answer go_to_step(3) st.rerun() st.markdown("
", unsafe_allow_html=True) # ========================= # STEP 3 # ========================= elif st.session_state.step == 3: st.markdown('
', unsafe_allow_html=True) st.markdown( '
Step 3 Β· Your guidance (informational only)
', unsafe_allow_html=True, ) if not st.session_state.final_answer: st.info("No summary available yet. Please go back and complete steps 1 and 2.") else: st.markdown(st.session_state.final_answer) st.markdown( '
CareCall AI does not replace a clinician or emergency services. ' 'If symptoms are severe, worsening, or worrying, seek in-person care or call emergency services.
', unsafe_allow_html=True, ) col1, col2 = st.columns(2) with col1: if st.button("Back", use_container_width=True): go_to_step(2) st.rerun() with col2: if st.button("Start over", use_container_width=True): st.session_state.image_bytes = None st.session_state.audio_bytes = None st.session_state.last_audio = None st.session_state.user_text = "" st.session_state.final_answer = "" go_to_step(1) st.rerun() st.markdown("
", unsafe_allow_html=True)