#!/usr/bin/env python # coding=utf-8 """ Travel Catalogue Creator - AI Agent for Personalized Travel Planning Creates comprehensive travel guides with weather, itineraries, packing lists & images """ import os import re import json import requests import yaml from typing import Optional from smolagents import CodeAgent, DuckDuckGoSearchTool, HfApiModel, tool from Gradio_UI import GradioUI # ====================== # TRAVEL TOOLS (SMOLAGENTS-COMPLIANT) # ====================== @tool def get_weather_forecast(location: str, travel_dates: str) -> str: """ Get weather forecast for destination using wttr.in API. Args: location: Destination city name (e.g., "Barcelona") travel_dates: Travel date range string (e.g., "October 15-19") Returns: Weather summary string with temperature and packing recommendations """ try: # Clean location name for API clean = re.sub(r"[^a-zA-Z0-9\s]", "", location).replace(" ", "+") resp = requests.get(f"http://wttr.in/{clean}?format=j1", timeout=10) resp.raise_for_status() data = resp.json() cur = data["current_condition"][0] temp = int(cur["temp_C"]) cond = cur["lang_en"][0]["value"].lower() # Generate packing recommendations based on temperature if temp > 25: pack = "Light clothing, shorts, breathable fabrics, sunscreen, hat" elif temp > 15: pack = "Light layers, long sleeves, light jacket" else: pack = "Warm layers, jacket, beanie recommended" # Add rain gear if needed if "rain" in cond or "shower" in cond: pack += " + compact umbrella + waterproof jacket" return f"{location} forecast ({travel_dates}): {temp}°C, {cond}. Packing: {pack}" except Exception as e: # Fallback to generic forecast return f"{location} typical weather: 15-25°C. Pack versatile layers, light jacket, and compact umbrella." @tool def convert_currency(amount: float, from_currency: str, to_currency: str) -> str: """ Convert currency amount using Frankfurter API (no API key required). Args: amount: Numeric amount to convert (e.g., 1200.0) from_currency: Source currency ISO code (e.g., "USD") to_currency: Target currency ISO code (e.g., "EUR") Returns: Formatted string showing conversion result and exchange rate """ try: from_curr = from_currency.upper()[:3] to_curr = to_currency.upper()[:3] # Handle same currency if from_curr == to_curr: return f"{amount:,.0f} {from_curr} = {amount:,.0f} {to_curr} (1 {from_curr} = 1.00 {to_curr})" # Get exchange rate resp = requests.get( f"https://api.frankfurter.app/latest", params={"from": from_curr, "to": to_curr}, timeout=10 ) resp.raise_for_status() rate = resp.json()["rates"][to_curr] converted = amount * rate return f"{amount:,.0f} {from_curr} = {converted:,.0f} {to_curr} (1 {from_curr} = {rate:.2f} {to_curr})" except Exception as e: # Fallback to showing amount without conversion return f"{amount:,.0f} {from_currency} = {amount:,.0f} {to_currency} (rate unavailable)" @tool def get_time_difference(origin_city: str, destination_city: str) -> str: """ Calculate time difference between origin and destination cities using heuristic mapping. Args: origin_city: Traveler's home city name (e.g., "New York") destination_city: Travel destination city name (e.g., "Paris") Returns: Human-readable string describing time difference and jet lag advice """ # Extended timezone mapping (UTC offsets) city_tz = { "new york": -5, "london": 0, "paris": 1, "tokyo": 9, "sydney": 10, "los angeles": -8, "chicago": -6, "mumbai": 5.5, "dubai": 4, "singapore": 8, "berlin": 1, "rome": 1, "barcelona": 1, "madrid": 1, "amsterdam": 1, "vienna": 1, "prague": 1, "budapest": 1, "warsaw": 1, "stockholm": 1, "oslo": 1, "copenhagen": 1, "helsinki": 2, "athens": 2, "istanbul": 3, "cairo": 2, "bangkok": 7, "seoul": 9, "beijing": 8, "shanghai": 8, "hong kong": 8, "manila": 8, "kuala lumpur": 8, "jakarta": 7, "delhi": 5.5, "rio de janeiro": -3, "sao paulo": -3, "buenos aires": -3, "mexico city": -6, "toronto": -5, "vancouver": -8, "lisbon": 0, "porto": 0, "brussels": 1, "zurich": 1, "geneva": 1, "milan": 1, "florence": 1, "venice": 1, "naples": 1, "dublin": 0, "edinburgh": 0, "glasgow": 0, "manchester": 0, "birmingham": 0, "krakow": 1, "gdansk": 1, "wroclaw": 1, "riga": 2, "vilnius": 2, "tallinn": 2, "sofia": 2, "bucharest": 2, "zagreb": 1, "ljubljana": 1, "belgrade": 1, "sarajevo": 1, "nicosia": 2, "valletta": 1, "reykjavik": 0, } origin_key = origin_city.lower().strip() dest_key = destination_city.lower().strip() origin_tz = city_tz.get(origin_key, 0) dest_tz = city_tz.get(dest_key, 0) diff = dest_tz - origin_tz if diff == 0: return f"No time difference between {origin_city} and {destination_city}." elif diff > 0: return f"{destination_city} is {diff} hours ahead of {origin_city}. Prepare for eastward jet lag (may cause fatigue)." else: return f"{destination_city} is {abs(diff)} hours behind {origin_city}. Prepare for westward jet lag (may cause insomnia)." @tool def generate_packing_list(destination: str, weather_summary: str, trip_days: int, trip_type: str) -> str: """ Generate customized packing checklist based on destination, weather, duration and trip type. Args: destination: Destination city or region name (e.g., "Barcelona") weather_summary: Weather forecast string containing temperature and conditions trip_days: Total number of travel days (integer >= 1) trip_type: Type of trip: "city", "beach", "mountain", or "mixed" Returns: Formatted multi-section packing checklist as string """ weather_lower = weather_summary.lower() # Analyze weather conditions cold = any(w in weather_lower for w in ["cold", "cool", "10°c", "11°c", "12°c", "13°c", "14°c", "jacket", "below 15"]) rain = any(w in weather_lower for w in ["rain", "shower", "drizzle", "umbrella", "precipitation"]) hot = any(w in weather_lower for w in ["hot", "warm", "25°c", "26°c", "27°c", "28°c", "29°c", "30°c", "above 25"]) # Calculate clothing quantities tops = min(trip_days, trip_days // 2 + 2) bottoms = min(trip_days // 2 + 1, 4) # Build clothing list clothes = [f"• Tops ({tops})", f"• Bottoms ({bottoms})"] if cold: clothes.extend([ "• Warm jacket/coat", "• Long underwear (if very cold)", "• Warm socks (x3)" ]) elif hot: clothes.extend([ "• Light breathable fabrics", "• Sun hat", "• Sunglasses", "• Reef-safe sunscreen (SPF 50+)" ]) else: clothes.append("• Light jacket or sweater (essential)") if rain: clothes.extend([ "• Compact travel umbrella", "• Quick-dry clothing", "• Waterproof shoes or sandals" ]) # Add trip-type specific items if trip_type == "beach": clothes.extend([ "• Swimsuit (x2)", "• Beach towel", "• Flip-flops/sandals", "• Beach bag" ]) elif trip_type == "mountain": clothes.extend([ "• Sturdy hiking shoes", "• Moisture-wicking base layers", "• Trekking socks (x3)", "• Daypack" ]) return ( f"🎒 SMART PACKING LIST ({trip_days}-day {trip_type} trip to {destination})\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "ESSENTIALS\n" "• Passport + photocopies + digital scans in cloud\n" "• Credit/debit cards + small amount of local currency\n" "• Universal power adapter\n" "• Phone + portable charger (10,000mAh+) + cables\n" "• Travel insurance documents (digital + physical)\n" "\nCLOTHING\n" + "\n".join(clothes) + "\n" "\nHEALTH & HYGIENE\n" "• Prescription medications (in original packaging + doctor's note)\n" "• Basic first-aid kit (bandages, antiseptic, pain relievers)\n" "• Hand sanitizer (60ml travel size)\n" "• Travel-sized toiletries (toothbrush, toothpaste, deodorant)\n" "\n💡 Pro tip: Roll clothes to save space. Pack one versatile outfit per day + 1 extra day buffer." ) @tool def build_itinerary(destination: str, attractions: str, budget_local: float, days: int) -> str: """ Create realistic day-by-day travel itinerary with time allocations and budget guidance. Args: destination: Destination city name (e.g., "Barcelona") attractions: Comma-separated list of attraction names (e.g., "Sagrada Familia, Park Guell") budget_local: Daily budget in local currency (float) days: Number of full travel days (integer >= 1) Returns: Formatted multi-day itinerary with time slots and daily budget allocation """ # Parse attractions list att_list = [a.strip() for a in attractions.split(",") if a.strip()] if not att_list: att_list = ["Old Town exploration", "Local museum", "Scenic viewpoint", "Local market"] lines = [ f"🗓️ {days}-DAY REALISTIC ITINERARY: {destination}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" ] # Generate day-by-day schedule for d in range(1, days + 1): primary = att_list[(d - 1) % len(att_list)] secondary = att_list[d % len(att_list)] if len(att_list) > 1 else "Leisurely café break" lines.extend([ f"\nDAY {d} | Budget: ~{budget_local:,.0f} local currency", f" 09:00 - 12:00 {primary} (arrive early to avoid crowds)", f" 12:30 - 14:00 Lunch at local spot (budget: ~{budget_local * 0.25:,.0f})", f" 14:30 - 17:00 {secondary}", f" 19:00+ Dinner + evening stroll (budget: ~{budget_local * 0.35:,.0f})" ]) lines.append("\n💡 Pro tips: Book major attractions online in advance. Use public transport day passes for 30%+ savings.") return "\n".join(lines) @tool def generate_travel_images(destination: str) -> str: """ Generate two travel image URLs using free stock photo services. Args: destination: Destination city name (e.g., "Lisbon") Returns: JSON-formatted string containing two image URLs with keys "landmark_image" and "street_scene_image" """ # Map popular destinations to actual quality travel photos # Using Pixabay's CDN which hosts free travel images destination_lower = destination.lower().strip() # High-quality travel images for popular destinations image_map = { "barcelona": { "landmark": "https://cdn.pixabay.com/photo/2017/01/31/21/23/architecture-2025080_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2016/11/14/04/45/elephant-1822636_960_720.jpg" }, "paris": { "landmark": "https://cdn.pixabay.com/photo/2015/10/06/18/26/eiffel-tower-975004_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2016/11/18/15/44/paris-1835941_960_720.jpg" }, "london": { "landmark": "https://cdn.pixabay.com/photo/2014/11/13/23/34/london-530055_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2016/11/29/04/19/bridge-1867744_960_720.jpg" }, "rome": { "landmark": "https://cdn.pixabay.com/photo/2016/11/14/05/21/rome-1822559_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2016/03/02/20/13/rome-1232437_960_720.jpg" }, "tokyo": { "landmark": "https://cdn.pixabay.com/photo/2019/04/20/11/39/japan-4141578_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2017/01/28/02/24/japan-2014616_960_720.jpg" }, "new york": { "landmark": "https://cdn.pixabay.com/photo/2017/06/07/15/47/new-york-city-2380683_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2016/01/19/17/41/statue-of-liberty-1149887_960_720.jpg" }, "amsterdam": { "landmark": "https://cdn.pixabay.com/photo/2017/07/31/11/59/canal-2558009_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2016/11/29/12/13/architecture-1869547_960_720.jpg" }, "lisbon": { "landmark": "https://cdn.pixabay.com/photo/2018/11/29/21/19/hamburg-3846525_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2019/08/28/10/37/portugal-4436951_960_720.jpg" }, "berlin": { "landmark": "https://cdn.pixabay.com/photo/2016/11/29/03/37/architecture-1867187_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2016/02/17/23/03/germany-1206011_960_720.jpg" }, "madrid": { "landmark": "https://cdn.pixabay.com/photo/2020/06/15/01/06/architecture-5299558_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2019/10/20/09/03/madrid-4563537_960_720.jpg" }, "prague": { "landmark": "https://cdn.pixabay.com/photo/2016/11/23/15/32/prague-1853890_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2017/09/05/10/19/prague-2717166_960_720.jpg" }, "dubai": { "landmark": "https://cdn.pixabay.com/photo/2016/11/22/23/40/burj-khalifa-1851204_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2020/02/08/10/37/dubai-4829188_960_720.jpg" }, "singapore": { "landmark": "https://cdn.pixabay.com/photo/2016/03/27/21/43/singapore-1284628_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2017/12/10/17/40/singapore-3010803_960_720.jpg" }, "istanbul": { "landmark": "https://cdn.pixabay.com/photo/2018/04/25/09/14/istanbul-3349451_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2020/06/28/14/20/istanbul-5349952_960_720.jpg" }, "athens": { "landmark": "https://cdn.pixabay.com/photo/2018/10/30/16/06/water-3784022_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2019/09/15/00/10/athens-4477350_960_720.jpg" }, "vienna": { "landmark": "https://cdn.pixabay.com/photo/2017/06/28/10/53/schonbrunn-palace-2450019_960_720.jpg", "street": "https://cdn.pixabay.com/photo/2018/05/30/00/24/thunderstorm-3440450_960_720.jpg" } } # Check if we have specific images for this destination if destination_lower in image_map: images = image_map[destination_lower] return json.dumps({ "landmark_image": images["landmark"], "street_scene_image": images["street"] }) # Generic high-quality travel images as fallback return json.dumps({ "landmark_image": "https://cdn.pixabay.com/photo/2016/11/29/12/13/architecture-1869547_960_720.jpg", "street_scene_image": "https://cdn.pixabay.com/photo/2016/11/22/19/15/hand-1850120_960_720.jpg" }) @tool def assemble_catalogue( destination: str, origin: str, dates: str, budget_summary: str, weather: str, timezone_info: str, itinerary: str, packing_list: str, image_urls_json: str ) -> str: """ Compile all travel research into a beautiful, structured Markdown travel catalogue. Args: destination: Destination city name (e.g., "Lisbon") origin: Traveler's home city (e.g., "London") dates: Travel date range (e.g., "Sep 20-22, 2026") budget_summary: Formatted budget conversion string weather: Weather forecast with packing advice timezone_info: Time difference and jet lag guidance itinerary: Day-by-day schedule with time allocations packing_list: Complete customized packing checklist image_urls_json: JSON string with "landmark_image" and "street_scene_image" URLs Returns: Complete Markdown-formatted travel catalogue with embedded images """ # Parse image URLs try: images = json.loads(image_urls_json) img1 = images.get("landmark_image", "https://picsum.photos/800/600?random=1") img2 = images.get("street_scene_image", "https://picsum.photos/800/600?random=2") except: img1 = "https://picsum.photos/800/600?random=1" img2 = "https://picsum.photos/800/600?random=2" # Count days in itinerary day_count = len(re.findall(r'DAY\s+\d+', itinerary)) # Build catalogue with proper Gradio-compatible Markdown catalogue = f"""# 🌍 {destination} Travel Catalogue *Planned from {origin} • {dates} • {budget_summary}* --- ## ⏰ Time Zone Adjustment {timezone_info} --- ## 🌤️ Weather Forecast & Packing Guidance {weather} --- ## 🗓️ Your Personalized {day_count}-Day Itinerary {itinerary} --- ## 🎒 Complete Packing Checklist {packing_list} --- ## 📸 Visual Inspiration ![{destination} Landmark]({img1}) *Iconic {destination} landmark* ![{destination} Street Scene]({img2}) *Vibrant local atmosphere* --- > 💡 **Travel Pro Tips** > > • Download offline Google Maps before departure > • Learn 5 basic phrases in the local language > • Keep digital copies of passport/insurance in cloud storage > • Budget 10-15% extra for spontaneous experiences > • Public transport passes often save 30%+ vs single tickets --- **Happy Travels! ✈️** """ return catalogue # ====================== # AGENT SETUP # ====================== def create_agent(): """Initialize and return the Travel Catalogue Creator agent""" print("🚀 Initializing Travel Catalogue Creator...") # Load system prompt from YAML with open("prompts.yaml", "r") as f: prompt_config = yaml.safe_load(f) # Pre-instantiate DuckDuckGo search tool web_search = DuckDuckGoSearchTool() # Configure model with error handling model = HfApiModel( max_tokens=2048, temperature=0.3, model_id="Qwen/Qwen2.5-Coder-32B-Instruct", ) # Create agent with all tools agent = CodeAgent( model=model, tools=[ web_search, # Pre-instantiated search tool get_weather_forecast, convert_currency, get_time_difference, generate_packing_list, build_itinerary, generate_travel_images, assemble_catalogue, ], max_steps=15, verbosity_level=1, name="TravelCatalogueCreator", description="Creates comprehensive, personalized travel catalogues with weather forecasts, custom itineraries, packing lists and visual inspiration" ) print("✅ Agent initialized successfully!") return agent if __name__ == "__main__": # Create agent agent = create_agent() # Create uploads directory os.makedirs("./uploads", exist_ok=True) # Launch Gradio UI print("🌐 Launching Gradio interface...") ui = GradioUI(agent, file_upload_folder="./uploads") ui.launch(share=False, server_name="0.0.0.0", server_port=7860)