AlexanderStaniel commited on
Commit
1362809
·
verified ·
1 Parent(s): c00905f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +446 -61
app.py CHANGED
@@ -1,22 +1,52 @@
1
- # 🚀 0. BRUTE FORCE INSTALLER
2
  import subprocess
3
  import sys
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,
12
- text=True,
13
- check=True
14
- )
15
- print(process.stdout)
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
22
  import asyncio
@@ -27,7 +57,7 @@ import gradio as gr
27
  from cachetools import TTLCache
28
  from langchain_core.output_parsers import JsonOutputParser
29
  from langchain_core.prompts import ChatPromptTemplate
30
- from langchain_core.pydantic_v1 import BaseModel, Field
31
  from langchain_openai import ChatOpenAI
32
  from playwright.async_api import async_playwright
33
 
@@ -41,6 +71,7 @@ ttl_cache = TTLCache(maxsize=100, ttl=3600)
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
@@ -52,24 +83,23 @@ class TravelRequest(BaseModel):
52
  budget: int = Field(description="Budget in USD (informational only)")
53
  interests: list[str] = Field(description="List of travel interests")
54
 
55
-
56
  # ✨ 3. EXTRACT USER REQUEST
57
  async def _extract_user_request(question: str) -> dict:
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
 
72
-
73
  # 🛠 4. CACHE WRAPPER
74
  async def _cached_search(func, details):
75
  key = (func.__name__, details['origin_city'], details['destination_city'], details['start_date'])
@@ -81,36 +111,106 @@ async def _cached_search(func, details):
81
  ttl_cache[key] = res
82
  return res
83
 
84
-
85
  # 🌐 5. PLAYWRIGHT SCRAPER FUNCTIONS
86
 
87
- async def _search_Google Flights(details):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  results = []
89
  try:
90
  async with async_playwright() as pw:
91
- browser = await pw.chromium.launch(headless=True)
92
- context = await browser.new_context(user_agent=random.choice(USER_AGENTS))
93
- page = await context.new_page()
94
 
95
  o = details["origin_city"]
96
  d = details["destination_city"]
97
  sd = details["start_date"]
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}")
@@ -120,9 +220,9 @@ async def _search_kayak(details):
120
  results = []
121
  try:
122
  async with async_playwright() as pw:
123
- browser = await pw.chromium.launch(headless=True)
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()
@@ -130,34 +230,296 @@ async def _search_kayak(details):
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,46 +530,69 @@ async def _gather_travel_data(req):
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"]
175
  md = f"## 🌏 Travel Plan to **{dest}**\n\n"
 
176
  flights = sorted([i for i in data if i["type"]=="flight" and i.get("price", 0) > 0], key=lambda x: x["price"])
177
  if flights:
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
 
189
-
190
  # 💬 8. MAIN BOT & UI
191
  async def ask_bot(question):
192
  if not question:
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,
206
- 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"),
 
 
 
 
 
207
  outputs=gr.Markdown(label="🚀 Your Travel Plan"),
208
  title="🛫 High Flyer AI Bot",
209
- description="Your personal AI travel agent. Uses Playwright to scrape multiple sources in real-time."
 
 
 
 
 
 
210
  )
211
 
212
  if __name__ == "__main__":
213
- iface.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
1
+ # 🚀 0. IMPROVED BRUTE FORCE INSTALLER
2
  import subprocess
3
  import sys
4
  import os
5
 
6
+ def install_playwright_browsers():
7
+ """More robust Playwright browser installation"""
8
+ commands_to_try = [
9
+ # Try installing all browsers first
10
+ [sys.executable, "-m", "playwright", "install"],
11
+ # Then try just chromium
12
  [sys.executable, "-m", "playwright", "install", "chromium"],
13
+ # Try with --with-deps flag
14
+ [sys.executable, "-m", "playwright", "install", "--with-deps", "chromium"],
15
+ # Try installing webkit as backup
16
+ [sys.executable, "-m", "playwright", "install", "webkit"],
17
+ ]
18
+
19
+ for i, cmd in enumerate(commands_to_try):
20
+ try:
21
+ print(f"--- Attempt {i+1}: {' '.join(cmd)} ---")
22
+ process = subprocess.run(
23
+ cmd,
24
+ capture_output=True,
25
+ text=True,
26
+ timeout=300, # 5 minute timeout
27
+ check=True
28
+ )
29
+ print(process.stdout)
30
+ if process.stderr:
31
+ print("STDERR:", process.stderr)
32
+ print(f"--- Attempt {i+1} succeeded ---")
33
+ return True
34
+ except subprocess.TimeoutExpired:
35
+ print(f"--- Attempt {i+1} timed out ---")
36
+ continue
37
+ except Exception as e:
38
+ print(f"--- Attempt {i+1} failed: {e} ---")
39
+ continue
40
+
41
+ print("--- All installation attempts failed ---")
42
+ return False
43
+
44
+ # Run the installation
45
+ print("--- Starting Playwright browser installation ---")
46
+ success = install_playwright_browsers()
47
+ if not success:
48
+ print("--- WARNING: Browser installation failed, scrapers may not work ---")
49
+ print("--- Installation process complete ---")
50
 
51
  # 🚀 1. IMPORTS & SETUP
52
  import asyncio
 
57
  from cachetools import TTLCache
58
  from langchain_core.output_parsers import JsonOutputParser
59
  from langchain_core.prompts import ChatPromptTemplate
60
+ from pydantic import BaseModel, Field # Fixed: Using pydantic directly instead of pydantic_v1
61
  from langchain_openai import ChatOpenAI
62
  from playwright.async_api import async_playwright
63
 
 
71
  USER_AGENTS = [
72
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
73
  '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',
74
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
75
  ]
76
 
77
  # 📦 2. DATA MODEL
 
83
  budget: int = Field(description="Budget in USD (informational only)")
84
  interests: list[str] = Field(description="List of travel interests")
85
 
 
86
  # ✨ 3. EXTRACT USER REQUEST
87
  async def _extract_user_request(question: str) -> dict:
88
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
89
  parser = JsonOutputParser(pydantic_object=TravelRequest)
90
  prompt = ChatPromptTemplate.from_messages([
91
+ ("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."),
92
  ("human", "{format_instructions}\n{request}")
93
  ]).partial(format_instructions=parser.get_format_instructions())
94
  logging.info(f"🔍 Extracting details from: {question}")
95
+ # Default to a one-way trip if end_date isn't found
96
  extracted = await (prompt | llm | parser).ainvoke({"request": question})
97
+ if not extracted.get("end_date"):
98
  start_dt = datetime.datetime.strptime(extracted["start_date"], "%Y-%m-%d")
99
+ end_dt = start_dt + datetime.timedelta(days=7) # Default to 7 days later
100
  extracted["end_date"] = end_dt.strftime("%Y-%m-%d")
101
  return extracted
102
 
 
103
  # 🛠 4. CACHE WRAPPER
104
  async def _cached_search(func, details):
105
  key = (func.__name__, details['origin_city'], details['destination_city'], details['start_date'])
 
111
  ttl_cache[key] = res
112
  return res
113
 
 
114
  # 🌐 5. PLAYWRIGHT SCRAPER FUNCTIONS
115
 
116
+ async def _get_browser_and_page(pw):
117
+ """Helper function to get browser and page with fallback options"""
118
+ browser_types = [
119
+ ('webkit', pw.webkit),
120
+ ('chromium', pw.chromium),
121
+ ('firefox', pw.firefox)
122
+ ]
123
+
124
+ for browser_name, browser_type in browser_types:
125
+ try:
126
+ browser = await browser_type.launch(
127
+ headless=True,
128
+ args=['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
129
+ )
130
+ context = await browser.new_context(
131
+ user_agent=random.choice(USER_AGENTS),
132
+ viewport={'width': 1920, 'height': 1080}
133
+ )
134
+ page = await context.new_page()
135
+ logging.info(f"✅ Successfully launched {browser_name} browser")
136
+ return browser, page
137
+ except Exception as e:
138
+ logging.warning(f"❌ Failed to launch {browser_name}: {e}")
139
+ continue
140
+
141
+ raise Exception("❌ All browser types failed to launch")
142
+
143
+ async def _search_google_flights(details):
144
  results = []
145
  try:
146
  async with async_playwright() as pw:
147
+ browser, page = await _get_browser_and_page(pw)
 
 
148
 
149
  o = details["origin_city"]
150
  d = details["destination_city"]
151
  sd = details["start_date"]
152
  ed = details["end_date"]
153
 
154
+ # More robust Google Flights URL construction
155
+ base_url = "https://www.google.com/travel/flights"
156
+ url = f"{base_url}?q=flights+from+{o}+to+{d}+on+{sd}"
157
+
158
  await page.goto(url, timeout=60000)
159
+ await asyncio.sleep(3) # Wait for page load
160
+
161
+ # Multiple selectors to try
162
+ selectors_to_try = [
163
+ '[data-testid="flight-offer"]',
164
+ '.pIav2d',
165
+ '.yR1fYc',
166
+ '.JMc5Xc'
167
+ ]
168
+
169
+ cards = []
170
+ for selector in selectors_to_try:
171
+ try:
172
+ await page.wait_for_selector(selector, timeout=10000)
173
+ cards = await page.query_selector_all(selector)
174
+ if cards:
175
+ logging.info(f"✅ Found {len(cards)} Google Flights results with selector: {selector}")
176
+ break
177
+ except:
178
+ continue
179
+
180
+ for card in cards[:3]:
181
+ try:
182
+ price_selectors = [
183
+ '[data-testid="price-text"]',
184
+ '.YMlIz',
185
+ '.U3gSDe',
186
+ 'span[aria-label*="dollars"]'
187
+ ]
188
+
189
+ price_text = None
190
+ for price_sel in price_selectors:
191
+ try:
192
+ price_el = await card.query_selector(price_sel)
193
+ if price_el:
194
+ price_text = await price_el.inner_text()
195
+ break
196
+ except:
197
+ continue
198
+
199
+ if price_text:
200
+ price_clean = ''.join(filter(str.isdigit, price_text))
201
+ price = float(price_clean) if price_clean else 0.0
202
+
203
+ results.append({
204
+ "source": "Google Flights",
205
+ "type": "flight",
206
+ "details": price_text.strip(),
207
+ "price": price,
208
+ "link": url
209
+ })
210
+ except Exception as e:
211
+ logging.warning(f"Error parsing Google Flights card: {e}")
212
+ continue
213
 
 
 
 
 
 
 
 
 
 
 
 
214
  await browser.close()
215
  except Exception as e:
216
  logging.error(f"Google Flights scraping failed: {e}")
 
220
  results = []
221
  try:
222
  async with async_playwright() as pw:
223
+ browser, page = await _get_browser_and_page(pw)
224
+
225
+ await asyncio.sleep(random.uniform(1, 3))
226
 
227
  o = details["origin_city"].upper()
228
  d = details["destination_city"].upper()
 
230
  ed = details["end_date"]
231
 
232
  url = f"https://www.kayak.com/flights/{o}-{d}/{sd}/{ed}"
233
+
234
  await page.goto(url, timeout=60000)
235
+ await asyncio.sleep(5) # Wait for results to load
236
+
237
+ selectors_to_try = [
238
+ ".resultWrapper",
239
+ "[data-testid='result-item']",
240
+ ".Common-Booking-MultiBookProvider",
241
+ ".item.flights",
242
+ ".resultInner"
243
+ ]
244
 
245
+ cards = []
246
+ for selector in selectors_to_try:
247
+ try:
248
+ await page.wait_for_selector(selector, timeout=15000)
249
+ cards = await page.query_selector_all(selector)
250
+ if cards:
251
+ logging.info(f"✅ Found {len(cards)} Kayak results with selector: {selector}")
252
+ break
253
+ except:
254
+ continue
255
 
256
  for card in cards[:3]:
257
+ try:
258
+ price_selectors = [
259
+ ".price-text",
260
+ "[data-testid='price']",
261
+ ".Common-Booking-MultiBookProvider-price",
262
+ ".price",
263
+ ".f8F1-price-text"
264
+ ]
265
+
266
+ price_text = None
267
+ for price_sel in price_selectors:
268
+ try:
269
+ price_el = await card.query_selector(price_sel)
270
+ if price_el:
271
+ price_text = await price_el.inner_text()
272
+ break
273
+ except:
274
+ continue
275
+
276
+ if price_text:
277
+ price_clean = ''.join(filter(str.isdigit, price_text))
278
+ price = float(price_clean) if price_clean else 0.0
279
  results.append({
280
+ "source": "Kayak",
281
+ "type": "flight",
282
+ "details": price_text.strip(),
283
+ "price": price,
284
+ "link": url
285
  })
286
+ except Exception as e:
287
+ logging.warning(f"Error parsing Kayak card: {e}")
288
+ continue
289
+
290
  await browser.close()
291
  except Exception as e:
292
  logging.error(f"Kayak scraping failed: {e}")
293
  return results
294
 
295
+ async def _search_trabber(details):
296
+ results = []
297
+ try:
298
+ async with async_playwright() as pw:
299
+ browser, page = await _get_browser_and_page(pw)
300
+
301
+ await asyncio.sleep(random.uniform(1, 3))
302
+
303
+ o = details["origin_city"].upper()
304
+ d = details["destination_city"].upper()
305
+ sd_parts = details["start_date"].split('-')
306
+ sd = f"{sd_parts[2]}{sd_parts[1]}{sd_parts[0][2:]}"
307
+
308
+ url = f"https://www.trabber.ca/flights-from-{o}-to-{d}-on-{sd}"
309
+
310
+ await page.goto(url, timeout=60000)
311
+ await asyncio.sleep(3)
312
+
313
+ try:
314
+ selectors_to_try = [
315
+ "#results_list_det tr",
316
+ ".results_row",
317
+ "tr[class*='result']"
318
+ ]
319
+
320
+ rows = []
321
+ for selector in selectors_to_try:
322
+ try:
323
+ await page.wait_for_selector(selector, timeout=15000)
324
+ rows = await page.query_selector_all(selector)
325
+ if rows:
326
+ logging.info(f"✅ Found {len(rows)} Trabber results")
327
+ break
328
+ except:
329
+ continue
330
+
331
+ for row in rows[:3]:
332
+ try:
333
+ price_selectors = [
334
+ "td.results_price a",
335
+ ".results_price",
336
+ "td[class*='price']"
337
+ ]
338
+
339
+ price_text = None
340
+ for price_sel in price_selectors:
341
+ try:
342
+ price_el = await row.query_selector(price_sel)
343
+ if price_el:
344
+ price_text = await price_el.inner_text()
345
+ break
346
+ except:
347
+ continue
348
+
349
+ if price_text:
350
+ price_clean = ''.join(filter(str.isdigit, price_text))
351
+ price = float(price_clean) if price_clean else 0.0
352
+ results.append({
353
+ "source": "Trabber",
354
+ "type": "flight",
355
+ "details": price_text.strip(),
356
+ "price": price,
357
+ "link": url
358
+ })
359
+ except Exception as e:
360
+ logging.warning(f"Error parsing Trabber row: {e}")
361
+ continue
362
+ except Exception as e:
363
+ logging.warning(f"Trabber selector timeout: {e}")
364
+
365
+ await browser.close()
366
+ except Exception as e:
367
+ logging.error(f"Trabber scraping failed: {e}")
368
+ return results
369
+
370
+ async def _search_travala(details):
371
+ results = []
372
+ try:
373
+ async with async_playwright() as pw:
374
+ browser, page = await _get_browser_and_page(pw)
375
+
376
+ await asyncio.sleep(random.uniform(1, 3))
377
+
378
+ url = "https://www.travala.com/deals"
379
+
380
+ await page.goto(url, timeout=60000)
381
+ await asyncio.sleep(3)
382
+
383
+ try:
384
+ selectors = [
385
+ "div[class*='DealCard_container__']",
386
+ ".deal-card",
387
+ "[data-testid='deal-card']",
388
+ ".deal-item",
389
+ "[class*='deal']"
390
+ ]
391
+
392
+ cards = []
393
+ for selector in selectors:
394
+ try:
395
+ await page.wait_for_selector(selector, timeout=10000)
396
+ cards = await page.query_selector_all(selector)
397
+ if cards:
398
+ logging.info(f"✅ Found {len(cards)} Travala deals")
399
+ break
400
+ except:
401
+ continue
402
+
403
+ for card in cards[:5]:
404
+ try:
405
+ title_selectors = [
406
+ "p[class*='DealCard_title__']",
407
+ ".deal-title",
408
+ "h3, h4",
409
+ "[class*='title']"
410
+ ]
411
+
412
+ title = "Travala Deal"
413
+ for title_sel in title_selectors:
414
+ try:
415
+ title_el = await card.query_selector(title_sel)
416
+ if title_el:
417
+ title = await title_el.inner_text()
418
+ break
419
+ except:
420
+ continue
421
+
422
+ results.append({
423
+ "source": "Travala",
424
+ "type": "deal",
425
+ "details": title[:100],
426
+ "price": 0,
427
+ "link": url
428
+ })
429
+ except Exception as e:
430
+ logging.warning(f"Error parsing Travala card: {e}")
431
+ continue
432
+ except Exception as e:
433
+ logging.warning(f"Travala selector timeout: {e}")
434
+
435
+ await browser.close()
436
+ except Exception as e:
437
+ logging.error(f"Travala scraping failed: {e}")
438
+ return results
439
+
440
+ async def _search_secretflying(details):
441
+ results = []
442
+ try:
443
+ async with async_playwright() as pw:
444
+ browser, page = await _get_browser_and_page(pw)
445
+
446
+ await asyncio.sleep(random.uniform(1, 3))
447
+
448
+ url = "https://www.secretflying.com/canada-deals/"
449
+
450
+ await page.goto(url, timeout=60000)
451
+ await asyncio.sleep(3)
452
+
453
+ try:
454
+ selectors = [
455
+ ".post-item",
456
+ "article",
457
+ ".entry",
458
+ ".post",
459
+ "[class*='post']"
460
+ ]
461
+
462
+ posts = []
463
+ for selector in selectors:
464
+ try:
465
+ await page.wait_for_selector(selector, timeout=10000)
466
+ posts = await page.query_selector_all(selector)
467
+ if posts:
468
+ logging.info(f"✅ Found {len(posts)} SecretFlying deals")
469
+ break
470
+ except:
471
+ continue
472
+
473
+ for post in posts[:5]:
474
+ try:
475
+ title_selectors = [
476
+ "h2.entry-title a",
477
+ ".entry-title a",
478
+ "h3 a",
479
+ "h2 a",
480
+ ".title a"
481
+ ]
482
+
483
+ title = "Deal"
484
+ link = url
485
+ for title_sel in title_selectors:
486
+ try:
487
+ title_el = await post.query_selector(title_sel)
488
+ if title_el:
489
+ title = await title_el.inner_text()
490
+ href = await title_el.get_attribute("href")
491
+ if href:
492
+ link = href if href.startswith('http') else f"https://www.secretflying.com{href}"
493
+ break
494
+ except:
495
+ continue
496
+
497
+ results.append({
498
+ "source": "SecretFlying",
499
+ "type": "deal",
500
+ "details": title[:100],
501
+ "price": 0,
502
+ "link": link
503
+ })
504
+ except Exception as e:
505
+ logging.warning(f"Error parsing SecretFlying post: {e}")
506
+ continue
507
+ except Exception as e:
508
+ logging.warning(f"SecretFlying selector timeout: {e}")
509
+
510
+ await browser.close()
511
+ except Exception as e:
512
+ logging.error(f"SecretFlying scraping failed: {e}")
513
+ return results
514
 
515
  # 🔄 6. GATHER ALL DATA
516
  async def _gather_travel_data(req):
517
  tasks = [
518
+ _cached_search(_search_google_flights, req),
519
  _cached_search(_search_kayak, req),
520
+ _cached_search(_search_trabber, req),
521
+ _cached_search(_search_travala, req),
522
+ _cached_search(_search_secretflying, req),
523
  ]
524
  results = await asyncio.gather(*tasks, return_exceptions=True)
525
  combined = []
 
530
  logging.error(f"A scraper task failed: {r}")
531
  return combined
532
 
 
533
  # ✍️ 7. FORMAT RESULTS
534
  def _format_response(req, data):
535
  dest = req["destination_city"]
536
  md = f"## 🌏 Travel Plan to **{dest}**\n\n"
537
+
538
  flights = sorted([i for i in data if i["type"]=="flight" and i.get("price", 0) > 0], key=lambda x: x["price"])
539
  if flights:
540
  md += "### ✈️ Flights\n"
541
  for item in flights[:8]:
542
  md += f"- **{item['source']}**: `{item['details']}` — **${item['price']:.2f}** ([Book]({item['link']}))\n"
543
+
544
+ deals = [i for i in data if i["type"]=="deal"]
545
+ if deals:
546
+ md += "\n### 💸 Hot Deals\n"
547
+ for item in deals[:8]:
548
+ md += f"- **{item['source']}**: {item['details']} ([Link]({item['link']}))\n"
549
+
550
+ if not flights and not deals:
551
  md += "No results found. The websites may be blocking our scrapers or there are no flights available for those dates.\n\n"
552
  md += "**Debug Info:**\n"
553
  md += f"- Searched for: {req['origin_city']} → {req['destination_city']}\n"
554
  md += f"- Dates: {req['start_date']} to {req['end_date']}\n"
555
+ md += f"- Total scrapers attempted: 5\n"
556
+ md += f"- Browser installation status: {'✅ Success' if success else '❌ Failed'}\n"
557
 
558
  return md
559
 
 
560
  # 💬 8. MAIN BOT & UI
561
  async def ask_bot(question):
562
  if not question:
563
  return "❓ Tell me where you want to go!"
564
  try:
565
  req = await _extract_user_request(question)
 
 
566
  all_data = await _gather_travel_data(req)
567
  return _format_response(req, all_data)
568
  except Exception as e:
569
  logging.error(f"An error occurred in the main process: {e}")
570
+ 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."
571
 
572
+ # 🎨 9. GRADIO INTERFACE
573
  iface = gr.Interface(
574
  fn=ask_bot,
575
+ inputs=gr.Textbox(
576
+ lines=4,
577
+ label="🌍 Your dream trip?",
578
+ placeholder="e.g., I want to fly from Halifax to Toronto for a week starting August 1st",
579
+ info="Tell me where you want to go, when, and from where!"
580
+ ),
581
  outputs=gr.Markdown(label="🚀 Your Travel Plan"),
582
  title="🛫 High Flyer AI Bot",
583
+ description="Your personal AI travel agent. Uses multiple browsers to scrape flight prices and deals in real-time.",
584
+ theme=gr.themes.Soft(),
585
+ examples=[
586
+ ["I want to fly from Halifax to Tokyo tomorrow after 3 pm"],
587
+ ["Find me cheap flights from YHZ to London UK next month"],
588
+ ["I need a flight from Toronto to New York on December 15th, budget $500"]
589
+ ]
590
  )
591
 
592
  if __name__ == "__main__":
593
+ iface.launch(
594
+ server_name="0.0.0.0",
595
+ server_port=7860,
596
+ share=False,
597
+ show_error=True
598
+ )