Atul1997 commited on
Commit
57fbe9e
·
verified ·
1 Parent(s): 2ad3347

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -471
app.py DELETED
@@ -1,471 +0,0 @@
1
- """
2
- Hotel Search App — Hugging Face Gradio Application
3
- Searches for hotels using natural language via SerpApi Google Hotels engine.
4
- """
5
-
6
- import os
7
- import re
8
- from datetime import datetime, timedelta
9
-
10
- import gradio as gr
11
- from dateutil import parser as date_parser
12
- try:
13
- from serpapi import GoogleSearch
14
- except ImportError:
15
- import serpapi
16
- GoogleSearch = None
17
-
18
- # ---------------------------------------------------------------------------
19
- # Constants
20
- # ---------------------------------------------------------------------------
21
-
22
- TRAVEL_AGENCY_DOMAINS = [
23
- "expedia", "booking.com", "hotels.com", "trivago", "kayak",
24
- "priceline", "orbitz", "travelocity", "agoda", "trip.com",
25
- "hotwire", "cheaptickets", "lastminute", "edreams", "opodo",
26
- "wotif", "zuji", "makemytrip", "goibibo", "yatra",
27
- ]
28
-
29
- AMENITY_KEYWORDS = [
30
- "pool", "gym", "fitness", "spa", "wifi", "wi-fi", "parking",
31
- "breakfast", "restaurant", "bar", "pet-friendly", "pets",
32
- "non-smoking", "smoke-free", "air conditioning", "laundry",
33
- "room service", "concierge", "shuttle", "beach", "ocean view",
34
- "balcony", "kitchen", "kitchenette", "suite", "jacuzzi",
35
- "hot tub", "business center", "ev charging",
36
- ]
37
-
38
- # ---------------------------------------------------------------------------
39
- # Input parser
40
- # ---------------------------------------------------------------------------
41
-
42
-
43
- def parse_user_input(text: str) -> dict:
44
- """Extract structured hotel search parameters from free-form text."""
45
-
46
- result = {
47
- "location": "",
48
- "check_in": "",
49
- "check_out": "",
50
- "adults": 2,
51
- "min_price": None,
52
- "max_price": None,
53
- "required_features": [],
54
- "desired_features": [],
55
- "avoid_features": [],
56
- }
57
-
58
- # --- Dates ----------------------------------------------------------
59
- date_patterns = [
60
- r"\d{1,2}[/-]\d{1,2}[/-]\d{2,4}",
61
- r"\d{4}[/-]\d{1,2}[/-]\d{1,2}",
62
- 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})?",
63
- ]
64
-
65
- raw_dates: list[str] = []
66
- for pat in date_patterns:
67
- raw_dates.extend(re.findall(pat, text, re.IGNORECASE))
68
-
69
- parsed_dates: list[datetime] = []
70
- for raw in raw_dates:
71
- try:
72
- parsed_dates.append(date_parser.parse(raw, fuzzy=True))
73
- except (ValueError, OverflowError):
74
- continue
75
-
76
- date_range = re.search(
77
- r"(\w+\s+\d{1,2})\s*[-–to]+\s*(\d{1,2})\s*,?\s*(\d{4})?", text, re.IGNORECASE
78
- )
79
- if date_range and not parsed_dates:
80
- try:
81
- month_day_start = date_range.group(1)
82
- day_end = date_range.group(2)
83
- year = date_range.group(3) or str(datetime.now().year)
84
- start = date_parser.parse(f"{month_day_start} {year}")
85
- month_name = re.match(r"[A-Za-z]+", month_day_start).group()
86
- end = date_parser.parse(f"{month_name} {day_end} {year}")
87
- parsed_dates = [start, end]
88
- except (ValueError, AttributeError):
89
- pass
90
-
91
- if len(parsed_dates) >= 2:
92
- parsed_dates.sort()
93
- result["check_in"] = parsed_dates[0].strftime("%Y-%m-%d")
94
- result["check_out"] = parsed_dates[1].strftime("%Y-%m-%d")
95
- elif len(parsed_dates) == 1:
96
- result["check_in"] = parsed_dates[0].strftime("%Y-%m-%d")
97
- result["check_out"] = (parsed_dates[0] + timedelta(days=1)).strftime("%Y-%m-%d")
98
-
99
- # --- Price ----------------------------------------------------------
100
- range_match = re.search(
101
- r"\$\s*(\d+)\s*(?:to|-|–)\s*\$\s*(\d+)", text, re.IGNORECASE
102
- )
103
- if range_match:
104
- result["min_price"] = int(range_match.group(1))
105
- result["max_price"] = int(range_match.group(2))
106
- else:
107
- upper = re.search(
108
- r"(?:under|below|less than|max(?:imum)?|budget\s*(?:of|is|:)?|up to|cheaper than)\s*\$?\s*(\d+)",
109
- text, re.IGNORECASE,
110
- )
111
- if upper:
112
- result["max_price"] = int(upper.group(1))
113
- lower = re.search(
114
- r"(?:above|over|more than|at least|min(?:imum)?|starting at)\s*\$?\s*(\d+)",
115
- text, re.IGNORECASE,
116
- )
117
- if lower:
118
- result["min_price"] = int(lower.group(1))
119
-
120
- # --- Adults ---------------------------------------------------------
121
- adults_match = re.search(r"(\d+)\s*(?:adults?|guests?|people|persons?|travelers?)", text, re.IGNORECASE)
122
- if adults_match:
123
- result["adults"] = max(1, int(adults_match.group(1)))
124
-
125
- # --- Location -------------------------------------------------------
126
- loc_patterns = [
127
- 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)))",
128
- r"(?:hotels?\s+)?(?:in|near|around|close to|next to|at)\s+([A-Z][A-Za-z\s,.']+?)$",
129
- ]
130
- for pat in loc_patterns:
131
- m = re.search(pat, text)
132
- if m:
133
- loc = m.group(1).strip().rstrip(" .,")
134
- if len(loc) > 2:
135
- result["location"] = loc
136
- break
137
-
138
- # --- Features -------------------------------------------------------
139
- req_patterns = [
140
- r"(?:must have|essential|has to have|mandatory)\s+(.+?)(?:\.|$)",
141
- r"(?:require[sd]?|need[sd]?|should have)\s+(?!a hotel|a room|a place|an? )(.+?)(?:\.|$)",
142
- ]
143
- des_patterns = [
144
- r"(?:would (?:like|love|prefer)|prefer(?:ably)?|nice to have|ideally|hopefully)\s+(.+?)(?:\.|$)",
145
- ]
146
- avoid_pats = [
147
- r"(?:(?:don'?t|do not) want|avoid|without|no |not interested in|stay away from)\s*(.+?)(?:\.|$)",
148
- ]
149
- for pats, key in [(req_patterns, "required_features"), (des_patterns, "desired_features"), (avoid_pats, "avoid_features")]:
150
- for pat in pats:
151
- for m in re.finditer(pat, text, re.IGNORECASE):
152
- features = [f.strip() for f in re.split(r",|(?:\s+and\s+)", m.group(1)) if f.strip()]
153
- result[key].extend(features)
154
-
155
- return result
156
-
157
-
158
- # ---------------------------------------------------------------------------
159
- # Hotel link resolver
160
- # ---------------------------------------------------------------------------
161
-
162
-
163
- def get_hotel_link(hotel: dict) -> str:
164
- """Return the best non-travel-agency link for a hotel."""
165
- for field in ("link", "website"):
166
- url = hotel.get(field, "")
167
- if url and not any(agency in url.lower() for agency in TRAVEL_AGENCY_DOMAINS):
168
- return url
169
-
170
- prices = hotel.get("prices", [])
171
- for p in prices:
172
- source = (p.get("source") or "").lower()
173
- if "official" in source or hotel.get("name", "").lower().split()[0] in source:
174
- link = p.get("link", "")
175
- if link:
176
- return link
177
-
178
- name = hotel.get("name", "Hotel")
179
- return f"https://www.google.com/search?q={name.replace(' ', '+')}+official+site"
180
-
181
-
182
- # ---------------------------------------------------------------------------
183
- # Result formatter
184
- # ---------------------------------------------------------------------------
185
-
186
-
187
- def format_hotel_results(properties: list[dict]) -> str:
188
- """Render hotel results as styled HTML cards."""
189
-
190
- if not properties:
191
- return (
192
- "<div style='text-align:center;padding:40px;'>"
193
- "<h3>No hotels found matching your criteria.</h3>"
194
- "<p>Try broadening your search — use a larger area or relax price/feature constraints.</p>"
195
- "</div>"
196
- )
197
-
198
- cards: list[str] = []
199
- for idx, hotel in enumerate(properties[:15], 1):
200
- name = hotel.get("name", "Unknown Hotel")
201
- rating = hotel.get("overall_rating", "N/A")
202
- reviews = hotel.get("reviews", 0)
203
- description = hotel.get("description", "No description available.")
204
-
205
- rpn = hotel.get("rate_per_night", {})
206
- price = rpn.get("lowest", "N/A") if isinstance(rpn, dict) else "N/A"
207
-
208
- hotel_class = hotel.get("hotel_class", "")
209
- amenities = hotel.get("amenities", [])
210
- images = hotel.get("images", [])
211
- link = get_hotel_link(hotel)
212
-
213
- # Thumbnail
214
- img_url = ""
215
- if images:
216
- first = images[0]
217
- if isinstance(first, dict):
218
- img_url = first.get("thumbnail") or first.get("original_image", "")
219
- elif isinstance(first, str):
220
- img_url = first
221
- img_html = (
222
- f"<img src='{img_url}' style='width:200px;height:150px;object-fit:cover;"
223
- f"border-radius:8px;' onerror=\"this.style.display='none'\">"
224
- if img_url
225
- else "<div style='width:200px;height:150px;background:#e0e0e0;border-radius:8px;"
226
- "display:flex;align-items:center;justify-content:center;color:#999;"
227
- "font-size:14px;'>No Image</div>"
228
- )
229
-
230
- # Stars
231
- stars = ""
232
- if hotel_class:
233
- try:
234
- n = int(float(str(hotel_class).replace("-star", "").replace("star", "").replace("hotel", "").strip()))
235
- stars = "&#11088;" * n
236
- except (ValueError, TypeError):
237
- stars = hotel_class
238
-
239
- # Rating colour
240
- try:
241
- r = float(str(rating))
242
- rating_color = "#4CAF50" if r >= 4.0 else "#FF9800" if r >= 3.0 else "#f44336"
243
- except (ValueError, TypeError):
244
- rating_color = "#9e9e9e"
245
-
246
- # Amenities chips (show up to 8)
247
- amenities_html = " ".join(
248
- f"<span style='background:#e8f5e9;color:#2e7d32;padding:2px 8px;"
249
- f"border-radius:12px;font-size:12px;margin:2px;display:inline-block;'>{a}</span>"
250
- for a in (amenities[:8] if amenities else [])
251
- )
252
-
253
- desc_short = description[:220] + ("..." if len(description) > 220 else "")
254
-
255
- card = f"""
256
- <div style='border:1px solid #e0e0e0;border-radius:12px;padding:20px;margin:12px 0;
257
- background:white;box-shadow:0 2px 8px rgba(0,0,0,0.08);display:flex;gap:20px;
258
- flex-wrap:wrap;'>
259
- <div style='flex-shrink:0;'>{img_html}</div>
260
- <div style='flex-grow:1;min-width:260px;'>
261
- <div style='display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;'>
262
- <div>
263
- <h3 style='margin:0 0 4px 0;color:#1a237e;'>#{idx} {name}</h3>
264
- <span style='color:#666;font-size:14px;'>{stars}</span>
265
- </div>
266
- <div style='text-align:right;'>
267
- <div style='font-size:24px;font-weight:bold;color:#1a237e;'>{price}</div>
268
- <div style='font-size:12px;color:#666;'>per night</div>
269
- </div>
270
- </div>
271
- <div style='margin:8px 0;'>
272
- <span style='background:{rating_color};color:white;padding:2px 10px;
273
- border-radius:4px;font-weight:bold;font-size:14px;'>{rating}</span>
274
- <span style='color:#666;font-size:13px;margin-left:8px;'>{reviews} reviews</span>
275
- </div>
276
- <p style='color:#555;font-size:14px;margin:8px 0;line-height:1.4;'>{desc_short}</p>
277
- <div style='margin:8px 0;'>{amenities_html}</div>
278
- <div style='margin-top:12px;'>
279
- <a href='{link}' target='_blank' rel='noopener noreferrer'
280
- style='background:#1a237e;color:white;padding:8px 20px;border-radius:6px;
281
- text-decoration:none;font-size:14px;font-weight:500;
282
- display:inline-block;'>Visit Hotel Website</a>
283
- </div>
284
- </div>
285
- </div>"""
286
- cards.append(card)
287
-
288
- return "<div style='font-family:Arial,sans-serif;'>" + "".join(cards) + "</div>"
289
-
290
-
291
- # ---------------------------------------------------------------------------
292
- # Parsed-parameters summary (shown above results)
293
- # ---------------------------------------------------------------------------
294
-
295
-
296
- def parsed_summary_html(parsed: dict, total: int) -> str:
297
- parts = [f"<strong>Found {total} hotel(s)</strong>"]
298
- if parsed["location"]:
299
- parts.append(f"Location: <em>{parsed['location']}</em>")
300
- if parsed["check_in"]:
301
- parts.append(f"Check-in: {parsed['check_in']}")
302
- if parsed["check_out"]:
303
- parts.append(f"Check-out: {parsed['check_out']}")
304
- if parsed["adults"] != 2:
305
- parts.append(f"Guests: {parsed['adults']}")
306
- if parsed["min_price"] and parsed["max_price"]:
307
- parts.append(f"Budget: ${parsed['min_price']}–${parsed['max_price']}/night")
308
- elif parsed["max_price"]:
309
- parts.append(f"Budget: up to ${parsed['max_price']}/night")
310
- elif parsed["min_price"]:
311
- parts.append(f"Budget: ${parsed['min_price']}+/night")
312
- if parsed["required_features"]:
313
- parts.append(f"Required: {', '.join(parsed['required_features'])}")
314
- if parsed["desired_features"]:
315
- parts.append(f"Preferred: {', '.join(parsed['desired_features'])}")
316
- if parsed["avoid_features"]:
317
- parts.append(f"Avoiding: {', '.join(parsed['avoid_features'])}")
318
-
319
- return (
320
- "<div style='background:#e3f2fd;padding:15px;border-radius:8px;margin-bottom:15px;"
321
- "line-height:1.8;'>" + " &nbsp;|&nbsp; ".join(parts) + "</div>"
322
- )
323
-
324
-
325
- # ---------------------------------------------------------------------------
326
- # Main search function
327
- # ---------------------------------------------------------------------------
328
-
329
-
330
- def search_hotels(user_input: str) -> str:
331
- """Parse user input, call SerpApi, and return formatted HTML results."""
332
-
333
- if not user_input or not user_input.strip():
334
- return (
335
- "<div style='text-align:center;padding:40px;'>"
336
- "<h3>Please enter a hotel description to search.</h3></div>"
337
- )
338
-
339
- key = os.environ.get("SERPAPI_KEY", "") or os.environ.get("SERPAPI_API_KEY", "")
340
- if not key:
341
- possible_paths = [
342
- os.path.join(os.getcwd(), "api_key.txt"),
343
- os.path.join(os.path.expanduser("~"), "Desktop", "Hotel App", "api_key.txt"),
344
- ]
345
- for key_file in possible_paths:
346
- if os.path.exists(key_file):
347
- with open(key_file) as f:
348
- key = f.read().strip()
349
- if key:
350
- break
351
- if not key:
352
- return (
353
- "<div style='text-align:center;padding:40px;'>"
354
- "<h3>SerpApi key required</h3>"
355
- "<p>Set <code>SERPAPI_KEY</code> as a Hugging Face Space secret, "
356
- "or place your key in <code>api_key.txt</code> for local testing.</p></div>"
357
- )
358
-
359
- try:
360
- parsed = parse_user_input(user_input)
361
-
362
- query = parsed["location"] if parsed["location"] else user_input.split(".")[0][:120]
363
-
364
- tomorrow = datetime.now() + timedelta(days=1)
365
- check_in = parsed["check_in"] or tomorrow.strftime("%Y-%m-%d")
366
- ci_dt = datetime.strptime(check_in, "%Y-%m-%d")
367
- if ci_dt.date() < datetime.now().date():
368
- ci_dt = tomorrow
369
- check_in = ci_dt.strftime("%Y-%m-%d")
370
- check_out = parsed["check_out"] or (ci_dt + timedelta(days=1)).strftime("%Y-%m-%d")
371
-
372
- params: dict = {
373
- "engine": "google_hotels",
374
- "q": query,
375
- "check_in_date": check_in,
376
- "check_out_date": check_out,
377
- "adults": parsed["adults"],
378
- "currency": "USD",
379
- "gl": "us",
380
- "hl": "en",
381
- "api_key": key,
382
- }
383
- if parsed["min_price"]:
384
- params["min_price"] = parsed["min_price"]
385
- if parsed["max_price"]:
386
- params["max_price"] = parsed["max_price"]
387
-
388
- if GoogleSearch is not None:
389
- search = GoogleSearch(params)
390
- results = search.get_dict()
391
- else:
392
- results = serpapi.search(params)
393
-
394
- if "error" in results:
395
- return (
396
- f"<div style='text-align:center;padding:40px;'>"
397
- f"<h3>SerpApi Error</h3><p>{results['error']}</p></div>"
398
- )
399
-
400
- properties = results.get("properties", [])
401
-
402
- filtered = [
403
- h for h in properties
404
- if not any(agency in get_hotel_link(h).lower() for agency in TRAVEL_AGENCY_DOMAINS)
405
- ]
406
- if len(filtered) < 3 and properties:
407
- filtered = properties
408
-
409
- summary = parsed_summary_html(parsed, len(filtered))
410
- return summary + format_hotel_results(filtered)
411
-
412
- except Exception as exc:
413
- return (
414
- f"<div style='text-align:center;padding:40px;'>"
415
- f"<h3>Something went wrong</h3>"
416
- f"<p>{exc}</p>"
417
- f"<p>Double-check your API key and try again.</p></div>"
418
- )
419
-
420
-
421
- # ---------------------------------------------------------------------------
422
- # Gradio UI
423
- # ---------------------------------------------------------------------------
424
-
425
- with gr.Blocks(title="AI Hotel Search") as app:
426
- gr.Markdown(
427
- "# AI Hotel Search\n"
428
- "### Find your perfect hotel using natural language\n"
429
- "Describe what you are looking for in plain English — include location, dates, "
430
- "budget, and any features you want or want to avoid."
431
- )
432
-
433
- user_input = gr.Textbox(
434
- label="Describe Your Ideal Hotel",
435
- placeholder=(
436
- "Example: I'm looking for a quiet, non-smoking hotel in downtown "
437
- "Austin, TX from March 15 to March 18, 2026. Budget under $200/night. "
438
- "Must have free parking and wifi. Would prefer a pool. Avoid hotels "
439
- "near highways."
440
- ),
441
- lines=5,
442
- )
443
- search_btn = gr.Button("Search Hotels", variant="primary", size="lg")
444
-
445
- with gr.Accordion("Search Tips & Examples", open=False):
446
- gr.Markdown(
447
- "**Good search examples:**\n\n"
448
- "- *\"Find me a hotel in San Francisco near Fisherman's Wharf, checking in "
449
- "April 5 and out April 8, 2026. Budget under $250/night. Must be non-smoking "
450
- "with free wifi. Would like ocean view.\"*\n"
451
- "- *\"Pet-friendly hotel in Nashville, TN for 2 adults from May 10-12, 2026. "
452
- "Price range $100-$180. Prefer boutique hotels. Avoid large chain hotels.\"*\n"
453
- "- *\"Luxury hotel in Miami Beach, March 20-23, 2026. $300-$500/night. Must "
454
- "have spa and pool. Prefer beachfront.\"*\n\n"
455
- "**Tips for best results:**\n\n"
456
- "- Always include a **location** (city, state, or landmark).\n"
457
- "- Specify **dates** in common formats (MM/DD/YYYY, Month Day Year, etc.).\n"
458
- "- Mention your **budget** with dollar amounts.\n"
459
- "- Distinguish between **must-have** and **nice-to-have** features.\n"
460
- "- State features to **avoid** explicitly.\n\n"
461
- "**Bad example (too vague):**\n\n"
462
- "- *\"Find me a nice hotel somewhere warm.\"* — no location, no dates, no budget."
463
- )
464
-
465
- results_output = gr.HTML(label="Search Results")
466
-
467
- search_btn.click(fn=search_hotels, inputs=[user_input], outputs=results_output)
468
- user_input.submit(fn=search_hotels, inputs=[user_input], outputs=results_output)
469
-
470
- if __name__ == "__main__":
471
- app.launch()