File size: 9,671 Bytes
fee6d7b
 
 
 
 
 
d897c25
 
 
fee6d7b
 
b07566a
d897c25
 
 
 
b07566a
 
 
 
 
 
 
 
 
 
 
 
 
 
d897c25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36b8053
 
d897c25
 
 
36b8053
 
 
 
 
 
 
 
 
 
d897c25
 
 
 
 
 
 
fee6d7b
d897c25
 
 
b07566a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36b8053
 
d897c25
b07566a
 
 
d897c25
 
 
 
 
 
 
 
fa39502
 
 
d897c25
 
 
 
 
 
 
 
 
 
 
 
fa39502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d897c25
fee6d7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d897c25
fee6d7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d897c25
fee6d7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c360f8
fee6d7b
 
 
6c360f8
fee6d7b
 
 
 
 
 
 
 
 
 
 
 
 
36b8053
fee6d7b
 
cca86bb
fee6d7b
 
 
 
 
 
 
d0b45a2
fee6d7b
 
 
36b8053
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
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

# Valid options for hospitals and plans (used in the conversational flow)
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

    # Handle case where text might be a list (multimodal Gradio format)
    if isinstance(text, list):
        # Extract text content from list of content parts
        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

    # Match hospitals by substring
    for name in HOSPITAL_CHOICES:
        if name.lower() in lower:
            hospital_name = name

    # Match plans by key
    for plan_key in plans.SAMPLE_PLANS.keys():
        if plan_key.lower() in lower:
            plan_name = plan_key

    # Allow "no insurance" as a phrase
    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

    # History is a list of {"role": ..., "content": ...} dicts (new Gradio format)
    for msg in history or []:
        # Only look at user messages for selections
        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

    # Also extract from the current message
    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."""

    # Normalize message in case it's a list (multimodal Gradio format)
    message = _normalize_message(message)

    # Determine the user's current hospital & plan from history/message
    hospital_name, plan_name = _get_current_hospital_and_plan(message, history)

    # If either is missing, show the options and ask the user to choose
    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)

        # Case 1: Both missing - show full welcome message
        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."
"""

        # Case 2: Have hospital, need insurance plan
        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".
"""

        # Case 3: Have insurance plan, need hospital
        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".
"""

    # --- 1) Coverage questions first ---
    if CoverageExplainer.identify_coverage_question(message):
        # Figure out which term (deductible, copay, coinsurance, etc.)
        term = CoverageExplainer.get_matching_term(message)

        if term:
            # Explain the specific term (NOT the whole message)
            explanation = CoverageExplainer.explain_term(term)

            # Add plan-specific context if a sample plan is selected
            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:
            # If we can't match a specific term, give the full coverage explainer
            return CoverageExplainer.explain_all_terms()

    # --- 2) Service cost estimation path ---
    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"]

    # Map plan name -> InsurancePlan object
    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)

    # --- 3) Format response with cost breakdown ---

    # Special handling for No Insurance so messaging isn't confusing
    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

    # For actual plans
    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}"""

    # Explain payment type
    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


# Gradio interface
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)