| |
| import subprocess |
| import sys |
| import os |
|
|
| def install_playwright_browsers(): |
| """More robust Playwright browser installation""" |
| commands_to_try = [ |
| |
| [sys.executable, "-m", "playwright", "install"], |
| |
| [sys.executable, "-m", "playwright", "install", "chromium"], |
| |
| [sys.executable, "-m", "playwright", "install", "--with-deps", "chromium"], |
| |
| [sys.executable, "-m", "playwright", "install", "webkit"], |
| ] |
| |
| for i, cmd in enumerate(commands_to_try): |
| try: |
| print(f"--- Attempt {i+1}: {' '.join(cmd)} ---") |
| process = subprocess.run( |
| cmd, |
| capture_output=True, |
| text=True, |
| timeout=300, |
| check=True |
| ) |
| print(process.stdout) |
| if process.stderr: |
| print("STDERR:", process.stderr) |
| print(f"--- Attempt {i+1} succeeded ---") |
| return True |
| except subprocess.TimeoutExpired: |
| print(f"--- Attempt {i+1} timed out ---") |
| continue |
| except Exception as e: |
| print(f"--- Attempt {i+1} failed: {e} ---") |
| continue |
| |
| print("--- All installation attempts failed ---") |
| return False |
|
|
| |
| print("--- Starting Playwright browser installation ---") |
| success = install_playwright_browsers() |
| if not success: |
| print("--- WARNING: Browser installation failed, scrapers may not work ---") |
| print("--- Installation process complete ---") |
|
|
| |
| import asyncio |
| import datetime |
| import logging |
| import random |
| import gradio as gr |
| from cachetools import TTLCache |
| from langchain_core.output_parsers import JsonOutputParser |
| from langchain_core.prompts import ChatPromptTemplate |
| from pydantic import BaseModel, Field |
| from langchain_openai import ChatOpenAI |
| from playwright.async_api import async_playwright |
|
|
| |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
|
|
| |
| ttl_cache = TTLCache(maxsize=100, ttl=3600) |
|
|
| |
| USER_AGENTS = [ |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', |
| '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', |
| 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' |
| ] |
|
|
| |
| class TravelRequest(BaseModel): |
| origin_city: str = Field(description="Starting city or airport code (e.g., YHZ)") |
| destination_city: str = Field(description="Destination city or airport code (e.g., YYZ)") |
| start_date: str = Field(description="Trip start date in YYYY-MM-DD format") |
| end_date: str = Field(description="Trip end date in YYYY-MM-DD format") |
| budget: int = Field(description="Budget in USD (informational only)") |
| interests: list[str] = Field(description="List of travel interests") |
|
|
| |
| async def _extract_user_request(question: str) -> dict: |
| llm = ChatOpenAI(model="gpt-4o", temperature=0) |
| parser = JsonOutputParser(pydantic_object=TravelRequest) |
| prompt = ChatPromptTemplate.from_messages([ |
| ("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."), |
| ("human", "{format_instructions}\n{request}") |
| ]).partial(format_instructions=parser.get_format_instructions()) |
| logging.info(f"🔍 Extracting details from: {question}") |
| |
| extracted = await (prompt | llm | parser).ainvoke({"request": question}) |
| if not extracted.get("end_date"): |
| start_dt = datetime.datetime.strptime(extracted["start_date"], "%Y-%m-%d") |
| end_dt = start_dt + datetime.timedelta(days=7) |
| extracted["end_date"] = end_dt.strftime("%Y-%m-%d") |
| return extracted |
|
|
| |
| async def _cached_search(func, details): |
| key = (func.__name__, details['origin_city'], details['destination_city'], details['start_date']) |
| if key in ttl_cache: |
| logging.info(f"🔁 Cache hit for {func.__name__}") |
| return ttl_cache[key] |
| logging.info(f"▶️ Cache miss for {func.__name__}") |
| res = await func(details) |
| ttl_cache[key] = res |
| return res |
|
|
| |
|
|
| async def _get_browser_and_page(pw): |
| """Helper function to get browser and page with fallback options""" |
| browser_types = [ |
| ('webkit', pw.webkit), |
| ('chromium', pw.chromium), |
| ('firefox', pw.firefox) |
| ] |
| |
| for browser_name, browser_type in browser_types: |
| try: |
| browser = await browser_type.launch( |
| headless=True, |
| args=['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'] |
| ) |
| context = await browser.new_context( |
| user_agent=random.choice(USER_AGENTS), |
| viewport={'width': 1920, 'height': 1080} |
| ) |
| page = await context.new_page() |
| logging.info(f"✅ Successfully launched {browser_name} browser") |
| return browser, page |
| except Exception as e: |
| logging.warning(f"❌ Failed to launch {browser_name}: {e}") |
| continue |
| |
| raise Exception("❌ All browser types failed to launch") |
|
|
| async def _search_google_flights(details): |
| results = [] |
| try: |
| async with async_playwright() as pw: |
| browser, page = await _get_browser_and_page(pw) |
| |
| o = details["origin_city"] |
| d = details["destination_city"] |
| sd = details["start_date"] |
| ed = details["end_date"] |
| |
| |
| base_url = "https://www.google.com/travel/flights" |
| url = f"{base_url}?q=flights+from+{o}+to+{d}+on+{sd}" |
| |
| await page.goto(url, timeout=60000) |
| await asyncio.sleep(3) |
| |
| |
| selectors_to_try = [ |
| '[data-testid="flight-offer"]', |
| '.pIav2d', |
| '.yR1fYc', |
| '.JMc5Xc' |
| ] |
| |
| cards = [] |
| for selector in selectors_to_try: |
| try: |
| await page.wait_for_selector(selector, timeout=10000) |
| cards = await page.query_selector_all(selector) |
| if cards: |
| logging.info(f"✅ Found {len(cards)} Google Flights results with selector: {selector}") |
| break |
| except: |
| continue |
| |
| for card in cards[:3]: |
| try: |
| price_selectors = [ |
| '[data-testid="price-text"]', |
| '.YMlIz', |
| '.U3gSDe', |
| 'span[aria-label*="dollars"]' |
| ] |
| |
| price_text = None |
| for price_sel in price_selectors: |
| try: |
| price_el = await card.query_selector(price_sel) |
| if price_el: |
| price_text = await price_el.inner_text() |
| break |
| except: |
| continue |
| |
| if price_text: |
| price_clean = ''.join(filter(str.isdigit, price_text)) |
| price = float(price_clean) if price_clean else 0.0 |
| |
| results.append({ |
| "source": "Google Flights", |
| "type": "flight", |
| "details": price_text.strip(), |
| "price": price, |
| "link": url |
| }) |
| except Exception as e: |
| logging.warning(f"Error parsing Google Flights card: {e}") |
| continue |
| |
| await browser.close() |
| except Exception as e: |
| logging.error(f"Google Flights scraping failed: {e}") |
| return results |
|
|
| async def _search_kayak(details): |
| results = [] |
| try: |
| async with async_playwright() as pw: |
| browser, page = await _get_browser_and_page(pw) |
| |
| await asyncio.sleep(random.uniform(1, 3)) |
| |
| o = details["origin_city"].upper() |
| d = details["destination_city"].upper() |
| sd = details["start_date"] |
| ed = details["end_date"] |
| |
| url = f"https://www.kayak.com/flights/{o}-{d}/{sd}/{ed}" |
| |
| await page.goto(url, timeout=60000) |
| await asyncio.sleep(5) |
| |
| selectors_to_try = [ |
| ".resultWrapper", |
| "[data-testid='result-item']", |
| ".Common-Booking-MultiBookProvider", |
| ".item.flights", |
| ".resultInner" |
| ] |
| |
| cards = [] |
| for selector in selectors_to_try: |
| try: |
| await page.wait_for_selector(selector, timeout=15000) |
| cards = await page.query_selector_all(selector) |
| if cards: |
| logging.info(f"✅ Found {len(cards)} Kayak results with selector: {selector}") |
| break |
| except: |
| continue |
| |
| for card in cards[:3]: |
| try: |
| price_selectors = [ |
| ".price-text", |
| "[data-testid='price']", |
| ".Common-Booking-MultiBookProvider-price", |
| ".price", |
| ".f8F1-price-text" |
| ] |
| |
| price_text = None |
| for price_sel in price_selectors: |
| try: |
| price_el = await card.query_selector(price_sel) |
| if price_el: |
| price_text = await price_el.inner_text() |
| break |
| except: |
| continue |
| |
| if price_text: |
| price_clean = ''.join(filter(str.isdigit, price_text)) |
| price = float(price_clean) if price_clean else 0.0 |
| results.append({ |
| "source": "Kayak", |
| "type": "flight", |
| "details": price_text.strip(), |
| "price": price, |
| "link": url |
| }) |
| except Exception as e: |
| logging.warning(f"Error parsing Kayak card: {e}") |
| continue |
| |
| await browser.close() |
| except Exception as e: |
| logging.error(f"Kayak scraping failed: {e}") |
| return results |
|
|
| async def _search_trabber(details): |
| results = [] |
| try: |
| async with async_playwright() as pw: |
| browser, page = await _get_browser_and_page(pw) |
| |
| await asyncio.sleep(random.uniform(1, 3)) |
| |
| o = details["origin_city"].upper() |
| d = details["destination_city"].upper() |
| sd_parts = details["start_date"].split('-') |
| sd = f"{sd_parts[2]}{sd_parts[1]}{sd_parts[0][2:]}" |
| |
| url = f"https://www.trabber.ca/flights-from-{o}-to-{d}-on-{sd}" |
| |
| await page.goto(url, timeout=60000) |
| await asyncio.sleep(3) |
| |
| try: |
| selectors_to_try = [ |
| "#results_list_det tr", |
| ".results_row", |
| "tr[class*='result']" |
| ] |
| |
| rows = [] |
| for selector in selectors_to_try: |
| try: |
| await page.wait_for_selector(selector, timeout=15000) |
| rows = await page.query_selector_all(selector) |
| if rows: |
| logging.info(f"✅ Found {len(rows)} Trabber results") |
| break |
| except: |
| continue |
| |
| for row in rows[:3]: |
| try: |
| price_selectors = [ |
| "td.results_price a", |
| ".results_price", |
| "td[class*='price']" |
| ] |
| |
| price_text = None |
| for price_sel in price_selectors: |
| try: |
| price_el = await row.query_selector(price_sel) |
| if price_el: |
| price_text = await price_el.inner_text() |
| break |
| except: |
| continue |
| |
| if price_text: |
| price_clean = ''.join(filter(str.isdigit, price_text)) |
| price = float(price_clean) if price_clean else 0.0 |
| results.append({ |
| "source": "Trabber", |
| "type": "flight", |
| "details": price_text.strip(), |
| "price": price, |
| "link": url |
| }) |
| except Exception as e: |
| logging.warning(f"Error parsing Trabber row: {e}") |
| continue |
| except Exception as e: |
| logging.warning(f"Trabber selector timeout: {e}") |
| |
| await browser.close() |
| except Exception as e: |
| logging.error(f"Trabber scraping failed: {e}") |
| return results |
|
|
| async def _search_travala(details): |
| results = [] |
| try: |
| async with async_playwright() as pw: |
| browser, page = await _get_browser_and_page(pw) |
| |
| await asyncio.sleep(random.uniform(1, 3)) |
| |
| url = "https://www.travala.com/deals" |
| |
| await page.goto(url, timeout=60000) |
| await asyncio.sleep(3) |
| |
| try: |
| selectors = [ |
| "div[class*='DealCard_container__']", |
| ".deal-card", |
| "[data-testid='deal-card']", |
| ".deal-item", |
| "[class*='deal']" |
| ] |
| |
| cards = [] |
| for selector in selectors: |
| try: |
| await page.wait_for_selector(selector, timeout=10000) |
| cards = await page.query_selector_all(selector) |
| if cards: |
| logging.info(f"✅ Found {len(cards)} Travala deals") |
| break |
| except: |
| continue |
| |
| for card in cards[:5]: |
| try: |
| title_selectors = [ |
| "p[class*='DealCard_title__']", |
| ".deal-title", |
| "h3, h4", |
| "[class*='title']" |
| ] |
| |
| title = "Travala Deal" |
| for title_sel in title_selectors: |
| try: |
| title_el = await card.query_selector(title_sel) |
| if title_el: |
| title = await title_el.inner_text() |
| break |
| except: |
| continue |
| |
| results.append({ |
| "source": "Travala", |
| "type": "deal", |
| "details": title[:100], |
| "price": 0, |
| "link": url |
| }) |
| except Exception as e: |
| logging.warning(f"Error parsing Travala card: {e}") |
| continue |
| except Exception as e: |
| logging.warning(f"Travala selector timeout: {e}") |
| |
| await browser.close() |
| except Exception as e: |
| logging.error(f"Travala scraping failed: {e}") |
| return results |
|
|
| async def _search_secretflying(details): |
| results = [] |
| try: |
| async with async_playwright() as pw: |
| browser, page = await _get_browser_and_page(pw) |
| |
| await asyncio.sleep(random.uniform(1, 3)) |
| |
| url = "https://www.secretflying.com/canada-deals/" |
| |
| await page.goto(url, timeout=60000) |
| await asyncio.sleep(3) |
| |
| try: |
| selectors = [ |
| ".post-item", |
| "article", |
| ".entry", |
| ".post", |
| "[class*='post']" |
| ] |
| |
| posts = [] |
| for selector in selectors: |
| try: |
| await page.wait_for_selector(selector, timeout=10000) |
| posts = await page.query_selector_all(selector) |
| if posts: |
| logging.info(f"✅ Found {len(posts)} SecretFlying deals") |
| break |
| except: |
| continue |
| |
| for post in posts[:5]: |
| try: |
| title_selectors = [ |
| "h2.entry-title a", |
| ".entry-title a", |
| "h3 a", |
| "h2 a", |
| ".title a" |
| ] |
| |
| title = "Deal" |
| link = url |
| for title_sel in title_selectors: |
| try: |
| title_el = await post.query_selector(title_sel) |
| if title_el: |
| title = await title_el.inner_text() |
| href = await title_el.get_attribute("href") |
| if href: |
| link = href if href.startswith('http') else f"https://www.secretflying.com{href}" |
| break |
| except: |
| continue |
| |
| results.append({ |
| "source": "SecretFlying", |
| "type": "deal", |
| "details": title[:100], |
| "price": 0, |
| "link": link |
| }) |
| except Exception as e: |
| logging.warning(f"Error parsing SecretFlying post: {e}") |
| continue |
| except Exception as e: |
| logging.warning(f"SecretFlying selector timeout: {e}") |
| |
| await browser.close() |
| except Exception as e: |
| logging.error(f"SecretFlying scraping failed: {e}") |
| return results |
|
|
| |
| async def _gather_travel_data(req): |
| tasks = [ |
| _cached_search(_search_google_flights, req), |
| _cached_search(_search_kayak, req), |
| _cached_search(_search_trabber, req), |
| _cached_search(_search_travala, req), |
| _cached_search(_search_secretflying, req), |
| ] |
| results = await asyncio.gather(*tasks, return_exceptions=True) |
| combined = [] |
| for r in results: |
| if isinstance(r, list): |
| combined.extend(r) |
| else: |
| logging.error(f"A scraper task failed: {r}") |
| return combined |
|
|
| |
| def _format_response(req, data): |
| dest = req["destination_city"] |
| md = f"## 🌏 Travel Plan to **{dest}**\n\n" |
| |
| flights = sorted([i for i in data if i["type"]=="flight" and i.get("price", 0) > 0], key=lambda x: x["price"]) |
| if flights: |
| md += "### ✈️ Flights\n" |
| for item in flights[:8]: |
| md += f"- **{item['source']}**: `{item['details']}` — **${item['price']:.2f}** ([Book]({item['link']}))\n" |
| |
| deals = [i for i in data if i["type"]=="deal"] |
| if deals: |
| md += "\n### 💸 Hot Deals\n" |
| for item in deals[:8]: |
| md += f"- **{item['source']}**: {item['details']} ([Link]({item['link']}))\n" |
| |
| if not flights and not deals: |
| md += "No results found. The websites may be blocking our scrapers or there are no flights available for those dates.\n\n" |
| md += "**Debug Info:**\n" |
| md += f"- Searched for: {req['origin_city']} → {req['destination_city']}\n" |
| md += f"- Dates: {req['start_date']} to {req['end_date']}\n" |
| md += f"- Total scrapers attempted: 5\n" |
| md += f"- Browser installation status: {'✅ Success' if success else '❌ Failed'}\n" |
|
|
| return md |
|
|
| |
| async def ask_bot(question): |
| if not question: |
| return "❓ Tell me where you want to go!" |
| try: |
| req = await _extract_user_request(question) |
| all_data = await _gather_travel_data(req) |
| return _format_response(req, all_data) |
| except Exception as e: |
| logging.error(f"An error occurred in the main process: {e}") |
| 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." |
|
|
| |
| iface = gr.Interface( |
| fn=ask_bot, |
| 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", |
| info="Tell me where you want to go, when, and from where!" |
| ), |
| outputs=gr.Markdown(label="🚀 Your Travel Plan"), |
| title="🛫 High Flyer AI Bot", |
| description="Your personal AI travel agent. Uses multiple browsers to scrape flight prices and deals in real-time.", |
| theme=gr.themes.Soft(), |
| examples=[ |
| ["I want to fly from Halifax to Tokyo tomorrow after 3 pm"], |
| ["Find me cheap flights from YHZ to London UK next month"], |
| ["I need a flight from Toronto to New York on December 15th, budget $500"] |
| ] |
| ) |
|
|
| if __name__ == "__main__": |
| iface.launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| share=False, |
| show_error=True |
| ) |