├── .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! [](https://travis-ci.org/watson-developer-cloud/botkit-middleware) [](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 | 
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 |
--------------------------------------------------------------------------------