trip-app / app.py
ghfdgh5645's picture
fd
0de7c21
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}&current=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">
&nbsp; Map Legend <br>
&nbsp; <i class="fa fa-map-marker fa-2x" style="color:blue"></i>&nbsp; Attractions <br>
&nbsp; <i class="fa fa-map-marker fa-2x" style="color:red"></i>&nbsp; Hotels/Resorts <br>
&nbsp; <i class="fa fa-map-marker fa-2x" style="color:orange"></i>&nbsp; Beaches <br>
&nbsp; <i class="fa fa-map-marker fa-2x" style="color:purple"></i>&nbsp; Historic Sites <br>
&nbsp; <i class="fa fa-map-marker fa-2x" style="color:darkgreen"></i>&nbsp; Restaurants <br>
&nbsp; <i class="fa fa-map-marker fa-2x" style="color:darkblue"></i>&nbsp; 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}&current=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.")