Spaces:
Sleeping
Sleeping
Alex Amari commited on
Commit ·
d37d00d
1
Parent(s): 9e8e8bd
Tier 2: input validation, rate limiting, and bug fixes
Browse files
app.py
CHANGED
|
@@ -5,6 +5,7 @@ Deployed on Hugging Face Spaces
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import os
|
|
|
|
| 8 |
import gradio as gr
|
| 9 |
from openai import OpenAI
|
| 10 |
from geopy.geocoders import Nominatim
|
|
@@ -129,6 +130,7 @@ TRANSLATIONS = {
|
|
| 129 |
"income_label": "Annual Household Income ($)",
|
| 130 |
"household_label": "Number of People in Household",
|
| 131 |
"snap_label": "Enrolled in SNAP or WIC?",
|
|
|
|
| 132 |
"yes": "Yes",
|
| 133 |
"no": "No",
|
| 134 |
"check_button": "Check Eligibility",
|
|
@@ -156,6 +158,7 @@ TRANSLATIONS = {
|
|
| 156 |
"income_label": "Ingreso Anual del Hogar ($)",
|
| 157 |
"household_label": "Número de Personas en el Hogar",
|
| 158 |
"snap_label": "¿Inscrito en SNAP o WIC?",
|
|
|
|
| 159 |
"yes": "Sí",
|
| 160 |
"no": "No",
|
| 161 |
"check_button": "Verificar Elegibilidad",
|
|
@@ -183,6 +186,10 @@ def calculate_fpl_percentage(income, household_size):
|
|
| 183 |
def find_nearby_hospitals(zip_code, lang):
|
| 184 |
t = TRANSLATIONS[lang]
|
| 185 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
location = geolocator.geocode(f"{zip_code}, Connecticut, USA")
|
| 187 |
if not location:
|
| 188 |
return "Invalid ZIP code" if lang == "en" else "Código postal inválido", []
|
|
@@ -213,11 +220,32 @@ def find_nearby_hospitals(zip_code, lang):
|
|
| 213 |
except Exception as e:
|
| 214 |
return f"Error: {str(e)}", []
|
| 215 |
|
| 216 |
-
def determine_eligibility(hospital_name, income, household_size, has_snap_wic, lang):
|
| 217 |
if "(" in hospital_name:
|
| 218 |
hospital_name = hospital_name.split(" (")[0]
|
| 219 |
|
| 220 |
t = TRANSLATIONS[lang]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
hospital = HOSPITALS[hospital_name]
|
| 222 |
fpl_percentage = calculate_fpl_percentage(income, household_size)
|
| 223 |
|
|
@@ -227,6 +255,7 @@ def determine_eligibility(hospital_name, income, household_size, has_snap_wic, l
|
|
| 227 |
"household_size": household_size,
|
| 228 |
"fpl_percentage": fpl_percentage,
|
| 229 |
"has_snap_wic": has_snap_wic,
|
|
|
|
| 230 |
"pa_24_81_eligible": False,
|
| 231 |
"contact": hospital["contact"],
|
| 232 |
"special_notes": hospital["special_notes"],
|
|
@@ -266,9 +295,10 @@ def generate_explanation_streaming(data):
|
|
| 266 |
fpl_pct = data["fpl_percentage"]
|
| 267 |
|
| 268 |
# Confidence-based language selection
|
|
|
|
| 269 |
if is_pa_24_81:
|
| 270 |
confidence = "statutory" # Law-based, highest confidence
|
| 271 |
-
elif is_eligible and not has_asset_limit and fpl_pct <
|
| 272 |
confidence = "strong" # Clear match, no complications
|
| 273 |
elif is_eligible and has_asset_limit:
|
| 274 |
confidence = "moderate" # Additional requirements exist
|
|
@@ -333,10 +363,16 @@ Explica en 2 párrafos claros y cálidos. Máximo 150 palabras."""
|
|
| 333 |
|
| 334 |
Explain in 2 clear, warm paragraphs. Maximum 150 words."""
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
user_prompt = f"""Hospital: {data['hospital']}
|
| 337 |
Income: ${data['income']:,.0f} ({data['fpl_percentage']}% FPL)
|
| 338 |
Household: {data['household_size']} people
|
| 339 |
-
SNAP/WIC: {'Yes' if data['has_snap_wic'] else 'No'}
|
| 340 |
Status: {data['eligibility_status']}
|
| 341 |
Discount: {data['discount_level']}
|
| 342 |
Contact: {data['contact']}
|
|
@@ -345,7 +381,7 @@ PA 24-81 Eligible: {'Yes' if data['pa_24_81_eligible'] else 'No'}"""
|
|
| 345 |
|
| 346 |
try:
|
| 347 |
stream = client.chat.completions.create(
|
| 348 |
-
model="gpt-4o",
|
| 349 |
messages=[
|
| 350 |
{"role": "system", "content": system_msg},
|
| 351 |
{"role": "user", "content": user_prompt}
|
|
@@ -361,8 +397,9 @@ PA 24-81 Eligible: {'Yes' if data['pa_24_81_eligible'] else 'No'}"""
|
|
| 361 |
collected_text += chunk.choices[0].delta.content
|
| 362 |
yield collected_text
|
| 363 |
|
| 364 |
-
except Exception
|
| 365 |
-
|
|
|
|
| 366 |
|
| 367 |
def format_results_streaming(data, lang):
|
| 368 |
"""
|
|
@@ -370,7 +407,14 @@ def format_results_streaming(data, lang):
|
|
| 370 |
"""
|
| 371 |
t = TRANSLATIONS[lang]
|
| 372 |
|
| 373 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
header = f"""## {data['hospital']}
|
| 375 |
|
| 376 |
### {t['status']}: {data['eligibility_status']}
|
|
@@ -381,14 +425,7 @@ def format_results_streaming(data, lang):
|
|
| 381 |
|
| 382 |
"""
|
| 383 |
|
| 384 |
-
#
|
| 385 |
-
partial_explanation = ""
|
| 386 |
-
for partial_text in generate_explanation_streaming(data):
|
| 387 |
-
partial_explanation = partial_text
|
| 388 |
-
full_output = header + partial_explanation
|
| 389 |
-
yield full_output
|
| 390 |
-
|
| 391 |
-
# Add footer after streaming completes
|
| 392 |
footer = f"""
|
| 393 |
|
| 394 |
---
|
|
@@ -412,8 +449,16 @@ def format_results_streaming(data, lang):
|
|
| 412 |
{t['disclaimer']}
|
| 413 |
"""
|
| 414 |
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
|
| 418 |
# Custom CSS for CT.gov styling
|
| 419 |
custom_css = """
|
|
@@ -550,9 +595,10 @@ def create_interface():
|
|
| 550 |
with step3:
|
| 551 |
selected_hospital_display = gr.Markdown()
|
| 552 |
|
| 553 |
-
income_input = gr.Number(label="Annual Household Income ($)", minimum=
|
| 554 |
household_input = gr.Number(label="Household Size", minimum=1, value=3, precision=0)
|
| 555 |
snap_radio = gr.Radio(choices=["Yes", "No"], label="Enrolled in SNAP or WIC?", value="No")
|
|
|
|
| 556 |
|
| 557 |
check_btn = gr.Button("Check Eligibility", variant="primary", size="lg")
|
| 558 |
back_btn_c = gr.Button("← Back", size="sm")
|
|
@@ -607,6 +653,7 @@ def create_interface():
|
|
| 607 |
gr.update(visible=False),
|
| 608 |
gr.update(visible=True),
|
| 609 |
gr.update(visible=False),
|
|
|
|
| 610 |
gr.update(choices=[t['yes'], t['no']], value=t['no'])
|
| 611 |
]
|
| 612 |
|
|
@@ -619,13 +666,26 @@ def create_interface():
|
|
| 619 |
gr.update(visible=False),
|
| 620 |
gr.update(visible=True),
|
| 621 |
gr.update(visible=False),
|
|
|
|
| 622 |
gr.update(choices=[t['yes'], t['no']], value=t['no'])
|
| 623 |
]
|
| 624 |
|
| 625 |
-
def check_eligibility_wrapper(hospital, income, household, snap, lang):
|
| 626 |
t = TRANSLATIONS[lang]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
has_snap = (snap == t['yes'])
|
| 628 |
-
|
|
|
|
| 629 |
|
| 630 |
yield [
|
| 631 |
"",
|
|
@@ -666,19 +726,21 @@ def create_interface():
|
|
| 666 |
continue_btn_a.click(
|
| 667 |
fn=continue_from_search,
|
| 668 |
inputs=[hospital_radio, lang_state],
|
| 669 |
-
outputs=[selected_hospital, selected_hospital_display, step1, step2a, step3, step4, snap_radio]
|
| 670 |
)
|
| 671 |
|
| 672 |
continue_btn_b.click(
|
| 673 |
fn=continue_from_dropdown,
|
| 674 |
inputs=[hospital_dropdown, lang_state],
|
| 675 |
-
outputs=[selected_hospital, selected_hospital_display, step1, step2b, step3, step4, snap_radio]
|
| 676 |
)
|
| 677 |
|
| 678 |
check_btn.click(
|
| 679 |
fn=check_eligibility_wrapper,
|
| 680 |
-
inputs=[selected_hospital, income_input, household_input, snap_radio, lang_state],
|
| 681 |
-
outputs=[results_output, step1, step2a, step3, step4]
|
|
|
|
|
|
|
| 682 |
)
|
| 683 |
|
| 684 |
return demo
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import os
|
| 8 |
+
import re
|
| 9 |
import gradio as gr
|
| 10 |
from openai import OpenAI
|
| 11 |
from geopy.geocoders import Nominatim
|
|
|
|
| 130 |
"income_label": "Annual Household Income ($)",
|
| 131 |
"household_label": "Number of People in Household",
|
| 132 |
"snap_label": "Enrolled in SNAP or WIC?",
|
| 133 |
+
"insurance_label": "Do you have health insurance?",
|
| 134 |
"yes": "Yes",
|
| 135 |
"no": "No",
|
| 136 |
"check_button": "Check Eligibility",
|
|
|
|
| 158 |
"income_label": "Ingreso Anual del Hogar ($)",
|
| 159 |
"household_label": "Número de Personas en el Hogar",
|
| 160 |
"snap_label": "¿Inscrito en SNAP o WIC?",
|
| 161 |
+
"insurance_label": "¿Tiene seguro de salud?",
|
| 162 |
"yes": "Sí",
|
| 163 |
"no": "No",
|
| 164 |
"check_button": "Verificar Elegibilidad",
|
|
|
|
| 186 |
def find_nearby_hospitals(zip_code, lang):
|
| 187 |
t = TRANSLATIONS[lang]
|
| 188 |
try:
|
| 189 |
+
if not re.match(r'^06[0-9]{3}$', str(zip_code).strip()):
|
| 190 |
+
msg = "Please enter a valid Connecticut ZIP code (060xx–069xx)." if lang == "en" else "Ingrese un código postal válido de Connecticut (060xx–069xx)."
|
| 191 |
+
return msg, []
|
| 192 |
+
|
| 193 |
location = geolocator.geocode(f"{zip_code}, Connecticut, USA")
|
| 194 |
if not location:
|
| 195 |
return "Invalid ZIP code" if lang == "en" else "Código postal inválido", []
|
|
|
|
| 220 |
except Exception as e:
|
| 221 |
return f"Error: {str(e)}", []
|
| 222 |
|
| 223 |
+
def determine_eligibility(hospital_name, income, household_size, has_snap_wic, lang, has_insurance=False):
|
| 224 |
if "(" in hospital_name:
|
| 225 |
hospital_name = hospital_name.split(" (")[0]
|
| 226 |
|
| 227 |
t = TRANSLATIONS[lang]
|
| 228 |
+
|
| 229 |
+
if hospital_name not in HOSPITALS:
|
| 230 |
+
error_msg = "Hospital not found. Please go back and select a valid hospital." if lang == "en" else "Hospital no encontrado. Vuelva atrás y seleccione un hospital válido."
|
| 231 |
+
return {
|
| 232 |
+
"hospital": hospital_name,
|
| 233 |
+
"income": income,
|
| 234 |
+
"household_size": household_size,
|
| 235 |
+
"fpl_percentage": 0,
|
| 236 |
+
"has_snap_wic": has_snap_wic,
|
| 237 |
+
"has_insurance": has_insurance,
|
| 238 |
+
"pa_24_81_eligible": False,
|
| 239 |
+
"contact": "N/A",
|
| 240 |
+
"special_notes": "",
|
| 241 |
+
"asset_limit": None,
|
| 242 |
+
"fap_url": "",
|
| 243 |
+
"lang": lang,
|
| 244 |
+
"eligibility_status": error_msg,
|
| 245 |
+
"discount_level": "N/A",
|
| 246 |
+
"error": True
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
hospital = HOSPITALS[hospital_name]
|
| 250 |
fpl_percentage = calculate_fpl_percentage(income, household_size)
|
| 251 |
|
|
|
|
| 255 |
"household_size": household_size,
|
| 256 |
"fpl_percentage": fpl_percentage,
|
| 257 |
"has_snap_wic": has_snap_wic,
|
| 258 |
+
"has_insurance": has_insurance,
|
| 259 |
"pa_24_81_eligible": False,
|
| 260 |
"contact": hospital["contact"],
|
| 261 |
"special_notes": hospital["special_notes"],
|
|
|
|
| 295 |
fpl_pct = data["fpl_percentage"]
|
| 296 |
|
| 297 |
# Confidence-based language selection
|
| 298 |
+
hospital_threshold = HOSPITALS[data["hospital"]]["free_care_threshold"] if data["hospital"] in HOSPITALS else 200
|
| 299 |
if is_pa_24_81:
|
| 300 |
confidence = "statutory" # Law-based, highest confidence
|
| 301 |
+
elif is_eligible and not has_asset_limit and fpl_pct < hospital_threshold * 0.8:
|
| 302 |
confidence = "strong" # Clear match, no complications
|
| 303 |
elif is_eligible and has_asset_limit:
|
| 304 |
confidence = "moderate" # Additional requirements exist
|
|
|
|
| 363 |
|
| 364 |
Explain in 2 clear, warm paragraphs. Maximum 150 words."""
|
| 365 |
|
| 366 |
+
insurance_note = ""
|
| 367 |
+
if data.get("has_insurance"):
|
| 368 |
+
insurance_note = "\nInsurance: Yes — Note that the hospital will bill insurance first; financial assistance applies to remaining balances."
|
| 369 |
+
else:
|
| 370 |
+
insurance_note = "\nInsurance: No"
|
| 371 |
+
|
| 372 |
user_prompt = f"""Hospital: {data['hospital']}
|
| 373 |
Income: ${data['income']:,.0f} ({data['fpl_percentage']}% FPL)
|
| 374 |
Household: {data['household_size']} people
|
| 375 |
+
SNAP/WIC: {'Yes' if data['has_snap_wic'] else 'No'}{insurance_note}
|
| 376 |
Status: {data['eligibility_status']}
|
| 377 |
Discount: {data['discount_level']}
|
| 378 |
Contact: {data['contact']}
|
|
|
|
| 381 |
|
| 382 |
try:
|
| 383 |
stream = client.chat.completions.create(
|
| 384 |
+
model="gpt-4o-mini",
|
| 385 |
messages=[
|
| 386 |
{"role": "system", "content": system_msg},
|
| 387 |
{"role": "user", "content": user_prompt}
|
|
|
|
| 397 |
collected_text += chunk.choices[0].delta.content
|
| 398 |
yield collected_text
|
| 399 |
|
| 400 |
+
except Exception:
|
| 401 |
+
fallback = "Detailed explanation temporarily unavailable. Please review the eligibility summary above and contact the hospital directly." if lang == "en" else "La explicación detallada no está disponible temporalmente. Revise el resumen de elegibilidad anterior y comuníquese directamente con el hospital."
|
| 402 |
+
yield fallback
|
| 403 |
|
| 404 |
def format_results_streaming(data, lang):
|
| 405 |
"""
|
|
|
|
| 407 |
"""
|
| 408 |
t = TRANSLATIONS[lang]
|
| 409 |
|
| 410 |
+
# Disclaimer banner at top
|
| 411 |
+
disclaimer_banner = f"""> {t['disclaimer']}
|
| 412 |
+
|
| 413 |
+
---
|
| 414 |
+
|
| 415 |
+
"""
|
| 416 |
+
|
| 417 |
+
# Header
|
| 418 |
header = f"""## {data['hospital']}
|
| 419 |
|
| 420 |
### {t['status']}: {data['eligibility_status']}
|
|
|
|
| 425 |
|
| 426 |
"""
|
| 427 |
|
| 428 |
+
# Footer (always shown regardless of AI explanation success)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
footer = f"""
|
| 430 |
|
| 431 |
---
|
|
|
|
| 449 |
{t['disclaimer']}
|
| 450 |
"""
|
| 451 |
|
| 452 |
+
base = disclaimer_banner + header
|
| 453 |
+
|
| 454 |
+
# Stream the explanation
|
| 455 |
+
partial_explanation = ""
|
| 456 |
+
for partial_text in generate_explanation_streaming(data):
|
| 457 |
+
partial_explanation = partial_text
|
| 458 |
+
yield base + partial_explanation
|
| 459 |
+
|
| 460 |
+
# Always append footer after streaming completes
|
| 461 |
+
yield base + partial_explanation + footer
|
| 462 |
|
| 463 |
# Custom CSS for CT.gov styling
|
| 464 |
custom_css = """
|
|
|
|
| 595 |
with step3:
|
| 596 |
selected_hospital_display = gr.Markdown()
|
| 597 |
|
| 598 |
+
income_input = gr.Number(label="Annual Household Income ($)", minimum=1, value=35000)
|
| 599 |
household_input = gr.Number(label="Household Size", minimum=1, value=3, precision=0)
|
| 600 |
snap_radio = gr.Radio(choices=["Yes", "No"], label="Enrolled in SNAP or WIC?", value="No")
|
| 601 |
+
insurance_radio = gr.Radio(choices=["Yes", "No"], label="Do you have health insurance?", value="No")
|
| 602 |
|
| 603 |
check_btn = gr.Button("Check Eligibility", variant="primary", size="lg")
|
| 604 |
back_btn_c = gr.Button("← Back", size="sm")
|
|
|
|
| 653 |
gr.update(visible=False),
|
| 654 |
gr.update(visible=True),
|
| 655 |
gr.update(visible=False),
|
| 656 |
+
gr.update(choices=[t['yes'], t['no']], value=t['no']),
|
| 657 |
gr.update(choices=[t['yes'], t['no']], value=t['no'])
|
| 658 |
]
|
| 659 |
|
|
|
|
| 666 |
gr.update(visible=False),
|
| 667 |
gr.update(visible=True),
|
| 668 |
gr.update(visible=False),
|
| 669 |
+
gr.update(choices=[t['yes'], t['no']], value=t['no']),
|
| 670 |
gr.update(choices=[t['yes'], t['no']], value=t['no'])
|
| 671 |
]
|
| 672 |
|
| 673 |
+
def check_eligibility_wrapper(hospital, income, household, snap, insurance, lang):
|
| 674 |
t = TRANSLATIONS[lang]
|
| 675 |
+
|
| 676 |
+
# Input validation
|
| 677 |
+
if income is None or income <= 0:
|
| 678 |
+
gr.Warning("Please enter a valid income greater than $0." if lang == "en" else "Ingrese un ingreso válido mayor que $0.")
|
| 679 |
+
yield [gr.update(), gr.update(), gr.update(), gr.update(), gr.update()]
|
| 680 |
+
return
|
| 681 |
+
if household is None or int(household) < 1:
|
| 682 |
+
gr.Warning("Household size must be at least 1." if lang == "en" else "El tamaño del hogar debe ser al menos 1.")
|
| 683 |
+
yield [gr.update(), gr.update(), gr.update(), gr.update(), gr.update()]
|
| 684 |
+
return
|
| 685 |
+
|
| 686 |
has_snap = (snap == t['yes'])
|
| 687 |
+
has_insurance = (insurance == t['yes'])
|
| 688 |
+
data = determine_eligibility(hospital, income, int(household), has_snap, lang, has_insurance=has_insurance)
|
| 689 |
|
| 690 |
yield [
|
| 691 |
"",
|
|
|
|
| 726 |
continue_btn_a.click(
|
| 727 |
fn=continue_from_search,
|
| 728 |
inputs=[hospital_radio, lang_state],
|
| 729 |
+
outputs=[selected_hospital, selected_hospital_display, step1, step2a, step3, step4, snap_radio, insurance_radio]
|
| 730 |
)
|
| 731 |
|
| 732 |
continue_btn_b.click(
|
| 733 |
fn=continue_from_dropdown,
|
| 734 |
inputs=[hospital_dropdown, lang_state],
|
| 735 |
+
outputs=[selected_hospital, selected_hospital_display, step1, step2b, step3, step4, snap_radio, insurance_radio]
|
| 736 |
)
|
| 737 |
|
| 738 |
check_btn.click(
|
| 739 |
fn=check_eligibility_wrapper,
|
| 740 |
+
inputs=[selected_hospital, income_input, household_input, snap_radio, insurance_radio, lang_state],
|
| 741 |
+
outputs=[results_output, step1, step2a, step3, step4],
|
| 742 |
+
api_name=False,
|
| 743 |
+
concurrency_limit=5
|
| 744 |
)
|
| 745 |
|
| 746 |
return demo
|