├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── app ├── assistant │ ├── message │ │ └── route.js │ └── thread │ │ └── route.js ├── chat │ ├── function │ │ └── route.js │ └── message │ │ └── route.js ├── layout.js ├── page.jsx ├── sandbox.jsx └── sandbox.module.css ├── assets └── main.css ├── components ├── dialog.jsx ├── dialog.module.css ├── loader.jsx ├── loadingtext.jsx ├── loadingtext.module.css └── openailogo.jsx ├── docs ├── screenshot01.png ├── screenshot02.png ├── screenshot03.png ├── screenshot04.png ├── screenshot05.png └── screenshot06.png ├── lib ├── get_event.json ├── get_events.json ├── get_hotel.json ├── get_reservation.json ├── get_weather.json ├── mockapi.js ├── quickcache.js ├── reserve_hotel.json ├── search_hotel.json └── utils.js ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── logo192.png └── logo512.png ├── service └── openai.js └── stores └── appstore.js /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=YOUR-OPENAI-API-KEY 2 | OPENAI_ASSISTANT_ID=YOUR-OPENAI-ASSISTANT-ID -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # My ignore list 16 | _bin 17 | stories 18 | .next 19 | .env 20 | out 21 | build 22 | *.bu.html 23 | *.bu.md 24 | *.bu.json 25 | *.bu.css 26 | *.bu.jsx 27 | *.bu.js 28 | 29 | # Editor directories and files 30 | .vscode/* 31 | !.vscode/extensions.json 32 | .idea 33 | .DS_Store 34 | *.suo 35 | *.ntvs* 36 | *.njsproj 37 | *.sln 38 | *.sw? 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-present SuperShaneski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | openai-api-function-call-sample 2 | ====== 3 | 4 | v0.0.2 5 | 6 | A sample app to demonstrate [Function calling](https://platform.openai.com/docs/guides/function-calling) using the latest format in [Chat Completions API](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) and also in [Assistants API](https://platform.openai.com/docs/assistants/overview). 7 | 8 | This application is built using manual setup of Next.js 13. 9 | 10 | --- 11 | 12 | 最新のフォーマットを使用した[Chat Completions API](https://platform.openai.com/docs/guides/text-generation/chat-completions-api)および[Assistants API](https://platform.openai.com/docs/assistants/overview)での「[function calling](https://platform.openai.com/docs/guides/function-calling)」をデモンストレーションするサンプルアプリケーション。 13 | 14 | このアプリケーションはNext.js 13の手動セットアップを使用して構築されています。 15 | 16 | 17 | **Updated**: Using [v4.18.0 OpenAI Node module](https://www.npmjs.com/package/openai) 18 | 19 | 20 | # Function Calling 21 | 22 | We will be using `gpt-3.5-turbo-1106`. You can also replace it with `gpt-4-1106-preview` by editing the [service/openai.js](/service/openai.js) file. These two models supports the new function calling format and [parallel function calling](https://platform.openai.com/docs/guides/function-calling/parallel-function-calling). 23 | 24 | In order to improve function calling, there are four key aspects to consider: 25 | * System prompts 26 | * Function composition 27 | * Effective function outputs 28 | * Looping through API calls 29 | 30 | 31 | ## System Prompt 32 | 33 | Even though function calling can be executed without the addition of a system prompt, my experience suggests that it is better to have one. It is like the icing on a cake. In this sample application, we’ve included a simple system prompt to facilitate operations. 34 | 35 | ```javascript 36 | const system_prompt = `You are a helpful personal assistant.\n\n` + 37 | `# Tools\n` + 38 | `You have the following tools that you can invoke based on the user inquiry.\n` + 39 | `- get_weather, when the user wants to know the weather forecast given a location and date.\n` + 40 | `- get_events, when the user wants to know events happening in a given location and date.\n` + 41 | `- get_event, when the user wants to know more about a particular event.\n` + 42 | `- search_hotel, when the user wants to search for hotel based on given location.\n` + 43 | `- get_hotel, when the user wants to know more about a particular hotel.\n` + 44 | `- reserve_hotel, when the user wants to make room reservation for a particular hotel.\n` + 45 | `- get_reservation, when the user wants to get the details of their reservation.\n` + 46 | `When the user is making hotel reservation, be sure to guide the user to fill up all required information.\n` + 47 | `When you fill up some of the required information yourself, be sure to confirm to user before proceeding.\n` + 48 | `Aside from the listed functions above, answer all other inquiries by telling the user that it is out of scope of your ability.\n\n` + 49 | `# User\n` + 50 | `If my full name is needed, please ask me for my full name.\n\n` + 51 | `# Language Support\n` + 52 | `Please reply in the language used by the user.\n\n` + 53 | `Today is ${today}` 54 | ``` 55 | 56 | Here, we have enumerated the available tools/functions and provided guidelines on when they should be invoked. We have also included additional instructions on how to manage certain functions and other general directives. Given that we’re dealing with events, it’s necessary to append the current date. 57 | 58 | 59 | ## Functions/Tools 60 | 61 | For this sample app, we have the following functions: 62 | * [get_weather](/lib/get_weather.json)(location, date) 63 | * [get_events](/lib/get_events.json)(location, date) 64 | * [get_event](/lib/get_event.json)(location, date, event) 65 | * [search_hotel](/lib/search_hotel.json)(location) 66 | * [get_hotel](/lib/get_hotel.json)(location, hotel) 67 | * [reserve_hotel](/lib/reserve_hotel.json)(hotel, location, fullName, numberOfGuests, checkInDate, checkOutDate, roomType, specialRequests) 68 | * [get_reservation](/lib/get_reservation.json)(hotel, location, reservationId) 69 | 70 | To handle the output for these functions/tools, I made a mock API call handler. See [mockapi.js](/lib/mockapi.js). 71 | To simulate actual data, I also ***"cache"*** the result to make it appear real so that you can go back and forth and have the same result using the same parameters. 72 | 73 | Now, when you are writing your own functions, you need to make sure that the names, parameters and descriptions actually makes sense and easily understandable. 74 | 75 | Do not use uncommon abreviations or acronyms in names and parameters, specially the function name. The function name should convey exactly what you are tring to achieve. The description should also be clear and avoid writing very long description. 76 | 77 | The parameters should also make sense in the context of the function. 78 | Otherwise, the AI will not know how to supply its value. Worst, the AI will probably make up its own parameter. 79 | 80 | A good rule of thumb is if a normal person can understand your function just by reading the JSON schema. 81 | 82 | 83 | ## Function Output 84 | 85 | This refers to the output of the external API when you supply the result from function calling. Check the [mock api](/lib/mockapi.js) handler for this app. 86 | 87 | You need to handle all probable cases, all errors so that the AI will know how to handle them on their own. Be descriptive in your status and message. The AI will pickup your wording when it summarizes the result. 88 | 89 | For example, if you ask for the weather forecast for a certain place without supplying the date, and `get_weather` is invoked, here is a sample function output: 90 | 91 | ```javascript 92 | { error: 'Invalid date', message: 'Please specify the date' } 93 | ``` 94 | 95 | Supply a clear and concise function output and let the AI deal with whatever the case might appear. 96 | Do not attempt to intercept it midway and handle it yourself. Just use the output to send the result back to the AI. 97 | 98 | 99 | ## API Call Loop 100 | 101 | The following is a basic guide on how to manage function calls in the Chat Completions API. 102 | 103 | Please note that we are adding the context to all API calls. This is very important. 104 | You need to add the context unless your application is just one shot function calling. 105 | 106 | ```javascript 107 | // prepare messages 108 | let messages = [{ role: 'system', content: system_prompt }] 109 | messages = messages.concat(history_context) 110 | messages.push({ role: 'user', content: user_query}) 111 | 112 | // 1st API call 113 | const result = await openai.chat.completions.create({ 114 | messages, 115 | tools, 116 | }) 117 | 118 | // Check the result if content is not null and display it to the user 119 | const text = result.message.content 120 | 121 | // check the result if it contains function calling 122 | const tools = result.message.tool_calls 123 | 124 | if(tools) { 125 | 126 | let isCompleted = false 127 | 128 | do { 129 | 130 | // process the function calling/ call to external API 131 | ... 132 | 133 | // 2nd API call 134 | const result = await openai.chat.completions.create({ 135 | messages, 136 | tools, 137 | }) 138 | 139 | if(!result.message.tool_calls) { 140 | isCompleted = true 141 | } 142 | 143 | } while(!isCompleted) 144 | 145 | } 146 | ``` 147 | 148 | If the first call does not trigger function calling, no need to call the function loop. 149 | 150 | Only when function calling is triggered, then we will handle it in a loop. 151 | The reason for this is there is a high possibility that the 2nd API call might still result with function calling. 152 | The AI often times call the functions on their own volition, if it see fit. 153 | So we will continue to process everything in a loop until the AI no longer calls function calling. 154 | Without handling it this way, you will curtail the AI's way to respond. 155 | Of course, it can run amok, so just in case, set a maximum loop limit before you hit the break. 156 | 157 | Now, if you look at my implementation, I am calling two endpoints separately for the [1st API call](/app/chat/message/route.js) and [2nd API call](/app/chat/function/route.js). This is a ***not so elegant way*** to handle what I just layed out above lol. I am doing this because there are cases when in 2nd function call, content (text) can be included in the result and I want to display it, too. If I am using streaming, this is not necessary, but alas, I do not know how to implement streaming in Next.js yet lol. 158 | 159 | Okay, so much for the explanations. Let's see how it all works. 160 | 161 | 162 | # Sample Conversation 163 | 164 | So, we start by asking for the events from a particular location, in this case, Sapporo. 165 | 166 | ![Get Events](./docs/screenshot01.png) 167 | 168 | Surprisingly, the AI send us the complete event info in one call. 169 | But under the hood, we can see that the AI called function calling twice! 170 | First, calling `get_events` to get all events given a location. 171 | 172 | ```javascript 173 | // function calling 174 | [ 175 | { 176 | id: 'call_fGx6ErPI3O2Ktw4WbDOCY4ob', 177 | type: 'function', 178 | function: { 179 | name: 'get_events', 180 | arguments: '{"location":"Sapporo","date":"2023-11-24"}' 181 | } 182 | } 183 | ] 184 | 185 | // mock output 186 | [ 187 | { 188 | tool_call_id: 'call_fGx6ErPI3O2Ktw4WbDOCY4ob', 189 | role: 'tool', 190 | name: 'get_events', 191 | content: '{\n' + 192 | ' "location": "Sapporo",\n' + 193 | ' "date": "2023-11-24",\n' + 194 | ' "event": "Street Dance Parade"\n' + 195 | '}' 196 | } 197 | ] 198 | ``` 199 | 200 | It then decided, what the heck, let's get the event information, too, invoking `get_event` function using the result from the previous function as parameters. 201 | 202 | ```javascript 203 | // function calling 204 | [ 205 | { 206 | id: 'call_SGy8QVzcX43DyiRo2D801JW0', 207 | type: 'function', 208 | function: { 209 | name: 'get_event', 210 | arguments: '{"event":"Street Dance Parade","location":"Sapporo","date":"2023-11-24"}' 211 | } 212 | } 213 | ] 214 | 215 | // mock output 216 | [ 217 | { 218 | tool_call_id: 'call_SGy8QVzcX43DyiRo2D801JW0', 219 | role: 'tool', 220 | name: 'get_event', 221 | content: '{\n' + 222 | ' "location": "Sapporo",\n' + 223 | ' "date": "2023-11-24",\n' + 224 | ' "event": "Street Dance Parade",\n' + 225 | ' "time": "11:00 - 15:00",\n' + 226 | ' "place": "City Park",\n' + 227 | ' "links": [\n' + 228 | ' {\n' + 229 | ' "title": "Event site",\n' + 230 | ' "url": "https://example.com/event/zem46p1c02plp6kyt33",\n' + 231 | ' "target": "_blank"\n' + 232 | ' },\n' + 233 | ' {\n' + 234 | ' "title": "Venue information",\n' + 235 | ' "url": "https://example.com/venue/jbkfiqb2bslp6kyt33",\n' + 236 | ' "target": "_blank"\n' + 237 | ' }\n' + 238 | ' ],\n' + 239 | ' "images": [\n' + 240 | ' {\n' + 241 | ' "alt": "Street Dance Parade",\n' + 242 | ' "src": "https://i.postimg.cc/xCd4HV0W/614a6c2b-b881-42f2-a8d4-95f8033b55fb.jpg"\n' + 243 | ' }\n' + 244 | ' ]\n' + 245 | '}' 246 | } 247 | ] 248 | 249 | // summary 250 | { 251 | index: 0, 252 | message: { 253 | role: 'assistant', 254 | content: 'The "Street Dance Parade" event will take place in Sapporo on November 24, 2023, from 11:00 to 15:00 at the City Park. You can find more information about the event on the [event site](https://example.com/event/zem46p1c02plp6kyt33) and the venue information on the [venue information page](https://example.com/venue/jbkfiqb2bslp6kyt33).\n' + 255 | '\n' + 256 | "Here's an image from the event:\n" + 257 | '![Street Dance Parade](https://i.postimg.cc/xCd4HV0W/614a6c2b-b881-42f2-a8d4-95f8033b55fb.jpg)' 258 | }, 259 | finish_reason: 'stop' 260 | } 261 | ``` 262 | Notice the links and images included in the summary. If you wish to incorporate links and images, remember that relative paths are not recognized. You will also need to use HTTPS URLs. 263 | 264 | Next, we then we call the weather and search for hotel and get more information about the hotel. 265 | 266 | ![Search Hotel](./docs/screenshot02.png) 267 | 268 | ```javascript 269 | // function calling 270 | [ 271 | { 272 | id: 'call_4s5gjt6pRfBrKwcPrgTLmrWH', 273 | type: 'function', 274 | function: { 275 | name: 'get_weather', 276 | arguments: '{"location":"Sapporo","date":"2023-11-24"}' 277 | } 278 | } 279 | ] 280 | 281 | // mock output 282 | [ 283 | { 284 | tool_call_id: 'call_4s5gjt6pRfBrKwcPrgTLmrWH', 285 | role: 'tool', 286 | name: 'get_weather', 287 | content: '{\n' + 288 | ' "location": "Sapporo",\n' + 289 | ' "date": "2023-11-24",\n' + 290 | ' "temperature": 12,\n' + 291 | ' "unit": "celsius",\n' + 292 | ' "condition": "Cloudy"\n' + 293 | '}' 294 | } 295 | ] 296 | 297 | ... 298 | 299 | // function calling 300 | [ 301 | { 302 | id: 'call_tn8TPH7bL7Yx7jOwVngfl8Ai', 303 | type: 'function', 304 | function: { name: 'search_hotel', arguments: '{"location":"Sapporo"}' } 305 | } 306 | ] 307 | 308 | // mock output 309 | [ 310 | { 311 | tool_call_id: 'call_tn8TPH7bL7Yx7jOwVngfl8Ai', 312 | role: 'tool', 313 | name: 'search_hotel', 314 | content: '{\n' + 315 | ' "location": "Sapporo",\n' + 316 | ' "items": [\n' + 317 | ' "The Oak Inn"\n' + 318 | ' ],\n' + 319 | ' "message": "Found 1 hotels"\n' + 320 | '}' 321 | } 322 | ] 323 | 324 | // summary 325 | { 326 | index: 0, 327 | message: { 328 | role: 'assistant', 329 | content: `I found a hotel in Sapporo called "The Oak Inn." If you'd like to know more about this hotel or make a reservation, just let me know!` 330 | }, 331 | finish_reason: 'stop' 332 | } 333 | 334 | // function calling 335 | [ 336 | { 337 | id: 'call_pWxeas45RkvL7HmjJdgCMiua', 338 | type: 'function', 339 | function: { 340 | name: 'get_hotel', 341 | arguments: '{"hotel":"The Oak Inn","location":"Sapporo"}' 342 | } 343 | } 344 | ] 345 | 346 | // mock output 347 | [ 348 | { 349 | tool_call_id: 'call_pWxeas45RkvL7HmjJdgCMiua', 350 | role: 'tool', 351 | name: 'get_hotel', 352 | content: '{\n' + 353 | ' "location": "Sapporo",\n' + 354 | ' "hotel": "The Oak Inn",\n' + 355 | ' "description": "Escape the hustle and bustle of everyday life and immerse yourself in the tranquil ambiance of The Oak Inn.\\nNestled amidst the lush greenery of a secluded paradise, our hotel offers a sanctuary of relaxation and rejuvenation.\\nOur spacious and elegantly appointed rooms provide a haven of comfort, while our attentive staff is dedicated to ensuring your stay is nothing short of exceptional.",\n' + 356 | ' "price": "15,721",\n' + 357 | ' "amenities": [\n' + 358 | ' "business center",\n' + 359 | ' "free wifi",\n' + 360 | ' "free breakfast"\n' + 361 | ' ],\n' + 362 | ' "website": "https://example.com/hotel/the_oak_inn/w8cljupp03olp6l2n43",\n' + 363 | ' "images": [\n' + 364 | ' {\n' + 365 | ' "alt": "The Oak Inn",\n' + 366 | ' "src": "https://i.postimg.cc/Xv4hjytN/dea57a4a-532b-43d2-85bb-0e0172d8c594.jpg"\n' + 367 | ' }\n' + 368 | ' ]\n' + 369 | '}' 370 | } 371 | ] 372 | 373 | // summary 374 | { 375 | index: 0, 376 | message: { 377 | role: 'assistant', 378 | content: '"The Oak Inn" in Sapporo is a tranquil and elegant hotel that offers spacious and elegantly appointed rooms, a business center, free WiFi, and complimentary breakfast. The price for a stay is 15,721 yen. You can find more information about the hotel on their [website](https://example.com/hotel/the_oak_inn/w8cljupp03olp6l2n43).\n' + 379 | '\n' + 380 | "Here's an image of the hotel:\n" + 381 | '![The Oak Inn](https://i.postimg.cc/Xv4hjytN/dea57a4a-532b-43d2-85bb-0e0172d8c594.jpg)\n' + 382 | '\n' + 383 | `If you'd like to make a reservation at "The Oak Inn," please let me know, and I can assist you with that!` 384 | }, 385 | finish_reason: 'stop' 386 | } 387 | ``` 388 | 389 | Unsatisfied, we asked to search for hotel in another location. This is to test how the AI can juggle the parameters. We just changed the location to Kita Hiroshima instead of Sapporo. 390 | 391 | ![Search Hotel](./docs/screenshot03.png) 392 | 393 | ```javascript 394 | // function calling 395 | [ 396 | { 397 | id: 'call_SGy8QVzcX43DyiRo2D801JW0', 398 | type: 'function', 399 | function: { 400 | name: 'search_hotel', 401 | arguments: '{"location":"kita hiroshima"}' 402 | } 403 | } 404 | ] 405 | 406 | // mock output 407 | [ 408 | { 409 | tool_call_id: 'call_SGy8QVzcX43DyiRo2D801JW0', 410 | role: 'tool', 411 | name: 'search_hotel', 412 | content: '{\n' + 413 | ' "location": "kita hiroshima",\n' + 414 | ' "items": [\n' + 415 | ' "Emerald Sakura Guesthouse",\n' + 416 | ' "Great River Suites"\n' + 417 | ' ],\n' + 418 | ' "message": "Found 2 hotels"\n' + 419 | '}' 420 | } 421 | ] 422 | 423 | // summary 424 | { 425 | index: 0, 426 | message: { 427 | role: 'assistant', 428 | content: `I also found two hotels in Kita Hiroshima: "Emerald Sakura Guesthouse" and "Great River Suites." If you'd like more information about these hotels or if you have any other preferences, feel free to let me know!` 429 | }, 430 | finish_reason: 'stop' 431 | } 432 | ``` 433 | 434 | Given the list of hotels, we asked for the cheapest hotel from the result. 435 | Now, we see the parallel function calling, where two `get_hotel` are called at the same time. 436 | I was hoping that the AI will determine the cheapest itself and just show me the cheapest one. 437 | However, it just displayed the result from the parallel function calling. 438 | 439 | ```javascript 440 | // function calling 441 | [ 442 | { 443 | id: 'call_MVxd99QRk1qvvqGSUmHUwnrW', 444 | type: 'function', 445 | function: { 446 | name: 'get_hotel', 447 | arguments: '{"hotel": "Emerald Sakura Guesthouse", "location": "Kita Hiroshima"}' 448 | } 449 | }, 450 | { 451 | id: 'call_0SwpNflkRGfwkVjCbFSdCmiQ', 452 | type: 'function', 453 | function: { 454 | name: 'get_hotel', 455 | arguments: '{"hotel": "Great River Suites", "location": "Kita Hiroshima"}' 456 | } 457 | } 458 | ] 459 | 460 | // mock output 461 | [ 462 | { 463 | tool_call_id: 'call_MVxd99QRk1qvvqGSUmHUwnrW', 464 | role: 'tool', 465 | name: 'get_hotel', 466 | content: '{\n' + 467 | ' "location": "Kita Hiroshima",\n' + 468 | ' "hotel": "Emerald Sakura Guesthouse",\n' + 469 | ' "message": "Hotel information not found"\n' + 470 | '}' 471 | }, 472 | { 473 | tool_call_id: 'call_0SwpNflkRGfwkVjCbFSdCmiQ', 474 | role: 'tool', 475 | name: 'get_hotel', 476 | content: '{\n' + 477 | ' "location": "Kita Hiroshima",\n' + 478 | ' "hotel": "Great River Suites",\n' + 479 | ' "description": "Escape the hustle and bustle of everyday life and immerse yourself in the tranquil ambiance of Great River Suites.\\nNestled amidst the lush greenery of a secluded paradise, our hotel offers a sanctuary of relaxation and rejuvenation.\\nOur spacious and elegantly appointed rooms provide a haven of comfort, while our attentive staff is dedicated to ensuring your stay is nothing short of exceptional.",\n' + 480 | ' "price": "18,152",\n' + 481 | ' "amenities": [\n' + 482 | ' "pool",\n' + 483 | ' "spa",\n' + 484 | ' "fitness center",\n' + 485 | ' "business center"\n' + 486 | ' ],\n' + 487 | ' "website": "https://example.com/hotel/great_river_suites/cchja5h3oeslp6l4yor",\n' + 488 | ' "images": [\n' + 489 | ' {\n' + 490 | ' "alt": "Great River Suites",\n' + 491 | ' "src": "https://i.postimg.cc/jjc1LSrH/d5592424-e3f0-4dfa-afb2-2dcc7308e321.jpg"\n' + 492 | ' }\n' + 493 | ' ]\n' + 494 | '}' 495 | } 496 | ] 497 | 498 | // summary 499 | { 500 | index: 0, 501 | message: { 502 | role: 'assistant', 503 | content: 'I found two hotels in Kita Hiroshima:\n' + 504 | '\n' + 505 | "1. **Emerald Sakura Guesthouse**: Unfortunately, I couldn't find detailed information about this hotel.\n" + 506 | '\n' + 507 | '2. **Great River Suites**: This hotel offers a tranquil ambiance, spacious and elegantly appointed rooms, a pool, spa, fitness center, and a business center. The price for a stay is 18,152 yen. You can find more information about the hotel on their [website](https://example.com/hotel/great_river_suites/cchja5h3oeslp6l4yor).\n' + 508 | '\n' + 509 | "Here's an image of Great River Suites:\n" + 510 | '![Great River Suites](https://i.postimg.cc/jjc1LSrH/d5592424-e3f0-4dfa-afb2-2dcc7308e321.jpg)\n' + 511 | '\n' + 512 | "If you'd like to know more about these hotels or make a reservation, feel free to ask!" 513 | }, 514 | finish_reason: 'stop' 515 | } 516 | ``` 517 | 518 | Now, to test how it remembers the previous calls, we ask for reservation to the hotel in Sapporo. 519 | 520 | ![Search Hotel](./docs/screenshot04.png) 521 | 522 | ```javascript 523 | // function calling 524 | [ 525 | { 526 | id: 'call_cTHFCY2CXK0fDFt807zGlSbd', 527 | type: 'function', 528 | function: { 529 | name: 'reserve_hotel', 530 | arguments: `{"hotel":"The Oak Inn","location":"Sapporo","fullName":"[user's full name]","numberOfGuests":1,"checkInDate":"2023-11-24","checkOutDate":"2023-11-25","roomType":"single","specialRequests":["non-smoking room","quiet room"]}` 531 | } 532 | } 533 | ] 534 | 535 | // mock output 536 | [ 537 | { 538 | tool_call_id: 'call_cTHFCY2CXK0fDFt807zGlSbd', 539 | role: 'tool', 540 | name: 'reserve_hotel', 541 | content: '{\n' + 542 | ' "status": "No name provided",\n' + 543 | ' "message": "Please ask user provide your full name",\n' + 544 | ' "hotel": "The Oak Inn",\n' + 545 | ' "location": "Sapporo",\n' + 546 | ' "numberOfGuests": 1,\n' + 547 | ' "checkInDate": "2023-11-24",\n' + 548 | ' "checkOutDate": "2023-11-25",\n' + 549 | ' "roomType": "single",\n' + 550 | ' "specialRequests": [\n' + 551 | ' "non-smoking room",\n' + 552 | ' "quiet room"\n' + 553 | ' ]\n' + 554 | '}' 555 | } 556 | ] 557 | 558 | // summary 559 | { 560 | index: 0, 561 | message: { 562 | role: 'assistant', 563 | content: 'It seems that I need your full name to proceed with the hotel reservation. Could you please provide me with your full name?' 564 | }, 565 | finish_reason: 'stop' 566 | } 567 | ``` 568 | 569 | It remembers! But `reserve_hotel` function needs the user full name. 570 | The main reason that the AI are prompting the user for their full name is the mock result from `reserve_hotel`. 571 | 572 | ```javascript 573 | ... 574 | content: '{\n' + 575 | ' "status": "No name provided",\n' + 576 | ' "message": "Please ask user provide your full name",\n' + 577 | ' "hotel": "The Oak Inn",\n' + 578 | ' "location": "Sapporo",\n' + 579 | ' "numberOfGuests": 1,\n' + 580 | ' "checkInDate": "2023-11-24",\n' + 581 | ' "checkOutDate": "2023-11-25",\n' + 582 | ' "roomType": "single",\n' + 583 | ' "specialRequests": [\n' + 584 | ' "non-smoking room",\n' + 585 | ' "quiet room"\n' + 586 | ' ]\n' + 587 | '}' 588 | ``` 589 | 590 | We provided `status` and `message` to explicitly tell the AI that we need the name. 591 | 592 | ```javascript 593 | // function calling 594 | [ 595 | { 596 | id: 'call_zZIFNSiitRAsC0dbGkZRRKkr', 597 | type: 'function', 598 | function: { 599 | name: 'reserve_hotel', 600 | arguments: '{"hotel":"The Oak Inn","location":"Sapporo","fullName":"Aoyama Yuta","numberOfGuests":2,"checkInDate":"2023-11-24","checkOutDate":"2023-11-25","roomType":"double","specialRequests":["wake-up call"]}' 601 | } 602 | } 603 | ] 604 | 605 | // mock output 606 | [ 607 | { 608 | tool_call_id: 'call_zZIFNSiitRAsC0dbGkZRRKkr', 609 | role: 'tool', 610 | name: 'reserve_hotel', 611 | content: '{\n' + 612 | ' "status": "Reservation successful",\n' + 613 | ' "reservationId": "jq7o28a2ngelp6l9f6c",\n' + 614 | ' "message": "Your reservation has been completed. Please present your reservationId at the front desk.",\n' + 615 | ' "hotel": "The Oak Inn",\n' + 616 | ' "location": "Sapporo",\n' + 617 | ' "fullName": "Aoyama Yuta",\n' + 618 | ' "numberOfGuests": 2,\n' + 619 | ' "checkInDate": "2023-11-24",\n' + 620 | ' "checkOutDate": "2023-11-25",\n' + 621 | ' "roomType": "double",\n' + 622 | ' "specialRequests": [\n' + 623 | ' "wake-up call"\n' + 624 | ' ]\n' + 625 | '}' 626 | } 627 | ] 628 | 629 | // summary 630 | { 631 | index: 0, 632 | message: { 633 | role: 'assistant', 634 | content: 'Your reservation at "The Oak Inn" in Sapporo has been successfully completed. Your reservation ID is jq7o28a2ngelp6l9f6c. Please present this reservation ID at the front desk when you arrive. If you need any further assistance or information, feel free to ask!' 635 | }, 636 | finish_reason: 'stop' 637 | } 638 | ``` 639 | 640 | The parallel function call illustrated above uses the same function. Let's give another example wherein the functions are different. 641 | 642 | > user: what is happening in sapporo on saturday and will it rain that day? 643 | 644 | ```javascript 645 | // function calling 646 | [ 647 | { 648 | id: 'call_TS9XlJ4SOO3c8PAJTEycoqyf', 649 | type: 'function', 650 | function: { 651 | name: 'get_events', 652 | arguments: '{"location": "Sapporo", "date": "2023-11-25"}' 653 | } 654 | }, 655 | { 656 | id: 'call_1tWqZRdOwIvr6NVULIKTxsNA', 657 | type: 'function', 658 | function: { 659 | name: 'get_weather', 660 | arguments: '{"location": "Sapporo", "date": "2023-11-25"}' 661 | } 662 | } 663 | ] 664 | 665 | // mock output 666 | [ 667 | { 668 | tool_call_id: 'call_TS9XlJ4SOO3c8PAJTEycoqyf', 669 | role: 'tool', 670 | name: 'get_events', 671 | content: '{\n' + 672 | ' "location": "Sapporo",\n' + 673 | ' "date": "2023-11-25",\n' + 674 | ' "event": "Soul Food Festival"\n' + 675 | '}' 676 | }, 677 | { 678 | tool_call_id: 'call_1tWqZRdOwIvr6NVULIKTxsNA', 679 | role: 'tool', 680 | name: 'get_weather', 681 | content: '{\n' + 682 | ' "location": "Sapporo",\n' + 683 | ' "date": "2023-11-25",\n' + 684 | ' "temperature": 4,\n' + 685 | ' "unit": "celsius",\n' + 686 | ' "condition": "Cloudy"\n' + 687 | '}' 688 | } 689 | ] 690 | 691 | // summary 692 | { 693 | index: 0, 694 | message: { 695 | role: 'assistant', 696 | content: 'The Soul Food Festival is happening in Sapporo on November 25, 2023. The weather forecast for Sapporo on the same day is 4°C with cloudy conditions.' 697 | }, 698 | finish_reason: 'stop' 699 | } 700 | ``` 701 | 702 | Here, we asked for the event and weather in a location at the same time. 703 | If you noticed, the two functions share the same parameters. What if the parameters are different? 704 | 705 | > user: What is happening in Otaru on Friday and what is the weather in Asahikawa tomorrow? 706 | 707 | ```javascript 708 | // function calling 709 | [ 710 | { 711 | id: 'call_vu0Uu7EPO3TOB88mTEP2LxCg', 712 | type: 'function', 713 | function: { 714 | name: 'get_events', 715 | arguments: '{"location": "Otaru", "date": "2023-11-24"}' 716 | } 717 | }, 718 | { 719 | id: 'call_I5qaXXhR7i4wfqlCLj8GTxnB', 720 | type: 'function', 721 | function: { 722 | name: 'get_weather', 723 | arguments: '{"location": "Asahikawa", "date": "2023-11-22"}' 724 | } 725 | } 726 | ] 727 | 728 | // mock output 729 | [ 730 | { 731 | tool_call_id: 'call_vu0Uu7EPO3TOB88mTEP2LxCg', 732 | role: 'tool', 733 | name: 'get_events', 734 | content: '"{\\"location\\":\\"Otaru\\",\\"date\\":\\"2023-11-24\\",\\"event\\":\\"Street Dance Parade\\"}"' 735 | }, 736 | { 737 | tool_call_id: 'call_I5qaXXhR7i4wfqlCLj8GTxnB', 738 | role: 'tool', 739 | name: 'get_weather', 740 | content: '"{\\"location\\":\\"Asahikawa\\",\\"date\\":\\"2023-11-22\\",\\"temperature\\":7,\\"unit\\":\\"celsius\\",\\"condition\\":\\"Sunny\\"}"' 741 | } 742 | ] 743 | 744 | // summary 745 | { 746 | index: 0, 747 | message: { 748 | role: 'assistant', 749 | content: 'The Street Dance Parade is happening in Otaru on November 24, 2023. The weather forecast for Asahikawa on November 22, 2023, is sunny with a temperature of 7°C.' 750 | }, 751 | finish_reason: 'stop' 752 | } 753 | ``` 754 | 755 | 756 | # Assistants API 757 | 758 | Select the Assistants API in drop down to run the function calling demo using Assistants API. 759 | You need to create the Assistants in the [dev page](https://platform.openai.com/assistants) first and copy the ***Assistant id*** to the [.env file](#setup). 760 | I do not want to create dynamic assistants. This way you can also test your Assistant in the [Playground](https://platform.openai.com/playground). 761 | 762 | ![TravelBuddyGPT](./docs/screenshot05.png) 763 | 764 | For this example, you may want to copy the system prompt 765 | 766 | ```md 767 | You are a helpful travel assistant named TravelBuddyGPT. 768 | You will provide the user with helpful information related to their queries about events, searching for hotel/accommodation, making hotel reservation and checking weather. You are equipped with the following functions for these tasks: 769 | - get_weather, when the user wants to know the weather forecast given a location and date. 770 | - get_events, when the user wants to know events happening in a given location and date. 771 | - get_event, when the user wants to know more about a particular event. 772 | - search_hotel, when the user wants to search for hotel based on given location. 773 | - get_hotel, when the user wants to know more about a particular hotel. 774 | - reserve_hotel, when the user wants to make room reservation for a particular hotel. 775 | - get_reservation, when the user wants to get the details of their reservation. 776 | When making reservation to hotel, make sure that the required information are filled up. 777 | If user has not provided any name, ask them for their full name before calling reserver_hotel. 778 | ``` 779 | 780 | Then add the functions one by one by copying the content of each function JSON schema from [lib](/lib/) directory. 781 | Sorry, currently, there is no easier way to do this. 782 | 783 | ![get_weather](./docs/screenshot06.png) 784 | 785 | Then run the app again and test it similar to what we did in Chat Completions. 786 | Please note that it takes more time compared to Chat Completions and often ends in failure. 787 | Check and monitor the console log in command console (not the browser's) to see everything. 788 | 789 | Be sure to delete the thread after you use it since currently we have no way of seeing all abandoned and undeleted threads. Use the blue **Reset** button to delete the ***thread id*** during Assistants API mode. 790 | 791 | Using Assistants API is pretty straight-forward 792 | 793 | ```javascript 794 | // retrieve assistant to get the instruction 795 | const assistant = await openai.beta.assistants.retrieve(process.env.OPENAI_ASSISTANT_ID) 796 | assistant_instructions = assistant.instructions 797 | 798 | // if thread_id exist, we check it if it is viable 799 | // otherwise, we create a new one 800 | if(thread_id) { 801 | 802 | const exist_thread = await openai.beta.threads.retrieve(thread_id) 803 | 804 | if(exist_thread.error) { 805 | thread_id = '' 806 | } 807 | 808 | } 809 | 810 | if(!thread_id) { 811 | 812 | const new_thread = await openai.beta.threads.create() 813 | 814 | thread_id = new_thread.id 815 | 816 | } 817 | 818 | // now, add the message to the thread 819 | let metadata = { 'id': message_id } 820 | 821 | let message = { role: 'user', content: message, metadata } 822 | 823 | const message = await openai.beta.threads.messages.create(thread_id, message) 824 | ``` 825 | 826 | Notice the [metadata](https://platform.openai.com/docs/api-reference/messages/object) included in the message. We will use it as reference point to mark the last message when we retrieve the messages later. 827 | 828 | Now, we run the thread. Here we append today's date information to the main instruction for the AI to know current date which is important in our functions. Please be aware that if you do not add the main instruction and just set a new prompt, [it will override it](https://platform.openai.com/docs/assistants/overview). 829 | 830 | ```javascript 831 | const run = await openai.beta.threads.runs.create( 832 | thread_id, 833 | { 834 | assistant_id: process.env.OPENAI_ASSISTANT_ID, 835 | instructions: assistant_instructions + `Today is ${new Date()}` 836 | } 837 | ) 838 | ``` 839 | 840 | Then we check the run status by every interval using a ***do while loop*** with a wait function. 841 | 842 | ```javascript 843 | do { 844 | 845 | const run = await openai.beta.threads.runs.retrieve(thread_id, run_id) 846 | 847 | if(run.status === 'completed') { 848 | // retrieve messages 849 | isCompleted = true 850 | } else if(run.status === 'requires_action') { 851 | // process function calling 852 | } else if(run.status === 'expired' || run.status === 'cancelled' || run.status === 'failed') { 853 | isCompleted = true 854 | } 855 | 856 | } while(!isCompleted) 857 | ``` 858 | 859 | When the status is `requires_action`, it means the AI is invoking our functions. 860 | Get the `required_action` property of the run object and check the `submit_tools_outputs` property. 861 | 862 | ```javascript 863 | const required_action = run.required_action 864 | const required_tools = required_action.submit_tool_outputs.tool_calls 865 | 866 | let tool_output_items = [] 867 | 868 | required_tools.forEach((rtool) => { 869 | 870 | const function_name = rtool.function.name 871 | const tool_args = JSON.parse(rtool.function.arguments) 872 | 873 | // get result from external API 874 | let tool_output = ... 875 | 876 | tool_output_items.push({ 877 | tool_call_id: rtool.id, 878 | output: JSON.stringify(tool_output) 879 | }) 880 | 881 | } 882 | ``` 883 | 884 | Then we submit the function call result back to the run and continue the loop. 885 | 886 | ```javascript 887 | const ret = await openai.beta.threads.runs.submitToolOutputs( 888 | thread_id, 889 | run_id, 890 | { 891 | tool_outputs: tool_outputs, 892 | } 893 | ) 894 | ``` 895 | 896 | It will take a while but once the status is `completed` then we can now retrieve the messages. This will include all the messages. 897 | 898 | ```javascript 899 | const messages = await openai.beta.threads.messages.list(thread_id) 900 | ``` 901 | 902 | Remember the metadata? To get only the new messages 903 | 904 | ```javascript 905 | let new_messages = [] 906 | 907 | for(let i = 0; i < messages.length; i++) { 908 | 909 | const msg = messages[i] 910 | 911 | if(messages[i].metadata.id === message_id) { 912 | break 913 | } else { 914 | new_messages.push({ 915 | id: msg.id, 916 | created_at: msg.created_at, 917 | role: msg.role, 918 | content: msg.content[0].text.value 919 | }) 920 | } 921 | } 922 | ``` 923 | 924 | This works because the messages are arranged in descending order of creations. 925 | So newest comes first until we hit the one marked by metadata. 926 | 927 | 928 | # Setup 929 | 930 | Clone the repository and install the dependencies 931 | 932 | ```sh 933 | git clone https://github.com/supershaneski/openai-api-function-call-sample.git myproject 934 | 935 | cd myproject 936 | 937 | npm install 938 | ``` 939 | 940 | Copy `.env.example` and rename it to `.env` then edit the `OPENAI_API_KEY` and use your own `OpenAI API key`. 941 | If you also want to use the **Assistants API**, please edit `OPENAI_ASSISTANT_ID` with your actual Assistant ID shown in the [Assistants page](https://platform.openai.com/assistants). 942 | 943 | ```javascript 944 | OPENAI_API_KEY=YOUR-OPENAI-API-KEY 945 | OPENAI_ASSISTANT_ID=YOUR-OPENAI-ASSISTANT-ID 946 | ``` 947 | 948 | Then run the app 949 | 950 | ```sh 951 | npm run dev 952 | ``` 953 | 954 | Open your browser to `http://localhost:4000/` to load the application page. 955 | -------------------------------------------------------------------------------- /app/assistant/message/route.js: -------------------------------------------------------------------------------- 1 | import { 2 | getAssistant, 3 | getThread, 4 | createThread, 5 | addMessage, 6 | getMessages, 7 | startRun, 8 | getRun, 9 | submitOutputs 10 | } from '../../../service/openai' 11 | import { wait } from '../../../lib/utils' 12 | import { callMockAPI } from '../../../lib/mockapi' 13 | 14 | export async function POST(request) { 15 | 16 | const { inquiry, threadId, messageId } = await request.json() 17 | 18 | if (!inquiry, !messageId) { 19 | return new Response('Bad request', { 20 | status: 400, 21 | }) 22 | } 23 | 24 | console.log("start assistant...", threadId, inquiry, (new Date()).toLocaleTimeString()) 25 | 26 | let thread_id = threadId ? threadId : '' 27 | let assistant_instructions = '' 28 | let messages_items = [] 29 | 30 | try { 31 | 32 | const assistant = await getAssistant() 33 | assistant_instructions = assistant.instructions 34 | 35 | if(thread_id) { 36 | 37 | const exist_thread = await getThread({ threadId: thread_id }) 38 | 39 | console.log('get-thread', exist_thread) 40 | 41 | if(exist_thread.error) { 42 | thread_id = '' 43 | } 44 | 45 | } 46 | 47 | if(!thread_id) { 48 | 49 | const new_thread = await createThread() 50 | 51 | console.log('new-thread', new_thread) 52 | 53 | thread_id = new_thread.id 54 | 55 | } 56 | 57 | const message = await addMessage({ threadId: thread_id, message: inquiry, messageId: messageId }) 58 | 59 | console.log('message', message) 60 | 61 | const run = await startRun({ 62 | threadId: thread_id, 63 | instructions: assistant_instructions + `\nToday is ${new Date()}.` 64 | }) 65 | 66 | console.log('run', run) 67 | 68 | const run_id = run.id 69 | 70 | let flagFinish = false 71 | 72 | let MAX_COUNT = 2 * 600 // 120s 73 | let TIME_DELAY = 100 // 100ms 74 | let count = 0 75 | 76 | do { 77 | 78 | console.log(`Loop: ${count}`) 79 | 80 | const run_data = await getRun({ threadId: thread_id, runId: run_id }) 81 | 82 | const status = run_data.status 83 | 84 | console.log(`Status: ${status} ${(new Date()).toLocaleTimeString()}`) 85 | 86 | if(status === 'completed') { 87 | 88 | const messages = await getMessages({ threadId: thread_id }) 89 | 90 | //console.log('all-messages', messages) 91 | 92 | let new_messages = [] 93 | 94 | for(let i = 0; i < messages.length; i++) { 95 | if (Object.prototype.hasOwnProperty.call(messages[i].metadata, 'id')) { 96 | if(messages[i].metadata.id === messageId) { 97 | break // last message 98 | } 99 | } else { 100 | new_messages.push(messages[i]) 101 | } 102 | } 103 | 104 | console.log('new-messages', new_messages) 105 | 106 | messages_items = new_messages 107 | 108 | flagFinish = true 109 | 110 | } else if(status === 'requires_action'){ 111 | 112 | console.log('run-data', run_data) 113 | 114 | const required_action = run_data.required_action 115 | const required_tools = required_action.submit_tool_outputs.tool_calls 116 | 117 | console.log('required-action', required_action) 118 | console.log('required-tools', required_tools) 119 | 120 | let tool_output_items = [] 121 | 122 | for(let rtool of required_tools) { 123 | 124 | const function_name = rtool.function.name 125 | const tool_args = JSON.parse(rtool.function.arguments) 126 | 127 | console.log("-", function_name, tool_args) 128 | 129 | let tool_output = callMockAPI(function_name, tool_args) 130 | 131 | tool_output_items.push({ 132 | tool_call_id: rtool.id, 133 | output: JSON.stringify(tool_output) 134 | }) 135 | 136 | } 137 | 138 | /* 139 | required_tools.forEach((rtool) => { 140 | 141 | const function_name = rtool.function.name 142 | const tool_args = JSON.parse(rtool.function.arguments) 143 | 144 | let tool_output = callMockAPI(function_name, tool_args) 145 | 146 | tool_output_items.push({ 147 | tool_call_id: rtool.id, 148 | output: JSON.stringify(tool_output) 149 | }) 150 | 151 | }) 152 | */ 153 | 154 | console.log('tools-output', tool_output_items) 155 | 156 | const ret_tool = await submitOutputs({ 157 | threadId: thread_id, 158 | runId: run_id, 159 | tool_outputs: tool_output_items 160 | }) 161 | 162 | console.log('ret-tool', ret_tool) 163 | 164 | } else if(status === 'expired' || status === 'cancelled' || status === 'failed') { 165 | 166 | flagFinish = true 167 | 168 | } 169 | 170 | if(!flagFinish) { 171 | 172 | count++ 173 | 174 | if(count >= MAX_COUNT) { 175 | 176 | flagFinish = true 177 | 178 | } else { 179 | 180 | await wait(TIME_DELAY) 181 | 182 | } 183 | 184 | } 185 | 186 | } while(!flagFinish) 187 | 188 | } catch(error) { 189 | 190 | console.log("assistant-error", error.name, error.message) 191 | 192 | } 193 | 194 | return new Response(JSON.stringify({ 195 | threadId: thread_id, 196 | messages: messages_items, 197 | }), { 198 | status: 200, 199 | }) 200 | 201 | } -------------------------------------------------------------------------------- /app/assistant/thread/route.js: -------------------------------------------------------------------------------- 1 | import { deleteThread } from '../../../service/openai' 2 | 3 | export async function POST(request) { 4 | 5 | const { threadId } = await request.json() 6 | 7 | if(!threadId) { 8 | return new Response('Bad request', { 9 | status: 400, 10 | }) 11 | } 12 | 13 | console.log("delete thread", threadId, (new Date()).toLocaleTimeString()) 14 | 15 | let result = null 16 | 17 | try { 18 | 19 | result = await deleteThread({ threadId }) 20 | 21 | console.log(result) 22 | 23 | } catch(error) { 24 | 25 | console.log(error.name, error.message) 26 | 27 | result = { error: true, message: error.message } 28 | 29 | } 30 | 31 | return new Response(JSON.stringify({ 32 | result 33 | }), { 34 | status: 200 35 | }) 36 | 37 | } -------------------------------------------------------------------------------- /app/chat/function/route.js: -------------------------------------------------------------------------------- 1 | import { chatCompletion } from '../../../service/openai' 2 | import { callMockAPI } from '../../../lib/mockapi' 3 | import { trim_array } from '../../../lib/utils' 4 | import get_weather from '../../../lib/get_weather.json' 5 | import get_events from '../../../lib/get_events.json' 6 | import get_event from '../../../lib/get_event.json' 7 | import search_hotel from '../../../lib/search_hotel.json' 8 | import get_hotel from '../../../lib/get_hotel.json' 9 | import reserve_hotel from '../../../lib/reserve_hotel.json' 10 | import get_reservation from '../../../lib/get_reservation.json' 11 | 12 | export async function POST(request) { 13 | 14 | const { tools, previous } = await request.json() 15 | 16 | if (!Array.isArray(tools) || !Array.isArray(previous)) { 17 | return new Response('Bad system prompt', { 18 | status: 400, 19 | }) 20 | } 21 | 22 | console.log('tool_calls', tools, (new Date()).toLocaleTimeString()) 23 | 24 | let tools_output = [] 25 | 26 | for(let tool of tools) { 27 | 28 | let tool_args = JSON.parse(tool.function.arguments) 29 | 30 | const tool_output = callMockAPI(tool.function.name, tool_args) 31 | 32 | tools_output.push({ 33 | tool_call_id: tool.id, 34 | role: 'tool', 35 | name: tool.function.name, 36 | content: JSON.stringify(tool_output, null, 2) 37 | }) 38 | 39 | } 40 | 41 | console.log('tools-api-output', tools_output) 42 | 43 | let context = trim_array(previous, 20) 44 | 45 | const today = new Date() 46 | 47 | const system_prompt = `You are a helpful personal assistant.\n\n` + 48 | `# Tools\n` + 49 | `You have the following tools that you can invoke based on the user inquiry.\n` + 50 | `- get_weather, when the user wants to know the weather forecast given a location and date.\n` + 51 | `- get_events, when the user wants to know events happening in a given location and date.\n` + 52 | `- get_event, when the user wants to know more about a particular event.\n` + 53 | `- search_hotel, when the user wants to search for hotel based on given location.\n` + 54 | `- get_hotel, when the user wants to know more about a particular hotel.\n` + 55 | `- reserve_hotel, when the user wants to make room reservation for a particular hotel.\n` + 56 | `- get_reservation, when the user wants to get the details of their reservation.\n` + 57 | `When the user is making hotel reservation, be sure to guide the user to fill up all required information.\n` + 58 | `When you fill up some of the required information yourself, be sure to confirm to user before proceeding.\n` + 59 | `Aside from the listed functions above, answer all other inquiries by telling the user that it is out of scope of your ability.\n\n` + 60 | `# User\n` + 61 | `If my full name is needed, please ask me for my full name.\n\n` + 62 | `# Language Support\n` + 63 | `Please reply in the language used by the user.\n\n` + 64 | `Today is ${today}` 65 | 66 | let messages = [{ role: 'system', content: system_prompt }] 67 | if(context.length > 0) { 68 | messages = messages.concat(context) 69 | } 70 | 71 | messages.push({ role: 'assistant', content: null, tool_calls: tools }) 72 | for(let output_item of tools_output) { 73 | messages.push(output_item) 74 | } 75 | 76 | let result_message = null 77 | 78 | try { 79 | 80 | let result = await chatCompletion({ 81 | temperature: 0.3, 82 | messages, 83 | tools: [ 84 | { type: 'function', function: get_weather }, 85 | { type: 'function', function: get_events }, 86 | { type: 'function', function: get_event }, 87 | { type: 'function', function: search_hotel }, 88 | { type: 'function', function: get_hotel }, 89 | { type: 'function', function: reserve_hotel }, 90 | { type: 'function', function: get_reservation }, 91 | ] 92 | }) 93 | 94 | console.log('chat-summary', result) 95 | 96 | result_message = result.message 97 | 98 | } catch(error) { 99 | 100 | console.log(error.name, error.message) 101 | 102 | } 103 | 104 | return new Response(JSON.stringify({ 105 | output: result_message, 106 | }), { 107 | status: 200, 108 | }) 109 | 110 | } -------------------------------------------------------------------------------- /app/chat/message/route.js: -------------------------------------------------------------------------------- 1 | import { chatCompletion } from '../../../service/openai' 2 | import { trim_array } from '../../../lib/utils' 3 | import get_weather from '../../../lib/get_weather.json' 4 | import get_events from '../../../lib/get_events.json' 5 | import get_event from '../../../lib/get_event.json' 6 | import search_hotel from '../../../lib/search_hotel.json' 7 | import get_hotel from '../../../lib/get_hotel.json' 8 | import reserve_hotel from '../../../lib/reserve_hotel.json' 9 | import get_reservation from '../../../lib/get_reservation.json' 10 | 11 | export async function POST(request) { 12 | 13 | const { inquiry, previous } = await request.json() 14 | 15 | if (!inquiry || !Array.isArray(previous)) { 16 | return new Response('Bad system prompt', { 17 | status: 400, 18 | }) 19 | } 20 | 21 | console.log('user', inquiry, (new Date()).toLocaleTimeString()) 22 | 23 | let context = trim_array(previous, 20) 24 | 25 | const today = new Date() 26 | 27 | const system_prompt = `You are a helpful personal assistant.\n\n` + 28 | `# Tools\n` + 29 | `You have the following tools that you can invoke based on the user inquiry.\n` + 30 | `- get_weather, when the user wants to know the weather forecast given a location and date.\n` + 31 | `- get_events, when the user wants to know events happening in a given location and date.\n` + 32 | `- get_event, when the user wants to know more about a particular event.\n` + 33 | `- search_hotel, when the user wants to search for hotel based on given location.\n` + 34 | `- get_hotel, when the user wants to know more about a particular hotel.\n` + 35 | `- reserve_hotel, when the user wants to make room reservation for a particular hotel.\n` + 36 | `- get_reservation, when the user wants to get the details of their reservation.\n` + 37 | `When the user is making hotel reservation, be sure to guide the user to fill up all required information.\n` + 38 | `When you fill up some of the required information yourself, be sure to confirm to user before proceeding.\n` + 39 | `Aside from the listed functions above, answer all other inquiries by telling the user that it is out of scope of your ability.\n\n` + 40 | `# User\n` + 41 | `If my full name is needed, please ask me for my full name.\n\n` + 42 | `# Language Support\n` + 43 | `Please reply in the language used by the user.\n\n` + 44 | `Today is ${today}` 45 | 46 | let messages = [{ role: 'system', content: system_prompt }] 47 | if(context.length > 0) { 48 | messages = messages.concat(context) 49 | } 50 | messages.push({ role: 'user', content: inquiry }) 51 | 52 | let result_message = null 53 | 54 | try { 55 | 56 | let result = await chatCompletion({ 57 | temperature: 0.3, 58 | messages, 59 | tools: [ 60 | { type: 'function', function: get_weather }, 61 | { type: 'function', function: get_events }, 62 | { type: 'function', function: get_event }, 63 | { type: 'function', function: search_hotel }, 64 | { type: 'function', function: get_hotel }, 65 | { type: 'function', function: reserve_hotel }, 66 | { type: 'function', function: get_reservation }, 67 | ] 68 | }) 69 | 70 | console.log('function-calling', result) 71 | 72 | result_message = result.message 73 | 74 | } catch(error) { 75 | 76 | console.log(error.name, error.message) 77 | 78 | } 79 | 80 | return new Response(JSON.stringify({ 81 | output: result_message, 82 | }), { 83 | status: 200, 84 | }) 85 | 86 | } -------------------------------------------------------------------------------- /app/layout.js: -------------------------------------------------------------------------------- 1 | import '../assets/main.css' 2 | 3 | import '@fontsource/roboto/300.css' 4 | import '@fontsource/roboto/400.css' 5 | import '@fontsource/roboto/500.css' 6 | import '@fontsource/roboto/700.css' 7 | 8 | export default function RootLayout({ children }) { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } -------------------------------------------------------------------------------- /app/page.jsx: -------------------------------------------------------------------------------- 1 | import Sandbox from "./sandbox" 2 | 3 | export const metadata = { 4 | title: process.env.siteTitle, 5 | description: 'A sample React app built using Next.JS powered by OpenAI API', 6 | viewport: 'maximum-scale=1.0, minimum-scale=1.0, initial-scale=1.0, width=device-width, user-scalable=0', 7 | icons: { 8 | icon: '/logo192.png', 9 | shortcut: '/logo192.png', 10 | } 11 | } 12 | 13 | export default function Page() { 14 | return 15 | } -------------------------------------------------------------------------------- /app/sandbox.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { createPortal } from 'react-dom' 5 | 6 | import NoSsr from '@mui/base/NoSsr' 7 | import Box from '@mui/material/Box' 8 | import TextField from '@mui/material/TextField' 9 | import InputAdornment from '@mui/material/InputAdornment' 10 | import IconButton from '@mui/material/IconButton' 11 | //import Button from '@mui/material/Button' 12 | import Fab from '@mui/material/Fab' 13 | 14 | import ResetIcon from '@mui/icons-material/RestartAlt' 15 | import ClearIcon from '@mui/icons-material/Clear' 16 | import SendIcon from '@mui/icons-material/Send' 17 | import SettingsIcon from '@mui/icons-material/Settings' 18 | //import DeleteIcon from '@mui/icons-material/DeleteForever' 19 | import PersonIcon from '@mui/icons-material/AccountCircle' 20 | 21 | import FormControl from '@mui/material/FormControl' 22 | import Select from '@mui/material/Select' 23 | import MenuItem from '@mui/material/MenuItem' 24 | 25 | import Markdown from 'react-markdown' 26 | 27 | import OpenAiIcon from '../components/openailogo' 28 | import LoadingText from '../components/loadingtext' 29 | //import Loader from '../components/loader' 30 | import Dialog from '../components/dialog' 31 | 32 | import { getUniqueId } from '../lib/utils' 33 | 34 | import useAppStore from '../stores/appStore' 35 | 36 | import classes from './sandbox.module.css' 37 | 38 | const FunctionTypes = [ 39 | { name: 'Using Chat Completions API', description: '' }, 40 | { name: 'Using Assistants API', description: 'You need to configure the Assistant in the Playground' } 41 | ] 42 | 43 | export default function Sandbox() { 44 | 45 | const storedMessages = useAppStore((state) => state.messages) 46 | const storedMode = useAppStore((state) => state.mode) 47 | const setMode = useAppStore((state) => state.setMode) 48 | const addMessage = useAppStore((state) => state.addMessage) 49 | const clearMessages = useAppStore((state) => state.clearMessages) 50 | 51 | const threadId = useAppStore((state) => state.threadId) 52 | const setThreadId = useAppStore((state) => state.setThreadId) 53 | 54 | const messageRef = React.useRef(null) 55 | const inputRef = React.useRef(null) 56 | 57 | const [isMounted, setMounted] = React.useState(false) 58 | 59 | const [loading, setLoading] = React.useState(false) 60 | const [inputText, setInputText] = React.useState('') 61 | const [messageItems, setMessageItems] = React.useState([]) 62 | 63 | const [funcType, setFuncType] = React.useState(0) 64 | 65 | const [isDialogShown, setDialogShown] = React.useState(false) 66 | const [selFuncType, setSelFuncType] = React.useState(0) 67 | 68 | React.useEffect(() => { 69 | 70 | setMounted(true) 71 | 72 | }, []) 73 | 74 | React.useEffect(() => { 75 | 76 | if(isMounted) { 77 | 78 | setFuncType(storedMode) 79 | setMessageItems(storedMessages) 80 | 81 | } 82 | 83 | }, [isMounted]) 84 | 85 | const handleDialogCancel = () => { 86 | setDialogShown(false) 87 | } 88 | 89 | const deleteThread = async () => { 90 | 91 | try { 92 | 93 | setLoading(true) 94 | 95 | const response = await fetch('/assistant/thread', { 96 | method: 'POST', 97 | headers: { 98 | 'Accept': 'application/json', 99 | 'Content-Type': 'application/json', 100 | }, 101 | body: JSON.stringify({ threadId: threadId }) 102 | }) 103 | 104 | if(!response.ok) { 105 | console.log('Oops, an error occurred', response.status) 106 | } 107 | 108 | const result = await response.json() 109 | 110 | console.log(result) 111 | 112 | } catch(error) { 113 | 114 | console.log(error.name, error.message) 115 | 116 | } finally { 117 | 118 | setLoading(false) 119 | setThreadId('') 120 | 121 | } 122 | 123 | } 124 | 125 | const handleDialogConfirm = async () => { 126 | 127 | await handleClearMessages() 128 | 129 | setMode(selFuncType) 130 | setFuncType(selFuncType) 131 | setDialogShown(false) 132 | 133 | } 134 | 135 | const submitChatCompletion = async () => { 136 | 137 | setLoading(true) 138 | 139 | const text = inputText 140 | 141 | setInputText('') 142 | inputRef.current.blur() 143 | 144 | let previous = messageItems.map((item) => { 145 | return { 146 | role: item.role, 147 | content: item.content, 148 | } 149 | }) 150 | 151 | const newUserMessage = { 152 | id: getUniqueId(), 153 | created_at: (new Date()).toISOString(), 154 | role: 'user', 155 | content: text 156 | } 157 | setMessageItems((prev) => [...prev, ...[newUserMessage]]) 158 | addMessage(newUserMessage) 159 | 160 | resetScroll() 161 | 162 | let result_tools = [] 163 | let isCompleted = false 164 | let MAX_LOOP_COUNT = 10 // Don't want to let it run loose 165 | let loopCount = 0 166 | 167 | try { 168 | 169 | do { 170 | 171 | const url = result_tools.length > 0 ? '/chat/function' : '/chat/message' 172 | 173 | const payload = result_tools.length > 0 ? { tools: result_tools, previous } : { inquiry: text, previous } 174 | 175 | const response = await fetch(url, { 176 | method: 'POST', 177 | headers: { 178 | 'Accept': 'application/json', 179 | 'Content-Type': 'application/json', 180 | }, 181 | body: JSON.stringify(payload) 182 | }) 183 | 184 | if(!response.ok) { 185 | console.log('Oops, an error occurred', response.status) 186 | } 187 | 188 | const result = await response.json() 189 | 190 | console.log(result) 191 | 192 | if(result.output.content) { 193 | 194 | console.log(result.output.content) 195 | 196 | const newAssistantMessage = { 197 | id: getUniqueId(), 198 | created_at: (new Date()).toISOString(), 199 | role: 'assistant', 200 | content: result.output.content 201 | } 202 | setMessageItems((prev) => [...prev, ...[newAssistantMessage]]) 203 | addMessage(newAssistantMessage) 204 | 205 | previous.push({ role: 'assistant', content: result.output.content }) 206 | 207 | resetScroll() 208 | 209 | } 210 | 211 | if(result.output.tool_calls) { 212 | 213 | loopCount++ 214 | 215 | if(loopCount >= MAX_LOOP_COUNT) { 216 | 217 | isCompleted = true 218 | 219 | } else { 220 | 221 | result_tools = result.output.tool_calls 222 | 223 | } 224 | 225 | } else { 226 | 227 | isCompleted = true 228 | 229 | } 230 | 231 | } while(!isCompleted) 232 | 233 | } catch(error) { 234 | 235 | console.log(error.name, error.message) 236 | 237 | } finally { 238 | 239 | setLoading(false) 240 | 241 | setTimeout(() => { 242 | inputRef.current.focus() 243 | }, 100) 244 | 245 | } 246 | } 247 | 248 | const submitAssistant = async () => { 249 | 250 | setLoading(true) 251 | 252 | const text = inputText 253 | 254 | setInputText('') 255 | inputRef.current.blur() 256 | 257 | const message_id = getUniqueId() 258 | 259 | const newUserMessage = { 260 | id: getUniqueId(), 261 | created_at: (new Date()).toISOString(), 262 | role: 'user', 263 | content: text 264 | } 265 | setMessageItems((prev) => [...prev, ...[newUserMessage]]) 266 | addMessage(newUserMessage) 267 | 268 | resetScroll() 269 | 270 | try { 271 | 272 | console.log('submit-assistant', threadId, text) 273 | 274 | const thread_id = threadId ? threadId : '' 275 | 276 | const response = await fetch('/assistant/message', { 277 | method: 'POST', 278 | headers: { 279 | 'Accept': 'application/json', 280 | 'Content-Type': 'application/json', 281 | }, 282 | body: JSON.stringify({ 283 | inquiry: text, 284 | threadId: thread_id, 285 | messageId: message_id, 286 | }) 287 | }) 288 | 289 | if(!response.ok) { 290 | console.log('Oops, an error occurred', response.status) 291 | } 292 | 293 | const result = await response.json() 294 | 295 | console.log("assistant", result) 296 | 297 | setThreadId(result.threadId) 298 | 299 | if(result.messages.length > 0) { 300 | 301 | let new_messages = [] 302 | 303 | for(let i = 0; i < result.messages.length; i++) { 304 | 305 | const msg = result.messages[i] 306 | 307 | if (Object.prototype.hasOwnProperty.call(msg.metadata, 'id')) { 308 | 309 | if(msg.metadata.id === message_id) { 310 | break // last message 311 | } 312 | 313 | } else { 314 | new_messages.push({ 315 | id: msg.id, 316 | created_at: msg.created_at, 317 | role: msg.role, 318 | content: msg.content[0].text.value 319 | }) 320 | } 321 | 322 | } 323 | 324 | if(new_messages.length > 0) { 325 | 326 | setMessageItems((prev) => [...prev, ...new_messages]) 327 | 328 | for(let newmsg of new_messages) { 329 | addMessage(newmsg) 330 | } 331 | 332 | resetScroll() 333 | 334 | } 335 | 336 | } 337 | 338 | } catch(error) { 339 | 340 | console.log(error.name, error.message) 341 | 342 | } finally { 343 | 344 | setLoading(false) 345 | 346 | setTimeout(() => { 347 | inputRef.current.focus() 348 | }, 100) 349 | 350 | } 351 | } 352 | 353 | const handleSubmit = async (e) => { 354 | e.preventDefault() 355 | 356 | if(funcType > 0) { 357 | 358 | submitAssistant() 359 | 360 | } else { 361 | 362 | submitChatCompletion() 363 | 364 | } 365 | 366 | } 367 | 368 | const resetScroll = () => { 369 | setTimeout(() => { 370 | messageRef.current.scrollTop = messageRef.current.scrollHeight + 24 371 | }, 100) 372 | } 373 | 374 | const handleClearMessages = async () => { 375 | 376 | if(funcType > 0 && threadId) { 377 | 378 | await deleteThread() 379 | 380 | } 381 | 382 | setMessageItems([]) 383 | clearMessages() 384 | 385 | } 386 | 387 | const handleChangeFunction = (e) => { 388 | 389 | setSelFuncType(e.target.value) 390 | setDialogShown(true) 391 | 392 | } 393 | 394 | return ( 395 |
396 |
397 |
398 | 399 | 418 | 419 |
420 |
421 | { 422 | messageItems.map((item) => { 423 | return ( 424 |
425 | { 426 | item.role === 'assistant' && 427 | 428 | } 429 | { 430 | item.role === 'function' && 431 | 432 | } 433 |
434 | { item.content } 435 |
436 | { 437 | item.role === 'user' && 438 | 439 | } 440 |
441 | ) 442 | }) 443 | } 444 | { 445 | loading && 446 |
447 | 448 |
449 | } 450 |
451 |
452 |
453 | 454 | 455 | 456 |
457 |
458 | 459 | 463 | setInputText(e.target.value)} 473 | InputProps={{ 474 | endAdornment: ( 475 | 476 | 477 | setInputText('')} 480 | > 481 | 482 | 483 | 487 | 488 | 489 | 490 | 491 | ), 492 | }} 493 | /> 494 | 495 | 496 |
497 |
498 |
499 | { 500 | isDialogShown && createPortal( 501 | , 506 | document.body 507 | ) 508 | } 509 |
510 | ) 511 | } -------------------------------------------------------------------------------- /app/sandbox.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | height: 100vh; 4 | } 5 | 6 | .main { 7 | position: relative; 8 | height: 100%; 9 | /*flex-shrink: 0;*/ 10 | } 11 | 12 | .header { 13 | box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px; 14 | background-color: #fff; 15 | /*background-color: #a5e8;*/ 16 | position: fixed; 17 | left: 0; 18 | top: 0; 19 | width: 100%; 20 | padding: 1rem; 21 | box-sizing: border-box; 22 | z-index: 5; 23 | } 24 | .header span.name { 25 | text-transform: capitalize; 26 | margin-right: 1rem; 27 | } 28 | .header span.desc { 29 | font-size: 0.9rem; 30 | opacity: 0.6; 31 | } 32 | 33 | .messages { 34 | background-color: #fff; 35 | /*position: relative;*/ 36 | /*margin-top: 80px;*/ 37 | position: absolute; 38 | left: 0; 39 | top: 80px; 40 | width: 100%; 41 | height: calc(100% - 150px); /*120*/ 42 | overflow-y: auto; 43 | scroll-behavior: smooth; 44 | z-index: 1; 45 | } 46 | .message { 47 | padding: 1rem 1rem 0 1rem; 48 | display: flex; 49 | } 50 | .message:last-child { 51 | padding-bottom: 1rem; 52 | } 53 | .text { 54 | background-color: #ccf6ff; 55 | border-radius: 12px; 56 | width: 100%; 57 | padding: 1rem; 58 | margin: 0; 59 | /*white-space: pre-wrap;*/ 60 | } 61 | .text img { 62 | width: 80%; 63 | max-width: 1024px; 64 | height: auto; 65 | } 66 | .message:nth-child(odd) .text { 67 | background-color: #efefef; 68 | } 69 | 70 | .chat { 71 | /*background-color: gray;*/ 72 | position: fixed; 73 | left: 0; 74 | bottom: 0; 75 | width: 100%; 76 | z-index: 5; 77 | display: flex; 78 | align-items: center; 79 | padding: 10px; 80 | } 81 | .tool { 82 | /*background-color: aquamarine;*/ 83 | width: 70px; 84 | } 85 | .input { 86 | /*background-color: palevioletred;*/ 87 | /*margin: 1rem 1rem 3rem 1rem;*/ 88 | width: calc(100% - 90px); 89 | } 90 | 91 | .loader { 92 | padding: 1rem 0; 93 | } -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vt-c-white: #fff; 3 | --vt-c-black: #181818; 4 | --vt-c-text-light-1: #2c3e50; 5 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 6 | } 7 | 8 | :root { 9 | --color-background: var(--vt-c-white); 10 | --color-text: var(--vt-c-text-light-1); 11 | --color-green-text: #00bd7e; 12 | --color-yellow-text: #F2A900; 13 | --color-red-text: #E74C3C; 14 | --color-blue-text: #00D8FF; 15 | } 16 | 17 | body { 18 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 19 | margin: 0; 20 | background-color: var(--color-background); 21 | color: var(--color-text); 22 | line-height: 1.4; 23 | font-size: 16px; 24 | text-rendering: optimizeLegibility; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | } 28 | -------------------------------------------------------------------------------- /components/dialog.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | import NoSsr from '@mui/base/NoSsr' 6 | import Button from '@mui/material/Button' 7 | 8 | import classes from './dialog.module.css' 9 | 10 | export default function Dialog({ 11 | disabled = false, 12 | onConfirm = undefined, 13 | onCancel = undefined, 14 | }) { 15 | return ( 16 |
29 | 30 |
31 |
32 |
{ `This will reset and delete your current conversation.\n\nAre you sure you want to continue?` }
33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 |
41 | ) 42 | } 43 | 44 | Dialog.propTypes = { 45 | /** 46 | * disabled property 47 | */ 48 | disabled: PropTypes.bool, 49 | /** 50 | * Confirm event handler 51 | */ 52 | onConfirm: PropTypes.func, 53 | /** 54 | * Cancel event handler 55 | */ 56 | onCancel: PropTypes.func, 57 | } -------------------------------------------------------------------------------- /components/dialog.module.css: -------------------------------------------------------------------------------- 1 | .dialog { 2 | box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px; 3 | background-color: #f5f5f5; 4 | position: relative; 5 | width: 300px; 6 | padding: 1rem; 7 | border-radius: 5px; 8 | } 9 | .contents { 10 | position: relative; 11 | /*background-color: rebeccapurple;*/ 12 | margin-bottom: 1rem; 13 | } 14 | .text { 15 | font-size: 1rem; 16 | white-space: pre-wrap; 17 | color: #555; 18 | line-height: 100%; 19 | } 20 | .action { 21 | border-top: 1px solid #e6e6e6; 22 | display: flex; 23 | justify-content: flex-end; 24 | padding-top: 1rem; 25 | } -------------------------------------------------------------------------------- /components/loader.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import NoSsr from '@mui/base/NoSsr' 5 | import CircularProgress from '@mui/material/CircularProgress' 6 | 7 | export default function Loader() { 8 | return ( 9 |
22 | 23 | 24 | 25 |
26 | ) 27 | } -------------------------------------------------------------------------------- /components/loadingtext.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | 5 | import classes from './loadingtext.module.css' 6 | 7 | export default function LoadingText() { 8 | 9 | const [data, setData] = React.useState(new Array(7).fill(0)) 10 | 11 | React.useEffect(() => { 12 | 13 | let cnt = 0 14 | 15 | const timer = setInterval(() => { 16 | 17 | setData((n) => { 18 | 19 | let d = n.map((m, i) => { 20 | return 2 * Math.sin(cnt + (2 * Math.PI * ((i + 1)/ 8))) 21 | }) 22 | 23 | return d 24 | }) 25 | 26 | cnt++ 27 | 28 | }, 100) 29 | 30 | return () => { 31 | clearInterval(timer) 32 | } 33 | 34 | }, []) 35 | 36 | return ( 37 |
38 |
39 | { 40 | data.map((n, index) => { 41 | return ( 42 |
47 | ) 48 | }) 49 | } 50 |
51 |
52 | ) 53 | } -------------------------------------------------------------------------------- /components/loadingtext.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | height: 24px; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | .inner { 10 | position: relative; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | 16 | .item { 17 | background-color: #999; 18 | position: relative; 19 | width: 8px; 20 | height: 8px; 21 | border-radius: 50%; 22 | margin-right: 3px; 23 | } 24 | .item:last-child { 25 | margin-right: 0; 26 | } -------------------------------------------------------------------------------- /components/openailogo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SvgIcon from '@mui/material/SvgIcon' 3 | 4 | const OpenAiIcon = (props) => { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default OpenAiIcon -------------------------------------------------------------------------------- /docs/screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-api-function-call-sample/dc64feb8822c8cec2b9b43f924545889d698efed/docs/screenshot01.png -------------------------------------------------------------------------------- /docs/screenshot02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-api-function-call-sample/dc64feb8822c8cec2b9b43f924545889d698efed/docs/screenshot02.png -------------------------------------------------------------------------------- /docs/screenshot03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-api-function-call-sample/dc64feb8822c8cec2b9b43f924545889d698efed/docs/screenshot03.png -------------------------------------------------------------------------------- /docs/screenshot04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-api-function-call-sample/dc64feb8822c8cec2b9b43f924545889d698efed/docs/screenshot04.png -------------------------------------------------------------------------------- /docs/screenshot05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-api-function-call-sample/dc64feb8822c8cec2b9b43f924545889d698efed/docs/screenshot05.png -------------------------------------------------------------------------------- /docs/screenshot06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-api-function-call-sample/dc64feb8822c8cec2b9b43f924545889d698efed/docs/screenshot06.png -------------------------------------------------------------------------------- /lib/get_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get_event", 3 | "description": "Get event information based on the given event name, location and date", 4 | "parameters": { 5 | "type": "object", 6 | "properties": { 7 | "event": { 8 | "type": "string", 9 | "description": "Name of the event" 10 | }, 11 | "location": { 12 | "type": "string", 13 | "description": "Location or place of the event" 14 | }, 15 | "date": { 16 | "type": "string", 17 | "description": "Date of the event in 'YYYY-MM-DD' format" 18 | } 19 | }, 20 | "required": [ 21 | "event", 22 | "location", 23 | "date" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /lib/get_events.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get_events", 3 | "description": "Get events based on the given location and date", 4 | "parameters": { 5 | "type": "object", 6 | "properties": { 7 | "location": { 8 | "type": "string", 9 | "description": "Location or place name of the event" 10 | }, 11 | "date": { 12 | "type": "string", 13 | "description": "Date of the event in 'YYYY-MM-DD' format" 14 | } 15 | }, 16 | "required": [ 17 | "location", 18 | "date" 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /lib/get_hotel.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get_hotel", 3 | "description": "Get hotel information based on the given hotel name and location or branch", 4 | "parameters": { 5 | "type": "object", 6 | "properties": { 7 | "hotel": { 8 | "type": "string", 9 | "description": "Name of the hotel" 10 | }, 11 | "location": { 12 | "type": "string", 13 | "description": "Location or branch of hotel" 14 | } 15 | }, 16 | "required": [ 17 | "hotel", 18 | "location" 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /lib/get_reservation.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get_reservation", 3 | "description": "Get hotel reservation information based on the reservation id", 4 | "parameters": { 5 | "type": "object", 6 | "properties": { 7 | "reservationId": { 8 | "type": "string", 9 | "description": "Reservation Id" 10 | }, 11 | "hotel": { 12 | "type": "string", 13 | "description": "Name of hotel" 14 | }, 15 | "location": { 16 | "type": "string", 17 | "description": "Location or branch of hotel" 18 | } 19 | }, 20 | "required": [ 21 | "reservationId", 22 | "hotel", 23 | "location" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /lib/get_weather.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get_weather", 3 | "description": "Get the weather forecast based on the given location and date", 4 | "parameters": { 5 | "type": "object", 6 | "properties": { 7 | "location": { 8 | "type": "string", 9 | "description": "Location or place name" 10 | }, 11 | "date": { 12 | "type": "string", 13 | "description": "Date of forecast in 'YYYY-MM-DD' format" 14 | } 15 | }, 16 | "required": [ 17 | "location", 18 | "date" 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /lib/mockapi.js: -------------------------------------------------------------------------------- 1 | import { getUniqueId } from "./utils" 2 | import QuickCache from "./quickcache" 3 | 4 | const FLAG_ALLOW_UNKNOWN_ERROR = false 5 | 6 | function getWeather({ location, date }) { 7 | 8 | if(!location) return { error: 'Invalid location', message: 'Please specify the location' } 9 | if(!date) return { error: 'Invalid date', message: 'Please specify the date' } 10 | 11 | if(QuickCache.exist('weather', location, date)) { 12 | const stored_data = QuickCache.retrieve('weather', location, date) 13 | if(stored_data) return stored_data 14 | } 15 | 16 | const temperature = Math.floor(25 * Math.random()) 17 | 18 | const conditions = ['Sunny', 'Cloudy', 'Rainy'] 19 | const index = Math.floor(conditions.length * Math.random()) 20 | 21 | const weather_data = { location, date, temperature, unit: 'celsius', condition: conditions[index] } 22 | 23 | QuickCache.save('weather', JSON.stringify(weather_data), location, date) 24 | 25 | return weather_data 26 | } 27 | 28 | function getEvents({ location, date }) { 29 | 30 | if(FLAG_ALLOW_UNKNOWN_ERROR) { 31 | 32 | let chance = Math.round(15 * Math.random()) 33 | 34 | if(chance === 13) { 35 | return { error: 'Unknown error', message: 'Failed to get events in specified location and date', location, date } 36 | } 37 | 38 | } 39 | 40 | if(!location) return { error: 'Invalid location', message: 'Please specify the location' } 41 | if(!date) return { error: 'Invalid date', message: 'Please specify the date' } 42 | 43 | if(QuickCache.exist('events', location, date)) { 44 | const stored_data = QuickCache.retrieve('events', location, date) 45 | if(stored_data) return stored_data 46 | } 47 | 48 | const events = ['Outdoor Fiesta', 'Sumo Exhibition', 'Art Festival', 'Street Dance Parade', 'Farm Marche', 'Folk Concert', 'Soul Food Festival', 'Earth Day', 'Ramen Festival', 'Jazz Festival'] 49 | const index = Math.floor(events.length * Math.random()) 50 | 51 | const events_data = { location, date, event: events[index] } 52 | 53 | QuickCache.save('events', JSON.stringify(events_data), location, date) 54 | 55 | return events_data 56 | 57 | } 58 | 59 | function getEvent({ location, date, event }) { 60 | 61 | let chance = Math.round(15 * Math.random()) 62 | 63 | if(FLAG_ALLOW_UNKNOWN_ERROR) { 64 | 65 | if(chance === 13) { 66 | return { error: 'Unknown error', message: 'Failed to get event information', event, location, date } 67 | } 68 | 69 | } 70 | 71 | if(!location) return { error: 'Invalid location', message: 'Please specify the location' } 72 | if(!date) return { error: 'Invalid date', message: 'Please specify the date' } 73 | if(!event) return { error: 'Invalid event', message: 'Please specify the event name' } 74 | 75 | if(QuickCache.exist('event', location, date, event)) { 76 | const stored_data = QuickCache.retrieve('event', location, date, event) 77 | if(stored_data) return stored_data 78 | } 79 | 80 | const times = ['10:00 - 18:00', '11:00 - 15:00', '13:00 - 18:00', '15:00 - 20:00', '18:00 - 22:00'] 81 | chance = Math.floor(times.length * Math.random()) 82 | const stime = times[chance] 83 | 84 | const places = ['People Hall', 'Community Center', 'City Stadium', 'City Park', 'River Park', 'United Stadium', 'Sports Center'] 85 | chance = Math.floor(places.length * Math.random()) 86 | const splace = places[chance] 87 | 88 | const event_id = getUniqueId() 89 | 90 | // Note: The images in the links are just placeholder but they exist. 91 | // If you want to use your own images or links, by experience, you need https and relative paths do not work. 92 | 93 | const links = [{ title: `Event site`, url: `https://example.com/event/${event_id}`, target: '_blank' }, { title: 'Venue information', url: `https://example.com/venue/${getUniqueId()}`, target: '_blank' }] 94 | 95 | chance = Math.floor(10 * Math.random()) 96 | 97 | const images = [{ alt: event, src: chance > 5 ? 'https://i.postimg.cc/cH55BkC6/5592e301-0407-473a-ada0-e413f0791076.jpg' : 'https://i.postimg.cc/xCd4HV0W/614a6c2b-b881-42f2-a8d4-95f8033b55fb.jpg' }] 98 | 99 | const event_data = { location, date, event, time: stime, place: splace, links, images } 100 | 101 | QuickCache.save('event', JSON.stringify(event_data), location, date, event) 102 | 103 | return event_data 104 | 105 | } 106 | 107 | function searchHotel({ location }) { 108 | 109 | let chance = Math.round(15 * Math.random()) 110 | 111 | if(FLAG_ALLOW_UNKNOWN_ERROR) { 112 | 113 | if(chance === 13) { 114 | return { error: 'Unknown error', message: 'Failed to search for hotels in specified location', location } 115 | } 116 | 117 | } 118 | 119 | if(!location) return { error: 'Invalid location', message: 'Please specify the location' } 120 | 121 | if(QuickCache.exist('hotels', location)) { 122 | const stored_data = QuickCache.retrieve('hotels', location) 123 | if(stored_data) return stored_data 124 | } 125 | 126 | chance = Math.round(10 * Math.random()) 127 | if(chance === 8) { 128 | return { location, items: [], message: 'Found no hotels in the given location.' } 129 | } 130 | 131 | const prefixes = ["Supreme", "Great", "Grand", "Park", "Central", "Royal", "Century", "The", "Green", "Millenium", "Emerald"] 132 | const names = ["Sakura", "Garden", "River", "City", "View", "Onitsuka", "Falcon", "Lion", "Oak", "Southern", "Northern", "Eastern", "Western"] 133 | const suffixes = ["Hotel", "Inn", "Suites", "Lodge", "Guesthouse", "Hostel", "Mansion"] 134 | 135 | chance = 1 + Math.round(6 * Math.random()) 136 | 137 | let items = [] 138 | 139 | for(let i = 0; i < chance; i++) { 140 | 141 | const index1 = Math.floor(prefixes.length * Math.random()) 142 | const index2 = Math.floor(names.length * Math.random()) 143 | const index3 = Math.floor(suffixes.length * Math.random()) 144 | 145 | items.push([prefixes[index1], names[index2], suffixes[index3]].join(' ')) 146 | 147 | } 148 | 149 | const hotels_data = { location, items, message: `Found ${items.length} hotels` } 150 | 151 | QuickCache.save('hotels', JSON.stringify(hotels_data), location) 152 | 153 | return hotels_data 154 | 155 | } 156 | 157 | function getHotel({ location, hotel }) { 158 | let chance = Math.round(15 * Math.random()) 159 | 160 | if(FLAG_ALLOW_UNKNOWN_ERROR) { 161 | 162 | if(chance === 13) { 163 | return { error: 'Unknown error', message: 'Failed to get hotel information', location, hotel } 164 | } 165 | 166 | } 167 | 168 | if(!location) return { error: 'Invalid location', message: 'Please specify the location or branch' } 169 | if(!hotel) return { error: 'Invalid hotel name', message: 'Please specify the name of hotel' } 170 | 171 | if(QuickCache.exist('hotel', location, hotel)) { 172 | const stored_data = QuickCache.retrieve('hotel', location, hotel) 173 | if(stored_data) return stored_data 174 | } 175 | 176 | chance = Math.round(10 * Math.random()) 177 | if(chance === 8) { 178 | return { location, hotel, message: 'Hotel information not found' } 179 | } 180 | 181 | let description1 = `Welcome to ${hotel}, the epitome of luxury and comfort.\n` + 182 | `Nestled in the heart of the ${location}, our hotel offers a stunning view of the skyline.\n` + 183 | `Our rooms are designed with elegance and equipped with modern amenities to ensure a memorable stay.\n` + 184 | `Indulge in our world-class cuisine at our in-house restaurant, or unwind at our state-of-the-art fitness center and spa.\n` + 185 | `With our dedicated staff ready to cater to your needs 24/7, we guarantee an unparalleled hospitality experience at ${hotel}.\n` + 186 | `We look forward to welcoming you soon!` 187 | 188 | let description2 = `Escape the hustle and bustle of everyday life and immerse yourself in the tranquil ambiance of ${hotel}.\n` + 189 | `Nestled amidst the lush greenery of a secluded paradise, our hotel offers a sanctuary of relaxation and rejuvenation.\n` + 190 | `Our spacious and elegantly appointed rooms provide a haven of comfort, while our attentive staff is dedicated to ensuring your stay is nothing short of exceptional.` 191 | 192 | let description3 = `Nestled in the heart of ${location}, ${hotel} offers a blend of comfort and convenience.\n` + 193 | `Our hotel is designed with a touch of luxury and furnished with a bespoke blend of amenities to provide an unforgettable stay for our guests.\n` + 194 | `Our rooms are spacious and feature modern decor and amenities.\n` + 195 | `Some rooms also offer stunning city views!` 196 | 197 | let description4 = `Whether you’re visiting for business or leisure, ${hotel} is the perfect place to experience ${location} and its surroundings.\n` + 198 | `Book your stay with us for an unforgettable experience!` 199 | 200 | let descriptions = [description1, description2, description3, description4] 201 | 202 | chance = Math.floor(descriptions.length * Math.random()) 203 | const description = descriptions[chance] 204 | 205 | const price = 3000 + Math.floor(20000 * Math.random()) 206 | 207 | const list_of_amenities = ['pool', 'spa', 'sauna', 'fitness center', 'business center', 'free wifi', 'free breakfast'] 208 | 209 | let amenities = [] 210 | 211 | for(let amenity of list_of_amenities) { 212 | chance = Math.round(10 * Math.random()) 213 | if(chance > 6) continue 214 | amenities.push(amenity) 215 | } 216 | 217 | // Note: The images in the links are just placeholder but they exist, for testing. 218 | // If you want to use your own images or links, by experience, you need https and relative paths do not work. 219 | 220 | const link_name = hotel.toLowerCase().split(' ').join('_') 221 | const website = `https://example.com/hotel/${link_name}/${getUniqueId()}` 222 | 223 | chance = Math.floor(10 * Math.random()) 224 | const images = [{ alt: hotel, src: chance > 5 ? 'https://i.postimg.cc/jjc1LSrH/d5592424-e3f0-4dfa-afb2-2dcc7308e321.jpg' : 'https://i.postimg.cc/Xv4hjytN/dea57a4a-532b-43d2-85bb-0e0172d8c594.jpg' }] 225 | 226 | const hotel_data = { location, hotel, description, price: price.toLocaleString(), amenities, website, images } 227 | 228 | QuickCache.save('hotel', JSON.stringify(hotel_data), location, hotel) 229 | 230 | return hotel_data 231 | } 232 | 233 | function reserveHotel({ 234 | hotel, 235 | location, 236 | fullName, 237 | numberOfGuests, 238 | checkInDate, 239 | checkOutDate, 240 | roomType, 241 | specialRequests 242 | }) { 243 | if(FLAG_ALLOW_UNKNOWN_ERROR) { 244 | 245 | let chance = Math.round(15 * Math.random()) 246 | 247 | if(chance === 13) { 248 | return { error: 'Unknown error', message: 'Failed to make hotel reservation. Please try again.', hotel, location } 249 | } 250 | 251 | } 252 | 253 | if(!location) return { error: 'Invalid location', message: 'Please specify the location or branch' } 254 | if(!hotel) return { error: 'Invalid hotel name', message: 'Please specify the name of hotel' } 255 | if(!fullName) return { error: 'Invalid name', message: 'Please specify your full name' } 256 | if(!numberOfGuests) return { error: 'Invalid guest number', message: 'Please specify the number of guests' } 257 | if(!checkInDate) return { error: 'Invalid Check-In date', message: 'Please specify the Check-In date' } 258 | if(!checkOutDate) return { error: 'Invalid Check-out data', message: 'Please specify the Check-out date' } 259 | if(!roomType) return { error: 'Invalid room type', message: 'Please specify the room type' } 260 | 261 | if(fullName.toLowerCase().indexOf('full name') >= 0) { 262 | return { status: 'No name provided', message: 'Please ask user provide your full name', hotel, location, numberOfGuests, checkInDate, checkOutDate, roomType, specialRequests } 263 | } 264 | 265 | const reservationId = getUniqueId() 266 | 267 | const reservation_data = { 268 | status: 'Reservation successful', 269 | reservationId: reservationId, 270 | message: 'Your reservation has been completed. Please present your reservationId at the front desk.', 271 | hotel, 272 | location, 273 | fullName, 274 | numberOfGuests, 275 | checkInDate, 276 | checkOutDate, 277 | roomType, 278 | specialRequests 279 | } 280 | 281 | QuickCache.save('reservation', JSON.stringify(reservation_data), location, hotel, reservationId) 282 | 283 | return reservation_data 284 | 285 | } 286 | 287 | function getReservation({ reservationId, hotel, location }) { 288 | 289 | let chance = Math.round(15 * Math.random()) 290 | 291 | if(FLAG_ALLOW_UNKNOWN_ERROR) { 292 | 293 | if(chance === 13) { 294 | return { status: 'Server is busy', message: 'Failed to get hotel reservation. Please try again later.', reservationId, hotel, location } 295 | } 296 | 297 | } 298 | 299 | if(!location) return { error: 'Invalid location', message: 'Please specify the location or branch' } 300 | if(!hotel) return { error: 'Invalid hotel name', message: 'Please specify the name of hotel' } 301 | if(!reservationId) return { error: 'Invalid reservation id', message: 'Please specify the reservation id provided' } 302 | 303 | chance = Math.round(10 * Math.random()) 304 | if(chance === 8) { 305 | return { status: "Server is busy", message: "Please try again later.", reservationId, hotel, location } 306 | } 307 | 308 | if(QuickCache.exist('reservation', location, hotel, reservationId)) { 309 | const stored_data = QuickCache.retrieve('reservation', location, hotel, reservationId) 310 | if(stored_data) return stored_data 311 | } 312 | 313 | return { status: "Reservation not found", message: "There is no record found.", reservationId, hotel, location } 314 | 315 | } 316 | 317 | export function callMockAPI( function_name, function_args ) { 318 | 319 | switch(function_name) { 320 | case 'get_weather': 321 | return getWeather(function_args) 322 | case 'get_events': 323 | return getEvents(function_args) 324 | case 'get_event': 325 | return getEvent(function_args) 326 | case 'search_hotel': 327 | return searchHotel(function_args) 328 | case 'get_hotel': 329 | return getHotel(function_args) 330 | case 'reserve_hotel': 331 | return reserveHotel(function_args) 332 | case 'get_reservation': 333 | return getReservation(function_args) 334 | default: 335 | return { error: 'unknown function', message: 'function not found' } 336 | } 337 | 338 | } -------------------------------------------------------------------------------- /lib/quickcache.js: -------------------------------------------------------------------------------- 1 | const weather = [] 2 | const events = [] 3 | const event = [] 4 | const hotels = [] 5 | const hotel = [] 6 | const reservation = [] 7 | 8 | function save(storeKey, value, ...itemKeys) { 9 | 10 | const stores = { 11 | 'weather': this.weather, 12 | 'events': this.events, 13 | 'event': this.event, 14 | 'hotels': this.hotels, 15 | 'hotel': this.hotel, 16 | 'reservation': this.reservation, 17 | } 18 | 19 | const selected_store = stores[storeKey] 20 | 21 | selected_store.push({ key: itemKeys.join("_"), value: JSON.stringify(value) }) 22 | 23 | } 24 | 25 | function exist(storeKey, ...itemKeys) { 26 | 27 | const stores = { 28 | 'weather': this.weather, 29 | 'events': this.events, 30 | 'event': this.event, 31 | 'hotels': this.hotels, 32 | 'hotel': this.hotel, 33 | 'reservation': this.reservation, 34 | } 35 | 36 | return stores[storeKey].some((qc) => qc.key === itemKeys.join("_")) 37 | 38 | } 39 | 40 | function retrieve(storeKey, ...itemKeys) { 41 | 42 | const stores = { 43 | 'weather': this.weather, 44 | 'events': this.events, 45 | 'event': this.event, 46 | 'hotels': this.hotels, 47 | 'hotel': this.hotel, 48 | 'reservation': this.reservation, 49 | } 50 | 51 | const stored_data = stores[storeKey].find((qc) => qc.key === itemKeys.join("_")) 52 | 53 | if(stored_data) { 54 | return JSON.parse(stored_data.value) 55 | } 56 | 57 | } 58 | 59 | const QuickCache = { 60 | weather, 61 | events, 62 | event, 63 | hotels, 64 | hotel, 65 | reservation, 66 | save, 67 | retrieve, 68 | exist, 69 | } 70 | 71 | export default QuickCache -------------------------------------------------------------------------------- /lib/reserve_hotel.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reserve_hotel", 3 | "description": "Reserve a room for the user in the hotel", 4 | "parameters": { 5 | "type": "object", 6 | "properties": { 7 | "hotel": { 8 | "type": "string", 9 | "description": "Name of the hotel" 10 | }, 11 | "location": { 12 | "type": "string", 13 | "description": "Location or branch of the hotel" 14 | }, 15 | "fullName": { 16 | "type": "string", 17 | "description": "Full name of the user making reservation" 18 | }, 19 | "numberOfGuests": { 20 | "type": "integer", 21 | "description": "Total number of people who will be staying in the room" 22 | }, 23 | "checkInDate": { 24 | "type": "string", 25 | "description": "Date when the guests will arrive in 'YYYY-MM-DD' format" 26 | }, 27 | "checkOutDate": { 28 | "type": "string", 29 | "description": "Date when the guests will leave in 'YYYY-MM-DD' format" 30 | }, 31 | "roomType": { 32 | "type": "string", 33 | "description": "Type of room desired (e.g., single, double, suite)", 34 | "enum": [ 35 | "single", 36 | "double", 37 | "suite" 38 | ] 39 | }, 40 | "specialRequests": { 41 | "type": "array", 42 | "description": "Any specific requests like a room on a certain floor, near the elevator, extra bed, etc.", 43 | "items": { 44 | "type": "string" 45 | } 46 | } 47 | }, 48 | "required": [ 49 | "hotel", 50 | "location", 51 | "fullName", 52 | "numberOfGuests", 53 | "checkInDate", 54 | "checkOutDate", 55 | "roomType", 56 | "specialRequests" 57 | ] 58 | } 59 | } -------------------------------------------------------------------------------- /lib/search_hotel.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search_hotel", 3 | "description": "Search for hotels based on the given location", 4 | "parameters": { 5 | "type": "object", 6 | "properties": { 7 | "location": { 8 | "type": "string", 9 | "description": "Location or place name where the user wants to find a hotel" 10 | } 11 | }, 12 | "required": [ 13 | "location" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | export function getUniqueId() { 2 | return Math.random().toString(36).substring(2) + (new Date()).getTime().toString(36); 3 | } 4 | 5 | export const isEven = (n) => { 6 | return n % 2 == 0; 7 | } 8 | 9 | export const trim_array = ( arr, max_length = 20 ) => { 10 | 11 | let new_arr = arr 12 | 13 | if(arr.length > max_length) { 14 | 15 | let cutoff = Math.ceil(arr.length - max_length) 16 | cutoff = isEven(cutoff) ? cutoff : cutoff + 1 17 | 18 | new_arr = arr.slice(cutoff) 19 | 20 | } 21 | 22 | return new_arr 23 | 24 | } 25 | 26 | export const wait = (delay) => { 27 | return new Promise((resolve) => setTimeout(resolve, delay)) 28 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | webpack: function(config) { 4 | config.module.rules.push({ 5 | test: /\.md$/, 6 | use: 'raw-loader', 7 | }) 8 | return config 9 | }, 10 | env: { 11 | siteTitle: 'OpenAI API - Function Call Sample', 12 | }, 13 | trailingSlash: true, 14 | experimental: { 15 | appDir: true, 16 | }, 17 | }; 18 | 19 | module.exports = nextConfig; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-api-function-call-sample", 3 | "version": "0.0.2", 4 | "description": "A sample React app built using Next.JS powered by OpenAI API", 5 | "scripts": { 6 | "dev": "next dev -p 4000", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@emotion/react": "^11.11.1", 12 | "@emotion/styled": "^11.11.0", 13 | "@fontsource/roboto": "^5.0.3", 14 | "@mui/icons-material": "^5.11.16", 15 | "@mui/material": "^5.13.5", 16 | "next": "^13.4.6", 17 | "openai": "^4.19.1", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-markdown": "^9.0.1", 21 | "zustand": "^4.4.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-api-function-call-sample/dc64feb8822c8cec2b9b43f924545889d698efed/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-api-function-call-sample/dc64feb8822c8cec2b9b43f924545889d698efed/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-api-function-call-sample/dc64feb8822c8cec2b9b43f924545889d698efed/public/logo512.png -------------------------------------------------------------------------------- /service/openai.js: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | 3 | const openai = new OpenAI({ 4 | apiKey: process.env.OPENAI_API_KEY, 5 | maxRetries: 3, 6 | timeout: 60 * 1000 7 | }) 8 | 9 | 10 | export async function getAssistant() { 11 | 12 | try { 13 | 14 | return await openai.beta.assistants.retrieve(process.env.OPENAI_ASSISTANT_ID) 15 | 16 | } catch(error) { 17 | console.log(error.name, error.message) 18 | throw error 19 | } 20 | } 21 | 22 | export async function createThread() { 23 | 24 | try { 25 | 26 | return await openai.beta.threads.create() 27 | 28 | } catch(error) { 29 | console.log(error.name, error.message) 30 | throw error 31 | } 32 | } 33 | 34 | export async function getThread({ 35 | threadId, 36 | }) { 37 | 38 | try { 39 | 40 | return await openai.beta.threads.retrieve(threadId) 41 | 42 | } catch(error) { 43 | console.log(error.name, error.message) 44 | //throw error 45 | return { 46 | error: true, 47 | message: error.message, 48 | } 49 | } 50 | 51 | } 52 | 53 | export async function deleteThread({ 54 | threadId, 55 | }) { 56 | 57 | try { 58 | 59 | return await openai.beta.threads.del(threadId) 60 | 61 | } catch(error) { 62 | console.log(error.name, error.message) 63 | //throw error 64 | return { 65 | error: true, 66 | message: error.message, 67 | } 68 | } 69 | 70 | } 71 | 72 | export async function addMessage({ 73 | threadId, 74 | message, 75 | messageId, 76 | }) { 77 | 78 | try { 79 | 80 | // add metadata 81 | let metadata = { 'id': messageId } 82 | //metadata['id'] = messageId 83 | 84 | return await openai.beta.threads.messages.create( 85 | threadId, 86 | { 87 | role: 'user', 88 | content: message, 89 | metadata, 90 | } 91 | ) 92 | 93 | } catch(error) { 94 | console.log(error.name, error.message) 95 | throw error 96 | } 97 | } 98 | 99 | export async function getMessages({ 100 | threadId, 101 | }) { 102 | 103 | try { 104 | 105 | const messages = await openai.beta.threads.messages.list(threadId) 106 | 107 | return messages.data 108 | 109 | } catch(error) { 110 | console.log(error.name, error.message) 111 | throw error 112 | } 113 | 114 | } 115 | 116 | export async function startRun({ threadId, instructions }) { 117 | 118 | try { 119 | 120 | let options = { 121 | assistant_id: process.env.OPENAI_ASSISTANT_ID, 122 | } 123 | 124 | if(instructions) { 125 | options.instructions = instructions 126 | } 127 | 128 | return await openai.beta.threads.runs.create( 129 | threadId, 130 | options 131 | ) 132 | 133 | } catch(error) { 134 | console.log(error.name, error.message) 135 | throw error 136 | } 137 | 138 | } 139 | 140 | export async function getRun({ 141 | threadId, 142 | runId, 143 | }) { 144 | 145 | try { 146 | 147 | return await openai.beta.threads.runs.retrieve(threadId, runId) 148 | 149 | } catch(error) { 150 | console.log(error.name, error.message) 151 | throw error 152 | } 153 | 154 | } 155 | 156 | export async function submitOutputs({ 157 | threadId, 158 | runId, 159 | tool_outputs 160 | }) { 161 | 162 | try { 163 | 164 | return await openai.beta.threads.runs.submitToolOutputs( 165 | threadId, 166 | runId, 167 | { 168 | tool_outputs: tool_outputs, 169 | } 170 | ) 171 | 172 | } catch(error) { 173 | console.log(error.name, error.message) 174 | throw error 175 | } 176 | 177 | } 178 | 179 | export async function chatCompletion({ 180 | model = 'gpt-3.5-turbo-1106', 181 | max_tokens = 2048, 182 | temperature = 0, 183 | messages, 184 | tools, 185 | }) { 186 | 187 | let options = { messages, model, temperature, max_tokens } 188 | 189 | if(tools) { 190 | 191 | options.tools = tools 192 | 193 | } 194 | 195 | try { 196 | 197 | const result = await openai.chat.completions.create(options) 198 | 199 | console.log(result) 200 | 201 | return result.choices[0] 202 | 203 | } catch(error) { 204 | 205 | console.log(error.name, error.message) 206 | 207 | throw error 208 | 209 | } 210 | 211 | } -------------------------------------------------------------------------------- /stores/appstore.js: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { persist, createJSONStorage } from 'zustand/middleware' 3 | 4 | export const APP_STORAGE_KEY = 'openai-api-function-call-sample-storage' 5 | 6 | const useAppStore = create( 7 | persist( 8 | (set, get) => ({ 9 | 10 | messages: [], 11 | threadId: '', 12 | runId: '', 13 | mode: 0, 14 | 15 | addMessage: (newmessage) => { 16 | 17 | let messages = get().messages.slice(0) 18 | messages.push(newmessage) 19 | 20 | set({ 21 | messages: messages 22 | }) 23 | }, 24 | clearMessages: () => set({ messages: [] }), 25 | setThreadId: (id) => set({ threadId: id }), 26 | setRunId: (id) => set({ runId: id }), 27 | setMode: (n) => set({ mode: n }), 28 | 29 | }), 30 | { 31 | name: APP_STORAGE_KEY, 32 | storage: createJSONStorage(() => localStorage), 33 | version: 1, 34 | } 35 | ) 36 | ) 37 | 38 | export default useAppStore --------------------------------------------------------------------------------