Im developing a bot using Microsoft Bot Framework. The user can issue search commands in the format : "#bot search" followed by a search term. The search yields results displayed as carousel. Below the carousel is a button to "Refine the search". The button click should pre-populate the previous search command in the user input area. This is so that the user can edit the previous search query without having to retype it again. Is this possible?
As far as inserting the previously entered text into the user's text box, I don't believe you can do that.
If you use Adaptive Cards with the Bot Framework, you can use an Adaptive Card object which has a text property, and set the button's action to send the message. The text can be set to whatever you want in "Refine the Search". Here's an example in node.js:
var restify = require('restify');
var builder = require('botbuilder');
var adaptive = require('adaptivecards');
let Adapt = () => {
var inMemoryStorage = new builder.MemoryBotStorage();
let response = "";
// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function() {
console.log('%s listening to %s', server.name, server.url);
});
// Create chat connector for communicating with the Bot Framework Service
var connector = new builder.ChatConnector({
// your app ID and password here
});
// Listen for messages from users
server.post('/api/messages', connector.listen());
// Bot structure. It will use an AdaptiveCard, and you will be able to refine the search in the textbox inside the card itself. Search will send the textbox's text to the bot and it will bring the card back up again with the updated text.
var bot = new builder.UniversalBot(connector, function (session) {
if (!!session.message.value && !!session.message.value.refineSearchText) {
response = session.message.value.refineSearchText;
} else {
response = "";
}
let attachTest = {
'contentType': 'application/vnd.microsoft.card.adaptive',
'content': {
'$schema': 'http://adaptivecards.io/schemas/adaptive-card.json',
'type': 'AdaptiveCard',
'version': '1.0',
'body': [ {"type": "TextBlock", "text": "Refine Search?"},
{
'type': "Input.Text",
"id": "refineSearchText",
"placeholder": "Search to refine",
"style":"text",
"value": `${response || session.message.text}`
}],
'actions': [
{
'type': 'Action.Submit',
'title': 'Search'
}
]
}
};
session.send(`You're refining: ${response}`);
let attachTestMsg = new builder.Message(session).addAttachment(attachTest);
session.send(attachTestMsg);
}).set("storage", inMemoryStorage);
};
Adapt();
If you're not using node.js, you may need to adapt this to the environment you are using.
Related
I am learning the Bot Framework V4 version and I have implemented a simple example. I would ask the user to enter two numbers via an adaptive card and once he clicks on the submit operation, I would just add them and send a response back to the user, the computed sum. Now I wanted to extend this further. I also want to add a multiplication option too. I am attaching the code below for your reference:
// ++PDIT: Documentation can be found here:https://learn.microsoft.com/en-us/javascript/api/botbuilder-core/turncontext?view=botbuilder-ts-latest#activity
// ++PDIT: Adaptive Cards Schema: https://adaptivecards.io/explorer/
const restify = require('restify');
const botbuilder = require('botbuilder');
// ++PDIT: Add the reference to the AdaptorCards' JSON here.
const InputNumbersCard = require('./resources/InputNumbers.json');
// Create bot adapter, which defines how the bot sends and receives messages.
// ++PDIT: If the adapter is fed with an empty appId and appPassword, then the
// processActivity would be treated as a conversation/dialog via an Emulator.
var adapter = new botbuilder.BotFrameworkAdapter({
appId: process.env.MicrosoftAppId,
appPassword: process.env.MicrosoftAppPassword
});
// Create HTTP server.
let server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
console.log(`\n${server.name} listening to ${server.url}`);
console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`);
});
// Listen for incoming requests at /api/messages.
server.post('/api/messages', (req, res) => {
// Use the adapter to process the incoming web request into a TurnContext object.
// ++PDIT: TurnContext object represents the bot's 'turn', in a conversation flow.
adapter.processActivity(req, res, async (turnContext) => {
// Do something with this incoming activity!
if ( turnContext.activity.type === 'message' ) {
// ++PDIT: Get the user's utterance here
const utterance = turnContext.activity.text;
// ++PDIT: 'postBack' - Upon submitting an activity dialog, the information
// stored by the user actually gets stored on the adaptive card itself.
if( turnContext.activity.channelData.postBack ) {
// AdaptiveCard submit operation.
var number1 = turnContext.activity.value.firstNumber;
var number2 = turnContext.activity.value.secondNumber;
var sum = Number(number1) + Number(number2);
await turnContext.sendActivity( 'The sum of the entered numbers is ' + sum );
}
else {
if ( utterance === 'add' || utterance === 'multiply' ) {
// send a reply
await turnContext.sendActivity( {
text: `Here is an Adaptive Card for entering the two number that you want to ${utterance}:`,
attachments:[botbuilder.CardFactory.adaptiveCard(InputNumbersCard)]
});
}
else {
// send a reply
await turnContext.sendActivity(`I heard you say '${ utterance }'. I am yet to learn that skill!.`);
}
}
}
});
});
My adaptive card looks like below:
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Enter the numbers that you would like to add"
},
{
"type": "Input.Text",
"id": "firstNumber",
"placeholder": "What is your first number?"
},
{
"type": "Input.Text",
"id": "secondNumber",
"placeholder": "What is your second number?"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Action"
}
]
}
I have read that when the user clicks on the submit button, the variable turnContext.activity.channelData.postBack would be true. But my question is this: How would I understand that the inputs that the user has entered are actually for 'add' operation or for 'multiple' operation?. I am using a single adaptive card as you can see as I felt add and multiply are just generic in the sense they need two constants to work with. So I am a bit lost here. Request you to point me to the right direction.
Thanks
Pavan.
I am using LUIS for azure search and QnA recognizer for the first time together in the same code.
I am using intent.match to match the recognizers.
My problem is:
for the first time, If I ask a question which matches to the QnA intent, it returns the answer from the QnA base.
followed by the questions which are matching with the azuresearch intents. Azure search also deliver the result.
But if I repeat the questions which must be matched to the QnA, it says "no intent handler found for null"
var util = require('util');
var _ = require('lodash');
var builder = require('botbuilder');
var restify = require('restify');
var cognitiveservices = require('botbuilder-cognitiveservices');
/// <reference path="../SearchDialogLibrary/index.d.ts" />
var SearchLibrary = require('../SearchDialogLibrary');
var AzureSearch = require('../SearchProviders/azure-search');
var qintent;
// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
console.log('%s listening to %s', server.name, server.url);
});
// Create chat bot and listen for messages
var connector = new builder.ChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
});
server.post('/api/messages', connector.listen());
// Bot Storage: Here we register the state storage for your bot.
// Default store: volatile in-memory store - Only for prototyping!
// We provide adapters for Azure Table, CosmosDb, SQL Azure, or you can implement your own!
// For samples and documentation, see: https://github.com/Microsoft/BotBuilder-Azure
var inMemoryStorage = new builder.MemoryBotStorage();
var qnarecognizer = new cognitiveservices.QnAMakerRecognizer({
knowledgeBaseId: '6b30ac2a-5ce9-4576-a1cb-c89abfb73889',
authKey: 'd457c897-71cf-46bf-a3d9-9a1f51246fad',
endpointHostName: 'https://qnadispatchwg.azurewebsites.net/qnamaker'
});
const LuisModelUrl = 'https://westeurope.api.cognitive.microsoft.com/luis/v2.0/apps/180a9aaa-9d67-4d40-b3d3-121917f4dbb8?subscription-key=39155bb750dc4b2abd84d410d80fce21&timezoneOffset=0&q=';
var recognizer = new builder.LuisRecognizer(LuisModelUrl);
// Bot with main dialog that triggers search and display its results
var bot = new builder.UniversalBot(connector);
bot.set('storage', inMemoryStorage);
var intents = new builder.IntentDialog({ recognizers: [recognizer, qnarecognizer] });
bot.dialog('/', intents);
intents.matches('Greeting', (session) => {
session.send('You reached the Greeting intent. You said \'%s\'.', session.message.text);
session.endDialog();
});
intents.matches('qna', [
function (session, args) {
console.log("my args in qna:=========================================================================================== %o", args);
var answerEntity = builder.EntityRecognizer.findEntity(args.entities, 'answer');
session.send(answerEntity.entity);
}
]);
intents.matches('Search.Aco', [
function (session, args, next) {
console.log("my args in SearchDialog:=========================================================================================== %o", args);
var intent = args.intent;
console.log("our intent is " + intent);
var entities = builder.EntityRecognizer.findEntity(args.entities, 'businessterm');
console.log("recognnized entitiy is: "+ entities.entity);
// qintent = title;
//session.send('You reached the Search.Aco intent. You enquire for the entitiy \'%s\'.', qintent);
//console.log(" SearchDialog: before updating the session.message.text------------->" + session.message.text);
session.message.text= entities.entity;
//console.log(" SearchDialog: after updating the session.message.text------------->" + session.message.text);
SearchLibrary.begin(session);
},
function (session, args, results) {
// Process selected search results
session.send(
'Done! For future reference, you selected these properties: %s',
args.selection.map(function (i) { return i.key; }).join(', '));
}
]);
var azureSearchClient = AzureSearch.create('aco-intel2', '4105C6676D0CDD9B2E7891952B9E9E00', 'azureblob-index');
var jobsResultsMapper = SearchLibrary.defaultResultsMapper(jobToSearchHit);
// Register Search Dialogs Library with bot
bot.library(SearchLibrary.create({
multipleSelection: true,
search: function (query) { return azureSearchClient.search(query).then(jobsResultsMapper); },
refiners: ['people', 'content', 'location']
}));
// Maps the AzureSearch Job Document into a SearchHit that the Search Library can use
function jobToSearchHit(acosearch) {
console.log("inside jobToSearchHit");
console.log("inside acosearch.DocUrl" + acosearch.DocUrl + "-------" + acosearch.metadata_storage_name);
return {
key: acosearch.id,
title: acosearch.metadata_storage_name,
description: acosearch.content.substring(0, 100)+"...",
documenturl:acosearch.DocUrl,
imageUrl: acosearch.imageurl
};
}
module.exports = { qintent: "qintent"};
for the second time if I am mixing the
This is how it is showing:
Emulator output
can you please help me to understand what is the difference between the intent.match and dialog' matches. I tried the following way too but it does not recognize the answering entity. How can be QnA be called from the following code?
var util = require('util');
var _ = require('lodash');
var builder = require('botbuilder');
var restify = require('restify');
var cognitiveservices = require('botbuilder-cognitiveservices');
/// <reference path="../SearchDialogLibrary/index.d.ts" />
var SearchLibrary = require('../SearchDialogLibrary');
var AzureSearch = require('../SearchProviders/azure-search');
var qintent;
// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
console.log('%s listening to %s', server.name, server.url);
});
// Create chat bot and listen for messages
var connector = new builder.ChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
});
server.post('/api/messages', connector.listen());
// Bot Storage: Here we register the state storage for your bot.
// Default store: volatile in-memory store - Only for prototyping!
// We provide adapters for Azure Table, CosmosDb, SQL Azure, or you can implement your own!
// For samples and documentation, see: https://github.com/Microsoft/BotBuilder-Azure
var inMemoryStorage = new builder.MemoryBotStorage();
var qnarecognizer = new cognitiveservices.QnAMakerRecognizer({
knowledgeBaseId: '6b30ac2a-5ce9-4576-a1cb-c89abfb73889',
authKey: 'd457c897-71cf-46bf-a3d9-9a1f51246fad',
endpointHostName: 'https://qnadispatchwg.azurewebsites.net/qnamaker'
});
const LuisModelUrl = 'https://westeurope.api.cognitive.microsoft.com/luis/v2.0/apps/180a9aaa-9d67-4d40-b3d3-121917f4dbb8?subscription-key=39155bb750dc4b2abd84d410d80fce21&timezoneOffset=0&q=';
var recognizer = new builder.LuisRecognizer(LuisModelUrl);
// Bot with main dialog that triggers search and display its results
var bot = new builder.UniversalBot(connector, function (session, args) {
session.send('You reached the default message handler. You said \'%s\'.', session.message.text);
}).set('storage', inMemoryStorage);
bot.recognizer(recognizer, qnarecognizer);
bot.dialog('GreetingDialog',
(session) => {
session.send('You reached the Greeting intent. You said \'%s\'.', session.message.text);
session.endDialog();
}
).triggerAction({
matches: 'Greeting'
})
bot.dialog('HelpDialog',
(session) => {
session.send('You reached the Help intent. You said \'%s\'.', session.message.text);
session.endDialog();
}
).triggerAction({
matches: 'Help'
})
bot.dialog('CancelDialog',
(session) => {
session.send('You reached the Cancel intent. You said \'%s\'.', session.message.text);
session.endDialog();
}
).triggerAction({
matches: 'Cancel'
})
bot.dialog('QnADialog',[
function (session, args, next) {
var answerEntity = builder.EntityRecognizer.findEntity(args.entities, 'answer');
console.log(answerEntity);
session.send(answerEntity.entity);
}
]).triggerAction({
matches: 'Search.QnA'
})
bot.dialog('SearchDialog', [
function (session, args) {
console.log("my args in SearchDialog:=========================================================================================== %o", args);
var intent = args.intent;
title = builder.EntityRecognizer.findEntity(intent.entities, 'businessterm');
console.log(title.entity);
qintent = title.entity;
//session.send('You reached the Search.Aco intent. You enquire for the entitiy \'%s\'.', qintent);
//console.log(" SearchDialog: before updating the session.message.text------------->" + session.message.text);
session.message.text= qintent;
//console.log(" SearchDialog: after updating the session.message.text------------->" + session.message.text);
SearchLibrary.begin(session);
},
function (session, args, results) {
// Process selected search results
session.send(
'Done! For future reference, you selected these properties: %s',
args.selection.map(function (i) { return i.key; }).join(', '));
}
]).triggerAction({
matches: 'Search.Aco'
});
var azureSearchClient = AzureSearch.create('aco-intel2', '4105C6676D0CDD9B2E7891952B9E9E00', 'azureblob-index');
var jobsResultsMapper = SearchLibrary.defaultResultsMapper(jobToSearchHit);
// Register Search Dialogs Library with bot
bot.library(SearchLibrary.create({
multipleSelection: true,
search: function (query) { return azureSearchClient.search(query).then(jobsResultsMapper); },
refiners: ['people', 'content', 'location']
}));
// Maps the AzureSearch Job Document into a SearchHit that the Search Library can use
function jobToSearchHit(acosearch) {
console.log("inside jobToSearchHit");
console.log("inside acosearch.DocUrl" + acosearch.DocUrl + "-------" + acosearch.metadata_storage_name);
return {
key: acosearch.id,
title: acosearch.metadata_storage_name,
description: acosearch.content.substring(0, 100)+"...",
documenturl:acosearch.DocUrl,
imageUrl: acosearch.imageurl
};
}
module.exports = { qintent: "qintent"};
This code gives me traces like following:
logs
Please help me understand whats wrong in the above codes. Would be a great help.
Also, the difference between the intent.match and dialogs match. As per my understanding, bot recognizes the session.message and match it with the 'match:' argurments and call the dialogs. so it can jump to and fro between dialogs.
in the first case, its strange for me because it doesnot do it second time.
Thanks in Advance,
Vivek
Using the IntentDialog class (e.g. var intents = new builder.IntentDialog( ... ) does not allow for intent interruption in the way that registering the dialog directly to the bot does.
Given the following example dialogs:
const bot = new UniversalBot(connector); // Assume an already created ChatConnector
bot.recognizer(new LuisRecognizer(LuisModelUrl); // LUIS model with HowAreYou and Weather intents.
bot.dialog('HowAreYou',[
function (session, args, next) {
builder.Prompts.text(session, `I\'m doing great! How are you?`);
},
function (session, args) {
var result = session.message.text;
session.send(`I\'m glad to hear you\'re doing ${result}!`);
]).triggerAction({
matches: 'HowAreYou'
});
bot.dialog('Weather', [
function (session, args) {
builder.Prompts.text(session `Which city do you want to find the weather forecast for?`);
},
function (session, args) {
// some logic to handle the response and call a weather api
}
]).triggerAction({
matches: 'Weather'
});
When a user asks "How are you?" the 'HowAreYou' intent is detected by LUIS and the bot begins the corresponding dialog. The bot then prompts the user "I'm doing great! How are you?'. If the user then says to the bot, "Show me the weather", this bot will receive that message and detect the "Weather" intent. After doing so, it will add the "Weather" dialog to the top of the dialog stack and begin the Weather dialog.
The UniversalBot doesn't prevent you from switching dialogs when a new intent is detected. In the code for the intent dialog, it continues any dialog you were already in and doesn't support switching to a new intent/dialog.
I am making a chat bot using the QnA template (Microsoft Azure). Basically, a user asks a question and the bot will try and find the answer in an FAQ document. If it fails, I want it to run a Bing search with the user's query and replies with the most accurate answer. I found this example that uses Bing Web Search API: https://learn.microsoft.com/en-us/azure/cognitive-services/bing-web-search/quickstarts/nodejs. For now, I just want the bot to reply with the first link of the search for example.
However, I don't know how to merge the code in the link, with the generated code for the QnA Bot (in Node.js):
var restify = require('restify');
var builder = require('botbuilder');
var botbuilder_azure = require("botbuilder-azure");
var builder_cognitiveservices = require("botbuilder-cognitiveservices");
var request = require('request');
// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
console.log('%s listening to %s', server.name, server.url);
});
// Create chat connector for communicating with the Bot Framework Service
var connector = new builder.ChatConnector({
appId: process.env.MicrosoftAppId,
appPassword: process.env.MicrosoftAppPassword,
openIdMetadata: process.env.BotOpenIdMetadata
});
// Listen for messages from users
server.post('/api/messages', connector.listen());
var tableName = 'botdata';
var azureTableClient = new botbuilder_azure.AzureTableClient(tableName, process.env['AzureWebJobsStorage']);
var tableStorage = new botbuilder_azure.AzureBotStorage({ gzipData: false }, azureTableClient);
// Create your bot with a function to receive messages from the user
var bot = new builder.UniversalBot(connector);
bot.set('storage', tableStorage);
// Recognizer and and Dialog for GA QnAMaker service
var recognizer = new builder_cognitiveservices.QnAMakerRecognizer({
knowledgeBaseId: process.env.QnAKnowledgebaseId,
authKey: process.env.QnAAuthKey || process.env.QnASubscriptionKey, // Backward compatibility with QnAMaker (Preview)
endpointHostName: process.env.QnAEndpointHostName
});
var basicQnAMakerDialog = new builder_cognitiveservices.QnAMakerDialog({
recognizers: [recognizer],
defaultMessage: 'Sorry, I cannot find anything on that topic',
qnaThreshold: 0.3
});
// Override the invokeAnswer function from QnAMakerDialog
builder_cognitiveservices.QnAMakerDialog.prototype.invokeAnswer = function (session, recognizeResult, threshold, noMatchMessage) {
var qnaMakerResult = recognizeResult;
session.privateConversationData.qnaFeedbackUserQuestion = session.message.text;
if (qnaMakerResult.score >= threshold && qnaMakerResult.answers.length > 0) {
if (this.isConfidentAnswer(qnaMakerResult) || this.qnaMakerTools == null) {
this.respondFromQnAMakerResult(session, qnaMakerResult);
this.defaultWaitNextMessage(session, qnaMakerResult);
}
else {
this.qnaFeedbackStep(session, qnaMakerResult);
}
}
else {
this.noMatch(session, noMatchMessage, qnaMakerResult);
}
};
// API call to Bing
basicQnAMakerDialog.noMatch = function (session, noMatchMessage, qnaMakerResult) {
var term = session.message.text;
var key = 'i hid it';
var options = {
url: "https://api.cognitive.microsoft.com/bing/v7.0/search?q=" + term,
method: 'GET',
headers : {
'Ocp-Apim-Subscription-Key' : key
}
};
request(options, function(err,res, body){
if(err){
console.error(err);
session.send(noMatchMessage);
}
body = JSON.parse(body);
session.send("I found a thing: " + body["webPages"]["value"][0]["name"]);
});
};
bot.dialog('basicQnAMakerDialog', basicQnAMakerDialog);
bot.dialog('/', //basicQnAMakerDialog);
[
function (session, results) {
session.replaceDialog('basicQnAMakerDialog');
},
]);
In the function inside the bot.dialog, I think I should add a condition such as: if the bot returns the default message, open a web page and let the "term" to search be the user's last message. However, I don't know how to code this exactly, and where to do so. Also, I don't know how to exit from the replaceDialog function in order to reply with something other than the default message or the answers in the FAQ.
PS: I don't have much experience with javascript or web development in general. Any help will be appreciated :)
What you are implementing involves two steps, extracting out the "noMatch" case to a method so you can make a full response message, and then the API call itself.
The "noMatch" method.
First, you will want to override the invokeAnswer function from QnAMakerDialog (This override changes nothing but adding a call to a separate method for the noMatch case.)
builder_cognitiveservices.QnAMakerDialog.prototype.invokeAnswer = function (session, recognizeResult, threshold, noMatchMessage) {
var qnaMakerResult = recognizeResult;
session.privateConversationData.qnaFeedbackUserQuestion = session.message.text;
if (qnaMakerResult.score >= threshold && qnaMakerResult.answers.length > 0) {
if (this.isConfidentAnswer(qnaMakerResult) || this.qnaMakerTools == null) {
this.respondFromQnAMakerResult(session, qnaMakerResult);
this.defaultWaitNextMessage(session, qnaMakerResult);
}
else {
this.qnaFeedbackStep(session, qnaMakerResult);
}
}
else {
this.noMatch(session, noMatchMessage, qnaMakerResult);
}
};
After this you will define your basicQnAMakerDialog as usual, but consider making the default message field something for an error case or "no results found". (This doesn't really matter, as you can arbitrarily decide what you send back within the noMatch method, but it's nice to have the default there)
var basicQnAMakerDialog = new builder_cognitiveservices.QnAMakerDialog({
recognizers: [recognizer],
defaultMessage: 'Sorry, I can't find anything on that topic'
qnaThreshold: 0.3
});
After this you can define the noMatch method seperately.
basicQnAMakerDialog.noMatch = function (session, noMatchMessage, qnaMakerResult) {
session.send(noMatchMessage); //Contains the default message
this.defaultWaitNextMessage(session, qnaMakerResult);
}
Within this method we can call the API or do basically whatever we want.
Credit to gliddell on this override
The API call.
Within the noMatch method, we will make an API call to Bing
basicQnAMakerDialog.noMatch = function (session, noMatchMessage, qnaMakerResult) {
var term = session.message.text;
var key = <Environment Var containing the Bing key>;
var options = {
url: "https://api.cognitive.microsoft.com/bing/v7.0/search?q=" + term,
method: 'GET',
headers : {
'Ocp-Apim-Subscription-Key' : key
}
};
request(options, function(err,res, body){
if(err){
console.error(err);
session.send(noMatchMessage);
}
body = JSON.parse(body);
session.send("I found a thing: " + body["webPages"]["value"][0]["name"]);
});
};
You can customize the message you are sending back as much as you like after you get the results from the search.
I'm testing out the idea of writing a bot to interact with our teams chat tool and our ticketing system. I have a basic bot that will go fetch the status of a task record in the system and respond back. I set that call up as a customAction. The issue I'm running into is the bot will work the first time you interact with it, but the second time it sends the default message instead. Any ideas what I have wrong?
var restify = require("restify");
var builder = require("botbuilder");
var getSnStatus = require("./getSnStatus");
var botAppInfo = require("./botStuffs.json");
//setup the Restify server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function() {
console.log('listening to %s', server.name, server.url);
});
//create chat connector using the ms bot framework
var connector = new builder.ChatConnector({
appId: botAppInfo.AppID,
appPassword: botAppInfo.password
});
//listen for messages from users
server.post('/api/messages', connector.listen());
// setup bot and Register in-memory storage
var inMemoryStorage = new builder.MemoryBotStorage();
var bot = new builder.UniversalBot(connector, [
function(session){
session.send("Hello, If you want to check the status of something just ask for the status or state with a number.");
}
]).set('storage', inMemoryStorage);
//look for the state or status word and run a state check if that's found
//anywhere in the text sent by the user.
//TODO handle a response with no results.
bot.customAction({
matches: /status|state/gi,
onSelectAction(session, args, next) {
words = session.message.text.split(" ");
for (i = 0; i < words.length; i++) {
if (words[i].match(/\d+/)) {
console.log("Run using the word: " + words[i]);
getSnStatus(words[i], function(state) {
state.result.forEach(function(element) {
session.send("The state of " + element.number +
" is: " + element.state);
});
session.endDialog();
})
}
}
}
})
The output I get is like this:
call:state INC1671479
response:The state of INC1671479 is: Open
call:state INC1671479
response: Hello, If you want to check the status of something just ask for the status or state with a number.
I was hoping that it would instead fire my custom action again on the second call.
Here is the working code after the issue was identified.
var restify = require("restify");
var builder = require("botbuilder");
var getSnStatus = require("./getSnStatus");
var botAppInfo = require("./botStuffs.json");
//setup the Restify server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function() {
console.log('listening to %s', server.name, server.url);
});
//create chat connector using the ms bot framework
var connector = new builder.ChatConnector({
appId: botAppInfo.AppID,
appPassword: botAppInfo.password
});
//listen for messages from users
server.post('/api/messages', connector.listen());
// setup bot and Register in-memory storage
var inMemoryStorage = new builder.MemoryBotStorage();
var bot = new builder.UniversalBot(connector, []).set('storage', inMemoryStorage);
bot.dialog("status", (session, args, next)=>{
words = session.message.text.split(" ");
for (i = 0; i < words.length; i++) {
if (words[i].match(/\d+/)) {
console.log("Run using the word: " + words[i]);
getSnStatus(words[i], function(state) {
state.result.forEach(function(element) {
session.send("The state of " + element.number +
" is: " + element.state);
});
})
}
}
}).triggerAction({matches: /status|state/gi});
The issue I'm running into is the bot will work the first time you interact with it, but the second time it sends the default message instead. Any ideas what I have wrong?
I think this behavior is by design, according to the official document: Bind a customAction, once the action is completed, control is passed back to the dialog that is at the top of the stack and the bot can continue.
So in your code, after the first time your customAction is completed, it will go to the root dialog by default, when the second time user input a "state", this dialog will handle the user's input.
The use of customAction is to perform for some quick action requests without manipulate the dialog stack. So for your scenario, I think it's better to Bind a triggerAction for example like this:
bot.dialog('status', (session, args, next)=>{
var message = session.message.text;
//your work to get status goes here.
}).triggerAction({matches: /status|state/gi});
How to send bot response based on user session in NodeJS.
I have created a sample NodeJS application using botframework and connecting to IBM watson to process the user query to get the appropriate response and sending back the response with 10secs delay.
I have generate the ngrok URL and configured it bot framework for web channel. When i test this in mutipl browsers simulataneously , i am getting the same response for all , not based on the user session and user request.
It's giving the latest request processed response to everyuser. I have copied the sample nodejs code below.
Can someone please look into my code and suggest me on how to send the response based on user session/conversationID?
NodeJS sample code :
'use strict';
var restify = require('restify');
var builder = require('botbuilder');
var Conversation = require('watson-developer-cloud/conversation/v1');
require('dotenv').config({silent: true});
var server = restify.createServer();
var contexts = { CandidateID : "89798" , Location : "USA" }
var workspace= 'XXXXXXXXXXXXXXXX';
let name,id,message,addr,watson;
var savedAddress;
server.listen(process.env.port || process.env.PORT || 3000, function () {
console.log('%s listening to %s', server.name, server.url);
});
// Create the service wrapper
var conversation = new Conversation({
username: 'XXXXXXXXXXXXXXXX',
password: 'XXXXXXXXXXXXXXXX',
url: 'https://gateway.watsonplatform.net/conversation/api',
version_date: 'XXXXXXXXXXXXXXXX'
});
// setup bot credentials
var connector = new builder.ChatConnector({
appId: 'XXXXXXXXXXXXXXXX',
appPassword: 'XXXXXXXXXXXXXXXX'
});
var bot = new builder.UniversalBot(connector,);
server.post('/api/messages', connector.listen());
// root dialog
bot.dialog('/', function (session, args) {
name = session.message.user.name;
id = session.message.user.id;
message = session.message.text;
savedAddress = session.message.address;
var payload = {
workspace_id: workspace,
context: contexts,
input: {
text: session.message.text
}
};
var conversationContext = {
workspaceId: workspace,
watsonContext: contexts
};
if (!conversationContext) {
conversationContext = {};
}
payload.context = conversationContext.watsonContext;
conversation.message(payload, function(err, response) {
if (err) {
console.log(err);
session.send(err);
} else {
console.log(JSON.stringify(response, null, 2));
conversationContext.watsonContext = response.context;
watson = response.output.text;
}
});
console.log(message);
setTimeout(() => {
startProactiveDialog(savedAddress);
}, 10000);
});
// handle the proactive initiated dialog
bot.dialog('/survey', function (session, args, next) {
if (session.message.text === "done") {
session.send("Great, back to the original conversation");
session.endDialog();
} else {
session.send(id + ' ' + watson);
}
});
// initiate a dialog proactively
function startProactiveDialog(address) {
bot.beginDialog(address, "*:/survey");
}
You need to make sure it passes back and forth all of the system context for that user's session. Our botkit middleware essentially does this for you.
https://github.com/watson-developer-cloud/botkit-middleware
It looks like that you are leveraging the sample at https://github.com/Microsoft/BotBuilder-Samples/tree/master/Node/core-proactiveMessages/startNewDialog Sending a dialog-based proactive message to specific user.
The sample is just for reference, it only saves the latest conversion at it only declare one variable savedAddress, each time the new user comes in, the new seession address will assign to savedAddress and cover the old value.
You can try to define an object to save user addresses for quick test:
var savedAddress = {};
bot.dialog('/', function (session, args) {
savedAddress[session.message.address.id] = session.message.address;
var message = 'Hey there, I\'m going to interrupt our conversation and start a survey in a few seconds.';
session.send(message);
message = 'You can also make me send a message by accessing: ';
message += 'http://localhost:' + server.address().port + '/api/CustomWebApi';
session.send(message);
setTimeout(() => {
startProactiveDialog(savedAddress[session.message.address.id]);
}, 5000);
});
For production stage, you can leverage Redis or some other cache storage to store this address list.
Hope it helps.