| import gradio as gr |
| from ai_logic.intent_parser import parse_intent |
| from data.data_loader import list_services, load_services_data, HOSPITALS |
| from insurance import plans, cost_estimator |
| from insurance.coverage_explainer import CoverageExplainer |
|
|
| |
| HOSPITAL_CHOICES = list(HOSPITALS.keys()) |
| PLAN_CHOICES = list(plans.SAMPLE_PLANS.keys()) + ["No Insurance"] |
|
|
|
|
| def _extract_hospital_and_plan(text): |
| """Best-effort extraction of hospital and plan names from free text.""" |
| if not text: |
| return None, None |
|
|
| |
| if isinstance(text, list): |
| |
| text_parts = [] |
| for part in text: |
| if isinstance(part, str): |
| text_parts.append(part) |
| elif isinstance(part, dict) and part.get("type") == "text": |
| text_parts.append(part.get("text", "")) |
| text = " ".join(text_parts) |
| |
| if not text: |
| return None, None |
|
|
| lower = text.lower() |
| hospital_name = None |
| plan_name = None |
|
|
| |
| for name in HOSPITAL_CHOICES: |
| if name.lower() in lower: |
| hospital_name = name |
|
|
| |
| for plan_key in plans.SAMPLE_PLANS.keys(): |
| if plan_key.lower() in lower: |
| plan_name = plan_key |
|
|
| |
| if "no insurance" in lower: |
| plan_name = "No Insurance" |
|
|
| return hospital_name, plan_name |
|
|
|
|
| def _get_current_hospital_and_plan(message, history): |
| """Look through the chat history (and latest message) for the most recent |
| hospital & plan mentioned by the user.""" |
| hospital_name = None |
| plan_name = None |
|
|
| |
| for msg in history or []: |
| |
| if msg.get("role") == "user": |
| user_text = msg.get("content", "") |
| h, p = _extract_hospital_and_plan(user_text) |
| if h: |
| hospital_name = h |
| if p: |
| plan_name = p |
|
|
| |
| h, p = _extract_hospital_and_plan(message) |
| if h: |
| hospital_name = h |
| if p: |
| plan_name = p |
|
|
| return hospital_name, plan_name |
|
|
|
|
| def _normalize_message(text): |
| """Convert message content to a plain string (handles Gradio's multimodal format).""" |
| if not text: |
| return "" |
| |
| if isinstance(text, list): |
| text_parts = [] |
| for part in text: |
| if isinstance(part, str): |
| text_parts.append(part) |
| elif isinstance(part, dict) and part.get("type") == "text": |
| text_parts.append(part.get("text", "")) |
| return " ".join(text_parts) |
| |
| return text |
|
|
|
|
| def respond(message, history): |
| """Main response function - now asks for hospital/plan conversationally.""" |
|
|
| |
| message = _normalize_message(message) |
|
|
| |
| hospital_name, plan_name = _get_current_hospital_and_plan(message, history) |
|
|
| |
| if hospital_name is None or plan_name is None: |
| hospitals_list = "\n".join(f"• {name}" for name in HOSPITAL_CHOICES) |
| plans_list = "\n".join(f"• {name}" for name in PLAN_CHOICES) |
|
|
| |
| if hospital_name is None and plan_name is None: |
| return f"""Hi, I'm the THICC Cost Chatbot. 🏥 |
| |
| Before I can estimate your costs, tell me **which hospital** you're using and **what insurance plan** you have. |
| |
| **Hospitals I currently support:** |
| {hospitals_list} |
| |
| **Insurance options I support:** |
| {plans_list} |
| |
| Please reply with something like: |
| • "I'm going to UCLA Medical Center and I have a PPO plan." |
| • "Cedars-Sinai Medical Center with No Insurance." |
| """ |
|
|
| |
| if hospital_name is not None and plan_name is None: |
| return f"""Great, I see you're going to **{hospital_name}**! 🏥 |
| |
| Now, what **insurance plan** do you have? |
| |
| **Insurance options I support:** |
| {plans_list} |
| |
| Please reply with your plan, like "PPO" or "No Insurance". |
| """ |
|
|
| |
| if hospital_name is None and plan_name is not None: |
| return f"""Got it, you have **{plan_name}**! 📋 |
| |
| Now, which **hospital** are you going to? |
| |
| **Hospitals I currently support:** |
| {hospitals_list} |
| |
| Please reply with the hospital name, like "UCLA Medical Center". |
| """ |
|
|
| |
| if CoverageExplainer.identify_coverage_question(message): |
| |
| term = CoverageExplainer.get_matching_term(message) |
|
|
| if term: |
| |
| explanation = CoverageExplainer.explain_term(term) |
|
|
| |
| if plan_name != "No Insurance" and plan_name in plans.SAMPLE_PLANS: |
| plan = plans.SAMPLE_PLANS[plan_name] |
| plan_details = { |
| "deductible": plan.deductible, |
| "copay": plan.copay, |
| "coinsurance": plan.coinsurance, |
| } |
| explanation += "\n\n---\n\n" |
| explanation += CoverageExplainer.format_plan_coverage_summary( |
| plan_name, plan_details |
| ) |
|
|
| return explanation |
| else: |
| |
| return CoverageExplainer.explain_all_terms() |
|
|
| |
| hospital_data_path = HOSPITALS.get(hospital_name) |
| services_data = load_services_data(hospital_data_path) |
| requested_info = parse_intent(message, services_data, hospital_name=hospital_name) |
|
|
| if requested_info is None or requested_info == "list_services": |
| services_list = list_services(services_data) |
| response = ( |
| "**Available services:**\n" |
| + "\n".join(f"• {service}" for service in services_list) |
| ) |
| response += ( |
| "\n\n💡 **Tip**: You can ask me about insurance terms like " |
| "'What is a deductible?' or 'Explain coinsurance'." |
| ) |
| return response |
|
|
| service_data = services_data[ |
| services_data["intent"].str.contains(requested_info, case=False, na=False) |
| ] |
|
|
| if service_data.empty: |
| return ( |
| "Sorry, no information found for your request.\n\nYou can:\n" |
| "• Ask about available services\n" |
| "• Ask about insurance terms (e.g., 'What is a copay?')\n" |
| "• Get cost estimates for specific procedures" |
| ) |
|
|
| service_description = service_data.iloc[0]["description"] |
| price = service_data.iloc[0]["negotiated_rate"] |
|
|
| |
| if plan_name == "No Insurance": |
| plan = plans.NO_INSURANCE_PLAN |
| else: |
| plan = plans.SAMPLE_PLANS.get(plan_name, plans.NO_INSURANCE_PLAN) |
|
|
| cost = cost_estimator.estimate_cost(price, plan, deductible_met=True) |
|
|
| |
|
|
| |
| if plan_name == "No Insurance": |
| response = f"""**Cost Estimate for {service_description}** |
| |
| • Hospital: {hospital_name} |
| • Insurance Plan: {plan_name} |
| • Estimated Cost: **${cost:.2f}** |
| |
| Because you selected **No Insurance**, this demo assumes you pay the full negotiated rate. |
| |
| 💡 **Understanding your cost**: |
| • Negotiated rate: ${price:.2f} |
| • Your insurance covers: $0.00 |
| • You pay: ${cost:.2f} |
| |
| If you want to see how deductibles, copays, and coinsurance work, try asking: |
| • "What is a deductible?" |
| • "Explain coinsurance" |
| """ |
| return response |
|
|
| |
| response = f"""**Cost Estimate for {service_description}** |
| |
| • Hospital: {hospital_name} |
| • Insurance Plan: {plan_name} |
| • Estimated Cost: **${cost:.2f}** |
| |
| This estimate assumes your deductible has been met. |
| |
| 💡 **Understanding your cost**: |
| • Negotiated rate: ${price:.2f} |
| • Your insurance covers: ${price - cost:.2f} |
| • You pay: ${cost:.2f}""" |
|
|
| |
| if getattr(plan, "copay", None) and cost == plan.copay: |
| response += ( |
| f"\n\n*You're paying a fixed copay of ${plan.copay:.2f} for this service.*" |
| ) |
| elif getattr(plan, "coinsurance", None) and plan.coinsurance > 0: |
| response += ( |
| f"\n\n*You're paying {plan.coinsurance*100:.0f}% coinsurance " |
| f"({plan.coinsurance*100:.0f}% of ${price:.2f}).*" |
| ) |
|
|
| response += ( |
| "\n\n**Need help?** Ask me 'What is coinsurance?' " |
| "or any other insurance term!" |
| ) |
|
|
| return response |
|
|
|
|
| |
| demo = gr.ChatInterface( |
| fn=respond, |
| title="THICC Cost Chatbot 🏥", |
| description="""Get healthcare cost estimates and understand your insurance coverage. |
| |
| **What you can ask:** |
| • Cost estimates: "How much does an MRI cost?" |
| • Coverage terms: "What is a deductible?" or "Explain coinsurance" |
| • Available services: "What services are available?" |
| """, |
| chatbot=gr.Chatbot(height="70vh"), |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch(ssr_mode=False) |