├── .gitattributes ├── 9781484235393.jpg ├── Contributing.md ├── LICENSE.txt ├── README.md ├── chapter01-echo-bot ├── app.js ├── env.defaults └── package.json ├── chapter01-fraudalert-bot ├── app (1).js ├── env (1).defaults └── package (1).json ├── chapter01-sentiment-bot ├── app.js ├── env.defaults └── package.json ├── chapter01-youtube-bot ├── app.js ├── env.defaults └── package.json ├── chapter05-calendar-bot ├── CalendarBotModel.json ├── README.md ├── Uploading-source-code.mp4 ├── app.js ├── constants.js ├── dialogs │ ├── addEntry.js │ ├── checkAvailability.js │ ├── editEntry.js │ ├── help.js │ ├── removeEntry.js │ └── summarize.js ├── entityTranslator.js ├── env.defaults ├── moveTranslator.js ├── package.json ├── support │ └── jasmine.json ├── tests │ ├── entityTranslator.spec.js │ └── moveTranslator.spec.js └── utils.js ├── chapter07-calendar-bot ├── CalendarBotModel.json ├── README.md ├── app.js ├── constants.js ├── dialogs │ ├── addEntry.js │ ├── auth.js │ ├── checkAvailability.js │ ├── editEntry.js │ ├── help.js │ ├── prechecks.js │ ├── primaryCalendar.js │ ├── removeEntry.js │ └── summarize.js ├── entityTranslator.js ├── env.defaults ├── moveTranslator.js ├── package.json ├── services │ └── calendar-api.js ├── spec │ ├── support │ │ └── jasmine.json │ └── tests │ │ ├── entityTranslator.spec.js │ │ └── moveTranslator.spec.js └── utils.js ├── chapter08-slack-interactive-messages-bot ├── README.md ├── app.js ├── env.defaults ├── package.json ├── slackApi.js └── stepData.js ├── chapter09-directline-webchat-and-voice-bot ├── README.md ├── app.js ├── env.defaults ├── package.json ├── public │ ├── app │ │ ├── chat.css │ │ └── chat.js │ └── index.html └── test.js ├── chapter10-calendar-bot ├── CalendarBotModel.json ├── README.md ├── app.js ├── constants.js ├── dialogs │ ├── addEntry.js │ ├── auth.js │ ├── checkAvailability.js │ ├── editEntry.js │ ├── help.js │ ├── prechecks.js │ ├── primaryCalendar.js │ ├── removeEntry.js │ └── summarize.js ├── entityTranslator.js ├── env.defaults ├── moveTranslator.js ├── package.json ├── services │ └── calendar-api.js ├── spec │ ├── support │ │ └── jasmine.json │ └── tests │ │ ├── entityTranslator.spec.js │ │ └── moveTranslator.spec.js ├── translatorMiddleware.js └── utils.js ├── chapter10-hot-dog-or-not-hot-dog ├── README.md ├── app.js ├── env.defaults └── package.json ├── chapter10-spell-check-bot ├── README.md ├── app.js ├── env.defaults └── package.json ├── chapter11-image-rendering-bot ├── README.md ├── app.js ├── cardTemplate.html ├── env.defaults ├── images │ ├── 13619778-5eab-41d8-bd93-a96a82269d1f.png │ ├── 3a84409b-1c4d-41e7-ace9-344e69890d24.png │ ├── 689b99da-907c-4039-a816-22b271b7ee66.png │ ├── 8c1db6de-0957-4b3d-9a22-5dc3f93850a9.png │ ├── 92e0630b-2e57-43aa-91d7-5e7bff5402af.png │ ├── b1e21779-9d97-4794-a5ae-cca905b65752.png │ ├── b4677df8-7ecb-49c1-94b0-d7a89f754cdb.png │ ├── cb07be5f-b8d0-4022-ae71-98791bf84386.png │ ├── cc0326ac-9749-4c0c-8bd9-17e863a2b4b3.png │ ├── da7759d5-abd7-4309-874b-5c3d615c8c36.png │ ├── e1cfa34b-d043-45e8-9cd8-1387bcb79f4e.png │ ├── f1ac4f39-161c-434a-8854-55b7f99a2e74.png │ └── f9a50830-2042-40b0-834f-7338b032bcce.png └── package.json ├── chapter12-calendar-bot ├── CalendarBotModel.json ├── README.md ├── app.js ├── constants.js ├── dialogs │ ├── addEntry.js │ ├── auth.js │ ├── checkAvailability.js │ ├── editEntry.js │ ├── help.js │ ├── humanEscalation.js │ ├── prechecks.js │ ├── primaryCalendar.js │ ├── removeEntry.js │ └── summarize.js ├── entityTranslator.js ├── env.defaults ├── moveTranslator.js ├── package.json ├── services │ └── calendar-api.js ├── spec │ ├── support │ │ └── jasmine.json │ └── tests │ │ ├── entityTranslator.spec.js │ │ └── moveTranslator.spec.js ├── translatorMiddleware.js └── utils.js ├── chapter12-facebook-human-escalation ├── README.md ├── app.js ├── env.defaults └── package.json ├── chapter13-calendar-bot ├── CalendarBotModel.json ├── README.md ├── app.js ├── chatbase.js ├── constants.js ├── dialogs │ ├── addEntry.js │ ├── auth.js │ ├── checkAvailability.js │ ├── editEntry.js │ ├── help.js │ ├── humanEscalation.js │ ├── prechecks.js │ ├── primaryCalendar.js │ ├── removeEntry.js │ └── summarize.js ├── entityTranslator.js ├── env.defaults ├── moveTranslator.js ├── package.json ├── services │ └── calendar-api.js ├── spec │ ├── support │ │ └── jasmine.json │ └── tests │ │ ├── entityTranslator.spec.js │ │ └── moveTranslator.spec.js ├── translatorMiddleware.js └── utils.js └── chapter14-alexa-skill-connector-bot ├── README.md ├── alexaConnector.js ├── alexaRecognizer.js ├── app.js ├── env.defaults ├── package.json └── skill ├── lambda.js └── model.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /9781484235393.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/9781484235393.jpg -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Apress Source Code 2 | 3 | Copyright for Apress source code belongs to the author(s). However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author(s) and other readers. 4 | 5 | ## How to Contribute 6 | 7 | 1. Make sure you have a GitHub account. 8 | 2. Fork the repository for the relevant book. 9 | 3. Create a new branch on which to make your change, e.g. 10 | `git checkout -b my_code_contribution` 11 | 4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted. 12 | 5. Submit a pull request. 13 | 14 | Thank you for your contribution! -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Freeware License, some rights reserved 2 | 3 | Copyright (c) 2018 Szymon Rozga 4 | 5 | Permission is hereby granted, free of charge, to anyone obtaining a copy 6 | of this software and associated documentation files (the "Software"), 7 | to work with the Software within the limits of freeware distribution and fair use. 8 | This includes the rights to use, copy, and modify the Software for personal use. 9 | Users are also allowed and encouraged to submit corrections and modifications 10 | to the Software for the benefit of other users. 11 | 12 | It is not allowed to reuse, modify, or redistribute the Software for 13 | commercial use in any way, or for a user’s educational materials such as books 14 | or blog articles without prior permission from the copyright holder. 15 | 16 | The above copyright notice and this permission notice need to be included 17 | in all copies or substantial portions of the software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS OR APRESS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apress Source Code 2 | 3 | This repository accompanies [*Practical Bot Development: Designing and Building Bots with Node.js and Microsoft Bot Builder Framework*](https://www.apress.com/9781484235393) by Szymon Rozga (Apress, 2018). 4 | 5 | [comment]: #cover 6 | ![Cover image](9781484235393.jpg) 7 | 8 | Download the files as a zip using the green button, or clone the repository to your machine using Git. 9 | 10 | ## Releases 11 | 12 | Release v1.0 corresponds to the code in the published book, without corrections or updates. 13 | 14 | ## Contributions 15 | 16 | See the file Contributing.md for more information on how you can contribute to this repository. -------------------------------------------------------------------------------- /chapter01-echo-bot/app.js: -------------------------------------------------------------------------------- 1 | // load env variables 2 | require('dotenv-extended').load(); 3 | 4 | const builder = require('botbuilder'); 5 | const restify = require('restify'); 6 | 7 | // setup our web server 8 | const server = restify.createServer(); 9 | server.listen(process.env.port || process.env.PORT || 3978, () => { 10 | console.log('%s listening to %s', server.name, server.url); 11 | }); 12 | 13 | // initialize the chat bot 14 | const connector = new builder.ChatConnector({ 15 | appId: process.env.MICROSOFT_APP_ID, 16 | appPassword: process.env.MICROSOFT_APP_PASSWORD 17 | }); 18 | server.post('/api/messages', connector.listen()); 19 | 20 | const bot = new builder.UniversalBot(connector, [ 21 | (session) => { 22 | session.send('echo: ' + session.message.text); 23 | } 24 | ]); 25 | const inMemoryStorage = new builder.MemoryBotStorage(); 26 | bot.set('storage', inMemoryStorage); -------------------------------------------------------------------------------- /chapter01-echo-bot/env.defaults: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | MICROSOFT_APP_ID= 3 | MICROSOFT_APP_PASSWORD= 4 | -------------------------------------------------------------------------------- /chapter01-echo-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-echo-bot", 3 | "version": "1.0.0", 4 | "description": "Echo Bot from Chapter 1, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "botbuilder": "^3.9.0", 12 | "dotenv-extended": "^1.0.4", 13 | "restify": "^4.3.0" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^4.10.0", 17 | "eslint-config-google": "^0.9.1", 18 | "eslint-config-standard": "^10.2.1", 19 | "eslint-plugin-import": "^2.8.0", 20 | "eslint-plugin-node": "^5.2.1", 21 | "eslint-plugin-promise": "^3.6.0", 22 | "eslint-plugin-standard": "^3.0.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /chapter01-fraudalert-bot/app (1).js: -------------------------------------------------------------------------------- 1 | // This loads the environment variables from the .env file 2 | require('dotenv-extended').load(); 3 | 4 | const builder = require('botbuilder'); 5 | const restify = require('restify'); 6 | 7 | // Setup Restify Server 8 | const server = restify.createServer(); 9 | server.listen(process.env.port || process.env.PORT || 3978, () => { 10 | console.log('%s listening to %s', server.name, server.url); 11 | }); 12 | 13 | // Create chat bot and listen to messages 14 | const connector = new builder.ChatConnector({ 15 | appId: process.env.MICROSOFT_APP_ID, 16 | appPassword: process.env.MICROSOFT_APP_PASSWORD 17 | }); 18 | server.post('/api/messages', connector.listen()); 19 | 20 | const fraudAlertSubscriptions = {}; // collection of subscriptions to alerts 21 | 22 | setInterval(() => { 23 | const keys = Object.keys(fraudAlertSubscriptions); 24 | if (keys.length === 0) return; 25 | 26 | const key = keys[Math.floor(Math.random() * keys.length)]; 27 | const address = fraudAlertSubscriptions[key]; 28 | delete fraudAlertSubscriptions[key]; 29 | const msg = new builder.Message().address(address); 30 | msg.text('We noticed some strange activity. Did you use your card for a $231.73 purchase on Amazon.com? Please call your bank for more information.'); 31 | msg.textLocale('en-US'); 32 | bot.send(msg); 33 | }, 10000); 34 | 35 | const bot = new builder.UniversalBot(connector, [ 36 | (session) => { 37 | const id = session.message.user.id; 38 | 39 | if (!fraudAlertSubscriptions[id]) { 40 | fraudAlertSubscriptions[id] = session.message.address; 41 | session.send('Hi there, you are now subscribed to fraud alerts'); 42 | } else { 43 | session.send('You are already subscribed to fraud alerts'); 44 | } 45 | } 46 | ]); 47 | const inMemoryStorage = new builder.MemoryBotStorage(); 48 | bot.set('storage', inMemoryStorage); -------------------------------------------------------------------------------- /chapter01-fraudalert-bot/env (1).defaults: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | MICROSOFT_APP_ID= 3 | MICROSOFT_APP_PASSWORD= 4 | -------------------------------------------------------------------------------- /chapter01-fraudalert-bot/package (1).json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-fraud-alert-bot", 3 | "version": "1.0.0", 4 | "description": "Fraud Alert Bot from Chapter 1, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "botbuilder": "^3.8.2", 12 | "dotenv-extended": "^1.0.4", 13 | "request": "^2.81.0", 14 | "restify": "^4.3.0" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^4.10.0", 18 | "eslint-config-google": "^0.9.1", 19 | "eslint-config-standard": "^10.2.1", 20 | "eslint-plugin-import": "^2.8.0", 21 | "eslint-plugin-node": "^5.2.1", 22 | "eslint-plugin-promise": "^3.6.0", 23 | "eslint-plugin-standard": "^3.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /chapter01-sentiment-bot/app.js: -------------------------------------------------------------------------------- 1 | // This loads the environment variables from the .env file 2 | require('dotenv-extended').load(); 3 | 4 | const builder = require('botbuilder'); 5 | const restify = require('restify'); 6 | const request = require('request'); 7 | 8 | // Setup Restify Server 9 | const server = restify.createServer(); 10 | server.listen(process.env.port || process.env.PORT || 3978, () => { 11 | console.log('%s listening to %s', server.name, server.url); 12 | }); 13 | 14 | const sentimentUri = 'https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment'; 15 | const sentimentKey = process.env.SENTIMENT_KEY; 16 | 17 | function options () { 18 | const options = { 19 | url: sentimentUri, 20 | method: 'POST', 21 | headers: { 22 | 'Ocp-Apim-Subscription-Key': sentimentKey 23 | } 24 | }; 25 | return options; 26 | } 27 | 28 | // Create chat bot and listen to messages 29 | const connector = new builder.ChatConnector({ 30 | appId: process.env.MICROSOFT_APP_ID, 31 | appPassword: process.env.MICROSOFT_APP_PASSWORD 32 | }); 33 | server.post('/api/messages', connector.listen()); 34 | 35 | const bot = new builder.UniversalBot(connector, [ 36 | session => { 37 | const data = { 38 | documents: [{ 39 | id: '1', 40 | language: 'en', 41 | text: session.message.text 42 | }] 43 | }; 44 | let opts = options(); 45 | opts.json = data; 46 | 47 | request(opts, (error, response, body) => { 48 | if (error) { 49 | session.endConversation('received error while fetching sentiment. please try again later.'); 50 | console.log('received error while fetching sentiment:\n' + error); 51 | return; 52 | } 53 | const score = body.documents[0].score; 54 | const msg = { 55 | attachments: [{ 56 | 'content': '' + score, 57 | 'contentType': 'text/plain' 58 | }] 59 | }; 60 | 61 | if (score < 0.15) { 62 | msg.text = 'that is really not cool'; 63 | } else if (score < 0.4) { 64 | msg.text = 'there\'s no need for that'; 65 | } else if (score < 0.6) { 66 | msg.text = 'alright'; 67 | } else if (score < 0.8) { 68 | msg.text = 'that\'s cool'; 69 | } else { 70 | msg.text = 'that\'s really nice!'; 71 | } 72 | session.send(msg); 73 | }); 74 | } 75 | ]); 76 | const inMemoryStorage = new builder.MemoryBotStorage(); 77 | bot.set('storage', inMemoryStorage); -------------------------------------------------------------------------------- /chapter01-sentiment-bot/env.defaults: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | MICROSOFT_APP_ID= 3 | MICROSOFT_APP_PASSWORD= 4 | SENTIMENT_KEY= -------------------------------------------------------------------------------- /chapter01-sentiment-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-sentiment-bot", 3 | "version": "1.0.0", 4 | "description": "Sentiment Bot from Chapter 1, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "botbuilder": "^3.8.2", 12 | "dotenv-extended": "^1.0.4", 13 | "request": "^2.81.0", 14 | "restify": "^4.3.0" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^4.10.0", 18 | "eslint-config-google": "^0.9.1", 19 | "eslint-config-standard": "^10.2.1", 20 | "eslint-plugin-import": "^2.8.0", 21 | "eslint-plugin-node": "^5.2.1", 22 | "eslint-plugin-promise": "^3.6.0", 23 | "eslint-plugin-standard": "^3.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /chapter01-youtube-bot/app.js: -------------------------------------------------------------------------------- 1 | // This loads the environment variables from the .env file 2 | require('dotenv-extended').load(); 3 | 4 | const builder = require('botbuilder'); 5 | const restify = require('restify'); 6 | const request = require('request'); 7 | const vsprintf = require('sprintf-js').vsprintf; 8 | 9 | const urlTemplate = 'https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q=%s&key=' + process.env.YOUTUBE_KEY; 10 | 11 | // Setup Restify Server 12 | const server = restify.createServer(); 13 | server.listen(process.env.port || process.env.PORT || 3978, () => { 14 | console.log('%s listening to %s', server.name, server.url); 15 | }); 16 | 17 | // Create chat bot and listen to messages 18 | const connector = new builder.ChatConnector({ 19 | appId: process.env.MICROSOFT_APP_ID, 20 | appPassword: process.env.MICROSOFT_APP_PASSWORD 21 | }); 22 | server.post('/api/messages', connector.listen()); 23 | 24 | const bot = new builder.UniversalBot(connector, [ 25 | session => { 26 | const url = vsprintf(urlTemplate, [session.message.text]); 27 | 28 | request.get(url, (err, response, body) => { 29 | if (err) { 30 | console.log('error while fetching video:\n' + err); 31 | session.endConversation('error while fetching video. please try again later.'); 32 | return; 33 | } 34 | 35 | const result = JSON.parse(body); 36 | // we have at most 5 results 37 | let cards = []; 38 | 39 | result.items.forEach(item => { 40 | const card = new builder.HeroCard(session) 41 | .title(item.snippet.title) 42 | .text(item.snippet.description) 43 | .images([ 44 | builder.CardImage.create(session, item.snippet.thumbnails.medium.url) 45 | ]) 46 | .buttons([ 47 | builder.CardAction.openUrl(session, 'https://www.youtube.com/watch?v=' + item.id.videoId, 'Watch Video') 48 | ]); 49 | cards.push(card); 50 | }); 51 | 52 | const reply = new builder.Message(session) 53 | .text('Here are some results for you') 54 | .attachmentLayout(builder.AttachmentLayout.carousel) 55 | .attachments(cards); 56 | 57 | session.send(reply); 58 | }); 59 | } 60 | ]); 61 | const inMemoryStorage = new builder.MemoryBotStorage(); 62 | bot.set('storage', inMemoryStorage); -------------------------------------------------------------------------------- /chapter01-youtube-bot/env.defaults: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | MICROSOFT_APP_ID= 3 | MICROSOFT_APP_PASSWORD= 4 | YOUTUBE_KEY= -------------------------------------------------------------------------------- /chapter01-youtube-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-youtube-search-bot", 3 | "version": "1.0.0", 4 | "description": "YouTube Search Bot from Chapter 1, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "botbuilder": "^3.11.0", 12 | "dotenv-extended": "^1.0.4", 13 | "request": "^2.83.0", 14 | "restify": "^4.3.1", 15 | "sprintf-js": "^1.1.1" 16 | }, 17 | "devDependencies": { 18 | "eslint": "^4.10.0", 19 | "eslint-config-google": "^0.9.1", 20 | "eslint-config-standard": "^10.2.1", 21 | "eslint-plugin-import": "^2.8.0", 22 | "eslint-plugin-node": "^5.2.1", 23 | "eslint-plugin-promise": "^3.6.0", 24 | "eslint-plugin-standard": "^3.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/README.md: -------------------------------------------------------------------------------- 1 | # Calendar Bot 2 | 3 | This bot is a demostration of how we can use the Microsoft Bot Builder SDK and LUIS to create a bot that is able to action on a calendar. Functionality includes add, remove, move appointments, summarize calendar and check availability. 4 | 5 | This git repo is: 6 | * chapter-5 - simple LUIS bot, no logic 7 | * chapter-7 - auth + api integration 8 | * chapter-10 - multi language support 9 | * master - equivalent to chapter 10 10 | 11 | ## Getting Started 12 | 13 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id, Bot Password, LUIS app id and LUIS subscription key. 14 | 15 | ### Installing and Running 16 | 17 | Easy. Peasy. 18 | 19 | ``` 20 | npm install 21 | npm start 22 | ``` 23 | 24 | By default, the bot will run on port 3978. Download the [Bot Framework Emulator](https://docs.microsoft.com/en-us/bot-framework/debug-bots-emulator) to test locally. -------------------------------------------------------------------------------- /chapter05-calendar-bot/Uploading-source-code.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter05-calendar-bot/Uploading-source-code.mp4 -------------------------------------------------------------------------------- /chapter05-calendar-bot/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | var builder = require('botbuilder'); 4 | var restify = require('restify'); 5 | var moment = require('moment'); 6 | var _ = require('underscore'); 7 | 8 | var constants = require('./constants'); 9 | 10 | // setup our web server 11 | var server = restify.createServer(); 12 | server.listen(process.env.port || process.env.PORT || 3978, function () { 13 | console.log('%s listening to %s', server.name, server.url); 14 | }); 15 | 16 | // initialize the chat bot 17 | var connector = new builder.ChatConnector({ 18 | appId: process.env.MICROSOFT_APP_ID, 19 | appPassword: process.env.MICROSOFT_APP_PASSWORD 20 | }); 21 | server.post('/api/messages', connector.listen()); 22 | 23 | var helpModule = require('./dialogs/help'); 24 | var addEntryModule = require('./dialogs/addEntry'); 25 | var removeEntryModule = require('./dialogs/removeEntry'); 26 | var editEntryModule = require('./dialogs/editEntry'); 27 | var checkAvailabilityModule = require('./dialogs/checkAvailability'); 28 | var summarizeModule = require('./dialogs/summarize'); 29 | 30 | var bot = new builder.UniversalBot(connector, [ 31 | function (session) { 32 | helpModule.help(session); 33 | } 34 | ]); 35 | bot.recognizer(new builder.LuisRecognizer('https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/' + process.env.LUIS_APP + '?subscription-key=' + process.env.LUIS_SUBSCRIPTION_KEY)); 36 | 37 | bot.library(addEntryModule.create()); 38 | bot.library(helpModule.create()); 39 | bot.library(removeEntryModule.create()); 40 | bot.library(editEntryModule.create()); 41 | bot.library(checkAvailabilityModule.create()); 42 | bot.library(summarizeModule.create()); -------------------------------------------------------------------------------- /chapter05-calendar-bot/constants.js: -------------------------------------------------------------------------------- 1 | exports.dialogNames = { 2 | Help: 'help', 3 | AddCalendarEntry: 'addCalendarEntry', 4 | AddCalendarEntryHelp: 'addCalendarEntryHelp', 5 | RemoveCalendarEntryHelp: 'removeCalendarEntryHelp', 6 | RemoveCalendarEntry: 'removeCalendarEntry', 7 | RemoveCalendarEntry_Time: 'removeCalendarEntry.time', 8 | RemoveCalendarEntry_Invitee: 'removeCalendarEntry.invitee', 9 | EditCalendarEntry: 'editCalendarEntry', 10 | ShowCalendarSummary: 'showCalendarSummary', 11 | CheckAvailability: 'checkAvailability' 12 | }; 13 | 14 | exports.intentNames = { 15 | Help: 'Help', 16 | AddCalendarEntry: 'AddCalendarEntry', 17 | RemoveCalendarEntry: 'DeleteCalendarEntry', 18 | CheckAvailability: 'CheckAvailability', 19 | EditCalendarEntry: 'EditCalendarEntry', 20 | ShowCalendarSummary: 'ShowCalendarSummary', 21 | None: 'None' 22 | }; 23 | 24 | exports.entityNames = { 25 | Chrono: 'chrono.duration', 26 | Invitee: 'CalendarBot.Invitee', 27 | Subject: 'CalendarBot.Subject', 28 | Location: 'CalendarBot.Location', 29 | Composite: { 30 | CalendarRequest: 'CalendarRequest' 31 | }, 32 | MeetingMove: { 33 | FromTime: 'MeetingMove::FromTime', 34 | ToTime: 'MeetingMove::ToTime' 35 | }, 36 | EntryVisibility: { 37 | Public: 'EntryVisbility::Public', 38 | Private: 'EntryVisbility::Private' 39 | }, 40 | Dates: { 41 | Date: 'builtin.datetimeV2.date', 42 | DateTime: 'builtin.datetimeV2.datetime', 43 | Time: 'builtin.datetimeV2.time', 44 | DateRange: 'builtin.datetimeV2.daterange', 45 | TimeRange: 'builtin.datetimeV2.timerange', 46 | DateTimeRange: 'builtin.datetimeV2.datetimerange', 47 | Set: 'builtin.datetimeV2.set', 48 | Duration: 'builtin.datetimeV2.duration' 49 | }, 50 | Email: 'builtin.email' 51 | }; 52 | 53 | exports.LUISTimePattern = 'HH:mm:ss'; 54 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/dialogs/addEntry.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const constants = require('../constants'); 3 | const et = require('../entityTranslator'); 4 | const utils = require('../utils'); 5 | const lib = new builder.Library('addEntry'); 6 | 7 | lib.dialog(constants.dialogNames.AddCalendarEntry, [ 8 | (session, args, next) => { 9 | // we need to figure out which entities we have in place and we start building our addEntry object 10 | const entry = new et.EntityTranslator(); 11 | et.EntityTranslatorUtils.attachAddEntities(entry, args.intent.entities); 12 | session.dialogData.addEntry = entry; 13 | next(); 14 | }, 15 | (session, results, next) => { 16 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 17 | 18 | if (!entry.hasDateTime) { 19 | // we collect the time in case it not defined in user's initial query 20 | builder.Prompts.time(session, 'When is this meeting?'); 21 | } 22 | else { 23 | next(); 24 | } 25 | }, 26 | (session, results, next) => { 27 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 28 | 29 | if (!entry.hasDateTime) { 30 | entry.setEntity(results.response); 31 | } 32 | 33 | // we HAVE to do this at each step of the waterfall, because sesison.dialogData serializes the object into a vanilla 34 | // js object, losing its identity. 35 | session.dialogData.addEntry = entry; 36 | 37 | if (!entry.hasSubject) { 38 | // collect meeting subject if not defined in user's initial query 39 | builder.Prompts.text(session, 'What is this meeting about?') 40 | } 41 | else { 42 | next(); 43 | } 44 | }, 45 | (session, results, next) => { 46 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 47 | 48 | if (!entry.hasSubject) { 49 | entry.setSubjectEntity(utils.wrapEntity(constants.entityNames.Subject, results.response)); 50 | } 51 | 52 | session.dialogData.addEntry = entry; 53 | 54 | // collect meeting location if not defined in user's initial query 55 | if (!entry.hasLocation) { 56 | builder.Prompts.text(session, 'Where is this meeting happening?') 57 | } 58 | else { 59 | next(); 60 | } 61 | 62 | }, 63 | (session, results, next) => { 64 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 65 | 66 | if (!entry.hasLocation) { 67 | entry.setLocationEntity(utils.wrapEntity(constants.entityNames.Location, results.response)); 68 | } 69 | 70 | session.dialogData.addEntry = entry; 71 | 72 | next(); 73 | }, 74 | (session, results) => { 75 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 76 | 77 | console.log(JSON.stringify(entry)); 78 | // TODO: take the data and call an API to add the calendar entry 79 | 80 | session.endDialog('Your appointment has been added!'); 81 | } 82 | ]).beginDialogAction(constants.dialogNames.AddCalendarEntryHelp, constants.dialogNames.AddCalendarEntryHelp, { matches: constants.intentNames.Help }) 83 | .triggerAction({ matches: constants.intentNames.AddCalendarEntry }); 84 | 85 | exports.create = () => { return lib.clone(); } 86 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/dialogs/checkAvailability.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const constants = require('../constants'); 3 | const et = require('../entityTranslator'); 4 | const lib = new builder.Library('checkAvailability'); 5 | 6 | lib.dialog(constants.dialogNames.CheckAvailability, [ 7 | (session, args, next) => { 8 | const entry = new et.EntityTranslator(); 9 | et.EntityTranslatorUtils.attachCheckAvailabilityEntities(entry, args.intent.entities); 10 | session.dialogData.entry = entry; 11 | 12 | if (!entry.hasDateTime && !entry.hasRange) { 13 | builder.Prompts.time(session, 'When are we checking availability for?'); 14 | } else { 15 | next(); 16 | } 17 | }, 18 | (session, results, next) => { 19 | const entry = new et.EntityTranslator(session.dialogData.entry); 20 | 21 | if (!entry.hasDateTime && !entry.hasRange) { 22 | entry.setEntity(results.response); // set the datetime entity 23 | } 24 | 25 | const target = entry.hasInvitee ? 'availability for ' + entry.invitee : 'my avalability '; 26 | if (entry.hasRange) { 27 | if (entry.isDateTimeEntityDateBased) { 28 | session.endDialog('checking ' + target + ' between ' + entry.range.start.format('L') + ' and ' + entry.range.end.format('L')); 29 | } else { 30 | session.endDialog('checking ' + target + ' between ' + entry.range.start.format('L LT') + ' and ' + entry.range.end.format('L LT')); 31 | } 32 | } else if (entry.hasDateTime) { 33 | if (entry.isDateTimeEntityDateBased) { 34 | session.endDialog('checking ' + target + ' on ' + entry.dateTime.format('L')); 35 | } else { 36 | session.endDialog('checking ' + target + ' on ' + entry.dateTime.format('L LT')); 37 | } 38 | } 39 | } 40 | ]).triggerAction({ matches: constants.intentNames.CheckAvailability }); 41 | exports.create = () => { return lib.clone(); } 42 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/dialogs/editEntry.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const _ = require('underscore'); 3 | const constants = require('../constants'); 4 | const mt = require('../moveTranslator'); 5 | const lib = new builder.Library('editEntry'); 6 | 7 | /* Building out an entire edit functionality can be a large undertaking. There are many possible attributes to edit in many possible ways. We start with the ability 8 | * to move appointments using natural language */ 9 | lib.dialog(constants.dialogNames.EditCalendarEntry, [ 10 | (session, args, next) => { 11 | // move from and move to are helper entities to help us figure out where the from and to times like. The LUIS range datetime 12 | // entities can be good but you could imagine a user phrasing the query differently. for example, move 4p meeting to 6pm, 13 | // would not be identified as a range 14 | const moveFrom = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.FromTime); 15 | const moveTo = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.ToTime); 16 | 17 | const allOtherEntities = _.where(args.intent.entities, function (p) { 18 | return p.type != constants.entityNames.MeetingMove.FromTime && p.type != constants.entityNames.MeetingMove.ToTime; 19 | }); 20 | 21 | // use the move translator to properly identiy the from and to 22 | const moveTranslator = new mt.MoveTranslator(); 23 | moveTranslator.applyEntities(moveTo, allOtherEntities, moveFrom); 24 | 25 | let prefix = 'Moving '; 26 | 27 | if (moveTranslator.hasSubject) prefix += (moveTranslator.subject + ' '); 28 | else prefix += 'meeting '; 29 | 30 | if (moveTranslator.hasInvitee) prefix += ('with ' + moveTranslator.invitee + ' '); 31 | 32 | if (moveTranslator.moveFrom && moveTranslator.moveTo) { 33 | if (moveTranslator.isDateBased) { 34 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L') + ' to ' + moveTranslator.moveTo.format('L')); 35 | } else { 36 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L LT') + ' to ' + moveTranslator.moveTo.format('L LT')); 37 | } 38 | } else if (!moveTranslator.moveFrom && moveTranslator.moveTo) { 39 | if (moveTranslator.isDateBased) { 40 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L')); 41 | } else { 42 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L LT')); 43 | } 44 | } else if (!moveTranslator.moveFrom && !moveTranslator.moveTo) { 45 | session.endDialog("I'm sorry, I'm not sure how to handle that request."); 46 | } 47 | } 48 | ]).triggerAction({ matches: constants.intentNames.EditCalendarEntry }); 49 | 50 | exports.create = () => { return lib.clone(); } 51 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/dialogs/help.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const constants = require('../constants'); 3 | const lib = new builder.Library('help'); 4 | 5 | exports.help = session => { 6 | session.beginDialog('help:' + constants.dialogNames.Help); 7 | }; 8 | 9 | // help message when help requested during the add calendar entry dialog 10 | lib.dialog(constants.dialogNames.AddCalendarEntryHelp, (session, args, next) => { 11 | var msg = "To add an appointment, we gather the following information: time, subject and location. You can also simply say 'add appointment with Bob tomorrow at 2pm for an hour for coffee' and we'll take it from there!"; 12 | session.endDialog(msg); 13 | }); 14 | 15 | // help message when help requested during the remove calendar entry dialog 16 | lib.dialog(constants.dialogNames.RemoveCalendarEntryHelp, (session, args, next) => { 17 | var msg = ""; 18 | session.endDialog(msg); 19 | }); 20 | 21 | // top level help 22 | lib.dialog(constants.dialogNames.Help, (session, args, next) => { 23 | session.endDialog("Hi, I am a calendar concierge bot. I can help you create, delete and move appointments. I can also tell you about your calendar and check your availability!"); 24 | }).triggerAction({ 25 | matches: constants.intentNames.Help, 26 | onSelectAction: (session, args, next) => { 27 | session.beginDialog(args.action, args); 28 | } 29 | }); 30 | 31 | exports.create = () => { return lib.clone(); } 32 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/dialogs/removeEntry.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const constants = require('../constants'); 3 | const et = require('../entityTranslator'); 4 | const utils = require('../utils'); 5 | 6 | const lib = new builder.Library('removeEntry'); 7 | 8 | lib.dialog(constants.dialogNames.RemoveCalendarEntry, [ 9 | (session, args, next) => { 10 | // we support deleting an appointment by finding it via the time or invitee or a combination of both 11 | const entry = new et.EntityTranslator(); 12 | et.EntityTranslatorUtils.attachRemoveEntities(entry, args.intent.entities); 13 | session.dialogData.entry = entry; 14 | next(); 15 | }, 16 | (session, results, next) => { 17 | const entry = new et.EntityTranslator(session.dialogData.entry); 18 | if (!entry.hasDateTime && !entry.hasInvitee) { 19 | session.dialogData.collectingTime = true; 20 | builder.Prompts.time(session, 'Which time do you want to clear?'); 21 | } 22 | else { 23 | next(); 24 | } 25 | }, 26 | (session, results, next) => { 27 | const entry = new et.EntityTranslator(session.dialogData.entry); 28 | 29 | if (session.dialogData.collectingTime) { 30 | entry.setEntity(results.response); 31 | } 32 | 33 | // if we have an invitee, we kick off the logic that deletes entries based on the invitee information or 34 | // both invitee + date/datetime, otherwise try to remove by time 35 | if (entry.hasInvitee) { 36 | session.beginDialog(constants.dialogNames.RemoveCalendarEntry_Invitee, { entry: entry }); 37 | } else if (entry.hasDateTime) { 38 | session.beginDialog(constants.dialogNames.RemoveCalendarEntry_Time, { entry: entry }); 39 | } else { 40 | session.endDialog("Hmm, I'm having trouble with that, please try again."); 41 | } 42 | }, 43 | (session, results, next) => { 44 | // results.success tells us whether the sub dialog has completed in the happy path 45 | session.endDialog(results.message); 46 | } 47 | ]).beginDialogAction(constants.dialogNames.RemoveCalendarEntryHelp, constants.dialogNames.RemoveCalendarEntryHelp, { matches: constants.intentNames.Help }) 48 | .triggerAction({ matches: constants.intentNames.RemoveCalendarEntry }); 49 | 50 | lib.dialog(constants.dialogNames.RemoveCalendarEntry_Time, [ 51 | (session, args) => { 52 | const entry = new et.EntityTranslator(args.entry); 53 | // if there is no time passed, end dialog. this path should fail. 54 | if (!entry.hasDateTime) { 55 | session.endDialogWithResult({ success: false, message: "I'm not sure about that. Please try again." }); 56 | return; 57 | } 58 | 59 | // TODO: logic to remove calendar entry. may need a confirmation dialog if 60 | // have multiple meetings at the same time. 61 | session.endDialog('removing entry for ' + entry.dateTime.format()); 62 | } 63 | ]); 64 | 65 | lib.dialog(constants.dialogNames.RemoveCalendarEntry_Invitee, [ 66 | (session, args) => { 67 | const entry = new et.EntityTranslator(args.entry); 68 | 69 | if (!entry.hasInvitee) { 70 | session.endDialogWithResult({ success: false, message: "I'm not sure about that. Please try again." }); 71 | return; 72 | } 73 | 74 | if (!entry.hasDateTime) { 75 | session.endDialog('removing entry for appointment with ' + entry.invitee); 76 | // just search for existing appointment by invitee 77 | } else if (entry.isDateTimeEntityDateBased) { 78 | session.endDialog('removing entry for appointment with ' + entry.invitee + ' on ' + entry.dateTime.format('l')); 79 | // search for meetings withn invitee on this date 80 | } else { 81 | session.endDialog('removing entry for appointment with ' + entry.invitee + 'at ' + entry.dateTime.format()); 82 | // search for meeting with invitee at this specific datetime. 83 | // TODO: if not found make suggestions 84 | } 85 | } 86 | ]); 87 | 88 | exports.create = () => { return lib.clone(); } 89 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/dialogs/summarize.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const constants = require('../constants'); 3 | const et = require('../entityTranslator'); 4 | const lib = new builder.Library('summarize'); 5 | 6 | lib.dialog(constants.dialogNames.ShowCalendarSummary, [ 7 | (session, args, next) => { 8 | const entry = new et.EntityTranslator(); 9 | et.EntityTranslatorUtils.attachSummaryEntities(entry, args.intent.entities); 10 | 11 | // at this point, the entry can have dateTime or range and/or subject and/or invitee. 12 | let suffix = ''; 13 | if (entry.hasInvitee || entry.hasSubject) { 14 | suffix += ' for '; 15 | if (entry.hasSubject) suffix += entry.subject; 16 | else suffix += 'meeting' 17 | if (entry.hasInvitee) suffix += ' with ' + entry.invitee; 18 | } 19 | 20 | if (entry.hasRange) { 21 | if (entry.isDateTimeEntityDateBased) { 22 | session.endDialog('summary between ' + entry.range.start.format('L') + ' and ' + entry.range.end.format('L') + suffix); 23 | } else { 24 | session.endDialog('summary between ' + entry.range.start.format('L LT') + ' and ' + entry.range.end.format('L LT') + suffix); 25 | } 26 | } else if (entry.hasDateTime) { 27 | if (entry.isDateTimeEntityDateBased) { 28 | session.endDialog('summary on ' + entry.dateTime.format('L') + suffix); 29 | } else { 30 | session.endDialog('summary on ' + entry.dateTime.format('L LT') + suffix); 31 | } 32 | } 33 | else { 34 | session.endDialog("Sorry I don't know what you mean"); 35 | } 36 | 37 | } 38 | ]).triggerAction({ matches: constants.intentNames.ShowCalendarSummary }); 39 | 40 | exports.create = () => { return lib.clone(); } 41 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/env.defaults: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | MICROSOFT_APP_ID= 3 | MICROSOFT_APP_PASSWORD= 4 | LUIS_APP= 5 | LUIS_SUBSCRIPTION_KEY= -------------------------------------------------------------------------------- /chapter05-calendar-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-dialog-basics", 3 | "version": "1.0.0", 4 | "description": "Calendar Bot Basic Dialog Bot - Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "botbuilder": "^3.9.1", 12 | "dotenv-extended": "^1.0.4", 13 | "momentjs": "^2.0.0", 14 | "restify": "^4.3.0", 15 | "underscore": "^1.8.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /chapter05-calendar-bot/utils.js: -------------------------------------------------------------------------------- 1 | exports.wrapEntity = function(entityType, value) { 2 | return { 3 | type: entityType, 4 | entity: value 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/README.md: -------------------------------------------------------------------------------- 1 | # Calendar Bot 2 | 3 | This bot is a demostration of how we can use the Microsoft Bot Builder SDK and LUIS to create a bot that is able to action on a calendar. Functionality includes add, remove, move appointments, summarize calendar and check availability. 4 | 5 | This git repo is: 6 | * chapter-5 - simple LUIS bot, no logic 7 | * chapter-7 - auth + api integration 8 | * chapter-10 - multi language support 9 | * master - equivalent to chapter 10 10 | 11 | ## Getting Started 12 | 13 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id, Bot Password, LUIS app id and LUIS subscription key. 14 | 15 | ### Installing and Running 16 | 17 | Easy. Peasy. 18 | 19 | ``` 20 | npm install 21 | npm start 22 | ``` 23 | 24 | By default, the bot will run on port 3978. Download the [Bot Framework Emulator](https://docs.microsoft.com/en-us/bot-framework/debug-bots-emulator) to test locally. -------------------------------------------------------------------------------- /chapter07-calendar-bot/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | const builder = require('botbuilder'); 4 | const restify = require('restify'); 5 | 6 | const constants = require('./constants'); 7 | const utils = require('./utils'); 8 | 9 | const helpModule = require('./dialogs/help'); 10 | const authModule = require('./dialogs/auth'); 11 | const addEntryModule = require('./dialogs/addEntry'); 12 | const removeEntryModule = require('./dialogs/removeEntry'); 13 | const editEntryModule = require('./dialogs/editEntry'); 14 | const checkAvailabilityModule = require('./dialogs/checkAvailability'); 15 | const summarizeModule = require('./dialogs/summarize'); 16 | const primaryCalendarModule = require('./dialogs/primaryCalendar'); 17 | const prechecksModule = require('./dialogs/prechecks'); 18 | 19 | authModule.setResolvePostLoginDialog((session, args) => { 20 | if (!session.privateConversationData.calendarId) { 21 | args.followUpDialog = primaryCalendarModule.getPrimaryCalendarDialogName(); 22 | args.followUpDialogArgs = { 23 | intent: { 24 | entities: [ 25 | utils.wrapEntity(constants.entityNames.Action, constants.entityValues.Action.set) 26 | ] 27 | } 28 | }; 29 | } 30 | return args; 31 | }); 32 | 33 | // setup our web server 34 | const server = restify.createServer(); 35 | server.use(restify.queryParser()); 36 | server.listen(process.env.port || process.env.PORT || 3978, () => { 37 | console.log('%s listening to %s', server.name, server.url); 38 | }); 39 | 40 | // initialize the chat bot 41 | const connector = new builder.ChatConnector({ 42 | appId: process.env.MICROSOFT_APP_ID, 43 | appPassword: process.env.MICROSOFT_APP_PASSWORD 44 | }); 45 | 46 | const bot = new builder.UniversalBot(connector, [ 47 | session => { 48 | helpModule.help(session); 49 | } 50 | ]); 51 | server.post('/api/messages', connector.listen()); 52 | server.get('/oauth2callback', (req, res, next) => { 53 | authModule.oAuth2Callback(bot, req, res, next); 54 | }); 55 | 56 | const luisModelUri = 'https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/' + process.env.LUIS_APP + '?subscription-key=' + process.env.LUIS_SUBSCRIPTION_KEY; 57 | bot.recognizer(new builder.LuisRecognizer(luisModelUri)); 58 | 59 | bot.library(addEntryModule.create()); 60 | bot.library(helpModule.create()); 61 | bot.library(authModule.create()); 62 | bot.library(removeEntryModule.create()); 63 | bot.library(editEntryModule.create()); 64 | bot.library(checkAvailabilityModule.create()); 65 | bot.library(summarizeModule.create()); 66 | bot.library(primaryCalendarModule.create()); 67 | bot.library(prechecksModule.create()); 68 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/constants.js: -------------------------------------------------------------------------------- 1 | exports.dialogNames = { 2 | Help: 'help', 3 | AddCalendarEntry: 'addCalendarEntry', 4 | AddCalendarEntryHelp: 'addCalendarEntryHelp', 5 | RemoveCalendarEntryHelp: 'removeCalendarEntryHelp', 6 | RemoveCalendarEntry: 'removeCalendarEntry', 7 | RemoveCalendarEntry_Time: 'removeCalendarEntry.time', 8 | RemoveCalendarEntry_Invitee: 'removeCalendarEntry.invitee', 9 | EditCalendarEntry: 'editCalendarEntry', 10 | ShowCalendarSummary: 'showCalendarSummary', 11 | CheckAvailability: 'checkAvailability', 12 | Login: 'login', 13 | PrimaryCalendar: 'primaryCalendar', 14 | EnsurePrimaryCalendar: 'ensurePrimaryCalendar', 15 | PreCheck_AuthAndPrimaryCalendar: 'preCheck_authAndPrimaryCalendar', 16 | Auth: { 17 | Login: 'auth.login', 18 | Logout: 'auth.logout', 19 | EnsureCredentials: 'auth.ensureCredentials', 20 | AuthConfirmation: 'auth.authConfirmation', 21 | StoreTokensAndResume: 'auth.storeAndResume', 22 | Error: 'auth.error' 23 | } 24 | }; 25 | 26 | exports.intentNames = { 27 | Help: 'Help', 28 | AddCalendarEntry: 'AddCalendarEntry', 29 | RemoveCalendarEntry: 'DeleteCalendarEntry', 30 | CheckAvailability: 'CheckAvailability', 31 | EditCalendarEntry: 'EditCalendarEntry', 32 | ShowCalendarSummary: 'ShowCalendarSummary', 33 | PrimaryCalendar: 'PrimaryCalendar', 34 | None: 'None' 35 | }; 36 | 37 | exports.entityNames = { 38 | Chrono: 'chrono.duration', 39 | Invitee: 'CalendarBot.Invitee', 40 | Action: 'Action', 41 | CalendarId: 'CalendarId', 42 | EventId: 'EventId', 43 | Subject: 'CalendarBot.Subject', 44 | Location: 'CalendarBot.Location', 45 | Composite: { 46 | CalendarRequest: 'CalendarRequest' 47 | }, 48 | MeetingMove: { 49 | FromTime: 'MeetingMove::FromTime', 50 | ToTime: 'MeetingMove::ToTime' 51 | }, 52 | EntryVisibility: { 53 | Public: 'EntryVisbility::Public', 54 | Private: 'EntryVisbility::Private' 55 | }, 56 | Dates: { 57 | Date: 'builtin.datetimeV2.date', 58 | DateTime: 'builtin.datetimeV2.datetime', 59 | Time: 'builtin.datetimeV2.time', 60 | DateRange: 'builtin.datetimeV2.daterange', 61 | TimeRange: 'builtin.datetimeV2.timerange', 62 | DateTimeRange: 'builtin.datetimeV2.datetimerange', 63 | Set: 'builtin.datetimeV2.set', 64 | Duration: 'builtin.datetimeV2.duration' 65 | }, 66 | Email: 'builtin.email' 67 | }; 68 | 69 | exports.entityValues = { 70 | Action: { 71 | get: 'get', 72 | set: 'set', 73 | clear: 'remove' 74 | } 75 | }; 76 | 77 | exports.LUISTimePattern = 'HH:mm:ss'; 78 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/dialogs/addEntry.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const moment = require('moment'); 3 | 4 | const constants = require('../constants'); 5 | const et = require('../entityTranslator'); 6 | const utils = require('../utils'); 7 | const gcalapi = require('../services/calendar-api'); 8 | 9 | const authModule = require('./auth'); 10 | const prechecksModule = require('./prechecks'); 11 | 12 | const lib = new builder.Library('addEntry'); 13 | 14 | lib.dialog(constants.dialogNames.AddCalendarEntry, [ 15 | (session, args) => { 16 | session.dialogData.intent = args.intent; 17 | prechecksModule.ensurePrechecks(session); 18 | }, 19 | (session, args, next) => { 20 | if (args.response.error) { 21 | session.endDialog(args.response.error); 22 | return; 23 | } 24 | next(); 25 | }, 26 | (session, args, next) => { 27 | // we need to figure out which entities we have in place and we start building our addEntry object 28 | const entry = new et.EntityTranslator(); 29 | et.EntityTranslatorUtils.attachAddEntities(entry, session.dialogData.intent.entities); 30 | session.dialogData.addEntry = entry; 31 | next(); 32 | }, 33 | (session, results, next) => { 34 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 35 | 36 | if (!entry.hasDateTime) { 37 | // we collect the time in case it not defined in user's initial query 38 | builder.Prompts.time(session, 'When is this meeting?'); 39 | } else { 40 | next(); 41 | } 42 | }, 43 | (session, results, next) => { 44 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 45 | 46 | if (!entry.hasDateTime) { 47 | entry.setEntity(results.response); 48 | } 49 | 50 | // we HAVE to do this at each step of the waterfall, because sesison.dialogData serializes the object into a vanilla 51 | // js object, losing its identity. 52 | session.dialogData.addEntry = entry; 53 | 54 | if (!entry.hasSubject) { 55 | // collect meeting subject if not defined in user's initial query 56 | builder.Prompts.text(session, 'What is this meeting about?') 57 | } else { 58 | next(); 59 | } 60 | }, 61 | (session, results, next) => { 62 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 63 | 64 | if (!entry.hasSubject) { 65 | entry.setSubjectEntity(utils.wrapEntity(constants.entityNames.Subject, results.response)); 66 | } 67 | 68 | session.dialogData.addEntry = entry; 69 | 70 | // collect meeting location if not defined in user's initial query 71 | if (!entry.hasLocation) { 72 | builder.Prompts.text(session, 'Where is this meeting happening?') 73 | } else { 74 | next(); 75 | } 76 | }, 77 | (session, results, next) => { 78 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 79 | 80 | if (!entry.hasLocation) { 81 | entry.setLocationEntity(utils.wrapEntity(constants.entityNames.Location, results.response)); 82 | } 83 | 84 | session.dialogData.addEntry = entry; 85 | 86 | next(); 87 | }, 88 | (session, results) => { 89 | const entry = new et.EntityTranslator(session.dialogData.addEntry); 90 | const auth = authModule.getAuthClientFromSession(session); 91 | 92 | let start, end; 93 | let p = null; 94 | if (entry.hasRange) { 95 | start = entry.range.start; 96 | end = entry.range.end; 97 | if (entry.isDateTimeEntityDateBased) { 98 | p = gcalapi.insertEvent(auth, session.privateConversationData.calendarId, start, end, entry.subject, entry.location); 99 | } else { 100 | p = gcalapi.insertAllDayEvent(auth, session.privateConversationData.calendarId, start, end, entry.subject, entry.location); 101 | } 102 | } else { 103 | start = entry.dateTime; 104 | if (!entry.isDateTimeEntityDateBased) { 105 | if (entry.hasDuration) { 106 | end = moment(start).add(entry.duration, 's'); 107 | } else { 108 | end = moment(start).add(30, 'm'); 109 | } 110 | 111 | p = gcalapi.insertEvent(auth, session.privateConversationData.calendarId, start, end, entry.subject, entry.location); 112 | } else { 113 | end = moment(start); 114 | 115 | p = gcalapi.insertAllDayEvent(auth, session.privateConversationData.calendarId, start, end, entry.subject, entry.location); 116 | } 117 | } 118 | 119 | p.then(result => { 120 | let evCard = utils.createEventCard(session, result); 121 | let msg = new builder.Message(session).text('Your appointment has been added.').attachmentLayout(builder.AttachmentLayout.carousel) 122 | .attachments([evCard]); 123 | session.send(msg); 124 | session.endDialog(); 125 | }).catch(err => { 126 | console.log(err); 127 | }); 128 | } 129 | ]).beginDialogAction(constants.dialogNames.AddCalendarEntryHelp, constants.dialogNames.AddCalendarEntryHelp, { matches: constants.intentNames.Help }) 130 | .triggerAction({ matches: constants.intentNames.AddCalendarEntry }); 131 | 132 | exports.create = () => { return lib.clone(); } 133 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/dialogs/editEntry.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const _ = require('underscore'); 3 | 4 | const constants = require('../constants'); 5 | const mt = require('../moveTranslator'); 6 | 7 | const lib = new builder.Library('editEntry'); 8 | 9 | /* Building out an entire edit functionality can be a large undertaking. There are many possible attributes to edit in many possible ways. We start with the ability 10 | * to move appointments using natural language */ 11 | lib.dialog(constants.dialogNames.EditCalendarEntry, [ 12 | (session, args, next) => { 13 | // move from and move to are helper entities to help us figure out where the from and to times like. The LUIS range datetime 14 | // entities can be good but you could imagine a user phrasing the query differently. for example, move 4p meeting to 6pm, 15 | // would not be identified as a range 16 | const moveFrom = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.FromTime); 17 | const moveTo = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.ToTime); 18 | 19 | const allOtherEntities = _.where(args.intent.entities, function (p) { 20 | return p.type !== constants.entityNames.MeetingMove.FromTime && p.type !== constants.entityNames.MeetingMove.ToTime; 21 | }); 22 | 23 | // use the move translator to properly identiy the from and to 24 | const moveTranslator = new mt.MoveTranslator(); 25 | moveTranslator.applyEntities(moveTo, allOtherEntities, moveFrom); 26 | 27 | let prefix = 'Moving '; 28 | 29 | if (moveTranslator.hasSubject) prefix += (moveTranslator.subject + ' '); 30 | else prefix += 'meeting '; 31 | 32 | if (moveTranslator.hasInvitee) prefix += ('with ' + moveTranslator.invitee + ' '); 33 | 34 | if (moveTranslator.moveFrom && moveTranslator.moveTo) { 35 | if (moveTranslator.isDateBased) { 36 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L') + ' to ' + moveTranslator.moveTo.format('L')); 37 | } else { 38 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L LT') + ' to ' + moveTranslator.moveTo.format('L LT')); 39 | } 40 | } else if (!moveTranslator.moveFrom && moveTranslator.moveTo) { 41 | if (moveTranslator.isDateBased) { 42 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L')); 43 | } else { 44 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L LT')); 45 | } 46 | } else if (!moveTranslator.moveFrom && !moveTranslator.moveTo) { 47 | session.endDialog("I'm sorry, I'm not sure how to handle that request."); 48 | } 49 | } 50 | ]).triggerAction({ matches: constants.intentNames.EditCalendarEntry }); 51 | 52 | exports.create = () => { return lib.clone(); } 53 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/dialogs/help.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | 3 | const constants = require('../constants'); 4 | 5 | const lib = new builder.Library('help'); 6 | 7 | exports.help = (session) => { 8 | session.beginDialog('help:' + constants.dialogNames.Help); 9 | }; 10 | 11 | // help message when help requested during the add calendar entry dialog 12 | lib.dialog(constants.dialogNames.AddCalendarEntryHelp, (session, args, next) => { 13 | const msg = "To add an appointment, we gather the following information: time, subject and location. You can also simply say 'add appointment with Bob tomorrow at 2pm for an hour for coffee' and we'll take it from there!"; 14 | session.endDialog(msg); 15 | }); 16 | 17 | // help message when help requested during the remove calendar entry dialog 18 | lib.dialog(constants.dialogNames.RemoveCalendarEntryHelp, (session, args, next) => { 19 | const msg = ''; 20 | session.endDialog(msg); 21 | }); 22 | 23 | // top level help 24 | lib.dialog(constants.dialogNames.Help, (session, args, next) => { 25 | session.endDialog('Hi, I am a calendar concierge bot. I can help you create, delete and move appointments. I can also tell you about your calendar and check your availability!'); 26 | }).triggerAction({ 27 | matches: constants.intentNames.Help, 28 | onSelectAction: (session, args, next) => { 29 | session.beginDialog(args.action, args); 30 | } 31 | }); 32 | 33 | exports.create = () => { return lib.clone(); } 34 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/dialogs/prechecks.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | 3 | const constants = require('../constants'); 4 | 5 | const authModule = require('./auth'); 6 | const primaryCalendarModule = require('./primaryCalendar'); 7 | 8 | const libName = 'prechecks'; 9 | const lib = new builder.Library(libName); 10 | 11 | lib.dialog(constants.dialogNames.PreCheck_AuthAndPrimaryCalendar, [ 12 | (session, args) => { 13 | authModule.ensureLoggedIn(session); 14 | }, 15 | (session, args) => { 16 | if (!args.response.authenticated) { 17 | session.endDialogWithResult({ response: { error: 'You must authenticate to continue.', error_auth: true } }); 18 | } else { 19 | primaryCalendarModule.ensurePrimaryCalendar(session); 20 | } 21 | }, 22 | (session, args, next) => { 23 | if (session.privateConversationData.calendarId) session.endDialogWithResult({ response: { } }); 24 | else session.endDialogWithResult({ response: { error: 'You must set a primary calendar to continue.', error_calendar: true } }); 25 | } 26 | ]); 27 | 28 | exports.create = () => { return lib.clone(); } 29 | exports.ensurePrechecks = session => { 30 | session.beginDialog(libName + ':' + constants.dialogNames.PreCheck_AuthAndPrimaryCalendar); 31 | }; 32 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/dialogs/summarize.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const _ = require('underscore'); 3 | const moment = require('moment'); 4 | 5 | const utils = require('../utils'); 6 | const constants = require('../constants'); 7 | const et = require('../entityTranslator'); 8 | const gcalapi = require('../services/calendar-api'); 9 | 10 | const authModule = require('./auth'); 11 | const prechecksModule = require('./prechecks'); 12 | 13 | const lib = new builder.Library('summarize'); 14 | 15 | lib.dialog(constants.dialogNames.ShowCalendarSummary, [ 16 | function (session, args) { 17 | session.dialogData.intent = args.intent; 18 | prechecksModule.ensurePrechecks(session); 19 | }, 20 | function (session, args, next) { 21 | if (args.response.error) { 22 | session.endDialog(args.response.error); 23 | return; 24 | } 25 | next(); 26 | }, 27 | function (session, args, next) { 28 | const auth = authModule.getAuthClientFromSession(session); 29 | const entry = new et.EntityTranslator(); 30 | et.EntityTranslatorUtils.attachSummaryEntities(entry, session.dialogData.intent.entities); 31 | let start = null; 32 | let end = null; 33 | 34 | if (entry.hasRange) { 35 | if (entry.isDateTimeEntityDateBased) { 36 | start = moment(entry.range.start).startOf('day'); 37 | end = moment(entry.range.end).endOf('day'); 38 | } else { 39 | start = moment(entry.range.start); 40 | end = moment(entry.range.end); 41 | } 42 | } else if (entry.hasDateTime) { 43 | if (entry.isDateTimeEntityDateBased) { 44 | start = moment(entry.dateTime).startOf('day'); 45 | end = moment(entry.dateTime).endOf('day'); 46 | } else { 47 | start = moment(entry.dateTime).add(-1, 'h'); 48 | end = moment(entry.dateTime).add(1, 'h'); 49 | } 50 | } else { 51 | session.endDialog("Sorry I don't know what you mean"); 52 | return; 53 | } 54 | 55 | const p = gcalapi.listEvents(auth, session.privateConversationData.calendarId, start, end); 56 | p.then(function (events) { 57 | let evs = _.sortBy(events, function (p) { 58 | if (p.start.date) { 59 | return moment(p.start.date).add(-1, 's').valueOf(); 60 | } else if (p.start.dateTime) { 61 | return moment(p.start.dateTime).valueOf(); 62 | } 63 | }); 64 | 65 | // should also potentially filter by subject 66 | evs = _.filter(evs, function (p) { 67 | if (!entry.hasSubject) return true; 68 | 69 | const containsSubject = entry.subject.toLowerCase().indexOf(entry.subject.toLowerCase()) >= 0; 70 | return containsSubject; 71 | }); 72 | 73 | const eventmsg = new builder.Message(session); 74 | if (evs.length > 1) { 75 | eventmsg.text('Here is what I found...'); 76 | } else if (evs.length === 1) { 77 | eventmsg.text('Here is the event I found.'); 78 | } else { 79 | eventmsg.text('Seems you have nothing going on then. What a sad existence you lead.'); 80 | } 81 | 82 | if (evs.length >= 1) { 83 | const cards = _.map(evs, function (p) { 84 | return utils.createEventCard(session, p); 85 | }); 86 | eventmsg.attachmentLayout(builder.AttachmentLayout.carousel); 87 | eventmsg.attachments(cards); 88 | } 89 | 90 | session.send(eventmsg); 91 | session.endDialog(); 92 | }); 93 | } 94 | ]).triggerAction({ matches: constants.intentNames.ShowCalendarSummary }); 95 | 96 | exports.create = function () { return lib.clone(); } 97 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/env.defaults: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | MICROSOFT_APP_ID= 3 | MICROSOFT_APP_PASSWORD= 4 | LUIS_APP= 5 | LUIS_SUBSCRIPTION_KEY= 6 | 7 | # Google OAuth Bits 8 | GOOGLE_OAUTH_CLIENT_ID= 9 | GOOGLE_OAUTH_CLIENT_SECRET= 10 | GOOGLE_OAUTH_REDIRECT_URI= 11 | 12 | # Security 13 | AES_PASSPHRASE= 14 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-calendar-bot-buildup", 3 | "version": "1.0.0", 4 | "description": "Calendar Bot - Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "botbuilder": "^3.13.1", 12 | "crypto-js": "^3.1.9-1", 13 | "dotenv-extended": "^1.0.4", 14 | "googleapis": "^22.2.0", 15 | "moment": "^2.19.4", 16 | "request": "^2.83.0", 17 | "restify": "^4.3.2", 18 | "underscore": "^1.8.3" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^4.10.0", 22 | "eslint-config-google": "^0.9.1", 23 | "eslint-config-standard": "^10.2.1", 24 | "eslint-plugin-import": "^2.8.0", 25 | "eslint-plugin-node": "^5.2.1", 26 | "eslint-plugin-promise": "^3.6.0", 27 | "eslint-plugin-standard": "^3.0.1", 28 | "jasmine-node": "^1.14.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/services/calendar-api.js: -------------------------------------------------------------------------------- 1 | const google = require('googleapis'); 2 | const calendar = google.calendar('v3'); 3 | 4 | function listEvents (auth, calendarId, start, end, subject) { 5 | const p = new Promise(function (resolve, reject) { 6 | calendar.events.list({ 7 | auth: auth, 8 | calendarId: calendarId, 9 | timeMin: start.toISOString(), 10 | timeMax: end.toISOString(), 11 | q: subject 12 | }, function (err, response) { 13 | if (err) reject(err); 14 | resolve(response.items); 15 | }); 16 | }); 17 | return p; 18 | } 19 | 20 | function listCalendars (auth) { 21 | const p = new Promise(function (resolve, reject) { 22 | calendar.calendarList.list({ 23 | auth: auth 24 | }, function (err, response) { 25 | if (err) reject(err); 26 | else resolve(response.items); 27 | }); 28 | }); 29 | return p; 30 | }; 31 | 32 | function getCalendar (auth, calendarId) { 33 | const p = new Promise(function (resolve, reject) { 34 | calendar.calendarList.get({ 35 | auth: auth, 36 | calendarId: calendarId 37 | }, function (err, response) { 38 | if (err) reject(err); 39 | else resolve(response); 40 | }); 41 | }); 42 | return p; 43 | }; 44 | 45 | function freeBusy (auth, calendarId, start, end) { 46 | const p = new Promise(function (resolve, reject) { 47 | const param = { 48 | auth: auth, 49 | resource: { 50 | timeMin: start.toISOString(), 51 | timeMax: end.toISOString(), 52 | items: [ { id: calendarId } ] 53 | } 54 | }; 55 | 56 | calendar.freebusy.query(param, function (err, response) { 57 | if (err) reject(err); 58 | else resolve(response); 59 | }); 60 | }); 61 | return p; 62 | } 63 | 64 | function insertEvent (auth, calendarId, start, end, summary, location) { 65 | const p = new Promise(function (resolve, reject) { 66 | calendar.events.insert({ 67 | auth: auth, 68 | calendarId: calendarId, 69 | resource: { 70 | end: { 71 | dateTime: end.toISOString() 72 | }, 73 | start: { 74 | dateTime: start.toISOString() 75 | }, 76 | summary: summary 77 | } 78 | }, function (err, response) { 79 | if (err) reject(err); 80 | else resolve(response); 81 | }); 82 | }); 83 | return p; 84 | } 85 | 86 | const allDayDateFormat = 'YYYY-MM-DD'; 87 | 88 | function insertAllDayEvent (auth, calendarId, start, end, summary, location) { 89 | const p = new Promise(function (resolve, reject) { 90 | calendar.events.insert({ 91 | auth: auth, 92 | calendarId: calendarId, 93 | resource: { 94 | end: { 95 | date: end.format(allDayDateFormat) 96 | }, 97 | start: { 98 | date: start.format(allDayDateFormat) 99 | }, 100 | summary: summary 101 | } 102 | }, function (err, response) { 103 | if (err) reject(err); 104 | else resolve(response); 105 | }); 106 | }); 107 | return p; 108 | } 109 | 110 | function removeEvent (auth, calendarId, eventId) { 111 | const p = new Promise(function (resolve, reject) { 112 | calendar.events.delete({ 113 | auth: auth, 114 | calendarId: calendarId, 115 | eventId: eventId 116 | }, function (err, response) { 117 | if (err) reject(err); 118 | else resolve(response); 119 | }); 120 | }); 121 | return p; 122 | } 123 | 124 | function getEvent (auth, calendarId, eventId) { 125 | const p = new Promise(function (resolve, reject) { 126 | calendar.events.get({ 127 | auth: auth, 128 | calendarId: calendarId, 129 | eventId: eventId 130 | }, function (err, response) { 131 | if (err) reject(err); 132 | else resolve(response); 133 | }); 134 | }); 135 | return p; 136 | } 137 | 138 | exports.removeEvent = removeEvent; 139 | exports.insertAllDayEvent = insertAllDayEvent; 140 | exports.listEvents = listEvents; 141 | exports.insertEvent = insertEvent; 142 | exports.getEvent = getEvent; 143 | exports.listCalendars = listCalendars; 144 | exports.getCalendar = getCalendar; 145 | exports.freeBusy = freeBusy; 146 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /chapter07-calendar-bot/utils.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const builder = require('botbuilder'); 3 | 4 | function wrapEntity (entityType, value) { 5 | return { 6 | type: entityType, 7 | entity: value, 8 | resolution: { 9 | values: [ 10 | value 11 | ] 12 | } 13 | }; 14 | }; 15 | 16 | function createCalendarCard (session, calendar) { 17 | const isPrimary = session.privateConversationData.calendarId === calendar.id; 18 | 19 | let subtitle = 'Your role: ' + calendar.accessRole; 20 | if (isPrimary) { 21 | subtitle = 'Primary\r\n' + subtitle; 22 | } 23 | let buttons = []; 24 | if (!isPrimary) { 25 | let btnval = 'Set primary calendar to ' + calendar.id; 26 | buttons = [builder.CardAction.postBack(session, btnval, 'Set as primary')]; 27 | } 28 | 29 | const heroCard = new builder.HeroCard(session) 30 | .title(calendar.summary) 31 | .subtitle(subtitle) 32 | .buttons(buttons); 33 | return heroCard; 34 | }; 35 | 36 | function createEventCard (session, event) { 37 | let start, end, subtitle; 38 | if (!event.start.date) { 39 | start = moment(event.start.dateTime); 40 | end = moment(event.end.dateTime); 41 | 42 | const diffInMinutes = end.diff(start, 'm'); 43 | const diffInHours = end.diff(start, 'h'); 44 | 45 | let duration = diffInMinutes + ' minutes'; 46 | if (diffInHours >= 1) { 47 | const hrs = Math.floor(diffInHours); 48 | const mins = diffInMinutes - (hrs * 60); 49 | 50 | if (mins === 0) { 51 | duration = hrs + 'hrs'; 52 | } else { 53 | duration = hrs + (hrs > 1 ? 'hrs ' : 'hr ') + (mins < 10 ? ('0' + mins) : mins) + 'mins'; 54 | } 55 | } 56 | subtitle = 'At ' + start.format('L LT') + ' for ' + duration; 57 | } else { 58 | start = moment(event.start.date); 59 | end = moment(event.end.date); 60 | 61 | const diffInDays = end.diff(start, 'd'); 62 | subtitle = 'All Day ' + start.format('L') + (diffInDays > 1 ? end.format('L') : ''); 63 | } 64 | 65 | const heroCard = new builder.HeroCard(session) 66 | .title(event.summary) 67 | .subtitle(subtitle) 68 | .buttons([ 69 | builder.CardAction.openUrl(session, event.htmlLink, 'Open Google Calendar'), 70 | builder.CardAction.postBack(session, 'Delete event with id ' + event.id, 'Delete') 71 | ]); 72 | return heroCard; 73 | }; 74 | 75 | exports.createEventCard = createEventCard; 76 | exports.wrapEntity = wrapEntity; 77 | exports.createCalendarCard = createCalendarCard; 78 | -------------------------------------------------------------------------------- /chapter08-slack-interactive-messages-bot/README.md: -------------------------------------------------------------------------------- 1 | # Slack Integration Sample Bot 2 | 3 | Practical Bot Development - Chapter 8 4 | 5 | This bot is a demostration of how we can use the Microsoft Bot Builder SDK to create a bot that connects into Slack using the Azure Bot Service. Both simple and multi step workflows are demostrated in the code. 6 | 7 | ## Getting Started 8 | 9 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id and Bot Password. 10 | 11 | ### Installing and Running 12 | 13 | Easy. Peasy. 14 | 15 | ``` 16 | npm install 17 | npm start 18 | ``` 19 | 20 | By default, the bot will run on port 3978. Follow the steps in Chapter 8 to establish connectivity between the bot and Slack. 21 | 22 | Once running, you can type 'simple' to kick off the simple flow and 'order pizza' to kick off the multi step pizza ordering flow. -------------------------------------------------------------------------------- /chapter08-slack-interactive-messages-bot/env.defaults: -------------------------------------------------------------------------------- 1 | MICROSOFT_APP_ID= 2 | MICROSOFT_APP_PASSWORD= -------------------------------------------------------------------------------- /chapter08-slack-interactive-messages-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-slackbot", 3 | "version": "1.0.0", 4 | "description": "Interactive Slack Messages from Chapter 8, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js", 7 | "debug": "node --nolazy --inspect-brk=9229 app.js" 8 | }, 9 | "author": "Szymon Rozga", 10 | "license": "MIT", 11 | "dependencies": { 12 | "botbuilder": "^3.14.1", 13 | "dotenv-extended": "^1.0.4", 14 | "restify": "^4.3.2", 15 | "underscore": "^1.8.3" 16 | }, 17 | "devDependencies": { 18 | "eslint": "^4.10.0", 19 | "eslint-config-google": "^0.9.1", 20 | "eslint-config-standard": "^10.2.1", 21 | "eslint-plugin-import": "^2.8.0", 22 | "eslint-plugin-node": "^5.2.1", 23 | "eslint-plugin-promise": "^3.6.0", 24 | "eslint-plugin-standard": "^3.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /chapter08-slack-interactive-messages-bot/slackApi.js: -------------------------------------------------------------------------------- 1 | const restify = require('restify'); 2 | 3 | function postEphemeral(token, channel, user, text, attachments) { 4 | return new Promise((resolve, reject) => { 5 | let client = restify.createJsonClient({ 6 | url: 'https://slack.com/api/chat.postEphemeral', 7 | headers: { 8 | Authorization: 'Bearer ' + token 9 | } 10 | }); 11 | console.log('posting ephemeral to user [%s] in [%s]', user, channel); 12 | client.post('', 13 | { 14 | channel: channel, 15 | user: user, 16 | text: text, 17 | attachments: attachments 18 | }, 19 | function (err, req, res, obj) { 20 | if (err) { 21 | console.log('%j', err); 22 | reject(err); 23 | return; 24 | } 25 | console.log('%d -> %j', res.statusCode, res.headers); 26 | console.log('%j', obj); 27 | resolve(obj); 28 | }); 29 | }); 30 | } 31 | 32 | function deleteChat(token, channel, messageTs) { 33 | return new Promise((resolve, reject) => { 34 | let client = restify.createJsonClient({ 35 | url: 'https://slack.com/api/chat.delete', 36 | headers: { 37 | Authorization: 'Bearer ' + token 38 | } 39 | }); 40 | console.log('deleting message [%s] in [%s]', messageTs, channel); 41 | client.post('', 42 | { 43 | channel: channel, 44 | ts: messageTs 45 | }, 46 | function (err, req, res, obj) { 47 | if (err) { 48 | console.log('%j', err); 49 | reject(err); 50 | return; 51 | } 52 | console.log('%d -> %j', res.statusCode, res.headers); 53 | console.log('%j', obj); 54 | resolve(obj); 55 | }); 56 | }); 57 | } 58 | 59 | 60 | function updateMessage(token, channel, ts, text, attachments) { 61 | return new Promise((resolve, reject) => { 62 | let client = restify.createJsonClient({ 63 | url: 'https://slack.com/api/chat.update', 64 | headers: { 65 | Authorization: 'Bearer ' + token 66 | } 67 | }); 68 | client.post('', 69 | { 70 | channel: channel, 71 | ts: ts, 72 | text: text, 73 | attachments: attachments 74 | }, 75 | function (err, req, res, obj) { 76 | if (err) { 77 | console.log('%j', err); 78 | reject(err); 79 | return; 80 | } 81 | console.log('%d -> %j', res.statusCode, res.headers); 82 | console.log('%j', obj); 83 | resolve(obj); 84 | }); 85 | }); 86 | }; 87 | 88 | function postMessage(token, channel, text, attachments) { 89 | return new Promise((resolve, reject) => { 90 | let client = restify.createJsonClient({ 91 | url: 'https://slack.com/api/chat.postMessage', 92 | headers: { 93 | Authorization: 'Bearer ' + token 94 | } 95 | }); 96 | client.post('', 97 | { 98 | channel: channel, 99 | text: text, 100 | attachments: attachments 101 | }, 102 | function (err, req, res, obj) { 103 | if (err) { 104 | console.log('%j', err); 105 | reject(err); 106 | return; 107 | } 108 | console.log('%d -> %j', res.statusCode, res.headers); 109 | console.log('%j', obj); 110 | resolve(obj); 111 | }); 112 | }); 113 | } 114 | 115 | exports.postMessage = postMessage; 116 | exports.postEphemeral = postEphemeral; 117 | exports.updateMessage = updateMessage; 118 | exports.deleteChat = deleteChat; -------------------------------------------------------------------------------- /chapter08-slack-interactive-messages-bot/stepData.js: -------------------------------------------------------------------------------- 1 | exports.multiStepData = { 2 | pizzatype: { 3 | text: 'Sauce', 4 | attachments: [ 5 | { 6 | callback_id: 'pizzatype', 7 | title: 'Choose a Pizza Sauce', 8 | actions: [ 9 | { 10 | name: 'regular', 11 | value: 'regular', 12 | text: 'Tomato Sauce', 13 | type: 'button' 14 | }, 15 | { 16 | name: 'step2b', 17 | value: 'oilandgarlic', 18 | text: 'Oil & Garlic', 19 | type: 'button' 20 | } 21 | 22 | ] 23 | } 24 | ] 25 | }, 26 | regular: { 27 | text: 'Pizza Type', 28 | attachments: [ 29 | { 30 | callback_id: 'ingredient', 31 | title: 'Do you want a regular or pepperoni pie?', 32 | actions: [ 33 | { 34 | name: 'regular', 35 | value: 'regular', 36 | text: 'Regular', 37 | type: 'button' 38 | }, 39 | { 40 | name: 'pepperoni', 41 | value: 'pepperoni', 42 | text: 'Pepperoni', 43 | type: 'button' 44 | } 45 | 46 | ] 47 | } 48 | ] 49 | }, 50 | oilandgarlic: { 51 | text: 'Extra Ingredients', 52 | attachments: [ 53 | { 54 | callback_id: 'ingredient', 55 | title: 'Do you want ricotta or caramelized onions?', 56 | actions: [ 57 | { 58 | name: 'ricotta', 59 | value: 'ricotta', 60 | text: 'Ricotta', 61 | type: 'button' 62 | }, 63 | { 64 | name: 'carmelizedonions', 65 | value: 'carmelizedonions', 66 | text: 'Caramelized Onions', 67 | type: 'button' 68 | } 69 | 70 | ] 71 | } 72 | ] 73 | }, 74 | collectsize: { 75 | text: 'Size', 76 | attachments: [ 77 | { 78 | text: 'Which size would you like?', 79 | callback_id: 'finish', 80 | actions: [ 81 | 82 | { 83 | name: 'size_list', 84 | text: 'Pick a pizza size...', 85 | type: 'select', 86 | options: [ 87 | { 88 | text: 'Small', 89 | value: 'small' 90 | }, 91 | { 92 | text: 'Medium', 93 | value: 'medium' 94 | }, 95 | { 96 | text: 'Large', 97 | value: 'large' 98 | } 99 | ] 100 | } 101 | ] 102 | } 103 | ] 104 | }, 105 | finish: { 106 | attachments: [{ 107 | color: 'good', 108 | text: 'Well done' 109 | }] 110 | } 111 | }; -------------------------------------------------------------------------------- /chapter09-directline-webchat-and-voice-bot/README.md: -------------------------------------------------------------------------------- 1 | # Directline Web Chat and Twilio Voice Bot 2 | 3 | Practical Bot Development - Chapter 9 4 | 5 | This bot is a demostration of talking to an existing Azure Bot Service bot using the Directline API. It consists of three components: 6 | 7 | 1. The bot 8 | 1. A custom web chat implementation 9 | 1. A voice bot implementation using Twilio 10 | 11 | ## Getting Started 12 | 13 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id and Bot Password. You will also need the Directline Key and a Key to utilize the Bing Speech API to convert SSML into speech audio files. 14 | 15 | ### Installing and Running 16 | 17 | Easy. Peasy. 18 | 19 | ``` 20 | npm install 21 | npm start 22 | ``` 23 | 24 | By default, the bot will run on port 3978. 25 | 26 | * Go to http://localhost:3978/ to test the custom web chat interface 27 | * If Twilio configured and ngrok running, you can call your Twilio number to call the bot -------------------------------------------------------------------------------- /chapter09-directline-webchat-and-voice-bot/env.defaults: -------------------------------------------------------------------------------- 1 | MICROSOFT_APP_ID= 2 | MICROSOFT_APP_PASSWORD= 3 | DL_KEY= 4 | SPEECH_SERVICE_KEY= 5 | BASE_URI= -------------------------------------------------------------------------------- /chapter09-directline-webchat-and-voice-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-directline-sample", 3 | "version": "1.0.0", 4 | "description": "Directline Sample from Chapter 9, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js", 7 | "debug": "node --nolazy --inspect-brk=9229 app.js" 8 | }, 9 | "author": "Szymon Rozga", 10 | "license": "MIT", 11 | "dependencies": { 12 | "botbuilder": "^3.12.0", 13 | "dotenv-extended": "^1.0.4", 14 | "md5": "^2.2.1", 15 | "moment": "^2.19.3", 16 | "request": "^2.88.0", 17 | "request-promise": "^4.2.2", 18 | "restify": "^4.3.2", 19 | "twilio": "^3.10.1", 20 | "underscore": "^1.8.3", 21 | "xmlbuilder": "^10.1.1" 22 | }, 23 | "devDependencies": { 24 | "cp-cli": "^1.1.0", 25 | "eslint": "^4.10.0", 26 | "eslint-config-google": "^0.9.1", 27 | "eslint-config-standard": "^10.2.1", 28 | "eslint-plugin-import": "^2.8.0", 29 | "eslint-plugin-node": "^5.2.1", 30 | "eslint-plugin-promise": "^3.6.0", 31 | "eslint-plugin-standard": "^3.0.1" 32 | }, 33 | "main": "app.js" 34 | } 35 | -------------------------------------------------------------------------------- /chapter09-directline-webchat-and-voice-bot/public/app/chat.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: Helvetica, Arial, sans-serif; 4 | margin: 10px; 5 | } 6 | 7 | .chat-client { 8 | max-width: 600px; 9 | margin: 20px; 10 | font-size: 16px; 11 | } 12 | 13 | .chat-history { 14 | border: 1px solid lightgray; 15 | height: 400px; 16 | overflow-x: hidden; 17 | overflow-y: scroll; 18 | } 19 | 20 | .chat-controls { 21 | height: 20px; 22 | } 23 | 24 | .chat-img { 25 | background-size: contain; 26 | height: 160px; 27 | max-width: 400px; 28 | } 29 | 30 | .chat-text-entry { 31 | width: 100%; 32 | border: 1px solid lightgray; 33 | padding: 5px; 34 | } 35 | 36 | .chat-entry-container { 37 | position: relative; 38 | margin: 5px; 39 | min-height: 40px; 40 | } 41 | 42 | .chat-entry { 43 | color: #666666; 44 | position: absolute; 45 | padding: 10px; 46 | min-width: 10px; 47 | max-width: 400px; 48 | overflow-y: auto; 49 | word-wrap: break-word; 50 | border-radius: 10px; 51 | } 52 | .chat-from-bot { 53 | right: 10px; 54 | background-color: #2198F4; 55 | border: 1px solid #2198F4; 56 | color: white; 57 | text-align:right; 58 | } 59 | .chat-from-user { 60 | background-color: #E5E4E9; 61 | border: 1px solid #E5E4E9; 62 | } -------------------------------------------------------------------------------- /chapter09-directline-webchat-and-voice-bot/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Direct Line Test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

Sample Direct Line Interface

17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /chapter09-directline-webchat-and-voice-bot/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter09-directline-webchat-and-voice-bot/test.js -------------------------------------------------------------------------------- /chapter10-calendar-bot/README.md: -------------------------------------------------------------------------------- 1 | # Calendar Bot 2 | 3 | This bot is a demostration of how we can use the Microsoft Bot Builder SDK and LUIS to create a bot that is able to action on a calendar. Functionality includes add, remove, move appointments, summarize calendar and check availability. 4 | 5 | This git repo is: 6 | * chapter-5 - simple LUIS bot, no logic 7 | * chapter-7 - auth + api integration 8 | * chapter-10 - multi language support 9 | * master - equivalent to chapter 10 10 | 11 | ## Getting Started 12 | 13 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id, Bot Password, LUIS app id and LUIS subscription key. 14 | 15 | ### Installing and Running 16 | 17 | Easy. Peasy. 18 | 19 | ``` 20 | npm install 21 | npm start 22 | ``` 23 | 24 | By default, the bot will run on port 3978. Download the [Bot Framework Emulator](https://docs.microsoft.com/en-us/bot-framework/debug-bots-emulator) to test locally. -------------------------------------------------------------------------------- /chapter10-calendar-bot/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | const builder = require('botbuilder'); 4 | const restify = require('restify'); 5 | 6 | const constants = require('./constants'); 7 | const utils = require('./utils'); 8 | 9 | const helpModule = require('./dialogs/help'); 10 | const authModule = require('./dialogs/auth'); 11 | const addEntryModule = require('./dialogs/addEntry'); 12 | const removeEntryModule = require('./dialogs/removeEntry'); 13 | const editEntryModule = require('./dialogs/editEntry'); 14 | const checkAvailabilityModule = require('./dialogs/checkAvailability'); 15 | const summarizeModule = require('./dialogs/summarize'); 16 | const primaryCalendarModule = require('./dialogs/primaryCalendar'); 17 | const prechecksModule = require('./dialogs/prechecks'); 18 | 19 | 20 | authModule.setResolvePostLoginDialog((session, args) => { 21 | if (!session.privateConversationData.calendarId) { 22 | args.followUpDialog = primaryCalendarModule.getPrimaryCalendarDialogName(); 23 | args.followUpDialogArgs = { 24 | intent: { 25 | entities: [ 26 | utils.wrapEntity(constants.entityNames.Action, constants.entityValues.Action.set) 27 | ] 28 | } 29 | }; 30 | } 31 | return args; 32 | }); 33 | 34 | // setup our web server 35 | const server = restify.createServer(); 36 | server.use(restify.queryParser()); 37 | server.listen(process.env.port || process.env.PORT || 3978, () => { 38 | console.log('%s listening to %s', server.name, server.url); 39 | }); 40 | 41 | // initialize the chat bot 42 | const connector = new builder.ChatConnector({ 43 | appId: process.env.MICROSOFT_APP_ID, 44 | appPassword: process.env.MICROSOFT_APP_PASSWORD 45 | }); 46 | 47 | const bot = new builder.UniversalBot(connector, [ 48 | session => { 49 | helpModule.help(session); 50 | } 51 | ]); 52 | server.post('/api/messages', connector.listen()); 53 | server.get('/oauth2callback', (req, res, next) => { 54 | authModule.oAuth2Callback(bot, req, res, next); 55 | }); 56 | 57 | const TranslatorMiddleware = require('./translatorMiddleware').TranslatorMiddleware; 58 | bot.use(new TranslatorMiddleware()); 59 | 60 | const luisModelUri = 'https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/' + process.env.LUIS_APP + '?subscription-key=' + process.env.LUIS_SUBSCRIPTION_KEY; 61 | bot.recognizer(new builder.LuisRecognizer(luisModelUri)); 62 | 63 | bot.library(addEntryModule.create()); 64 | bot.library(helpModule.create()); 65 | bot.library(authModule.create()); 66 | bot.library(removeEntryModule.create()); 67 | bot.library(editEntryModule.create()); 68 | bot.library(checkAvailabilityModule.create()); 69 | bot.library(summarizeModule.create()); 70 | bot.library(primaryCalendarModule.create()); 71 | bot.library(prechecksModule.create()); 72 | -------------------------------------------------------------------------------- /chapter10-calendar-bot/constants.js: -------------------------------------------------------------------------------- 1 | exports.dialogNames = { 2 | Help: 'help', 3 | AddCalendarEntry: 'addCalendarEntry', 4 | AddCalendarEntryHelp: 'addCalendarEntryHelp', 5 | RemoveCalendarEntryHelp: 'removeCalendarEntryHelp', 6 | RemoveCalendarEntry: 'removeCalendarEntry', 7 | RemoveCalendarEntry_Time: 'removeCalendarEntry.time', 8 | RemoveCalendarEntry_Invitee: 'removeCalendarEntry.invitee', 9 | EditCalendarEntry: 'editCalendarEntry', 10 | ShowCalendarSummary: 'showCalendarSummary', 11 | CheckAvailability: 'checkAvailability', 12 | Login: 'login', 13 | PrimaryCalendar: 'primaryCalendar', 14 | EnsurePrimaryCalendar: 'ensurePrimaryCalendar', 15 | PreCheck_AuthAndPrimaryCalendar: 'preCheck_authAndPrimaryCalendar', 16 | Auth: { 17 | Login: 'auth.login', 18 | Logout: 'auth.logout', 19 | EnsureCredentials: 'auth.ensureCredentials', 20 | AuthConfirmation: 'auth.authConfirmation', 21 | StoreTokensAndResume: 'auth.storeAndResume', 22 | Error: 'auth.error' 23 | } 24 | }; 25 | 26 | exports.intentNames = { 27 | Help: 'Help', 28 | AddCalendarEntry: 'AddCalendarEntry', 29 | RemoveCalendarEntry: 'DeleteCalendarEntry', 30 | CheckAvailability: 'CheckAvailability', 31 | EditCalendarEntry: 'EditCalendarEntry', 32 | ShowCalendarSummary: 'ShowCalendarSummary', 33 | PrimaryCalendar: 'PrimaryCalendar', 34 | None: 'None' 35 | }; 36 | 37 | exports.entityNames = { 38 | Chrono: 'chrono.duration', 39 | Invitee: 'CalendarBot.Invitee', 40 | Action: 'Action', 41 | CalendarId: 'CalendarId', 42 | EventId: 'EventId', 43 | Subject: 'CalendarBot.Subject', 44 | Location: 'CalendarBot.Location', 45 | Composite: { 46 | CalendarRequest: 'CalendarRequest' 47 | }, 48 | MeetingMove: { 49 | FromTime: 'MeetingMove::FromTime', 50 | ToTime: 'MeetingMove::ToTime' 51 | }, 52 | EntryVisibility: { 53 | Public: 'EntryVisbility::Public', 54 | Private: 'EntryVisbility::Private' 55 | }, 56 | Dates: { 57 | Date: 'builtin.datetimeV2.date', 58 | DateTime: 'builtin.datetimeV2.datetime', 59 | Time: 'builtin.datetimeV2.time', 60 | DateRange: 'builtin.datetimeV2.daterange', 61 | TimeRange: 'builtin.datetimeV2.timerange', 62 | DateTimeRange: 'builtin.datetimeV2.datetimerange', 63 | Set: 'builtin.datetimeV2.set', 64 | Duration: 'builtin.datetimeV2.duration' 65 | }, 66 | Email: 'builtin.email' 67 | }; 68 | 69 | exports.entityValues = { 70 | Action: { 71 | get: 'get', 72 | set: 'set', 73 | clear: 'remove' 74 | } 75 | }; 76 | 77 | exports.LUISTimePattern = 'HH:mm:ss'; 78 | -------------------------------------------------------------------------------- /chapter10-calendar-bot/dialogs/editEntry.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const _ = require('underscore'); 3 | 4 | const constants = require('../constants'); 5 | const mt = require('../moveTranslator'); 6 | 7 | const lib = new builder.Library('editEntry'); 8 | 9 | /* Building out an entire edit functionality can be a large undertaking. There are many possible attributes to edit in many possible ways. We start with the ability 10 | * to move appointments using natural language */ 11 | lib.dialog(constants.dialogNames.EditCalendarEntry, [ 12 | (session, args, next) => { 13 | // move from and move to are helper entities to help us figure out where the from and to times like. The LUIS range datetime 14 | // entities can be good but you could imagine a user phrasing the query differently. for example, move 4p meeting to 6pm, 15 | // would not be identified as a range 16 | const moveFrom = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.FromTime); 17 | const moveTo = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.ToTime); 18 | 19 | const allOtherEntities = _.where(args.intent.entities, function (p) { 20 | return p.type !== constants.entityNames.MeetingMove.FromTime && p.type !== constants.entityNames.MeetingMove.ToTime; 21 | }); 22 | 23 | // use the move translator to properly identiy the from and to 24 | const moveTranslator = new mt.MoveTranslator(); 25 | moveTranslator.applyEntities(moveTo, allOtherEntities, moveFrom); 26 | 27 | let prefix = 'Moving '; 28 | 29 | if (moveTranslator.hasSubject) prefix += (moveTranslator.subject + ' '); 30 | else prefix += 'meeting '; 31 | 32 | if (moveTranslator.hasInvitee) prefix += ('with ' + moveTranslator.invitee + ' '); 33 | 34 | if (moveTranslator.moveFrom && moveTranslator.moveTo) { 35 | if (moveTranslator.isDateBased) { 36 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L') + ' to ' + moveTranslator.moveTo.format('L')); 37 | } else { 38 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L LT') + ' to ' + moveTranslator.moveTo.format('L LT')); 39 | } 40 | } else if (!moveTranslator.moveFrom && moveTranslator.moveTo) { 41 | if (moveTranslator.isDateBased) { 42 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L')); 43 | } else { 44 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L LT')); 45 | } 46 | } else if (!moveTranslator.moveFrom && !moveTranslator.moveTo) { 47 | session.endDialog("I'm sorry, I'm not sure how to handle that request."); 48 | } 49 | } 50 | ]).triggerAction({ matches: constants.intentNames.EditCalendarEntry }); 51 | 52 | exports.create = () => { return lib.clone(); } 53 | -------------------------------------------------------------------------------- /chapter10-calendar-bot/dialogs/help.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | 3 | const constants = require('../constants'); 4 | 5 | const lib = new builder.Library('help'); 6 | 7 | exports.help = (session) => { 8 | session.beginDialog('help:' + constants.dialogNames.Help); 9 | }; 10 | 11 | // help message when help requested during the add calendar entry dialog 12 | lib.dialog(constants.dialogNames.AddCalendarEntryHelp, (session, args, next) => { 13 | const msg = "To add an appointment, we gather the following information: time, subject and location. You can also simply say 'add appointment with Bob tomorrow at 2pm for an hour for coffee' and we'll take it from there!"; 14 | session.endDialog(msg); 15 | }); 16 | 17 | // help message when help requested during the remove calendar entry dialog 18 | lib.dialog(constants.dialogNames.RemoveCalendarEntryHelp, (session, args, next) => { 19 | const msg = ''; 20 | session.endDialog(msg); 21 | }); 22 | 23 | // top level help 24 | lib.dialog(constants.dialogNames.Help, (session, args, next) => { 25 | session.endDialog('Hi, I am a calendar concierge bot. I can help you create, delete and move appointments. I can also tell you about your calendar and check your availability!'); 26 | }).triggerAction({ 27 | matches: constants.intentNames.Help, 28 | onSelectAction: (session, args, next) => { 29 | session.beginDialog(args.action, args); 30 | } 31 | }); 32 | 33 | exports.create = () => { return lib.clone(); } 34 | -------------------------------------------------------------------------------- /chapter10-calendar-bot/dialogs/prechecks.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | 3 | const constants = require('../constants'); 4 | 5 | const authModule = require('./auth'); 6 | const primaryCalendarModule = require('./primaryCalendar'); 7 | 8 | const libName = 'prechecks'; 9 | const lib = new builder.Library(libName); 10 | 11 | lib.dialog(constants.dialogNames.PreCheck_AuthAndPrimaryCalendar, [ 12 | (session, args) => { 13 | authModule.ensureLoggedIn(session); 14 | }, 15 | (session, args) => { 16 | if (!args.response.authenticated) { 17 | session.endDialogWithResult({ response: { error: 'You must authenticate to continue.', error_auth: true } }); 18 | } else { 19 | primaryCalendarModule.ensurePrimaryCalendar(session); 20 | } 21 | }, 22 | (session, args, next) => { 23 | if (session.privateConversationData.calendarId) session.endDialogWithResult({ response: { } }); 24 | else session.endDialogWithResult({ response: { error: 'You must set a primary calendar to continue.', error_calendar: true } }); 25 | } 26 | ]); 27 | 28 | exports.create = () => { return lib.clone(); } 29 | exports.ensurePrechecks = session => { 30 | session.beginDialog(libName + ':' + constants.dialogNames.PreCheck_AuthAndPrimaryCalendar); 31 | }; 32 | -------------------------------------------------------------------------------- /chapter10-calendar-bot/dialogs/summarize.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const _ = require('underscore'); 3 | const moment = require('moment'); 4 | 5 | const utils = require('../utils'); 6 | const constants = require('../constants'); 7 | const et = require('../entityTranslator'); 8 | const gcalapi = require('../services/calendar-api'); 9 | 10 | const authModule = require('./auth'); 11 | const prechecksModule = require('./prechecks'); 12 | 13 | const lib = new builder.Library('summarize'); 14 | 15 | lib.dialog(constants.dialogNames.ShowCalendarSummary, [ 16 | function (session, args) { 17 | session.dialogData.intent = args.intent; 18 | prechecksModule.ensurePrechecks(session); 19 | }, 20 | function (session, args, next) { 21 | if (args.response.error) { 22 | session.endDialog(args.response.error); 23 | return; 24 | } 25 | next(); 26 | }, 27 | function (session, args, next) { 28 | const auth = authModule.getAuthClientFromSession(session); 29 | const entry = new et.EntityTranslator(); 30 | et.EntityTranslatorUtils.attachSummaryEntities(entry, session.dialogData.intent.entities); 31 | let start = null; 32 | let end = null; 33 | 34 | if (entry.hasRange) { 35 | if (entry.isDateTimeEntityDateBased) { 36 | start = moment(entry.range.start).startOf('day'); 37 | end = moment(entry.range.end).endOf('day'); 38 | } else { 39 | start = moment(entry.range.start); 40 | end = moment(entry.range.end); 41 | } 42 | } else if (entry.hasDateTime) { 43 | if (entry.isDateTimeEntityDateBased) { 44 | start = moment(entry.dateTime).startOf('day'); 45 | end = moment(entry.dateTime).endOf('day'); 46 | } else { 47 | start = moment(entry.dateTime).add(-1, 'h'); 48 | end = moment(entry.dateTime).add(1, 'h'); 49 | } 50 | } else { 51 | session.endDialog("Sorry I don't know what you mean"); 52 | return; 53 | } 54 | 55 | const p = gcalapi.listEvents(auth, session.privateConversationData.calendarId, start, end); 56 | p.then(function (events) { 57 | let evs = _.sortBy(events, function (p) { 58 | if (p.start.date) { 59 | return moment(p.start.date).add(-1, 's').valueOf(); 60 | } else if (p.start.dateTime) { 61 | return moment(p.start.dateTime).valueOf(); 62 | } 63 | }); 64 | 65 | // should also potentially filter by subject 66 | evs = _.filter(evs, function (p) { 67 | if (!entry.hasSubject) return true; 68 | 69 | const containsSubject = entry.subject.toLowerCase().indexOf(entry.subject.toLowerCase()) >= 0; 70 | return containsSubject; 71 | }); 72 | 73 | const eventmsg = new builder.Message(session); 74 | if (evs.length > 1) { 75 | eventmsg.text('Here is what I found...'); 76 | } else if (evs.length === 1) { 77 | eventmsg.text('Here is the event I found.'); 78 | } else { 79 | eventmsg.text('Seems you have nothing going on then. What a sad existence you lead.'); 80 | } 81 | 82 | if (evs.length >= 1) { 83 | const cards = _.map(evs, function (p) { 84 | return utils.createEventCard(session, p); 85 | }); 86 | eventmsg.attachmentLayout(builder.AttachmentLayout.carousel); 87 | eventmsg.attachments(cards); 88 | } 89 | 90 | session.send(eventmsg); 91 | session.endDialog(); 92 | }); 93 | } 94 | ]).triggerAction({ matches: constants.intentNames.ShowCalendarSummary }); 95 | 96 | exports.create = function () { return lib.clone(); } 97 | -------------------------------------------------------------------------------- /chapter10-calendar-bot/env.defaults: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | MICROSOFT_APP_ID= 3 | MICROSOFT_APP_PASSWORD= 4 | LUIS_APP= 5 | LUIS_SUBSCRIPTION_KEY= 6 | 7 | # Google OAuth Bits 8 | GOOGLE_OAUTH_CLIENT_ID= 9 | GOOGLE_OAUTH_CLIENT_SECRET= 10 | GOOGLE_OAUTH_REDIRECT_URI= 11 | 12 | # Security 13 | AES_PASSPHRASE= 14 | 15 | # Cognitive Services 16 | TA_KEY= 17 | TA_ENDPOINT= 18 | TRANSLATOR_KEY= -------------------------------------------------------------------------------- /chapter10-calendar-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-calendar-bot-buildup", 3 | "version": "1.0.0", 4 | "description": "Calendar Bot - Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "botbuilder": "^3.13.1", 12 | "cognitive-services": "^0.6.2", 13 | "crypto-js": "^3.1.9-1", 14 | "dotenv-extended": "^1.0.4", 15 | "googleapis": "^22.2.0", 16 | "moment": "^2.19.4", 17 | "mstranslator": "^3.0.0", 18 | "request": "^2.83.0", 19 | "restify": "^4.3.2", 20 | "underscore": "^1.8.3" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^4.15.0", 24 | "eslint-config-google": "^0.9.1", 25 | "eslint-config-standard": "^10.2.1", 26 | "eslint-plugin-import": "^2.8.0", 27 | "eslint-plugin-node": "^5.2.1", 28 | "eslint-plugin-promise": "^3.6.0", 29 | "eslint-plugin-standard": "^3.0.1", 30 | "jasmine-node": "^1.14.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /chapter10-calendar-bot/services/calendar-api.js: -------------------------------------------------------------------------------- 1 | const google = require('googleapis'); 2 | const calendar = google.calendar('v3'); 3 | 4 | function listEvents (auth, calendarId, start, end, subject) { 5 | const p = new Promise(function (resolve, reject) { 6 | calendar.events.list({ 7 | auth: auth, 8 | calendarId: calendarId, 9 | timeMin: start.toISOString(), 10 | timeMax: end.toISOString(), 11 | q: subject 12 | }, function (err, response) { 13 | if (err) reject(err); 14 | resolve(response.items); 15 | }); 16 | }); 17 | return p; 18 | } 19 | 20 | function listCalendars (auth) { 21 | const p = new Promise(function (resolve, reject) { 22 | calendar.calendarList.list({ 23 | auth: auth 24 | }, function (err, response) { 25 | if (err) reject(err); 26 | else resolve(response.items); 27 | }); 28 | }); 29 | return p; 30 | }; 31 | 32 | function getCalendar (auth, calendarId) { 33 | const p = new Promise(function (resolve, reject) { 34 | calendar.calendarList.get({ 35 | auth: auth, 36 | calendarId: calendarId 37 | }, function (err, response) { 38 | if (err) reject(err); 39 | else resolve(response); 40 | }); 41 | }); 42 | return p; 43 | }; 44 | 45 | function freeBusy (auth, calendarId, start, end) { 46 | const p = new Promise(function (resolve, reject) { 47 | const param = { 48 | auth: auth, 49 | resource: { 50 | timeMin: start.toISOString(), 51 | timeMax: end.toISOString(), 52 | items: [ { id: calendarId } ] 53 | } 54 | }; 55 | 56 | calendar.freebusy.query(param, function (err, response) { 57 | if (err) reject(err); 58 | else resolve(response); 59 | }); 60 | }); 61 | return p; 62 | } 63 | 64 | function insertEvent (auth, calendarId, start, end, summary, location) { 65 | const p = new Promise(function (resolve, reject) { 66 | calendar.events.insert({ 67 | auth: auth, 68 | calendarId: calendarId, 69 | resource: { 70 | end: { 71 | dateTime: end.toISOString() 72 | }, 73 | start: { 74 | dateTime: start.toISOString() 75 | }, 76 | summary: summary 77 | } 78 | }, function (err, response) { 79 | if (err) reject(err); 80 | else resolve(response); 81 | }); 82 | }); 83 | return p; 84 | } 85 | 86 | const allDayDateFormat = 'YYYY-MM-DD'; 87 | 88 | function insertAllDayEvent (auth, calendarId, start, end, summary, location) { 89 | const p = new Promise(function (resolve, reject) { 90 | calendar.events.insert({ 91 | auth: auth, 92 | calendarId: calendarId, 93 | resource: { 94 | end: { 95 | date: end.format(allDayDateFormat) 96 | }, 97 | start: { 98 | date: start.format(allDayDateFormat) 99 | }, 100 | summary: summary 101 | } 102 | }, function (err, response) { 103 | if (err) reject(err); 104 | else resolve(response); 105 | }); 106 | }); 107 | return p; 108 | } 109 | 110 | function removeEvent (auth, calendarId, eventId) { 111 | const p = new Promise(function (resolve, reject) { 112 | calendar.events.delete({ 113 | auth: auth, 114 | calendarId: calendarId, 115 | eventId: eventId 116 | }, function (err, response) { 117 | if (err) reject(err); 118 | else resolve(response); 119 | }); 120 | }); 121 | return p; 122 | } 123 | 124 | function getEvent (auth, calendarId, eventId) { 125 | const p = new Promise(function (resolve, reject) { 126 | calendar.events.get({ 127 | auth: auth, 128 | calendarId: calendarId, 129 | eventId: eventId 130 | }, function (err, response) { 131 | if (err) reject(err); 132 | else resolve(response); 133 | }); 134 | }); 135 | return p; 136 | } 137 | 138 | exports.removeEvent = removeEvent; 139 | exports.insertAllDayEvent = insertAllDayEvent; 140 | exports.listEvents = listEvents; 141 | exports.insertEvent = insertEvent; 142 | exports.getEvent = getEvent; 143 | exports.listCalendars = listCalendars; 144 | exports.getCalendar = getCalendar; 145 | exports.freeBusy = freeBusy; 146 | -------------------------------------------------------------------------------- /chapter10-calendar-bot/spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /chapter10-calendar-bot/translatorMiddleware.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore'); 2 | const cognitiveServices = require('cognitive-services'); 3 | const translator = require('mstranslator'); 4 | 5 | const textAnalytics = new cognitiveServices.textAnalytics({ 6 | apiKey: process.env.TA_KEY, 7 | endpoint: process.env.TA_ENDPOINT 8 | }); 9 | const translatorApi = new translator({ api_key: process.env.TRANSLATOR_KEY }, true); 10 | const userLanguageMap = {}; 11 | 12 | class TranslatorMiddleware { 13 | receive(event, next) { 14 | if (event.type !== 'message') { next(); return; } 15 | 16 | if (event.text == null || event.text.length == 0) { 17 | // if there is not input and we already have a language, leave as is, otherwise set to English 18 | userLanguageMap[event.user.id] = userLanguageMap[event.user.id] || 'en'; 19 | next(); 20 | return; 21 | } 22 | 23 | textAnalytics.detectLanguage({ 24 | body: { 25 | documents: [ 26 | { 27 | id: "1", 28 | text: event.text 29 | } 30 | ] 31 | } 32 | }).then(result => { 33 | const languageOptions = _.find(result.documents, p => p.id === "1").detectedLanguages; 34 | let lang = 'en'; 35 | 36 | if (languageOptions && languageOptions.length > 0) { 37 | lang = languageOptions[0].iso6391Name; 38 | } 39 | userLanguageMap[event.user.id] = lang; 40 | 41 | if (lang === 'en') next(); 42 | else { 43 | translatorApi.translate({ 44 | text: event.text, 45 | from: languageOptions[0].iso6391Name, 46 | to: 'en' 47 | }, (err, result) => { 48 | if (err) { 49 | console.error(err); 50 | lang = 'en'; 51 | userLanguageMap[event.user.id] = lang; 52 | next(); 53 | } 54 | else { 55 | event.text = result; 56 | next(); 57 | } 58 | }); 59 | } 60 | }); 61 | } 62 | send(event, next) { 63 | if (event.type === 'message') { 64 | const userLang = userLanguageMap[event.address.user.id] || 'en'; 65 | 66 | if (userLang === 'en') { next(); } 67 | else { 68 | translatorApi.translate({ 69 | text: event.text, 70 | from: 'en', 71 | to: userLang 72 | }, (err, result) => { 73 | if (err) { 74 | console.error(err); 75 | next(); 76 | } 77 | else { 78 | event.text = result; 79 | next(); 80 | } 81 | }); 82 | } 83 | } 84 | else { 85 | next(); 86 | } 87 | } 88 | } 89 | 90 | exports.TranslatorMiddleware = TranslatorMiddleware; -------------------------------------------------------------------------------- /chapter10-calendar-bot/utils.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const builder = require('botbuilder'); 3 | 4 | function wrapEntity (entityType, value) { 5 | return { 6 | type: entityType, 7 | entity: value, 8 | resolution: { 9 | values: [ 10 | value 11 | ] 12 | } 13 | }; 14 | }; 15 | 16 | function createCalendarCard (session, calendar) { 17 | const isPrimary = session.privateConversationData.calendarId === calendar.id; 18 | 19 | let subtitle = 'Your role: ' + calendar.accessRole; 20 | if (isPrimary) { 21 | subtitle = 'Primary\r\n' + subtitle; 22 | } 23 | let buttons = []; 24 | if (!isPrimary) { 25 | let btnval = 'Set primary calendar to ' + calendar.id; 26 | buttons = [builder.CardAction.postBack(session, btnval, 'Set as primary')]; 27 | } 28 | 29 | const heroCard = new builder.HeroCard(session) 30 | .title(calendar.summary) 31 | .subtitle(subtitle) 32 | .buttons(buttons); 33 | return heroCard; 34 | }; 35 | 36 | function createEventCard (session, event) { 37 | let start, end, subtitle; 38 | if (!event.start.date) { 39 | start = moment(event.start.dateTime); 40 | end = moment(event.end.dateTime); 41 | 42 | const diffInMinutes = end.diff(start, 'm'); 43 | const diffInHours = end.diff(start, 'h'); 44 | 45 | let duration = diffInMinutes + ' minutes'; 46 | if (diffInHours >= 1) { 47 | const hrs = Math.floor(diffInHours); 48 | const mins = diffInMinutes - (hrs * 60); 49 | 50 | if (mins === 0) { 51 | duration = hrs + 'hrs'; 52 | } else { 53 | duration = hrs + (hrs > 1 ? 'hrs ' : 'hr ') + (mins < 10 ? ('0' + mins) : mins) + 'mins'; 54 | } 55 | } 56 | subtitle = 'At ' + start.format('L LT') + ' for ' + duration; 57 | } else { 58 | start = moment(event.start.date); 59 | end = moment(event.end.date); 60 | 61 | const diffInDays = end.diff(start, 'd'); 62 | subtitle = 'All Day ' + start.format('L') + (diffInDays > 1 ? end.format('L') : ''); 63 | } 64 | 65 | const heroCard = new builder.HeroCard(session) 66 | .title(event.summary) 67 | .subtitle(subtitle) 68 | .buttons([ 69 | builder.CardAction.openUrl(session, event.htmlLink, 'Open Google Calendar'), 70 | builder.CardAction.postBack(session, 'Delete event with id ' + event.id, 'Delete') 71 | ]); 72 | return heroCard; 73 | }; 74 | 75 | exports.createEventCard = createEventCard; 76 | exports.wrapEntity = wrapEntity; 77 | exports.createCalendarCard = createCalendarCard; 78 | -------------------------------------------------------------------------------- /chapter10-hot-dog-or-not-hot-dog/README.md: -------------------------------------------------------------------------------- 1 | # Hot Dog or Not Hot Dog Bot 2 | 3 | Practical Bot Development - Chapter 10 4 | 5 | This bot is a demostration of using Microsoft Cognitive Services Computer Vision Service to determine whether an image is a hot dog or not (ala, Silicon Valley S4E4) 6 | 7 | ## Getting Started 8 | 9 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id and Bot Password. You will also need the Computer Vision service endpoint and key. Both can be grabbed from the Azure Poral. 10 | 11 | ### Installing and Running 12 | 13 | Easy. Peasy. 14 | 15 | ``` 16 | npm install 17 | npm start 18 | ``` 19 | 20 | By default, the bot will run on port 3978. Send images to the bot and it will respond whether it found a hot dog or not. -------------------------------------------------------------------------------- /chapter10-hot-dog-or-not-hot-dog/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | const builder = require('botbuilder'); 4 | const restify = require('restify'); 5 | const moment = require('moment'); 6 | const _ = require('underscore'); 7 | const cognitiveServices = require('cognitive-services'); 8 | const fs = require('fs'); 9 | const request = require('request'); 10 | const uuid = require('uuid'); 11 | 12 | // Setup Restify Server 13 | const server = restify.createServer(); 14 | server.listen(process.env.port || process.env.PORT || 3978, () => { 15 | console.log('%s listening to %s', server.name, server.url); 16 | }); 17 | 18 | // Create chat bot and listen to messages 19 | const connector = new builder.ChatConnector({ 20 | appId: process.env.MICROSOFT_APP_ID, 21 | appPassword: process.env.MICROSOFT_APP_PASSWORD 22 | }); 23 | server.post('/api/messages', connector.listen()); 24 | 25 | const bot = new builder.UniversalBot(connector, [ 26 | session => { 27 | session.beginDialog('hot-dog-or-not-hot-dog'); 28 | }, 29 | session => { 30 | session.endConversation(); 31 | } 32 | ]); 33 | const inMemoryStorage = new builder.MemoryBotStorage(); 34 | bot.set('storage', inMemoryStorage); 35 | 36 | const getImage = function (uri, filename) { 37 | return new Promise((resolve, reject) => { 38 | request.head(uri, function (err, res, body) { 39 | request(uri).pipe(fs.createWriteStream(filename)) 40 | .on('error', () => { reject(); }) 41 | .on('close', () => { 42 | resolve(); 43 | }); 44 | }); 45 | }); 46 | }; 47 | 48 | bot.dialog('hot-dog-or-not-hot-dog', [ 49 | (session, arg) => { 50 | if (session.message.attachments == null || session.message.attachments.length == 0 || session.message.attachments[0].contentType.indexOf('image') < 0) { 51 | session.send('Not supported. Require an image to be sent!'); 52 | return; 53 | } 54 | 55 | // let them know we're thinking.... 56 | session.sendTyping(); 57 | 58 | const id = uuid(); 59 | const dirName = 'images'; 60 | 61 | if (!fs.existsSync(dirName)) { 62 | fs.mkdirSync(dirName); 63 | } 64 | const imagePath = dirName + '/' + id; 65 | const imageUrl = session.message.attachments[0].contentUrl; 66 | 67 | getImage(imageUrl, imagePath).then(() => { 68 | const cv = new cognitiveServices.computerVision({ apiKey: process.env.CV_KEY, endpoint: process.env.CV_ENDPOINT }); 69 | return cv.describeImage({ 70 | headers: { 'Content-Type': 'application/octet-stream' }, 71 | body: fs.readFileSync(imagePath) 72 | }); 73 | }).then((analysis) => { 74 | if (analysis.description.tags) { 75 | if (_.find(analysis.description.tags, p => p === 'hotdog')) { 76 | session.send('HOT DOG!'); 77 | } 78 | else { 79 | session.send('not hot dog'); 80 | } 81 | } 82 | else { 83 | session.send('not hot dog'); 84 | } 85 | fs.unlinkSync(imagePath); 86 | }); 87 | } 88 | ]); 89 | -------------------------------------------------------------------------------- /chapter10-hot-dog-or-not-hot-dog/env.defaults: -------------------------------------------------------------------------------- 1 | MICROSOFT_APP_ID= 2 | MICROSOFT_APP_PASSWORD= 3 | CV_KEY= 4 | CV_ENDPOINT= -------------------------------------------------------------------------------- /chapter10-hot-dog-or-not-hot-dog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hot-dog-or-not-hot-dog-sample", 3 | "version": "1.0.0", 4 | "description": "Hot Dog or Not Hot Dog from Chapter 9, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js", 7 | "debug": "node --nolazy --inspect-brk=9229 app.js" 8 | }, 9 | "author": "Szymon Rozga", 10 | "license": "MIT", 11 | "dependencies": { 12 | "botbuilder": "^3.12.0", 13 | "cognitive-services": "^0.6.2", 14 | "dotenv-extended": "^1.0.4", 15 | "moment": "^2.19.3", 16 | "request": "^2.83.0", 17 | "restify": "^4.3.2", 18 | "underscore": "^1.8.3", 19 | "uuid": "^3.1.0" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^4.10.0", 23 | "eslint-config-google": "^0.9.1", 24 | "eslint-config-standard": "^10.2.1", 25 | "eslint-plugin-import": "^2.8.0", 26 | "eslint-plugin-node": "^5.2.1", 27 | "eslint-plugin-promise": "^3.6.0", 28 | "eslint-plugin-standard": "^3.0.1" 29 | }, 30 | "main": "app.js" 31 | } 32 | -------------------------------------------------------------------------------- /chapter10-spell-check-bot/README.md: -------------------------------------------------------------------------------- 1 | # Spell Checker Bot 2 | 3 | Practical Bot Development - Chapter 10 4 | 5 | This bot is a demostration of talking to the Microsoft Bing Spell Check API to automatically fix spell check errors in user input. The bot utilizes middleware to always spell check any incoming text. 6 | 7 | ## Getting Started 8 | 9 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id and Bot Password. You will also need a Spell Check V7 API key. 10 | 11 | ### Installing and Running 12 | 13 | Easy. Peasy. 14 | 15 | ``` 16 | npm install 17 | npm start 18 | ``` 19 | 20 | By default, the bot will run on port 3978. -------------------------------------------------------------------------------- /chapter10-spell-check-bot/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | const builder = require('botbuilder'); 4 | const restify = require('restify'); 5 | const moment = require('moment'); 6 | const _ = require('underscore'); 7 | const cognitiveServices = require('cognitive-services'); 8 | 9 | // Setup Restify Server 10 | const server = restify.createServer(); 11 | server.listen(process.env.port || process.env.PORT || 3978, () => { 12 | console.log('%s listening to %s', server.name, server.url); 13 | }); 14 | 15 | // Create chat bot and listen to messages 16 | const connector = new builder.ChatConnector({ 17 | appId: process.env.MICROSOFT_APP_ID, 18 | appPassword: process.env.MICROSOFT_APP_PASSWORD 19 | }); 20 | server.post('/api/messages', connector.listen()); 21 | 22 | 23 | // const welcomeMsg = 'Say \'proof\' or \'spell\' to select spell check mode'; 24 | const bot = new builder.UniversalBot(connector, [ 25 | session => { 26 | session.beginDialog('middleware-dialog'); 27 | } 28 | // , 29 | // (session, arg, next) => { 30 | // if (session.message.text === 'proof') { 31 | // session.beginDialog('spell-check-dialog', { mode: 'proof' }); 32 | // } else if (session.message.text === 'spell') { 33 | // session.beginDialog('spell-check-dialog', { mode: 'spell' }); 34 | // } else { 35 | // session.send(welcomeMsg); 36 | // } 37 | // }, 38 | // session => { 39 | // session.send(welcomeMsg); 40 | // } 41 | ]); 42 | const inMemoryStorage = new builder.MemoryBotStorage(); 43 | bot.set('storage', inMemoryStorage); 44 | 45 | bot.dialog('spell-check-dialog', [ 46 | (session, arg) => { 47 | session.dialogData.mode = arg.mode; 48 | builder.Prompts.text(session, 'Enter your input text. Say \'exit\' to reconfigure mode.'); 49 | }, 50 | (session, arg) => { 51 | session.sendTyping(); 52 | 53 | const text = arg.response; 54 | 55 | if (text === 'exit') { 56 | session.endDialog('ok, done.'); 57 | return; 58 | } 59 | 60 | spellCheck(text, session.dialogData.mode).then(response => { 61 | session.send(resultText); 62 | session.replaceDialog('spell-check-dialog', { mode: session.dialogData.mode }); 63 | }); 64 | } 65 | ]); 66 | 67 | function spellCheck(text, mode) { 68 | const parameters = { 69 | mkt: 'en-US', 70 | mode: mode, 71 | text: text 72 | }; 73 | 74 | const spellCheckClient = new cognitiveServices.bingSpellCheckV7({ 75 | apiKey: process.env.SC_KEY 76 | }) 77 | 78 | return spellCheckClient.spellCheck({ 79 | parameters 80 | }).then(response => { 81 | console.log(response); // we do this so we can easily inspect the resulting object 82 | const resultText = applySpellCheck(text, response.flaggedTokens); 83 | return resultText; 84 | }); 85 | } 86 | 87 | function applySpellCheck(originalText, possibleProblems) { 88 | let tempText = originalText; 89 | let diff = 0; 90 | 91 | for (let i = 0; i < possibleProblems.length; i++) { 92 | const problemToken = possibleProblems[i]; 93 | const offset = problemToken.offset; 94 | const originalTokenLength = problemToken.token.length; 95 | 96 | const suggestionObj = problemToken.suggestions[0]; 97 | if (suggestionObj.score < .5) { 98 | continue; 99 | } 100 | 101 | const suggestion = suggestionObj.suggestion; 102 | const lengthDiff = suggestion.length - originalTokenLength; 103 | 104 | tempText = tempText.substring(0, offset + diff) + suggestion + tempText.substring(offset + diff + originalTokenLength); 105 | 106 | diff += lengthDiff; 107 | } 108 | 109 | return tempText; 110 | } 111 | 112 | bot.dialog('middleware-dialog', [ 113 | (session, arg) => { 114 | let text = session.message.text; 115 | session.send(text); 116 | } 117 | ]); 118 | 119 | // middleware to always convert incoming text thorugh spell check 120 | bot.use({ 121 | receive: function (event, next) { 122 | if (event.type === 'message') { 123 | spellCheck(event.text, 'spell').then(resultText => { 124 | event.text = resultText; 125 | next(); 126 | }); 127 | } 128 | }, 129 | send: function (event, next) { 130 | next(); 131 | } 132 | }); -------------------------------------------------------------------------------- /chapter10-spell-check-bot/env.defaults: -------------------------------------------------------------------------------- 1 | MICROSOFT_APP_ID= 2 | MICROSOFT_APP_PASSWORD= 3 | SC_KEY= -------------------------------------------------------------------------------- /chapter10-spell-check-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spell-check-bot", 3 | "version": "1.0.0", 4 | "description": "Spell Check Bot from Chapter 10, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js", 7 | "debug": "node --nolazy --inspect-brk=9229 app.js" 8 | }, 9 | "author": "Szymon Rozga", 10 | "license": "MIT", 11 | "dependencies": { 12 | "botbuilder": "^3.12.0", 13 | "cognitive-services": "^0.6.2", 14 | "dotenv-extended": "^1.0.4", 15 | "moment": "^2.19.3", 16 | "restify": "^4.3.2", 17 | "underscore": "^1.8.3" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^4.10.0", 21 | "eslint-config-google": "^0.9.1", 22 | "eslint-config-standard": "^10.2.1", 23 | "eslint-plugin-import": "^2.8.0", 24 | "eslint-plugin-node": "^5.2.1", 25 | "eslint-plugin-promise": "^3.6.0", 26 | "eslint-plugin-standard": "^3.0.1" 27 | }, 28 | "main": "app.js" 29 | } 30 | -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/README.md: -------------------------------------------------------------------------------- 1 | # Image Rendering Bot 2 | 3 | Practical Bot Development - Chapter 11 4 | 5 | This bot demostrates the process of rendering a HTML template using headless Chrome using puppeteer. The bot stores rendered images in Azure blob storage. 6 | 7 | ## Getting Started 8 | 9 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id and Bot Password. You will also need a trial account with Intrinio to fetch financial data. Lastly, to store images in Azure Blob Storage, you will need an Azure Storage connection string. 10 | 11 | ### Installing and Running 12 | 13 | Easy. Peasy. 14 | 15 | ``` 16 | npm install 17 | npm start 18 | ``` 19 | 20 | By default, the bot will run on port 3978. 21 | 22 | * Connect your emulator to http://localhost:3978/api/messages 23 | * Enter symbol names such as AAPL, MSFT or FB to get a quote and rendered image -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/cardTemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 104 | 105 | 106 | 107 | 108 |
109 |
110 | ${companyName} 111 | ${ticker} 112 |
113 |
114 | ${last_price} 115 | ${change} 116 | ${percent_change} 117 | 118 | 119 | 120 | 121 |
122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 |
Bid${bid}
Ask${ask}
52 Week Low${52weeklow}
52 Week High${52weekhigh}
140 |
141 | 142 | -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/env.defaults: -------------------------------------------------------------------------------- 1 | MICROSOFT_APP_ID= 2 | MICROSOFT_APP_PASSWORD= 3 | 4 | INTRINIO_USER= 5 | INTRINIO_PASS= 6 | 7 | IMAGE_STORAGE_CONNECTION_STRING= -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/13619778-5eab-41d8-bd93-a96a82269d1f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/13619778-5eab-41d8-bd93-a96a82269d1f.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/3a84409b-1c4d-41e7-ace9-344e69890d24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/3a84409b-1c4d-41e7-ace9-344e69890d24.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/689b99da-907c-4039-a816-22b271b7ee66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/689b99da-907c-4039-a816-22b271b7ee66.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/8c1db6de-0957-4b3d-9a22-5dc3f93850a9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/8c1db6de-0957-4b3d-9a22-5dc3f93850a9.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/92e0630b-2e57-43aa-91d7-5e7bff5402af.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/92e0630b-2e57-43aa-91d7-5e7bff5402af.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/b1e21779-9d97-4794-a5ae-cca905b65752.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/b1e21779-9d97-4794-a5ae-cca905b65752.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/b4677df8-7ecb-49c1-94b0-d7a89f754cdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/b4677df8-7ecb-49c1-94b0-d7a89f754cdb.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/cb07be5f-b8d0-4022-ae71-98791bf84386.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/cb07be5f-b8d0-4022-ae71-98791bf84386.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/cc0326ac-9749-4c0c-8bd9-17e863a2b4b3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/cc0326ac-9749-4c0c-8bd9-17e863a2b4b3.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/da7759d5-abd7-4309-874b-5c3d615c8c36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/da7759d5-abd7-4309-874b-5c3d615c8c36.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/e1cfa34b-d043-45e8-9cd8-1387bcb79f4e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/e1cfa34b-d043-45e8-9cd8-1387bcb79f4e.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/f1ac4f39-161c-434a-8854-55b7f99a2e74.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/f1ac4f39-161c-434a-8854-55b7f99a2e74.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/images/f9a50830-2042-40b0-834f-7338b032bcce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Practical-Bot-Development/c8eb653ba3018f9f52be0208b8c607fce9ccde8d/chapter11-image-rendering-bot/images/f9a50830-2042-40b0-834f-7338b032bcce.png -------------------------------------------------------------------------------- /chapter11-image-rendering-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stock-quote-image-bot", 3 | "version": "1.0.0", 4 | "description": "Stock Quote Image Bot from Chapter 11, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js", 7 | "debug": "node --nolazy --inspect-brk=9229 app.js" 8 | }, 9 | "author": "Szymon Rozga", 10 | "license": "MIT", 11 | "dependencies": { 12 | "azure-storage": "^2.7.0", 13 | "botbuilder": "^3.12.0", 14 | "dotenv-extended": "^1.0.4", 15 | "moment": "^2.19.3", 16 | "puppeteer": "^0.13.0", 17 | "request": "^2.83.0", 18 | "restify": "^4.3.2", 19 | "sprintf": "^0.1.5", 20 | "underscore": "^1.8.3", 21 | "uuid": "^3.1.0" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^4.10.0", 25 | "eslint-config-google": "^0.9.1", 26 | "eslint-config-standard": "^10.2.1", 27 | "eslint-plugin-import": "^2.8.0", 28 | "eslint-plugin-node": "^5.2.1", 29 | "eslint-plugin-promise": "^3.6.0", 30 | "eslint-plugin-standard": "^3.0.1" 31 | }, 32 | "main": "app.js" 33 | } 34 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/README.md: -------------------------------------------------------------------------------- 1 | # Calendar Bot 2 | 3 | This bot is a demostration of how we can use the Microsoft Bot Builder SDK and LUIS to create a bot that is able to action on a calendar. Functionality includes add, remove, move appointments, summarize calendar and check availability. 4 | 5 | This git repo is: 6 | * chapter-5 - simple LUIS bot, no logic 7 | * chapter-7 - auth + api integration 8 | * chapter-10 - multi language support 9 | * chapter-12 - human handover 10 | * master - equivalent to chapter 10 11 | 12 | ## Getting Started 13 | 14 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id, Bot Password, LUIS app id and LUIS subscription key. 15 | 16 | ### Installing and Running 17 | 18 | Easy. Peasy. 19 | 20 | ``` 21 | npm install 22 | npm start 23 | ``` 24 | 25 | By default, the bot will run on port 3978. Download the [Bot Framework Emulator](https://docs.microsoft.com/en-us/bot-framework/debug-bots-emulator) to test locally. -------------------------------------------------------------------------------- /chapter12-calendar-bot/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | const builder = require('botbuilder'); 4 | const restify = require('restify'); 5 | 6 | const constants = require('./constants'); 7 | const utils = require('./utils'); 8 | 9 | const helpModule = require('./dialogs/help'); 10 | const authModule = require('./dialogs/auth'); 11 | const addEntryModule = require('./dialogs/addEntry'); 12 | const removeEntryModule = require('./dialogs/removeEntry'); 13 | const editEntryModule = require('./dialogs/editEntry'); 14 | const checkAvailabilityModule = require('./dialogs/checkAvailability'); 15 | const summarizeModule = require('./dialogs/summarize'); 16 | const primaryCalendarModule = require('./dialogs/primaryCalendar'); 17 | const prechecksModule = require('./dialogs/prechecks'); 18 | const humanEscalationModule = require('./dialogs/humanEscalation'); 19 | 20 | authModule.setResolvePostLoginDialog((session, args) => { 21 | if (!session.privateConversationData.calendarId) { 22 | args.followUpDialog = primaryCalendarModule.getPrimaryCalendarDialogName(); 23 | args.followUpDialogArgs = { 24 | intent: { 25 | entities: [ 26 | utils.wrapEntity(constants.entityNames.Action, constants.entityValues.Action.set) 27 | ] 28 | } 29 | }; 30 | } 31 | return args; 32 | }); 33 | 34 | // setup our web server 35 | const server = restify.createServer(); 36 | server.use(restify.queryParser()); 37 | server.listen(process.env.port || process.env.PORT || 3978, () => { 38 | console.log('%s listening to %s', server.name, server.url); 39 | }); 40 | 41 | // initialize the chat bot 42 | const connector = new builder.ChatConnector({ 43 | appId: process.env.MICROSOFT_APP_ID, 44 | appPassword: process.env.MICROSOFT_APP_PASSWORD 45 | }); 46 | 47 | const bot = new builder.UniversalBot(connector, [ 48 | session => { 49 | helpModule.help(session); 50 | } 51 | ]); 52 | server.post('/api/messages', connector.listen()); 53 | server.get('/oauth2callback', (req, res, next) => { 54 | authModule.oAuth2Callback(bot, req, res, next); 55 | }); 56 | 57 | // removing multi language support for now 58 | // const TranslatorMiddleware = require('./translatorMiddleware').TranslatorMiddleware; 59 | // bot.use(new TranslatorMiddleware()); 60 | 61 | humanEscalationModule.pageAccessToken(process.env.PAGE_ACCESS_TOKEN); 62 | 63 | const luisModelUri = 'https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/' + process.env.LUIS_APP + '?subscription-key=' + process.env.LUIS_SUBSCRIPTION_KEY; 64 | bot.recognizer(new builder.LuisRecognizer(luisModelUri)); 65 | 66 | bot.library(addEntryModule.create()); 67 | bot.library(helpModule.create()); 68 | bot.library(authModule.create()); 69 | bot.library(removeEntryModule.create()); 70 | bot.library(editEntryModule.create()); 71 | bot.library(checkAvailabilityModule.create()); 72 | bot.library(summarizeModule.create()); 73 | bot.library(primaryCalendarModule.create()); 74 | bot.library(prechecksModule.create()); 75 | bot.library(humanEscalationModule.create()); 76 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/constants.js: -------------------------------------------------------------------------------- 1 | exports.dialogNames = { 2 | Help: 'help', 3 | AddCalendarEntry: 'addCalendarEntry', 4 | AddCalendarEntryHelp: 'addCalendarEntryHelp', 5 | RemoveCalendarEntryHelp: 'removeCalendarEntryHelp', 6 | RemoveCalendarEntry: 'removeCalendarEntry', 7 | RemoveCalendarEntry_Time: 'removeCalendarEntry.time', 8 | RemoveCalendarEntry_Invitee: 'removeCalendarEntry.invitee', 9 | EditCalendarEntry: 'editCalendarEntry', 10 | ShowCalendarSummary: 'showCalendarSummary', 11 | CheckAvailability: 'checkAvailability', 12 | Login: 'login', 13 | PrimaryCalendar: 'primaryCalendar', 14 | EnsurePrimaryCalendar: 'ensurePrimaryCalendar', 15 | PreCheck_AuthAndPrimaryCalendar: 'preCheck_authAndPrimaryCalendar', 16 | Auth: { 17 | Login: 'auth.login', 18 | Logout: 'auth.logout', 19 | EnsureCredentials: 'auth.ensureCredentials', 20 | AuthConfirmation: 'auth.authConfirmation', 21 | StoreTokensAndResume: 'auth.storeAndResume', 22 | Error: 'auth.error' 23 | } 24 | }; 25 | 26 | exports.intentNames = { 27 | Help: 'Help', 28 | HumanHandover: 'HumanHandover', 29 | AddCalendarEntry: 'AddCalendarEntry', 30 | RemoveCalendarEntry: 'DeleteCalendarEntry', 31 | CheckAvailability: 'CheckAvailability', 32 | EditCalendarEntry: 'EditCalendarEntry', 33 | ShowCalendarSummary: 'ShowCalendarSummary', 34 | PrimaryCalendar: 'PrimaryCalendar', 35 | None: 'None' 36 | }; 37 | 38 | exports.entityNames = { 39 | Chrono: 'chrono.duration', 40 | Invitee: 'CalendarBot.Invitee', 41 | Action: 'Action', 42 | CalendarId: 'CalendarId', 43 | EventId: 'EventId', 44 | Subject: 'CalendarBot.Subject', 45 | Location: 'CalendarBot.Location', 46 | Composite: { 47 | CalendarRequest: 'CalendarRequest' 48 | }, 49 | MeetingMove: { 50 | FromTime: 'MeetingMove::FromTime', 51 | ToTime: 'MeetingMove::ToTime' 52 | }, 53 | EntryVisibility: { 54 | Public: 'EntryVisbility::Public', 55 | Private: 'EntryVisbility::Private' 56 | }, 57 | Dates: { 58 | Date: 'builtin.datetimeV2.date', 59 | DateTime: 'builtin.datetimeV2.datetime', 60 | Time: 'builtin.datetimeV2.time', 61 | DateRange: 'builtin.datetimeV2.daterange', 62 | TimeRange: 'builtin.datetimeV2.timerange', 63 | DateTimeRange: 'builtin.datetimeV2.datetimerange', 64 | Set: 'builtin.datetimeV2.set', 65 | Duration: 'builtin.datetimeV2.duration' 66 | }, 67 | Email: 'builtin.email' 68 | }; 69 | 70 | exports.entityValues = { 71 | Action: { 72 | get: 'get', 73 | set: 'set', 74 | clear: 'remove' 75 | } 76 | }; 77 | 78 | exports.LUISTimePattern = 'HH:mm:ss'; 79 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/dialogs/editEntry.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const _ = require('underscore'); 3 | 4 | const constants = require('../constants'); 5 | const mt = require('../moveTranslator'); 6 | 7 | const lib = new builder.Library('editEntry'); 8 | 9 | /* Building out an entire edit functionality can be a large undertaking. There are many possible attributes to edit in many possible ways. We start with the ability 10 | * to move appointments using natural language */ 11 | lib.dialog(constants.dialogNames.EditCalendarEntry, [ 12 | (session, args, next) => { 13 | // move from and move to are helper entities to help us figure out where the from and to times like. The LUIS range datetime 14 | // entities can be good but you could imagine a user phrasing the query differently. for example, move 4p meeting to 6pm, 15 | // would not be identified as a range 16 | const moveFrom = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.FromTime); 17 | const moveTo = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.ToTime); 18 | 19 | const allOtherEntities = _.where(args.intent.entities, function (p) { 20 | return p.type !== constants.entityNames.MeetingMove.FromTime && p.type !== constants.entityNames.MeetingMove.ToTime; 21 | }); 22 | 23 | // use the move translator to properly identiy the from and to 24 | const moveTranslator = new mt.MoveTranslator(); 25 | moveTranslator.applyEntities(moveTo, allOtherEntities, moveFrom); 26 | 27 | let prefix = 'Moving '; 28 | 29 | if (moveTranslator.hasSubject) prefix += (moveTranslator.subject + ' '); 30 | else prefix += 'meeting '; 31 | 32 | if (moveTranslator.hasInvitee) prefix += ('with ' + moveTranslator.invitee + ' '); 33 | 34 | if (moveTranslator.moveFrom && moveTranslator.moveTo) { 35 | if (moveTranslator.isDateBased) { 36 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L') + ' to ' + moveTranslator.moveTo.format('L')); 37 | } else { 38 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L LT') + ' to ' + moveTranslator.moveTo.format('L LT')); 39 | } 40 | } else if (!moveTranslator.moveFrom && moveTranslator.moveTo) { 41 | if (moveTranslator.isDateBased) { 42 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L')); 43 | } else { 44 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L LT')); 45 | } 46 | } else if (!moveTranslator.moveFrom && !moveTranslator.moveTo) { 47 | session.endDialog("I'm sorry, I'm not sure how to handle that request."); 48 | } 49 | } 50 | ]).triggerAction({ matches: constants.intentNames.EditCalendarEntry }); 51 | 52 | exports.create = () => { return lib.clone(); } 53 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/dialogs/help.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | 3 | const constants = require('../constants'); 4 | 5 | const lib = new builder.Library('help'); 6 | 7 | exports.help = (session) => { 8 | session.beginDialog('help:' + constants.dialogNames.Help); 9 | }; 10 | 11 | // help message when help requested during the add calendar entry dialog 12 | lib.dialog(constants.dialogNames.AddCalendarEntryHelp, (session, args, next) => { 13 | const msg = "To add an appointment, we gather the following information: time, subject and location. You can also simply say 'add appointment with Bob tomorrow at 2pm for an hour for coffee' and we'll take it from there!"; 14 | session.endDialog(msg); 15 | }); 16 | 17 | // help message when help requested during the remove calendar entry dialog 18 | lib.dialog(constants.dialogNames.RemoveCalendarEntryHelp, (session, args, next) => { 19 | const msg = ''; 20 | session.endDialog(msg); 21 | }); 22 | 23 | // top level help 24 | lib.dialog(constants.dialogNames.Help, (session, args, next) => { 25 | session.endDialog('Hi, I am a calendar concierge bot. I can help you create, delete and move appointments. I can also tell you about your calendar and check your availability!'); 26 | }).triggerAction({ 27 | matches: constants.intentNames.Help, 28 | onSelectAction: (session, args, next) => { 29 | session.beginDialog(args.action, args); 30 | } 31 | }); 32 | 33 | exports.create = () => { return lib.clone(); } 34 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/dialogs/humanEscalation.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const constants = require('../constants'); 3 | const request = require('request'); 4 | 5 | const libName = 'humanEscalation'; 6 | const escalateDialogName = 'escalate'; 7 | 8 | const lib = new builder.Library(libName); 9 | 10 | let pageAccessToken = null; 11 | exports.pageAccessToken = (val) => { 12 | if(val) pageAccessToken = val; 13 | return pageAccessToken; 14 | }; 15 | 16 | exports.escalateToHuman = (session, pageAccessTokenArg, userId) => { 17 | session.beginDialog(libName + ':' + escalateDialogName, { pageAccessToken: pageAccessTokenArg || pageAccessToken }); 18 | }; 19 | 20 | lib.dialog(escalateDialogName, (session, args, next) => { 21 | handover(session.message.address.user.id, args.pageAccessToken || pageAccessToken); 22 | session.endDialog('Just hold tight... getting someone for you...'); 23 | }).triggerAction({ 24 | matches: constants.intentNames.HumanHandover 25 | }); 26 | 27 | exports.create = () => { return lib.clone(); } 28 | 29 | function makeFacebookGraphRequest(d, psid, metadata, procedure, pageAccessToken) { 30 | const data = Object.assign({}, d); 31 | data.recipient = { 'id': psid }; 32 | data.metadata = metadata; 33 | 34 | const options = { 35 | uri: "https://graph.facebook.com/v2.6/me/" + procedure + "?access_token=" + pageAccessToken, 36 | json: data, 37 | method: 'POST' 38 | }; 39 | return new Promise((resolve, reject) => { 40 | request(options, function (error, response, body) { 41 | if (error) { 42 | console.log(error); 43 | reject(error); 44 | return; 45 | } 46 | console.log(body); 47 | resolve(); 48 | }); 49 | }); 50 | } 51 | 52 | const secondaryApp = 263902037430900; // this is the ID to utilize when handing over to a page 53 | function handover(psid, pageAccessToken) { 54 | return makeFacebookGraphRequest({ 'target_app_id': secondaryApp }, psid, 'test', 'pass_thread_control', pageAccessToken); 55 | } 56 | 57 | function takeControl(psid, pageAccessToken) { 58 | return makeFacebookGraphRequest({}, psid, 'test', 'take_thread_control', pageAccessToken); 59 | } -------------------------------------------------------------------------------- /chapter12-calendar-bot/dialogs/prechecks.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | 3 | const constants = require('../constants'); 4 | 5 | const authModule = require('./auth'); 6 | const primaryCalendarModule = require('./primaryCalendar'); 7 | 8 | const libName = 'prechecks'; 9 | const lib = new builder.Library(libName); 10 | 11 | lib.dialog(constants.dialogNames.PreCheck_AuthAndPrimaryCalendar, [ 12 | (session, args) => { 13 | authModule.ensureLoggedIn(session); 14 | }, 15 | (session, args) => { 16 | if (!args.response.authenticated) { 17 | session.endDialogWithResult({ response: { error: 'You must authenticate to continue.', error_auth: true } }); 18 | } else { 19 | primaryCalendarModule.ensurePrimaryCalendar(session); 20 | } 21 | }, 22 | (session, args, next) => { 23 | if (session.privateConversationData.calendarId) session.endDialogWithResult({ response: { } }); 24 | else session.endDialogWithResult({ response: { error: 'You must set a primary calendar to continue.', error_calendar: true } }); 25 | } 26 | ]); 27 | 28 | exports.create = () => { return lib.clone(); } 29 | exports.ensurePrechecks = session => { 30 | session.beginDialog(libName + ':' + constants.dialogNames.PreCheck_AuthAndPrimaryCalendar); 31 | }; 32 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/dialogs/summarize.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const _ = require('underscore'); 3 | const moment = require('moment'); 4 | 5 | const utils = require('../utils'); 6 | const constants = require('../constants'); 7 | const et = require('../entityTranslator'); 8 | const gcalapi = require('../services/calendar-api'); 9 | 10 | const authModule = require('./auth'); 11 | const prechecksModule = require('./prechecks'); 12 | 13 | const lib = new builder.Library('summarize'); 14 | 15 | lib.dialog(constants.dialogNames.ShowCalendarSummary, [ 16 | function (session, args) { 17 | session.dialogData.intent = args.intent; 18 | prechecksModule.ensurePrechecks(session); 19 | }, 20 | function (session, args, next) { 21 | if (args.response.error) { 22 | session.endDialog(args.response.error); 23 | return; 24 | } 25 | next(); 26 | }, 27 | function (session, args, next) { 28 | const auth = authModule.getAuthClientFromSession(session); 29 | const entry = new et.EntityTranslator(); 30 | et.EntityTranslatorUtils.attachSummaryEntities(entry, session.dialogData.intent.entities); 31 | let start = null; 32 | let end = null; 33 | 34 | if (entry.hasRange) { 35 | if (entry.isDateTimeEntityDateBased) { 36 | start = moment(entry.range.start).startOf('day'); 37 | end = moment(entry.range.end).endOf('day'); 38 | } else { 39 | start = moment(entry.range.start); 40 | end = moment(entry.range.end); 41 | } 42 | } else if (entry.hasDateTime) { 43 | if (entry.isDateTimeEntityDateBased) { 44 | start = moment(entry.dateTime).startOf('day'); 45 | end = moment(entry.dateTime).endOf('day'); 46 | } else { 47 | start = moment(entry.dateTime).add(-1, 'h'); 48 | end = moment(entry.dateTime).add(1, 'h'); 49 | } 50 | } else { 51 | session.endDialog("Sorry I don't know what you mean"); 52 | return; 53 | } 54 | 55 | const p = gcalapi.listEvents(auth, session.privateConversationData.calendarId, start, end); 56 | p.then(function (events) { 57 | let evs = _.sortBy(events, function (p) { 58 | if (p.start.date) { 59 | return moment(p.start.date).add(-1, 's').valueOf(); 60 | } else if (p.start.dateTime) { 61 | return moment(p.start.dateTime).valueOf(); 62 | } 63 | }); 64 | 65 | // should also potentially filter by subject 66 | evs = _.filter(evs, function (p) { 67 | if (!entry.hasSubject) return true; 68 | 69 | const containsSubject = entry.subject.toLowerCase().indexOf(entry.subject.toLowerCase()) >= 0; 70 | return containsSubject; 71 | }); 72 | 73 | const eventmsg = new builder.Message(session); 74 | if (evs.length > 1) { 75 | eventmsg.text('Here is what I found...'); 76 | } else if (evs.length === 1) { 77 | eventmsg.text('Here is the event I found.'); 78 | } else { 79 | eventmsg.text('Seems you have nothing going on then. What a sad existence you lead.'); 80 | } 81 | 82 | if (evs.length >= 1) { 83 | const cards = _.map(evs, function (p) { 84 | return utils.createEventCard(session, p); 85 | }); 86 | eventmsg.attachmentLayout(builder.AttachmentLayout.carousel); 87 | eventmsg.attachments(cards); 88 | } 89 | 90 | session.send(eventmsg); 91 | session.endDialog(); 92 | }); 93 | } 94 | ]).triggerAction({ matches: constants.intentNames.ShowCalendarSummary }); 95 | 96 | exports.create = function () { return lib.clone(); } 97 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/env.defaults: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | MICROSOFT_APP_ID= 3 | MICROSOFT_APP_PASSWORD= 4 | LUIS_APP= 5 | LUIS_SUBSCRIPTION_KEY= 6 | 7 | # Google OAuth Bits 8 | GOOGLE_OAUTH_CLIENT_ID= 9 | GOOGLE_OAUTH_CLIENT_SECRET= 10 | GOOGLE_OAUTH_REDIRECT_URI= 11 | 12 | # Security 13 | AES_PASSPHRASE= 14 | 15 | # Cognitive Services 16 | TA_KEY= 17 | TA_ENDPOINT= 18 | TRANSLATOR_KEY= 19 | 20 | # Facebook 21 | PAGE_ACCESS_TOKEN= -------------------------------------------------------------------------------- /chapter12-calendar-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-calendar-bot-buildup", 3 | "version": "1.0.0", 4 | "description": "Calendar Bot - Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "botbuilder": "^3.13.1", 12 | "cognitive-services": "^0.6.2", 13 | "crypto-js": "^3.1.9-1", 14 | "dotenv-extended": "^1.0.4", 15 | "googleapis": "^22.2.0", 16 | "moment": "^2.19.4", 17 | "mstranslator": "^3.0.0", 18 | "request": "^2.83.0", 19 | "restify": "^4.3.2", 20 | "underscore": "^1.8.3" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^4.15.0", 24 | "eslint-config-google": "^0.9.1", 25 | "eslint-config-standard": "^10.2.1", 26 | "eslint-plugin-import": "^2.8.0", 27 | "eslint-plugin-node": "^5.2.1", 28 | "eslint-plugin-promise": "^3.6.0", 29 | "eslint-plugin-standard": "^3.0.1", 30 | "jasmine-node": "^1.14.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/services/calendar-api.js: -------------------------------------------------------------------------------- 1 | const google = require('googleapis'); 2 | const calendar = google.calendar('v3'); 3 | 4 | function listEvents (auth, calendarId, start, end, subject) { 5 | const p = new Promise(function (resolve, reject) { 6 | calendar.events.list({ 7 | auth: auth, 8 | calendarId: calendarId, 9 | timeMin: start.toISOString(), 10 | timeMax: end.toISOString(), 11 | q: subject 12 | }, function (err, response) { 13 | if (err) reject(err); 14 | resolve(response.items); 15 | }); 16 | }); 17 | return p; 18 | } 19 | 20 | function listCalendars (auth) { 21 | const p = new Promise(function (resolve, reject) { 22 | calendar.calendarList.list({ 23 | auth: auth 24 | }, function (err, response) { 25 | if (err) reject(err); 26 | else resolve(response.items); 27 | }); 28 | }); 29 | return p; 30 | }; 31 | 32 | function getCalendar (auth, calendarId) { 33 | const p = new Promise(function (resolve, reject) { 34 | calendar.calendarList.get({ 35 | auth: auth, 36 | calendarId: calendarId 37 | }, function (err, response) { 38 | if (err) reject(err); 39 | else resolve(response); 40 | }); 41 | }); 42 | return p; 43 | }; 44 | 45 | function freeBusy (auth, calendarId, start, end) { 46 | const p = new Promise(function (resolve, reject) { 47 | const param = { 48 | auth: auth, 49 | resource: { 50 | timeMin: start.toISOString(), 51 | timeMax: end.toISOString(), 52 | items: [ { id: calendarId } ] 53 | } 54 | }; 55 | 56 | calendar.freebusy.query(param, function (err, response) { 57 | if (err) reject(err); 58 | else resolve(response); 59 | }); 60 | }); 61 | return p; 62 | } 63 | 64 | function insertEvent (auth, calendarId, start, end, summary, location) { 65 | const p = new Promise(function (resolve, reject) { 66 | calendar.events.insert({ 67 | auth: auth, 68 | calendarId: calendarId, 69 | resource: { 70 | end: { 71 | dateTime: end.toISOString() 72 | }, 73 | start: { 74 | dateTime: start.toISOString() 75 | }, 76 | summary: summary 77 | } 78 | }, function (err, response) { 79 | if (err) reject(err); 80 | else resolve(response); 81 | }); 82 | }); 83 | return p; 84 | } 85 | 86 | const allDayDateFormat = 'YYYY-MM-DD'; 87 | 88 | function insertAllDayEvent (auth, calendarId, start, end, summary, location) { 89 | const p = new Promise(function (resolve, reject) { 90 | calendar.events.insert({ 91 | auth: auth, 92 | calendarId: calendarId, 93 | resource: { 94 | end: { 95 | date: end.format(allDayDateFormat) 96 | }, 97 | start: { 98 | date: start.format(allDayDateFormat) 99 | }, 100 | summary: summary 101 | } 102 | }, function (err, response) { 103 | if (err) reject(err); 104 | else resolve(response); 105 | }); 106 | }); 107 | return p; 108 | } 109 | 110 | function removeEvent (auth, calendarId, eventId) { 111 | const p = new Promise(function (resolve, reject) { 112 | calendar.events.delete({ 113 | auth: auth, 114 | calendarId: calendarId, 115 | eventId: eventId 116 | }, function (err, response) { 117 | if (err) reject(err); 118 | else resolve(response); 119 | }); 120 | }); 121 | return p; 122 | } 123 | 124 | function getEvent (auth, calendarId, eventId) { 125 | const p = new Promise(function (resolve, reject) { 126 | calendar.events.get({ 127 | auth: auth, 128 | calendarId: calendarId, 129 | eventId: eventId 130 | }, function (err, response) { 131 | if (err) reject(err); 132 | else resolve(response); 133 | }); 134 | }); 135 | return p; 136 | } 137 | 138 | exports.removeEvent = removeEvent; 139 | exports.insertAllDayEvent = insertAllDayEvent; 140 | exports.listEvents = listEvents; 141 | exports.insertEvent = insertEvent; 142 | exports.getEvent = getEvent; 143 | exports.listCalendars = listCalendars; 144 | exports.getCalendar = getCalendar; 145 | exports.freeBusy = freeBusy; 146 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /chapter12-calendar-bot/translatorMiddleware.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore'); 2 | const cognitiveServices = require('cognitive-services'); 3 | const translator = require('mstranslator'); 4 | 5 | const textAnalytics = new cognitiveServices.textAnalytics({ 6 | apiKey: process.env.TA_KEY, 7 | endpoint: process.env.TA_ENDPOINT 8 | }); 9 | const translatorApi = new translator({ api_key: process.env.TRANSLATOR_KEY }, true); 10 | const userLanguageMap = {}; 11 | 12 | class TranslatorMiddleware { 13 | receive(event, next) { 14 | if (event.type !== 'message') { next(); return; } 15 | 16 | if (event.text == null || event.text.length == 0) { 17 | // if there is not input and we already have a language, leave as is, otherwise set to English 18 | userLanguageMap[event.user.id] = userLanguageMap[event.user.id] || 'en'; 19 | next(); 20 | return; 21 | } 22 | 23 | textAnalytics.detectLanguage({ 24 | body: { 25 | documents: [ 26 | { 27 | id: "1", 28 | text: event.text 29 | } 30 | ] 31 | } 32 | }).then(result => { 33 | const languageOptions = _.find(result.documents, p => p.id === "1").detectedLanguages; 34 | let lang = 'en'; 35 | 36 | if (languageOptions && languageOptions.length > 0) { 37 | lang = languageOptions[0].iso6391Name; 38 | } 39 | userLanguageMap[event.user.id] = lang; 40 | 41 | if (lang === 'en') next(); 42 | else { 43 | translatorApi.translate({ 44 | text: event.text, 45 | from: languageOptions[0].iso6391Name, 46 | to: 'en' 47 | }, (err, result) => { 48 | if (err) { 49 | console.error(err); 50 | lang = 'en'; 51 | userLanguageMap[event.user.id] = lang; 52 | next(); 53 | } 54 | else { 55 | event.text = result; 56 | next(); 57 | } 58 | }); 59 | } 60 | }); 61 | } 62 | send(event, next) { 63 | if (event.type === 'message') { 64 | const userLang = userLanguageMap[event.address.user.id] || 'en'; 65 | 66 | if (userLang === 'en') { next(); } 67 | else { 68 | translatorApi.translate({ 69 | text: event.text, 70 | from: 'en', 71 | to: userLang 72 | }, (err, result) => { 73 | if (err) { 74 | console.error(err); 75 | next(); 76 | } 77 | else { 78 | event.text = result; 79 | next(); 80 | } 81 | }); 82 | } 83 | } 84 | else { 85 | next(); 86 | } 87 | } 88 | } 89 | 90 | exports.TranslatorMiddleware = TranslatorMiddleware; -------------------------------------------------------------------------------- /chapter12-calendar-bot/utils.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const builder = require('botbuilder'); 3 | 4 | function wrapEntity (entityType, value) { 5 | return { 6 | type: entityType, 7 | entity: value, 8 | resolution: { 9 | values: [ 10 | value 11 | ] 12 | } 13 | }; 14 | }; 15 | 16 | function createCalendarCard (session, calendar) { 17 | const isPrimary = session.privateConversationData.calendarId === calendar.id; 18 | 19 | let subtitle = 'Your role: ' + calendar.accessRole; 20 | if (isPrimary) { 21 | subtitle = 'Primary\r\n' + subtitle; 22 | } 23 | let buttons = []; 24 | if (!isPrimary) { 25 | let btnval = 'Set primary calendar to ' + calendar.id; 26 | buttons = [builder.CardAction.postBack(session, btnval, 'Set as primary')]; 27 | } 28 | 29 | const heroCard = new builder.HeroCard(session) 30 | .title(calendar.summary) 31 | .subtitle(subtitle) 32 | .buttons(buttons); 33 | return heroCard; 34 | }; 35 | 36 | function createEventCard (session, event) { 37 | let start, end, subtitle; 38 | if (!event.start.date) { 39 | start = moment(event.start.dateTime); 40 | end = moment(event.end.dateTime); 41 | 42 | const diffInMinutes = end.diff(start, 'm'); 43 | const diffInHours = end.diff(start, 'h'); 44 | 45 | let duration = diffInMinutes + ' minutes'; 46 | if (diffInHours >= 1) { 47 | const hrs = Math.floor(diffInHours); 48 | const mins = diffInMinutes - (hrs * 60); 49 | 50 | if (mins === 0) { 51 | duration = hrs + 'hrs'; 52 | } else { 53 | duration = hrs + (hrs > 1 ? 'hrs ' : 'hr ') + (mins < 10 ? ('0' + mins) : mins) + 'mins'; 54 | } 55 | } 56 | subtitle = 'At ' + start.format('L LT') + ' for ' + duration; 57 | } else { 58 | start = moment(event.start.date); 59 | end = moment(event.end.date); 60 | 61 | const diffInDays = end.diff(start, 'd'); 62 | subtitle = 'All Day ' + start.format('L') + (diffInDays > 1 ? end.format('L') : ''); 63 | } 64 | 65 | const heroCard = new builder.HeroCard(session) 66 | .title(event.summary) 67 | .subtitle(subtitle) 68 | .buttons([ 69 | builder.CardAction.openUrl(session, event.htmlLink, 'Open Google Calendar'), 70 | builder.CardAction.postBack(session, 'Delete event with id ' + event.id, 'Delete') 71 | ]); 72 | return heroCard; 73 | }; 74 | 75 | exports.createEventCard = createEventCard; 76 | exports.wrapEntity = wrapEntity; 77 | exports.createCalendarCard = createCalendarCard; 78 | -------------------------------------------------------------------------------- /chapter12-facebook-human-escalation/README.md: -------------------------------------------------------------------------------- 1 | # Human Handover Facebook Bot 2 | 3 | Practical Bot Development - Chapter 12 4 | 5 | This bot demostrates the process running a basic bot on Facebook that can handover the conversation from the bot to a human supporting the bot Facebook Page. 6 | 7 | ## Getting Started 8 | 9 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id and Bot Password. You will also need your Facebook Page Access Token. 10 | 11 | ### Installing and Running 12 | 13 | Easy. Peasy. 14 | 15 | ``` 16 | npm install 17 | npm start 18 | ``` 19 | 20 | By default, the bot will run on port 3978. 21 | 22 | * Connect the Facebook connector to your local bot on https://localhost:3978/api/messages and interact with it through Facebook Messenger. 23 | * Say the word "human" to get transfered over into the Facebook Inbox -------------------------------------------------------------------------------- /chapter12-facebook-human-escalation/app.js: -------------------------------------------------------------------------------- 1 | // load env variables 2 | require('dotenv-extended').load(); 3 | 4 | const builder = require('botbuilder'); 5 | const restify = require('restify'); 6 | const request = require('request'); 7 | 8 | const pageAccessToken = process.env.PAGE_ACCESS_TOKEN; 9 | 10 | function makeRequest(d, psid, metadata, procedure) { 11 | const data = Object.assign({}, d); 12 | data.recipient = { 'id': psid }; 13 | data.metadata = metadata; 14 | 15 | const options = { 16 | uri: "https://graph.facebook.com/v2.6/me/" + procedure + "?access_token=" + pageAccessToken, 17 | json: data, 18 | method: 'POST' 19 | }; 20 | return new Promise((resolve, reject) => { 21 | request(options, function (error, response, body) { 22 | if (error) { 23 | console.log(error); 24 | reject(error); 25 | return; 26 | } 27 | console.log(body); 28 | resolve(); 29 | }); 30 | }); 31 | } 32 | 33 | function handover(psid) { 34 | return makeRequest({ 'target_app_id': 263902037430900 }, psid, 'test', 'pass_thread_control'); 35 | } 36 | 37 | function takeControl(psid) { 38 | return makeRequest({}, psid, 'test', 'take_thread_control'); 39 | } 40 | 41 | // setup our web server 42 | const server = restify.createServer(); 43 | server.listen(process.env.port || process.env.PORT || 3978, () => { 44 | console.log('%s listening to %s', server.name, server.url); 45 | }); 46 | console.log('app id: ' + process.env.MICROSOFT_APP_ID); 47 | // initialize the chat bot 48 | const connector = new builder.ChatConnector({ 49 | appId: process.env.MICROSOFT_APP_ID, 50 | appPassword: process.env.MICROSOFT_APP_PASSWORD 51 | }); 52 | const listen = connector.listen(); 53 | server.post('/api/messages', (req, res) => { 54 | console.log('incoming!'); 55 | listen(req, res); 56 | }); 57 | 58 | const bot = new builder.UniversalBot(connector, [ 59 | (session) => { 60 | console.log(JSON.stringify(session.message)); 61 | if (session.message.text.toLowerCase() === 'human') { 62 | let psid = session.message.address.user.id; 63 | session.send('connecting you...'); 64 | handover(psid); 65 | return; 66 | } 67 | session.send('echo: ' + session.message.text); 68 | } 69 | ]); 70 | const inMemoryStorage = new builder.MemoryBotStorage(); 71 | bot.set('storage', inMemoryStorage); -------------------------------------------------------------------------------- /chapter12-facebook-human-escalation/env.defaults: -------------------------------------------------------------------------------- 1 | MICROSOFT_APP_ID= 2 | MICROSOFT_APP_PASSWORD= 3 | PAGE_ACCESS_TOKEN= -------------------------------------------------------------------------------- /chapter12-facebook-human-escalation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-human-handover-fb-bot", 3 | "version": "1.0.0", 4 | "description": "Human Handover Facebook Bot from Chapter 12, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "botbuilder": "^3.14.0", 12 | "dotenv-extended": "^1.0.4", 13 | "request": "^2.83.0", 14 | "restify": "^4.3.0" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^4.10.0", 18 | "eslint-config-google": "^0.9.1", 19 | "eslint-config-standard": "^10.2.1", 20 | "eslint-plugin-import": "^2.8.0", 21 | "eslint-plugin-node": "^5.2.1", 22 | "eslint-plugin-promise": "^3.6.0", 23 | "eslint-plugin-standard": "^3.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /chapter13-calendar-bot/README.md: -------------------------------------------------------------------------------- 1 | # Calendar Bot 2 | 3 | This bot is a demostration of how we can use the Microsoft Bot Builder SDK and LUIS to create a bot that is able to action on a calendar. Functionality includes add, remove, move appointments, summarize calendar and check availability. 4 | 5 | This git repo is: 6 | * chapter-5 - simple LUIS bot, no logic 7 | * chapter-7 - auth + api integration 8 | * chapter-10 - multi language support 9 | * chapter-12 - human handover 10 | * chapter-13 - bot analytics 11 | * master - equivalent to chapter 10 12 | 13 | ## Getting Started 14 | 15 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id, Bot Password, LUIS app id and LUIS subscription key. 16 | 17 | ### Installing and Running 18 | 19 | Easy. Peasy. 20 | 21 | ``` 22 | npm install 23 | npm start 24 | ``` 25 | 26 | By default, the bot will run on port 3978. Download the [Bot Framework Emulator](https://docs.microsoft.com/en-us/bot-framework/debug-bots-emulator) to test locally. -------------------------------------------------------------------------------- /chapter13-calendar-bot/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | const builder = require('botbuilder'); 4 | const restify = require('restify'); 5 | 6 | const constants = require('./constants'); 7 | const utils = require('./utils'); 8 | 9 | const helpModule = require('./dialogs/help'); 10 | const authModule = require('./dialogs/auth'); 11 | const addEntryModule = require('./dialogs/addEntry'); 12 | const removeEntryModule = require('./dialogs/removeEntry'); 13 | const editEntryModule = require('./dialogs/editEntry'); 14 | const checkAvailabilityModule = require('./dialogs/checkAvailability'); 15 | const summarizeModule = require('./dialogs/summarize'); 16 | const primaryCalendarModule = require('./dialogs/primaryCalendar'); 17 | const prechecksModule = require('./dialogs/prechecks'); 18 | const humanEscalationModule = require('./dialogs/humanEscalation'); 19 | 20 | authModule.setResolvePostLoginDialog((session, args) => { 21 | if (!session.privateConversationData.calendarId) { 22 | args.followUpDialog = primaryCalendarModule.getPrimaryCalendarDialogName(); 23 | args.followUpDialogArgs = { 24 | intent: { 25 | entities: [ 26 | utils.wrapEntity(constants.entityNames.Action, constants.entityValues.Action.set) 27 | ] 28 | } 29 | }; 30 | } 31 | return args; 32 | }); 33 | 34 | // setup our web server 35 | const server = restify.createServer(); 36 | server.use(restify.queryParser()); 37 | server.listen(process.env.port || process.env.PORT || 3978, () => { 38 | console.log('%s listening to %s', server.name, server.url); 39 | }); 40 | 41 | // initialize the chat bot 42 | const connector = new builder.ChatConnector({ 43 | appId: process.env.MICROSOFT_APP_ID, 44 | appPassword: process.env.MICROSOFT_APP_PASSWORD 45 | }); 46 | 47 | const bot = new builder.UniversalBot(connector, [ 48 | session => { 49 | helpModule.help(session); 50 | } 51 | ]); 52 | var inMemoryStorage = new builder.MemoryBotStorage(); 53 | bot.set('storage', inMemoryStorage); 54 | 55 | server.post('/api/messages', connector.listen()); 56 | server.get('/oauth2callback', (req, res, next) => { 57 | authModule.oAuth2Callback(bot, req, res, next); 58 | }); 59 | 60 | // removing multi language support for now 61 | // const TranslatorMiddleware = require('./translatorMiddleware').TranslatorMiddleware; 62 | // bot.use(new TranslatorMiddleware()); 63 | 64 | humanEscalationModule.pageAccessToken(process.env.PAGE_ACCESS_TOKEN); 65 | 66 | const luisModelUri = 'https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/' + process.env.LUIS_APP + '?subscription-key=' + process.env.LUIS_SUBSCRIPTION_KEY; 67 | bot.recognizer(new builder.LuisRecognizer(luisModelUri)); 68 | 69 | const chatbase = require('./chatbase'); 70 | bot.use(chatbase.middleware); // install the sender middleware 71 | 72 | bot.library(addEntryModule.create()); 73 | bot.library(helpModule.create()); 74 | bot.library(authModule.create()); 75 | bot.library(removeEntryModule.create()); 76 | bot.library(editEntryModule.create()); 77 | bot.library(checkAvailabilityModule.create()); 78 | bot.library(summarizeModule.create()); 79 | bot.library(primaryCalendarModule.create()); 80 | bot.library(prechecksModule.create()); 81 | bot.library(humanEscalationModule.create()); 82 | 83 | // setup dashbot 84 | const dashbotApiMap = { 85 | facebook: process.env.DASHBOT_FB_KEY 86 | }; 87 | const dashbot = require('dashbot')(dashbotApiMap).microsoft; 88 | dashbot.setFacebookToken(process.env.PAGE_ACCESS_TOKEN); // only needed for Facebook Bots 89 | bot.use(dashbot); -------------------------------------------------------------------------------- /chapter13-calendar-bot/chatbase.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | const chatbase = require('@google/chatbase') 4 | .setApiKey(process.env.CHATBASE_KEY) // Your Chatbase API Key 5 | .setAsTypeUser() 6 | .setVersion('1.0') 7 | .setPlatform('SAMPLE'); // The platform you are interacting with the user over 8 | 9 | exports.chatbase = chatbase; 10 | chatbase.build = function (text, user_id, args, handled) { 11 | let intent = args; 12 | if (typeof (intent) !== 'string') { 13 | intent = args && args.intent && args.intent.intent; 14 | } 15 | 16 | var msg = chatbase.newMessage(); 17 | msg.setIntent(intent).setUserId(user_id).setMessage(text); 18 | 19 | if (handled === undefined && !intent) { 20 | msg.setAsNotHandled(); 21 | } else if (handled === true) { 22 | msg.setAsHandled(); 23 | } else if (handled === false) { 24 | msg.setAsNotHandled(); 25 | } 26 | 27 | return msg; 28 | } 29 | 30 | exports.middleware = { 31 | // receive: function (event, next) { 32 | // chatbase.newMessage() 33 | // .setAsTypeUser() 34 | // .setMessage(event.text) 35 | // .send() 36 | // .then(() => { 37 | // next(); 38 | // }) 39 | // .catch(err => { 40 | // console.error(err); 41 | // next(); 42 | // }); 43 | // }, 44 | send: function (event, next) { 45 | if (event.type === 'message') { 46 | const msg = chatbase.newMessage() 47 | .setAsTypeAgent() 48 | .setUserId(event.address.user.id) 49 | .setMessage(event.text); 50 | if (!event.text && event.attachments) { 51 | msg.setMessage(event.attachmentLayout); 52 | } 53 | msg.send() 54 | .then(() => { 55 | next(); 56 | }) 57 | .catch(err => { 58 | console.error(err); 59 | next(); 60 | }); 61 | } else { 62 | next(); 63 | } 64 | } 65 | }; -------------------------------------------------------------------------------- /chapter13-calendar-bot/constants.js: -------------------------------------------------------------------------------- 1 | exports.dialogNames = { 2 | Help: 'help', 3 | AddCalendarEntry: 'addCalendarEntry', 4 | AddCalendarEntryHelp: 'addCalendarEntryHelp', 5 | RemoveCalendarEntryHelp: 'removeCalendarEntryHelp', 6 | RemoveCalendarEntry: 'removeCalendarEntry', 7 | RemoveCalendarEntry_Time: 'removeCalendarEntry.time', 8 | RemoveCalendarEntry_Invitee: 'removeCalendarEntry.invitee', 9 | EditCalendarEntry: 'editCalendarEntry', 10 | ShowCalendarSummary: 'showCalendarSummary', 11 | CheckAvailability: 'checkAvailability', 12 | Login: 'login', 13 | PrimaryCalendar: 'primaryCalendar', 14 | EnsurePrimaryCalendar: 'ensurePrimaryCalendar', 15 | PreCheck_AuthAndPrimaryCalendar: 'preCheck_authAndPrimaryCalendar', 16 | Auth: { 17 | Login: 'auth.login', 18 | Logout: 'auth.logout', 19 | EnsureCredentials: 'auth.ensureCredentials', 20 | AuthConfirmation: 'auth.authConfirmation', 21 | StoreTokensAndResume: 'auth.storeAndResume', 22 | Error: 'auth.error' 23 | } 24 | }; 25 | 26 | exports.intentNames = { 27 | Help: 'Help', 28 | HumanHandover: 'HumanHandover', 29 | AddCalendarEntry: 'AddCalendarEntry', 30 | RemoveCalendarEntry: 'DeleteCalendarEntry', 31 | CheckAvailability: 'CheckAvailability', 32 | EditCalendarEntry: 'EditCalendarEntry', 33 | ShowCalendarSummary: 'ShowCalendarSummary', 34 | PrimaryCalendar: 'PrimaryCalendar', 35 | None: 'None' 36 | }; 37 | 38 | exports.entityNames = { 39 | Chrono: 'chrono.duration', 40 | Invitee: 'CalendarBot.Invitee', 41 | Action: 'Action', 42 | CalendarId: 'CalendarId', 43 | EventId: 'EventId', 44 | Subject: 'CalendarBot.Subject', 45 | Location: 'CalendarBot.Location', 46 | Composite: { 47 | CalendarRequest: 'CalendarRequest' 48 | }, 49 | MeetingMove: { 50 | FromTime: 'MeetingMove::FromTime', 51 | ToTime: 'MeetingMove::ToTime' 52 | }, 53 | EntryVisibility: { 54 | Public: 'EntryVisbility::Public', 55 | Private: 'EntryVisbility::Private' 56 | }, 57 | Dates: { 58 | Date: 'builtin.datetimeV2.date', 59 | DateTime: 'builtin.datetimeV2.datetime', 60 | Time: 'builtin.datetimeV2.time', 61 | DateRange: 'builtin.datetimeV2.daterange', 62 | TimeRange: 'builtin.datetimeV2.timerange', 63 | DateTimeRange: 'builtin.datetimeV2.datetimerange', 64 | Set: 'builtin.datetimeV2.set', 65 | Duration: 'builtin.datetimeV2.duration' 66 | }, 67 | Email: 'builtin.email' 68 | }; 69 | 70 | exports.entityValues = { 71 | Action: { 72 | get: 'get', 73 | set: 'set', 74 | clear: 'remove' 75 | } 76 | }; 77 | 78 | exports.LUISTimePattern = 'HH:mm:ss'; 79 | -------------------------------------------------------------------------------- /chapter13-calendar-bot/dialogs/editEntry.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const _ = require('underscore'); 3 | 4 | const constants = require('../constants'); 5 | const mt = require('../moveTranslator'); 6 | 7 | const lib = new builder.Library('editEntry'); 8 | 9 | /* Building out an entire edit functionality can be a large undertaking. There are many possible attributes to edit in many possible ways. We start with the ability 10 | * to move appointments using natural language */ 11 | lib.dialog(constants.dialogNames.EditCalendarEntry, [ 12 | (session, args, next) => { 13 | // move from and move to are helper entities to help us figure out where the from and to times like. The LUIS range datetime 14 | // entities can be good but you could imagine a user phrasing the query differently. for example, move 4p meeting to 6pm, 15 | // would not be identified as a range 16 | const moveFrom = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.FromTime); 17 | const moveTo = builder.EntityRecognizer.findEntity(args.intent.entities, constants.entityNames.MeetingMove.ToTime); 18 | 19 | const allOtherEntities = _.where(args.intent.entities, function (p) { 20 | return p.type !== constants.entityNames.MeetingMove.FromTime && p.type !== constants.entityNames.MeetingMove.ToTime; 21 | }); 22 | 23 | // use the move translator to properly identiy the from and to 24 | const moveTranslator = new mt.MoveTranslator(); 25 | moveTranslator.applyEntities(moveTo, allOtherEntities, moveFrom); 26 | 27 | let prefix = 'Moving '; 28 | 29 | if (moveTranslator.hasSubject) prefix += (moveTranslator.subject + ' '); 30 | else prefix += 'meeting '; 31 | 32 | if (moveTranslator.hasInvitee) prefix += ('with ' + moveTranslator.invitee + ' '); 33 | 34 | if (moveTranslator.moveFrom && moveTranslator.moveTo) { 35 | if (moveTranslator.isDateBased) { 36 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L') + ' to ' + moveTranslator.moveTo.format('L')); 37 | } else { 38 | session.endDialog(prefix + ' from ' + moveTranslator.moveFrom.format('L LT') + ' to ' + moveTranslator.moveTo.format('L LT')); 39 | } 40 | } else if (!moveTranslator.moveFrom && moveTranslator.moveTo) { 41 | if (moveTranslator.isDateBased) { 42 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L')); 43 | } else { 44 | session.endDialog(prefix + ' to ' + moveTranslator.moveTo.format('L LT')); 45 | } 46 | } else if (!moveTranslator.moveFrom && !moveTranslator.moveTo) { 47 | session.endDialog("I'm sorry, I'm not sure how to handle that request."); 48 | } 49 | } 50 | ]).triggerAction({ matches: constants.intentNames.EditCalendarEntry }); 51 | 52 | exports.create = () => { return lib.clone(); } 53 | -------------------------------------------------------------------------------- /chapter13-calendar-bot/dialogs/help.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | 3 | const constants = require('../constants'); 4 | const chatbase = require('../chatbase').chatbase; 5 | 6 | const lib = new builder.Library('help'); 7 | 8 | exports.help = (session) => { 9 | session.beginDialog('help:' + constants.dialogNames.Help); 10 | }; 11 | 12 | // help message when help requested during the add calendar entry dialog 13 | lib.dialog(constants.dialogNames.AddCalendarEntryHelp, (session, args, next) => { 14 | chatbase.build(session.message.text, session.message.address.user.id, args).send(); 15 | const msg = "To add an appointment, we gather the following information: time, subject and location. You can also simply say 'add appointment with Bob tomorrow at 2pm for an hour for coffee' and we'll take it from there!"; 16 | session.endDialog(msg); 17 | }); 18 | 19 | // help message when help requested during the remove calendar entry dialog 20 | lib.dialog(constants.dialogNames.RemoveCalendarEntryHelp, (session, args, next) => { 21 | chatbase.build(session.message.text, session.message.address.user.id, args).send(); 22 | const msg = ''; 23 | session.endDialog(msg); 24 | }); 25 | 26 | // top level help 27 | lib.dialog(constants.dialogNames.Help, (session, args, next) => { 28 | chatbase.build(session.message.text, session.message.address.user.id, args).send(); 29 | session.endDialog('Hi, I am a calendar concierge bot. I can help you create, delete and move appointments. I can also tell you about your calendar and check your availability!'); 30 | }).triggerAction({ 31 | matches: constants.intentNames.Help, 32 | onSelectAction: (session, args, next) => { 33 | session.beginDialog(args.action, args); 34 | } 35 | }); 36 | 37 | exports.create = () => { return lib.clone(); } 38 | -------------------------------------------------------------------------------- /chapter13-calendar-bot/dialogs/humanEscalation.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const constants = require('../constants'); 3 | const request = require('request'); 4 | const chatbase = require('../chatbase').chatbase; 5 | 6 | const libName = 'humanEscalation'; 7 | const escalateDialogName = 'escalate'; 8 | 9 | const lib = new builder.Library(libName); 10 | 11 | let pageAccessToken = null; 12 | exports.pageAccessToken = (val) => { 13 | if(val) pageAccessToken = val; 14 | return pageAccessToken; 15 | }; 16 | 17 | exports.escalateToHuman = (session, pageAccessTokenArg, userId) => { 18 | session.beginDialog(libName + ':' + escalateDialogName, { pageAccessToken: pageAccessTokenArg || pageAccessToken }); 19 | }; 20 | 21 | lib.dialog(escalateDialogName, (session, args, next) => { 22 | chatbase.build(session.message.text, session.message.address.user.id, args, true).send(); 23 | handover(session.message.address.user.id, args.pageAccessToken || pageAccessToken); 24 | session.endDialog('Just hold tight... getting someone for you...'); 25 | }).triggerAction({ 26 | matches: constants.intentNames.HumanHandover 27 | }); 28 | 29 | exports.create = () => { return lib.clone(); } 30 | 31 | function makeFacebookGraphRequest(d, psid, metadata, procedure, pageAccessToken) { 32 | const data = Object.assign({}, d); 33 | data.recipient = { 'id': psid }; 34 | data.metadata = metadata; 35 | 36 | const options = { 37 | uri: "https://graph.facebook.com/v2.6/me/" + procedure + "?access_token=" + pageAccessToken, 38 | json: data, 39 | method: 'POST' 40 | }; 41 | return new Promise((resolve, reject) => { 42 | request(options, function (error, response, body) { 43 | if (error) { 44 | console.log(error); 45 | reject(error); 46 | return; 47 | } 48 | console.log(body); 49 | resolve(); 50 | }); 51 | }); 52 | } 53 | 54 | const secondaryApp = 263902037430900; // this is the ID to utilize when handing over to a page 55 | function handover(psid, pageAccessToken) { 56 | return makeFacebookGraphRequest({ 'target_app_id': secondaryApp }, psid, 'test', 'pass_thread_control', pageAccessToken); 57 | } 58 | 59 | function takeControl(psid, pageAccessToken) { 60 | return makeFacebookGraphRequest({}, psid, 'test', 'take_thread_control', pageAccessToken); 61 | } -------------------------------------------------------------------------------- /chapter13-calendar-bot/dialogs/prechecks.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | 3 | const constants = require('../constants'); 4 | 5 | const authModule = require('./auth'); 6 | const primaryCalendarModule = require('./primaryCalendar'); 7 | 8 | const libName = 'prechecks'; 9 | const lib = new builder.Library(libName); 10 | 11 | lib.dialog(constants.dialogNames.PreCheck_AuthAndPrimaryCalendar, [ 12 | (session, args) => { 13 | authModule.ensureLoggedIn(session); 14 | }, 15 | (session, args) => { 16 | if (!args.response.authenticated) { 17 | session.endDialogWithResult({ response: { error: 'You must authenticate to continue.', error_auth: true } }); 18 | } else { 19 | primaryCalendarModule.ensurePrimaryCalendar(session); 20 | } 21 | }, 22 | (session, args, next) => { 23 | if (session.privateConversationData.calendarId) session.endDialogWithResult({ response: { } }); 24 | else session.endDialogWithResult({ response: { error: 'You must set a primary calendar to continue.', error_calendar: true } }); 25 | } 26 | ]); 27 | 28 | exports.create = () => { return lib.clone(); } 29 | exports.ensurePrechecks = session => { 30 | session.beginDialog(libName + ':' + constants.dialogNames.PreCheck_AuthAndPrimaryCalendar); 31 | }; 32 | -------------------------------------------------------------------------------- /chapter13-calendar-bot/dialogs/summarize.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const _ = require('underscore'); 3 | const moment = require('moment'); 4 | 5 | const utils = require('../utils'); 6 | const constants = require('../constants'); 7 | const et = require('../entityTranslator'); 8 | const gcalapi = require('../services/calendar-api'); 9 | const chatbase = require('../chatbase').chatbase; 10 | 11 | const authModule = require('./auth'); 12 | const prechecksModule = require('./prechecks'); 13 | 14 | const lib = new builder.Library('summarize'); 15 | 16 | lib.dialog(constants.dialogNames.ShowCalendarSummary, [ 17 | function (session, args) { 18 | chatbase.build(session.message.text, session.message.address.user.id, args, true).send(); 19 | session.dialogData.intent = args.intent; 20 | prechecksModule.ensurePrechecks(session); 21 | }, 22 | function (session, args, next) { 23 | if (args.response.error) { 24 | session.endDialog(args.response.error); 25 | return; 26 | } 27 | next(); 28 | }, 29 | function (session, args, next) { 30 | const auth = authModule.getAuthClientFromSession(session); 31 | const entry = new et.EntityTranslator(); 32 | et.EntityTranslatorUtils.attachSummaryEntities(entry, session.dialogData.intent.entities); 33 | let start = null; 34 | let end = null; 35 | 36 | if (entry.hasRange) { 37 | if (entry.isDateTimeEntityDateBased) { 38 | start = moment(entry.range.start).startOf('day'); 39 | end = moment(entry.range.end).endOf('day'); 40 | } else { 41 | start = moment(entry.range.start); 42 | end = moment(entry.range.end); 43 | } 44 | } else if (entry.hasDateTime) { 45 | if (entry.isDateTimeEntityDateBased) { 46 | start = moment(entry.dateTime).startOf('day'); 47 | end = moment(entry.dateTime).endOf('day'); 48 | } else { 49 | start = moment(entry.dateTime).add(-1, 'h'); 50 | end = moment(entry.dateTime).add(1, 'h'); 51 | } 52 | } else { 53 | chatbase.build(session.message.text, session.message.address.user.id, null, false).send(); 54 | session.endDialog("Sorry I don't know what you mean"); 55 | return; 56 | } 57 | 58 | const p = gcalapi.listEvents(auth, session.privateConversationData.calendarId, start, end); 59 | p.then(function (events) { 60 | let evs = _.sortBy(events, function (p) { 61 | if (p.start.date) { 62 | return moment(p.start.date).add(-1, 's').valueOf(); 63 | } else if (p.start.dateTime) { 64 | return moment(p.start.dateTime).valueOf(); 65 | } 66 | }); 67 | 68 | // should also potentially filter by subject 69 | evs = _.filter(evs, function (p) { 70 | if (!entry.hasSubject) return true; 71 | 72 | const containsSubject = entry.subject.toLowerCase().indexOf(entry.subject.toLowerCase()) >= 0; 73 | return containsSubject; 74 | }); 75 | 76 | const eventmsg = new builder.Message(session); 77 | if (evs.length > 1) { 78 | eventmsg.text('Here is what I found...'); 79 | } else if (evs.length === 1) { 80 | eventmsg.text('Here is the event I found.'); 81 | } else { 82 | eventmsg.text('Seems you have nothing going on then. What a sad existence you lead.'); 83 | } 84 | 85 | if (evs.length >= 1) { 86 | const cards = _.map(evs, function (p) { 87 | return utils.createEventCard(session, p); 88 | }); 89 | eventmsg.attachmentLayout(builder.AttachmentLayout.carousel); 90 | eventmsg.attachments(cards); 91 | } 92 | 93 | 94 | session.send(eventmsg); 95 | session.endDialog(); 96 | }); 97 | } 98 | ]).triggerAction({ matches: constants.intentNames.ShowCalendarSummary }); 99 | 100 | exports.create = function () { return lib.clone(); } 101 | -------------------------------------------------------------------------------- /chapter13-calendar-bot/env.defaults: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | MICROSOFT_APP_ID= 3 | MICROSOFT_APP_PASSWORD= 4 | LUIS_APP= 5 | LUIS_SUBSCRIPTION_KEY= 6 | 7 | # Google OAuth Bits 8 | GOOGLE_OAUTH_CLIENT_ID= 9 | GOOGLE_OAUTH_CLIENT_SECRET= 10 | GOOGLE_OAUTH_REDIRECT_URI= 11 | 12 | # Security 13 | AES_PASSPHRASE= 14 | 15 | # Cognitive Services 16 | TA_KEY= 17 | TA_ENDPOINT= 18 | TRANSLATOR_KEY= 19 | 20 | # Facebook 21 | PAGE_ACCESS_TOKEN= 22 | 23 | # Analytics 24 | CHATBASE_KEY= 25 | DASHBOT_FB_KEY= -------------------------------------------------------------------------------- /chapter13-calendar-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-calendar-bot-buildup", 3 | "version": "1.0.0", 4 | "description": "Calendar Bot - Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "author": "Szymon Rozga", 9 | "license": "MIT", 10 | "dependencies": { 11 | "@google/chatbase": "^1.0.0", 12 | "botbuilder": "^3.13.1", 13 | "cognitive-services": "^0.6.2", 14 | "crypto-js": "^3.1.9-1", 15 | "dashbot": "^9.4.3", 16 | "dotenv-extended": "^1.0.4", 17 | "googleapis": "^22.2.0", 18 | "moment": "^2.19.4", 19 | "mstranslator": "^3.0.0", 20 | "request": "^2.83.0", 21 | "restify": "^4.3.2", 22 | "underscore": "^1.8.3" 23 | }, 24 | "devDependencies": { 25 | "eslint": "^4.15.0", 26 | "eslint-config-google": "^0.9.1", 27 | "eslint-config-standard": "^10.2.1", 28 | "eslint-plugin-import": "^2.8.0", 29 | "eslint-plugin-node": "^5.2.1", 30 | "eslint-plugin-promise": "^3.6.0", 31 | "eslint-plugin-standard": "^3.0.1", 32 | "jasmine-node": "^1.14.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chapter13-calendar-bot/services/calendar-api.js: -------------------------------------------------------------------------------- 1 | const google = require('googleapis'); 2 | const calendar = google.calendar('v3'); 3 | 4 | function listEvents (auth, calendarId, start, end, subject) { 5 | const p = new Promise(function (resolve, reject) { 6 | calendar.events.list({ 7 | auth: auth, 8 | calendarId: calendarId, 9 | timeMin: start.toISOString(), 10 | timeMax: end.toISOString(), 11 | q: subject 12 | }, function (err, response) { 13 | if (err) reject(err); 14 | resolve(response.items); 15 | }); 16 | }); 17 | return p; 18 | } 19 | 20 | function listCalendars (auth) { 21 | const p = new Promise(function (resolve, reject) { 22 | calendar.calendarList.list({ 23 | auth: auth 24 | }, function (err, response) { 25 | if (err) reject(err); 26 | else resolve(response.items); 27 | }); 28 | }); 29 | return p; 30 | }; 31 | 32 | function getCalendar (auth, calendarId) { 33 | const p = new Promise(function (resolve, reject) { 34 | calendar.calendarList.get({ 35 | auth: auth, 36 | calendarId: calendarId 37 | }, function (err, response) { 38 | if (err) reject(err); 39 | else resolve(response); 40 | }); 41 | }); 42 | return p; 43 | }; 44 | 45 | function freeBusy (auth, calendarId, start, end) { 46 | const p = new Promise(function (resolve, reject) { 47 | const param = { 48 | auth: auth, 49 | resource: { 50 | timeMin: start.toISOString(), 51 | timeMax: end.toISOString(), 52 | items: [ { id: calendarId } ] 53 | } 54 | }; 55 | 56 | calendar.freebusy.query(param, function (err, response) { 57 | if (err) reject(err); 58 | else resolve(response); 59 | }); 60 | }); 61 | return p; 62 | } 63 | 64 | function insertEvent (auth, calendarId, start, end, summary, location) { 65 | const p = new Promise(function (resolve, reject) { 66 | calendar.events.insert({ 67 | auth: auth, 68 | calendarId: calendarId, 69 | resource: { 70 | end: { 71 | dateTime: end.toISOString() 72 | }, 73 | start: { 74 | dateTime: start.toISOString() 75 | }, 76 | summary: summary 77 | } 78 | }, function (err, response) { 79 | if (err) reject(err); 80 | else resolve(response); 81 | }); 82 | }); 83 | return p; 84 | } 85 | 86 | const allDayDateFormat = 'YYYY-MM-DD'; 87 | 88 | function insertAllDayEvent (auth, calendarId, start, end, summary, location) { 89 | const p = new Promise(function (resolve, reject) { 90 | calendar.events.insert({ 91 | auth: auth, 92 | calendarId: calendarId, 93 | resource: { 94 | end: { 95 | date: end.format(allDayDateFormat) 96 | }, 97 | start: { 98 | date: start.format(allDayDateFormat) 99 | }, 100 | summary: summary 101 | } 102 | }, function (err, response) { 103 | if (err) reject(err); 104 | else resolve(response); 105 | }); 106 | }); 107 | return p; 108 | } 109 | 110 | function removeEvent (auth, calendarId, eventId) { 111 | const p = new Promise(function (resolve, reject) { 112 | calendar.events.delete({ 113 | auth: auth, 114 | calendarId: calendarId, 115 | eventId: eventId 116 | }, function (err, response) { 117 | if (err) reject(err); 118 | else resolve(response); 119 | }); 120 | }); 121 | return p; 122 | } 123 | 124 | function getEvent (auth, calendarId, eventId) { 125 | const p = new Promise(function (resolve, reject) { 126 | calendar.events.get({ 127 | auth: auth, 128 | calendarId: calendarId, 129 | eventId: eventId 130 | }, function (err, response) { 131 | if (err) reject(err); 132 | else resolve(response); 133 | }); 134 | }); 135 | return p; 136 | } 137 | 138 | exports.removeEvent = removeEvent; 139 | exports.insertAllDayEvent = insertAllDayEvent; 140 | exports.listEvents = listEvents; 141 | exports.insertEvent = insertEvent; 142 | exports.getEvent = getEvent; 143 | exports.listCalendars = listCalendars; 144 | exports.getCalendar = getCalendar; 145 | exports.freeBusy = freeBusy; 146 | -------------------------------------------------------------------------------- /chapter13-calendar-bot/spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /chapter13-calendar-bot/translatorMiddleware.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore'); 2 | const cognitiveServices = require('cognitive-services'); 3 | const translator = require('mstranslator'); 4 | 5 | const textAnalytics = new cognitiveServices.textAnalytics({ 6 | apiKey: process.env.TA_KEY, 7 | endpoint: process.env.TA_ENDPOINT 8 | }); 9 | const translatorApi = new translator({ api_key: process.env.TRANSLATOR_KEY }, true); 10 | const userLanguageMap = {}; 11 | 12 | class TranslatorMiddleware { 13 | receive(event, next) { 14 | if (event.type !== 'message') { next(); return; } 15 | 16 | if (event.text == null || event.text.length == 0) { 17 | // if there is not input and we already have a language, leave as is, otherwise set to English 18 | userLanguageMap[event.user.id] = userLanguageMap[event.user.id] || 'en'; 19 | next(); 20 | return; 21 | } 22 | 23 | textAnalytics.detectLanguage({ 24 | body: { 25 | documents: [ 26 | { 27 | id: "1", 28 | text: event.text 29 | } 30 | ] 31 | } 32 | }).then(result => { 33 | const languageOptions = _.find(result.documents, p => p.id === "1").detectedLanguages; 34 | let lang = 'en'; 35 | 36 | if (languageOptions && languageOptions.length > 0) { 37 | lang = languageOptions[0].iso6391Name; 38 | } 39 | userLanguageMap[event.user.id] = lang; 40 | 41 | if (lang === 'en') next(); 42 | else { 43 | translatorApi.translate({ 44 | text: event.text, 45 | from: languageOptions[0].iso6391Name, 46 | to: 'en' 47 | }, (err, result) => { 48 | if (err) { 49 | console.error(err); 50 | lang = 'en'; 51 | userLanguageMap[event.user.id] = lang; 52 | next(); 53 | } 54 | else { 55 | event.text = result; 56 | next(); 57 | } 58 | }); 59 | } 60 | }); 61 | } 62 | send(event, next) { 63 | if (event.type === 'message') { 64 | const userLang = userLanguageMap[event.address.user.id] || 'en'; 65 | 66 | if (userLang === 'en') { next(); } 67 | else { 68 | translatorApi.translate({ 69 | text: event.text, 70 | from: 'en', 71 | to: userLang 72 | }, (err, result) => { 73 | if (err) { 74 | console.error(err); 75 | next(); 76 | } 77 | else { 78 | event.text = result; 79 | next(); 80 | } 81 | }); 82 | } 83 | } 84 | else { 85 | next(); 86 | } 87 | } 88 | } 89 | 90 | exports.TranslatorMiddleware = TranslatorMiddleware; -------------------------------------------------------------------------------- /chapter13-calendar-bot/utils.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const builder = require('botbuilder'); 3 | 4 | function wrapEntity (entityType, value) { 5 | return { 6 | type: entityType, 7 | entity: value, 8 | resolution: { 9 | values: [ 10 | value 11 | ] 12 | } 13 | }; 14 | }; 15 | 16 | function createCalendarCard (session, calendar) { 17 | const isPrimary = session.privateConversationData.calendarId === calendar.id; 18 | 19 | let subtitle = 'Your role: ' + calendar.accessRole; 20 | if (isPrimary) { 21 | subtitle = 'Primary\r\n' + subtitle; 22 | } 23 | let buttons = []; 24 | if (!isPrimary) { 25 | let btnval = 'Set primary calendar to ' + calendar.id; 26 | buttons = [builder.CardAction.postBack(session, btnval, 'Set as primary')]; 27 | } 28 | 29 | const heroCard = new builder.HeroCard(session) 30 | .title(calendar.summary) 31 | .subtitle(subtitle) 32 | .buttons(buttons); 33 | return heroCard; 34 | }; 35 | 36 | function createEventCard (session, event) { 37 | let start, end, subtitle; 38 | if (!event.start.date) { 39 | start = moment(event.start.dateTime); 40 | end = moment(event.end.dateTime); 41 | 42 | const diffInMinutes = end.diff(start, 'm'); 43 | const diffInHours = end.diff(start, 'h'); 44 | 45 | let duration = diffInMinutes + ' minutes'; 46 | if (diffInHours >= 1) { 47 | const hrs = Math.floor(diffInHours); 48 | const mins = diffInMinutes - (hrs * 60); 49 | 50 | if (mins === 0) { 51 | duration = hrs + 'hrs'; 52 | } else { 53 | duration = hrs + (hrs > 1 ? 'hrs ' : 'hr ') + (mins < 10 ? ('0' + mins) : mins) + 'mins'; 54 | } 55 | } 56 | subtitle = 'At ' + start.format('L LT') + ' for ' + duration; 57 | } else { 58 | start = moment(event.start.date); 59 | end = moment(event.end.date); 60 | 61 | const diffInDays = end.diff(start, 'd'); 62 | subtitle = 'All Day ' + start.format('L') + (diffInDays > 1 ? end.format('L') : ''); 63 | } 64 | 65 | const heroCard = new builder.HeroCard(session) 66 | .title(event.summary) 67 | .subtitle(subtitle) 68 | .buttons([ 69 | builder.CardAction.openUrl(session, event.htmlLink, 'Open Google Calendar'), 70 | builder.CardAction.postBack(session, 'Delete event with id ' + event.id, 'Delete') 71 | ]); 72 | return heroCard; 73 | }; 74 | 75 | exports.createEventCard = createEventCard; 76 | exports.wrapEntity = wrapEntity; 77 | exports.createCalendarCard = createCalendarCard; 78 | -------------------------------------------------------------------------------- /chapter14-alexa-skill-connector-bot/README.md: -------------------------------------------------------------------------------- 1 | # Alexa Skill Connector Bot 2 | Practical Bot Development - Chapter 14 3 | 4 | This code is a demostration of writing a Bot Builder based bot that supports Alexa skills. In particular, we chose to develop a Bot Builder backend to support the financial bot interaction model developed during the chapter. There are a number of different components in here: 5 | 1. Under /skill, we include the interaction model JSON as well as the original AWS Lambda skill code. 6 | 1. We develop a Bot Builder bot that follows the same logic flow as the AWS Lambda skill code 7 | 1. We create a custom recognizer for our bot to accept Alexa intent and slot data and show how the Bot Builder can handle things such as built-in Alexa intents 8 | 1. We show the code for a proof of concept Alexa Skill to Bot Builder connector. 9 | 10 | ## Getting Started 11 | 12 | These instructions will get you a copy of the project up and running on your local machine. You will need to make sure you create a .env file which has your Bot Id and Bot Password. You will also need the Directline Key. 13 | 14 | ### Installing and Running 15 | 16 | Easy. Peasy. 17 | 18 | ``` 19 | npm install 20 | npm start 21 | ``` 22 | 23 | By default, the bot will run on port 3978. 24 | 25 | * Connect your Alexa skill to your local bot running on http://localhost:3978/api/alexa 26 | * Set up an Alexa skill as per chapter 14 and connect the configuration end point to your local /api/alexa endpoint. -------------------------------------------------------------------------------- /chapter14-alexa-skill-connector-bot/alexaRecognizer.js: -------------------------------------------------------------------------------- 1 | exports.recognizer = { 2 | recognize: function (context, done) { 3 | const msg = context.message; 4 | 5 | // we only look at directline messages that include additional data 6 | if (msg.address.channelId === 'directline' && msg.sourceEvent) { 7 | 8 | const alexaMessage = msg.sourceEvent.directline.alexaMessage; 9 | 10 | // skip if no alexaMessage 11 | if (alexaMessage) { 12 | if (alexaMessage.request.type === 'IntentRequest') { 13 | // Pass IntentRequest into the dialogs. 14 | // The odd thing is that the slots and entities structure is different. If we mix LUIS/Alexa 15 | // it would make sense to normalize the format. 16 | const alexaIntent = alexaMessage.request.intent; 17 | const response = { 18 | intent: alexaIntent.name, 19 | entities: alexaIntent.slots, 20 | score: 1.0 21 | }; 22 | done(null, response); 23 | return; 24 | } else if (alexaMessage.request.type === 'LaunchRequest' || alexaMessage.request.type === 'SessionEndedRequest') { 25 | // LaunchRequest and SessionEndedRequest are simply passed through as intents 26 | const response = { 27 | intent: alexaMessage.request.type, 28 | score: 1.0 29 | }; 30 | done(null, response); 31 | return; 32 | } 33 | } 34 | } 35 | done(null, { score: 0 }); 36 | } 37 | }; -------------------------------------------------------------------------------- /chapter14-alexa-skill-connector-bot/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | const builder = require('botbuilder'); 4 | const restify = require('restify'); 5 | const moment = require('moment'); 6 | const _ = require('underscore'); 7 | 8 | const alexaRecognizer = require('./alexaRecognizer').recognizer; 9 | const alexaConnector = require('./alexaConnector'); 10 | 11 | // Setup Restify Server 12 | const server = restify.createServer(); 13 | server.listen(process.env.port || process.env.PORT || 3978, () => { 14 | console.log('%s listening to %s', server.name, server.url); 15 | }); 16 | 17 | // Create chat bot and listen to messages 18 | const connector = new builder.ChatConnector({ 19 | appId: process.env.MICROSOFT_APP_ID, 20 | appPassword: process.env.MICROSOFT_APP_PASSWORD 21 | }); 22 | server.post('/api/messages', connector.listen()); 23 | 24 | server.use(restify.bodyParser({ mapParams: false })); 25 | server.post('/api/alexa', (req, res, next) => { 26 | alexaConnector.handler(req, res, next); 27 | }); 28 | 29 | const bot = new builder.UniversalBot(connector, [ 30 | session => { 31 | let response = 'Sorry, I am not sure how to help you on this one. Please try again.'; 32 | let msg = new builder.Message(session).text(response).speak(response).sourceEvent({ 33 | directline: { 34 | keepSessionOpen: false 35 | } 36 | }); 37 | session.send(msg); 38 | } 39 | ]); 40 | 41 | bot.recognizer(alexaRecognizer); 42 | 43 | bot.dialog('QuoteDialog', [ 44 | (session, args) => { 45 | let quoteitem = args.intent.entities.QuoteItem.value; 46 | session.privateConversationData.quoteitem = quoteitem; 47 | 48 | let response = 'Looking up quote for ' + quoteitem; 49 | let reprompt = 'What else can I help you with?'; 50 | let msg = new builder.Message(session).text(response).speak(response).sourceEvent({ 51 | directline: { 52 | reprompt: reprompt, 53 | keepSessionOpen: true 54 | } 55 | }); 56 | session.send(msg); 57 | } 58 | ]) 59 | .triggerAction({ matches: 'QuoteIntent' }) 60 | .beginDialogAction('moreQuoteAction', 'MoreQuoteDialog', { matches: 'AMAZON.MoreIntent' }); 61 | 62 | bot.dialog('MoreQuoteDialog', session => { 63 | let quoteitem = session.privateConversationData.quoteitem; 64 | let response = 'Getting more quote information for ' + quoteitem; 65 | let reprompt = 'What else can I help you with?'; 66 | let msg = new builder.Message(session).text(response).speak(response).sourceEvent({ 67 | directline: { 68 | reprompt: reprompt, 69 | keepSessionOpen: true 70 | } 71 | }); 72 | session.send(msg); 73 | session.endDialog(); 74 | }); 75 | 76 | bot.dialog('AccountInformationDialog', [ 77 | (session, args) => { 78 | let accounttype = args.intent.entities.AccountType.value; 79 | session.privateConversationData.accounttype = accounttype; 80 | 81 | let response = 'Looking up account type information for ' + accounttype; 82 | let reprompt = 'What else can I help you with?'; 83 | let msg = new builder.Message(session).text(response).speak(response).sourceEvent({ 84 | directline: { 85 | keepSessionOpen: true, 86 | reprompt: reprompt 87 | } 88 | }); 89 | session.send(msg); 90 | } 91 | ]) 92 | .triggerAction({ matches: 'GetAccountTypeInformationIntent' }) 93 | .beginDialogAction('moreAccountTypeInformationAction', 'MoreAccountInformationDialog', { matches: 'AMAZON.MoreIntent' }); 94 | 95 | bot.dialog('MoreAccountInformationDialog', session => { 96 | let accounttype = session.privateConversationData.accounttype; 97 | let response = 'Getting more account type information for ' + accounttype; 98 | let reprompt = 'What else can I help you with?'; 99 | let msg = new builder.Message(session).text(response).speak(response).sourceEvent({ 100 | directline: { 101 | keepSessionOpen: true, 102 | reprompt: reprompts 103 | } 104 | }); 105 | session.send(msg); 106 | session.endDialog(); 107 | }); 108 | 109 | 110 | 111 | bot.dialog('CloseSession', session => { 112 | let response = 'Ok. Good bye.'; 113 | let msg = new builder.Message(session).text(response).speak(response).sourceEvent({ 114 | directline: { 115 | keepSessionOpen: false 116 | } 117 | }); 118 | session.send(msg); 119 | session.endDialog(); 120 | }).triggerAction({ matches: 'AMAZON.CancelIntent' }); 121 | 122 | bot.dialog('EndSession', session => { 123 | session.endConversation(); 124 | }).triggerAction({ matches: 'SessionEndedRequest' }); 125 | 126 | bot.dialog('LaunchBot', session => { 127 | let response = 'Welcome to finance skill! I can get your information about quotes or account types.'; 128 | let msg = new builder.Message(session).text(response).speak(response).sourceEvent({ 129 | directline: { 130 | keepSessionOpen: true 131 | } 132 | }); 133 | session.send(msg); 134 | session.endDialog(); 135 | }).triggerAction({ matches: 'LaunchRequest' }); 136 | 137 | const inMemoryStorage = new builder.MemoryBotStorage(); 138 | bot.set('storage', inMemoryStorage); 139 | -------------------------------------------------------------------------------- /chapter14-alexa-skill-connector-bot/env.defaults: -------------------------------------------------------------------------------- 1 | MICROSOFT_APP_ID= 2 | MICROSOFT_APP_PASSWORD= 3 | DL_KEY= -------------------------------------------------------------------------------- /chapter14-alexa-skill-connector-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-bot-development-alexa-skill-sample", 3 | "version": "1.0.0", 4 | "description": "Alexa Skill Sample Connector + Bot Builder Implementation from Chapter 14, Practical Bot Development", 5 | "scripts": { 6 | "start": "node app.js", 7 | "debug": "node --nolazy --inspect-brk=9229 app.js" 8 | }, 9 | "author": "Szymon Rozga", 10 | "license": "MIT", 11 | "dependencies": { 12 | "bingspeech-api-client": "^2.3.1", 13 | "bootstrap": "^3.3.7", 14 | "botbuilder": "^3.12.0", 15 | "botframework-directlinejs": "^0.9.13", 16 | "dotenv-extended": "^1.0.4", 17 | "jquery": "^3.2.1", 18 | "md5": "^2.2.1", 19 | "moment": "^2.19.3", 20 | "restify": "^4.3.2", 21 | "twilio": "^3.10.1", 22 | "underscore": "^1.8.3" 23 | }, 24 | "devDependencies": { 25 | "cp-cli": "^1.1.0", 26 | "eslint": "^4.10.0", 27 | "eslint-config-google": "^0.9.1", 28 | "eslint-config-standard": "^10.2.1", 29 | "eslint-plugin-import": "^2.8.0", 30 | "eslint-plugin-node": "^5.2.1", 31 | "eslint-plugin-promise": "^3.6.0", 32 | "eslint-plugin-standard": "^3.0.1" 33 | }, 34 | "main": "app.js" 35 | } 36 | -------------------------------------------------------------------------------- /chapter14-alexa-skill-connector-bot/skill/lambda.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Alexa = require('alexa-sdk'); 4 | const defaultHandlers = { 5 | 'LaunchRequest': function () { 6 | this.emit(':ask', 'Welcome to finance skill! I can get your information about quotes or account types.', 'What can I help you with?'); 7 | }, 8 | 'GetAccountTypeInformationIntent': function () { 9 | this.handler.state = 'AccountInfo'; 10 | this.emitWithState(this.event.request.intent.name); 11 | }, 12 | 'QuoteIntent': function () { 13 | this.handler.state = 'Quote'; 14 | this.emitWithState(this.event.request.intent.name); 15 | }, 16 | 'AMAZON.CancelIntent': function () { 17 | this.emit(':tell', 'Ok. Bye.'); 18 | }, 19 | 'Unhandled': function () { 20 | console.log(JSON.stringify(this.event)); 21 | this.emit(':ask', "I'm not sure what you are talking about.", 'What can I help you with?'); 22 | } 23 | }; 24 | 25 | const quoteStateHandlers = Alexa.CreateStateHandler('Quote', { 26 | 'LaunchRequest': function () { 27 | this.handler.state = ''; 28 | this.emitWithState('LaunchRequest'); 29 | }, 30 | 'AMAZON.MoreIntent': function () { 31 | this.emit(':ask', 'More information for quote item ' + this.attributes.quoteitem, 'What else can I help you with?'); 32 | }, 33 | 'AMAZON.CancelIntent': function () { 34 | this.handler.state = ''; 35 | this.emitWithState(this.event.request.intent.name); 36 | }, 37 | 'QuoteIntent': function () { 38 | console.log(JSON.stringify(this.event)); 39 | let intent = this.event.request.intent; 40 | let quoteitem = null; 41 | if (intent && intent.slots.QuoteItem) { 42 | quoteitem = intent.slots.QuoteItem.value; 43 | } else { 44 | quoteitem = this.attributes.quoteitem; 45 | } 46 | this.attributes.quoteitem = quoteitem; 47 | this.emit(':ask', 'Quote for ' + quoteitem, 'What else can I help you with?'); 48 | }, 49 | 'GetAccountTypeInformationIntent': function () { 50 | this.handler.state = ''; 51 | this.emitWithState(this.event.request.intent.name); 52 | }, 53 | 'Unhandled': function () { 54 | console.log(JSON.stringify(this.event)); 55 | this.emit(':ask', "I'm not sure what you are talking about.", 'What can I help you with?'); 56 | } 57 | }); 58 | 59 | const accountInfoStateHandlers = Alexa.CreateStateHandler('AccountInfo', { 60 | 'LaunchRequest': function () { 61 | this.handler.state = ''; 62 | this.emitWithState('LaunchRequest'); 63 | }, 64 | 'AMAZON.MoreIntent': function () { 65 | this.emit(':ask', 'More information for account ' + this.attributes.accounttype, 'What else can I help you with?'); 66 | }, 67 | 'AMAZON.CancelIntent': function () { 68 | this.handler.state = ''; 69 | this.emitWithState(this.event.request.intent.name); 70 | }, 71 | 'GetAccountTypeInformationIntent': function () { 72 | console.log(JSON.stringify(this.event)); 73 | let intent = this.event.request.intent; 74 | let accounttype = null; 75 | if (intent && intent.slots.AccountType) { 76 | accounttype = intent.slots.AccountType.value; 77 | } else { 78 | accounttype = this.attributes.accounttype; 79 | } 80 | this.attributes.accounttype = accounttype; 81 | this.emit(':ask', 'Information for ' + accounttype, 'What else can I help you with?'); 82 | }, 83 | 'QuoteIntent': function () { 84 | this.handler.state = ''; 85 | this.emitWithState(this.event.request.intent.name); 86 | }, 87 | 'Unhandled': function () { 88 | console.log(JSON.stringify(this.event)); 89 | this.emit(':ask', "I'm not sure what you are talking about.", 'What can I help you with?'); 90 | } 91 | }); 92 | 93 | exports.handler = function (event, context, callback) { 94 | const alexa = Alexa.handler(event, context, callback); 95 | alexa.registerHandlers(defaultHandlers, quoteStateHandlers, accountInfoStateHandlers); 96 | alexa.execute(); 97 | }; 98 | --------------------------------------------------------------------------------