ABDALLALSWAITI's picture
Upload app.py with huggingface_hub
1bbd0cf verified
"""
🌍 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,
)