AlexanderStaniel commited on
Commit
148d6a9
·
verified ·
1 Parent(s): d07ff34

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +29 -214
app.py CHANGED
@@ -4,11 +4,8 @@ import sys
4
  import os
5
 
6
  # This block forces the installation of the browser Playwright needs.
7
- # It's a workaround for stubborn server environments.
8
  try:
9
  print("--- Ensuring Playwright browsers are installed ---")
10
- # We use 'playwright install chromium' to be specific and save space.
11
- # Using sys.executable ensures we use the python env's playwright.
12
  process = subprocess.run(
13
  [sys.executable, "-m", "playwright", "install", "chromium"],
14
  capture_output=True,
@@ -19,7 +16,6 @@ try:
19
  print("--- Playwright browsers installation check complete ---")
20
  except Exception as e:
21
  print(f"--- An error occurred during browser installation: {e} ---")
22
- # Even if it fails, we try to continue.
23
  pass
24
 
25
  # 🚀 1. IMPORTS & SETUP
@@ -45,7 +41,6 @@ ttl_cache = TTLCache(maxsize=100, ttl=3600)
45
  USER_AGENTS = [
46
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
47
  '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',
48
- 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
49
  ]
50
 
51
  # 📦 2. DATA MODEL
@@ -63,15 +58,14 @@ async def _extract_user_request(question: str) -> dict:
63
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
64
  parser = JsonOutputParser(pydantic_object=TravelRequest)
65
  prompt = ChatPromptTemplate.from_messages([
66
- ("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."),
67
  ("human", "{format_instructions}\n{request}")
68
  ]).partial(format_instructions=parser.get_format_instructions())
69
  logging.info(f"🔍 Extracting details from: {question}")
70
- # Default to a one-way trip if end_date isn't found
71
  extracted = await (prompt | llm | parser).ainvoke({"request": question})
72
- if not extracted.get("end_date"):
73
  start_dt = datetime.datetime.strptime(extracted["start_date"], "%Y-%m-%d")
74
- end_dt = start_dt + datetime.timedelta(days=7) # Default to 7 days later
75
  extracted["end_date"] = end_dt.strftime("%Y-%m-%d")
76
  return extracted
77
 
@@ -104,39 +98,24 @@ async def _search_Google Flights(details):
104
  ed = details["end_date"]
105
 
106
  url = f"https://www.google.com/travel/flights/search?tfs=CBwQAhokEgoyMDI1LTA4LTAxagwIAhIIL20vMDVxdGwyDAg"
107
-
108
  await page.goto(url, timeout=60000)
109
 
110
- try:
111
- await page.wait_for_selector('[data-testid="flight-offer"]', timeout=20000)
112
- cards = await page.query_selector_all('[data-testid="flight-offer"]')
113
-
114
- for card in cards[:3]:
115
- try:
116
- price_el = await card.query_selector('[data-testid="price-text"]')
117
- if price_el:
118
- price_text = await price_el.inner_text()
119
- price_clean = ''.join(filter(str.isdigit, price_text))
120
- price = float(price_clean) if price_clean else 0.0
121
-
122
- results.append({
123
- "source": "Google Flights",
124
- "type": "flight",
125
- "details": price_text.strip(),
126
- "price": price,
127
- "link": url
128
- })
129
- except Exception as e:
130
- logging.warning(f"Error parsing Google Flights card: {e}")
131
- continue
132
- except Exception as e:
133
- logging.warning(f"Google Flights selector timeout: {e}")
134
  await browser.close()
135
  except Exception as e:
136
  logging.error(f"Google Flights scraping failed: {e}")
137
  return results
138
 
139
- # ... (rest of the scraper functions remain the same)
140
  async def _search_kayak(details):
141
  results = []
142
  try:
@@ -145,199 +124,40 @@ async def _search_kayak(details):
145
  context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
146
  page = await context.new_page()
147
 
148
- await asyncio.sleep(random.uniform(1, 3))
149
-
150
  o = details["origin_city"].upper()
151
  d = details["destination_city"].upper()
152
  sd = details["start_date"]
153
  ed = details["end_date"]
154
 
155
  url = f"https://www.kayak.com/flights/{o}-{d}/{sd}/{ed}"
156
-
157
  await page.goto(url, timeout=60000)
158
 
159
- selectors_to_try = [".resultWrapper", "[data-testid='result-item']", ".Common-Booking-MultiBookProvider"]
160
-
161
- cards = []
162
- for selector in selectors_to_try:
163
- try:
164
- await page.wait_for_selector(selector, timeout=10000)
165
- cards = await page.query_selector_all(selector)
166
- if cards:
167
- break
168
- except:
169
- continue
170
 
171
  for card in cards[:3]:
172
- try:
173
- price_selectors = [".price-text", "[data-testid='price']", ".Common-Booking-MultiBookProvider-price"]
174
- price_text = None
175
- for price_sel in price_selectors:
176
- try:
177
- price_el = await card.query_selector(price_sel)
178
- if price_el:
179
- price_text = await price_el.inner_text()
180
- break
181
- except:
182
- continue
183
-
184
- if price_text:
185
- price_clean = ''.join(filter(str.isdigit, price_text))
186
- price = float(price_clean) if price_clean else 0.0
187
  results.append({
188
  "source": "Kayak", "type": "flight", "details": price_text.strip(),
189
- "price": price, "link": url
190
  })
191
- except Exception as e:
192
- logging.warning(f"Error parsing Kayak card: {e}")
193
- continue
194
  await browser.close()
195
  except Exception as e:
196
  logging.error(f"Kayak scraping failed: {e}")
197
  return results
198
 
199
- async def _search_trabber(details):
200
- results = []
201
- try:
202
- async with async_playwright() as pw:
203
- browser = await pw.chromium.launch(headless=True)
204
- context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
205
- page = await context.new_page()
206
- await asyncio.sleep(random.uniform(1, 3))
207
- o = details["origin_city"].upper()
208
- d = details["destination_city"].upper()
209
- sd_parts = details["start_date"].split('-')
210
- sd = f"{sd_parts[2]}{sd_parts[1]}{sd_parts[0][2:]}"
211
- url = f"https://www.trabber.ca/flights-from-{o}-to-{d}-on-{sd}"
212
- await page.goto(url, timeout=60000)
213
- try:
214
- await page.wait_for_selector("#results_list_det tr", timeout=20000)
215
- rows = await page.query_selector_all("#results_list_det tr")
216
- for row in rows[:3]:
217
- try:
218
- price_el = await row.query_selector("td.results_price a, .results_price")
219
- if price_el:
220
- price_text = await price_el.inner_text()
221
- price_clean = ''.join(filter(str.isdigit, price_text))
222
- price = float(price_clean) if price_clean else 0.0
223
- results.append({
224
- "source": "Trabber", "type": "flight", "details": price_text.strip(),
225
- "price": price, "link": url
226
- })
227
- except Exception as e:
228
- logging.warning(f"Error parsing Trabber row: {e}")
229
- continue
230
- except Exception as e:
231
- logging.warning(f"Trabber selector timeout: {e}")
232
- await browser.close()
233
- except Exception as e:
234
- logging.error(f"Trabber scraping failed: {e}")
235
- return results
236
-
237
- async def _search_travala(details):
238
- results = []
239
- try:
240
- async with async_playwright() as pw:
241
- browser = await pw.chromium.launch(headless=True)
242
- context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
243
- page = await context.new_page()
244
- await asyncio.sleep(random.uniform(1, 3))
245
- url = "https://www.travala.com/deals"
246
- await page.goto(url, timeout=60000)
247
- try:
248
- selectors = ["div[class*='DealCard_container__']", ".deal-card", "[data-testid='deal-card']"]
249
- cards = []
250
- for selector in selectors:
251
- try:
252
- await page.wait_for_selector(selector, timeout=10000)
253
- cards = await page.query_selector_all(selector)
254
- if cards:
255
- break
256
- except:
257
- continue
258
- for card in cards[:5]:
259
- try:
260
- title_selectors = ["p[class*='DealCard_title__']", ".deal-title", "h3, h4"]
261
- title = "Travala Deal"
262
- for title_sel in title_selectors:
263
- try:
264
- title_el = await card.query_selector(title_sel)
265
- if title_el:
266
- title = await title_el.inner_text()
267
- break
268
- except:
269
- continue
270
- results.append({
271
- "source": "Travala", "type": "deal", "details": title[:100], "price": 0, "link": url
272
- })
273
- except Exception as e:
274
- logging.warning(f"Error parsing Travala card: {e}")
275
- continue
276
- except Exception as e:
277
- logging.warning(f"Travala selector timeout: {e}")
278
- await browser.close()
279
- except Exception as e:
280
- logging.error(f"Travala scraping failed: {e}")
281
- return results
282
-
283
- async def _search_secretflying(details):
284
- results = []
285
- try:
286
- async with async_playwright() as pw:
287
- browser = await pw.chromium.launch(headless=True)
288
- context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
289
- page = await context.new_page()
290
- await asyncio.sleep(random.uniform(1, 3))
291
- url = "https://www.secretflying.com/canada-deals/"
292
- await page.goto(url, timeout=60000)
293
- try:
294
- selectors = [".post-item", "article", ".entry"]
295
- posts = []
296
- for selector in selectors:
297
- try:
298
- await page.wait_for_selector(selector, timeout=10000)
299
- posts = await page.query_selector_all(selector)
300
- if posts:
301
- break
302
- except:
303
- continue
304
- for post in posts[:5]:
305
- try:
306
- title_selectors = ["h2.entry-title a", ".entry-title a", "h3 a", "h2 a"]
307
- title = "Deal"
308
- link = url
309
- for title_sel in title_selectors:
310
- try:
311
- title_el = await post.query_selector(title_sel)
312
- if title_el:
313
- title = await title_el.inner_text()
314
- href = await title_el.get_attribute("href")
315
- if href:
316
- link = href if href.startswith('http') else f"https://www.secretflying.com{href}"
317
- break
318
- except:
319
- continue
320
- results.append({
321
- "source": "SecretFlying", "type": "deal", "details": title[:100], "price": 0, "link": link
322
- })
323
- except Exception as e:
324
- logging.warning(f"Error parsing SecretFlying post: {e}")
325
- continue
326
- except Exception as e:
327
- logging.warning(f"SecretFlying selector timeout: {e}")
328
- await browser.close()
329
- except Exception as e:
330
- logging.error(f"SecretFlying scraping failed: {e}")
331
- return results
332
 
333
  # 🔄 6. GATHER ALL DATA
334
  async def _gather_travel_data(req):
335
  tasks = [
336
  _cached_search(_search_Google Flights, req),
337
  _cached_search(_search_kayak, req),
338
- _cached_search(_search_trabber, req),
339
- _cached_search(_search_travala, req),
340
- _cached_search(_search_secretflying, req),
341
  ]
342
  results = await asyncio.gather(*tasks, return_exceptions=True)
343
  combined = []
@@ -348,6 +168,7 @@ async def _gather_travel_data(req):
348
  logging.error(f"A scraper task failed: {r}")
349
  return combined
350
 
 
351
  # ✍️ 7. FORMAT RESULTS
352
  def _format_response(req, data):
353
  dest = req["destination_city"]
@@ -357,19 +178,11 @@ def _format_response(req, data):
357
  md += "### ✈️ Flights\n"
358
  for item in flights[:8]:
359
  md += f"- **{item['source']}**: `{item['details']}` — **${item['price']:.2f}** ([Book]({item['link']}))\n"
360
-
361
- deals = [i for i in data if i["type"]=="deal"]
362
- if deals:
363
- md += "\n### 💸 Hot Deals\n"
364
- for item in deals[:8]:
365
- md += f"- **{item['source']}**: {item['details']} ([Link]({item['link']}))\n"
366
-
367
- if not flights and not deals:
368
  md += "No results found. The websites may be blocking our scrapers or there are no flights available for those dates.\n\n"
369
  md += "**Debug Info:**\n"
370
  md += f"- Searched for: {req['origin_city']} → {req['destination_city']}\n"
371
  md += f"- Dates: {req['start_date']} to {req['end_date']}\n"
372
- md += f"- Total scrapers attempted: 5\n"
373
 
374
  return md
375
 
@@ -380,11 +193,13 @@ async def ask_bot(question):
380
  return "❓ Tell me where you want to go!"
381
  try:
382
  req = await _extract_user_request(question)
 
 
383
  all_data = await _gather_travel_data(req)
384
  return _format_response(req, all_data)
385
  except Exception as e:
386
  logging.error(f"An error occurred in the main process: {e}")
387
- 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."
388
 
389
  iface = gr.Interface(
390
  fn=ask_bot,
 
4
  import os
5
 
6
  # This block forces the installation of the browser Playwright needs.
 
7
  try:
8
  print("--- Ensuring Playwright browsers are installed ---")
 
 
9
  process = subprocess.run(
10
  [sys.executable, "-m", "playwright", "install", "chromium"],
11
  capture_output=True,
 
16
  print("--- Playwright browsers installation check complete ---")
17
  except Exception as e:
18
  print(f"--- An error occurred during browser installation: {e} ---")
 
19
  pass
20
 
21
  # 🚀 1. IMPORTS & SETUP
 
41
  USER_AGENTS = [
42
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
43
  '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',
 
44
  ]
45
 
46
  # 📦 2. DATA MODEL
 
58
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
59
  parser = JsonOutputParser(pydantic_object=TravelRequest)
60
  prompt = ChatPromptTemplate.from_messages([
61
+ ("system", f"You are a travel assistant. Extract flight details from the user's request. Assume the current year is {datetime.datetime.now().year} unless specified otherwise. Today's date is {datetime.date.today().isoformat()}."),
62
  ("human", "{format_instructions}\n{request}")
63
  ]).partial(format_instructions=parser.get_format_instructions())
64
  logging.info(f"🔍 Extracting details from: {question}")
 
65
  extracted = await (prompt | llm | parser).ainvoke({"request": question})
66
+ if not extracted.get("end_date") and extracted.get("start_date"):
67
  start_dt = datetime.datetime.strptime(extracted["start_date"], "%Y-%m-%d")
68
+ end_dt = start_dt + datetime.timedelta(days=7) # Default to a 7-day trip
69
  extracted["end_date"] = end_dt.strftime("%Y-%m-%d")
70
  return extracted
71
 
 
98
  ed = details["end_date"]
99
 
100
  url = f"https://www.google.com/travel/flights/search?tfs=CBwQAhokEgoyMDI1LTA4LTAxagwIAhIIL20vMDVxdGwyDAg"
 
101
  await page.goto(url, timeout=60000)
102
 
103
+ await page.wait_for_selector('div[role="main"]', timeout=20000)
104
+ price_elements = await page.query_selector_all('span[aria-label*="dollars"]')
105
+
106
+ for elem in price_elements[:3]:
107
+ price_text = await elem.inner_text()
108
+ price_clean = ''.join(filter(str.isdigit, price_text))
109
+ if price_clean:
110
+ results.append({
111
+ "source": "Google Flights", "type": "flight", "details": price_text.strip(),
112
+ "price": float(price_clean) / 100, "link": url
113
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  await browser.close()
115
  except Exception as e:
116
  logging.error(f"Google Flights scraping failed: {e}")
117
  return results
118
 
 
119
  async def _search_kayak(details):
120
  results = []
121
  try:
 
124
  context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
125
  page = await context.new_page()
126
 
 
 
127
  o = details["origin_city"].upper()
128
  d = details["destination_city"].upper()
129
  sd = details["start_date"]
130
  ed = details["end_date"]
131
 
132
  url = f"https://www.kayak.com/flights/{o}-{d}/{sd}/{ed}"
 
133
  await page.goto(url, timeout=60000)
134
 
135
+ await page.wait_for_selector(".resultWrapper", timeout=20000)
136
+ cards = await page.query_selector_all(".resultWrapper")
 
 
 
 
 
 
 
 
 
137
 
138
  for card in cards[:3]:
139
+ price_el = await card.query_selector(".price-text")
140
+ if price_el:
141
+ price_text = await price_el.inner_text()
142
+ price_clean = ''.join(filter(str.isdigit, price_text))
143
+ if price_clean:
 
 
 
 
 
 
 
 
 
 
144
  results.append({
145
  "source": "Kayak", "type": "flight", "details": price_text.strip(),
146
+ "price": float(price_clean), "link": url
147
  })
 
 
 
148
  await browser.close()
149
  except Exception as e:
150
  logging.error(f"Kayak scraping failed: {e}")
151
  return results
152
 
153
+ # ... other scrapers ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
  # 🔄 6. GATHER ALL DATA
156
  async def _gather_travel_data(req):
157
  tasks = [
158
  _cached_search(_search_Google Flights, req),
159
  _cached_search(_search_kayak, req),
160
+ # Add other scrapers here as we build them
 
 
161
  ]
162
  results = await asyncio.gather(*tasks, return_exceptions=True)
163
  combined = []
 
168
  logging.error(f"A scraper task failed: {r}")
169
  return combined
170
 
171
+
172
  # ✍️ 7. FORMAT RESULTS
173
  def _format_response(req, data):
174
  dest = req["destination_city"]
 
178
  md += "### ✈️ Flights\n"
179
  for item in flights[:8]:
180
  md += f"- **{item['source']}**: `{item['details']}` — **${item['price']:.2f}** ([Book]({item['link']}))\n"
181
+ else:
 
 
 
 
 
 
 
182
  md += "No results found. The websites may be blocking our scrapers or there are no flights available for those dates.\n\n"
183
  md += "**Debug Info:**\n"
184
  md += f"- Searched for: {req['origin_city']} → {req['destination_city']}\n"
185
  md += f"- Dates: {req['start_date']} to {req['end_date']}\n"
 
186
 
187
  return md
188
 
 
193
  return "❓ Tell me where you want to go!"
194
  try:
195
  req = await _extract_user_request(question)
196
+ if not req.get('origin_city') or not req.get('destination_city') or not req.get('start_date'):
197
+ return "🤔 I couldn't understand all the details. Please provide at least a starting city, a destination, and a date."
198
  all_data = await _gather_travel_data(req)
199
  return _format_response(req, all_data)
200
  except Exception as e:
201
  logging.error(f"An error occurred in the main process: {e}")
202
+ return f"😵 Oh no! Something went wrong: {str(e)}\n\nPlease try asking in a different way."
203
 
204
  iface = gr.Interface(
205
  fn=ask_bot,