Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- app (1).py +687 -0
- requirements (3).txt +3 -0
app (1).py
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Connecticut Hospital Financial Assistance Screener v4
|
| 4 |
+
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
|
| 11 |
+
from geopy.distance import geodesic
|
| 12 |
+
import time
|
| 13 |
+
|
| 14 |
+
# Load API key from environment variable (set in HF Spaces secrets)
|
| 15 |
+
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
|
| 16 |
+
client = OpenAI(api_key=OPENAI_API_KEY)
|
| 17 |
+
DEMO_PASSWORD = "ct2026"
|
| 18 |
+
|
| 19 |
+
geolocator = Nominatim(user_agent="ct_hospital_screener")
|
| 20 |
+
|
| 21 |
+
HOSPITALS = {
|
| 22 |
+
"Yale New Haven Health": {
|
| 23 |
+
"free_care_threshold": 250,
|
| 24 |
+
"sliding_scale_max": 550,
|
| 25 |
+
"asset_limit": None,
|
| 26 |
+
"contact": "877-442-2455",
|
| 27 |
+
"special_notes": "No asset limit",
|
| 28 |
+
"location": (41.3083, -72.9279),
|
| 29 |
+
"city": "New Haven",
|
| 30 |
+
"fap_url": "https://www.ynhhs.org/patient-care/billing-insurance/FAP-guidelines"
|
| 31 |
+
},
|
| 32 |
+
"Hartford HealthCare": {
|
| 33 |
+
"free_care_threshold": 250,
|
| 34 |
+
"sliding_scale_max": 550,
|
| 35 |
+
"asset_limit": "Liquid asset review",
|
| 36 |
+
"contact": "877-442-2455",
|
| 37 |
+
"special_notes": "Asset review required",
|
| 38 |
+
"location": (41.7658, -72.6734),
|
| 39 |
+
"city": "Hartford",
|
| 40 |
+
"fap_url": "https://hartfordhealthcare.org/patients-visitors/patients/billing-insurance/financial-assistance"
|
| 41 |
+
},
|
| 42 |
+
"Trinity Health of New England": {
|
| 43 |
+
"free_care_threshold": 200,
|
| 44 |
+
"sliding_scale_max": 400,
|
| 45 |
+
"asset_limit": None,
|
| 46 |
+
"contact": "860-714-1657",
|
| 47 |
+
"special_notes": "Medicaid exhaustion required",
|
| 48 |
+
"location": (41.7658, -72.6734),
|
| 49 |
+
"city": "Hartford",
|
| 50 |
+
"fap_url": "https://www.trinityhealthofne.org/for-patients/billing-and-financial-resources"
|
| 51 |
+
},
|
| 52 |
+
"Stamford Health": {
|
| 53 |
+
"free_care_threshold": 250,
|
| 54 |
+
"sliding_scale_max": 400,
|
| 55 |
+
"asset_limit": None,
|
| 56 |
+
"contact": "203-276-7572",
|
| 57 |
+
"special_notes": "No carve-outs",
|
| 58 |
+
"location": (41.0534, -73.5387),
|
| 59 |
+
"city": "Stamford",
|
| 60 |
+
"fap_url": "https://www.stamfordhealth.org/patients/fap/"
|
| 61 |
+
},
|
| 62 |
+
"Bristol Hospital": {
|
| 63 |
+
"free_care_threshold": 250,
|
| 64 |
+
"sliding_scale_max": 400,
|
| 65 |
+
"asset_limit": "$7,500 / $15,000",
|
| 66 |
+
"contact": "860-585-3035",
|
| 67 |
+
"special_notes": "Strict asset limits",
|
| 68 |
+
"location": (41.6718, -72.9493),
|
| 69 |
+
"city": "Bristol",
|
| 70 |
+
"fap_url": "https://www.bristolhealth.org/patients-and-visitors/cost-care-and-financial-assistance"
|
| 71 |
+
},
|
| 72 |
+
"Day Kimball Healthcare": {
|
| 73 |
+
"free_care_threshold": 200,
|
| 74 |
+
"sliding_scale_max": 400,
|
| 75 |
+
"asset_limit": "$100,000",
|
| 76 |
+
"contact": "860-928-7024",
|
| 77 |
+
"special_notes": "240-day window",
|
| 78 |
+
"location": (41.8528, -71.9004),
|
| 79 |
+
"city": "Putnam",
|
| 80 |
+
"fap_url": "https://www.daykimball.org/resources/financial-services/"
|
| 81 |
+
},
|
| 82 |
+
"UConn Health": {
|
| 83 |
+
"free_care_threshold": 400,
|
| 84 |
+
"sliding_scale_max": 400,
|
| 85 |
+
"asset_limit": None,
|
| 86 |
+
"contact": "860-679-4120",
|
| 87 |
+
"special_notes": "Most generous in CT",
|
| 88 |
+
"location": (41.7295, -72.7935),
|
| 89 |
+
"city": "Farmington",
|
| 90 |
+
"fap_url": "https://www.uconnhealth.org/patients-visitors/patient-resources/billing-costs-insurance"
|
| 91 |
+
},
|
| 92 |
+
"Nuvance Health": {
|
| 93 |
+
"free_care_threshold": 250,
|
| 94 |
+
"sliding_scale_max": 400,
|
| 95 |
+
"asset_limit": "Not specified",
|
| 96 |
+
"contact": "845-788-9012",
|
| 97 |
+
"special_notes": "Multi-location",
|
| 98 |
+
"location": (41.3948, -73.4540),
|
| 99 |
+
"city": "Danbury",
|
| 100 |
+
"fap_url": "https://www.nuvancehealth.org/patients-and-visitors/billing-and-insurance/patient-financial-assistance"
|
| 101 |
+
},
|
| 102 |
+
"Middlesex Health": {
|
| 103 |
+
"free_care_threshold": 200,
|
| 104 |
+
"sliding_scale_max": 400,
|
| 105 |
+
"asset_limit": "Not specified",
|
| 106 |
+
"contact": "860-358-6150",
|
| 107 |
+
"special_notes": "Limited docs",
|
| 108 |
+
"location": (41.5623, -72.6506),
|
| 109 |
+
"city": "Middletown",
|
| 110 |
+
"fap_url": "https://middlesexhealth.org/patients-and-visitors/financial-assistance-services"
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
# 2025 Federal Poverty Level Guidelines
|
| 115 |
+
# Source: https://aspe.hhs.gov/topics/poverty-economic-mobility/poverty-guidelines
|
| 116 |
+
# Updated: January 2025. Check annually for new guidelines.
|
| 117 |
+
FPL_2025 = {1: 15650, 2: 21150, 3: 26650, 4: 32150, 5: 37650, 6: 43150, 7: 48650, 8: 54150}
|
| 118 |
+
|
| 119 |
+
TRANSLATIONS = {
|
| 120 |
+
"en": {
|
| 121 |
+
"title": "Connecticut Hospital Financial Assistance Screener",
|
| 122 |
+
"beta_tag": "BETA",
|
| 123 |
+
"subtitle": "Check your eligibility for financial assistance",
|
| 124 |
+
"help_choose": "Help me find a hospital",
|
| 125 |
+
"know_hospital": "I know my hospital",
|
| 126 |
+
"zip_label": "Enter your ZIP code",
|
| 127 |
+
"search_button": "Search Hospitals",
|
| 128 |
+
"select_hospital": "Select Hospital",
|
| 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",
|
| 135 |
+
"continue_button": "Continue",
|
| 136 |
+
"back_button": "Back",
|
| 137 |
+
"miles_away": "miles away",
|
| 138 |
+
"free_care": "Free care up to",
|
| 139 |
+
"sliding_scale": "Sliding scale up to",
|
| 140 |
+
"status": "Status",
|
| 141 |
+
"contact": "Contact",
|
| 142 |
+
"sources": "Source Documents",
|
| 143 |
+
"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.",
|
| 144 |
+
"eligible": "LIKELY ELIGIBLE",
|
| 145 |
+
"not_eligible": "MAY NOT QUALIFY"
|
| 146 |
+
},
|
| 147 |
+
"es": {
|
| 148 |
+
"title": "Evaluador de Asistencia Financiera Hospitalaria de Connecticut",
|
| 149 |
+
"beta_tag": "BETA",
|
| 150 |
+
"subtitle": "Verifique su elegibilidad para asistencia financiera",
|
| 151 |
+
"help_choose": "Ayúdame a encontrar un hospital",
|
| 152 |
+
"know_hospital": "Conozco mi hospital",
|
| 153 |
+
"zip_label": "Ingrese su código postal",
|
| 154 |
+
"search_button": "Buscar Hospitales",
|
| 155 |
+
"select_hospital": "Seleccione Hospital",
|
| 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",
|
| 162 |
+
"continue_button": "Continuar",
|
| 163 |
+
"back_button": "Atrás",
|
| 164 |
+
"miles_away": "millas de distancia",
|
| 165 |
+
"free_care": "Atención gratuita hasta",
|
| 166 |
+
"sliding_scale": "Escala móvil hasta",
|
| 167 |
+
"status": "Estado",
|
| 168 |
+
"contact": "Contacto",
|
| 169 |
+
"sources": "Documentos Fuente",
|
| 170 |
+
"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.",
|
| 171 |
+
"eligible": "PROBABLEMENTE ELEGIBLE",
|
| 172 |
+
"not_eligible": "PUEDE NO CALIFICAR"
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
def calculate_fpl_percentage(income, household_size):
|
| 177 |
+
if household_size <= 8:
|
| 178 |
+
fpl_base = FPL_2025[household_size]
|
| 179 |
+
else:
|
| 180 |
+
fpl_base = FPL_2025[8] + (5500 * (household_size - 8))
|
| 181 |
+
return round((income / fpl_base) * 100, 1)
|
| 182 |
+
|
| 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", []
|
| 189 |
+
|
| 190 |
+
user_coords = (location.latitude, location.longitude)
|
| 191 |
+
distances = []
|
| 192 |
+
for name, data in HOSPITALS.items():
|
| 193 |
+
dist = geodesic(user_coords, data["location"]).miles
|
| 194 |
+
distances.append((name, dist, data))
|
| 195 |
+
|
| 196 |
+
distances.sort(key=lambda x: x[1])
|
| 197 |
+
nearest = distances[:3]
|
| 198 |
+
|
| 199 |
+
choices = []
|
| 200 |
+
cards = ""
|
| 201 |
+
for name, dist, data in nearest:
|
| 202 |
+
choice_label = f"{name} ({data['city']}, {dist:.1f} mi)"
|
| 203 |
+
choices.append(choice_label)
|
| 204 |
+
cards += f"""### {name}
|
| 205 |
+
📍 {data['city']} — {dist:.1f} {t['miles_away']}
|
| 206 |
+
💰 {t['free_care']} {data['free_care_threshold']}% FPL | {t['sliding_scale']} {data['sliding_scale_max']}% FPL
|
| 207 |
+
📞 {data['contact']}
|
| 208 |
+
|
| 209 |
+
---
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
return cards, choices
|
| 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 |
+
|
| 224 |
+
result = {
|
| 225 |
+
"hospital": hospital_name,
|
| 226 |
+
"income": income,
|
| 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"],
|
| 233 |
+
"asset_limit": hospital["asset_limit"],
|
| 234 |
+
"fap_url": hospital["fap_url"],
|
| 235 |
+
"lang": lang
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
if has_snap_wic and fpl_percentage <= 250:
|
| 239 |
+
result["pa_24_81_eligible"] = True
|
| 240 |
+
result["eligibility_status"] = t["eligible"]
|
| 241 |
+
result["discount_level"] = "Presumptive Eligibility (PA 24-81)" if lang == "en" else "Elegibilidad Presuntiva (PA 24-81)"
|
| 242 |
+
elif fpl_percentage <= hospital["free_care_threshold"]:
|
| 243 |
+
result["eligibility_status"] = t["eligible"]
|
| 244 |
+
result["discount_level"] = "100% Discount" if lang == "en" else "100% de Descuento"
|
| 245 |
+
elif fpl_percentage <= hospital["sliding_scale_max"]:
|
| 246 |
+
result["eligibility_status"] = t["eligible"]
|
| 247 |
+
result["discount_level"] = "Partial Discount" if lang == "en" else "Descuento Parcial"
|
| 248 |
+
else:
|
| 249 |
+
result["eligibility_status"] = t["not_eligible"]
|
| 250 |
+
result["discount_level"] = "Income exceeds thresholds" if lang == "en" else "Ingresos superan umbrales"
|
| 251 |
+
|
| 252 |
+
return result
|
| 253 |
+
|
| 254 |
+
def generate_explanation_streaming(data):
|
| 255 |
+
"""
|
| 256 |
+
Streaming version - yields text as it's generated
|
| 257 |
+
Uses confidence-based language tiers
|
| 258 |
+
"""
|
| 259 |
+
lang = data["lang"]
|
| 260 |
+
t = TRANSLATIONS[lang]
|
| 261 |
+
|
| 262 |
+
# Determine confidence level for language calibration
|
| 263 |
+
is_pa_24_81 = data["pa_24_81_eligible"]
|
| 264 |
+
is_eligible = data["eligibility_status"] == t["eligible"]
|
| 265 |
+
has_asset_limit = data["asset_limit"] is not None
|
| 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
|
| 275 |
+
elif is_eligible:
|
| 276 |
+
confidence = "strong" # Generally eligible
|
| 277 |
+
else:
|
| 278 |
+
confidence = "negative" # Not eligible
|
| 279 |
+
|
| 280 |
+
# Build the system message with appropriate language guidance
|
| 281 |
+
if lang == "es":
|
| 282 |
+
if confidence == "statutory":
|
| 283 |
+
lang_guide = """LENGUAJE DEFINITIVO - Este es un derecho estatutario:
|
| 284 |
+
- Usa "califica" o "es elegible" con confianza
|
| 285 |
+
- Enfatiza que es una ley estatal (PA 24-81)
|
| 286 |
+
- No uses "puede" - esto es definitivo"""
|
| 287 |
+
elif confidence == "strong":
|
| 288 |
+
lang_guide = """LENGUAJE CONFIADO:
|
| 289 |
+
- Usa "parece ser elegible" o "aparentemente califica"
|
| 290 |
+
- Sé positivo pero incluye "basado en la información proporcionada"
|
| 291 |
+
- Anima a confirmar con el hospital"""
|
| 292 |
+
elif confidence == "moderate":
|
| 293 |
+
lang_guide = """LENGUAJE CAUTELOSO:
|
| 294 |
+
- Usa "puede calificar" o "podría ser elegible"
|
| 295 |
+
- IMPORTANTE: Menciona los requisitos adicionales (límites de activos, etc.)
|
| 296 |
+
- Enfatiza la necesidad de contactar al hospital"""
|
| 297 |
+
else:
|
| 298 |
+
lang_guide = """LENGUAJE AMABLE PERO DIRECTO:
|
| 299 |
+
- Sé claro: "sus ingresos superan los umbrales"
|
| 300 |
+
- Sugiere alternativas (otros hospitales, otros programas)
|
| 301 |
+
- Mantén un tono esperanzador"""
|
| 302 |
+
|
| 303 |
+
system_msg = f"""Eres un defensor de la salud que ayuda a explicar la elegibilidad para asistencia financiera.
|
| 304 |
+
|
| 305 |
+
{lang_guide}
|
| 306 |
+
|
| 307 |
+
Explica en 2 párrafos claros y cálidos. Máximo 150 palabras."""
|
| 308 |
+
else:
|
| 309 |
+
if confidence == "statutory":
|
| 310 |
+
lang_guide = """DEFINITIVE LANGUAGE - This is a statutory right:
|
| 311 |
+
- Use "you qualify" or "you are eligible" confidently
|
| 312 |
+
- Emphasize this is Connecticut state law (PA 24-81)
|
| 313 |
+
- Don't use "may" - this is definitive"""
|
| 314 |
+
elif confidence == "strong":
|
| 315 |
+
lang_guide = """CONFIDENT LANGUAGE:
|
| 316 |
+
- Use "you appear to be eligible" or "you likely qualify"
|
| 317 |
+
- Be positive but include "based on the information provided"
|
| 318 |
+
- Encourage hospital confirmation"""
|
| 319 |
+
elif confidence == "moderate":
|
| 320 |
+
lang_guide = """CAUTIOUS LANGUAGE:
|
| 321 |
+
- Use "you may qualify" or "you could be eligible"
|
| 322 |
+
- IMPORTANT: Mention additional requirements (asset limits, etc.)
|
| 323 |
+
- Emphasize need to contact hospital"""
|
| 324 |
+
else:
|
| 325 |
+
lang_guide = """GENTLE BUT DIRECT LANGUAGE:
|
| 326 |
+
- Be clear: "your income exceeds the eligibility thresholds"
|
| 327 |
+
- Suggest alternatives (other hospitals, other programs)
|
| 328 |
+
- Keep tone hopeful"""
|
| 329 |
+
|
| 330 |
+
system_msg = f"""You are a healthcare advocate helping explain financial assistance eligibility.
|
| 331 |
+
|
| 332 |
+
{lang_guide}
|
| 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']}
|
| 343 |
+
Asset Limit: {data['asset_limit'] or 'None'}
|
| 344 |
+
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}
|
| 352 |
+
],
|
| 353 |
+
temperature=0.7,
|
| 354 |
+
max_tokens=300,
|
| 355 |
+
stream=True
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
collected_text = ""
|
| 359 |
+
for chunk in stream:
|
| 360 |
+
if chunk.choices[0].delta.content:
|
| 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 |
+
"""
|
| 369 |
+
Generator that yields progressive updates to the results
|
| 370 |
+
"""
|
| 371 |
+
t = TRANSLATIONS[lang]
|
| 372 |
+
|
| 373 |
+
# Start with header (instant)
|
| 374 |
+
header = f"""## {data['hospital']}
|
| 375 |
+
|
| 376 |
+
### {t['status']}: {data['eligibility_status']}
|
| 377 |
+
|
| 378 |
+
💵 ${data['income']:,.0f} ({data['fpl_percentage']}% FPL)
|
| 379 |
+
|
| 380 |
+
---
|
| 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 |
+
---
|
| 395 |
+
|
| 396 |
+
📞 **{t['contact']}:** {data['contact']}
|
| 397 |
+
|
| 398 |
+
---
|
| 399 |
+
|
| 400 |
+
### {t['sources']}
|
| 401 |
+
|
| 402 |
+
📄 [**{data['hospital']} Financial Assistance Policy**]({data['fap_url']})
|
| 403 |
+
|
| 404 |
+
📄 [**Connecticut Public Act 24-81**](https://www.cga.ct.gov/2024/ba/pdf/2024HB-05320-R000149-BA.pdf)
|
| 405 |
+
|
| 406 |
+
📄 [**2025 Federal Poverty Level Guidelines**](https://www.healthcare.gov/glossary/federal-poverty-level-fpl/)
|
| 407 |
+
|
| 408 |
+
📄 [**CT Office of the Healthcare Advocate**](https://portal.ct.gov/oha)
|
| 409 |
+
|
| 410 |
+
---
|
| 411 |
+
|
| 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 = """
|
| 420 |
+
/* CT.gov inspired styling */
|
| 421 |
+
.gradio-container {
|
| 422 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
| 423 |
+
max-width: 1200px !important;
|
| 424 |
+
margin: auto !important;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
/* CT Blue header */
|
| 428 |
+
h1 {
|
| 429 |
+
color: #0d6efd !important;
|
| 430 |
+
font-weight: 600 !important;
|
| 431 |
+
border-bottom: 3px solid #0d6efd;
|
| 432 |
+
padding-bottom: 1rem;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
/* Buttons - CT Blue */
|
| 436 |
+
.gr-button-primary {
|
| 437 |
+
background: #0d6efd !important;
|
| 438 |
+
border: none !important;
|
| 439 |
+
color: white !important;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.gr-button-primary:hover {
|
| 443 |
+
background: #0a58ca !important;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.gr-button-secondary {
|
| 447 |
+
background: #6c757d !important;
|
| 448 |
+
border: none !important;
|
| 449 |
+
color: white !important;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
/* Form inputs */
|
| 453 |
+
.gr-box, .gr-input, .gr-dropdown {
|
| 454 |
+
border: 1px solid #ced4da !important;
|
| 455 |
+
border-radius: 0.375rem !important;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
/* Card-like sections */
|
| 459 |
+
.gr-group {
|
| 460 |
+
background: #f8f9fa !important;
|
| 461 |
+
padding: 1.5rem !important;
|
| 462 |
+
border-radius: 0.5rem !important;
|
| 463 |
+
border: 1px solid #dee2e6 !important;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
/* Labels */
|
| 467 |
+
label {
|
| 468 |
+
color: #212529 !important;
|
| 469 |
+
font-weight: 500 !important;
|
| 470 |
+
margin-bottom: 0.5rem !important;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
/* Results cards */
|
| 474 |
+
.markdown-text h2 {
|
| 475 |
+
color: #0d6efd !important;
|
| 476 |
+
border-bottom: 2px solid #e9ecef;
|
| 477 |
+
padding-bottom: 0.5rem;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.markdown-text h3 {
|
| 481 |
+
color: #495057 !important;
|
| 482 |
+
font-size: 1.1rem;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
/* Source links section */
|
| 486 |
+
.markdown-text a {
|
| 487 |
+
color: #0d6efd !important;
|
| 488 |
+
text-decoration: none;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.markdown-text a:hover {
|
| 492 |
+
text-decoration: underline;
|
| 493 |
+
}
|
| 494 |
+
"""
|
| 495 |
+
|
| 496 |
+
def create_interface():
|
| 497 |
+
with gr.Blocks(
|
| 498 |
+
theme=gr.themes.Default(primary_hue="blue", neutral_hue="slate"),
|
| 499 |
+
css=custom_css
|
| 500 |
+
) as demo:
|
| 501 |
+
|
| 502 |
+
lang_state = gr.State("en")
|
| 503 |
+
selected_hospital = gr.State("")
|
| 504 |
+
|
| 505 |
+
# Header with Beta tag
|
| 506 |
+
with gr.Row():
|
| 507 |
+
with gr.Column(scale=4):
|
| 508 |
+
gr.Markdown("# 🏥 Connecticut Hospital Financial Assistance <span style='background-color: #ffc107; color: #664d03; padding: 4px 12px; border-radius: 4px; font-size: 0.75em; font-weight: bold; margin-left: 0.5rem;'>BETA</span>")
|
| 509 |
+
with gr.Column(scale=1):
|
| 510 |
+
lang_toggle = gr.Radio(
|
| 511 |
+
choices=["English", "Español"],
|
| 512 |
+
value="English",
|
| 513 |
+
label="Language",
|
| 514 |
+
container=False
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
# Step 1: Choose path
|
| 518 |
+
step1 = gr.Group(visible=True)
|
| 519 |
+
with step1:
|
| 520 |
+
gr.Markdown("### How would you like to start?")
|
| 521 |
+
with gr.Row():
|
| 522 |
+
help_btn = gr.Button("🔍 Help me find a hospital", variant="primary", size="lg")
|
| 523 |
+
know_btn = gr.Button("🏥 I know my hospital", variant="secondary", size="lg")
|
| 524 |
+
|
| 525 |
+
# Step 2a: Find hospital by ZIP
|
| 526 |
+
step2a = gr.Group(visible=False)
|
| 527 |
+
with step2a:
|
| 528 |
+
gr.Markdown("### Enter your ZIP code")
|
| 529 |
+
zip_input = gr.Textbox(label="ZIP Code", placeholder="e.g., 06511", max_lines=1)
|
| 530 |
+
search_btn = gr.Button("Search", variant="primary")
|
| 531 |
+
|
| 532 |
+
nearby_cards = gr.Markdown()
|
| 533 |
+
hospital_radio = gr.Radio(label="Select a hospital", choices=[], visible=False)
|
| 534 |
+
continue_btn_a = gr.Button("Continue", variant="primary", visible=False)
|
| 535 |
+
back_btn_a = gr.Button("← Back", size="sm")
|
| 536 |
+
|
| 537 |
+
# Step 2b: Select known hospital
|
| 538 |
+
step2b = gr.Group(visible=False)
|
| 539 |
+
with step2b:
|
| 540 |
+
gr.Markdown("### Select your hospital")
|
| 541 |
+
hospital_dropdown = gr.Dropdown(
|
| 542 |
+
choices=list(HOSPITALS.keys()),
|
| 543 |
+
label="Hospital"
|
| 544 |
+
)
|
| 545 |
+
continue_btn_b = gr.Button("Continue", variant="primary")
|
| 546 |
+
back_btn_b = gr.Button("← Back", size="sm")
|
| 547 |
+
|
| 548 |
+
# Step 3: Eligibility form
|
| 549 |
+
step3 = gr.Group(visible=False)
|
| 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")
|
| 559 |
+
|
| 560 |
+
# Step 4: Results (with streaming)
|
| 561 |
+
step4 = gr.Group(visible=False)
|
| 562 |
+
with step4:
|
| 563 |
+
results_output = gr.Markdown()
|
| 564 |
+
restart_btn = gr.Button("Check Another Hospital", variant="secondary")
|
| 565 |
+
|
| 566 |
+
# Navigation functions
|
| 567 |
+
def show_find_path():
|
| 568 |
+
return [
|
| 569 |
+
gr.update(visible=False),
|
| 570 |
+
gr.update(visible=True),
|
| 571 |
+
gr.update(visible=False),
|
| 572 |
+
gr.update(visible=False),
|
| 573 |
+
gr.update(visible=False)
|
| 574 |
+
]
|
| 575 |
+
|
| 576 |
+
def show_know_path():
|
| 577 |
+
return [
|
| 578 |
+
gr.update(visible=False),
|
| 579 |
+
gr.update(visible=False),
|
| 580 |
+
gr.update(visible=True),
|
| 581 |
+
gr.update(visible=False),
|
| 582 |
+
gr.update(visible=False)
|
| 583 |
+
]
|
| 584 |
+
|
| 585 |
+
def back_to_start():
|
| 586 |
+
return [
|
| 587 |
+
gr.update(visible=True),
|
| 588 |
+
gr.update(visible=False),
|
| 589 |
+
gr.update(visible=False),
|
| 590 |
+
gr.update(visible=False),
|
| 591 |
+
gr.update(visible=False)
|
| 592 |
+
]
|
| 593 |
+
|
| 594 |
+
def search_hospitals_wrapper(zip_code, lang):
|
| 595 |
+
cards, choices = find_nearby_hospitals(zip_code, lang)
|
| 596 |
+
if choices:
|
| 597 |
+
return cards, gr.update(choices=choices, visible=True, value=choices[0]), gr.update(visible=True)
|
| 598 |
+
return cards, gr.update(visible=False), gr.update(visible=False)
|
| 599 |
+
|
| 600 |
+
def continue_from_search(hospital_choice, lang):
|
| 601 |
+
t = TRANSLATIONS[lang]
|
| 602 |
+
hospital_name = hospital_choice.split(" (")[0] if "(" in hospital_choice else hospital_choice
|
| 603 |
+
return [
|
| 604 |
+
hospital_name,
|
| 605 |
+
f"### Selected: {hospital_name}",
|
| 606 |
+
gr.update(visible=False),
|
| 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 |
+
|
| 613 |
+
def continue_from_dropdown(hospital_name, lang):
|
| 614 |
+
t = TRANSLATIONS[lang]
|
| 615 |
+
return [
|
| 616 |
+
hospital_name,
|
| 617 |
+
f"### Selected: {hospital_name}",
|
| 618 |
+
gr.update(visible=False),
|
| 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 |
+
"",
|
| 632 |
+
gr.update(visible=False),
|
| 633 |
+
gr.update(visible=False),
|
| 634 |
+
gr.update(visible=False),
|
| 635 |
+
gr.update(visible=True)
|
| 636 |
+
]
|
| 637 |
+
|
| 638 |
+
for partial_result in format_results_streaming(data, lang):
|
| 639 |
+
yield [
|
| 640 |
+
partial_result,
|
| 641 |
+
gr.update(),
|
| 642 |
+
gr.update(),
|
| 643 |
+
gr.update(),
|
| 644 |
+
gr.update()
|
| 645 |
+
]
|
| 646 |
+
|
| 647 |
+
def update_lang(choice):
|
| 648 |
+
return "es" if choice == "Español" else "en"
|
| 649 |
+
|
| 650 |
+
# Wire up events
|
| 651 |
+
lang_toggle.change(fn=update_lang, inputs=[lang_toggle], outputs=[lang_state])
|
| 652 |
+
|
| 653 |
+
help_btn.click(fn=show_find_path, outputs=[step1, step2a, step2b, step3, step4])
|
| 654 |
+
know_btn.click(fn=show_know_path, outputs=[step1, step2a, step2b, step3, step4])
|
| 655 |
+
|
| 656 |
+
back_btn_a.click(fn=back_to_start, outputs=[step1, step2a, step2b, step3, step4])
|
| 657 |
+
back_btn_b.click(fn=back_to_start, outputs=[step1, step2a, step2b, step3, step4])
|
| 658 |
+
restart_btn.click(fn=back_to_start, outputs=[step1, step2a, step2b, step3, step4])
|
| 659 |
+
|
| 660 |
+
search_btn.click(
|
| 661 |
+
fn=search_hospitals_wrapper,
|
| 662 |
+
inputs=[zip_input, lang_state],
|
| 663 |
+
outputs=[nearby_cards, hospital_radio, continue_btn_a]
|
| 664 |
+
)
|
| 665 |
+
|
| 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
|
| 685 |
+
|
| 686 |
+
demo = create_interface()
|
| 687 |
+
demo.launch(auth=("oha", DEMO_PASSWORD))
|
requirements (3).txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0
|
| 2 |
+
openai>=1.0
|
| 3 |
+
geopy>=2.4
|