|
|
""" |
|
|
🌍 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() |
|
|
|
|
|
|
|
|
|
|
|
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/" |
|
|
|
|
|
|
|
|
if not NEBIUS_API_KEY: |
|
|
|
|
|
NEBIUS_API_KEY = "v1.CmQKHHN0YXRpY2tleS1lMDBxZHo3Nzdzcnl5YWI2aGMSIXNlcnZpY2VhY2NvdW50LWUwMHFlNjY1a214YXJ5bTZnYTIMCIGJ88gGEKfcq5UBOgwIgIyLlAcQgO6m7AJAAloDZTAw.AAAAAAAAAAFpaIe7TQXIIO9jnQJWji15jqL-5Gts-kuJHPIqA8JxedCZSxHmDYTmU6QnsUXQHLlitYOwId8GjSGCdT1JHJ0K" |
|
|
print("⚠️ Using fallback API key") |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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_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"]}, |
|
|
} |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
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": [ |
|
|
|
|
|
("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"), |
|
|
], |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 "" |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
dest_clean = destination.split(",")[0].strip() if destination else "Paris" |
|
|
origin_clean = origin.split(",")[0].strip() if origin else "New York" |
|
|
|
|
|
|
|
|
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(" ", "-") |
|
|
|
|
|
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 "" |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
dest_clean = destination.split(",")[0].strip() if destination else "Paris" |
|
|
origin_clean = origin.split(",")[0].strip() if origin else "New York" |
|
|
|
|
|
|
|
|
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", []) |
|
|
|
|
|
|
|
|
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": |
|
|
|
|
|
resp = await mcp_client.call_tool("weather", "get_forecast", { |
|
|
"city": destination, |
|
|
"dates": f"{start_date} to {end_date}" |
|
|
}) |
|
|
elif agent_key == "flights": |
|
|
|
|
|
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": |
|
|
|
|
|
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": |
|
|
|
|
|
resp = await mcp_client.call_tool("activities", "search_activities", { |
|
|
"city": destination, |
|
|
"interest": interest_type |
|
|
}) |
|
|
elif agent_key == "dining": |
|
|
|
|
|
resp = await mcp_client.call_tool("dining", "find_restaurants", { |
|
|
"city": destination, |
|
|
"cuisine": "local", |
|
|
"buffet": False |
|
|
}) |
|
|
elif agent_key == "transport": |
|
|
|
|
|
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 = [] |
|
|
|
|
|
|
|
|
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_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>''') |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
dest_clean = dest.split(",")[0].strip() |
|
|
origin_clean = origin.split(",")[0].strip() |
|
|
|
|
|
|
|
|
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", []) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 = company_info or {} |
|
|
|
|
|
try: |
|
|
|
|
|
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: |
|
|
|
|
|
if result.startswith("/") and os.path.exists(result): |
|
|
return (f"✅ Professional travel poster generated!", result) |
|
|
|
|
|
elif os.path.exists(result): |
|
|
return (f"✅ Professional travel poster generated!", result) |
|
|
|
|
|
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"] |
|
|
|
|
|
if "```json" in content: |
|
|
content = content.split("```json")[1].split("```")[0] |
|
|
elif "```" in content: |
|
|
content = content.split("```")[1].split("```")[0] |
|
|
|
|
|
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)} |
|
|
|
|
|
|
|
|
|
|
|
def create_app(): |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
|
|
|
mcp_client_state = gr.State(None) |
|
|
trip_data_state = gr.State({}) |
|
|
current_agent_state = gr.State(-1) |
|
|
agent_results_state = gr.State({}) |
|
|
|
|
|
|
|
|
with gr.Column(elem_classes="main-wrapper"): |
|
|
|
|
|
|
|
|
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> |
|
|
""") |
|
|
|
|
|
|
|
|
with gr.Row(elem_id="main-content"): |
|
|
|
|
|
|
|
|
with gr.Column(scale=3): |
|
|
with gr.Column(elem_classes="glass-card"): |
|
|
|
|
|
|
|
|
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", |
|
|
) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
with gr.Column(scale=1): |
|
|
|
|
|
|
|
|
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> |
|
|
""") |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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", |
|
|
) |
|
|
|
|
|
|
|
|
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" |
|
|
) |
|
|
|
|
|
|
|
|
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> |
|
|
""") |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
history = history + [{"role": "user", "content": message}] |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
needs_suggestion = trip_data.get("needs_suggestion", False) or trip_data.get("destination", "").upper() == "SUGGEST" |
|
|
|
|
|
if needs_suggestion or not trip_data.get("destination"): |
|
|
|
|
|
mcp_client = MCPClient() |
|
|
try: |
|
|
await mcp_client.connect_server("recommendations", MCP_SERVERS["recommendations"]) |
|
|
except Exception as e: |
|
|
print(f"Failed to connect recommendations: {e}") |
|
|
|
|
|
|
|
|
origin = trip_data.get("origin", "") |
|
|
budget = trip_data.get("budget", "moderate") |
|
|
travelers = trip_data.get("travelers", 2) |
|
|
interests = ",".join(trip_data.get("interests", [])) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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", "") |
|
|
|
|
|
|
|
|
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 |
|
|
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 |
|
|
|
|
|
|
|
|
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] |
|
|
) |
|
|
|
|
|
|
|
|
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_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 |
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
app = create_app() |
|
|
app.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
share=True, |
|
|
show_error=True, |
|
|
) |
|
|
|