botbuilder-handoff
Version:
Bot hand off module for the Microsoft Bot Framework. It allows you to transfer a customer from talking to a bot to talking to a human.
296 lines • 12.3 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const bluebird = require("bluebird");
const request = require("request");
const _ = require("lodash");
const mongoose = require("mongoose");
exports.mongoose = mongoose;
mongoose.Promise = bluebird;
const handoff_1 = require("./handoff");
const indexExports = require('./index');
// -------------------
// Bot Framework types
// -------------------
exports.IIdentitySchema = new mongoose.Schema({
id: { type: String, required: true },
isGroup: { type: Boolean, required: false },
name: { type: String, required: false },
}, {
_id: false,
strict: false,
});
exports.IAddressSchema = new mongoose.Schema({
bot: { type: exports.IIdentitySchema, required: true },
channelId: { type: String, required: true },
conversation: { type: exports.IIdentitySchema, required: false },
user: { type: exports.IIdentitySchema, required: true },
id: { type: String, required: false },
serviceUrl: { type: String, required: false },
useAuth: { type: Boolean, required: false }
}, {
strict: false,
id: false,
_id: false
});
// -------------
// Handoff types
// -------------
exports.TranscriptLineSchema = new mongoose.Schema({
timestamp: {},
from: String,
sentimentScore: Number,
state: Number,
text: String
});
exports.ConversationSchema = new mongoose.Schema({
customer: { type: exports.IAddressSchema, required: true },
agent: { type: exports.IAddressSchema, required: false },
state: {
type: Number,
required: true,
min: 0,
max: 3
},
transcript: [exports.TranscriptLineSchema]
});
exports.ConversationModel = mongoose.model('Conversation', exports.ConversationSchema);
exports.BySchema = new mongoose.Schema({
bestChoice: Boolean,
agentConversationId: String,
customerConversationId: String,
customerName: String,
customerId: String,
});
exports.ByModel = mongoose.model('By', exports.BySchema);
// -----------------
// Mongoose Provider
// -----------------
class MongooseProvider {
init() { }
addToTranscript(by, message, from) {
return __awaiter(this, void 0, void 0, function* () {
let sentimentScore = -1;
let text = message.text;
let datetime = new Date().toISOString();
const conversation = yield this.getConversation(by);
if (!conversation)
return false;
if (from == "Customer") {
if (indexExports._textAnalyticsKey) {
sentimentScore = yield this.collectSentiment(text);
}
datetime = message.localTimestamp ? message.localTimestamp : message.timestamp;
}
conversation.transcript.push({
timestamp: datetime,
from: from,
sentimentScore: sentimentScore,
state: conversation.state,
text
});
if (indexExports._appInsights) {
// You can't log embedded json objects in application insights, so we are flattening the object to one item.
// Also, have to stringify the object so functions from mongodb don't get logged
let latestTranscriptItem = conversation.transcript.length - 1;
let x = JSON.parse(JSON.stringify(conversation.transcript[latestTranscriptItem]));
x['botId'] = conversation.customer.bot.id;
x['customerId'] = conversation.customer.user.id;
x['customerName'] = conversation.customer.user.name;
x['customerChannelId'] = conversation.customer.channelId;
x['customerConversationId'] = conversation.customer.conversation.id;
if (conversation.agent) {
x['agentId'] = conversation.agent.user.id;
x['agentName'] = conversation.agent.user.name;
x['agentChannelId'] = conversation.agent.channelId;
x['agentConversationId'] = conversation.agent.conversation.id;
}
indexExports._appInsights.client.trackEvent("Transcript", x);
}
return yield this.updateConversation(conversation);
});
}
connectCustomerToAgent(by, agentAddress) {
return __awaiter(this, void 0, void 0, function* () {
const conversation = yield this.getConversation(by);
if (conversation) {
conversation.state = handoff_1.ConversationState.Agent;
conversation.agent = agentAddress;
}
const success = yield this.updateConversation(conversation);
if (success)
return conversation;
else
return null;
});
}
queueCustomerForAgent(by) {
return __awaiter(this, void 0, void 0, function* () {
const conversation = yield this.getConversation(by);
if (!conversation) {
return false;
}
else {
conversation.state = handoff_1.ConversationState.Waiting;
return yield this.updateConversation(conversation);
}
});
}
connectCustomerToBot(by) {
return __awaiter(this, void 0, void 0, function* () {
const conversation = yield this.getConversation(by);
if (!conversation) {
return false;
}
else {
conversation.state = handoff_1.ConversationState.Bot;
if (indexExports._retainData === "true") {
//if retain data is true, AND the user has spoken to an agent - delete the agent record
//this is necessary to avoid a bug where the agent cannot connect to another user after disconnecting with a user
if (conversation.agent) {
conversation.agent = null;
return yield this.updateConversation(conversation);
}
else {
//otherwise, just update the conversation
return yield this.updateConversation(conversation);
}
}
else {
//if retain data is false, delete the whole conversation after talking to agent
if (conversation.agent) {
return yield this.deleteConversation(conversation);
}
else {
//otherwise, just update the conversation
return yield this.updateConversation(conversation);
}
}
}
});
}
getConversation(by, customerAddress) {
return __awaiter(this, void 0, void 0, function* () {
if (by.customerName) {
const conversation = yield exports.ConversationModel.findOne({ 'customer.user.name': by.customerName });
return conversation;
}
else if (by.customerId) {
const conversation = yield exports.ConversationModel.findOne({ 'customer.user.id': by.customerId });
return conversation;
}
else if (by.agentConversationId) {
const conversation = yield exports.ConversationModel.findOne({ 'agent.conversation.id': by.agentConversationId });
if (conversation)
return conversation;
else
return null;
}
else if (by.customerConversationId) {
let conversation = yield exports.ConversationModel.findOne({ 'customer.conversation.id': by.customerConversationId });
if (!conversation && customerAddress) {
conversation = yield this.createConversation(customerAddress);
}
return conversation;
}
else if (by.bestChoice) {
const waitingLongest = yield this.getCurrentConversations();
waitingLongest
.filter(conversation => conversation.state === handoff_1.ConversationState.Waiting)
.sort((x, y) => y.transcript[y.transcript.length - 1].timestamp - x.transcript[x.transcript.length - 1].timestamp);
return waitingLongest.length > 0 && waitingLongest[0];
}
return null;
});
}
getCurrentConversations() {
return __awaiter(this, void 0, void 0, function* () {
let conversations;
try {
conversations = yield exports.ConversationModel.find();
}
catch (error) {
console.log('Failed loading conversations');
console.log(error);
}
return conversations;
});
}
createConversation(customerAddress) {
return __awaiter(this, void 0, void 0, function* () {
return yield exports.ConversationModel.create({
customer: customerAddress,
state: handoff_1.ConversationState.Bot,
transcript: []
});
});
}
updateConversation(conversation) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
exports.ConversationModel.findByIdAndUpdate(conversation._id, conversation).then((error) => {
resolve(true);
}).catch((error) => {
console.log('Failed to update conversation');
console.log(conversation);
resolve(false);
});
});
});
}
deleteConversation(conversation) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve) => {
exports.ConversationModel.findByIdAndRemove(conversation._id).then((error) => {
resolve(true);
});
});
});
}
collectSentiment(text) {
return __awaiter(this, void 0, void 0, function* () {
if (text == null || text == '')
return;
let _sentimentUrl = 'https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment';
let _sentimentId = 'bot-analytics';
let _sentimentKey = indexExports._textAnalyticsKey;
let options = {
url: _sentimentUrl,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': _sentimentKey
},
json: true,
body: {
"documents": [
{
"language": "en",
"id": _sentimentId,
"text": text
}
]
}
};
return new Promise(function (resolve, reject) {
request(options, (error, response, body) => {
if (error) {
reject(error);
}
let result = _.find(body.documents, { id: _sentimentId }) || {};
let score = result.score || null;
resolve(score);
});
});
});
}
}
exports.MongooseProvider = MongooseProvider;
//# sourceMappingURL=mongoose-provider.js.map