Update app.py
Browse files
app.py
CHANGED
|
@@ -26,14 +26,11 @@ def get_destination_image(city: str) -> str:
|
|
| 26 |
"""Fetch a real image of the destination using SerpAPI."""
|
| 27 |
cache_key = city.lower()
|
| 28 |
|
| 29 |
-
# Check cache first
|
| 30 |
if cache_key in IMAGE_CACHE:
|
| 31 |
return IMAGE_CACHE[cache_key]
|
| 32 |
|
| 33 |
-
# Try SerpAPI for real images
|
| 34 |
if SERP_API_KEY:
|
| 35 |
try:
|
| 36 |
-
# Search for beautiful images of the city
|
| 37 |
params = {
|
| 38 |
"q": f"{city} city landmark beautiful view",
|
| 39 |
"api_key": SERP_API_KEY,
|
|
@@ -53,7 +50,6 @@ def get_destination_image(city: str) -> str:
|
|
| 53 |
except Exception as e:
|
| 54 |
print(f"Image fetch error: {e}")
|
| 55 |
|
| 56 |
-
# If no image found, return None (don't show fake image)
|
| 57 |
return None
|
| 58 |
|
| 59 |
# -------------------- WEATHER FUNCTION --------------------
|
|
@@ -86,56 +82,32 @@ def get_weather(city: str) -> dict:
|
|
| 86 |
except Exception as e:
|
| 87 |
return {"error": f"Weather error: {str(e)}"}
|
| 88 |
|
| 89 |
-
# -------------------- ATTRACTIONS FUNCTION --------------------
|
| 90 |
def get_real_attractions(city: str) -> list:
|
| 91 |
-
"""Get real attractions using
|
| 92 |
|
| 93 |
-
# Try
|
| 94 |
-
if SERP_API_KEY:
|
| 95 |
-
try:
|
| 96 |
-
params = {
|
| 97 |
-
"q": f"top tourist attractions in {city}",
|
| 98 |
-
"api_key": SERP_API_KEY,
|
| 99 |
-
"engine": "google",
|
| 100 |
-
"num": 6
|
| 101 |
-
}
|
| 102 |
-
response = requests.get("https://serpapi.com/search", params=params, timeout=10)
|
| 103 |
-
data = response.json()
|
| 104 |
-
|
| 105 |
-
attractions = []
|
| 106 |
-
for result in data.get("organic_results", [])[:6]:
|
| 107 |
-
title = result.get("title", "")
|
| 108 |
-
# Skip Wikipedia and TripAdvisor pages to get direct attraction names
|
| 109 |
-
if "wikipedia" not in title.lower() and "tripadvisor" not in title.lower():
|
| 110 |
-
# Clean up the title
|
| 111 |
-
name = title.split(" - ")[0].split(" | ")[0].split(":")[0].strip()
|
| 112 |
-
if len(name) > 5 and len(name) < 100: # Valid attraction name
|
| 113 |
-
attractions.append({
|
| 114 |
-
"name": name,
|
| 115 |
-
"description": result.get("snippet", f"Popular attraction in {city}"),
|
| 116 |
-
"entry_fee": "Check website",
|
| 117 |
-
"duration_hours": 2
|
| 118 |
-
})
|
| 119 |
-
|
| 120 |
-
if attractions and len(attractions) >= 3:
|
| 121 |
-
return attractions[:6]
|
| 122 |
-
except Exception as e:
|
| 123 |
-
print(f"SerpAPI error: {e}")
|
| 124 |
-
|
| 125 |
-
# Fallback to OpenAI if SerpAPI fails or no results
|
| 126 |
if client:
|
| 127 |
try:
|
| 128 |
prompt = f"""List the top 6 REAL tourist attractions in {city}. For each attraction, provide:
|
| 129 |
-
- Name (actual famous attraction name
|
| 130 |
-
- A brief description (
|
| 131 |
- Typical entry fee in USD (use numbers, 0 if free)
|
| 132 |
- Approximate visit duration in hours
|
| 133 |
|
| 134 |
Return ONLY a valid JSON array with keys: name, description, entry_fee, duration_hours.
|
| 135 |
|
| 136 |
-
Example
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
Important: Use REAL attractions specific to {city}.
|
| 139 |
|
| 140 |
response = client.chat.completions.create(
|
| 141 |
model="gpt-3.5-turbo",
|
|
@@ -184,12 +156,41 @@ Important: Use REAL attractions specific to {city}. Do not use generic names."""
|
|
| 184 |
except Exception as e:
|
| 185 |
print(f"OpenAI attractions error: {e}")
|
| 186 |
|
| 187 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
return [
|
| 189 |
-
{"name": f"Historic {city} City Center", "description": f"
|
| 190 |
-
{"name": f"{city} Cultural Museum", "description": f"Discover
|
| 191 |
-
{"name": f"{city} Central Park", "description": f"
|
| 192 |
-
{"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}
|
| 193 |
]
|
| 194 |
|
| 195 |
# -------------------- BUDGET CALCULATION --------------------
|
|
@@ -234,32 +235,23 @@ def generate_itinerary(destination: str, start_date: str, num_days: int,
|
|
| 234 |
budget_amount: float, budget_currency: str, departure_city: str = ""):
|
| 235 |
"""Main itinerary generation function."""
|
| 236 |
try:
|
| 237 |
-
# Validate inputs
|
| 238 |
if not destination:
|
| 239 |
return "β Please enter a destination city."
|
| 240 |
|
| 241 |
if num_days < 1 or num_days > 14:
|
| 242 |
return "β Number of days must be between 1 and 14."
|
| 243 |
|
| 244 |
-
# Get weather
|
| 245 |
weather = get_weather(destination)
|
| 246 |
if "error" in weather:
|
| 247 |
return f"β Weather error: {weather['error']}"
|
| 248 |
|
| 249 |
-
# Get real attractions
|
| 250 |
attractions = get_real_attractions(destination)
|
| 251 |
-
|
| 252 |
-
# Get destination image
|
| 253 |
destination_image = get_destination_image(destination)
|
| 254 |
-
|
| 255 |
-
# Calculate budget
|
| 256 |
budget_data = calculate_budget(num_days, budget_amount)
|
| 257 |
|
| 258 |
-
# Format dates
|
| 259 |
start = datetime.strptime(start_date, "%Y-%m-%d")
|
| 260 |
end = start + timedelta(days=int(num_days) - 1)
|
| 261 |
|
| 262 |
-
# Generate HTML itinerary
|
| 263 |
html = generate_html_itinerary(
|
| 264 |
destination, weather, attractions, budget_data,
|
| 265 |
num_days, start, end, budget_amount, departure_city, destination_image
|
|
@@ -272,9 +264,8 @@ def generate_itinerary(destination: str, start_date: str, num_days: int,
|
|
| 272 |
|
| 273 |
def generate_html_itinerary(destination, weather, attractions, budget_data,
|
| 274 |
num_days, start_date, end_date, budget_amount, departure_city, destination_image):
|
| 275 |
-
"""Create beautiful HTML itinerary with
|
| 276 |
|
| 277 |
-
# Weather details
|
| 278 |
weather_temp = f"{weather['temperature']:.1f}Β°C" if isinstance(weather['temperature'], (int, float)) else str(weather['temperature'])
|
| 279 |
weather_condition = weather['condition'].capitalize()
|
| 280 |
|
|
@@ -288,78 +279,81 @@ def generate_html_itinerary(destination, weather, attractions, budget_data,
|
|
| 288 |
</div>
|
| 289 |
"""
|
| 290 |
|
| 291 |
-
#
|
| 292 |
-
attractions_html = ""
|
| 293 |
-
for attr in attractions[:6]:
|
| 294 |
-
attractions_html += f"""
|
| 295 |
-
<div style="background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 12px; border-left: 3px solid #667eea;">
|
| 296 |
-
<strong style="font-size: 1.1em; color: #333;">π {attr['name']}</strong>
|
| 297 |
-
<div style="color: #666; margin: 8px 0; font-size: 0.95em;">{attr['description']}</div>
|
| 298 |
-
<div style="display: flex; gap: 15px; margin-top: 8px; font-size: 0.85em;">
|
| 299 |
-
<span>β±οΈ {attr['duration_hours']} hrs</span>
|
| 300 |
-
<span>ποΈ {attr['entry_fee']}</span>
|
| 301 |
-
</div>
|
| 302 |
-
</div>
|
| 303 |
-
"""
|
| 304 |
-
|
| 305 |
-
# Daily itinerary
|
| 306 |
daily_html = ""
|
| 307 |
-
|
|
|
|
|
|
|
| 308 |
|
| 309 |
for day in range(1, num_days + 1):
|
| 310 |
current_date = start_date + timedelta(days=day-1)
|
| 311 |
date_str = current_date.strftime("%A, %B %d")
|
| 312 |
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
| 316 |
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
for attr in day_attractions:
|
| 327 |
-
daily_html += f"""
|
| 328 |
-
<div style="margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
|
| 329 |
-
<strong>β¨ {attr['name']}</strong><br>
|
| 330 |
-
<span style="font-size: 0.85em; color: #666;">{attr['description'][:100]}...</span>
|
| 331 |
-
<div style="font-size: 0.75em; color: #888; margin-top: 5px;">
|
| 332 |
-
β±οΈ {attr['duration_hours']} hrs | ποΈ {attr['entry_fee']}
|
| 333 |
-
</div>
|
| 334 |
-
</div>
|
| 335 |
-
"""
|
| 336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
daily_html += f"""
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
<div style="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
</div>
|
| 342 |
</div>
|
| 343 |
-
</div>
|
| 344 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
-
# Destination Image Section
|
| 347 |
image_html = ""
|
| 348 |
if destination_image:
|
| 349 |
image_html = f"""
|
| 350 |
<div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05); text-align: center;">
|
| 351 |
<h2 style="margin: 0 0 15px 0; color: #667eea;">πΈ A Glimpse of {destination}</h2>
|
| 352 |
-
<div style="font-size: 0.85em; color: #888; margin-bottom: 15px;">π Real image from {destination} - Let this inspire your journey!</div>
|
| 353 |
<div style="border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
|
| 354 |
-
<img src="{destination_image}"
|
| 355 |
-
style="width: 100%; max-height: 400px; object-fit: cover; transition: transform 0.3s;"
|
| 356 |
-
onmouseover="this.style.transform='scale(1.02)'"
|
| 357 |
-
onmouseout="this.style.transform='scale(1)'"
|
| 358 |
-
alt="{destination}">
|
| 359 |
</div>
|
| 360 |
-
<p style="margin-top: 15px; font-size: 0.9em; color: #666;">
|
| 361 |
-
π― <strong>Ready for your adventure?</strong> {destination} awaits with unforgettable experiences!
|
| 362 |
-
</p>
|
| 363 |
</div>
|
| 364 |
"""
|
| 365 |
|
|
@@ -371,7 +365,7 @@ def generate_html_itinerary(destination, weather, attractions, budget_data,
|
|
| 371 |
<div><strong>π¨ Accommodation</strong><br>${budget_data['accommodation']:.0f}<br><small>(${budget_data['daily']['accommodation']}/day)</small></div>
|
| 372 |
<div><strong>π½οΈ Food & Dining</strong><br>${budget_data['food']:.0f}<br><small>(${budget_data['daily']['food']}/day)</small></div>
|
| 373 |
<div><strong>π Local Transport</strong><br>${budget_data['transport']:.0f}<br><small>(${budget_data['daily']['transport']}/day)</small></div>
|
| 374 |
-
<div><strong>ποΈ Activities
|
| 375 |
<div style="border-top: 2px solid rgba(255,255,255,0.3); padding-top: 10px; grid-column: 1/-1;">
|
| 376 |
<strong>π° Total Estimated Cost</strong><br><span style="font-size: 1.2em;">${budget_data['total']:.0f}</span>
|
| 377 |
{f" (Your budget: ${budget_amount:.0f})" if budget_amount else ""}
|
|
@@ -380,19 +374,9 @@ def generate_html_itinerary(destination, weather, attractions, budget_data,
|
|
| 380 |
</div>
|
| 381 |
"""
|
| 382 |
|
| 383 |
-
#
|
| 384 |
-
|
| 385 |
-
<
|
| 386 |
-
<h3 style="margin: 0 0 15px 0; color: #667eea;">π‘ Smart Travel Tips</h3>
|
| 387 |
-
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
|
| 388 |
-
<div>π« <strong>Book in Advance</strong><br>Save time and money on popular attractions</div>
|
| 389 |
-
<div>π <strong>Public Transport</strong><br>Get day passes for unlimited travel savings</div>
|
| 390 |
-
<div>π± <strong>Offline Maps</strong><br>Download Google Maps before your trip</div>
|
| 391 |
-
<div>π΅ <strong>Local Currency</strong><br>Carry cash for markets and small vendors</div>
|
| 392 |
-
<div>π <strong>Learn Basic Phrases</strong><br>A few local words go a long way</div>
|
| 393 |
-
<div>πΈ <strong>Early Bird</strong><br>Visit popular spots early to avoid crowds</div>
|
| 394 |
-
</div>
|
| 395 |
-
</div>
|
| 396 |
"""
|
| 397 |
|
| 398 |
# Complete HTML
|
|
@@ -404,11 +388,6 @@ def generate_html_itinerary(destination, weather, attractions, budget_data,
|
|
| 404 |
<p style="margin: 10px 0 0; opacity: 0.9; font-size: 1.1em;">
|
| 405 |
Intelligent Itinerary Generation β’ Real-time Data β’ Smart Recommendations
|
| 406 |
</p>
|
| 407 |
-
<div style="margin-top: 15px;">
|
| 408 |
-
<span style="background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.85em;">
|
| 409 |
-
π§ Powered by Multi-Agent AI System
|
| 410 |
-
</span>
|
| 411 |
-
</div>
|
| 412 |
<h2 style="margin: 20px 0 0; font-size: 1.8em;">π {destination}</h2>
|
| 413 |
<p style="margin: 10px 0 0; opacity: 0.9;">
|
| 414 |
{num_days} Days of Adventure β’ {start_date.strftime('%B %d')} - {end_date.strftime('%B %d, %Y')}
|
|
@@ -416,39 +395,27 @@ def generate_html_itinerary(destination, weather, attractions, budget_data,
|
|
| 416 |
{f"<p style='margin: 5px 0 0; opacity: 0.8;'>βοΈ From: {departure_city}</p>" if departure_city else ""}
|
| 417 |
</div>
|
| 418 |
|
| 419 |
-
<!-- AI Agent Badge -->
|
| 420 |
-
<div style="text-align: center; margin-bottom: 20px;">
|
| 421 |
-
<div style="display: inline-block; background: #e8f0fe; padding: 8px 20px; border-radius: 30px;">
|
| 422 |
-
<span style="color: #667eea;">π€</span>
|
| 423 |
-
<strong>AI Agents Active:</strong> Weather Agent β’ Attraction Scout β’ Budget Optimizer β’ Itinerary Planner
|
| 424 |
-
</div>
|
| 425 |
-
</div>
|
| 426 |
-
|
| 427 |
<!-- Quick Stats -->
|
| 428 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;">
|
| 429 |
<div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
|
| 430 |
<div style="font-size: 2em;">π€οΈ</div>
|
| 431 |
<strong>{weather_temp}</strong><br>
|
| 432 |
<small>{weather_condition}</small>
|
| 433 |
-
<div style="font-size: 0.75em; color: #888; margin-top: 5px;">Weather Agent</div>
|
| 434 |
</div>
|
| 435 |
<div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
|
| 436 |
<div style="font-size: 2em;">π
</div>
|
| 437 |
<strong>{num_days} Days</strong><br>
|
| 438 |
<small>Full Itinerary</small>
|
| 439 |
-
<div style="font-size: 0.75em; color: #888; margin-top: 5px;">Planner Agent</div>
|
| 440 |
</div>
|
| 441 |
<div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
|
| 442 |
<div style="font-size: 2em;">π―</div>
|
| 443 |
-
<strong>{len(attractions)}+
|
| 444 |
<small>To Explore</small>
|
| 445 |
-
<div style="font-size: 0.75em; color: #888; margin-top: 5px;">Scout Agent</div>
|
| 446 |
</div>
|
| 447 |
<div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
|
| 448 |
<div style="font-size: 2em;">π°</div>
|
| 449 |
<strong>${budget_data['total']:.0f}</strong><br>
|
| 450 |
-
<small>
|
| 451 |
-
<div style="font-size: 0.75em; color: #888; margin-top: 5px;">Budget Agent</div>
|
| 452 |
</div>
|
| 453 |
</div>
|
| 454 |
|
|
@@ -457,25 +424,26 @@ def generate_html_itinerary(destination, weather, attractions, budget_data,
|
|
| 457 |
<!-- Budget Section -->
|
| 458 |
{budget_html}
|
| 459 |
|
| 460 |
-
<!-- Top Attractions -->
|
| 461 |
-
<div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
|
| 462 |
-
<h2 style="margin: 0 0 15px 0; color: #667eea;">β¨ AI-Curated Top Attractions in {destination}</h2>
|
| 463 |
-
<div style="font-size: 0.85em; color: #888; margin-bottom: 15px;">π Scout Agent Analysis β’ Real-time Data</div>
|
| 464 |
-
{attractions_html}
|
| 465 |
-
</div>
|
| 466 |
-
|
| 467 |
<!-- Daily Itinerary -->
|
| 468 |
<div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
|
| 469 |
<h2 style="margin: 0 0 15px 0; color: #667eea;">π
AI-Generated {num_days}-Day Itinerary</h2>
|
| 470 |
-
<div style="font-size: 0.85em; color: #888; margin-bottom: 15px;">π€ Planner Agent β’ Optimized Schedule</div>
|
| 471 |
{daily_html}
|
| 472 |
</div>
|
| 473 |
|
| 474 |
-
<!-- Destination Image
|
| 475 |
{image_html}
|
| 476 |
|
| 477 |
<!-- Travel Tips -->
|
| 478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
|
| 480 |
<!-- Booking Links -->
|
| 481 |
<div style="text-align: center; padding: 20px; margin-top: 20px; background: #f8f9fa; border-radius: 12px;">
|
|
@@ -484,13 +452,13 @@ def generate_html_itinerary(destination, weather, attractions, budget_data,
|
|
| 484 |
ποΈ <a href="https://www.booking.com/searchresults.html?ss={destination.replace(' ', '+')}" target="_blank" style="color: #667eea;">Search Hotels</a> |
|
| 485 |
βοΈ <a href="https://www.skyscanner.net/" target="_blank" style="color: #667eea;">Search Flights</a> |
|
| 486 |
ποΈ <a href="https://www.tripadvisor.com/Search?q={destination}" target="_blank" style="color: #667eea;">Read Reviews</a>
|
|
|
|
| 487 |
</p>
|
| 488 |
</div>
|
| 489 |
|
| 490 |
<!-- Footer -->
|
| 491 |
<div style="text-align: center; padding: 20px; margin-top: 20px; font-size: 0.85em; color: #666;">
|
| 492 |
-
<p>π€ Agentic AI System β’ Powered by OpenAI, OpenWeather & SerpAPI
|
| 493 |
-
<p>β‘ Weather Agent β’ Scout Agent β’ Budget Agent β’ Planner Agent β’ All Working in Harmony</p>
|
| 494 |
</div>
|
| 495 |
</div>
|
| 496 |
"""
|
|
@@ -529,18 +497,10 @@ with gr.Blocks(css=css, title="π€ Agentic AI Travel Planner") as demo:
|
|
| 529 |
Multi-Agent System β’ Intelligent Itinerary Generation β’ Real-time Data Processing
|
| 530 |
</p>
|
| 531 |
<div style="margin-top: 15px;">
|
| 532 |
-
<span style="background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.85em; margin: 0 5px;">
|
| 533 |
-
|
| 534 |
-
</span>
|
| 535 |
-
<span style="background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.85em; margin: 0 5px;">
|
| 536 |
-
π Scout Agent
|
| 537 |
-
</span>
|
| 538 |
-
<span style="background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.85em; margin: 0 5px;">
|
| 539 |
-
π° Budget Agent
|
| 540 |
-
</span>
|
| 541 |
-
<span style="background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.85em; margin: 0 5px;">
|
| 542 |
-
π
Planner Agent
|
| 543 |
-
</span>
|
| 544 |
</div>
|
| 545 |
</div>
|
| 546 |
""")
|
|
@@ -567,7 +527,7 @@ with gr.Blocks(css=css, title="π€ Agentic AI Travel Planner") as demo:
|
|
| 567 |
num_days = gr.Slider(
|
| 568 |
label="Duration (Days)",
|
| 569 |
minimum=1,
|
| 570 |
-
maximum=
|
| 571 |
value=3,
|
| 572 |
step=1
|
| 573 |
)
|
|
@@ -601,7 +561,7 @@ with gr.Blocks(css=css, title="π€ Agentic AI Travel Planner") as demo:
|
|
| 601 |
)
|
| 602 |
|
| 603 |
with gr.Row():
|
| 604 |
-
generate_btn = gr.Button("
|
| 605 |
|
| 606 |
output = gr.HTML()
|
| 607 |
|
|
|
|
| 26 |
"""Fetch a real image of the destination using SerpAPI."""
|
| 27 |
cache_key = city.lower()
|
| 28 |
|
|
|
|
| 29 |
if cache_key in IMAGE_CACHE:
|
| 30 |
return IMAGE_CACHE[cache_key]
|
| 31 |
|
|
|
|
| 32 |
if SERP_API_KEY:
|
| 33 |
try:
|
|
|
|
| 34 |
params = {
|
| 35 |
"q": f"{city} city landmark beautiful view",
|
| 36 |
"api_key": SERP_API_KEY,
|
|
|
|
| 50 |
except Exception as e:
|
| 51 |
print(f"Image fetch error: {e}")
|
| 52 |
|
|
|
|
| 53 |
return None
|
| 54 |
|
| 55 |
# -------------------- WEATHER FUNCTION --------------------
|
|
|
|
| 82 |
except Exception as e:
|
| 83 |
return {"error": f"Weather error: {str(e)}"}
|
| 84 |
|
| 85 |
+
# -------------------- REAL ATTRACTIONS FUNCTION --------------------
|
| 86 |
def get_real_attractions(city: str) -> list:
|
| 87 |
+
"""Get real attractions using OpenAI first for better quality."""
|
| 88 |
|
| 89 |
+
# Try OpenAI first for accurate attraction names and activities
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
if client:
|
| 91 |
try:
|
| 92 |
prompt = f"""List the top 6 REAL tourist attractions in {city}. For each attraction, provide:
|
| 93 |
+
- Name (actual famous attraction name)
|
| 94 |
+
- A brief description including specific ACTIVITIES you can do there (use action words like: swimming, hiking, skating, climbing, boating, cycling, shopping, dining, photography, sightseeing, museum visiting, etc.)
|
| 95 |
- Typical entry fee in USD (use numbers, 0 if free)
|
| 96 |
- Approximate visit duration in hours
|
| 97 |
|
| 98 |
Return ONLY a valid JSON array with keys: name, description, entry_fee, duration_hours.
|
| 99 |
|
| 100 |
+
Example for Paris:
|
| 101 |
+
[
|
| 102 |
+
{{
|
| 103 |
+
"name": "Eiffel Tower",
|
| 104 |
+
"description": "Climb to the top for panoramic views, dine at the restaurant, and enjoy photography at sunset.",
|
| 105 |
+
"entry_fee": 25,
|
| 106 |
+
"duration_hours": 2.5
|
| 107 |
+
}}
|
| 108 |
+
]
|
| 109 |
|
| 110 |
+
Important: Use REAL attractions specific to {city}. Make descriptions include ACTION WORDS like hiking, swimming, exploring, climbing."""
|
| 111 |
|
| 112 |
response = client.chat.completions.create(
|
| 113 |
model="gpt-3.5-turbo",
|
|
|
|
| 156 |
except Exception as e:
|
| 157 |
print(f"OpenAI attractions error: {e}")
|
| 158 |
|
| 159 |
+
# Fallback to SerpAPI
|
| 160 |
+
if SERP_API_KEY:
|
| 161 |
+
try:
|
| 162 |
+
params = {
|
| 163 |
+
"q": f"top tourist attractions in {city}",
|
| 164 |
+
"api_key": SERP_API_KEY,
|
| 165 |
+
"engine": "google",
|
| 166 |
+
"num": 6
|
| 167 |
+
}
|
| 168 |
+
response = requests.get("https://serpapi.com/search", params=params, timeout=10)
|
| 169 |
+
data = response.json()
|
| 170 |
+
|
| 171 |
+
attractions = []
|
| 172 |
+
for result in data.get("organic_results", [])[:6]:
|
| 173 |
+
title = result.get("title", "")
|
| 174 |
+
if "wikipedia" not in title.lower() and "tripadvisor" not in title.lower():
|
| 175 |
+
name = title.split(" - ")[0].split(" | ")[0].split(":")[0].strip()
|
| 176 |
+
if len(name) > 5 and len(name) < 100:
|
| 177 |
+
attractions.append({
|
| 178 |
+
"name": name,
|
| 179 |
+
"description": result.get("snippet", f"Explore this popular attraction in {city}"),
|
| 180 |
+
"entry_fee": "Check website",
|
| 181 |
+
"duration_hours": 2
|
| 182 |
+
})
|
| 183 |
+
|
| 184 |
+
if attractions and len(attractions) >= 3:
|
| 185 |
+
return attractions[:6]
|
| 186 |
+
except Exception as e:
|
| 187 |
+
print(f"SerpAPI error: {e}")
|
| 188 |
+
|
| 189 |
+
# Ultimate fallback
|
| 190 |
return [
|
| 191 |
+
{"name": f"Historic {city} City Center", "description": f"Explore the historic heart of {city} with walking tours, shopping, and photography.", "entry_fee": "Free", "duration_hours": 2},
|
| 192 |
+
{"name": f"{city} Cultural Museum", "description": f"Discover art and history through guided tours and interactive exhibits.", "entry_fee": "$10", "duration_hours": 2},
|
| 193 |
+
{"name": f"{city} Central Park", "description": f"Enjoy hiking, picnicking, and outdoor activities in this beautiful green space.", "entry_fee": "Free", "duration_hours": 1.5}
|
|
|
|
| 194 |
]
|
| 195 |
|
| 196 |
# -------------------- BUDGET CALCULATION --------------------
|
|
|
|
| 235 |
budget_amount: float, budget_currency: str, departure_city: str = ""):
|
| 236 |
"""Main itinerary generation function."""
|
| 237 |
try:
|
|
|
|
| 238 |
if not destination:
|
| 239 |
return "β Please enter a destination city."
|
| 240 |
|
| 241 |
if num_days < 1 or num_days > 14:
|
| 242 |
return "β Number of days must be between 1 and 14."
|
| 243 |
|
|
|
|
| 244 |
weather = get_weather(destination)
|
| 245 |
if "error" in weather:
|
| 246 |
return f"β Weather error: {weather['error']}"
|
| 247 |
|
|
|
|
| 248 |
attractions = get_real_attractions(destination)
|
|
|
|
|
|
|
| 249 |
destination_image = get_destination_image(destination)
|
|
|
|
|
|
|
| 250 |
budget_data = calculate_budget(num_days, budget_amount)
|
| 251 |
|
|
|
|
| 252 |
start = datetime.strptime(start_date, "%Y-%m-%d")
|
| 253 |
end = start + timedelta(days=int(num_days) - 1)
|
| 254 |
|
|
|
|
| 255 |
html = generate_html_itinerary(
|
| 256 |
destination, weather, attractions, budget_data,
|
| 257 |
num_days, start, end, budget_amount, departure_city, destination_image
|
|
|
|
| 264 |
|
| 265 |
def generate_html_itinerary(destination, weather, attractions, budget_data,
|
| 266 |
num_days, start_date, end_date, budget_amount, departure_city, destination_image):
|
| 267 |
+
"""Create beautiful HTML itinerary with max 3 attractions per day."""
|
| 268 |
|
|
|
|
| 269 |
weather_temp = f"{weather['temperature']:.1f}Β°C" if isinstance(weather['temperature'], (int, float)) else str(weather['temperature'])
|
| 270 |
weather_condition = weather['condition'].capitalize()
|
| 271 |
|
|
|
|
| 279 |
</div>
|
| 280 |
"""
|
| 281 |
|
| 282 |
+
# Daily itinerary - MAX 3 ATTRACTIONS PER DAY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
daily_html = ""
|
| 284 |
+
# Distribute attractions: max 3 per day
|
| 285 |
+
attractions_per_day = min(3, len(attractions))
|
| 286 |
+
total_attractions = min(len(attractions), attractions_per_day * num_days)
|
| 287 |
|
| 288 |
for day in range(1, num_days + 1):
|
| 289 |
current_date = start_date + timedelta(days=day-1)
|
| 290 |
date_str = current_date.strftime("%A, %B %d")
|
| 291 |
|
| 292 |
+
# Get attractions for this day (max 3 different attractions)
|
| 293 |
+
start_idx = (day-1) * attractions_per_day
|
| 294 |
+
end_idx = min(day * attractions_per_day, total_attractions)
|
| 295 |
+
day_attractions = attractions[start_idx:end_idx] if start_idx < len(attractions) else []
|
| 296 |
|
| 297 |
+
# If no more unique attractions, use activities based on weather
|
| 298 |
+
if not day_attractions:
|
| 299 |
+
weather_lower = weather_condition.lower()
|
| 300 |
+
if "rain" in weather_lower:
|
| 301 |
+
activities = ["Visit indoor museums", "Enjoy shopping malls", "Try local cooking classes"]
|
| 302 |
+
elif "sun" in weather_lower or "clear" in weather_lower:
|
| 303 |
+
activities = ["Outdoor hiking", "Parks and gardens", "City walking tours"]
|
| 304 |
+
else:
|
| 305 |
+
activities = ["Cultural experiences", "Local food tasting", "Art gallery visits"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
|
| 307 |
+
day_attractions = [
|
| 308 |
+
{"name": activities[0], "description": f"Enjoy {activities[0].lower()} in {destination}", "entry_fee": "Varies", "duration_hours": 2},
|
| 309 |
+
{"name": activities[1], "description": f"Experience {activities[1].lower()} in {destination}", "entry_fee": "Varies", "duration_hours": 2},
|
| 310 |
+
{"name": activities[2], "description": f"Explore {activities[2].lower()} in {destination}", "entry_fee": "Varies", "duration_hours": 2}
|
| 311 |
+
][:attractions_per_day]
|
| 312 |
+
|
| 313 |
+
daily_html += f"""
|
| 314 |
+
<div style="background: white; border-radius: 12px; margin-bottom: 20px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
|
| 315 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 12px 20px; color: white;">
|
| 316 |
+
<h3 style="margin: 0; font-size: 1.2em;">Day {day} Β· {date_str}</h3>
|
| 317 |
+
</div>
|
| 318 |
+
<div style="padding: 20px;">
|
| 319 |
+
"""
|
| 320 |
+
|
| 321 |
+
for i, attr in enumerate(day_attractions, 1):
|
| 322 |
daily_html += f"""
|
| 323 |
+
<div style="margin-bottom: 15px; padding: 12px; background: #f8f9fa; border-radius: 8px;">
|
| 324 |
+
<div style="display: flex; align-items: center; gap: 10px;">
|
| 325 |
+
<div style="background: #667eea; color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold;">
|
| 326 |
+
{i}
|
| 327 |
+
</div>
|
| 328 |
+
<div style="flex: 1;">
|
| 329 |
+
<strong style="font-size: 1em;">π― {attr['name']}</strong><br>
|
| 330 |
+
<span style="font-size: 0.85em; color: #666;">{attr['description']}</span>
|
| 331 |
+
<div style="font-size: 0.75em; color: #888; margin-top: 5px;">
|
| 332 |
+
β±οΈ {attr['duration_hours']} hrs | ποΈ {attr['entry_fee']}
|
| 333 |
+
</div>
|
| 334 |
+
</div>
|
| 335 |
</div>
|
| 336 |
</div>
|
|
|
|
| 337 |
"""
|
| 338 |
+
|
| 339 |
+
daily_html += f"""
|
| 340 |
+
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
| 341 |
+
<div><span style="font-size: 1.1em;">π½οΈ</span> <strong>Lunch:</strong> Try authentic local cuisine at a nearby restaurant</div>
|
| 342 |
+
<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>
|
| 343 |
+
</div>
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
"""
|
| 347 |
|
| 348 |
+
# Destination Image Section
|
| 349 |
image_html = ""
|
| 350 |
if destination_image:
|
| 351 |
image_html = f"""
|
| 352 |
<div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05); text-align: center;">
|
| 353 |
<h2 style="margin: 0 0 15px 0; color: #667eea;">πΈ A Glimpse of {destination}</h2>
|
|
|
|
| 354 |
<div style="border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
|
| 355 |
+
<img src="{destination_image}" style="width: 100%; max-height: 400px; object-fit: cover;" alt="{destination}">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
</div>
|
|
|
|
|
|
|
|
|
|
| 357 |
</div>
|
| 358 |
"""
|
| 359 |
|
|
|
|
| 365 |
<div><strong>π¨ Accommodation</strong><br>${budget_data['accommodation']:.0f}<br><small>(${budget_data['daily']['accommodation']}/day)</small></div>
|
| 366 |
<div><strong>π½οΈ Food & Dining</strong><br>${budget_data['food']:.0f}<br><small>(${budget_data['daily']['food']}/day)</small></div>
|
| 367 |
<div><strong>π Local Transport</strong><br>${budget_data['transport']:.0f}<br><small>(${budget_data['daily']['transport']}/day)</small></div>
|
| 368 |
+
<div><strong>ποΈ Activities</strong><br>${budget_data['activities']:.0f}<br><small>(${budget_data['daily']['activities']}/day)</small></div>
|
| 369 |
<div style="border-top: 2px solid rgba(255,255,255,0.3); padding-top: 10px; grid-column: 1/-1;">
|
| 370 |
<strong>π° Total Estimated Cost</strong><br><span style="font-size: 1.2em;">${budget_data['total']:.0f}</span>
|
| 371 |
{f" (Your budget: ${budget_amount:.0f})" if budget_amount else ""}
|
|
|
|
| 374 |
</div>
|
| 375 |
"""
|
| 376 |
|
| 377 |
+
# Currency conversion link
|
| 378 |
+
currency_link = f"""
|
| 379 |
+
<a href="https://www.xe.com/currencyconverter/" target="_blank" style="color: #667eea; text-decoration: none; margin-left: 15px;">π± Currency Converter</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
"""
|
| 381 |
|
| 382 |
# Complete HTML
|
|
|
|
| 388 |
<p style="margin: 10px 0 0; opacity: 0.9; font-size: 1.1em;">
|
| 389 |
Intelligent Itinerary Generation β’ Real-time Data β’ Smart Recommendations
|
| 390 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
<h2 style="margin: 20px 0 0; font-size: 1.8em;">π {destination}</h2>
|
| 392 |
<p style="margin: 10px 0 0; opacity: 0.9;">
|
| 393 |
{num_days} Days of Adventure β’ {start_date.strftime('%B %d')} - {end_date.strftime('%B %d, %Y')}
|
|
|
|
| 395 |
{f"<p style='margin: 5px 0 0; opacity: 0.8;'>βοΈ From: {departure_city}</p>" if departure_city else ""}
|
| 396 |
</div>
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
<!-- Quick Stats -->
|
| 399 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;">
|
| 400 |
<div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
|
| 401 |
<div style="font-size: 2em;">π€οΈ</div>
|
| 402 |
<strong>{weather_temp}</strong><br>
|
| 403 |
<small>{weather_condition}</small>
|
|
|
|
| 404 |
</div>
|
| 405 |
<div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
|
| 406 |
<div style="font-size: 2em;">π
</div>
|
| 407 |
<strong>{num_days} Days</strong><br>
|
| 408 |
<small>Full Itinerary</small>
|
|
|
|
| 409 |
</div>
|
| 410 |
<div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
|
| 411 |
<div style="font-size: 2em;">π―</div>
|
| 412 |
+
<strong>{min(len(attractions), num_days * 3)}+ Activities</strong><br>
|
| 413 |
<small>To Explore</small>
|
|
|
|
| 414 |
</div>
|
| 415 |
<div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
|
| 416 |
<div style="font-size: 2em;">π°</div>
|
| 417 |
<strong>${budget_data['total']:.0f}</strong><br>
|
| 418 |
+
<small>Estimated Total</small>
|
|
|
|
| 419 |
</div>
|
| 420 |
</div>
|
| 421 |
|
|
|
|
| 424 |
<!-- Budget Section -->
|
| 425 |
{budget_html}
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
<!-- Daily Itinerary -->
|
| 428 |
<div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
|
| 429 |
<h2 style="margin: 0 0 15px 0; color: #667eea;">π
AI-Generated {num_days}-Day Itinerary</h2>
|
| 430 |
+
<div style="font-size: 0.85em; color: #888; margin-bottom: 15px;">π€ Planner Agent β’ Optimized Schedule β’ Max 3 Activities Per Day</div>
|
| 431 |
{daily_html}
|
| 432 |
</div>
|
| 433 |
|
| 434 |
+
<!-- Destination Image -->
|
| 435 |
{image_html}
|
| 436 |
|
| 437 |
<!-- Travel Tips -->
|
| 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 you go</div>
|
| 444 |
+
<div>π΅ <strong>Local Currency</strong><br>Carry cash for markets</div>
|
| 445 |
+
</div>
|
| 446 |
+
</div>
|
| 447 |
|
| 448 |
<!-- Booking Links -->
|
| 449 |
<div style="text-align: center; padding: 20px; margin-top: 20px; background: #f8f9fa; border-radius: 12px;">
|
|
|
|
| 452 |
ποΈ <a href="https://www.booking.com/searchresults.html?ss={destination.replace(' ', '+')}" target="_blank" style="color: #667eea;">Search Hotels</a> |
|
| 453 |
βοΈ <a href="https://www.skyscanner.net/" target="_blank" style="color: #667eea;">Search Flights</a> |
|
| 454 |
ποΈ <a href="https://www.tripadvisor.com/Search?q={destination}" target="_blank" style="color: #667eea;">Read Reviews</a>
|
| 455 |
+
{currency_link}
|
| 456 |
</p>
|
| 457 |
</div>
|
| 458 |
|
| 459 |
<!-- Footer -->
|
| 460 |
<div style="text-align: center; padding: 20px; margin-top: 20px; font-size: 0.85em; color: #666;">
|
| 461 |
+
<p>π€ Agentic AI System β’ Powered by OpenAI, OpenWeather & SerpAPI</p>
|
|
|
|
| 462 |
</div>
|
| 463 |
</div>
|
| 464 |
"""
|
|
|
|
| 497 |
Multi-Agent System β’ Intelligent Itinerary Generation β’ Real-time Data Processing
|
| 498 |
</p>
|
| 499 |
<div style="margin-top: 15px;">
|
| 500 |
+
<span style="background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.85em; margin: 0 5px;">π§ Weather Agent</span>
|
| 501 |
+
<span style="background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.85em; margin: 0 5px;">π Scout Agent</span>
|
| 502 |
+
<span style="background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.85em; margin: 0 5px;">π° Budget Agent</span>
|
| 503 |
+
<span style="background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.85em; margin: 0 5px;">π
Planner Agent</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
</div>
|
| 505 |
</div>
|
| 506 |
""")
|
|
|
|
| 527 |
num_days = gr.Slider(
|
| 528 |
label="Duration (Days)",
|
| 529 |
minimum=1,
|
| 530 |
+
maximum=7,
|
| 531 |
value=3,
|
| 532 |
step=1
|
| 533 |
)
|
|
|
|
| 561 |
)
|
| 562 |
|
| 563 |
with gr.Row():
|
| 564 |
+
generate_btn = gr.Button("βοΈ Plan My Trip", variant="primary", size="lg")
|
| 565 |
|
| 566 |
output = gr.HTML()
|
| 567 |
|