This commit is contained in:
Matt Shim 2018-04-12 11:14:23 -07:00 коммит произвёл Kamran Iqbal
Родитель 2ed63a0cb4
Коммит ca22ab35af
19 изменённых файлов: 7389 добавлений и 0 удалений

59
blog-samples/Node/Blog-Redux-Bot/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,59 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env

Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Matt Shim
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Просмотреть файл

@ -0,0 +1,103 @@
# Redux-Bot
![redux-abs-logo][redux-abs-logo]
BotBuilder v3 Node.js bot with Redux state management
# Overview
This sample is to showcase the flexibility of the Azure Bot Service, by demonstrating that you can author rich conversational experiences using whatever technologies/libraries you'd like. This sample bot uses [Redux](https://redux.js.org/), a popular JavaScript framework for application state management, and [Redux-Saga](https://redux-saga.js.org/) to recreate an existing [city search bot sample](https://github.com/Microsoft/BotBuilder-Samples/tree/master/Node/core-CustomState).
![city-search-bot][city-search-bot]
# To run this sample
Clone BotFramework-Samples repo, and CD into this folder (Blog-Redux-Bot). From your CLI, install the node_module dependencies:
```
npm install
```
## Emulator
This bot runs by default on **localhost:3978**, which you can run directly using the [Bot Framework Emulator](https://github.com/Microsoft/BotFramework-Emulator).
```
node app.js
```
## UI Redux Store Render
![ui-state][ui-state]
Included in **public/index.html** is a simple web app using a custom web chat instance which communicates with the Azure Bot Service over the DirectLine channel.
[Official ngrok page](https://ngrok.com/)
![ngrok][ngrok]
On Azure, in bot channels registration, copy and paste the ngrok port forwarding address to the messaging endpoint in settings--> configuration as shown. **Ensure that this address ends in /api/messages.**
![channel-reg][channel-registration]
In your CLI, navigate to the redux bot root folder and run:
```
npm start
```
This will run the custom web application on **localhost:3000**.
Lastly, provision the [DirectLine Secret](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-channel-connect-directline) in the URL header of your browser. This will allow the Azure Bot Service to communicate with your local hosted custom web chat instance.
![dl-secret][dl-secret]
Now, when you interact with the bot, any state changes to the Redux Store will be rendered on the web page.
# To-do
* [ ] More tests for Redux
* [X] Cleaner code inside `app.js`
* [ ] Investigate possibility to use ChatConnector directly
# Hiccups
## Turning `conversationUpdate` event into an action
It would be great if a new member joined, we sent a welcome message right away.
* `bot.on('conversationUpdate')` event handler does not associate with `session` object
* `session` object is required to create a Redux store
Looking at other sample code on the same scenario, instead of sending the greeting thru `session.send`, it must be sent thru `bot.send` with an addressed message. Thus, it further proves that `conversationUpdate` is not associated with any `session` object.
Because our Redux store design requires `session` object, thus, `conversationUpdate` cannot be turn into an action.
## Multi-turn dialog
It is intuitive to write code in `redux-saga` like this:
```js
takeEvery(RECEIVE_MESSAGE, function* (action) {
yield put(promptText('What is your name?'));
action = yield take(RECEIVE_RESULT);
yield put(sendMessage(`Hello, ${ action.payload.response }`);
});
```
But this would require a dialog to resume in the middle of a saga (resume at the `yield take` line). Due to the nature of serverless functions, it is difficult to implement a saga that works this way.
# References
* [Redux](https://github.com/reactjs/redux)
* [Redux-Saga](https://github.com/redux-saga/redux-saga)
* [BotBuilder SDK](https://github.com/Microsoft/BotBuilder)
* [ngrok](https://ngrok.com/)
[redux-abs-logo]: ../../images/redux-abs-logo.png
[ngrok]: ../../images/ngrok-forward.png
[city-search-bot]: ../../images/redux-bot-02.png
[channel-registration]: ../../images/bot-channels-ngrok.png
[bot-webchat]: ../../images/redux-bot-01.png
[ui-state]: ../../images/redux-store-02.png
[dl-secret]: ../../images/direct-line-secret-url.png

Просмотреть файл

@ -0,0 +1,43 @@
require('dotenv').config();
const builder = require('botbuilder');
const restify = require('restify');
const inMemoryStorage = new builder.MemoryBotStorage();
const connector = new builder.ChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
})
const bot = new builder.UniversalBot(connector).set('storage', inMemoryStorage); // Register in memory storage
// Redux
const loadStore = require('./redux/loadStore');
const DialogActions = require('./redux/dialogActions');
// Create server
const server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
console.log(`${ server.name } listening to ${ server.url }`);
});
server.post('/api/messages', connector.listen());
//=========================================================
// Bot Recognizers
//=========================================================
// const LuisAppID = process.env.LUIS_APP_ID; // Your-LUIS-App-ID
// const LuisKey = process.env.LUIS_APP_KEY; // Your-LUIS-Key
// const LuisModel = `https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/${ LuisAppID }?subscription-key=${ LuisKey }`;
// const recognizer = new builder.LuisRecognizer(LuisModel);
bot.dialog('/', new builder.SimpleDialog((session, result) => {
const store = loadStore(session);
const { attachments, text } = session.message || {};
if (attachments || result || text) {
store.dispatch(DialogActions.receiveMessage(text, attachments, result));
}
}));

6801
blog-samples/Node/Blog-Redux-Bot/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,25 @@
{
"name": "redux-bot",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "npx concurrently --names \"bot,web\" --kill-others \"npm run start:bot\" \"npm run start:web\"",
"start:bot": "npx node-dev .",
"start:web": "npx serve -p 3000 public",
"test": "jest"
},
"private": true,
"author": "",
"license": "MIT",
"dependencies": {
"botbuilder": "^3.14.0",
"dotenv": "^5.0.1",
"redux": "^3.7.2",
"redux-saga": "^0.16.0",
"restify": "^6.3.4"
},
"devDependencies": {
"jest": "^22.4.3"
}
}

Просмотреть файл

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Bot Chat</title>
<link href="https://unpkg.com/botframework-webchat/botchat.css" rel="stylesheet" />
<style>
html, body {
height: 100%;
margin: 0;
}
#container {
display: flex;
height: 100%;
}
#container > div.state {
display: flex;
flex: 1;
flex-direction: column;
margin-left: 1em;
}
#container > div.state > pre {
flex: 1;
margin: 0;
overflow: auto;
}
#container > #botChat {
box-shadow: 0 0 10px rgba(0, 0, 0, .2);
display: flex;
flex-basis: 460px;
flex-shrink: 0;
width: 460px;
}
#container > #botChat .wc-app {
flex: 1;
}
#container > #botChat .wc-chatview-panel {
height: 100%;
position: relative;
width: 100%;
}
</style>
</head>
<body>
<div id="container">
<div id="botChat"></div>
<div class="state">
<h1>Redux server store</h1>
<pre id="store">Nothing to see here</pre>
<ul id="actions"></ul>
</div>
</div>
<script src="https://unpkg.com/botframework-webchat/botchat.js"></script>
<script>
const params = new URLSearchParams(location.search);
window['botchatDebug'] = params.get('debug') && params.get('debug') === 'true';
const botConnection = new BotChat.DirectLine({
domain: params.get('domain'),
secret: params.get('s'),
token: params.get('t'),
webSocket: params.get('webSocket') && params.get('webSocket') === 'true' // defaults to true
});
BotChat.App({
bot: {
id: params.get('botid') || 'botid',
name: params.get('botname') || 'botname'
},
botConnection: botConnection,
showUploadButton: true,
user: {
id: params.get('userid') || 'userid',
name: params.get('username') || 'username'
}
}, document.getElementById('botChat'));
botConnection.activity$
.filter(activity => activity.type === 'event' && activity.name === 'store')
.subscribe(activity => {
document.querySelector('pre#store').innerText = JSON.stringify(activity.value, null, 2);
});
botConnection.activity$
.filter(activity => activity.type === 'event' && activity.name === 'action')
.subscribe(activity => {
const li = document.createElement('li');
const pre = document.createElement('pre');
pre.innerText = JSON.stringify(activity.value).substr(0, 150);
li.appendChild(pre);
document.querySelector('ul#actions').appendChild(li);
});
setInterval(() => {
document.querySelector('.wc-shellinput').focus();
}, 300);
</script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,25 @@
const RESET = 'RESET';
const SET_CITY = 'SET_CITY';
const SET_USERNAME = 'SET_USERNAME';
function reset() {
return { type: RESET };
}
function setCity(city) {
return { type: SET_CITY, payload: { city } };
}
function setUsername(username) {
return { type: SET_USERNAME, payload: { username } };
}
module.exports = {
RESET,
SET_CITY,
SET_USERNAME,
reset,
setCity,
setUsername
};

Просмотреть файл

@ -0,0 +1,51 @@
const PROMPT_TEXT = 'DIALOG/PROMPT_TEXT';
const RECEIVE_MESSAGE = 'DIALOG/RECEIVE_MESSAGE';
const SEND_EVENT = 'DIALOG/SEND_EVENT';
const SEND_MESSAGE = 'DIALOG/SEND_MESSAGE';
const END_CONVERSATION = 'DIALOG/END_CONVERSATION';
function promptText(text) {
return { type: PROMPT_TEXT, payload: { text } };
}
function receiveMessage(text, attachments, result) {
return {
type: RECEIVE_MESSAGE,
payload: { attachments, result, text }
};
}
function sendEvent(name, value) {
return {
type: SEND_EVENT,
payload: { name, value }
};
}
function sendMessage(text, attachments) {
return {
type: SEND_MESSAGE,
payload: { attachments, text }
};
}
function endConversation(text, attachments, result){
return {
type: END_CONVERSATION,
payload: { attachments, result, text }
};
}
module.exports = {
PROMPT_TEXT,
RECEIVE_MESSAGE,
SEND_EVENT,
SEND_MESSAGE,
END_CONVERSATION,
promptText,
receiveMessage,
sendEvent,
sendMessage,
endConversation
};

Просмотреть файл

@ -0,0 +1,50 @@
const { applyMiddleware, createStore } = require('redux');
const { default: createSagaMiddleware } = require('redux-saga');
const createDefaultSaga = require('./sagas/default');
const createDialogSagas = require('./sagas/dialog');
const reducer = require('./reducer');
module.exports = function loadStore(session) {
const saga = createSagaMiddleware();
const store = createStore(
reducer,
// Restore the store from conversationData
session.conversationData,
applyMiddleware(
saga,
store => next => action => {
// Send action to web page for debugging
session.send({
type: 'event',
name: 'action',
value: action
});
return next(action);
}
)
);
store.subscribe(() => {
// Save the store to conversationData
session.conversationData = store.getState();
session.save();
// Send store state to web page for debugging
session.send({
type: 'event',
name: 'store',
value: store.getState()
});
});
saga.run(function* () {
yield* createDialogSagas(session);
yield* createDefaultSaga(session);
});
return store;
};

Просмотреть файл

@ -0,0 +1,26 @@
const { RESET, SET_CITY, SET_USERNAME } = require('./conversationActions');
const DEFAULT_STATE = {
city: null,
username: null
};
function conversationReducer(state = DEFAULT_STATE, action) {
switch (action.type) {
case RESET:
state = DEFAULT_STATE;
break;
case SET_CITY:
state = { ...state, city: action.payload.city };
break;
case SET_USERNAME:
state = { ...state, username: action.payload.username };
break;
}
return state;
}
module.exports = conversationReducer;

Просмотреть файл

@ -0,0 +1,44 @@
const { put, select, takeEvery } = require('redux-saga/effects');
const { reset, setCity, setUsername } = require('../conversationActions');
const { promptText, RECEIVE_MESSAGE, sendMessage, endConversation } = require('../dialogActions');
module.exports = function* (session) {
yield takeEvery(RECEIVE_MESSAGE, function* (action) {
const { text } = action.payload;
const changeCityMatch = /^change city to (.*)/i.exec(text);
const currentCityMatch = /^current city/i.exec(text);
const resetMatch = /^reset/i.exec(text);
const endConversationMatch = /^end conversation/i.exec(text);
let { city, username } = yield select();
if (!city) {
city = 'Seattle';
yield put(setCity(city));
yield put(sendMessage(`Welcome to the Search City bot. I\'m currently configured to search for things in ${ city }`));
yield put(promptText('Before get started, please tell me your name?'));
} else if (!username) {
yield put(setUsername(text));
yield put(sendMessage(`Welcome ${ text }!\n * If you want to know which city I'm using for my searches type 'current city'. \n * Want to change the current city? Type 'change city to cityName'. \n * Want to change it just for your searches? Type 'change my city to cityName'`));
} else if (changeCityMatch) {
const newCity = changeCityMatch[1];
yield put(setCity(newCity));
yield put(sendMessage(`All set ${ username }. From now on, all my searches will be for things in ${ newCity }.`));
} else if (currentCityMatch) {
yield put(sendMessage(`Hey ${ username }, I\'m currently configured to search for things in ${ city }.`));
} else if (resetMatch) {
yield put(reset());
yield put(sendMessage('Oops... I\'m suffering from a memory loss...'));
} else if (endConversationMatch){
yield put(endConversation());
yield put(sendMessage('Ending Conversation...'));
} else {
const { city, username } = yield select();
const messageText = action.payload.text.trim();
yield put(sendMessage(`${ username }, wait a few seconds. Searching for \'${ messageText }\' in \'${ city }\'...`));
yield put(sendMessage(`https://www.bing.com/search?q=${ encodeURIComponent(`${ messageText } in ${ city }`) }`));
}
});
};

Просмотреть файл

@ -0,0 +1,31 @@
const builder = require('botbuilder');
const { takeEvery } = require('redux-saga/effects');
const DialogActions = require('../dialogActions');
module.exports = function* (session) {
yield takeEvery(DialogActions.PROMPT_TEXT, function* (action) {
builder.Prompts.text(session, action.payload.text);
});
yield takeEvery(DialogActions.END_CONVERSATION, function* (action){
session.endConversation('Bye!');
});
yield takeEvery(DialogActions.SEND_EVENT, function* (action) {
const { name, value } = action.payload;
session.send({ type: 'event', name, value });
});
yield takeEvery(DialogActions.SEND_MESSAGE, function* (action) {
const { attachments, text } = action.payload;
message = new builder.Message(session);
text && message.text(text);
attachments && message.attachments(attachments);
session.send(message);
});
};

Двоичные данные
blog-samples/images/bot-channels-ngrok.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 39 KiB

Двоичные данные
blog-samples/images/direct-line-secret-url.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.7 KiB

Двоичные данные
blog-samples/images/ngrok-forward.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 17 KiB

Двоичные данные
blog-samples/images/redux-abs-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 26 KiB

Двоичные данные
blog-samples/images/redux-bot-02.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 39 KiB

Двоичные данные
blog-samples/images/redux-store-02.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 97 KiB