AlexanderStaniel commited on
Commit
7c589e6
·
verified ·
1 Parent(s): 43bd1bc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +149 -57
app.py CHANGED
@@ -3,29 +3,40 @@ import os
3
  import asyncio
4
  import json
5
  import datetime
 
6
  from dotenv import load_dotenv
7
 
8
  # --- Langchain Imports ---
 
9
  from langchain_core.prompts import ChatPromptTemplate
10
  from langchain_core.output_parsers import JsonOutputParser
11
  from langchain_core.pydantic_v1 import BaseModel, Field
12
  from langchain_openai import ChatOpenAI
13
- from langchain_google_genai import ChatGoogleGenerativeAI
14
  from langchain_core.messages import SystemMessage, HumanMessage
15
 
16
- # Load environment variables from .env file (for local testing)
17
- # On Hugging Face Spaces, secrets are automatically available as env vars.
18
  load_dotenv()
19
 
20
  # --- 1. API KEY CHECK ---
21
- # This will make the app fail early if the key isn't set
 
22
  try:
23
  openai_api_key = os.environ["OPENAI_API_KEY"]
24
  except KeyError:
 
25
  raise EnvironmentError("Missing OPENAI_API_KEY. Please set it in a .env file locally, or in Hugging Face Space secrets.")
26
 
 
 
 
 
 
 
 
 
27
  # --- 2. PYDANTIC DATA STRUCTURE DEFINITION ---
28
- # Defines the JSON structure we want the LLM to output
29
  class TravelRequest(BaseModel):
30
  departure_city: str = Field(description="The city or airport of departure. Infer if not specified.")
31
  destination_city: str = Field(description="The city or airport of the travel destination.")
@@ -38,110 +49,190 @@ class TravelRequest(BaseModel):
38
  number_of_travelers: int = Field(description="Number of adults traveling. Default to 1 if not specified.")
39
  activity_interests: str = Field(description="Specific interests for activities (e.g., 'museums', 'hiking').")
40
 
41
- # --- 3. CONCEPTUAL ASYNC SEARCH FUNCTIONS ---
42
- # You will implement the real logic for each of these. For now, they simulate a delay.
 
 
43
  async def search_skyscanner(details):
44
- # Simulate network delay and return mock data for a flight
45
  print(f"Searching Skyscanner for: {details['destination_city']}")
46
- await asyncio.sleep(2)
47
  return [{"source": "Skyscanner", "type": "flight", "details": "Flight to Paris", "price": 850.00, "link": "https://www.skyscanner.com"}]
48
 
49
  async def search_expedia(details):
50
- # Simulate network delay and return mock data for a hotel
51
  print(f"Searching Expedia for: {details['destination_city']}")
52
- await asyncio.sleep(1.5)
53
  return [{"source": "Expedia", "type": "hotel", "details": "Hotel in Paris", "price": 150.00, "link": "https://www.expedia.com"}]
54
 
55
  async def scrape_Google_Flights(details):
56
- # Simulate network delay and return mock data for a scraped flight
57
  print(f"Scraping Google Flights for: {details['destination_city']}")
58
- await asyncio.sleep(3)
59
  return [{"source": "Google Flights (Scraped)", "type": "flight", "details": "Cheaper Flight to Paris", "price": 814.00, "link": "https://www.google.com/flights"}]
60
 
61
  async def search_get_your_guide(details):
62
- # Simulate network delay and return mock data for an activity
63
  print(f"Searching GetYourGuide for: {details['activity_interests']}")
64
- await asyncio.sleep(1)
65
  return [{"source": "GetYourGuide", "type": "activity", "details": "Eiffel Tower Tour", "price": 50.00, "link": "https://www.getyourguide.com"}]
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  async def search_llm_redundancy(details, llm, llm_name):
68
- # Query another LLM for additional, general travel ideas
 
 
 
69
  print(f"Querying {llm_name} for additional ideas...")
70
  prompt = f"""As a helpful travel assistant, provide some brief travel suggestions for a trip based on these details: {details}.
71
  Focus on general tips, hidden gems, or activity ideas. Do not suggest specific prices or flights."""
72
  messages = [SystemMessage(content=prompt), HumanMessage(content="What are your suggestions?")]
73
  response = await llm.ainvoke(messages)
74
- return {"source": llm_name, "type": "insight", "details": response.content}
 
75
 
76
- # --- 4. MAIN BOT LOGIC ---
77
  async def ask_bot(question):
78
  """
79
  This is the main ASYNCHRONOUS function for the bot.
80
- It orchestrates the extraction, search, and output formatting.
 
81
  """
82
- # Part 1: Extract structured data from user request using an LLM
83
  try:
84
- # Initialize the LLM for extraction (GPT-4o for precise JSON output)
85
  llm_extractor = ChatOpenAI(model="gpt-4o", temperature=0)
86
- # Define the parser for the Pydantic data model
87
  parser = JsonOutputParser(pydantic_object=TravelRequest)
88
- # Create the prompt template for extraction, including format instructions
89
  extraction_prompt = ChatPromptTemplate.from_messages([
90
  ("system", "You are a helpful travel assistant. Your goal is to accurately extract travel details from a user's natural language request and output them as a JSON object, following the specified schema. Be precise and infer intelligently. The current date is {current_date}."),
91
  ("human", "Extract travel details from this request:\n\n{format_instructions}\n\nUser request: {request}"),
92
  ]).partial(format_instructions=parser.get_format_instructions())
93
- # Create the LangChain expression language chain
94
  extraction_chain = extraction_prompt | llm_extractor | parser
95
- # Get the current date to help the LLM with date parsing
96
  current_date_str = datetime.date.today().strftime("%Y-%m-%d")
97
- # Invoke the chain to get the structured request
98
  structured_request = await extraction_chain.ainvoke({"request": question, "current_date": current_date_str})
99
  except Exception as e:
100
- # Handle errors during the extraction phase
101
  return f"Error extracting request details: {e}. Please try rephrasing your request."
102
 
103
- # Part 2: Gather all search tasks to run concurrently
104
- # Initialize LLM instances for redundancy search (e.g., creative suggestions)
105
  llm_openai_creative = ChatOpenAI(model="gpt-4o", temperature=0.7)
106
- # To enable Gemini redundancy, uncomment the line below and ensure GOOGLE_API_KEY is set in your .env
107
  # llm_gemini_search = ChatGoogleGenerativeAI(model="gemini-1.5-flash", temperature=0.7)
108
 
 
109
  tasks = [
110
- search_skyscanner(structured_request), # Flight search
111
- search_expedia(structured_request), # Hotel search
112
- scrape_Google_Flights(structured_request), # Another flight source (simulated scraping)
113
- search_get_your_guide(structured_request), # Activities search
114
- search_llm_redundancy(structured_request, llm_openai_creative, "ChatGPT (Creative)"), # AI insights
 
 
115
  # search_llm_redundancy(structured_request, llm_gemini_search, "Gemini"), # Uncomment for Gemini insights
116
  ]
117
 
118
- # Part 3: Run all tasks concurrently and collect results
119
  results = await asyncio.gather(*tasks, return_exceptions=True)
120
 
121
- # Part 4: Normalize, filter, and rank results
122
  all_results = []
123
  for res in results:
124
  if isinstance(res, Exception):
125
- # Log any exceptions from the concurrent tasks
126
  print(f"A search task failed: {res}")
127
- elif res:
128
- # Extend the combined results list if the task was successful
129
- all_results.extend(res)
130
 
131
- # Separate and sort results by type (e.g., flights by price)
132
  flights = sorted([r for r in all_results if r['type'] == 'flight'], key=lambda x: x['price'])
133
  hotels = sorted([r for r in all_results if r['type'] == 'hotel'], key=lambda x: x['price'])
134
  activities = [r for r in all_results if r['type'] == 'activity']
135
  insights = [r for r in all_results if r['type'] == 'insight']
 
136
 
137
- # Part 5: Generate the final formatted output in Markdown
138
  dest_city = structured_request['destination_city']
139
  output_text = f"## ✨ Your Personalized Travel Plan to {dest_city}! ✨\n\n"
140
 
141
  # Format Flights section
142
  output_text += f"### ✈️ Top Flights\n"
143
  if flights:
144
- for flight in flights[:3]: # Display top 3 flights
145
  output_text += f"* **{flight['details']}** from **{flight['source']}**\n"
146
  output_text += f" * Price: **C${flight['price']:,.2f}**\n"
147
  output_text += f" * 🔗 [**View Deal**]({flight['link']})\n"
@@ -152,7 +243,7 @@ async def ask_bot(question):
152
  # Format Hotels section
153
  output_text += f"### 🏨 Top Accommodations\n"
154
  if hotels:
155
- for hotel in hotels[:2]: # Display top 2 hotels
156
  output_text += f"* **{hotel['details']}** from **{hotel['source']}**\n"
157
  output_text += f" * Price: **C${hotel['price']:,.2f}** / night\n"
158
  output_text += f" * 🔗 [**View Deal**]({hotel['link']})\n"
@@ -163,7 +254,7 @@ async def ask_bot(question):
163
  # Format Activities section
164
  output_text += f"### 🎉 Fun Activities\n"
165
  if activities:
166
- for activity in activities[:2]: # Display top 2 activities
167
  output_text += f"* **{activity['details']}** from **{activity['source']}**\n"
168
  output_text += f" * Price: **C${activity['price']:,.2f}**\n"
169
  output_text += f" * 🔗 [**View Activity**]({activity['link']})\n"
@@ -171,7 +262,17 @@ async def ask_bot(question):
171
  output_text += "_No specific activities found, but there's always something to explore!_\n"
172
  output_text += "\n"
173
 
174
- # Format AI Insights section (from redundant LLMs)
 
 
 
 
 
 
 
 
 
 
175
  output_text += "### 💡 Additional AI Insights (for Redundancy)\n"
176
  if insights:
177
  for insight in insights:
@@ -180,15 +281,12 @@ async def ask_bot(question):
180
  output_text += "_No additional AI insights available._\n"
181
  output_text += "\n"
182
 
183
- # Add a disclaimer
184
  output_text += "> **Disclaimer:** Prices and availability change rapidly. Click the links for the most up-to-date information. Happy travels! 🚀"
185
 
186
  return output_text
187
 
188
  # --- 5. DEFINE THE GRADIO INTERFACE ---
189
- # Using gr.Blocks for a more customizable layout compared to gr.Interface
190
  with gr.Blocks(theme=gr.themes.Soft()) as iface:
191
- # Main title for the application
192
  gr.Markdown(
193
  """
194
  # ✨ The Ultimate Global Travel Planner Bot ✨
@@ -196,29 +294,23 @@ with gr.Blocks(theme=gr.themes.Soft()) as iface:
196
  """
197
  )
198
  with gr.Row():
199
- # User input text box with more lines and a guiding placeholder
200
  user_input = gr.Textbox(lines=5, label="✈️ Tell me about your dream trip!", placeholder="e.g., I wanna go to Paris from Halifax, maybe around August for a week or so. Cheap as possible, no crazy layovers!")
201
  with gr.Row():
202
- # Submit button to trigger the bot's logic
203
  submit_button = gr.Button("Find My Trip!", variant="primary")
204
  with gr.Row():
205
- # Area to display the Markdown formatted output
206
  output_display = gr.Markdown(label="🌟 Your Travel Plan:")
207
 
208
- # Examples to help users understand how to phrase their requests
209
  gr.Examples(
210
  examples=[
211
  ["I need a cheap flight from Toronto to London around July for 5 days."],
212
  ["Looking for a beach resort in Mexico for 2 people in December, not too expensive."],
213
  ["Find me some cool things to do in Tokyo in spring."]
214
  ],
215
- inputs=user_input # Connect examples to the user input box
216
  )
217
 
218
- # Connect the submit button's click event to the ask_bot function
219
  submit_button.click(fn=ask_bot, inputs=user_input, outputs=output_display)
220
 
221
  # --- 6. LAUNCH THE APP ---
222
- # This block ensures the Gradio app runs when the script is executed
223
  if __name__ == "__main__":
224
  iface.launch()
 
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).
19
+ # On Hugging Face Spaces, secrets are automatically available as environment variables.
20
  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 ---
 
40
  class TravelRequest(BaseModel):
41
  departure_city: str = Field(description="The city or airport of departure. Infer if not specified.")
42
  destination_city: str = Field(description="The city or airport of the travel destination.")
 
49
  number_of_travelers: int = Field(description="Number of adults traveling. Default to 1 if not specified.")
50
  activity_interests: str = Field(description="Specific interests for activities (e.g., 'museums', 'hiking').")
51
 
52
+ # --- 3. ASYNC SEARCH FUNCTIONS (Some Simulated, Some Live) ---
53
+ # All these functions are designed to return a LIST of dictionaries,
54
+ # even if only one dictionary is returned. This is crucial for `all_results.extend()`.
55
+
56
  async def search_skyscanner(details):
57
+ """Simulates searching for flights on Skyscanner."""
58
  print(f"Searching Skyscanner for: {details['destination_city']}")
59
+ await asyncio.sleep(2) # Simulate network delay
60
  return [{"source": "Skyscanner", "type": "flight", "details": "Flight to Paris", "price": 850.00, "link": "https://www.skyscanner.com"}]
61
 
62
  async def search_expedia(details):
63
+ """Simulates searching for hotels on Expedia."""
64
  print(f"Searching Expedia for: {details['destination_city']}")
65
+ await asyncio.sleep(1.5) # Simulate network delay
66
  return [{"source": "Expedia", "type": "hotel", "details": "Hotel in Paris", "price": 150.00, "link": "https://www.expedia.com"}]
67
 
68
  async def scrape_Google_Flights(details):
69
+ """Simulates scraping flight data from Google Flights."""
70
  print(f"Scraping Google Flights for: {details['destination_city']}")
71
+ await asyncio.sleep(3) # Simulate network delay
72
  return [{"source": "Google Flights (Scraped)", "type": "flight", "details": "Cheaper Flight to Paris", "price": 814.00, "link": "https://www.google.com/flights"}]
73
 
74
  async def search_get_your_guide(details):
75
+ """Simulates searching for activities on GetYourGuide."""
76
  print(f"Searching GetYourGuide for: {details['activity_interests']}")
77
+ await asyncio.sleep(1) # Simulate network delay
78
  return [{"source": "GetYourGuide", "type": "activity", "details": "Eiffel Tower Tour", "price": 50.00, "link": "https://www.getyourguide.com"}]
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) ---
174
  async def ask_bot(question):
175
  """
176
  This is the main ASYNCHRONOUS function for the bot.
177
+ It orchestrates the extraction of user intent, concurrent searching,
178
+ data processing, and final output formatting.
179
  """
180
+ # Part 1: Extract structured data from the user's natural language request using an LLM.
181
  try:
 
182
  llm_extractor = ChatOpenAI(model="gpt-4o", temperature=0)
 
183
  parser = JsonOutputParser(pydantic_object=TravelRequest)
 
184
  extraction_prompt = ChatPromptTemplate.from_messages([
185
  ("system", "You are a helpful travel assistant. Your goal is to accurately extract travel details from a user's natural language request and output them as a JSON object, following the specified schema. Be precise and infer intelligently. The current date is {current_date}."),
186
  ("human", "Extract travel details from this request:\n\n{format_instructions}\n\nUser request: {request}"),
187
  ]).partial(format_instructions=parser.get_format_instructions())
 
188
  extraction_chain = extraction_prompt | llm_extractor | parser
 
189
  current_date_str = datetime.date.today().strftime("%Y-%m-%d")
 
190
  structured_request = await extraction_chain.ainvoke({"request": question, "current_date": current_date_str})
191
  except Exception as e:
 
192
  return f"Error extracting request details: {e}. Please try rephrasing your request."
193
 
194
+ # Part 2: Gather all search tasks to run concurrently.
 
195
  llm_openai_creative = ChatOpenAI(model="gpt-4o", temperature=0.7)
196
+ # Uncomment the line below and ensure GOOGLE_API_KEY is set in your .env for Gemini redundancy.
197
  # llm_gemini_search = ChatGoogleGenerativeAI(model="gemini-1.5-flash", temperature=0.7)
198
 
199
+ # Compile all the asynchronous search functions into a list of tasks.
200
  tasks = [
201
+ search_skyscanner(structured_request), # Flight search task (simulated)
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
 
211
+ # Part 3: Run all tasks concurrently and collect their results.
212
  results = await asyncio.gather(*tasks, return_exceptions=True)
213
 
214
+ # Part 4: Normalize, filter, and rank the collected results.
215
  all_results = []
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'])
224
  activities = [r for r in all_results if r['type'] == 'activity']
225
  insights = [r for r in all_results if r['type'] == 'insight']
226
+ vouchers = [r for r in all_results if r['type'] == 'voucher']
227
 
228
+ # Part 5: Generate the final formatted output in Markdown.
229
  dest_city = structured_request['destination_city']
230
  output_text = f"## ✨ Your Personalized Travel Plan to {dest_city}! ✨\n\n"
231
 
232
  # Format Flights section
233
  output_text += f"### ✈️ Top Flights\n"
234
  if flights:
235
+ for flight in flights[:3]:
236
  output_text += f"* **{flight['details']}** from **{flight['source']}**\n"
237
  output_text += f" * Price: **C${flight['price']:,.2f}**\n"
238
  output_text += f" * 🔗 [**View Deal**]({flight['link']})\n"
 
243
  # Format Hotels section
244
  output_text += f"### 🏨 Top Accommodations\n"
245
  if hotels:
246
+ for hotel in hotels[:2]:
247
  output_text += f"* **{hotel['details']}** from **{hotel['source']}**\n"
248
  output_text += f" * Price: **C${hotel['price']:,.2f}** / night\n"
249
  output_text += f" * 🔗 [**View Deal**]({hotel['link']})\n"
 
254
  # Format Activities section
255
  output_text += f"### 🎉 Fun Activities\n"
256
  if activities:
257
+ for activity in activities[:2]:
258
  output_text += f"* **{activity['details']}** from **{activity['source']}**\n"
259
  output_text += f" * Price: **C${activity['price']:,.2f}**\n"
260
  output_text += f" * 🔗 [**View Activity**]({activity['link']})\n"
 
262
  output_text += "_No specific activities found, but there's always something to explore!_\n"
263
  output_text += "\n"
264
 
265
+ # Format Flight Vouchers & Deals section
266
+ output_text += f"### 💰 Flight Vouchers & Deals\n"
267
+ if vouchers:
268
+ for voucher in vouchers[:2]:
269
+ output_text += f"* **{voucher['details']}** from **{voucher['source']}**\n"
270
+ output_text += f" * 🔗 [**Claim Deal!**]({voucher['link']})\n"
271
+ else:
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:
 
281
  output_text += "_No additional AI insights available._\n"
282
  output_text += "\n"
283
 
 
284
  output_text += "> **Disclaimer:** Prices and availability change rapidly. Click the links for the most up-to-date information. Happy travels! 🚀"
285
 
286
  return output_text
287
 
288
  # --- 5. DEFINE THE GRADIO INTERFACE ---
 
289
  with gr.Blocks(theme=gr.themes.Soft()) as iface:
 
290
  gr.Markdown(
291
  """
292
  # ✨ The Ultimate Global Travel Planner Bot ✨
 
294
  """
295
  )
296
  with gr.Row():
 
297
  user_input = gr.Textbox(lines=5, label="✈️ Tell me about your dream trip!", placeholder="e.g., I wanna go to Paris from Halifax, maybe around August for a week or so. Cheap as possible, no crazy layovers!")
298
  with gr.Row():
 
299
  submit_button = gr.Button("Find My Trip!", variant="primary")
300
  with gr.Row():
 
301
  output_display = gr.Markdown(label="🌟 Your Travel Plan:")
302
 
 
303
  gr.Examples(
304
  examples=[
305
  ["I need a cheap flight from Toronto to London around July for 5 days."],
306
  ["Looking for a beach resort in Mexico for 2 people in December, not too expensive."],
307
  ["Find me some cool things to do in Tokyo in spring."]
308
  ],
309
+ inputs=user_input
310
  )
311
 
 
312
  submit_button.click(fn=ask_bot, inputs=user_input, outputs=output_display)
313
 
314
  # --- 6. LAUNCH THE APP ---
 
315
  if __name__ == "__main__":
316
  iface.launch()