Atul1997's picture
Upload app.py
36ca00a verified
"""
Hotel Search App — Hugging Face Gradio Application
Searches for hotels using natural language via SerpApi Google Hotels engine.
"""
import os
import re
from datetime import datetime, timedelta
import gradio as gr
from dateutil import parser as date_parser
try:
from serpapi import GoogleSearch
except ImportError:
import serpapi
GoogleSearch = None
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
TRAVEL_AGENCY_DOMAINS = [
"expedia", "booking.com", "hotels.com", "trivago", "kayak",
"priceline", "orbitz", "travelocity", "agoda", "trip.com",
"hotwire", "cheaptickets", "lastminute", "edreams", "opodo",
"wotif", "zuji", "makemytrip", "goibibo", "yatra",
]
AMENITY_KEYWORDS = [
"pool", "gym", "fitness", "spa", "wifi", "wi-fi", "parking",
"breakfast", "restaurant", "bar", "pet-friendly", "pets",
"non-smoking", "smoke-free", "air conditioning", "laundry",
"room service", "concierge", "shuttle", "beach", "ocean view",
"balcony", "kitchen", "kitchenette", "suite", "jacuzzi",
"hot tub", "business center", "ev charging",
]
# ---------------------------------------------------------------------------
# Input parser
# ---------------------------------------------------------------------------
def parse_user_input(text: str) -> dict:
"""Extract structured hotel search parameters from free-form text."""
result = {
"location": "",
"check_in": "",
"check_out": "",
"adults": 2,
"min_price": None,
"max_price": None,
"required_features": [],
"desired_features": [],
"avoid_features": [],
}
# --- Dates ----------------------------------------------------------
date_patterns = [
r"\d{1,2}[/-]\d{1,2}[/-]\d{2,4}",
r"\d{4}[/-]\d{1,2}[/-]\d{1,2}",
r"(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}(?:\s*,?\s*\d{4})?",
]
raw_dates: list[str] = []
for pat in date_patterns:
raw_dates.extend(re.findall(pat, text, re.IGNORECASE))
parsed_dates: list[datetime] = []
for raw in raw_dates:
try:
parsed_dates.append(date_parser.parse(raw, fuzzy=True))
except (ValueError, OverflowError):
continue
date_range = re.search(
r"(\w+\s+\d{1,2})\s*[-–to]+\s*(\d{1,2})\s*,?\s*(\d{4})?", text, re.IGNORECASE
)
if date_range and not parsed_dates:
try:
month_day_start = date_range.group(1)
day_end = date_range.group(2)
year = date_range.group(3) or str(datetime.now().year)
start = date_parser.parse(f"{month_day_start} {year}")
month_name = re.match(r"[A-Za-z]+", month_day_start).group()
end = date_parser.parse(f"{month_name} {day_end} {year}")
parsed_dates = [start, end]
except (ValueError, AttributeError):
pass
if len(parsed_dates) >= 2:
parsed_dates.sort()
result["check_in"] = parsed_dates[0].strftime("%Y-%m-%d")
result["check_out"] = parsed_dates[1].strftime("%Y-%m-%d")
elif len(parsed_dates) == 1:
result["check_in"] = parsed_dates[0].strftime("%Y-%m-%d")
result["check_out"] = (parsed_dates[0] + timedelta(days=1)).strftime("%Y-%m-%d")
# --- Price ----------------------------------------------------------
range_match = re.search(
r"\$\s*(\d+)\s*(?:to|-|–)\s*\$\s*(\d+)", text, re.IGNORECASE
)
if range_match:
result["min_price"] = int(range_match.group(1))
result["max_price"] = int(range_match.group(2))
else:
upper = re.search(
r"(?:under|below|less than|max(?:imum)?|budget\s*(?:of|is|:)?|up to|cheaper than)\s*\$?\s*(\d+)",
text, re.IGNORECASE,
)
if upper:
result["max_price"] = int(upper.group(1))
lower = re.search(
r"(?:above|over|more than|at least|min(?:imum)?|starting at)\s*\$?\s*(\d+)",
text, re.IGNORECASE,
)
if lower:
result["min_price"] = int(lower.group(1))
# --- Adults ---------------------------------------------------------
adults_match = re.search(r"(\d+)\s*(?:adults?|guests?|people|persons?|travelers?)", text, re.IGNORECASE)
if adults_match:
result["adults"] = max(1, int(adults_match.group(1)))
# --- Location -------------------------------------------------------
loc_patterns = [
r"(?:hotels?\s+)?(?:in|near|around|close to|next to|at)\s+([A-Z][A-Za-z\s,.']+?)(?:\s+(?:from|for|on|with|under|below|between|that|which|checking|budget|\d|\.|\,\s*(?:I|we|for|from|checking)))",
r"(?:hotels?\s+)?(?:in|near|around|close to|next to|at)\s+([A-Z][A-Za-z\s,.']+?)$",
]
for pat in loc_patterns:
m = re.search(pat, text)
if m:
loc = m.group(1).strip().rstrip(" .,")
if len(loc) > 2:
result["location"] = loc
break
# --- Features -------------------------------------------------------
req_patterns = [
r"(?:must have|essential|has to have|mandatory)\s+(.+?)(?:\.|$)",
r"(?:require[sd]?|need[sd]?|should have)\s+(?!a hotel|a room|a place|an? )(.+?)(?:\.|$)",
]
des_patterns = [
r"(?:would (?:like|love|prefer)|prefer(?:ably)?|nice to have|ideally|hopefully)\s+(.+?)(?:\.|$)",
]
avoid_pats = [
r"(?:(?:don'?t|do not) want|avoid|without|no |not interested in|stay away from)\s*(.+?)(?:\.|$)",
]
for pats, key in [(req_patterns, "required_features"), (des_patterns, "desired_features"), (avoid_pats, "avoid_features")]:
for pat in pats:
for m in re.finditer(pat, text, re.IGNORECASE):
features = [f.strip() for f in re.split(r",|(?:\s+and\s+)", m.group(1)) if f.strip()]
result[key].extend(features)
return result
# ---------------------------------------------------------------------------
# Hotel link resolver
# ---------------------------------------------------------------------------
def get_hotel_link(hotel: dict) -> str:
"""Return the best non-travel-agency link for a hotel."""
for field in ("link", "website"):
url = hotel.get(field, "")
if url and not any(agency in url.lower() for agency in TRAVEL_AGENCY_DOMAINS):
return url
prices = hotel.get("prices", [])
for p in prices:
source = (p.get("source") or "").lower()
if "official" in source or hotel.get("name", "").lower().split()[0] in source:
link = p.get("link", "")
if link:
return link
name = hotel.get("name", "Hotel")
return f"https://www.google.com/search?q={name.replace(' ', '+')}+official+site"
# ---------------------------------------------------------------------------
# Result formatter
# ---------------------------------------------------------------------------
def format_hotel_results(properties: list[dict]) -> str:
"""Render hotel results as styled HTML cards."""
if not properties:
return (
"<div style='text-align:center;padding:40px;'>"
"<h3>No hotels found matching your criteria.</h3>"
"<p>Try broadening your search — use a larger area or relax price/feature constraints.</p>"
"</div>"
)
cards = []
for idx, hotel in enumerate(properties[:15], 1):
try:
name = str(hotel.get("name", "Unknown Hotel") or "Unknown Hotel")
rating = hotel.get("overall_rating", "N/A")
reviews = hotel.get("reviews", 0) or 0
description = str(hotel.get("description", "") or "No description available.")
rpn = hotel.get("rate_per_night") or {}
price = rpn.get("lowest", "N/A") if isinstance(rpn, dict) else "N/A"
hotel_class = hotel.get("hotel_class", "") or ""
amenities = hotel.get("amenities") or []
images = hotel.get("images") or []
link = get_hotel_link(hotel)
# Thumbnail
img_url = ""
if images:
first = images[0]
if isinstance(first, dict):
img_url = first.get("thumbnail") or first.get("original_image", "")
elif isinstance(first, str):
img_url = first
img_html = (
f"<img src='{img_url}' style='width:200px;height:150px;object-fit:cover;"
f"border-radius:8px;' onerror=\"this.style.display='none'\">"
if img_url
else "<div style='width:200px;height:150px;background:#e0e0e0;border-radius:8px;"
"display:flex;align-items:center;justify-content:center;color:#999;"
"font-size:14px;'>No Image</div>"
)
# Stars
stars = ""
if hotel_class:
try:
n = int(float(str(hotel_class).replace("-star", "").replace("star", "").replace("hotel", "").strip()))
stars = "&#11088;" * min(n, 5)
except (ValueError, TypeError):
stars = str(hotel_class)
# Rating colour
try:
r = float(str(rating))
rating_color = "#4CAF50" if r >= 4.0 else "#FF9800" if r >= 3.0 else "#f44336"
except (ValueError, TypeError):
rating_color = "#9e9e9e"
# Amenities chips (show up to 8)
amenities_html = " ".join(
f"<span style='background:#e8f5e9;color:#2e7d32;padding:2px 8px;"
f"border-radius:12px;font-size:12px;margin:2px;display:inline-block;'>{str(a)}</span>"
for a in (amenities[:8] if amenities else [])
)
# Escape special HTML characters in user-facing text
safe_name = name.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
safe_desc = description[:220].replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
if len(description) > 220:
safe_desc += "..."
card = f"""
<div style='border:1px solid #e0e0e0;border-radius:12px;padding:20px;margin:12px 0;
background:white;box-shadow:0 2px 8px rgba(0,0,0,0.08);display:flex;gap:20px;
flex-wrap:wrap;'>
<div style='flex-shrink:0;'>{img_html}</div>
<div style='flex-grow:1;min-width:260px;'>
<div style='display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;'>
<div>
<h3 style='margin:0 0 4px 0;color:#1a237e;'>#{idx} {safe_name}</h3>
<span style='color:#666;font-size:14px;'>{stars}</span>
</div>
<div style='text-align:right;'>
<div style='font-size:24px;font-weight:bold;color:#1a237e;'>{price}</div>
<div style='font-size:12px;color:#666;'>per night</div>
</div>
</div>
<div style='margin:8px 0;'>
<span style='background:{rating_color};color:white;padding:2px 10px;
border-radius:4px;font-weight:bold;font-size:14px;'>{rating}</span>
<span style='color:#666;font-size:13px;margin-left:8px;'>{reviews} reviews</span>
</div>
<p style='color:#555;font-size:14px;margin:8px 0;line-height:1.4;'>{safe_desc}</p>
<div style='margin:8px 0;'>{amenities_html}</div>
<div style='margin-top:12px;'>
<a href='{link}' target='_blank' rel='noopener noreferrer'
style='background:#1a237e;color:white;padding:8px 20px;border-radius:6px;
text-decoration:none;font-size:14px;font-weight:500;
display:inline-block;'>Visit Hotel Website</a>
</div>
</div>
</div>"""
cards.append(card)
except Exception:
continue
return "<div style='font-family:Arial,sans-serif;'>" + "".join(cards) + "</div>"
# ---------------------------------------------------------------------------
# Parsed-parameters summary (shown above results)
# ---------------------------------------------------------------------------
def parsed_summary_html(parsed: dict, total: int) -> str:
parts = [f"<strong>Found {total} hotel(s)</strong>"]
if parsed["location"]:
parts.append(f"Location: <em>{parsed['location']}</em>")
if parsed["check_in"]:
parts.append(f"Check-in: {parsed['check_in']}")
if parsed["check_out"]:
parts.append(f"Check-out: {parsed['check_out']}")
if parsed["adults"] != 2:
parts.append(f"Guests: {parsed['adults']}")
if parsed["min_price"] and parsed["max_price"]:
parts.append(f"Budget: ${parsed['min_price']}–${parsed['max_price']}/night")
elif parsed["max_price"]:
parts.append(f"Budget: up to ${parsed['max_price']}/night")
elif parsed["min_price"]:
parts.append(f"Budget: ${parsed['min_price']}+/night")
if parsed["required_features"]:
parts.append(f"Required: {', '.join(parsed['required_features'])}")
if parsed["desired_features"]:
parts.append(f"Preferred: {', '.join(parsed['desired_features'])}")
if parsed["avoid_features"]:
parts.append(f"Avoiding: {', '.join(parsed['avoid_features'])}")
return (
"<div style='background:#e3f2fd;padding:15px;border-radius:8px;margin-bottom:15px;"
"line-height:1.8;'>" + " &nbsp;|&nbsp; ".join(parts) + "</div>"
)
# ---------------------------------------------------------------------------
# Main search function
# ---------------------------------------------------------------------------
def search_hotels(user_input: str) -> str:
"""Parse user input, call SerpApi, and return formatted HTML results."""
if not user_input or not user_input.strip():
return (
"<div style='text-align:center;padding:40px;'>"
"<h3>Please enter a hotel description to search.</h3></div>"
)
key = os.environ.get("SERPAPI_KEY", "") or os.environ.get("SERPAPI_API_KEY", "")
if not key:
possible_paths = [
os.path.join(os.getcwd(), "api_key.txt"),
os.path.join(os.path.expanduser("~"), "Desktop", "Hotel App", "api_key.txt"),
]
for key_file in possible_paths:
if os.path.exists(key_file):
with open(key_file) as f:
key = f.read().strip()
if key:
break
if not key:
return (
"<div style='text-align:center;padding:40px;'>"
"<h3>SerpApi key required</h3>"
"<p>Set <code>SERPAPI_KEY</code> as a Hugging Face Space secret, "
"or place your key in <code>api_key.txt</code> for local testing.</p></div>"
)
try:
parsed = parse_user_input(user_input)
query = parsed["location"] if parsed["location"] else user_input.split(".")[0][:120]
tomorrow = datetime.now() + timedelta(days=1)
check_in = parsed["check_in"] or tomorrow.strftime("%Y-%m-%d")
ci_dt = datetime.strptime(check_in, "%Y-%m-%d")
if ci_dt.date() < datetime.now().date():
ci_dt = tomorrow
check_in = ci_dt.strftime("%Y-%m-%d")
check_out = parsed["check_out"] or (ci_dt + timedelta(days=1)).strftime("%Y-%m-%d")
params: dict = {
"engine": "google_hotels",
"q": query,
"check_in_date": check_in,
"check_out_date": check_out,
"adults": parsed["adults"],
"currency": "USD",
"gl": "us",
"hl": "en",
"api_key": key,
}
if parsed["min_price"]:
params["min_price"] = parsed["min_price"]
if parsed["max_price"]:
params["max_price"] = parsed["max_price"]
if GoogleSearch is not None:
search = GoogleSearch(params)
results = search.get_dict()
else:
results = serpapi.search(params)
if "error" in results:
return (
f"<div style='text-align:center;padding:40px;'>"
f"<h3>SerpApi Error</h3><p>{results['error']}</p></div>"
)
properties = results.get("properties", [])
filtered = [
h for h in properties
if not any(agency in get_hotel_link(h).lower() for agency in TRAVEL_AGENCY_DOMAINS)
]
if len(filtered) < 3 and properties:
filtered = properties
summary = parsed_summary_html(parsed, len(filtered))
return summary + format_hotel_results(filtered)
except Exception as exc:
return (
f"<div style='text-align:center;padding:40px;'>"
f"<h3>Something went wrong</h3>"
f"<p>{exc}</p>"
f"<p>Double-check your API key and try again.</p></div>"
)
# ---------------------------------------------------------------------------
# Gradio UI
# ---------------------------------------------------------------------------
with gr.Blocks(title="AI Hotel Search") as app:
gr.Markdown(
"# AI Hotel Search\n"
"### Find your perfect hotel using natural language\n"
"Describe what you are looking for in plain English — include location, dates, "
"budget, and any features you want or want to avoid."
)
user_input = gr.Textbox(
label="Describe Your Ideal Hotel",
placeholder=(
"Example: I'm looking for a quiet, non-smoking hotel in downtown "
"Austin, TX from March 15 to March 18, 2026. Budget under $200/night. "
"Must have free parking and wifi. Would prefer a pool. Avoid hotels "
"near highways."
),
lines=5,
)
search_btn = gr.Button("Search Hotels", variant="primary", size="lg")
with gr.Accordion("Search Tips & Examples", open=False):
gr.Markdown(
"**Good search examples:**\n\n"
"- *\"Find me a hotel in San Francisco near Fisherman's Wharf, checking in "
"April 5 and out April 8, 2026. Budget under $250/night. Must be non-smoking "
"with free wifi. Would like ocean view.\"*\n"
"- *\"Pet-friendly hotel in Nashville, TN for 2 adults from May 10-12, 2026. "
"Price range $100-$180. Prefer boutique hotels. Avoid large chain hotels.\"*\n"
"- *\"Luxury hotel in Miami Beach, March 20-23, 2026. $300-$500/night. Must "
"have spa and pool. Prefer beachfront.\"*\n\n"
"**Tips for best results:**\n\n"
"- Always include a **location** (city, state, or landmark).\n"
"- Specify **dates** in common formats (MM/DD/YYYY, Month Day Year, etc.).\n"
"- Mention your **budget** with dollar amounts.\n"
"- Distinguish between **must-have** and **nice-to-have** features.\n"
"- State features to **avoid** explicitly.\n\n"
"**Bad example (too vague):**\n\n"
"- *\"Find me a nice hotel somewhere warm.\"* — no location, no dates, no budget."
)
results_output = gr.HTML(label="Search Results")
search_btn.click(fn=search_hotels, inputs=[user_input], outputs=results_output)
user_input.submit(fn=search_hotels, inputs=[user_input], outputs=results_output)
if __name__ == "__main__":
app.launch()