Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| from dotenv import load_dotenv | |
| import os | |
| import requests | |
| import json | |
| import threading | |
| from groq import Groq | |
| import folium | |
| from streamlit_folium import folium_static | |
| import pandas as pd | |
| import time | |
| from datetime import datetime | |
| import matplotlib.pyplot as plt | |
| import io | |
| import os | |
| from dotenv import load_dotenv | |
| from groq import Groq | |
| import time | |
| import logging | |
| from typing import Callable, Any, List, Dict | |
| # Set up logging | |
| logging.basicConfig(level=logging.INFO, | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger("GroqAPIManager") | |
| # Load environment variables | |
| load_dotenv() | |
| # App configuration | |
| st.set_page_config( | |
| page_title="Tourist Spot Search & Assistant", | |
| page_icon="🏝️", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| class GroqKeyManager: | |
| def __init__(self): | |
| """Initialize with multiple API keys from environment variables""" | |
| # Get the main API key | |
| self.api_keys = [] | |
| # Add the main API key | |
| main_key = os.getenv("GROQ_API_KEY") | |
| if main_key: | |
| self.api_keys.append(main_key) | |
| # Add additional numbered keys | |
| i = 1 | |
| while True: | |
| key_name = f"GROQ_API_KEY_{i}" | |
| key = os.getenv(key_name) | |
| if key: | |
| self.api_keys.append(key) | |
| i += 1 | |
| else: | |
| break | |
| if not self.api_keys: | |
| raise ValueError("No Groq API keys found in environment variables") | |
| self.current_index = 0 | |
| self.clients = {key: Groq(api_key=key) for key in self.api_keys} | |
| logger.info(f"Initialized with {len(self.api_keys)} API keys") | |
| def get_current_key(self): | |
| """Get the currently active API key""" | |
| return self.api_keys[self.current_index] | |
| def get_current_client(self): | |
| """Get the client for the currently active API key""" | |
| return self.clients[self.get_current_key()] | |
| def rotate_key(self): | |
| """Rotate to the next API key""" | |
| old_index = self.current_index | |
| self.current_index = (self.current_index + 1) % len(self.api_keys) | |
| logger.info(f"Rotated from key index {old_index} to {self.current_index}") | |
| return self.get_current_key() | |
| def execute_with_fallback(self, operation: Callable, messages: List[Dict[str, str]], max_retries=3): | |
| """ | |
| Execute an operation with automatic key rotation on rate limit errors | |
| Args: | |
| operation: A callable that takes a client and messages and returns a response | |
| messages: The messages to pass to the operation | |
| max_retries: Maximum number of retries across all keys | |
| Returns: | |
| The response from the operation | |
| """ | |
| attempts = 0 | |
| retry_delay = 1 # Start with 1 second retry delay | |
| while attempts < max_retries * len(self.api_keys): | |
| current_client = self.get_current_client() | |
| try: | |
| logger.info(f"Attempting request with key index {self.current_index}") | |
| return operation(current_client, messages) | |
| except Exception as e: | |
| attempts += 1 | |
| error_message = str(e).lower() | |
| # Check for rate limit related errors | |
| if any(phrase in error_message for phrase in ["rate limit", "quota exceeded", "too many requests", "429"]): | |
| logger.warning(f"Rate limit hit with key index {self.current_index}: {e}") | |
| # If we've tried all keys, implement exponential backoff | |
| if attempts % len(self.api_keys) == 0: | |
| wait_time = min(retry_delay * 2, 60) # Cap at 60 seconds | |
| logger.info(f"All keys have been tried. Waiting {wait_time}s before retrying...") | |
| time.sleep(wait_time) | |
| retry_delay *= 2 | |
| # Rotate to next key | |
| self.rotate_key() | |
| else: | |
| # For non-rate-limit errors, just log and re-raise | |
| logger.error(f"Non-rate-limit error occurred: {e}") | |
| raise | |
| # If we've exhausted all retries | |
| raise Exception(f"Failed after {attempts} attempts across {len(self.api_keys)} API keys") | |
| key_manager = GroqKeyManager() | |
| # Load environment variables | |
| load_dotenv() | |
| # Initialize Groq client | |
| API_KEY = os.getenv("GROQ_API_KEY") | |
| client = Groq(api_key=API_KEY) | |
| # Custom CSS for better styling | |
| st.markdown(""" | |
| <style> | |
| .main { | |
| background-color: #1e1e1e; | |
| color: white; | |
| } | |
| .stTabs [data-baseweb="tab-list"] { | |
| gap: 24px; | |
| } | |
| .stTabs [data-baseweb="tab"] { | |
| height: 50px; | |
| white-space: pre-wrap; | |
| background-color: #2d2d2d; | |
| border-radius: 4px 4px 0px 0px; | |
| gap: 1px; | |
| padding-top: 10px; | |
| padding-bottom: 10px; | |
| } | |
| .stTabs [aria-selected="true"] { | |
| background-color: #3d3d3d; | |
| } | |
| .stTextInput>div>div>input { | |
| background-color: #3d3d3d; | |
| color: white; | |
| } | |
| .stTextArea>div>div>textarea { | |
| background-color: #3d3d3d; | |
| color: white; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| def clean_json_string_(json_str): | |
| """ | |
| Clean and fix common JSON string issues from LLM outputs. | |
| Args: | |
| json_str (str): The potentially problematic JSON string | |
| Returns: | |
| str: A cleaned JSON string that should parse correctly | |
| """ | |
| # Remove any markdown code blocks | |
| json_str = re.sub(r'```(?:json)?\s*|\s*```', '', json_str) | |
| # Ensure all strings are properly terminated | |
| # This is a basic fix - more complex cases might need additional handling | |
| fixed_str = '' | |
| in_string = False | |
| escape_next = False | |
| for char in json_str: | |
| fixed_str += char | |
| if escape_next: | |
| escape_next = False | |
| continue | |
| if char == '\\': | |
| escape_next = True | |
| elif char == '"' and not escape_next: | |
| in_string = not in_string | |
| # If we end with an unterminated string, add the missing quote | |
| if in_string: | |
| fixed_str += '"' | |
| # Check for unbalanced braces and brackets | |
| open_braces = fixed_str.count('{') | |
| close_braces = fixed_str.count('}') | |
| open_brackets = fixed_str.count('[') | |
| close_brackets = fixed_str.count(']') | |
| # Add missing closing braces/brackets if needed | |
| fixed_str += '}' * (open_braces - close_braces) | |
| fixed_str += ']' * (open_brackets - close_brackets) | |
| return fixed_str | |
| def extract_and_parse_json(json_str): | |
| """ | |
| Extract and parse JSON from a string that might contain other text. | |
| Args: | |
| json_str (str): String containing JSON data | |
| Returns: | |
| dict: Parsed JSON data | |
| """ | |
| try: | |
| # First attempt: try to parse as-is | |
| return json.loads(json_str) | |
| except json.JSONDecodeError: | |
| # Second attempt: try to clean the string | |
| cleaned_json = clean_json_string_(json_str) | |
| try: | |
| return json.loads(cleaned_json) | |
| except json.JSONDecodeError: | |
| # Third attempt: try to use regex to extract JSON object | |
| match = re.search(r'({.*})', json_str, re.DOTALL) | |
| if match: | |
| try: | |
| extracted_json = match.group(1) | |
| cleaned_extracted = clean_json_string(extracted_json) | |
| return json.loads(cleaned_extracted) | |
| except json.JSONDecodeError: | |
| raise | |
| else: | |
| raise | |
| def clean_json_string(json_str): | |
| """ | |
| Clean JSON string by removing comments and ensuring proper formatting. | |
| Args: | |
| json_str (str): The JSON string that may contain comments | |
| Returns: | |
| str: A cleaned JSON string ready for parsing | |
| """ | |
| # Remove single-line comments (both // and anything after it on the same line) | |
| clean_str = re.sub(r'//.*?$', '', json_str, flags=re.MULTILINE) | |
| # Remove trailing commas before closing brackets or braces | |
| clean_str = re.sub(r',(\s*[\]}])', r'\1', clean_str) | |
| return clean_str | |
| def extract_json_from_markdown(markdown_text): | |
| """ | |
| Extract JSON from markdown code blocks and clean it. | |
| Args: | |
| markdown_text (str): Markdown text that may contain JSON in code blocks | |
| Returns: | |
| str: Extracted and cleaned JSON string | |
| """ | |
| # Extract JSON from code blocks if present | |
| json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', markdown_text) | |
| if json_match: | |
| json_str = json_match.group(1) | |
| else: | |
| # If no code block, try to find JSON-like content | |
| json_str = markdown_text | |
| # Clean the JSON string | |
| clean_json = clean_json_string(json_str) | |
| return clean_json | |
| # Sidebar with app info | |
| with st.sidebar: | |
| st.title("Tourist Assistant") | |
| st.write("An app to help you explore tourist spots and plan your trips in Bangladesh.") | |
| # About section | |
| with st.expander("About", expanded=False): | |
| st.write(""" | |
| This app provides three main features: | |
| 1. **Tourist Spot Search**: Find tourist attractions in any location | |
| 2. **Intelligent Chat**: Ask questions about travel in natural language | |
| 3. **Trip Planner**: Get personalized trip itineraries based on your preferences | |
| """) | |
| # Credits | |
| st.markdown("---") | |
| st.caption("Powered AI Project Solution") | |
| # Main app with tabs | |
| tab1, tab2, tab3 = st.tabs(["🔍 Tourist Spot Search", "💬 Intelligent Chat", "🗺️ Trip Planner"]) | |
| # ------------- Tourist Spot Search Tab ------------- | |
| with tab1: | |
| st.header("Find Tourist Spots") | |
| # Initialize session state variables if they don't exist | |
| if 'tourist_spots' not in st.session_state: | |
| st.session_state.tourist_spots = [] | |
| if 'search_location' not in st.session_state: | |
| st.session_state.search_location = "" | |
| if 'location_coords' not in st.session_state: | |
| st.session_state.location_coords = None | |
| if 'search_radius' not in st.session_state: | |
| st.session_state.search_radius = 5 # Default to 5km | |
| if 'country' not in st.session_state: | |
| st.session_state.country = "" | |
| if 'spot_descriptions' not in st.session_state: | |
| st.session_state.spot_descriptions = {} # Cache for generated descriptions | |
| # Search bar with enhanced radius control | |
| col1, col2, col3 = st.columns([3, 1, 1]) | |
| with col1: | |
| location = st.text_input("Enter Location:", placeholder="e.g., Dhaka, Cox's Bazar", | |
| value=st.session_state.search_location) | |
| with col2: | |
| radius = st.number_input("Radius (km):", min_value=1, max_value=150, | |
| value=st.session_state.search_radius, step=5, | |
| help="Larger radius (>50km) may take longer to load") | |
| with col3: | |
| search_button = st.button("🔍 Search", use_container_width=True) | |
| # Function to get weather data from different providers | |
| def get_weather_data(lat, lon): | |
| """ | |
| Try multiple weather APIs and use the first one that works. | |
| Returns dict with temperature, description, and forecast data. | |
| """ | |
| try: | |
| # First try Open-Meteo (completely free) | |
| url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t=temperature_2m,weather_code&hourly=precipitation,weather_code&forecast_days=3" | |
| response = requests.get(url, timeout=5) | |
| if response.status_code == 200: | |
| data = response.json() | |
| # Convert weather code to description using Open-Meteo mapping | |
| weather_codes = { | |
| 0: "clear sky", 1: "mainly clear", 2: "partly cloudy", 3: "overcast", | |
| 45: "fog", 48: "depositing rime fog", 51: "light drizzle", 53: "moderate drizzle", | |
| 55: "dense drizzle", 56: "light freezing drizzle", 57: "dense freezing drizzle", | |
| 61: "slight rain", 63: "moderate rain", 65: "heavy rain", | |
| 66: "light freezing rain", 67: "heavy freezing rain", 71: "slight snow fall", | |
| 73: "moderate snow fall", 75: "heavy snow fall", 77: "snow grains", | |
| 80: "slight rain showers", 81: "moderate rain showers", 82: "violent rain showers", | |
| 85: "slight snow showers", 86: "heavy snow showers", 95: "thunderstorm", | |
| 96: "thunderstorm with slight hail", 99: "thunderstorm with heavy hail" | |
| } | |
| weather_code = data['current']['weather_code'] | |
| # Process hourly precipitation forecast for 48 hours (2 days) | |
| next_48h_precip = data['hourly']['precipitation'][:48] | |
| hourly_weather_codes = data['hourly']['weather_code'][:48] | |
| # Get daily breakdown | |
| day1_precip = next_48h_precip[:24] | |
| day2_precip = next_48h_precip[24:48] | |
| day1_weather_codes = hourly_weather_codes[:24] | |
| day2_weather_codes = hourly_weather_codes[24:48] | |
| # Check for rain in forecast | |
| rain_weather_codes = [51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82, 95, 96, 99] | |
| # Check for rain chance on first day | |
| day1_rain_chance = any(p > 0.1 for p in day1_precip) | |
| day1_rain_hours = sum(1 for p in day1_precip if p > 0.1) | |
| day1_has_rain_codes = any(code in rain_weather_codes for code in day1_weather_codes) | |
| # Check for rain chance on second day | |
| day2_rain_chance = any(p > 0.1 for p in day2_precip) | |
| day2_rain_hours = sum(1 for p in day2_precip if p > 0.1) | |
| day2_has_rain_codes = any(code in rain_weather_codes for code in day2_weather_codes) | |
| forecast_data = { | |
| 'next_48h': { | |
| 'rain_chance': any(p > 0.1 for p in next_48h_precip) or any(code in rain_weather_codes for code in hourly_weather_codes), | |
| 'rain_hours': sum(1 for p in next_48h_precip if p > 0.1), | |
| 'max_precipitation': max(next_48h_precip) if next_48h_precip else 0, | |
| }, | |
| 'day1': { | |
| 'rain_chance': day1_rain_chance or day1_has_rain_codes, | |
| 'rain_hours': day1_rain_hours, | |
| 'max_precipitation': max(day1_precip) if day1_precip else 0, | |
| }, | |
| 'day2': { | |
| 'rain_chance': day2_rain_chance or day2_has_rain_codes, | |
| 'rain_hours': day2_rain_hours, | |
| 'max_precipitation': max(day2_precip) if day2_precip else 0, | |
| } | |
| } | |
| return { | |
| 'temperature': data['current']['temperature_2m'], | |
| 'description': weather_codes.get(weather_code, "unknown weather"), | |
| 'forecast': forecast_data | |
| } | |
| except Exception as e: | |
| st.warning(f"Weather data unavailable: {str(e)}") | |
| return None | |
| # Helper function to calculate distance between two points | |
| def calculate_distance(lat1, lon1, lat2, lon2): | |
| """Calculate distance in kilometers between two points using Haversine formula""" | |
| from math import radians, sin, cos, sqrt, atan2 | |
| # Convert coordinates to radians | |
| lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) | |
| # Haversine formula | |
| dlon = lon2 - lon1 | |
| dlat = lat2 - lat1 | |
| a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 | |
| c = 2 * atan2(sqrt(a), sqrt(1-a)) | |
| r = 6371 # Radius of Earth in kilometers | |
| return r * c | |
| def get_seasonal_tip(location): | |
| month = datetime.now().month | |
| if month in [11, 12, 1, 2]: # Winter | |
| return f"Visit during {location}'s cool, dry season (November-February) for pleasant weather." | |
| elif month in [3, 4, 5]: # Summer | |
| return f"Prepare for {location}'s hot season (March-May); early mornings are best for outdoor activities." | |
| else: # Monsoon | |
| return f"Bring rain gear for {location}'s monsoon season (June-October); indoor attractions may be ideal." | |
| # Process search | |
| if search_button and location: | |
| # Update session state | |
| st.session_state.search_location = location | |
| st.session_state.search_radius = radius | |
| # Convert radius to meters for the API | |
| radius_meters = radius * 1000 | |
| # Display loading message with progress | |
| with st.spinner(f"Searching for tourist spots within {radius}km of {location}..."): | |
| try: | |
| # Get coordinates for the location using Nominatim | |
| nominatim_url = f"https://nominatim.openstreetmap.org/search?q={location}&format=json" | |
| response = requests.get(nominatim_url, headers={'User-Agent': 'TouristApp/1.0'}, timeout=10) | |
| data = response.json() | |
| if not data: | |
| st.error(f"No location found for '{location}'.") | |
| else: | |
| lat = data[0]['lat'] | |
| lon = data[0]['lon'] | |
| # Store location coordinates and additional location data in session state | |
| st.session_state.location_coords = (float(lat), float(lon)) | |
| st.session_state.country = data[0].get('display_name', '').split(',')[-1].strip() | |
| # Use a more efficient Overpass query strategy | |
| overpass_url = "https://overpass-api.de/api/interpreter" | |
| # Adjust timeout based on radius size | |
| timeout_seconds = min(60, 20 + (radius // 10) * 5) | |
| # Primary query: Tourist attractions and hotels | |
| # Add waterfall to the primary query | |
| query1 = f""" | |
| [out:json][timeout:{timeout_seconds}]; | |
| ( | |
| node["tourism"="attraction"](around:{radius_meters},{lat},{lon}); | |
| way["tourism"="attraction"](around:{radius_meters},{lat},{lon}); | |
| relation["tourism"="attraction"](around:{radius_meters},{lat},{lon}); | |
| node["tourism"="resort"](around:{radius_meters},{lat},{lon}); | |
| way["tourism"="resort"](around:{radius_meters},{lat},{lon}); | |
| node["tourism"="hotel"](around:{radius_meters},{lat},{lon}); | |
| way["tourism"="hotel"](around:{radius_meters},{lat},{lon}); | |
| node["tourism"="viewpoint"](around:{radius_meters},{lat},{lon}); | |
| way["tourism"="viewpoint"](around:{radius_meters},{lat},{lon}); | |
| node["natural"="beach"](around:{radius_meters},{lat},{lon}); | |
| way["natural"="beach"](around:{radius_meters},{lat},{lon}); | |
| node["natural"="waterfall"](around:{radius_meters},{lat},{lon}); | |
| way["natural"="waterfall"](around:{radius_meters},{lat},{lon}); | |
| node["natural"="forest"](around:{radius_meters},{lat},{lon}); | |
| way["natural"="forest"](around:{radius_meters},{lat},{lon}); | |
| relation["natural"="forest"](around:{radius_meters},{lat},{lon}); | |
| node["landuse"="forest"](around:{radius_meters},{lat},{lon}); | |
| way["landuse"="forest"](around:{radius_meters},{lat},{lon}); | |
| relation["landuse"="forest"](around:{radius_meters},{lat},{lon}); | |
| ); | |
| out body center; | |
| """ | |
| # Send the first query with increased timeout for larger radius | |
| response = requests.post(overpass_url, data={"data": query1}, timeout=timeout_seconds) | |
| data = response.json() | |
| # Process results | |
| tourist_spots = [] | |
| # Process results from the first query | |
| for element in data.get('elements', []): | |
| if 'tags' in element: | |
| # Skip elements without names | |
| if 'name' not in element['tags']: | |
| continue | |
| name = element['tags'].get('name') | |
| # Get coordinates | |
| if element['type'] == 'node': | |
| lat_val = element.get('lat', 0) | |
| lon_val = element.get('lon', 0) | |
| else: | |
| # For ways and relations, use center point | |
| lat_val = element.get('center', {}).get('lat', float(lat)) | |
| lon_val = element.get('center', {}).get('lon', float(lon)) | |
| # Determine category with more specific classification | |
| category = "other" | |
| if 'tourism' in element['tags']: | |
| category = element['tags']['tourism'] | |
| elif 'natural' in element['tags']: | |
| category = element['tags']['natural'] | |
| elif 'amenity' in element['tags']: | |
| category = element['tags']['amenity'] | |
| # Extract location details for better descriptions | |
| location_details = { | |
| 'street': element['tags'].get('addr:street', ''), | |
| 'city': element['tags'].get('addr:city', ''), | |
| 'state': element['tags'].get('addr:state', ''), | |
| 'country': element['tags'].get('addr:country', st.session_state.country) | |
| } | |
| # Add to our list with enhanced metadata | |
| tourist_spots.append({ | |
| 'id': element.get('id', ''), | |
| 'type': element.get('type', ''), | |
| 'tags': element['tags'], | |
| 'lat': lat_val, | |
| 'lon': lon_val, | |
| 'category': category, | |
| 'location_details': location_details | |
| }) | |
| # If we got few results, try a secondary query with expanded categories | |
| if len(tourist_spots) < 10: | |
| query2 = f""" | |
| [out:json][timeout:{timeout_seconds}]; | |
| ( | |
| node["historic"](around:{radius_meters},{lat},{lon}); | |
| way["historic"](around:{radius_meters},{lat},{lon}); | |
| relation["historic"](around:{radius_meters},{lat},{lon}); | |
| node["leisure"="park"](around:{radius_meters},{lat},{lon}); | |
| way["leisure"="park"](around:{radius_meters},{lat},{lon}); | |
| node["leisure"="water_park"](around:{radius_meters},{lat},{lon}); | |
| way["leisure"="water_park"](around:{radius_meters},{lat},{lon}); | |
| node["tourism"="museum"](around:{radius_meters},{lat},{lon}); | |
| way["tourism"="museum"](around:{radius_meters},{lat},{lon}); | |
| node["tourism"="gallery"](around:{radius_meters},{lat},{lon}); | |
| way["tourism"="gallery"](around:{radius_meters},{lat},{lon}); | |
| node["amenity"="restaurant"](around:{radius_meters},{lat},{lon})[cuisine]; | |
| way["amenity"="restaurant"](around:{radius_meters},{lat},{lon})[cuisine]; | |
| node["leisure"="nature_reserve"](around:{radius_meters},{lat},{lon}); | |
| way["leisure"="nature_reserve"](around:{radius_meters},{lat},{lon}); | |
| node["boundary"="protected_area"](around:{radius_meters},{lat},{lon}); | |
| way["boundary"="protected_area"](around:{radius_meters},{lat},{lon}); | |
| ); | |
| out body center; | |
| """ | |
| try: | |
| response2 = requests.post(overpass_url, data={"data": query2}, timeout=timeout_seconds) | |
| data2 = response2.json() | |
| # Process additional results | |
| for element in data2.get('elements', []): | |
| if 'tags' in element and 'name' in element['tags']: | |
| name = element['tags'].get('name') | |
| # Get coordinates | |
| if element['type'] == 'node': | |
| lat_val = element.get('lat', 0) | |
| lon_val = element.get('lon', 0) | |
| else: | |
| lat_val = element.get('center', {}).get('lat', float(lat)) | |
| lon_val = element.get('center', {}).get('lon', float(lon)) | |
| # More specific category classification | |
| if 'tourism' in element['tags']: | |
| category = element['tags']['tourism'] | |
| elif 'historic' in element['tags']: | |
| category = f"historic_{element['tags']['historic']}" | |
| elif 'leisure' in element['tags']: | |
| category = f"leisure_{element['tags']['leisure']}" | |
| elif 'amenity' in element['tags']: | |
| if element['tags'].get('amenity') == 'restaurant' and 'cuisine' in element['tags']: | |
| category = f"restaurant_{element['tags'].get('cuisine')}" | |
| else: | |
| category = element['tags']['amenity'] | |
| else: | |
| category = "other" | |
| # Extract location details | |
| location_details = { | |
| 'street': element['tags'].get('addr:street', ''), | |
| 'city': element['tags'].get('addr:city', ''), | |
| 'state': element['tags'].get('addr:state', ''), | |
| 'country': element['tags'].get('addr:country', st.session_state.country) | |
| } | |
| # Add to our list | |
| tourist_spots.append({ | |
| 'id': element.get('id', ''), | |
| 'type': element.get('type', ''), | |
| 'tags': element['tags'], | |
| 'lat': lat_val, | |
| 'lon': lon_val, | |
| 'category': category, | |
| 'location_details': location_details | |
| }) | |
| except Exception as e: | |
| st.warning(f"Limited results available. Some attraction types couldn't be loaded.") | |
| # Filter out duplicates by ID | |
| unique_spots = {} | |
| for spot in tourist_spots: | |
| if spot['id'] not in unique_spots: | |
| unique_spots[spot['id']] = spot | |
| tourist_spots = list(unique_spots.values()) | |
| # Sort spots by name | |
| tourist_spots.sort(key=lambda x: x['tags'].get('name', '')) | |
| # Store in session state | |
| st.session_state.tourist_spots = tourist_spots | |
| # If still no results, use a more general approach | |
| if not tourist_spots: | |
| st.warning(f"No specific tourist spots found. Trying more general search...") | |
| # Use a very simple query just to get something | |
| general_query = f""" | |
| [out:json][timeout:{timeout_seconds}]; | |
| ( | |
| node["tourism"](around:{radius_meters},{lat},{lon}); | |
| way["tourism"](around:{radius_meters},{lat},{lon}); | |
| node["leisure"](around:{radius_meters},{lat},{lon}); | |
| way["leisure"](around:{radius_meters},{lat},{lon}); | |
| ); | |
| out body center; | |
| """ | |
| try: | |
| response3 = requests.post(overpass_url, data={"data": general_query}, timeout=timeout_seconds) | |
| data3 = response3.json() | |
| # Process general results | |
| general_spots = [] | |
| for element in data3.get('elements', []): | |
| if 'tags' in element and 'name' in element['tags']: | |
| name = element['tags'].get('name') | |
| # Get coordinates | |
| if element['type'] == 'node': | |
| lat_val = element.get('lat', 0) | |
| lon_val = element.get('lon', 0) | |
| else: | |
| lat_val = element.get('center', {}).get('lat', float(lat)) | |
| lon_val = element.get('center', {}).get('lon', float(lon)) | |
| # Determine the most specific category available | |
| if 'tourism' in element['tags']: | |
| category = element['tags']['tourism'] | |
| elif 'leisure' in element['tags']: | |
| category = element['tags']['leisure'] | |
| else: | |
| category = 'general' | |
| # Extract location details | |
| location_details = { | |
| 'street': element['tags'].get('addr:street', ''), | |
| 'city': element['tags'].get('addr:city', ''), | |
| 'state': element['tags'].get('addr:state', ''), | |
| 'country': element['tags'].get('addr:country', st.session_state.country) | |
| } | |
| general_spots.append({ | |
| 'id': element.get('id', ''), | |
| 'type': element.get('type', ''), | |
| 'tags': element['tags'], | |
| 'lat': lat_val, | |
| 'lon': lon_val, | |
| 'category': category, | |
| 'location_details': location_details | |
| }) | |
| st.session_state.tourist_spots = general_spots | |
| except Exception as e: | |
| st.error(f"Could not find any tourist spots in this area. Try a different location or increase the radius.") | |
| except requests.exceptions.Timeout: | |
| st.error("Search timed out. Try a smaller radius or a different location.") | |
| except Exception as e: | |
| st.error(f"Error searching for tourist spots: {str(e)}") | |
| # Display results if tourist spots exist in session state | |
| if st.session_state.tourist_spots: | |
| # Display results count with better formatting | |
| if len(st.session_state.tourist_spots) > 0: | |
| st.success(f"Found {len(st.session_state.tourist_spots)} tourist spots within {st.session_state.search_radius}km of {st.session_state.search_location}.") | |
| # Create filter options for better user experience | |
| spot_categories = sorted(list(set([spot['category'] for spot in st.session_state.tourist_spots]))) | |
| selected_categories = st.multiselect("Filter by category:", | |
| ["All"] + spot_categories, | |
| default=["All"]) | |
| # Apply filters | |
| filtered_spots = st.session_state.tourist_spots | |
| if selected_categories and "All" not in selected_categories: | |
| filtered_spots = [spot for spot in st.session_state.tourist_spots | |
| if spot['category'] in selected_categories] | |
| # Display map with all spots if location coordinates exist | |
| if st.session_state.location_coords: | |
| m = folium.Map(location=st.session_state.location_coords, zoom_start=12) | |
| # Add search center marker | |
| folium.Marker( | |
| location=st.session_state.location_coords, | |
| popup="Search Center", | |
| tooltip="Search Center", | |
| icon=folium.Icon(color='green', icon='home') | |
| ).add_to(m) | |
| # Add search radius circle | |
| folium.Circle( | |
| location=st.session_state.location_coords, | |
| radius=st.session_state.search_radius * 1000, # Convert km to m | |
| color='green', | |
| fill=True, | |
| fill_opacity=0.1 | |
| ).add_to(m) | |
| # Create a legend for the map | |
| legend_html = ''' | |
| <div style="position: fixed; | |
| bottom: 50px; left: 50px; width: 180px; height: 160px; | |
| border:2px solid grey; z-index:9999; font-size:14px; | |
| background-color:white; padding: 10px"> | |
| Map Legend <br> | |
| <i class="fa fa-map-marker fa-2x" style="color:blue"></i> Attractions <br> | |
| <i class="fa fa-map-marker fa-2x" style="color:red"></i> Hotels/Resorts <br> | |
| <i class="fa fa-map-marker fa-2x" style="color:orange"></i> Beaches <br> | |
| <i class="fa fa-map-marker fa-2x" style="color:purple"></i> Historic Sites <br> | |
| <i class="fa fa-map-marker fa-2x" style="color:darkgreen"></i> Restaurants <br> | |
| <i class="fa fa-map-marker fa-2x" style="color:darkblue"></i> Other <br> | |
| </div> | |
| ''' | |
| m.get_root().html.add_child(folium.Element(legend_html)) | |
| # Enhanced marker color scheme for better category visualization | |
| for spot in filtered_spots: | |
| if spot['lat'] != 0 and spot['lon'] != 0: | |
| name = spot['tags'].get('name', 'Unnamed') | |
| category = spot['category'] | |
| # More nuanced color coding based on category | |
| if 'resort' in category or 'hotel' in category: | |
| color = 'red' | |
| icon_name = 'home' | |
| elif 'attraction' in category: | |
| color = 'blue' | |
| icon_name = 'info-sign' | |
| elif 'beach' in category: | |
| color = 'orange' | |
| icon_name = 'tint' | |
| elif 'historic' in category: | |
| color = 'purple' | |
| icon_name = 'university' | |
| elif 'viewpoint' in category: | |
| color = 'green' | |
| icon_name = 'camera' | |
| elif 'museum' in category or 'gallery' in category: | |
| color = 'darkpurple' | |
| icon_name = 'book' | |
| elif 'park' in category: | |
| color = 'lightgreen' | |
| icon_name = 'tree-conifer' | |
| elif 'restaurant' in category: | |
| color = 'darkgreen' | |
| icon_name = 'cutlery' | |
| elif 'waterfall' in category: | |
| color = 'lightblue' | |
| icon_name = 'tint' | |
| else: | |
| color = 'darkblue' | |
| icon_name = 'star' | |
| # Enhanced popup with more details | |
| popup_content = f""" | |
| <style> | |
| .popup-content {{ | |
| font-family: Arial, sans-serif; | |
| padding: 5px; | |
| max-width: 250px; | |
| }} | |
| .popup-title {{ | |
| font-weight: bold; | |
| font-size: 16px; | |
| margin-bottom: 5px; | |
| }} | |
| </style> | |
| <div class="popup-content"> | |
| <div class="popup-title">{name}</div> | |
| <div>Type: {category}</div> | |
| """ | |
| # Add additional details if available | |
| if 'website' in spot['tags']: | |
| popup_content += f'<div>Website: <a href="{spot["tags"]["website"]}" target="_blank">Link</a></div>' | |
| if 'opening_hours' in spot['tags']: | |
| popup_content += f'<div>Hours: {spot["tags"]["opening_hours"]}</div>' | |
| popup_content += '</div>' | |
| folium.Marker( | |
| location=[spot['lat'], spot['lon']], | |
| popup=popup_content, | |
| tooltip=name, | |
| icon=folium.Icon(color=color, icon=icon_name) | |
| ).add_to(m) | |
| # Display the map | |
| st.subheader("Tourist Spots Map") | |
| folium_static(m, width=800, height=500) | |
| # Display spots in a selectbox with categories | |
| spot_display_names = [f"{spot['tags'].get('name', f'Spot {i+1}')} ({spot['category']})" | |
| for i, spot in enumerate(filtered_spots)] | |
| if spot_display_names: | |
| selected_spot_display = st.selectbox("Select a spot for details:", spot_display_names) | |
| # Extract actual name from display name | |
| selected_spot_name = selected_spot_display.split(" (")[0] if " (" in selected_spot_display else selected_spot_display | |
| # Find the selected spot | |
| selected_spot = next((spot for spot in filtered_spots | |
| if spot['tags'].get('name', '') == selected_spot_name), None) | |
| if selected_spot: | |
| st.subheader(f"Details: {selected_spot_name}") | |
| # Create columns for details and map | |
| col1, col2 = st.columns([1, 1]) | |
| with col1: | |
| # Display detailed information with enhanced formatting | |
| spot_type = selected_spot['category'] | |
| st.write(f"**Type:** {spot_type.replace('_', ' ').title()}") | |
| # Show all available tag information that might be useful | |
| if 'description' in selected_spot['tags']: | |
| st.write(f"**Description:** {selected_spot['tags']['description']}") | |
| if 'website' in selected_spot['tags']: | |
| st.write(f"**Website:** [{selected_spot['tags']['website']}]({selected_spot['tags']['website']})") | |
| if 'opening_hours' in selected_spot['tags']: | |
| st.write(f"**Opening Hours:** {selected_spot['tags']['opening_hours']}") | |
| if 'phone' in selected_spot['tags']: | |
| st.write(f"**Phone:** {selected_spot['tags']['phone']}") | |
| if 'addr:street' in selected_spot['tags']: | |
| address_parts = [] | |
| if selected_spot['tags'].get('addr:housenumber'): | |
| address_parts.append(selected_spot['tags'].get('addr:housenumber')) | |
| if selected_spot['tags'].get('addr:street'): | |
| address_parts.append(selected_spot['tags'].get('addr:street')) | |
| if selected_spot['tags'].get('addr:city'): | |
| address_parts.append(selected_spot['tags'].get('addr:city')) | |
| if address_parts: | |
| st.write(f"**Address:** {', '.join(address_parts)}") | |
| # Additional tags that might be useful | |
| if 'cuisine' in selected_spot['tags']: | |
| st.write(f"**Cuisine:** {selected_spot['tags']['cuisine'].replace(';', ', ').title()}") | |
| if 'stars' in selected_spot['tags']: | |
| st.write(f"**Rating:** {'⭐' * int(float(selected_spot['tags']['stars']))}") | |
| # Get current weather data for the location | |
| weather_data = get_weather_data(selected_spot['lat'], selected_spot['lon']) | |
| if weather_data: | |
| st.write(f"**Current Weather:** {weather_data['description'].title()}, {weather_data['temperature']}°C") | |
| # Generate more human-like description based on available data | |
| if 'description' not in selected_spot['tags']: | |
| st.write("**About This Place:**") | |
| # Check if we have a cached description | |
| spot_id = selected_spot['id'] | |
| if spot_id in st.session_state.spot_descriptions: | |
| st.write(st.session_state.spot_descriptions[spot_id]) | |
| else: | |
| # Attempt to create a human-like description from available data | |
| location_str = st.session_state.search_location | |
| if selected_spot['location_details']['city']: | |
| location_str = selected_spot['location_details']['city'] | |
| country_str = selected_spot['location_details']['country'] or st.session_state.country | |
| weather_info = "" | |
| if weather_data: | |
| weather_info = f"The current weather is {weather_data['description']} with a temperature of {weather_data['temperature']}°C." | |
| # Create a more context-aware description based on the category | |
| with st.spinner("Generating description..."): | |
| try: | |
| # Improved structured prompt | |
| prompt = f"""Create a concise, natural description (100 - 120 words) for this tourist spot: | |
| - Name: '{selected_spot_name}' | |
| - Category: {spot_type.replace('_', ' ')} | |
| - Location: {location_str}, {country_str} | |
| Focus only on: | |
| 1. What makes this place special or unique (be specific to the actual location if possible) | |
| 2. One activity visitors typically enjoy here (tailored to the type of location) | |
| 3. A practical tip based on the current weather: {weather_data['description'] if weather_data else 'unknown'}, {weather_data['temperature'] if weather_data else ''}°C | |
| For forest locations, mention: | |
| - One notable tree species or ecosystem feature likely found here | |
| - A suggestion about the types of wildlife visitors might spot | |
| - Appropriate hiking recommendations based on the weather conditions | |
| For beaches or waterfalls, include: | |
| - Water characteristics (color, temperature, clarity if possible) | |
| - Best time of day to visit based on lighting and crowds | |
| Write as an experienced tour guide in simple, direct language. Avoid generic phrases like "worth visiting" or "popular destination." | |
| """ | |
| messages = [ | |
| {"role": "system", "content": "You are a knowledgeable local tour guide providing authentic information about tourist destinations. Your descriptions sound natural and engaging, like a real person talking."}, | |
| {"role": "user", "content": prompt} | |
| ] | |
| # Replace with your actual LLM implementation | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-specdec", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=200, | |
| ), | |
| messages | |
| ) | |
| description = completion.choices[0].message.content | |
| st.write(description) | |
| # Cache the description | |
| st.session_state.spot_descriptions[spot_id] = description | |
| except Exception as e: | |
| # Fallback description if LLM fails | |
| fallback_desc = f"{selected_spot_name} is a {spot_type.replace('_', ' ')} in {st.session_state.search_location}. " | |
| if 'waterfall' in spot_type: | |
| fallback_desc += "Enjoy the sound of cascading water while hiking nearby trails. " | |
| elif 'beach' in spot_type: | |
| fallback_desc += "Relax on the sandy shores or swim in the waves. " | |
| elif 'historic' in spot_type: | |
| fallback_desc += "Explore its unique history through preserved structures. " | |
| elif 'restaurant' in spot_type: | |
| cuisine = selected_spot['tags'].get('cuisine', 'local').replace(';', ', ') | |
| fallback_desc += f"Savor {cuisine} dishes in a cozy setting. " | |
| elif 'hotel' in spot_type or 'resort' in spot_type: | |
| fallback_desc += "Rest comfortably after a day of exploring. " | |
| else: | |
| fallback_desc += "Check out its unique features on-site. " | |
| if weather_data: | |
| if "rain" in weather_data['description']: | |
| fallback_desc += f"Bring an umbrella as it’s rainy ({weather_data['temperature']}°C) today." | |
| elif "sun" in weather_data['description'] or "clear" in weather_data['description']: | |
| fallback_desc += f"Perfect day to visit with sunny weather ({weather_data['temperature']}°C)." | |
| elif "snow" in weather_data['description']: | |
| fallback_desc += f"Dress warmly for snowy conditions ({weather_data['temperature']}°C)." | |
| else: | |
| fallback_desc += f"Current weather is {weather_data['description']} ({weather_data['temperature']}°C)." | |
| elif 'forest' in spot_type or ('landuse' in selected_spot['tags'] and selected_spot['tags']['landuse'] == 'forest'): | |
| fallback_desc += "Explore winding trails beneath the canopy and listen for birdcalls. " | |
| if weather_data: | |
| if "rain" in weather_data['description']: | |
| fallback_desc += f"The forest floor may be slippery with rain ({weather_data['temperature']}°C), so wear proper hiking boots." | |
| elif "sun" in weather_data['description'] or "clear" in weather_data['description']: | |
| fallback_desc += f"With today's clear weather ({weather_data['temperature']}°C), you'll get beautiful dappled sunlight through the trees." | |
| else: | |
| fallback_desc += f"Current forest conditions: {weather_data['description']} ({weather_data['temperature']}°C)." | |
| elif 'nature_reserve' in spot_type or 'protected_area' in spot_type: | |
| fallback_desc += "Home to protected ecosystems and possibly rare wildlife. " | |
| if weather_data: | |
| fallback_desc += f"With {weather_data['description']} conditions ({weather_data['temperature']}°C), bring appropriate gear for your hike." | |
| st.write(fallback_desc) | |
| # Cache the fallback description | |
| st.session_state.spot_descriptions[spot_id] = fallback_desc | |
| with col2: | |
| # Display spot-specific map | |
| if selected_spot['lat'] != 0 and selected_spot['lon'] != 0: | |
| spot_map = folium.Map(location=[selected_spot['lat'], selected_spot['lon']], zoom_start=15) | |
| # Determine icon based on category | |
| category = selected_spot['category'] | |
| if 'waterfall' in category: | |
| icon_color = 'lightblue' | |
| icon_name = 'tint' | |
| elif 'resort' in category or 'hotel' in category: | |
| icon_color = 'red' | |
| icon_name = 'home' | |
| elif 'beach' in category: | |
| icon_color = 'orange' | |
| icon_name = 'tint' | |
| elif 'historic' in category: | |
| icon_color = 'purple' | |
| icon_name = 'university' | |
| elif 'restaurant' in category: | |
| icon_color = 'darkgreen' | |
| icon_name = 'cutlery' | |
| elif 'attraction' in category: | |
| icon_color = 'blue' | |
| icon_name = 'info-sign' | |
| elif 'viewpoint' in category: | |
| icon_color = 'green' | |
| icon_name = 'camera' | |
| elif 'museum' in category or 'gallery' in category: | |
| icon_color = 'darkpurple' | |
| icon_name = 'book' | |
| elif 'park' in category: | |
| icon_color = 'lightgreen' | |
| icon_name = 'tree-conifer' | |
| elif 'forest' in category or ('landuse' in selected_spot['tags'] and selected_spot['tags']['landuse'] == 'forest'): | |
| icon_color = 'darkgreen' # Changed from 'color' to 'icon_color' | |
| icon_name = 'tree-conifer' | |
| elif 'nature_reserve' in category or ('boundary' in selected_spot['tags'] and selected_spot['tags']['boundary'] == 'protected_area'): | |
| icon_color = 'green' # Changed from 'color' to 'icon_color' | |
| icon_name = 'leaf' | |
| else: | |
| icon_color = 'darkblue' | |
| icon_name = 'star' | |
| folium.Marker( | |
| location=[selected_spot['lat'], selected_spot['lon']], | |
| popup=selected_spot_name, | |
| tooltip=selected_spot_name, | |
| icon=folium.Icon(color=icon_color, icon=icon_name) | |
| ).add_to(spot_map) | |
| folium_static(spot_map, width=400, height=300) | |
| # Enhanced directions and information options | |
| import urllib.parse | |
| encoded_name = urllib.parse.quote(f"{selected_spot_name} {st.session_state.search_location}") | |
| st.subheader("Get Directions & Information:") | |
| # Google Maps link | |
| gmaps_url = f"https://www.google.com/maps/dir/?api=1&destination={selected_spot['lat']},{selected_spot['lon']}" | |
| st.write(f"[🧭 Get Directions on Google Maps]({gmaps_url})") | |
| # Google Search link | |
| search_url = f"https://www.google.com/search?q={encoded_name}" | |
| st.write(f"[🔍 View Search Results on Google]({search_url})") | |
| # Google Reviews link | |
| reviews_url = f"https://www.google.com/search?q={encoded_name}+reviews" | |
| st.write(f"[⭐ Read Reviews on Google]({reviews_url})") | |
| # Add a section for user questions about the selected spot | |
| st.subheader("Ask About This Place") | |
| user_question = st.text_input(f"What would you like to know about {selected_spot_name}?", | |
| placeholder="e.g., What’s the best time to visit?") | |
| if user_question: | |
| with st.spinner("Getting your answer..."): | |
| try: | |
| # Check if the question is about weather or rain | |
| weather_keywords = ['rain', 'weather', 'forecast', 'precipitation', 'sunny', 'cloudy', 'storm', 'thunder'] | |
| is_weather_question = any(keyword in user_question.lower() for keyword in weather_keywords) | |
| # Get weather data with forecast | |
| current_weather_data = get_weather_data(selected_spot['lat'], selected_spot['lon']) | |
| if is_weather_question and current_weather_data and 'forecast' in current_weather_data: | |
| forecast = current_weather_data['forecast'] | |
| # Check if question mentions time periods | |
| two_days_keywords = ['next 2 days', 'next two days', '2 days', 'two days', '48 hours', 'tomorrow'] | |
| is_two_days = any(keyword in user_question.lower() for keyword in two_days_keywords) | |
| # Create a weather-specific response based on the forecast | |
| if 'rain' in user_question.lower() or 'precipitation' in user_question.lower() or 'storm' in user_question.lower(): | |
| if is_two_days: | |
| # 2-day forecast | |
| day1 = forecast['day1'] | |
| day2 = forecast['day2'] | |
| day1_intensity = "light" if day1['max_precipitation'] < 1 else "moderate" if day1['max_precipitation'] < 5 else "heavy" | |
| day2_intensity = "light" if day2['max_precipitation'] < 1 else "moderate" if day2['max_precipitation'] < 5 else "heavy" | |
| if day1['rain_chance'] and day2['rain_chance']: | |
| weather_answer = f"Yes, there's a chance of rain at {selected_spot_name} in the next 2 days. Today: {day1_intensity} rain for approximately {day1['rain_hours']} hours. Tomorrow: {day2_intensity} rain for approximately {day2['rain_hours']} hours." | |
| elif day1['rain_chance']: | |
| weather_answer = f"There's a chance of {day1_intensity} rain today at {selected_spot_name} for approximately {day1['rain_hours']} hours, but tomorrow looks dry based on current forecasts." | |
| elif day2['rain_chance']: | |
| weather_answer = f"Today looks dry at {selected_spot_name}, but tomorrow there's a chance of {day2_intensity} rain for approximately {day2['rain_hours']} hours." | |
| else: | |
| weather_answer = f"No rain is expected at {selected_spot_name} for the next 2 days based on current forecasts. The current weather is {current_weather_data['description']} at {current_weather_data['temperature']}°C." | |
| else: | |
| # Default to 24-hour forecast | |
| hours = forecast['day1']['rain_hours'] | |
| intensity = "light" if forecast['day1']['max_precipitation'] < 1 else "moderate" if forecast['day1']['max_precipitation'] < 5 else "heavy" | |
| if forecast['day1']['rain_chance']: | |
| weather_answer = f"Yes, there's a chance of {intensity} rain in the next 24 hours at {selected_spot_name}. Rain is expected for approximately {hours} hours." | |
| else: | |
| weather_answer = f"No rain is expected at {selected_spot_name} in the next 24 hours based on current forecasts. The current weather is {current_weather_data['description']} at {current_weather_data['temperature']}°C." | |
| st.write(f"**Weather Forecast:** {weather_answer}") | |
| else: | |
| # For general weather questions | |
| if is_two_days: | |
| day1_forecast = f"Today: {current_weather_data['description']}, {current_weather_data['temperature']}°C. Rain is {'expected' if forecast['day1']['rain_chance'] else 'not expected'}." | |
| day2_forecast = f"Tomorrow: Rain is {'expected' if forecast['day2']['rain_chance'] else 'not expected'}." | |
| weather_answer = f"{day1_forecast} {day2_forecast}" | |
| else: | |
| rain_info = f"Rain is {'expected' if forecast['day1']['rain_chance'] else 'not expected'} in the next 24 hours." | |
| weather_answer = f"Current weather at {selected_spot_name} is {current_weather_data['description']} at {current_weather_data['temperature']}°C. {rain_info}" | |
| st.write(f"**Weather Forecast:** {weather_answer}") | |
| else: | |
| # Handle non-weather questions or cases where weather data is unavailable | |
| spot_info = { | |
| 'name': selected_spot_name, | |
| 'category': spot_type.replace('_', ' '), | |
| 'location': f"{st.session_state.search_location}, {st.session_state.country}", | |
| 'tags': selected_spot['tags'], | |
| 'weather': f"{current_weather_data['description']}, {current_weather_data['temperature']}°C" if current_weather_data else "unknown" | |
| } | |
| if current_weather_data and 'forecast' in current_weather_data: | |
| spot_info['weather_forecast'] = f"Rain {'expected' if current_weather_data['forecast']['day1']['rain_chance'] else 'not expected'} in next 24 hours" | |
| if spot_id in st.session_state.spot_descriptions: | |
| spot_info['description'] = st.session_state.spot_descriptions[spot_id] | |
| # Define the prompt for non-weather questions | |
| prompt = f"""You are a local tour guide with deep knowledge about {selected_spot_name}, a {spot_type.replace('_', ' ')} in {st.session_state.search_location}, {st.session_state.country}. | |
| Here's the available information: {spot_info}. | |
| A visitor has asked: '{user_question}' | |
| Provide a concise, accurate answer (80-100 words) based only on this data and logical inferences from the category and weather. | |
| Avoid speculation beyond the provided information. Answer in a friendly, direct tone as if speaking to the visitor.""" | |
| messages = [ | |
| {"role": "system", "content": "You are a knowledgeable local tour guide providing authentic, accurate answers about tourist destinations based on given data."}, | |
| {"role": "user", "content": prompt} | |
| ] | |
| # Call the LLM | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-specdec", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=150, | |
| ), | |
| messages | |
| ) | |
| st.write(f"**Answer:** {completion.choices[0].message.content}") | |
| except Exception as e: | |
| st.error(f"Sorry, I couldn't process your question due to an error: {str(e)}. Try asking something else!") | |
| with tab2: | |
| st.header("Tourist Chat Assistant") | |
| # Initialize session state for chat | |
| if "messages" not in st.session_state: | |
| st.session_state.messages = [ | |
| {"role": "assistant", "content": """Tourism Assistant! 🇧🇩 | |
| """} | |
| ] | |
| if "feedback" not in st.session_state: | |
| st.session_state.feedback = [] | |
| if "spot_descriptions" not in st.session_state: | |
| st.session_state.spot_descriptions = {} | |
| if "cached_searches" not in st.session_state: | |
| st.session_state.cached_searches = {} | |
| # Sidebar controls with improved UI | |
| with st.sidebar.expander("Chat Settings", expanded=False): | |
| st.checkbox("Enable Detailed Search", value=True, key="enable_search", | |
| help="Uses OpenStreetMap data for precise location searches.") | |
| st.slider("Search Radius (km)", min_value=10, max_value=200, value=100, step=10, key="search_radius", | |
| help="Set the search radius around the specified location.") | |
| st.checkbox("Include Weather Data", value=True, key="include_weather", | |
| help="Adds current weather information to location details.") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("🧹 Clear Chat", use_container_width=True): | |
| st.session_state.messages = [ | |
| {"role": "assistant", "content": "How can I assist with your Bangladesh tourism queries today?"} | |
| ] | |
| st.rerun() | |
| with col2: | |
| if st.button("📊 Export Feedback", use_container_width=True): | |
| # This would download the feedback data in a real implementation | |
| if st.session_state.feedback: | |
| st.success(f"Feedback exported ({len(st.session_state.feedback)} items)") | |
| else: | |
| st.info("No feedback to export") | |
| # Display chat messages with enhanced UI | |
| for i, message in enumerate(st.session_state.messages): | |
| with st.chat_message(message["role"], avatar="👤" if message["role"] == "user" else "🇧🇩"): | |
| st.write(message["content"]) | |
| if message["role"] == "assistant" and i == len(st.session_state.messages) - 1: | |
| cols = st.columns([1, 1, 8]) | |
| with cols[0]: | |
| if st.button("👍", help="This response was helpful", key=f"like_{i}"): | |
| st.session_state.feedback.append({"response": message["content"], "feedback": "positive", "timestamp": datetime.now().isoformat()}) | |
| st.toast("Thanks for your positive feedback!") | |
| with cols[1]: | |
| if st.button("👎", help="This response needs improvement", key=f"dislike_{i}"): | |
| st.session_state.feedback.append({"response": message["content"], "feedback": "negative", "timestamp": datetime.now().isoformat()}) | |
| st.toast("Thank you for your feedback. We'll work to improve!") | |
| # User input | |
| if prompt := st.chat_input("Ask me about tourist spots ..."): | |
| st.session_state.messages.append({"role": "user", "content": prompt}) | |
| with st.chat_message("user", avatar="👤"): | |
| st.write(prompt) | |
| # Generate assistant response | |
| with st.chat_message("assistant", avatar="🇧🇩"): | |
| message_placeholder = st.empty() | |
| full_response = "" | |
| try: | |
| # Improved system prompt for more professional responses | |
| system_prompt = """You are an expert tourism assistant for Bangladesh with comprehensive knowledge of the country's attractions, culture, and travel infrastructure. Provide accurate, professional, and helpful responses based on the user's question, chat history, and provided data. | |
| Chat History: | |
| {chat_history} | |
| Guidelines: | |
| - For search queries: Provide a curated list of up to 5 relevant locations with precise coordinates, interactive map links, and comprehensive details including amenities, unique features, and insider tips. Format should be professional and consistent. | |
| - For specific location inquiries: Deliver a concise yet comprehensive profile including historical/cultural context, visitor information, current weather conditions, and practical travel advice. | |
| - For weather queries: Provide detailed weather information with tailored travel recommendations based on conditions. | |
| - For accommodation searches: Include price ranges, amenities, location benefits, and booking suggestions. | |
| - Maintain a professional, knowledgeable tone while being conversational and engaging. | |
| - Use proper formatting with bullet points for clarity and include practical, seasonally-appropriate travel tips.""" | |
| # Get recent chat history for context | |
| chat_history = "\n".join([f"{m['role']}: {m['content']}" for m in st.session_state.messages[-6:-1]]) | |
| system_prompt = system_prompt.format(chat_history=chat_history) | |
| additional_data = "" | |
| prompt_lower = prompt.lower() | |
| enable_search = st.session_state.get("enable_search", True) | |
| include_weather = st.session_state.get("include_weather", True) | |
| radius_meters = st.session_state.get("search_radius", 100) * 1000 # Convert km to meters | |
| # Enhanced query classification with more nuanced keywords | |
| search_keywords = ["show", "find", "list", "spots", "attractions", "hotels", "resorts", "beaches", | |
| "forests", "waterfalls", "museums", "temples", "mosques", "restaurants", "cafes"] | |
| detail_keywords = ["tell me about", "what is", "describe", "details", "information on", "facts about"] | |
| weather_keywords = ["weather", "rain", "forecast", "temperature", "climate", "season"] | |
| activity_keywords = ["things to do", "activities", "adventure", "hiking", "swimming", "shopping", "tours"] | |
| # Improved query classification with scoring system | |
| search_score = sum(2 if keyword in prompt_lower else 0 for keyword in search_keywords) | |
| detail_score = sum(3 if keyword in prompt_lower else 0 for keyword in detail_keywords) | |
| weather_score = sum(3 if keyword in prompt_lower else 0 for keyword in weather_keywords) | |
| activity_score = sum(2 if keyword in prompt_lower else 0 for keyword in activity_keywords) | |
| is_search_query = enable_search and search_score > 0 | |
| is_detail_query = detail_score > search_score and detail_score > weather_score | |
| is_weather_query = weather_score > search_score and weather_score > detail_score | |
| is_activity_query = activity_score > 0 and not is_search_query and not is_detail_query and not is_weather_query | |
| # Helper function to get coordinates with error handling and caching | |
| def get_coordinates(location): | |
| # Check cache first | |
| location_key = location.lower().strip() | |
| if location_key in st.session_state.cached_searches: | |
| return st.session_state.cached_searches[location_key] | |
| try: | |
| # Add Bangladesh to the query for better results | |
| search_query = f"{location}, Bangladesh" | |
| if location.lower() == "bangladesh": | |
| search_query = "Bangladesh" | |
| nominatim_url = f"https://nominatim.openstreetmap.org/search?q={search_query}&format=json" | |
| response = requests.get(nominatim_url, headers={'User-Agent': 'BangladeshTouristApp/2.0'}, timeout=10) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if data: | |
| result = (float(data[0]['lat']), float(data[0]['lon'])) | |
| # Cache the result | |
| st.session_state.cached_searches[location_key] = result | |
| return result | |
| return None | |
| except Exception as e: | |
| st.error(f"Error fetching coordinates: {str(e)}") | |
| return None | |
| # Enhanced weather function with more detailed information | |
| def get_weather_data(lat, lon): | |
| try: | |
| url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&hourly=temperature_2m,precipitation,weather_code&forecast_days=3" | |
| response = requests.get(url, timeout=10) | |
| if response.status_code == 200: | |
| data = response.json() | |
| weather_codes = { | |
| 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", | |
| 45: "Fog", 48: "Depositing rime fog", | |
| 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle", | |
| 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", | |
| 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow", | |
| 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers", | |
| 95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail" | |
| } | |
| current_code = data['current']['weather_code'] | |
| current_temp = data['current']['temperature_2m'] | |
| current_humidity = data['current']['relative_humidity_2m'] | |
| current_wind = data['current']['wind_speed_10m'] | |
| # Process hourly data for forecasts | |
| hourly_temps = data['hourly']['temperature_2m'] | |
| hourly_precip = data['hourly']['precipitation'] | |
| hourly_weather = data['hourly']['weather_code'] | |
| # Create daily forecasts | |
| day1_hours = 24 | |
| day2_hours = 48 | |
| day1_max_temp = max(hourly_temps[:day1_hours]) | |
| day1_min_temp = min(hourly_temps[:day1_hours]) | |
| day1_rain_chance = any(p > 0.1 for p in hourly_precip[:day1_hours]) | |
| day1_weather_codes = hourly_weather[:day1_hours] | |
| day1_main_weather = max(set(day1_weather_codes), key=day1_weather_codes.count) | |
| day2_max_temp = max(hourly_temps[day1_hours:day2_hours]) | |
| day2_min_temp = min(hourly_temps[day1_hours:day2_hours]) | |
| day2_rain_chance = any(p > 0.1 for p in hourly_precip[day1_hours:day2_hours]) | |
| day2_weather_codes = hourly_weather[day1_hours:day2_hours] | |
| day2_main_weather = max(set(day2_weather_codes), key=day2_weather_codes.count) | |
| forecast = { | |
| 'day1': { | |
| 'min_temp': day1_min_temp, | |
| 'max_temp': day1_max_temp, | |
| 'rain_chance': day1_rain_chance, | |
| 'description': weather_codes.get(day1_main_weather, "Mixed conditions") | |
| }, | |
| 'day2': { | |
| 'min_temp': day2_min_temp, | |
| 'max_temp': day2_max_temp, | |
| 'rain_chance': day2_rain_chance, | |
| 'description': weather_codes.get(day2_main_weather, "Mixed conditions") | |
| } | |
| } | |
| # Generate travel recommendations based on weather | |
| recommendations = [] | |
| if current_code in [0, 1]: # Clear or mainly clear | |
| recommendations.append("Perfect weather for outdoor activities and photography.") | |
| elif current_code in [61, 63, 65, 80, 81, 82]: # Rain | |
| recommendations.append("Consider indoor activities or bring rain gear.") | |
| if current_temp > 30: | |
| recommendations.append("Stay hydrated and seek shade during midday hours.") | |
| elif current_temp < 15: | |
| recommendations.append("Bring layered clothing for cooler temperatures.") | |
| return { | |
| 'temperature': current_temp, | |
| 'humidity': current_humidity, | |
| 'wind_speed': current_wind, | |
| 'description': weather_codes.get(current_code, "Unknown conditions"), | |
| 'forecast': forecast, | |
| 'recommendations': recommendations | |
| } | |
| return None | |
| except Exception as e: | |
| st.error(f"Error fetching weather data: {str(e)}") | |
| return None | |
| # 1. Enhanced Search Queries with more detail and better organization | |
| if is_search_query: | |
| # Extract location from query with improved parsing | |
| location_parts = ["in", "near", "around", "at"] | |
| location_query = None | |
| for part in location_parts: | |
| if part in prompt_lower: | |
| location_query = prompt_lower.split(part)[-1].strip().replace("?", "").strip() | |
| break | |
| if not location_query: | |
| location_query = "Bangladesh" | |
| # Determine what type of locations to search for | |
| spot_types = { | |
| "hotels": ["hotel", "resort", "accommodation", "place to stay", "lodging"], | |
| "beaches": ["beach", "sea", "ocean", "shore", "coast"], | |
| "forests": ["forest", "jungle", "woods", "nature reserve", "national park"], | |
| "waterfalls": ["waterfall", "falls", "cascade"], | |
| "restaurants": ["restaurant", "dining", "food", "eat", "café", "cafe"], | |
| "museums": ["museum", "gallery", "exhibition"], | |
| "religious": ["temple", "mosque", "shrine", "church"], | |
| "attractions": ["attraction", "viewpoint", "landmark", "tourist spot", "sight", "sightseeing"] | |
| } | |
| # Find the most relevant spot type based on query | |
| spot_type = "attractions" # Default | |
| max_matches = 0 | |
| for type_key, type_keywords in spot_types.items(): | |
| matches = sum(1 for keyword in type_keywords if keyword in prompt_lower) | |
| if matches > max_matches: | |
| max_matches = matches | |
| spot_type = type_key | |
| # Check for qualifiers | |
| is_affordable = any(word in prompt_lower for word in ["affordable", "budget", "cheap", "inexpensive"]) | |
| is_luxury = any(word in prompt_lower for word in ["luxury", "high-end", "expensive", "premium"]) | |
| is_family = any(word in prompt_lower for word in ["family", "kid", "children"]) | |
| with st.spinner(f"Searching for {spot_type} in {location_query}..."): | |
| # Check cache for previous searches | |
| search_cache_key = f"{spot_type}_{location_query}_{radius_meters}" | |
| if search_cache_key in st.session_state.cached_searches: | |
| spots = st.session_state.cached_searches[search_cache_key] | |
| else: | |
| coords = get_coordinates(location_query) | |
| if not coords: | |
| additional_data = f"\nNo location found for '{location_query}'. Please check the spelling or try a more well-known location in Bangladesh." | |
| else: | |
| lat, lon = coords | |
| overpass_url = "https://overpass-api.de/api/interpreter" | |
| query_parts = [] | |
| # Enhanced and more specific Overpass query based on spot type | |
| if spot_type == "attractions": | |
| query_parts.extend([ | |
| f'node["tourism"="attraction"](around:{radius_meters},{lat},{lon});', | |
| f'way["tourism"="attraction"](around:{radius_meters},{lat},{lon});', | |
| f'node["tourism"="viewpoint"](around:{radius_meters},{lat},{lon});', | |
| f'way["tourism"="viewpoint"](around:{radius_meters},{lat},{lon});', | |
| f'node["historic"](around:{radius_meters},{lat},{lon});', | |
| f'way["historic"](around:{radius_meters},{lat},{lon});', | |
| f'node["natural"="beach"](around:{radius_meters},{lat},{lon});', | |
| f'way["natural"="beach"](around:{radius_meters},{lat},{lon});', | |
| f'node["natural"="waterfall"](around:{radius_meters},{lat},{lon});', | |
| f'way["natural"="waterfall"](around:{radius_meters},{lat},{lon});' | |
| ]) | |
| elif spot_type == "hotels": | |
| query_parts.extend([ | |
| f'node["tourism"="hotel"](around:{radius_meters},{lat},{lon});', | |
| f'way["tourism"="hotel"](around:{radius_meters},{lat},{lon});', | |
| f'node["tourism"="guest_house"](around:{radius_meters},{lat},{lon});', | |
| f'way["tourism"="guest_house"](around:{radius_meters},{lat},{lon});', | |
| f'node["tourism"="hostel"](around:{radius_meters},{lat},{lon});', | |
| f'way["tourism"="hostel"](around:{radius_meters},{lat},{lon});', | |
| f'node["tourism"="resort"](around:{radius_meters},{lat},{lon});', | |
| f'way["tourism"="resort"](around:{radius_meters},{lat},{lon});' | |
| ]) | |
| elif spot_type == "beaches": | |
| query_parts.extend([ | |
| f'node["natural"="beach"](around:{radius_meters},{lat},{lon});', | |
| f'way["natural"="beach"](around:{radius_meters},{lat},{lon});', | |
| f'node["leisure"="beach_resort"](around:{radius_meters},{lat},{lon});', | |
| f'way["leisure"="beach_resort"](around:{radius_meters},{lat},{lon});' | |
| ]) | |
| elif spot_type == "forests": | |
| query_parts.extend([ | |
| f'node["natural"="forest"](around:{radius_meters},{lat},{lon});', | |
| f'way["natural"="forest"](around:{radius_meters},{lat},{lon});', | |
| f'node["leisure"="nature_reserve"](around:{radius_meters},{lat},{lon});', | |
| f'way["leisure"="nature_reserve"](around:{radius_meters},{lat},{lon});', | |
| f'node["boundary"="protected_area"](around:{radius_meters},{lat},{lon});', | |
| f'way["boundary"="protected_area"](around:{radius_meters},{lat},{lon});' | |
| ]) | |
| elif spot_type == "waterfalls": | |
| query_parts.extend([ | |
| f'node["natural"="waterfall"](around:{radius_meters},{lat},{lon});', | |
| f'way["natural"="waterfall"](around:{radius_meters},{lat},{lon});' | |
| ]) | |
| elif spot_type == "restaurants": | |
| query_parts.extend([ | |
| f'node["amenity"="restaurant"](around:{radius_meters},{lat},{lon});', | |
| f'way["amenity"="restaurant"](around:{radius_meters},{lat},{lon});', | |
| f'node["amenity"="cafe"](around:{radius_meters},{lat},{lon});', | |
| f'way["amenity"="cafe"](around:{radius_meters},{lat},{lon});' | |
| ]) | |
| elif spot_type == "museums": | |
| query_parts.extend([ | |
| f'node["tourism"="museum"](around:{radius_meters},{lat},{lon});', | |
| f'way["tourism"="museum"](around:{radius_meters},{lat},{lon});', | |
| f'node["tourism"="gallery"](around:{radius_meters},{lat},{lon});', | |
| f'way["tourism"="gallery"](around:{radius_meters},{lat},{lon});' | |
| ]) | |
| elif spot_type == "religious": | |
| query_parts.extend([ | |
| f'node["amenity"="place_of_worship"](around:{radius_meters},{lat},{lon});', | |
| f'way["amenity"="place_of_worship"](around:{radius_meters},{lat},{lon});' | |
| ]) | |
| query = f"[out:json][timeout:30];({' '.join(query_parts)});out body center;" | |
| try: | |
| response = requests.post(overpass_url, data={"data": query}, timeout=30) | |
| if response.status_code == 200: | |
| data = response.json() | |
| spots = [] | |
| for element in data.get('elements', []): | |
| if 'tags' in element and 'name' in element['tags']: | |
| name = element['tags']['name'] | |
| lat_val = element.get('lat', element.get('center', {}).get('lat', lat)) | |
| lon_val = element.get('lon', element.get('center', {}).get('lon', lon)) | |
| # Extract more detailed information | |
| category = element['tags'].get('tourism', | |
| element['tags'].get('natural', | |
| element['tags'].get('amenity', | |
| element['tags'].get('historic', 'attraction')))) | |
| # Gather comprehensive details | |
| details = { | |
| 'phone': element['tags'].get('phone', element['tags'].get('contact:phone', 'Not available')), | |
| 'website': element['tags'].get('website', element['tags'].get('contact:website', 'Not available')), | |
| 'address': element['tags'].get('addr:full', | |
| f"{element['tags'].get('addr:housenumber', '')} " | |
| f"{element['tags'].get('addr:street', '')}").strip() or 'Not available', | |
| 'description': element['tags'].get('description', 'No description available'), | |
| 'opening_hours': element['tags'].get('opening_hours', 'Not specified'), | |
| 'amenities': [], | |
| 'wheelchair': element['tags'].get('wheelchair', 'Not specified'), | |
| 'stars': element['tags'].get('stars', 'Not rated'), | |
| 'fee': element['tags'].get('fee', 'Not specified') | |
| } | |
| # Extract amenities for accommodations | |
| for tag in ['internet', 'wifi', 'swimming_pool', 'restaurant', 'bar', 'air_conditioning']: | |
| if element['tags'].get(tag) == 'yes': | |
| details['amenities'].append(tag.replace('_', ' ').title()) | |
| spots.append({ | |
| 'name': name, | |
| 'lat': lat_val, | |
| 'lon': lon_val, | |
| 'category': category, | |
| 'id': element['id'], | |
| 'details': details | |
| }) | |
| # Apply filters based on qualifiers | |
| if is_affordable and spot_type == "hotels": | |
| # Filter for lower-star or budget accommodations if specified | |
| spots = [s for s in spots if s['details']['stars'] in ['1', '2', '3', 'Not rated']] | |
| elif is_luxury and spot_type == "hotels": | |
| # Filter for higher-star accommodations if specified | |
| spots = [s for s in spots if s['details']['stars'] in ['4', '5']] | |
| # Sort spots by relevance - prioritize those with more complete information | |
| def relevance_score(spot): | |
| score = 0 | |
| if spot['details']['phone'] != 'Not available': | |
| score += 1 | |
| if spot['details']['website'] != 'Not available': | |
| score += 2 | |
| if spot['details']['description'] != 'No description available': | |
| score += 3 | |
| if len(spot['details']['amenities']) > 0: | |
| score += len(spot['details']['amenities']) | |
| return score | |
| spots.sort(key=relevance_score, reverse=True) | |
| spots = spots[:5] # Limit to top 5 most relevant results | |
| # Cache the results | |
| st.session_state.cached_searches[search_cache_key] = spots | |
| else: | |
| spots = [] | |
| except Exception as e: | |
| st.error(f"Error in Overpass API request: {str(e)}") | |
| spots = [] | |
| if spots: | |
| # Generate professionally formatted response with rich details | |
| spot_list = [] | |
| for s in spots: | |
| # Create Google Maps direction link | |
| gmaps_url = f"https://www.google.com/maps/dir/?api=1&destination={s['lat']},{s['lon']}" | |
| # Create more info search link | |
| search_url = f"https://www.google.com/search?q={s['name'].replace(' ', '+')}+{location_query}+Bangladesh" | |
| # Format amenities list | |
| amenities_text = ", ".join(s['details']['amenities']) if s['details']['amenities'] else "Basic amenities" | |
| # Format opening hours | |
| hours_text = s['details']['opening_hours'] if s['details']['opening_hours'] != 'Not specified' else "Hours may vary" | |
| # Format qualifier-specific descriptions | |
| qualifier_prefix = "" | |
| if is_affordable and spot_type == "hotels": | |
| qualifier_prefix = "An affordable accommodation option with " | |
| elif is_luxury and spot_type == "hotels": | |
| qualifier_prefix = "A premium accommodation offering " | |
| elif is_family and spot_type == "hotels": | |
| qualifier_prefix = "A family-friendly stay with " | |
| else: | |
| if spot_type == "hotels": | |
| qualifier_prefix = "A hotel featuring " | |
| elif spot_type == "beaches": | |
| qualifier_prefix = "A beautiful beach with " | |
| elif spot_type == "attractions": | |
| qualifier_prefix = "A popular attraction offering " | |
| elif spot_type == "restaurants": | |
| qualifier_prefix = "A dining venue providing " | |
| else: | |
| qualifier_prefix = "A destination with " | |
| # Create a detailed professional description | |
| description = f"{qualifier_prefix}{amenities_text}. " | |
| if spot_type == "hotels": | |
| if s['details']['stars'] != 'Not rated': | |
| description += f"{s['details']['stars']}⭐ rated. " | |
| if s['details']['phone'] != 'Not available': | |
| description += f"Contact: {s['details']['phone']}. " | |
| description += f"Hours: {hours_text}." | |
| elif spot_type in ["beaches", "forests", "waterfalls"]: | |
| if s['details']['fee'] != 'Not specified': | |
| description += f"Entry fee: {s['details']['fee']}. " | |
| description += f"Access: {s['details']['wheelchair'] if s['details']['wheelchair'] != 'Not specified' else 'Information not available'}." | |
| else: | |
| description += f"Hours: {hours_text}." | |
| # Format each spot entry professionally | |
| spot_list.append(f"- **{s['name']}** ({s['lat']:.6f}, {s['lon']:.6f}) - {description} \n [📍 Directions]({gmaps_url}) | [🔍 More Info]({search_url})") | |
| # Add weather information if enabled | |
| weather_info = "" | |
| if include_weather and coords: | |
| lat, lon = coords | |
| weather_data = get_weather_data(lat, lon) | |
| if weather_data: | |
| weather_info = f"\n\n**Current Weather in {location_query}**: {weather_data['temperature']}°C, {weather_data['description']}, {weather_data['humidity']}% humidity. \n*Travel Tip*: {weather_data['recommendations'][0] if weather_data['recommendations'] else 'Check local forecasts for updates.'}" | |
| # Professional opening and closing text | |
| qualifier_text = "affordable " if is_affordable else "luxury " if is_luxury else "family-friendly " if is_family else "" | |
| radius_km = radius_meters/1000 | |
| opening_text = f"\n**Top {qualifier_text}{spot_type.title()} near {location_query}** (within {radius_km} km):" | |
| closing_text = f"\n\n**Travel Tips for {location_query}**: \n- {get_seasonal_tip(location_query)} \n- Carry local currency (Bangladeshi Taka) for small purchases. \n- Always respect local customs and dress modestly, especially at religious sites." | |
| # Combine all elements into a professional response | |
| additional_data = opening_text + "\n" + "\n".join(spot_list) + weather_info + closing_text | |
| else: | |
| additional_data = f"\nI couldn't find any {spot_type} within {radius_meters/1000}km of {location_query}. Would you like to try a different location or category?" | |
| # 2. Enhanced Location Detail Queries with comprehensive information | |
| elif is_detail_query: | |
| # Extract the spot name more accurately | |
| spot_name = None | |
| for keyword in detail_keywords: | |
| if keyword in prompt_lower: | |
| spot_name = prompt_lower.split(keyword)[-1].strip().replace("?", "").strip() | |
| break | |
| if not spot_name: | |
| spot_name = prompt_lower | |
| with st.spinner(f"Fetching comprehensive details for {spot_name}..."): | |
| # Use more precise Overpass query for better results | |
| overpass_url = "https://overpass-api.de/api/interpreter" | |
| query = f""" | |
| [out:json][timeout:30]; | |
| ( | |
| node["name"~"{spot_name}",i]; | |
| way["name"~"{spot_name}",i]; | |
| relation["name"~"{spot_name}",i]; | |
| ); | |
| out body center; | |
| """ | |
| try: | |
| response = requests.post(overpass_url, data={"data": query}, timeout=30) | |
| if response.status_code == 200: | |
| data = response.json() | |
| spots = [] | |
| for element in data.get('elements', []): | |
| if 'tags' in element and 'name' in element['tags']: | |
| name = element['tags']['name'] | |
| lat_val = element.get('lat', element.get('center', {}).get('lat', 0)) | |
| lon_val = element.get('lon', element.get('center', {}).get('lon', 0)) | |
| category = element['tags'].get('tourism', | |
| element['tags'].get('natural', | |
| element['tags'].get('amenity', | |
| element['tags'].get('historic', 'attraction')))) | |
| details = { | |
| 'phone': element['tags'].get('phone', 'Not available'), | |
| 'website': element['tags'].get('website', 'Not available'), | |
| 'address': element['tags'].get('addr:full', 'Not available'), | |
| 'description': element['tags'].get('description', 'No description available'), | |
| 'opening_hours': element['tags'].get('opening_hours', 'Not specified'), | |
| 'amenities': [], | |
| 'wheelchair': element['tags'].get('wheelchair', 'Not specified'), | |
| 'stars': element['tags'].get('stars', 'Not rated'), | |
| 'fee': element['tags'].get('fee', 'Not specified') | |
| } | |
| for tag in ['internet', 'wifi', 'swimming_pool', 'restaurant', 'bar', 'air_conditioning']: | |
| if element['tags'].get(tag) == 'yes': | |
| details['amenities'].append(tag.replace('_', ' ').title()) | |
| spots.append({ | |
| 'name': name, | |
| 'lat': lat_val, | |
| 'lon': lon_val, | |
| 'category': category, | |
| 'id': element['id'], | |
| 'details': details | |
| }) | |
| if spots: | |
| # Take the most relevant spot (highest detail) | |
| spot = max(spots, key=lambda s: len([k for k, v in s['details'].items() if v != 'Not available' and v != 'Not specified'])) | |
| # Fetch weather if enabled | |
| weather_info = "" | |
| if include_weather and spot['lat'] and spot['lon']: | |
| weather_data = get_weather_data(spot['lat'], spot['lon']) | |
| if weather_data: | |
| weather_info = (f"\n\n**Current Weather**: {weather_data['temperature']}°C, {weather_data['description']}, " | |
| f"{weather_data['humidity']}% humidity, Wind: {weather_data['wind_speed']} km/h. \n" | |
| f"**Tomorrow's Forecast**: {weather_data['forecast']['day1']['min_temp']}°C - " | |
| f"{weather_data['forecast']['day1']['max_temp']}°C, {weather_data['forecast']['day1']['description']}, " | |
| f"Rain: {'Yes' if weather_data['forecast']['day1']['rain_chance'] else 'No'}.") | |
| # Generate a professional description using LLM if needed | |
| spot_id = spot['id'] | |
| if spot_id not in st.session_state.spot_descriptions: | |
| prompt_desc = f"""As a Bangladesh tourism expert, write a concise (50-70 words), professional description for: | |
| - Name: {spot['name']} | |
| - Type: {spot['category'].replace('_', ' ').title()} | |
| - Location: {location_query}, Bangladesh | |
| Include: | |
| 1. Historical/cultural significance or unique features | |
| 2. Key activity for visitors | |
| 3. Practical tip based on weather: {weather_data['description'] if weather_data else 'unknown'}, {weather_data['temperature'] if weather_data else 'unknown'}°C | |
| Use a knowledgeable, engaging tone.""" | |
| messages_desc = [ | |
| {"role": "system", "content": "You are a Bangladesh tourism expert providing professional, engaging descriptions."}, | |
| {"role": "user", "content": prompt_desc} | |
| ] | |
| completion_desc = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-specdec", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=150, | |
| ), | |
| messages | |
| ) | |
| st.session_state.spot_descriptions[spot_id] = completion_desc.choices[0].message.content | |
| gmaps_url = f"https://www.google.com/maps/dir/?api=1&destination={spot['lat']},{spot['lon']}" | |
| search_url = f"https://www.google.com/search?q={urllib.parse.quote(spot['name'] + ' ' + location_query)}" | |
| additional_data = (f"\n**{spot['name']}** ({spot['category'].replace('_', ' ').title()} in {location_query})\n" | |
| f"- **Coordinates**: {spot['lat']:.6f}, {spot['lon']:.6f} \n" | |
| f"- **Description**: {st.session_state.spot_descriptions[spot_id]} \n" | |
| f"- **Contact**: {spot['details']['phone']} \n" | |
| f"- **Website**: {spot['details']['website']} \n" | |
| f"- **Address**: {spot['details']['address']} \n" | |
| f"- **Hours**: {spot['details']['opening_hours']} \n" | |
| f"- **Accessibility**: {spot['details']['wheelchair']} \n" | |
| f"- [📍 Directions]({gmaps_url}) | [🔍 More Info]({search_url}){weather_info}") | |
| else: | |
| additional_data = f"\nNo detailed information found for '{spot_name}'. Try refining your query or asking about a different location." | |
| except Exception as e: | |
| additional_data = f"\nError fetching details for '{spot_name}': {str(e)}. Try again or ask differently." | |
| # 3. Enhanced Weather Queries with detailed forecast and tips | |
| elif is_weather_query: | |
| location_query = prompt_lower.split("in")[-1].strip() if "in" in prompt_lower else "Bangladesh" | |
| with st.spinner(f"Fetching weather for {location_query}..."): | |
| coords = get_coordinates(location_query) | |
| if coords: | |
| lat, lon = coords | |
| weather_data = get_weather_data(lat, lon) | |
| if weather_data: | |
| forecast_text = (f"- **Today**: {weather_data['temperature']}°C, {weather_data['description']}, " | |
| f"Humidity: {weather_data['humidity']}%, Wind: {weather_data['wind_speed']} km/h \n" | |
| f"- **Tomorrow**: {weather_data['forecast']['day1']['min_temp']}°C - " | |
| f"{weather_data['forecast']['day1']['max_temp']}°C, {weather_data['forecast']['day1']['description']}, " | |
| f"Rain: {'Yes' if weather_data['forecast']['day1']['rain_chance'] else 'No'} \n" | |
| f"- **Day After**: {weather_data['forecast']['day2']['min_temp']}°C - " | |
| f"{weather_data['forecast']['day2']['max_temp']}°C, {weather_data['forecast']['day2']['description']}, " | |
| f"Rain: {'Yes' if weather_data['forecast']['day2']['rain_chance'] else 'No'}") | |
| tips = "\n".join([f"- {tip}" for tip in weather_data['recommendations']]) if weather_data['recommendations'] else "- Check local updates." | |
| additional_data = (f"\n**Weather Forecast for {location_query}**:\n{forecast_text}\n\n" | |
| f"**Travel Recommendations**:\n{tips}") | |
| else: | |
| additional_data = f"\nUnable to fetch weather data for '{location_query}'. Try again later." | |
| else: | |
| additional_data = f"\nNo location found for '{location_query}' to fetch weather data." | |
| # 4. Activity Queries with suggestions based on location | |
| elif is_activity_query: | |
| location_query = prompt_lower.split("in")[-1].strip() if "in" in prompt_lower else "Bangladesh" | |
| with st.spinner(f"Finding activities in {location_query}..."): | |
| coords = get_coordinates(location_query) | |
| if coords: | |
| lat, lon = coords | |
| overpass_url = "https://overpass-api.de/api/interpreter" | |
| query = f""" | |
| [out:json][timeout:30]; | |
| ( | |
| node["tourism"](around:{radius_meters},{lat},{lon}); | |
| way["tourism"](around:{radius_meters},{lat},{lon}); | |
| node["leisure"](around:{radius_meters},{lat},{lon}); | |
| way["leisure"](around:{radius_meters},{lat},{lon}); | |
| node["natural"](around:{radius_meters},{lat},{lon}); | |
| way["natural"](around:{radius_meters},{lat},{lon}); | |
| ); | |
| out body center; | |
| """ | |
| try: | |
| response = requests.post(overpass_url, data={"data": query}, timeout=30) | |
| data = response.json() | |
| activities = [] | |
| for element in data.get('elements', []): | |
| if 'tags' in element and 'name' in element['tags']: | |
| name = element['tags']['name'] | |
| category = element['tags'].get('tourism', element['tags'].get('leisure', element['tags'].get('natural', 'activity'))) | |
| lat_val = element.get('lat', element.get('center', {}).get('lat', lat)) | |
| lon_val = element.get('lon', element.get('center', {}).get('lon', lon)) | |
| activities.append({'name': name, 'category': category, 'lat': lat_val, 'lon': lon_val}) | |
| if activities: | |
| activity_list = [] | |
| for a in activities[:5]: # Limit to top 5 for brevity | |
| gmaps_url = f"https://www.google.com/maps/dir/?api=1&destination={a['lat']},{a['lon']}" | |
| suggestion = f"- **{a['name']}** ({a['category'].replace('_', ' ').title()}): Explore this {a['category']} \n [📍 Directions]({gmaps_url})" | |
| activity_list.append(suggestion) | |
| additional_data = f"\n**Activities in {location_query}**:\n" + "\n".join(activity_list) + "\n\n**Tip**: Check opening hours before visiting." | |
| else: | |
| additional_data = f"\nNo specific activities found in {location_query}. Try a different location or broader search." | |
| except Exception as e: | |
| additional_data = f"\nError finding activities: {str(e)}." | |
| else: | |
| additional_data = f"\nNo location found for '{location_query}'." | |
| # Helper function for seasonal tips | |
| # def get_seasonal_tip(location): | |
| # month = datetime.now().month | |
| # if month in [11, 12, 1, 2]: # Winter | |
| # return f"Visit during {location}'s cool, dry season (November-February) for pleasant weather." | |
| # elif month in [3, 4, 5]: # Summer | |
| # return f"Prepare for {location}'s hot season (March-May); early mornings are best for outdoor activities." | |
| # else: # Monsoon | |
| # return f"Bring rain gear for {location}'s monsoon season (June-October); indoor attractions may be ideal." | |
| # Generate final response with LLM | |
| system_prompt += additional_data | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": prompt} | |
| ] | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-specdec", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=2048, | |
| stream = True | |
| ), | |
| messages | |
| ) | |
| for chunk in completion: | |
| if chunk.choices[0].delta.content: | |
| full_response += chunk.choices[0].delta.content | |
| time.sleep(0.02) | |
| message_placeholder.markdown(full_response + "▌") | |
| message_placeholder.markdown(full_response) | |
| except Exception as e: | |
| full_response = f"Sorry, I couldn’t process your request due to an error: {str(e)}. Please try again or refine your question!" | |
| message_placeholder.error(full_response) | |
| st.session_state.messages.append({"role": "assistant", "content": full_response}) | |
| # Chat Tips with improved guidance | |
| with st.expander("💡 How to Use This Assistant", expanded=False): | |
| st.markdown(""" | |
| **Maximize Your Experience:** | |
| - **Search**: "Find affordable hotels in Chittagong" or "Show me beaches near Cox's Bazar" (adjust radius in settings). | |
| - **Details**: "Tell me about Sundarbans" for in-depth info. | |
| - **Weather**: "What's the weather like in Dhaka?" for forecasts and tips. | |
| - **Activities**: "Things to do in Sylhet" for tailored suggestions. | |
| - Use 👍/👎 to help me improve responses! | |
| """) | |
| # ------------- Trip Planner Tab (tab3) ------------- | |
| import streamlit as st | |
| import folium | |
| from streamlit_folium import folium_static | |
| import requests | |
| import polyline | |
| import geopy.distance | |
| import pandas as pd | |
| import re | |
| from folium import plugins | |
| import json | |
| from datetime import datetime, timedelta | |
| # Assuming 'client' (Groq client) is already defined in the broader scope from your original code | |
| # If not, you'd need to initialize it here with: client = Groq(api_key=os.getenv("GROQ_API_KEY")) | |
| with tab3: | |
| st.header("Plan Your Trip", help="Create a customized travel itinerary") | |
| # Define tabs for Bangladesh planner, Global planner, and Saved Plans | |
| plan_input, global_plan_input, saved_plans = st.tabs(["Create New Plan (Bangladesh)", "Global Trip Planner", "Saved Plans"]) | |
| # ------------- Bangladesh Trip Planner ------------- | |
| with plan_input: | |
| with st.form("trip_planner_form"): | |
| st.subheader("Trip Details") | |
| start_date = st.date_input("Start Date:", datetime.now().date() + timedelta(days=7), | |
| help="When do you plan to start your trip?") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| budget = st.number_input("Budget (Taka):", min_value=1000, value=10000, step=1000, | |
| help="Your total budget for the trip in Bangladeshi Taka") | |
| with col2: | |
| duration = st.number_input("Duration (Days):", min_value=1, value=3, step=1, | |
| help="Number of days for your trip") | |
| with col3: | |
| travelers = st.number_input("Number of Travelers:", min_value=1, value=2, step=1, | |
| help="How many people are traveling") | |
| location_col1, location_col2 = st.columns([2, 1]) | |
| with location_col1: | |
| location = st.text_input("Starting Location:", placeholder="e.g., Dhaka", | |
| help="Your departure point in Bangladesh") | |
| with location_col2: | |
| travel_mode = st.selectbox("Travel Mode:", | |
| ["No Preference", "Public Transport", "Rental Car", "Private Vehicle", "Flight", "Guided Tour"], | |
| help="Your preferred mode of transportation") | |
| st.subheader("Travel Preferences") | |
| pref_col1, pref_col2 = st.columns(2) | |
| with pref_col1: | |
| accommodation_type = st.selectbox("Accommodation Type:", | |
| ["Budget Friendly", "Mid-range", "Luxury", "Mix of options"], | |
| help="Select your preferred accommodation category") | |
| food_preferences = st.multiselect("Food Preferences:", | |
| ["Local Cuisine", "International", "Vegetarian", "Halal", "Street Food", "Fine Dining"], | |
| default=["Local Cuisine"], | |
| help="Select your food preferences") | |
| with pref_col2: | |
| preference_options = ["Nature", "History", "Beaches", "Mountains", "Cultural Sites", | |
| "Food", "Adventure", "Relaxation", "Shopping", "Wildlife"] | |
| preferences = st.multiselect("Activity Preferences:", preference_options, | |
| default=["Nature"], | |
| help="Select your travel preferences") | |
| pace = st.select_slider("Travel Pace:", | |
| options=["Very Relaxed", "Relaxed", "Moderate", "Active", "Very Active"], | |
| value="Moderate", | |
| help="How packed do you want your itinerary to be?") | |
| with st.expander("Advanced Options"): | |
| col_adv1, col_adv2 = st.columns(2) | |
| with col_adv1: | |
| avoid_locations = st.text_input("Avoid Locations:", | |
| placeholder="e.g., Chittagong, Cox's Bazar", | |
| help="Locations you want to avoid, separated by commas") | |
| must_visit = st.text_input("Must-Visit Places:", | |
| placeholder="e.g., Sundarbans, Srimangal", | |
| help="Places you definitely want to include, separated by commas") | |
| with col_adv2: | |
| max_travel_hours = st.slider("Max Travel Hours Per Day:", | |
| min_value=1, max_value=12, value=6, | |
| help="Maximum hours you're willing to spend traveling per day") | |
| accessibility_needs = st.checkbox("Accessibility Requirements", | |
| help="Check if you have special accessibility needs") | |
| special_requests = st.text_area("Special Requests:", | |
| placeholder="Any special requirements or requests? (allergies, specific interests, etc.)", | |
| help="Additional information to customize your trip") | |
| plan_name = st.text_input("Save Plan As:", | |
| placeholder="e.g., Family Trip 2025", | |
| help="Give your trip plan a name to save it for later") | |
| submitted = st.form_submit_button("🗺️ Plan My Trip", use_container_width=True) | |
| # ------------- Global Trip Planner ------------- | |
| with global_plan_input: | |
| with st.form("global_trip_planner_form"): | |
| st.subheader("Global Trip Details") | |
| start_date_global = st.date_input("Start Date:", datetime.now().date() + timedelta(days=7), | |
| key="global_start_date", help="When do you plan to start your global trip?") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| budget_global = st.number_input("Budget (USD):", min_value=100, value=1000, step=100, | |
| key="global_budget", help="Your total budget in US Dollars") | |
| with col2: | |
| duration_global = st.number_input("Duration (Days):", min_value=1, value=7, step=1, | |
| key="global_duration", help="Number of days for your trip") | |
| with col3: | |
| travelers_global = st.number_input("Number of Travelers:", min_value=1, value=2, step=1, | |
| key="global_travelers", help="How many people are traveling") | |
| location_col1, location_col2 = st.columns([2, 1]) | |
| with location_col1: | |
| start_location_global = st.text_input("Starting Location:", | |
| placeholder="e.g., New York, Tokyo", | |
| key="global_start_location", | |
| help="Your departure point anywhere in the world") | |
| with location_col2: | |
| countries = st.text_input("Countries to Visit:", | |
| placeholder="e.g., Japan, Italy, Brazil", | |
| key="global_countries", | |
| help="List countries you want to visit, separated by commas") | |
| travel_mode_global = st.selectbox("Travel Mode:", | |
| ["No Preference", "Air Travel", "Train", "Road Trip", "Cruise", "Mixed Modes"], | |
| key="global_travel_mode", | |
| help="Your preferred mode of transportation globally") | |
| st.subheader("Travel Preferences") | |
| pref_col1, pref_col2 = st.columns(2) | |
| with pref_col1: | |
| accommodation_type_global = st.selectbox("Accommodation Type:", | |
| ["Budget Friendly", "Mid-range", "Luxury", "Hostels", "Vacation Rentals", "Mix of options"], | |
| key="global_accommodation", | |
| help="Select your preferred accommodation category") | |
| food_preferences_global = st.multiselect("Food Preferences:", | |
| ["Local Cuisine", "International", "Vegetarian", "Vegan", "Halal", "Street Food", "Fine Dining"], | |
| default=["Local Cuisine"], | |
| key="global_food", | |
| help="Select your food preferences") | |
| with pref_col2: | |
| preference_options_global = ["Nature", "History", "Beaches", "Mountains", "Cultural Sites", | |
| "Food", "Adventure", "Relaxation", "Shopping", "Wildlife", "Urban Exploration"] | |
| preferences_global = st.multiselect("Activity Preferences:", preference_options_global, | |
| default=["Nature"], | |
| key="global_preferences", | |
| help="Select your travel preferences") | |
| pace_global = st.select_slider("Travel Pace:", | |
| options=["Very Relaxed", "Relaxed", "Moderate", "Active", "Very Active"], | |
| value="Moderate", | |
| key="global_pace", | |
| help="How packed do you want your itinerary to be?") | |
| with st.expander("Advanced Options"): | |
| col_adv1, col_adv2 = st.columns(2) | |
| with col_adv1: | |
| avoid_locations_global = st.text_input("Avoid Locations:", | |
| placeholder="e.g., Florida, Mumbai", | |
| key="global_avoid", | |
| help="Locations or regions you want to avoid, separated by commas") | |
| must_visit_global = st.text_input("Must-Visit Places:", | |
| placeholder="e.g., Eiffel Tower, Great Wall", | |
| key="global_must_visit", | |
| help="Specific places you definitely want to include, separated by commas") | |
| with col_adv2: | |
| max_travel_hours_global = st.slider("Max Travel Hours Per Day:", | |
| min_value=1, max_value=24, value=8, | |
| key="global_max_travel", | |
| help="Maximum hours you're willing to spend traveling per day") | |
| visa_needs = st.checkbox("Include Visa Information", | |
| key="global_visa", | |
| help="Check if you need visa requirements included") | |
| special_requests_global = st.text_area("Special Requests:", | |
| placeholder="e.g., Need pet-friendly options, prefer eco-friendly travel", | |
| key="global_requests", | |
| help="Additional information to customize your global trip") | |
| plan_name_global = st.text_input("Save Plan As:", | |
| placeholder="e.g., World Adventure 2025", | |
| key="global_plan_name", | |
| help="Give your global trip plan a name to save it for later") | |
| submitted_global = st.form_submit_button("🌍 Plan My Global Trip", use_container_width=True) | |
| # ------------- Saved Plans ------------- | |
| with saved_plans: | |
| st.subheader("Your Saved Trip Plans") | |
| if 'saved_trip_plans' not in st.session_state: | |
| st.session_state.saved_trip_plans = [] | |
| if len(st.session_state.saved_trip_plans) == 0: | |
| st.info("You haven't saved any trip plans yet. Create a new plan and save it to see it here.") | |
| else: | |
| for i, plan in enumerate(st.session_state.saved_trip_plans): | |
| plan_type = plan.get("type", "Bangladesh") | |
| with st.expander(f"{plan['name']} ({plan['location']} - {plan['duration']} days) - {plan_type}"): | |
| st.markdown(plan['summary']) | |
| col1, col2, col3 = st.columns([1, 1, 1]) | |
| with col1: | |
| st.button("View Details", key=f"view_{i}") | |
| with col2: | |
| st.button("Edit Plan", key=f"edit_{i}") | |
| with col3: | |
| st.button("Delete", key=f"delete_{i}") | |
| # ------------- Handle Bangladesh Trip Submission ------------- | |
| if submitted: | |
| if not location: | |
| st.error("Please enter a starting location.") | |
| elif not plan_name: | |
| st.warning("Please give your trip plan a name to save it.") | |
| else: | |
| with st.spinner("Planning your trip... This might take a moment."): | |
| try: | |
| preferences_str = ", ".join(preferences) if preferences else "No specific preferences" | |
| food_pref_str = ", ".join(food_preferences) if food_preferences else "No specific food preferences" | |
| must_visit_str = must_visit if must_visit else "No specific must-visit places" | |
| avoid_locations_str = avoid_locations if avoid_locations else "No places to avoid" | |
| end_date = start_date + timedelta(days=duration) | |
| prompt = f"""Please create a detailed travel plan in Bangladesh with the following details: | |
| - Starting location: {location} | |
| - Trip dates: {start_date.strftime('%d %b, %Y')} to {end_date.strftime('%d %b, %Y')} | |
| - Budget: {budget} Taka | |
| - Duration: {duration} days | |
| - Number of travelers: {travelers} | |
| - Preferred travel mode: {travel_mode} | |
| - Accommodation preference: {accommodation_type} | |
| - Food preferences: {food_pref_str} | |
| - Activity preferences: {preferences_str} | |
| - Travel pace: {pace} | |
| - Must-visit locations: {must_visit_str} | |
| - Locations to avoid: {avoid_locations_str} | |
| - Maximum travel time per day: {max_travel_hours} hours | |
| - Accessibility requirements: {"Yes" if accessibility_needs else "No"} | |
| - Special requests: {special_requests if special_requests else 'None'} | |
| The plan should include: | |
| 1. Suggested destinations in sequence of visit with specific dates and provide google directions link and more info search google link for each. some times the link are broken. so make sure you gave accurate googel direction link and more info search link. | |
| 2. Specific accommodation options within budget (with exact names, estimated prices, and brief descriptions). I repeat make the budget based on user budget. do not make very difference | |
| 3. Transportation recommendations between destinations (with estimated costs, travel times, and departure times) | |
| 4. Daily activities and sightseeing with timing (morning/afternoon/evening) | |
| 5. Recommended restaurants for each meal with cuisine types and price ranges | |
| 6. Detailed cost breakdown to ensure it stays within budget | |
| 7. Travel tips specific to the locations and time of year | |
| 8. Practical information about each destination (weather, local customs, etc.) | |
| Please be specific about locations in Bangladesh, include exact names of hotels/resorts, tourist spots, and keep the plan within budget.""" | |
| messages = [ | |
| {"role": "system", "content": "You are a professional travel planner specializing in Bangladesh tourism. Provide detailed, practical, and budget-conscious travel plans with specific locations, accommodations, and activities."}, | |
| {"role": "user", "content": prompt} | |
| ] | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-specdec", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=2500, | |
| ), | |
| messages | |
| ) | |
| plan = completion.choices[0].message.content | |
| plan_summary = f"📍 **From:** {location}\n📅 **Dates:** {start_date.strftime('%d %b')} - {end_date.strftime('%d %b, %Y')}\n💰 **Budget:** {budget:,} Taka\n👥 **Travelers:** {travelers}" | |
| plan_data = { | |
| "name": plan_name, | |
| "location": location, | |
| "duration": duration, | |
| "budget": budget, | |
| "start_date": start_date.strftime('%Y-%m-%d'), | |
| "travelers": travelers, | |
| "summary": plan_summary, | |
| "full_plan": plan, | |
| "created_on": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), | |
| "type": "Bangladesh" | |
| } | |
| st.session_state.saved_trip_plans.append(plan_data) | |
| plan_tab1, plan_tab2, plan_tab3, plan_tab4 = st.tabs(["Complete Plan", "Route Map", "Cost Breakdown", "Day-by-Day"]) | |
| with plan_tab1: | |
| st.subheader(f"Your Trip Plan: {plan_name}") | |
| with st.container(): | |
| cols = st.columns([1, 1, 1, 1]) | |
| with cols[0]: | |
| st.markdown("**From:**") | |
| st.markdown(f"### {location}") | |
| with cols[1]: | |
| st.markdown("**Duration:**") | |
| st.markdown(f"### {duration} days") | |
| with cols[2]: | |
| st.markdown("**Budget:**") | |
| st.markdown(f"### {budget:,} Tk") | |
| with cols[3]: | |
| st.markdown("**Travelers:**") | |
| st.markdown(f"### {travelers}") | |
| st.divider() | |
| st.markdown(plan) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.download_button( | |
| label="📄 Download as Text", | |
| data=plan.encode(), | |
| file_name=f"{plan_name.replace(' ', '_')}.txt", | |
| mime="text/plain", | |
| ) | |
| with col2: | |
| st.download_button( | |
| label="📋 Download as PDF", | |
| data=plan.encode(), | |
| file_name=f"{plan_name.replace(' ', '_')}.txt", | |
| mime="text/plain", | |
| ) | |
| with plan_tab2: | |
| with st.spinner("Creating detailed route map..."): | |
| extract_prompt = f"""Extract all specific location names (cities, towns, tourist spots, etc.) mentioned in this travel plan that will be visited in sequence. | |
| Provide only a comma-separated list with no additional text or explanation, starting with the departure location: | |
| {plan}""" | |
| messages = [ | |
| {"role": "system", "content": "You extract specific location names from text without adding any comments or explanations."}, | |
| {"role": "user", "content": extract_prompt} | |
| ] | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.1-8b-instant", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=200, | |
| ), | |
| messages | |
| ) | |
| locations_text = completion.choices[0].message.content | |
| locations = [loc.strip() for loc in locations_text.split(',')] | |
| if location not in locations: | |
| locations = [location] + locations | |
| coords = [] | |
| for loc in locations: | |
| try: | |
| cache_key = loc.lower().replace(' ', '_') | |
| cached_location = st.session_state.get(f'geo_cache_{cache_key}') | |
| if cached_location: | |
| coords.append((loc, cached_location[0], cached_location[1])) | |
| else: | |
| nominatim_url = f"https://nominatim.openstreetmap.org/search?q={loc}, Bangladesh&format=json" | |
| response = requests.get(nominatim_url, headers={'User-Agent': 'TouristApp/1.0'}) | |
| data = response.json() | |
| if data: | |
| lat = float(data[0]['lat']) | |
| lon = float(data[0]['lon']) | |
| st.session_state[f'geo_cache_{cache_key}'] = (lat, lon) | |
| coords.append((loc, lat, lon)) | |
| except Exception as e: | |
| st.warning(f"Could not find coordinates for {loc}: {str(e)}") | |
| continue | |
| if len(coords) > 1: | |
| st.subheader("Trip Route Map") | |
| st.markdown("This map shows your complete travel route. Use the tools to zoom, measure distances, or view in fullscreen.") | |
| trip_map = folium.Map(location=[23.8103, 90.4125], zoom_start=9, control_scale=True) | |
| title_html = f''' | |
| <div style="position: fixed; | |
| top: 10px; left: 50px; width: 250px; height: 30px; | |
| background-color: white; border-radius: 5px; z-index: 900; | |
| font-size: 14pt; font-weight: bold; text-align: center; | |
| line-height: 30px;"> | |
| {plan_name} Route | |
| </div> | |
| ''' | |
| trip_map.get_root().html.add_child(folium.Element(title_html)) | |
| bounds = [] | |
| for i, (name, lat, lon) in enumerate(coords): | |
| if i == 0: | |
| icon = folium.Icon(color='green', icon='play', prefix='fa') | |
| popup_content = f""" | |
| <div style="width: 200px;"> | |
| <h4 style="color:green;">Starting Point</h4> | |
| <b>{name}</b><br> | |
| Day: 1<br> | |
| <i>Your journey begins here</i> | |
| </div> | |
| """ | |
| elif i == len(coords) - 1: | |
| icon = folium.Icon(color='red', icon='flag-checkered', prefix='fa') | |
| popup_content = f""" | |
| <div style="width: 200px;"> | |
| <h4 style="color:red;">Final Destination</h4> | |
| <b>{name}</b><br> | |
| Day: {min(i+1, duration)}<br> | |
| <i>Your journey ends here</i> | |
| </div> | |
| """ | |
| else: | |
| days_estimate = min(i+1, duration) | |
| icon = folium.DivIcon(html=f'<div style="font-size: 12pt; color: white; background-color: #3186cc; border-radius: 50%; text-align: center; width: 25px; height: 25px; line-height: 25px;"><b>{i}</b></div>') | |
| popup_content = f""" | |
| <div style="width: 200px;"> | |
| <h4 style="color:#3186cc;">Stop #{i}</h4> | |
| <b>{name}</b><br> | |
| Approximate Day: {days_estimate}<br> | |
| </div> | |
| """ | |
| folium.Marker( | |
| location=[lat, lon], | |
| popup=folium.Popup(popup_content, max_width=250), | |
| tooltip=f"{i}. {name}", | |
| icon=icon | |
| ).add_to(trip_map) | |
| bounds.append([lat, lon]) | |
| for i in range(len(coords) - 1): | |
| start = coords[i] | |
| end = coords[i + 1] | |
| try: | |
| osrm_url = f"http://router.project-osrm.org/route/v1/driving/{start[2]},{start[1]};{end[2]},{end[1]}?overview=full&geometries=polyline" | |
| response = requests.get(osrm_url, timeout=5) | |
| route_data = response.json() | |
| if route_data.get('code') == 'Ok' and route_data.get('routes'): | |
| route = route_data['routes'][0] | |
| distance = round(route['distance'] / 1000, 2) | |
| duration_mins = round(route['duration'] / 60) | |
| if duration_mins >= 60: | |
| hours = duration_mins // 60 | |
| mins = duration_mins % 60 | |
| duration_text = f"{hours}h {mins}m" | |
| else: | |
| duration_text = f"{duration_mins}m" | |
| decoded_polyline = polyline.decode(route['geometry']) | |
| folium.PolyLine( | |
| locations=decoded_polyline, | |
| weight=4, | |
| color=f'#{(i * 40) % 256:02x}40ff', | |
| opacity=0.8, | |
| tooltip=f"{start[0]} to {end[0]}: {distance} km ({duration_text})" | |
| ).add_to(trip_map) | |
| if len(decoded_polyline) > 10: | |
| mid_idx = len(decoded_polyline) // 2 | |
| mid_point = decoded_polyline[mid_idx] | |
| distance_icon = folium.DivIcon( | |
| html=f''' | |
| <div style="background-color:white; border:2px solid gray; | |
| border-radius:50%; width:auto; height:auto; padding:3px; | |
| font-size:10pt; text-align:center; white-space:nowrap;"> | |
| {distance} km | |
| </div> | |
| ''' | |
| ) | |
| folium.Marker( | |
| location=mid_point, | |
| icon=distance_icon, | |
| ).add_to(trip_map) | |
| else: | |
| folium.PolyLine( | |
| locations=[[start[1], start[2]], [end[1], end[2]]], | |
| weight=3, | |
| color='gray', | |
| opacity=0.6, | |
| dash_array='5', | |
| tooltip=f"Direct line: {start[0]} to {end[0]} (approximate)" | |
| ).add_to(trip_map) | |
| except Exception: | |
| folium.PolyLine( | |
| locations=[[start[1], start[2]], [end[1], end[2]]], | |
| weight=3, | |
| color='gray', | |
| opacity=0.6, | |
| dash_array='5', | |
| tooltip=f"Direct line: {start[0]} to {end[0]} (approximate)" | |
| ).add_to(trip_map) | |
| folium.LayerControl().add_to(trip_map) | |
| plugins.Fullscreen().add_to(trip_map) | |
| plugins.MeasureControl(position='bottomleft', primary_length_unit='kilometers').add_to(trip_map) | |
| trip_map.fit_bounds(bounds) | |
| st.markdown(""" | |
| <style> | |
| .map-container { | |
| border: 2px solid #ddd; | |
| border-radius: 5px; | |
| padding: 5px; | |
| box-shadow: 0 0 5px rgba(0,0,0,0.1); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| with st.container(): | |
| st.markdown('<div class="map-container">', unsafe_allow_html=True) | |
| folium_static(trip_map, width=800, height=600) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.subheader("Destinations & Journey Details") | |
| distances = [] | |
| durations = [] | |
| total_distance = 0 | |
| total_duration = 0 | |
| for i in range(len(coords) - 1): | |
| start = coords[i] | |
| end = coords[i + 1] | |
| try: | |
| osrm_url = f"http://router.project-osrm.org/route/v1/driving/{start[2]},{start[1]};{end[2]},{end[1]}?overview=false" | |
| response = requests.get(osrm_url, timeout=3) | |
| route_data = response.json() | |
| if route_data.get('code') == 'Ok' and route_data.get('routes'): | |
| distance = round(route_data['routes'][0]['distance'] / 1000, 2) | |
| duration = round(route_data['routes'][0]['duration'] / 60) | |
| else: | |
| start_point = (start[1], start[2]) | |
| end_point = (end[1], end[2]) | |
| distance = round(geopy.distance.distance(start_point, end_point).kilometers, 2) | |
| duration = round(distance * 60 / 50) | |
| except Exception: | |
| start_point = (start[1], start[2]) | |
| end_point = (end[1], end[2]) | |
| distance = round(geopy.distance.distance(start_point, end_point).kilometers, 2) | |
| duration = round(distance * 60 / 50) | |
| distances.append(distance) | |
| durations.append(duration) | |
| total_distance += distance | |
| total_duration += duration | |
| location_data = [] | |
| current_date = start_date | |
| current_time = datetime(2000, 1, 1, 9, 0) | |
| for i, (name, lat, lon) in enumerate(coords): | |
| if i > 0: | |
| travel_days = max(1, durations[i-1] // (max_travel_hours * 60)) | |
| current_date += timedelta(days=travel_days) | |
| travel_mins = durations[i-1] | |
| current_time = (datetime.combine(current_date, current_time.time()) + | |
| timedelta(minutes=travel_mins)) | |
| if current_time.hour >= 20: | |
| current_date += timedelta(days=1) | |
| current_time = datetime(2000, 1, 1, 9, 0) | |
| day_number = (current_date - start_date).days + 1 | |
| if day_number > duration: | |
| day_text = f"Beyond plan" | |
| else: | |
| day_text = f"Day {day_number}" | |
| row_data = { | |
| "Stop": i + 1, | |
| "Location": name, | |
| "Estimated Day": day_text, | |
| "Date": current_date.strftime("%d %b"), | |
| } | |
| if i < len(coords) - 1: | |
| hours = durations[i] // 60 | |
| mins = durations[i] % 60 | |
| duration_text = f"{hours}h {mins}m" if hours > 0 else f"{mins}m" | |
| row_data["Distance to Next"] = f"{distances[i]:.1f} km" | |
| row_data["Travel Time"] = duration_text | |
| else: | |
| row_data["Distance to Next"] = "-" | |
| row_data["Travel Time"] = "-" | |
| location_data.append(row_data) | |
| location_df = pd.DataFrame(location_data) | |
| st.dataframe(location_df, use_container_width=True, hide_index=True) | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("Total Distance", f"{total_distance:.1f} km") | |
| with col2: | |
| total_hours = total_duration // 60 | |
| total_mins = total_duration % 60 | |
| st.metric("Total Travel Time", f"{total_hours}h {total_mins}m") | |
| with col3: | |
| st.metric("Destinations", f"{len(coords)} locations") | |
| if total_distance > 500: | |
| st.info("💡 This itinerary involves significant travel distances. Consider overnight stays between destinations to avoid fatigue.") | |
| elif total_distance > 200: | |
| st.info("💡 Plan for rest stops every 2-3 hours during longer journey segments.") | |
| with plan_tab3: | |
| with st.spinner("Creating detailed cost breakdown..."): | |
| cost_prompt = f"""Extract and organize the cost breakdown from this travel plan in a structured way. | |
| Categorize expenses into these exact categories: Accommodation, Transportation, Food, Activities, and Miscellaneous. | |
| For each category, list each item with its cost in Tk (Taka), days (if applicable), and notes (if applicable). | |
| Provide a summary with totals for each category and overall total, per person, and per day costs. | |
| Format the output as plain text with clear sections and no JSON. Use the following structure: | |
| Accommodation: | |
| - Item: [description], Cost: [cost] Tk, Days: [days], Notes: [notes] | |
| Transportation: | |
| - Item: [description], Cost: [cost] Tk, Notes: [notes] | |
| Food: | |
| - Item: [description], Cost: [cost] Tk, Notes: [notes] | |
| Activities: | |
| - Item: [description], Cost: [cost] Tk, Notes: [notes] | |
| Miscellaneous: | |
| - Item: [description], Cost: [cost] Tk, Notes: [notes] | |
| Summary: | |
| - Total Cost: [total] Tk | |
| - Accommodation Total: [accommodation_total] Tk | |
| - Transportation Total: [transportation_total] Tk | |
| - Food Total: [food_total] Tk | |
| - Activities Total: [activities_total] Tk | |
| - Miscellaneous Total: [miscellaneous_total] Tk | |
| - Per Person: [per_person] Tk | |
| - Per Day: [per_day] Tk | |
| If you need to add any additional notes, please add them after the entire summary section with a clear "Note:" prefix on a separate line. | |
| If exact costs or details are missing, estimate based on typical Bangladesh tourism prices and note it as an estimate in the notes field. | |
| Based on this plan: | |
| {plan}""" | |
| messages = [ | |
| {"role": "system", "content": "You extract and organize cost information from travel plans into a structured plain text format."}, | |
| {"role": "user", "content": cost_prompt} | |
| ] | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-specdec", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=1000, | |
| ), | |
| messages | |
| ) | |
| cost_text = completion.choices[0].message.content.strip() | |
| try: | |
| # Display the cost breakdown as plain text | |
| st.subheader("Detailed Cost Breakdown") | |
| st.text(cost_text) | |
| # Extract cost data from text format | |
| lines = cost_text.split('\n') | |
| categories = {"Accommodation": [], "Transportation": [], "Food": [], "Activities": [], "Miscellaneous": []} | |
| summary_data = {} | |
| current_category = None | |
| summary_section = False | |
| notes = [] | |
| # Parse the text to extract items and summary data | |
| for line in lines: | |
| line = line.strip() | |
| # Skip empty lines | |
| if not line: | |
| continue | |
| # Check for note section | |
| if line.startswith("Note:"): | |
| notes.append(line) | |
| continue | |
| # Check for category headers | |
| if any(line.startswith(cat + ":") for cat in categories.keys()): | |
| current_category = line.split(":")[0].strip() | |
| continue | |
| # Check for summary section | |
| if line == "Summary:" or "Summary:" in line: | |
| summary_section = True | |
| current_category = None | |
| continue | |
| # Parse items in categories | |
| if current_category and current_category in categories and line.startswith("- Item:"): | |
| item_data = {"item": "", "cost": 0, "days": 1, "notes": ""} | |
| parts = line.split(", ") | |
| for part in parts: | |
| part = part.strip() | |
| if part.startswith("- Item:"): | |
| item_data["item"] = part.replace("- Item:", "").strip() | |
| elif "Cost:" in part: | |
| cost_str = part.replace("Cost:", "").replace("Tk", "").strip() | |
| try: | |
| item_data["cost"] = float(cost_str.replace(",", "")) | |
| except ValueError: | |
| item_data["cost"] = 0 | |
| elif "Days:" in part: | |
| days_str = part.replace("Days:", "").strip() | |
| try: | |
| item_data["days"] = int(days_str) | |
| except ValueError: | |
| item_data["days"] = 1 | |
| elif "Notes:" in part: | |
| item_data["notes"] = part.replace("Notes:", "").strip() | |
| # Add the item to the current category | |
| categories[current_category].append(item_data) | |
| # Parse summary data | |
| if summary_section and line.startswith("-"): | |
| parts = line.split(":") | |
| if len(parts) >= 2: | |
| key = parts[0].replace("-", "").strip() | |
| value_part = parts[1].strip() | |
| # Extract numeric value | |
| value_str = value_part.replace("Tk", "").strip() | |
| try: | |
| value = float(value_str.replace(",", "")) | |
| summary_data[key] = value | |
| except ValueError: | |
| summary_data[key] = 0 | |
| # Create a pie chart for category totals | |
| if "Total Cost" in summary_data: | |
| category_totals = { | |
| "Accommodation": summary_data.get("Accommodation Total", 0), | |
| "Transportation": summary_data.get("Transportation Total", 0), | |
| "Food": summary_data.get("Food Total", 0), | |
| "Activities": summary_data.get("Activities Total", 0), | |
| "Miscellaneous": summary_data.get("Miscellaneous Total", 0) | |
| } | |
| # Filter out zero values | |
| category_totals = {k: v for k, v in category_totals.items() if v > 0} | |
| if category_totals: | |
| # Display summary metrics | |
| st.subheader("Summary Metrics") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("Total Cost", f"{int(summary_data.get('Total Cost', 0)):,} Tk") | |
| with col2: | |
| st.metric("Per Person", f"{int(summary_data.get('Per Person', 1)):,} Tk") | |
| with col3: | |
| st.metric("Per Day", f"{int(summary_data.get('Per Day', 2)):,} Tk") | |
| # Display detailed breakdown tables | |
| st.subheader("Detailed Item Breakdown") | |
| for category, items in categories.items(): | |
| if items: | |
| st.write(f"**{category}**") | |
| df = pd.DataFrame(items) | |
| # Format cost column with comma separators and 'Tk' suffix | |
| df['cost'] = df['cost'].apply(lambda x: f"{int(x):,} Tk") | |
| # Rename columns for better display | |
| df.columns = ['Item', 'Cost', 'Days', 'Notes'] | |
| st.table(df) | |
| # Display any notes | |
| if notes: | |
| st.subheader("Additional Notes") | |
| for note in notes: | |
| st.write(note) | |
| except Exception as e: | |
| st.error(f"Error processing cost breakdown: {str(e)}") | |
| st.markdown("### Raw Cost Breakdown") | |
| st.text(cost_text) | |
| # Day-by-day itinerary tab | |
| with plan_tab4: | |
| st.subheader("Day-by-Day Itinerary") | |
| with st.spinner("Organizing your daily schedule..."): | |
| day_prompt = f"""Extract and organize a day-by-day itinerary from this travel plan. For each day, include: | |
| - Date (e.g., "12 Mar, 2025") | |
| - Location(s) visited | |
| - Activities with approximate timing (morning, afternoon, evening) | |
| - Accommodation details (name and location) | |
| - Meals (breakfast, lunch, dinner) with recommended restaurants | |
| Format the output as a JSON object with this structure: | |
| {{ | |
| "days": [ | |
| {{ | |
| "day": 1, | |
| "date": "12 Mar, 2025", | |
| "location": "Dhaka", | |
| "activities": [ | |
| {{"time": "morning", "description": "Visit Lalbagh Fort"}}, | |
| ... | |
| ], | |
| "accommodation": "Hotel XYZ, Dhaka", | |
| "meals": {{ | |
| "breakfast": "Hotel XYZ Restaurant", | |
| "lunch": "Local Dhaba", | |
| "dinner": "Fakruddin Biryani" | |
| }} | |
| }}, | |
| ... | |
| ] | |
| }} | |
| Based on this plan: | |
| {plan}""" | |
| messages = [ | |
| {"role": "system", "content": "You extract and organize daily itineraries from travel plans into a structured JSON format."}, | |
| {"role": "user", "content": day_prompt} | |
| ] | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-specdec", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=1500, | |
| ), | |
| messages | |
| ) | |
| day_json_str = completion.choices[0].message.content.strip() | |
| day_extracted_json = extract_json_from_markdown(day_json_str) | |
| try: | |
| day_data = json.loads(day_extracted_json) | |
| days = day_data["days"] | |
| # Display each day as an expander | |
| for day in days: | |
| with st.expander(f"Day {day['day']} - {day['date']} ({day['location']})"): | |
| st.markdown(f"**Location:** {day['location']}") | |
| # Activities | |
| st.markdown("**Activities:**") | |
| for activity in day["activities"]: | |
| st.write(f"- {activity['time'].capitalize()}: {activity['description']}") | |
| # Accommodation | |
| st.markdown(f"**Accommodation:** {day['accommodation']}") | |
| # Meals | |
| st.markdown("**Meals:**") | |
| meals = day["meals"] | |
| st.write(f"- Breakfast: {meals['breakfast']}") | |
| st.write(f"- Lunch: {meals['lunch']}") | |
| st.write(f"- Dinner: {meals['dinner']}") | |
| except json.JSONDecodeError as e: | |
| st.error(f"Error parsing day-by-day itinerary: {str(e)}") | |
| st.markdown("### Raw Itinerary") | |
| st.markdown(day_json_str) | |
| except Exception as e: | |
| st.error(f"Error generating trip plan: {str(e)}") | |
| st.info("Please try again or adjust your inputs. If the issue persists, contact support.") | |
| # ------------- Handle Global Trip Submission ------------- | |
| if submitted_global: | |
| if not start_location_global or not countries: | |
| st.error("Please enter a starting location and at least one country to visit.") | |
| st.stop() | |
| elif not plan_name_global: | |
| st.warning("Please give your global trip plan a name to save it.") | |
| st.stop() | |
| else: | |
| with st.spinner("Planning your global trip... This might take a moment."): | |
| try: | |
| preferences_str_global = ", ".join(preferences_global) if preferences_global else "No specific preferences" | |
| food_pref_str_global = ", ".join(food_preferences_global) if food_preferences_global else "No specific food preferences" | |
| must_visit_str_global = must_visit_global if must_visit_global else "No specific must-visit places" | |
| avoid_locations_str_global = avoid_locations_global if avoid_locations_global else "No places to avoid" | |
| end_date_global = start_date_global + timedelta(days=duration_global) | |
| prompt_global = f"""Please create a detailed global travel plan with the following details: | |
| - Starting location: {start_location_global} | |
| - Countries to visit: {countries} | |
| - Trip dates: {start_date_global.strftime('%d %b, %Y')} to {end_date_global.strftime('%d %b, %Y')} | |
| - Budget: {budget_global} USD | |
| - Duration: {duration_global} days | |
| - Number of travelers: {travelers_global} | |
| - Preferred travel mode: {travel_mode_global} | |
| - Accommodation preference: {accommodation_type_global} | |
| - Food preferences: {food_pref_str_global} | |
| - Activity preferences: {preferences_str_global} | |
| - Travel pace: {pace_global} | |
| - Must-visit locations: {must_visit_str_global} | |
| - Locations to avoid: {avoid_locations_str_global} | |
| - Maximum travel time per day: {max_travel_hours_global} hours | |
| - Visa information required: {"Yes" if visa_needs else "No"} | |
| - Special requests: {special_requests_global if special_requests_global else 'None'} | |
| The plan should include: | |
| 1. Suggested destinations in sequence of visit with specific dates, Google Maps directions links, and additional info search links. | |
| 2. Specific accommodation options within budget (with exact names, estimated prices in USD, and brief descriptions). i repeat do budget under the budget provided by the user. | |
| 3. Transportation recommendations between destinations (with estimated costs in USD, travel times, and options like flights, trains, etc.). | |
| 4. Daily activities and sightseeing with timing (morning/afternoon/evening). | |
| 5. Recommended restaurants for each meal with cuisine types and price ranges in USD. | |
| 6. Detailed cost breakdown to ensure it stays within budget (in USD). | |
| 7. Travel tips specific to the locations, countries, and time of year (including visa advice if requested). | |
| 8. Practical information about each destination (weather, local customs, currency, etc.). | |
| Be specific about locations worldwide, include exact names of hotels, tourist spots, and ensure the plan fits the budget.""" | |
| messages = [ | |
| {"role": "system", "content": "You are a professional global travel planner with expertise in worldwide destinations."}, | |
| {"role": "user", "content": prompt_global} | |
| ] | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=2000, | |
| ), | |
| messages | |
| ) | |
| plan_global = completion.choices[0].message.content | |
| plan_summary_global = f"📍 **From:** {start_location_global}\n🌍 **Countries:** {countries}\n📅 **Dates:** {start_date_global.strftime('%d %b')} - {end_date_global.strftime('%d %b, %Y')}\n💰 **Budget:** {budget_global:,} USD\n👥 **Travelers:** {travelers_global}" | |
| plan_data_global = { | |
| "name": plan_name_global, | |
| "location": start_location_global, | |
| "countries": countries, | |
| "duration": duration_global, | |
| "budget": budget_global, | |
| "start_date": start_date_global.strftime('%Y-%m-%d'), | |
| "travelers": travelers_global, | |
| "summary": plan_summary_global, | |
| "full_plan": plan_global, | |
| "created_on": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), | |
| "type": "Global" | |
| } | |
| st.session_state.saved_trip_plans.append(plan_data_global) | |
| plan_tab1, plan_tab2, plan_tab3, plan_tab4 = st.tabs(["Complete Plan", "Route Map", "Cost Breakdown", "Day-by-Day"]) | |
| with plan_tab1: | |
| st.subheader(f"Your Global Trip Plan: {plan_name_global}") | |
| with st.container(): | |
| cols = st.columns([1, 1, 1, 1]) | |
| with cols[0]: | |
| st.markdown("**From:**") | |
| st.markdown(f"### {start_location_global}") | |
| with cols[1]: | |
| st.markdown("**Duration:**") | |
| st.markdown(f"### {duration_global} days") | |
| with cols[2]: | |
| st.markdown("**Budget:**") | |
| st.markdown(f"### {budget_global:,} USD") | |
| with cols[3]: | |
| st.markdown("**Travelers:**") | |
| st.markdown(f"### {travelers_global}") | |
| st.divider() | |
| st.markdown(plan_global) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.download_button( | |
| label="📄 Download as Text", | |
| data=plan_global.encode(), | |
| file_name=f"{plan_name_global.replace(' ', '_')}.txt", | |
| mime="text/plain", | |
| ) | |
| with col2: | |
| st.download_button( | |
| label="📋 Download as PDF", | |
| data=plan_global.encode(), | |
| file_name=f"{plan_name_global.replace(' ', '_')}.txt", | |
| mime="text/plain", | |
| ) | |
| with plan_tab2: | |
| with st.spinner("Creating global route map..."): | |
| extract_prompt = f"""Extract all specific location names (cities, towns, tourist spots, etc.) mentioned in this travel plan that will be visited in sequence. | |
| Provide only a comma-separated list with no additional text, starting with the departure location: | |
| {plan_global}""" | |
| messages = [ | |
| {"role": "system", "content": "You extract specific location names from text without adding comments."}, | |
| {"role": "user", "content": extract_prompt} | |
| ] | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=200, | |
| ), | |
| messages | |
| ) | |
| locations_text = completion.choices[0].message.content | |
| locations = [loc.strip() for loc in locations_text.split(',')] | |
| if start_location_global not in locations: | |
| locations = [start_location_global] + locations | |
| coords = [] | |
| for loc in locations: | |
| cache_key = loc.lower().replace(' ', '_') | |
| cached_location = st.session_state.get(f'geo_cache_{cache_key}') | |
| if cached_location: | |
| coords.append((loc, cached_location[0], cached_location[1])) | |
| else: | |
| nominatim_url = f"https://nominatim.openstreetmap.org/search?q={loc}&format=json" | |
| response = requests.get(nominatim_url, headers={'User-Agent': 'TouristApp/1.0'}) | |
| data = response.json() | |
| if data: | |
| lat = float(data[0]['lat']) | |
| lon = float(data[0]['lon']) | |
| st.session_state[f'geo_cache_{cache_key}'] = (lat, lon) | |
| coords.append((loc, lat, lon)) | |
| if len(coords) > 1: | |
| st.subheader("Global Trip Route Map") | |
| st.markdown("This map shows your global travel route. Note that long-distance routes (e.g., flights) are approximated.") | |
| trip_map = folium.Map(location=[coords[0][1], coords[0][2]], zoom_start=2, control_scale=True) | |
| title_html = f''' | |
| <div style="position: fixed; | |
| top: 10px; left: 50px; width: 250px; height: 30px; | |
| background-color: white; border-radius: 5px; z-index: 900; | |
| font-size: 14pt; font-weight: bold; text-align: center; | |
| line-height: 30px;"> | |
| {plan_name_global} Route | |
| </div> | |
| ''' | |
| trip_map.get_root().html.add_child(folium.Element(title_html)) | |
| bounds = [] | |
| for i, (name, lat, lon) in enumerate(coords): | |
| if i == 0: | |
| icon = folium.Icon(color='green', icon='play', prefix='fa') | |
| popup_content = f""" | |
| <div style="width: 200px;"> | |
| <h4 style="color:green;">Starting Point</h4> | |
| <b>{name}</b><br> | |
| Day: 1<br> | |
| <i>Your global journey begins here</i> | |
| </div> | |
| """ | |
| elif i == len(coords) - 1: | |
| icon = folium.Icon(color='red', icon='flag-checkered', prefix='fa') | |
| popup_content = f""" | |
| <div style="width: 200px;"> | |
| <h4 style="color:red;">Final Destination</h4> | |
| <b>{name}</b><br> | |
| Day: {min(i+1, duration_global)}<br> | |
| <i>Your journey ends here</i> | |
| </div> | |
| """ | |
| else: | |
| days_estimate = min(i+1, duration_global) | |
| icon = folium.DivIcon(html=f'<div style="font-size: 12pt; color: white; background-color: #3186cc; border-radius: 50%; text-align: center; width: 25px; height: 25px; line-height: 25px;"><b>{i}</b></div>') | |
| popup_content = f""" | |
| <div style="width: 200px;"> | |
| <h4 style="color:#3186cc;">Stop #{i}</h4> | |
| <b>{name}</b><br> | |
| Approximate Day: {days_estimate}<br> | |
| </div> | |
| """ | |
| folium.Marker( | |
| location=[lat, lon], | |
| popup=folium.Popup(popup_content, max_width=250), | |
| tooltip=f"{i}. {name}", | |
| icon=icon | |
| ).add_to(trip_map) | |
| bounds.append([lat, lon]) | |
| for i in range(len(coords) - 1): | |
| start = coords[i] | |
| end = coords[i + 1] | |
| try: | |
| osrm_url = f"http://router.project-osrm.org/route/v1/driving/{start[2]},{start[1]};{end[2]},{end[1]}?overview=full&geometries=polyline" | |
| response = requests.get(osrm_url, timeout=5) | |
| route_data = response.json() | |
| if route_data.get('code') == 'Ok' and route_data.get('routes'): | |
| route = polyline.decode(route_data['routes'][0]['geometry']) | |
| distance = round(route_data['routes'][0]['distance'] / 1000, 2) | |
| duration_mins = round(route_data['routes'][0]['duration'] / 60) | |
| duration_text = f"{duration_mins // 60}h {duration_mins % 60}m" if duration_mins >= 60 else f"{duration_mins}m" | |
| folium.PolyLine( | |
| route, | |
| weight=4, | |
| color='blue', | |
| opacity=0.7, | |
| tooltip=f"{start[0]} to {end[0]}: {distance} km ({duration_text})" | |
| ).add_to(trip_map) | |
| else: | |
| start_point = (start[1], start[2]) | |
| end_point = (end[1], end[2]) | |
| distance = round(geopy.distance.distance(start_point, end_point).kilometers, 2) | |
| folium.PolyLine( | |
| [[start[1], start[2]], [end[1], end[2]]], | |
| weight=3, | |
| color='gray', | |
| opacity=0.6, | |
| dash_array='5', | |
| tooltip=f"Direct line: {start[0]} to {end[0]} ({distance} km, approximate)" | |
| ).add_to(trip_map) | |
| except Exception: | |
| start_point = (start[1], start[2]) | |
| end_point = (end[1], end[2]) | |
| distance = round(geopy.distance.distance(start_point, end_point).kilometers, 2) | |
| folium.PolyLine( | |
| [[start[1], start[2]], [end[1], end[2]]], | |
| weight=3, | |
| color='gray', | |
| opacity=0.6, | |
| dash_array='5', | |
| tooltip=f"Direct line: {start[0]} to {end[0]} ({distance} km, approximate)" | |
| ).add_to(trip_map) | |
| folium.LayerControl().add_to(trip_map) | |
| plugins.Fullscreen().add_to(trip_map) | |
| plugins.MeasureControl(position='bottomleft', primary_length_unit='kilometers').add_to(trip_map) | |
| trip_map.fit_bounds(bounds) | |
| st.markdown(""" | |
| <style> | |
| .map-container { | |
| border: 2px solid #ddd; | |
| border-radius: 5px; | |
| padding: 5px; | |
| box-shadow: 0 0 5px rgba(0,0,0,0.1); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| with st.container(): | |
| st.markdown('<div class="map-container">', unsafe_allow_html=True) | |
| folium_static(trip_map, width=800, height=600) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.subheader("Destinations & Journey Details") | |
| distances = [] | |
| durations = [] | |
| total_distance = 0 | |
| total_duration = 0 | |
| for i in range(len(coords) - 1): | |
| start = coords[i] | |
| end = coords[i + 1] | |
| try: | |
| osrm_url = f"http://router.project-osrm.org/route/v1/driving/{start[2]},{start[1]};{end[2]},{end[1]}?overview=false" | |
| response = requests.get(osrm_url, timeout=3) | |
| route_data = response.json() | |
| if route_data.get('code') == 'Ok' and route_data.get('routes'): | |
| distance = round(route_data['routes'][0]['distance'] / 1000, 2) | |
| duration = round(route_data['routes'][0]['duration'] / 60) | |
| else: | |
| start_point = (start[1], start[2]) | |
| end_point = (end[1], end[2]) | |
| distance = round(geopy.distance.distance(start_point, end_point).kilometers, 2) | |
| duration = round(distance * 60 / 50) # Assuming 50 km/h for approximation | |
| except Exception: | |
| start_point = (start[1], start[2]) | |
| end_point = (end[1], end[2]) | |
| distance = round(geopy.distance.distance(start_point, end_point).kilometers, 2) | |
| duration = round(distance * 60 / 50) | |
| distances.append(distance) | |
| durations.append(duration) | |
| total_distance += distance | |
| total_duration += duration | |
| location_data = [] | |
| current_date = start_date_global | |
| current_time = datetime(2000, 1, 1, 9, 0) | |
| for i, (name, lat, lon) in enumerate(coords): | |
| if i > 0: | |
| travel_days = max(1, durations[i-1] // (max_travel_hours_global * 60)) | |
| current_date += timedelta(days=travel_days) | |
| travel_mins = durations[i-1] | |
| current_time = (datetime.combine(current_date, current_time.time()) + | |
| timedelta(minutes=travel_mins)) | |
| if current_time.hour >= 20: | |
| current_date += timedelta(days=1) | |
| current_time = datetime(2000, 1, 1, 9, 0) | |
| day_number = (current_date - start_date_global).days + 1 | |
| if day_number > duration_global: | |
| day_text = f"Beyond plan" | |
| else: | |
| day_text = f"Day {day_number}" | |
| row_data = { | |
| "Stop": i + 1, | |
| "Location": name, | |
| "Estimated Day": day_text, | |
| "Date": current_date.strftime("%d %b"), | |
| } | |
| if i < len(coords) - 1: | |
| hours = durations[i] // 60 | |
| mins = durations[i] % 60 | |
| duration_text = f"{hours}h {mins}m" if hours > 0 else f"{mins}m" | |
| row_data["Distance to Next"] = f"{distances[i]:.1f} km" | |
| row_data["Travel Time"] = duration_text | |
| else: | |
| row_data["Travel Time"] = "-" | |
| location_data.append(row_data) | |
| location_df = pd.DataFrame(location_data) | |
| st.dataframe(location_df, use_container_width=True, hide_index=True) | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("Total Distance", f"{total_distance:.1f} km") | |
| with col2: | |
| total_hours = total_duration // 60 | |
| total_mins = total_duration % 60 | |
| st.metric("Total Travel Time", f"{total_hours}h {total_mins}m") | |
| with col3: | |
| st.metric("Destinations", f"{len(coords)} locations") | |
| if total_distance > 5000: | |
| st.info("💡 This global itinerary involves significant distances, likely requiring flights or other long-haul transport.") | |
| elif total_distance > 1000: | |
| st.info("💡 Consider flight options or overnight travel for longer segments.") | |
| with plan_tab3: | |
| with st.spinner("Creating detailed cost breakdown..."): | |
| cost_prompt_global = f"""Extract and organize the cost breakdown from this global travel plan in a structured way. | |
| Categorize expenses into these exact categories: Accommodation, Transportation, Food, Activities, and Miscellaneous. | |
| For each category, list each item with its cost in USD, days (if applicable), and notes (if applicable). | |
| Provide a summary with totals for each category and overall total, per person, and per day costs. | |
| Format the output as plain text with clear sections and no JSON. Use the following structure: | |
| Accommodation: | |
| - Item: [description], Cost: $[cost], Days: [days], Notes: [notes] | |
| Transportation: | |
| - Item: [description], Cost: $[cost], Notes: [notes] | |
| Food: | |
| - Item: [description], Cost: $[cost], Notes: [notes] | |
| Activities: | |
| - Item: [description], Cost: $[cost], Notes: [notes] | |
| Miscellaneous: | |
| - Item: [description], Cost: $[cost], Notes: [notes] | |
| Summary: | |
| - Total Cost: $[total] | |
| - Accommodation Total: $[accommodation_total] | |
| - Transportation Total: $[transportation_total] | |
| - Food Total: $[food_total] | |
| - Activities Total: $[activities_total] | |
| - Miscellaneous Total: $[miscellaneous_total] | |
| - Per Person: $[per_person] | |
| - Per Day: $[per_day] | |
| If you need to add any additional notes, please add them after the entire summary section with a clear "Note:" prefix on a separate line. | |
| If exact costs or details are missing, estimate based on typical tourism prices and note it as an estimate in the notes field. | |
| Based on this plan: | |
| {plan_global}""" | |
| messages = [ | |
| {"role": "system", "content": "You extract and organize cost information from global travel plans into a structured plain text format."}, | |
| {"role": "user", "content": cost_prompt_global} | |
| ] | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=1500, | |
| ), | |
| messages | |
| ) | |
| cost_text = completion.choices[0].message.content.strip() | |
| try: | |
| # Display the cost breakdown as plain text | |
| st.subheader("Detailed Cost Breakdown") | |
| st.text(cost_text) | |
| # Extract cost data from text format | |
| lines = cost_text.split('\n') | |
| categories = {"Accommodation": [], "Transportation": [], "Food": [], "Activities": [], "Miscellaneous": []} | |
| summary_data = {} | |
| current_category = None | |
| summary_section = False | |
| notes = [] | |
| # Parse the text to extract items and summary data | |
| for line in lines: | |
| line = line.strip() | |
| # Skip empty lines | |
| if not line: | |
| continue | |
| # Check for note section | |
| if line.startswith("Note:"): | |
| notes.append(line) | |
| continue | |
| # Check for category headers | |
| if any(line.startswith(cat + ":") for cat in categories.keys()): | |
| current_category = line.split(":")[0].strip() | |
| continue | |
| # Check for summary section | |
| if line == "Summary:" or "Summary:" in line: | |
| summary_section = True | |
| current_category = None | |
| continue | |
| # Parse items in categories | |
| if current_category and current_category in categories and line.startswith("- Item:"): | |
| item_data = {"item": "", "cost": 0, "days": 1, "notes": ""} | |
| parts = line.split(", ") | |
| for part in parts: | |
| part = part.strip() | |
| if part.startswith("- Item:"): | |
| item_data["item"] = part.replace("- Item:", "").strip() | |
| elif "Cost:" in part: | |
| cost_str = part.replace("Cost:", "").replace("$", "").strip() | |
| # Handle range values (e.g., "20-50") | |
| if "-" in cost_str: | |
| range_values = cost_str.split("-") | |
| min_value = float(range_values[0].replace(",", "").strip()) | |
| max_value = float(range_values[1].replace(",", "").strip()) | |
| # Use the average of min and max | |
| item_data["cost"] = (min_value + max_value) / 2 | |
| else: | |
| try: | |
| item_data["cost"] = float(cost_str.replace(",", "")) | |
| except ValueError: | |
| item_data["cost"] = 0 | |
| elif "Days:" in part: | |
| days_str = part.replace("Days:", "").strip() | |
| try: | |
| item_data["days"] = int(days_str) | |
| except ValueError: | |
| item_data["days"] = 1 | |
| elif "Notes:" in part: | |
| item_data["notes"] = part.replace("Notes:", "").strip() | |
| # Add the item to the current category | |
| categories[current_category].append(item_data) | |
| # Parse summary data | |
| if summary_section and line.startswith("-"): | |
| parts = line.split(":") | |
| if len(parts) >= 2: | |
| key = parts[0].replace("-", "").strip() | |
| value_part = parts[1].strip() | |
| # Handle ranges (e.g., "$20-$50") | |
| if "-" in value_part and "$" in value_part.split("-")[1]: | |
| range_values = value_part.split("-") | |
| min_value = float(range_values[0].replace("$", "").replace(",", "").strip()) | |
| max_value = float(range_values[1].replace("$", "").replace(",", "").strip()) | |
| # Use the average of min and max | |
| value = (min_value + max_value) / 2 | |
| else: | |
| # Regular single value | |
| try: | |
| value = float(value_part.replace("$", "").replace(",", "")) | |
| summary_data[key] = value | |
| except ValueError: | |
| summary_data[key] = 0 | |
| # Create a pie chart for category totals | |
| if "Total Cost" in summary_data: | |
| category_totals = { | |
| "Accommodation": summary_data.get("Accommodation Total", 0), | |
| "Transportation": summary_data.get("Transportation Total", 0), | |
| "Food": summary_data.get("Food Total", 0), | |
| "Activities": summary_data.get("Activities Total", 0), | |
| "Miscellaneous": summary_data.get("Miscellaneous Total", 0) | |
| } | |
| # Filter out zero values | |
| category_totals = {k: v for k, v in category_totals.items() if v > 0} | |
| if category_totals: | |
| # Display summary metrics | |
| st.subheader("Summary Metrics") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("Total Cost", f"${int(summary_data.get('Total Cost', 0)):,}") | |
| with col2: | |
| st.metric("Per Person", f"${int(summary_data.get('Per Person', 0)):,}") | |
| with col3: | |
| st.metric("Per Day", f"${int(summary_data.get('Per Day', 0)):,}") | |
| # Display detailed breakdown tables | |
| st.subheader("Detailed Item Breakdown") | |
| for category, items in categories.items(): | |
| if items: | |
| st.write(f"**{category}**") | |
| df = pd.DataFrame(items) | |
| if not df.empty: | |
| # Format cost column with comma separators and '$' prefix | |
| df['cost'] = df['cost'].apply(lambda x: f"${int(x):,}") | |
| # Rename columns for better display | |
| df.columns = ['Item', 'Cost', 'Days', 'Notes'] | |
| st.table(df) | |
| # Display any notes | |
| if notes: | |
| st.subheader("Additional Notes") | |
| for note in notes: | |
| st.write(note) | |
| # # Add currency exchange information | |
| # st.subheader("Currency Exchange") | |
| # exchange_col1, exchange_col2 = st.columns(2) | |
| # with exchange_col1: | |
| # amount = st.number_input("Amount in USD", min_value=0.0, value=float(summary_data.get('Total Cost', 1000))) | |
| # with exchange_col2: | |
| # currency = st.selectbox("Convert to", | |
| # ["EUR", "GBP", "JPY", "AUD", "CAD", "CHF", "CNY", "INR", "BDT"]) | |
| # # Mock exchange rates (in real app, you would fetch these from an API) | |
| # exchange_rates = { | |
| # "EUR": 0.85, "GBP": 0.73, "JPY": 110.50, "AUD": 1.35, | |
| # "CAD": 1.25, "CHF": 0.92, "CNY": 6.45, "INR": 73.50, "BDT": 85.50 | |
| # } | |
| # converted = amount * exchange_rates[currency] | |
| # st.write(f"${amount:,.2f} USD = {converted:,.2f} {currency}") | |
| # Add a download button for the cost breakdown | |
| cost_csv = io.StringIO() | |
| for category, items in categories.items(): | |
| if items: | |
| df = pd.DataFrame(items) | |
| if not df.empty: | |
| df['category'] = category | |
| df.to_csv(cost_csv, index=False, mode='a') | |
| st.download_button( | |
| label="Download Cost Breakdown", | |
| data=cost_csv.getvalue(), | |
| file_name="travel_cost_breakdown.csv", | |
| mime="text/csv", | |
| ) | |
| except Exception as e: | |
| st.error(f"Error processing cost breakdown: {str(e)}") | |
| st.markdown("### Raw Cost Breakdown") | |
| st.text(cost_text) | |
| with plan_tab4: | |
| st.subheader("Day-by-Day Itinerary") | |
| with st.spinner("Organizing your global daily schedule..."): | |
| day_prompt_global = f"""Extract and organize a day-by-day itinerary from this global travel plan. For each day, include: | |
| - Date (e.g., "12 Mar, 2025") | |
| - Location(s) visited | |
| - Activities with approximate timing (morning, afternoon, evening) | |
| - Accommodation details (name and location) | |
| - Meals (breakfast, lunch, dinner) with recommended restaurants | |
| Format the output as a JSON object with this structure: | |
| {{ | |
| "days": [ | |
| {{ | |
| "day": 1, | |
| "date": "12 Mar, 2025", | |
| "location": "Tokyo", | |
| "activities": [ | |
| {{"time": "morning", "description": "Visit Tokyo Tower"}}, | |
| ... | |
| ], | |
| "accommodation": "Hotel ABC, Tokyo", | |
| "meals": {{ | |
| "breakfast": "Hotel ABC Cafe", | |
| "lunch": "Sushi Zanmai", | |
| "dinner": "Ramen Ichiraku" | |
| }} | |
| }}, | |
| ... | |
| ] | |
| }} | |
| Based on this plan: | |
| {plan_global}""" | |
| messages = [ | |
| {"role": "system", "content": "You extract and organize daily itineraries from global travel plans into a structured JSON format."}, | |
| {"role": "user", "content": day_prompt_global} | |
| ] | |
| completion = key_manager.execute_with_fallback( | |
| lambda client, msgs: client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=msgs, | |
| temperature=0.3, | |
| max_tokens=1500, | |
| ), | |
| messages | |
| ) | |
| day_json_str = completion.choices[0].message.content.strip() | |
| day_extracted_json = extract_json_from_markdown(day_json_str) | |
| try: | |
| day_data = json.loads(day_extracted_json) | |
| days = day_data["days"] | |
| for day in days: | |
| with st.expander(f"Day {day['day']} - {day['date']} ({day['location']})"): | |
| st.markdown(f"**Location:** {day['location']}") | |
| st.markdown("**Activities:**") | |
| for activity in day["activities"]: | |
| st.write(f"- {activity['time'].capitalize()}: {activity['description']}") | |
| st.markdown(f"**Accommodation:** {day['accommodation']}") | |
| st.markdown("**Meals:**") | |
| meals = day["meals"] | |
| st.write(f"- Breakfast: {meals['breakfast']}") | |
| st.write(f"- Lunch: {meals['lunch']}") | |
| st.write(f"- Dinner: {meals['dinner']}") | |
| except json.JSONDecodeError as e: | |
| st.error(f"Error parsing day-by-day itinerary: {str(e)}") | |
| st.markdown("### Raw Itinerary") | |
| st.markdown(day_json_str) | |
| except Exception as e: | |
| st.error(f"Error generating global trip plan: {str(e)}") | |
| st.info("Please try again or adjust your inputs. If the issue persists, contact support.") |