AlexanderStaniel commited on
Commit
cdf3cfa
·
verified ·
1 Parent(s): d0268bb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +309 -96
app.py CHANGED
@@ -1,34 +1,35 @@
1
- # 1. IMPORTS & SETUP
2
  import asyncio
3
  import datetime
4
  import logging
5
  import os
 
6
  import gradio as gr
7
  from cachetools import TTLCache
8
  from langchain_core.output_parsers import JsonOutputParser
9
  from langchain_core.prompts import ChatPromptTemplate
10
  from langchain_core.pydantic_v1 import BaseModel, Field
11
  from langchain_openai import ChatOpenAI
 
12
 
13
- # Configure logging
14
- logging.basicConfig(
15
- level=logging.INFO,
16
- format='%(asctime)s - %(levelname)s - %(message)s'
17
- )
18
 
19
- # Simple cache
20
  ttl_cache = TTLCache(maxsize=100, ttl=3600)
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")
27
- end_date: str = Field(description="Trip end date")
28
- budget: int = Field(description="Budget in USD")
29
  interests: list[str] = Field(description="List of interests")
30
 
31
- # 3. EXTRACT REQUEST
 
32
  async def _extract_user_request(question: str) -> dict:
33
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
34
  parser = JsonOutputParser(pydantic_object=TravelRequest)
@@ -37,74 +38,292 @@ async def _extract_user_request(question: str) -> dict:
37
  ("human", "{format_instructions}\\n{request}")
38
  ]).partial(format_instructions=parser.get_format_instructions())
39
  date_str = datetime.date.today().isoformat()
40
- logging.info(f"Extracting: {question}")
41
  return await (prompt | llm | parser).ainvoke({
42
  "request": question,
43
  "current_date": date_str
44
  })
45
 
46
- # 4. SEARCH HELPERS
 
47
  async def _cached_search(func, details):
48
  key = (func.__name__, str(details))
49
  if key in ttl_cache:
50
- logging.info(f"Cache hit for {func.__name__}")
51
  return ttl_cache[key]
52
- logging.info(f"Cache miss for {func.__name__}")
53
  res = await func(details)
54
  ttl_cache[key] = res
55
  return res
56
 
57
- async def _search_skyscanner(details):
58
- try:
59
- logging.info(f"Skyscanner: {details['destination_city']}")
60
- await asyncio.sleep(1)
61
- return [{
62
- "source": "Skyscanner",
63
- "type": "flight",
64
- "details": f"Flight to {details['destination_city']}",
65
- "price": 800,
66
- "link": "https://skyscanner.com"
67
- }]
68
- except Exception as e:
69
- logging.error(e)
70
- return []
71
-
72
- async def _search_expedia(details):
73
- try:
74
- logging.info(f"Expedia: {details['destination_city']}")
75
- await asyncio.sleep(1)
76
- return [{
77
- "source": "Expedia",
78
- "type": "hotel",
79
- "details": f"Hotel in {details['destination_city']}",
80
- "price": 1200,
81
- "link": "https://expedia.com"
82
- }]
83
- except Exception as e:
84
- logging.error(e)
85
- return []
86
-
87
- async def _search_getyourguide(details):
88
- try:
89
- logging.info(f"GetYourGuide: {details['interests']}")
90
- await asyncio.sleep(1)
91
- return [{
92
- "source": "GetYourGuide",
93
- "type": "activity",
94
- "details": f"Tour: {details['interests']}",
95
- "price": 150,
96
- "link": "https://getyourguide.com"
97
- }]
98
- except Exception as e:
99
- logging.error(e)
100
- return []
101
-
102
- # 5. GATHER RESULTS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  async def _gather_travel_data(req):
104
  tasks = [
105
- _cached_search(_search_skyscanner, req),
106
- _cached_search(_search_expedia, req),
107
- _cached_search(_search_getyourguide, req)
 
 
 
 
 
 
 
108
  ]
109
  results = await asyncio.gather(*tasks, return_exceptions=True)
110
  combined = []
@@ -113,44 +332,38 @@ async def _gather_travel_data(req):
113
  combined.extend(r)
114
  return combined
115
 
116
- # 6. FORMAT OUTPUT
 
117
  def _format_response(req, data):
118
- dest = req['destination_city']
119
- text = f"## Travel Plan for {dest}\n\n"
120
- flights = [i for i in data if i['type']=='flight']
121
- hotels = [i for i in data if i['type']=='hotel']
122
- acts = [i for i in data if i['type']=='activity']
123
-
124
- if flights:
125
- text += "### Flights:\n"
126
- for f in flights:
127
- text += f"- {f['details']} (${f['price']}) [Book]({f['link']})\n"
128
-
129
- if hotels:
130
- text += "\n### Hotels:\n"
131
- for h in hotels:
132
- text += f"- {h['details']} (${h['price']}) [Book]({h['link']})\n"
133
-
134
- if acts:
135
- text += "\n### Activities:\n"
136
- for a in acts:
137
- text += f"- {a['details']} (${a['price']}) [Book]({a['link']})\n"
138
-
139
- return text
140
-
141
- # 7. MAIN & UI
142
  async def ask_bot(question):
143
  if not question:
144
- return "Tell me your trip!"
145
  req = await _extract_user_request(question)
146
- data = await _gather_travel_data(req)
147
- return _format_response(req, data)
148
 
149
  iface = gr.Interface(
150
  fn=ask_bot,
151
- inputs=gr.Textbox(lines=4, label="Your dream trip?", placeholder="E.g., a week in Tokyo..."),
152
- outputs=gr.Markdown(label="Your Travel Plan"),
153
- title="AI Travel Bot"
154
  )
155
 
156
  if __name__ == "__main__":
 
1
+ # 🚀 1. IMPORTS & SETUP
2
  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
10
  from langchain_core.prompts import ChatPromptTemplate
11
  from langchain_core.pydantic_v1 import BaseModel, Field
12
  from langchain_openai import ChatOpenAI
13
+ from playwright.async_api import async_playwright
14
 
15
+ # 🥳 Logging config
16
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
17
 
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
33
  async def _extract_user_request(question: str) -> dict:
34
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
35
  parser = JsonOutputParser(pydantic_object=TravelRequest)
 
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]
54
+ logging.info(f"▶️ Cache miss for {func.__name__}")
55
  res = await func(details)
56
  ttl_cache[key] = res
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
+ async def _search_secretflying(details):
199
+ results = []
200
+ async with async_playwright() as pw:
201
+ browser = await pw.chromium.launch()
202
+ page = await browser.new_page()
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"
210
+ href = await title_el.get_attribute("href") if title_el else url
211
+ results.append({
212
+ "source": "SecretFlying",
213
+ "type": "deal",
214
+ "details": p.strip(),
215
+ "price": 0,
216
+ "link": href
217
+ })
218
+ await browser.close()
219
+ return results
220
+
221
+ async def _search_thrifty(details):
222
+ results = []
223
+ async with async_playwright() as pw:
224
+ browser = await pw.chromium.launch()
225
+ page = await browser.new_page()
226
+ url = "https://thriftytraveler.com/flight-deals/"
227
+ await page.goto(url)
228
+ await page.wait_for_selector(".tt-deal-card", timeout=20000)
229
+ cards = await page.query_selector_all(".tt-deal-card")
230
+ for card in cards[:5]:
231
+ p_el = await card.query_selector(".tt-price")
232
+ title_el = await card.query_selector("h2.tt-card-title")
233
+ href = await card.query_selector("a")
234
+ p = await p_el.inner_text() if p_el else "0"
235
+ link = await href.get_attribute("href") if href else url
236
+ price = float(p.replace("$","").replace(",","")) if "$" in p else 0.0
237
+ results.append({
238
+ "source": "ThriftyTraveler",
239
+ "type": "deal",
240
+ "details": await title_el.inner_text(),
241
+ "price": price,
242
+ "link": link
243
+ })
244
+ await browser.close()
245
+ return results
246
+
247
+ async def _search_going(details):
248
+ results = []
249
+ async with async_playwright() as pw:
250
+ browser = await pw.chromium.launch()
251
+ page = await browser.new_page()
252
+ url = "https://going.com/"
253
+ await page.goto(url)
254
+ await page.wait_for_selector(".flight-alert-card", timeout=20000)
255
+ cards = await page.query_selector_all(".flight-alert-card")
256
+ for card in cards[:5]:
257
+ title_el = await card.query_selector(".alert-title")
258
+ link_el = await card.query_selector("a")
259
+ p = await title_el.inner_text() if title_el else "Deal"
260
+ href = await link_el.get_attribute("href") if link_el else url
261
+ results.append({
262
+ "source": "Going",
263
+ "type": "deal",
264
+ "details": p.strip(),
265
+ "price": 0,
266
+ "link": href
267
+ })
268
+ await browser.close()
269
+ return results
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]:
277
+ results.append({
278
+ "source": "DollarFlightClub",
279
+ "type": "deal",
280
+ "details": entry.title,
281
+ "price": 0,
282
+ "link": entry.link
283
+ })
284
+ return results
285
+
286
+ async def _search_edreams(details):
287
+ results = []
288
+ async with async_playwright() as pw:
289
+ browser = await pw.chromium.launch()
290
+ page = await browser.new_page()
291
+ url = "https://www.edreams.com/prime/flash-deals/"
292
+ await page.goto(url)
293
+ await page.wait_for_selector(".flash-deal", timeout=20000)
294
+ deals = await page.query_selector_all(".flash-deal")
295
+ for d in deals[:5]:
296
+ title_el = await d.query_selector(".flash-deal__title")
297
+ p_el = await d.query_selector(".flash-deal__price")
298
+ link_el = await d.query_selector("a")
299
+ title = await title_el.inner_text() if title_el else "Deal"
300
+ p = await p_el.inner_text() if p_el else "0"
301
+ href = await link_el.get_attribute("href") if link_el else url
302
+ price = float(p.replace("$","").replace(",","")) if "$" in p else 0.0
303
+ results.append({
304
+ "source": "eDreamsPrime",
305
+ "type": "deal",
306
+ "details": title.strip(),
307
+ "price": price,
308
+ "link": href
309
+ })
310
+ await browser.close()
311
+ return results
312
+
313
+
314
+ # 🔄 6. GATHER ALL DATA
315
  async def _gather_travel_data(req):
316
  tasks = [
317
+ _cached_search(_search_google_flights, req),
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),
325
+ _cached_search(_search_dollarclub, req),
326
+ _cached_search(_search_edreams, req),
327
  ]
328
  results = await asyncio.gather(*tasks, return_exceptions=True)
329
  combined = []
 
332
  combined.extend(r)
333
  return combined
334
 
335
+
336
+ # ✍️ 7. FORMAT RESULTS
337
  def _format_response(req, data):
338
+ dest = req["destination_city"]
339
+ md = f"## 🌏 Travel Plan to **{dest}**\n\n"
340
+
341
+ # Flights
342
+ md += "### ✈️ Flights & Deals\n"
343
+ for item in sorted([i for i in data if i["type"]=="flight"], key=lambda x: x["price"])[:8]:
344
+ md += f"- **{item['source']}**: {item['details']} — `${item['price']:.2f}` ([Book]({item['link']}))\n"
345
+
346
+ # Other deals
347
+ md += "\n### 💸 Hot Deals & Coupons\n"
348
+ for item in [i for i in data if i["type"]=="deal"][:8]:
349
+ md += f"- **{item['source']}**: {item['details']} ([Link]({item['link']}))\n"
350
+
351
+ return md
352
+
353
+
354
+ # 💬 8. MAIN BOT & UI
 
 
 
 
 
 
 
355
  async def ask_bot(question):
356
  if not question:
357
+ return "Tell me where you want to go!"
358
  req = await _extract_user_request(question)
359
+ all_data = await _gather_travel_data(req)
360
+ return _format_response(req, all_data)
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__":