Alex Amari commited on
Commit
d37d00d
·
1 Parent(s): 9e8e8bd

Tier 2: input validation, rate limiting, and bug fixes

Browse files
Files changed (1) hide show
  1. app.py +86 -24
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 < data.get("threshold", 200) * 0.8:
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 as e:
365
- yield f"Error: {str(e)}"
 
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
- # Start with header (instant)
 
 
 
 
 
 
 
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
- # Stream the explanation
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
- final_output = header + partial_explanation + footer
416
- yield final_output
 
 
 
 
 
 
 
 
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=0, value=35000)
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
- data = determine_eligibility(hospital, income, int(household), has_snap, lang)
 
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