AlexanderStaniel commited on
Commit
623c4be
·
verified ·
1 Parent(s): 7d62a23

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -109
app.py CHANGED
@@ -14,7 +14,7 @@ from playwright.async_api import async_playwright
14
  # --- Langchain Imports ---
15
  from langchain_core.prompts import ChatPromptTemplate
16
  from langchain_core.output_parsers import JsonOutputParser
17
- from langchain_core.pydantic_v1 import BaseModel, Field
18
  from langchain_openai import ChatOpenAI
19
  from langchain_google_genai import ChatGoogleGenerativeAI
20
  from langchain_core.messages import SystemMessage, HumanMessage
@@ -23,34 +23,37 @@ from langchain_core.messages import SystemMessage, HumanMessage
23
  # On Hugging Face Spaces, secrets are automatically available as environment variables.
24
  load_dotenv()
25
 
26
- # --- Playwright Browser Installation Workaround for Hugging Face Spaces ---
27
- # This block ensures Playwright browsers are installed when the app starts.
28
- # Assumes system dependencies are handled by packages.txt during the build.
29
- PLAYWRIGHT_BROWSERS_PATH = os.path.expanduser("~/.cache/ms-playwright")
30
- PLAYWRIGHT_INSTALLED_FLAG = "PLAYWRIGHT_BROWSERS_INSTALLED"
31
-
32
- # Check if installation has been attempted during this container's lifetime
33
- if not os.path.exists(os.path.join(PLAYWRIGHT_BROWSERS_PATH, "chromium")) and \
34
- os.environ.get(PLAYWRIGHT_INSTALLED_FLAG, "false") == "false":
35
- print("Playwright browsers not found. Attempting to install them now (this might take a moment)...")
36
  try:
37
- # Run the playwright install command for the browsers themselves.
38
- # This should now succeed if the system dependencies from packages.txt are met.
39
- result = subprocess.run([sys.executable, "-m", "playwright", "install"], check=True, capture_output=True, text=True)
40
- os.environ[PLAYWRIGHT_INSTALLED_FLAG] = "true" # Mark as installed for this session
41
- print("Playwright browsers installed successfully!")
 
42
  if result.stdout:
43
  print("Playwright Install STDOUT:\n", result.stdout)
44
  if result.stderr:
45
  print("Playwright Install STDERR:\n", result.stderr)
46
 
 
 
 
47
  except subprocess.CalledProcessError as e:
48
- print(f"Error during Playwright browser installation: {e}. STDOUT: {e.stdout.decode()} STDERR: {e.stderr.decode()}")
 
 
 
 
 
49
  except Exception as e:
50
  print(f"Unexpected error during Playwright browser installation: {e}")
51
- else:
52
- print("Playwright browsers already appear to be installed or installation skipped.")
53
 
 
 
54
 
55
  # --- 1. API KEY CHECK ---
56
  # This block ensures that the necessary API keys are set before the application starts.
@@ -59,9 +62,6 @@ try:
59
  except KeyError:
60
  raise EnvironmentError("Missing OPENAI_API_KEY. Please set it in a .env file locally, or in Hugging Face Space secrets.")
61
 
62
- # Removed PERPLEXITY_API_KEY check as it's no longer used.
63
-
64
-
65
  # --- 2. PYDANTIC DATA STRUCTURE DEFINITION ---
66
  class TravelRequest(BaseModel):
67
  departure_city: str = Field(description="The city or airport of departure. Infer if not specified.")
@@ -76,9 +76,6 @@ class TravelRequest(BaseModel):
76
  activity_interests: str = Field(description="Specific interests for activities (e.g., 'museums', 'hiking').")
77
 
78
  # --- 3. ASYNC SEARCH FUNCTIONS (Some Simulated, Some Live) ---
79
- # All these functions are designed to return a LIST of dictionaries,
80
- # even if only one dictionary is returned. This is crucial for `all_results.extend()`.
81
-
82
  async def search_skyscanner(details):
83
  """Simulates searching for flights on Skyscanner."""
84
  print(f"Searching Skyscanner for: {details['destination_city']}")
@@ -105,115 +102,107 @@ async def search_get_your_guide(details):
105
 
106
  async def scrape_flight_vouchers(details):
107
  """
108
- Performs real web scraping for flight vouchers and promo codes using Playwright.
109
- This function navigates to common deal sites and attempts to extract voucher information.
110
- Note: Web scraping is highly dependent on website structure and may require frequent updates.
111
  """
112
- print(f"Starting real web scraping for flight vouchers related to: {details.get('destination_city', 'general travel')}")
113
- vouchers_found = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
- # Define target websites. Removed Groupon due to persistent ERR_HTTP2_PROTOCOL_ERROR.
116
  target_urls = [
117
  "https://www.retailmenot.com/coupons/flights",
118
  ]
119
 
120
- browser = None # Initialize browser variable outside the try block
121
  try:
122
  async with async_playwright() as p:
123
- # Launch a headless Chromium browser (runs in the background without a visual window).
124
- browser = await p.chromium.launch(headless=True)
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- # Add a human-like User-Agent to try and bypass basic bot detection.
127
  context = await browser.new_context(
128
- user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
129
  )
130
  page = await context.new_page()
131
 
132
  for url in target_urls:
133
  try:
134
  print(f"Navigating to: {url}")
135
- # Use 'domcontentloaded' or 'load' as 'networkidle' can sometimes wait indefinitely.
136
- await page.goto(url, wait_until='domcontentloaded', timeout=60000)
137
-
138
- # --- Generic scraping strategy ---
139
- deals_locator = page.locator('div:has-text("coupon code"), div:has-text("promo code"), div:has-text("discount"), '
140
- 'a[href*="deal"], a[href*="promo"], a[href*="discount"], '
141
- '[class*="deal"], [class*="promo"]')
142
 
 
 
143
  deals = await deals_locator.all()
144
 
145
- for i, deal_elem in enumerate(deals):
146
- if len(vouchers_found) >= 3: # Limit the number of vouchers found
147
- break
148
  try:
149
  text_content = await deal_elem.inner_text()
150
- link = await deal_elem.get_attribute('href') if await deal_elem.get_attribute('href') else url
151
-
152
- if len(text_content.strip()) > 20 and \
153
- ("flight" in text_content.lower() or \
154
- "travel" in text_content.lower() or \
155
- details.get('destination_city', '').lower() in text_content.lower()):
156
-
157
  code_match = re.search(r'\b[A-Z0-9]{4,10}\b', text_content)
158
  code = code_match.group(0) if code_match else "N/A"
159
 
160
  vouchers_found.append({
161
  "source": f"Scraped from {url.split('/')[2]}",
162
  "type": "voucher",
163
- "details": f"{text_content[:150]}..." if len(text_content) > 150 else text_content,
164
  "price": f"Code: {code}",
165
- "link": link
166
  })
167
  except Exception as e_deal:
168
- print(f"Error processing deal element on {url}: {e_deal}")
169
  continue
170
- except Exception as e_url_nav: # Catching general Exception for Playwright errors
171
- print(f"Navigation error for {url}: {e_url_nav}")
172
- vouchers_found.append({
173
- "source": f"Scraper Error for {url.split('/')[2]}",
174
- "type": "voucher",
175
- "details": f"Could not retrieve vouchers from {url.split('/')[2]} due to navigation error. (Error: {e_url_nav})",
176
- "price": "N/A",
177
- "link": url
178
- })
179
  continue
180
- except Exception as e_browser_launch: # Catching general Exception for browser launch errors
181
- print(f"Playwright browser launch error: {e_browser_launch}. Ensure `playwright install` was run and system dependencies are met.")
182
- return [{
183
- "source": "Scraper Initialization Error",
184
- "type": "voucher",
185
- "details": f"Failed to start web scraper: {e_browser_launch}. Please ensure Playwright browsers are installed locally and on Hugging Face.",
186
- "price": "N/A",
187
- "link": "#"
188
- }]
189
- except Exception as e_general:
190
- print(f"An unexpected error occurred during scraping: {e_general}")
191
  return [{
192
- "source": "Scraper General Error",
193
  "type": "voucher",
194
- "details": f"An unexpected error prevented voucher scraping: {e_general}",
195
  "price": "N/A",
196
  "link": "#"
197
  }]
198
- finally:
199
- if browser:
200
- await browser.close()
201
 
202
  if not vouchers_found:
203
- print("No specific vouchers found during scraping.")
204
  return [{
205
- "source": "Web Scraping",
206
  "type": "voucher",
207
- "details": "No specific flight vouchers or codes were found matching your criteria on the browsed sites.",
208
- "price": "N/A",
209
- "link": "#"
210
  }]
 
211
  return vouchers_found
212
 
213
-
214
- # search_perplexity_web function is removed
215
-
216
-
217
  async def search_llm_redundancy(details, llm, llm_name):
218
  """
219
  Queries another LLM for additional, general travel ideas or insights.
@@ -222,15 +211,18 @@ async def search_llm_redundancy(details, llm, llm_name):
222
  prompt = f"""As a helpful travel assistant, provide some brief travel suggestions for a trip based on these details: {details}.
223
  Focus on general tips, hidden gems, or activity ideas. Do not suggest specific prices or flights."""
224
  messages = [SystemMessage(content=prompt), HumanMessage(content="What are your suggestions?")]
225
- response = await llm.ainvoke(messages)
226
- return [{"source": llm_name, "type": "insight", "details": response.content}]
 
 
 
 
 
227
 
228
  # --- 4. MAIN BOT LOGIC (ask_bot function) ---
229
  async def ask_bot(question):
230
  """
231
  This is the main ASYNCHRONOUS function for the bot.
232
- It orchestrates the extraction of user intent, concurrent searching,
233
- data processing, and final output formatting.
234
  """
235
  # Part 1: Extract structured data from the user's natural language request using an LLM.
236
  try:
@@ -248,19 +240,15 @@ async def ask_bot(question):
248
 
249
  # Part 2: Gather all search tasks to run concurrently.
250
  llm_openai_creative = ChatOpenAI(model="gpt-4o", temperature=0.7)
251
- # Uncomment the line below and ensure GOOGLE_API_KEY is set in your .env for Gemini redundancy.
252
- # llm_gemini_search = ChatGoogleGenerativeAI(model="gemini-1.5-flash", temperature=0.7)
253
 
254
  # Compile all the asynchronous search functions into a list of tasks.
255
  tasks = [
256
- search_skyscanner(structured_request), # Flight search task (simulated)
257
- search_expedia(structured_request), # Hotel search task (simulated)
258
- scrape_Google_Flights(structured_request), # Another flight source task (simulated scraping)
259
- search_get_your_guide(structured_request), # Activities search task (simulated)
260
- scrape_flight_vouchers(structured_request), # NEW: Live Flight voucher search task (Playwright)
261
- search_llm_redundancy(structured_request, llm_openai_creative, "ChatGPT (Creative)"), # AI insights from OpenAI (LIVE)
262
- # search_perplexity_web(structured_request) # Removed Perplexity AI task
263
- # search_llm_redundancy(structured_request, llm_gemini_search, "Gemini"), # Uncomment for Gemini insights
264
  ]
265
 
266
  # Part 3: Run all tasks concurrently and collect their results.
@@ -327,8 +315,8 @@ async def ask_bot(question):
327
  output_text += "_No specific flight vouchers or codes found at this time._\n"
328
  output_text += "\n"
329
 
330
- # Format AI Insights section (from redundant LLMs)
331
- output_text += "### 💡 Additional AI Insights (for Redundancy)\n"
332
  if insights:
333
  for insight in insights:
334
  output_text += f"* **{insight['source']} says:** {insight['details']}\n"
@@ -340,12 +328,17 @@ async def ask_bot(question):
340
 
341
  return output_text
342
 
 
 
 
 
 
343
  # --- 5. DEFINE THE GRADIO INTERFACE ---
344
  with gr.Blocks(theme=gr.themes.Soft()) as iface:
345
  gr.Markdown(
346
  """
347
  # ✨ The Ultimate Global Travel Planner Bot - Bazinga Edition ✨
348
- Your smart AI assistant for finding the best flights, hotels, and activities worldwide BAZINGA!
349
  """
350
  )
351
  with gr.Row():
@@ -364,8 +357,8 @@ with gr.Blocks(theme=gr.themes.Soft()) as iface:
364
  inputs=user_input
365
  )
366
 
367
- submit_button.click(fn=ask_bot, inputs=user_input, outputs=output_display)
368
 
369
  # --- 6. LAUNCH THE APP ---
370
  if __name__ == "__main__":
371
- iface.launch()
 
14
  # --- Langchain Imports ---
15
  from langchain_core.prompts import ChatPromptTemplate
16
  from langchain_core.output_parsers import JsonOutputParser
17
+ from pydantic import BaseModel, Field
18
  from langchain_openai import ChatOpenAI
19
  from langchain_google_genai import ChatGoogleGenerativeAI
20
  from langchain_core.messages import SystemMessage, HumanMessage
 
23
  # On Hugging Face Spaces, secrets are automatically available as environment variables.
24
  load_dotenv()
25
 
26
+ # --- Improved Playwright Browser Installation for Hugging Face Spaces ---
27
+ async def ensure_playwright_browsers():
28
+ """Ensures Playwright browsers are installed and available."""
 
 
 
 
 
 
 
29
  try:
30
+ # First, install Playwright browsers
31
+ print("Installing Playwright browsers...")
32
+ result = subprocess.run([
33
+ sys.executable, "-m", "playwright", "install", "chromium"
34
+ ], check=True, capture_output=True, text=True)
35
+
36
  if result.stdout:
37
  print("Playwright Install STDOUT:\n", result.stdout)
38
  if result.stderr:
39
  print("Playwright Install STDERR:\n", result.stderr)
40
 
41
+ print("Playwright browsers installed successfully!")
42
+ return True
43
+
44
  except subprocess.CalledProcessError as e:
45
+ print(f"Error during Playwright browser installation: {e}")
46
+ if e.stdout:
47
+ print("STDOUT:", e.stdout)
48
+ if e.stderr:
49
+ print("STDERR:", e.stderr)
50
+ return False
51
  except Exception as e:
52
  print(f"Unexpected error during Playwright browser installation: {e}")
53
+ return False
 
54
 
55
+ # Initialize browsers on startup
56
+ browsers_ready = False
57
 
58
  # --- 1. API KEY CHECK ---
59
  # This block ensures that the necessary API keys are set before the application starts.
 
62
  except KeyError:
63
  raise EnvironmentError("Missing OPENAI_API_KEY. Please set it in a .env file locally, or in Hugging Face Space secrets.")
64
 
 
 
 
65
  # --- 2. PYDANTIC DATA STRUCTURE DEFINITION ---
66
  class TravelRequest(BaseModel):
67
  departure_city: str = Field(description="The city or airport of departure. Infer if not specified.")
 
76
  activity_interests: str = Field(description="Specific interests for activities (e.g., 'museums', 'hiking').")
77
 
78
  # --- 3. ASYNC SEARCH FUNCTIONS (Some Simulated, Some Live) ---
 
 
 
79
  async def search_skyscanner(details):
80
  """Simulates searching for flights on Skyscanner."""
81
  print(f"Searching Skyscanner for: {details['destination_city']}")
 
102
 
103
  async def scrape_flight_vouchers(details):
104
  """
105
+ Performs web scraping for flight vouchers with better error handling for Hugging Face Spaces.
 
 
106
  """
107
+ global browsers_ready
108
+
109
+ print(f"Starting web scraping for flight vouchers related to: {details.get('destination_city', 'general travel')}")
110
+
111
+ # Check if browsers are ready, if not try to install them
112
+ if not browsers_ready:
113
+ browsers_ready = await ensure_playwright_browsers()
114
+
115
+ if not browsers_ready:
116
+ return [{
117
+ "source": "Web Scraping (Disabled)",
118
+ "type": "voucher",
119
+ "details": "Web scraping is temporarily disabled due to browser installation issues. This is common on Hugging Face Spaces. The app will still work with simulated data.",
120
+ "price": "N/A",
121
+ "link": "#"
122
+ }]
123
 
124
+ vouchers_found = []
125
  target_urls = [
126
  "https://www.retailmenot.com/coupons/flights",
127
  ]
128
 
 
129
  try:
130
  async with async_playwright() as p:
131
+ browser = await p.chromium.launch(
132
+ headless=True,
133
+ args=[
134
+ '--no-sandbox',
135
+ '--disable-setuid-sandbox',
136
+ '--disable-dev-shm-usage',
137
+ '--disable-accelerated-2d-canvas',
138
+ '--no-first-run',
139
+ '--no-zygote',
140
+ '--disable-gpu',
141
+ '--disable-web-security',
142
+ '--disable-features=VizDisplayCompositor'
143
+ ]
144
+ )
145
 
 
146
  context = await browser.new_context(
147
+ user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
148
  )
149
  page = await context.new_page()
150
 
151
  for url in target_urls:
152
  try:
153
  print(f"Navigating to: {url}")
154
+ await page.goto(url, wait_until='domcontentloaded', timeout=30000)
 
 
 
 
 
 
155
 
156
+ # Simple scraping strategy
157
+ deals_locator = page.locator('div:has-text("coupon"), div:has-text("promo"), div:has-text("discount")')
158
  deals = await deals_locator.all()
159
 
160
+ for i, deal_elem in enumerate(deals[:3]): # Limit to 3 deals
 
 
161
  try:
162
  text_content = await deal_elem.inner_text()
163
+ if len(text_content.strip()) > 20:
 
 
 
 
 
 
164
  code_match = re.search(r'\b[A-Z0-9]{4,10}\b', text_content)
165
  code = code_match.group(0) if code_match else "N/A"
166
 
167
  vouchers_found.append({
168
  "source": f"Scraped from {url.split('/')[2]}",
169
  "type": "voucher",
170
+ "details": f"{text_content[:100]}..." if len(text_content) > 100 else text_content,
171
  "price": f"Code: {code}",
172
+ "link": url
173
  })
174
  except Exception as e_deal:
175
+ print(f"Error processing deal element: {e_deal}")
176
  continue
177
+
178
+ except Exception as e_url:
179
+ print(f"Navigation error for {url}: {e_url}")
 
 
 
 
 
 
180
  continue
181
+
182
+ await browser.close()
183
+
184
+ except Exception as e:
185
+ print(f"Playwright error: {e}")
 
 
 
 
 
 
186
  return [{
187
+ "source": "Web Scraping (Error)",
188
  "type": "voucher",
189
+ "details": f"Web scraping encountered an error: {str(e)[:100]}... This is common on cloud platforms. Using simulated data instead.",
190
  "price": "N/A",
191
  "link": "#"
192
  }]
 
 
 
193
 
194
  if not vouchers_found:
195
+ # Return some simulated voucher data as fallback
196
  return [{
197
+ "source": "Travel Deals (Simulated)",
198
  "type": "voucher",
199
+ "details": "Get 10% off your next flight booking with major airlines - check airline websites directly for current promotions",
200
+ "price": "Code: SAVE10",
201
+ "link": "https://www.google.com/flights"
202
  }]
203
+
204
  return vouchers_found
205
 
 
 
 
 
206
  async def search_llm_redundancy(details, llm, llm_name):
207
  """
208
  Queries another LLM for additional, general travel ideas or insights.
 
211
  prompt = f"""As a helpful travel assistant, provide some brief travel suggestions for a trip based on these details: {details}.
212
  Focus on general tips, hidden gems, or activity ideas. Do not suggest specific prices or flights."""
213
  messages = [SystemMessage(content=prompt), HumanMessage(content="What are your suggestions?")]
214
+
215
+ try:
216
+ response = await llm.ainvoke(messages)
217
+ return [{"source": llm_name, "type": "insight", "details": response.content}]
218
+ except Exception as e:
219
+ print(f"Error querying {llm_name}: {e}")
220
+ return [{"source": f"{llm_name} (Error)", "type": "insight", "details": f"Unable to get insights from {llm_name} due to: {str(e)[:100]}"}]
221
 
222
  # --- 4. MAIN BOT LOGIC (ask_bot function) ---
223
  async def ask_bot(question):
224
  """
225
  This is the main ASYNCHRONOUS function for the bot.
 
 
226
  """
227
  # Part 1: Extract structured data from the user's natural language request using an LLM.
228
  try:
 
240
 
241
  # Part 2: Gather all search tasks to run concurrently.
242
  llm_openai_creative = ChatOpenAI(model="gpt-4o", temperature=0.7)
 
 
243
 
244
  # Compile all the asynchronous search functions into a list of tasks.
245
  tasks = [
246
+ search_skyscanner(structured_request),
247
+ search_expedia(structured_request),
248
+ scrape_Google_Flights(structured_request),
249
+ search_get_your_guide(structured_request),
250
+ scrape_flight_vouchers(structured_request),
251
+ search_llm_redundancy(structured_request, llm_openai_creative, "ChatGPT (Creative)"),
 
 
252
  ]
253
 
254
  # Part 3: Run all tasks concurrently and collect their results.
 
315
  output_text += "_No specific flight vouchers or codes found at this time._\n"
316
  output_text += "\n"
317
 
318
+ # Format AI Insights section
319
+ output_text += "### 💡 Additional AI Insights\n"
320
  if insights:
321
  for insight in insights:
322
  output_text += f"* **{insight['source']} says:** {insight['details']}\n"
 
328
 
329
  return output_text
330
 
331
+ # Wrapper function to handle the async call
332
+ def bot_wrapper(question):
333
+ """Synchronous wrapper for the async ask_bot function."""
334
+ return asyncio.run(ask_bot(question))
335
+
336
  # --- 5. DEFINE THE GRADIO INTERFACE ---
337
  with gr.Blocks(theme=gr.themes.Soft()) as iface:
338
  gr.Markdown(
339
  """
340
  # ✨ The Ultimate Global Travel Planner Bot - Bazinga Edition ✨
341
+ Your smart AI assistant for finding the best flights, hotels, and activities worldwide!
342
  """
343
  )
344
  with gr.Row():
 
357
  inputs=user_input
358
  )
359
 
360
+ submit_button.click(fn=bot_wrapper, inputs=user_input, outputs=output_display)
361
 
362
  # --- 6. LAUNCH THE APP ---
363
  if __name__ == "__main__":
364
+ iface.launch()