Update app.py
Browse files
app.py
CHANGED
|
@@ -5,8 +5,6 @@ from datetime import datetime, timedelta
|
|
| 5 |
from functools import lru_cache
|
| 6 |
import gradio as gr
|
| 7 |
import openai
|
| 8 |
-
import re
|
| 9 |
-
from typing import List, Dict, Any
|
| 10 |
|
| 11 |
# -------------------- CONFIGURATION --------------------
|
| 12 |
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
|
@@ -19,59 +17,9 @@ if not OPENAI_API_KEY:
|
|
| 19 |
else:
|
| 20 |
client = openai.OpenAI(api_key=OPENAI_API_KEY)
|
| 21 |
|
| 22 |
-
# Global attractions cache
|
| 23 |
-
ATTRACTIONS_CACHE = {}
|
| 24 |
-
|
| 25 |
-
# Real images for famous attractions (only where we have verified images)
|
| 26 |
-
REAL_IMAGES = {
|
| 27 |
-
# Paris
|
| 28 |
-
"eiffel tower": "https://images.unsplash.com/photo-1543349689-9a4d426bee8e?w=800&h=500&fit=crop",
|
| 29 |
-
"louvre museum": "https://images.unsplash.com/photo-1564910443436-deafeb5e7d6c?w=800&h=500&fit=crop",
|
| 30 |
-
"notre-dame cathedral": "https://images.unsplash.com/photo-1491336477066-31156b5e4f35?w=800&h=500&fit=crop",
|
| 31 |
-
"arc de triomphe": "https://images.unsplash.com/photo-1566566108883-476d3293f1d3?w=800&h=500&fit=crop",
|
| 32 |
-
"seine river": "https://images.unsplash.com/photo-1493707553966-283afac8c358?w=800&h=500&fit=crop",
|
| 33 |
-
"montmartre": "https://images.unsplash.com/photo-1522093007474-d86e9bf7ba6f?w=800&h=500&fit=crop",
|
| 34 |
-
|
| 35 |
-
# Kigali
|
| 36 |
-
"kigali genocide memorial": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Kigali_Genocide_Memorial_03.jpg/800px-Kigali_Genocide_Memorial_03.jpg",
|
| 37 |
-
"inema arts center": "https://media-cdn.tripadvisor.com/media/photo-s/0e/9a/3f/8c/inema-arts-center.jpg",
|
| 38 |
-
|
| 39 |
-
# Tokyo
|
| 40 |
-
"senso-ji temple": "https://images.unsplash.com/photo-1564507592333-c60657eea523?w=800&h=500&fit=crop",
|
| 41 |
-
"tokyo tower": "https://images.unsplash.com/photo-1542051841857-5f90071e7989?w=800&h=500&fit=crop",
|
| 42 |
-
"shibuya crossing": "https://images.unsplash.com/photo-1542051841857-5f90071e7989?w=800&h=500&fit=crop",
|
| 43 |
-
|
| 44 |
-
# New York
|
| 45 |
-
"statue of liberty": "https://images.unsplash.com/photo-1505765050516-f72a3e10db15?w=800&h=500&fit=crop",
|
| 46 |
-
"times square": "https://images.unsplash.com/photo-1485871981521-5b1fd3805eee?w=800&h=500&fit=crop",
|
| 47 |
-
"central park": "https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=800&h=500&fit=crop",
|
| 48 |
-
|
| 49 |
-
# London
|
| 50 |
-
"big ben": "https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?w=800&h=500&fit=crop",
|
| 51 |
-
"london eye": "https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?w=800&h=500&fit=crop",
|
| 52 |
-
"tower bridge": "https://images.unsplash.com/photo-1529655683826-aba9b3e77383?w=800&h=500&fit=crop",
|
| 53 |
-
|
| 54 |
-
# Rome
|
| 55 |
-
"colosseum": "https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=800&h=500&fit=crop",
|
| 56 |
-
"vatican": "https://images.unsplash.com/photo-1549294413-26f195200c16?w=800&h=500&fit=crop",
|
| 57 |
-
"trevi fountain": "https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=800&h=500&fit=crop",
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
def get_attraction_image(attraction_name: str) -> str:
|
| 61 |
-
"""Get real image URL if available, otherwise return None."""
|
| 62 |
-
name_lower = attraction_name.lower()
|
| 63 |
-
|
| 64 |
-
# Check exact match or partial match
|
| 65 |
-
for key, url in REAL_IMAGES.items():
|
| 66 |
-
if key in name_lower:
|
| 67 |
-
return url
|
| 68 |
-
|
| 69 |
-
# No image found
|
| 70 |
-
return None
|
| 71 |
-
|
| 72 |
# -------------------- WEATHER FUNCTION --------------------
|
| 73 |
def get_weather(city: str) -> dict:
|
| 74 |
-
"""Fetch real-time weather data for any city
|
| 75 |
if not OPENWEATHER_API_KEY:
|
| 76 |
return {
|
| 77 |
"city": city,
|
|
@@ -79,8 +27,7 @@ def get_weather(city: str) -> dict:
|
|
| 79 |
"condition": "sunny",
|
| 80 |
"humidity": 60,
|
| 81 |
"wind_speed": 10,
|
| 82 |
-
"
|
| 83 |
-
"note": "Demo mode - Add OpenWeather API key for real data"
|
| 84 |
}
|
| 85 |
try:
|
| 86 |
url = "https://api.openweathermap.org/data/2.5/weather"
|
|
@@ -89,161 +36,142 @@ def get_weather(city: str) -> dict:
|
|
| 89 |
data = response.json()
|
| 90 |
if response.status_code != 200:
|
| 91 |
return {"error": f"Weather API error: {data.get('message', 'unknown')}"}
|
| 92 |
-
|
| 93 |
return {
|
| 94 |
"city": city,
|
| 95 |
"temperature": data['main']['temp'],
|
| 96 |
-
"feels_like": data['main']['feels_like'],
|
| 97 |
"condition": data['weather'][0]['description'],
|
| 98 |
"humidity": data['main']['humidity'],
|
| 99 |
"wind_speed": data['wind']['speed'],
|
| 100 |
"precipitation": data.get('rain', {}).get('1h', 0)
|
| 101 |
}
|
| 102 |
except Exception as e:
|
| 103 |
-
return {"error": f"Weather
|
| 104 |
|
| 105 |
-
# -------------------- ATTRACTIONS
|
| 106 |
-
def
|
| 107 |
-
"""
|
| 108 |
-
if not client:
|
| 109 |
-
return get_fallback_attractions(city)
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
-
Return ONLY a JSON array with keys: name, description, entry_fee, duration_hours.
|
| 120 |
|
| 121 |
-
Example:
|
| 122 |
-
[
|
| 123 |
-
{{
|
| 124 |
-
"name": "Eiffel Tower",
|
| 125 |
-
"description": "Iconic iron lattice tower offering breathtaking panoramic views of Paris.",
|
| 126 |
-
"entry_fee": 25,
|
| 127 |
-
"duration_hours": 2.5
|
| 128 |
-
}}
|
| 129 |
-
]
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
| 163 |
else:
|
| 164 |
-
fee_display =
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
-
result
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
})
|
| 174 |
-
|
| 175 |
-
# Cache the results
|
| 176 |
-
ATTRACTIONS_CACHE[city] = result
|
| 177 |
-
return result
|
| 178 |
-
|
| 179 |
-
except Exception as e:
|
| 180 |
-
print(f"OpenAI attractions error: {e}")
|
| 181 |
-
return get_fallback_attractions(city)
|
| 182 |
-
|
| 183 |
-
def get_fallback_attractions(city: str) -> List[Dict]:
|
| 184 |
-
"""Provide fallback attractions for any city."""
|
| 185 |
-
city_lower = city.lower()
|
| 186 |
-
|
| 187 |
-
# Real attractions for major cities
|
| 188 |
-
real_attractions = {
|
| 189 |
-
"kigali": [
|
| 190 |
-
{"name": "Kigali Genocide Memorial", "description": "A powerful memorial and museum honoring the victims of the 1994 genocide, with exhibits and gardens.", "entry_fee": "Free", "duration_hours": 3},
|
| 191 |
-
{"name": "Inema Arts Center", "description": "Vibrant contemporary art gallery showcasing Rwandan artists with colorful paintings and sculptures.", "entry_fee": "Free", "duration_hours": 2},
|
| 192 |
-
{"name": "Mount Kigali", "description": "The highest point in Kigali offering panoramic views of the city and surrounding hills.", "entry_fee": "Free", "duration_hours": 3},
|
| 193 |
-
{"name": "Kimironko Market", "description": "Kigali's largest local market where you can experience daily life and buy fresh produce.", "entry_fee": "Free", "duration_hours": 2},
|
| 194 |
-
{"name": "Nyamirambo Women's Center", "description": "Community center offering walking tours and cultural experiences supporting local women.", "entry_fee": "$20", "duration_hours": 3},
|
| 195 |
-
{"name": "Presidential Palace Museum", "description": "Former presidential residence with fascinating history and exhibits.", "entry_fee": "$5", "duration_hours": 2}
|
| 196 |
-
],
|
| 197 |
-
"paris": [
|
| 198 |
-
{"name": "Eiffel Tower", "description": "Iconic iron lattice tower offering breathtaking panoramic views of Paris.", "entry_fee": "$25", "duration_hours": 2.5},
|
| 199 |
-
{"name": "Louvre Museum", "description": "World's largest art museum, home to the Mona Lisa and thousands of artworks.", "entry_fee": "$20", "duration_hours": 4},
|
| 200 |
-
{"name": "Notre-Dame Cathedral", "description": "Magnificent Gothic cathedral with stunning architecture and history.", "entry_fee": "Free", "duration_hours": 1.5},
|
| 201 |
-
{"name": "Montmartre", "description": "Charming hilltop village with artists' square and SacrΓ©-CΕur basilica.", "entry_fee": "Free", "duration_hours": 3},
|
| 202 |
-
{"name": "Seine River Cruise", "description": "Relaxing boat tour passing by Paris's most famous landmarks.", "entry_fee": "$15", "duration_hours": 1},
|
| 203 |
-
{"name": "Arc de Triomphe", "description": "Monumental arch honoring French soldiers with panoramic city views.", "entry_fee": "$13", "duration_hours": 2}
|
| 204 |
-
]
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
for key, attractions in real_attractions.items():
|
| 208 |
-
if key in city_lower:
|
| 209 |
-
return attractions
|
| 210 |
|
| 211 |
-
#
|
| 212 |
return [
|
| 213 |
-
{"name": f"{city} City Center", "description": f"The
|
| 214 |
-
{"name": f"{city}
|
| 215 |
-
{"name": f"{city}
|
| 216 |
-
{"name": f"{city}
|
| 217 |
]
|
| 218 |
|
| 219 |
-
def get_attractions(city: str) -> Dict:
|
| 220 |
-
"""Get attractions from cache or fetch new ones."""
|
| 221 |
-
city_clean = city.strip().lower()
|
| 222 |
-
if city_clean in ATTRACTIONS_CACHE:
|
| 223 |
-
return {"city": city, "attractions": ATTRACTIONS_CACHE[city_clean]}
|
| 224 |
-
|
| 225 |
-
attractions = fetch_attractions_via_openai(city)
|
| 226 |
-
return {"city": city, "attractions": attractions}
|
| 227 |
-
|
| 228 |
# -------------------- BUDGET CALCULATION --------------------
|
| 229 |
-
def calculate_budget(num_days: int, budget_amount: float = None) ->
|
| 230 |
"""Calculate budget based on days and user input."""
|
| 231 |
if budget_amount:
|
| 232 |
daily_budget = budget_amount / num_days
|
| 233 |
if daily_budget < 100:
|
| 234 |
-
level = "
|
| 235 |
daily_rates = {"accommodation": 50, "food": 35, "transport": 15, "activities": 10}
|
| 236 |
elif daily_budget < 200:
|
| 237 |
-
level = "
|
| 238 |
daily_rates = {"accommodation": 100, "food": 60, "transport": 25, "activities": 20}
|
| 239 |
elif daily_budget < 400:
|
| 240 |
-
level = "
|
| 241 |
daily_rates = {"accommodation": 180, "food": 100, "transport": 35, "activities": 35}
|
| 242 |
else:
|
| 243 |
-
level = "
|
| 244 |
daily_rates = {"accommodation": 300, "food": 150, "transport": 50, "activities": 60}
|
| 245 |
else:
|
| 246 |
-
level = "
|
| 247 |
daily_rates = {"accommodation": 100, "food": 60, "transport": 25, "activities": 20}
|
| 248 |
|
| 249 |
accommodation = daily_rates["accommodation"] * num_days
|
|
@@ -269,18 +197,18 @@ def generate_itinerary(destination: str, start_date: str, num_days: int,
|
|
| 269 |
try:
|
| 270 |
# Validate inputs
|
| 271 |
if not destination:
|
| 272 |
-
return "
|
| 273 |
|
| 274 |
if num_days < 1 or num_days > 14:
|
| 275 |
-
return "
|
| 276 |
|
| 277 |
-
# Get
|
| 278 |
weather = get_weather(destination)
|
| 279 |
if "error" in weather:
|
| 280 |
-
return f"
|
| 281 |
|
| 282 |
-
|
| 283 |
-
attractions =
|
| 284 |
|
| 285 |
# Calculate budget
|
| 286 |
budget_data = calculate_budget(num_days, budget_amount)
|
|
@@ -289,21 +217,20 @@ def generate_itinerary(destination: str, start_date: str, num_days: int,
|
|
| 289 |
start = datetime.strptime(start_date, "%Y-%m-%d")
|
| 290 |
end = start + timedelta(days=int(num_days) - 1)
|
| 291 |
|
| 292 |
-
# Generate
|
| 293 |
-
html =
|
| 294 |
destination, weather, attractions, budget_data,
|
| 295 |
-
num_days, start, end, budget_amount,
|
| 296 |
)
|
| 297 |
|
| 298 |
return html
|
| 299 |
|
| 300 |
except Exception as e:
|
| 301 |
-
return f"
|
| 302 |
|
| 303 |
-
def
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
"""Create a stunning itinerary with real images when available."""
|
| 307 |
|
| 308 |
# Weather details
|
| 309 |
weather_temp = f"{weather['temperature']:.1f}Β°C" if isinstance(weather['temperature'], (int, float)) else str(weather['temperature'])
|
|
@@ -319,44 +246,21 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
|
|
| 319 |
</div>
|
| 320 |
"""
|
| 321 |
|
| 322 |
-
# Attractions list
|
| 323 |
attractions_html = ""
|
| 324 |
for attr in attractions[:6]:
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
#
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
<
|
| 332 |
-
<div style="flex: 0 0 200px;">
|
| 333 |
-
<img src="{img_url}" style="width: 100%; height: 150px; object-fit: cover;" alt="{attr['name']}">
|
| 334 |
-
</div>
|
| 335 |
-
<div style="flex: 1; padding: 15px;">
|
| 336 |
-
<strong style="font-size: 1.1em; color: #333;">π {attr['name']}</strong>
|
| 337 |
-
<div style="color: #666; margin: 8px 0; font-size: 0.95em;">{attr['description']}</div>
|
| 338 |
-
<div style="display: flex; gap: 15px; margin-top: 8px; font-size: 0.85em;">
|
| 339 |
-
<span>β±οΈ {attr['duration_hours']} hrs</span>
|
| 340 |
-
<span>ποΈ {attr['entry_fee']}</span>
|
| 341 |
-
</div>
|
| 342 |
-
</div>
|
| 343 |
-
</div>
|
| 344 |
</div>
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
# Without image
|
| 348 |
-
attractions_html += f"""
|
| 349 |
-
<div style="background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 12px; border-left: 3px solid #667eea;">
|
| 350 |
-
<strong style="font-size: 1.1em; color: #333;">π {attr['name']}</strong>
|
| 351 |
-
<div style="color: #666; margin: 8px 0; font-size: 0.95em;">{attr['description']}</div>
|
| 352 |
-
<div style="display: flex; gap: 15px; margin-top: 8px; font-size: 0.85em;">
|
| 353 |
-
<span>β±οΈ {attr['duration_hours']} hrs</span>
|
| 354 |
-
<span>ποΈ {attr['entry_fee']}</span>
|
| 355 |
-
</div>
|
| 356 |
-
</div>
|
| 357 |
-
"""
|
| 358 |
|
| 359 |
-
# Daily itinerary
|
| 360 |
daily_html = ""
|
| 361 |
per_day = max(1, len(attractions) // max(1, num_days))
|
| 362 |
|
|
@@ -378,23 +282,7 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
|
|
| 378 |
"""
|
| 379 |
|
| 380 |
for attr in day_attractions:
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
if img_url:
|
| 384 |
-
daily_html += f"""
|
| 385 |
-
<div style="display: flex; gap: 15px; margin-bottom: 20px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
|
| 386 |
-
<img src="{img_url}" style="width: 80px; height: 80px; object-fit: cover; border-radius: 8px;" alt="{attr['name']}">
|
| 387 |
-
<div style="flex: 1;">
|
| 388 |
-
<strong>{attr['name']}</strong><br>
|
| 389 |
-
<span style="font-size: 0.85em; color: #666;">{attr['description'][:100]}...</span>
|
| 390 |
-
<div style="font-size: 0.75em; color: #888; margin-top: 5px;">
|
| 391 |
-
β±οΈ {attr['duration_hours']} hrs | ποΈ {attr['entry_fee']}
|
| 392 |
-
</div>
|
| 393 |
-
</div>
|
| 394 |
-
</div>
|
| 395 |
-
"""
|
| 396 |
-
else:
|
| 397 |
-
daily_html += f"""
|
| 398 |
<div style="margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
|
| 399 |
<strong>β¨ {attr['name']}</strong><br>
|
| 400 |
<span style="font-size: 0.85em; color: #666;">{attr['description'][:100]}...</span>
|
|
@@ -402,24 +290,21 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
|
|
| 402 |
β±οΈ {attr['duration_hours']} hrs | ποΈ {attr['entry_fee']}
|
| 403 |
</div>
|
| 404 |
</div>
|
| 405 |
-
|
| 406 |
|
| 407 |
daily_html += f"""
|
| 408 |
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
| 409 |
-
<div><span style="font-size: 1.1em;">π½οΈ</span> <strong>Lunch:</strong> Try authentic local cuisine</div>
|
| 410 |
-
<div style="margin-top: 8px;"><span style="font-size: 1.1em;">π</span> <strong>Evening:</strong> Explore local markets
|
| 411 |
</div>
|
| 412 |
</div>
|
| 413 |
</div>
|
| 414 |
"""
|
| 415 |
|
| 416 |
# Budget breakdown
|
| 417 |
-
level_names = {"budget": "Budget", "moderate": "Moderate", "comfortable": "Comfortable", "luxury": "Luxury"}
|
| 418 |
-
level_display = level_names.get(budget_data['level'], "Moderate")
|
| 419 |
-
|
| 420 |
budget_html = f"""
|
| 421 |
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white; margin: 20px 0;">
|
| 422 |
-
<h3 style="margin: 0 0 15px 0;">π° Budget Breakdown ({
|
| 423 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px;">
|
| 424 |
<div><strong>π¨ Accommodation</strong><br>${budget_data['accommodation']:.0f}<br><small>(${budget_data['daily']['accommodation']}/day)</small></div>
|
| 425 |
<div><strong>π½οΈ Food & Dining</strong><br>${budget_data['food']:.0f}<br><small>(${budget_data['daily']['food']}/day)</small></div>
|
|
@@ -438,10 +323,12 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
|
|
| 438 |
<div style="background: #f0f4ff; padding: 20px; border-radius: 12px; margin: 20px 0;">
|
| 439 |
<h3 style="margin: 0 0 15px 0; color: #667eea;">π‘ Smart Travel Tips</h3>
|
| 440 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
|
| 441 |
-
<div>π« <strong>Book in Advance</strong><br>Save time and money</div>
|
| 442 |
-
<div>π <strong>Public Transport</strong><br>Get day passes for savings</div>
|
| 443 |
-
<div>π± <strong>Offline Maps</strong><br>Download before
|
| 444 |
-
<div>π΅ <strong>Local Currency</strong><br>Carry cash for markets</div>
|
|
|
|
|
|
|
| 445 |
</div>
|
| 446 |
</div>
|
| 447 |
"""
|
|
@@ -514,7 +401,7 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
|
|
| 514 |
|
| 515 |
<!-- Footer -->
|
| 516 |
<div style="text-align: center; padding: 20px; margin-top: 20px; font-size: 0.85em; color: #666;">
|
| 517 |
-
<p>β¨ TravelBuddy AI β’ Powered by OpenAI β’ Real-time Weather β’ Smart Recommendations</p>
|
| 518 |
</div>
|
| 519 |
</div>
|
| 520 |
"""
|
|
@@ -561,7 +448,7 @@ with gr.Blocks(css=css, title="β¨ TravelBuddy AI - Smart Travel Planner") as de
|
|
| 561 |
gr.Markdown("### π― Where's Your Next Adventure?")
|
| 562 |
destination = gr.Textbox(
|
| 563 |
label="Destination",
|
| 564 |
-
placeholder="e.g., Paris,
|
| 565 |
lines=1,
|
| 566 |
show_label=False
|
| 567 |
)
|
|
@@ -624,18 +511,19 @@ with gr.Blocks(css=css, title="β¨ TravelBuddy AI - Smart Travel Planner") as de
|
|
| 624 |
# Examples section
|
| 625 |
gr.HTML("""
|
| 626 |
<div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee;">
|
| 627 |
-
<h3>π
|
| 628 |
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
|
| 629 |
<button onclick="document.querySelector('#destination input').value='Paris';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πΌ Paris</button>
|
| 630 |
-
<button onclick="document.querySelector('#destination input').value='Kigali';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">π Kigali</button>
|
| 631 |
<button onclick="document.querySelector('#destination input').value='Tokyo';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πΎ Tokyo</button>
|
|
|
|
| 632 |
<button onclick="document.querySelector('#destination input').value='New York';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">π½ New York</button>
|
| 633 |
<button onclick="document.querySelector('#destination input').value='Rome';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">ποΈ Rome</button>
|
|
|
|
| 634 |
</div>
|
| 635 |
</div>
|
| 636 |
|
| 637 |
<div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee; color: #666;">
|
| 638 |
-
<small>Powered by OpenAI,
|
| 639 |
</div>
|
| 640 |
""")
|
| 641 |
|
|
|
|
| 5 |
from functools import lru_cache
|
| 6 |
import gradio as gr
|
| 7 |
import openai
|
|
|
|
|
|
|
| 8 |
|
| 9 |
# -------------------- CONFIGURATION --------------------
|
| 10 |
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
|
|
|
| 17 |
else:
|
| 18 |
client = openai.OpenAI(api_key=OPENAI_API_KEY)
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# -------------------- WEATHER FUNCTION --------------------
|
| 21 |
def get_weather(city: str) -> dict:
|
| 22 |
+
"""Fetch real-time weather data for any city."""
|
| 23 |
if not OPENWEATHER_API_KEY:
|
| 24 |
return {
|
| 25 |
"city": city,
|
|
|
|
| 27 |
"condition": "sunny",
|
| 28 |
"humidity": 60,
|
| 29 |
"wind_speed": 10,
|
| 30 |
+
"note": "Add OpenWeather API key for real data"
|
|
|
|
| 31 |
}
|
| 32 |
try:
|
| 33 |
url = "https://api.openweathermap.org/data/2.5/weather"
|
|
|
|
| 36 |
data = response.json()
|
| 37 |
if response.status_code != 200:
|
| 38 |
return {"error": f"Weather API error: {data.get('message', 'unknown')}"}
|
|
|
|
| 39 |
return {
|
| 40 |
"city": city,
|
| 41 |
"temperature": data['main']['temp'],
|
|
|
|
| 42 |
"condition": data['weather'][0]['description'],
|
| 43 |
"humidity": data['main']['humidity'],
|
| 44 |
"wind_speed": data['wind']['speed'],
|
| 45 |
"precipitation": data.get('rain', {}).get('1h', 0)
|
| 46 |
}
|
| 47 |
except Exception as e:
|
| 48 |
+
return {"error": f"Weather error: {str(e)}"}
|
| 49 |
|
| 50 |
+
# -------------------- ATTRACTIONS FUNCTION --------------------
|
| 51 |
+
def get_real_attractions(city: str) -> list:
|
| 52 |
+
"""Get real attractions using SerpAPI first, then OpenAI as fallback."""
|
|
|
|
|
|
|
| 53 |
|
| 54 |
+
# Try SerpAPI first (gives real-time search results)
|
| 55 |
+
if SERP_API_KEY:
|
| 56 |
+
try:
|
| 57 |
+
params = {
|
| 58 |
+
"q": f"top tourist attractions in {city}",
|
| 59 |
+
"api_key": SERP_API_KEY,
|
| 60 |
+
"engine": "google",
|
| 61 |
+
"num": 6
|
| 62 |
+
}
|
| 63 |
+
response = requests.get("https://serpapi.com/search", params=params, timeout=10)
|
| 64 |
+
data = response.json()
|
| 65 |
+
|
| 66 |
+
attractions = []
|
| 67 |
+
for result in data.get("organic_results", [])[:6]:
|
| 68 |
+
title = result.get("title", "")
|
| 69 |
+
# Skip Wikipedia and TripAdvisor pages to get direct attraction names
|
| 70 |
+
if "wikipedia" not in title.lower() and "tripadvisor" not in title.lower():
|
| 71 |
+
# Clean up the title
|
| 72 |
+
name = title.split(" - ")[0].split(" | ")[0].split(":")[0].strip()
|
| 73 |
+
if len(name) > 5 and len(name) < 100: # Valid attraction name
|
| 74 |
+
attractions.append({
|
| 75 |
+
"name": name,
|
| 76 |
+
"description": result.get("snippet", f"Popular attraction in {city}"),
|
| 77 |
+
"entry_fee": "Check website",
|
| 78 |
+
"duration_hours": 2
|
| 79 |
+
})
|
| 80 |
+
|
| 81 |
+
if attractions and len(attractions) >= 3:
|
| 82 |
+
return attractions[:6]
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f"SerpAPI error: {e}")
|
| 85 |
+
|
| 86 |
+
# Fallback to OpenAI if SerpAPI fails or no results
|
| 87 |
+
if client:
|
| 88 |
+
try:
|
| 89 |
+
prompt = f"""List the top 6 REAL tourist attractions in {city}. For each attraction, provide:
|
| 90 |
+
- Name (actual famous attraction name, not generic like 'city center')
|
| 91 |
+
- A brief description (1 sentence)
|
| 92 |
+
- Typical entry fee in USD (use numbers, 0 if free)
|
| 93 |
+
- Approximate visit duration in hours
|
| 94 |
|
| 95 |
+
Return ONLY a valid JSON array with keys: name, description, entry_fee, duration_hours.
|
| 96 |
|
| 97 |
+
Example: [{{"name": "Eiffel Tower", "description": "Iconic iron tower with panoramic views", "entry_fee": 25, "duration_hours": 2.5}}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
Important: Use REAL attractions specific to {city}. Do not use generic names."""
|
| 100 |
+
|
| 101 |
+
response = client.chat.completions.create(
|
| 102 |
+
model="gpt-3.5-turbo",
|
| 103 |
+
messages=[{"role": "user", "content": prompt}],
|
| 104 |
+
temperature=0.7,
|
| 105 |
+
max_tokens=1000
|
| 106 |
+
)
|
| 107 |
+
content = response.choices[0].message.content
|
| 108 |
+
|
| 109 |
+
# Clean response
|
| 110 |
+
content = content.strip()
|
| 111 |
+
if content.startswith('```json'):
|
| 112 |
+
content = content[7:]
|
| 113 |
+
if content.startswith('```'):
|
| 114 |
+
content = content[3:]
|
| 115 |
+
if content.endswith('```'):
|
| 116 |
+
content = content[:-3]
|
| 117 |
+
|
| 118 |
+
attractions = json.loads(content)
|
| 119 |
+
if isinstance(attractions, dict) and "attractions" in attractions:
|
| 120 |
+
attractions = attractions["attractions"]
|
| 121 |
+
elif not isinstance(attractions, list):
|
| 122 |
+
attractions = [attractions]
|
| 123 |
+
|
| 124 |
+
result = []
|
| 125 |
+
for a in attractions[:6]:
|
| 126 |
+
fee = a.get("entry_fee", 0)
|
| 127 |
+
if isinstance(fee, (int, float)):
|
| 128 |
+
if fee == 0:
|
| 129 |
+
fee_display = "Free"
|
| 130 |
+
else:
|
| 131 |
+
fee_display = f"${fee}"
|
| 132 |
else:
|
| 133 |
+
fee_display = str(fee)
|
| 134 |
+
|
| 135 |
+
result.append({
|
| 136 |
+
"name": a.get("name", "Unknown"),
|
| 137 |
+
"description": a.get("description", f"A must-visit attraction in {city}"),
|
| 138 |
+
"entry_fee": fee_display,
|
| 139 |
+
"duration_hours": a.get("duration_hours", 2)
|
| 140 |
+
})
|
| 141 |
|
| 142 |
+
if result:
|
| 143 |
+
return result
|
| 144 |
+
|
| 145 |
+
except Exception as e:
|
| 146 |
+
print(f"OpenAI attractions error: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
# Ultimate fallback - generic but informative
|
| 149 |
return [
|
| 150 |
+
{"name": f"Historic {city} City Center", "description": f"The historic heart of {city} with beautiful architecture and local culture.", "entry_fee": "Free", "duration_hours": 2},
|
| 151 |
+
{"name": f"{city} Cultural Museum", "description": f"Discover the rich history and cultural heritage of {city}.", "entry_fee": "$10", "duration_hours": 2},
|
| 152 |
+
{"name": f"{city} Central Park", "description": f"A beautiful green space perfect for relaxation and outdoor activities.", "entry_fee": "Free", "duration_hours": 1.5},
|
| 153 |
+
{"name": f"{city} Local Market", "description": f"Experience local life at this vibrant market with fresh produce and crafts.", "entry_fee": "Free", "duration_hours": 1.5}
|
| 154 |
]
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
# -------------------- BUDGET CALCULATION --------------------
|
| 157 |
+
def calculate_budget(num_days: int, budget_amount: float = None) -> dict:
|
| 158 |
"""Calculate budget based on days and user input."""
|
| 159 |
if budget_amount:
|
| 160 |
daily_budget = budget_amount / num_days
|
| 161 |
if daily_budget < 100:
|
| 162 |
+
level = "Budget"
|
| 163 |
daily_rates = {"accommodation": 50, "food": 35, "transport": 15, "activities": 10}
|
| 164 |
elif daily_budget < 200:
|
| 165 |
+
level = "Moderate"
|
| 166 |
daily_rates = {"accommodation": 100, "food": 60, "transport": 25, "activities": 20}
|
| 167 |
elif daily_budget < 400:
|
| 168 |
+
level = "Comfortable"
|
| 169 |
daily_rates = {"accommodation": 180, "food": 100, "transport": 35, "activities": 35}
|
| 170 |
else:
|
| 171 |
+
level = "Luxury"
|
| 172 |
daily_rates = {"accommodation": 300, "food": 150, "transport": 50, "activities": 60}
|
| 173 |
else:
|
| 174 |
+
level = "Moderate"
|
| 175 |
daily_rates = {"accommodation": 100, "food": 60, "transport": 25, "activities": 20}
|
| 176 |
|
| 177 |
accommodation = daily_rates["accommodation"] * num_days
|
|
|
|
| 197 |
try:
|
| 198 |
# Validate inputs
|
| 199 |
if not destination:
|
| 200 |
+
return "β Please enter a destination city."
|
| 201 |
|
| 202 |
if num_days < 1 or num_days > 14:
|
| 203 |
+
return "β Number of days must be between 1 and 14."
|
| 204 |
|
| 205 |
+
# Get weather
|
| 206 |
weather = get_weather(destination)
|
| 207 |
if "error" in weather:
|
| 208 |
+
return f"β Weather error: {weather['error']}"
|
| 209 |
|
| 210 |
+
# Get real attractions
|
| 211 |
+
attractions = get_real_attractions(destination)
|
| 212 |
|
| 213 |
# Calculate budget
|
| 214 |
budget_data = calculate_budget(num_days, budget_amount)
|
|
|
|
| 217 |
start = datetime.strptime(start_date, "%Y-%m-%d")
|
| 218 |
end = start + timedelta(days=int(num_days) - 1)
|
| 219 |
|
| 220 |
+
# Generate HTML itinerary
|
| 221 |
+
html = generate_html_itinerary(
|
| 222 |
destination, weather, attractions, budget_data,
|
| 223 |
+
num_days, start, end, budget_amount, departure_city
|
| 224 |
)
|
| 225 |
|
| 226 |
return html
|
| 227 |
|
| 228 |
except Exception as e:
|
| 229 |
+
return f"β An unexpected error occurred: {str(e)}"
|
| 230 |
|
| 231 |
+
def generate_html_itinerary(destination, weather, attractions, budget_data,
|
| 232 |
+
num_days, start_date, end_date, budget_amount, departure_city):
|
| 233 |
+
"""Create beautiful HTML itinerary."""
|
|
|
|
| 234 |
|
| 235 |
# Weather details
|
| 236 |
weather_temp = f"{weather['temperature']:.1f}Β°C" if isinstance(weather['temperature'], (int, float)) else str(weather['temperature'])
|
|
|
|
| 246 |
</div>
|
| 247 |
"""
|
| 248 |
|
| 249 |
+
# Attractions list
|
| 250 |
attractions_html = ""
|
| 251 |
for attr in attractions[:6]:
|
| 252 |
+
attractions_html += f"""
|
| 253 |
+
<div style="background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 12px; border-left: 3px solid #667eea;">
|
| 254 |
+
<strong style="font-size: 1.1em; color: #333;">π {attr['name']}</strong>
|
| 255 |
+
<div style="color: #666; margin: 8px 0; font-size: 0.95em;">{attr['description']}</div>
|
| 256 |
+
<div style="display: flex; gap: 15px; margin-top: 8px; font-size: 0.85em;">
|
| 257 |
+
<span>β±οΈ {attr['duration_hours']} hrs</span>
|
| 258 |
+
<span>ποΈ {attr['entry_fee']}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
</div>
|
| 260 |
+
</div>
|
| 261 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
+
# Daily itinerary
|
| 264 |
daily_html = ""
|
| 265 |
per_day = max(1, len(attractions) // max(1, num_days))
|
| 266 |
|
|
|
|
| 282 |
"""
|
| 283 |
|
| 284 |
for attr in day_attractions:
|
| 285 |
+
daily_html += f"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
<div style="margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
|
| 287 |
<strong>β¨ {attr['name']}</strong><br>
|
| 288 |
<span style="font-size: 0.85em; color: #666;">{attr['description'][:100]}...</span>
|
|
|
|
| 290 |
β±οΈ {attr['duration_hours']} hrs | ποΈ {attr['entry_fee']}
|
| 291 |
</div>
|
| 292 |
</div>
|
| 293 |
+
"""
|
| 294 |
|
| 295 |
daily_html += f"""
|
| 296 |
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
| 297 |
+
<div><span style="font-size: 1.1em;">π½οΈ</span> <strong>Lunch:</strong> Try authentic local cuisine at a nearby restaurant</div>
|
| 298 |
+
<div style="margin-top: 8px;"><span style="font-size: 1.1em;">π</span> <strong>Evening:</strong> Explore local markets, enjoy cultural shows, or relax at a cafe</div>
|
| 299 |
</div>
|
| 300 |
</div>
|
| 301 |
</div>
|
| 302 |
"""
|
| 303 |
|
| 304 |
# Budget breakdown
|
|
|
|
|
|
|
|
|
|
| 305 |
budget_html = f"""
|
| 306 |
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white; margin: 20px 0;">
|
| 307 |
+
<h3 style="margin: 0 0 15px 0;">π° Budget Breakdown ({budget_data['level']} Travel Style)</h3>
|
| 308 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px;">
|
| 309 |
<div><strong>π¨ Accommodation</strong><br>${budget_data['accommodation']:.0f}<br><small>(${budget_data['daily']['accommodation']}/day)</small></div>
|
| 310 |
<div><strong>π½οΈ Food & Dining</strong><br>${budget_data['food']:.0f}<br><small>(${budget_data['daily']['food']}/day)</small></div>
|
|
|
|
| 323 |
<div style="background: #f0f4ff; padding: 20px; border-radius: 12px; margin: 20px 0;">
|
| 324 |
<h3 style="margin: 0 0 15px 0; color: #667eea;">π‘ Smart Travel Tips</h3>
|
| 325 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
|
| 326 |
+
<div>π« <strong>Book in Advance</strong><br>Save time and money on popular attractions</div>
|
| 327 |
+
<div>π <strong>Public Transport</strong><br>Get day passes for unlimited travel savings</div>
|
| 328 |
+
<div>π± <strong>Offline Maps</strong><br>Download Google Maps before your trip</div>
|
| 329 |
+
<div>π΅ <strong>Local Currency</strong><br>Carry cash for markets and small vendors</div>
|
| 330 |
+
<div>π <strong>Learn Basic Phrases</strong><br>A few local words go a long way</div>
|
| 331 |
+
<div>πΈ <strong>Early Bird</strong><br>Visit popular spots early to avoid crowds</div>
|
| 332 |
</div>
|
| 333 |
</div>
|
| 334 |
"""
|
|
|
|
| 401 |
|
| 402 |
<!-- Footer -->
|
| 403 |
<div style="text-align: center; padding: 20px; margin-top: 20px; font-size: 0.85em; color: #666;">
|
| 404 |
+
<p>β¨ TravelBuddy AI β’ Powered by OpenAI, OpenWeather & SerpAPI β’ Real-time Weather β’ Smart Recommendations</p>
|
| 405 |
</div>
|
| 406 |
</div>
|
| 407 |
"""
|
|
|
|
| 448 |
gr.Markdown("### π― Where's Your Next Adventure?")
|
| 449 |
destination = gr.Textbox(
|
| 450 |
label="Destination",
|
| 451 |
+
placeholder="e.g., Paris, Tokyo, Nairobi, New York, Rome...",
|
| 452 |
lines=1,
|
| 453 |
show_label=False
|
| 454 |
)
|
|
|
|
| 511 |
# Examples section
|
| 512 |
gr.HTML("""
|
| 513 |
<div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee;">
|
| 514 |
+
<h3>π Popular Destinations to Try</h3>
|
| 515 |
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
|
| 516 |
<button onclick="document.querySelector('#destination input').value='Paris';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πΌ Paris</button>
|
|
|
|
| 517 |
<button onclick="document.querySelector('#destination input').value='Tokyo';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πΎ Tokyo</button>
|
| 518 |
+
<button onclick="document.querySelector('#destination input').value='Nairobi';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">π¦ Nairobi</button>
|
| 519 |
<button onclick="document.querySelector('#destination input').value='New York';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">π½ New York</button>
|
| 520 |
<button onclick="document.querySelector('#destination input').value='Rome';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">ποΈ Rome</button>
|
| 521 |
+
<button onclick="document.querySelector('#destination input').value='Bangkok';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">π Bangkok</button>
|
| 522 |
</div>
|
| 523 |
</div>
|
| 524 |
|
| 525 |
<div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee; color: #666;">
|
| 526 |
+
<small>Powered by OpenAI, OpenWeather API, and SerpAPI β’ Real-time data for accurate travel planning β¨</small>
|
| 527 |
</div>
|
| 528 |
""")
|
| 529 |
|