Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
botkit-middleware-watson
Advanced tools
A middleware for using Watson Assistant in a Botkit-powered bot.
This middleware plugin for Botkit allows developers to easily integrate a Watson Assistant workspace with multiple social channels like Slack, Facebook, and Twilio. Customers can have simultaneous, independent conversations with a single workspace through different channels.
receive
: used as middleware in Botkit.interpret
: an alias of receive
, used in message-filtering and implementing app actions.sendToWatson
: same as above, but it can update context before making request, used in implementing app actions.hear
: used for intent matching.updateContext
: used in implementing app actions (sendToWatson does it better now).readContext
: used in implementing event handlers.before
: pre-process requests before sending to Watson Assistant (Conversation).after
: post-process responses before forwarding them to Botkit.$ npm install botkit-middleware-watson --save
Sign up for an IBM Cloud account.
Download the IBM Cloud CLI.
Create an instance of the Watson Assistant service and get your credentials:
apikey
value, or copy the username
and password
values if your service instance doesn't provide an apikey
.url
value.Create a workspace using the Watson Assistant service and copy the workspace_id
.
This document shows code snippets for using a Slack bot with the middleware. (If you want examples for the other channels, see the examples/multi-bot folder. The multi-bot example app shows how to connect to Slack, Facebook, and Twilio IPM bots running on a single Express server.)
You need a Slack token for your Slack bot to talk to Watson Assistant.
If you have an existing Slack bot, then copy the Slack token from your Slack settings page.
Otherwise, follow Botkit's instructions to create your Slack bot from scratch. When your bot is ready, you are provided with a Slack token.
This section walks you through code snippets to set up your Slack bot. If you want, you can jump straight to the full example.
In your app, add the following lines to create your Slack controller using Botkit:
var slackController = Botkit.slackbot();
Spawn a Slack bot using the controller:
var slackBot = slackController.spawn({
token: YOUR_SLACK_TOKEN
});
Create the middleware object which you'll use to connect to the Watson Assistant service.
If your credentials are username
and password
use:
var watsonMiddleware = require('botkit-middleware-watson')({
username: YOUR_ASSISTANT_USERNAME,
password: YOUR_ASSISTANT_PASSWORD,
url: YOUR_ASSISTANT_URL,
workspace_id: YOUR_WORKSPACE_ID,
version: '2018-07-10',
minimum_confidence: 0.50, // (Optional) Default is 0.75
});
If your credentials is apikey
use:
var watsonMiddleware = require('botkit-middleware-watson')({
iam_apikey: YOUR_API_KEY,
url: YOUR_ASSISTANT_URL,
workspace_id: YOUR_WORKSPACE_ID,
version: '2018-07-10',
minimum_confidence: 0.50, // (Optional) Default is 0.75
});
Tell your Slackbot to use the watsonMiddleware for incoming messages:
slackController.middleware.receive.use(watsonMiddleware.receive);
slackBot.startRTM();
Finally, make your bot listen to incoming messages and respond with Watson Conversation:
slackController.hears(['.*'], ['direct_message', 'direct_mention', 'mention'], function(bot, message) {
if (message.watsonError) {
bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
} else {
bot.reply(message, message.watsonData.output.text.join('\n'));
}
});
The middleware attaches the watsonData
object to message. This contains the text response from Conversation.
If any error happened in middleware, error is assigned to watsonError
property of the message.
Then you're all set!
When middleware is registered, the receive function is triggered on every message. If you would like to make your bot to only respond to direct messages using Assistant, you can achieve this in 2 ways:
slackController.hears(['.*'], ['direct_message'], function(bot, message) {
middleware.interpret(bot, message, function() {
if (message.watsonError) {
bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
} else {
bot.reply(message, message.watsonData.output.text.join('\n'));
}
});
});
var receiveMiddleware = function (bot, message, next) {
if (message.type === 'direct_message') {
watsonMiddleware.receive(bot, message, next);
} else {
next();
}
};
slackController.middleware.receive.use(receiveMiddleware);
To use the setup parameter minimum_confidence
, you have multiple options:
For example:
controller.hears(['.*'], ['direct_message', 'direct_mention', 'mention', 'message_received'], function(bot, message) {
if (message.watsonError) {
bot.reply(message, "Sorry, there are technical problems."); // deal with watson error
} else {
if (message.watsonData.intents.length == 0) {
bot.reply(message, "Sorry, I could not understand the message."); // was any intent recognized?
} else if (message.watsonData.intents[0].confidence < watsonMiddleware.minimum_confidence) {
bot.reply(message, "Sorry, I am not sure what you have said."); // is the confidence high enough?
} else {
bot.reply(message, message.watsonData.output.text.join('\n')); // reply with Watson response
}
}
});
You can find the default implementation of this function here. If you want, you can redefine this function in the same way that watsonMiddleware.before and watsonMiddleware.after can be redefined. Refer to the Botkit Middleware documentation for an example. Then, to use this function instead of Botkit's default pattern matcher (that does not use minimum_confidence), plug it in using:
controller.changeEars(watsonMiddleware.hear)
Note: if you want your own hear()
function to implement pattern matching like Botkit's default one, you will likely need to implement that yourself. Botkit's default set of 'ears' is the hears_regexp
function which is implemented here.
Watson Assistant side of app action is documented in Developer Cloud A common scenario of processing actions is
Using sendToWatson to update context simplifies the bot code compared to solution using updateContext below.
function checkBalance(context, callback) {
//do something real here
var contextDelta = {
validAccount: true,
accountBalance: 95.33
};
callback(null, context);
}
var checkBalanceAsync = Promise.promisify(checkBalance);
var processWatsonResponse = function (bot, message) {
if (message.watsonError) {
return bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
}
if (typeof message.watsonData.output !== 'undefined') {
//send "Please wait" to users
bot.reply(message, message.watsonData.output.text.join('\n'));
if (message.watsonData.output.action === 'check_balance') {
var newMessage = clone(message);
newMessage.text = 'balance result';
checkBalanceAsync(message.watsonData.context).then(function (contextDelta) {
return watsonMiddleware.sendToWatsonAsync(bot, newMessage, contextDelta);
}).catch(function (error) {
newMessage.watsonError = error;
}).then(function () {
return processWatsonResponse(bot, newMessage);
});
}
}
};
controller.on('message_received', processWatsonResponse);
Since 1.4.0 it is possible to update context from controller code.
function checkBalance(context, callback) {
//this version of function updates only the context object
context.validAccount = true;
context.accountBalance = 95.33;
callback(null, context);
}
var checkBalanceAsync = Promise.promisify(checkBalance);
var processWatsonResponse = function (bot, message) {
if (message.watsonError) {
return bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
}
if (typeof message.watsonData.output !== 'undefined') {
//send "Please wait" to users
bot.reply(message, message.watsonData.output.text.join('\n'));
if (message.watsonData.output.action === 'check_balance') {
var newMessage = clone(message);
newMessage.text = 'balance result';
//check balance
checkBalanceAsync(message.watsonData.context).then(function (context) {
//update context in storage
return watsonMiddleware.updateContextAsync(message.user, context);
}).then(function () {
//send message to watson (it reads updated context from storage)
return watsonMiddleware.sendToWatsonAsync(bot, newMessage);
}).catch(function (error) {
newMessage.watsonError = error;
}).then(function () {
//send results to user
return processWatsonResponse(bot, newMessage);
});
}
}
};
controller.on('message_received', processWatsonResponse);
Before v1.4.0 only middleware.after callback can update context, and only controller can send replies to user. The downside is that it is impossible to send "Please wait message".
function checkBalance(watsonResponse, callback) {
//middleware.after function must pass a complete Watson respose to callback
watsonResponse.context.validAccount = true;
watsonResponse.context.accountBalance = 95.33;
callback(null, watsonResponse);
}
watsonMiddleware.after = function(message, watsonResponse, callback) {
//real action happens in middleware.after
if (typeof watsonResponse !== 'undefined' && typeof watsonResponse.output !== 'undefined') {
if (watsonResponse.output.action === 'check_balance') {
return checkBalance(watsonResponse, callback);
}
}
callback(null, watsonResponse);
};
var processWatsonResponse = function(bot, message) {
if (message.watsonError) {
return bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
}
if (typeof message.watsonData.output !== 'undefined') {
//send "Please wait" to users
bot.reply(message, message.watsonData.output.text.join('\n'));
if (message.watsonData.output.action === 'check_balance') {
var newMessage = clone(message);
newMessage.text = 'balance result';
//send to watson
watsonMiddleware.interpret(bot, newMessage, function() {
//send results to user
processWatsonResponse(bot, newMessage);
});
}
}
};
controller.on('message_received', processWatsonResponse);
Events are messages having type different than message
.
Example of handler:
controller.on('facebook_postback', function(bot, message) {
bot.reply(message, 'Great Choice!!!! (' + message.payload + ')');
});
Since they usually have no text, events aren't processed by middleware and have no watsonData attribute. If event handler wants to make use of some data from context, it has to read it first. Example:
controller.on('facebook_postback', function(bot, message) {
watsonMiddleware.readContext(message.user, function(err, context) {
if (!context) {
context = {};
}
//do something useful here
myFunction(context.field1, context.field2, function(err, result) {
const newMessage = clone(message);
newMessage.text = 'postback result';
watsonMiddleware.sendToWatson(bot, newMessage, {postbackResult: 'success'}, function(err) {
if (err) {
newMessage.watsonError = error;
}
processWatsonResponse(bot, newMessage);
});
});
});
});
The Watson middleware also includes a hear()
function which provides a mechanism to
developers to fire handler functions based on the most likely intent of the user.
This allows a developer to create handler functions for specific intents in addition
to using the data provided by Watson to power the conversation.
The hear()
function can be used on individual handler functions, or can be used globally.
Used on an individual handler:
slackController.hears(['hello'], ['direct_message', 'direct_mention', 'mention'], watsonMiddleware.hear, function(bot, message) {
bot.reply(message, message.watsonData.output.text.join('\n'));
// now do something special related to the hello intent
});
Used globally:
slackController.changeEars(watsonMiddleware.hear);
slackController.hears(['hello'], ['direct_message', 'direct_mention', 'mention'], function(bot, message) {
bot.reply(message, message.watsonData.output.text.join('\n'));
// now do something special related to the hello intent
});
before
and after
The before and after callbacks can be used to perform some tasks before and after Assistant is called. One may use it to modify the request/response payloads, execute business logic like accessing a database or making calls to external services.
They can be customized as follows:
middleware.before = function(message, assistantPayload, callback) {
// Code here gets executed before making the call to Assistant.
callback(null, customizedPayload);
}
middleware.after = function(message, assistantResponse, callback) {
// Code here gets executed after the call to Assistant.
callback(null, assistantResponse);
}
If you need to make use of multiple workspaces in a single bot, workspace_id can be changed dynamically by setting workspace_id property in context.
Example of setting workspace_id to id provided as a property of hello message:
function handleHelloEvent(bot, message) {
message.type = 'welcome';
var contextDelta = {};
if (message.workspaceId) {
contextDelta.workspace_id = message.workspaceId;
}
watsonMiddleware.sendToWatsonAsync(bot, message, contextDelta).catch(function (error) {
message.watsonError = error;
}).then(function () {
bot.reply(message, message.watsonData.output.text.join('\n'));
});
}
controller.on('hello', handleHelloEvent);
This library is licensed under Apache 2.0. Full license text is available in LICENSE.
FAQs
A middleware for using Watson Assistant in a Botkit-powered bot.
The npm package botkit-middleware-watson receives a total of 19 weekly downloads. As such, botkit-middleware-watson popularity was classified as not popular.
We found that botkit-middleware-watson demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.