NSamson1 commited on
Commit
b5fc855
Β·
verified Β·
1 Parent(s): 285f5e6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +146 -258
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 worldwide."""
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
- "precipitation": 0,
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 service unavailable: {str(e)}"}
104
 
105
- # -------------------- ATTRACTIONS FUNCTIONS --------------------
106
- def fetch_attractions_via_openai(city: str) -> List[Dict]:
107
- """Fetch attractions for ANY city using OpenAI."""
108
- if not client:
109
- return get_fallback_attractions(city)
110
 
111
- try:
112
- prompt = f"""
113
- List the top 6 must-visit tourist attractions in {city}. For each attraction, provide:
114
- 1. Name (real attraction name)
115
- 2. A vivid, engaging 2-sentence description
116
- 3. Entry fee (in USD, use 0 for free)
117
- 4. Visit duration in hours
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- Make sure to use REAL attractions specific to {city}.
132
- """
133
- response = client.chat.completions.create(
134
- model="gpt-3.5-turbo",
135
- messages=[{"role": "user", "content": prompt}],
136
- temperature=0.7,
137
- max_tokens=1500
138
- )
139
- content = response.choices[0].message.content
140
-
141
- # Clean the response
142
- content = content.strip()
143
- if content.startswith('```json'):
144
- content = content[7:]
145
- if content.startswith('```'):
146
- content = content[3:]
147
- if content.endswith('```'):
148
- content = content[:-3]
149
-
150
- attractions = json.loads(content)
151
-
152
- if isinstance(attractions, dict) and "attractions" in attractions:
153
- attractions = attractions["attractions"]
154
- elif not isinstance(attractions, list):
155
- attractions = [attractions]
156
-
157
- result = []
158
- for a in attractions[:6]:
159
- fee = a.get("entry_fee", 0)
160
- if isinstance(fee, (int, float)):
161
- if fee == 0:
162
- fee_display = "Free"
 
163
  else:
164
- fee_display = f"${fee}"
165
- else:
166
- fee_display = str(fee)
 
 
 
 
 
167
 
168
- result.append({
169
- "name": a.get("name", "Unknown"),
170
- "entry_fee": fee_display,
171
- "duration_hours": a.get("duration_hours", 2),
172
- "description": a.get("description", f"A must-visit attraction in {city}"),
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
- # Generic attractions
212
  return [
213
- {"name": f"{city} City Center", "description": f"The vibrant heart of {city} with historic architecture and charming cafes.", "entry_fee": "Free", "duration_hours": 2},
214
- {"name": f"{city} Main Market", "description": f"Experience local life at this vibrant market with fresh produce and crafts.", "entry_fee": "Free", "duration_hours": 1.5},
215
- {"name": f"{city} Cathedral", "description": f"A magnificent religious site showcasing stunning architecture.", "entry_fee": "Free", "duration_hours": 1},
216
- {"name": f"{city} Museum", "description": f"Discover the rich cultural heritage and artistic treasures of {city}.", "entry_fee": "$12", "duration_hours": 2}
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) -> Dict:
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 = "budget"
235
  daily_rates = {"accommodation": 50, "food": 35, "transport": 15, "activities": 10}
236
  elif daily_budget < 200:
237
- level = "moderate"
238
  daily_rates = {"accommodation": 100, "food": 60, "transport": 25, "activities": 20}
239
  elif daily_budget < 400:
240
- level = "comfortable"
241
  daily_rates = {"accommodation": 180, "food": 100, "transport": 35, "activities": 35}
242
  else:
243
- level = "luxury"
244
  daily_rates = {"accommodation": 300, "food": 150, "transport": 50, "activities": 60}
245
  else:
246
- level = "moderate"
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 "<div style='color: red; padding: 20px; text-align: center;'>❌ Please enter a destination city.</div>"
273
 
274
  if num_days < 1 or num_days > 14:
275
- return "<div style='color: red; padding: 20px; text-align: center;'>❌ Number of days must be between 1 and 14.</div>"
276
 
277
- # Get data
278
  weather = get_weather(destination)
279
  if "error" in weather:
280
- return f"<div style='color: red; padding: 20px; text-align: center;'>❌ Weather error: {weather['error']}</div>"
281
 
282
- attractions_data = get_attractions(destination)
283
- attractions = attractions_data["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 beautiful HTML itinerary
293
- html = generate_beautiful_itinerary(
294
  destination, weather, attractions, budget_data,
295
- num_days, start, end, budget_amount, budget_currency, departure_city
296
  )
297
 
298
  return html
299
 
300
  except Exception as e:
301
- return f"<div style='color: red; padding: 20px; text-align: center;'>❌ An unexpected error occurred: {str(e)}</div>"
302
 
303
- def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
304
- num_days, start_date, end_date, budget_amount,
305
- budget_currency, departure_city):
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 with images (only if real image exists)
323
  attractions_html = ""
324
  for attr in attractions[:6]:
325
- img_url = get_attraction_image(attr['name'])
326
-
327
- if img_url:
328
- # With image
329
- attractions_html += f"""
330
- <div style="background: white; border-radius: 12px; margin-bottom: 20px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
331
- <div style="display: flex; flex-wrap: wrap;">
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
- else:
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 with images (only if real image exists)
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
- img_url = get_attraction_image(attr['name'])
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 or enjoy cultural entertainment</div>
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 ({level_display} Travel Style)</h3>
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 you go</div>
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, Kigali, Tokyo, New York, Bali...",
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>🌟 Try These Destinations</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, Weather API β€’ Smart travel planning for the modern explorer ✨</small>
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