AlexanderStaniel commited on
Commit
2f4fa63
·
verified ·
1 Parent(s): d1dda4b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +371 -316
app.py CHANGED
@@ -3,7 +3,7 @@ import asyncio
3
  import datetime
4
  import logging
5
  import os
6
- import feedparser
7
  import gradio as gr
8
  from cachetools import TTLCache
9
  from langchain_core.output_parsers import JsonOutputParser
@@ -18,15 +18,21 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
18
  # 🧠 Simple in-memory cache (1h TTL)
19
  ttl_cache = TTLCache(maxsize=100, ttl=3600)
20
 
 
 
 
 
 
 
21
 
22
  # 📦 2. DATA MODEL
23
  class TravelRequest(BaseModel):
24
- origin_city: str = Field(description="Starting city or airport")
25
- destination_city: str = Field(description="Destination city or airport")
26
- start_date: str = Field(description="Trip start date YYYY-MM-DD")
27
- end_date: str = Field(description="Trip end date YYYY-MM-DD")
28
- budget: int = Field(description="Budget in USD (ignored by scrapers)")
29
- interests: list[str] = Field(description="List of interests")
30
 
31
 
32
  # ✨ 3. EXTRACT USER REQUEST
@@ -34,20 +40,16 @@ async def _extract_user_request(question: str) -> dict:
34
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
35
  parser = JsonOutputParser(pydantic_object=TravelRequest)
36
  prompt = ChatPromptTemplate.from_messages([
37
- ("system", "You are a travel assistant. Extract details."),
38
  ("human", "{format_instructions}\n{request}")
39
  ]).partial(format_instructions=parser.get_format_instructions())
40
- date_str = datetime.date.today().isoformat()
41
- logging.info(f"🔍 Extracting: {question}")
42
- return await (prompt | llm | parser).ainvoke({
43
- "request": question,
44
- "current_date": date_str
45
- })
46
 
47
 
48
  # 🛠 4. CACHE WRAPPER
49
  async def _cached_search(func, details):
50
- key = (func.__name__, str(details))
51
  if key in ttl_cache:
52
  logging.info(f"🔁 Cache hit for {func.__name__}")
53
  return ttl_cache[key]
@@ -57,332 +59,380 @@ async def _cached_search(func, details):
57
  return res
58
 
59
 
60
- # 🌐 5. SCRAPER FUNCTIONS
61
 
62
- async def _search_Google Flights(details):
63
  results = []
64
- async with async_playwright() as pw:
65
- browser = await pw.chromium.launch()
66
- page = await browser.new_page()
67
- origin = details["origin_city"].replace(" ", "+")
68
- dest = details["destination_city"].replace(" ", "+")
69
- dates = f"{details['start_date']}--{details['end_date']}"
70
- url = f"https://www.google.com/travel/flights?q={origin}.{dest}.{dates}"
71
- await page.goto(url)
72
- await page.wait_for_selector("div[role=main] .gws-flights-results__result", timeout=20000)
73
- cards = await page.query_selector_all("div[role=main] .gws-flights-results__result")
74
- for card in cards[:3]:
75
- price_el = await card.query_selector("div.gws-flights-results__price")
76
- link_el = await card.query_selector("a")
77
- p = await price_el.inner_text() if price_el else "$0"
78
- href = await link_el.get_attribute("href") if link_el else url
79
- price = float(p.replace("$","").replace(",","")) if "$" in p else 0.0
80
- results.append({
81
- "source": "Google Flights",
82
- "type": "flight",
83
- "details": p.strip(),
84
- "price": price,
85
- "link": href
86
- })
87
- await browser.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  return results
89
 
90
  async def _search_kayak(details):
91
  results = []
92
- async with async_playwright() as pw:
93
- browser = await pw.chromium.launch()
94
- page = await browser.new_page()
95
- origin = details["origin_city"].upper()
96
- dest = details["destination_city"].upper()
97
- url = f"https://www.kayak.com/flights/{origin}-{dest}/{details['start_date']}/{details['end_date']}"
98
- await page.goto(url)
99
- await page.wait_for_selector(".resultWrapper", timeout=20000)
100
- cards = await page.query_selector_all(".resultWrapper")
101
- for card in cards[:3]:
102
- p_el = await card.query_selector(".price")
103
- link_el = await card.query_selector("a.link")
104
- p = await p_el.inner_text() if p_el else "$0"
105
- href = (await link_el.get_attribute("href")) if link_el else url
106
- price = float(p.replace("$","").replace(",","")) if "$" in p else 0.0
107
- results.append({
108
- "source": "Kayak",
109
- "type": "flight",
110
- "details": p.strip(),
111
- "price": price,
112
- "link": f"https://www.kayak.com{href}"
113
- })
114
- await browser.close()
115
- return results
116
-
117
- async def _search_momondo(details):
118
- results = []
119
- async with async_playwright() as pw:
120
- browser = await pw.chromium.launch()
121
- page = await browser.new_page()
122
- origin = details["origin_city"].upper()
123
- dest = details["destination_city"].upper()
124
- url = f"https://www.momondo.com/flight-search/{origin}/{dest}/{details['start_date']}/{details['end_date']}"
125
- await page.goto(url)
126
- await page.wait_for_selector(".resultWrapper", timeout=20000)
127
- cards = await page.query_selector_all(".resultWrapper")
128
- for card in cards[:3]:
129
- p_el = await card.query_selector(".price-text")
130
- link_el = await card.query_selector("a.link")
131
- p = await p_el.inner_text() if p_el else "$0"
132
- href = (await link_el.get_attribute("href")) if link_el else url
133
- price = float(p.replace("$","").replace(",","")) if "$" in p else 0.0
134
- results.append({
135
- "source": "Momondo",
136
- "type": "flight",
137
- "details": p.strip(),
138
- "price": price,
139
- "link": f"https://www.momondo.com{href}"
140
- })
141
- await browser.close()
142
- return results
143
-
144
- async def _search_wego(details):
145
- results = []
146
- async with async_playwright() as pw:
147
- browser = await pw.chromium.launch()
148
- page = await browser.new_page()
149
- origin = details["origin_city"].replace(" ", "-")
150
- dest = details["destination_city"].replace(" ", "-")
151
- url = f"https://www.wego.com/flights/{origin}/{dest}/{details['start_date']}/{details['end_date']}"
152
- await page.goto(url)
153
- await page.wait_for_selector(".flight-card", timeout=20000)
154
- cards = await page.query_selector_all(".flight-card")
155
- for card in cards[:3]:
156
- p_el = await card.query_selector(".price")
157
- link_el = await card.query_selector("a")
158
- p = await p_el.inner_text() if p_el else "0"
159
- href = await link_el.get_attribute("href") if link_el else url
160
- price = float(p.replace("$","").replace(",","")) if "$" in p else 0.0
161
- results.append({
162
- "source": "Wego",
163
- "type": "flight",
164
- "details": p.strip(),
165
- "price": price,
166
- "link": href
167
- })
168
- await browser.close()
169
  return results
170
 
171
- async def _search_skiplagged(details):
172
  results = []
173
- async with async_playwright() as pw:
174
- browser = await pw.chromium.launch()
175
- page = await browser.new_page()
176
- origin = details["origin_city"].upper()
177
- dest = details["destination_city"].upper()
178
- url = f"https://skiplagged.com/flights/{origin}/{dest}/{details['start_date']}"
179
- await page.goto(url)
180
- await page.wait_for_selector(".result", timeout=20000)
181
- cards = await page.query_selector_all(".result")
182
- for card in cards[:3]:
183
- p_el = await card.query_selector(".price")
184
- link_el = await card.query_selector("a")
185
- p = await p_el.inner_text() if p_el else "0"
186
- href = await link_el.get_attribute("href") if link_el else url
187
- price = float(p.replace("$","").replace(",","")) if "$" in p else 0.0
188
- results.append({
189
- "source": "Skiplagged",
190
- "type": "flight",
191
- "details": p.strip(),
192
- "price": price,
193
- "link": href
194
- })
195
- await browser.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  return results
197
 
198
- # ✨✨ NEW SPY BOT! ✨✨
199
  async def _search_travala(details):
200
  results = []
201
- async with async_playwright() as pw:
202
- browser = await pw.chromium.launch()
203
- page = await browser.new_page()
204
- # Travala focuses on "activities" or "deals" so we'll search the main deals page
205
- # It's harder to search for specific flights, so we'll grab the top deals
206
- url = "https://www.travala.com/deals"
207
- await page.goto(url)
208
- try:
209
- await page.wait_for_selector("div[class*='DealCard_container__']", timeout=20000)
210
- cards = await page.query_selector_all("div[class*='DealCard_container__']")
211
- for card in cards[:5]: # Get top 5 deals
212
- title_el = await card.query_selector("p[class*='DealCard_title__']")
213
- price_el = await card.query_selector("span[class*='DealCard_price__']")
214
- link_el = await card.query_selector("a")
 
 
 
 
 
215
 
216
- title = await title_el.inner_text() if title_el else "Travala Deal"
217
- price_text = await price_el.inner_text() if price_el else "0"
218
- href = await link_el.get_attribute("href") if link_el else url
 
 
 
 
 
 
219
 
220
- # Clean up the price, which might be like "$50 OFF"
221
- price = 0.0
222
- if "OFF" in price_text:
223
- # It's a discount, so we can't get a final price easily. List as a deal.
224
- details_text = f"{title} ({price_text})"
225
- else:
226
- details_text = title
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
- results.append({
229
- "source": "Travala",
230
- "type": "deal",
231
- "details": details_text,
232
- "price": price, # Price is often not available directly
233
- "link": f"https://www.travala.com{href}"
234
- })
235
- except Exception as e:
236
- logging.error(f"Travala scraping failed: {e}")
237
- results.append({
238
- "source": "Travala",
239
- "type": "deal",
240
- "details": "Could not scrape Travala deals.",
241
- "price": 0,
242
- "link": url
243
- })
244
 
245
- await browser.close()
 
 
246
  return results
247
 
248
-
249
  async def _search_secretflying(details):
250
  results = []
251
- async with async_playwright() as pw:
252
- browser = await pw.chromium.launch()
253
- page = await browser.new_page()
254
- url = "https://secretflying.com/"
255
- await page.goto(url)
256
- await page.wait_for_selector(".post-item", timeout=20000)
257
- posts = await page.query_selector_all(".post-item")
258
- for post in posts[:5]:
259
- title_el = await post.query_selector("h2.entry-title a")
260
- p = await title_el.inner_text() if title_el else "Deal"
261
- href = await title_el.get_attribute("href") if title_el else url
262
- results.append({
263
- "source": "SecretFlying",
264
- "type": "deal",
265
- "details": p.strip(),
266
- "price": 0,
267
- "link": href
268
- })
269
- await browser.close()
270
- return results
271
-
272
- async def _search_thrifty(details):
273
- results = []
274
- async with async_playwright() as pw:
275
- browser = await pw.chromium.launch()
276
- page = await browser.new_page()
277
- url = "https://thriftytraveler.com/flight-deals/"
278
- await page.goto(url)
279
- await page.wait_for_selector(".tt-deal-card", timeout=20000)
280
- cards = await page.query_selector_all(".tt-deal-card")
281
- for card in cards[:5]:
282
- p_el = await card.query_selector(".tt-price")
283
- title_el = await card.query_selector("h2.tt-card-title")
284
- href = await card.query_selector("a")
285
- p = await p_el.inner_text() if p_el else "0"
286
- link = await href.get_attribute("href") if href else url
287
- price = float(p.replace("$","").replace(",","")) if "$" in p else 0.0
288
- results.append({
289
- "source": "ThriftyTraveler",
290
- "type": "deal",
291
- "details": await title_el.inner_text(),
292
- "price": price,
293
- "link": link
294
- })
295
- await browser.close()
296
- return results
297
-
298
- async def _search_going(details):
299
- results = []
300
- async with async_playwright() as pw:
301
- browser = await pw.chromium.launch()
302
- page = await browser.new_page()
303
- url = "https://going.com/"
304
- await page.goto(url)
305
- await page.wait_for_selector(".flight-alert-card", timeout=20000)
306
- cards = await page.query_selector_all(".flight-alert-card")
307
- for card in cards[:5]:
308
- title_el = await card.query_selector(".alert-title")
309
- link_el = await card.query_selector("a")
310
- p = await title_el.inner_text() if title_el else "Deal"
311
- href = await link_el.get_attribute("href") if link_el else url
312
- results.append({
313
- "source": "Going",
314
- "type": "deal",
315
- "details": p.strip(),
316
- "price": 0,
317
- "link": href
318
- })
319
- await browser.close()
320
- return results
321
-
322
- async def _search_dollarclub(details):
323
- # parse RSS feed
324
- feed = feedparser.parse("https://dollarflightclub.com/feed/")
325
- results = []
326
- for entry in feed.entries[:5]:
327
- results.append({
328
- "source": "DollarFlightClub",
329
- "type": "deal",
330
- "details": entry.title,
331
- "price": 0,
332
- "link": entry.link
333
- })
334
- return results
335
-
336
- async def _search_edreams(details):
337
- results = []
338
- async with async_playwright() as pw:
339
- browser = await pw.chromium.launch()
340
- page = await browser.new_page()
341
- url = "https://www.edreams.com/prime/flash-deals/"
342
- await page.goto(url)
343
- await page.wait_for_selector(".flash-deal", timeout=20000)
344
- deals = await page.query_selector_all(".flash-deal")
345
- for d in deals[:5]:
346
- title_el = await d.query_selector(".flash-deal__title")
347
- p_el = await d.query_selector(".flash-deal__price")
348
- link_el = await d.query_selector("a")
349
- title = await title_el.inner_text() if title_el else "Deal"
350
- p = await p_el.inner_text() if p_el else "0"
351
- href = await link_el.get_attribute("href") if link_el else url
352
- price = float(p.replace("$","").replace(",","")) if "$" in p else 0.0
353
- results.append({
354
- "source": "eDreamsPrime",
355
- "type": "deal",
356
- "details": title.strip(),
357
- "price": price,
358
- "link": href
359
- })
360
- await browser.close()
361
  return results
362
 
363
 
364
  # 🔄 6. GATHER ALL DATA
365
  async def _gather_travel_data(req):
366
  tasks = [
367
- _cached_search(_search_Google Flights, req),
368
  _cached_search(_search_kayak, req),
369
- _cached_search(_search_momondo, req),
370
- _cached_search(_search_wego, req),
371
- _cached_search(_search_skiplagged, req),
372
- _cached_search(_search_travala, req), # ✨ OUR NEW BOT IS ON THE TEAM!
373
  _cached_search(_search_secretflying, req),
374
- _cached_search(_search_thrifty, req),
375
- _cached_search(_search_going, req),
376
- _cached_search(_search_dollarclub, req),
377
- _cached_search(_search_edreams, req),
378
  ]
379
  results = await asyncio.gather(*tasks, return_exceptions=True)
380
  combined = []
381
  for r in results:
382
- if not isinstance(r, Exception):
383
  combined.extend(r)
384
  else:
385
- logging.error(f"A task failed: {r}")
386
  return combined
387
 
388
 
@@ -390,20 +440,25 @@ async def _gather_travel_data(req):
390
  def _format_response(req, data):
391
  dest = req["destination_city"]
392
  md = f"## 🌏 Travel Plan to **{dest}**\n\n"
393
-
394
- # Flights
395
- flights = sorted([i for i in data if i["type"]=="flight" and i["price"] > 0], key=lambda x: x["price"])
396
  if flights:
397
  md += "### ✈️ Flights\n"
398
  for item in flights[:8]:
399
- md += f"- **{item['source']}**: {item['details']} — `${item['price']:.2f}` ([Book]({item['link']}))\n"
400
 
401
- # Other deals
402
  deals = [i for i in data if i["type"]=="deal"]
403
  if deals:
404
- md += "\n### 💸 Hot Deals & Coupons\n"
405
  for item in deals[:8]:
406
  md += f"- **{item['source']}**: {item['details']} ([Link]({item['link']}))\n"
 
 
 
 
 
 
 
407
 
408
  return md
409
 
@@ -417,15 +472,15 @@ async def ask_bot(question):
417
  all_data = await _gather_travel_data(req)
418
  return _format_response(req, all_data)
419
  except Exception as e:
420
- logging.error(f"An error occurred in ask_bot: {e}")
421
- return "😵 Oh no! Something went wrong. I couldn't get the travel plans. Please try asking in a different way."
422
 
423
  iface = gr.Interface(
424
  fn=ask_bot,
425
- inputs=gr.Textbox(lines=4, label="🌍 Your dream trip? (e.g., 'I want to fly from New York to Paris for a week in September')"),
426
  outputs=gr.Markdown(label="🚀 Your Travel Plan"),
427
  title="🛫 High Flyer AI Bot",
428
- description="Your personal AI travel agent. Find the best flights and deals from over 10 sources in real-time."
429
  )
430
 
431
  if __name__ == "__main__":
 
3
  import datetime
4
  import logging
5
  import os
6
+ import random
7
  import gradio as gr
8
  from cachetools import TTLCache
9
  from langchain_core.output_parsers import JsonOutputParser
 
18
  # 🧠 Simple in-memory cache (1h TTL)
19
  ttl_cache = TTLCache(maxsize=100, ttl=3600)
20
 
21
+ # User agents for stealth
22
+ USER_AGENTS = [
23
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
24
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
25
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
26
+ ]
27
 
28
  # 📦 2. DATA MODEL
29
  class TravelRequest(BaseModel):
30
+ origin_city: str = Field(description="Starting city or airport code (e.g., YHZ)")
31
+ destination_city: str = Field(description="Destination city or airport code (e.g., YYZ)")
32
+ start_date: str = Field(description="Trip start date in YYYY-MM-DD format")
33
+ end_date: str = Field(description="Trip end date in YYYY-MM-DD format")
34
+ budget: int = Field(description="Budget in USD (informational only)")
35
+ interests: list[str] = Field(description="List of travel interests")
36
 
37
 
38
  # ✨ 3. EXTRACT USER REQUEST
 
40
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
41
  parser = JsonOutputParser(pydantic_object=TravelRequest)
42
  prompt = ChatPromptTemplate.from_messages([
43
+ ("system", "You are a travel assistant. Extract flight details from the user's request. Assume the current year is 2025 unless specified otherwise. Today's date is July 8, 2025."),
44
  ("human", "{format_instructions}\n{request}")
45
  ]).partial(format_instructions=parser.get_format_instructions())
46
+ logging.info(f"🔍 Extracting details from: {question}")
47
+ return await (prompt | llm | parser).ainvoke({"request": question})
 
 
 
 
48
 
49
 
50
  # 🛠 4. CACHE WRAPPER
51
  async def _cached_search(func, details):
52
+ key = (func.__name__, details['origin_city'], details['destination_city'], details['start_date'])
53
  if key in ttl_cache:
54
  logging.info(f"🔁 Cache hit for {func.__name__}")
55
  return ttl_cache[key]
 
59
  return res
60
 
61
 
62
+ # 🌐 5. PLAYWRIGHT SCRAPER FUNCTIONS
63
 
64
+ async def _search_google_flights(details):
65
  results = []
66
+ try:
67
+ async with async_playwright() as pw:
68
+ browser = await pw.chromium.launch(headless=True)
69
+ context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
70
+ page = await context.new_page()
71
+
72
+ # Add random delay
73
+ await asyncio.sleep(random.uniform(1, 3))
74
+
75
+ o = details["origin_city"]
76
+ d = details["destination_city"]
77
+ sd = details["start_date"]
78
+ ed = details["end_date"]
79
+
80
+ # More reliable Google Flights URL
81
+ url = f"https://www.google.com/travel/flights/search?tfs=CBwQAhokEgoyMDI1LTA4LTAxagwIAhIIL20vMDVxdGwyDAg"
82
+
83
+ await page.goto(url, timeout=60000)
84
+
85
+ # Wait for any flight results to load
86
+ try:
87
+ await page.wait_for_selector('[data-testid="flight-offer"]', timeout=20000)
88
+ cards = await page.query_selector_all('[data-testid="flight-offer"]')
89
+
90
+ for card in cards[:3]:
91
+ try:
92
+ price_el = await card.query_selector('[data-testid="price-text"]')
93
+ if not price_el:
94
+ price_el = await card.query_selector('span[aria-label*="dollars"]')
95
+
96
+ if price_el:
97
+ price_text = await price_el.inner_text()
98
+ price_clean = ''.join(filter(str.isdigit, price_text))
99
+ price = float(price_clean) if price_clean else 0.0
100
+
101
+ results.append({
102
+ "source": "Google Flights",
103
+ "type": "flight",
104
+ "details": price_text.strip(),
105
+ "price": price,
106
+ "link": url
107
+ })
108
+ except Exception as e:
109
+ logging.warning(f"Error parsing Google Flights card: {e}")
110
+ continue
111
+
112
+ except Exception as e:
113
+ logging.warning(f"Google Flights selector timeout: {e}")
114
+ # Fallback: try to find any price-like elements
115
+ price_elements = await page.query_selector_all('span[aria-label*="dollar"], span[aria-label*="price"]')
116
+ for elem in price_elements[:3]:
117
+ try:
118
+ text = await elem.inner_text()
119
+ if '$' in text:
120
+ results.append({
121
+ "source": "Google Flights",
122
+ "type": "flight",
123
+ "details": text.strip(),
124
+ "price": 0.0,
125
+ "link": url
126
+ })
127
+ except:
128
+ continue
129
+
130
+ await browser.close()
131
+
132
+ except Exception as e:
133
+ logging.error(f"Google Flights scraping failed: {e}")
134
+
135
  return results
136
 
137
  async def _search_kayak(details):
138
  results = []
139
+ try:
140
+ async with async_playwright() as pw:
141
+ browser = await pw.chromium.launch(headless=True)
142
+ context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
143
+ page = await context.new_page()
144
+
145
+ await asyncio.sleep(random.uniform(1, 3))
146
+
147
+ o = details["origin_city"].upper()
148
+ d = details["destination_city"].upper()
149
+ sd = details["start_date"]
150
+ ed = details["end_date"]
151
+
152
+ url = f"https://www.kayak.com/flights/{o}-{d}/{sd}/{ed}"
153
+
154
+ await page.goto(url, timeout=60000)
155
+
156
+ # Multiple selector attempts
157
+ selectors_to_try = [
158
+ ".resultWrapper",
159
+ "[data-testid='result-item']",
160
+ ".Common-Booking-MultiBookProvider",
161
+ ".result-column"
162
+ ]
163
+
164
+ cards = []
165
+ for selector in selectors_to_try:
166
+ try:
167
+ await page.wait_for_selector(selector, timeout=10000)
168
+ cards = await page.query_selector_all(selector)
169
+ if cards:
170
+ break
171
+ except:
172
+ continue
173
+
174
+ for card in cards[:3]:
175
+ try:
176
+ # Try multiple price selectors
177
+ price_selectors = [
178
+ ".price-text",
179
+ "[data-testid='price']",
180
+ ".Common-Booking-MultiBookProvider-price",
181
+ "span[aria-label*='price']"
182
+ ]
183
+
184
+ price_text = None
185
+ for price_sel in price_selectors:
186
+ try:
187
+ price_el = await card.query_selector(price_sel)
188
+ if price_el:
189
+ price_text = await price_el.inner_text()
190
+ break
191
+ except:
192
+ continue
193
+
194
+ if price_text:
195
+ price_clean = ''.join(filter(str.isdigit, price_text))
196
+ price = float(price_clean) if price_clean else 0.0
197
+
198
+ results.append({
199
+ "source": "Kayak",
200
+ "type": "flight",
201
+ "details": price_text.strip(),
202
+ "price": price,
203
+ "link": url
204
+ })
205
+
206
+ except Exception as e:
207
+ logging.warning(f"Error parsing Kayak card: {e}")
208
+ continue
209
+
210
+ await browser.close()
211
+
212
+ except Exception as e:
213
+ logging.error(f"Kayak scraping failed: {e}")
214
+
 
215
  return results
216
 
217
+ async def _search_trabber(details):
218
  results = []
219
+ try:
220
+ async with async_playwright() as pw:
221
+ browser = await pw.chromium.launch(headless=True)
222
+ context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
223
+ page = await context.new_page()
224
+
225
+ await asyncio.sleep(random.uniform(1, 3))
226
+
227
+ o = details["origin_city"].upper()
228
+ d = details["destination_city"].upper()
229
+ sd_parts = details["start_date"].split('-')
230
+ sd = f"{sd_parts[2]}{sd_parts[1]}{sd_parts[0][2:]}" # DDMMYY format
231
+
232
+ url = f"https://www.trabber.ca/flights-from-{o}-to-{d}-on-{sd}"
233
+
234
+ await page.goto(url, timeout=60000)
235
+
236
+ try:
237
+ await page.wait_for_selector("#results_list_det tr", timeout=20000)
238
+ rows = await page.query_selector_all("#results_list_det tr")
239
+
240
+ for row in rows[:3]:
241
+ try:
242
+ price_el = await row.query_selector("td.results_price a, .results_price")
243
+ if price_el:
244
+ price_text = await price_el.inner_text()
245
+ price_clean = ''.join(filter(str.isdigit, price_text))
246
+ price = float(price_clean) if price_clean else 0.0
247
+
248
+ results.append({
249
+ "source": "Trabber",
250
+ "type": "flight",
251
+ "details": price_text.strip(),
252
+ "price": price,
253
+ "link": url
254
+ })
255
+ except Exception as e:
256
+ logging.warning(f"Error parsing Trabber row: {e}")
257
+ continue
258
+
259
+ except Exception as e:
260
+ logging.warning(f"Trabber selector timeout: {e}")
261
+
262
+ await browser.close()
263
+
264
+ except Exception as e:
265
+ logging.error(f"Trabber scraping failed: {e}")
266
+
267
  return results
268
 
 
269
  async def _search_travala(details):
270
  results = []
271
+ try:
272
+ async with async_playwright() as pw:
273
+ browser = await pw.chromium.launch(headless=True)
274
+ context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
275
+ page = await context.new_page()
276
+
277
+ await asyncio.sleep(random.uniform(1, 3))
278
+
279
+ url = "https://www.travala.com/deals"
280
+
281
+ await page.goto(url, timeout=60000)
282
+
283
+ try:
284
+ # Try multiple selectors for deals
285
+ selectors = [
286
+ "div[class*='DealCard_container__']",
287
+ ".deal-card",
288
+ "[data-testid='deal-card']"
289
+ ]
290
 
291
+ cards = []
292
+ for selector in selectors:
293
+ try:
294
+ await page.wait_for_selector(selector, timeout=10000)
295
+ cards = await page.query_selector_all(selector)
296
+ if cards:
297
+ break
298
+ except:
299
+ continue
300
 
301
+ for card in cards[:5]:
302
+ try:
303
+ title_selectors = [
304
+ "p[class*='DealCard_title__']",
305
+ ".deal-title",
306
+ "h3, h4"
307
+ ]
308
+
309
+ title = "Travala Deal"
310
+ for title_sel in title_selectors:
311
+ try:
312
+ title_el = await card.query_selector(title_sel)
313
+ if title_el:
314
+ title = await title_el.inner_text()
315
+ break
316
+ except:
317
+ continue
318
+
319
+ results.append({
320
+ "source": "Travala",
321
+ "type": "deal",
322
+ "details": title[:100],
323
+ "price": 0,
324
+ "link": url
325
+ })
326
+
327
+ except Exception as e:
328
+ logging.warning(f"Error parsing Travala card: {e}")
329
+ continue
330
+
331
+ except Exception as e:
332
+ logging.warning(f"Travala selector timeout: {e}")
333
 
334
+ await browser.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
+ except Exception as e:
337
+ logging.error(f"Travala scraping failed: {e}")
338
+
339
  return results
340
 
 
341
  async def _search_secretflying(details):
342
  results = []
343
+ try:
344
+ async with async_playwright() as pw:
345
+ browser = await pw.chromium.launch(headless=True)
346
+ context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
347
+ page = await context.new_page()
348
+
349
+ await asyncio.sleep(random.uniform(1, 3))
350
+
351
+ url = "https://www.secretflying.com/canada-deals/"
352
+
353
+ await page.goto(url, timeout=60000)
354
+
355
+ try:
356
+ # Try multiple selectors for posts
357
+ selectors = [
358
+ ".post-item",
359
+ "article",
360
+ ".entry"
361
+ ]
362
+
363
+ posts = []
364
+ for selector in selectors:
365
+ try:
366
+ await page.wait_for_selector(selector, timeout=10000)
367
+ posts = await page.query_selector_all(selector)
368
+ if posts:
369
+ break
370
+ except:
371
+ continue
372
+
373
+ for post in posts[:5]:
374
+ try:
375
+ title_selectors = [
376
+ "h2.entry-title a",
377
+ ".entry-title a",
378
+ "h3 a",
379
+ "h2 a"
380
+ ]
381
+
382
+ title = "Deal"
383
+ link = url
384
+
385
+ for title_sel in title_selectors:
386
+ try:
387
+ title_el = await post.query_selector(title_sel)
388
+ if title_el:
389
+ title = await title_el.inner_text()
390
+ href = await title_el.get_attribute("href")
391
+ if href:
392
+ link = href if href.startswith('http') else f"https://www.secretflying.com{href}"
393
+ break
394
+ except:
395
+ continue
396
+
397
+ results.append({
398
+ "source": "SecretFlying",
399
+ "type": "deal",
400
+ "details": title[:100],
401
+ "price": 0,
402
+ "link": link
403
+ })
404
+
405
+ except Exception as e:
406
+ logging.warning(f"Error parsing SecretFlying post: {e}")
407
+ continue
408
+
409
+ except Exception as e:
410
+ logging.warning(f"SecretFlying selector timeout: {e}")
411
+
412
+ await browser.close()
413
+
414
+ except Exception as e:
415
+ logging.error(f"SecretFlying scraping failed: {e}")
416
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  return results
418
 
419
 
420
  # 🔄 6. GATHER ALL DATA
421
  async def _gather_travel_data(req):
422
  tasks = [
423
+ _cached_search(_search_google_flights, req),
424
  _cached_search(_search_kayak, req),
425
+ _cached_search(_search_trabber, req),
426
+ _cached_search(_search_travala, req),
 
 
427
  _cached_search(_search_secretflying, req),
 
 
 
 
428
  ]
429
  results = await asyncio.gather(*tasks, return_exceptions=True)
430
  combined = []
431
  for r in results:
432
+ if isinstance(r, list):
433
  combined.extend(r)
434
  else:
435
+ logging.error(f"A scraper task failed: {r}")
436
  return combined
437
 
438
 
 
440
  def _format_response(req, data):
441
  dest = req["destination_city"]
442
  md = f"## 🌏 Travel Plan to **{dest}**\n\n"
443
+
444
+ flights = sorted([i for i in data if i["type"]=="flight" and i.get("price", 0) > 0], key=lambda x: x["price"])
 
445
  if flights:
446
  md += "### ✈️ Flights\n"
447
  for item in flights[:8]:
448
+ md += f"- **{item['source']}**: `{item['details']}`**${item['price']:.2f}** ([Book]({item['link']}))\n"
449
 
 
450
  deals = [i for i in data if i["type"]=="deal"]
451
  if deals:
452
+ md += "\n### 💸 Hot Deals\n"
453
  for item in deals[:8]:
454
  md += f"- **{item['source']}**: {item['details']} ([Link]({item['link']}))\n"
455
+
456
+ if not flights and not deals:
457
+ md += "No results found. The websites may be blocking our scrapers or there are no flights available for those dates.\n\n"
458
+ md += "**Debug Info:**\n"
459
+ md += f"- Searched for: {req['origin_city']} → {req['destination_city']}\n"
460
+ md += f"- Dates: {req['start_date']} to {req['end_date']}\n"
461
+ md += f"- Total scrapers attempted: 5\n"
462
 
463
  return md
464
 
 
472
  all_data = await _gather_travel_data(req)
473
  return _format_response(req, all_data)
474
  except Exception as e:
475
+ logging.error(f"An error occurred in the main process: {e}")
476
+ return f"😵 Oh no! Something went wrong: {str(e)}\n\nPlease try asking in a different way or check if your OpenAI API key is set correctly."
477
 
478
  iface = gr.Interface(
479
  fn=ask_bot,
480
+ inputs=gr.Textbox(lines=4, label="🌍 Your dream trip?", placeholder="e.g., I want to fly from Halifax to Toronto for a week starting August 1st"),
481
  outputs=gr.Markdown(label="🚀 Your Travel Plan"),
482
  title="🛫 High Flyer AI Bot",
483
+ description="Your personal AI travel agent. Uses Playwright to scrape multiple sources in real-time."
484
  )
485
 
486
  if __name__ == "__main__":