├── config ├── dev.sample.json └── index.js ├── images ├── workout1.png └── workout2.png ├── bot ├── handlers │ ├── save-income-data.js │ ├── save-expense-data.js │ ├── summary.js │ └── set_alarm.js └── scenarios │ ├── dialogs.json │ ├── sage.pegg.router.json │ ├── smoking.json │ ├── router.json │ ├── sage.pegg.new-income.json │ ├── sage.pegg.new-expense.json │ ├── botGames.json │ ├── validationsAndCarousels.json │ └── stomachPain.json ├── .gitignore ├── package.json ├── LICENSE ├── .vscode └── launch.json ├── README.md ├── proxyMode.js ├── router.js ├── exceed.js ├── index.js └── loadOnDemand.js /config/dev.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "MICROSOFT_APP_ID": "", 3 | "MICROSOFT_APP_PASSWORD": "" 4 | } -------------------------------------------------------------------------------- /images/workout1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/bot-trees/HEAD/images/workout1.png -------------------------------------------------------------------------------- /images/workout2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/bot-trees/HEAD/images/workout2.png -------------------------------------------------------------------------------- /bot/handlers/save-income-data.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (session, next) => { 3 | session.send("income data saved..."); 4 | return next(); 5 | } -------------------------------------------------------------------------------- /bot/handlers/save-expense-data.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (session, next) => { 3 | session.dialogData.data.expense = {}; 4 | session.send("expense data saved..."); 5 | return next(); 6 | } -------------------------------------------------------------------------------- /bot/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 | } -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Please refer to the dev.sample.json file. 3 | * Copy this file and create a new file named "dev.private.json". 4 | * Fill in the details for the features you'de like to support. 5 | * You don't have to fill in all settings, but leave those you're not using blank. 6 | */ 7 | 8 | var nconf = require('nconf'); 9 | var path = require('path'); 10 | 11 | var envFile = path.join(__dirname, 'dev.private.json'); 12 | var config = nconf.env().file({ file: envFile }); 13 | 14 | module.exports = config; -------------------------------------------------------------------------------- /bot/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 | } -------------------------------------------------------------------------------- /bot/scenarios/dialogs.json: -------------------------------------------------------------------------------- 1 | { 2 | "dialogs": [ 3 | { 4 | "path": "/stomachPain", 5 | "regex": "^stomach", 6 | "scenario": "stomachPain" 7 | }, 8 | { 9 | "path": "/smoking", 10 | "regex": "^(smoke|smoking)", 11 | "scenario": "smoking" 12 | }, 13 | { 14 | "path": "/botGames", 15 | "regex": "^(bot|games|commands)", 16 | "scenario": "botGames" 17 | }, 18 | { 19 | "path": "/pegg", 20 | "regex": "^(sage|pegg)", 21 | "scenario": "sage.pegg.router" 22 | }, 23 | { 24 | "path": "/validations", 25 | "regex": "^(validation|carousel)", 26 | "scenario": "validationsAndCarousels" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # remove private 7 | *.private.* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | .DS_Store 43 | -------------------------------------------------------------------------------- /bot/scenarios/sage.pegg.router.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sage.pegg.router", 3 | "type": "sequence", 4 | "steps": [ 5 | { 6 | "id": "userIntent", 7 | "type": "prompt", 8 | "data": { "text": "What do you want to do?" } 9 | }, 10 | { 11 | "id": "scoredIntent", 12 | "type": "score", 13 | "data": { 14 | "models": [ "sage-router" ] 15 | }, 16 | "scenarios": [ 17 | { 18 | "condition": "scoredIntent.intent == 'sage.new-expense'", 19 | "steps": [ { "subScenario": "sage.pegg.new-expense" } ] 20 | }, 21 | { 22 | "condition": "scoredIntent.intent == 'sage.new-income'", 23 | "steps": [ { "subScenario": "sage.pegg.new-income" } ] 24 | } 25 | ] 26 | } 27 | ], 28 | "models": [ 29 | { 30 | "name": "sage-router", 31 | "url": "https://api.projectoxford.ai/luis/v1/application?id=65f369d0-f62b-43d9-ac7d-d02404262d76&subscription-key=a7454fa784574b44b9018ce35fcceda8&q=" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-trees", 3 | "version": "1.0.0", 4 | "description": "A sample app for using the bot-graph-dialog node module", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/CatalystCode/bot-trees.git" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/CatalystCode/bot-trees/issues" 18 | }, 19 | "homepage": "https://github.com/CatalystCode/bot-trees#readme", 20 | "dependencies": { 21 | "bot-graph-dialog": "^3.8.4", 22 | "botbuilder": "^3.8.4", 23 | "express": "^4.14.0", 24 | "extend": "^3.0.0", 25 | "jsep": "^0.3.0", 26 | "nconf": "^0.8.4", 27 | "promise": "^7.1.1", 28 | "request": "^2.74.0", 29 | "request-promise": "^4.1.1", 30 | "strformat": "0.0.7", 31 | "underscore": "^1.8.3", 32 | "uuid": "^3.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bot/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/proxyMode.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "console": "internalConsole", 21 | "sourceMaps": false, 22 | "outDir": null 23 | }, 24 | { 25 | "name": "Attach", 26 | "type": "node", 27 | "request": "attach", 28 | "port": 5858, 29 | "address": "localhost", 30 | "restart": false, 31 | "sourceMaps": false, 32 | "outDir": null, 33 | "localRoot": "${workspaceRoot}", 34 | "remoteRoot": null 35 | }, 36 | { 37 | "name": "Attach to Process", 38 | "type": "node", 39 | "request": "attach", 40 | "processId": "${command.PickProcess}", 41 | "port": 5858, 42 | "sourceMaps": false, 43 | "outDir": null 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /bot/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 | } -------------------------------------------------------------------------------- /bot/scenarios/sage.pegg.new-income.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sage.pegg.new-income", 3 | "type": "sequence", 4 | "steps": [ 5 | { 6 | "type": "handler", 7 | "data": { "name": "save-income-data.js" } 8 | }, 9 | { 10 | "scenarios": [ 11 | { 12 | "condition": "!expense.amount", 13 | "steps": [ 14 | { 15 | "id": "expanse.amount", 16 | "type": "prompt", 17 | "data": { "type": "text", "text": "How much did you pay?" } 18 | } 19 | ] 20 | } 21 | ] 22 | }, 23 | { 24 | "scenarios": [ 25 | { 26 | "condition": "!expense.time", 27 | "steps": [ 28 | { 29 | "id": "expanse.time", 30 | "type": "prompt", 31 | "data": { "type": "date", "text": "When did you spend it?" } 32 | } 33 | ] 34 | } 35 | ] 36 | }, 37 | { 38 | "scenarios": [ 39 | { 40 | "condition": "!expense.item", 41 | "steps": [ 42 | { 43 | "id": "expanse.item", 44 | "type": "prompt", 45 | "data": { "type": "text", "text": "What was it for?" } 46 | } 47 | ] 48 | } 49 | ] 50 | }, 51 | { 52 | "type": "text", 53 | "data": { "text": "Thank you!" } 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /bot/scenarios/sage.pegg.new-expense.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sage.pegg.new-expense", 3 | "type": "sequence", 4 | "steps": [ 5 | { 6 | "type": "handler", 7 | "data": { "name": "save-expense-data.js" } 8 | }, 9 | { 10 | "type": "sequence", 11 | "scenarios": [ 12 | { 13 | "condition": "!expense.amount", 14 | "steps": [ 15 | { 16 | "id": "expense.amount", 17 | "type": "prompt", 18 | "data": { "type": "text", "text": "How much did you pay?" } 19 | } 20 | ] 21 | } 22 | ] 23 | }, 24 | { 25 | "type": "sequence", 26 | "scenarios": [ 27 | { 28 | "condition": "!expense.time", 29 | "steps": [ 30 | { 31 | "id": "expense.time", 32 | "type": "prompt", 33 | "data": { "type": "time", "text": "When did you spend it?" } 34 | } 35 | ] 36 | } 37 | ] 38 | }, 39 | { 40 | "type": "sequence", 41 | "scenarios": [ 42 | { 43 | "condition": "!expense.item", 44 | "steps": [ 45 | { 46 | "id": "expense.item", 47 | "type": "prompt", 48 | "data": { "type": "text", "text": "What was it for?" } 49 | } 50 | ] 51 | } 52 | ] 53 | }, 54 | { 55 | "type": "text", 56 | "data": { "text": "Thank you!" } 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bot Trees 2 | This is a sample bot app that uses the [bot-graph-dialog](https://github.com/CatalystCode/bot-graph-dialog) extension for dynamically loading graph-based dialogs. 3 | Use this app as a reference for using the `bot-graph-dialog` extension. 4 | 5 | **Read more about the motivation for developing this extension [here](https://www.microsoft.com/developerblog/real-life-code/2016/11/11/Bot-Graph-Dialog.html)** 6 | 7 | 8 | ## Getting Started 9 | 10 | **Note**- This page assumes you're already familar with the [core concepts](https://docs.botframework.com/en-us/node/builder/guides/core-concepts/#navtitle) of bots and how to run them locally on your machine. 11 | You'll need to have a bot provisioned in the developer portal. Follow [these instructions](https://docs.botframework.com/en-us/csharp/builder/sdkreference/gettingstarted.html) (look for the _Registering your Bot with the Microsoft Bot Framework_ section) to register your bot with the Microsoft Bot Framework. 12 | 13 | ``` 14 | git clone https://github.com/CatalystCode/bot-trees.git 15 | cd bot-trees 16 | npm install 17 | ``` 18 | 19 | Create a `config/dev.private.json` base on the `config/dev.sample.json` file. Edit it and add your bot App id and password. 20 | 21 | After connecting your bot to this endpoint, run `npm start`. 22 | 23 | 24 | 25 | ## Samples 26 | There are a few sample files in the root of this project: 27 | 28 | ## index.js 29 | This is the default sample which loads the different dialogs from the `bot/scenarios/dialogs.json` file. 30 | It adds each of the scenarios on the bot and binds it to the relevant RegEx as defined in the scenario. 31 | 32 | ## loadOnDemand.js 33 | This file demonstrates how to reload scenarios on-demand in cases that a scenario was modified on the backend. 34 | In this scenario, if a user was in the middle of a dialog that was updated, he would get a message saying the dialog was changed and that he will need to restart the dialog. 35 | 36 | To test this scenario, after starting a dialog such as the `stomachPain` by sending `stomach` to the bot: 37 | 38 | 1. Answer the first question the bot asks. 39 | 2. Change the version of the stomach pain scenario in the `bot/scenarios/stomachPain.json` file, and maybe the text of the first question, then save the file. 40 | 3. Browse the following URL `http://localhost:3978/api/load/stomachPain` to reload the scenario. 41 | 4. Continue the conversation with the bot. You should get a message that the dialog was updated and that you need to start over. You should see the updated question now. 42 | 43 | **Comment** If the `version` field in the root of the dialog object is specified, this will be considered as the scenario version. If the version was not specified, the bot will assign a version by hashing the content of the json file. 44 | 45 | ## router.js 46 | This file demonstrates how to use [LUIS](https://www.luis.ai/) in order to extract a user intent, and then branch to the next scenario based on the result that was received from LUIS. 47 | 48 | # License 49 | [MIT](LICENSE) 50 | 51 | -------------------------------------------------------------------------------- /bot/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.js", 25 | "js": [ 26 | "module.exports = (session, next, data) => { ", 27 | " var intent = session.dialogData.data[data.source]; ", 28 | " var alarmTime = null; ", 29 | " if (intent.actions[0].parameters[0].name == \"time\") { ", 30 | " // alarmTime = intent.entities... ", 31 | " // use intent.entities to extract relevant information ", 32 | " // assuming extracted alarmTime ", 33 | " alarmTime = '2016-10-10 10:10'; ", 34 | " } ", 35 | " if (data.target && alarmTime) { ", 36 | " session.dialogData.data[data.target] = alarmTime; ", 37 | " } ", 38 | " session.send('Alarm set for ' + alarmTime); ", 39 | " return next(); ", 40 | "} " 41 | ], 42 | "js_canBeEitherAStringOrAnArrayAsAbove": "module.exports = function (session, next, data) {}", 43 | "source": "userIntent", 44 | "target": "alarmTime" 45 | } 46 | } 47 | ] 48 | }, 49 | { 50 | "condition": "userIntent.intent == 'start over'", 51 | "nodeId": "whatDoYouWantToDo" 52 | }, 53 | { 54 | "condition": "userIntent.intent == 'quit'", 55 | "steps": [ 56 | { 57 | "type": "end", 58 | "data": { "text": "Thank you, goodbye." } 59 | } 60 | ] 61 | } 62 | ] 63 | }, 64 | { 65 | "type": "text", 66 | "data": { "text": "Done doing '{whatDoYouWantToDo}'." } 67 | } 68 | ], 69 | "models": [ 70 | { 71 | "name": "bot-common-responses", 72 | "url": "https://api.projectoxford.ai/luis/v1/application?id=7ff935f4-fe33-4a2a-b155-b82dbbf456ed&subscription-key=d7b46a6c72bf46c1b67f2c4f21acf960&q=" 73 | }, 74 | { 75 | "name": "Bottle", 76 | "url": "https://api.projectoxford.ai/luis/v1/application?id=0a2cc164-5a19-47b7-b85e-41914d9037ba&subscription-key=d7b46a6c72bf46c1b67f2c4f21acf960&q=" 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /proxyMode.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var util = require('util'); 3 | var express = require('express'); 4 | var builder = require('botbuilder'); 5 | var GraphDialog = require('bot-graph-dialog'); 6 | var config = require('./config'); 7 | var fs = require('fs'); 8 | 9 | var port = process.env.PORT || 3978; 10 | var app = express(); 11 | 12 | var microsoft_app_id = config.get('MICROSOFT_APP_ID'); 13 | var microsoft_app_password = config.get('MICROSOFT_APP_PASSWORD'); 14 | 15 | var connector = new builder.ChatConnector({ 16 | appId: microsoft_app_id, 17 | appPassword: microsoft_app_password, 18 | }); 19 | 20 | var bot = new builder.UniversalBot(connector); 21 | var intents = new builder.IntentDialog(); 22 | 23 | var scenariosPath = path.join(__dirname, 'bot', 'scenarios'); 24 | var handlersPath = path.join(__dirname, 'bot', 'handlers'); 25 | 26 | 27 | class ProxyNavigator { 28 | 29 | constructor() { 30 | 31 | this.nodes = [ 32 | { 33 | "type": "prompt", 34 | "data": { "type": "number", "text": "How old are you?" } 35 | }, 36 | { 37 | "type": "prompt", 38 | "data": { "type": "number", "text": "what's your height?" } 39 | }, 40 | { 41 | "type": "sequence", 42 | "steps": [ 43 | { 44 | "type": "text", 45 | "data": { "text": "message 1..." } 46 | }, 47 | { 48 | "type": "text", 49 | "data": { "text": "message 2..." } 50 | }, 51 | { 52 | "type": "text", 53 | "data": { "text": "message 3..." } 54 | }, 55 | { 56 | "type": "prompt", 57 | "data": { "type": "time", "text": "When did it start?" } 58 | } 59 | ] 60 | } 61 | ]; 62 | } 63 | 64 | // returns the current node of the dialog 65 | async getCurrentNode(session) { 66 | console.log(`getCurrentNode, message: ${JSON.stringify(session.message, true, 2)}`); 67 | 68 | var index = session.privateConversationData._currIndex || 0; 69 | var internalIndex = session.privateConversationData._currInternalIndex; 70 | var node = this.nodes[index]; 71 | 72 | if (!isNaN(internalIndex)) { 73 | node = node.steps[internalIndex]; 74 | } 75 | 76 | return node; 77 | }; 78 | 79 | // resolves the next node in the dialog 80 | async getNextNode(session) { 81 | console.log(`getNextNode, message: ${util.inspect(session.message)}`); 82 | //console.log(`result from previous call: ${session.dialogData.data[varname]}`); 83 | 84 | var index = session.privateConversationData._currIndex || 0; 85 | var node = this.nodes[index]; 86 | var internalIndex = session.privateConversationData._currInternalIndex; 87 | if (isNaN(internalIndex) || (node.steps && internalIndex + 1 > node.steps.length - 1)) { 88 | internalIndex = undefined; 89 | index++; 90 | } 91 | 92 | if (index > this.nodes.length -1) { 93 | index = 0; 94 | } 95 | 96 | node = this.nodes[index]; 97 | session.privateConversationData._currIndex = index; 98 | 99 | if (node.steps) { 100 | if (isNaN(internalIndex)) 101 | internalIndex = 0; 102 | else 103 | internalIndex++; 104 | 105 | if (internalIndex > node.steps.length - 1) { 106 | internalIndex = 0; 107 | } 108 | session.privateConversationData._currInternalIndex = internalIndex; 109 | node = node.steps[internalIndex]; 110 | } 111 | else { 112 | delete session.privateConversationData._currInternalIndex; 113 | } 114 | 115 | return node; 116 | }; 117 | } 118 | 119 | process.nextTick(async () => { 120 | var navigator = new ProxyNavigator(); 121 | var graphDialog = await GraphDialog.create({ bot, navigator }); 122 | bot.dialog('/', graphDialog.getDialog()); 123 | console.log(`proxy graph dialog loaded successfully`); 124 | }); 125 | 126 | 127 | app.post('/api/messages', connector.listen()); 128 | 129 | app.listen(port, () => { 130 | console.log('listening on port %s', port); 131 | }); 132 | -------------------------------------------------------------------------------- /bot/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 | } -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var builder = require('botbuilder'); 4 | var GraphDialog = require('bot-graph-dialog'); 5 | var config = require('./config'); 6 | var fs = require('fs'); 7 | 8 | var port = process.env.PORT || 3978; 9 | var app = express(); 10 | 11 | var microsoft_app_id = config.get('MICROSOFT_APP_ID'); 12 | var microsoft_app_password = config.get('MICROSOFT_APP_PASSWORD'); 13 | 14 | var connector = new builder.ChatConnector({ 15 | appId: microsoft_app_id, 16 | appPassword: microsoft_app_password, 17 | }); 18 | 19 | var bot = new builder.UniversalBot(connector); 20 | var intents = new builder.IntentDialog(); 21 | 22 | var scenariosPath = path.join(__dirname, 'bot', 'scenarios'); 23 | var handlersPath = path.join(__dirname, 'bot', 'handlers'); 24 | 25 | bot.dialog('/', intents); 26 | 27 | intents.matches(/^(help|hi|hello)/i, [ 28 | function (session) { 29 | session.send('Hi, how can I help you?'); 30 | } 31 | ]); 32 | 33 | process.nextTick(async () => { 34 | try { 35 | var graphDialog = await GraphDialog.create({ 36 | bot, 37 | scenario: 'router', 38 | loadScenario, 39 | loadHandler, 40 | customTypeHandlers: getCustomTypeHandlers() 41 | }); 42 | 43 | intents.onDefault(graphDialog.getDialog()); 44 | } 45 | catch(err) { 46 | console.error(`error loading dialog: ${err.message}`) 47 | } 48 | }); 49 | 50 | // this allows you to extend the json with more custom node types, 51 | // by providing your implementation to processing each custom type. 52 | // in the end of your implemention you should call the next callbacks 53 | // to allow the framework to continue with the dialog. 54 | // refer to the customTypeStepDemo node in the stomachPain.json scenario for an example. 55 | function getCustomTypeHandlers() { 56 | return [ 57 | { 58 | name: 'myCustomType', 59 | execute: (session, next, data) => { 60 | console.log(`in custom node type handler: customTypeStepDemo, data: ${data.someData}`); 61 | return next(); 62 | } 63 | } 64 | ]; 65 | } 66 | 67 | // this is the handler for loading scenarios from external datasource 68 | // in this implementation we're just reading it from a file 69 | // but it can come from any external datasource like a file, db, etc. 70 | function loadScenario(scenario) { 71 | return new Promise((resolve, reject) => { 72 | console.log('loading scenario', scenario); 73 | // implement loadScenario from external datasource. 74 | // in this example we're loading from local file 75 | var scenarioPath = path.join(scenariosPath, scenario + '.json'); 76 | 77 | return fs.readFile(scenarioPath, 'utf8', (err, content) => { 78 | if (err) { 79 | console.error("error loading json: " + scenarioPath); 80 | return reject(err); 81 | } 82 | 83 | var scenarioObj = JSON.parse(content); 84 | 85 | // simulating long load period 86 | setTimeout(() => { 87 | console.log('resolving scenario', scenarioPath); 88 | resolve(scenarioObj); 89 | }, Math.random() * 3000); 90 | }); 91 | }); 92 | } 93 | 94 | // this is the handler for loading handlers from external datasource 95 | // in this implementation we're just reading it from a file 96 | // but it can come from any external datasource like a file, db, etc. 97 | // 98 | // NOTE: handlers can also be embeded in the scenario json. See scenarios/botGames.json for an example. 99 | function loadHandler(handler) { 100 | return new Promise((resolve, reject) => { 101 | console.log('loading handler', handler); 102 | // implement loadHandler from external datasource. 103 | // in this example we're loading from local file 104 | var handlerPath = path.join(handlersPath, handler); 105 | var handlerString = null; 106 | return fs.readFile(handlerPath, 'utf8', (err, content) => { 107 | if (err) { 108 | console.error("error loading handler: " + handlerPath); 109 | return reject(err); 110 | } 111 | // simulating long load period 112 | setTimeout(() => { 113 | console.log('resolving handler', handler); 114 | resolve(content); 115 | }, Math.random() * 3000); 116 | }); 117 | }); 118 | } 119 | 120 | 121 | app.post('/api/messages', connector.listen()); 122 | 123 | app.listen(port, () => { 124 | console.log('listening on port %s', port); 125 | }); 126 | -------------------------------------------------------------------------------- /bot/scenarios/stomachPain.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "stomachPain", 3 | "version": "1", 4 | "type": "sequence", 5 | "steps": [ 6 | { 7 | "id": "age", 8 | "type": "prompt", 9 | "data": { "type": "number", "text": "How old are you?" } 10 | }, 11 | { 12 | "id": "whenStart", 13 | "type": "prompt", 14 | "data": { "type": "time", "text": "When did it start?" } 15 | }, 16 | { 17 | "id": "customTypeStepDemo", 18 | "type": "myCustomType", 19 | "data": { "someData": "someData" } 20 | }, 21 | { 22 | "subScenario": "smoking" 23 | }, 24 | { 25 | "id": "workout", 26 | "type": "prompt", 27 | "data": { 28 | "type": "choice", 29 | "text": "What kind of workout do you do?", 30 | "options": [ "None", "Crossfit", "TRX", "Kung-Fu" ], 31 | "config": { 32 | "listStyle": "button" 33 | } 34 | }, 35 | "varname": "workout", 36 | "scenarios": [ 37 | { 38 | "condition": "workout == 'Crossfit'", 39 | "steps": [ 40 | { 41 | "id": "cfWeight", 42 | "type": "prompt", 43 | "data": { "type":"number", "text": "How much do you lift?" }, 44 | "scenarios": [ 45 | { 46 | "condition": "cfWeight <= 100", 47 | "steps": [ 48 | { 49 | "id": "cfWeightSmallReason", 50 | "type": "prompt", 51 | "data": { "text": "Why so lite?" } 52 | } 53 | ] 54 | }, 55 | { 56 | "condition": "cfWeight > 100", 57 | "steps": [ 58 | { 59 | "id": "cfCrazy", 60 | "type": "prompt", 61 | "data": { "type":"confirm", "text": "Are you crazy?" }, 62 | "scenarios": [ 63 | { 64 | "condition": "cfCrazy", 65 | "steps": [ 66 | { 67 | "id": "takePills", 68 | "type": "text", 69 | "data": {"text": "Please start taking your pills" } 70 | } 71 | ] 72 | }, 73 | { 74 | "condition": "!cfCrazy", 75 | "steps": [ 76 | { 77 | "id": "cfHowLong", 78 | "type": "prompt", 79 | "data": { "type":"number", "text": "How many years have you been working out?" } 80 | }, 81 | { 82 | "id": "cfInstructor", 83 | "type": "prompt", 84 | "data": { "text": "Who is your instructor?" } 85 | }, 86 | { 87 | "id": "cfWhere", 88 | "type": "prompt", 89 | "data": { "text": "Where do you work out?" } 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | ] 96 | } 97 | ] 98 | } 99 | ] 100 | }, 101 | { 102 | "condition": "workout == 'TRX'", 103 | "steps": [ 104 | { 105 | "id": "trxStrength", 106 | "type": "prompt", 107 | "data": { 108 | "type":"choice", 109 | "text": "What is the strength of the rope?", 110 | "options": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 111 | } 112 | } 113 | ] 114 | }, 115 | { 116 | "condition": "workout == 'Kung-Fu'", 117 | "steps": [ 118 | { 119 | "id": "kfYears", 120 | "type": "prompt", 121 | "data": { "type":"number", "text": "How many years have you neen practicing?" } 122 | } 123 | ] 124 | } 125 | ] 126 | }, 127 | { 128 | "type": "text", 129 | "data": { "text": "Please wait while we finalize the data..." } 130 | }, 131 | { 132 | "id": "finalize", 133 | "type": "handler", 134 | "data": { "name": "summary.js" } 135 | } 136 | ] 137 | } -------------------------------------------------------------------------------- /exceed.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var util = require('util'); 3 | var express = require('express'); 4 | var builder = require('botbuilder'); 5 | var GraphDialog = require('bot-graph-dialog'); 6 | var request = require('request-promise'); 7 | var config = require('./config'); 8 | var fs = require('fs'); 9 | 10 | var port = process.env.PORT || 3978; 11 | var app = express(); 12 | 13 | var microsoft_app_id = config.get('MICROSOFT_APP_ID'); 14 | var microsoft_app_password = config.get('MICROSOFT_APP_PASSWORD'); 15 | 16 | var connector = new builder.ChatConnector({ 17 | appId: microsoft_app_id, 18 | appPassword: microsoft_app_password, 19 | }); 20 | 21 | var bot = new builder.UniversalBot(connector); 22 | 23 | // an implementation of the Navigator interface 24 | // which will act as the proxy for the backend API 25 | class ProxyNavigator { 26 | 27 | constructor() { 28 | // backend root URL 29 | this.apiUrl = "http://86d54bb8.ngrok.io/api/msBotFramework"; 30 | } 31 | 32 | // returns the current node of the dialog 33 | async getCurrentNode(session) { 34 | console.log(`getCurrentNode, message: ${JSON.stringify(session.message, true, 2)}`); 35 | 36 | var node; 37 | if (session.privateConversationData._currentNode) { 38 | try { 39 | node = JSON.parse(session.privateConversationData._currentNode); 40 | } 41 | catch(err) { 42 | console.error(`error parsing current node json: ${session.privateConversationData._currentNode}`); 43 | } 44 | } 45 | 46 | if (!node) { 47 | node = await this.getNextNode(session); 48 | } 49 | 50 | // in case of a node with a few steps (internal nodes) 51 | var internalIndex = session.privateConversationData._currInternalIndex; 52 | if (node && node.steps && !isNaN(internalIndex)) { 53 | node = node.steps[internalIndex]; 54 | } 55 | 56 | return node; 57 | }; 58 | 59 | // resolves the next node in the dialog 60 | async getNextNode(session) { 61 | console.log(`getNextNode, message: ${JSON.stringify(session.message, true, 2)}`); 62 | 63 | var node; 64 | if (session.privateConversationData._currentNode) { 65 | try { 66 | node = JSON.parse(session.privateConversationData._currentNode); 67 | } 68 | catch(err) { 69 | console.error(`error parsing current node json: ${session.privateConversationData._currentNode}`); 70 | } 71 | } 72 | 73 | if (node && node.steps) { 74 | var internalIndex = session.privateConversationData._currInternalIndex; 75 | internalIndex = isNaN(internalIndex) ? 0 : internalIndex + 1; 76 | if (internalIndex > node.steps.length - 1) { 77 | // get next node from remote api 78 | return await this.resolveNextRemoteNode(session); 79 | } 80 | 81 | session.privateConversationData._currInternalIndex = internalIndex; 82 | return node.steps[internalIndex]; 83 | } 84 | 85 | return await this.resolveNextRemoteNode(session); 86 | }; 87 | 88 | async resolveNextRemoteNode(session) { 89 | var body = { 90 | message: session.message 91 | }; 92 | 93 | // add the resolved value from the bot-graph-dialog. 94 | // in case of a 'time' prompt- "2 days ago" will be resolved to the 95 | // actual date 2 days ago 96 | if (session.privateConversationData._lastResolvedResult) { 97 | body.resolved = session.privateConversationData._lastResolvedResult; 98 | } 99 | 100 | var node = await this.callApi({ 101 | uri: this.apiUrl + '/message', 102 | body, 103 | method: 'POST', 104 | json: true 105 | }); 106 | 107 | delete session.privateConversationData._currInternalIndex; 108 | 109 | if (!node.steps) { 110 | node = { 111 | type: 'sequence', 112 | steps: [ node ] 113 | } 114 | } 115 | 116 | node.steps[node.steps.length - 1].stop = true; 117 | 118 | session.privateConversationData._currInternalIndex = 0; 119 | session.privateConversationData._currentNode = node ? JSON.stringify(node) : null; 120 | 121 | node = node.steps[0]; 122 | return node; 123 | } 124 | 125 | // calls the remote API 126 | async callApi(opts) { 127 | console.log(`invoking http call: ${util.inspect(opts)}`); 128 | 129 | try { 130 | var result = await request(opts); 131 | } 132 | catch(err) { 133 | console.error(`error invoking request: ${err.message}, opts: ${opts}`); 134 | return; 135 | } 136 | 137 | console.log(`got result: ${util.inspect(result)}`); 138 | return result.response; 139 | } 140 | } 141 | 142 | process.nextTick(async () => { 143 | var navigator = new ProxyNavigator(); 144 | var graphDialog = await GraphDialog.create({ bot, navigator, proxyMode: true }); 145 | bot.dialog('/', graphDialog.getDialog()); 146 | console.log(`proxy graph dialog loaded successfully`); 147 | }); 148 | 149 | app.post('/api/messages', connector.listen()); 150 | 151 | app.listen(port, () => { 152 | console.log('listening on port %s', port); 153 | }); 154 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var util = require('util'); 3 | var express = require('express'); 4 | var builder = require('botbuilder'); 5 | var GraphDialog = require('bot-graph-dialog'); 6 | var config = require('./config'); 7 | var fs = require('fs'); 8 | 9 | var port = process.env.PORT || 3978; 10 | var app = express(); 11 | 12 | var microsoft_app_id = config.get('MICROSOFT_APP_ID'); 13 | var microsoft_app_password = config.get('MICROSOFT_APP_PASSWORD'); 14 | 15 | var connector = new builder.ChatConnector({ 16 | appId: microsoft_app_id, 17 | appPassword: microsoft_app_password, 18 | }); 19 | 20 | var bot = new builder.UniversalBot(connector); 21 | var intents = new builder.IntentDialog(); 22 | 23 | var scenariosPath = path.join(__dirname, 'bot', 'scenarios'); 24 | var handlersPath = path.join(__dirname, 'bot', 'handlers'); 25 | 26 | bot.dialog('/', intents); 27 | 28 | intents.matches(/^(help|hi|hello)/i, [ 29 | session => { 30 | session.send('Hi, how can I help you?'); 31 | } 32 | ]); 33 | 34 | // dynamically load dialogs from external datasource 35 | // create a GraphDialog for each and bind it to the intents object 36 | process.nextTick(async () => { 37 | 38 | var dialogs = await loadDialogs(); 39 | 40 | dialogs.forEach(async dialog => { 41 | console.log(`loading scenario: ${dialog.scenario} for regex: ${dialog.regex}`); 42 | 43 | var re = new RegExp(dialog.regex, 'i'); 44 | intents.matches(re, [ 45 | function (session) { 46 | session.beginDialog(dialog.path, {}); 47 | } 48 | ]); 49 | 50 | try { 51 | var graphDialog = await GraphDialog.create({ 52 | bot, 53 | scenario: dialog.scenario, 54 | loadScenario, 55 | loadHandler, 56 | customTypeHandlers: getCustomTypeHandlers() 57 | }); 58 | } 59 | catch(err) { 60 | console.error(`error loading dialog: ${err.message}`); 61 | } 62 | 63 | bot.dialog(dialog.path, graphDialog.getDialog()); 64 | 65 | console.log(`graph dialog loaded successfully: scenario ${dialog.scenario} for regExp: ${dialog.regex}`); 66 | 67 | }); 68 | }); 69 | 70 | 71 | // this allows you to extend the json with more custom node types, 72 | // by providing your implementation to processing each custom type. 73 | // in the end of your implemention you should call the next callbacks 74 | // to allow the framework to continue with the dialog. 75 | // refer to the customTypeStepDemo node in the stomachPain.json scenario for an example. 76 | function getCustomTypeHandlers() { 77 | return [ 78 | { 79 | name: 'myCustomType', 80 | execute: (session, next, data) => { 81 | console.log(`in custom node type handler: customTypeStepDemo, data: ${data.someData}`); 82 | return next(); 83 | } 84 | } 85 | ]; 86 | } 87 | 88 | // this is the handler for loading scenarios from external datasource 89 | // in this implementation we're just reading it from a file 90 | // but it can come from any external datasource like a file, db, etc. 91 | function loadScenario(scenario) { 92 | return new Promise((resolve, reject) => { 93 | console.log('loading scenario', scenario); 94 | // implement loadScenario from external datasource. 95 | // in this example we're loading from local file 96 | var scenarioPath = path.join(scenariosPath, scenario + '.json'); 97 | 98 | return fs.readFile(scenarioPath, 'utf8', (err, content) => { 99 | if (err) { 100 | console.error("error loading json: " + scenarioPath); 101 | return reject(err); 102 | } 103 | 104 | var scenarioObj = JSON.parse(content); 105 | 106 | // simulating long load period 107 | setTimeout(() => { 108 | console.log('resolving scenario', scenarioPath); 109 | resolve(scenarioObj); 110 | }, Math.random() * 3000); 111 | }); 112 | }); 113 | } 114 | 115 | // this is the handler for loading handlers from external datasource 116 | // in this implementation we're just reading it from a file 117 | // but it can come from any external datasource like a file, db, etc. 118 | // 119 | // NOTE: handlers can also be embeded in the scenario json. See scenarios/botGames.json for an example. 120 | function loadHandler(handler) { 121 | return new Promise((resolve, reject) => { 122 | console.log('loading handler', handler); 123 | // implement loadHandler from external datasource. 124 | // in this example we're loading from local file 125 | var handlerPath = path.join(handlersPath, handler); 126 | var handlerString = null; 127 | return fs.readFile(handlerPath, 'utf8', (err, content) => { 128 | if (err) { 129 | console.error("error loading handler: " + handlerPath); 130 | return reject(err); 131 | } 132 | // simulating long load period 133 | setTimeout(() => { 134 | console.log('resolving handler', handler); 135 | resolve(content); 136 | }, Math.random() * 3000); 137 | }); 138 | }); 139 | } 140 | 141 | // this is the handler for loading scenarios from external datasource 142 | // in this implementation we're just reading it from the file scnearios/dialogs.json 143 | // but it can come from any external datasource like a file, db, etc. 144 | function loadDialogs() { 145 | return new Promise((resolve, reject) => { 146 | console.log('loading dialogs'); 147 | 148 | var dialogsPath = path.join(scenariosPath, "dialogs.json"); 149 | return fs.readFile(dialogsPath, 'utf8', (err, content) => { 150 | if (err) { 151 | console.error("error loading json: " + dialogsPath); 152 | return reject(err); 153 | } 154 | 155 | var dialogs = JSON.parse(content); 156 | 157 | // simulating long load period 158 | setTimeout(() => { 159 | console.log('resolving dialogs', dialogsPath); 160 | resolve(dialogs.dialogs); 161 | }, Math.random() * 3000); 162 | }); 163 | }); 164 | } 165 | 166 | app.post('/api/messages', connector.listen()); 167 | 168 | app.listen(port, () => { 169 | console.log('listening on port %s', port); 170 | }); 171 | -------------------------------------------------------------------------------- /loadOnDemand.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var builder = require('botbuilder'); 4 | var GraphDialog = require('bot-graph-dialog'); 5 | var config = require('./config'); 6 | var fs = require('fs'); 7 | 8 | var port = process.env.PORT || 3978; 9 | var app = express(); 10 | 11 | var microsoft_app_id = config.get('MICROSOFT_APP_ID'); 12 | var microsoft_app_password = config.get('MICROSOFT_APP_PASSWORD'); 13 | 14 | var connector = new builder.ChatConnector({ 15 | appId: microsoft_app_id, 16 | appPassword: microsoft_app_password, 17 | }); 18 | 19 | var bot = new builder.UniversalBot(connector); 20 | var intents = new builder.IntentDialog(); 21 | 22 | var scenariosPath = path.join(__dirname, 'bot', 'scenarios'); 23 | var handlersPath = path.join(__dirname, 'bot', 'handlers'); 24 | var dialogsMapById = {}; 25 | var dialogsMapByPath = {}; 26 | 27 | bot.dialog('/', intents); 28 | 29 | intents.matches(/^(help|hi|hello)/i, [ 30 | function (session) { 31 | session.send('Hi, how can I help you?'); 32 | } 33 | ]); 34 | 35 | // dynamically load dialog from a remote datasource 36 | // create a GraphDialog instance and bind it on the bot 37 | 38 | async function loadDialog(dialog) { 39 | 40 | console.log(`loading scenario: ${dialog.scenario} for regex: ${dialog.regex}`); 41 | 42 | var re = new RegExp(dialog.regex, 'i'); 43 | 44 | intents.matches(re, [ 45 | function (session) { 46 | session.beginDialog(dialog.path); 47 | } 48 | ]); 49 | 50 | try { 51 | var graphDialog = await GraphDialog.create({ 52 | bot, 53 | scenario: dialog.scenario, 54 | loadScenario, 55 | loadHandler, 56 | customTypeHandlers: getCustomTypeHandlers(), 57 | onBeforeProcessingStep 58 | }); 59 | } 60 | catch(err) { 61 | console.error(`error loading dialog: ${err.message}`); 62 | throw err; 63 | } 64 | 65 | dialog.graphDialog = graphDialog; 66 | dialogsMapById[graphDialog.getDialogId()] = dialog; 67 | dialogsMapByPath[dialog.path] = dialog; 68 | 69 | bot.dialog(dialog.path, graphDialog.getDialog()); 70 | console.log(`graph dialog loaded successfully: scenario ${dialog.scenario} version ${graphDialog.getDialogVersion()} for regExp: ${dialog.regex} on path ${dialog.path}`); 71 | 72 | } 73 | 74 | // trigger dynamic load of the dialogs 75 | loadDialogs() 76 | .then(dialogs => dialogs.forEach(dialog => loadDialog(dialog))) 77 | .catch(err => console.error(`error loading dialogs dynamically: ${err.message}`)); 78 | 79 | // intercept change in scenario version before processing each dialog step 80 | // if there was a change in the version, restart the dialog 81 | // TODO: think about adding this internally to the GraphDialog so users gets this as default behaviour. 82 | function onBeforeProcessingStep(session, args, next) { 83 | 84 | session.sendTyping(); 85 | var dialogVersion = this.getDialogVersion(); 86 | 87 | if (!session.privateConversationData._dialogVersion) { 88 | session.privateConversationData._dialogVersion = dialogVersion; 89 | } 90 | 91 | if (session.privateConversationData._dialogVersion !== dialogVersion) { 92 | session.send("Dialog updated. We'll have to start over."); 93 | return this.restartDialog(session); 94 | } 95 | 96 | return next(); 97 | } 98 | 99 | 100 | // this allows you to extend the json with more custom node types, 101 | // by providing your implementation to processing each custom type. 102 | // in the end of your implemention you should call the next callbacks 103 | // to allow the framework to continue with the dialog. 104 | // refer to the customTypeStepDemo node in the stomachPain.json scenario for an example. 105 | function getCustomTypeHandlers() { 106 | return [ 107 | { 108 | name: 'myCustomType', 109 | execute: (session, next, data) => { 110 | console.log(`in custom node type handler: customTypeStepDemo, data: ${data.someData}`); 111 | return next(); 112 | } 113 | } 114 | ]; 115 | } 116 | 117 | // this is the handler for loading scenarios from external datasource 118 | // in this implementation we're just reading it from a file 119 | // but it can come from any external datasource like a file, db, etc. 120 | function loadScenario(scenario) { 121 | return new Promise((resolve, reject) => { 122 | console.log('loading scenario', scenario); 123 | // implement loadScenario from external datasource. 124 | // in this example we're loading from local file 125 | var scenarioPath = path.join(scenariosPath, scenario + '.json'); 126 | 127 | return fs.readFile(scenarioPath, 'utf8', (err, content) => { 128 | if (err) { 129 | console.error("error loading json: " + scenarioPath); 130 | return reject(err); 131 | } 132 | 133 | var scenarioObj = JSON.parse(content); 134 | 135 | // simulating long load period 136 | setTimeout(() => { 137 | console.log('resolving scenario', scenarioPath); 138 | resolve(scenarioObj); 139 | }, Math.random() * 3000); 140 | }); 141 | }); 142 | } 143 | 144 | // this is the handler for loading handlers from external datasource 145 | // in this implementation we're just reading it from a file 146 | // but it can come from any external datasource like a file, db, etc. 147 | // 148 | // NOTE: handlers can also be embeded in the scenario json. See scenarios/botGames.json for an example. 149 | function loadHandler(handler) { 150 | return new Promise((resolve, reject) => { 151 | console.log('loading handler', handler); 152 | // implement loadHandler from external datasource. 153 | // in this example we're loading from local file 154 | var handlerPath = path.join(handlersPath, handler); 155 | var handlerString = null; 156 | return fs.readFile(handlerPath, 'utf8', (err, content) => { 157 | if (err) { 158 | console.error("error loading handler: " + handlerPath); 159 | return reject(err); 160 | } 161 | // simulating long load period 162 | setTimeout(() => { 163 | console.log('resolving handler', handler); 164 | resolve(content); 165 | }, Math.random() * 3000); 166 | }); 167 | }); 168 | } 169 | 170 | // this is the handler for loading scenarios from external datasource 171 | // in this implementation we're just reading it from the file scnearios/dialogs.json 172 | // but it can come from any external datasource like a file, db, etc. 173 | function loadDialogs() { 174 | return new Promise((resolve, reject) => { 175 | console.log('loading dialogs'); 176 | 177 | var dialogsPath = path.join(scenariosPath, "dialogs.json"); 178 | return fs.readFile(dialogsPath, 'utf8', (err, content) => { 179 | if (err) { 180 | console.error("error loading json: " + dialogsPath); 181 | return reject(err); 182 | } 183 | 184 | var dialogs = JSON.parse(content); 185 | 186 | // simulating long load period 187 | setTimeout(() => { 188 | console.log('resolving dialogs', dialogsPath); 189 | resolve(dialogs.dialogs); 190 | }, Math.random() * 3000); 191 | }); 192 | }); 193 | } 194 | 195 | app.post('/api/messages', connector.listen()); 196 | 197 | // endpoint for reloading scenario on demand 198 | app.get('/api/load/:scenario', async (req, res) => { 199 | var scenario = req.params.scenario; 200 | console.log(`reloading scenario: ${scenario}`); 201 | var dialog = dialogsMapById[scenario]; 202 | 203 | try { 204 | await dialog.graphDialog.reload(); 205 | var msg = `scenario id '${scenario}' reloaded`; 206 | console.log(msg); 207 | return res.end(msg); 208 | } 209 | catch(err) { 210 | return res.end(`error loading dialog: ${err.message}`) 211 | } 212 | 213 | }); 214 | 215 | app.listen(port, () => { 216 | console.log('listening on port %s', port); 217 | }); 218 | 219 | --------------------------------------------------------------------------------