├── .DS_Store ├── .cache └── .gitignore ├── .env.template ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── babel.config.json ├── benchmark └── benchmark.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── booking │ ├── booking_facilities.js │ ├── booking_prompts.js │ └── booking_url_generator.js ├── config.js ├── index.js ├── llms │ ├── anthropic_utils.js │ ├── groq_utils.js │ ├── llm.js │ └── open_ai_utils.js └── utils │ ├── cache.js │ └── logger.js └── tests └── booking.test.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaialon/ai-search/52bfb4c6dc226cf464e8796554b0281015913501/.DS_Store -------------------------------------------------------------------------------- /.cache/.gitignore: -------------------------------------------------------------------------------- 1 | /*[0-9]* -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY="sk-***" 2 | ANTHROPIC_API_KEY="sk-ant-***" 3 | GROQ_API_KEY="gsk_***" 4 | VERBOSE_LOGGING="true" 5 | PORT=8010 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.10.0 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "all", 10 | "bracketSpacing": true, 11 | "bracketSameLine": false, 12 | "arrowParens": "avoid", 13 | "proseWrap": "never" 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Server", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/src/index.js" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Shai Alon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Search - Booking.com Example 2 | 3 | AI Search is a server application leveraging OpenAI's API to perform intelligent search operations on the Booking.com travel site. 4 | 5 | It's Designed with Node.js, and allows users to query structured information in natural language 6 | 7 | [The code is explained in this Webinar in detail.](https://www.youtube.com/watch?v=NpQJfNXqREA) 8 | 9 | [![image](https://github.com/shaialon/ai-search/assets/3126207/e6b4d836-f8a2-42fe-b073-0183683322d3)](https://www.youtube.com/watch?v=NpQJfNXqREA) 10 | 11 | ## Features 12 | 13 | - Interactive `fastify` server for creating Booking.com urls, based on the users' natural language input. 14 | - Stylish terminal output with `chalk` and clickable links with `terminal-link`. 15 | - Persistent local storage for caching results and queries with `node-persist`. 16 | 17 | ## Prerequisites 18 | 19 | - Node.js version >= 20.10.0 20 | - You can set up [Node Version Manager](https://github.com/nvm-sh/nvm) for it. 21 | 22 | ## Installation 23 | 24 | ### Clone the repository 25 | 26 | ```bash 27 | git clone https://github.com/shaialon/ai-search.git 28 | cd ai-search 29 | ``` 30 | 31 | ### install the dependencies: 32 | 33 | run `nvm use` to have it choose the correct node version. run `npm install` to install the various dependencies. 34 | 35 | ### Configuration 36 | 37 | Create a `.env` file in the root directory and add your OpenAI API key: 38 | 39 | ``` 40 | OPENAI_API_KEY=your_api_key_here 41 | ``` 42 | 43 | ## Usage 44 | 45 | To start the application server, run: 46 | 47 | ```bash 48 | npm start 49 | ``` 50 | 51 | You can then make requests like so (via POSTMAN or just a browser): 52 | 53 | ``` 54 | GET http://localhost:8010/ai_search?search={{url_encoded_query_here}} 55 | ``` 56 | 57 | ### To run tests: 58 | 59 | ```bash 60 | npm test 61 | ``` 62 | 63 | ## Contributions 64 | 65 | Contributions are welcome! Please open an issue to discuss your idea or submit a pull request. 66 | 67 | Especially appreciated: 68 | 69 | - [ ] Support Booking flexible dates 70 | - [x] Adding support for Anthropic Claude 3 71 | - [ ] Adding support for Google Gemini (via Langchain.js) 72 | - [ ] Adding a RAG component 73 | - [ ] Get data dynamically from Booking 74 | 75 | ## Disclaimer 76 | 77 | This project is not affiliated with Booking.com, and is meant for demonstration purposes only. 78 | 79 | ## License 80 | 81 | This project is licensed under the MIT License. See the LICENSE file for details. 82 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /benchmark/benchmark.js: -------------------------------------------------------------------------------- 1 | export const tests = [ 2 | { 3 | name: "Very simple", 4 | query: `I need 2 nights in Paris. 1 Adult. from 14-06-2024 to 16-06-2024.`, 5 | url: `https://www.booking.com/searchresults.html?ss=Paris&ssne=Paris&ssne_untouched=Paris&lang=en&checkin=2024-06-14&checkout=2024-06-16&group_adults=1`, 6 | }, 7 | { 8 | name: "Simple + Kids + Feature", 9 | query: `I need 7 nights in Lisbon. 2 Adults, 2 kids ages: 8, 10. from 14-07-2024. With Pool.`, 10 | url: `https://www.booking.com/searchresults.html?ss=Lisbon&ssne=Lisbon&ssne_untouched=Lisbon&lang=en&checkin=2024-07-14&checkout=2024-07-21&group_adults=2&group_children=2&age=8&age=10&nflt=hotelfacility%3D433`, 11 | }, 12 | { 13 | name: "Broad query - 2 rooms, with private pool", 14 | query: `My brother in law jonathan and I want to arrange a hotel in Eilat for our families. We are 2 adults - each couple. Make sure it has a pool private to us, we are looking for a 5 night stay on the 2nd week of August. Our budget is 1500 shekels per night per room (we need two rooms).`, 15 | url: `https://www.booking.com/searchresults.html?ss=Eilat&ssne=Eilat&ssne_untouched=Eilat&lang=en&checkin=2024-08-05&checkout=2024-08-10&group_adults=4&no_rooms=2&selected_currency=ILS&nflt=price%3DILS-min-3000-1%3B%3Broomfacility%3D93`, 16 | }, 17 | { 18 | name: "Advanced Query - Vague facilities, free cancellation, pet friendly - WITH BASSINET", 19 | query: `I'm looking for accommodations in Madrid for a 4-night stay starting on the 12th of April, 2024. We are a family of 2 adults, 2 kids (8 and 6), and a 6-months baby (we'll need a bassinet), and we'll be bringing our dog. Must have washer and a swimmin pool. It's critical that the booking comes with a free cancellation policy, as our plans might change. Our budget is up to 4000 Euro for the whole stay. Must have 2 separate bedrooms... `, 20 | url: `https://www.booking.com/searchresults.html?ss=Madrid&ssne=Madrid&ssne_untouched=Madrid&lang=en&checkin=2024-04-12&checkout=2024-04-16&group_adults=2&group_children=3&age=8&age=6&age=0&selected_currency=EUR&nflt=price%3DEUR-min-1000-1%3B%3Broomfacility%3D34%3Bhotelfacility%3D433%3Broomfacility%3D175%3Bhotelfacility%3D4%3Bfc%3D2%3Bentire_place_bedroom_count%3D2`, 21 | }, 22 | { 23 | name: "ALL CAPS! Bedroooms, bathrooms, budget", 24 | query: `ME + 4 KIDS + 2 DOGS. 3 NIGHTS AT LAKE TAHO. 2 BEDROOMS, 2 BATHROOMS. 3RD WEEK OF SEPTEMBER. BUDGET: 900 BUCKS PER NIGHT.`, 25 | url: `https://www.booking.com/searchresults.html?ss=Lake+Tahoe&ssne=Lake+Tahoe&ssne_untouched=Lake+Tahoe&lang=en&checkin=2024-09-16&checkout=2024-09-19&group_adults=1&group_children=4&selected_currency=USD&nflt=price%3DUSD-min-900-1%3B%3Bhotelfacility%3D4%3Bentire_place_bedroom_count%3D2%3Bmin_bathrooms%3D2`, 26 | }, 27 | { 28 | name: "Hebrew query - very vague", 29 | query: `. אח שלי גי-בוט , תארגן מלון באיביזה לי ולאשתי, שלוש לילות מתחילת אוגוסט. אנחנו זוג צעיר ואנחנו מחפשים מלון. רוצה משהו דה-בסט מ 10 אלף שקל ללילה עד 20 אלף. חשוב! בא לי לפנק אותה במסאז' כפרה עליה!`, 30 | url: `https://www.booking.com/searchresults.html?ss=Ibiza&ssne=Ibiza&ssne_untouched=Ibiza&lang=he&checkin=2024-08-01&checkout=2024-08-04&group_adults=2&selected_currency=ILS&nflt=price%3DILS-10000-20000-1%3B%3Bpopular_activities%3D55`, 31 | }, 32 | { 33 | name: "Barcelona - with location, Desk, Coffee, Kids ages", 34 | query: `We want two nights in Fort Pienc Barcelona on the 20th of this month - coming with 2 kids (ages 5 and 9), so I need 2 bedrooms. I must have my morning coffee - so make sure there is a machine there. Our flight lands at 2 am, so make sure there is 24 hours reception!`, 35 | url: `https://www.booking.com/searchresults.html?ss=Fort+Pienc%2C+Barcelona&ssne=Fort+Pienc%2C+Barcelona&ssne_untouched=Fort+Pienc%2C+Barcelona&lang=en&checkin=2024-03-20&checkout=2024-03-22&group_adults=1&group_children=2&age=5&age=9&nflt=roomfacility%3D998%3Bhotelfacility%3D8%3Bentire_place_bedroom_count%3D2`, 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // transform: {}, 3 | transform: { 4 | "^.+\\.js$": "babel-jest", 5 | }, 6 | // transformIgnorePatterns: [], 7 | // moduleNameMapper: {}, 8 | // Add this to handle ESM 9 | // extensionsToTreatAsEsm: [".js"], 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-seaarch", 3 | "version": "1.1.0", 4 | "description": "AI Search - For Booking.com", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node src/index.js", 9 | "test": "NODE_OPTIONS=\"--experimental-vm-modules\" npx jest" 10 | }, 11 | "engines": { 12 | "node": ">=20.10.0" 13 | }, 14 | "author": "Shai Alon shaialon84@gmail.com", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@anthropic-ai/sdk": "^0.17.1", 18 | "chalk": "^5.3.0", 19 | "dotenv": "^16.4.2", 20 | "fastify": "^4.26.2", 21 | "groq-sdk": "^0.3.2", 22 | "node-persist": "^4.0.1", 23 | "openai": "^4.27.0", 24 | "terminal-link": "^3.0.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/preset-env": "^7.24.0", 28 | "babel-jest": "^29.7.0", 29 | "jest": "^29.7.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/booking/booking_facilities.js: -------------------------------------------------------------------------------- 1 | const booking_facilities = [ 2 | { 3 | name: "Only show available properties", 4 | synonyms: [ 5 | "show only available accommodations", 6 | "list only available properties", 7 | "display only available lodgings", 8 | ], 9 | key: "oos=1", 10 | }, 11 | { 12 | name: "Free WiFi", 13 | synonyms: ["complimentary wireless internet", "free Wi-Fi access", "free internet connection"], 14 | key: "hotelfacility=107", 15 | }, 16 | { 17 | name: "Breakfast Included", 18 | synonyms: ["complimentary breakfast", "breakfast provided", "free breakfast"], 19 | key: "mealplan=1", 20 | }, 21 | { 22 | name: "Hotels", 23 | synonyms: ["hotel accommodations", "lodging hotels", "hotel properties"], 24 | key: "ht_id=204", 25 | }, 26 | { 27 | name: "Vacation Homes", 28 | synonyms: ["holiday homes", "vacation properties", "holiday rentals"], 29 | key: "ht_id=220", 30 | }, 31 | { 32 | name: "Beachfront", 33 | synonyms: ["oceanfront", "seaside location", "beachside"], 34 | key: "ht_beach=1", 35 | }, 36 | { 37 | name: "Hostels", 38 | synonyms: ["backpacker lodgings", "youth hostels", "budget accommodations"], 39 | key: "ht_id=203", 40 | }, 41 | { 42 | name: "Guesthouses", 43 | synonyms: ["B&Bs", "bed and breakfasts", "inn"], 44 | key: "ht_id=216", 45 | }, 46 | { 47 | name: "No prepayment", 48 | synonyms: ["pay at stay", "no deposit required", "no upfront payment"], 49 | key: "fc=5", 50 | }, 51 | { 52 | name: "Campgrounds", 53 | synonyms: ["camping sites", "RV parks", "campsites"], 54 | key: "ht_id=214", 55 | }, 56 | { 57 | name: "Entire homes & apartments", 58 | synonyms: ["whole apartments", "full homes rental", "entire house"], 59 | key: "privacy_type=3", 60 | }, 61 | { 62 | name: "Apartments", 63 | synonyms: ["apartment units", "flats", "condos"], 64 | key: "ht_id=201", 65 | }, 66 | { 67 | name: "Villas", 68 | synonyms: ["luxury villas", "private villas", "holiday villas"], 69 | key: "ht_id=213", 70 | }, 71 | { 72 | name: "Non-smoking rooms", 73 | synonyms: ["smoke-free rooms", "no smoking accommodations", "non-smoker rooms"], 74 | key: "hotelfacility=16", 75 | }, 76 | { 77 | name: "Family rooms", 78 | synonyms: ["family suites", "rooms for families", "family accommodations"], 79 | key: "hotelfacility=28", 80 | }, 81 | { 82 | name: "Parking", 83 | synonyms: ["car parking", "vehicle parking", "parking available"], 84 | key: "hotelfacility=2", 85 | }, 86 | { 87 | name: "Swimming pool", 88 | synonyms: ["pool", "outdoor pool", "indoor pool"], 89 | key: "hotelfacility=433", 90 | }, 91 | { 92 | name: "Kitchen facilities", 93 | synonyms: ["kitchen amenities", "cooking facilities", "kitchenette"], 94 | key: "mealplan=999", 95 | }, 96 | { 97 | name: "All-inclusive", 98 | synonyms: ["all-inclusive rate", "everything included", "all expenses paid"], 99 | key: "mealplan=4", 100 | }, 101 | { 102 | name: "Breakfast & lunch included", 103 | synonyms: ["half board", "breakfast and lunch provided", "morning and noon meals"], 104 | key: "mealplan=8", 105 | }, 106 | { 107 | name: "Breakfast & dinner included", 108 | synonyms: ["full board", "breakfast and dinner provided", "morning and evening meals"], 109 | key: "mealplan=9", 110 | }, 111 | { 112 | name: "Wonderful: 9+", 113 | synonyms: ["excellent rating: 9+", "superb: 9+", "outstanding: 9+"], 114 | key: "review_score=90", 115 | }, 116 | { 117 | name: "Very Good: 8+", 118 | synonyms: ["great rating: 8+", "very good reviews: 8+", "highly rated: 8+"], 119 | key: "review_score=80", 120 | }, 121 | { 122 | name: "Good: 7+", 123 | synonyms: ["good rating: 7+", "positive reviews: 7+", "favorable: 7+"], 124 | key: "review_score=70", 125 | }, 126 | { 127 | name: "Pleasant: 6+", 128 | synonyms: ["nice rating: 6+", "pleasant reviews: 6+", "satisfactory: 6+"], 129 | key: "review_score=60", 130 | }, 131 | { 132 | name: "Free cancellation", 133 | synonyms: ["no-cost cancellation", "complimentary cancellation", "free of charge cancellation"], 134 | key: "fc=2", 135 | }, 136 | { 137 | name: "Private bathroom", 138 | synonyms: ["ensuite bathroom", "private washroom", "own bathroom"], 139 | key: "roomfacility=38", 140 | }, 141 | { 142 | name: "Air conditioning", 143 | synonyms: ["AC", "aircon", "climate control"], 144 | key: "roomfacility=11", 145 | }, 146 | { 147 | name: "Private pool", 148 | synonyms: ["personal pool", "own pool", "exclusive pool"], 149 | key: "roomfacility=93", 150 | }, 151 | { 152 | name: "Kitchen/Kitchenette", 153 | synonyms: ["small kitchen", "kitchen facilities", "compact kitchen"], 154 | key: "roomfacility=999", 155 | }, 156 | { 157 | name: "Balcony", 158 | synonyms: ["terrace", "patio", "deck"], 159 | key: "roomfacility=17", 160 | }, 161 | { 162 | name: "Sea view", 163 | synonyms: ["ocean view", "beach view", "coastal view"], 164 | key: "roomfacility=108", 165 | }, 166 | { 167 | name: "Kitchen", 168 | synonyms: ["full kitchen", "cooking area", "cookhouse"], 169 | key: "roomfacility=45", 170 | }, 171 | { 172 | name: "Refrigerator", 173 | synonyms: ["fridge", "cooler", "icebox"], 174 | key: "roomfacility=22", 175 | }, 176 | { 177 | name: "Washing machine", 178 | synonyms: ["laundry machine", "clothes washer", "washer"], 179 | key: "roomfacility=34", 180 | }, 181 | { 182 | name: "Terrace", 183 | synonyms: ["patio", "balcony", "deck"], 184 | key: "roomfacility=123", 185 | }, 186 | { 187 | name: "Upper floors accessible by elevator", 188 | synonyms: ["elevator access to upper floors", "lift access", "upper level lift access"], 189 | key: "roomfacility=132", 190 | }, 191 | { 192 | name: "View", 193 | synonyms: ["scenery", "panorama", "outlook"], 194 | key: "roomfacility=81", 195 | }, 196 | { 197 | name: "Towels", 198 | synonyms: ["bath towels", "hand towels", "linens"], 199 | key: "roomfacility=124", 200 | }, 201 | { 202 | name: "Pet friendly", 203 | synonyms: ["pets allowed", "animal friendly", "dog friendly"], 204 | key: "hotelfacility=4", 205 | }, 206 | { 207 | name: "Shower", 208 | synonyms: ["bath", "rain shower", "shower stall"], 209 | key: "roomfacility=4", 210 | }, 211 | { 212 | name: "Hot tub", 213 | synonyms: ["jacuzzi", "whirlpool", "spa bath"], 214 | key: "roomfacility=14", 215 | }, 216 | { 217 | name: "Linens", 218 | synonyms: ["beddings", "sheets", "bed linens"], 219 | key: "roomfacility=125", 220 | }, 221 | { 222 | name: "Wonderful", 223 | synonyms: ["excellent", "superb", "outstanding"], 224 | key: "review_score=90", 225 | }, 226 | { 227 | name: "5 stars", 228 | synonyms: ["5-star", "5 star", "5*"], 229 | key: "class=5", 230 | }, 231 | { 232 | name: "Spa", 233 | synonyms: ["wellness center"], 234 | key: "hotelfacility=54", 235 | }, 236 | { 237 | name: "Airport shuttle", 238 | synonyms: ["shuttle service", "airport transfer", "shuttle bus", "shuttle"], 239 | key: "hotelfacility=17", 240 | }, 241 | { 242 | name: "Facilities for disabled guests", 243 | synonyms: ["handicap accessible", "disabled facilities", "accessible accommodations"], 244 | key: "hotelfacility=25", 245 | }, 246 | { 247 | name: "24-hour front desk", 248 | synonyms: ["24/7 reception", "round-the-clock check-in", "all-day front desk"], 249 | key: "hotelfacility=8", 250 | }, 251 | { 252 | name: "Room service", 253 | synonyms: ["in-room dining", "dining in the room", "room delivery"], 254 | key: "hotelfacility=5", 255 | }, 256 | { 257 | name: "Restaurant", 258 | synonyms: ["dining", "eatery", "food service"], 259 | key: "hotelfacility=3", 260 | }, 261 | { 262 | name: "Fitness center", 263 | synonyms: ["gym", "workout room", "exercise area"], 264 | key: "hotelfacility=11", 265 | }, 266 | { 267 | name: "Electric vehicle charging station", 268 | synonyms: ["EV charging", "electric car charging", "electric vehicle charging"], 269 | key: "hotelfacility=182", 270 | }, 271 | { 272 | name: "Family-Friendly Properties", 273 | synonyms: ["family-friendly", "kid-friendly", "child-friendly"], 274 | key: "family_friendly_property=1", 275 | }, 276 | { 277 | name: "Bed and Breakfasts", 278 | synonyms: ["B&B", "breakfast included", "morning meal"], 279 | key: "ht_id=208", 280 | }, 281 | { 282 | name: "Homestays", 283 | synonyms: ["home rentals", "private homes", "residential properties"], 284 | key: "ht_id=222", 285 | }, 286 | { 287 | name: "Boats", 288 | synonyms: ["yachts", "sailboats", "watercraft"], 289 | key: "ht_id=215", 290 | }, 291 | { 292 | name: "Very Good", 293 | synonyms: ["great", "very good", "highly rated"], 294 | key: "review_score=80", 295 | }, 296 | { 297 | name: "Good", 298 | synonyms: ["positive", "favorable", "satisfactory"], 299 | key: "review_score=70", 300 | }, 301 | { 302 | name: "Pleasant", 303 | synonyms: ["nice", "pleasant", "satisfactory"], 304 | key: "review_score=60", 305 | }, 306 | { 307 | name: "Guests' Favorite Area", 308 | synonyms: ["popular area", "loved by guests", "guests' favorite"], 309 | key: "di=9580", 310 | }, 311 | { 312 | name: "Bathtub", 313 | synonyms: ["bath", "soaking tub", "bath tub", "tub"], 314 | key: "roomfacility=5", 315 | }, 316 | { 317 | name: "Toilet", 318 | synonyms: ["restroom", "lavatory", "commode"], 319 | key: "roomfacility=31", 320 | }, 321 | { 322 | name: "Electric kettle", 323 | synonyms: ["tea kettle", "water boiler", "hot water pot", "kettle", "pot"], 324 | key: "roomfacility=86", 325 | }, 326 | { 327 | name: "TV", 328 | synonyms: ["television", "flat-screen TV", "cable TV"], 329 | key: "roomfacility=8", 330 | }, 331 | { 332 | name: "Coffee/Tea maker", 333 | synonyms: ["coffee machine", "tea maker", "coffee"], 334 | key: "roomfacility=998", 335 | }, 336 | { 337 | name: "Childrens' cribs", 338 | synonyms: ["baby crib", "infant crib", "crib", "cot", "baby bed", "bassinet", "carrycot"], 339 | key: "roomfacility=175", 340 | }, 341 | { 342 | name: "Mountain view", 343 | synonyms: ["hill view", "mountain scenery", "mountain outlook"], 344 | key: "roomfacility=112", 345 | }, 346 | { 347 | name: "Toilet paper", 348 | synonyms: ["bathroom tissue", "toilet roll", "bath tissue", "TP"], 349 | key: "roomfacility=141", 350 | }, 351 | { 352 | name: "Interconnecting room(s) available", 353 | synonyms: ["connecting rooms", "adjoining rooms", "connecting door"], 354 | key: "roomfacility=73", 355 | }, 356 | { 357 | name: "Single-room AC for guest accommodation", 358 | synonyms: ["individual room air conditioning", "personal air conditioning", "single room AC"], 359 | key: "roomfacility=230", 360 | }, 361 | { 362 | name: "Accessible by elevator", 363 | synonyms: ["lift accessible", "elevator access", "lift access", "elavator", "lift"], 364 | key: "roomfacility=189", 365 | }, 366 | { 367 | name: "Rooftop pool", 368 | synonyms: ["top-floor pool", "upper-level pool", "rooftop swimming pool"], 369 | key: "roomfacility=157", 370 | }, 371 | { 372 | name: "Soundproof", 373 | synonyms: ["noise-free", "soundproofed", "quiet"], 374 | key: "roomfacility=79", 375 | }, 376 | { 377 | name: "Hairdryer", 378 | synonyms: ["blow dryer", "hair dryer", "hair blower", "dryer"], 379 | key: "roomfacility=12", 380 | }, 381 | { 382 | name: "1 star", 383 | synonyms: ["1-star", "1 star", "1*"], 384 | key: "class=1", 385 | }, 386 | { 387 | name: "2 stars", 388 | synonyms: ["2-star", "2 star", "2*"], 389 | key: "class=2", 390 | }, 391 | { 392 | name: "3 stars", 393 | synonyms: ["3-star", "3 star", "3*"], 394 | key: "class=3", 395 | }, 396 | { 397 | name: "4 stars", 398 | synonyms: ["4-star", "4 star", "4*"], 399 | key: "class=4", 400 | }, 401 | { 402 | name: "Unrated", 403 | synonyms: ["no rating", "unrated", "no stars"], 404 | key: "class=0", 405 | }, 406 | { 407 | name: "Fitness", 408 | synonyms: ["exercise", "workout", "sports"], 409 | key: "popular_activities=253", 410 | }, 411 | { 412 | name: "Massage", 413 | synonyms: [], 414 | key: "popular_activities=55", 415 | }, 416 | { 417 | name: "Bicycle rental", 418 | synonyms: ["bike rental", "bicycle hire", "bike hire", "bike"], 419 | key: "popular_activities=447", 420 | }, 421 | // { 422 | // name: "Less than 1 km", 423 | // key: "distance=1000", 424 | // }, 425 | // { 426 | // name: "Less than 3 km", 427 | // key: "distance=3000", 428 | // }, 429 | // { 430 | // name: "Less than 5 km", 431 | // key: "distance=5000", 432 | // }, 433 | { 434 | name: "Book without credit card", 435 | synonyms: ["no credit card required", "book without payment", "no card needed"], 436 | key: "fc=4", 437 | }, 438 | { 439 | name: "Twin beds", 440 | synonyms: ["two single beds", "twin single beds", "two separate beds"], 441 | key: "tdb=2", 442 | }, 443 | { 444 | name: "Double bed", 445 | synonyms: ["full bed", "queen bed", "king bed", "double room"], 446 | key: "tdb=3", 447 | }, 448 | // { 449 | // name: "Arc de Triomphe", 450 | // key: "popular_nearby_landmarks=936", 451 | // }, 452 | // { 453 | // name: "Louvre Museum", 454 | // key: "popular_nearby_landmarks=935", 455 | // }, 456 | // { 457 | // name: "Eiffel Tower", 458 | // key: "popular_nearby_landmarks=735", 459 | // }, 460 | // { 461 | // name: "Louise Michel Metro Station", 462 | // key: "popular_nearby_landmarks=9781", 463 | // }, 464 | // { 465 | // name: "Saint-Mandé Metro Station", 466 | // key: "popular_nearby_landmarks=9894", 467 | // }, 468 | // { 469 | // name: "Novotel", 470 | // key: "chaincode=1050", 471 | // }, 472 | // { 473 | // name: "Mercure", 474 | // key: "chaincode=1051", 475 | // }, 476 | // { 477 | // name: "Adagio Aparthotels", 478 | // key: "chaincode=3707", 479 | // }, 480 | // { 481 | // name: "HipHop Hostels", 482 | // key: "chaincode=7352", 483 | // }, 484 | // { 485 | // name: "Citadines", 486 | // key: "chaincode=1585", 487 | // }, 488 | // { 489 | // name: "ibis", 490 | // key: "chaincode=1053", 491 | // }, 492 | // { 493 | // name: "Astotel", 494 | // key: "chaincode=1166", 495 | // }, 496 | // { 497 | // name: "ibis Budget", 498 | // key: "chaincode=3186", 499 | // }, 500 | // { 501 | // name: "Relais & Châteaux", 502 | // key: "chaincode=1440", 503 | // }, 504 | // { 505 | // name: "Pullman Hotels and Resorts", 506 | // key: "chaincode=1854", 507 | // }, 508 | { 509 | name: "Wheelchair accessible", 510 | synonyms: ["handicap accessible", "disabled facilities", "accessible accommodations", "handicap friendly"], 511 | key: "accessible_facilities=185", 512 | }, 513 | { 514 | name: "Toilet with grab rails", 515 | synonyms: ["grab bars", "handrails", "toilet rails"], 516 | key: "accessible_facilities=186", 517 | }, 518 | { 519 | name: "Raised toilet", 520 | synonyms: ["high toilet", "tall toilet", "elevated toilet"], 521 | key: "accessible_facilities=187", 522 | }, 523 | { 524 | name: "Lowered sink", 525 | synonyms: ["accessible sink", "handicap sink", "disabled sink"], 526 | key: "accessible_facilities=188", 527 | }, 528 | { 529 | name: "Bathroom emergency cord", 530 | synonyms: ["emergency pull cord", "bathroom alarm", "safety cord"], 531 | key: "accessible_facilities=189", 532 | }, 533 | { 534 | name: "Visual aids (Braille)", 535 | synonyms: ["Braille signs", "tactile signs", "Braille labels", "Braille"], 536 | key: "accessible_facilities=211", 537 | }, 538 | { 539 | name: "Visual aids (tactile signs)", 540 | synonyms: ["tactile signs", "Braille labels", "tactile labels"], 541 | key: "accessible_facilities=212", 542 | }, 543 | { 544 | name: "Auditory guidance", 545 | synonyms: ["audio guidance", "sound guidance", "auditory signs"], 546 | key: "accessible_facilities=213", 547 | }, 548 | { 549 | name: "Entire unit located on ground floor", 550 | synonyms: ["ground floor"], 551 | key: "accessible_room_facilities=131", 552 | }, 553 | { 554 | name: "Entire unit wheelchair accessible", 555 | synonyms: ["room wheelchair accessible"], 556 | key: "accessible_room_facilities=134", 557 | }, 558 | { 559 | name: "Adapted bath", 560 | synonyms: ["accessible bath", "handicap bath", "disabled bath"], 561 | key: "accessible_room_facilities=148", 562 | }, 563 | { 564 | name: "Roll-in shower", 565 | synonyms: ["accessible shower", "handicap shower", "disabled shower"], 566 | key: "accessible_room_facilities=149", 567 | }, 568 | { 569 | name: "Walk-in shower", 570 | synonyms: ["accessible shower", "handicap shower", "disabled shower"], 571 | key: "accessible_room_facilities=150", 572 | }, 573 | { 574 | name: "Lower sink", 575 | synonyms: ["accessible sink", "handicap sink", "disabled sink"], 576 | key: "accessible_room_facilities=152", 577 | }, 578 | { 579 | name: "Emergency cord in bathroom", 580 | synonyms: ["bathroom alarm", "safety cord", "emergency pull cord"], 581 | key: "accessible_room_facilities=153", 582 | }, 583 | { 584 | name: "Shower chair", 585 | synonyms: ["shower seat", "shower bench", "shower stool"], 586 | key: "accessible_room_facilities=154", 587 | }, 588 | // { 589 | // name: "Level 3+", 590 | // key: "SustainablePropertyLevelFilter=4", 591 | // }, 592 | // { 593 | // name: "Level 3 and higher", 594 | // key: "SustainablePropertyLevelFilter=3", 595 | // }, 596 | // { 597 | // name: "Level 2 and higher", 598 | // key: "SustainablePropertyLevelFilter=2", 599 | // }, 600 | // { 601 | // name: "Level 1 and higher", 602 | // key: "SustainablePropertyLevelFilter=1", 603 | // }, 604 | ]; 605 | 606 | function normalizeStr(str) { 607 | return str.replace(/_/g, " ").toLowerCase().trim(); 608 | } 609 | 610 | const bookingFacilitiesMap = booking_facilities.reduce((acc, facility) => { 611 | acc[facility.name.toLowerCase().trim()] = facility.key; 612 | facility.synonyms.forEach(synonym => { 613 | acc[normalizeStr(synonym)] = facility.key; 614 | }); 615 | return acc; 616 | }); 617 | 618 | export function getBookingFacilityByName(name) { 619 | return bookingFacilitiesMap[normalizeStr(name)]; 620 | } 621 | -------------------------------------------------------------------------------- /src/booking/booking_prompts.js: -------------------------------------------------------------------------------- 1 | import { llmCompletionWithCache } from "../llms/llm.js"; 2 | import { convertStructuredFiltersToUrl } from "./booking_url_generator.js"; 3 | import { logLink } from "../utils/logger.js"; 4 | 5 | const SYSTEM_PROMPT = 6 | `You translate human search queries into structured filters for Booking.com stays (hotels, homes, etc.). 7 | Example: 8 | I'm looking for accommodations in London for a 4-night stay starting on the 12th of February, 2024. We are a family of 2 adults, 2 kids, and a 6-months baby, and we'll be bringing our dog. Must have washer and a jacuzzi. It's critical that the booking comes with a free cancellation policy, as our plans might change. Our budget is up to 600 quid per night for each room. Must have 2 separate bedrooms... we need a crib for the baby. 9 | 10 | Output (the comment is not part of the output, it's just for explanation purposes): 11 | { 12 | // MANDATORY fields: 13 | "language_IETF": "en", // Realize the language of the search. Reply in IETF language tag format such as "en", "he" - without subtags. 14 | "location": "London", // The location can be a city, a region, or a neighborhood. It must be a string. 15 | "check_in_date": "2024-02-12", // The check-in date (MANDATORY! Either specified, or caculated from the check-out date and the number of nights). MUST BE IN THE FUTURE - after ${ 16 | new Date().toISOString().split("T")[0] 17 | }! 18 | "check_out_date": "2024-02-16", // The check-out date (MANDATORY! Either specified, or caculated from the check-in date and the number of nights) 19 | "nights": 4, // The number of nights (MANDATORY! Either specified, or caculated from the check-in and check-out dates) 20 | "guests": { // The number of guests (if specified) 21 | "adults": 2, 22 | "kids": 3, // The number of kids AND infants (if specified) 23 | "infants": 1, // The number of infants (if specified) 24 | "kids_ages": [0] // The ages of the kids in YEARS (if specified) 25 | }, 26 | 27 | // Facilities is an important catch-all for any type of additional requirement that is not captured in other fields! 28 | "facilities": ["Washing machine","Hot tub","Crib"], // The amenities / facilities that user EXPLICITLY asked for. Don't add things that the user didn't ask for. Best select from the list of popular facilities below. 29 | 30 | // Other OPTIONAL fields include if specified: 31 | "price_per_night": {"currency":"GBP", "lte":1200} // OPTIONAL (if specified) - The price per night, supports "gte" and "lte" operators. The currency if specified. This is for all rooms in the order ( so 600x2 ). 32 | "seprate_rooms": 1, // The number of seprate units/rooms in the order (if specified) 33 | "bedrooms_min": 2, // The minimum number of bedrooms (per room) (if specified) 34 | "bathrooms_min": 2, // The minimum number of bathrooms (per room) (if specified) 35 | "pet_friendly": true, // The pet-friendly flag. The "true" value is added automatically, as the user is traveling with a dog. 36 | "free_cancellation": true, // If the user explicitly mentioned the free cancellation policy. 37 | 38 | "brand" // The brand of the accommodation (like "Hilton", "Marriott", "Hyatt", etc.) 39 | "rating" // The rating of the accommodation (supports "gte" and "lte" operators) - scale from 1 to 10 40 | 41 | "meals" // Enum (self, breakfast, breakfast_and_lunch, breakfast_and_dinner, all_inclusive) 42 | "property_type" // Enum (Entire homes & apartments, Apartments, Hotels, Guesthouses, Vacation Homes, Hostels, Campgrounds) 43 | "room_type" // Enum (single, double, twin, suite, studio, apartment, villa, chalet, bungalow, cabin, house, boat, tent, treehouse, igloo) 44 | } 45 | 46 | NOTES: 47 | - If the user does not mention some parameters - do not include them in the output. 48 | - If the question is not in English, mark so in language_IETF, and translate it to English before processing. 49 | - Popular Facilities Include (You can still choose out of the list): "Free WiFi", "Breakfast Included", "Hotels", "Vacation Homes", "Beachfront", "Hostels", "Guesthouses", "No prepayment", "Campgrounds", "Entire homes & apartments", "Apartments", "Villas", "Non-smoking rooms", "Family rooms", "Parking", "Swimming pool", "All-inclusive", "Breakfast & lunch included", "Breakfast & dinner included", "Free cancellation", "Private bathroom", "Air conditioning", "Private pool", "Kitchen", "Balcony", "Sea view", "Kitchen", "Refrigerator", "Washing machine", "Terrace", "Upper floors accessible by elevator", "View", "Towels", "Pet friendly", "Shower", "Hot tub", "Linens", "Wonderful", "Spa", "Airport shuttle", "Facilities for disabled guests", "24-hour front desk", "Room service", "Restaurant", "Fitness center", "Electric vehicle charging station", "Family-Friendly Properties", "Bed and Breakfasts", "Homestays", "Boats", "Very Good", "Good", "Pleasant", "Guests' Favorite Area", "Bathtub", "Toilet", "Electric kettle", "TV", "Coffee/Tea maker", "Crib", "Mountain view", "Toilet paper", "Interconnecting room(s) available", "Single-room AC for guest accommodation", "Accessible by elevator", "Rooftop pool", "Soundproof", "Hairdryer", "Fitness", "Massage", "Bicycle rental", "Book without credit card", "Twin beds", "Double bed", "Wheelchair accessible", "Toilet with grab rails", "Raised toilet", "Lowered sink", "Bathroom emergency cord", "Visual aids (Braille)", "Visual aids (tactile signs)", "Auditory guidance", "Entire unit located on ground floor", "Entire unit wheelchair accessible", "Adapted bath", "Roll-in shower", "Walk-in shower", "Lower sink", "Emergency cord in bathroom", "Shower chair" 50 | - Current date is ${ 51 | new Date().toISOString().split("T")[0] 52 | } YYYY-MM-DD. Pay EXTRA attention to the dates, and make sure to get them correctly based on the user's input. 53 | - Always respond in Minified JSON! 54 | `.trim(); 55 | 56 | async function convertUserQueryToStructuredFilters(userQ) { 57 | const payload = { 58 | messages: [ 59 | { role: "system", content: SYSTEM_PROMPT }, 60 | { role: "user", content: userQ }, 61 | ], 62 | }; 63 | return await llmCompletionWithCache(payload); 64 | } 65 | 66 | export async function processUserQuery(userQ) { 67 | const start = Date.now(); 68 | const structuredFilters = await convertUserQueryToStructuredFilters(userQ); 69 | const duration = Date.now() - start; 70 | const url = convertStructuredFiltersToUrl(structuredFilters); 71 | 72 | logLink(url, "🔎 " + userQ, " Open Link\n", duration); 73 | return url; 74 | } 75 | -------------------------------------------------------------------------------- /src/booking/booking_url_generator.js: -------------------------------------------------------------------------------- 1 | import { getBookingFacilityByName } from "./booking_facilities.js"; 2 | import { logRed, logGreen } from "../utils/logger.js"; 3 | 4 | export function convertStructuredFiltersToUrl(filters) { 5 | const baseURL = `https://www.booking.com/searchresults.html`; 6 | 7 | const urlParams = new URLSearchParams(); 8 | 9 | const { location, brand = "" } = filters; 10 | // urlParams.append("ss", `${brand} ${location}`.trim()); 11 | urlParams.append("ss", location); 12 | urlParams.append("ssne", location); 13 | urlParams.append("ssne_untouched", location); 14 | 15 | urlParams.append("lang", filters.language_IETF?.toLowerCase() || "en"); 16 | urlParams.append("checkin", filters.check_in_date); 17 | urlParams.append("checkout", filters.check_out_date); 18 | 19 | const { guests } = filters; 20 | if (guests) { 21 | urlParams.append("group_adults", guests.adults); 22 | if (guests.kids) { 23 | urlParams.append("group_children", guests.kids); 24 | } 25 | const kidsAges = guests.kids_ages || []; 26 | kidsAges.forEach(age => { 27 | urlParams.append("age", age); 28 | }); 29 | // urlParams.append("group_children", guests.infants); 30 | // no_rooms 31 | } 32 | if (filters?.seprate_rooms > 1) { 33 | urlParams.append("no_rooms", filters.seprate_rooms); 34 | } 35 | const nflt = []; 36 | if (filters.price_per_night) { 37 | const { lte, gte, currency } = filters.price_per_night; 38 | // Example for USD up to 230 per night: `USD-min-230-1;hotelfacility=4` 39 | const price = `price=${currency || "USD"}-${gte || "min"}-${lte || "max"}-1;`; 40 | nflt.push(price); 41 | if (currency) { 42 | urlParams.append("selected_currency", currency); 43 | } 44 | } 45 | 46 | if (filters.facilities) { 47 | for (const facility of filters.facilities) { 48 | const facilityKey = getBookingFacilityByName(facility); 49 | if (facilityKey) { 50 | logGreen(facility + " : " + facilityKey + " INJECTED!"); 51 | nflt.push(facilityKey); 52 | } else { 53 | logRed(facility + " NO MATCH!"); 54 | } 55 | } 56 | } 57 | if (filters.pet_friendly) { 58 | nflt.push(getBookingFacilityByName("Pet friendly")); 59 | logGreen("Pet friendly INJECTED!"); 60 | } 61 | if (filters.free_cancellation) { 62 | nflt.push(getBookingFacilityByName("Free cancellation")); 63 | logGreen("Free cancellation INJECTED!"); 64 | } 65 | if (filters.bedrooms_min) { 66 | nflt.push(`entire_place_bedroom_count=${filters.bedrooms_min}`); 67 | logGreen(`Bedrooms limitation ${filters.bedrooms_min} INJECTED!`); 68 | } 69 | if (filters.bathrooms_min) { 70 | nflt.push(`min_bathrooms=${filters.bathrooms_min}`); 71 | logGreen(`Bathrooms limitation ${filters.bathrooms_min} INJECTED!`); 72 | } 73 | if (nflt.length) { 74 | urlParams.append("nflt", nflt.join(";")); 75 | } 76 | 77 | // sb_travel_purpose=leisure 78 | 79 | return `${baseURL}?${urlParams.toString()}`; 80 | } 81 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | 3 | export const llmModels = { 4 | // OpenAI 5 | GPT_3_5: "gpt-3.5-turbo-0125", 6 | GPT_4_TURBO: "gpt-4-turbo-preview", 7 | GPT_4: "gpt-4", 8 | // Anthropic 9 | CLAUDE_3_HAIKU: "claude-3-haiku-20240307", // fastestt 10 | CLAUDE_3_SONNET: "claude-3-sonnet-20240229", // Balanced 11 | CLAUDE_3_OPUS: "claude-3-opus-20240229", // Stronger 12 | 13 | // Groq 14 | MIXTRAL: "mixtral-8x7b-32768", 15 | LLAMA2_70B: "llama2-70b-4096", 16 | }; 17 | 18 | const IS_TEST = process.env.JEST_WORKER_ID !== undefined; 19 | 20 | export const config = Object.freeze({ 21 | OPENAI_API_KEY: process.env["OPENAI_API_KEY"], 22 | ANTHROPIC_API_KEY: process.env["ANTHROPIC_API_KEY"], 23 | GROQ_API_KEY: process.env["GROQ_API_KEY"], 24 | MODEL: llmModels.CLAUDE_3_HAIKU, 25 | VERBOSE_LOGGING: true && !IS_TEST, // process.env["VERBOSE_LOGGING"] === "true", 26 | IS_TEST, 27 | PORT: process.env["PORT"] || 8010, 28 | }); 29 | 30 | export const LOGGER_OPTIONS = { 31 | depth: null, 32 | colors: true, 33 | }; 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { processUserQuery } from "../src/booking/booking_prompts.js"; 2 | // import { tests } from "../benchmark/benchmark.js"; 3 | 4 | import { config } from "./config.js"; 5 | 6 | import Fastify from "fastify"; 7 | const fastify = Fastify({ 8 | logger: true, 9 | }); 10 | 11 | fastify.route({ 12 | method: "GET", 13 | url: "/ai_search", 14 | schema: { 15 | querystring: { 16 | type: "object", 17 | properties: { 18 | search: { type: "string" }, 19 | }, 20 | required: ["search"], 21 | }, 22 | response: { 23 | 200: { 24 | type: "object", 25 | properties: { 26 | status: { type: "string" }, 27 | url: { type: "string" }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | 33 | handler: async (request, reply) => { 34 | const url = await processUserQuery(request.query.search); 35 | return { status: "success", url: url }; 36 | }, 37 | }); 38 | 39 | try { 40 | await fastify.listen({ port: config.PORT }); 41 | } catch (err) { 42 | fastify.log.error(err); 43 | process.exit(1); 44 | } 45 | 46 | // processUserQuery(tests[6].query); 47 | -------------------------------------------------------------------------------- /src/llms/anthropic_utils.js: -------------------------------------------------------------------------------- 1 | import { config, llmModels, LOGGER_OPTIONS } from "../config.js"; 2 | import Anthropic from "@anthropic-ai/sdk"; 3 | 4 | const anthropic = new Anthropic({ 5 | apiKey: config.ANTHROPIC_API_KEY, 6 | }); 7 | 8 | const LOG = config.VERBOSE_LOGGING; 9 | 10 | const ANTHROPIC_DEFAULTS = { 11 | model: llmModels.CLAUDE_3_SONNET, 12 | // response_format: { type: "json_object" }, 13 | temperature: 0, 14 | max_tokens: 1000, 15 | }; 16 | 17 | function convertOpenAIPayloadToAnthropic(payload) { 18 | // if the first message is a system message, remove it from the messages array, and add it as a key "system" in the payload 19 | if (payload.messages[0].role === "system") { 20 | payload.system = payload.messages.shift().content; 21 | } 22 | return payload; 23 | } 24 | 25 | // I provided the below function since Clause seems to be weak at adhering to the system message format. Example: System of "You tell jokes", User of "Tell about tomatoes" - will not provide a joke! 26 | function condenseSystemMessageIntoUser(payload) { 27 | // if the first message is a system message, remove it from the messages array, and add it as a key "system" in the payload 28 | if (payload.messages[0].role === "system") { 29 | const systemText = payload.messages.shift().content; 30 | payload.messages[0].content = `${systemText}\n${payload.messages[0].content}`; 31 | } 32 | return payload; 33 | } 34 | 35 | export async function anthropicCompletion(payload) { 36 | const anthropicPayload = convertOpenAIPayloadToAnthropic({ 37 | ...ANTHROPIC_DEFAULTS, 38 | ...payload, 39 | }); 40 | const chatCompletion = await anthropic.messages.create(anthropicPayload); 41 | 42 | LOG && console.dir(chatCompletion.usage, LOGGER_OPTIONS); 43 | 44 | const structuredResponse = JSON.parse(chatCompletion.content[0].text); 45 | 46 | return structuredResponse; 47 | } 48 | -------------------------------------------------------------------------------- /src/llms/groq_utils.js: -------------------------------------------------------------------------------- 1 | import { config, llmModels, LOGGER_OPTIONS } from "../config.js"; 2 | import Groq from "groq-sdk"; 3 | 4 | const groq = new Groq({ 5 | apiKey: config.GROQ_API_KEY || "MISSING_GROQ_API_KEY", 6 | }); 7 | 8 | const LOG = config.VERBOSE_LOGGING; 9 | 10 | const GROQ_DEFAULTS = { 11 | model: llmModels.MIXTRAL, 12 | // response_format: { type: "json_object" }, 13 | temperature: 0, 14 | max_tokens: 1024, 15 | stream: false, 16 | }; 17 | export async function groqCompletion(payload) { 18 | const chatCompletion = await groq.chat.completions.create({ 19 | ...GROQ_DEFAULTS, 20 | ...payload, 21 | }); 22 | 23 | LOG && console.dir(chatCompletion.usage, LOGGER_OPTIONS); 24 | 25 | const structuredResponse = JSON.parse(chatCompletion.choices[0].message.content); 26 | 27 | return structuredResponse; 28 | } 29 | -------------------------------------------------------------------------------- /src/llms/llm.js: -------------------------------------------------------------------------------- 1 | import { config, llmModels, LOGGER_OPTIONS } from "../config.js"; 2 | import { getFromCache, setToCache } from "../utils/cache.js"; 3 | 4 | import { openAICompletion } from "./open_ai_utils.js"; 5 | import { anthropicCompletion } from "./anthropic_utils.js"; 6 | import { groqCompletion } from "./groq_utils.js"; 7 | 8 | const LOG = config.VERBOSE_LOGGING; 9 | 10 | const GROQ_MODELS = new Set([llmModels.MIXTRAL, llmModels.LLAMA2_70B]); 11 | 12 | function identifyLLMProviderByModelName(modelName) { 13 | if (modelName.startsWith("gpt-")) { 14 | return "OpenAI"; 15 | } else if (modelName.startsWith("claude-")) { 16 | return "Anthropic"; 17 | } else if (GROQ_MODELS.has(modelName)) { 18 | return "Groq"; 19 | } 20 | return null; 21 | } 22 | 23 | const RESOLVERS = { 24 | OpenAI: openAICompletion, 25 | Anthropic: anthropicCompletion, 26 | Groq: groqCompletion, 27 | }; 28 | 29 | export async function llmCompletionWithCache(payload) { 30 | const payloadWithModel = { 31 | ...payload, 32 | model: payload.model || config.MODEL, 33 | }; 34 | let chatCompletion = await getFromCache(payloadWithModel); 35 | if (!chatCompletion) { 36 | // Identify the LLM provider by the model name 37 | const llmProvider = identifyLLMProviderByModelName(payloadWithModel.model); 38 | LOG && console.log(`Making a request to ${llmProvider}: ${payloadWithModel.model}`); 39 | LOG && console.time("Query LLM Execution"); 40 | 41 | // Choose the correct LLM provider 42 | chatCompletion = await RESOLVERS[llmProvider](payloadWithModel); 43 | 44 | LOG && console.timeEnd("Query LLM Execution"); 45 | 46 | setToCache(payloadWithModel, chatCompletion); 47 | } 48 | 49 | LOG && console.log(`Response from LLM:`); 50 | LOG && console.dir(chatCompletion, LOGGER_OPTIONS); 51 | 52 | return chatCompletion; 53 | } 54 | -------------------------------------------------------------------------------- /src/llms/open_ai_utils.js: -------------------------------------------------------------------------------- 1 | import { config, llmModels, LOGGER_OPTIONS } from "../config.js"; 2 | import OpenAI from "openai"; 3 | 4 | const openai = new OpenAI({ 5 | apiKey: config.OPENAI_API_KEY, 6 | }); 7 | 8 | const LOG = config.VERBOSE_LOGGING; 9 | 10 | const OPENAI_DEFAULTS = { 11 | model: llmModels.GPT_4_TURBO, 12 | response_format: { type: "json_object" }, 13 | temperature: 0, 14 | seed: 100, 15 | }; 16 | export async function openAICompletion(payload) { 17 | const chatCompletion = await openai.chat.completions.create({ 18 | ...OPENAI_DEFAULTS, 19 | ...payload, 20 | }); 21 | 22 | LOG && console.dir(chatCompletion.usage, LOGGER_OPTIONS); 23 | 24 | const structuredResponse = JSON.parse(chatCompletion.choices[0].message.content); 25 | 26 | return structuredResponse; 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/cache.js: -------------------------------------------------------------------------------- 1 | // A basic cache to save LLM tokens and development sanity 2 | import { config } from "../config.js"; 3 | import storage from "node-persist"; 4 | import crypto from "crypto"; 5 | import { logGreen } from "./logger.js"; 6 | 7 | const LOG = config.VERBOSE_LOGGING; 8 | 9 | // Initialize the storage 10 | await storage.init({ 11 | dir: ".cache", 12 | }); 13 | 14 | function generateCacheKey(input) { 15 | // Convert input to a string if it's an object 16 | let stringInput; 17 | if (typeof input === "object") { 18 | try { 19 | stringInput = JSON.stringify(input); 20 | } catch (error) { 21 | console.error("Failed to stringify input:", error); 22 | // Handle non-serializable input, fallback, or throw error 23 | throw new Error("Input cannot be serialized"); 24 | } 25 | } else { 26 | stringInput = input; 27 | } 28 | return crypto.createHash("sha256").update(stringInput).digest("hex"); 29 | } 30 | 31 | export async function getFromCache(payload) { 32 | const key = generateCacheKey(payload); 33 | const data = await storage.getItem(key); // Use the generated key to get the item 34 | LOG && data && logGreen("Got Cache hit!"); 35 | return data; 36 | } 37 | 38 | // Implement set cache 39 | export async function setToCache(payload, value) { 40 | const key = generateCacheKey(payload); 41 | await storage.setItem(key, value); // Save the value with the generated key 42 | LOG && logGreen("Saved to cache!"); 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import terminalLink from "terminal-link"; 3 | import { config } from "../config.js"; 4 | const { IS_TEST } = config; 5 | 6 | function logIfNotTest(message) { 7 | if (IS_TEST) { 8 | return; 9 | } 10 | console.log(message); 11 | } 12 | 13 | export function logGrey(message) { 14 | logIfNotTest(chalk.grey(message)); 15 | } 16 | 17 | export function logYellow(message) { 18 | logIfNotTest(chalk.yellow(message)); 19 | } 20 | 21 | export function logRed(message) { 22 | logIfNotTest(chalk.red(message)); 23 | } 24 | 25 | export function logGreen(message) { 26 | logIfNotTest(chalk.green(message)); 27 | } 28 | 29 | export function logBlue(message) { 30 | logIfNotTest(chalk.blue(message)); 31 | } 32 | 33 | export function logLink(url, anchorText, message, duration) { 34 | if (IS_TEST) { 35 | return; 36 | } 37 | logGrey("-----------"); 38 | let text = ""; 39 | if (duration) { 40 | text += chalk.green.bold(`Took (${duration.toLocaleString()} ms).`); 41 | } 42 | if (message) { 43 | text += chalk.blue.bold(message); 44 | } 45 | if (anchorText) { 46 | text += chalk.yellow(` ` + anchorText); 47 | } 48 | console.log(terminalLink(text, url)); 49 | logGrey("-----------"); 50 | } 51 | -------------------------------------------------------------------------------- /tests/booking.test.js: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals"; 2 | import chalk from "chalk"; 3 | import terminalLink from "terminal-link"; 4 | import { processUserQuery } from "../src/booking/booking_prompts.js"; 5 | import { tests } from "../benchmark/benchmark.js"; 6 | const TEST_TIMEOUT = 30000; 7 | 8 | for (const { name, query, url } of tests) { 9 | test.concurrent( 10 | `${chalk.underline.black.bold.bgYellowBright(name)} 🔎 ${query} ${terminalLink( 11 | chalk.blue.bold("Open Expected Link"), 12 | url, 13 | )}`, 14 | async () => { 15 | const data = await processUserQuery(query); 16 | expect(data).toBe(url); 17 | }, 18 | TEST_TIMEOUT, 19 | ); 20 | } 21 | --------------------------------------------------------------------------------