phhttps commited on
Commit
e88d9e1
·
1 Parent(s): 0e200a9

feat: add advanced search options (adults, children, pets, dynamic budget)

Browse files
api.py CHANGED
@@ -54,7 +54,10 @@ async def search_deals(
54
  checkin: str = Query(..., description="Check-in date (YYYY-MM-DD)"),
55
  checkout: str = Query(..., description="Check-out date (YYYY-MM-DD)"),
56
  adults: int = 4,
57
- pets: int = 1
 
 
 
58
  ):
59
  print(f"\n--- [API Request] Suche gestartet: {cities} ---")
60
  city_list = [c.strip() for c in cities.split(",")]
@@ -66,7 +69,10 @@ async def search_deals(
66
  checkin=checkin,
67
  checkout=checkout,
68
  group_size=adults,
69
- pets=pets
 
 
 
70
  )
71
  print(f"--- [API Request] Agent fertig. {results.get('total_deals_found')} Deals gefunden.")
72
 
 
54
  checkin: str = Query(..., description="Check-in date (YYYY-MM-DD)"),
55
  checkout: str = Query(..., description="Check-out date (YYYY-MM-DD)"),
56
  adults: int = 4,
57
+ children: int = 0,
58
+ pets: int = 1,
59
+ budget: int = 250,
60
+ budget_type: str = "night"
61
  ):
62
  print(f"\n--- [API Request] Suche gestartet: {cities} ---")
63
  city_list = [c.strip() for c in cities.split(",")]
 
69
  checkin=checkin,
70
  checkout=checkout,
71
  group_size=adults,
72
+ children=children,
73
+ pets=pets,
74
+ budget=budget,
75
+ budget_type=budget_type
76
  )
77
  print(f"--- [API Request] Agent fertig. {results.get('total_deals_found')} Deals gefunden.")
78
 
booking_scraper.py CHANGED
@@ -11,8 +11,8 @@ class BookingScraper:
11
  def __init__(self):
12
  self.firecrawl_key = os.getenv("FIRECRAWL_API_KEY") or os.getenv("firecrawl_api_key")
13
 
14
- async def search_booking(self, city: str, checkin: str, checkout: str, adults: int = 4) -> List[Dict]:
15
- url = self._build_booking_url(city, checkin, checkout, adults)
16
  d1 = datetime.strptime(checkin, "%Y-%m-%d")
17
  d2 = datetime.strptime(checkout, "%Y-%m-%d")
18
  nights = max(1, (d2 - d1).days)
@@ -30,9 +30,23 @@ class BookingScraper:
30
  except Exception: pass
31
  return []
32
 
33
- def _build_booking_url(self, city: str, checkin: str, checkout: str, adults: int):
34
  base = "https://www.booking.com/searchresults.html"
35
- params = [f"ss={quote(city)}", f"checkin={checkin}", f"checkout={checkout}", f"group_adults={adults}", "no_rooms=1", "selected_currency=EUR", "nflt=ht_id%3D220%3Bhotelfacility%3D14"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  return f"{base}?{'&'.join(params)}"
37
 
38
  def _parse_html(self, soup, city, checkin, checkout, nights):
 
11
  def __init__(self):
12
  self.firecrawl_key = os.getenv("FIRECRAWL_API_KEY") or os.getenv("firecrawl_api_key")
13
 
14
+ async def search_booking(self, city: str, checkin: str, checkout: str, adults: int = 4, children: int = 0) -> List[Dict]:
15
+ url = self._build_booking_url(city, checkin, checkout, adults, children)
16
  d1 = datetime.strptime(checkin, "%Y-%m-%d")
17
  d2 = datetime.strptime(checkout, "%Y-%m-%d")
18
  nights = max(1, (d2 - d1).days)
 
30
  except Exception: pass
31
  return []
32
 
33
+ def _build_booking_url(self, city: str, checkin: str, checkout: str, adults: int, children: int):
34
  base = "https://www.booking.com/searchresults.html"
35
+ params = [
36
+ f"ss={quote(city)}",
37
+ f"checkin={checkin}",
38
+ f"checkout={checkout}",
39
+ f"group_adults={adults}",
40
+ f"group_children={children}",
41
+ "no_rooms=1",
42
+ "selected_currency=EUR",
43
+ "nflt=ht_id%3D220%3Bhotelfacility%3D14"
44
+ ]
45
+ # If children, we need to add their ages (defaulting to 5 for now)
46
+ if children > 0:
47
+ for _ in range(children):
48
+ params.append("req_children=5")
49
+
50
  return f"{base}?{'&'.join(params)}"
51
 
52
  def _parse_html(self, soup, city, checkin, checkout, nights):
frontend_dashboard.html CHANGED
@@ -53,12 +53,31 @@
53
  </div>
54
  </div>
55
 
56
- <div>
57
- <label class="block text-sm font-medium text-gray-700">Gäste</label>
58
- <select id="input-guests" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm p-2 bg-gray-50 border">
59
- <option value="4">4 Personen + 1 Hund</option>
60
- <option value="2">2 Personen + 1 Hund</option>
61
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  </div>
63
 
64
  <button id="search-btn" class="w-full bg-emerald-600 text-white py-3 rounded-xl font-semibold hover:bg-emerald-700 transition shadow-md mt-4">
@@ -91,7 +110,7 @@
91
  <!-- Footer -->
92
  <footer class="mt-12 bg-gray-800 text-gray-400 py-8">
93
  <div class="container mx-auto px-4 text-center">
94
- <p>© 2026 Lars Urlaubs-Deals - Mit ❤️ für 4 Personen & 1 Hund gebaut.</p>
95
  </div>
96
  </footer>
97
 
@@ -104,7 +123,11 @@
104
  const cities = document.getElementById('input-cities').value;
105
  const checkin = document.getElementById('input-checkin').value;
106
  const checkout = document.getElementById('input-checkout').value;
107
- const adults = document.getElementById('input-guests').value;
 
 
 
 
108
 
109
  if (!cities || !checkin || !checkout) {
110
  alert("Bitte fülle alle Felder aus!");
@@ -117,7 +140,8 @@
117
  dealGrid.innerHTML = '<div class="col-span-2 text-center py-20"><i class="fa-solid fa-compass fa-spin fa-3x text-emerald-600 mb-4"></i><p class="text-lg font-bold">Live-Suche aktiv...</p><p class="text-gray-500">Wir durchsuchen Booking.com und Airbnb (ca. 60 Sek.)</p></div>';
118
 
119
  try {
120
- const response = await fetch(`/search?cities=${encodeURIComponent(cities)}&checkin=${checkin}&checkout=${checkout}&adults=${adults}`);
 
121
  const data = await response.json();
122
 
123
  renderDeals(data.top_10_deals);
 
53
  </div>
54
  </div>
55
 
56
+ <div class="grid grid-cols-2 gap-2">
57
+ <div>
58
+ <label class="block text-sm font-medium text-gray-700">Erwachsene</label>
59
+ <input type="number" id="input-adults" value="4" min="1" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm p-2 bg-gray-50 border">
60
+ </div>
61
+ <div>
62
+ <label class="block text-sm font-medium text-gray-700">Kinder</label>
63
+ <input type="number" id="input-children" value="0" min="0" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm p-2 bg-gray-50 border">
64
+ </div>
65
+ </div>
66
+
67
+ <div class="flex items-center space-x-2 py-2">
68
+ <input type="checkbox" id="input-pets" checked class="w-4 h-4 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500">
69
+ <label for="input-pets" class="text-sm font-medium text-gray-700">Reisen mit Hund</label>
70
+ </div>
71
+
72
+ <div class="border-t pt-4">
73
+ <div class="flex justify-between items-center mb-1">
74
+ <label class="block text-sm font-medium text-gray-700">Max. Budget (€)</label>
75
+ <select id="input-budget-type" class="text-xs border-none bg-transparent text-emerald-600 font-bold focus:ring-0">
76
+ <option value="night">pro Nacht</option>
77
+ <option value="total">Gesamt</option>
78
+ </select>
79
+ </div>
80
+ <input type="number" id="input-budget" value="250" step="10" class="block w-full border-gray-300 rounded-md shadow-sm p-2 bg-gray-50 border focus:ring-emerald-500 focus:border-emerald-500">
81
  </div>
82
 
83
  <button id="search-btn" class="w-full bg-emerald-600 text-white py-3 rounded-xl font-semibold hover:bg-emerald-700 transition shadow-md mt-4">
 
110
  <!-- Footer -->
111
  <footer class="mt-12 bg-gray-800 text-gray-400 py-8">
112
  <div class="container mx-auto px-4 text-center">
113
+ <p>© 2026 Lars Urlaubs-Deals - Mit ❤️ für Lars & seinen Hund gebaut.</p>
114
  </div>
115
  </footer>
116
 
 
123
  const cities = document.getElementById('input-cities').value;
124
  const checkin = document.getElementById('input-checkin').value;
125
  const checkout = document.getElementById('input-checkout').value;
126
+ const adults = document.getElementById('input-adults').value;
127
+ const children = document.getElementById('input-children').value;
128
+ const pets = document.getElementById('input-pets').checked ? 1 : 0;
129
+ const budget = document.getElementById('input-budget').value;
130
+ const budgetType = document.getElementById('input-budget-type').value;
131
 
132
  if (!cities || !checkin || !checkout) {
133
  alert("Bitte fülle alle Felder aus!");
 
140
  dealGrid.innerHTML = '<div class="col-span-2 text-center py-20"><i class="fa-solid fa-compass fa-spin fa-3x text-emerald-600 mb-4"></i><p class="text-lg font-bold">Live-Suche aktiv...</p><p class="text-gray-500">Wir durchsuchen Booking.com und Airbnb (ca. 60 Sek.)</p></div>';
141
 
142
  try {
143
+ const url = `/search?cities=${encodeURIComponent(cities)}&checkin=${checkin}&checkout=${checkout}&adults=${adults}&children=${children}&pets=${pets}&budget=${budget}&budget_type=${budgetType}`;
144
+ const response = await fetch(url);
145
  const data = await response.json();
146
 
147
  renderDeals(data.top_10_deals);
holland_agent.py CHANGED
@@ -48,32 +48,32 @@ class VacationAgent:
48
  checkin: str,
49
  checkout: str,
50
  group_size: int = 4,
51
- pets: int = 1
 
 
 
52
  ) -> Dict:
53
  """
54
  Main method - finds and ranks vacation deals
55
-
56
- Args:
57
- cities: List of city names or regions (e.g., ["Amsterdam", "Berlin"])
58
- checkin: Check-in date (YYYY-MM-DD)
59
- checkout: Check-out date (YYYY-MM-DD)
60
- group_size: Number of adults
61
- pets: Number of pets
62
-
63
- Returns:
64
- Dictionary with search results, ranked deals, and summary
65
  """
66
- print(f"\n🤖 Vacation Deal Finder")
67
- print(f" Destinations: {', '.join(cities)}")
68
- print(f" Dates: {checkin} → {checkout}")
69
- print(f" Group: {group_size} adults + {pets} dog(s)")
70
- print(f" Budget: €{self.budget_min}-{self.budget_max}/night\n")
71
-
72
  # Calculate nights
73
  d1 = datetime.strptime(checkin, "%Y-%m-%d")
74
  d2 = datetime.strptime(checkout, "%Y-%m-%d")
75
  nights = max(1, (d2 - d1).days)
76
- print(f" Duration: {nights} night(s)")
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  # Start search across all cities with staggered parallel scraping
79
  print("🔍 Searching accommodations...")
@@ -83,7 +83,7 @@ class VacationAgent:
83
  if delay > 0:
84
  await asyncio.sleep(delay)
85
  return await self._search_single_city(
86
- city, checkin, checkout, nights, group_size, pets
87
  )
88
 
89
  # Stagger cities by 2s to avoid thundering herd
@@ -159,29 +159,20 @@ class VacationAgent:
159
  checkout: str,
160
  nights: int,
161
  group_size: int,
 
162
  pets: int
163
  ) -> List[Dict]:
164
  """
165
  Search all sources for a single city/region
166
-
167
- Args:
168
- city: City name or region
169
- checkin: Check-in date
170
- checkout: Check-out date
171
- nights: Number of nights
172
- group_size: Number of adults
173
- pets: Number of pets
174
-
175
- Returns:
176
- List of deals from all sources
177
  """
178
  deals = []
179
 
180
  # Parallel search: Booking.com + Airbnb concurrently
181
  async def _search_booking():
182
  try:
 
183
  return await self.booking_scraper.search_booking(
184
- city, checkin, checkout, group_size
185
  )
186
  except Exception as e:
187
  print(f" Warning: Booking.com search failed for {city}: {e}")
@@ -189,8 +180,9 @@ class VacationAgent:
189
 
190
  async def _search_airbnb():
191
  try:
 
192
  return await self.airbnb_scraper.search_airbnb(
193
- city, checkin, checkout, group_size
194
  )
195
  except Exception as e:
196
  print(f" Warning: Airbnb search failed for {city}: {e}")
 
48
  checkin: str,
49
  checkout: str,
50
  group_size: int = 4,
51
+ children: int = 0,
52
+ pets: int = 1,
53
+ budget: int = 250,
54
+ budget_type: str = "night"
55
  ) -> Dict:
56
  """
57
  Main method - finds and ranks vacation deals
 
 
 
 
 
 
 
 
 
 
58
  """
 
 
 
 
 
 
59
  # Calculate nights
60
  d1 = datetime.strptime(checkin, "%Y-%m-%d")
61
  d2 = datetime.strptime(checkout, "%Y-%m-%d")
62
  nights = max(1, (d2 - d1).days)
63
+
64
+ # Budget calculation
65
+ if budget_type == "total":
66
+ self.budget_max = round(budget / nights)
67
+ else:
68
+ self.budget_max = budget
69
+
70
+ self.ranker.budget_max = self.budget_max
71
+
72
+ print(f"\n🤖 Vacation Deal Finder")
73
+ print(f" Destinations: {', '.join(cities)}")
74
+ print(f" Dates: {checkin} → {checkout} ({nights} nights)")
75
+ print(f" Group: {group_size} adults, {children} children, {pets} dog(s)")
76
+ print(f" Budget: €{self.budget_max}/night (Calculated from {budget} {budget_type})\n")
77
 
78
  # Start search across all cities with staggered parallel scraping
79
  print("🔍 Searching accommodations...")
 
83
  if delay > 0:
84
  await asyncio.sleep(delay)
85
  return await self._search_single_city(
86
+ city, checkin, checkout, nights, group_size, children, pets
87
  )
88
 
89
  # Stagger cities by 2s to avoid thundering herd
 
159
  checkout: str,
160
  nights: int,
161
  group_size: int,
162
+ children: int,
163
  pets: int
164
  ) -> List[Dict]:
165
  """
166
  Search all sources for a single city/region
 
 
 
 
 
 
 
 
 
 
 
167
  """
168
  deals = []
169
 
170
  # Parallel search: Booking.com + Airbnb concurrently
171
  async def _search_booking():
172
  try:
173
+ # Passing budget_max to scraper if supported (will update scraper next)
174
  return await self.booking_scraper.search_booking(
175
+ city, checkin, checkout, group_size, children
176
  )
177
  except Exception as e:
178
  print(f" Warning: Booking.com search failed for {city}: {e}")
 
180
 
181
  async def _search_airbnb():
182
  try:
183
+ # Passing budget_max and children to airbnb scraper
184
  return await self.airbnb_scraper.search_airbnb(
185
+ city, checkin, checkout, group_size, children, pets, self.budget_max
186
  )
187
  except Exception as e:
188
  print(f" Warning: Airbnb search failed for {city}: {e}")
patchright_airbnb_scraper.py CHANGED
@@ -10,14 +10,13 @@ class PatchrightAirbnbScraper:
10
  def __init__(self):
11
  self.firecrawl_key = os.getenv("FIRECRAWL_API_KEY") or os.getenv("firecrawl_api_key")
12
 
13
- async def search_airbnb(self, region: str, checkin: str, checkout: str, adults: int = 4) -> List[Dict]:
14
  if not self.firecrawl_key: return []
15
  d1 = datetime.strptime(checkin, "%Y-%m-%d")
16
  d2 = datetime.strptime(checkout, "%Y-%m-%d")
17
  nights = max(1, (d2 - d1).days)
18
  # Add price_max to filter out luxury villas and ensure better budget fit
19
- # Assume a max budget per night of ~300 to be safe, or use the budget_max if passed (defaulting to 500 here to be safe)
20
- url = f"https://www.airbnb.com/s/{quote(region)}/homes?checkin={checkin}&checkout={checkout}&adults={adults}&price_max=600"
21
 
22
  try:
23
  async with httpx.AsyncClient(timeout=90.0) as client:
 
10
  def __init__(self):
11
  self.firecrawl_key = os.getenv("FIRECRAWL_API_KEY") or os.getenv("firecrawl_api_key")
12
 
13
+ async def search_airbnb(self, region: str, checkin: str, checkout: str, adults: int = 4, children: int = 0, pets: int = 1, budget_max: int = 500) -> List[Dict]:
14
  if not self.firecrawl_key: return []
15
  d1 = datetime.strptime(checkin, "%Y-%m-%d")
16
  d2 = datetime.strptime(checkout, "%Y-%m-%d")
17
  nights = max(1, (d2 - d1).days)
18
  # Add price_max to filter out luxury villas and ensure better budget fit
19
+ url = f"https://www.airbnb.com/s/{quote(region)}/homes?checkin={checkin}&checkout={checkout}&adults={adults}&children={children}&pets={pets}&price_max={budget_max}"
 
20
 
21
  try:
22
  async with httpx.AsyncClient(timeout=90.0) as client: