├── .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 |  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 | '' 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 |  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 | '\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 |  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 | '\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 |  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 |  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 |  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