├── .eslintignore ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples └── simple-bot │ ├── .env │ ├── README.md │ ├── package.json │ └── simple-bot-slack.js ├── greenkeeper.json ├── jest.config.js ├── lib ├── index.d.ts ├── index.js ├── index.js.map ├── utils.d.ts ├── utils.js └── utils.js.map ├── package-lock.json ├── package.json ├── src ├── index.ts └── utils.ts ├── test ├── context-store.test.ts ├── middleware-delete-user-data.test.ts ├── middleware-receive.test.ts ├── middleware-send-to-watson.test.ts └── post-message.test.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | examples 3 | coverage 4 | test -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es6: true 3 | node: true 4 | jest: true 5 | extends: 6 | - 'plugin:@typescript-eslint/recommended' 7 | - 'plugin:prettier/recommended' 8 | 9 | parser: '@typescript-eslint/parser' 10 | parserOptions: 11 | project: './tsconfig.json' 12 | plugins: 13 | - '@typescript-eslint' 14 | rules: 15 | no-console: 16 | - warn 17 | '@typescript-eslint/no-explicit-any': 18 | - warn 19 | '@typescript-eslint/camelcase': 20 | - off 21 | prettier/prettier: 22 | - error 23 | - singleQuote: true 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Mac 30 | *.DS_Store 31 | node_modules 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | test 3 | src 4 | coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: "10" 3 | cache: 4 | directories: 5 | - node_modules 6 | script: 7 | - npm run lint 8 | - npm test -- --coverage && codecov 9 | 10 | before_deploy: 11 | - npm run build 12 | deploy: 13 | - provider: script 14 | skip_cleanup: true 15 | script: npx semantic-release 16 | on: 17 | node: 10 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.8.0 2 | 3 | * Added semantic-releases 4 | * Update node-sdk to 3.7.0 5 | * Update to use the Watson Assistant class 6 | 7 | ## v1.6.1 8 | 9 | * Fixed function signatures in Typescript definition file 10 | 11 | ## v1.6.0 12 | 13 | * New methods sendToWatsonAsync, interpretAsync, readContextAsync, updateContextAsync return promises. 14 | 15 | ## v1.5.0 16 | 17 | * Exported readContext method. 18 | * sendToWatson method can update context. 19 | * Added Typescript definition. 20 | * Empty message with type=welcome can be used to indicate welcome event. 21 | 22 | ## v1.4.2 23 | 24 | * Fixed critical issue introduced in v1.4.1 25 | * updateContext fails on first write for a new user when simple storage is used (It happens because simple storage returns an error when record does not exist). 26 | 27 | ## v1.4.1 28 | 29 | * `updateContext` actually preserves other data stored in users storage 30 | 31 | ## v1.4.0 32 | 33 | The following changes were introduced with the v1.4.0 release: 34 | * Added `updateContext` function to middleware. 35 | * `interpret` is just an alias of `receive`. 36 | * `sendToWatson` is a new alias of `receive`. 37 | * Fixed error handling in `utils.updateContext`. 38 | * If any error happens in `receive`, it is assigned to `message.watsonError` 39 | 40 | ## v1.3.1 41 | 42 | Added minimum confidence like optional config parameter 43 | 44 | ```javascript 45 | var middleware = require('botkit-middleware-watson')({ 46 | username: process.env.CONVERSATION_USERNAME, 47 | password: process.env.CONVERSATION_PASSWORD, 48 | workspace_id: process.env.WORKSPACE_ID, 49 | minimum_confidence: 0.50, // (Optional) Default is 0.75 50 | }); 51 | ``` 52 | 53 | ## v1.3.0 54 | 55 | Fixed the parameters for the after() call in the interpret function. 56 | 57 | ## v1.2.0 58 | 59 | Fixed the invocation of after() in _receive_ and _interpret_. The order of invocation is now: 60 | * before() 61 | * conversation() 62 | * after() 63 | 64 | ## v1.1.0 65 | 66 | The following changes were introduced with the v1.1.0 release: 67 | 68 | * Added `interpret` function to middleware. It is to be used when you need to send only _heard_ utterances to Watson. 69 | Works like the receive function but needs to be explicitly called. 70 | 71 | 72 | ## v1.0.0 73 | 74 | The following changes were introduced with the v1.0.0 release: 75 | 76 | * Breaking Change: Config parameters need to be passed into the function call when requiring the middleware: 77 | 78 | ```javascript 79 | var middleware = require('botkit-middleware-watson')({ 80 | username: process.env.CONVERSATION_USERNAME, 81 | password: process.env.CONVERSATION_PASSWORD, 82 | workspace_id: process.env.WORKSPACE_ID 83 | }); 84 | ``` 85 | 86 | * Added `hears` function to middleware 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Use IBM Watson's Assistant service to chat with your Botkit-powered Bot! [![Build Status](https://travis-ci.org/watson-developer-cloud/botkit-middleware.svg?branch=master)](https://travis-ci.org/watson-developer-cloud/botkit-middleware) [![Greenkeeper badge](https://badges.greenkeeper.io/watson-developer-cloud/botkit-middleware.svg)](https://greenkeeper.io/) 2 | 3 | This middleware plugin for [Botkit](http://howdy.ai/botkit) allows developers to easily integrate a [Watson Assistant](https://www.ibm.com/watson/ai-assistant/) workspace with multiple social channels like Slack, Facebook, and Twilio. Customers can have simultaneous, independent conversations with a single workspace through different channels. 4 | 5 |
6 | Table of Contents 7 | 8 | - [Middleware Overview](#middleware-overview) 9 | - [Function Overview](#function-overview) 10 | - [Installation](#installation) 11 | - [Prerequisites](#prerequisites) 12 | - [Acquire channel credentials](#acquire-channel-credentials) 13 | - [Bot setup](#bot-setup) 14 | - [Features](#features) 15 | - [Message filtering](#message-filtering) 16 | - [Using interpret function instead of registering middleware](#using-interpret-function-instead-of-registering-middleware) 17 | - [Using middleware wrapper](#using-middleware-wrapper) 18 | - [Minimum Confidence](#minimum-confidence) 19 | - [Use it manually in your self-defined controller.hears() function(s)](#use-it-manually-in-your-self-defined-controllerhears-functions) 20 | - [Use the middleware's hear() function](#use-the-middlewares-hear-function) 21 | - [Implementing app actions](#implementing-app-actions) 22 | - [Using sendToWatson to update context](#using-sendtowatson-to-update-context) 23 | - [Implementing event handlers](#implementing-event-handlers) 24 | - [Intent matching](#intent-matching) 25 | - [`before` and `after`](#before-and-after) 26 | - [Dynamic workspace](#dynamic-workspace) 27 | - [Information security](#information-security) 28 | - [Labeling user data](#labeling-user-data) 29 | - [Deleting user data](#deleting-user-data) 30 |
31 | 32 | ## Middleware Overview 33 | 34 | - Automatically manages context in multi-turn conversations to keep track of where the user left off in the conversation. 35 | - Allows greater flexibility in message handling. 36 | - Handles external databases for context storage. 37 | - Easily integrates with third-party services. 38 | - Exposes the following functions to developers: 39 | 40 | ## Function Overview 41 | 42 | - `receive`: used as [middleware in Botkit](#bot-setup). 43 | - `interpret`: an alias of `receive`, used in [message-filtering](#message-filtering) and [implementing app actions](#implementing-app-actions). 44 | - `sendToWatson`: same as above, but it can update context before making request, used in [implementing app actions](#implementing-app-actions). 45 | - `hear`: used for [intent matching](#intent-matching). 46 | - `updateContext`: used in [implementing app actions](#implementing-app-actions) (sendToWatson does it better now). 47 | - `readContext`: used in [implementing event handlers](#implementing-event-handlers). 48 | - `before`: [pre-process](#before-and-after) requests before sending to Watson Assistant (formerly Conversation). 49 | - `after`: [post-process](#before-and-after) responses before forwarding them to Botkit. 50 | - `deleteUserData`: [deletes](#information-security) all data associated with a specified customer ID. 51 | 52 | ## Installation 53 | 54 | ```sh 55 | $ npm install botkit-middleware-watson 56 | ``` 57 | 58 | ## Prerequisites 59 | 60 | ### Create an instance of Watson Assistant 61 | 62 | _💡 You can skip this step if you have credentials to access an existing instance of Watson Assistant. This would be the case for Cloud Pak for Data._ 63 | 64 | 1. Sign up for an [IBM Cloud account](https://cloud.ibm.com/registration/). 65 | 1. Create an instance of the Watson Assistant service and get your credentials: 66 | - Go to the [Watson Assistant](https://cloud.ibm.com/catalog/services/assistant) page in the IBM Cloud Catalog. 67 | - Log in to your IBM Cloud account. 68 | - Click **Create**. 69 | - Copy the `apikey` value, or copy the `username` and `password` values if your service instance doesn't provide an `apikey`. 70 | - Copy the `url` value. 71 | 72 | ## Configure your Assistant 73 | 74 | 1. Create a workspace using the Watson Assistant service and copy the `workspace_id`. If you don't know how to create a workspace follow the [Getting Started tutorial](https://cloud.ibm.com/docs/services/assistant/getting-started.html). 75 | 76 | ### Acquire channel credentials 77 | 78 | This document shows code snippets for using a Slack bot with the middleware. You need a _Slack token_ for your Slack bot to talk to Watson Assistant. If you have an existing Slack bot, then copy the Slack token from your Slack settings page. 79 | 80 | Otherwise, follow [Botkit's instructions](https://botkit.ai/docs/provisioning/slack-events-api.html) to create your Slack bot from scratch. When your bot is ready, you are provided with a Slack token. 81 | 82 | ### Bot setup 83 | 84 | This section walks you through code snippets to set up your Slack bot. If you want, you can jump straight to the [full example](/examples/simple-bot). 85 | 86 | In your app, add the following lines to create your Slack controller using Botkit: 87 | 88 | ```js 89 | import { WatsonMiddleware } from 'botkit-middleware-watson'; 90 | import Botkit = require('botkit'); 91 | const { SlackAdapter } = require('botbuilder-adapter-slack'); 92 | 93 | const adapter = new SlackAdapter({ 94 | clientSigningSecret: process.env.SLACK_SECRET, 95 | botToken: process.env.SLACK_TOKEN 96 | }); 97 | 98 | const controller = new Botkit({ 99 | adapter, 100 | // ...other options 101 | }); 102 | ``` 103 | 104 | Create the middleware object which you'll use to connect to the Watson Assistant service. 105 | 106 | If your credentials are `username` and `password` use: 107 | 108 | ```js 109 | const watsonMiddleware = new WatsonMiddleware({ 110 | username: YOUR_ASSISTANT_USERNAME, 111 | password: YOUR_ASSISTANT_PASSWORD, 112 | url: YOUR_ASSISTANT_URL, 113 | workspace_id: YOUR_WORKSPACE_ID, 114 | version: '2018-07-10', 115 | minimum_confidence: 0.5, // (Optional) Default is 0.75 116 | }); 117 | ``` 118 | 119 | If your credentials is `apikey` use: 120 | 121 | ```js 122 | const watsonMiddleware = new WatsonMiddleware({ 123 | iam_apikey: YOUR_API_KEY, 124 | url: YOUR_ASSISTANT_URL, 125 | workspace_id: YOUR_WORKSPACE_ID, 126 | version: '2018-07-10', 127 | minimum_confidence: 0.5, // (Optional) Default is 0.75 128 | }); 129 | ``` 130 | 131 | If your service is running in the IBM Cloud Pak for Data use: 132 | 133 | ```js 134 | const watsonMiddleware = new WatsonMiddleware({ 135 | icp4d_url: YOUR_CLOUD_PAK_ASSISTANT_URL, 136 | icp4d_access_token: YOUR_CLOUD_PAK_ACCESS_TOKEN, 137 | disable_ssl_verification: true, 138 | workspace_id: YOUR_WORKSPACE_ID, 139 | version: '2018-07-10', 140 | minimum_confidence: 0.5, // (Optional) Default is 0.75 141 | }); 142 | ``` 143 | 144 | Tell your Slackbot to use the _watsonMiddleware_ for incoming messages: 145 | 146 | ```js 147 | controller.middleware.receive.use( 148 | watsonMiddleware.receive.bind(watsonMiddleware), 149 | ); 150 | ``` 151 | 152 | Finally, make your bot _listen_ to incoming messages and respond with Watson Assistant: 153 | 154 | ```js 155 | controller.hears( 156 | ['.*'], 157 | ['direct_message', 'direct_mention', 'mention'], 158 | async function(bot, message) { 159 | if (message.watsonError) { 160 | await bot.reply( 161 | message, 162 | "I'm sorry, but for technical reasons I can't respond to your message", 163 | ); 164 | } else { 165 | await bot.reply(message, message.watsonData.output.text.join('\n')); 166 | } 167 | }, 168 | ); 169 | ``` 170 | 171 | The middleware attaches the `watsonData` object to _message_. This contains the text response from Assistant. 172 | If any error happened in middleware, error is assigned to `watsonError` property of the _message_. 173 | 174 | Then you're all set! 175 | 176 | ## Features 177 | 178 | ### Message filtering 179 | 180 | When middleware is registered, the receive function is triggered on _every_ message. 181 | If you would like to make your bot to only respond to _direct messages_ using Assistant, you can achieve this in 2 ways: 182 | 183 | #### Using interpret function instead of registering middleware 184 | 185 | ```js 186 | slackController.hears(['.*'], ['direct_message'], async (bot, message) => { 187 | await middleware.interpret(bot, message); 188 | if (message.watsonError) { 189 | bot.reply( 190 | message, 191 | "I'm sorry, but for technical reasons I can't respond to your message", 192 | ); 193 | } else { 194 | bot.reply(message, message.watsonData.output.text.join('\n')); 195 | } 196 | }); 197 | ``` 198 | 199 | #### Using middleware wrapper 200 | 201 | ```js 202 | const receiveMiddleware = (bot, message, next) => { 203 | if (message.type === 'direct_message') { 204 | watsonMiddleware.receive(bot, message, next); 205 | } else { 206 | next(); 207 | } 208 | }; 209 | 210 | slackController.middleware.receive.use(receiveMiddleware); 211 | ``` 212 | 213 | ### Minimum Confidence 214 | 215 | To use the setup parameter `minimum_confidence`, you have multiple options: 216 | 217 | #### Use it manually in your self-defined controller.hears() function(s) 218 | 219 | For example: 220 | 221 | ```js 222 | controller.hears( 223 | ['.*'], 224 | ['direct_message', 'direct_mention', 'mention', 'message_received'], 225 | async (bot, message) => { 226 | if (message.watsonError) { 227 | await bot.reply(message, 'Sorry, there are technical problems.'); // deal with watson error 228 | } else { 229 | if (message.watsonData.intents.length == 0) { 230 | await bot.reply(message, 'Sorry, I could not understand the message.'); // was any intent recognized? 231 | } else if ( 232 | message.watsonData.intents[0].confidence < 233 | watsonMiddleware.minimum_confidence 234 | ) { 235 | await bot.reply(message, 'Sorry, I am not sure what you have said.'); // is the confidence high enough? 236 | } else { 237 | await bot.reply(message, message.watsonData.output.text.join('\n')); // reply with Watson response 238 | } 239 | } 240 | }, 241 | ); 242 | ``` 243 | 244 | #### Use the middleware's hear() function 245 | 246 | You can find the default implementation of this function [here](https://github.com/watson-developer-cloud/botkit-middleware/blob/e29b002f2a004f6df57ddf240a3fdf8cb28f95d0/lib/middleware/index.js#L40). If you want, you can redefine this function in the same way that watsonMiddleware.before and watsonMiddleware.after can be redefined. Refer to the [Botkit Middleware documentation](https://botkit.ai/docs/core.html#controllerhears) for an example. Then, to use this function instead of Botkit's default pattern matcher (that does not use minimum_confidence), plug it in using: 247 | 248 | ```js 249 | controller.changeEars(watsonMiddleware.hear); 250 | ``` 251 | 252 | Note: if you want your own `hear()` function to implement pattern matching like Botkit's default one, you will likely need to implement that yourself. Botkit's default set of 'ears' is the `hears_regexp` function which is implemented [here](https://github.com/howdyai/botkit/blob/77b7d7f80c46d5c8194453667d22118b7850e252/lib/CoreBot.js#L1187). 253 | 254 | ### Implementing app actions 255 | 256 | Watson Assistant side of app action is documented in [Developer Cloud](https://cloud.ibm.com/docs/services/assistant/deploy-custom-app.html#deploy-custom-app) 257 | A common scenario of processing actions is: 258 | 259 | - Send message to user "Please wait while I ..." 260 | - Perform action 261 | - Persist results in conversation context 262 | - Send message to Watson with updated context 263 | - Send result message(s) to user. 264 | 265 | ### Using sendToWatson to update context 266 | 267 | ```js 268 | const checkBalance = async (context) => { 269 | //do something real here 270 | const contextDelta = { 271 | validAccount: true, 272 | accountBalance: 95.33 273 | }; 274 | return context; 275 | }); 276 | 277 | const processWatsonResponse = async (bot, message) => { 278 | if (message.watsonError) { 279 | return await bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message"); 280 | } 281 | if (typeof message.watsonData.output !== 'undefined') { 282 | //send "Please wait" to users 283 | await bot.reply(message, message.watsonData.output.text.join('\n')); 284 | 285 | if (message.watsonData.output.action === 'check_balance') { 286 | const newMessage = clone(message); 287 | newMessage.text = 'balance result'; 288 | 289 | try { 290 | const contextDelta = await checkBalance(message.watsonData.context); 291 | await watsonMiddleware.sendToWatson(bot, newMessage, contextDelta); 292 | } catch(error) { 293 | newMessage.watsonError = error; 294 | } 295 | return await processWatsonResponse(bot, newMessage); 296 | } 297 | } 298 | }; 299 | 300 | controller.on('message_received', processWatsonResponse); 301 | ``` 302 | 303 | ### Using updateContext to update context 304 | 305 | sendToWatson should cover majority of use cases, 306 | but updateContext method can be useful when you want to update context from bot code, 307 | but there is no need to make a special request to Watson. 308 | 309 | ```js 310 | if (params.amount) { 311 | const context = message.watsonData.context; 312 | context.paymentAmount = params.amount; 313 | await watsonMiddleware.updateContext(message.user, context); 314 | } 315 | ``` 316 | 317 | ## Implementing event handlers 318 | 319 | Events are messages having type different than `message`. 320 | 321 | [Example](https://github.com/howdyai/botkit/blob/master/packages/docs/reference/facebook.md#facebookeventtypemiddleware) of handler: 322 | 323 | ```js 324 | controller.on('facebook_postback', async (bot, message) => { 325 | await bot.reply(message, `Great Choice. (${message.payload})`); 326 | }); 327 | ``` 328 | 329 | Since they usually have no text, events aren't processed by middleware and have no watsonData attribute. 330 | If event handler wants to make use of some data from context, it has to read it first. 331 | Example: 332 | 333 | ```js 334 | controller.on('facebook_postback', async (bot, message) => { 335 | const context = watsonMiddleware.readContext(message.user); 336 | //do something useful here 337 | const result = await myFunction(context.field1, context.field2); 338 | const newMessage = { ...message, text: 'postback result' }; 339 | await watsonMiddleware.sendToWatson(bot, newMessage, { 340 | postbackResult: 'success', 341 | }); 342 | }); 343 | ``` 344 | 345 | ### Intent matching 346 | 347 | The Watson middleware also includes a `hear()` function which provides a mechanism to 348 | developers to fire handler functions based on the most likely intent of the user. 349 | This allows a developer to create handler functions for specific intents in addition 350 | to using the data provided by Watson to power the conversation. 351 | 352 | The `hear()` function can be used on individual handler functions, or can be used globally. 353 | 354 | Used on an individual handler: 355 | 356 | ```js 357 | slackController.hears( 358 | ['hello'], 359 | ['direct_message', 'direct_mention', 'mention'], 360 | watsonMiddleware.hear, 361 | async function(bot, message) { 362 | await bot.reply(message, message.watsonData.output.text.join('\n')); 363 | // now do something special related to the hello intent 364 | }, 365 | ); 366 | ``` 367 | 368 | Used globally: 369 | 370 | ```js 371 | slackController.changeEars(watsonMiddleware.hear.bind(watsonMiddleware)); 372 | 373 | slackController.hears( 374 | ['hello'], 375 | ['direct_message', 'direct_mention', 'mention'], 376 | async (bot, message) => { 377 | await bot.reply(message, message.watsonData.output.text.join('\n')); 378 | // now do something special related to the hello intent 379 | }, 380 | ); 381 | ``` 382 | 383 | #### `before` and `after` 384 | 385 | The _before_ and _after_ async calls can be used to perform some tasks _before_ and _after_ Assistant is called. One may use it to modify the request/response payloads, execute business logic like accessing a database or making calls to external services. 386 | 387 | They can be customized as follows: 388 | 389 | ```js 390 | middleware.before = (message, assistantPayload) => async () => { 391 | // Code here gets executed before making the call to Assistant. 392 | return assistantPayload; 393 | }; 394 | ``` 395 | 396 | ```js 397 | middleware.after = (message, assistantResponse) => async () => { 398 | // Code here gets executed after the call to Assistant. 399 | return assistantResponse; 400 | }); 401 | ``` 402 | 403 | #### Dynamic workspace 404 | 405 | If you need to make use of multiple workspaces in a single bot, `workspace_id` can be changed dynamically by setting `workspace_id` property in context. 406 | 407 | Example of setting `workspace_id` to id provided as a property of hello message: 408 | 409 | ```js 410 | async handleHelloEvent = (bot, message) => { 411 | message.type = 'welcome'; 412 | const contextDelta = {}; 413 | 414 | if (message.workspaceId) { 415 | contextDelta.workspace_id = message.workspaceId; 416 | } 417 | 418 | try { 419 | await watsonMiddleware.sendToWatson(bot, message, contextDelta); 420 | } catch(error) { 421 | message.watsonError = error; 422 | } 423 | await bot.reply(message, message.watsonData.output.text.join('\n')); 424 | } 425 | 426 | controller.on('hello', handleHelloEvent); 427 | ``` 428 | 429 | ## Information security 430 | 431 | It may be necessary to be to able delete message logs associated with particular customer in order to comply with 432 | [GDPR or HIPPA regulations](https://cloud.ibm.com/docs/services/assistant?topic=assistant-information-security#information-security) 433 | 434 | ### Labeling User Data 435 | 436 | Messages can be labeled with customer id by adding `x-watson-metadata` header to request in `before` hook: 437 | 438 | ```js 439 | watsonMiddleware.before = async (message, payload) => { 440 | // it is up to you to implement calculateCustomerId function 441 | customerId = calculateCustomerId(payload.context); 442 | payload.headers['X-Watson-Metadata'] = 'customer_id=' + customerId; 443 | 444 | return payload; 445 | }; 446 | ``` 447 | 448 | ### Deleting User Data 449 | 450 | ```js 451 | try { 452 | await watsonMiddleware.deleteUserData(customerId); 453 | //Customer data was deleted successfully 454 | } catch (e) { 455 | //Failed to delete 456 | } 457 | ``` 458 | 459 | ## License 460 | 461 | This library is licensed under Apache 2.0. Full license text is available in [LICENSE](LICENSE). 462 | -------------------------------------------------------------------------------- /examples/simple-bot/.env: -------------------------------------------------------------------------------- 1 | #WATSON 2 | ASSISTANT_URL=https://gateway.watsonplatform.net/assistant/api 3 | # Germany 4 | #ASSISTANT_URL=https://gateway-fra.watsonplatform.net/assistant/api 5 | 6 | # Assistant credentials 7 | ASSISTANT_IAM_APIKEY=apikey 8 | WORKSPACE_ID=your_workspace_id 9 | 10 | #SLACK 11 | SLACK_CLIENT_SIGNING_SECRET=secret 12 | SLACK_TOKEN=token 13 | -------------------------------------------------------------------------------- /examples/simple-bot/README.md: -------------------------------------------------------------------------------- 1 | # Simple bot 2 | 3 | This document describes how to set up an Express app which talks to a Slack bot. 4 | 5 | ### ⚠️ This is just a sample, please follow documentation of Botkit to create a new bot and then plug Watson middleware into it 6 | 7 | 1. Install the dependencies 8 | 9 | ``` 10 | npm install 11 | ``` 12 | 13 | 2. Add your Slack token and Watson Assistant credentials to `.env` 14 | 15 | 3. Start the app 16 | 17 | ``` 18 | npm start 19 | ``` 20 | 21 | 4. Launch Slack, send direct messages to your Slack bot and get responses from your Watson Assistant workspace. 22 | 23 | ![promisechains](https://cloud.githubusercontent.com/assets/5727607/19366644/fe122c2a-9165-11e6-9728-b18a5d9e1198.gif) 24 | -------------------------------------------------------------------------------- /examples/simple-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botkit-middleware-watson-simple-example", 3 | "version": "0.0.1", 4 | "description": "Simple Slack bot example using botkit-middleware-watson with Botkit.", 5 | "main": "simple-bot-slack.js", 6 | "scripts": { 7 | "start": "node simple-bot-slack.js" 8 | }, 9 | "license": "Apache-2.0", 10 | "dependencies": { 11 | "botbuilder-adapter-slack": "^1.0.1", 12 | "botkit": "^4.0.1", 13 | "botkit-middleware-watson": "^2.0.0", 14 | "dotenv": "^8.0.0", 15 | "express": "^4.16.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/simple-bot/simple-bot-slack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | require('dotenv').config(); 18 | 19 | const { Botkit } = require('botkit'); 20 | const { MemoryStorage } = require('botbuilder'); 21 | const { SlackAdapter } = require('botbuilder-adapter-slack'); 22 | 23 | const express = require('express'); 24 | const WatsonMiddleware = require('botkit-middleware-watson').WatsonMiddleware; 25 | 26 | const middleware = new WatsonMiddleware({ 27 | iam_apikey: process.env.ASSISTANT_IAM_APIKEY, 28 | workspace_id: process.env.WORKSPACE_ID, 29 | url: process.env.ASSISTANT_URL || 'https://gateway.watsonplatform.net/assistant/api', 30 | version: '2018-07-10' 31 | }); 32 | 33 | // Configure your bot. 34 | const adapter = new SlackAdapter({ 35 | clientSigningSecret: process.env.SLACK_CLIENT_SIGNING_SECRET, 36 | botToken: process.env.SLACK_TOKEN, 37 | }); 38 | const controller = new Botkit({ 39 | adapter, 40 | storage: new MemoryStorage(), 41 | // ...other options 42 | }); 43 | 44 | 45 | controller.hears(['.*'], ['direct_message', 'direct_mention', 'mention'], async (bot, message) => { 46 | console.log('Slack message received'); 47 | await middleware.interpret(bot, message); 48 | if (message.watsonError) { 49 | console.log(message.watsonError); 50 | await bot.reply(message, message.watsonError.description || message.watsonError.error); 51 | } else if (message.watsonData && 'output' in message.watsonData) { 52 | await bot.reply(message, message.watsonData.output.text.join('\n')); 53 | } else { 54 | console.log('Error: received message in unknown format. (Is your connection with Watson Assistant up and running?)'); 55 | await bot.reply(message, 'I\'m sorry, but for technical reasons I can\'t respond to your message'); 56 | } 57 | }); 58 | 59 | // Create an Express app 60 | const app = express(); 61 | const port = process.env.PORT || 5000; 62 | app.set('port', port); 63 | app.listen(port, function() { 64 | console.log('Client server listening on port ' + port); 65 | }); 66 | -------------------------------------------------------------------------------- /greenkeeper.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": { 3 | "default": { 4 | "packages": [ 5 | "examples/simple-bot/package.json", 6 | "package.json" 7 | ] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | globals: { 7 | global: {}, 8 | }, 9 | testEnvironment: 'node', 10 | }; 11 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import Botkit = require('botkit'); 17 | import AssistantV1 = require('ibm-watson/assistant/v1'); 18 | import { Context, MessageParams, MessageResponse } from 'ibm-watson/assistant/v1'; 19 | import { Storage } from 'botbuilder'; 20 | import { BotkitMessage } from 'botkit'; 21 | export interface WatsonMiddlewareConfig extends AssistantV1.Options { 22 | workspace_id: string; 23 | minimum_confidence?: number; 24 | storage?: Storage; 25 | } 26 | /** 27 | * @deprecated please use AssistantV1.MessageParams instead 28 | */ 29 | export declare type Payload = MessageParams; 30 | export declare type BotkitWatsonMessage = BotkitMessage & { 31 | watsonData?: MessageResponse; 32 | watsonError?: string; 33 | }; 34 | export interface ContextDelta { 35 | [index: string]: any; 36 | } 37 | export declare class WatsonMiddleware { 38 | private readonly config; 39 | private conversation; 40 | private storage; 41 | private readonly minimumConfidence; 42 | private readonly ignoreType; 43 | constructor({ iam_apikey, minimum_confidence, storage, ...config }: WatsonMiddlewareConfig); 44 | hear(patterns: string[], message: Botkit.BotkitMessage): boolean; 45 | before(message: Botkit.BotkitMessage, payload: MessageParams): Promise; 46 | after(message: Botkit.BotkitMessage, response: MessageResponse): Promise; 47 | sendToWatson(bot: Botkit.BotWorker, message: Botkit.BotkitMessage, contextDelta: ContextDelta): Promise; 48 | receive(bot: Botkit.BotWorker, message: Botkit.BotkitMessage): Promise; 49 | interpret(bot: Botkit.BotWorker, message: Botkit.BotkitMessage): Promise; 50 | readContext(user: string): Promise; 51 | updateContext(user: string, context: Context): Promise<{ 52 | context: Context; 53 | }>; 54 | deleteUserData(customerId: string): Promise; 55 | } 56 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | /** 4 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 19 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 20 | return new (P || (P = Promise))(function (resolve, reject) { 21 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 22 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 23 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 24 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 25 | }); 26 | }; 27 | var __rest = (this && this.__rest) || function (s, e) { 28 | var t = {}; 29 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 30 | t[p] = s[p]; 31 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 32 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 33 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 34 | t[p[i]] = s[p[i]]; 35 | } 36 | return t; 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | exports.WatsonMiddleware = void 0; 40 | // eslint-disable-next-line @typescript-eslint/no-var-requires 41 | const debug = require('debug')('watson-middleware:index'); 42 | const AssistantV1 = require("ibm-watson/assistant/v1"); 43 | const auth_1 = require("ibm-watson/auth"); 44 | const utils_1 = require("./utils"); 45 | const deepMerge = require("deepmerge"); 46 | class WatsonMiddleware { 47 | constructor(_a) { 48 | var { iam_apikey, minimum_confidence, storage } = _a, config = __rest(_a, ["iam_apikey", "minimum_confidence", "storage"]); 49 | this.minimumConfidence = 0.75; 50 | // These are initiated by Slack itself and not from the end-user. Won't send these to WCS. 51 | this.ignoreType = ['presence_change', 'reconnect_url']; 52 | if (minimum_confidence) { 53 | this.minimumConfidence = minimum_confidence; 54 | } 55 | if (storage) { 56 | this.storage = storage; 57 | } 58 | if (iam_apikey) { 59 | config.authenticator = new auth_1.IamAuthenticator({ 60 | apikey: iam_apikey, 61 | }); 62 | } 63 | this.config = config; 64 | debug('Creating Assistant object with parameters: ' + 65 | JSON.stringify(this.config, null, 2)); 66 | this.conversation = new AssistantV1(this.config); 67 | } 68 | hear(patterns, message) { 69 | if (message.watsonData && message.watsonData.intents) { 70 | for (let p = 0; p < patterns.length; p++) { 71 | for (let i = 0; i < message.watsonData.intents.length; i++) { 72 | if (message.watsonData.intents[i].intent === patterns[p] && 73 | message.watsonData.intents[i].confidence >= this.minimumConfidence) { 74 | return true; 75 | } 76 | } 77 | } 78 | } 79 | return false; 80 | } 81 | before(message, payload) { 82 | return Promise.resolve(payload); 83 | } 84 | after(message, response) { 85 | return Promise.resolve(response); 86 | } 87 | sendToWatson(bot, message, contextDelta) { 88 | return __awaiter(this, void 0, void 0, function* () { 89 | if ((!message.text && message.type !== 'welcome') || 90 | this.ignoreType.indexOf(message.type) !== -1 || 91 | message.reply_to || 92 | message.bot_id) { 93 | // Ignore messages initiated by Slack. Reply with dummy output object 94 | message.watsonData = { 95 | output: { 96 | text: [], 97 | }, 98 | }; 99 | return; 100 | } 101 | this.storage = bot.controller.storage; 102 | try { 103 | const userContext = yield utils_1.readContext(message.user, this.storage); 104 | const payload = { 105 | workspaceId: this.config.workspace_id, 106 | }; 107 | if (message.text) { 108 | // text can not contain the following characters: tab, new line, carriage return. 109 | const sanitizedText = message.text.replace(/[\r\n\t]/g, ' '); 110 | payload.input = { 111 | text: sanitizedText, 112 | }; 113 | } 114 | if (userContext) { 115 | payload.context = userContext; 116 | } 117 | if (contextDelta) { 118 | if (!userContext) { 119 | //nothing to merge, this is the first context 120 | payload.context = contextDelta; 121 | } 122 | else { 123 | payload.context = deepMerge(payload.context, contextDelta); 124 | } 125 | } 126 | if (payload.context && 127 | payload.context.workspace_id && 128 | payload.context.workspace_id.length === 36) { 129 | payload.workspaceId = payload.context.workspace_id; 130 | } 131 | const watsonRequest = yield this.before(message, payload); 132 | let watsonResponse = yield utils_1.postMessage(this.conversation, watsonRequest); 133 | if (typeof watsonResponse.output.error === 'string') { 134 | debug('Error: %s', watsonResponse.output.error); 135 | message.watsonError = watsonResponse.output.error; 136 | } 137 | watsonResponse = yield this.after(message, watsonResponse); 138 | message.watsonData = watsonResponse; 139 | yield utils_1.updateContext(message.user, this.storage, watsonResponse); 140 | } 141 | catch (error) { 142 | message.watsonError = error; 143 | debug('Error: %s', JSON.stringify(error, Object.getOwnPropertyNames(error), 2)); 144 | } 145 | }); 146 | } 147 | receive(bot, message) { 148 | return __awaiter(this, void 0, void 0, function* () { 149 | return this.sendToWatson(bot, message, null); 150 | }); 151 | } 152 | interpret(bot, message) { 153 | return __awaiter(this, void 0, void 0, function* () { 154 | return this.sendToWatson(bot, message, null); 155 | }); 156 | } 157 | readContext(user) { 158 | return __awaiter(this, void 0, void 0, function* () { 159 | if (!this.storage) { 160 | throw new Error('readContext is called before the first this.receive call'); 161 | } 162 | return utils_1.readContext(user, this.storage); 163 | }); 164 | } 165 | updateContext(user, context) { 166 | return __awaiter(this, void 0, void 0, function* () { 167 | if (!this.storage) { 168 | throw new Error('updateContext is called before the first this.receive call'); 169 | } 170 | return utils_1.updateContext(user, this.storage, { 171 | context: context, 172 | }); 173 | }); 174 | } 175 | deleteUserData(customerId) { 176 | return __awaiter(this, void 0, void 0, function* () { 177 | const params = { 178 | customerId: customerId, 179 | return_response: true, 180 | }; 181 | try { 182 | const response = yield this.conversation.deleteUserData(params); 183 | debug('deleteUserData response', response); 184 | } 185 | catch (err) { 186 | throw new Error('Failed to delete user data, response code: ' + 187 | err.code + 188 | ', message: ' + 189 | err.message); 190 | } 191 | }); 192 | } 193 | } 194 | exports.WatsonMiddleware = WatsonMiddleware; 195 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /lib/utils.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Storage } from 'botbuilder'; 17 | import AssistantV1 = require('ibm-watson/assistant/v1'); 18 | import { Context, MessageParams, MessageResponse } from 'ibm-watson/assistant/v1'; 19 | export declare function readContext(userId: string, storage: Storage): Promise; 20 | export declare function updateContext(userId: string, storage: Storage, watsonResponse: { 21 | context: Context; 22 | }): Promise<{ 23 | context: Context; 24 | }>; 25 | export declare function postMessage(conversation: AssistantV1, payload: MessageParams): Promise; 26 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | /** 4 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 19 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 20 | return new (P || (P = Promise))(function (resolve, reject) { 21 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 22 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 23 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 24 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 25 | }); 26 | }; 27 | Object.defineProperty(exports, "__esModule", { value: true }); 28 | exports.postMessage = exports.updateContext = exports.readContext = void 0; 29 | // eslint-disable-next-line @typescript-eslint/no-var-requires 30 | const debug = require('debug')('watson-middleware:utils'); 31 | const storagePrefix = 'user.'; 32 | function readContext(userId, storage) { 33 | return __awaiter(this, void 0, void 0, function* () { 34 | const itemId = storagePrefix + userId; 35 | try { 36 | const result = yield storage.read([itemId]); 37 | if (typeof result[itemId] !== 'undefined' && result[itemId].context) { 38 | debug('User: %s, Context: %s', userId, JSON.stringify(result[itemId].context, null, 2)); 39 | return result[itemId].context; 40 | } 41 | } 42 | catch (err) { 43 | debug('User: %s, read context error: %s', userId, err); 44 | } 45 | return null; 46 | }); 47 | } 48 | exports.readContext = readContext; 49 | function updateContext(userId, storage, watsonResponse) { 50 | return __awaiter(this, void 0, void 0, function* () { 51 | const itemId = storagePrefix + userId; 52 | let userData = {}; 53 | try { 54 | const result = yield storage.read([itemId]); 55 | if (typeof result[itemId] !== 'undefined') { 56 | debug('User: %s, Data: %s', userId, JSON.stringify(result[itemId], null, 2)); 57 | userData = result[itemId]; 58 | } 59 | } 60 | catch (err) { 61 | debug('User: %s, read context error: %s', userId, err); 62 | } 63 | userData.id = userId; 64 | userData.context = watsonResponse.context; 65 | const changes = {}; 66 | changes[itemId] = userData; 67 | yield storage.write(changes); 68 | return watsonResponse; 69 | }); 70 | } 71 | exports.updateContext = updateContext; 72 | function postMessage(conversation, payload) { 73 | return __awaiter(this, void 0, void 0, function* () { 74 | debug('Assistant Request: %s', JSON.stringify(payload, null, 2)); 75 | const response = yield conversation.message(payload); 76 | debug('Assistant Response: %s', JSON.stringify(response, null, 2)); 77 | return response.result; 78 | }); 79 | } 80 | exports.postMessage = postMessage; 81 | //# sourceMappingURL=utils.js.map -------------------------------------------------------------------------------- /lib/utils.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";AAAA,uDAAuD;AACvD;;;;;;;;;;;;;;GAcG;;;;;;;;;;;;AAEH,8DAA8D;AAC9D,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,yBAAyB,CAAC,CAAC;AAS1D,MAAM,aAAa,GAAG,OAAO,CAAC;AAE9B,SAAsB,WAAW,CAC/B,MAAc,EACd,OAAgB;;QAEhB,MAAM,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;QAEtC,IAAI;YACF,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YAC5C,IAAI,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;gBACnE,KAAK,CACH,uBAAuB,EACvB,MAAM,EACN,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAChD,CAAC;gBACF,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;aAC/B;SACF;QAAC,OAAO,GAAG,EAAE;YACZ,KAAK,CAAC,kCAAkC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;SACxD;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CAAA;AApBD,kCAoBC;AAED,SAAsB,aAAa,CACjC,MAAc,EACd,OAAgB,EAChB,cAAoC;;QAEpC,MAAM,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;QAEtC,IAAI,QAAQ,GAAQ,EAAE,CAAC;QACvB,IAAI;YACF,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YAC5C,IAAI,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,WAAW,EAAE;gBACzC,KAAK,CACH,oBAAoB,EACpB,MAAM,EACN,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CACxC,CAAC;gBACF,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;aAC3B;SACF;QAAC,OAAO,GAAG,EAAE;YACZ,KAAK,CAAC,kCAAkC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;SACxD;QAED,QAAQ,CAAC,EAAE,GAAG,MAAM,CAAC;QACrB,QAAQ,CAAC,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC;QAC1C,MAAM,OAAO,GAAG,EAAE,CAAC;QACnB,OAAO,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC;QAC3B,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAE7B,OAAO,cAAc,CAAC;IACxB,CAAC;CAAA;AA7BD,sCA6BC;AAED,SAAsB,WAAW,CAC/B,YAAyB,EACzB,OAAsB;;QAEtB,KAAK,CAAC,uBAAuB,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACrD,KAAK,CAAC,wBAAwB,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACnE,OAAO,QAAQ,CAAC,MAAM,CAAC;IACzB,CAAC;CAAA;AARD,kCAQC"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botkit-middleware-watson", 3 | "version": "2.0.1", 4 | "description": "A middleware for using Watson Assistant in a Botkit-powered bot.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "node ./node_modules/typescript/bin/tsc --p ./tsconfig.json", 9 | "pretest": "npm run build", 10 | "test": "jest test --coverage --forceExit", 11 | "lint": "npm run build && eslint '*/**/*.ts' --quiet --fix", 12 | "version": "npm run build && git add -A lib", 13 | "precommit": "lint-staged" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/watson-developer-cloud/botkit-middleware.git" 18 | }, 19 | "keywords": [ 20 | "bot", 21 | "botkit", 22 | "chatbot", 23 | "conversation", 24 | "assistant", 25 | "ibm", 26 | "watson" 27 | ], 28 | "license": "Apache-2.0", 29 | "devDependencies": { 30 | "@types/jest": "^26.0.20", 31 | "@types/nock": "^11.1.0", 32 | "@types/sinon": "^9.0.10", 33 | "@typescript-eslint/eslint-plugin": "^4.15.2", 34 | "@typescript-eslint/parser": "^4.15.2", 35 | "botbuilder-adapter-web": "^1.0.9", 36 | "clone": "^2.1.2", 37 | "codecov": "^3.8.1", 38 | "eslint": "^7.20.0", 39 | "eslint-config-prettier": "^8.0.0", 40 | "eslint-plugin-prettier": "^3.3.1", 41 | "husky": "^5.1.1", 42 | "jest": "^26.6.3", 43 | "lint-staged": "^10.5.4", 44 | "nock": "^13.0.7", 45 | "prettier": "^2.2.1", 46 | "sinon": "^9.2.4", 47 | "ts-jest": "^26.5.1", 48 | "typescript": "^4.1.5" 49 | }, 50 | "prettier": { 51 | "printWidth": 80, 52 | "singleQuote": true, 53 | "trailingComma": "all" 54 | }, 55 | "dependencies": { 56 | "botkit": "^4.10.0", 57 | "debug": "^4.3.1", 58 | "deepmerge": "^4.2.2", 59 | "ibm-watson": "^6.0.2" 60 | }, 61 | "lint-staged": { 62 | "src/**/*.ts": [ 63 | "npm run build", 64 | "npm run lint", 65 | "git add" 66 | ] 67 | }, 68 | "husky": { 69 | "hooks": { 70 | "pre-commit": "lint-staged" 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /** 3 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-var-requires 19 | const debug = require('debug')('watson-middleware:index'); 20 | import Botkit = require('botkit'); 21 | import AssistantV1 = require('ibm-watson/assistant/v1'); 22 | import { AuthenticatorInterface } from 'ibm-cloud-sdk-core/auth'; 23 | import { 24 | Context, 25 | MessageParams, 26 | MessageResponse, 27 | } from 'ibm-watson/assistant/v1'; 28 | import { IamAuthenticator } from 'ibm-watson/auth'; 29 | import { Storage } from 'botbuilder'; 30 | import { readContext, updateContext, postMessage } from './utils'; 31 | import deepMerge = require('deepmerge'); 32 | import { BotkitMessage } from 'botkit'; 33 | 34 | export interface WatsonMiddlewareConfig extends AssistantV1.Options { 35 | workspace_id: string; 36 | minimum_confidence?: number; 37 | storage?: Storage; 38 | } 39 | 40 | /** 41 | * @deprecated please use AssistantV1.MessageParams instead 42 | */ 43 | export type Payload = MessageParams; 44 | 45 | export type BotkitWatsonMessage = BotkitMessage & { 46 | watsonData?: MessageResponse; 47 | watsonError?: string; 48 | }; 49 | 50 | export interface ContextDelta { 51 | [index: string]: any; 52 | } 53 | 54 | export class WatsonMiddleware { 55 | private readonly config: WatsonMiddlewareConfig; 56 | private conversation: AssistantV1; 57 | private storage: Storage; 58 | private readonly minimumConfidence: number = 0.75; 59 | // These are initiated by Slack itself and not from the end-user. Won't send these to WCS. 60 | private readonly ignoreType = ['presence_change', 'reconnect_url']; 61 | 62 | public constructor({ 63 | iam_apikey, 64 | minimum_confidence, 65 | storage, 66 | ...config 67 | }: WatsonMiddlewareConfig) { 68 | if (minimum_confidence) { 69 | this.minimumConfidence = minimum_confidence; 70 | } 71 | if (storage) { 72 | this.storage = storage; 73 | } 74 | if (iam_apikey) { 75 | config.authenticator = new IamAuthenticator({ 76 | apikey: iam_apikey, 77 | }); 78 | } 79 | this.config = config; 80 | 81 | debug( 82 | 'Creating Assistant object with parameters: ' + 83 | JSON.stringify(this.config, null, 2), 84 | ); 85 | this.conversation = new AssistantV1(this.config); 86 | } 87 | 88 | public hear(patterns: string[], message: Botkit.BotkitMessage): boolean { 89 | if (message.watsonData && message.watsonData.intents) { 90 | for (let p = 0; p < patterns.length; p++) { 91 | for (let i = 0; i < message.watsonData.intents.length; i++) { 92 | if ( 93 | message.watsonData.intents[i].intent === patterns[p] && 94 | message.watsonData.intents[i].confidence >= this.minimumConfidence 95 | ) { 96 | return true; 97 | } 98 | } 99 | } 100 | } 101 | return false; 102 | } 103 | 104 | public before( 105 | message: Botkit.BotkitMessage, 106 | payload: MessageParams, 107 | ): Promise { 108 | return Promise.resolve(payload); 109 | } 110 | 111 | public after( 112 | message: Botkit.BotkitMessage, 113 | response: MessageResponse, 114 | ): Promise { 115 | return Promise.resolve(response); 116 | } 117 | 118 | public async sendToWatson( 119 | bot: Botkit.BotWorker, 120 | message: Botkit.BotkitMessage, 121 | contextDelta: ContextDelta, 122 | ): Promise { 123 | if ( 124 | (!message.text && message.type !== 'welcome') || 125 | this.ignoreType.indexOf(message.type) !== -1 || 126 | message.reply_to || 127 | message.bot_id 128 | ) { 129 | // Ignore messages initiated by Slack. Reply with dummy output object 130 | message.watsonData = { 131 | output: { 132 | text: [], 133 | }, 134 | }; 135 | return; 136 | } 137 | 138 | this.storage = bot.controller.storage; 139 | 140 | try { 141 | const userContext = await readContext(message.user, this.storage); 142 | 143 | const payload: MessageParams = { 144 | workspaceId: this.config.workspace_id, 145 | }; 146 | if (message.text) { 147 | // text can not contain the following characters: tab, new line, carriage return. 148 | const sanitizedText = message.text.replace(/[\r\n\t]/g, ' '); 149 | payload.input = { 150 | text: sanitizedText, 151 | }; 152 | } 153 | if (userContext) { 154 | payload.context = userContext; 155 | } 156 | if (contextDelta) { 157 | if (!userContext) { 158 | //nothing to merge, this is the first context 159 | payload.context = contextDelta; 160 | } else { 161 | payload.context = deepMerge(payload.context, contextDelta); 162 | } 163 | } 164 | if ( 165 | payload.context && 166 | payload.context.workspace_id && 167 | payload.context.workspace_id.length === 36 168 | ) { 169 | payload.workspaceId = payload.context.workspace_id; 170 | } 171 | 172 | const watsonRequest = await this.before(message, payload); 173 | let watsonResponse = await postMessage(this.conversation, watsonRequest); 174 | if (typeof watsonResponse.output.error === 'string') { 175 | debug('Error: %s', watsonResponse.output.error); 176 | message.watsonError = watsonResponse.output.error; 177 | } 178 | watsonResponse = await this.after(message, watsonResponse); 179 | 180 | message.watsonData = watsonResponse; 181 | await updateContext(message.user, this.storage, watsonResponse); 182 | } catch (error) { 183 | message.watsonError = error; 184 | debug( 185 | 'Error: %s', 186 | JSON.stringify(error, Object.getOwnPropertyNames(error), 2), 187 | ); 188 | } 189 | } 190 | 191 | public async receive( 192 | bot: Botkit.BotWorker, 193 | message: Botkit.BotkitMessage, 194 | ): Promise { 195 | return this.sendToWatson(bot, message, null); 196 | } 197 | 198 | public async interpret( 199 | bot: Botkit.BotWorker, 200 | message: Botkit.BotkitMessage, 201 | ): Promise { 202 | return this.sendToWatson(bot, message, null); 203 | } 204 | 205 | public async readContext(user: string): Promise { 206 | if (!this.storage) { 207 | throw new Error( 208 | 'readContext is called before the first this.receive call', 209 | ); 210 | } 211 | return readContext(user, this.storage); 212 | } 213 | 214 | public async updateContext( 215 | user: string, 216 | context: Context, 217 | ): Promise<{ context: Context }> { 218 | if (!this.storage) { 219 | throw new Error( 220 | 'updateContext is called before the first this.receive call', 221 | ); 222 | } 223 | return updateContext(user, this.storage, { 224 | context: context, 225 | }); 226 | } 227 | 228 | public async deleteUserData(customerId: string) { 229 | const params = { 230 | customerId: customerId, 231 | return_response: true, 232 | }; 233 | try { 234 | const response = await this.conversation.deleteUserData(params); 235 | debug('deleteUserData response', response); 236 | } catch (err) { 237 | throw new Error( 238 | 'Failed to delete user data, response code: ' + 239 | err.code + 240 | ', message: ' + 241 | err.message, 242 | ); 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /** 3 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-var-requires 19 | const debug = require('debug')('watson-middleware:utils'); 20 | import { Storage } from 'botbuilder'; 21 | import AssistantV1 = require('ibm-watson/assistant/v1'); 22 | import { 23 | Context, 24 | MessageParams, 25 | MessageResponse, 26 | } from 'ibm-watson/assistant/v1'; 27 | 28 | const storagePrefix = 'user.'; 29 | 30 | export async function readContext( 31 | userId: string, 32 | storage: Storage, 33 | ): Promise { 34 | const itemId = storagePrefix + userId; 35 | 36 | try { 37 | const result = await storage.read([itemId]); 38 | if (typeof result[itemId] !== 'undefined' && result[itemId].context) { 39 | debug( 40 | 'User: %s, Context: %s', 41 | userId, 42 | JSON.stringify(result[itemId].context, null, 2), 43 | ); 44 | return result[itemId].context; 45 | } 46 | } catch (err) { 47 | debug('User: %s, read context error: %s', userId, err); 48 | } 49 | return null; 50 | } 51 | 52 | export async function updateContext( 53 | userId: string, 54 | storage: Storage, 55 | watsonResponse: { context: Context }, 56 | ): Promise<{ context: Context }> { 57 | const itemId = storagePrefix + userId; 58 | 59 | let userData: any = {}; 60 | try { 61 | const result = await storage.read([itemId]); 62 | if (typeof result[itemId] !== 'undefined') { 63 | debug( 64 | 'User: %s, Data: %s', 65 | userId, 66 | JSON.stringify(result[itemId], null, 2), 67 | ); 68 | userData = result[itemId]; 69 | } 70 | } catch (err) { 71 | debug('User: %s, read context error: %s', userId, err); 72 | } 73 | 74 | userData.id = userId; 75 | userData.context = watsonResponse.context; 76 | const changes = {}; 77 | changes[itemId] = userData; 78 | await storage.write(changes); 79 | 80 | return watsonResponse; 81 | } 82 | 83 | export async function postMessage( 84 | conversation: AssistantV1, 85 | payload: MessageParams, 86 | ): Promise { 87 | debug('Assistant Request: %s', JSON.stringify(payload, null, 2)); 88 | const response = await conversation.message(payload); 89 | debug('Assistant Response: %s', JSON.stringify(response, null, 2)); 90 | return response.result; 91 | } 92 | -------------------------------------------------------------------------------- /test/context-store.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { readContext, updateContext } from '../lib/utils'; 18 | import { Botkit, BotkitMessage } from 'botkit'; 19 | import { MemoryStorage } from 'botbuilder'; 20 | import { WebAdapter } from 'botbuilder-adapter-web'; 21 | import sinon = require('sinon'); 22 | 23 | const message: BotkitMessage = { 24 | type: 'message', 25 | channel: 'D2BQEJJ1X', 26 | user: 'U2BLZSKFG', 27 | text: 'Hello there!', 28 | ts: '1475776074.000004', 29 | team: 'T2BM5DPJ6', 30 | incoming_message: null, 31 | reference: null, 32 | }; 33 | 34 | const conversation_response = { 35 | intents: [], 36 | entities: [], 37 | input: { 38 | text: 'Hello there!', 39 | }, 40 | output: { 41 | log_messages: [], 42 | text: [ 43 | 'Hi. It looks like a nice drive today. What would you like me to do? ', 44 | ], 45 | nodes_visited: ['node_1_1467221909631'], 46 | }, 47 | context: { 48 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 49 | system: { 50 | dialog_stack: ['root'], 51 | dialog_turn_counter: 1, 52 | dialog_request_counter: 1, 53 | }, 54 | default_counter: 0, 55 | }, 56 | }; 57 | 58 | const adapter = new WebAdapter({ noServer: true }); 59 | const controller = new Botkit({ 60 | adapter: adapter, 61 | storage: new MemoryStorage(), //specifying storage explicitly eliminates 3 lines of warning output 62 | disable_webserver: true, 63 | }); 64 | 65 | const storage = controller.storage; 66 | 67 | test('should read context correctly', function() { 68 | return readContext(message.user, storage).then(function(context) { 69 | expect(context).toEqual(null); 70 | }); 71 | }); 72 | 73 | test('should suppress storage error', function() { 74 | const storageStub = sinon.stub(storage, 'read').rejects('error message'); 75 | 76 | return readContext(message.user, storage) 77 | .then(function() { 78 | storageStub.restore(); 79 | }) 80 | .catch(function(err) { 81 | storageStub.restore(); 82 | throw err; 83 | }); 84 | }); 85 | 86 | test('should store context of the first response', function() { 87 | const itemId = 'user.' + message.user; 88 | return updateContext(message.user, storage, conversation_response) 89 | .then(function() { 90 | return storage.read([itemId]); 91 | }) 92 | .then(function(data) { 93 | expect(data[itemId].context).toEqual(conversation_response.context); 94 | }); 95 | }); 96 | 97 | test('should ignore storage error on read when user is not saved yet', function() { 98 | const storageStub1 = sinon 99 | .stub(storage, 'read') 100 | .rejects(new Error('error message')); 101 | const storageStub2 = sinon.stub(storage, 'write').resolves(); 102 | 103 | const watsonResponse = { 104 | context: { 105 | a: 1, 106 | }, 107 | }; 108 | return updateContext('NEWUSER3', storage, watsonResponse) 109 | .then(function(response) { 110 | expect(response).toEqual(watsonResponse); 111 | storageStub1.restore(); 112 | storageStub2.restore(); 113 | }) 114 | .catch(function() { 115 | storageStub1.restore(); 116 | storageStub2.restore(); 117 | }); 118 | }); 119 | 120 | test('should return storage error on write', function() { 121 | const storageStub = sinon.stub(storage, 'write').rejects('error message'); 122 | 123 | return updateContext(message.user, storage, conversation_response) 124 | .then(function(err) { 125 | expect(err).toEqual('error message'); 126 | storageStub.restore(); 127 | }) 128 | .catch(function() { 129 | storageStub.restore(); 130 | }); 131 | }); 132 | 133 | test('should update existing context', function() { 134 | const firstContext = { 135 | a: 1, 136 | b: 2, 137 | }; 138 | const secondContext = { 139 | c: 3, 140 | d: 4, 141 | }; 142 | //first update 143 | return updateContext(message.user, storage, { 144 | context: firstContext, 145 | }) 146 | .then(function() { 147 | //second update 148 | return updateContext(message.user, storage, { 149 | context: secondContext, 150 | }); 151 | }) 152 | .then(function() { 153 | return storage.read(['user.U2BLZSKFG']); 154 | }) 155 | .then(function(data) { 156 | expect(data['user.U2BLZSKFG'].context).toEqual(secondContext); 157 | }); 158 | }); 159 | 160 | test('should preserve other data in storage', function() { 161 | const user = { 162 | id: 'U2BLZSKFX', 163 | profile: { 164 | age: 23, 165 | sex: 'male', 166 | }, 167 | }; 168 | 169 | const newContext = { 170 | a: 1, 171 | b: 2, 172 | }; 173 | 174 | const itemId = 'user.' + user.id; 175 | 176 | const existingData = {}; 177 | existingData[itemId] = user; 178 | 179 | return storage 180 | .write(existingData) 181 | .then(function() { 182 | return updateContext(user.id, storage, { 183 | context: newContext, 184 | }); 185 | }) 186 | .then(function() { 187 | return storage.read([itemId]); 188 | }) 189 | .then(function(data) { 190 | expect(data[itemId].profile).toEqual(user.profile); 191 | expect(data[itemId].context).toEqual(newContext); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /test/middleware-delete-user-data.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { WatsonMiddleware } from '../lib'; 18 | import nock = require('nock'); 19 | import { NoAuthAuthenticator } from 'ibm-watson/auth'; 20 | 21 | //Watson Assistant params 22 | const service = { 23 | authenticator: new NoAuthAuthenticator(), 24 | url: 'http://ibm.com:80', 25 | version: '2018-07-10', 26 | }; 27 | 28 | const workspaceId = 'zyxwv-54321'; 29 | 30 | const customerId = 'XXXXXX'; 31 | 32 | const middleware = new WatsonMiddleware({ 33 | ...service, 34 | workspace_id: workspaceId, 35 | }); 36 | 37 | beforeEach(function() { 38 | nock.disableNetConnect(); 39 | }); 40 | 41 | afterEach(function() { 42 | nock.cleanAll(); 43 | }); 44 | 45 | test('makes delete request', async () => { 46 | nock(service.url) 47 | .delete(`/v1/user_data?version=2018-07-10&customer_id=${customerId}`) 48 | .reply(202, ''); 49 | 50 | await middleware.deleteUserData(customerId); 51 | }); 52 | 53 | test('throws error on unexpected response code', async () => { 54 | nock(service.url) 55 | .delete(`/v1/user_data?version=2018-07-10&customer_id=${customerId}`) 56 | .reply(404, ''); 57 | 58 | return await expect(middleware.deleteUserData(customerId)).rejects.toThrow( 59 | 'Failed to delete user data, response code: 404, message: null', 60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /test/middleware-receive.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Botkit, BotkitMessage } from 'botkit'; 18 | import { MemoryStorage } from 'botbuilder'; 19 | import { WebAdapter } from 'botbuilder-adapter-web'; 20 | import { WatsonMiddleware, BotkitWatsonMessage } from '../lib'; 21 | import { NoAuthAuthenticator } from 'ibm-watson/auth'; 22 | import nock = require('nock'); 23 | 24 | //Watson Assistant params 25 | const service = { 26 | authenticator: new NoAuthAuthenticator(), 27 | url: 'http://ibm.com:80', 28 | version: '2018-07-10', 29 | }; 30 | const workspaceId = 'zyxwv-54321'; 31 | const path = `/v1/workspaces/${workspaceId}/message`; 32 | const pathWithQuery = `${path}?version=${service.version}`; 33 | const message: BotkitMessage = { 34 | type: 'message', 35 | channel: 'D2BQEJJ1X', 36 | user: 'U2BLZSKFG', 37 | text: 'hi', 38 | ts: '1475776074.000004', 39 | team: 'T2BM5DPJ6', 40 | reference: null, 41 | incoming_message: null, 42 | }; 43 | 44 | const adapter = new WebAdapter({ noServer: true }); 45 | const controller = new Botkit({ 46 | adapter: adapter, 47 | storage: new MemoryStorage(), //specifying storage explicitly eliminates 3 lines of warning output 48 | disable_webserver: true, 49 | }); 50 | 51 | const middleware = new WatsonMiddleware({ 52 | ...service, 53 | workspace_id: workspaceId, 54 | }); 55 | let bot = null; 56 | 57 | beforeEach(function(done) { 58 | nock.disableNetConnect(); 59 | controller.spawn({}).then(botWorker => { 60 | bot = botWorker; 61 | done(); 62 | }); 63 | }); 64 | 65 | afterEach(function() { 66 | nock.cleanAll(); 67 | }); 68 | 69 | test('should make first call to Assistant', async () => { 70 | const expectedWatsonData = { 71 | intents: [], 72 | entities: [], 73 | input: { 74 | text: 'hi', 75 | }, 76 | output: { 77 | log_messages: [], 78 | text: ['Hello from Watson Assistant!'], 79 | nodes_visited: ['node_1_1467221909631'], 80 | }, 81 | context: { 82 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 83 | system: { 84 | dialog_stack: ['root'], 85 | dialog_turn_counter: 1, 86 | dialog_request_counter: 1, 87 | }, 88 | }, 89 | }; 90 | nock(service.url) 91 | .post(pathWithQuery) 92 | .reply(200, expectedWatsonData); 93 | 94 | const receivedMessage: BotkitWatsonMessage = { ...message }; 95 | await middleware.receive(bot, receivedMessage); 96 | expect(receivedMessage.watsonData).toEqual(expectedWatsonData); 97 | }); 98 | 99 | test('should make second call to Assistant', async () => { 100 | const receivedMessage: BotkitWatsonMessage = { 101 | ...message, 102 | text: 'What can you do?', 103 | }; 104 | 105 | const expectedWatsonData = { 106 | intents: [], 107 | entities: [], 108 | input: { 109 | text: 'What can you do?', 110 | }, 111 | output: { 112 | log_messages: [], 113 | text: ['I can tell you about myself. I have a charming personality!'], 114 | nodes_visited: ['node_3_1467221909631'], 115 | }, 116 | context: { 117 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 118 | system: { 119 | dialog_stack: ['root'], 120 | dialog_turn_counter: 2, 121 | dialog_request_counter: 2, 122 | }, 123 | }, 124 | }; 125 | 126 | nock(service.url) 127 | .post(pathWithQuery) 128 | .reply(200, expectedWatsonData); 129 | 130 | await middleware.receive(bot, receivedMessage); 131 | expect(receivedMessage.watsonData).toEqual(expectedWatsonData); 132 | }); 133 | 134 | test('should pass empty welcome message to Assistant', async () => { 135 | const expectedWatsonData = { 136 | intents: [], 137 | entities: [], 138 | input: { 139 | text: 'hi', 140 | }, 141 | output: { 142 | log_messages: [], 143 | text: ['Hello from Watson Assistant!'], 144 | nodes_visited: ['node_1_1467221909631'], 145 | }, 146 | context: { 147 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 148 | system: { 149 | dialog_stack: ['root'], 150 | dialog_turn_counter: 1, 151 | dialog_request_counter: 1, 152 | }, 153 | }, 154 | }; 155 | nock(service.url) 156 | .post(pathWithQuery) 157 | .reply(200, expectedWatsonData); 158 | 159 | const welcomeMessage: BotkitWatsonMessage = { ...message, type: 'welcome' }; 160 | 161 | await middleware.receive(bot, welcomeMessage); 162 | expect(welcomeMessage.watsonData).toEqual(expectedWatsonData); 163 | }); 164 | 165 | test('should replace not-permitted characters in message text', async () => { 166 | // text can not contain the following characters: tab, new line, carriage return. 167 | const receivedMessage: BotkitWatsonMessage = { 168 | ...message, 169 | text: 'What\tcan\tyou\r\ndo?', 170 | }; 171 | const expectedMessage = 'What can you do?'; 172 | 173 | const expectedWatsonData = { 174 | intents: [], 175 | entities: [], 176 | input: { 177 | text: expectedMessage, 178 | }, 179 | output: { 180 | log_messages: [], 181 | text: ['I can tell you about myself. I have a charming personality!'], 182 | nodes_visited: ['node_3_1467221909631'], 183 | }, 184 | context: { 185 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 186 | system: { 187 | dialog_stack: ['root'], 188 | dialog_turn_counter: 2, 189 | dialog_request_counter: 2, 190 | }, 191 | }, 192 | }; 193 | 194 | nock(service.url) 195 | .post(pathWithQuery, ({ input }) => input.text === expectedMessage) 196 | .reply(200, expectedWatsonData); 197 | 198 | await middleware.receive(bot, receivedMessage); 199 | expect(receivedMessage.watsonData).toEqual(expectedWatsonData); 200 | }); 201 | -------------------------------------------------------------------------------- /test/middleware-send-to-watson.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Botkit } from 'botkit'; 18 | import { updateContext } from '../lib/utils'; 19 | import { clonePrototype } from 'clone'; 20 | import { MemoryStorage } from 'botbuilder'; 21 | import { WebAdapter } from 'botbuilder-adapter-web'; 22 | import { WatsonMiddleware, BotkitWatsonMessage } from '../lib'; 23 | import { Context } from 'ibm-watson/assistant/v1'; 24 | import { NoAuthAuthenticator } from 'ibm-watson/auth'; 25 | import nock = require('nock'); 26 | 27 | //Watson Assistant params 28 | const service = { 29 | authenticator: new NoAuthAuthenticator(), 30 | url: 'http://ibm.com:80', 31 | version: '2018-07-10', 32 | }; 33 | const workspaceId = 'zyxwv-54321'; 34 | const path = `/v1/workspaces/${workspaceId}/message`; 35 | const pathWithQuery = `${path}?version=${service.version}`; 36 | const message = { 37 | type: 'message', 38 | channel: 'D2BQEJJ1X', 39 | user: 'U2BLZSKFG', 40 | text: 'hi', 41 | ts: '1475776074.000004', 42 | team: 'T2BM5DPJ6', 43 | reference: null, 44 | incoming_message: null, 45 | }; 46 | 47 | const adapter = new WebAdapter({ noServer: true }); 48 | const controller = new Botkit({ 49 | adapter: adapter, 50 | storage: new MemoryStorage(), //specifying storage explicitly eliminates 3 lines of warning output 51 | disable_webserver: true, 52 | }); 53 | const middleware = new WatsonMiddleware({ 54 | ...service, 55 | workspace_id: workspaceId, 56 | }); 57 | 58 | let bot; 59 | 60 | beforeEach(function(done) { 61 | nock.disableNetConnect(); 62 | controller.spawn({}).then(botWorker => { 63 | bot = botWorker; 64 | done(); 65 | }); 66 | }); 67 | 68 | afterEach(function() { 69 | nock.cleanAll(); 70 | }); 71 | 72 | test('should update context if contextDelta is provided', async () => { 73 | const storedContext = { 74 | a: 1, 75 | b: 'string', 76 | c: { 77 | d: 2, 78 | e: 3, 79 | f: { 80 | g: 4, 81 | h: 5, 82 | }, 83 | }, 84 | }; 85 | const contextDelta = { 86 | b: null, 87 | j: 'new string', 88 | c: { 89 | f: { 90 | g: 5, 91 | i: 6, 92 | }, 93 | }, 94 | }; 95 | const expectedContextInRequest = { 96 | a: 1, 97 | b: null, 98 | c: { 99 | d: 2, 100 | e: 3, 101 | f: { 102 | g: 5, 103 | h: 5, 104 | i: 6, 105 | }, 106 | }, 107 | j: 'new string', 108 | }; 109 | const expectedContextInResponse: Context = { 110 | ...clonePrototype(expectedContextInRequest), 111 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 112 | system: { 113 | dialog_stack: ['root'], 114 | dialog_turn_counter: 1, 115 | dialog_request_counter: 1, 116 | }, 117 | }; 118 | 119 | const messageToSend: BotkitWatsonMessage = { ...message }; 120 | const expectedRequest = { 121 | input: { 122 | text: messageToSend.text, 123 | }, 124 | context: expectedContextInRequest, 125 | }; 126 | const mockedWatsonResponse = { 127 | intents: [], 128 | entities: [], 129 | input: { 130 | text: 'hi', 131 | }, 132 | output: { 133 | log_messages: [], 134 | text: ['Hello from Watson Assistant!'], 135 | nodes_visited: ['node_1_1467221909631'], 136 | }, 137 | context: expectedContextInResponse, 138 | }; 139 | 140 | //verify request and return mocked response 141 | nock(service.url) 142 | .post(pathWithQuery, expectedRequest) 143 | .reply(200, mockedWatsonResponse); 144 | 145 | await updateContext(messageToSend.user, controller.storage, { 146 | context: storedContext, 147 | }); 148 | await middleware.sendToWatson(bot, messageToSend, contextDelta); 149 | expect(messageToSend.watsonData.context).toEqual(expectedContextInResponse); 150 | }); 151 | 152 | test('should make request to different workspace, if workspace_id is set in context', async function() { 153 | const newWorkspaceId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; 154 | const expectedPath = `/v1/workspaces/${newWorkspaceId}/message`; 155 | const storedContext = { 156 | workspace_id: newWorkspaceId, 157 | }; 158 | const messageToSend: BotkitWatsonMessage = { ...message }; 159 | const expectedContextInResponse: Context = { 160 | ...clonePrototype(storedContext), 161 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 162 | system: { 163 | dialog_stack: ['root'], 164 | dialog_turn_counter: 1, 165 | dialog_request_counter: 1, 166 | }, 167 | }; 168 | 169 | const expectedRequest = { 170 | input: { 171 | text: messageToSend.text, 172 | }, 173 | context: storedContext, 174 | }; 175 | 176 | const mockedWatsonResponse = { 177 | intents: [], 178 | entities: [], 179 | input: { 180 | text: 'hi', 181 | }, 182 | output: { 183 | log_messages: [], 184 | text: ['Hello from Watson Assistant!'], 185 | nodes_visited: ['node_1_1467221909631'], 186 | }, 187 | context: expectedContextInResponse, 188 | }; 189 | 190 | //verify request and return mocked response 191 | nock(service.url) 192 | .post(expectedPath + '?version=' + service.version, expectedRequest) 193 | .reply(200, mockedWatsonResponse); 194 | 195 | try { 196 | await updateContext(messageToSend.user, controller.storage, { 197 | context: storedContext, 198 | }); 199 | await middleware.sendToWatson(bot, messageToSend, {}); 200 | expect(messageToSend.watsonError).toBeUndefined(); 201 | } catch (err) { 202 | throw err; 203 | } 204 | }); 205 | -------------------------------------------------------------------------------- /test/post-message.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016-2019 IBM Corp. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import utils = require('../lib/utils'); 18 | import nock = require('nock'); 19 | import AssistantV1 = require('ibm-watson/assistant/v1'); 20 | import { NoAuthAuthenticator } from 'ibm-watson/auth'; 21 | 22 | //Watson Assistant params 23 | const service = { 24 | authenticator: new NoAuthAuthenticator(), 25 | url: 'http://ibm.com:80', 26 | version: '2018-07-10', 27 | }; 28 | const workspaceId = 'zyxwv-54321'; 29 | const path = `/v1/workspaces/${workspaceId}/message`; 30 | const pathWithQuery = `${path}?version=${service.version}`; 31 | const conversation = new AssistantV1(service); 32 | 33 | beforeEach(function() { 34 | nock.disableNetConnect(); 35 | }); 36 | 37 | afterEach(function() { 38 | nock.cleanAll(); 39 | }); 40 | 41 | it('should initiate a conversation', function() { 42 | const expected = { 43 | intents: [], 44 | entities: [], 45 | input: { 46 | text: 'hi', 47 | }, 48 | output: { 49 | log_messages: [], 50 | text: ['Hello from Watson Assistant!'], 51 | nodes_visited: ['node_1_1467221909631'], 52 | }, 53 | context: { 54 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 55 | system: { 56 | dialog_stack: ['root'], 57 | dialog_turn_counter: 1, 58 | dialog_request_counter: 1, 59 | }, 60 | }, 61 | }; 62 | 63 | nock(service.url) 64 | .post(pathWithQuery) 65 | .reply(200, expected); 66 | 67 | return utils 68 | .postMessage(conversation, { 69 | workspaceId: workspaceId, 70 | input: { 71 | text: 'hi', 72 | }, 73 | }) 74 | .then(function(response) { 75 | expect(response).toEqual(expected); 76 | }); 77 | }); 78 | 79 | it('should continue a conversation', async () => { 80 | const expected = { 81 | intents: [], 82 | entities: [], 83 | input: { 84 | text: 'What can you do?', 85 | }, 86 | output: { 87 | log_messages: [], 88 | text: ['I can tell you about myself. I have a charming personality!'], 89 | nodes_visited: ['node_3_1467221909631'], 90 | }, 91 | context: { 92 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 93 | system: { 94 | dialog_stack: ['root'], 95 | dialog_turn_counter: 2, 96 | dialog_request_counter: 2, 97 | }, 98 | }, 99 | }; 100 | 101 | nock(service.url) 102 | .post(pathWithQuery) 103 | .reply(200, expected); 104 | 105 | const response = await utils.postMessage(conversation, { 106 | workspaceId: workspaceId, 107 | input: { 108 | text: 'What can you do?', 109 | }, 110 | context: { 111 | conversation_id: '8a79f4db-382c-4d56-bb88-1b320edf9eae', 112 | system: { 113 | dialog_stack: ['root'], 114 | dialog_turn_counter: 1, 115 | dialog_request_counter: 1, 116 | }, 117 | }, 118 | }); 119 | expect(response).toEqual(expected); 120 | }); 121 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "sourceMap": true, 7 | "outDir": "./lib", 8 | "rootDir": "./src", 9 | "lib": [ 10 | "es6", 11 | "dom" 12 | ] 13 | }, 14 | "include": [ 15 | "./src/*", 16 | ], 17 | "exclude": [ 18 | "./node_modules", 19 | "./lib" 20 | ] 21 | } 22 | --------------------------------------------------------------------------------