Alex Amari commited on
Commit
718b72b
·
1 Parent(s): d37d00d

Tiers 3-4: code organization, map placeholder, download, accessibility

Browse files
__pycache__/app.cpython-312-pytest-7.4.4.pyc ADDED
Binary file (44.3 kB). View file
 
app.py CHANGED
@@ -2,23 +2,33 @@
2
  """
3
  Connecticut Hospital Financial Assistance Screener v4
4
  Deployed on Hugging Face Spaces
 
 
 
5
  """
6
 
 
 
7
  import os
8
  import re
 
9
  import gradio as gr
10
  from openai import OpenAI
11
  from geopy.geocoders import Nominatim
12
  from geopy.distance import geodesic
13
  import time
14
 
15
- # Load API key from environment variable (set in HF Spaces secrets)
16
  OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
17
  client = OpenAI(api_key=OPENAI_API_KEY)
18
  DEMO_PASSWORD = "ct2026"
19
 
20
  geolocator = Nominatim(user_agent="ct_hospital_screener")
21
 
 
 
 
 
 
22
  HOSPITALS = {
23
  "Yale New Haven Health": {
24
  "free_care_threshold": 250,
@@ -46,7 +56,7 @@ HOSPITALS = {
46
  "asset_limit": None,
47
  "contact": "860-714-1657",
48
  "special_notes": "Medicaid exhaustion required",
49
- "location": (41.7658, -72.6734),
50
  "city": "Hartford",
51
  "fap_url": "https://www.trinityhealthofne.org/for-patients/billing-and-financial-resources"
52
  },
@@ -112,11 +122,14 @@ HOSPITALS = {
112
  }
113
  }
114
 
115
- # 2025 Federal Poverty Level Guidelines
116
  # Source: https://aspe.hhs.gov/topics/poverty-economic-mobility/poverty-guidelines
117
  # Updated: January 2025. Check annually for new guidelines.
 
118
  FPL_2025 = {1: 15650, 2: 21150, 3: 26650, 4: 32150, 5: 37650, 6: 43150, 7: 48650, 8: 54150}
119
 
 
 
120
  TRANSLATIONS = {
121
  "en": {
122
  "title": "Connecticut Hospital Financial Assistance Screener",
@@ -144,7 +157,8 @@ TRANSLATIONS = {
144
  "sources": "Source Documents",
145
  "disclaimer": "⚠️ This result was generated by AI and is for informational purposes only. It does not guarantee accuracy or constitute legal or financial advice. Please contact the hospital directly to confirm eligibility and complete the formal application process.",
146
  "eligible": "LIKELY ELIGIBLE",
147
- "not_eligible": "MAY NOT QUALIFY"
 
148
  },
149
  "es": {
150
  "title": "Evaluador de Asistencia Financiera Hospitalaria de Connecticut",
@@ -172,27 +186,51 @@ TRANSLATIONS = {
172
  "sources": "Documentos Fuente",
173
  "disclaimer": "⚠️ Este resultado fue generado por IA y es solo para fines informativos. No garantiza precisión ni constituye asesoramiento legal o financiero. Comuníquese directamente con el hospital para confirmar la elegibilidad y completar el proceso de solicitud formal.",
174
  "eligible": "PROBABLEMENTE ELEGIBLE",
175
- "not_eligible": "PUEDE NO CALIFICAR"
 
176
  }
177
  }
178
 
 
 
179
  def calculate_fpl_percentage(income, household_size):
 
 
 
 
 
180
  if household_size <= 8:
181
  fpl_base = FPL_2025[household_size]
182
  else:
183
  fpl_base = FPL_2025[8] + (5500 * (household_size - 8))
184
  return round((income / fpl_base) * 100, 1)
185
 
 
 
 
 
 
 
 
 
 
 
 
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", []
196
 
197
  user_coords = (location.latitude, location.longitude)
198
  distances = []
@@ -209,9 +247,9 @@ def find_nearby_hospitals(zip_code, lang):
209
  choice_label = f"{name} ({data['city']}, {dist:.1f} mi)"
210
  choices.append(choice_label)
211
  cards += f"""### {name}
212
- 📍 {data['city']} — {dist:.1f} {t['miles_away']}
213
- 💰 {t['free_care']} {data['free_care_threshold']}% FPL | {t['sliding_scale']} {data['sliding_scale_max']}% FPL
214
- 📞 {data['contact']}
215
 
216
  ---
217
  """
@@ -221,6 +259,12 @@ def find_nearby_hospitals(zip_code, lang):
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
 
@@ -280,10 +324,14 @@ def determine_eligibility(hospital_name, income, household_size, has_snap_wic, l
280
 
281
  return result
282
 
 
 
283
  def generate_explanation_streaming(data):
284
- """
285
- Streaming version - yields text as it's generated
286
- Uses confidence-based language tiers
 
 
287
  """
288
  lang = data["lang"]
289
  t = TRANSLATIONS[lang]
@@ -401,9 +449,14 @@ PA 24-81 Eligible: {'Yes' if data['pa_24_81_eligible'] else 'No'}"""
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
- """
406
- Generator that yields progressive updates to the results
 
 
 
407
  """
408
  t = TRANSLATIONS[lang]
409
 
@@ -419,7 +472,7 @@ def format_results_streaming(data, lang):
419
 
420
  ### {t['status']}: {data['eligibility_status']}
421
 
422
- 💵 ${data['income']:,.0f} ({data['fpl_percentage']}% FPL)
423
 
424
  ---
425
 
@@ -430,19 +483,19 @@ def format_results_streaming(data, lang):
430
 
431
  ---
432
 
433
- 📞 **{t['contact']}:** {data['contact']}
434
 
435
  ---
436
 
437
- ### {t['sources']}
438
 
439
- 📄 [**{data['hospital']} Financial Assistance Policy**]({data['fap_url']})
440
 
441
- 📄 [**Connecticut Public Act 24-81**](https://www.cga.ct.gov/2024/ba/pdf/2024HB-05320-R000149-BA.pdf)
442
 
443
- 📄 [**2025 Federal Poverty Level Guidelines**](https://www.healthcare.gov/glossary/federal-poverty-level-fpl/)
444
 
445
- 📄 [**CT Office of the Healthcare Advocate**](https://portal.ct.gov/oha)
446
 
447
  ---
448
 
@@ -460,7 +513,37 @@ def format_results_streaming(data, lang):
460
  # Always append footer after streaming completes
461
  yield base + partial_explanation + footer
462
 
463
- # Custom CSS for CT.gov styling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  custom_css = """
465
  /* CT.gov inspired styling */
466
  .gradio-container {
@@ -538,7 +621,10 @@ label {
538
  }
539
  """
540
 
 
 
541
  def create_interface():
 
542
  with gr.Blocks(
543
  theme=gr.themes.Default(primary_hue="blue", neutral_hue="slate"),
544
  css=custom_css
@@ -546,6 +632,7 @@ def create_interface():
546
 
547
  lang_state = gr.State("en")
548
  selected_hospital = gr.State("")
 
549
 
550
  # Header with Beta tag
551
  with gr.Row():
@@ -564,8 +651,17 @@ def create_interface():
564
  with step1:
565
  gr.Markdown("### How would you like to start?")
566
  with gr.Row():
567
- help_btn = gr.Button("🔍 Help me find a hospital", variant="primary", size="lg")
568
- know_btn = gr.Button("🏥 I know my hospital", variant="secondary", size="lg")
 
 
 
 
 
 
 
 
 
569
 
570
  # Step 2a: Find hospital by ZIP
571
  step2a = gr.Group(visible=False)
@@ -607,10 +703,15 @@ def create_interface():
607
  step4 = gr.Group(visible=False)
608
  with step4:
609
  results_output = gr.Markdown()
610
- restart_btn = gr.Button("Check Another Hospital", variant="secondary")
 
 
 
 
 
611
 
612
- # Navigation functions
613
  def show_find_path():
 
614
  return [
615
  gr.update(visible=False),
616
  gr.update(visible=True),
@@ -620,6 +721,7 @@ def create_interface():
620
  ]
621
 
622
  def show_know_path():
 
623
  return [
624
  gr.update(visible=False),
625
  gr.update(visible=False),
@@ -629,6 +731,7 @@ def create_interface():
629
  ]
630
 
631
  def back_to_start():
 
632
  return [
633
  gr.update(visible=True),
634
  gr.update(visible=False),
@@ -638,12 +741,14 @@ def create_interface():
638
  ]
639
 
640
  def search_hospitals_wrapper(zip_code, lang):
 
641
  cards, choices = find_nearby_hospitals(zip_code, lang)
642
  if choices:
643
  return cards, gr.update(choices=choices, visible=True, value=choices[0]), gr.update(visible=True)
644
  return cards, gr.update(visible=False), gr.update(visible=False)
645
 
646
  def continue_from_search(hospital_choice, lang):
 
647
  t = TRANSLATIONS[lang]
648
  hospital_name = hospital_choice.split(" (")[0] if "(" in hospital_choice else hospital_choice
649
  return [
@@ -658,6 +763,7 @@ def create_interface():
658
  ]
659
 
660
  def continue_from_dropdown(hospital_name, lang):
 
661
  t = TRANSLATIONS[lang]
662
  return [
663
  hospital_name,
@@ -671,16 +777,17 @@ def create_interface():
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'])
@@ -692,7 +799,8 @@ def create_interface():
692
  gr.update(visible=False),
693
  gr.update(visible=False),
694
  gr.update(visible=False),
695
- gr.update(visible=True)
 
696
  ]
697
 
698
  for partial_result in format_results_streaming(data, lang):
@@ -701,11 +809,24 @@ def create_interface():
701
  gr.update(),
702
  gr.update(),
703
  gr.update(),
704
- gr.update()
 
705
  ]
706
 
 
 
 
 
 
 
 
 
 
 
 
707
  def update_lang(choice):
708
- return "es" if choice == "Español" else "en"
 
709
 
710
  # Wire up events
711
  lang_toggle.change(fn=update_lang, inputs=[lang_toggle], outputs=[lang_state])
@@ -738,12 +859,103 @@ def create_interface():
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
747
 
 
 
 
 
748
  demo = create_interface()
749
- demo.launch(auth=("oha", DEMO_PASSWORD))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  """
3
  Connecticut Hospital Financial Assistance Screener v4
4
  Deployed on Hugging Face Spaces
5
+
6
+ Single-file Gradio application that screens CT residents for hospital
7
+ financial assistance eligibility based on FPL thresholds and PA 24-81.
8
  """
9
 
10
+ # === IMPORTS AND CONFIGURATION ===
11
+
12
  import os
13
  import re
14
+ import tempfile
15
  import gradio as gr
16
  from openai import OpenAI
17
  from geopy.geocoders import Nominatim
18
  from geopy.distance import geodesic
19
  import time
20
 
 
21
  OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
22
  client = OpenAI(api_key=OPENAI_API_KEY)
23
  DEMO_PASSWORD = "ct2026"
24
 
25
  geolocator = Nominatim(user_agent="ct_hospital_screener")
26
 
27
+ # Simple in-memory cache for ZIP code geocoding results
28
+ _zip_geocode_cache = {}
29
+
30
+ # === HOSPITAL DATA ===
31
+
32
  HOSPITALS = {
33
  "Yale New Haven Health": {
34
  "free_care_threshold": 250,
 
56
  "asset_limit": None,
57
  "contact": "860-714-1657",
58
  "special_notes": "Medicaid exhaustion required",
59
+ "location": (41.7726, -72.6932), # Saint Francis Hospital, Hartford
60
  "city": "Hartford",
61
  "fap_url": "https://www.trinityhealthofne.org/for-patients/billing-and-financial-resources"
62
  },
 
122
  }
123
  }
124
 
125
+ # === FEDERAL POVERTY LEVEL DATA ===
126
  # Source: https://aspe.hhs.gov/topics/poverty-economic-mobility/poverty-guidelines
127
  # Updated: January 2025. Check annually for new guidelines.
128
+
129
  FPL_2025 = {1: 15650, 2: 21150, 3: 26650, 4: 32150, 5: 37650, 6: 43150, 7: 48650, 8: 54150}
130
 
131
+ # === TRANSLATIONS ===
132
+
133
  TRANSLATIONS = {
134
  "en": {
135
  "title": "Connecticut Hospital Financial Assistance Screener",
 
157
  "sources": "Source Documents",
158
  "disclaimer": "⚠️ This result was generated by AI and is for informational purposes only. It does not guarantee accuracy or constitute legal or financial advice. Please contact the hospital directly to confirm eligibility and complete the formal application process.",
159
  "eligible": "LIKELY ELIGIBLE",
160
+ "not_eligible": "MAY NOT QUALIFY",
161
+ "download_button": "Download Results"
162
  },
163
  "es": {
164
  "title": "Evaluador de Asistencia Financiera Hospitalaria de Connecticut",
 
186
  "sources": "Documentos Fuente",
187
  "disclaimer": "⚠️ Este resultado fue generado por IA y es solo para fines informativos. No garantiza precisión ni constituye asesoramiento legal o financiero. Comuníquese directamente con el hospital para confirmar la elegibilidad y completar el proceso de solicitud formal.",
188
  "eligible": "PROBABLEMENTE ELEGIBLE",
189
+ "not_eligible": "PUEDE NO CALIFICAR",
190
+ "download_button": "Descargar Resultados"
191
  }
192
  }
193
 
194
+ # === ELIGIBILITY LOGIC ===
195
+
196
  def calculate_fpl_percentage(income, household_size):
197
+ """Calculate income as a percentage of the Federal Poverty Level.
198
+
199
+ For households larger than 8, adds $5,500 per additional person
200
+ to the base FPL amount per HHS guidelines.
201
+ """
202
  if household_size <= 8:
203
  fpl_base = FPL_2025[household_size]
204
  else:
205
  fpl_base = FPL_2025[8] + (5500 * (household_size - 8))
206
  return round((income / fpl_base) * 100, 1)
207
 
208
+ # === HOSPITAL SEARCH ===
209
+
210
+ def _geocode_zip(zip_code):
211
+ """Geocode a CT ZIP code with in-memory caching to avoid redundant API calls."""
212
+ zip_code = str(zip_code).strip()
213
+ if zip_code in _zip_geocode_cache:
214
+ return _zip_geocode_cache[zip_code]
215
+ location = geolocator.geocode(f"{zip_code}, Connecticut, USA")
216
+ _zip_geocode_cache[zip_code] = location
217
+ return location
218
+
219
  def find_nearby_hospitals(zip_code, lang):
220
+ """Find the 3 nearest CT hospitals to a given ZIP code.
221
+
222
+ Validates the ZIP is a CT format (060xx-069xx), geocodes it,
223
+ then returns markdown cards and choice labels sorted by distance.
224
+ """
225
  t = TRANSLATIONS[lang]
226
  try:
227
  if not re.match(r'^06[0-9]{3}$', str(zip_code).strip()):
228
+ msg = "Please enter a valid Connecticut ZIP code (060xx\u2013069xx)." if lang == "en" else "Ingrese un c\u00f3digo postal v\u00e1lido de Connecticut (060xx\u2013069xx)."
229
  return msg, []
230
 
231
+ location = _geocode_zip(zip_code)
232
  if not location:
233
+ return "Invalid ZIP code" if lang == "en" else "C\u00f3digo postal inv\u00e1lido", []
234
 
235
  user_coords = (location.latitude, location.longitude)
236
  distances = []
 
247
  choice_label = f"{name} ({data['city']}, {dist:.1f} mi)"
248
  choices.append(choice_label)
249
  cards += f"""### {name}
250
+ 📍 Location: {data['city']} — {dist:.1f} {t['miles_away']}
251
+ 💰 Assistance: {t['free_care']} {data['free_care_threshold']}% FPL | {t['sliding_scale']} {data['sliding_scale_max']}% FPL
252
+ 📞 Phone: {data['contact']}
253
 
254
  ---
255
  """
 
259
  return f"Error: {str(e)}", []
260
 
261
  def determine_eligibility(hospital_name, income, household_size, has_snap_wic, lang, has_insurance=False):
262
+ """Determine financial assistance eligibility for a given hospital.
263
+
264
+ Checks PA 24-81 presumptive eligibility first, then free care and
265
+ sliding scale thresholds. Returns a result dict with all data needed
266
+ for the explanation and display.
267
+ """
268
  if "(" in hospital_name:
269
  hospital_name = hospital_name.split(" (")[0]
270
 
 
324
 
325
  return result
326
 
327
+ # === AI EXPLANATION GENERATION ===
328
+
329
  def generate_explanation_streaming(data):
330
+ """Stream an AI-generated explanation of the eligibility result.
331
+
332
+ Selects a confidence tier (statutory / strong / moderate / negative)
333
+ to calibrate the language used by the LLM, then streams the response
334
+ token-by-token. Falls back to a static message on API failure.
335
  """
336
  lang = data["lang"]
337
  t = TRANSLATIONS[lang]
 
449
  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."
450
  yield fallback
451
 
452
+ # === RESULTS FORMATTING ===
453
+
454
  def format_results_streaming(data, lang):
455
+ """Build and stream the full results page as markdown.
456
+
457
+ Renders a disclaimer banner, eligibility header, streamed AI explanation,
458
+ and a footer with contact info and source links. The footer is always
459
+ appended regardless of whether the AI call succeeded.
460
  """
461
  t = TRANSLATIONS[lang]
462
 
 
472
 
473
  ### {t['status']}: {data['eligibility_status']}
474
 
475
+ 💵 Income: ${data['income']:,.0f} ({data['fpl_percentage']}% FPL)
476
 
477
  ---
478
 
 
483
 
484
  ---
485
 
486
+ 📞 Contact: **{data['contact']}**
487
 
488
  ---
489
 
490
+ ### 📄 {t['sources']}
491
 
492
+ 📄 Document: [**{data['hospital']} Financial Assistance Policy**]({data['fap_url']})
493
 
494
+ 📄 Document: [**Connecticut Public Act 24-81**](https://www.cga.ct.gov/2024/ba/pdf/2024HB-05320-R000149-BA.pdf)
495
 
496
+ 📄 Document: [**2025 Federal Poverty Level Guidelines**](https://www.healthcare.gov/glossary/federal-poverty-level-fpl/)
497
 
498
+ 📄 Document: [**CT Office of the Healthcare Advocate**](https://portal.ct.gov/oha)
499
 
500
  ---
501
 
 
513
  # Always append footer after streaming completes
514
  yield base + partial_explanation + footer
515
 
516
+
517
+ def generate_download_content(data, lang):
518
+ """Generate a plain-text summary of the eligibility result for download."""
519
+ t = TRANSLATIONS[lang]
520
+ lines = [
521
+ f"Connecticut Hospital Financial Assistance — Eligibility Summary",
522
+ f"={'=' * 59}",
523
+ f"",
524
+ f"Hospital: {data['hospital']}",
525
+ f"Status: {data['eligibility_status']}",
526
+ f"Discount Level: {data['discount_level']}",
527
+ f"",
528
+ f"Annual Household Income: ${data['income']:,.0f}",
529
+ f"Household Size: {data['household_size']}",
530
+ f"FPL Percentage: {data['fpl_percentage']}%",
531
+ f"SNAP/WIC Enrolled: {'Yes' if data['has_snap_wic'] else 'No'}",
532
+ f"Has Insurance: {'Yes' if data.get('has_insurance') else 'No'}",
533
+ f"PA 24-81 Eligible: {'Yes' if data['pa_24_81_eligible'] else 'No'}",
534
+ f"",
535
+ f"Contact: {data['contact']}",
536
+ f"Asset Limit: {data['asset_limit'] or 'None'}",
537
+ f"FAP URL: {data['fap_url']}",
538
+ f"",
539
+ f"---",
540
+ f"",
541
+ t['disclaimer'],
542
+ ]
543
+ return "\n".join(lines)
544
+
545
+ # === CUSTOM CSS ===
546
+
547
  custom_css = """
548
  /* CT.gov inspired styling */
549
  .gradio-container {
 
621
  }
622
  """
623
 
624
+ # === GRADIO UI ===
625
+
626
  def create_interface():
627
+ """Build and return the Gradio Blocks interface with all steps and event wiring."""
628
  with gr.Blocks(
629
  theme=gr.themes.Default(primary_hue="blue", neutral_hue="slate"),
630
  css=custom_css
 
632
 
633
  lang_state = gr.State("en")
634
  selected_hospital = gr.State("")
635
+ last_result_state = gr.State(None) # Stores the most recent eligibility result dict
636
 
637
  # Header with Beta tag
638
  with gr.Row():
 
651
  with step1:
652
  gr.Markdown("### How would you like to start?")
653
  with gr.Row():
654
+ help_btn = gr.Button("🔍 Search: Help me find a hospital", variant="primary", size="lg")
655
+ know_btn = gr.Button("🏥 Select: I know my hospital", variant="secondary", size="lg")
656
+
657
+ # TODO: Replace with actual OHA Google Map embed code
658
+ gr.HTML(
659
+ '<div style="background: #e9ecef; border: 2px dashed #adb5bd; border-radius: 8px; '
660
+ 'padding: 2rem; text-align: center; color: #6c757d; margin-top: 1rem;">'
661
+ '<p style="font-size: 1.1rem; margin: 0;">🗺️ Map: Connecticut Hospital Locations</p>'
662
+ '<p style="font-size: 0.85rem; margin: 0.5rem 0 0 0;">Interactive map coming soon</p>'
663
+ '</div>'
664
+ )
665
 
666
  # Step 2a: Find hospital by ZIP
667
  step2a = gr.Group(visible=False)
 
703
  step4 = gr.Group(visible=False)
704
  with step4:
705
  results_output = gr.Markdown()
706
+ with gr.Row():
707
+ restart_btn = gr.Button("🔄 Restart: Check Another Hospital", variant="secondary")
708
+ download_btn = gr.Button("📥 Download: Save Results", variant="secondary")
709
+ download_file = gr.File(visible=False, label="Download")
710
+
711
+ # --- Navigation functions ---
712
 
 
713
  def show_find_path():
714
+ """Show the ZIP code search path (Step 2a)."""
715
  return [
716
  gr.update(visible=False),
717
  gr.update(visible=True),
 
721
  ]
722
 
723
  def show_know_path():
724
+ """Show the hospital dropdown path (Step 2b)."""
725
  return [
726
  gr.update(visible=False),
727
  gr.update(visible=False),
 
731
  ]
732
 
733
  def back_to_start():
734
+ """Return to Step 1."""
735
  return [
736
  gr.update(visible=True),
737
  gr.update(visible=False),
 
741
  ]
742
 
743
  def search_hospitals_wrapper(zip_code, lang):
744
+ """Geocode a ZIP and return nearby hospital cards and radio choices."""
745
  cards, choices = find_nearby_hospitals(zip_code, lang)
746
  if choices:
747
  return cards, gr.update(choices=choices, visible=True, value=choices[0]), gr.update(visible=True)
748
  return cards, gr.update(visible=False), gr.update(visible=False)
749
 
750
  def continue_from_search(hospital_choice, lang):
751
+ """Advance from ZIP search results to the eligibility form."""
752
  t = TRANSLATIONS[lang]
753
  hospital_name = hospital_choice.split(" (")[0] if "(" in hospital_choice else hospital_choice
754
  return [
 
763
  ]
764
 
765
  def continue_from_dropdown(hospital_name, lang):
766
+ """Advance from hospital dropdown to the eligibility form."""
767
  t = TRANSLATIONS[lang]
768
  return [
769
  hospital_name,
 
777
  ]
778
 
779
  def check_eligibility_wrapper(hospital, income, household, snap, insurance, lang):
780
+ """Validate inputs, run eligibility check, and stream results."""
781
  t = TRANSLATIONS[lang]
782
 
783
  # Input validation
784
  if income is None or income <= 0:
785
+ gr.Warning("Please enter a valid income greater than $0." if lang == "en" else "Ingrese un ingreso v\u00e1lido mayor que $0.")
786
+ yield [gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), None]
787
  return
788
  if household is None or int(household) < 1:
789
+ gr.Warning("Household size must be at least 1." if lang == "en" else "El tama\u00f1o del hogar debe ser al menos 1.")
790
+ yield [gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), None]
791
  return
792
 
793
  has_snap = (snap == t['yes'])
 
799
  gr.update(visible=False),
800
  gr.update(visible=False),
801
  gr.update(visible=False),
802
+ gr.update(visible=True),
803
+ data
804
  ]
805
 
806
  for partial_result in format_results_streaming(data, lang):
 
809
  gr.update(),
810
  gr.update(),
811
  gr.update(),
812
+ gr.update(),
813
+ data
814
  ]
815
 
816
+ def download_results(last_result, lang):
817
+ """Generate a temporary text file with the eligibility summary for download."""
818
+ if not last_result:
819
+ gr.Warning("No results to download." if lang == "en" else "No hay resultados para descargar.")
820
+ return gr.update(visible=False)
821
+ content = generate_download_content(last_result, lang)
822
+ tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', prefix='eligibility_summary_', delete=False)
823
+ tmp.write(content)
824
+ tmp.close()
825
+ return gr.update(value=tmp.name, visible=True)
826
+
827
  def update_lang(choice):
828
+ """Convert the language toggle selection to a language code."""
829
+ return "es" if choice == "Espa\u00f1ol" else "en"
830
 
831
  # Wire up events
832
  lang_toggle.change(fn=update_lang, inputs=[lang_toggle], outputs=[lang_state])
 
859
  check_btn.click(
860
  fn=check_eligibility_wrapper,
861
  inputs=[selected_hospital, income_input, household_input, snap_radio, insurance_radio, lang_state],
862
+ outputs=[results_output, step1, step2a, step3, step4, last_result_state],
863
  api_name=False,
864
  concurrency_limit=5
865
  )
866
 
867
+ download_btn.click(
868
+ fn=download_results,
869
+ inputs=[last_result_state, lang_state],
870
+ outputs=[download_file]
871
+ )
872
+
873
  return demo
874
 
875
+ # === LAUNCH ===
876
+ # HF Spaces auto-detects the `demo` Blocks instance.
877
+ # The explicit launch() is guarded so pytest can import without starting a server.
878
+
879
  demo = create_interface()
880
+
881
+ if __name__ == "__main__":
882
+ demo.launch(auth=("oha", DEMO_PASSWORD))
883
+
884
+
885
+ # === TESTS (pytest-compatible) ===
886
+ # Run with: OPENAI_API_KEY=test pytest app.py -v
887
+
888
+
889
+ def test_calculate_fpl_percentage_single_person():
890
+ """FPL for a single person earning exactly the FPL amount should be 100%."""
891
+ result = calculate_fpl_percentage(15650, 1)
892
+ assert result == 100.0
893
+
894
+
895
+ def test_calculate_fpl_percentage_household_of_4():
896
+ """A family of 4 at $32,150 should be exactly 100% FPL."""
897
+ result = calculate_fpl_percentage(32150, 4)
898
+ assert result == 100.0
899
+
900
+
901
+ def test_calculate_fpl_percentage_large_household():
902
+ """Household of 10 should use the overflow formula: FPL_8 + 5500*(10-8)."""
903
+ expected_base = 54150 + (5500 * 2) # 65150
904
+ income = expected_base
905
+ result = calculate_fpl_percentage(income, 10)
906
+ assert result == 100.0
907
+
908
+
909
+ def test_calculate_fpl_percentage_half():
910
+ """Income at half of FPL for household of 1 should be ~50%."""
911
+ result = calculate_fpl_percentage(15650 / 2, 1)
912
+ assert result == 50.0
913
+
914
+
915
+ def test_determine_eligibility_free_care():
916
+ """Income well below the free care threshold should yield 100% Discount."""
917
+ result = determine_eligibility("Yale New Haven Health", 10000, 1, False, "en")
918
+ assert result["eligibility_status"] == "LIKELY ELIGIBLE"
919
+ assert result["discount_level"] == "100% Discount"
920
+
921
+
922
+ def test_determine_eligibility_sliding_scale():
923
+ """Income between free care and sliding scale thresholds → Partial Discount."""
924
+ # Yale: free_care=250%, sliding=550%. For household of 1, FPL=$15,650.
925
+ # 300% FPL = $46,950 → above 250% but below 550%.
926
+ result = determine_eligibility("Yale New Haven Health", 46950, 1, False, "en")
927
+ assert result["eligibility_status"] == "LIKELY ELIGIBLE"
928
+ assert result["discount_level"] == "Partial Discount"
929
+
930
+
931
+ def test_determine_eligibility_not_eligible():
932
+ """Income above sliding scale max should not qualify."""
933
+ # 600% FPL for household of 1 = $93,900 → above Yale's 550%.
934
+ result = determine_eligibility("Yale New Haven Health", 93900, 1, False, "en")
935
+ assert result["eligibility_status"] == "MAY NOT QUALIFY"
936
+
937
+
938
+ def test_determine_eligibility_pa_24_81():
939
+ """SNAP/WIC enrollee under 250% FPL triggers PA 24-81 presumptive eligibility."""
940
+ result = determine_eligibility("Yale New Haven Health", 20000, 1, True, "en")
941
+ assert result["pa_24_81_eligible"] is True
942
+ assert "PA 24-81" in result["discount_level"]
943
+
944
+
945
+ def test_determine_eligibility_unknown_hospital():
946
+ """An unknown hospital name should return an error result, not crash."""
947
+ result = determine_eligibility("Nonexistent Hospital", 30000, 2, False, "en")
948
+ assert result.get("error") is True
949
+ assert "not found" in result["eligibility_status"].lower()
950
+
951
+
952
+ def test_determine_eligibility_insurance_flag():
953
+ """The has_insurance flag should be passed through to the result dict."""
954
+ result = determine_eligibility("Yale New Haven Health", 10000, 1, False, "en", has_insurance=True)
955
+ assert result["has_insurance"] is True
956
+
957
+
958
+ def test_determine_eligibility_spanish():
959
+ """Spanish language should return Spanish status strings."""
960
+ result = determine_eligibility("Yale New Haven Health", 10000, 1, False, "es")
961
+ assert result["eligibility_status"] == "PROBABLEMENTE ELEGIBLE"