├── .gitignore ├── .vscode └── tasks.json ├── LICENSE ├── README.md ├── examples ├── handlers │ ├── set_alarm.js │ └── summary.js └── scenarios │ ├── botGames.json │ ├── router.json │ ├── smoking.json │ ├── stomachPain.json │ └── validationsAndCarousels.json ├── index.js ├── lib ├── GraphDialog.js ├── Luis.js ├── Navigator.js ├── Node.js ├── Parser.js ├── Scenario.js ├── Validator.js ├── common.js ├── conditionHandler.js └── intentScorer.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .DS_Store 39 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "tsc", 6 | "isShellCommand": true, 7 | "args": ["-p", "."], 8 | "showOutput": "silent", 9 | "problemMatcher": "$tsc" 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Catalyst Code 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 | # bot-graph-dialog 2 | 3 | This node module is an extension for Microsoft Bot Framework. 4 | 5 | Use this library to define graph-based dialogs instead of the waterfall dialogs supported today. 6 | Also, instead of hard coding your dialogs, use this library to define your dialogs as jsons. 7 | These dialogs can be loaded dynamically from any external data source (db, file system, etc.) 8 | 9 | Code sample for how to use this library can be found [here](https://github.com/CatalystCode/bot-trees). 10 | 11 | 12 | ## Getting Started 13 | 14 | 15 | ### Usage 16 | 17 | ``` 18 | npm install [--save] bot-graph-dialog 19 | ``` 20 | 21 | ### Code Sample 22 | 23 | ```js 24 | var builder = require('botbuilder'); 25 | var BotGraphDialog = require('bot-graph-dialog'); 26 | 27 | var connector = new builder.ChatConnector({ 28 | appId: '', 29 | appPassword: '', 30 | }); 31 | var bot = new builder.UniversalBot(connector); 32 | var intents = new builder.IntentDialog(); 33 | bot.dialog('/', intents); 34 | 35 | // handler for loading scenarios from external datasource 36 | function loadScenario(scenario) { 37 | return new Promise((resolve, reject) => { 38 | console.log('loading scenario', scenario); 39 | 40 | // implement loadScenario from external datasource. 41 | var scenarioObj = ... 42 | resolve(scenarioObj); 43 | }); 44 | } 45 | 46 | BotGraphDialog.GraphDialog 47 | .fromScenario({ 48 | bot, 49 | scenario: '', 50 | loadScenario 51 | }) 52 | // attach stomach pain graph dialog to handle 'stomach' message 53 | .then(graphDialog => intents.matches(/^stomach/i, graphDialog.getDialog())); 54 | 55 | ``` 56 | 57 | **See sample bot app [here](https://github.com/CatalystCode/bot-trees)** 58 | 59 | 60 | ## Sample Scenarios 61 | 62 | Follow [these samples](examples/scenarios) as a reference to create your own scenarios. 63 | 64 | 65 | ## Schema Break Down 66 | 67 | Each step\scenario in the schema is recursive. 68 | 69 | * `id` - The id for the step 70 | * `type` [required] - The type of the step: 71 | * `text` 72 | * `sequence` 73 | * `prompt` 74 | * `score` 75 | * `heroCard` 76 | * `carousel` 77 | * `handler` 78 | * `end` 79 | * `steps` - An array of steps or other scenarios 80 | * `models` - see [#Models] 81 | 82 | ### Steps Types 83 | 84 | #### type: "text" 85 | 86 | Display a text which can also be formatted with dialog variables. 87 | 88 | Properties: 89 | 90 | ```json 91 | { 92 | "type": "text", 93 | "data": { "text": "Text to print" } 94 | } 95 | ``` 96 | 97 | #### type: "sequence" 98 | 99 | This step is a wrapper of one or more subsidiary steps. 100 | 101 | Properties: 102 | 103 | ```json 104 | { 105 | "id": "step id", 106 | "type": "sequence", 107 | "steps": [ {...}, {...} ] 108 | } 109 | ``` 110 | 111 | #### type: "prompt" 112 | 113 | Prompt for a defined user response. 114 | 115 | Properties: 116 | 117 | ```json 118 | { 119 | "type": "prompt", 120 | "data": { 121 | "type": "text", 122 | "text": "Text to print with the prompt options" 123 | } 124 | } 125 | ``` 126 | 127 | Prompt types can be one of: 128 | 129 | * `text` [default] - Prompts for free text. This options is set in case no `type` property is provided 130 | * `number` - Request for a valid number 131 | * `time` - Request for a time construct like "2 hours ago", "yesterday", etc. 132 | * `confirm` - Yes \ No 133 | 134 | #### type: "score" 135 | 136 | Get a text from the user and resolve the intent against a single or multiple intent recognition (Luis) APIs. 137 | 138 | Properties: 139 | 140 | ```json 141 | { 142 | "id": "intentId", 143 | "type": "score", 144 | "data": { 145 | "models": [ "model1", "model2" ] 146 | }, 147 | "scenarios": [ 148 | { 149 | "condition": "intentId.intent == 'intent_1'", 150 | "nodeId": "Node Id to jump to" 151 | }, 152 | { 153 | "condition": "intentId.intent == 'intent_2'", 154 | "steps": [ { ... }, { ... } ] 155 | }, 156 | { 157 | "condition": "intentId.intent == 'intent_3'", 158 | "steps": [ { "subScenario": "intent_3_scenario" } ] 159 | } 160 | ] 161 | } 162 | ``` 163 | 164 | For models, see [#Models]. 165 | Under models, specify one or more models you have defined under `models` property. 166 | 167 | Under scenarios, define a condition for expected intent and which scenario \ step it should jump to. 168 | 169 | 170 | #### type: heroCard 171 | 172 | Creates a Hero Card, displaying images and using buttons 173 | 174 | ```json 175 | { 176 | "id": "myCard", 177 | "type": "heroCard", 178 | "data": { 179 | "title": "Space Needle", 180 | "subtitle": "Our Subtitle", 181 | "text": "The Space Needle is an observation tower in Seattle, Washington, a landmark of the Pacific Northwest, and an icon of Seattle.", 182 | "images": [ 183 | "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7c/Seattlenighttimequeenanne.jpg/320px-Seattlenighttimequeenanne.jpg" 184 | ], 185 | "tap": { 186 | "action": "openUrl", 187 | "value": "https://en.wikipedia.org/wiki/Space_Needle" 188 | }, 189 | "buttons": [ 190 | { 191 | "label": "Wikipedia", 192 | "action": "openUrl", 193 | "value": "https://en.wikipedia.org/wiki/Space_Needle" 194 | } 195 | ] 196 | } 197 | } 198 | ``` 199 | 200 | 201 | #### type: carousel 202 | 203 | Creates a Carousel control, which is a list of Hero Card controls in the same structure as demonstrated above: 204 | 205 | ```json 206 | { 207 | "id": "myCarousel", 208 | "type": "carousel", 209 | "data": { 210 | "text": "Can you see a carousel of hero cards bellow?", 211 | "cards": [ 212 | ] 213 | } 214 | } 215 | ```` 216 | 217 | #### type: "handler" 218 | 219 | Enables providing a custom code to handle a specific step. 220 | 221 | Properties: 222 | 223 | ```json 224 | { 225 | "type": "handler", 226 | "data": { "name": "handler" } 227 | } 228 | ``` 229 | 230 | `name` the name for this handler. This will be provided to the callback for loading this handler. 231 | The data that is collected during the dialog can be found in `session.dialogData.data[]` 232 | 233 | #### type: "end" 234 | 235 | End the dialog and ignore any further steps. 236 | 237 | Properties: 238 | 239 | ```json 240 | { 241 | "type": "end", 242 | "data": { "text": "Optional text to display before ending the conversation." } 243 | } 244 | ``` 245 | 246 | ### Models 247 | 248 | This property defined the models that can be used for intent recognition throughout the dialog. 249 | 250 | ```json 251 | { 252 | "type": "sequence", 253 | "steps": [ ... ], 254 | "models": [ 255 | { 256 | "name": "model1", 257 | "url": "https://model1.url&q=" 258 | }, 259 | { 260 | "name": "model2", 261 | "url": "https://model2.url&q=" 262 | }, 263 | { 264 | "name": "model3", 265 | "url": "https://model3.url&q=" 266 | } 267 | ] 268 | } 269 | ``` 270 | 271 | ### Text Format 272 | When prompting or displaying text, it is possible to use a format to insert session variables like that: 273 | 274 | ```json 275 | { 276 | "id": "userName", 277 | "type": "prompt", 278 | "data": { 279 | "type": "text", 280 | "text": "what is your name?" 281 | } 282 | }, 283 | { 284 | "type": "text", 285 | "data": { "text": "Welcome {userName}!" } 286 | } 287 | ``` 288 | 289 | ## Validations 290 | 291 | There following validations are currently supported for validating user input. When a node contains a validation condition, the user will be prompted to provide the value until it satisfies the validation condition: 292 | 293 | 294 | ### date validation 295 | 296 | ```json 297 | { 298 | "id": "flightDate", 299 | "type": "prompt", 300 | "data": { 301 | "type": "time", 302 | "text": "When would you like to fly?", 303 | "validation": { 304 | "type": "date", 305 | "setup": { 306 | "min_date": "2016-11-15 00:00:00", 307 | "max_date": "2016-11-25 23:59:59", 308 | "invalid_msg": "Oops, wrong date!" 309 | } 310 | } 311 | } 312 | } 313 | ``` 314 | 315 | ### regex validation 316 | 317 | ```json 318 | { 319 | "id": "isTesting", 320 | "type": "prompt", 321 | "data": { 322 | "type": "text", 323 | "text": "What are you doing? (I'll validate using regex: ^test)", 324 | "validation": { 325 | "type": "regex", 326 | "setup": { 327 | "pattern": "^test" 328 | } 329 | } 330 | } 331 | } 332 | ``` 333 | 334 | 335 | # License 336 | [MIT](LICENSE) 337 | -------------------------------------------------------------------------------- /examples/handlers/set_alarm.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (session, next, data) => { 3 | 4 | var intent = session.dialogData.data[data.source]; 5 | var alarmTime = null; 6 | if (intent.actions[0].parameters[0].name == "time") { 7 | // alarmTime = intent.entities... 8 | // use intent.entities to extract relevant information 9 | // assuming extracted alarmTime 10 | 11 | alarmTime = '2016-10-10 10:10'; 12 | } 13 | 14 | if (data.target && alarmTime) { 15 | session.dialogData.data[data.target] = alarmTime; 16 | } 17 | 18 | session.send('Alarm set for ' + alarmTime); 19 | return next(); 20 | } -------------------------------------------------------------------------------- /examples/handlers/summary.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (session, next) => { 3 | var summary = "Summary: "; 4 | for (var prop in session.dialogData.data) { 5 | summary += prop + ': [' + session.dialogData.data[prop] + ']; '; 6 | } 7 | session.send(summary); 8 | return next(); 9 | } -------------------------------------------------------------------------------- /examples/scenarios/botGames.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "botGames", 3 | "type": "sequence", 4 | "steps": [ 5 | { 6 | "id": "whatDoYouWantToDo", 7 | "type": "prompt", 8 | "data": { "text": "try to set an alarm or commands like quit, start over, etc..." } 9 | }, 10 | { 11 | "id": "userIntent", 12 | "type": "score", 13 | "data": { 14 | "source": "whatDoYouWantToDo", 15 | "models": [ "bot-common-responses", "Bottle" ] 16 | }, 17 | "scenarios": [ 18 | { 19 | "condition": "userIntent.intent == 'set alarm'", 20 | "steps": [ 21 | { 22 | "type": "handler", 23 | "data": { 24 | "name": "set_alarm", 25 | "source": "userIntent", 26 | "target": "alarmTime" 27 | } 28 | } 29 | ] 30 | }, 31 | { 32 | "condition": "userIntent.intent == 'start over'", 33 | "nodeId": "whatDoYouWantToDo" 34 | }, 35 | { 36 | "condition": "userIntent.intent == 'quit'", 37 | "steps": [ 38 | { 39 | "type": "end", 40 | "data": { "text": "Thank you, goodbye." } 41 | } 42 | ] 43 | } 44 | ] 45 | }, 46 | { 47 | "type": "text", 48 | "data": { "text": "Done doing '{whatDoYouWantToDo}'." } 49 | } 50 | ], 51 | "models": [ 52 | { 53 | "name": "bot-common-responses", 54 | "url": "https://api.projectoxford.ai/luis/v1/application?id=7ff935f4-fe33-4a2a-b155-b82dbbf456ed&subscription-key=d7b46a6c72bf46c1b67f2c4f21acf960&q=" 55 | }, 56 | { 57 | "name": "Bottle", 58 | "url": "https://api.projectoxford.ai/luis/v1/application?id=0a2cc164-5a19-47b7-b85e-41914d9037ba&subscription-key=d7b46a6c72bf46c1b67f2c4f21acf960&q=" 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /examples/scenarios/router.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "router", 3 | "type": "sequence", 4 | "steps": [ 5 | { 6 | "id": "scenarioIntent", 7 | "type": "score", 8 | "data": { 9 | "models": [ "dialog-router" ] 10 | }, 11 | "scenarios": [ 12 | { 13 | "condition": "scenarioIntent.intent == 'stomachPain'", 14 | "steps": [ { "subScenario": "stomachPain" } ] 15 | }, 16 | { 17 | "condition": "scenarioIntent.intent == 'botGames'", 18 | "steps": [ { "subScenario": "botGames" } ] 19 | } 20 | ] 21 | } 22 | ], 23 | "models": [ 24 | { 25 | "name": "dialog-router", 26 | "url": "https://api.projectoxford.ai/luis/v1/application?id=86e0ddab-7309-45e7-937a-ed92725004cf&subscription-key=d7b46a6c72bf46c1b67f2c4f21acf960&q=" 27 | }, 28 | { 29 | "name": "bot-common-responses", 30 | "url": "https://api.projectoxford.ai/luis/v1/application?id=7ff935f4-fe33-4a2a-b155-b82dbbf456ed&subscription-key=d7b46a6c72bf46c1b67f2c4f21acf960&q=" 31 | }, 32 | { 33 | "name": "Bottle", 34 | "url": "https://api.projectoxford.ai/luis/v1/application?id=0a2cc164-5a19-47b7-b85e-41914d9037ba&subscription-key=d7b46a6c72bf46c1b67f2c4f21acf960&q=" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /examples/scenarios/smoking.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "smoking", 3 | "type": "sequence", 4 | "steps": [ 5 | { 6 | "id": "isSmoking", 7 | "type": "prompt", 8 | "data": { "type": "confirm", "text": "Do you smoke?" }, 9 | "scenarios": [ 10 | { 11 | "condition": "isSmoking", 12 | "steps": [ 13 | { 14 | "id": "smokeTime", 15 | "type": "prompt", 16 | "data": { "type": "number", "text": "For how many years?" } 17 | } 18 | ] 19 | }, 20 | { 21 | "condition": "!isSmoking", 22 | "steps": [ 23 | { 24 | "id": "sureNotSmoking", 25 | "type": "prompt", 26 | "data": { "type":"confirm", "text": "Are you sure?" }, 27 | "scenarios": [ 28 | { 29 | "condition": "!sureNotSmoking", 30 | "nodeId": "isSmoking" 31 | } 32 | ] 33 | } 34 | ] 35 | }] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /examples/scenarios/stomachPain.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "stomachPain", 3 | "type": "sequence", 4 | "steps": [ 5 | { 6 | "id": "age", 7 | "type": "prompt", 8 | "data": { "type": "number", "text": "How old are you?" } 9 | }, 10 | { 11 | "id": "whenStart", 12 | "type": "prompt", 13 | "data": { "type": "time", "text": "When did it start?" } 14 | }, 15 | { 16 | "subScenario": "smoking" 17 | }, 18 | { 19 | "id": "workout", 20 | "type": "prompt", 21 | "data": { 22 | "type": "choice", 23 | "text": "What kind of workout do you do?", 24 | "options": [ "None", "Crossfit", "TRX", "Kung-Fu" ] 25 | }, 26 | "varname": "workout", 27 | "scenarios": [ 28 | { 29 | "condition": "workout == 'Crossfit'", 30 | "steps": [ 31 | { 32 | "id": "cfWeight", 33 | "type": "prompt", 34 | "data": { "type":"number", "text": "How much do you lift?" }, 35 | "scenarios": [ 36 | { 37 | "condition": "cfWeight <= 100", 38 | "steps": [ 39 | { 40 | "id": "cfWeightSmallReason", 41 | "type": "prompt", 42 | "data": { "text": "Why so lite?" } 43 | } 44 | ] 45 | }, 46 | { 47 | "condition": "cfWeight > 100", 48 | "steps": [ 49 | { 50 | "id": "cfCrazy", 51 | "type": "prompt", 52 | "data": { "type":"confirm", "text": "Are you crazy?" }, 53 | "scenarios": [ 54 | { 55 | "condition": "cfCrazy", 56 | "steps": [ 57 | { 58 | "id": "takePills", 59 | "type": "text", 60 | "data": {"text": "Please start taking your pills" } 61 | } 62 | ] 63 | }, 64 | { 65 | "condition": "!cfCrazy", 66 | "steps": [ 67 | { 68 | "id": "cfHowLong", 69 | "type": "prompt", 70 | "data": { "type":"number", "text": "How many years have you been working out?" } 71 | }, 72 | { 73 | "id": "cfInstructor", 74 | "type": "prompt", 75 | "data": { "text": "Who is your instructor?" } 76 | }, 77 | { 78 | "id": "cfWhere", 79 | "type": "prompt", 80 | "data": { "text": "Where do you work out?" } 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | ] 91 | }, 92 | { 93 | "condition": "workout == 'TRX'", 94 | "steps": [ 95 | { 96 | "id": "trxStrength", 97 | "type": "prompt", 98 | "data": { 99 | "type":"choice", 100 | "text": "What is the strength of the rope?", 101 | "options": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 102 | } 103 | } 104 | ] 105 | }, 106 | { 107 | "condition": "workout == 'Kung-Fu'", 108 | "steps": [ 109 | { 110 | "id": "kfYears", 111 | "type": "prompt", 112 | "data": { "type":"number", "text": "How many years have you neen practicing?" } 113 | } 114 | ] 115 | } 116 | ] 117 | }, 118 | { 119 | "type": "text", 120 | "data": { "text": "Please wait while we finalize the data..." } 121 | }, 122 | { 123 | "id": "finalize", 124 | "type": "handler", 125 | "data": { "name": "summary" } 126 | } 127 | ] 128 | } -------------------------------------------------------------------------------- /examples/scenarios/validationsAndCarousels.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "validationsAndCarousels", 3 | "type": "sequence", 4 | "steps": [ 5 | { 6 | "id": "myCarousel", 7 | "type": "carousel", 8 | "data": { 9 | "text": "Can you see a carousel of hero cards bellow?", 10 | "cards": [ 11 | { 12 | "id": "myOtherCard", 13 | "type": "heroCard", 14 | "data": { 15 | "title": "Space Needle", 16 | "text": "The Space Needle is an observation tower in Seattle, Washington, a landmark of the Pacific Northwest, and an icon of Seattle.", 17 | "images": [ 18 | "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7c/Seattlenighttimequeenanne.jpg/320px-Seattlenighttimequeenanne.jpg" 19 | ], 20 | "buttons": [ 21 | { 22 | "label": "Wikipedia", 23 | "action": "openUrl", 24 | "value": "https://en.wikipedia.org/wiki/Space_Needle" 25 | }, 26 | { 27 | "label": "Select", 28 | "action": "imBack", 29 | "value": "select:100" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "id": "myOtherOtherCard", 36 | "type": "heroCard", 37 | "data": { 38 | "title": "Pikes Place Market", 39 | "text": "Pike Place Market is a public market overlooking the Elliott Bay waterfront in Seattle, Washington, United States.", 40 | "images": [ 41 | "https://upload.wikimedia.org/wikipedia/en/thumb/2/2a/PikePlaceMarket.jpg/320px-PikePlaceMarket.jpg" 42 | ], 43 | "buttons": [ 44 | { 45 | "label": "Wikipedia", 46 | "action": "openUrl", 47 | "value": "https://en.wikipedia.org/wiki/Pike_Place_Market" 48 | }, 49 | { 50 | "label": "Select", 51 | "action": "imBack", 52 | "value": "select:101" 53 | } 54 | ] 55 | } 56 | } 57 | ] 58 | } 59 | }, 60 | { 61 | "id": "myCard", 62 | "type": "heroCard", 63 | "data": { 64 | "title": "Space Needle", 65 | "subtitle": "Our Subtitle", 66 | "text": "The Space Needle is an observation tower in Seattle, Washington, a landmark of the Pacific Northwest, and an icon of Seattle.", 67 | "images": [ 68 | "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7c/Seattlenighttimequeenanne.jpg/320px-Seattlenighttimequeenanne.jpg" 69 | ], 70 | "tap": { 71 | "action": "openUrl", 72 | "value": "https://en.wikipedia.org/wiki/Space_Needle" 73 | }, 74 | "buttons": [ 75 | { 76 | "label": "Wikipedia", 77 | "action": "openUrl", 78 | "value": "https://en.wikipedia.org/wiki/Space_Needle" 79 | } 80 | ] 81 | } 82 | }, 83 | { 84 | "type": "text", 85 | "data": { 86 | "text": "Lets start!" 87 | } 88 | }, 89 | { 90 | "id": "isTesting", 91 | "type": "prompt", 92 | "data": { 93 | "type": "text", 94 | "text": "What are you doing? (I'll validate using regex: ^test)", 95 | "validation": { 96 | "type": "regex", 97 | "setup": { 98 | "pattern": "^test" 99 | } 100 | } 101 | } 102 | }, 103 | { 104 | "id": "flightDate", 105 | "type": "prompt", 106 | "data": { 107 | "type": "time", 108 | "text": "When would you like to fly?", 109 | "validation": { 110 | "type": "date", 111 | "setup": { 112 | "min_date": "2016-11-15 00:00:00", 113 | "max_date": "2016-11-25 23:59:59", 114 | "invalid_msg": "Oops, wrong date!" 115 | } 116 | } 117 | } 118 | }, 119 | { 120 | "type": "text", 121 | "data": { 122 | "text": "All good :)" 123 | } 124 | } 125 | ] 126 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/GraphDialog'); 3 | -------------------------------------------------------------------------------- /lib/GraphDialog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require('util'); 4 | var builder = require('botbuilder'); 5 | var strformat = require('strformat'); 6 | var uuid = require('uuid'); 7 | 8 | var Parser = require('./Parser'); 9 | var Navigator = require('./Navigator'); 10 | var Node = require('./Node'); 11 | var IntentScorer = require('./IntentScorer'); 12 | var Common = require('./Common'); 13 | var Validator = require('./Validator'); 14 | 15 | // the GraphDialog class manages the dialog's state 16 | class GraphDialog { 17 | 18 | constructor(options) { 19 | this.options = options = options || {}; 20 | if (!options.bot) throw new Error('please provide the bot object'); 21 | 22 | // if set to true, will not travel to next step as the current step needs to be validated 23 | this.validateCurrentNode = false; 24 | 25 | this.parser = null; 26 | 27 | // initialize custom handlers 28 | options.customTypeHandlers = options.customTypeHandlers || []; 29 | this.internalPath = '/_' + uuid.v4(); 30 | 31 | this.setBotDialog(); 32 | 33 | this.customTypeHandlers = new Common.Map(); 34 | for (let handler of options.customTypeHandlers) { 35 | this.customTypeHandlers.add(handler.name, handler); 36 | } 37 | } 38 | 39 | getDialogVersion() { 40 | return this.parser ? this.parser.version : null; 41 | } 42 | 43 | getDialogId() { 44 | return this.parser ? this.parser.root.id : null; 45 | } 46 | 47 | // initialize a graph based on graph options like a predefined JSON schema 48 | async init() { 49 | this.nav = this.options.navigator; 50 | this.proxyMode = this.options.proxyMode; 51 | if (!this.nav) { 52 | try { 53 | this.parser = new Parser(this.options); 54 | await this.parser.init(); 55 | console.log('parser is ready'); 56 | this.nav = new Navigator(this.parser); 57 | } 58 | catch(err) { 59 | throw new Error(`error initializing parser. options: ${util.inspect(this.options)}, error: ${err.message}`); 60 | } 61 | } 62 | 63 | return this; 64 | } 65 | 66 | // generate a new graph dialog constructed based on a scenario name 67 | static async create(options = {}) { 68 | var graphDialog = new GraphDialog(options); 69 | return await graphDialog.init(); 70 | } 71 | 72 | // reload instance, for example in case that it was updated in the backend 73 | async reload() { 74 | return this.init(); 75 | } 76 | 77 | // restart dialog 78 | restartDialog(session) { 79 | console.log('calling loop function after restarting dialog'); 80 | 81 | session.privateConversationData = {}; 82 | var dialogIndex = -1; 83 | var callstack = session.sessionState.callstack || []; 84 | 85 | for (var i = callstack.length - 1; i >= 0; i--) { 86 | var item = callstack[i]; 87 | var path = item.id.split('*:')[1]; 88 | if (path === this.internalPath) { 89 | dialogIndex = i; 90 | break; 91 | } 92 | }; 93 | 94 | session.cancelDialog(dialogIndex, this.internalPath); 95 | } 96 | 97 | // returns the dialog steps to bind to the bot object 98 | getDialog() { 99 | console.log('get dialog'); 100 | return (session, results, next) => { 101 | console.log('calling loop function for the first time'); 102 | session.beginDialog(this.internalPath); 103 | }; 104 | } 105 | 106 | // this is where the magic happens. loops this list of steps for each node. 107 | setBotDialog() { 108 | var _this = this; 109 | this.options.bot.dialog(this.internalPath, [ 110 | async (session, args, next) => { 111 | session.dialogData.data = args || {}; 112 | if (typeof _this.options.onBeforeProcessingStep === 'function') 113 | return await _this.options.onBeforeProcessingStep.call(_this, session, args, next); 114 | else 115 | return next(); 116 | }, 117 | async (session, args, next) => { 118 | return await _this.stepInteractionHandler(session, args, next); 119 | }, 120 | async (session, args, next) => { 121 | return await _this.stepResultCollectionHandler(session, args, next); 122 | }, 123 | async (session, args, next) => { 124 | return await _this.setNextStepHandler(session, args, next); 125 | }, 126 | async (session, args, next) => { 127 | if (typeof _this.options.onAfterProcessingStep === 'function') 128 | return await _this.options.onAfterProcessingStep.call(_this, session, args, next); 129 | else 130 | return next(); 131 | }, 132 | (session, args, next) => { 133 | console.log('calling loop function'); 134 | session.replaceDialog(_this.internalPath, session.dialogData.data); 135 | } 136 | ]); 137 | } 138 | 139 | // this is where the bot interacts with the user 140 | async stepInteractionHandler(session, results, next) { 141 | session.privateConversationData._lastMessage = session.message && session.message.text; 142 | var currentNode = await this.normalizeNode(await this.nav.getCurrentNode(session)); 143 | console.log("perform action: " + currentNode.id + ", " + currentNode.type); 144 | 145 | switch (currentNode.type) { 146 | 147 | case Node.NodeType.text: 148 | session.send(this.generateTextMessage(session, currentNode)); 149 | return next(); 150 | 151 | case Node.NodeType.prompt: 152 | console.log("builder.ListStyle.button: " + builder.ListStyle["button"]); 153 | var promptType = currentNode.data.type || 'text'; 154 | currentNode.data.options = currentNode.data.options || {}; 155 | 156 | var listStyle = (currentNode.data.config 157 | && currentNode.data.config.listStyle 158 | && builder.ListStyle[currentNode.data.config.listStyle]) 159 | || builder.ListStyle.button; 160 | 161 | if (promptType === 'confirm' && !currentNode.data.options.listStyle) { 162 | currentNode.data.options.listStyle = listStyle; 163 | } 164 | 165 | builder.Prompts[promptType](session, currentNode.data.text, currentNode.data.options, { listStyle }); 166 | break; 167 | 168 | case Node.NodeType.score: 169 | var botModels = currentNode.data.models.map(model => this.nav.models.get(model)); 170 | var score_text = session.dialogData.data[currentNode.data.source] || session.privateConversationData._lastMessage; 171 | console.log("LUIS scoring for node: " + currentNode.id + ", text: '" + score_text + "' LUIS models: " + util.inspect(botModels)); 172 | var intents = await IntentScorer.collectIntents(botModels, score_text, currentNode.data.threashold); 173 | if (intents && intents.length) { 174 | this.stepResultCollectionHandler(session, { response: intents[0] }, next); 175 | } 176 | break; 177 | 178 | case Node.NodeType.handler: 179 | var handlerName = currentNode.data.name; 180 | var handler = this.nav.handlers.get(handlerName); 181 | console.log('calling handler: ', currentNode.id, handlerName); 182 | handler(session, next, currentNode.data); 183 | break; 184 | 185 | case Node.NodeType.sequence: 186 | return next(); 187 | 188 | case Node.NodeType.end: 189 | console.log('ending dialog, node:', currentNode.id); 190 | session.send(currentNode.data.text || 'Bye bye!'); 191 | session.endConversation(); 192 | break; 193 | 194 | case Node.NodeType.heroCard: 195 | session.send(this.generateHeroCardMessage(session, currentNode)); 196 | return next(); 197 | 198 | case Node.NodeType.carousel: 199 | session.send(this.generateCarouselMessage(session, currentNode, builder.AttachmentLayout.carousel)); 200 | return next(); 201 | 202 | case Node.NodeType.list: 203 | session.send(this.generateCarouselMessage(session, currentNode, builder.AttachmentLayout.list)); 204 | return next(); 205 | 206 | case Node.NodeType.adaptive: 207 | session.send(this.generateAdaptiveCardMessage(session, currentNode)); 208 | return next(); 209 | 210 | default: 211 | var customHandler = this.customTypeHandlers.get(currentNode.typeName); 212 | 213 | if (customHandler) { 214 | console.log("invoking custom node type handler: " + currentNode.typeName); 215 | return customHandler.execute(session, next, currentNode.data); 216 | } 217 | 218 | var msg = 'Node type ' + currentNode.type + ' is not recognized'; 219 | console.error(msg); 220 | throw new Error(msg); 221 | 222 | } 223 | } 224 | 225 | // generates a HeroCard (to be attached to a Message) 226 | generateHeroCard(session, data) { 227 | var hero = new builder.HeroCard(session); 228 | 229 | if (data.title) hero.title(data.title); 230 | if (data.subtitle) hero.subtitle(data.subtitle); 231 | if (data.text) hero.text(data.text); 232 | 233 | if (data.images && data.images.length > 0) { 234 | hero.images([ 235 | builder.CardImage.create(session, data.images[0]) 236 | ]); 237 | } 238 | 239 | if (data.tap) { 240 | switch (data.tap.action) { 241 | case "openUrl": 242 | hero.tap(builder.CardAction.openUrl(session, data.tap.value)); 243 | break; 244 | } 245 | } 246 | 247 | if (data.buttons) { 248 | var buttons = this.addButtonsByType(session, data.buttons); 249 | if (buttons.length > 0) { 250 | hero.buttons(buttons); 251 | } 252 | } 253 | return hero; 254 | } 255 | 256 | // generate text message with suggested actions 257 | generateTextMessage(session, node) { 258 | 259 | var text = strformat(node.data.text, session.dialogData.data); 260 | var msg = new builder.Message(session).text(text); 261 | 262 | if (node.data.buttons) { 263 | var buttons = this.addButtonsByType(session, node.data.buttons); 264 | if (buttons.length > 0) { 265 | var suggestedActions = builder.SuggestedActions.create( 266 | session, 267 | buttons); 268 | msg.suggestedActions(suggestedActions); 269 | } 270 | } 271 | 272 | console.log("text message node " + node.id + ", msg: '" + msg + "'"); 273 | return msg; 274 | } 275 | 276 | // generates a HeroCard Message 277 | generateHeroCardMessage(session, node) { 278 | var hero = this.generateHeroCard(session, node.data); 279 | return new builder.Message(session) 280 | .textFormat(builder.TextFormat.xml) 281 | .attachments([hero]); 282 | }; 283 | 284 | // generates a Carousel Message 285 | generateCarouselMessage(session, node, attachmentLayout) { 286 | //var _this = this; 287 | var data = node.data; 288 | 289 | if (data.text) { 290 | var text = strformat(data.text, session.dialogData.data); 291 | session.send(text); 292 | } 293 | 294 | if (data.cards && data.cards.length > 0) { 295 | var cards = []; 296 | for (let item of data.cards) { 297 | cards.push(this.generateHeroCard(session, item.data)); 298 | } 299 | 300 | return new builder.Message(session) 301 | .textFormat(builder.TextFormat.xml) 302 | .attachmentLayout(attachmentLayout) 303 | .attachments(cards); 304 | } 305 | } 306 | 307 | generateAdaptiveCardMessage(session, node) { 308 | if (node.data.text) { 309 | var text = strformat(data.text, session.dialogData.data); 310 | session.send(text); 311 | } 312 | 313 | return new builder.Message(session) 314 | .addAttachment({ 315 | contentType: "application/vnd.microsoft.card.adaptive", 316 | content: node.data.content 317 | }); 318 | 319 | } 320 | 321 | addButtonsByType(session, inputButtons) { 322 | var buttons = []; 323 | for (let item of inputButtons) { 324 | switch (item.action) { 325 | case "openUrl": 326 | buttons.push(builder.CardAction.openUrl(session, item.value, item.label)); 327 | break; 328 | case "imBack": 329 | buttons.push(builder.CardAction.imBack(session, item.value, item.label)); 330 | break; 331 | case "postBack": 332 | buttons.push(builder.CardAction.postBack(session, item.value, item.label)); 333 | break; 334 | } 335 | } 336 | return buttons; 337 | } 338 | 339 | // handling collection of the user input 340 | async stepResultCollectionHandler(session, results, next) { 341 | 342 | if (this.proxyMode) { 343 | return next(); 344 | } 345 | 346 | delete session.privateConversationData._lastResolvedResult; 347 | var currentNode = await this.normalizeNode(await this.nav.getCurrentNode(session)); 348 | var varname = currentNode.varname; 349 | 350 | if (!(results.response && varname)) 351 | return next(); 352 | 353 | if (currentNode.data.validation && currentNode.data.validation.type) { 354 | var invalidMsg = currentNode.data.validation.setup.invalid_msg ? currentNode.data.validation.setup.invalid_msg : 'Invalid value'; 355 | var isValid = Validator.validate(currentNode.data.validation.type, results.response, currentNode.data.validation.setup); 356 | if (!isValid) { 357 | session.send(invalidMsg); 358 | this.validateCurrentNode = true; 359 | return next(); 360 | } 361 | } 362 | 363 | var value = null; 364 | switch (currentNode.type) { 365 | case Node.NodeType.prompt: 366 | switch (currentNode.data.type) { 367 | case 'time': 368 | value = builder.EntityRecognizer.resolveTime([results.response]); 369 | break; 370 | case 'choice': 371 | value = results.response.entity; 372 | break; 373 | default: 374 | value = results.response; 375 | } 376 | break; 377 | default: 378 | value = results.response; 379 | } 380 | 381 | session.dialogData.data[varname] = value; 382 | session.privateConversationData._lastResolvedResult = value; 383 | 384 | console.log('collecting response for node: %s, variable: %s, value: %s', currentNode.id, varname, value); 385 | return next(); 386 | }; 387 | 388 | // resolves next node on the graph 389 | async setNextStepHandler(session, args, next) { 390 | var nextNode = null; 391 | 392 | var currentNode = await this.normalizeNode(await this.nav.getCurrentNode(session)); 393 | 394 | if (currentNode.stop) { 395 | return session.endConversation(); 396 | } 397 | 398 | if (this.validateCurrentNode) { 399 | nextNode = currentNode; 400 | this.validateCurrentNode = false; 401 | } 402 | else { 403 | nextNode = await this.normalizeNode(await this.nav.getNextNode(session)); 404 | } 405 | 406 | if (nextNode) { 407 | console.log("step handler node: " + nextNode.id); 408 | } 409 | else { 410 | console.log('ending dialog'); 411 | return session.endConversation(); 412 | } 413 | 414 | return next(); 415 | } 416 | 417 | async normalizeNode(node) { 418 | 419 | if (!node) return; 420 | 421 | if (node._customType_) { 422 | return node; 423 | } 424 | 425 | var parser = new Parser({ graph: node }); 426 | await parser.init(); 427 | return parser.root 428 | } 429 | 430 | } 431 | 432 | module.exports = GraphDialog; 433 | -------------------------------------------------------------------------------- /lib/Luis.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // wrapper class for a LuisModel details 4 | exports.LuisModel = class { 5 | constructor(name, url) { 6 | if (!name) throw new Error(`param 'name' was not provided`); 7 | if (!url) throw new Error(`param 'url' was not provided`); 8 | 9 | this.name = name; 10 | this.url = url; 11 | } 12 | } 13 | 14 | // wrapper class for an intent score 15 | exports.IntentScore = class { 16 | constructor(name, model, score) { 17 | if (!name) throw new Error(`param 'name' was not provided`); 18 | if (!model) throw new Error(`param 'model' was not provided`); 19 | if (!score) throw new Error(`param 'score' was not provided`); 20 | 21 | this.name = name; 22 | this.model = model; 23 | this.score = score; 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /lib/Navigator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ConditionHandler = require('./ConditionHandler'); 4 | 5 | // helper class to navigate the dialog graph 6 | class Navigator { 7 | 8 | constructor(parser, options = {}) { 9 | this.parser = parser; 10 | this.options = options; 11 | this.models = parser.models; 12 | this.handlers = parser.handlers; 13 | } 14 | 15 | // returns the current node of the dialog 16 | getCurrentNode(session) { 17 | console.log('getCurrentNode'); 18 | var currNodeId = session.privateConversationData._currentNodeId; 19 | if (!currNodeId || !this.parser.getNodeInstanceById(currNodeId)) { 20 | var root = this.parser.root; 21 | session.privateConversationData._currentNodeId = root && root.id; 22 | return root; 23 | } 24 | var current = this.parser.getNodeInstanceById(currNodeId); 25 | return current; 26 | }; 27 | 28 | // resolves the next node in the dialog 29 | getNextNode(session) { 30 | console.log('getNextNode'); 31 | 32 | var next = null; 33 | var current = this.parser.getNodeInstanceById(session.privateConversationData._currentNodeId); 34 | 35 | // if there are child scenarios, see if one of them answers a condition. 36 | // in case it is, choose the first step in that scenario to as the next step. 37 | var scenarios = current.scenarios; 38 | for (var i = 0; i < current.scenarios.size(); i++) { 39 | var scenario = current.scenarios.get(i); 40 | if (ConditionHandler.evaluateExpression(session.dialogData.data, scenario.condition)) { 41 | next = scenario.node || scenario.steps.get(0); 42 | } 43 | } 44 | 45 | // if no next yet, get the first step 46 | next = next || current.steps.get(0); 47 | 48 | // if no next yet, travel the graph, look for next at parents... 49 | // if there is no selected scenario, move to the next node. 50 | // if there is no next node, look recursively for next on parent nodes. 51 | var nodeNavigator = current; 52 | while (!next && nodeNavigator) { 53 | next = nodeNavigator.next; 54 | nodeNavigator = nodeNavigator.parent; 55 | } 56 | 57 | console.log("getNextNode: [current: " + current.id + ", next: " + (next && next.id) + "]"); 58 | session.privateConversationData._currentNodeId = next && next.id; 59 | return next; 60 | }; 61 | } 62 | 63 | module.exports = Navigator; 64 | -------------------------------------------------------------------------------- /lib/Node.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Common = require('./Common'); 4 | 5 | // the Node class representing a node in the dialog graph 6 | class Node { 7 | 8 | constructor(node, type) { 9 | if (!node.id) throw new Error(`node does not have an 'id'`); 10 | this.id = node.id; 11 | 12 | // resolve node type 13 | if (typeof type === 'string') { 14 | this.type = NodeType[type]; 15 | this.typeName = type; 16 | } 17 | else 18 | this.type = type; 19 | 20 | this.varname = node.varname || this.id; 21 | this.steps = new Common.List(); 22 | this.scenarios = new Common.List(); 23 | this.body = node; 24 | this.data = node.data; 25 | this.stop = node.stop; 26 | 27 | this._customType_ = true; 28 | } 29 | 30 | } 31 | 32 | // types of nodes currently supported natively 33 | var NodeType = {}; 34 | (function (NodeType) { 35 | NodeType[NodeType["text"] = 0] = "text"; 36 | NodeType[NodeType["prompt"] = 1] = "prompt"; 37 | NodeType[NodeType["score"] = 2] = "score"; 38 | NodeType[NodeType["handler"] = 3] = "handler"; 39 | NodeType[NodeType["sequence"] = 4] = "sequence"; 40 | NodeType[NodeType["end"] = 5] = "end"; 41 | NodeType[NodeType["heroCard"] = 6] = "heroCard"; 42 | NodeType[NodeType["carousel"] = 7] = "carousel"; 43 | NodeType[NodeType["list"] = 8] = "list"; 44 | NodeType[NodeType["adaptive"] = 9] = "adaptive"; 45 | })(NodeType); 46 | 47 | Node.NodeType = NodeType; 48 | 49 | module.exports = Node; 50 | -------------------------------------------------------------------------------- /lib/Parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require('util'); 4 | var Node = require('./Node'); 5 | var Scenario = require('./Scenario'); 6 | var Luis = require('./Luis'); 7 | var Common = require('./Common'); 8 | var extend = require('extend'); 9 | var crypto = require('crypto'); 10 | 11 | // parses a json based scenario 12 | class Parser { 13 | 14 | constructor (options) { 15 | this.options = options; 16 | this.uniqueNodeId = 1; 17 | this.root = null; 18 | this.version = null; 19 | this.nodes = {}; 20 | this.models = new Common.Map(); 21 | this.handlers = new Common.Map(); 22 | } 23 | 24 | // loads and parses a scenario 25 | async init() { 26 | if (!this.options.graph) { 27 | try { 28 | var graph = await this.options.loadScenario(this.options.scenario); 29 | return await this.normalizeGraph(graph); 30 | } 31 | catch(err) { 32 | console.error(`error loading scenario. options: ${util.inspect(this.options)}, error: ${util.inspect(err)}`); 33 | throw new Error(`Error loading scenario '${this.options.scenario}': ${err.message}`); 34 | } 35 | } 36 | 37 | // graph was provided 38 | return await this.normalizeGraph(this.options.graph); 39 | } 40 | 41 | // gets a node instance by its Id 42 | getNodeInstanceById(id) { 43 | var node = this.nodes[id]; 44 | return node && node._instance; 45 | } 46 | 47 | // normalize the graph 48 | async normalizeGraph(origGraph) { 49 | 50 | try { 51 | 52 | // create a copy of the graph object 53 | var graph = {}; 54 | extend(true, graph, origGraph); 55 | 56 | console.log('loading scenario:', graph.id); 57 | this.updateModels(graph.models); 58 | 59 | await this.recursive(graph); 60 | 61 | // first iteration- create Node instances 62 | var nodes = this.nodes; 63 | for (var nodeId in nodes) { 64 | var node = nodes[nodeId]; 65 | var inst = new Node(node, node.type); 66 | node._instance = inst; 67 | } 68 | 69 | // second iteration- connect reference to Node instances 70 | for (var nodeId in nodes) { 71 | 72 | var node = nodes[nodeId]; 73 | var inst = node._instance; 74 | if (node._parent) inst.parent = node._parent._instance; 75 | if (node._prev) inst.prev = node._prev._instance; 76 | if (node._next) inst.next = node._next._instance; 77 | 78 | for (let step of node.steps || []) { 79 | inst.steps.add(step._instance); 80 | } 81 | 82 | for (let scenario of node.scenarios || []) { 83 | var scene = new Scenario( scenario.condition, 84 | scenario.nodeId ? this.nodes[scenario.nodeId]._instance : null); 85 | 86 | for (let step of scenario.steps || []) { 87 | scene.steps.add(step._instance); 88 | }; 89 | 90 | inst.scenarios.add(scene); 91 | } 92 | } 93 | 94 | // third iteration- remove unneccessary data/references 95 | for (var nodeId in nodes) { 96 | var node = nodes[nodeId]; 97 | var inst = node._instance; 98 | delete node._visited; 99 | delete node._parent; 100 | delete node._prev; 101 | delete node._next; 102 | } 103 | 104 | this.root = graph._instance; 105 | this.version = graph.version || this.calculateHash(JSON.stringify(origGraph)); 106 | } 107 | catch(err) { 108 | console.error(`error normalizing graph: ${util.inspect(origGraph)}, error: ${util.inspect(err)}`); 109 | throw new Error(`Error normalizing graph '${util.inspect(origGraph)}': ${err.message}`); 110 | } 111 | } 112 | 113 | // initialize a node in the graph 114 | async initNode(parent, nodes, nodeItem, index) { 115 | try { 116 | 117 | if (nodeItem._visited) return; 118 | 119 | nodeItem._visited = true; 120 | nodeItem.id = nodeItem.id || `_node_${this.uniqueNodeId++}`; 121 | 122 | if (parent) nodeItem._parent = parent; 123 | if (index > 0) nodeItem._prev = nodes[index - 1]; 124 | if (nodes.length > index + 1) nodeItem._next = nodes[index + 1]; 125 | 126 | if (this.isSubScenario(nodeItem)) { 127 | console.log("sub-scenario for node: " + nodeItem.id + " [embedding sub scenario: " + nodeItem.subScenario + "]"); 128 | var scenarioObj = await this.options.loadScenario(nodeItem.subScenario); 129 | extend(true, nodeItem, scenarioObj); 130 | this.updateModels(scenarioObj.models); 131 | console.log('node:', nodeItem.id, nodeItem._parent && nodeItem._parent.id ? '[parent: ' + nodeItem._parent.id + ']' : '', nodeItem._next && nodeItem._next.id ? '[next: ' + nodeItem._next.id + ']' : '', nodeItem._prev && nodeItem._prev.id ? '[prev: ' + nodeItem._prev.id + ']' : ''); 132 | return this.recursive(nodeItem); 133 | } 134 | 135 | if (nodeItem.type === 'handler') { 136 | var handler = nodeItem.data.name || ''; 137 | console.log("loading handler for node: " + nodeItem.id + " [embedding sub scenario: " + handler + "]"); 138 | 139 | var jsCode = null; 140 | if (nodeItem.data.js) { 141 | var content = nodeItem.data.js; 142 | 143 | if (Array.isArray(content)) 144 | jsCode = content.join('\n'); 145 | } 146 | else { 147 | try { 148 | jsCode = await this.options.loadHandler(handler); 149 | } 150 | catch(err) { 151 | console.error(`error loading handler: ${handler}: error: ${err.message}`); 152 | } 153 | } 154 | 155 | var func = this.getHandlerFunc(jsCode); 156 | if (!func) { 157 | console.error(`error loading handler ${handler}: js code: ${jsCode}`); 158 | throw new Error(`error loading handler ${handler}: js code: ${jsCode}`); 159 | } 160 | 161 | this.handlers.add(handler, func); 162 | } 163 | 164 | console.log('node:', nodeItem.id, nodeItem._parent && nodeItem._parent.id ? '[parent: ' + nodeItem._parent.id + ']' : '', nodeItem._next && nodeItem._next.id ? '[next: ' + nodeItem._next.id + ']' : '', nodeItem._prev && nodeItem._prev.id ? '[prev: ' + nodeItem._prev.id + ']' : ''); 165 | await this.recursive(nodeItem); 166 | 167 | } 168 | catch(err) { 169 | console.error(`error initNode: ${util.inspect(arguments)}, error: ${util.inspect(err)}`); 170 | throw new Error(`Error initNode'${util.inspect(node)}': ${err.message}`); 171 | } 172 | } 173 | 174 | // initialize a collecton of nodes 175 | async initNodes(parent, nodes = []) { 176 | for (var i=0; i await this.initNodes(node, scenario.steps))); 191 | // it keeps the order of the calls such that it waits for a call the end before invoking the next one. 192 | // this is what we want to do, since the order is important. 193 | for (let scenario of node.scenarios || []) { 194 | await this.initNodes(node, scenario.steps); 195 | } 196 | 197 | this.nodes[node.id] = node; 198 | } 199 | 200 | // checks if this is a sub-scenario node 201 | isSubScenario(nodeItem) { 202 | if (!nodeItem.subScenario) return false; 203 | 204 | var parent = nodeItem._parent; 205 | while (parent) { 206 | if (nodeItem.subScenario === parent.id) { 207 | console.error(`recursive subScenario found: ${nodeItem.subScenario}`); 208 | throw new Error(`recursive subScenario found: ${nodeItem.subScenario}`); 209 | } 210 | parent = parent._parent; 211 | } 212 | 213 | return true; 214 | } 215 | 216 | // gets a handler function from a string 217 | getHandlerFunc(funcText) { 218 | var text = `(() => { 219 | return module => { 220 | ${funcText} 221 | } 222 | })()`; 223 | var wrapperFunc = eval(text); 224 | var m = {}; 225 | wrapperFunc(m); 226 | return typeof m.exports === 'function' ? m.exports : null; 227 | } 228 | 229 | // updates the internal LUIS models collection 230 | updateModels(models = []) { 231 | for (let model of models) { 232 | this.models.add(model.name, new Luis.LuisModel(model.name, model.url)); 233 | } 234 | } 235 | 236 | // calculates hash of an input text 237 | calculateHash(text) { 238 | return crypto.createHash('md5').update(text).digest('hex'); 239 | } 240 | 241 | } 242 | 243 | module.exports = Parser; 244 | -------------------------------------------------------------------------------- /lib/Scenario.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Common = require('./Common'); 4 | 5 | // a scenario class 6 | class Scenario { 7 | 8 | constructor(condition, node) { 9 | 10 | if (!condition) throw new Error(`param 'condition' was not provided`); 11 | // node is optional, no need to validate 12 | 13 | this.condition = condition; 14 | this.node = node; 15 | this.steps = new Common.List(); 16 | } 17 | 18 | } 19 | 20 | module.exports = Scenario; 21 | -------------------------------------------------------------------------------- /lib/Validator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class Validator { 4 | 5 | constructor() { 6 | } 7 | 8 | // validates a value against given type rule 9 | static validate(type, value, configuration) { 10 | switch (type) { 11 | case 'date': 12 | return Validator.validateDate(value, configuration); 13 | case 'regex': 14 | return Validator.validateRegex(value, configuration); 15 | default: 16 | return false; 17 | } 18 | } 19 | 20 | // validates a date 21 | static validateDate(value, configuration) { 22 | var date = value.resolution.start.getTime(); 23 | 24 | if (configuration.min_date) { 25 | var dateMin = new Date(configuration.min_date).getTime(); 26 | if (date < dateMin) return false; 27 | } 28 | 29 | if (configuration.max_date) { 30 | var dateMax = new Date(configuration.max_date).getTime(); 31 | if (date > dateMax) return false; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | static validateRegex(value, configuration) { 38 | var regex = new RegExp(configuration.pattern); 39 | var isValid = regex.test(value); 40 | return isValid; 41 | } 42 | 43 | } 44 | 45 | module.exports = Validator; 46 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.List = class { 4 | 5 | constructor() { 6 | this.items = []; 7 | } 8 | 9 | size() { 10 | return this.items.length; 11 | }; 12 | 13 | add(value) { 14 | this.items.push(value); 15 | }; 16 | 17 | get(index) { 18 | return index < this.size() ? this.items[index] : null; 19 | }; 20 | } 21 | 22 | exports.Map = class { 23 | 24 | constructor() { 25 | this.items = {}; 26 | } 27 | 28 | add(key, value) { 29 | this.items[key] = value; 30 | }; 31 | 32 | has(key) { 33 | return key in this.items; 34 | }; 35 | 36 | get(key) { 37 | return this.items[key]; 38 | }; 39 | 40 | keys() { 41 | return Object.keys(this.items); 42 | }; 43 | 44 | values() { 45 | var items = this.items; 46 | return Object.keys(this.items).map(key => items[key]); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /lib/conditionHandler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var jsep = require('jsep'); 3 | 4 | /** 5 | * Parsing and calculating conditional expressions from strings 6 | * i.e.: age >= 30 & (age * 2) < 40 7 | */ 8 | class ConditionHandler { 9 | 10 | /** 11 | * Recursively perform an evaluation of an expression 12 | * @param {any} object 13 | * @param {string|jsep.IExpression} expression 14 | * @returns any 15 | */ 16 | static evaluateExpression(object, expression) { 17 | var exp = typeof expression === 'string' ? jsep(expression) : expression; 18 | 19 | switch (exp.type) { 20 | case 'BinaryExpression': 21 | var bexp = exp; 22 | var value1 = this.evaluateExpression(object, bexp.left); 23 | var value2 = this.evaluateExpression(object, bexp.right); 24 | return this.calculateExpression(bexp.operator, value1, value2); 25 | 26 | case 'UnaryExpression': 27 | var uexp = exp; 28 | var value = this.evaluateExpression(object, uexp.argument); 29 | return this.calculateExpression(uexp.operator, value); 30 | 31 | case 'Identifier': 32 | return object[exp.name]; 33 | 34 | case 'MemberExpression': 35 | var mexp = exp; 36 | var parent = this.evaluateExpression(object, mexp.object); 37 | return this.evaluateExpression(parent, mexp.property); 38 | 39 | case 'Literal': 40 | return exp.value; 41 | 42 | default: 43 | throw new Error('condition type ' + exp.type + ' is not recognized'); 44 | } 45 | } 46 | 47 | /** 48 | * Calculate an expression accoring to the operator 49 | * @param {any} operator 50 | * @param {any} value1 51 | * @param {any=null} value2 52 | * @returns any 53 | */ 54 | static calculateExpression(operator, value1, value2) { 55 | switch (operator) { 56 | case '!': 57 | return !value1; 58 | case '<': 59 | return value1 < value2; 60 | case '>': 61 | return value1 > value2; 62 | case '<=': 63 | return value1 <= value2; 64 | case '>=': 65 | return value1 >= value2; 66 | case '=': 67 | case '==': 68 | return value1 == value2; 69 | case '===': 70 | return value1 === value2; 71 | case '!=': 72 | case '<>': 73 | return value1 != value2; 74 | case '!==': 75 | return value1 !== value2; 76 | case '-': 77 | return value1 - value2; 78 | case '+': 79 | return value1 + value2; 80 | case '*': 81 | return value1 * value2; 82 | case '/': 83 | return value1 / value2; 84 | case '%': 85 | return value1 % value2; 86 | default: 87 | break; 88 | } 89 | } 90 | } 91 | 92 | module.exports = ConditionHandler; 93 | -------------------------------------------------------------------------------- /lib/intentScorer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var request = require('request-promise'); 4 | var _ = require('underscore'); 5 | 6 | // score intents from a single or multiple intent scoring APIs 7 | class IntentScorer { 8 | 9 | constructor() { 10 | } 11 | 12 | // collect response from all models 13 | static async collectIntents(models, text, threashold = 0) { 14 | if (!models) throw new Error('Please provide models array'); 15 | if (!text) throw new Error('Please provide text'); 16 | 17 | var intents = await Promise.all(models.map(async model => await IntentScorer.scoreIntent(model, text, threashold))); 18 | var sortedIntents = _.sortBy(_.compact(intents), 'score').reverse(); 19 | return sortedIntents; 20 | } 21 | 22 | // scores a specific intent, invoke actual request to LUIS 23 | static async scoreIntent(model, text, threashold = 0) { 24 | var url = model.url + encodeURIComponent(text); 25 | try { 26 | var json = await request(url, { json: true }); 27 | } 28 | catch(err) { 29 | var msg = `error calling LUIS: url: ${url}, error: ${err.message}`; 30 | console.error(msg); 31 | throw new Error(msg); 32 | } 33 | 34 | if (!json || !json.intents || !json.intents.length) 35 | return; 36 | 37 | if (json.intents[0].score < threashold) 38 | return; 39 | 40 | var intent = json.intents[0]; 41 | intent.entities = json.entities; 42 | intent.model = model.name; 43 | return intent; 44 | } 45 | } 46 | 47 | module.exports = IntentScorer; 48 | 49 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-graph-dialog", 3 | "version": "3.4.5", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ajv": { 8 | "version": "4.11.8", 9 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", 10 | "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", 11 | "requires": { 12 | "co": "4.6.0", 13 | "json-stable-stringify": "1.0.1" 14 | } 15 | }, 16 | "asap": { 17 | "version": "2.0.6", 18 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 19 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 20 | }, 21 | "asn1": { 22 | "version": "0.2.3", 23 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", 24 | "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" 25 | }, 26 | "assert-plus": { 27 | "version": "0.2.0", 28 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", 29 | "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" 30 | }, 31 | "async": { 32 | "version": "1.5.2", 33 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", 34 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" 35 | }, 36 | "asynckit": { 37 | "version": "0.4.0", 38 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 39 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 40 | }, 41 | "aws-sign2": { 42 | "version": "0.6.0", 43 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", 44 | "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" 45 | }, 46 | "aws4": { 47 | "version": "1.6.0", 48 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", 49 | "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" 50 | }, 51 | "base64url": { 52 | "version": "2.0.0", 53 | "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", 54 | "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" 55 | }, 56 | "bcrypt-pbkdf": { 57 | "version": "1.0.1", 58 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", 59 | "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", 60 | "optional": true, 61 | "requires": { 62 | "tweetnacl": "0.14.5" 63 | } 64 | }, 65 | "bluebird": { 66 | "version": "3.5.0", 67 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", 68 | "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" 69 | }, 70 | "boom": { 71 | "version": "2.10.1", 72 | "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", 73 | "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", 74 | "requires": { 75 | "hoek": "2.16.3" 76 | } 77 | }, 78 | "botbuilder": { 79 | "version": "3.8.4", 80 | "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-3.8.4.tgz", 81 | "integrity": "sha1-/O3EK5J+zwUMJL5SDBKAgkQ3pwg=", 82 | "requires": { 83 | "async": "1.5.2", 84 | "base64url": "2.0.0", 85 | "chrono-node": "1.3.4", 86 | "jsonwebtoken": "7.4.1", 87 | "promise": "7.3.1", 88 | "request": "2.81.0", 89 | "rsa-pem-from-mod-exp": "0.8.4", 90 | "sprintf-js": "1.1.1", 91 | "url-join": "1.1.0" 92 | } 93 | }, 94 | "buffer-equal-constant-time": { 95 | "version": "1.0.1", 96 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 97 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 98 | }, 99 | "caseless": { 100 | "version": "0.12.0", 101 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 102 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 103 | }, 104 | "chrono-node": { 105 | "version": "1.3.4", 106 | "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-1.3.4.tgz", 107 | "integrity": "sha1-/CqSCGNuCdb9exLZSuJECTfeJL0=", 108 | "requires": { 109 | "moment": "2.18.1" 110 | } 111 | }, 112 | "co": { 113 | "version": "4.6.0", 114 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 115 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 116 | }, 117 | "combined-stream": { 118 | "version": "1.0.5", 119 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", 120 | "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", 121 | "requires": { 122 | "delayed-stream": "1.0.0" 123 | } 124 | }, 125 | "cryptiles": { 126 | "version": "2.0.5", 127 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", 128 | "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", 129 | "requires": { 130 | "boom": "2.10.1" 131 | } 132 | }, 133 | "dashdash": { 134 | "version": "1.14.1", 135 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 136 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 137 | "requires": { 138 | "assert-plus": "1.0.0" 139 | }, 140 | "dependencies": { 141 | "assert-plus": { 142 | "version": "1.0.0", 143 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 144 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 145 | } 146 | } 147 | }, 148 | "delayed-stream": { 149 | "version": "1.0.0", 150 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 151 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 152 | }, 153 | "ecc-jsbn": { 154 | "version": "0.1.1", 155 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", 156 | "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", 157 | "optional": true, 158 | "requires": { 159 | "jsbn": "0.1.1" 160 | } 161 | }, 162 | "ecdsa-sig-formatter": { 163 | "version": "1.0.9", 164 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", 165 | "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", 166 | "requires": { 167 | "base64url": "2.0.0", 168 | "safe-buffer": "5.1.1" 169 | } 170 | }, 171 | "extend": { 172 | "version": "3.0.1", 173 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", 174 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" 175 | }, 176 | "extsprintf": { 177 | "version": "1.0.2", 178 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", 179 | "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" 180 | }, 181 | "forever-agent": { 182 | "version": "0.6.1", 183 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 184 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 185 | }, 186 | "form-data": { 187 | "version": "2.1.4", 188 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", 189 | "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", 190 | "requires": { 191 | "asynckit": "0.4.0", 192 | "combined-stream": "1.0.5", 193 | "mime-types": "2.1.15" 194 | } 195 | }, 196 | "getpass": { 197 | "version": "0.1.7", 198 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 199 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 200 | "requires": { 201 | "assert-plus": "1.0.0" 202 | }, 203 | "dependencies": { 204 | "assert-plus": { 205 | "version": "1.0.0", 206 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 207 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 208 | } 209 | } 210 | }, 211 | "har-schema": { 212 | "version": "1.0.5", 213 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", 214 | "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" 215 | }, 216 | "har-validator": { 217 | "version": "4.2.1", 218 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", 219 | "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", 220 | "requires": { 221 | "ajv": "4.11.8", 222 | "har-schema": "1.0.5" 223 | } 224 | }, 225 | "hawk": { 226 | "version": "3.1.3", 227 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", 228 | "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", 229 | "requires": { 230 | "boom": "2.10.1", 231 | "cryptiles": "2.0.5", 232 | "hoek": "2.16.3", 233 | "sntp": "1.0.9" 234 | } 235 | }, 236 | "hoek": { 237 | "version": "2.16.3", 238 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", 239 | "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" 240 | }, 241 | "http-signature": { 242 | "version": "1.1.1", 243 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", 244 | "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", 245 | "requires": { 246 | "assert-plus": "0.2.0", 247 | "jsprim": "1.4.0", 248 | "sshpk": "1.13.1" 249 | } 250 | }, 251 | "is-typedarray": { 252 | "version": "1.0.0", 253 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 254 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 255 | }, 256 | "isemail": { 257 | "version": "1.2.0", 258 | "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", 259 | "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" 260 | }, 261 | "isstream": { 262 | "version": "0.1.2", 263 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 264 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 265 | }, 266 | "joi": { 267 | "version": "6.10.1", 268 | "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", 269 | "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", 270 | "requires": { 271 | "hoek": "2.16.3", 272 | "isemail": "1.2.0", 273 | "moment": "2.18.1", 274 | "topo": "1.1.0" 275 | } 276 | }, 277 | "jsbn": { 278 | "version": "0.1.1", 279 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 280 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 281 | "optional": true 282 | }, 283 | "jsep": { 284 | "version": "0.3.1", 285 | "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.3.1.tgz", 286 | "integrity": "sha512-faJGoKcyoHF5aqEI67U85HNhnPmfVD4hZLt5Ko/ieFRl4tHQ/zcSz1RdvLGlM1R4u53Sso25a9AtDeL/LGBDXw==" 287 | }, 288 | "json-schema": { 289 | "version": "0.2.3", 290 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 291 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 292 | }, 293 | "json-stable-stringify": { 294 | "version": "1.0.1", 295 | "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", 296 | "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", 297 | "requires": { 298 | "jsonify": "0.0.0" 299 | } 300 | }, 301 | "json-stringify-safe": { 302 | "version": "5.0.1", 303 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 304 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 305 | }, 306 | "jsonify": { 307 | "version": "0.0.0", 308 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", 309 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" 310 | }, 311 | "jsonwebtoken": { 312 | "version": "7.4.1", 313 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz", 314 | "integrity": "sha1-fKMk9SFfi+A5zTWmxFu4y3SkSPs=", 315 | "requires": { 316 | "joi": "6.10.1", 317 | "jws": "3.1.4", 318 | "lodash.once": "4.1.1", 319 | "ms": "2.0.0", 320 | "xtend": "4.0.1" 321 | } 322 | }, 323 | "jsprim": { 324 | "version": "1.4.0", 325 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", 326 | "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", 327 | "requires": { 328 | "assert-plus": "1.0.0", 329 | "extsprintf": "1.0.2", 330 | "json-schema": "0.2.3", 331 | "verror": "1.3.6" 332 | }, 333 | "dependencies": { 334 | "assert-plus": { 335 | "version": "1.0.0", 336 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 337 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 338 | } 339 | } 340 | }, 341 | "jwa": { 342 | "version": "1.1.5", 343 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", 344 | "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", 345 | "requires": { 346 | "base64url": "2.0.0", 347 | "buffer-equal-constant-time": "1.0.1", 348 | "ecdsa-sig-formatter": "1.0.9", 349 | "safe-buffer": "5.1.1" 350 | } 351 | }, 352 | "jws": { 353 | "version": "3.1.4", 354 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", 355 | "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", 356 | "requires": { 357 | "base64url": "2.0.0", 358 | "jwa": "1.1.5", 359 | "safe-buffer": "5.1.1" 360 | } 361 | }, 362 | "lodash": { 363 | "version": "4.17.4", 364 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 365 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 366 | }, 367 | "lodash.once": { 368 | "version": "4.1.1", 369 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 370 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 371 | }, 372 | "mime-db": { 373 | "version": "1.27.0", 374 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", 375 | "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" 376 | }, 377 | "mime-types": { 378 | "version": "2.1.15", 379 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", 380 | "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", 381 | "requires": { 382 | "mime-db": "1.27.0" 383 | } 384 | }, 385 | "moment": { 386 | "version": "2.18.1", 387 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", 388 | "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" 389 | }, 390 | "ms": { 391 | "version": "2.0.0", 392 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 393 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 394 | }, 395 | "oauth-sign": { 396 | "version": "0.8.2", 397 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", 398 | "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" 399 | }, 400 | "performance-now": { 401 | "version": "0.2.0", 402 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", 403 | "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" 404 | }, 405 | "promise": { 406 | "version": "7.3.1", 407 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 408 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 409 | "requires": { 410 | "asap": "2.0.6" 411 | } 412 | }, 413 | "punycode": { 414 | "version": "1.4.1", 415 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 416 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 417 | }, 418 | "qs": { 419 | "version": "6.4.0", 420 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", 421 | "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" 422 | }, 423 | "request": { 424 | "version": "2.81.0", 425 | "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", 426 | "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", 427 | "requires": { 428 | "aws-sign2": "0.6.0", 429 | "aws4": "1.6.0", 430 | "caseless": "0.12.0", 431 | "combined-stream": "1.0.5", 432 | "extend": "3.0.1", 433 | "forever-agent": "0.6.1", 434 | "form-data": "2.1.4", 435 | "har-validator": "4.2.1", 436 | "hawk": "3.1.3", 437 | "http-signature": "1.1.1", 438 | "is-typedarray": "1.0.0", 439 | "isstream": "0.1.2", 440 | "json-stringify-safe": "5.0.1", 441 | "mime-types": "2.1.15", 442 | "oauth-sign": "0.8.2", 443 | "performance-now": "0.2.0", 444 | "qs": "6.4.0", 445 | "safe-buffer": "5.1.1", 446 | "stringstream": "0.0.5", 447 | "tough-cookie": "2.3.2", 448 | "tunnel-agent": "0.6.0", 449 | "uuid": "3.1.0" 450 | }, 451 | "dependencies": { 452 | "uuid": { 453 | "version": "3.1.0", 454 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 455 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" 456 | } 457 | } 458 | }, 459 | "request-promise": { 460 | "version": "4.2.1", 461 | "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.1.tgz", 462 | "integrity": "sha1-fuxWyJMXqCLL/qmbA5zlQ8LhX2c=", 463 | "requires": { 464 | "bluebird": "3.5.0", 465 | "request-promise-core": "1.1.1", 466 | "stealthy-require": "1.1.1", 467 | "tough-cookie": "2.3.2" 468 | } 469 | }, 470 | "request-promise-core": { 471 | "version": "1.1.1", 472 | "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", 473 | "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", 474 | "requires": { 475 | "lodash": "4.17.4" 476 | } 477 | }, 478 | "rsa-pem-from-mod-exp": { 479 | "version": "0.8.4", 480 | "resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.4.tgz", 481 | "integrity": "sha1-NipCxtMEBW1JOz8SvOq7LGV2ptQ=" 482 | }, 483 | "safe-buffer": { 484 | "version": "5.1.1", 485 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 486 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 487 | }, 488 | "sntp": { 489 | "version": "1.0.9", 490 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", 491 | "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", 492 | "requires": { 493 | "hoek": "2.16.3" 494 | } 495 | }, 496 | "sprintf-js": { 497 | "version": "1.1.1", 498 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", 499 | "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" 500 | }, 501 | "sshpk": { 502 | "version": "1.13.1", 503 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", 504 | "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", 505 | "requires": { 506 | "asn1": "0.2.3", 507 | "assert-plus": "1.0.0", 508 | "bcrypt-pbkdf": "1.0.1", 509 | "dashdash": "1.14.1", 510 | "ecc-jsbn": "0.1.1", 511 | "getpass": "0.1.7", 512 | "jsbn": "0.1.1", 513 | "tweetnacl": "0.14.5" 514 | }, 515 | "dependencies": { 516 | "assert-plus": { 517 | "version": "1.0.0", 518 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 519 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 520 | } 521 | } 522 | }, 523 | "stealthy-require": { 524 | "version": "1.1.1", 525 | "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", 526 | "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" 527 | }, 528 | "strformat": { 529 | "version": "0.0.7", 530 | "resolved": "https://registry.npmjs.org/strformat/-/strformat-0.0.7.tgz", 531 | "integrity": "sha1-i2O+wZlXaLuaW8YAdPTN3/BEpMQ=" 532 | }, 533 | "stringstream": { 534 | "version": "0.0.5", 535 | "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", 536 | "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" 537 | }, 538 | "topo": { 539 | "version": "1.1.0", 540 | "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", 541 | "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", 542 | "requires": { 543 | "hoek": "2.16.3" 544 | } 545 | }, 546 | "tough-cookie": { 547 | "version": "2.3.2", 548 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", 549 | "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", 550 | "requires": { 551 | "punycode": "1.4.1" 552 | } 553 | }, 554 | "tunnel-agent": { 555 | "version": "0.6.0", 556 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 557 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 558 | "requires": { 559 | "safe-buffer": "5.1.1" 560 | } 561 | }, 562 | "tweetnacl": { 563 | "version": "0.14.5", 564 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 565 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 566 | "optional": true 567 | }, 568 | "underscore": { 569 | "version": "1.8.3", 570 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", 571 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" 572 | }, 573 | "url-join": { 574 | "version": "1.1.0", 575 | "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", 576 | "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=" 577 | }, 578 | "uuid": { 579 | "version": "2.0.3", 580 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", 581 | "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" 582 | }, 583 | "verror": { 584 | "version": "1.3.6", 585 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", 586 | "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", 587 | "requires": { 588 | "extsprintf": "1.0.2" 589 | } 590 | }, 591 | "xtend": { 592 | "version": "4.0.1", 593 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 594 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 595 | } 596 | } 597 | } 598 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-graph-dialog", 3 | "version": "3.8.5", 4 | "description": "bot graph dialog", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/CatalystCode/bot-graph-dialog.git" 12 | }, 13 | "keywords": [ 14 | "bot", 15 | "graph", 16 | "dialog" 17 | ], 18 | "author": "", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/CatalystCode/bot-graph-dialog/issues" 22 | }, 23 | "homepage": "https://github.com/CatalystCode/bot-graph-dialog#readme", 24 | "dependencies": { 25 | "botbuilder": "^3.8.4", 26 | "extend": "^3.0.0", 27 | "jsep": "^0.3.0", 28 | "request-promise": "^4.1.1", 29 | "strformat": "0.0.7", 30 | "underscore": "^1.8.3", 31 | "uuid": "^2.0.3" 32 | }, 33 | "gitHead": "01fd40b70f7e84c71806785dd3fe54500956403f", 34 | "_id": "bot-graph-dialog@3.4.5", 35 | "_shasum": "695bc3b0e10a8147dd8e1bbce2f26868807f5fde", 36 | "_from": "bot-graph-dialog@3.4.5", 37 | "_npmVersion": "2.15.1", 38 | "_nodeVersion": "4.4.4", 39 | "_npmUser": { 40 | "name": "amiturgman", 41 | "email": "ami.turgman@microsoft.com" 42 | }, 43 | "dist": { 44 | "shasum": "695bc3b0e10a8147dd8e1bbce2f26868807f5fde", 45 | "tarball": "https://registry.npmjs.org/bot-graph-dialog/-/bot-graph-dialog-3.4.5.tgz" 46 | }, 47 | "maintainers": [ 48 | { 49 | "name": "amiturgman", 50 | "email": "ami.turgman@microsoft.com" 51 | } 52 | ], 53 | "_npmOperationalInternal": { 54 | "host": "packages-12-west.internal.npmjs.com", 55 | "tmp": "tmp/bot-graph-dialog-3.4.5.tgz_1481033480636_0.8297735422383994" 56 | }, 57 | "directories": {}, 58 | "_resolved": "https://registry.npmjs.org/bot-graph-dialog/-/bot-graph-dialog-3.4.5.tgz" 59 | } 60 | --------------------------------------------------------------------------------