Files changed (1) hide show
  1. app.py +277 -29
app.py CHANGED
@@ -16,7 +16,154 @@ class ShabbatCandleLightingAgent:
16
  self.base_url = "https://www.chabad.org"
17
  self.candle_lighting_url = "https://www.chabad.org/calendar/candlelighting_cdo/aid/6226/jewish/Accurate-Shabbat-Candle-Lighting-Times.htm"
18
 
19
- def search_city_on_chabad(self, city_name: str) -> Optional[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  """
21
  Search for a city on Chabad.org and get the specific URL for candle lighting times
22
  """
@@ -205,15 +352,50 @@ class ShabbatCandleLightingAgent:
205
 
206
  return None
207
 
208
- def get_shabbat_times(self, city_input: str = "") -> str:
209
  """
210
  Main function to get Shabbat candle lighting times
211
  """
212
  try:
213
  city_name = city_input.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
  if not city_name:
216
- return self._format_error_response("❌ Please enter a city name to search for Shabbat times.")
217
 
218
  # Search Chabad.org for the city
219
  print(f"🔍 Searching for Shabbat times in: {city_name}")
@@ -246,14 +428,54 @@ class ShabbatCandleLightingAgent:
246
 
247
  # Format and return the response
248
  if result_data and result_data.get('found_data'):
249
- return self._format_success_response(result_data, source, city_url)
 
250
  else:
251
- return self._format_not_found_response(city_name, city_url)
 
252
 
253
  except Exception as e:
254
  print(f"Main function error: {e}")
255
  return self._format_error_response(f"❌ An error occurred: {str(e)}")
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  def _format_success_response(self, data: Dict[str, Any], source: str, url: str = None) -> str:
258
  """Format successful response with Shabbat times"""
259
  candle_time = data.get('candle_lighting_time', 'Not found')
@@ -376,26 +598,37 @@ def create_shabbat_interface():
376
  city_input = gr.Textbox(
377
  label="🏙️ City Name",
378
  placeholder="Enter city name (e.g., 'New York', 'London, UK', 'Tel Aviv, Israel')",
379
- info="Please enter your city name to get accurate Shabbat times",
380
  lines=1
381
  )
382
 
383
- search_btn = gr.Button(
384
- "🔍 Get Shabbat Times",
385
- variant="primary",
386
- size="lg"
387
- )
 
 
 
 
 
 
 
388
 
389
  gr.Markdown("""
390
- ### 💡 **City Input Tips:**
391
- - Use full city names for better results
392
- - Add country for common city names (e.g., "Paris, France")
393
- - Try nearest major city if yours isn't found
394
- - Use English city names for best search results
 
 
 
 
 
395
 
396
  ### 🇮🇱 **Popular Israeli Cities:**
397
- Jerusalem, Tel Aviv, Haifa, Beer Sheva, Netanya, Ashdod,
398
- Petah Tikva, Ramat Gan, Bnei Brak, Herzliya
399
 
400
  ### 🌍 **International Examples:**
401
  London, UK • Paris, France • New York, USA • Toronto, Canada
@@ -407,19 +640,25 @@ def create_shabbat_interface():
407
  value="""
408
  # 🕯️ Welcome!
409
 
410
- 👆 **Enter your city name above and click 'Get Shabbat Times'**
411
 
412
- Or click one of the popular cities below for a quick search.
 
 
 
 
413
 
414
  ### 🇮🇱 **Common Israeli Cities:**
415
- Use any of these city names: Jerusalem, Tel Aviv, Haifa, Beer Sheva, Netanya, Ashdod
416
 
417
  ### 🌍 **International Cities:**
418
- Include country name: London, UK • Paris, France • New York, USA
419
 
420
  ---
421
 
422
  **🕯️ Every Friday evening, Jewish families around the world light candles to welcome Shabbat**
 
 
423
  """
424
  )
425
 
@@ -451,17 +690,24 @@ Include country name: London, UK • Paris, France • New York, USA
451
  btn = gr.Button(f"📍 {city}", size="sm")
452
  example_btns.append((btn, city))
453
 
454
- # Button click handlers
455
  search_btn.click(
456
  fn=agent.get_shabbat_times,
457
- inputs=[city_input],
 
 
 
 
 
 
 
458
  outputs=output_area
459
  )
460
 
461
  # Example button handlers
462
  for btn, city in example_btns:
463
  btn.click(
464
- fn=lambda c=city: agent.get_shabbat_times(c),
465
  outputs=output_area
466
  )
467
 
@@ -494,10 +740,12 @@ Include country name: London, UK • Paris, France • New York, USA
494
  - Some communities light candles at different intervals before sunset
495
 
496
  ### 🔧 **Technical Features:**
497
- - Multiple search strategies for maximum success on Chabad.org
498
- - Intelligent pattern matching to extract candle lighting times
499
- - Fallback to alternative Jewish calendar sources if needed
500
- - Manual city input for maximum accuracy and reliability
 
 
501
  """)
502
 
503
  gr.Markdown("""
 
16
  self.base_url = "https://www.chabad.org"
17
  self.candle_lighting_url = "https://www.chabad.org/calendar/candlelighting_cdo/aid/6226/jewish/Accurate-Shabbat-Candle-Lighting-Times.htm"
18
 
19
+ def get_user_ip_location(self) -> Optional[Dict[str, Any]]:
20
+ """
21
+ Enhanced IP-based location detection using multiple high-quality services
22
+ """
23
+ try:
24
+ # Enhanced list of location services with better accuracy
25
+ services = [
26
+ {
27
+ "url": "https://ipapi.co/json/",
28
+ "parser": "ipapi_co",
29
+ "accuracy": "high"
30
+ },
31
+ {
32
+ "url": "http://ip-api.com/json/?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,isp,query",
33
+ "parser": "ip_api",
34
+ "accuracy": "medium"
35
+ },
36
+ {
37
+ "url": "https://ipgeolocation.abstractapi.com/v1/?api_key=free",
38
+ "parser": "abstractapi",
39
+ "accuracy": "high"
40
+ },
41
+ {
42
+ "url": "https://freegeoip.app/json/",
43
+ "parser": "freegeoip",
44
+ "accuracy": "medium"
45
+ },
46
+ {
47
+ "url": "https://api.ipify.org?format=json",
48
+ "parser": "ipify_then_geocode",
49
+ "accuracy": "low"
50
+ }
51
+ ]
52
+
53
+ best_result = None
54
+ all_results = []
55
+
56
+ for service in services:
57
+ try:
58
+ headers = {
59
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
60
+ 'Accept': 'application/json',
61
+ 'Accept-Language': 'en-US,en;q=0.9'
62
+ }
63
+
64
+ response = requests.get(service["url"], headers=headers, timeout=8)
65
+ if response.status_code == 200:
66
+ data = response.json()
67
+ parsed_location = self._parse_ip_location_data(data, service["parser"])
68
+
69
+ if parsed_location and parsed_location.get('city'):
70
+ parsed_location['accuracy_level'] = service["accuracy"]
71
+ parsed_location['source'] = service["parser"]
72
+ all_results.append(parsed_location)
73
+ print(f"✅ {service['parser']}: {parsed_location['city']}, {parsed_location.get('country', '')}")
74
+
75
+ # Keep the best result (prefer high accuracy sources)
76
+ if not best_result or service["accuracy"] == "high":
77
+ best_result = parsed_location
78
+
79
+ except Exception as e:
80
+ print(f"❌ Service {service['parser']} failed: {e}")
81
+ continue
82
+
83
+ # Return best result with confidence assessment
84
+ if best_result:
85
+ best_result['all_results'] = all_results
86
+ best_result['confidence'] = self._assess_location_confidence(all_results)
87
+ print(f"🎯 Best location: {best_result['city']}, {best_result.get('country', '')} (confidence: {best_result['confidence']})")
88
+ return best_result
89
+
90
+ except Exception as e:
91
+ print(f"Location detection error: {e}")
92
+
93
+ return None
94
+
95
+ def _parse_ip_location_data(self, data: dict, parser_type: str) -> Optional[Dict[str, Any]]:
96
+ """Parse location data from different IP geolocation APIs"""
97
+ try:
98
+ if parser_type == "ipapi_co":
99
+ if data.get('city') and not data.get('error'):
100
+ return {
101
+ 'city': data.get('city'),
102
+ 'country': data.get('country_name'),
103
+ 'region': data.get('region'),
104
+ 'lat': data.get('latitude'),
105
+ 'lng': data.get('longitude'),
106
+ 'timezone': data.get('timezone'),
107
+ 'isp': data.get('org')
108
+ }
109
+
110
+ elif parser_type == "ip_api":
111
+ if data.get('status') == 'success':
112
+ return {
113
+ 'city': data.get('city'),
114
+ 'country': data.get('country'),
115
+ 'region': data.get('regionName'),
116
+ 'lat': data.get('lat'),
117
+ 'lng': data.get('lon'),
118
+ 'timezone': data.get('timezone'),
119
+ 'isp': data.get('isp')
120
+ }
121
+
122
+ elif parser_type == "abstractapi":
123
+ if data.get('city') and not data.get('error'):
124
+ return {
125
+ 'city': data.get('city'),
126
+ 'country': data.get('country'),
127
+ 'region': data.get('region'),
128
+ 'lat': data.get('latitude'),
129
+ 'lng': data.get('longitude'),
130
+ 'timezone': data.get('timezone', {}).get('name') if isinstance(data.get('timezone'), dict) else data.get('timezone'),
131
+ 'isp': data.get('connection', {}).get('isp_name') if isinstance(data.get('connection'), dict) else None
132
+ }
133
+
134
+ elif parser_type == "freegeoip":
135
+ if data.get('city'):
136
+ return {
137
+ 'city': data.get('city'),
138
+ 'country': data.get('country_name'),
139
+ 'region': data.get('region_name'),
140
+ 'lat': data.get('latitude'),
141
+ 'lng': data.get('longitude'),
142
+ 'timezone': data.get('time_zone')
143
+ }
144
+
145
+ except Exception as e:
146
+ print(f"Parse error for {parser_type}: {e}")
147
+
148
+ return None
149
+
150
+ def _assess_location_confidence(self, all_results: list) -> str:
151
+ """Assess confidence level based on multiple results"""
152
+ if len(all_results) == 0:
153
+ return "none"
154
+ elif len(all_results) == 1:
155
+ return "low"
156
+
157
+ # Check if multiple services agree on the city
158
+ cities = [result.get('city', '').lower() for result in all_results if result.get('city')]
159
+ unique_cities = set(cities)
160
+
161
+ if len(unique_cities) == 1 and len(cities) >= 2:
162
+ return "high"
163
+ elif len(unique_cities) <= 2 and len(cities) >= 2:
164
+ return "medium"
165
+ else:
166
+ return "low"
167
  """
168
  Search for a city on Chabad.org and get the specific URL for candle lighting times
169
  """
 
352
 
353
  return None
354
 
355
+ def get_shabbat_times(self, city_input: str = "", auto_detect: bool = False) -> str:
356
  """
357
  Main function to get Shabbat candle lighting times
358
  """
359
  try:
360
  city_name = city_input.strip()
361
+ location_info = ""
362
+
363
+ # Auto-detect location using enhanced IP geolocation if requested
364
+ if auto_detect and not city_name:
365
+ print("🔍 Attempting to detect your location using IP geolocation...")
366
+ location = self.get_user_ip_location()
367
+ if location and location.get('city'):
368
+ city_name = location['city']
369
+ if location.get('country') and location['country'] not in city_name:
370
+ city_name += f", {location['country']}"
371
+
372
+ # Add location detection info
373
+ confidence = location.get('confidence', 'unknown')
374
+ source = location.get('source', 'unknown')
375
+
376
+ if confidence == 'high':
377
+ status_icon = "✅"
378
+ status_text = "High confidence"
379
+ elif confidence == 'medium':
380
+ status_icon = "⚠️"
381
+ status_text = "Medium confidence - please verify"
382
+ else:
383
+ status_icon = "🔍"
384
+ status_text = "Low confidence - may be inaccurate"
385
+
386
+ location_info = f"""
387
+ **🌐 IP-Based Location Detection:**
388
+ {status_icon} **Detected:** {city_name}
389
+ **Confidence:** {status_text}
390
+ **Source:** {source}
391
+
392
+ {('⚠️ *Note: IP location shows your internet provider\'s location, not exact address. For most accurate results, enter your city manually.*' if confidence != 'high' else '✅ *Multiple services confirmed this location*')}
393
+
394
+ ---
395
+ """
396
 
397
  if not city_name:
398
+ return self._format_error_response("❌ Please enter a city name or enable auto-detection to search for Shabbat times.")
399
 
400
  # Search Chabad.org for the city
401
  print(f"🔍 Searching for Shabbat times in: {city_name}")
 
428
 
429
  # Format and return the response
430
  if result_data and result_data.get('found_data'):
431
+ response = self._format_success_response(result_data, source, city_url)
432
+ return location_info + response
433
  else:
434
+ response = self._format_not_found_response(city_name, city_url)
435
+ return location_info + response
436
 
437
  except Exception as e:
438
  print(f"Main function error: {e}")
439
  return self._format_error_response(f"❌ An error occurred: {str(e)}")
440
 
441
+ def _format_geolocation_instructions(self) -> str:
442
+ """Instructions for using browser geolocation"""
443
+ return f"""
444
+ # 🕯️ Shabbat Candle Lighting Times
445
+
446
+ ## 📍 Automatic Location Detection
447
+
448
+ To automatically detect your location, we need access to your device's GPS. Please follow these steps:
449
+
450
+ ### 🔒 **Step 1: Enable Location Access**
451
+ 1. Click the **"🌐 Get My Location"** button below
452
+ 2. Your browser will ask for permission to access your location
453
+ 3. Click **"Allow"** in the permission popup
454
+ 4. Wait for your coordinates to be detected
455
+
456
+ ### 📱 **Step 2: Location will be converted to city**
457
+ Once we get your GPS coordinates, we'll:
458
+ - Convert them to the nearest city name
459
+ - Automatically search for Shabbat times
460
+ - Display the results for your location
461
+
462
+ ### ⚠️ **Privacy Note:**
463
+ - Your location is only used to find the nearest city
464
+ - No location data is stored or shared
465
+ - You can deny permission and enter your city manually instead
466
+
467
+ ### 🌐 **Location Requirements:**
468
+ - **HTTPS connection** (secure website)
469
+ - **GPS-enabled device** (phone, tablet, or laptop with location services)
470
+ - **Browser permission** for location access
471
+
472
+ ---
473
+
474
+ **Manual Alternative:** Enter your city name in the field above if you prefer not to share your location.
475
+
476
+ **Date:** {datetime.datetime.now().strftime('%B %d, %Y at %I:%M %p')}
477
+ """
478
+
479
  def _format_success_response(self, data: Dict[str, Any], source: str, url: str = None) -> str:
480
  """Format successful response with Shabbat times"""
481
  candle_time = data.get('candle_lighting_time', 'Not found')
 
598
  city_input = gr.Textbox(
599
  label="🏙️ City Name",
600
  placeholder="Enter city name (e.g., 'New York', 'London, UK', 'Tel Aviv, Israel')",
601
+ info="Enter manually or use automatic location detection below",
602
  lines=1
603
  )
604
 
605
+ # Add IP-based geolocation detection
606
+ with gr.Row():
607
+ detect_ip_btn = gr.Button(
608
+ "🌐 Auto-Detect (IP Location)",
609
+ variant="secondary",
610
+ size="sm"
611
+ )
612
+ search_btn = gr.Button(
613
+ "🔍 Get Shabbat Times",
614
+ variant="primary",
615
+ size="sm"
616
+ )
617
 
618
  gr.Markdown("""
619
+ ### 💡 **How to Use:**
620
+
621
+ **🅰️ Manual Entry** (Most Accurate)
622
+ - Enter your city name above and click "Get Shabbat Times"
623
+ - Use format: "City, Country" for best results
624
+
625
+ **🅱️ Auto-Detection** (Uses Your IP Location)
626
+ - Click "Auto-Detect" to automatically find your approximate location
627
+ - ⚠️ Note: IP location shows your internet provider's area, not exact address
628
+ - May be off by 10-50 km from your actual location
629
 
630
  ### 🇮🇱 **Popular Israeli Cities:**
631
+ Jerusalem, Tel Aviv, Haifa, Beer Sheva, Netanya, Ashdod
 
632
 
633
  ### 🌍 **International Examples:**
634
  London, UK • Paris, France • New York, USA • Toronto, Canada
 
640
  value="""
641
  # 🕯️ Welcome!
642
 
643
+ ### 🎯 **Two Ways to Find Shabbat Times:**
644
 
645
+ **🅰️ Manual Entry** (Most Reliable)
646
+ 👆 Enter your city name above and click "Get Shabbat Times"
647
+
648
+ **🅱️ Auto-Detection** (Uses IP Geolocation)
649
+ 👆 Click "Auto-Detect" to find your approximate location via internet provider
650
 
651
  ### 🇮🇱 **Common Israeli Cities:**
652
+ Use: Jerusalem, Tel Aviv, Haifa, Beer Sheva, Netanya, Ashdod
653
 
654
  ### 🌍 **International Cities:**
655
+ Format: London, UK • Paris, France • New York, USA
656
 
657
  ---
658
 
659
  **🕯️ Every Friday evening, Jewish families around the world light candles to welcome Shabbat**
660
+
661
+ **⚠️ Note:** Auto-detection uses IP location (approximate) - for best accuracy, enter city manually
662
  """
663
  )
664
 
 
690
  btn = gr.Button(f"📍 {city}", size="sm")
691
  example_btns.append((btn, city))
692
 
693
+ # Button click handlers
694
  search_btn.click(
695
  fn=agent.get_shabbat_times,
696
+ inputs=[city_input, gr.State(False)], # auto_detect = False for manual search
697
+ outputs=output_area
698
+ )
699
+
700
+ # IP-based auto-detection
701
+ detect_ip_btn.click(
702
+ fn=agent.get_shabbat_times,
703
+ inputs=[gr.State(""), gr.State(True)], # empty city, auto_detect = True
704
  outputs=output_area
705
  )
706
 
707
  # Example button handlers
708
  for btn, city in example_btns:
709
  btn.click(
710
+ fn=lambda c=city: agent.get_shabbat_times(c, False),
711
  outputs=output_area
712
  )
713
 
 
740
  - Some communities light candles at different intervals before sunset
741
 
742
  ### 🔧 **Technical Features:**
743
+ - **Browser GPS Geolocation** - Uses your device's actual GPS hardware
744
+ - **Reverse Geocoding** - Converts GPS coordinates to city names
745
+ - **Multiple search strategies** for maximum success on Chabad.org
746
+ - **Intelligent pattern matching** to extract candle lighting times
747
+ - **Fallback to alternative sources** if needed
748
+ - **Privacy-focused** - location data never stored or shared
749
  """)
750
 
751
  gr.Markdown("""