import gradio as gr import requests import json import datetime import re from bs4 import BeautifulSoup from typing import Optional, Dict, Any from urllib.parse import quote, urljoin class ShabbatCandleLightingAgent: """ AI Agent for finding Shabbat candle lighting times worldwide using Chabad.org """ def __init__(self): self.base_url = "https://www.chabad.org" self.candle_lighting_url = "https://www.chabad.org/calendar/candlelighting_cdo/aid/6226/jewish/Accurate-Shabbat-Candle-Lighting-Times.htm" def get_user_ip_location(self) -> Optional[Dict[str, Any]]: """ Enhanced IP-based location detection using multiple high-quality services """ try: # Enhanced list of location services with better accuracy services = [ { "url": "https://ipapi.co/json/", "parser": "ipapi_co", "accuracy": "high" }, { "url": "http://ip-api.com/json/?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,isp,query", "parser": "ip_api", "accuracy": "medium" }, { "url": "https://ipgeolocation.abstractapi.com/v1/?api_key=free", "parser": "abstractapi", "accuracy": "high" }, { "url": "https://freegeoip.app/json/", "parser": "freegeoip", "accuracy": "medium" }, { "url": "https://api.ipify.org?format=json", "parser": "ipify_then_geocode", "accuracy": "low" } ] best_result = None all_results = [] for service in services: try: headers = { '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', 'Accept': 'application/json', 'Accept-Language': 'en-US,en;q=0.9' } response = requests.get(service["url"], headers=headers, timeout=8) if response.status_code == 200: data = response.json() parsed_location = self._parse_ip_location_data(data, service["parser"]) if parsed_location and parsed_location.get('city'): parsed_location['accuracy_level'] = service["accuracy"] parsed_location['source'] = service["parser"] all_results.append(parsed_location) print(f"✅ {service['parser']}: {parsed_location['city']}, {parsed_location.get('country', '')}") # Keep the best result (prefer high accuracy sources) if not best_result or service["accuracy"] == "high": best_result = parsed_location except Exception as e: print(f"❌ Service {service['parser']} failed: {e}") continue # Return best result with confidence assessment if best_result: best_result['all_results'] = all_results best_result['confidence'] = self._assess_location_confidence(all_results) print(f"🎯 Best location: {best_result['city']}, {best_result.get('country', '')} (confidence: {best_result['confidence']})") return best_result except Exception as e: print(f"Location detection error: {e}") return None def _parse_ip_location_data(self, data: dict, parser_type: str) -> Optional[Dict[str, Any]]: """Parse location data from different IP geolocation APIs""" try: if parser_type == "ipapi_co": if data.get('city') and not data.get('error'): return { 'city': data.get('city'), 'country': data.get('country_name'), 'region': data.get('region'), 'lat': data.get('latitude'), 'lng': data.get('longitude'), 'timezone': data.get('timezone'), 'isp': data.get('org') } elif parser_type == "ip_api": if data.get('status') == 'success': return { 'city': data.get('city'), 'country': data.get('country'), 'region': data.get('regionName'), 'lat': data.get('lat'), 'lng': data.get('lon'), 'timezone': data.get('timezone'), 'isp': data.get('isp') } elif parser_type == "abstractapi": if data.get('city') and not data.get('error'): return { 'city': data.get('city'), 'country': data.get('country'), 'region': data.get('region'), 'lat': data.get('latitude'), 'lng': data.get('longitude'), 'timezone': data.get('timezone', {}).get('name') if isinstance(data.get('timezone'), dict) else data.get('timezone'), 'isp': data.get('connection', {}).get('isp_name') if isinstance(data.get('connection'), dict) else None } elif parser_type == "freegeoip": if data.get('city'): return { 'city': data.get('city'), 'country': data.get('country_name'), 'region': data.get('region_name'), 'lat': data.get('latitude'), 'lng': data.get('longitude'), 'timezone': data.get('time_zone') } except Exception as e: print(f"Parse error for {parser_type}: {e}") return None def _assess_location_confidence(self, all_results: list) -> str: """Assess confidence level based on multiple results""" if len(all_results) == 0: return "none" elif len(all_results) == 1: return "low" # Check if multiple services agree on the city cities = [result.get('city', '').lower() for result in all_results if result.get('city')] unique_cities = set(cities) if len(unique_cities) == 1 and len(cities) >= 2: return "high" elif len(unique_cities) <= 2 and len(cities) >= 2: return "medium" else: return "low" """ Search for a city on Chabad.org and get the specific URL for candle lighting times """ try: # Clean and format city name city_clean = city_name.strip() search_query = quote(city_clean) headers = { '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', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1' } # Try different search approaches for Chabad.org search_strategies = [ # Direct city page approach f"https://www.chabad.org/calendar/candlelighting_cdo/jewish/{search_query}.htm", # Search with city parameter f"https://www.chabad.org/calendar/candlelighting_cdo/aid/6226/jewish/Accurate-Shabbat-Candle-Lighting-Times.htm?city={search_query}", # Alternative format f"https://www.chabad.org/calendar/candlelighting_cdo/aid/6226/jewish/Candle-Lighting-Times-{search_query}.htm", # General search f"https://www.chabad.org/calendar/candlelighting_cdo/aid/6226/jewish/Accurate-Shabbat-Candle-Lighting-Times.htm" ] for url in search_strategies: try: response = requests.get(url, headers=headers, timeout=15) if response.status_code == 200: # Check if the response contains candle lighting information content_lower = response.text.lower() if any(keyword in content_lower for keyword in ['candle lighting', 'light candles', 'shabbat', 'friday']): return url except Exception as e: print(f"Failed to fetch {url}: {e}") continue # Try POST request to the main search page try: post_data = { 'city': city_clean, 'submit': 'Get Times' } response = requests.post(self.candle_lighting_url, data=post_data, headers=headers, timeout=15) if response.status_code == 200 and 'candle lighting' in response.text.lower(): return response.url except Exception as e: print(f"POST request failed: {e}") except Exception as e: print(f"City search error: {e}") return self.candle_lighting_url # Return main page as fallback def extract_candle_lighting_times(self, html_content: str, city_name: str) -> Dict[str, Any]: """ Extract candle lighting times from Chabad.org HTML content using multiple strategies """ try: soup = BeautifulSoup(html_content, 'html.parser') text = soup.get_text().lower() # Multiple patterns to find candle lighting times candle_patterns = [ r'candle\s+lighting[:\s]*(\d{1,2}:\d{2}(?:\s*[ap]m)?)', r'light\s+candles[:\s]*(\d{1,2}:\d{2}(?:\s*[ap]m)?)', r'friday[:\s]*(\d{1,2}:\d{2}(?:\s*[ap]m)?)', r'(\d{1,2}:\d{2}\s*[ap]m?)(?=.*candle)', r'(\d{1,2}:\d{2}\s*[ap]m?)(?=.*light)', r'(\d{1,2}:\d{2}\s*[ap]m?)(?=.*friday)' ] # Patterns for Havdalah times havdalah_patterns = [ r'havdalah[:\s]*(\d{1,2}:\d{2}(?:\s*[ap]m)?)', r'saturday[:\s]*(\d{1,2}:\d{2}(?:\s*[ap]m)?)', r'(\d{1,2}:\d{2}\s*[ap]m?)(?=.*havdalah)', r'(\d{1,2}:\d{2}\s*[ap]m?)(?=.*saturday)' ] # Find candle lighting time candle_time = None for pattern in candle_patterns: match = re.search(pattern, text, re.IGNORECASE) if match: candle_time = match.group(1) break # Find Havdalah time havdalah_time = None for pattern in havdalah_patterns: match = re.search(pattern, text, re.IGNORECASE) if match: havdalah_time = match.group(1) break # Look for times in common HTML elements if not candle_time or not havdalah_time: # Check for time elements with specific classes or IDs time_selectors = [ '[class*="time"]', '[class*="candle"]', '[class*="light"]', '[id*="time"]', '[id*="candle"]', 'td', 'span', 'div' ] for selector in time_selectors: elements = soup.select(selector) for element in elements: element_text = element.get_text().strip() time_match = re.search(r'\d{1,2}:\d{2}(?:\s*[ap]m)?', element_text, re.IGNORECASE) if time_match: time_found = time_match.group() if not candle_time and any(word in element_text.lower() for word in ['candle', 'light', 'friday']): candle_time = time_found elif not havdalah_time and any(word in element_text.lower() for word in ['havdalah', 'saturday']): havdalah_time = time_found # Calculate current Shabbat dates now = datetime.datetime.now() days_until_friday = (4 - now.weekday()) % 7 if days_until_friday == 0 and now.hour >= 18: # If it's Friday evening, get next Friday days_until_friday = 7 friday = now + datetime.timedelta(days=days_until_friday) saturday = friday + datetime.timedelta(days=1) return { 'city': city_name, 'candle_lighting_time': candle_time, 'havdalah_time': havdalah_time, 'friday_date': friday.strftime('%B %d, %Y'), 'saturday_date': saturday.strftime('%B %d, %Y'), 'found_data': bool(candle_time or havdalah_time), 'search_successful': True } except Exception as e: print(f"Time extraction error: {e}") return { 'city': city_name, 'candle_lighting_time': None, 'havdalah_time': None, 'friday_date': None, 'saturday_date': None, 'found_data': False, 'search_successful': False, 'error': str(e) } def get_backup_times(self, city_name: str) -> Optional[Dict[str, Any]]: """ Try alternative sources for Shabbat times when Chabad.org fails """ try: # Alternative Jewish calendar websites backup_urls = [ f"https://www.hebcal.com/shabbat?city={quote(city_name)}", f"https://www.myzmanim.com/search.aspx?city={quote(city_name)}" ] headers = { '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' } for url in backup_urls: try: response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: times = self.extract_candle_lighting_times(response.text, city_name) if times.get('found_data'): times['source'] = 'backup' return times except Exception as e: print(f"Backup source {url} failed: {e}") continue except Exception as e: print(f"Backup sources error: {e}") return None def get_shabbat_times(self, city_input: str = "", auto_detect: bool = False) -> str: """ Main function to get Shabbat candle lighting times """ try: city_name = city_input.strip() location_info = "" # Auto-detect location using enhanced IP geolocation if requested if auto_detect and not city_name: print("🔍 Attempting to detect your location using IP geolocation...") location = self.get_user_ip_location() if location and location.get('city'): city_name = location['city'] if location.get('country') and location['country'] not in city_name: city_name += f", {location['country']}" # Add location detection info confidence = location.get('confidence', 'unknown') source = location.get('source', 'unknown') if confidence == 'high': status_icon = "✅" status_text = "High confidence" elif confidence == 'medium': status_icon = "⚠️" status_text = "Medium confidence - please verify" else: status_icon = "🔍" status_text = "Low confidence - may be inaccurate" location_info = f""" **🌐 IP-Based Location Detection:** {status_icon} **Detected:** {city_name} **Confidence:** {status_text} **Source:** {source} {('⚠️ *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*')} --- """ if not city_name: return self._format_error_response("❌ Please enter a city name or enable auto-detection to search for Shabbat times.") # Search Chabad.org for the city print(f"🔍 Searching for Shabbat times in: {city_name}") city_url = self.search_city_on_chabad(city_name) result_data = None source = "Chabad.org" if city_url: try: headers = { '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' } response = requests.get(city_url, headers=headers, timeout=15) if response.status_code == 200: result_data = self.extract_candle_lighting_times(response.text, city_name) print(f"📊 Extraction result: {result_data}") except Exception as e: print(f"❌ Chabad.org fetch error: {e}") # Try backup sources if Chabad.org didn't work well if not result_data or not result_data.get('found_data'): print("🔄 Trying backup sources...") backup_data = self.get_backup_times(city_name) if backup_data and backup_data.get('found_data'): result_data = backup_data source = "Alternative Jewish calendar source" # Format and return the response if result_data and result_data.get('found_data'): response = self._format_success_response(result_data, source, city_url) return location_info + response else: response = self._format_not_found_response(city_name, city_url) return location_info + response except Exception as e: print(f"Main function error: {e}") return self._format_error_response(f"❌ An error occurred: {str(e)}") def _format_geolocation_instructions(self) -> str: """Instructions for using browser geolocation""" return f""" # 🕯️ Shabbat Candle Lighting Times ## 📍 Automatic Location Detection To automatically detect your location, we need access to your device's GPS. Please follow these steps: ### 🔒 **Step 1: Enable Location Access** 1. Click the **"🌐 Get My Location"** button below 2. Your browser will ask for permission to access your location 3. Click **"Allow"** in the permission popup 4. Wait for your coordinates to be detected ### 📱 **Step 2: Location will be converted to city** Once we get your GPS coordinates, we'll: - Convert them to the nearest city name - Automatically search for Shabbat times - Display the results for your location ### ⚠️ **Privacy Note:** - Your location is only used to find the nearest city - No location data is stored or shared - You can deny permission and enter your city manually instead ### 🌐 **Location Requirements:** - **HTTPS connection** (secure website) - **GPS-enabled device** (phone, tablet, or laptop with location services) - **Browser permission** for location access --- **Manual Alternative:** Enter your city name in the field above if you prefer not to share your location. **Date:** {datetime.datetime.now().strftime('%B %d, %Y at %I:%M %p')} """ def _format_success_response(self, data: Dict[str, Any], source: str, url: str = None) -> str: """Format successful response with Shabbat times""" candle_time = data.get('candle_lighting_time', 'Not found') havdalah_time = data.get('havdalah_time', 'Not found') response = f""" # 🕯️ Shabbat Candle Lighting Times ## 📍 Location: {data.get('city', 'Unknown')} ### 🕯️ **Candle Lighting (Friday)** **Time:** {candle_time} **Date:** {data.get('friday_date', 'This Friday')} ### ⭐ **Havdalah (Saturday)** **Time:** {havdalah_time} **Date:** {data.get('saturday_date', 'This Saturday')} --- ## 📋 **Important Notes:** - 🕯️ Light Shabbat candles **18 minutes before sunset** on Friday - ⭐ Havdalah is performed after **3 stars appear** on Saturday night - 🌍 Times are specific to your geographic location - ⏰ Always verify times with your local rabbi or Jewish community ## 📚 **Source Information:** **Data from:** {source} **Retrieved:** {datetime.datetime.now().strftime('%B %d, %Y at %I:%M %p')} {f"**URL:** {url}" if url else ""} --- ## 🙏 **Shabbat Shalom!** *May your Shabbat be filled with peace, joy, and spiritual connection.* **🔔 Reminder:** Set an alarm 30 minutes before candle lighting time to prepare properly. """ return response def _format_not_found_response(self, city_name: str, url: str = None) -> str: """Format response when times are not found""" return f""" # 🕯️ Shabbat Candle Lighting Times ## ❌ Times Not Found for {city_name} We searched multiple sources but couldn't find specific Shabbat candle lighting times for **{city_name}**. ### 🔍 **What you can try:** 1. **Check spelling** - Make sure the city name is spelled correctly 2. **Add country** - Try "City, Country" format (e.g., "Paris, France") 3. **Use major city** - Try the nearest major city instead 4. **Alternative spellings** - Try different variations of the city name ### 📍 **General Guidelines for {city_name}:** - 🕯️ **Candle lighting:** Typically 18 minutes before sunset on Friday - ⭐ **Havdalah:** Approximately 25-42 minutes after sunset on Saturday - 🌍 **Location matters:** Times vary significantly by geographic location ### 🌐 **Manual Resources:** - 📱 **Chabad.org:** [Direct search]({url or 'https://www.chabad.org/calendar/candlelighting_cdo/aid/6226/jewish/Accurate-Shabbat-Candle-Lighting-Times.htm'}) - 📱 **Hebcal.com:** Alternative Jewish calendar - 📞 **Local contacts:** Your synagogue or Chabad house ### 📞 **Local Resources:** - Contact your local Chabad house or synagogue - Check with your Jewish community center - Consult your rabbi for precise local customs --- **Retrieved:** {datetime.datetime.now().strftime('%B %d, %Y at %I:%M %p')} """ def _format_error_response(self, error_message: str) -> str: """Format error response""" return f""" # 🕯️ Shabbat Candle Lighting Times ## ⚠️ Error {error_message} ### 🔄 **Please try:** 1. **Refresh** and try again 2. **Check internet connection** 3. **Use a different city name** 4. **Visit Chabad.org directly:** [Candle Lighting Times](https://www.chabad.org/calendar/candlelighting_cdo/aid/6226/jewish/Accurate-Shabbat-Candle-Lighting-Times.htm) ### 📞 **Alternative:** Contact your local synagogue or Chabad house for accurate times. --- **Time:** {datetime.datetime.now().strftime('%B %d, %Y at %I:%M %p')} """ def create_shabbat_interface(): """Create the Gradio interface for the Shabbat Candle Lighting Agent""" agent = ShabbatCandleLightingAgent() with gr.Blocks(title="🕯️ Shabbat Candle Lighting Times", theme=gr.themes.Soft()) as app: gr.Markdown(""" # 🕯️ Shabbat Candle Lighting Times AI Agent **Get accurate Shabbat candle lighting and Havdalah times for any city worldwide** This AI agent searches Chabad.org and other reliable Jewish sources to find precise ritual times for your location. --- """) with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 📍 Location Input") city_input = gr.Textbox( label="🏙️ City Name", placeholder="Enter city name (e.g., 'New York', 'London, UK', 'Tel Aviv, Israel')", info="Enter manually or use automatic location detection below", lines=1 ) # Add IP-based geolocation detection with gr.Row(): detect_ip_btn = gr.Button( "🌐 Auto-Detect (IP Location)", variant="secondary", size="sm" ) search_btn = gr.Button( "🔍 Get Shabbat Times", variant="primary", size="sm" ) gr.Markdown(""" ### 💡 **How to Use:** **🅰️ Manual Entry** (Most Accurate) - Enter your city name above and click "Get Shabbat Times" - Use format: "City, Country" for best results **🅱️ Auto-Detection** (Uses Your IP Location) - Click "Auto-Detect" to automatically find your approximate location - ⚠️ Note: IP location shows your internet provider's area, not exact address - May be off by 10-50 km from your actual location ### 🇮🇱 **Popular Israeli Cities:** Jerusalem, Tel Aviv, Haifa, Beer Sheva, Netanya, Ashdod ### 🌍 **International Examples:** London, UK • Paris, France • New York, USA • Toronto, Canada """) with gr.Column(scale=2): output_area = gr.Markdown( label="📋 Shabbat Times Results", value=""" # 🕯️ Welcome! ### 🎯 **Two Ways to Find Shabbat Times:** **🅰️ Manual Entry** (Most Reliable) 👆 Enter your city name above and click "Get Shabbat Times" **🅱️ Auto-Detection** (Uses IP Geolocation) 👆 Click "Auto-Detect" to find your approximate location via internet provider ### 🇮🇱 **Common Israeli Cities:** Use: Jerusalem, Tel Aviv, Haifa, Beer Sheva, Netanya, Ashdod ### 🌍 **International Cities:** Format: London, UK • Paris, France • New York, USA --- **🕯️ Every Friday evening, Jewish families around the world light candles to welcome Shabbat** **⚠️ Note:** Auto-detection uses IP location (approximate) - for best accuracy, enter city manually """ ) # Example buttons for popular cities gr.Markdown("### 🌍 Quick Search - Popular Cities:") with gr.Row(): # Israeli cities first for the detected user popular_cities = [ "Jerusalem, Israel", "Tel Aviv, Israel", "Haifa, Israel", "Beer Sheva, Israel", "Netanya, Israel", "Ashdod, Israel", # International cities "New York, NY", "London, UK", "Paris, France", "Toronto, Canada", "Los Angeles, CA", "Miami, FL" ] example_btns = [] for i in range(0, len(popular_cities), 4): with gr.Row(): for city in popular_cities[i:i+4]: btn = gr.Button(f"📍 {city}", size="sm") example_btns.append((btn, city)) # Button click handlers search_btn.click( fn=agent.get_shabbat_times, inputs=[city_input, gr.State(False)], # auto_detect = False for manual search outputs=output_area ) # IP-based auto-detection detect_ip_btn.click( fn=agent.get_shabbat_times, inputs=[gr.State(""), gr.State(True)], # empty city, auto_detect = True outputs=output_area ) # Example button handlers for btn, city in example_btns: btn.click( fn=lambda c=city: agent.get_shabbat_times(c, False), outputs=output_area ) # Resources and information with gr.Accordion("📚 Resources & Information", open=False): gr.Markdown(""" ### 🕯️ **About Shabbat Candle Lighting:** **Candle Lighting Time:** - Performed on Friday evening before sunset - Typically 18 minutes before sunset (varies by community) - Marks the beginning of Shabbat - Usually done by the woman of the house **Havdalah Time:** - Performed on Saturday evening after sunset - When three medium-sized stars are visible in the sky - Marks the end of Shabbat - Includes blessings over wine, spices, and candle ### 🌐 **Data Sources:** - **Primary:** [Chabad.org](https://www.chabad.org/calendar/candlelighting_cdo/aid/6226/jewish/Accurate-Shabbat-Candle-Lighting-Times.htm) - **Backup:** Hebcal.com and other Jewish calendar sites - **Method:** Intelligent web scraping with multiple fallback strategies ### ⚠️ **Important Disclaimers:** - Always consult your local rabbi for specific customs - Times may vary based on local Jewish community practices - This tool is for reference only - not a substitute for religious guidance - Some communities light candles at different intervals before sunset ### 🔧 **Technical Features:** - **Browser GPS Geolocation** - Uses your device's actual GPS hardware - **Reverse Geocoding** - Converts GPS coordinates to city names - **Multiple search strategies** for maximum success on Chabad.org - **Intelligent pattern matching** to extract candle lighting times - **Fallback to alternative sources** if needed - **Privacy-focused** - location data never stored or shared """) gr.Markdown(""" --- **🙏 Shabbat Shalom!** | Built with ❤️ for the global Jewish community *For questions about Jewish law or local customs, please consult your rabbi or local Jewish community.* """) return app # Main execution if __name__ == "__main__": app = create_shabbat_interface() # Launch the application app.launch( server_name="0.0.0.0", server_port=7860, share=False, debug=False, show_error=True )