Spaces:
Sleeping
Sleeping
| """ | |
| 🌍 AI Travel Concierge - MCP 1st Birthday Hackathon | |
| Premium Chat Interface with Gradio 6 Best Practices | |
| Award-winning UX/UI with: | |
| - Modern messages format (OpenAI-style) | |
| - Smooth animations and transitions | |
| - Real-time agent status updates | |
| - Beautiful booking cards with links | |
| - Optional AI-generated travel poster | |
| """ | |
| import gradio as gr | |
| import asyncio | |
| import json | |
| import os | |
| import httpx | |
| from datetime import datetime, timedelta | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| # ============== CONFIGURATION ============== | |
| # Try multiple possible environment variable names for HF Spaces compatibility | |
| NEBIUS_API_KEY = os.getenv("OPENAI_API_KEY") or os.getenv("NEBIUS_API_KEY") or os.getenv("HF_TOKEN") | |
| NEBIUS_BASE_URL = os.getenv("OPENAI_BASE_URL") or os.getenv("NEBIUS_BASE_URL") or "https://api.studio.nebius.ai/v1/" | |
| # Fallback for HF Spaces if secrets aren't loading | |
| if not NEBIUS_API_KEY: | |
| # Temporary fallback - will be removed after testing | |
| NEBIUS_API_KEY = "v1.CmQKHHN0YXRpY2tleS1lMDBxZHo3Nzdzcnl5YWI2aGMSIXNlcnZpY2VhY2NvdW50LWUwMHFlNjY1a214YXJ5bTZnYTIMCIGJ88gGEKfcq5UBOgwIgIyLlAcQgO6m7AJAAloDZTAw.AAAAAAAAAAFpaIe7TQXIIO9jnQJWji15jqL-5Gts-kuJHPIqA8JxedCZSxHmDYTmU6QnsUXQHLlitYOwId8GjSGCdT1JHJ0K" | |
| print("⚠️ Using fallback API key") | |
| # Debug: Print all environment variables that might be relevant | |
| print("=" * 50) | |
| print("🔍 DEBUG: Checking environment variables...") | |
| all_keys = [k for k in os.environ.keys()] | |
| api_related = [k for k in all_keys if any(x in k.upper() for x in ['API', 'KEY', 'TOKEN', 'OPENAI', 'NEBIUS'])] | |
| print(f"API-related env vars: {api_related}") | |
| # Validate API key on startup | |
| if NEBIUS_API_KEY: | |
| print(f"✓ API key found (length: {len(NEBIUS_API_KEY)})") | |
| print(f"✓ Base URL: {NEBIUS_BASE_URL}") | |
| else: | |
| print("❌ No API key available!") | |
| print("=" * 50) | |
| MODEL = "Qwen/Qwen3-235B-A22B-Instruct-2507" | |
| # MCP Server configurations | |
| MCP_SERVERS = { | |
| "flights": {"command": "python", "args": ["flights_server.py"]}, | |
| "hotels": {"command": "python", "args": ["hotels_server.py"]}, | |
| "activities": {"command": "python", "args": ["activities_server.py"]}, | |
| "dining": {"command": "python", "args": ["dining_server.py"]}, | |
| "transport": {"command": "python", "args": ["transport_server.py"]}, | |
| "weather": {"command": "python", "args": ["weather_server.py"]}, | |
| "poster": {"command": "python", "args": ["poster_server.py"]}, | |
| "recommendations": {"command": "python", "args": ["recommendations_server.py"]}, | |
| } | |
| # ============== MCP CLIENT ============== | |
| class MCPClient: | |
| def __init__(self): | |
| self.processes = {} | |
| self.tools_cache = {} | |
| async def connect_server(self, name: str, config: dict): | |
| import subprocess | |
| process = subprocess.Popen( | |
| [config["command"]] + config["args"], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| cwd=os.path.dirname(os.path.abspath(__file__)) | |
| ) | |
| self.processes[name] = process | |
| init_request = { | |
| "jsonrpc": "2.0", "id": 1, "method": "initialize", | |
| "params": { | |
| "protocolVersion": "2024-11-05", | |
| "capabilities": {}, | |
| "clientInfo": {"name": "travel-concierge", "version": "1.0.0"} | |
| } | |
| } | |
| response = await self._send_request(name, init_request) | |
| init_notification = {"jsonrpc": "2.0", "method": "notifications/initialized"} | |
| process.stdin.write((json.dumps(init_notification) + "\n").encode()) | |
| process.stdin.flush() | |
| tools_request = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} | |
| tools_response = await self._send_request(name, tools_request) | |
| if tools_response and "result" in tools_response: | |
| self.tools_cache[name] = tools_response["result"].get("tools", []) | |
| return response | |
| async def _send_request(self, server_name: str, request: dict) -> dict: | |
| process = self.processes.get(server_name) | |
| if not process: | |
| return {"error": f"Server {server_name} not connected"} | |
| try: | |
| process.stdin.write((json.dumps(request) + "\n").encode()) | |
| process.stdin.flush() | |
| response_line = process.stdout.readline().decode().strip() | |
| if response_line: | |
| return json.loads(response_line) | |
| except Exception as e: | |
| return {"error": str(e)} | |
| return {} | |
| async def call_tool(self, server_name: str, tool_name: str, arguments: dict) -> dict: | |
| request = { | |
| "jsonrpc": "2.0", "id": 100, | |
| "method": "tools/call", | |
| "params": {"name": tool_name, "arguments": arguments} | |
| } | |
| response = await self._send_request(server_name, request) | |
| if "result" in response: | |
| content = response["result"].get("content", []) | |
| if content and isinstance(content, list): | |
| return {"success": True, "data": content[0].get("text", "")} | |
| return {"success": False, "error": response.get("error", "Unknown error")} | |
| async def close_all(self): | |
| for name, process in self.processes.items(): | |
| try: | |
| process.terminate() | |
| process.wait(timeout=2) | |
| except: | |
| process.kill() | |
| self.processes.clear() | |
| # ============== AGENT CONFIG ============== | |
| AGENTS = { | |
| "weather": {"name": "Weather Agent", "icon": "🌤️", "color": "#4FC3F7", "tool": "get_weather_forecast"}, | |
| "flights": {"name": "Flight Agent", "icon": "✈️", "color": "#7C4DFF", "tool": "search_flights"}, | |
| "hotels": {"name": "Hotel Agent", "icon": "🏨", "color": "#FF7043", "tool": "search_hotels"}, | |
| "activities": {"name": "Activities Agent", "icon": "🎯", "color": "#66BB6A", "tool": "search_activities"}, | |
| "dining": {"name": "Dining Agent", "icon": "🍽️", "color": "#FFA726", "tool": "search_restaurants"}, | |
| "transport": {"name": "Transport Agent", "icon": "🚗", "color": "#42A5F5", "tool": "search_transport"}, | |
| } | |
| AGENT_SEQUENCE = ["weather", "flights", "hotels", "activities", "dining", "transport"] | |
| BOOKING_LINKS = { | |
| "flights": [ | |
| # Tested and working flight booking URLs - with return dates | |
| ("Google Flights", "https://www.google.com/travel/flights?q=flights%20from%20{origin_encoded}%20to%20{dest_encoded}%20{date}%20to%20{checkout}", "#4285F4"), | |
| ("Skyscanner", "https://www.skyscanner.com/transport/flights/{origin_lower}/{dest_lower}/{date}/{checkout}/?adults={travelers}&cabinclass=economy", "#1DB4A4"), | |
| ("Kayak", "https://www.kayak.com/flights/{origin}-{dest}/{date}/{checkout}/{travelers}adults?sort=bestflight_a", "#FF690F"), | |
| ("Momondo", "https://www.momondo.com/flight-search/{origin}-{dest}/{date}/{checkout}/{travelers}adults", "#E91E63"), | |
| ], | |
| "hotels": [ | |
| ("Booking.com", "https://www.booking.com/searchresults.html?ss={dest_encoded}&checkin={checkin}&checkout={checkout}&group_adults={travelers}&no_rooms=1", "#003580"), | |
| ("Hotels.com", "https://www.hotels.com/Hotel-Search?destination={dest_encoded}&startDate={checkin}&endDate={checkout}&rooms=1&adults={travelers}", "#D32F2F"), | |
| ("Airbnb", "https://www.airbnb.com/s/{dest_encoded}/homes?checkin={checkin}&checkout={checkout}&adults={travelers}", "#FF5A5F"), | |
| ("Agoda", "https://www.agoda.com/search?city={dest_encoded}&checkIn={checkin}&checkOut={checkout}&rooms=1&adults={travelers}", "#5E2CA5"), | |
| ], | |
| "activities": [ | |
| ("Viator", "https://www.viator.com/searchResults/all?text={dest_encoded}", "#1A1A1A"), | |
| ("GetYourGuide", "https://www.getyourguide.com/s/?q={dest_encoded}&date_from={checkin}&date_to={checkout}", "#FF5533"), | |
| ("TripAdvisor", "https://www.tripadvisor.com/Search?q={dest_encoded}%20things%20to%20do", "#00AF87"), | |
| ("Klook", "https://www.klook.com/search/?keyword={dest_encoded}", "#FF5722"), | |
| ], | |
| "dining": [ | |
| ("TripAdvisor", "https://www.tripadvisor.com/Search?q={dest_encoded}%20restaurants", "#00AF87"), | |
| ("Yelp", "https://www.yelp.com/search?find_desc=restaurants&find_loc={dest_encoded}", "#D32323"), | |
| ("OpenTable", "https://www.opentable.com/s?term={dest_encoded}", "#DA3743"), | |
| ("TheFork", "https://www.thefork.com/search/?cityId={dest_encoded}", "#00665E"), | |
| ], | |
| "transport": [ | |
| ("Rome2Rio", "https://www.rome2rio.com/s/{origin_encoded}/{dest_encoded}", "#00BCD4"), | |
| ("Uber", "https://m.uber.com/looking", "#000000"), | |
| ("GetTransfer", "https://gettransfer.com/en?utm_source=widget&from={dest_encoded}%20Airport&to={dest_encoded}%20City%20Center", "#4CAF50"), | |
| ("Bolt", "https://bolt.eu/", "#34D186"), | |
| ], | |
| } | |
| # ============== HELPER FUNCTIONS ============== | |
| def make_booking_links_html(agent_key: str, trip_data: dict) -> str: | |
| """Generate beautiful booking link buttons in HTML""" | |
| links = BOOKING_LINKS.get(agent_key, []) | |
| if not links: | |
| return "" | |
| # Get trip data | |
| destination = trip_data.get("destination", "Paris") | |
| origin = trip_data.get("origin", "New York") | |
| start_date = trip_data.get("start_date", "") | |
| end_date = trip_data.get("end_date", "") | |
| travelers = trip_data.get("travelers", 2) | |
| # Calculate nights | |
| nights = 7 | |
| try: | |
| from datetime import datetime | |
| d1 = datetime.strptime(start_date, "%Y-%m-%d") | |
| d2 = datetime.strptime(end_date, "%Y-%m-%d") | |
| nights = (d2 - d1).days | |
| except: | |
| pass | |
| # Clean destination and origin (remove country suffix like ", France") | |
| dest_clean = destination.split(",")[0].strip() if destination else "Paris" | |
| origin_clean = origin.split(",")[0].strip() if origin else "New York" | |
| # URL encoding formats | |
| import urllib.parse | |
| dest_encoded = urllib.parse.quote(dest_clean) # URL encoded: Dubai | |
| origin_encoded = urllib.parse.quote(origin_clean) # URL encoded: New%20York | |
| dest_lower = dest_clean.lower().replace(" ", "-") # lowercase with dashes: dubai | |
| origin_lower = origin_clean.lower().replace(" ", "-") # lowercase with dashes: new-york | |
| buttons = [] | |
| for name, url_template, color in links: | |
| try: | |
| url = url_template.format( | |
| dest=dest_clean, | |
| origin=origin_clean, | |
| dest_encoded=dest_encoded, | |
| origin_encoded=origin_encoded, | |
| dest_lower=dest_lower, | |
| origin_lower=origin_lower, | |
| date=start_date, | |
| checkin=start_date, | |
| checkout=end_date, | |
| travelers=travelers, | |
| nights=nights | |
| ) | |
| buttons.append(f'<a href="{url}" target="_blank" style="display:inline-block;margin:4px;padding:10px 18px;background:{color};color:white;border-radius:20px;text-decoration:none;font-weight:600;font-size:13px;">{name}</a>') | |
| except Exception as e: | |
| pass | |
| if not buttons: | |
| return "" | |
| return f''' | |
| <div style="margin-top:15px;padding:15px;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:15px;"> | |
| <span style="color:white;font-weight:bold;font-size:14px;">🔗 Book Now:</span><br> | |
| {"".join(buttons)} | |
| </div>''' | |
| def make_booking_links_markdown(agent_key: str, trip_data: dict) -> str: | |
| """Generate booking links in Markdown format for chat messages""" | |
| links = BOOKING_LINKS.get(agent_key, []) | |
| if not links: | |
| return "" | |
| # Get trip data | |
| destination = trip_data.get("destination", "Paris") | |
| origin = trip_data.get("origin", "New York") | |
| start_date = trip_data.get("start_date", "") | |
| end_date = trip_data.get("end_date", "") | |
| travelers = trip_data.get("travelers", 2) | |
| # Calculate nights | |
| nights = 7 | |
| try: | |
| from datetime import datetime | |
| d1 = datetime.strptime(start_date, "%Y-%m-%d") | |
| d2 = datetime.strptime(end_date, "%Y-%m-%d") | |
| nights = (d2 - d1).days | |
| except: | |
| pass | |
| # Clean destination and origin | |
| dest_clean = destination.split(",")[0].strip() if destination else "Paris" | |
| origin_clean = origin.split(",")[0].strip() if origin else "New York" | |
| # URL encoding formats | |
| import urllib.parse | |
| dest_encoded = urllib.parse.quote(dest_clean) | |
| origin_encoded = urllib.parse.quote(origin_clean) | |
| dest_lower = dest_clean.lower().replace(" ", "-") | |
| origin_lower = origin_clean.lower().replace(" ", "-") | |
| link_strs = [] | |
| for name, url_template, color in links: | |
| try: | |
| url = url_template.format( | |
| dest=dest_clean, | |
| origin=origin_clean, | |
| dest_encoded=dest_encoded, | |
| origin_encoded=origin_encoded, | |
| dest_lower=dest_lower, | |
| origin_lower=origin_lower, | |
| date=start_date, | |
| checkin=start_date, | |
| checkout=end_date, | |
| travelers=travelers, | |
| nights=nights | |
| ) | |
| link_strs.append(f"[{name}]({url})") | |
| except: | |
| pass | |
| if not link_strs: | |
| return "" | |
| return f"\n\n🔗 **Book Now:** {' | '.join(link_strs)}" | |
| async def run_agent(mcp_client: MCPClient, agent_key: str, trip_data: dict) -> str: | |
| """Run an agent and return results""" | |
| origin = trip_data.get("origin", "") | |
| destination = trip_data.get("destination", "") | |
| start_date = trip_data.get("start_date", "") | |
| end_date = trip_data.get("end_date", "") | |
| travelers = trip_data.get("travelers", 1) | |
| budget = trip_data.get("budget", "moderate") | |
| interests = trip_data.get("interests", []) | |
| # Map interests to activity types | |
| interest_map = {"culture": "history", "food": "food", "art": "art", "adventure": "adventure", "nature": "adventure"} | |
| interest_type = "general" | |
| for i in interests: | |
| if i.lower() in interest_map: | |
| interest_type = interest_map[i.lower()] | |
| break | |
| try: | |
| if agent_key == "weather": | |
| # Tool: get_forecast(city, dates) | |
| resp = await mcp_client.call_tool("weather", "get_forecast", { | |
| "city": destination, | |
| "dates": f"{start_date} to {end_date}" | |
| }) | |
| elif agent_key == "flights": | |
| # Tool: search_flights(origin, destination, date, passengers, return_date) | |
| resp = await mcp_client.call_tool("flights", "search_flights", { | |
| "origin": origin, | |
| "destination": destination, | |
| "date": start_date, | |
| "passengers": travelers, | |
| "return_date": end_date | |
| }) | |
| elif agent_key == "hotels": | |
| # Tool: search_hotels(location, check_in, check_out, stars) | |
| stars = 5 if budget == "luxury" else 4 if budget == "moderate" else 3 | |
| resp = await mcp_client.call_tool("hotels", "search_hotels", { | |
| "location": destination, | |
| "check_in": start_date, | |
| "check_out": end_date, | |
| "stars": stars | |
| }) | |
| elif agent_key == "activities": | |
| # Tool: search_activities(city, interest) | |
| resp = await mcp_client.call_tool("activities", "search_activities", { | |
| "city": destination, | |
| "interest": interest_type | |
| }) | |
| elif agent_key == "dining": | |
| # Tool: find_restaurants(city, cuisine, buffet) | |
| resp = await mcp_client.call_tool("dining", "find_restaurants", { | |
| "city": destination, | |
| "cuisine": "local", | |
| "buffet": False | |
| }) | |
| elif agent_key == "transport": | |
| # Tool: book_transfer(pickup, dropoff, passengers) | |
| resp = await mcp_client.call_tool("transport", "book_transfer", { | |
| "pickup": f"{destination} Airport", | |
| "dropoff": f"{destination} City Center", | |
| "passengers": travelers | |
| }) | |
| else: | |
| return "Agent not found." | |
| return resp.get("data", "No data") if resp.get("success") else f"Error: {resp.get('error', 'Unknown')}" | |
| except Exception as e: | |
| return f"Error: {str(e)}" | |
| def generate_final_report_html(trip_data: dict, agent_results: dict) -> str: | |
| """Generate beautiful final report with all booking links""" | |
| dest = trip_data.get("destination", "Destination") | |
| origin = trip_data.get("origin", "Origin") | |
| start = trip_data.get("start_date", "") | |
| end = trip_data.get("end_date", "") | |
| travelers = trip_data.get("travelers", 2) | |
| budget = trip_data.get("budget", "moderate").title() | |
| dest_url = dest.replace(" ", "+") | |
| o = origin[:3].upper() if len(origin) >= 3 else origin.upper() | |
| d = dest[:3].upper() if len(dest) >= 3 else dest.upper() | |
| sections = [] | |
| # Header Card | |
| sections.append(f''' | |
| <div style="background:linear-gradient(135deg,#667eea,#764ba2);border-radius:20px;padding:30px;margin:20px 0;color:white;text-align:center;"> | |
| <h1 style="margin:0;font-size:28px;">🎉 Your Trip to {dest}!</h1> | |
| <p style="margin:10px 0 0;opacity:0.9;">{origin} → {dest} • {start} to {end} • {travelers} travelers • {budget}</p> | |
| </div>''') | |
| # Agent Results Cards | |
| agent_configs = [ | |
| ("flights", "✈️", "Flights", "#EDE7F6", "#7C4DFF"), | |
| ("hotels", "🏨", "Hotels", "#FBE9E7", "#FF7043"), | |
| ("activities", "🎯", "Activities", "#E8F5E9", "#66BB6A"), | |
| ("dining", "🍽️", "Dining", "#FFF3E0", "#FFA726"), | |
| ("transport", "🚗", "Transport", "#E3F2FD", "#42A5F5"), | |
| ("weather", "🌤️", "Weather", "#E1F5FE", "#4FC3F7"), | |
| ] | |
| for key, icon, title, bg, accent in agent_configs: | |
| data = agent_results.get(key, "Research completed") | |
| links_html = make_booking_links_html(key, trip_data) if key != "weather" else "" | |
| sections.append(f''' | |
| <div style="background:{bg};border-radius:15px;padding:20px;margin:15px 0;border-left:4px solid {accent};"> | |
| <h3 style="color:{accent};margin:0 0 10px;font-size:18px;">{icon} {title}</h3> | |
| <div style="background:white;border-radius:10px;padding:15px;white-space:pre-wrap;font-size:14px;line-height:1.6;">{data}</div> | |
| {links_html} | |
| </div>''') | |
| # Footer | |
| sections.append(''' | |
| <div style="text-align:center;padding:20px;background:#f8f9fa;border-radius:15px;margin-top:20px;"> | |
| <p style="color:#666;margin:0;">✨ Have an amazing trip! ✨</p> | |
| <p style="color:#888;font-size:12px;margin:8px 0 0;">Generated by AI Travel Concierge • MCP Hackathon</p> | |
| </div>''') | |
| return "".join(sections) | |
| def generate_final_report_markdown(trip_data: dict, agent_results: dict) -> str: | |
| """Generate final report in Markdown format for chat display""" | |
| import urllib.parse | |
| dest = trip_data.get("destination", "Destination") | |
| origin = trip_data.get("origin", "Origin") | |
| start = trip_data.get("start_date", "") | |
| end = trip_data.get("end_date", "") | |
| travelers = trip_data.get("travelers", 2) | |
| budget = trip_data.get("budget", "moderate").title() | |
| # Clean versions for URLs | |
| dest_clean = dest.split(",")[0].strip() | |
| origin_clean = origin.split(",")[0].strip() | |
| # URL encoding | |
| dest_encoded = urllib.parse.quote(dest_clean) | |
| origin_encoded = urllib.parse.quote(origin_clean) | |
| dest_lower = dest_clean.lower().replace(" ", "-") | |
| origin_lower = origin_clean.lower().replace(" ", "-") | |
| report = f"""# 🎉 Your Complete Trip to {dest}! | |
| **{origin} → {dest}** • {start} to {end} • {travelers} travelers • {budget} Budget | |
| --- | |
| ## ✈️ Flights | |
| {agent_results.get('flights', 'No flight data')} | |
| 🔗 **Book Now:** [Google Flights](https://www.google.com/travel/flights?q=flights%20from%20{origin_encoded}%20to%20{dest_encoded}%20{start}%20to%20{end}) | [Skyscanner](https://www.skyscanner.com/transport/flights/{origin_lower}/{dest_lower}/{start}/{end}/?adults={travelers}&cabinclass=economy) | [Kayak](https://www.kayak.com/flights/{origin_clean}-{dest_clean}/{start}/{end}/{travelers}adults?sort=bestflight_a) | [Momondo](https://www.momondo.com/flight-search/{origin_clean}-{dest_clean}/{start}/{end}/{travelers}adults) | |
| --- | |
| ## 🏨 Hotels | |
| {agent_results.get('hotels', 'No hotel data')} | |
| 🔗 **Book Now:** [Booking.com](https://www.booking.com/searchresults.html?ss={dest_encoded}&checkin={start}&checkout={end}&group_adults={travelers}&no_rooms=1) | [Hotels.com](https://www.hotels.com/Hotel-Search?destination={dest_encoded}&startDate={start}&endDate={end}&rooms=1&adults={travelers}) | [Airbnb](https://www.airbnb.com/s/{dest_encoded}/homes?checkin={start}&checkout={end}&adults={travelers}) | [Agoda](https://www.agoda.com/search?city={dest_encoded}&checkIn={start}&checkOut={end}&rooms=1&adults={travelers}) | |
| --- | |
| ## 🎯 Activities & Tours | |
| {agent_results.get('activities', 'No activities data')} | |
| 🔗 **Book Now:** [Viator](https://www.viator.com/searchResults/all?text={dest_encoded}) | [GetYourGuide](https://www.getyourguide.com/s/?q={dest_encoded}&date_from={start}&date_to={end}) | [TripAdvisor](https://www.tripadvisor.com/Search?q={dest_encoded}%20things%20to%20do) | [Klook](https://www.klook.com/search/?keyword={dest_encoded}) | |
| --- | |
| ## 🍽️ Dining | |
| {agent_results.get('dining', 'No dining data')} | |
| 🔗 **Reserve:** [TripAdvisor](https://www.tripadvisor.com/Search?q={dest_encoded}%20restaurants) | [Yelp](https://www.yelp.com/search?find_desc=restaurants&find_loc={dest_encoded}) | [OpenTable](https://www.opentable.com/s?term={dest_encoded}) | [TheFork](https://www.thefork.com/search/?cityId={dest_encoded}) | |
| --- | |
| ## 🚗 Local Transport | |
| {agent_results.get('transport', 'No transport data')} | |
| 🔗 **Book:** [Rome2Rio](https://www.rome2rio.com/s/{origin_encoded}/{dest_encoded}) | [Uber](https://m.uber.com/looking) | [GetTransfer](https://gettransfer.com/en?utm_source=widget&from={dest_encoded}%20Airport&to={dest_encoded}%20City%20Center) | [Bolt](https://bolt.eu/) | |
| --- | |
| ## 🌤️ Weather Forecast | |
| {agent_results.get('weather', 'No weather data')} | |
| --- | |
| ### ✨ Have an amazing trip! ✨ | |
| *Generated by AI Travel Concierge • MCP 1st Birthday Hackathon* | |
| --- | |
| **Optional:** Would you like me to generate a beautiful **AI travel poster** for your trip? Click the button below! 🎨""" | |
| return report | |
| async def generate_poster(mcp_client: MCPClient, trip_data: dict, company_info: dict = None) -> tuple: | |
| """Generate travel poster using Flux-dev2 via Modal. Returns (message, image_path)""" | |
| destination = trip_data.get("destination", "Beautiful Destination") | |
| origin = trip_data.get("origin", "") | |
| start_date = trip_data.get("start_date", "") | |
| end_date = trip_data.get("end_date", "") | |
| travelers = trip_data.get("travelers", 2) | |
| budget = trip_data.get("budget", "moderate") | |
| interests = trip_data.get("interests", []) | |
| # Calculate duration | |
| duration = "" | |
| if start_date and end_date: | |
| try: | |
| from datetime import datetime | |
| start = datetime.strptime(start_date, "%Y-%m-%d") | |
| end = datetime.strptime(end_date, "%Y-%m-%d") | |
| nights = (end - start).days | |
| duration = f"{nights}N {nights+1}D" | |
| except: | |
| pass | |
| # Format dates nicely | |
| dates_display = "" | |
| if start_date and end_date: | |
| try: | |
| from datetime import datetime | |
| start = datetime.strptime(start_date, "%Y-%m-%d") | |
| end = datetime.strptime(end_date, "%Y-%m-%d") | |
| dates_display = f"{start.strftime('%b %d')} - {end.strftime('%b %d, %Y')}" | |
| except: | |
| dates_display = f"{start_date} to {end_date}" | |
| # Company info defaults | |
| company_info = company_info or {} | |
| try: | |
| # Tool: generate_poster_image with full trip details and company branding | |
| resp = await mcp_client.call_tool("poster", "generate_poster_image", { | |
| "destination": destination, | |
| "origin": origin, | |
| "dates": dates_display, | |
| "duration": duration, | |
| "price": company_info.get("price", ""), | |
| "travelers": travelers, | |
| "budget": budget, | |
| "interests": ", ".join(interests) if interests else "travel", | |
| "company_name": company_info.get("company_name", ""), | |
| "company_phone": company_info.get("company_phone", ""), | |
| "company_email": company_info.get("company_email", ""), | |
| "company_website": company_info.get("company_website", ""), | |
| "inclusions": company_info.get("inclusions", "Flights, Hotels, Tours, Meals"), | |
| "tagline": company_info.get("tagline", "") | |
| }) | |
| result = resp.get("data", "") if resp.get("success") else None | |
| if result: | |
| # Check if it's a file path | |
| if result.startswith("/") and os.path.exists(result): | |
| return (f"✅ Professional travel poster generated!", result) | |
| # Check if it's in the current directory | |
| elif os.path.exists(result): | |
| return (f"✅ Professional travel poster generated!", result) | |
| # Check for local poster files | |
| else: | |
| import glob | |
| poster_files = glob.glob(f"poster_{destination.lower().replace(' ', '_').replace(',', '')}*.jpg") | |
| if poster_files: | |
| latest = max(poster_files, key=os.path.getmtime) | |
| return (f"✅ Professional travel poster generated!", os.path.abspath(latest)) | |
| return (f"Poster generated: {result}", None) | |
| else: | |
| return (f"❌ Error: {resp.get('error', 'Unknown error')}", None) | |
| except Exception as e: | |
| return (f"❌ Error: {str(e)}", None) | |
| async def parse_trip_request(message: str) -> dict: | |
| """Parse user message to extract trip details using AI""" | |
| if not NEBIUS_API_KEY: | |
| print("❌ ERROR: No API key configured!") | |
| return {"error": "API key not configured. Please add OPENAI_API_KEY to Space secrets."} | |
| async with httpx.AsyncClient(timeout=30.0) as client: | |
| try: | |
| response = await client.post( | |
| f"{NEBIUS_BASE_URL}chat/completions", | |
| headers={"Authorization": f"Bearer {NEBIUS_API_KEY}", "Content-Type": "application/json"}, | |
| json={ | |
| "model": MODEL, | |
| "messages": [ | |
| {"role": "system", "content": """Extract trip details from user message. Return ONLY valid JSON. | |
| IMPORTANT: If user doesn't know where to go, says "suggest", "recommend", "I don't know", "undecided", "best destinations", "where should I go", "surprise me", or similar - set destination to "SUGGEST". | |
| Return JSON format: | |
| {"origin": "city or empty", "destination": "city or SUGGEST", "start_date": "YYYY-MM-DD", "end_date": "YYYY-MM-DD", "travelers": number, "budget": "budget/moderate/luxury", "interests": ["list"], "needs_suggestion": true/false} | |
| Set needs_suggestion=true if user is undecided about destination. | |
| Use defaults if missing: dates=30 days from now for 7 days, travelers=2, budget=moderate."""}, | |
| {"role": "user", "content": message} | |
| ], | |
| "temperature": 0.3, | |
| "max_tokens": 500 | |
| } | |
| ) | |
| response.raise_for_status() | |
| content = response.json()["choices"][0]["message"]["content"] | |
| # Clean up response | |
| if "```json" in content: | |
| content = content.split("```json")[1].split("```")[0] | |
| elif "```" in content: | |
| content = content.split("```")[1].split("```")[0] | |
| # Remove any thinking tags | |
| if "<think>" in content: | |
| content = content.split("</think>")[-1] | |
| return json.loads(content.strip()) | |
| except httpx.HTTPStatusError as e: | |
| print(f"API HTTP error: {e.response.status_code} - {e.response.text}") | |
| return {"error": f"API error: {e.response.status_code}"} | |
| except Exception as e: | |
| print(f"Parse error: {e}") | |
| return {"error": str(e)} | |
| # ============== GRADIO 6 APP ============== | |
| def create_app(): | |
| # ✨ AWARD-WINNING PREMIUM CSS ✨ | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap'); | |
| :root { | |
| --primary: #6366f1; | |
| --primary-dark: #4f46e5; | |
| --secondary: #8b5cf6; | |
| --accent: #ec4899; | |
| --success: #10b981; | |
| --warning: #f59e0b; | |
| --error: #ef4444; | |
| --bg-dark: #0f172a; | |
| --bg-card: #1e293b; | |
| --bg-hover: #334155; | |
| --text-primary: #f8fafc; | |
| --text-secondary: #94a3b8; | |
| --border: #334155; | |
| --glow: rgba(99, 102, 241, 0.4); | |
| } | |
| * { | |
| font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif !important; | |
| box-sizing: border-box; | |
| } | |
| body, .gradio-container { | |
| background: var(--bg-dark) !important; | |
| color: var(--text-primary) !important; | |
| } | |
| .gradio-container { | |
| max-width: 1600px !important; | |
| margin: 0 auto !important; | |
| padding: 0 !important; | |
| } | |
| /* ===== MAIN LAYOUT ===== */ | |
| .main-wrapper { | |
| min-height: 100vh; | |
| background: linear-gradient(180deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .main-wrapper::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: | |
| radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99, 102, 241, 0.3), transparent), | |
| radial-gradient(ellipse 60% 40% at 80% 50%, rgba(139, 92, 246, 0.15), transparent), | |
| radial-gradient(ellipse 50% 30% at 20% 80%, rgba(236, 72, 153, 0.1), transparent); | |
| pointer-events: none; | |
| } | |
| /* ===== HEADER ===== */ | |
| .hero-header { | |
| text-align: center; | |
| padding: 50px 20px 40px; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .hero-title { | |
| font-size: 3.5rem; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, #fff 0%, #a5b4fc 50%, #c4b5fd 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin: 0 0 12px; | |
| letter-spacing: -0.02em; | |
| text-shadow: 0 0 80px rgba(99, 102, 241, 0.5); | |
| } | |
| .hero-subtitle { | |
| color: var(--text-secondary); | |
| font-size: 1.15rem; | |
| font-weight: 500; | |
| margin: 0 0 24px; | |
| } | |
| .badge-row { | |
| display: flex; | |
| justify-content: center; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .badge { | |
| background: rgba(99, 102, 241, 0.15); | |
| border: 1px solid rgba(99, 102, 241, 0.3); | |
| padding: 8px 16px; | |
| border-radius: 50px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: #a5b4fc; | |
| backdrop-filter: blur(10px); | |
| transition: all 0.3s ease; | |
| } | |
| .badge:hover { | |
| background: rgba(99, 102, 241, 0.25); | |
| border-color: rgba(99, 102, 241, 0.5); | |
| transform: translateY(-2px); | |
| } | |
| /* ===== GLASS CARDS ===== */ | |
| .glass-card { | |
| background: rgba(30, 41, 59, 0.8) !important; | |
| backdrop-filter: blur(20px) !important; | |
| border: 1px solid rgba(99, 102, 241, 0.2) !important; | |
| border-radius: 24px !important; | |
| box-shadow: | |
| 0 25px 50px -12px rgba(0, 0, 0, 0.5), | |
| 0 0 0 1px rgba(255, 255, 255, 0.05), | |
| inset 0 1px 0 rgba(255, 255, 255, 0.1) !important; | |
| padding: 24px !important; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .glass-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 1px; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); | |
| } | |
| /* ===== CHATBOT ===== */ | |
| .chatbot-wrap { | |
| border: none !important; | |
| background: transparent !important; | |
| } | |
| .chatbot-wrap > div { | |
| background: rgba(15, 23, 42, 0.6) !important; | |
| border-radius: 20px !important; | |
| border: 1px solid var(--border) !important; | |
| } | |
| /* Message styling */ | |
| .message { | |
| padding: 16px 20px !important; | |
| border-radius: 20px !important; | |
| margin: 8px 0 !important; | |
| max-width: 85% !important; | |
| animation: messageSlide 0.3s ease-out; | |
| } | |
| @keyframes messageSlide { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .user-message { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%) !important; | |
| color: white !important; | |
| margin-left: auto !important; | |
| border-bottom-right-radius: 6px !important; | |
| } | |
| .bot-message { | |
| background: var(--bg-card) !important; | |
| border: 1px solid var(--border) !important; | |
| color: var(--text-primary) !important; | |
| border-bottom-left-radius: 6px !important; | |
| } | |
| /* ===== INPUT FIELD ===== */ | |
| .input-container input, .input-container textarea { | |
| background: var(--bg-card) !important; | |
| border: 2px solid var(--border) !important; | |
| border-radius: 16px !important; | |
| padding: 16px 20px !important; | |
| font-size: 15px !important; | |
| color: var(--text-primary) !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .input-container input:focus, .input-container textarea:focus { | |
| border-color: var(--primary) !important; | |
| box-shadow: 0 0 0 4px var(--glow), 0 0 30px var(--glow) !important; | |
| outline: none !important; | |
| } | |
| .input-container input::placeholder { | |
| color: var(--text-secondary) !important; | |
| } | |
| /* ===== BUTTONS ===== */ | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%) !important; | |
| color: white !important; | |
| border: none !important; | |
| border-radius: 14px !important; | |
| padding: 14px 28px !important; | |
| font-weight: 700 !important; | |
| font-size: 15px !important; | |
| cursor: pointer !important; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
| box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4), 0 0 0 0 rgba(99, 102, 241, 0) !important; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn-primary::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); | |
| transition: 0.5s; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-3px) scale(1.02) !important; | |
| box-shadow: 0 8px 30px rgba(99, 102, 241, 0.5), 0 0 50px rgba(99, 102, 241, 0.3) !important; | |
| } | |
| .btn-primary:hover::before { | |
| left: 100%; | |
| } | |
| .btn-secondary { | |
| background: transparent !important; | |
| color: var(--text-primary) !important; | |
| border: 2px solid var(--border) !important; | |
| border-radius: 14px !important; | |
| padding: 12px 20px !important; | |
| font-weight: 600 !important; | |
| font-size: 14px !important; | |
| cursor: pointer !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .btn-secondary:hover { | |
| background: var(--bg-hover) !important; | |
| border-color: var(--primary) !important; | |
| color: #a5b4fc !important; | |
| transform: translateY(-2px) !important; | |
| } | |
| .btn-poster { | |
| background: linear-gradient(135deg, var(--accent) 0%, #f472b6 100%) !important; | |
| box-shadow: 0 4px 15px rgba(236, 72, 153, 0.4) !important; | |
| } | |
| .btn-poster:hover { | |
| box-shadow: 0 8px 30px rgba(236, 72, 153, 0.5), 0 0 50px rgba(236, 72, 153, 0.3) !important; | |
| } | |
| .btn-continue { | |
| background: linear-gradient(135deg, var(--success) 0%, #34d399 100%) !important; | |
| box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4) !important; | |
| width: 100% !important; | |
| margin-top: 16px !important; | |
| } | |
| .btn-continue:hover { | |
| box-shadow: 0 8px 30px rgba(16, 185, 129, 0.5) !important; | |
| } | |
| /* ===== AGENT STATUS PANEL ===== */ | |
| .agent-panel { | |
| background: rgba(30, 41, 59, 0.6) !important; | |
| border-radius: 20px !important; | |
| padding: 20px !important; | |
| border: 1px solid var(--border) !important; | |
| } | |
| .panel-title { | |
| color: var(--text-primary); | |
| font-size: 15px; | |
| font-weight: 700; | |
| margin: 0 0 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .agent-item { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 12px 16px; | |
| margin: 6px 0; | |
| border-radius: 12px; | |
| background: var(--bg-dark); | |
| border: 1px solid transparent; | |
| transition: all 0.3s ease; | |
| } | |
| .agent-item.active { | |
| background: rgba(245, 158, 11, 0.1); | |
| border-color: rgba(245, 158, 11, 0.3); | |
| } | |
| .agent-item.active .agent-status { | |
| color: var(--warning); | |
| } | |
| .agent-item.done { | |
| background: rgba(16, 185, 129, 0.1); | |
| border-color: rgba(16, 185, 129, 0.3); | |
| } | |
| .agent-item.done .agent-status { | |
| color: var(--success); | |
| } | |
| .agent-name { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-weight: 600; | |
| font-size: 14px; | |
| color: var(--text-primary); | |
| } | |
| .agent-icon { | |
| font-size: 18px; | |
| } | |
| .agent-status { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| } | |
| /* ===== QUICK BUTTONS ROW ===== */ | |
| .quick-section { | |
| margin-top: 20px; | |
| padding-top: 20px; | |
| border-top: 1px solid var(--border); | |
| } | |
| .quick-label { | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| font-weight: 600; | |
| margin-bottom: 12px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| /* ===== FOOTER ===== */ | |
| .footer { | |
| text-align: center; | |
| padding: 30px 20px; | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .footer a { | |
| color: var(--primary); | |
| text-decoration: none; | |
| } | |
| /* ===== ANIMATIONS ===== */ | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-10px); } | |
| } | |
| @keyframes glow { | |
| 0%, 100% { box-shadow: 0 0 20px var(--glow); } | |
| 50% { box-shadow: 0 0 40px var(--glow), 0 0 60px var(--glow); } | |
| } | |
| @keyframes pulse-ring { | |
| 0% { transform: scale(0.8); opacity: 1; } | |
| 100% { transform: scale(2); opacity: 0; } | |
| } | |
| .animate-float { animation: float 3s ease-in-out infinite; } | |
| .animate-glow { animation: glow 2s ease-in-out infinite; } | |
| /* ===== SCROLLBAR ===== */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-dark); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--primary); | |
| } | |
| /* ===== POSTER IMAGE DISPLAY ===== */ | |
| .poster-preview { | |
| margin-top: 16px; | |
| border-radius: 16px; | |
| overflow: hidden; | |
| background: linear-gradient(135deg, rgba(236, 72, 153, 0.1), rgba(139, 92, 246, 0.1)); | |
| border: 2px solid rgba(236, 72, 153, 0.3); | |
| transition: all 0.3s ease; | |
| } | |
| .poster-preview:hover { | |
| border-color: rgba(236, 72, 153, 0.5); | |
| box-shadow: 0 8px 30px rgba(236, 72, 153, 0.2); | |
| } | |
| .poster-preview img { | |
| border-radius: 12px !important; | |
| transition: transform 0.3s ease; | |
| } | |
| .poster-preview:hover img { | |
| transform: scale(1.02); | |
| } | |
| .poster-preview .label-wrap { | |
| background: linear-gradient(135deg, var(--accent), #f472b6) !important; | |
| padding: 10px 16px !important; | |
| border-radius: 12px 12px 0 0 !important; | |
| } | |
| .poster-preview .label-wrap span { | |
| color: white !important; | |
| font-weight: 700 !important; | |
| font-size: 14px !important; | |
| } | |
| .poster-preview .download-button, .poster-preview .share-button { | |
| background: var(--primary) !important; | |
| color: white !important; | |
| border-radius: 8px !important; | |
| padding: 8px 12px !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .poster-preview .download-button:hover, .poster-preview .share-button:hover { | |
| background: var(--secondary) !important; | |
| transform: translateY(-2px) !important; | |
| } | |
| /* ===== RESPONSIVE ===== */ | |
| @media (max-width: 768px) { | |
| .hero-title { font-size: 2.2rem; } | |
| .hero-subtitle { font-size: 1rem; } | |
| .glass-card { padding: 16px !important; border-radius: 18px !important; } | |
| } | |
| """ | |
| with gr.Blocks(title="🌍 AI Travel Concierge | MCP Hackathon", theme=gr.themes.Base(), css=custom_css) as app: | |
| # State variables | |
| mcp_client_state = gr.State(None) | |
| trip_data_state = gr.State({}) | |
| current_agent_state = gr.State(-1) | |
| agent_results_state = gr.State({}) | |
| # ===== MAIN WRAPPER ===== | |
| with gr.Column(elem_classes="main-wrapper"): | |
| # ===== HERO HEADER ===== | |
| gr.HTML(""" | |
| <div class="hero-header"> | |
| <h1 class="hero-title">✨ AI Travel Concierge</h1> | |
| <p class="hero-subtitle">Your premium AI-powered travel planning experience</p> | |
| <div class="badge-row"> | |
| <span class="badge">🤖 7 Specialized AI Agents</span> | |
| <span class="badge">🔗 Real Booking Links</span> | |
| <span class="badge">🎨 AI Poster Generator</span> | |
| <span class="badge">⚡ Powered by MCP</span> | |
| </div> | |
| </div> | |
| """) | |
| # ===== MAIN CONTENT ===== | |
| with gr.Row(elem_id="main-content"): | |
| # ===== CHAT COLUMN ===== | |
| with gr.Column(scale=3): | |
| with gr.Column(elem_classes="glass-card"): | |
| # Chatbot | |
| chatbot = gr.Chatbot( | |
| value=[{"role": "assistant", "content": """## 👋 Welcome to AI Travel Concierge! | |
| I'm your **premium travel planning assistant**, powered by **8 specialized AI agents** working together to create your perfect trip. | |
| ### 🎯 Two ways to get started: | |
| **Option 1 - I know where I want to go:** | |
| Tell me your destination, dates, travelers, and budget. | |
| *Example: "Plan a trip from London to Dubai, Dec 29 - Jan 5, 2 people, luxury"* | |
| **Option 2 - Help me decide! 🤔** | |
| Not sure where to go? Click **"Find My Perfect Destination"** and I'll recommend the best places based on your: | |
| - ❤️ Interests (beach, culture, adventure, food...) | |
| - 💰 Budget | |
| - 📅 Travel dates | |
| - 🔥 Current deals & offers | |
| --- | |
| **Quick actions:** Click a destination below or ask me anything! ⬇️"""}], | |
| height=500, | |
| type="messages", | |
| show_label=False, | |
| elem_classes="chatbot-wrap", | |
| ) | |
| # Input Area | |
| with gr.Row(elem_classes="input-container"): | |
| user_input = gr.Textbox( | |
| placeholder="✈️ Describe your dream trip or ask for recommendations...", | |
| show_label=False, | |
| scale=5, | |
| container=False, | |
| lines=1, | |
| ) | |
| send_btn = gr.Button("Send →", elem_classes="btn-primary", scale=1) | |
| # Quick Actions Row | |
| gr.HTML('<div class="quick-section"><p class="quick-label">🚀 Quick Actions</p></div>') | |
| with gr.Row(): | |
| help_decide_btn = gr.Button("🤔 Find My Perfect Destination", elem_classes="btn-primary", size="sm") | |
| deals_btn = gr.Button("🔥 See Current Deals", elem_classes="btn-secondary", size="sm") | |
| # Quick Destinations | |
| gr.HTML('<div class="quick-section"><p class="quick-label">⚡ Popular Destinations</p></div>') | |
| with gr.Row(): | |
| quick_paris = gr.Button("🗼 Paris", elem_classes="btn-secondary", size="sm") | |
| quick_bali = gr.Button("🏝️ Bali", elem_classes="btn-secondary", size="sm") | |
| quick_tokyo = gr.Button("🌸 Tokyo", elem_classes="btn-secondary", size="sm") | |
| quick_rome = gr.Button("🏛️ Rome", elem_classes="btn-secondary", size="sm") | |
| quick_dubai = gr.Button("🏜️ Dubai", elem_classes="btn-secondary", size="sm") | |
| # ===== SIDEBAR ===== | |
| with gr.Column(scale=1): | |
| # Agent Status Panel | |
| with gr.Column(elem_classes="agent-panel"): | |
| gr.HTML('<p class="panel-title">🤖 AI Agent Status</p>') | |
| agent_status_html = gr.HTML(""" | |
| <div class="agent-item"><span class="agent-name"><span class="agent-icon">🌤️</span>Weather</span><span class="agent-status">Ready</span></div> | |
| <div class="agent-item"><span class="agent-name"><span class="agent-icon">✈️</span>Flights</span><span class="agent-status">Ready</span></div> | |
| <div class="agent-item"><span class="agent-name"><span class="agent-icon">🏨</span>Hotels</span><span class="agent-status">Ready</span></div> | |
| <div class="agent-item"><span class="agent-name"><span class="agent-icon">🎯</span>Activities</span><span class="agent-status">Ready</span></div> | |
| <div class="agent-item"><span class="agent-name"><span class="agent-icon">🍽️</span>Dining</span><span class="agent-status">Ready</span></div> | |
| <div class="agent-item"><span class="agent-name"><span class="agent-icon">🚗</span>Transport</span><span class="agent-status">Ready</span></div> | |
| """) | |
| # Action Buttons | |
| continue_btn = gr.Button("▶️ Continue to Next Agent", elem_classes="btn-primary btn-continue", visible=False, size="lg") | |
| poster_btn = gr.Button("🎨 Generate Travel Poster", elem_classes="btn-primary btn-poster", visible=False, size="lg") | |
| # Poster Preview Section | |
| gr.HTML('<div id="poster-section" style="margin-top:16px;"></div>') | |
| poster_image = gr.Image( | |
| label="🎨 Your AI-Generated Travel Poster", | |
| type="filepath", | |
| visible=False, | |
| show_download_button=True, | |
| show_share_button=True, | |
| container=True, | |
| height=400, | |
| elem_classes="poster-preview", | |
| ) | |
| # Company Branding Section (collapsible) | |
| with gr.Accordion("🏢 Company Branding (Optional)", open=False, elem_classes="agent-panel"): | |
| gr.HTML('<p style="color:#94a3b8;font-size:12px;margin-bottom:12px;">Add your travel company details to the poster</p>') | |
| company_name = gr.Textbox( | |
| label="Company Name", | |
| placeholder="e.g., Wanderlust Travel", | |
| elem_classes="input-container" | |
| ) | |
| company_phone = gr.Textbox( | |
| label="Phone Number", | |
| placeholder="e.g., +1 800 555 1234", | |
| elem_classes="input-container" | |
| ) | |
| company_website = gr.Textbox( | |
| label="Website", | |
| placeholder="e.g., www.travel.com", | |
| elem_classes="input-container" | |
| ) | |
| poster_price = gr.Textbox( | |
| label="Price Display", | |
| placeholder="e.g., From $999 or $1,700 Per Person", | |
| elem_classes="input-container" | |
| ) | |
| poster_tagline = gr.Textbox( | |
| label="Custom Tagline", | |
| placeholder="e.g., Discover Your Journey", | |
| elem_classes="input-container" | |
| ) | |
| poster_inclusions = gr.Textbox( | |
| label="Inclusions (comma-separated)", | |
| placeholder="e.g., Flights, Hotels, Meals, Tours, Visa", | |
| value="Flights, Hotels, Tours & Transfers, Meals", | |
| elem_classes="input-container" | |
| ) | |
| # ===== FOOTER ===== | |
| gr.HTML(""" | |
| <div class="footer"> | |
| <p>Built with ❤️ for <strong>MCP 1st Birthday Hackathon</strong></p> | |
| <p style="margin-top:8px;opacity:0.7;">Powered by Nebius AI • Flux Image Generation • Model Context Protocol</p> | |
| </div> | |
| """) | |
| # ============== EVENT HANDLERS ============== | |
| def update_agent_status(current: int, results: dict) -> str: | |
| """Update agent status panel""" | |
| agents = [ | |
| ("🌤️", "Weather", "weather"), | |
| ("✈️", "Flights", "flights"), | |
| ("🏨", "Hotels", "hotels"), | |
| ("🎯", "Activities", "activities"), | |
| ("🍽️", "Dining", "dining"), | |
| ("🚗", "Transport", "transport"), | |
| ] | |
| html = '' | |
| for i, (icon, name, key) in enumerate(agents): | |
| if key in results: | |
| cls = "done" | |
| status = '<span class="agent-status" style="color:#10b981;">✓ Complete</span>' | |
| elif i == current: | |
| cls = "active" | |
| status = '<span class="agent-status" style="color:#f59e0b;">⏳ Working...</span>' | |
| else: | |
| cls = "" | |
| status = '<span class="agent-status">Ready</span>' | |
| html += f'<div class="agent-item {cls}"><span class="agent-name"><span class="agent-icon">{icon}</span>{name}</span>{status}</div>' | |
| return html | |
| async def handle_message(message: str, history: list, mcp_client, trip_data: dict, current_agent: int, agent_results: dict): | |
| """Handle user message""" | |
| if not message.strip(): | |
| yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) | |
| return | |
| # Add user message | |
| history = history + [{"role": "user", "content": message}] | |
| # If no trip data, parse the request | |
| if not trip_data.get("destination"): | |
| history = history + [{"role": "assistant", "content": "🔍 **Analyzing your trip request...**"}] | |
| yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) | |
| trip_data = await parse_trip_request(message) | |
| # Check for API error | |
| if trip_data.get("error"): | |
| history[-1] = {"role": "assistant", "content": f"""❌ **API Error** | |
| {trip_data.get('error')} | |
| Please check that the API keys are configured correctly in Space settings."""} | |
| yield history, mcp_client, {}, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) | |
| return | |
| # Check if user needs destination suggestions | |
| needs_suggestion = trip_data.get("needs_suggestion", False) or trip_data.get("destination", "").upper() == "SUGGEST" | |
| if needs_suggestion or not trip_data.get("destination"): | |
| # Initialize MCP for recommendations | |
| mcp_client = MCPClient() | |
| try: | |
| await mcp_client.connect_server("recommendations", MCP_SERVERS["recommendations"]) | |
| except Exception as e: | |
| print(f"Failed to connect recommendations: {e}") | |
| # Get smart recommendations | |
| origin = trip_data.get("origin", "") | |
| budget = trip_data.get("budget", "moderate") | |
| travelers = trip_data.get("travelers", 2) | |
| interests = ",".join(trip_data.get("interests", [])) | |
| # Calculate trip days | |
| trip_days = 7 | |
| try: | |
| if trip_data.get("start_date") and trip_data.get("end_date"): | |
| from datetime import datetime | |
| d1 = datetime.strptime(trip_data["start_date"], "%Y-%m-%d") | |
| d2 = datetime.strptime(trip_data["end_date"], "%Y-%m-%d") | |
| trip_days = (d2 - d1).days | |
| except: | |
| pass | |
| # Get travel month | |
| travel_month = 0 | |
| try: | |
| if trip_data.get("start_date"): | |
| travel_month = int(trip_data["start_date"].split("-")[1]) | |
| except: | |
| pass | |
| history[-1] = {"role": "assistant", "content": f"""🧭 **I'll help you find the perfect destination!** | |
| Let me search for the best options based on: | |
| • 👥 **Travelers:** {travelers} | |
| • 📅 **Days:** {trip_days} | |
| • 💰 **Budget:** {budget.title()} | |
| {f"• 📍 **From:** {origin}" if origin else ""} | |
| {f"• ❤️ **Interests:** {interests}" if interests else ""} | |
| 🔍 Searching for deals and recommendations..."""} | |
| yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) | |
| # Call recommendations agent | |
| result = await mcp_client.call_tool("recommendations", "get_destination_recommendations", { | |
| "origin": origin, | |
| "budget": budget, | |
| "travel_month": travel_month, | |
| "interests": interests, | |
| "travelers": travelers, | |
| "trip_days": trip_days | |
| }) | |
| if result.get("success"): | |
| recommendations_text = result.get("data", "") | |
| history[-1] = {"role": "assistant", "content": f"""{recommendations_text} | |
| --- | |
| 🎯 **To continue planning:** Just tell me which destination you'd like! | |
| *Example: "I want to go to Dubai" or "Let's do Bali" or just type the destination name.*"""} | |
| else: | |
| history[-1] = {"role": "assistant", "content": """🌍 **Here are some amazing destinations to consider:** | |
| ### 🔥 Hot Deals Right Now: | |
| • **Dubai** - 25% OFF Winter Sun Sale ☀️ | |
| • **Bali** - 30% OFF Early Bird 2026 🌴 | |
| • **Maldives** - 20% OFF Honeymoon Special 🏝️ | |
| • **Tokyo** - 15% OFF Cherry Blossom Preview 🌸 | |
| ### 💡 Based on your preferences: | |
| **For Beach & Relaxation:** Maldives, Bali, Phuket, Cancún | |
| **For Culture & History:** Rome, Paris, Tokyo, Istanbul | |
| **For Adventure:** Iceland, Cape Town, Marrakech | |
| **For Budget-Friendly:** Budapest, Lisbon, Vietnam | |
| --- | |
| 🎯 **Tell me which destination interests you**, or give me more details: | |
| • *"I want beaches and luxury"* | |
| • *"Looking for culture on a budget"* | |
| • *"Surprise me with something adventurous!"*"""} | |
| yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) | |
| return | |
| # Initialize MCP | |
| mcp_client = MCPClient() | |
| for name, config in MCP_SERVERS.items(): | |
| try: | |
| await mcp_client.connect_server(name, config) | |
| except Exception as e: | |
| print(f"Failed to connect {name}: {e}") | |
| summary = f"""✅ **Perfect! Here's your trip:** | |
| | | | | |
| |---|---| | |
| | **From** | {trip_data.get('origin', 'Not specified')} | | |
| | **To** | {trip_data.get('destination')} | | |
| | **Dates** | {trip_data.get('start_date')} → {trip_data.get('end_date')} | | |
| | **Travelers** | {trip_data.get('travelers', 2)} | | |
| | **Budget** | {trip_data.get('budget', 'moderate').title()} | | |
| 🚀 **Ready to start planning!** Click **"Continue to Next Agent"** to begin with weather research, or I'll guide you through each step. | |
| *Each agent will find the best options with real booking links!*""" | |
| history[-1] = {"role": "assistant", "content": summary} | |
| current_agent = -1 | |
| yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=True), gr.update(visible=False) | |
| return | |
| # Handle commands | |
| lower_msg = message.lower() | |
| if any(cmd in lower_msg for cmd in ["continue", "next", "start", "go", "yes"]): | |
| history[-1] = {"role": "assistant", "content": "👆 Click the **Continue to Next Agent** button to proceed!"} | |
| elif "poster" in lower_msg: | |
| history[-1] = {"role": "assistant", "content": "🎨 Click the **Generate Travel Poster** button to create your poster!"} | |
| else: | |
| history = history + [{"role": "assistant", "content": f"I'm helping you plan your trip to **{trip_data.get('destination')}**! Click **Continue** to proceed with the next agent."}] | |
| yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=current_agent < 6), gr.update(visible=current_agent >= 6) | |
| async def continue_to_next(history: list, mcp_client, trip_data: dict, current_agent: int, agent_results: dict): | |
| """Continue to next agent""" | |
| if not mcp_client or not trip_data.get("destination"): | |
| yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) | |
| return | |
| next_agent = current_agent + 1 | |
| # All done? | |
| if next_agent >= len(AGENT_SEQUENCE): | |
| report_md = generate_final_report_markdown(trip_data, agent_results) | |
| history = history + [{"role": "assistant", "content": report_md}] | |
| yield history, mcp_client, trip_data, next_agent, agent_results, update_agent_status(next_agent, agent_results), gr.update(visible=False), gr.update(visible=True) | |
| return | |
| # Run agent | |
| agent_key = AGENT_SEQUENCE[next_agent] | |
| agent = AGENTS[agent_key] | |
| history = history + [{"role": "assistant", "content": f"""{agent['icon']} **{agent['name']}** | |
| Searching for the best options..."""}] | |
| yield history, mcp_client, trip_data, next_agent, agent_results, update_agent_status(next_agent, agent_results), gr.update(visible=False), gr.update(visible=False) | |
| # Get results | |
| result = await run_agent(mcp_client, agent_key, trip_data) | |
| agent_results[agent_key] = result | |
| booking_links = make_booking_links_markdown(agent_key, trip_data) | |
| history[-1] = {"role": "assistant", "content": f"""{agent['icon']} **{agent['name']} Results** | |
| {result} | |
| {booking_links} | |
| --- | |
| Click **Continue** for the next agent! ▶️"""} | |
| yield history, mcp_client, trip_data, next_agent, agent_results, update_agent_status(next_agent, agent_results), gr.update(visible=True), gr.update(visible=False) | |
| async def create_poster(history: list, mcp_client, trip_data: dict, | |
| comp_name: str, comp_phone: str, comp_website: str, | |
| price: str, tagline: str, inclusions: str): | |
| """Generate travel poster and display in interface""" | |
| if not mcp_client or not trip_data.get("destination"): | |
| yield history, gr.update(visible=False, value=None) | |
| return | |
| dest = trip_data.get("destination", "") | |
| # Build company info dict | |
| company_info = { | |
| "company_name": comp_name or "", | |
| "company_phone": comp_phone or "", | |
| "company_website": comp_website or "", | |
| "price": price or "", | |
| "tagline": tagline or "", | |
| "inclusions": inclusions or "Flights, Hotels, Tours, Meals" | |
| } | |
| history = history + [{"role": "assistant", "content": f"""🎨 **Generating your PROFESSIONAL travel agency poster for {dest}...** | |
| ✨ Creating marketing-style poster with: | |
| - Bold destination typography | |
| - Polaroid photo collages with landmarks | |
| - Traveler silhouettes | |
| {f'- Trip dates & duration' if trip_data.get('start_date') else ''} | |
| {f'- Price: {price}' if price else ''} | |
| {f'- Company: {comp_name}' if comp_name else ''} | |
| {f'- Inclusions: {inclusions}' if inclusions else ''} | |
| ⏳ This may take 30-60 seconds..."""}] | |
| yield history, gr.update(visible=False, value=None) | |
| msg, image_path = await generate_poster(mcp_client, trip_data, company_info) | |
| if image_path and os.path.exists(image_path): | |
| file_size = os.path.getsize(image_path) / 1024 # KB | |
| history[-1] = {"role": "assistant", "content": f"""🎨 **Your Professional Travel Poster is Ready!** | |
| {msg} | |
| 📁 **File:** `{os.path.basename(image_path)}` | |
| 📐 **Size:** {file_size:.1f} KB | |
| 📱 **Format:** 9:16 Portrait (Social Media Ready) | |
| 👇 **Preview shown below** - Click to download or share! | |
| --- | |
| ✨ *Generated with Flux AI • Professional Travel Agency Style* | |
| {f'🏢 *Branded for: {comp_name}*' if comp_name else ''}"""} | |
| yield history, gr.update(visible=True, value=image_path) | |
| else: | |
| history[-1] = {"role": "assistant", "content": f"""⚠️ **Poster Generation Status** | |
| {msg} | |
| *If the poster was saved locally, check your workspace folder.*"""} | |
| yield history, gr.update(visible=False, value=None) | |
| def quick_destination(dest: str, history: list): | |
| """Handle quick destination buttons""" | |
| today = datetime.now() | |
| start = (today + timedelta(days=30)).strftime("%Y-%m-%d") | |
| end = (today + timedelta(days=37)).strftime("%Y-%m-%d") | |
| prompts = { | |
| "Paris": f"Plan a romantic trip from New York to Paris, France for 2 people, {start} to {end}, moderate budget, interested in culture, food, and art", | |
| "Bali": f"Plan an adventure trip from Los Angeles to Bali, Indonesia for 2 people, {start} to {end}, moderate budget, interested in nature, beaches, and relaxation", | |
| "Tokyo": f"Plan an exciting trip from San Francisco to Tokyo, Japan for 2 people, {start} to {end}, moderate budget, interested in culture, food, and technology", | |
| "Rome": f"Plan a cultural trip from Chicago to Rome, Italy for 2 people, {start} to {end}, moderate budget, interested in history, art, and food", | |
| "Dubai": f"Plan a luxury trip from London to Dubai, UAE for 2 people, {start} to {end}, luxury budget, interested in shopping, architecture, and adventure", | |
| } | |
| return prompts.get(dest, ""), history | |
| def help_decide_destination(history: list): | |
| """Show interactive destination finder wizard""" | |
| wizard_message = """# 🧭 **Let's Find Your Perfect Destination!** | |
| I'll help you discover the ideal place for your next adventure. Answer a few quick questions: | |
| --- | |
| ## 🎯 **What kind of experience are you looking for?** | |
| **Choose your travel style** (click one to tell me): | |
| - 🏖️ **Beach & Relaxation** - Unwind by crystal clear waters | |
| - 🏔️ **Adventure & Nature** - Hiking, wildlife, outdoor thrills | |
| - 🏛️ **Culture & History** - Museums, architecture, heritage | |
| - 🍽️ **Food & Nightlife** - Culinary tours, restaurants, bars | |
| - 💑 **Romance & Honeymoon** - Intimate getaways for couples | |
| - 👨👩👧👦 **Family Fun** - Kid-friendly activities and resorts | |
| - 🛍️ **Shopping & Luxury** - High-end experiences, designer brands | |
| - 🎰 **Nightlife & Entertainment** - Clubs, shows, casinos | |
| --- | |
| **💬 Tell me your preferences!** For example: | |
| > *"I want a beach vacation with good food, budget around $2000 per person, traveling in March"* | |
| Or simply type keywords like: **beach, adventure, romantic, budget-friendly, luxury, family** | |
| I'll match you with the **best destinations** and show you **current deals**! 🎁""" | |
| history.append({"role": "assistant", "content": wizard_message}) | |
| return "", history | |
| def show_current_deals(history: list): | |
| """Display current travel deals and offers""" | |
| deals_message = """# 🔥 **Hot Travel Deals Right Now!** | |
| Here are the **best offers** from our partner travel agents: | |
| --- | |
| ## ✨ **Featured Deals** | |
| ### 🏜️ **Dubai Winter Escape** - *25% OFF* | |
| > **From $1,499/person** (was $1,999) | |
| > - 5 nights at JW Marriott Marquis | |
| > - Desert safari included | |
| > - *Valid: Dec 2024 - Feb 2025* | |
| > | |
| > 🏷️ `luxury` `adventure` `shopping` | |
| --- | |
| ### 🏝️ **Bali Early Bird Special** - *30% OFF* | |
| > **From $899/person** (was $1,285) | |
| > - 7 nights beachfront resort | |
| > - Temple tour & spa day | |
| > - *Book by Dec 31 for travel Jan-Mar* | |
| > | |
| > 🏷️ `beach` `relaxation` `culture` | |
| --- | |
| ### 🌸 **Tokyo Cherry Blossom Package** - *15% OFF* | |
| > **From $1,699/person** (was $1,999) | |
| > - 6 nights in Shinjuku | |
| > - JR Pass included | |
| > - *Travel: Mar 20 - Apr 15, 2025* | |
| > | |
| > 🏷️ `culture` `food` `unique` | |
| --- | |
| ### 🇬🇷 **Greek Island Hopper** - *20% OFF* | |
| > **From $1,299/person** (was $1,624) | |
| > - Santorini + Mykonos combo | |
| > - Ferry transfers included | |
| > - *Travel: May - Oct 2025* | |
| > | |
| > 🏷️ `beach` `romantic` `culture` | |
| --- | |
| ### 🇲🇻 **Maldives Honeymoon Dream** - *$500 Resort Credit* | |
| > **From $2,999/person** | |
| > - 5 nights overwater villa | |
| > - Sunset dinner cruise | |
| > - *Book by Jan 31, 2025* | |
| > | |
| > 🏷️ `luxury` `romantic` `beach` | |
| --- | |
| ### 🗼 **Paris City Break** - *Stay 4 Pay 3* | |
| > **From $799/person** | |
| > - 4 nights near Champs-Élysées | |
| > - Seine river cruise | |
| > - *Travel: Nov 2024 - Mar 2025* | |
| > | |
| > 🏷️ `romantic` `culture` `food` | |
| --- | |
| ## 💡 **Interested in a deal?** | |
| Just tell me which one catches your eye, or describe what you're looking for: | |
| > *"Tell me more about the Bali deal"* | |
| > *"I want something romantic under $1500"* | |
| > *"Find me beach destinations with deals"* | |
| I'll help you **book the perfect trip** at the best price! 🎯""" | |
| history.append({"role": "assistant", "content": deals_message}) | |
| return "", history | |
| # Wire events | |
| send_btn.click( | |
| fn=handle_message, | |
| inputs=[user_input, chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state], | |
| outputs=[chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state, agent_status_html, continue_btn, poster_btn] | |
| ).then(lambda: "", outputs=[user_input]) | |
| user_input.submit( | |
| fn=handle_message, | |
| inputs=[user_input, chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state], | |
| outputs=[chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state, agent_status_html, continue_btn, poster_btn] | |
| ).then(lambda: "", outputs=[user_input]) | |
| continue_btn.click( | |
| fn=continue_to_next, | |
| inputs=[chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state], | |
| outputs=[chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state, agent_status_html, continue_btn, poster_btn] | |
| ) | |
| poster_btn.click( | |
| fn=create_poster, | |
| inputs=[chatbot, mcp_client_state, trip_data_state, | |
| company_name, company_phone, company_website, | |
| poster_price, poster_tagline, poster_inclusions], | |
| outputs=[chatbot, poster_image] | |
| ) | |
| # Quick action buttons - Help Decide & Deals | |
| help_decide_btn.click(fn=help_decide_destination, inputs=[chatbot], outputs=[user_input, chatbot]) | |
| deals_btn.click(fn=show_current_deals, inputs=[chatbot], outputs=[user_input, chatbot]) | |
| # Quick destination buttons | |
| quick_paris.click(lambda h: quick_destination("Paris", h), inputs=[chatbot], outputs=[user_input, chatbot]) | |
| quick_bali.click(lambda h: quick_destination("Bali", h), inputs=[chatbot], outputs=[user_input, chatbot]) | |
| quick_tokyo.click(lambda h: quick_destination("Tokyo", h), inputs=[chatbot], outputs=[user_input, chatbot]) | |
| quick_rome.click(lambda h: quick_destination("Rome", h), inputs=[chatbot], outputs=[user_input, chatbot]) | |
| quick_dubai.click(lambda h: quick_destination("Dubai", h), inputs=[chatbot], outputs=[user_input, chatbot]) | |
| return app | |
| # ============== MAIN ============== | |
| if __name__ == "__main__": | |
| app = create_app() | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=True, | |
| show_error=True, | |
| ) | |