Update app.py
Browse files
app.py
CHANGED
|
@@ -3,7 +3,7 @@ import asyncio
|
|
| 3 |
import datetime
|
| 4 |
import logging
|
| 5 |
import os
|
| 6 |
-
|
| 7 |
import gradio as gr
|
| 8 |
from cachetools import TTLCache
|
| 9 |
from langchain_core.output_parsers import JsonOutputParser
|
|
@@ -35,7 +35,7 @@ async def _extract_user_request(question: str) -> dict:
|
|
| 35 |
parser = JsonOutputParser(pydantic_object=TravelRequest)
|
| 36 |
prompt = ChatPromptTemplate.from_messages([
|
| 37 |
("system", "You are a travel assistant. Extract details."),
|
| 38 |
-
("human", "{format_instructions}\
|
| 39 |
]).partial(format_instructions=parser.get_format_instructions())
|
| 40 |
date_str = datetime.date.today().isoformat()
|
| 41 |
logging.info(f"🔍 Extracting: {question}")
|
|
@@ -59,7 +59,7 @@ async def _cached_search(func, details):
|
|
| 59 |
|
| 60 |
# 🌐 5. SCRAPER FUNCTIONS
|
| 61 |
|
| 62 |
-
async def
|
| 63 |
results = []
|
| 64 |
async with async_playwright() as pw:
|
| 65 |
browser = await pw.chromium.launch()
|
|
@@ -195,6 +195,57 @@ async def _search_skiplagged(details):
|
|
| 195 |
await browser.close()
|
| 196 |
return results
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
async def _search_secretflying(details):
|
| 199 |
results = []
|
| 200 |
async with async_playwright() as pw:
|
|
@@ -203,7 +254,7 @@ async def _search_secretflying(details):
|
|
| 203 |
url = "https://secretflying.com/"
|
| 204 |
await page.goto(url)
|
| 205 |
await page.wait_for_selector(".post-item", timeout=20000)
|
| 206 |
-
posts = await page.query_selector_all(".post-item")
|
| 207 |
for post in posts[:5]:
|
| 208 |
title_el = await post.query_selector("h2.entry-title a")
|
| 209 |
p = await title_el.inner_text() if title_el else "Deal"
|
|
@@ -270,7 +321,6 @@ async def _search_going(details):
|
|
| 270 |
|
| 271 |
async def _search_dollarclub(details):
|
| 272 |
# parse RSS feed
|
| 273 |
-
import feedparser
|
| 274 |
feed = feedparser.parse("https://dollarflightclub.com/feed/")
|
| 275 |
results = []
|
| 276 |
for entry in feed.entries[:5]:
|
|
@@ -314,11 +364,12 @@ async def _search_edreams(details):
|
|
| 314 |
# 🔄 6. GATHER ALL DATA
|
| 315 |
async def _gather_travel_data(req):
|
| 316 |
tasks = [
|
| 317 |
-
_cached_search(
|
| 318 |
_cached_search(_search_kayak, req),
|
| 319 |
_cached_search(_search_momondo, req),
|
| 320 |
_cached_search(_search_wego, req),
|
| 321 |
_cached_search(_search_skiplagged, req),
|
|
|
|
| 322 |
_cached_search(_search_secretflying, req),
|
| 323 |
_cached_search(_search_thrifty, req),
|
| 324 |
_cached_search(_search_going, req),
|
|
@@ -330,6 +381,8 @@ async def _gather_travel_data(req):
|
|
| 330 |
for r in results:
|
| 331 |
if not isinstance(r, Exception):
|
| 332 |
combined.extend(r)
|
|
|
|
|
|
|
| 333 |
return combined
|
| 334 |
|
| 335 |
|
|
@@ -339,14 +392,18 @@ def _format_response(req, data):
|
|
| 339 |
md = f"## 🌏 Travel Plan to **{dest}**\n\n"
|
| 340 |
|
| 341 |
# Flights
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
md +=
|
| 345 |
-
|
|
|
|
|
|
|
| 346 |
# Other deals
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
md +=
|
|
|
|
|
|
|
| 350 |
|
| 351 |
return md
|
| 352 |
|
|
@@ -355,16 +412,21 @@ def _format_response(req, data):
|
|
| 355 |
async def ask_bot(question):
|
| 356 |
if not question:
|
| 357 |
return "❓ Tell me where you want to go!"
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
iface = gr.Interface(
|
| 363 |
fn=ask_bot,
|
| 364 |
-
inputs=gr.Textbox(lines=4, label="🌍 Your dream trip?"),
|
| 365 |
outputs=gr.Markdown(label="🚀 Your Travel Plan"),
|
| 366 |
-
title="🛫 High Flyer AI Bot"
|
|
|
|
| 367 |
)
|
| 368 |
|
| 369 |
if __name__ == "__main__":
|
| 370 |
-
iface.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
| 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
|
|
|
|
| 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}")
|
|
|
|
| 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()
|
|
|
|
| 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:
|
|
|
|
| 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"
|
|
|
|
| 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]:
|
|
|
|
| 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),
|
|
|
|
| 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 |
|
|
|
|
| 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 |
|
|
|
|
| 412 |
async def ask_bot(question):
|
| 413 |
if not question:
|
| 414 |
return "❓ Tell me where you want to go!"
|
| 415 |
+
try:
|
| 416 |
+
req = await _extract_user_request(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__":
|
| 432 |
+
iface.launch(server_name="0.0.0.0", server_port=7860)
|