├── 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 |
--------------------------------------------------------------------------------