├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── gulpfile.js ├── index.js ├── lib ├── Bot.js ├── BotTypes.js └── ContextStore.js ├── package.json ├── test ├── BotTest.js ├── BotTypesTest.js └── ContextStoreTest.js └── wiki ├── CLASSIFIER.md └── SKILLS.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage 4 | 5 | *.iml -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | test 3 | .travis.yml 4 | gulpfile.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | after_success: 5 | - npm run coveralls 6 | - npm run lint 7 | branches: 8 | only: 9 | - master 10 | deploy: 11 | on: 12 | branch: master 13 | provider: npm 14 | skip_cleanup: true 15 | email: manthanhd@live.com 16 | api_key: 17 | secure: Tp6gfkOfvEvDFqb4GN8wEurQM1Wt/kZE3Tqo8Vx/2lhytE87bhovAwQj6Xl2d8oH+RxZYOoRpxbMy/mY94m7bYNrfSy90Kgjp0GuoQqJTivy83EZw+wfW5eS97uqYC9omjOjL9dx4o/WzbsVGkIxJ3IKfHaGan2sUA54rkW320t9onUDSSyo3Kz4WEMa0hzt10q1UPksMYMEsEQxHNgWzycNIv4bTOSYE9kjfI/x7LHifAasqZRlfQzNbgmg0XzHM1Y3AaQSRXZCEpTOwayqHQwj4qxurfgAYe7erB8t/eKVht+IxJfqcjbEuzJHVVZBiMxUG2buL7aWLoXRUL/zHvUjq6JXvPe8TTJ80DIrceyGI/dmqzMycnZZDa9ZVGhEtaAVOK4mM2J+ZWdfzsL3OSx/QJ3f85C5WVL2oXbXrpl4Xf/berMSTn60o5GUfnBGBN6Ny8AurnUXsoecB19+GN7IBrRfsSEWpIM2uvIcmpM+7tc+0xQByhynJ6MhX/mzR0jIkacFfTW76CRnfiWnoPqFcHs2Oh80KE4LYZtN1RUx97KW+aEL7E8r13o/sGpy6xrVO9woHtDQurRzCIHhKeohSTjPgGeN0O8U4uSjzArlaNp0jq9Mc4mPiji7fw7dey6WOoU50Yo1oQ/naC5Y6evQOrAe0nKgb0ooGbtYgv8= 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Talkify Changelog 2 | 3 | High level features between major versions are outlined here. 4 | 5 | ## 2016-11-09 Version 2.0.0 @manthanhd 6 | 7 | * Methods in the `Response` object are chainable. 8 | * Skills can now lock a conversation for one request/response in order to process a follow-up question. 9 | * The classifier interface defined in [talkify-classifier](https://github.com/manthanhd/talkify-classifier) npm module can be used to write a custom classifier. 10 | * Default classifier is [talkify-natural-classifier](https://github.com/manthanhd/talkify-natural-classifier). 11 | * Helpers `StaticResponseSkill` and `StaticRandomResponseSkill` are now available. 12 | 13 | ## 2016-10-25 Version 1.0.0 @manthanhd 14 | 15 | * Skills now have name as its first parameter. 16 | * Skill can be mapped to undefined topic. This allows execution of skills when no topic is found or a skill-topic mapping is not found. 17 | * While adding a new skill, you can now specify minimum required confidence level as second parameter. This allows you to map multiple skills to the same topic but at different confidence levels. 18 | * Fix for protecting skill references from cross-context async calls. 19 | 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Talkify 2 | 3 | 4 | * [Code of Conduct](#code-of-conduct) 5 | * [Contributing code](#contributing-code) 6 | * [1. Fork](#1.-fork) 7 | * [2. Develop code](#2.-develop-code) 8 | * [3. Merge contribution](#3.-merge-contribution) 9 | 10 | 11 | 12 | ## Code of Conduct 13 | 14 | The code of conduct is simple. Please treat others like you would like to be treated yourself. Please be kind and courteous. Insulting behaviour, publicly or privately will not be tolerated. 15 | 16 | Harassment is unacceptable. If you are harassed, privately or publicly, please do not hesitate to contact me or anyone in this repository with a photo or email or capture of the harassment if possible. This includes behaviour considered spamming, trolling, flaming or baiting. 17 | 18 | I am dedicated to making this a safe place to collaborate and communicate. 19 | 20 | ## Contributing code 21 | 22 | Please follow the following steps to contribute code to this repo. 23 | 24 | ### 1. Fork 25 | 26 | Click on the fork button on the top left of the [repository page](https://github.com/manthanhd/talkify) to fork the project and checkout your local copy. 27 | 28 | ``` 29 | $ git clone git@github.com:username/talkify.git 30 | $ cd node 31 | $ git remote add upstream git://github.com/manthanhd/talkify.git 32 | ``` 33 | 34 | ### 2. Develop code 35 | 36 | Make sure you create a new branch before you start your work. The simplest way to create a new branch locally is to use the `checkout -b` command. 37 | 38 | ``` 39 | $ git checkout -b branch_name 40 | ``` 41 | 42 | When you are making your changes, you should run `npm test` to make sure that all the tests pass. At the very least, make sure that all the tests pass at the end of your changes. 43 | 44 | Your changes are expected to have self-contained tests within it. Please ensure that you write test cases covering scenarios. The idea here is 'leave it in a better state than you found it in'. 45 | 46 | Make sure you make commits with useful messages. Great commits are less than 50 characters long. If you need to provide more details, use new lines. 47 | 48 | When you are ready, push your commits: 49 | 50 | ``` 51 | $ git push origin branch_name 52 | ``` 53 | 54 | 55 | ### 3. Merge contribution 56 | 57 | Navigate to your repository URL. This might be ` https://github.com/username/talkify`. Provided you have just pushed your code, GitHub should provide you with a prompt to raise a pull request. 58 | 59 | Provide as much information as you can as to what value the pull request brings. Please make an effort to reference any existing issues (if any) that the pull request impacts. 60 | 61 | I will try to review your pull request within a few days of opening. If there are comments, please address them in the commits and once you push them, please comment back on the pull request when it is done, replying to the original comment. 62 | 63 | By submitting a pull request, you agree that the work you have submitted is your own and/or is covered under an appropriate open source license under which you are allowed to make the aforementioned contribution in part or whole. You are aware of the fact that the contribution that you submit is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. 64 | 65 | *Inspired from Node.js [Contributing](https://github.com/nodejs/node/blob/a4d396d85874046ffe6647ecb953fd78e16bcba3/CONTRIBUTING.md) and [Code of Conduct](https://raw.githubusercontent.com/nodejs/node/fcf7696bc1b64c61a6263d1f13f2af8501dbd207/CODE_OF_CONDUCT.md) guides*. 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Manthan Dave 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Talkify 2 | Framework for developing chat bot applications. 3 | 4 | [![npm version](https://badge.fury.io/js/talkify.svg)](https://badge.fury.io/js/talkify) [![Build Status](https://travis-ci.org/manthanhd/talkify.svg?branch=master)](https://travis-ci.org/manthanhd/talkify) [![Coverage Status](https://coveralls.io/repos/github/manthanhd/talkify/badge.svg?branch=master)](https://coveralls.io/github/manthanhd/talkify?branch=master) 5 | 6 | 7 | * [Usage](#usage) 8 | * [Setup](#setup) 9 | * [Code Tutorial](#code-tutorial) 10 | * [Initialize](#initialize) 11 | * [Train](#train) 12 | * [Add Skills](#add-skills) 13 | * [Resolve queries](#resolve-queries) 14 | * [Chainable methods](#chainable-methods) 15 | * [Configuration Options](#configuration-options) 16 | * [Context Store](#context-store) 17 | * [Classifier](#classifier) 18 | * [Extending bot](#extending-bot) 19 | * [Context management](#context-management) 20 | * [Custom Classifier](#custom-classifier) 21 | * [Reference Documentation](#reference-documentation) 22 | * [Contributing](#contributing) 23 | 24 | 25 | 26 | # Usage 27 | ## Setup 28 | 29 | Make sure you have node and npm installed. As of now, this module has been tested against 0.12 node version within the [Travis CI](https://travis-ci.org/manthanhd/talkify) pipeline. 30 | 31 | Simply run `npm install` command to install: 32 | 33 | ```bash 34 | npm install --save talkify 35 | ``` 36 | 37 | ## Code Tutorial 38 | 39 | ### Initialize 40 | 41 | Require the main module, types and dependencies. The following command loads everything that you need from the module. 42 | 43 | ```javascript 44 | // Core dependency 45 | const talkify = require('talkify'); 46 | const Bot = talkify.Bot; 47 | 48 | // Types dependencies 49 | const BotTypes = talkify.BotTypes; 50 | const Message = BotTypes.Message; 51 | const SingleLineMessage = BotTypes.SingleLineMessage; 52 | const MultiLineMessage = BotTypes.MultiLineMessage; 53 | 54 | // Skills dependencies 55 | const Skill = BotTypes.Skill; 56 | 57 | // Training dependencies 58 | const TrainingDocument = BotTypes.TrainingDocument; 59 | ``` 60 | 61 | Once the dependencies have been loaded, you can initialise the bot core. 62 | 63 | ```javascript 64 | const bot = new Bot(); 65 | ``` 66 | 67 | The `Bot()` constructor also accepts parameters in the form of configuration object. Here you can pass in configuration switch values or alternate implementations for things like `ContextStore` and `Classifier` etc. We'll cover that afterwards in the [Configuration Options](#configuration-options) section. 68 | 69 | ### Train 70 | 71 | Once Bot has been initialised, the first thing you should do is to train it. To train it one document at a time synchronously, you can use the `train` method: 72 | 73 | ```javascript 74 | bot.trainAll([ 75 | new TrainingDocument('how_are_you', 'how are you'), 76 | new TrainingDocument('how_are_you', 'how are you going'), 77 | new TrainingDocument('how_are_you', 'how is it going'), 78 | 79 | new TrainingDocument('help', 'how can you help'), 80 | new TrainingDocument('help', 'i need some help'), 81 | new TrainingDocument('help', 'how could you assist me') 82 | ], function() {}); 83 | ``` 84 | 85 | The code above trains the bot to recognise the topic `how_are_you` when the text looks like `how are you` or `how are you doing` as well as `how is it going` but to recognise topic `help` when the text looks like `how can you help` or `i need some help` as well as `how can you assist me`. This is how you would train the bot. 86 | 87 | The `trainAll` method accepts an array of `TrainingDocument` objects as well as a callback function. The `TrainingDocument` object constructor accepts two parameters. These are `topicName` and `trainingData`. The `topicName` parameter is the name of the topic you want to train the `trainingData` for and the `trainingData` is the sentence that you are feeding the bot as its training data. The `topicName` will later on map to actual skills the bot can respond to. 88 | 89 | The callback for the `trainAll` method is a function that the bot can call when the training is complete. If you have too much training data, you should implement this properly. In this example, since there is not much training data, we've passed in an empty `function`. 90 | 91 | Needless to say, the bot gets better with more training data. In this tutorial we are using the default classifier, which currently is the `LogisticRegression` classifier from the [talkify-natural-classifier](https://github.com/manthanhd/talkify-natural-classifier) library. This classifier typically needs bit more training data to start with but is more accurate than others in most conditions. 92 | 93 | ### Add Skills 94 | 95 | Once you have trained the bot for some topics, you need to add some skills. Skills are actions that the bot will execute when it recognises a topic. So topics and skills map to 1:1. 96 | 97 | To add a skill, you need to create it first. A skill requires three things. Name of the skill that is unique to the bot. The name is used to relate skills later on within the context. A topic that it maps to and a function that the bot will call in order to execute the skill. This function will take four parameters, namely: `context, request, response, next`. The `context` parameter is used to store any useful contextual information from that skill. The `request` parameter contains information about the request, same for `response`. The `next` parameter is a function that you can call to let the bot 98 | know that you are done processing. Here's what a skill looks like: 99 | 100 | ```javascript 101 | var howAction = function(context, request, response, next) { 102 | response.message = new SingleLineMessage('You asked: \"' + request.message.content + '\". I\'m doing well. Thanks for asking.'); 103 | next(); 104 | }; 105 | 106 | var helpAction = function(context, request, response, next) { 107 | response.message = new SingleLineMessage('You asked: \"' + request.message.content + '\". I can tell you how I\'m doing if you ask nicely.'); 108 | next(); 109 | }; 110 | 111 | var howSkill = new Skill('how_skill', 'how_are_you', howAction); 112 | var helpSkill = new Skill('help_skill', 'help', helpAction); 113 | ``` 114 | 115 | **Note:** Name of a skill can be undefined. However, please be aware that doing so will mean that the bot will execute that skill whenever its confidence level is 0 for responding to a given query. 116 | 117 | Once you have defined some skills, you need to add them to the bot. Add the skill to the bot like so: 118 | 119 | ```javascript 120 | bot.addSkill(howSkill); 121 | bot.addSkill(helpSkill); 122 | ``` 123 | 124 | ### Resolve queries 125 | 126 | Once added, you can now ask bot to resolve something. This is where you are querying the bot with a sentence and it will respond with a message asynchronously. The resolve function takes in three parameters: `contextId, text, callback`. The `contextId` helps bot resolve context from any previous conversation. The `text` is the question or piece of natural language string that the bot needs to interpret and respond to. Lastly, the `callback` is the callback function that the bot will call 127 | with `err, messages` parameters to indicate an error (if any) and it's reply messages. 128 | 129 | ```javascript 130 | var resolved = function(err, messages) { 131 | if(err) return console.error(err); 132 | 133 | return console.log(messages); 134 | }; 135 | 136 | bot.resolve(123, 'Assistance required', resolved); 137 | ``` 138 | 139 | Run it like a simple node file and it should print the following in the console. 140 | 141 | ``` 142 | [ { type: 'SingleLine', 143 | content: 'You asked: "Assistance required". I can tell you how I\'m doing if you ask nicely.' } ] 144 | ``` 145 | 146 | Try changing `bot.resolve` to this and notice the change in response. 147 | 148 | ```javascript 149 | bot.resolve(456, 'How\'s it going?', resolved); 150 | ``` 151 | 152 | Let's ask two things at once. Change `bot.resolve` again to: 153 | 154 | ```javascript 155 | bot.resolve(456, 'How\'s it going? Assistance required please.', resolved); 156 | ``` 157 | 158 | When you run your code, you should get two messages back: 159 | 160 | ```javascript 161 | [ { type: 'SingleLine', 162 | content: 'You asked: "How\'s it going? Assistance required please.". I\'m doing well. Thanks for asking.' }, 163 | { type: 'SingleLine', 164 | content: 'You asked: "How\'s it going? Assistance required please.". I can tell you how I\'m doing if you ask nicely.' } ] 165 | ``` 166 | 167 | ### Chainable methods 168 | Currently `train`, `addSkill` and `resolve` methods are chainable. That means you can create Bot object and cascade methods like is mentioned below. 169 | 170 | ```javascript 171 | new Bot().train(topic, sentence).addSkill(skill).resolve(....) 172 | ``` 173 | 174 | ## Configuration Options 175 | 176 | ### Context Store 177 | 178 | The bot core also accepts an alternate implementation for the built in context store. Please see [Context management](#context-management) for more details. 179 | 180 | ### Classifier 181 | 182 | You can also supply your own version of the classifier to the bot. This option was primarily used to make testing easier, however, it can still be used in production if you have a better version of the built-in classifier. 183 | 184 | The built in classifier is the [talkify-natural-classifier](https://github.com/manthanhd/talkify-natural-classifier). This classifier provides two implementations: 185 | 186 | * `LogisticRegressionClassifier` 187 | * `BayesClassifier` 188 | 189 | The `LogisticRegressionClassifier` is the default classifier. If you prefer to implement the `BayesClassifier` from `talkify-natural-classifier`, you can do the following: 190 | 191 | ```javascript 192 | var BayesClassifier = require('talkify-natural-classifier').BayesClassifier; 193 | 194 | var bot = new Bot({classifier: new BayesClassifier()}); 195 | ``` 196 | 197 | If you prefer to use IBM Watson's Natural Language Processing Classifier instead, you should use the [talkify-watson-classifier](https://github.com/manthanhd/talkify-watson-classifier) library instead. Please see the guide on the Github repository page for more details on how to use that classifier. 198 | 199 | If you think yours work better, give me a shout! I'd be delighted to know and possibly work towards implementing it within the core module. 200 | 201 | ### Skill resolution strategy 202 | 203 | To provide your own implementation of Skill Resolution Strategy, simply pass the function definition in configuration object as follows: 204 | 205 | ```javascript 206 | var mySkillResolutionStrategy = function() { 207 | this.addSkill = function (skill, options) { ... }; 208 | this.getSkills = function () {...}; 209 | this.resolve = function (err, resolutionContext, callback) { 210 | ... 211 | }; 212 | return this; 213 | }; 214 | 215 | var bot = new Bot({ 216 | skillResolutionStrategy: mySkillResolutionStrategy 217 | }); 218 | ``` 219 | 220 | The bot core will create an instance of your skill resolution strategy object on init and will use it as single instance across all resolutions. 221 | 222 | ### Topic resolution strategy 223 | 224 | To provide your own implementation of Topic Resolution Strategy, simply pass the function definition in configuration object as follows: 225 | 226 | ```javascript 227 | var myTopicResolutionStrategy = function() { 228 | this.collect = function (classification, classificationContext, callback) { callback() }; 229 | this.resolve = function (callback) { callback([{name: "topic_name", confidence: 0.5]) }; 230 | return this; 231 | }; 232 | 233 | var bot = new Bot({ 234 | topicResolutionStrategy: myTopicResolutionStrategy 235 | }); 236 | ``` 237 | 238 | The bot core will create a new instance of your topic resolution strategy for every call it receives into the resolve method. 239 | 240 | ## Extending bot 241 | 242 | ### Context management 243 | By default, the bot core uses its built in version of ContextStore. If you look into lib/ContextStore.js, you'll find that it is a very simple implementation where the context is stored in a simple in-memory map with the `contextId` being the key and the context object being the value. Of course when you come to deploy this, the built-in context store will be very limiting. 244 | 245 | Extending the context store is very easy. Within the config, you can provide your own implementation for the ContextStore object. The following code provides a very trivial implementation that simply logs the values to the console. 246 | 247 | ```javascript 248 | var myContextStore = { 249 | put: function(id, context, callback) { 250 | console.info('put'); 251 | console.info(id); 252 | console.info(context); 253 | }, 254 | 255 | get: function(id, callback) { 256 | console.info('get'); 257 | console.info(id); 258 | }, 259 | 260 | remove: function(id, callback) { 261 | console.info('remove'); 262 | console.info(id); 263 | } 264 | } 265 | 266 | var bot = new Bot({contextStore: myContextStore}); 267 | ``` 268 | 269 | The current spec for `ContextStore` requires three functions to be implemented. These are `put, get and remove`. As long as these methods are provided, the bot does not care where the value for `contextStore` field in config comes from. 270 | 271 | If you were to run that code with some query resolves, you will find that the remove function never gets called. This is a work in progress as currently there is no limit as to how long a context must be remembered. 272 | 273 | ### Custom Classifier 274 | 275 | As mentioned before, the default classifier that the bot uses is from the [talkify-natural-classifier](https://github.com/manthanhd/talkify-natural-classifier) library. You are free to write your own classifier and use it in your application. To do this, you need to extend the classifier interface defined in the [talkify-classifier](https://github.com/manthanhd/talkify-classifier) library. 276 | 277 | Once you have successfully extended that implementation, you can supply your classifier to the bot like so: 278 | 279 | ```javascript 280 | var myClassifier = new MyAwesomeClassifier(); 281 | var bot = new Bot({ classifier: myClassifier }); 282 | ``` 283 | 284 | I'd love to see your implementation of the talkify classifier. If you have extended the interface and successfully implemented your classifier give me a shout! I'd be delighted to know your experience using this library. 285 | 286 | Since version 2.1.0, you can specify multiple classifiers for your bot. See [docs on classifier](./wiki/CLASSIFIER.md) for more info. 287 | 288 | ### Skill Resolution Strategy 289 | 290 | A skill resolution strategy is a component that is able to output a skill, given a resolution context. A resolution context is an object comprised of a list of topics and the original sentence, essential ingredients needed to resolve a skill. 291 | 292 | ``` 293 | +-------------+ 294 | | Topic | | | 295 | +---------+ | +----> +--------------+ 296 | |-----------+ | | | 297 | +-------------+ | Skill | +---------+ 298 | | Resolution +----> | Skill | 299 | | Strategy | +---------+ 300 | +------------+ | | 301 | | Sentence | +---> +--------------+ 302 | +------------+ 303 | ``` 304 | 305 | ### Topic Resolution Strategy 306 | 307 | A topic resolution strategy allows you to plug in custom logic to resolve a topic, given classification data. When plugging in a custom topic resolution strategy, the bot core expects the function definition to be passed in instead of result of the function execution. This is because the topic resolution strategy object is constructed using `new` for every call to `resolve` method. 308 | 309 | The process of topic resolution works in two parts: 310 | 311 | #### Step 1 312 | 313 | First stage of the topic resolution process is the collection phase. Here, the bot core sends the classification for every classification set returned from the classifier along with any required context. The bot core also passes in a callback function which is required to be invoked to let the bot core know that the invocation was successful. 314 | 315 | ``` 316 | +------------------+ + 317 | | Classification | | 318 | +------------------+ | 319 | | +-----------+ 320 | +--------> Collect | 321 | | +-----------+ 322 | +-----------+ | 323 | | Context | | 324 | +-----------+ + 325 | ``` 326 | 327 | #### Step 2 328 | 329 | Second stage is the resolution phase. Here, the bot core is expecting a list of classifications to be returned. The resolution is called only after all collections have finished executing. 330 | 331 | ``` 332 | +-----------+ +---------+-+-+ 333 | | Resolve +---->+ Topic | | | 334 | +-----------+ +---------+ | | 335 | |-----------+ | 336 | +-------------+ 337 | ``` 338 | 339 | A topic resolution strategy object must expose two methods: 340 | 341 | * collect 342 | * resolve 343 | 344 | The collect method is called everytime a classifier returns classification(s). It is called with `classification, context, callback` signature. The `classification` object contains the classification returned from the classifier (or set of classifiers if using quorums). The `context` object is the object containing request context. The last parameter `callback` is the function that must be invoked to let the bot core know that you have finished collecting the passed in parameters. 345 | 346 | The resolve method is called once after the bot core is done calling `collect` on your topic resolution strategy. This is the final call from bot core and is meant to collect topic resolution information. The `resolve` method is called with a `callback` parameter. This is the callback function that must be called with two parameters `error, topics`. The error parameter must be defined as an error object in case an error has occurred when resolving the topic. In any other case, this object must be `undefined`. The second `topics` parameter must be an array of topics resolved by the resolution strategy. 347 | 348 | # Reference Documentation 349 | 350 | * [Classifier](./wiki/CLASSIFIER.md) 351 | * [Skills](./wiki/SKILLS.md) 352 | 353 | # Contributing 354 | 355 | Please see the [contributing guide](./CONTRIBUTING.md) for more details. 356 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by manthanhd on 19/10/2016. 3 | */ 4 | var gulp = require('gulp'); 5 | var jslint = require('gulp-jshint'); 6 | var stylish = require('jshint-stylish'); 7 | var shell = require('gulp-shell'); 8 | 9 | gulp.task('lint', function () { 10 | return gulp.src(['./lib/**.js']) 11 | .pipe(jslint()) 12 | .pipe(jslint.reporter(stylish)); 13 | }); 14 | 15 | gulp.task('test', shell.task('istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec')); 16 | 17 | gulp.task('default', ['lint', 'test']); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | BotTypes: require('./lib/BotTypes'), 3 | Bot: require('./lib/Bot') 4 | }; 5 | -------------------------------------------------------------------------------- /lib/Bot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by manthanhd on 17/10/2016. 3 | */ 4 | function DefaultSkillResolutionStrategyFn() { 5 | const util = require('util'); 6 | const extend = util._extend; 7 | const skillsMap = {}; 8 | const undefinedSkills = []; 9 | 10 | this.addSkill = function(skill, options) { 11 | var minConfidence = 0; 12 | if(options instanceof Number) { 13 | minConfidence = options; 14 | } else if (options && options.minConfidence !== undefined) { 15 | minConfidence = options.minConfidence; 16 | } 17 | 18 | var skillClone = extend({}, skill); 19 | var botSkillObject = {skill: skillClone, minConfidence: minConfidence}; 20 | 21 | if (skillClone.topic === undefined || skillClone.topic === 'undefined') { 22 | skillClone.topic = undefined; 23 | undefinedSkills.push(botSkillObject); 24 | } 25 | 26 | if(skillClone.topic instanceof Array) { 27 | skillClone.topic.forEach(function(topic) { 28 | if(skillsMap[topic] === undefined) skillsMap[topic] = []; 29 | skillsMap[topic].push(skillClone); 30 | }); 31 | } else { 32 | if (skillsMap[skillClone.topic] === undefined) { 33 | skillsMap[skillClone.topic] = []; 34 | } 35 | 36 | skillsMap[skillClone.topic].push(botSkillObject); 37 | } 38 | }; 39 | 40 | this.getSkills = function() { 41 | var topics = Object.keys(skillsMap); 42 | var skills = []; 43 | 44 | topics.forEach(function (topic) { 45 | skills.push(skillsMap[topic]); 46 | }); 47 | 48 | return skills; 49 | }; 50 | 51 | var _resolveSkillFromTopicWithConfidence = function ResolveSkillFromTopicWithConfidenceFn(topic) { 52 | if (topic.name === undefined) { 53 | if (undefinedSkills && undefinedSkills.length > 0) { 54 | return undefinedSkills[0]; 55 | } 56 | 57 | return; 58 | } 59 | 60 | var botSkills = skillsMap[topic.name]; 61 | var minConfidenceSkill = undefinedSkills[0]; 62 | var minDistance = 999; 63 | if (botSkills !== undefined && botSkills.length > 0) { 64 | for (var i = 0; i < botSkills.length; i++) { 65 | var botSkill = botSkills[i]; 66 | var distance = topic.confidence - botSkill.minConfidence; 67 | if (distance < 0) continue; 68 | if (distance < minDistance) { 69 | minDistance = distance; 70 | minConfidenceSkill = botSkill; 71 | } 72 | if (distance === 0) break; 73 | } 74 | } 75 | 76 | return minConfidenceSkill; 77 | }; 78 | 79 | this.resolve = function(err, resolutionContext, callback) { 80 | var topics = resolutionContext.topics; 81 | var sentence = resolutionContext.sentence; 82 | var mostConfidentTopic = topics[0]; 83 | var resolvedBotSkill = _resolveSkillFromTopicWithConfidence(mostConfidentTopic); 84 | var skill = resolvedBotSkill ? resolvedBotSkill.skill : undefined; 85 | 86 | if (skill === undefined) { 87 | return callback(new Error("could not resolve any skills")); 88 | } 89 | 90 | var clonedSkill = extend({}, skill); 91 | clonedSkill.sentence = sentence; 92 | clonedSkill.topic = mostConfidentTopic; 93 | 94 | return callback(undefined, clonedSkill); 95 | }; 96 | 97 | return this; 98 | } 99 | 100 | function DefaultTopicResolutionStrategyFn() { 101 | const classifications = []; 102 | 103 | this.collect = function(classification, classificationContext, callback) { 104 | classifications.push(classification); 105 | return callback(); 106 | }; 107 | 108 | this.resolve = function(callback) { 109 | var topics = []; 110 | 111 | classifications.forEach(function(classification) { 112 | topics.push({name: classification.label, confidence: classification.value}); 113 | }); 114 | 115 | return callback(undefined, topics); 116 | }; 117 | 118 | return this; 119 | } 120 | 121 | function Bot(config) { 122 | config = config || {}; 123 | const async = require('async'); 124 | const util = require('util'); 125 | const extend = util._extend; 126 | const LogisticRegressionClassifier = require('talkify-natural-classifier').LogisticRegressionClassifier; 127 | var classifiers = config.classifier || config.classifiers; 128 | if (!classifiers) classifiers = [new LogisticRegressionClassifier()]; 129 | if (!(classifiers instanceof Array)) classifiers = [classifiers]; 130 | 131 | /** 132 | * Defines how a skill should be resolved from a given set of topics 133 | * @type {*} 134 | */ 135 | const skillResolutionStrategy = config.skillResolutionStrategy || DefaultSkillResolutionStrategyFn(); 136 | 137 | const topicResolutionStrategy = config.topicResolutionStrategy || DefaultTopicResolutionStrategyFn; 138 | 139 | const BotTypes = require('./BotTypes'); 140 | const Skill = BotTypes.Skill; 141 | const SingleLineMessage = BotTypes.SingleLineMessage; 142 | const Correspondence = BotTypes.Correspondence; 143 | const Context = BotTypes.Context; 144 | 145 | const BuiltInContextStore = require('./ContextStore'); 146 | const contextStore = config.contextStore || new BuiltInContextStore(); 147 | 148 | var botObject = this; 149 | 150 | this.train = function TrainBotFn(topic, text, callback) { 151 | var classifier = botObject.getClassifier(); 152 | if (!topic || (typeof topic !== 'string' && !(topic instanceof String))) throw new TypeError('topic must be a String and cannot be undefined'); 153 | 154 | if (!text || (!(text instanceof Array) && typeof text !== 'string' && !(text instanceof String))) throw new TypeError('text must be a String and cannot be undefined'); 155 | 156 | classifier.trainDocument({topic: topic, text: text}, function (err) { 157 | if (err) return callback(err); 158 | 159 | return classifier.initialize(callback); 160 | }); 161 | 162 | return botObject; 163 | }; 164 | 165 | this.trainAll = function TrainAllBotFn(documents, finished) { 166 | finished = (finished) ? finished : function (err) { 167 | throw err; 168 | }; 169 | var classifier = botObject.getClassifier(); 170 | if (!documents || !(documents instanceof Array)) return finished(new TypeError('expected documents to exist and be of type Array')); 171 | classifier.trainDocument(documents, function (err) { 172 | if (err) return callback(err); 173 | 174 | return classifier.initialize(finished); 175 | }); 176 | return botObject; 177 | }; 178 | 179 | this.addSkill = function AddSkillFn(skill, options) { 180 | if (!skill || !(skill instanceof Skill)) throw new TypeError('skill parameter is of invalid type. Must be defined and be of type Skill'); 181 | 182 | // Allows us to keep backwards compatibility with people who still use 1 with skill as confidence option 183 | var evaluatedOptions = options instanceof Object ? options : {minConfidence: options}; 184 | 185 | skillResolutionStrategy.addSkill(skill, evaluatedOptions); 186 | 187 | return botObject; 188 | }; 189 | 190 | this.getSkills = function GetSkillsFn() { 191 | return skillResolutionStrategy.getSkills(); 192 | }; 193 | 194 | var _resolveContext = function _ResolveContext(id, callback) { 195 | var retrieved = function (err, contextRecord) { 196 | if (err) return callback(err); 197 | 198 | if (contextRecord === undefined) return callback(err, new Context(id)); 199 | 200 | return callback(err, contextRecord.context); 201 | }; 202 | 203 | return contextStore.get(id, retrieved); 204 | }; 205 | 206 | var _classifyToTopics = function ClassifyToTopicFn(sentence, done) { 207 | return async.map(classifiers, function (classifier, classificationDone) { 208 | return process.nextTick(function() { 209 | var classified = function (err, classifications) { 210 | var topicResolver = new topicResolutionStrategy(); 211 | async.each(classifications, function(classification, callback) { 212 | var context = {}; 213 | topicResolver.collect(classification, context, callback); 214 | }, function(err) { 215 | topicResolver.resolve(classificationDone); 216 | }); 217 | }; 218 | 219 | return classifier.getClassifications(sentence, classified); 220 | }); 221 | }, function (err, classificationArrays) { 222 | if (err) return done(err, classificationArrays); 223 | var classifications = []; 224 | classificationArrays.forEach(function(classificationArray) { 225 | classificationArray.forEach(function(classification) { 226 | classifications.push(classification); 227 | }); 228 | }); 229 | var indeterminates = []; 230 | return async.filter(classifications, function (classification, mapped) { 231 | if (classification.name === undefined) { 232 | indeterminates.push(classification); 233 | return mapped(undefined, false); 234 | } 235 | 236 | return mapped(undefined, true); 237 | }, function (err, allMapped) { 238 | return async.sortBy(allMapped, function (classification, sorted) { 239 | return sorted(undefined, classification.confidence * -1); 240 | }, function (err, sortedClassifications) { 241 | var allClassifications = sortedClassifications.concat(indeterminates); 242 | return done(err, allClassifications); 243 | }); 244 | }); 245 | }); 246 | }; 247 | 248 | var _getSkillByName = function (name, skillList) { 249 | var skills = skillList || botObject.getSkills(); 250 | for (var i = 0; i < skills.length; i++) { 251 | var skill = skills[i]; 252 | if (skill instanceof Array) { 253 | var returnSkill = _getSkillByName(name, skill); 254 | if (returnSkill) return returnSkill; 255 | continue; 256 | } 257 | 258 | if (skill.name === name) return skill; 259 | if (skill.skill.name === name) return skill.skill; 260 | } 261 | }; 262 | 263 | var _resolveSkills = function _ResolveSkillsFn(sentences, doneCallback) { 264 | return async.mapSeries(sentences, function (sentence, callback) { 265 | var classified = function (err, topics) { 266 | return process.nextTick(function() { 267 | return skillResolutionStrategy.resolve(err, { 268 | sentence: sentence, 269 | topics: topics 270 | }, callback); 271 | }); 272 | }; 273 | 274 | return _classifyToTopics(sentence, classified); 275 | }, function (err, resolvedSkills) { 276 | return doneCallback(err, resolvedSkills); 277 | }); 278 | }; 279 | 280 | var _applySkills = function _ApplySkillsFn(context, request, response, skills, skillsApplied) { 281 | return async.mapSeries(skills, function (skill, callback) { 282 | return process.nextTick(function() { 283 | var next = function () { 284 | var responseMessage = response.message; 285 | if (responseMessage) { 286 | delete response.message; 287 | } 288 | 289 | if (context.lock) { 290 | response.isFinal = true; 291 | } 292 | 293 | if (response.isFinal === true) { 294 | return callback('break', responseMessage); 295 | } 296 | 297 | return callback(undefined, responseMessage); 298 | }; 299 | 300 | response.lockConversationForNext = function LockConversationForNextFn() { 301 | context.lock = skill.name; 302 | return response; 303 | }; 304 | 305 | response.final = function FinalFn() { 306 | response.isFinal = true; 307 | return response; 308 | }; 309 | 310 | response.send = function SendFn(message) { 311 | response.message = message; 312 | return next(); 313 | }; 314 | 315 | request.sentence.current = skill.sentence; 316 | request.sentence.index = skills.indexOf(skill); 317 | 318 | request.skill.current = skill; 319 | request.skill.index = request.sentence.index; 320 | 321 | skill.apply(context.data, request, response, next); 322 | }); 323 | }, function (err, messages) { 324 | if (err === 'break') err = undefined; 325 | 326 | return skillsApplied(err, messages); 327 | }); 328 | }; 329 | 330 | this.resolve = function ResolveFn(id, content, callback) { 331 | var sentences = content.replace(/([.?!])\s*(?=[A-Z])/g, "$1|").split("|"); 332 | var request = new Correspondence(id, new SingleLineMessage(content)); 333 | request.sentence = {all: sentences}; 334 | var response = new Correspondence(id); 335 | var context; 336 | var messages; 337 | 338 | var contextMemorized = function (err, memorizedContext) { 339 | if (err) return callback(err, messages); 340 | 341 | return callback(err, messages); 342 | }; 343 | 344 | var skillsApplied = function (err, responseMessages) { 345 | messages = responseMessages; 346 | return contextStore.put(id, context, contextMemorized); 347 | }; 348 | 349 | var skillsResolved = function (err, skills) { 350 | if (err) return callback(err); 351 | 352 | request.skill = {all: skills}; 353 | return _applySkills(context, request, response, skills, skillsApplied); 354 | }; 355 | 356 | var contextResolved = function (err, resolvedContext) { 357 | context = resolvedContext; 358 | 359 | if (context.lock) { 360 | var skill = _getSkillByName(context.lock); 361 | skill = extend({}, skill); 362 | delete context.lock; 363 | request.skill = {all: [skill]}; 364 | return _applySkills(context, request, response, [skill], skillsApplied); 365 | } 366 | 367 | return _resolveSkills(sentences, skillsResolved); 368 | }; 369 | 370 | _resolveContext(id, contextResolved); 371 | return botObject; 372 | }; 373 | 374 | this.getContextStore = function GetContextStoreFn() { 375 | return contextStore; 376 | }; 377 | 378 | this.getClassifiers = function GetClassifiersFn() { 379 | return classifiers; 380 | }; 381 | 382 | this.getClassifier = function GetClassifierFn() { 383 | return classifiers[classifiers.length - 1]; 384 | }; 385 | 386 | return this; 387 | } 388 | 389 | module.exports = Bot; 390 | -------------------------------------------------------------------------------- /lib/BotTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by manthanhd on 17/10/2016. 3 | */ 4 | function Message(type, content) { 5 | if(typeof type !== 'string' && !(type instanceof String)) throw new TypeError('type attribute for Message must be a String'); 6 | this.type = type; 7 | this.content = content; 8 | } 9 | 10 | function SingleLineMessage(content) { 11 | return new Message('SingleLine', content); 12 | } 13 | 14 | function MultiLineMessage(content) { 15 | if(!(content instanceof Array)) throw new TypeError('content for MultiLineMessage is expected to be Array'); 16 | return new Message('MultiLine', content); 17 | } 18 | 19 | function Correspondence(id, message) { 20 | if(message && !(message instanceof Message)) throw new TypeError('message attribute must be of type Message'); 21 | this.id = id; 22 | this.message = message; 23 | } 24 | 25 | function TrainingDocument(topic, text) { 26 | if(!topic || (typeof topic !== 'string' && !(topic instanceof String))) throw new TypeError('topic parameter is of invalid type. Must exist and be a String.'); 27 | if(!text || (typeof text !== 'string' && !(text instanceof String))) throw new TypeError('text parameter is of invalid type. Must exist and be a String.'); 28 | this.topic = topic; 29 | this.text = text; 30 | } 31 | 32 | function Context(id) { 33 | if(!id) throw new TypeError('Expected id for Context to exist'); 34 | this.id = id; 35 | this.data = {}; 36 | } 37 | 38 | function Skill(name, topic, skillFn) { 39 | if(!name || (typeof name !== 'string' && !(name instanceof String))) throw new TypeError('name parameter is of invalid type. Must exist and be a String'); 40 | if(topic !== undefined && (typeof topic !== 'string' && !(topic instanceof String) && !(topic instanceof Array))) throw new TypeError('topic parameter is of invalid type. Must exist and be a String or an array'); 41 | if(!skillFn || !(skillFn instanceof Function)) throw new TypeError('skillFn parameter is of invalid type. Must exist and be a function'); 42 | this.name = name; 43 | this.topic = topic; 44 | this.apply = skillFn; 45 | } 46 | 47 | function StaticResponseSkill(name, topic, staticResponse) { 48 | this.topic = topic; 49 | var _staticStringResponse, _staticMessageResponse; 50 | if(typeof staticResponse === 'string' || staticResponse instanceof String) _staticStringResponse = staticResponse; 51 | else if (staticResponse instanceof Message) _staticMessageResponse = staticResponse; 52 | else throw new TypeError('Expected staticResponse object to be of type Message or String'); 53 | 54 | this.apply = function(context, request, response, next) { 55 | if(_staticMessageResponse) { 56 | response.message = _staticMessageResponse; 57 | } else { 58 | response.message = new SingleLineMessage(_staticStringResponse); 59 | } 60 | 61 | return next(); 62 | }; 63 | 64 | return new Skill(name, topic, this.apply); 65 | } 66 | 67 | function StaticRandomResponseSkill(name, topic, staticResponses) { 68 | this.topic = topic; 69 | if(staticResponses === undefined) throw new TypeError('Expected staticResponses to be defined'); 70 | if(!(staticResponses instanceof Array)) throw new TypeError('Expected staticResponses to be of type Array'); 71 | if(staticResponses.length === 0) throw new TypeError("expected staticResponses to have at least one item"); 72 | 73 | for(var i = 0; i < staticResponses.length; i++) { 74 | var staticResponse = staticResponses[i]; 75 | if (!(typeof staticResponse === 'string' || staticResponse instanceof String) && !(staticResponse instanceof Message)) throw new TypeError('Expected staticResponse object to be of type Message or String'); 76 | } 77 | 78 | this.apply = function ApplyStaticRandomResponseSkillFn(context, request, response, next) { 79 | var _getRandomIndex = function _getRandomIndexFn(min, max) { 80 | return parseInt(Math.random() * (max - min) + min); 81 | }; 82 | 83 | var chosenResponseIndex; 84 | if(this._getRandomIndex) { 85 | chosenResponseIndex = this._getRandomIndex(0, staticResponses.length); 86 | } else { 87 | chosenResponseIndex = _getRandomIndex(0, staticResponses.length); 88 | } 89 | 90 | var chosenResponse = staticResponses[chosenResponseIndex]; 91 | 92 | if(typeof staticResponse === 'string' || chosenResponse instanceof String) { 93 | response.message = new SingleLineMessage(chosenResponse); 94 | } else { 95 | response.message = chosenResponse; 96 | } 97 | 98 | return next(); 99 | }; 100 | 101 | return new Skill(name, topic, this.apply); 102 | } 103 | 104 | exports.Message = Message; 105 | exports.Correspondence = Correspondence; 106 | exports.Context = Context; 107 | exports.SingleLineMessage = SingleLineMessage; 108 | exports.MultiLineMessage = MultiLineMessage; 109 | exports.TrainingDocument = TrainingDocument; 110 | exports.Skill = Skill; 111 | exports.StaticResponseSkill = StaticResponseSkill; 112 | exports.StaticRandomResponseSkill = StaticRandomResponseSkill; -------------------------------------------------------------------------------- /lib/ContextStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by manthanhd on 18/10/2016. 3 | */ 4 | function ContextStore() { 5 | const store = {}; 6 | 7 | this.put = function PutContextFn(id, context, callback) { 8 | callback = callback || function(){}; 9 | 10 | if(id === undefined) return callback(new TypeError('Parameter id must be defined')); 11 | 12 | store[id] = {value: context}; 13 | return callback(undefined, {id: id, context: context}); 14 | }; 15 | 16 | this.get = function GetContextFn(id, callback) { 17 | if(id === undefined) return callback(new TypeError('Parameter id must be defined')); 18 | if(callback === undefined) throw new TypeError('Parameter callback must be defined'); 19 | 20 | var retrievedContext = store[id]; 21 | if(retrievedContext === undefined) return callback(undefined, undefined); 22 | 23 | var contextRecord = {id: id, context: retrievedContext.value}; 24 | return callback(undefined, contextRecord); 25 | }; 26 | 27 | this.remove = function RemoveContextFn(id, callback) { 28 | callback = callback || function() {}; 29 | if(id === undefined) return callback(new TypeError('Parameter id must be defined')); 30 | delete store[id]; 31 | return callback(undefined, undefined); 32 | }; 33 | } 34 | 35 | module.exports = ContextStore; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "talkify", 3 | "version": "2.2.0", 4 | "description": "Framework for developing chat bot applications.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec", 8 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 9 | "lint": "node ./node_modules/gulp/bin/gulp.js lint" 10 | }, 11 | "keywords": [ 12 | "bot", 13 | "chat", 14 | "ai", 15 | "nlp", 16 | "framework" 17 | ], 18 | "author": "Manthan Dave (https://www.manthanhd.com)", 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/manthanhd/talkify.git" 23 | }, 24 | "dependencies": { 25 | "async": "^2.1.2", 26 | "talkify-classifier": "^1.0.2", 27 | "talkify-natural-classifier": "^1.0.5" 28 | }, 29 | "devDependencies": { 30 | "coveralls": "^2.11.14", 31 | "expect": "^1.20.2", 32 | "gulp": "^3.9.1", 33 | "gulp-shell": "^0.5.2", 34 | "istanbul": "^0.4.5", 35 | "jshint": "^2.9.3", 36 | "jshint-stylish": "^2.2.1", 37 | "mocha": "^3.1.2", 38 | "mocha-lcov-reporter": "^1.2.0", 39 | "mockery": "^2.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/BotTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by manthanhd on 17/10/2016. 3 | */ 4 | const expect = require('expect'); 5 | const async = require('async'); 6 | const mockery = require('mockery'); 7 | 8 | function mockClassifierWithMockClassifierFactory() { 9 | const TalkifyClassifier = require('talkify-classifier'); 10 | var mockClassifier = new TalkifyClassifier(); 11 | expect.spyOn(mockClassifier, 'trainDocument'); 12 | expect.spyOn(mockClassifier, 'initialize'); 13 | 14 | var mockLrClassifier = { 15 | LogisticRegressionClassifier: function () { 16 | return mockClassifier; 17 | } 18 | }; 19 | 20 | mockery.registerMock('talkify-natural-classifier', mockLrClassifier); 21 | return mockClassifier; 22 | } 23 | 24 | describe('Bot', function () { 25 | const BotTypes = require('../lib/BotTypes'); 26 | const SingleLineMessage = BotTypes.SingleLineMessage; 27 | const Skill = BotTypes.Skill; 28 | const Message = BotTypes.Message; 29 | const Bot = require('../lib/Bot'); 30 | 31 | beforeEach(function (done) { 32 | mockery.enable({warnOnUnregistered: false, warnOnReplace: false}); 33 | done(); 34 | }); 35 | 36 | afterEach(function (done) { 37 | mockery.deregisterAll(); 38 | mockery.disable(); 39 | done(); 40 | }); 41 | 42 | describe('', function () { 43 | it('works with passed in classifier', function () { 44 | var TalkifyClassifier = require('talkify-classifier'); 45 | var fakeClassifier = new TalkifyClassifier(); 46 | var bot = new Bot({classifier: fakeClassifier}); 47 | var classifier = bot.getClassifier(); 48 | expect(classifier).toExist(); 49 | expect(classifier).toBe(fakeClassifier); 50 | expect(classifier).toBeA(TalkifyClassifier); 51 | }); 52 | 53 | it('allows configuring multiple classifiers', function() { 54 | var TalkifyClassifier = require('talkify-classifier'); 55 | var fakeClassifier1 = new TalkifyClassifier(); 56 | var fakeClassifier2 = new TalkifyClassifier(); 57 | var bot = new Bot({classifier: [fakeClassifier1, fakeClassifier2]}); 58 | var classifiers = bot.getClassifiers(); 59 | expect(classifiers).toExist(); 60 | expect(classifiers.length).toBe(2); 61 | expect(classifiers[0]).toBe(fakeClassifier1); 62 | expect(classifiers[1]).toBe(fakeClassifier2); 63 | }); 64 | }); 65 | 66 | describe('train', function () { 67 | it('trains single document when parameters are valid', function (done) { 68 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 69 | 70 | var bot = new Bot(); 71 | bot.train('topic', 'text'); 72 | 73 | expect(mockClassifier.trainDocument.calls[0].arguments[0]).toEqual({text:'text', topic:'topic'}); 74 | done(); 75 | }); 76 | 77 | it('throws TypeError when topic is undefined', function (done) { 78 | var bot = new Bot(); 79 | try { 80 | bot.train(undefined); 81 | done('should have failed.'); 82 | } catch (e) { 83 | expect(e).toBeA(TypeError); 84 | done(); 85 | } 86 | }); 87 | 88 | it('throws TypeError when topic is not String', function (done) { 89 | var bot = new Bot(); 90 | try { 91 | bot.train([]); 92 | done('should have failed.'); 93 | } catch (e) { 94 | expect(e).toBeA(TypeError); 95 | done(); 96 | } 97 | }); 98 | 99 | it('throws TypeError when text is undefined', function (done) { 100 | var bot = new Bot(); 101 | try { 102 | bot.train('topic', undefined); 103 | done('should have failed.'); 104 | } catch (e) { 105 | expect(e).toBeA(TypeError); 106 | done(); 107 | } 108 | }); 109 | 110 | it('throws TypeError when text is not String', function (done) { 111 | var bot = new Bot(); 112 | try { 113 | bot.train('topic', []); 114 | done('should have failed.'); 115 | } catch (e) { 116 | expect(e).toBeA(TypeError); 117 | done(); 118 | } 119 | }); 120 | }); 121 | 122 | describe('trainAll', function () { 123 | 124 | it('trains all documents when parameters are valid', function (done) { 125 | var bot = new Bot(); 126 | bot.trainAll([{text: 'hello', topic: 'topic'}, {text: 'hello2', topic: 'topic2'}], function (err) { 127 | expect(err).toNotExist(); 128 | done(); 129 | }); 130 | }); 131 | 132 | it('throws TypeError when documents is not an array', function (done) { 133 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 134 | 135 | var bot = new Bot({classifierPreference: 'naive_bayes'}); 136 | bot.trainAll('not_an_array', function (err) { 137 | expect(err).toExist(); 138 | expect(err).toBeA(TypeError); 139 | done(); 140 | }); 141 | }); 142 | 143 | it('throws TypeError when documents is undefined', function (done) { 144 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 145 | 146 | var bot = new Bot({classifierPreference: 'naive_bayes'}); 147 | bot.trainAll(undefined, function (err) { 148 | expect(err).toExist(); 149 | expect(err).toBeA(TypeError); 150 | done(); 151 | }); 152 | }); 153 | 154 | it('requires optional finished callback', function(done) { 155 | const bot = new Bot(); 156 | try { 157 | bot.trainAll([{text: "hello", topic: "greeting"}]); 158 | done(); 159 | } catch (e) { 160 | done(e); 161 | } 162 | }); 163 | 164 | it('throws errors synchronously when finished callback is not defined', function(done) { 165 | try { 166 | const bot = new Bot(); 167 | bot.trainAll(undefined); 168 | done('should have failed'); 169 | } catch (e) { 170 | expect(e).toExist(); 171 | expect(e).toBeA(TypeError); 172 | done(); 173 | } 174 | }) 175 | }); 176 | 177 | describe('addSkill', function () { 178 | 179 | it('adds given skill', function (done) { 180 | var bot = new Bot(); 181 | 182 | const fakeSkillFn = function (context, req, res, next) { 183 | }; 184 | 185 | const fakeSkill = new Skill('fakeskill', 'topic', fakeSkillFn); 186 | bot.addSkill(fakeSkill); 187 | 188 | const skills = bot.getSkills(); 189 | expect(skills.length).toBe(1); 190 | done(); 191 | }); 192 | 193 | it('throws TypeError when skill is undefined', function (done) { 194 | var bot = new Bot(); 195 | try { 196 | bot.addSkill(undefined); 197 | done('should have failed.'); 198 | } catch (e) { 199 | expect(e).toBeA(TypeError); 200 | done(); 201 | } 202 | }); 203 | 204 | it('throws TypeError when skill is not of type Skill', function (done) { 205 | var bot = new Bot(); 206 | try { 207 | bot.addSkill([]); 208 | done('should have failed.'); 209 | } catch (e) { 210 | expect(e).toBeA(TypeError); 211 | done(); 212 | } 213 | }); 214 | 215 | it('adds multiple skills with different confidence levels to the same topic', function (done) { 216 | var bot = new Bot(); 217 | try { 218 | bot.addSkill(new Skill('name', 'topic', function () { 219 | }), 20); 220 | bot.addSkill(new Skill('anothername', 'topic', function () { 221 | }), 50); 222 | var skills = bot.getSkills(); 223 | expect(skills.length).toBe(1); 224 | expect(skills).toBeA(Array); 225 | done(); 226 | } catch (e) { 227 | done(e); 228 | } 229 | }); 230 | 231 | it('adds skills to all topics listed in array', function(done) { 232 | var bot = new Bot(); 233 | try { 234 | bot.addSkill(new Skill('name', ['topic', 'topic2'], function () { 235 | }), 50); 236 | 237 | var skills = bot.getSkills(); 238 | expect(skills.length).toBe(2); 239 | expect(skills).toBeA(Array); 240 | done(); 241 | } catch (e) { 242 | done(e); 243 | } 244 | }); 245 | }); 246 | 247 | describe('resolve', function () { 248 | it("resolves multi sentence message into a multi message response", function (done) { 249 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 250 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 251 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 1}]); 252 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 253 | }); 254 | 255 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 256 | response.message = new SingleLineMessage('mytopic response'); 257 | return next() 258 | })); 259 | 260 | var fakeMyAnotherTopicSkill = new Skill('myanotherfakeskill', 'myanothertopic', expect.createSpy().andCall(function (context, request, response, next) { 261 | response.message = new SingleLineMessage('myanothertopic response'); 262 | return next() 263 | })); 264 | 265 | var bot = new Bot(); 266 | bot.addSkill(fakeMyTopicSkill); 267 | bot.addSkill(fakeMyAnotherTopicSkill); 268 | 269 | return bot.resolve(123, "Hello. Hi", function (err, messages) { 270 | if (err) return done(err); 271 | 272 | expect(messages).toBeA(Array); 273 | expect(messages.length).toBe(2); 274 | expect(messages[0]).toBeA(Message); 275 | expect(messages[0].content).toBe('mytopic response'); 276 | expect(messages[1]).toBeA(Message); 277 | expect(messages[1].content).toBe('myanothertopic response'); 278 | done(); 279 | }); 280 | }); 281 | 282 | it('saves context by correspondance id', function (done) { 283 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 284 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 285 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 1}]); 286 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 287 | }); 288 | 289 | var contextStore = { 290 | put: function (id, context, callback) { 291 | return callback(undefined, context); 292 | }, 293 | 294 | get: function (id, callback) { 295 | return callback(undefined, undefined); 296 | } 297 | }; 298 | 299 | var contextStore_putSpy = expect.spyOn(contextStore, 'put').andCallThrough(); 300 | var contextStore_getSpy = expect.spyOn(contextStore, 'get').andCallThrough(); 301 | 302 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 303 | response.message = new SingleLineMessage('mytopic response'); 304 | return next() 305 | })); 306 | 307 | var fakeMyAnotherTopicSkill = new Skill('myanotherfakeskill', 'myanothertopic', expect.createSpy().andCall(function (context, request, response, next) { 308 | response.message = new SingleLineMessage('myanothertopic response'); 309 | return next() 310 | })); 311 | 312 | var bot = new Bot({contextStore: contextStore}); 313 | bot.addSkill(fakeMyTopicSkill); 314 | bot.addSkill(fakeMyAnotherTopicSkill); 315 | 316 | return bot.resolve(123, "Hello. Hi", function (err, messages) { 317 | if (err) return done(err); 318 | 319 | expect(contextStore_putSpy).toHaveBeenCalled(); 320 | expect(contextStore_getSpy).toHaveBeenCalled(); 321 | done(); 322 | }); 323 | }); 324 | 325 | it('returns messages as well as error when failed to memorize context', function (done) { 326 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 327 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 328 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 1}]); 329 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 330 | }); 331 | 332 | var contextStore = { 333 | put: function (id, context, callback) { 334 | return callback(new Error("hurr durr i failed to memorize context"), context); 335 | }, 336 | 337 | get: function (id, callback) { 338 | return callback(undefined, undefined); 339 | } 340 | }; 341 | 342 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 343 | response.message = new SingleLineMessage('mytopic response'); 344 | return next() 345 | })); 346 | 347 | var fakeMyAnotherTopicSkill = new Skill('myanotherfakeskill', 'myanothertopic', expect.createSpy().andCall(function (context, request, response, next) { 348 | response.message = new SingleLineMessage('myanothertopic response'); 349 | return next() 350 | })); 351 | 352 | var bot = new Bot({contextStore: contextStore}); 353 | bot.addSkill(fakeMyTopicSkill); 354 | bot.addSkill(fakeMyAnotherTopicSkill); 355 | 356 | return bot.resolve(123, "Hello. Hi", function (err, messages) { 357 | expect(err).toExist(); 358 | expect(err.message).toBe('hurr durr i failed to memorize context'); 359 | 360 | expect(messages).toExist(); 361 | expect(messages.length).toBe(2); 362 | done(); 363 | }); 364 | }); 365 | 366 | it('has access to sentence metadata when skill is processing the request', function (done) { 367 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 368 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 369 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 1}]); 370 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 371 | }); 372 | 373 | var fakeTopicSkillCalled = false; 374 | var fakeAnotherTopicSkillCalled = false; 375 | 376 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 377 | expect(request.sentence).toExist(); 378 | expect(request.sentence.current).toBe("Hello."); 379 | expect(request.sentence.index).toBe(0); 380 | 381 | expect(request.sentence.all).toExist(); 382 | expect(request.sentence.all.length).toBe(2); 383 | expect(request.sentence.all[0]).toBe("Hello."); 384 | expect(request.sentence.all[1]).toBe("Hi."); 385 | fakeTopicSkillCalled = true; 386 | 387 | response.message = new SingleLineMessage('mytopic response'); 388 | return next(); 389 | })); 390 | 391 | var fakeMyAnotherTopicSkill = new Skill('myanotherfakeskill', 'myanothertopic', expect.createSpy().andCall(function (context, request, response, next) { 392 | expect(request.sentence).toExist(); 393 | expect(request.sentence.current).toBe("Hi."); 394 | expect(request.sentence.index).toBe(1); 395 | 396 | expect(request.sentence.all).toExist(); 397 | expect(request.sentence.all.length).toBe(2); 398 | expect(request.sentence.all[0]).toBe("Hello."); 399 | expect(request.sentence.all[1]).toBe("Hi."); 400 | 401 | fakeAnotherTopicSkillCalled = true; 402 | 403 | response.message = new SingleLineMessage('myanothertopic response'); 404 | return next(); 405 | })); 406 | 407 | var bot = new Bot(); 408 | bot.addSkill(fakeMyTopicSkill); 409 | bot.addSkill(fakeMyAnotherTopicSkill); 410 | 411 | return bot.resolve(123, "Hello. Hi.", function (err, messages) { 412 | expect(fakeTopicSkillCalled).toBe(true); 413 | expect(fakeAnotherTopicSkillCalled).toBe(true); 414 | 415 | return done(); 416 | }); 417 | }); 418 | 419 | it('has access to topic metadata when skill is processing the request', function (done) { 420 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 421 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 422 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 1}]); 423 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 424 | }); 425 | 426 | var fakeTopicSkillCalled = false; 427 | var fakeAnotherTopicSkillCalled = false; 428 | 429 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 430 | expect(request.skill).toExist(); 431 | expect(request.skill.index).toBe(0); 432 | 433 | expect(request.skill.all).toExist(); 434 | expect(request.skill.all.length).toBe(2); 435 | expect(request.skill.all[0].name).toBe(fakeMyTopicSkill.name); 436 | expect(request.skill.all[1].name).toBe(fakeMyAnotherTopicSkill.name); 437 | fakeTopicSkillCalled = true; 438 | 439 | response.message = new SingleLineMessage('mytopic response'); 440 | return next(); 441 | })); 442 | 443 | var fakeMyAnotherTopicSkill = new Skill('myanotherfakeskill', 'myanothertopic', expect.createSpy().andCall(function (context, request, response, next) { 444 | expect(request.skill).toExist(); 445 | expect(request.skill.index).toBe(1); 446 | 447 | expect(request.skill.all).toExist(); 448 | expect(request.skill.all.length).toBe(2); 449 | expect(request.skill.all[0].name).toBe(fakeMyTopicSkill.name); 450 | expect(request.skill.all[1].name).toBe(fakeMyAnotherTopicSkill.name); 451 | fakeAnotherTopicSkillCalled = true; 452 | 453 | response.message = new SingleLineMessage('myanothertopic response'); 454 | return next(); 455 | })); 456 | 457 | var bot = new Bot(); 458 | bot.addSkill(fakeMyTopicSkill); 459 | bot.addSkill(fakeMyAnotherTopicSkill); 460 | 461 | return bot.resolve(123, "Hello. Hi.", function (err, messages) { 462 | expect(fakeTopicSkillCalled).toBe(true); 463 | expect(fakeAnotherTopicSkillCalled).toBe(true); 464 | return done(); 465 | }); 466 | }); 467 | 468 | it('does not call the next skill when previous skill calls final()', function (done) { 469 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 470 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 471 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 1}]); 472 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 473 | }); 474 | 475 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 476 | response.final().send(new SingleLineMessage('mytopic response')); 477 | })); 478 | 479 | var fakeMyAnotherTopicSkill = new Skill('myanotherfakeskill', 'myanothertopic', expect.createSpy().andCall(function (context, request, response, next) { 480 | done('This skill should not have been called.'); 481 | })); 482 | 483 | var bot = new Bot(); 484 | bot.addSkill(fakeMyTopicSkill, 1); 485 | bot.addSkill(fakeMyAnotherTopicSkill, 1); 486 | 487 | return bot.resolve(123, "Hello. Hi.", function (err, messages) { 488 | expect(messages.length).toBe(1); 489 | expect(messages[0].content).toBe('mytopic response'); 490 | return done(); 491 | }); 492 | }); 493 | 494 | it('resolves context from a previously saved context with the built in context store', function (done) { 495 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 496 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 497 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 1}]); 498 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 499 | }); 500 | 501 | var firstRun = true; 502 | 503 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 504 | if (firstRun === true) { 505 | expect(context.something).toNotExist(); 506 | } else { 507 | expect(context.something).toExist(); 508 | } 509 | 510 | response.message = new SingleLineMessage('mytopic response'); 511 | return next(); 512 | })); 513 | 514 | var bot = new Bot(); 515 | bot.addSkill(fakeMyTopicSkill); 516 | 517 | bot.resolve(123, "Hello.", function (err, messages) { 518 | expect(err).toNotExist(); 519 | }); 520 | return bot.resolve(123, "Hello.", function (err, messages) { 521 | expect(err).toNotExist(); 522 | done(); 523 | }); 524 | }); 525 | 526 | it('returns skills could not be resolved error when it couldn\'t resolve skills', function (done) { 527 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 528 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 529 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 0.2}]); 530 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 531 | }); 532 | 533 | var firstRun = true; 534 | 535 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 536 | console.log("skill should not be called"); 537 | if (firstRun === true) { 538 | expect(context.something).toNotExist(); 539 | } else { 540 | expect(context.something).toExist(); 541 | } 542 | 543 | response.message = new SingleLineMessage('mytopic response'); 544 | return next(); 545 | })); 546 | 547 | var bot = new Bot(); 548 | bot.addSkill(fakeMyTopicSkill, 1); 549 | 550 | bot.resolve(123, "Hello.", function (err, messages) { 551 | expect(err).toExist(); 552 | done(); 553 | }); 554 | }); 555 | 556 | it('returns skills could not be resolved error when it couldn\'t resolve skills when minConfidence is used as options object', function (done) { 557 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 558 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 559 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 0.2}]); 560 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 561 | }); 562 | 563 | var firstRun = true; 564 | 565 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 566 | console.log("skill should not be called"); 567 | if (firstRun === true) { 568 | expect(context.something).toNotExist(); 569 | } else { 570 | expect(context.something).toExist(); 571 | } 572 | 573 | response.message = new SingleLineMessage('mytopic response'); 574 | return next(); 575 | })); 576 | 577 | var bot = new Bot(); 578 | bot.addSkill(fakeMyTopicSkill, {minConfidence: 1}); 579 | 580 | bot.resolve(123, "Hello.", function (err, messages) { 581 | expect(err).toExist(); 582 | done(); 583 | }); 584 | }); 585 | 586 | it('calls mapped undefined skill when skill cannot be found for a topic', function (done) { 587 | const TrainingDocument = require('../lib/BotTypes').TrainingDocument; 588 | mockery.deregisterAll(); 589 | mockery.disable(); 590 | 591 | var fakeMyTopicSkill = new Skill('myskill', undefined, expect.createSpy().andCall(function (context, request, response, next) { 592 | response.message = new SingleLineMessage('mytopic response'); 593 | return next(); 594 | })); 595 | 596 | var bot = new Bot(); 597 | bot.addSkill(fakeMyTopicSkill); 598 | 599 | var resolved = function (err, messages) { 600 | expect(err).toNotExist(); 601 | 602 | expect(messages).toExist(); 603 | expect(messages.length).toBe(1); 604 | expect(messages[0].content).toBe('mytopic response'); 605 | done(); 606 | }; 607 | return bot.resolve(123, "kiwi", resolved); 608 | }); 609 | 610 | it('calls mapped undefined skill when skill with closest positive distance from defined minConfidence cannot be found for a topic', function (done) { 611 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 612 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 613 | return callback(undefined, [{label: 'myanothertopic', value: 0.5}]); 614 | }); 615 | 616 | var fakeMyTopicSkill = new Skill('myskill', undefined, expect.createSpy().andCall(function (context, request, response, next) { 617 | response.message = new SingleLineMessage('mytopic response'); 618 | return next(); 619 | })); 620 | 621 | var fakeAnotherTopicSkill = new Skill('myfakeanothertopicskill', 'myanothertopic', function(context, request, response) { 622 | return done('should not have called this skill'); 623 | }); 624 | 625 | var bot = new Bot(); 626 | bot.addSkill(fakeMyTopicSkill); 627 | bot.addSkill(fakeAnotherTopicSkill, 0.8); 628 | 629 | var resolved = function (err, messages) { 630 | expect(err).toNotExist(); 631 | 632 | expect(messages).toExist(); 633 | expect(messages.length).toBe(1); 634 | expect(messages[0].content).toBe('mytopic response'); 635 | done(); 636 | }; 637 | return bot.resolve(123, "kiwi", resolved); 638 | }); 639 | 640 | it('calls skills based on confidence level', function (done) { 641 | var fakeMyTopicSkill = new Skill('myskill', 'hello', expect.createSpy().andCall(function (context, request, response, next) { 642 | response.message = new SingleLineMessage('mytopic response'); 643 | return next(); 644 | })); 645 | 646 | var bot = new Bot(); 647 | async.series([ 648 | function(done) { 649 | bot.train('hello', 'hey there', done); 650 | }, 651 | function(done) { 652 | bot.train('hello', 'hello there', done); 653 | }, 654 | function(done) { 655 | bot.train('hello', 'hello', done); 656 | } 657 | ], function() { 658 | bot.addSkill(fakeMyTopicSkill, 0.4); 659 | 660 | var resolved = function (err, messages) { 661 | expect(err).toNotExist(); 662 | 663 | expect(messages).toExist(); 664 | expect(messages.length).toBe(1); 665 | expect(messages[0].content).toBe('mytopic response'); 666 | done(); 667 | }; 668 | 669 | return bot.resolve(123, "kiwi", resolved); 670 | }); 671 | }); 672 | 673 | it('resolves to the same skill that locked the conversation', function (done) { 674 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 675 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 676 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 1}]); 677 | if (sentence === 'Hi.') return callback(undefined, [{label: 'myanothertopic', value: 1}]); 678 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 679 | }); 680 | 681 | var firstRun = true; 682 | 683 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 684 | if (firstRun === true) { 685 | response.message = new SingleLineMessage('I need more information please!'); 686 | response.lockConversationForNext(); 687 | firstRun = false; 688 | return next(); 689 | } 690 | 691 | return done(); 692 | })); 693 | 694 | var fakeAnotherMyTopicSkill = new Skill('myfakeskill', 'myanothertopic', expect.createSpy().andCall(function (context, request, response, next) { 695 | return done('should not have called this topic'); 696 | })); 697 | 698 | var bot = new Bot(); 699 | bot.addSkill(fakeMyTopicSkill); 700 | bot.addSkill(fakeAnotherMyTopicSkill); 701 | 702 | return bot.resolve(123, "Hello.", function (err, messages) { 703 | expect(err).toNotExist(); 704 | 705 | return bot.resolve(123, "Hi.", function (err, messages) { 706 | expect(err).toNotExist(); 707 | }); 708 | }); 709 | }); 710 | 711 | it('lockConversationForNext only lasts for a single next conversation', function (done) { 712 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 713 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 714 | if (sentence === 'Hello.') return callback(undefined, [{label: 'mytopic', value: 1}]); 715 | if (sentence === 'Hi.') return callback(undefined, [{label: 'myanothertopic', value: 1}]); 716 | return callback(undefined, [{label: 'myanothertopic', value: 1}]); 717 | }); 718 | 719 | var firstRun = true; 720 | 721 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 722 | if (firstRun === true) { 723 | response.message = new SingleLineMessage('I need more information please!'); 724 | response.lockConversationForNext(); 725 | firstRun = false; 726 | context.runs = 1; 727 | return next(); 728 | } 729 | 730 | context.runs = 2; 731 | 732 | return next(); 733 | })); 734 | 735 | var fakeAnotherMyTopicSkill = new Skill('myfakeskill', 'myanothertopic', expect.createSpy().andCall(function (context, request, response, next) { 736 | expect(firstRun).toBe(false); 737 | expect(context.runs).toBe(2); 738 | return done(); 739 | })); 740 | 741 | var bot = new Bot(); 742 | bot.addSkill(fakeMyTopicSkill); 743 | bot.addSkill(fakeAnotherMyTopicSkill); 744 | 745 | return bot.resolve(123, "Hello.", function (err, messages) { 746 | expect(err).toNotExist(); 747 | 748 | return bot.resolve(123, "Hi.", function (err, messages) { 749 | expect(err).toNotExist(); 750 | return bot.resolve(123, "Hi.", function (err, messages) { 751 | expect(err).toNotExist(); 752 | }); 753 | }); 754 | }); 755 | }); 756 | 757 | it('lock information is not available to the skill', function (done) { 758 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 759 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 760 | return callback(undefined, [{label: 'mytopic', value: 1}]); 761 | }); 762 | 763 | var firstRun = true; 764 | 765 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 766 | if (firstRun === true) { 767 | response.message = new SingleLineMessage('I need more information please!'); 768 | response.lockConversationForNext(); 769 | 770 | expect(context.lock).toNotExist(); 771 | 772 | firstRun = false; 773 | context.runs = 1; 774 | return next(); 775 | } 776 | 777 | expect(context.lock).toNotExist(); 778 | return done(); 779 | })); 780 | 781 | var bot = new Bot(); 782 | bot.addSkill(fakeMyTopicSkill); 783 | 784 | return bot.resolve(123, "Hello.", function (err, messages) { 785 | expect(err).toNotExist(); 786 | 787 | return bot.resolve(123, "Hi.", function (err, messages) { 788 | expect(err).toNotExist(); 789 | }); 790 | }); 791 | }); 792 | 793 | it('works with response.send to send message and next', function (done) { 794 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 795 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 796 | return callback(undefined, [{label: 'mytopic', value: 1}]); 797 | }); 798 | 799 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 800 | return response.send(new Message('SingleLine', 'Hello there!')); 801 | })); 802 | 803 | var bot = new Bot(); 804 | bot.addSkill(fakeMyTopicSkill); 805 | 806 | return bot.resolve(123, "Hello.", function (err, messages) { 807 | expect(err).toNotExist(); 808 | 809 | expect(messages).toExist(); 810 | expect(messages.length).toBe(1); 811 | expect(messages[0].type).toBe('SingleLine'); 812 | expect(messages[0].content).toBe('Hello there!'); 813 | return done(); 814 | }); 815 | }); 816 | 817 | it('final() method is chainable with same effect as send(message, true)', function (done) { 818 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 819 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 820 | return sentence === 'Hello.' ? callback(undefined, [{label: 'mytopic', value: 1}]) : callback(undefined, [{label: 'myanothertopic', value: 1}]); 821 | }); 822 | 823 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 824 | expect(response.final()).toBe(response); 825 | return response.final().send(new Message('SingleLine', 'Hello there!')); 826 | })); 827 | 828 | var fakeMyTopicSkill2 = new Skill('myfakeskill2', 'myanothertopic', expect.createSpy().andCall(function (context, request, response, next) { 829 | return done('should not have executed this skill'); 830 | })); 831 | 832 | var bot = new Bot(); 833 | bot.addSkill(fakeMyTopicSkill); 834 | bot.addSkill(fakeMyTopicSkill2); 835 | 836 | return bot.resolve(123, "Hello. Hi!", function (err, messages) { 837 | expect(err).toNotExist(); 838 | 839 | expect(messages).toExist(); 840 | expect(messages.length).toBe(1); 841 | expect(messages[0].type).toBe('SingleLine'); 842 | expect(messages[0].content).toBe('Hello there!'); 843 | return done(); 844 | }); 845 | }); 846 | 847 | it('lockConversationForNext method is chainable', function (done) { 848 | var mockClassifier = mockClassifierWithMockClassifierFactory(); 849 | mockClassifier.getClassifications = expect.createSpy().andCall(function (sentence, callback) { 850 | return callback(undefined, [{label: 'mytopic', value: 1}]); 851 | }); 852 | 853 | var firstRun = true; 854 | 855 | var fakeMyTopicSkill = new Skill('myfakeskill', 'mytopic', expect.createSpy().andCall(function (context, request, response, next) { 856 | if (firstRun === true) { 857 | var returnVal = response.lockConversationForNext(); 858 | expect(returnVal).toBe(response); 859 | 860 | expect(context.lock).toNotExist(); 861 | 862 | firstRun = false; 863 | context.runs = 1; 864 | return response.lockConversationForNext().send(new SingleLineMessage('I need more information please!')); 865 | } 866 | 867 | expect(context.lock).toNotExist(); 868 | return done(); 869 | })); 870 | 871 | var bot = new Bot(); 872 | bot.addSkill(fakeMyTopicSkill); 873 | 874 | return bot.resolve(123, "Hello.", function (err, messages) { 875 | expect(err).toNotExist(); 876 | 877 | return bot.resolve(123, "Hi.", function (err, messages) { 878 | expect(err).toNotExist(); 879 | }); 880 | }); 881 | }); 882 | 883 | it('resolves across multiple classifiers', function(done) { 884 | var fakeMyTopicSkill = new Skill('myskill', 'hello', expect.createSpy().andCall(function (context, request, response) { 885 | var resolutionConfidence = request.skill.current.topic.confidence; 886 | expect(resolutionConfidence).toBe(0.5); 887 | return done(); 888 | })); 889 | 890 | var TalkifyClassifier = require('talkify-classifier'); 891 | var fakeClassifier1 = new TalkifyClassifier(); 892 | fakeClassifier1.getClassifications = function(input, callback) { 893 | return callback(undefined, [{label: 'hello', value: 0.2}]); 894 | }; 895 | 896 | var fakeClassifier2 = new TalkifyClassifier(); 897 | fakeClassifier2.getClassifications = function(input, callback) { 898 | return callback(undefined, [{label: 'hello', value: 0.5}]); 899 | }; 900 | 901 | var bot = new Bot({classifiers: [fakeClassifier1, fakeClassifier2]}); 902 | bot.addSkill(fakeMyTopicSkill, 0.4); 903 | 904 | return bot.resolve(1, 'hello this is a message', function(err, messages) { 905 | // placeholder callback 906 | }); 907 | }); 908 | 909 | it('prioritises low confidence definite resolution over high confidence indeterminate resolution', function(done) { 910 | var fakeMyTopicSkill = new Skill('myskill', 'hello', expect.createSpy().andCall(function (context, request, response) { 911 | var resolutionConfidence = request.skill.current.topic.confidence; 912 | expect(resolutionConfidence).toBe(0.1); 913 | return done(); 914 | })); 915 | 916 | var TalkifyClassifier = require('talkify-classifier'); 917 | var fakeClassifier1 = new TalkifyClassifier(); 918 | fakeClassifier1.getClassifications = function(input, callback) { 919 | return callback(undefined, [{label: 'hello', value: 0.1}]); 920 | }; 921 | 922 | var fakeClassifier2 = new TalkifyClassifier(); 923 | fakeClassifier2.getClassifications = function(input, callback) { 924 | return callback(undefined, [{label: undefined, value: 1}]); 925 | }; 926 | 927 | var bot = new Bot({classifiers: [fakeClassifier1, fakeClassifier2]}); 928 | bot.addSkill(fakeMyTopicSkill); 929 | 930 | return bot.resolve(1, 'hello this is a message', function(err, messages) { 931 | // placeholder callback 932 | }); 933 | }); 934 | 935 | it('uses configured skill resolution strategy', function (done) { 936 | var fakeMyTopicSkill = new Skill('myskill', 'hello', expect.createSpy().andCall(function (context, request, response, next) { 937 | response.message = new SingleLineMessage('mytopic response'); 938 | return next(); 939 | })); 940 | 941 | var calls = 0; 942 | 943 | var mockSkillResolutionStrategy = function() { 944 | const skills = []; 945 | this.addSkill = function(skill, options) { 946 | calls++; 947 | expect(skill).toBe(fakeMyTopicSkill); 948 | return skills.push(skill); 949 | }; 950 | 951 | this.getSkills = function() { 952 | 953 | }; 954 | 955 | this.resolve = function(err, resolutionContext, callback) { 956 | calls++; 957 | expect(err).toBe(null); 958 | return callback(undefined, fakeMyTopicSkill); 959 | }; 960 | 961 | return this; 962 | }; 963 | 964 | var bot = new Bot({skillResolutionStrategy: mockSkillResolutionStrategy()}); 965 | 966 | bot.addSkill(fakeMyTopicSkill, 0.4); 967 | 968 | var resolved = function (err, messages) { 969 | expect(calls).toBe(2); 970 | expect(err).toNotExist(); 971 | 972 | expect(messages).toExist(); 973 | expect(messages.length).toBe(1); 974 | expect(messages[0].content).toBe('mytopic response'); 975 | done(); 976 | }; 977 | 978 | return bot.resolve(123, "kiwi", resolved); 979 | }); 980 | 981 | it('uses configured topic resolution strategy', function(done) { 982 | var calls = 0; 983 | var mockTopicResolutionStrategy = function() { 984 | const classifications = []; 985 | 986 | this.collect = function(classification, classificationContext, callback) { 987 | calls++; 988 | classifications.push(classification); 989 | return callback(); 990 | }; 991 | 992 | this.resolve = function(callback) { 993 | calls++; 994 | var topics = []; 995 | 996 | classifications.forEach(function(classification) { 997 | topics.push({name: classification.label, confidence: classification.value}); 998 | }); 999 | 1000 | return callback(undefined, topics); 1001 | }; 1002 | 1003 | return this; 1004 | }; 1005 | var bot = new Bot({topicResolutionStrategy: mockTopicResolutionStrategy}); 1006 | bot.resolve(1, 'something', function(err){ 1007 | expect(calls).toBe(2); 1008 | done(); 1009 | }); 1010 | }); 1011 | }); 1012 | 1013 | describe('getContextStore', function () { 1014 | const ContextStore = require('../lib/ContextStore'); 1015 | it('gets context store', function () { 1016 | var bot = new Bot(); 1017 | var contextStore = bot.getContextStore(); 1018 | expect(contextStore).toExist(); 1019 | expect(contextStore).toBeA(ContextStore); 1020 | }); 1021 | 1022 | it('gets passed in context store', function () { 1023 | var fakeContextStore = { 1024 | put: function () { 1025 | }, get: function () { 1026 | }, remove: function () { 1027 | } 1028 | }; 1029 | var bot = new Bot({contextStore: fakeContextStore}); 1030 | var contextStore = bot.getContextStore(); 1031 | expect(contextStore).toExist(); 1032 | expect(contextStore).toNotBeA(ContextStore); 1033 | expect(contextStore).toBe(fakeContextStore); 1034 | }); 1035 | }); 1036 | 1037 | describe('getClassifier', function () { 1038 | it('gets initialised default classifier', function () { 1039 | var bot = new Bot(); 1040 | var classifier = bot.getClassifier(); 1041 | expect(classifier).toExist(); 1042 | }); 1043 | 1044 | it('gets passed in classifier', function () { 1045 | var natural = require('talkify-natural-classifier'); 1046 | var fakeClassifier = new natural.BayesClassifier(); 1047 | var bot = new Bot({classifier: fakeClassifier}); 1048 | var classifier = bot.getClassifier(); 1049 | expect(classifier).toExist(); 1050 | expect(classifier).toNotBeA(natural.LogisticRegressionClassifier); 1051 | expect(classifier).toBe(fakeClassifier); 1052 | }); 1053 | 1054 | it('returns last registered classifier', function() { 1055 | var TalkifyClassifier = require('talkify-classifier'); 1056 | var fakeClassifier1 = new TalkifyClassifier(); 1057 | var fakeClassifier2 = new TalkifyClassifier(); 1058 | var bot = new Bot({classifier: [fakeClassifier1, fakeClassifier2]}); 1059 | var classifier = bot.getClassifier(); 1060 | expect(classifier).toExist(); 1061 | expect(classifier).toBe(fakeClassifier2); 1062 | }) 1063 | }); 1064 | 1065 | describe('chainableMethods', function () { 1066 | it('chainable train method', function (done) { 1067 | const bot = new Bot(); 1068 | const returnReference = bot.train('topic', 'text', function(){}); 1069 | 1070 | expect(returnReference).toBe(bot); 1071 | done(); 1072 | }); 1073 | 1074 | it('chainable addSkill method', function (done) { 1075 | const bot = new Bot(); 1076 | const fakeSkillFn = function (context, req, res, next) { 1077 | }; 1078 | const returnReference = bot.addSkill(new Skill('fakeskill', 'topic', fakeSkillFn)); 1079 | 1080 | expect(returnReference).toBe(bot); 1081 | done(); 1082 | }); 1083 | 1084 | it('chainable resolve method', function (done) { 1085 | const bot = new Bot(); 1086 | const returnReference = bot.resolve(123, "Hello. Hi.", function (err, messages) { 1087 | }); 1088 | 1089 | expect(returnReference).toBe(bot); 1090 | done(); 1091 | }); 1092 | }); 1093 | }); 1094 | -------------------------------------------------------------------------------- /test/BotTypesTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by manthanhd on 17/10/2016. 3 | */ 4 | const expect = require('expect'); 5 | 6 | describe('Message', function() { 7 | const Message = require("../lib/BotTypes").Message; 8 | 9 | it("throws TypeError when initialised with type attribute as array", function(done) { 10 | try { 11 | new Message([]); 12 | done('should have failed'); 13 | } catch (e) { 14 | expect(e).toBeA(TypeError); 15 | done(); 16 | } 17 | }); 18 | 19 | it('creates a new instance of Message when passed valid parameters', function(done) { 20 | var message = new Message('SingleLine', 'Hello World!'); 21 | expect(message).toExist(); 22 | expect(message.type).toBe('SingleLine'); 23 | expect(message.content).toBe('Hello World!'); 24 | done(); 25 | }); 26 | }); 27 | 28 | describe('SingleLineMessage', function() { 29 | const SingleLineMessage = require("../lib/BotTypes").SingleLineMessage; 30 | 31 | it('creates a new instance of Message when passed valid parameters', function(done) { 32 | var message = new SingleLineMessage('Hello World!'); 33 | expect(message).toExist(); 34 | expect(message.type).toBe('SingleLine'); 35 | expect(message.content).toBe('Hello World!'); 36 | done(); 37 | }); 38 | }); 39 | 40 | describe('MultiLineMessage', function() { 41 | const MultiLineMessage = require("../lib/BotTypes").MultiLineMessage; 42 | 43 | it("throws TypeError when initialised with content attribute as string", function(done) { 44 | try { 45 | new MultiLineMessage(''); 46 | done('should have failed'); 47 | } catch (e) { 48 | expect(e).toBeA(TypeError); 49 | done(); 50 | } 51 | }); 52 | 53 | it('creates a new instance of Message when passed valid parameters', function(done) { 54 | var message = new MultiLineMessage(['Hello World!']); 55 | expect(message).toExist(); 56 | expect(message.type).toBe('MultiLine'); 57 | expect(message.content.length).toBe(1); 58 | expect(message.content[0]).toBe('Hello World!'); 59 | done(); 60 | }); 61 | }); 62 | 63 | describe('Correspondence', function() { 64 | const Message = require('../lib/BotTypes').Message; 65 | const Correspondence = require("../lib/BotTypes").Correspondence; 66 | 67 | it("throws TypeError when initialised with message attribute as array", function(done) { 68 | try { 69 | new Correspondence(123, []); 70 | done('should have failed'); 71 | } catch (e) { 72 | expect(e).toBeA(TypeError); 73 | done(); 74 | } 75 | }); 76 | 77 | it('creates a new instance of Correspondence when passed valid parameters', function(done) { 78 | var correspondence = new Correspondence('abcde12345', new Message('SingleLine', 'Hello World!')); 79 | expect(correspondence).toExist(); 80 | expect(correspondence.id).toBe('abcde12345'); 81 | expect(correspondence.message).toExist(); 82 | expect(correspondence.message.type).toBe('SingleLine'); 83 | expect(correspondence.message.content).toBe('Hello World!'); 84 | done(); 85 | }); 86 | }); 87 | 88 | describe('Context', function() { 89 | const Context = require("../lib/BotTypes").Context; 90 | 91 | it("throws TypeError when initialised with undefined id field", function(done) { 92 | try { 93 | new Context(); 94 | done('should have failed'); 95 | } catch (e) { 96 | expect(e).toBeA(TypeError); 97 | done(); 98 | } 99 | }); 100 | 101 | it('creates a new instance of Context when passed valid parameters', function(done) { 102 | var context = new Context(123); 103 | expect(context).toExist(); 104 | expect(context.id).toBe(123); 105 | done(); 106 | }); 107 | }); 108 | 109 | describe('TrainingDocument', function() { 110 | const TrainingDocument = require('../lib/BotTypes').TrainingDocument; 111 | 112 | it("throws TypeError when initialised with undefined values", function(done) { 113 | try { 114 | new TrainingDocument(); 115 | done('should have failed'); 116 | } catch (e) { 117 | expect(e).toBeA(TypeError); 118 | done(); 119 | } 120 | }); 121 | 122 | it('throws type error when topic parameter is not string', function(done) { 123 | try { 124 | new TrainingDocument([], ''); 125 | done('should have failed'); 126 | } catch (e) { 127 | expect(e).toBeA(TypeError); 128 | done(); 129 | } 130 | }); 131 | 132 | it('throws type error when text parameter is not string', function(done) { 133 | try { 134 | new TrainingDocument('', []); 135 | done('should have failed'); 136 | } catch (e) { 137 | expect(e).toBeA(TypeError); 138 | done(); 139 | } 140 | }); 141 | 142 | it('returns a new TrainingDocument object when initialised with valid parameters', function() { 143 | var trainingDocument = new TrainingDocument('topic', 'text'); 144 | expect(trainingDocument).toExist(); 145 | expect(trainingDocument.topic).toBe('topic'); 146 | expect(trainingDocument.text).toBe('text'); 147 | }); 148 | 149 | it('throws TypeError when text is not of type string', function(done) { 150 | try { 151 | new TrainingDocument('topic', {}); 152 | } catch (e) { 153 | expect(e).toBeA(TypeError); 154 | done(); 155 | } 156 | }); 157 | 158 | it('throws TypeError when text is undefined', function(done) { 159 | try { 160 | new TrainingDocument('topic', undefined); 161 | } catch (e) { 162 | expect(e).toBeA(TypeError); 163 | done(); 164 | } 165 | }); 166 | }); 167 | 168 | describe('Skill', function() { 169 | const Skill = require('../lib/BotTypes').Skill; 170 | 171 | it('throws TypeError when name parameter is not string', function(done) { 172 | try { 173 | new Skill([], 'mytopic', function(){}); 174 | done('should have failed'); 175 | } catch (e) { 176 | expect(e).toBeA(TypeError); 177 | done(); 178 | } 179 | }); 180 | 181 | it('throws TypeError when topic parameter is undefined', function(done) { 182 | try { 183 | new Skill('name', undefined); 184 | done('should have failed'); 185 | } catch (e) { 186 | expect(e).toBeA(TypeError); 187 | done(); 188 | } 189 | }); 190 | 191 | it('throws TypeError when topic parameter is not string', function(done) { 192 | try { 193 | new Skill('name', []); 194 | done('should have failed'); 195 | } catch (e) { 196 | expect(e).toBeA(TypeError); 197 | done(); 198 | } 199 | }); 200 | 201 | it('throws TypeError when skillFn parameter is undefined', function(done) { 202 | try { 203 | new Skill('name', '', undefined); 204 | done('should have failed'); 205 | } catch (e) { 206 | expect(e).toBeA(TypeError); 207 | done(); 208 | } 209 | }); 210 | 211 | it('throws TypeError when skillFn parameter is not string', function(done) { 212 | try { 213 | new Skill('name', '', []); 214 | done('should have failed'); 215 | } catch (e) { 216 | expect(e).toBeA(TypeError); 217 | done(); 218 | } 219 | }); 220 | 221 | it('returns new Skill object when parameters are valid', function(done) { 222 | const skillFn = function(context, req, res, next) { 223 | 224 | }; 225 | var skill = new Skill('name', 'topic', skillFn); 226 | expect(skill.name).toBe('name'); 227 | expect(skill.topic).toBe('topic'); 228 | expect(skill.apply).toBe(skillFn); 229 | done(); 230 | }); 231 | }); 232 | 233 | describe('StaticResponseSkill', function() { 234 | const Message = require('../lib/BotTypes').Message; 235 | const SingleLineMessage = require('../lib/BotTypes').SingleLineMessage; 236 | const MultiLineMessage = require('../lib/BotTypes').MultiLineMessage; 237 | const StaticResponseSkill = require('../lib/BotTypes').StaticResponseSkill; 238 | 239 | it('returns new skill that always returns given static string response', function(done) { 240 | var skill = new StaticResponseSkill('myskill', 'hello', 'hey there!'); 241 | expect(skill.topic).toBe('hello'); 242 | var fakeResponseObject = {}; 243 | return skill.apply({}, {}, fakeResponseObject, function() { 244 | expect(fakeResponseObject.message).toBeA(Message); 245 | expect(fakeResponseObject.message.content).toBe('hey there!'); 246 | done(); 247 | }); 248 | }); 249 | 250 | it('returns new skill that always returns given static message response', function(done) { 251 | var singleLineMessage = new SingleLineMessage('hey there!'); 252 | var skill = new StaticResponseSkill('myskill', 'hello', singleLineMessage); 253 | expect(skill.topic).toBe('hello'); 254 | var fakeResponseObject = {}; 255 | return skill.apply({}, {}, fakeResponseObject, function() { 256 | expect(fakeResponseObject.message).toBeA(Message); 257 | expect(fakeResponseObject.message).toBe(singleLineMessage); 258 | expect(fakeResponseObject.message.content).toBe('hey there!'); 259 | done(); 260 | }); 261 | }); 262 | 263 | it('throws type error when static response object is neither of type Message nor of type String', function() { 264 | try { 265 | new StaticResponseSkill('myskill', 'hello', {}); 266 | } catch (e) { 267 | expect(e).toBeA(TypeError); 268 | } 269 | }); 270 | }); 271 | 272 | describe('StaticRandomResponseSkill', function() { 273 | const Message = require('../lib/BotTypes').Message; 274 | const SingleLineMessage = require('../lib/BotTypes').SingleLineMessage; 275 | const MultiLineMessage = require('../lib/BotTypes').MultiLineMessage; 276 | const StaticResponseSkill = require('../lib/BotTypes').StaticResponseSkill; 277 | const StaticRandomResponseSkill = require('../lib/BotTypes').StaticRandomResponseSkill; 278 | 279 | it('returns new skill that returns random response from given string responses', function(done) { 280 | var skill = new StaticRandomResponseSkill('myrandomskill', 'hello', ['awesome', 'amazing', 'one', 'two']); 281 | skill._getRandomIndex = function(min, max) { 282 | return 3; 283 | }; 284 | 285 | var fakeResponseObject = {}; 286 | return skill.apply({}, {}, fakeResponseObject, function() { 287 | expect(fakeResponseObject.message).toBeA(Message); 288 | expect(fakeResponseObject.message.content).toBe('two'); 289 | 290 | fakeResponseObject = {}; 291 | skill._getRandomIndex = function(min, max) { 292 | return 0; 293 | }; 294 | 295 | return skill.apply({}, {}, fakeResponseObject, function() { 296 | expect(fakeResponseObject.message).toBeA(Message); 297 | expect(fakeResponseObject.message.content).toBe('awesome'); 298 | done(); 299 | }); 300 | }); 301 | }); 302 | 303 | it('returns new skill that returns random response from given Message responses', function(done) { 304 | var awesomeSingleLineResponse = new SingleLineMessage('awesome'); 305 | var amazingSingleLineResponse = new SingleLineMessage('amazing'); 306 | var oneSingleLineResponse = new SingleLineMessage('one'); 307 | var twoSingleLineResponse = new SingleLineMessage('two'); 308 | var skill = new StaticRandomResponseSkill('myrandomskill', 'hello', [awesomeSingleLineResponse, amazingSingleLineResponse, oneSingleLineResponse, twoSingleLineResponse]); 309 | skill._getRandomIndex = function(min, max) { 310 | return 3; 311 | }; 312 | 313 | var fakeResponseObject = {}; 314 | return skill.apply({}, {}, fakeResponseObject, function() { 315 | expect(fakeResponseObject.message).toBeA(Message); 316 | expect(fakeResponseObject.message).toBe(twoSingleLineResponse); 317 | 318 | fakeResponseObject = {}; 319 | skill._getRandomIndex = function(min, max) { 320 | return 0; 321 | }; 322 | 323 | return skill.apply({}, {}, fakeResponseObject, function() { 324 | expect(fakeResponseObject.message).toBeA(Message); 325 | expect(fakeResponseObject.message).toBe(awesomeSingleLineResponse); 326 | done(); 327 | }); 328 | }); 329 | }); 330 | 331 | it('throws TypeError when one of the items in the array is not of valid type', function(done) { 332 | try { 333 | var skill = new StaticRandomResponseSkill('myrandomskill', 'hello', ['awesome', {}]); 334 | } catch (e) { 335 | expect(e).toBeA(TypeError); 336 | done(); 337 | } 338 | }); 339 | 340 | it('throws TypeError when the responses parameter is not an array', function(done) { 341 | try { 342 | new StaticRandomResponseSkill('myrandomskill', 'hello', {}); 343 | } catch (e) { 344 | expect(e).toBeA(TypeError); 345 | done(); 346 | } 347 | }); 348 | 349 | it('throws TypeError when the responses parameter is undefined', function(done) { 350 | try { 351 | new StaticRandomResponseSkill('myrandomskill', 'hello', undefined); 352 | } catch (e) { 353 | expect(e).toBeA(TypeError); 354 | done(); 355 | } 356 | }); 357 | 358 | it('throws TypeError when the topic parameter is undefined', function(done) { 359 | try { 360 | new StaticRandomResponseSkill('myrandomskill', undefined); 361 | } catch (e) { 362 | expect(e).toBeA(TypeError); 363 | done(); 364 | } 365 | }); 366 | 367 | it('throws TypeErrpr when responses array is empty', function(done) { 368 | try { 369 | new StaticRandomResponseSkill('myrandomskill', 'topic', []); 370 | } catch (e) { 371 | expect(e).toBeA(TypeError); 372 | done(); 373 | } 374 | }); 375 | }); -------------------------------------------------------------------------------- /test/ContextStoreTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by manthanhd on 19/10/2016. 3 | */ 4 | const expect = require('expect'); 5 | 6 | describe('ContextStore', function() { 7 | describe('put', function() { 8 | const ContextStore = require('../lib/ContextStore'); 9 | 10 | it('puts context against given ID', function(done) { 11 | const store = new ContextStore(); 12 | store.put(123, {subject: 'hammer'}, function(err, storedRecord) { 13 | if(err) return done('should not have errored'); 14 | 15 | expect(storedRecord).toExist(); 16 | expect(storedRecord.id).toBe(123); 17 | expect(storedRecord.context).toExist(); 18 | expect(storedRecord.context.subject).toBe('hammer'); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('returns TypeError when ID is undefined', function(done) { 24 | const store = new ContextStore(); 25 | store.put(undefined, {subject: 'car'}, function(err, storedRecord) { 26 | expect(err).toExist(); 27 | expect(err).toBeA(TypeError); 28 | expect(storedRecord).toNotExist(); 29 | done(); 30 | }); 31 | }); 32 | 33 | it('does not throw error when callback is undefined', function(done) { 34 | const store = new ContextStore(); 35 | try { 36 | store.put(123, {subject: 'dogs'}, undefined); 37 | done(); 38 | } catch (e) { 39 | done(e); 40 | } 41 | }); 42 | }); 43 | 44 | describe('get', function() { 45 | const ContextStore = require('../lib/ContextStore'); 46 | 47 | it('gets saved context', function(done) { 48 | const store = new ContextStore(); 49 | 50 | var retrieved = function(err, contextRecord) { 51 | expect(err).toNotExist(); 52 | expect(contextRecord).toExist(); 53 | expect(contextRecord.id).toBe(123); 54 | expect(contextRecord.context).toExist(); 55 | expect(contextRecord.context.subject).toBe('camel'); 56 | done(err); 57 | }; 58 | 59 | var stored = function(err, storedRecord) { 60 | store.get(123, retrieved); 61 | }; 62 | 63 | store.put(123, {subject: 'camel'}, stored); 64 | }); 65 | 66 | it('returns TypeError when lookup ID is undefined', function(done) { 67 | const store = new ContextStore(); 68 | 69 | var retrieved = function(err, contextRecord) { 70 | expect(err).toExist(); 71 | expect(contextRecord).toNotExist(); 72 | expect(err).toBeA(TypeError); 73 | done(contextRecord); 74 | }; 75 | 76 | store.get(undefined, retrieved); 77 | }); 78 | 79 | it('throws TypeError when callback is undefined', function(done) { 80 | const store = new ContextStore(); 81 | 82 | try { 83 | store.get(123); 84 | done('should have failed') 85 | } catch (e) { 86 | expect(e).toExist(); 87 | expect(e).toBeA(TypeError); 88 | done(); 89 | } 90 | }); 91 | 92 | it('gets undefined result if key does not exist', function(done) { 93 | const store = new ContextStore(); 94 | 95 | store.get(999, function(err, contextRecord) { 96 | if(err) return done(err); 97 | 98 | expect(contextRecord).toNotExist(); 99 | done(); 100 | }); 101 | }); 102 | 103 | it('gets context record with undefined context value if key exists but context does not exist', function(done) { 104 | const store = new ContextStore(); 105 | 106 | var retrieved = function(err, contextRecord) { 107 | expect(err).toNotExist(); 108 | expect(contextRecord).toExist(); 109 | expect(contextRecord.id).toBe(123); 110 | expect(contextRecord.context).toNotExist(); 111 | done(err); 112 | }; 113 | 114 | var stored = function(err, storedRecord) { 115 | store.get(123, retrieved); 116 | }; 117 | 118 | store.put(123, undefined, stored); 119 | }); 120 | }); 121 | 122 | describe('remove', function() { 123 | const ContextStore = require('../lib/ContextStore'); 124 | 125 | it('removes specified key', function(done) { 126 | const store = new ContextStore(); 127 | 128 | var retrieveRemoved = function(err, contextRecord) { 129 | expect(err).toNotExist(); 130 | expect(contextRecord).toNotExist(); 131 | done(); 132 | }; 133 | 134 | var removed = function(err, contextRecord) { 135 | store.get(123, retrieveRemoved); 136 | }; 137 | 138 | var retrieved = function(err, contextRecord) { 139 | store.remove(123, removed); 140 | }; 141 | 142 | var stored = function(err, storedRecord) { 143 | store.get(123, retrieved); 144 | }; 145 | 146 | store.put(123, {subject: 'marshmallow'}, stored); 147 | }); 148 | 149 | it('returns TypeError when removing undefined key', function(done) { 150 | const store = new ContextStore(); 151 | 152 | var removed = function(err, contextRecord) { 153 | expect(err).toExist(); 154 | expect(err).toBeA(TypeError); 155 | expect(contextRecord).toNotExist(); 156 | done(); 157 | }; 158 | 159 | store.remove(undefined, removed); 160 | }); 161 | 162 | it('does not throw error when callback is undefined', function(done) { 163 | const store = new ContextStore(); 164 | 165 | try { 166 | store.remove(123, undefined); 167 | done(); 168 | } catch (e) { 169 | done(e); 170 | } 171 | }) 172 | }); 173 | }); -------------------------------------------------------------------------------- /wiki/CLASSIFIER.md: -------------------------------------------------------------------------------- 1 | # Classifier 2 | 3 | At an abstract level, a classifier is a module that is able to classify a bunch of given text into a series of topics with decreasing level of confidence. Once the bot receives the list of classified topics, it tries to map the topic that has the highest level of confidence with a skill. 4 | 5 | From the perspective of the bot, a classifier is an asynchronous black box. Hence, any implementation of the classifier abstract interface can be used with the bot. 6 | 7 | The bot core depends on the [talkify-natural-classifier](https://github.com/manthanhd/talkify-natural-classifier) library. It provides two different classifier implementations: 8 | 9 | 1. Logistic Regression (`LogisticRegressionClassifier`) 10 | 2. Naive Bayes (`NaiveBayesClassifier`) 11 | 12 | The further implementation of these two classifiers is based on the [NaturalNode/Natural](https://github.com/NaturalNode/natural) NLP Node.js 13 | 14 | ## Custom classifier 15 | 16 | A custom classifier needs to extend the [talkify-classifier](https://github.com/manthanhd/talkify-classifier) interface. This interface defines three methods: 17 | 18 | 1. trainDocument 19 | 2. getClassifications 20 | 3. initialize 21 | 22 | Easiest way would be to define an object that implements those three methods and then passing it to the `classifier` configuration option when instantiating the bot core. Here's a quick snippet: 23 | 24 | ```javascript 25 | var myClassifier = { 26 | trainDocument: function(trainingData, callback) { 27 | ... 28 | }, 29 | getClassifications: function(input, callback) { 30 | ... 31 | }, 32 | initialize: function(callback) { 33 | ... 34 | } 35 | }; 36 | 37 | var options = { 38 | classifier: myClassifier 39 | }; 40 | 41 | var Bot = require('talkify').Bot; 42 | var myBot = new Bot(options); 43 | ``` 44 | 45 | Lets go through each of the methods that a classifier needs to support in order to function with the Bot. 46 | 47 | ### trainDocument(trainingData, callback) 48 | 49 | Most classifiers, in one way or another, need to be trained. If the classifier is a micro-classifier (like the one that comes built in with the Bot) then it will need to be trained every time the Bot is initialized. This is because the training data stays within the Bot's memory. This is not too much of a concern because the built-in classifier is usually very quick to train. 50 | 51 | Some complex classifiers like the [IBM Watson Classifier](https://github.com/manthanhd/talkify-watson-classifier) need not be trained every time the Bot is initialized as the training data resides on IBM's cloud servers. In this case, while you still should implement the trainDocument method, it's implementation could be kept empty. 52 | 53 | The implementation of this method must accept two parameters. These are `trainingData` and `callback`. The input provided within the `trainingData` object could be a single `TrainingDocument` object or an array of objects of `TrainingDocument` type. The implementation of this method in [NaturalClassifier function of talkify-natural-classifier](https://github.com/manthanhd/talkify-natural-classifier/blob/master/lib/NaturalClassifier.js) might help. 54 | 55 | The `TrainingDocument` object that is provided as a single or an array within `trainingData` has two publicly accessible attributes. These are `topic` and `text`. So when you are within your `trainDocument` function, you should be able to do this: 56 | 57 | ```javascript 58 | var myClassifier = { 59 | trainDocument: function(trainingData, callback) { 60 | if(trainingData instanceof Array) { 61 | for(var i = 0; i < trainingData.length; i++) { 62 | var topic = trainingData[i].topic; 63 | var text = trainingData[i].text; 64 | console.log('TrainingData[%s], topic: %s, text: %s', i, topic, text); 65 | } 66 | return callback(undefined, true); 67 | } 68 | 69 | var topic = trainingData.topic; 70 | var text = trainingData.text; 71 | return callback(undefined, true); 72 | }, 73 | ... 74 | }; 75 | ``` 76 | 77 | Here in this example, we are handling both cases, one where trainingData could be a single object as well as another where it could be an array. In either case, the object (or objects) that you receive will have at least one object with two aforementioned attributes. 78 | 79 | When you are done, make sure you call the callback to let the bot know that you are done. The `callback` must be called with two parameters, namely, `error` and `result`. The `error` parameter could contain an `Error` object if there is an error or a literal `undefined` object. On the other hand, the `result` object should contain the status of the result. In most cases, this is simply `true`. 80 | 81 | Lines 9 and 14 show an example invocation of the callback in a success scenario. In case of a failure: 82 | 83 | ```javascript 84 | ... 85 | var err = new Error('Uh oh. Something went wrong.'); 86 | callback(err); 87 | ... 88 | ``` 89 | 90 | #### Buffering your training data 91 | 92 | In many cases, you might want to buffer your training data so that you can efficiently process your entire training data set at once. This can be achieved by leveraging the `initialize` method. At the end of every call to `trainDocument`, the Bot will call the `initialize` method. 93 | 94 | This means that you can buffer your training when the `trainDocument` method is call and when the time is right, process it in the `initialize` method. 95 | 96 | ### initialize(callback) 97 | 98 | The `initialize` method is there to provide your classifier with an opportunity to finish any remaining processing after training. This could be some network or a database call. If you choose to [buffer your training data](#buffer-your-training-data), the initialize method could be a good place to complete the remainder of your processing. 99 | 100 | The initialize method must accept one parameter, namely `callback`. Whatever you do, make sure you call the callback method at the end when you are done processing. 101 | 102 | When calling this callback, you only need to pass a parameter value in when there is an error. Here's a quick example of a success scenario: 103 | 104 | ```javascript 105 | var myClassifier = { 106 | initialize: function(callback) { 107 | ... 108 | return callback(); 109 | }, 110 | ... 111 | }; 112 | ``` 113 | 114 | If something goes wrong, you can simply call: 115 | 116 | ```javascript 117 | ... 118 | var err = new Error('Whoops, something went wrong.'); 119 | callback(err); 120 | ``` 121 | 122 | ### getClassifications(text, callback) 123 | 124 | The `getClassifications` method is one of the most simple looking methods, however, at the same time, it is also one of the most important. This is because using this is how the Bot will receive classifications for text which in turn it will use to execute skills in order to respond to queries. 125 | 126 | An implementation of this method must accept two parameters, namely `text` and `callback`. 127 | 128 | The text parameter will always be a `string` as it is the input text being received from the end-user that is being requested for classification. 129 | 130 | The `callback` parameter will be a function that accepts two arguments, namely `error` and `classifications[]` array. Call this function when you have successfully managed to classify the string. 131 | 132 | When calling the callback in an error scenario, make sure that the first parameter is not null. Ideally you'd want this to be an instance of an `Error` object like so: 133 | 134 | ```javascript 135 | ... 136 | var err = new Error('Not very good.'); 137 | callback(err); 138 | ``` 139 | 140 | However, you should set this parameter to `undefined` in case of a success with a non-empty array as a parameter to the `classifications[]` array. Here's an example snippet: 141 | 142 | ```javascript 143 | var myClassifier = { 144 | getClassifications: function(trainingData, callback) { 145 | ... 146 | return callback(undefined, [ {label: 'MyTopic', value: 0.5} ]); 147 | }, 148 | ... 149 | }; 150 | ``` 151 | 152 | The `classifications[]` array must contain objects with at least two attributes, namely `label` and `value`. The value for the `label` attribute must be a `string` while that of the `value` attribute must be a number. As shown in the above example, the array has one classification object of `topic` MyTopic and `value` 0.5. This means that the classifier has classified the given `text` to be of `topic` MyTopic with a confidence value of 0.5, i.e. it is 50% confident on the result. Note that the first parameter is explicitly set to `undefined` as the classification was successful. 153 | 154 | As of now, the array must have objects that have the value of the `value` field in decreasing order, i.e. highest first. The value of this field must be between 0 and 1. 155 | 156 | In cases where a classification could not be determined, the classifier must return an array with an object whose topic is `undefined` and value is set to an arbitrary value (usually 1 as it is most confident that the answer is indeterminate). Here's an example: 157 | 158 | ```javascript 159 | var myClassifier = { 160 | getClassifications: function(trainingData, callback) { 161 | ... 162 | return callback(undefined, [ {label: undefined, value: 1} ]); 163 | }, 164 | ... 165 | }; 166 | ``` 167 | 168 | ## Adding a custom classifier to Bot 169 | 170 | A custom classifier instance can be passed as part of the configuration object to the Bot during its instantiation. Here's a quick example: 171 | 172 | ```javascript 173 | var myClassifier = {...} 174 | var options = { 175 | classifier: myClassifier 176 | }; 177 | var Bot = require('talkify').Bot; 178 | var myBot = new Bot(options); 179 | ``` 180 | 181 | Notice how the options object contains an attribute called `classifier` with the value set to the `myClassifier` variable. 182 | 183 | A single bot instance can accept multiple classifiers. This way you can have multiple classifiers, each specialised in a single topic. This generally results in more accurate classification over a broad range of topics. 184 | 185 | In order to add multiple classifiers, you must assign an array containing all your classifier objects to the `classifier` options attribute. 186 | 187 | ```javascript 188 | var myClassifier1 = {...} 189 | var myClassifier2 = {...} 190 | var options = { 191 | classifier: [myClassifier1, myClassifier2] 192 | }; 193 | var Bot = require('talkify').Bot; 194 | var myBot = new Bot(options); 195 | ``` 196 | 197 | This will configure the myBot object to use two classifiers: `myClassifier1` and `myClassifier2`. 198 | 199 | For readability, you can optionally use `classifiers` options attribute instead of `classifier` attribute when specifying multiple classifiers. -------------------------------------------------------------------------------- /wiki/SKILLS.md: -------------------------------------------------------------------------------- 1 | # Skills 2 | 3 | This page is intended to serve as a guide to what skills are, how they plug into the Bot core and how they can be used. 4 | 5 | 6 | * [Concept](#concept) 7 | * [Basics](#basics) 8 | * [The apply function](#the-apply-function) 9 | * [Context](#context) 10 | * [Request](#request) 11 | * [Response](#response) 12 | * [Adding skill](#adding-skill) 13 | * [Helpers](#helpers) 14 | * [StaticResponse](#staticresponse) 15 | * [StaticRandomResponse](#staticrandomresponse) 16 | 17 | 18 | 19 | ## Concept 20 | 21 | ### Basics 22 | 23 | At its core, a skill is an object that represents an action that the bot executes to respond back to the user as part of a conversation. The object is called a Skill object and the action is the JavaScript skill function that is passed in when instantiating a new skill. 24 | 25 | At a basic level, a skill object can be created like so: 26 | 27 | ```javascript 28 | var skillName = 'my_help_skill'; 29 | var topicName = 'help'; 30 | var helpSkill = new Skill(skillName, topicName, function ApplyFn(context, request, response, next) { 31 | ... 32 | return next(); 33 | }); 34 | ``` 35 | 36 | A skill name is expected to be unique. However, please note that multiple skills (with different names, of course) can work with the same topic name at different confidence levels (see [Adding Skill](#adding-skill)). 37 | 38 | For every skill, an apply function must be defined. 39 | 40 | ### The apply function 41 | 42 | The apply function is called whenever a skill is matched against a given incoming message. It is called with four function parameters, namely: `context, request, response, next`. These four objects are there to help you construct a meaningful conversational response. 43 | 44 | #### Context 45 | 46 | The context object is a simple object, designed to serve as a key-value map. Every interaction with the bot requires a correspondance ID. This ID is used to create a new context if one does not already exist or to load an existing one. 47 | 48 | The idea is to allow skills to infer values from previously saved ones if they have not been explicitly specified in a given query. 49 | 50 | For example, a skill that extracts and resolves a ticket number can use the context to save the ticket number when a given sentene contains one. Next time, when the same skill is called as part of a different statement, the ticket number may not be present in the sentence. In this scenario, the ticket number can be retrieved from the context that was persisted previously. 51 | 52 | #### Request 53 | 54 | The request object serves to provide data about the request itself. Currently, the request object contains the following: 55 | 56 | | Attribute | Data Type | Description | 57 | |----------:|-----------|-------------------------------------------------------| 58 | | id | String | Correspondance ID of the request | 59 | | message | Message | Message object representing the incoming message | 60 | | sentence | Object | Object containing `all[]` array containing all sentences in the request, `current` string representing the current sentence that is being processed and `index` number representing the index within the sentence array of the current sentence that is being processed. | 61 | | skill | Object | Object containing `all[]` array containing all skills resolved as part of the request, `current` Skill representing the current sentence that is being processed and `index` number representing the index within the skill array of the current skill that is being processed. | 62 | 63 | #### Response 64 | 65 | **response.message** 66 | 67 | The response object can be used to respond to the query from the skill. Basic usage to respond with a message is: 68 | 69 | ```javascript 70 | response.message = new Message('SingleLine', 'Hey there!'); 71 | ``` 72 | 73 | **next()** 74 | 75 | Once you have responded to a message, you may want to do something else too. That is fine, as long as you remember to call the next() function which is passed in as the fourth parameter to the apply function. 76 | 77 | **Make sure you call next() or else the bot will never know that the skill has finished processing the request.** 78 | 79 | **response.final()** 80 | 81 | All skills are resolved and executed as part of a skill resolution chain. This is true especially when the bot is processing multi-sentence requests. However, sometimes, you may want to set a final message to the response, preventing the rest of the execution chain from responding. This can be achieved by calling the `final()` function within the response object. Basic usage is like so: 82 | 83 | ```javascript 84 | response.message = new SingleLineMessage('Sorry I do not understand what you are trying to say.'); 85 | response.final(); 86 | response.next(); 87 | ``` 88 | 89 | **response.lockConversationForNext()** 90 | 91 | If your skill requires more information from the user, you can use the follow-up feature. This can be achieved by acquiring a lock on the conversation for the next call. You can use this feature like so: 92 | 93 | ```javascript 94 | response.lockConversationForNext(); 95 | ``` 96 | 97 | This tells the bot to lock the conversation for the skill you are in such that the next time bot gets a message from the end-user, it sends the request straight to your skill without resolving the topic at all. 98 | 99 | However, keep in mind that as the method name suggests, the `lockConversationForNext` method only reserves the lock for the next one call. The lock gets released as soon as your skill is called the next time. If you need yet more information, you will have to call that method again. 100 | 101 | As a best practice, make sure you provide a prompt to the user before you call the `lockConversationForNext` by setting a message on `response.message`. This will help the end-user see what he/she is being asked for. 102 | 103 | Also, as a side-note, when `lockConversationForNext` is called, the bot abandons execution of the rest of the skill chain for multi-sentence messages. Hence, there is no need to call `response.final()` after acquiring a lock. 104 | 105 | **response.send(message)** 106 | 107 | The `send` method is a wrapper for `response.message` and `next()` calls. It accepts a single parameter, namely `message` where you can pass in a `Message` object. Passing a `Message` object here has the same effect as doing `response.message = message`. 108 | 109 | It is merely there for convenience, allowing you to write cleaner code. Here's how it replaces the old way of doing things: 110 | 111 | ```javascript 112 | response.message = new Message(...); 113 | return next(); 114 | ``` 115 | 116 | with the new way of doing things: 117 | 118 | ```javascript 119 | return response.send(new Message(...)); 120 | ``` 121 | 122 | **Pro Tip:** 123 | 124 | You can chain methods to simplify your statements. For instance, the below: 125 | 126 | ```javascript 127 | return response.lockConversationForNext().final().send(new Message(...)); 128 | ``` 129 | 130 | has the same effect as: 131 | 132 | ```javascript 133 | response.lockConversationForNext(); 134 | response.final(); 135 | response.message = new Message(...); 136 | return next(); 137 | ``` 138 | 139 | ## Adding skill 140 | 141 | For every topic the bot has been trained for, it must have a corresponding skill that it can call to resolve that topic. Thus, the number of skills must be *at least* equal to the number of topics. At a basic level, a skill can be added to the bot like so: 142 | 143 | ```javascript 144 | bot.addSkill(skillObject); 145 | ``` 146 | 147 | You can map more skills than the number of topics. This is because skills can be mapped at different minimum confidence levels to a topic. The confidence level can be specified as the second parameter to the addSkill method on the bot. Basic usage is like so: 148 | 149 | ```javascript 150 | bot.addSkill(skillObject, 50); 151 | ``` 152 | 153 | According to the above example, the bot will only resolve the above skill when its confidence in the resolved topic from the sentence is at least 50%. 154 | 155 | Multiple topics can be added at multiple skill levels. The bot will always resolve the skill closest to its confidence level. 156 | 157 | ```javscript 158 | bot.addSkill(skillA); 159 | bot.addSkill(skillB, 40); 160 | bot.addSkill(skillC, 60); 161 | ``` 162 | 163 | In the above example, `skillC` gets executed when the bot is at least 60% confident in its topic resolution, `skillB` when it is 40% confident and `skillA` below 40%. Notice that we did not need to specify confidence level for `skillA` because when skills are added, the default minimum confidence level at which they are executed is always 0. 164 | 165 | **Pro Tip:** 166 | 167 | The `addSkill` method is chainable. This means that doing the following: 168 | 169 | ```javascript 170 | bot.addSkill(skill1).addSkill(skill2).addSkill(skill3); 171 | ``` 172 | 173 | will have the same effect as: 174 | 175 | ```javascript 176 | bot.addSkill(skill1); 177 | bot.addSkill(skill2); 178 | bot.addSkill(skill3); 179 | ``` 180 | 181 | ## Helpers 182 | 183 | To make skill-making easier, some helpers are available. These are wrappers around the skill object that allow you to common tasks within a skill. 184 | 185 | ### StaticResponse 186 | 187 | The `StaticResponse` helper can help you create a skill that statically responds using a single message object. The constructor requires you to pass in three parameters. These are `skillName` as a `string`, `topicName` as a `string` and `Message` which could be a `string`, `array` or a `Message` object. 188 | 189 | Here's syntax for all the ways it can be used. 190 | 191 | ```javascript 192 | var skillName = 'MyAwesomeSkill'; 193 | var topic = 'some_topic'; 194 | 195 | var singleLineSkill = new StaticResponseSkill(skillName, topic, 'My Static Response'); 196 | var multiLineSkill = new StaticResponseSkill(skillName, topic, ['Static', 'Response']); 197 | var messageObjectSkill = new StaticResponseSkill(skillName, topic, [new Message('MultiLine', ['Multi', 'Line'])]); 198 | ``` 199 | 200 | ### StaticRandomResponse 201 | 202 | If you ever wanted to return a random response from a list of responses, `StaticRandomResponse` helper is your friend. The constructor requires you to pass in three parameters. These are `skillName` as a `string`, `topicName` as a `string` and `Message` which could be a `string[]`, `Message[]` object. 203 | 204 | Here's a syntax for all the ways it can be used. 205 | 206 | ```javscript 207 | var skillName = 'MyAwesomeSkill'; 208 | var topic = 'some_topic'; 209 | 210 | var singleLineSkill = new StaticRandomResponseSkill(skillName, topic, ['random', 'skill']); 211 | var messageObjectSkill = new StaticRandomResponseSkill(skillName, topic, [new SingleLineMessage('Random Response One'), new SingleLineMessage('Random Response Two')]); 212 | ``` 213 | --------------------------------------------------------------------------------