AlexanderStaniel commited on
Commit
47d4698
·
verified ·
1 Parent(s): 7c589e6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -93
app.py CHANGED
@@ -3,16 +3,18 @@ import os
3
  import asyncio
4
  import json
5
  import datetime
6
- import requests # For making HTTP requests to external APIs like Perplexity AI
7
  from dotenv import load_dotenv
8
 
 
 
 
9
  # --- Langchain Imports ---
10
- # These libraries are essential for interacting with language models and parsing their outputs.
11
  from langchain_core.prompts import ChatPromptTemplate
12
  from langchain_core.output_parsers import JsonOutputParser
13
  from langchain_core.pydantic_v1 import BaseModel, Field
14
  from langchain_openai import ChatOpenAI
15
- from langchain_google_genai import ChatGoogleGenerativeAI # Kept for potential future use or redundancy
16
  from langchain_core.messages import SystemMessage, HumanMessage
17
 
18
  # Load environment variables from .env file (for local testing).
@@ -21,19 +23,12 @@ load_dotenv()
21
 
22
  # --- 1. API KEY CHECK ---
23
  # This block ensures that the necessary API keys are set before the application starts.
24
- # This makes the app "fail early" if a critical dependency is missing.
25
  try:
26
  openai_api_key = os.environ["OPENAI_API_KEY"]
27
  except KeyError:
28
- # If the key is not found, an error is raised, prompting the user to set it up.
29
  raise EnvironmentError("Missing OPENAI_API_KEY. Please set it in a .env file locally, or in Hugging Face Space secrets.")
30
 
31
- # Perplexity AI API Key Check
32
- perplexity_api_key = None # Initialize to None
33
- try:
34
- perplexity_api_key = os.environ["PERPLEXITY_API_KEY"]
35
- except KeyError:
36
- print("Warning: PERPLEXITY_API_KEY not found. Perplexity AI web search will not function. Please add it to your .env file or Hugging Face Space secrets for live web search.")
37
 
38
 
39
  # --- 2. PYDANTIC DATA STRUCTURE DEFINITION ---
@@ -79,95 +74,124 @@ async def search_get_your_guide(details):
79
 
80
  async def scrape_flight_vouchers(details):
81
  """
82
- This function currently simulates searching for flight vouchers and promo codes.
83
- You will implement real web crawling logic here using Playwright later.
84
- """
85
- print(f"Searching for flight vouchers related to: {details.get('destination_city', 'general travel')}")
86
- await asyncio.sleep(4) # Simulate a longer crawl time due to web scraping complexity
87
- return [{
88
- "source": "VoucherSiteExample",
89
- "type": "voucher",
90
- "details": "20% off selected Summer Flights!",
91
- "price": "N/A", # Vouchers typically don't have a direct price, but a discount amount/code
92
- "link": "https://www.example-vouchers.com/summer-deal"
93
- },
94
- {
95
- "source": "AirlineDeals",
96
- "type": "voucher",
97
- "details": "Flat $50 off on flights to Europe with code EUROFLY",
98
- "price": "N/A",
99
- "link": "https://www.airline-deals.com/europe-promo"
100
- }]
101
-
102
- async def search_perplexity_web(details):
103
  """
104
- Performs a live web search using Perplexity AI API based on the user's travel request.
105
- This demonstrates fetching real-time general information from the web.
106
- """
107
- if not perplexity_api_key:
108
- return [] # Return an empty list if API key is not set.
109
-
110
- search_query = (
111
- f"Best travel tips for {details.get('destination_city', 'general travel')}. "
112
- f"Budget: {details.get('budget_preference', 'any')}. "
113
- f"Travel dates: {details.get('departure_date', 'any')} to {details.get('return_date', 'any')}. "
114
- f"Interests: {details.get('activity_interests', 'any activities')}."
115
- )
116
-
117
- url = "https://api.perplexity.ai/chat/completions" # Perplexity AI's chat completions endpoint for web search
118
- headers = {
119
- "Authorization": f"Bearer {perplexity_api_key}",
120
- "Content-Type": "application/json"
121
- }
122
- payload = {
123
- "model": "llama-3-sonar-small-32k-online",
124
- "messages": [
125
- {"role": "system", "content": "You are a helpful assistant that performs web searches and provides concise, travel-related summaries or tips."},
126
- {"role": "user", "content": search_query}
127
- ],
128
- "temperature": 0.2,
129
- "max_tokens": 500
130
- }
131
 
132
- print(f"Searching Perplexity AI for: '{search_query}'")
133
  try:
134
- response = await asyncio.to_thread(requests.post, url, headers=headers, json=payload, timeout=20)
135
- response.raise_for_status()
136
- data = response.json()
137
-
138
- if data and data.get("choices") and data["choices"][0].get("message"):
139
- content = data["choices"][0]["message"]["content"]
140
- # Corrected to always return a list of dictionaries.
141
- return [{
142
- "source": "Perplexity AI Web Search",
143
- "type": "insight",
144
- "details": content,
145
- "link": "https://www.perplexity.ai/"
146
- }]
147
- else:
148
- print("Perplexity AI response was empty or malformed.")
149
- return [] # Returns an empty list
150
- except requests.exceptions.Timeout:
151
- print("Perplexity AI request timed out after 20 seconds.")
152
- # Corrected to always return a list of dictionaries.
153
- return [{"source": "Perplexity AI Web Search", "type": "insight", "details": "Web search timed out. Please try again.", "link": "https://www.perplexity.ai/"}]
154
- except requests.exceptions.RequestException as e:
155
- print(f"Error calling Perplexity AI: {e}")
156
- # Corrected to always return a list of dictionaries.
157
- return [{"source": "Perplexity AI Web Search", "type": "insight", "details": f"Failed to get web insights: {e}", "link": "https://www.perplexity.ai/"}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
 
160
  async def search_llm_redundancy(details, llm, llm_name):
161
  """
162
  Queries another LLM for additional, general travel ideas or insights.
163
- This function was the main cause of the TypeError, now fixed to return a list.
164
  """
165
  print(f"Querying {llm_name} for additional ideas...")
166
  prompt = f"""As a helpful travel assistant, provide some brief travel suggestions for a trip based on these details: {details}.
167
  Focus on general tips, hidden gems, or activity ideas. Do not suggest specific prices or flights."""
168
  messages = [SystemMessage(content=prompt), HumanMessage(content="What are your suggestions?")]
169
  response = await llm.ainvoke(messages)
170
- # FIX: Ensure this always returns a list of dictionaries, even if it's just one item.
171
  return [{"source": llm_name, "type": "insight", "details": response.content}]
172
 
173
  # --- 4. MAIN BOT LOGIC (ask_bot function) ---
@@ -202,9 +226,9 @@ async def ask_bot(question):
202
  search_expedia(structured_request), # Hotel search task (simulated)
203
  scrape_Google_Flights(structured_request), # Another flight source task (simulated scraping)
204
  search_get_your_guide(structured_request), # Activities search task (simulated)
205
- scrape_flight_vouchers(structured_request), # Flight voucher search task (simulated)
206
  search_llm_redundancy(structured_request, llm_openai_creative, "ChatGPT (Creative)"), # AI insights from OpenAI (LIVE)
207
- search_perplexity_web(structured_request) # Live Web Search from Perplexity AI (LIVE if key set)
208
  # search_llm_redundancy(structured_request, llm_gemini_search, "Gemini"), # Uncomment for Gemini insights
209
  ]
210
 
@@ -216,8 +240,8 @@ async def ask_bot(question):
216
  for res in results:
217
  if isinstance(res, Exception):
218
  print(f"A search task failed: {res}")
219
- elif res: # This 'elif res' checks if res is not None and not empty.
220
- all_results.extend(res) # res must be an iterable (like a list)
221
 
222
  flights = sorted([r for r in all_results if r['type'] == 'flight'], key=lambda x: x['price'])
223
  hotels = sorted([r for r in all_results if r['type'] == 'hotel'], key=lambda x: x['price'])
@@ -272,7 +296,7 @@ async def ask_bot(question):
272
  output_text += "_No specific flight vouchers or codes found at this time._\n"
273
  output_text += "\n"
274
 
275
- # Format AI Insights section (from redundant LLMs, now including Perplexity AI)
276
  output_text += "### 💡 Additional AI Insights (for Redundancy)\n"
277
  if insights:
278
  for insight in insights:
@@ -289,8 +313,8 @@ async def ask_bot(question):
289
  with gr.Blocks(theme=gr.themes.Soft()) as iface:
290
  gr.Markdown(
291
  """
292
- # ✨ The Ultimate Global Travel Planner Bot ✨
293
- Your smart AI assistant for finding the best flights, hotels, and activities worldwide!
294
  """
295
  )
296
  with gr.Row():
 
3
  import asyncio
4
  import json
5
  import datetime
6
+ import re # For regular expressions (used in voucher scraping)
7
  from dotenv import load_dotenv
8
 
9
+ # --- Playwright Imports for Web Scraping ---
10
+ from playwright.async_api import async_playwright
11
+
12
  # --- Langchain Imports ---
 
13
  from langchain_core.prompts import ChatPromptTemplate
14
  from langchain_core.output_parsers import JsonOutputParser
15
  from langchain_core.pydantic_v1 import BaseModel, Field
16
  from langchain_openai import ChatOpenAI
17
+ from langchain_google_genai import ChatGoogleGenerativeAI
18
  from langchain_core.messages import SystemMessage, HumanMessage
19
 
20
  # Load environment variables from .env file (for local testing).
 
23
 
24
  # --- 1. API KEY CHECK ---
25
  # This block ensures that the necessary API keys are set before the application starts.
 
26
  try:
27
  openai_api_key = os.environ["OPENAI_API_KEY"]
28
  except KeyError:
 
29
  raise EnvironmentError("Missing OPENAI_API_KEY. Please set it in a .env file locally, or in Hugging Face Space secrets.")
30
 
31
+ # Removed PERPLEXITY_API_KEY check as it's no longer used.
 
 
 
 
 
32
 
33
 
34
  # --- 2. PYDANTIC DATA STRUCTURE DEFINITION ---
 
74
 
75
  async def scrape_flight_vouchers(details):
76
  """
77
+ Performs real web scraping for flight vouchers and promo codes using Playwright.
78
+ This function navigates to common deal sites and attempts to extract voucher information.
79
+ Note: Web scraping is highly dependent on website structure and may require frequent updates.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  """
81
+ print(f"Starting real web scraping for flight vouchers related to: {details.get('destination_city', 'general travel')}")
82
+ vouchers_found = []
83
+
84
+ # Define target websites. Removed Groupon due to persistent ERR_HTTP2_PROTOCOL_ERROR.
85
+ target_urls = [
86
+ "https://www.retailmenot.com/coupons/flights",
87
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ browser = None # Initialize browser variable outside the try block
90
  try:
91
+ async with async_playwright() as p:
92
+ # Launch a headless Chromium browser (runs in the background without a visual window).
93
+ browser = await p.chromium.launch(headless=True)
94
+
95
+ # Add a human-like User-Agent to try and bypass basic bot detection.
96
+ context = await browser.new_context(
97
+ 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"
98
+ )
99
+ page = await context.new_page()
100
+
101
+ for url in target_urls:
102
+ try:
103
+ print(f"Navigating to: {url}")
104
+ # Use 'domcontentloaded' or 'load' as 'networkidle' can sometimes wait indefinitely.
105
+ await page.goto(url, wait_until='domcontentloaded', timeout=60000)
106
+
107
+ # --- Generic scraping strategy ---
108
+ deals_locator = page.locator('div:has-text("coupon code"), div:has-text("promo code"), div:has-text("discount"), '
109
+ 'a[href*="deal"], a[href*="promo"], a[href*="discount"], '
110
+ '[class*="deal"], [class*="promo"]')
111
+
112
+ deals = await deals_locator.all()
113
+
114
+ for i, deal_elem in enumerate(deals):
115
+ if len(vouchers_found) >= 3: # Limit the number of vouchers found
116
+ break
117
+ try:
118
+ text_content = await deal_elem.inner_text()
119
+ link = await deal_elem.get_attribute('href') if await deal_elem.get_attribute('href') else url
120
+
121
+ if len(text_content.strip()) > 20 and \
122
+ ("flight" in text_content.lower() or \
123
+ "travel" in text_content.lower() or \
124
+ details.get('destination_city', '').lower() in text_content.lower()):
125
+
126
+ code_match = re.search(r'\b[A-Z0-9]{4,10}\b', text_content)
127
+ code = code_match.group(0) if code_match else "N/A"
128
+
129
+ vouchers_found.append({
130
+ "source": f"Scraped from {url.split('/')[2]}",
131
+ "type": "voucher",
132
+ "details": f"{text_content[:150]}..." if len(text_content) > 150 else text_content,
133
+ "price": f"Code: {code}",
134
+ "link": link
135
+ })
136
+ except Exception as e_deal:
137
+ print(f"Error processing deal element on {url}: {e_deal}")
138
+ continue
139
+ except Exception as e_url_nav: # Catching general Exception for Playwright errors
140
+ print(f"Navigation error for {url}: {e_url_nav}")
141
+ vouchers_found.append({
142
+ "source": f"Scraper Error for {url.split('/')[2]}",
143
+ "type": "voucher",
144
+ "details": f"Could not retrieve vouchers from {url.split('/')[2]} due to navigation error. (Error: {e_url_nav})",
145
+ "price": "N/A",
146
+ "link": url
147
+ })
148
+ continue
149
+ except Exception as e_browser_launch: # Catching general Exception for browser launch errors
150
+ print(f"Playwright browser launch error: {e_browser_launch}. Ensure `playwright install` was run.")
151
+ return [{
152
+ "source": "Scraper Initialization Error",
153
+ "type": "voucher",
154
+ "details": f"Failed to start web scraper: {e_browser_launch}. Please ensure Playwright browsers are installed locally and on Hugging Face.",
155
+ "price": "N/A",
156
+ "link": "#"
157
+ }]
158
+ except Exception as e_general:
159
+ print(f"An unexpected error occurred during scraping: {e_general}")
160
+ return [{
161
+ "source": "Scraper General Error",
162
+ "type": "voucher",
163
+ "details": f"An unexpected error prevented voucher scraping: {e_general}",
164
+ "price": "N/A",
165
+ "link": "#"
166
+ }]
167
+ finally:
168
+ if browser:
169
+ await browser.close()
170
+
171
+ if not vouchers_found:
172
+ print("No specific vouchers found during scraping.")
173
+ return [{
174
+ "source": "Web Scraping",
175
+ "type": "voucher",
176
+ "details": "No specific flight vouchers or codes were found matching your criteria on the browsed sites.",
177
+ "price": "N/A",
178
+ "link": "#"
179
+ }]
180
+ return vouchers_found
181
+
182
+
183
+ # search_perplexity_web function is removed
184
 
185
 
186
  async def search_llm_redundancy(details, llm, llm_name):
187
  """
188
  Queries another LLM for additional, general travel ideas or insights.
 
189
  """
190
  print(f"Querying {llm_name} for additional ideas...")
191
  prompt = f"""As a helpful travel assistant, provide some brief travel suggestions for a trip based on these details: {details}.
192
  Focus on general tips, hidden gems, or activity ideas. Do not suggest specific prices or flights."""
193
  messages = [SystemMessage(content=prompt), HumanMessage(content="What are your suggestions?")]
194
  response = await llm.ainvoke(messages)
 
195
  return [{"source": llm_name, "type": "insight", "details": response.content}]
196
 
197
  # --- 4. MAIN BOT LOGIC (ask_bot function) ---
 
226
  search_expedia(structured_request), # Hotel search task (simulated)
227
  scrape_Google_Flights(structured_request), # Another flight source task (simulated scraping)
228
  search_get_your_guide(structured_request), # Activities search task (simulated)
229
+ scrape_flight_vouchers(structured_request), # NEW: Live Flight voucher search task (Playwright)
230
  search_llm_redundancy(structured_request, llm_openai_creative, "ChatGPT (Creative)"), # AI insights from OpenAI (LIVE)
231
+ # search_perplexity_web(structured_request) # Removed Perplexity AI task
232
  # search_llm_redundancy(structured_request, llm_gemini_search, "Gemini"), # Uncomment for Gemini insights
233
  ]
234
 
 
240
  for res in results:
241
  if isinstance(res, Exception):
242
  print(f"A search task failed: {res}")
243
+ elif res:
244
+ all_results.extend(res)
245
 
246
  flights = sorted([r for r in all_results if r['type'] == 'flight'], key=lambda x: x['price'])
247
  hotels = sorted([r for r in all_results if r['type'] == 'hotel'], key=lambda x: x['price'])
 
296
  output_text += "_No specific flight vouchers or codes found at this time._\n"
297
  output_text += "\n"
298
 
299
+ # Format AI Insights section (from redundant LLMs)
300
  output_text += "### 💡 Additional AI Insights (for Redundancy)\n"
301
  if insights:
302
  for insight in insights:
 
313
  with gr.Blocks(theme=gr.themes.Soft()) as iface:
314
  gr.Markdown(
315
  """
316
+ # ✨ The Ultimate Global Travel Planner Bot - Bazinga Edition
317
+ Your smart AI assistant for finding the best flights, hotels, and activities worldwide BAZINGA!
318
  """
319
  )
320
  with gr.Row():