I'm playing a bit with chatbots using Bot Framework from MS.
I created one supposed to book a flight, and it has LUIS integrated.
My question is: once I'm in the flight booking dialog triggered by LUIS, I want to check if all the informations regarding the flight are given by the user (departure city, arrival city, date, airline...). So if I'm missing an info, for example the city of departure, the bot will ask 'can you give me the departure city?' qnd if I write 'from London', LUIS detects that as a new flight reservation and triggers another dialog. but I want it to stay in the dialog obviously!
Here is the dialog code so far, just in case if departure city is missing:
// Main dialog with LUIS
bot.dialog('FlightBookingDialog', [
function (session, args, next) {
// Resolve and store any entity passed from LUIS.
var intent = args.intent;
session.dialogData.airline = builder.EntityRecognizer.findEntity(intent.entities, 'Airline');
session.dialogData.class = builder.EntityRecognizer.findEntity(intent.entities, 'Class');
session.dialogData.date_time = builder.EntityRecognizer.findEntity(intent.entities, 'builtin.datetimeV2');
session.dialogData.departure = builder.EntityRecognizer.findEntity(intent.entities, 'Departure');
session.dialogData.destination = builder.EntityRecognizer.findEntity(intent.entities, 'Destination');
session.dialogData.number_tickets = builder.EntityRecognizer.findEntity(intent.entities, 'number');
session.send("I see you want to travel, great !");
if(!session.dialogData.departure) {
builder.Prompts.text(session, "Can you specify me a departure city please ?");
} else {
next();
}
},
function (session, args, results, next) {
if (results.response) {
builder.LuisRecognizer.recognize(session.message.text, luisModelUrl,
function(err, intents, entities) {
if(entities) {
var departure = builder.EntityRecognizer.findEntity(intents.entities, 'Departure');
if (departure) {
session.dialogData.departure = departure;
}
}
}
);
};
session.send("Good !");
},
]).triggerAction({
matches: 'FlightBooking'
});
If I understand correctly, I think you can update the code to have multiple matches. From this documentation page the code may be updated to look like this:
var intents = new builder.IntentDialog({ recognizers: [recognizer] })
.matches('Greeting', (session) => {
session.send('You reached Greeting intent, you said \'%s\'.', session.message.text);
})
.matches('Note.Create', [(session, args, next) => {
// Resolve and store any Note.Title entity passed from LUIS.
var intent = args.intent;
var title = builder.EntityRecognizer.findEntity(intent.entities, 'Note.Title');
var note = session.dialogData.note = {
title: title ? title.entity : null,
};
// Prompt for title
if (!note.title) {
builder.Prompts.text(session, 'What would you like to call your note?');
} else {
next();
}
},
(session, results, next) => {
var note = session.dialogData.note;
if (results.response) {
note.title = results.response;
}
// Prompt for the text of the note
if (!note.text) {
builder.Prompts.text(session, 'What would you like to say in your note?');
} else {
next();
}
},
(session, results) => {
var note = session.dialogData.note;
if (results.response) {
note.text = results.response;
}
// If the object for storing notes in session.userData doesn't exist yet, initialize it
if (!session.userData.notes) {
session.userData.notes = {};
console.log("initializing session.userData.notes in CreateNote dialog");
}
// Save notes in the notes object
session.userData.notes[note.title] = note;
// Send confirmation to user
session.endDialog('Creating note named "%s" with text "%s"',
note.title, note.text);
}])
Note that there is no endDialog method until the end when the bot has all the information it needs to process the request.
Hope that helps or at least gets you on the right path!
I found the solution here:
https://github.com/Microsoft/BotFramework-Samples/tree/master/docs-samples/Node/basics-naturalLanguage
They are exactly talking about my problem, I post it here so people in the same situation can see it.
You have to go to the IntentDialog part, Ctrl+F it.
Related
I am very new to bot builder. I need to use LUIs intents and then eventually call azure-search to search the intent.
However, below is my code. In the Aco-Saerch intent, I want to extract the intent from the user's message and then want to pass to the query in the azure-search.
In the dialog SearchDialog, waterfall functions are not proceeding after the first function. Can anyone help me out whats wrong here.
I am trying the use the azure search code and libraries from this git-hub shared code: Realstate azure search node js code
In future, I want to add QnA recognizer too. Aim is : search the query in qnA base, if found, return else, use the LUIS to find out the intent and then pass it the azure search.
var util = require('util');
var _ = require('lodash');
var builder = require('botbuilder');
var restify = require('restify');
/// <reference path="../SearchDialogLibrary/index.d.ts" />
var SearchLibrary = require('../SearchDialogLibrary');
var AzureSearch = require('../SearchProviders/azure-search');
// 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();
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);
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('SearchDialog', [
function (session, args) {
var intent = args.intent;
var title = builder.EntityRecognizer.findEntity(intent.entities, 'businessterm');
console.log(title.entity);
session.send('You reached the Search.Aco intent. You enquire for the entitiy \'%s\'.', title.entity);
},
function (session, args, results) {
// Trigger Search
console.log("Inside the SearchLibrary.begin");
SearchLibrary.begin(session);
console.log("after the SearchLibrary.begin");
},
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
};
}
Your dialog isn't advancing as there is nothing for it to act on. Thus, it reaches the end of the first function with nowhere to move on to. You have a few options. You can:
You can include a prompt or button (i.e. some sort of action) to move the dialog along. Ex:
bot.dialog('SearchDialog', [
function (session, args) {
var intent = args.intent;
var title = builder.EntityRecognizer.findEntity(intent.entities, 'businessterm');
console.log(title.entity);
session.send('You reached the Search.Aco intent. You enquire for the entitiy \'%s\'.', title.entity);
builder.Prompts.choice(session, "Start search?", "Yes|No", { listStyle: builder.ListStyle["button"] });
},
function (session, args, results) {
// Trigger Search
console.log("Inside the SearchLibrary.begin");
SearchLibrary.begin(session);
console.log("after the SearchLibrary.begin");
}
You can marry the first and second function. However, again, you will need something to move the dialog along from the search function to the search results function.
bot.dialog('SearchDialog', [
function (session, args) {
var intent = args.intent;
var title = builder.EntityRecognizer.findEntity(intent.entities, 'businessterm');
console.log(title.entity);
session.send('You reached the Search.Aco intent. You enquire for the entitiy \'%s\'.', title.entity);
// Trigger Search
console.log("Inside the SearchLibrary.begin");
SearchLibrary.begin(session);
console.log("after the SearchLibrary.begin");
},
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'
});
You can make the search function a separate dialog that is initiated at the end of the first dialog function and, once completes, returns and continues the original dialog.
bot.dialog('SearchDialog', [
function (session, args) {
var intent = args.intent;
var title = builder.EntityRecognizer.findEntity(intent.entities, 'businessterm');
console.log(title.entity);
session.send('You reached the Search.Aco intent. You enquire for the entitiy \'%s\'.', title.entity);
session.beginDialog('/searchAzure');
},
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'
});
bot.dialog('/searchAzure', [
function (session) {
// Trigger Search
console.log("Inside the SearchLibrary.begin");
SearchLibrary.begin(session);
console.log("after the SearchLibrary.begin");
session.endDialog();
}
]);
I'm currently building a chatbot with Azure's Bot service. I use a NLU-Bot, mixed with a waterfall flow, because dependent on the intent I want to get some specific information.
Therefore I match the intent1 and want to
var intents = new builder.IntentDialog({ recognizers: [recognizer] })
.matches('intent1', (session, args, results) =>{
session.beginDialog("getRequiredInformations");
session.send("I received your information");
})
bot.dialog('getRequiredInformations', [
(session) =>{
var levels = ['Beginner', 'Intermediate', 'Expert'];
builder.Prompts.choice(session, "What's your level ?", levels, { listStyle: builder.ListStyle.button, maxRetries: 0 });
},
(session, results) => {
session.conversationData.level = results.response.entity;
}
]);
What I want to do is to wait until we received the answer from the getRequiredInformations dialog and then continue with the original dialog containing the recognized intent. With the code above the session.send("I received your information");is sent before the user entered an answer.
I also tried with bot.beginDialogAction('getRequiredInformations', 'getRequiredInformations'); but I think it's not possible to call this inside the dialog.
How am I able to achieve this?
Found there several misstakes in your code snippet, please refer the following modification:
bot.dialog('intent1', [(session, args, next) => {
session.beginDialog("getRequiredInformations");
}, (session, args, next) => {
session.send("I received your information");
session.send(session.conversationData.level)
}]).triggerAction({
matches: 'intent1'
})
bot.dialog('getRequiredInformations', [
(session) => {
var levels = ['Beginner', 'Intermediate', 'Expert'];
builder.Prompts.choice(session, "What's your level ?", levels, {
listStyle: builder.ListStyle.button,
maxRetries: 0
});
},
(session, results) => {
session.conversationData.level = results.response.entity;
session.endDialog();
}
]);
But this means there is not really an opportunity to wait for the dialog to finish like a callback or anything, I need to do this with a waterfall?
If I understand correct, you can try to use the following code snippet to enable the luis recognizer only if there is no dialog in stack.
var recognizer = new builder.LuisRecognizer(luisAppUrl)
.onEnabled(function (context, callback) {
var enabled = context.dialogStack().length == 0;
callback(null, enabled);
});
Move the send to next waterfall step of the intent1 dialog. I think this should work.
var intents = new builder.IntentDialog({ recognizers: [recognizer] })
.matches('intent1', [(session, args, results) =>{
session.beginDialog("getRequiredInformations");
}, (session, args, results) =>{
session.send("I received your information");
}]);
matches method takes IWaterfallStep or IWaterfallStep[]. More info here.
I have a single dialog which has multiple Prompts(Prompts.text, Prompts.number, Prompts.Choice, Prompts.confirm). Though Prompts.choice and Prompts.confirm seems to have inbuilt validations but how to validate Prompts.text?
I have gone through this thread How to handle wrong input from the user? but it was rectified by converting text into choice.
Also I do not want to restart the whole dialog as it ask the questions form beginning then as shown in create custom prompts to validate input
Here is shorter version for my dialog:
bot.dialog('/getDetails', [
function (session, args, next) {
let options = {
retryPrompt: 'The response id invalid'
}
builder.Prompts.text(session, 'What is your full name?', options);
//passing options as argument works for Prompts.choice, which seems an inbuilt validation
},
function (session, results, next){
var name = session.dialogData.name;
//How to to reprompt if user does not enters its full name?
if (results.response) {
name.fullname = results.response;
}
builder.Prompts.text(session, 'Can you please provide your country name?');
},
function (session, results) {
var name = session.dialogData.name;
//How to reprompt only last Prompts.text if user enter an invlid value?
if (results.response) {
name.text = results.response;
}
}
}]).triggerAction({
matches: 'GetDetails',
})
Here is how i solved it through DialogAction.validatedPrompt
bot.dialog('/getDetail', [
function (session) {
session.beginDialog('/validateAge', { prompt: "What's your age?" });
//if false response, then prmopts "I did not understand {age}""
},
function (session, results) {
if (results.response) {
session.send("Thank you for adding your age");
}
}
]).triggerAction({
matches: /^lets validate$/i
})
bot.dialog('/validateAge', builder.DialogAction.validatedPrompt(builder.PromptType.text, function (response) {
if(response> 0 && response < 70){
return response;
}
}));
I use following method to ask question by bot when user connects first time. But after user answering the question, instead of going to next step bot goes to new dialog.
This works fine with emulator, but not with directline api.
bot.on('conversationUpdate', function (message) {
if (message.membersAdded) {
message.membersAdded.forEach(function (identity) {
if (identity.id === message.address.bot.id) {
bot.beginDialog(message.address, '/startConversation');
}
});
}
});
bot.dialog('/startConversation', [
function (session) {
builder.Prompts.text(session, 'I\'m the SPF VA Assistant. How may I address you?');
},
function (session, results) {
session.userData.name = results.response;
session.send('Hi, %s, Please choose one of the options below to lodge a Police report.', session.userData.name);
var cards = getReportsAsCards();
var reportOptions = new builder.Message(session)
.attachmentLayout(builder.AttachmentLayout.carousel)
.attachments(cards);
session.send(reportOptions).endDialog();
}
]);
After you send the question "Please choose one of the options below..." you are calling endDialog(). This is what causes it to go to the new dialog like you said.
If you want to prompt the user for input, use a Choice Prompt, and add another step in your waterfall dialog to handle the user's choice input.
For example, to get the user's name, first ask the question using a text prompt, then handle the user's response in step 2 of the waterfall dialog. The user's response text will be available in the results.response object in step 2.
bot.dialog('greetings', [
// Step 1
function (session) {
builder.Prompts.text(session, 'Hi! What is your name?');
},
// Step 2
function (session, results) {
session.endDialog('Hello %s!', results.response);
}
]);
To prompt the user with a series of choices use a waterfall dialog combined with a builder.Prompts.choice() call. For example:
var noiseComplaintName = "Noise Complaint";
var trashDumpingName = "Trash Dumping";
var weirdSmellName = "Weird Smell";
var trafficSignalName = "Traffic Signal";
var reportTypes = [
noiseComplaintName,
trashDumpingName,
weirdSmellName,
trafficSignalName
];
bot.dialog('pick-report-type', [
function(session) {
session.send('Choice prompt example:');
var promptOptions = {
listStyle: builder.ListStyle.button
};
builder.Prompts.choice(session, "What do you want to report?", reportTypes, promptOptions);
},
function (session, results) {
// handle response from choice prompt
var selectedReportType = results.response.entity;
switch (selectedReportType) {
case noiseComplaintName:
// handle response here
break;
case trashDumpingName:
// handle response here
break;
case weirdSmellName:
// handle response here
break;
case trafficSignalName:
// handle response here
break;
default
// handle response here
break;
}
}
]);
For a more detailed example that combines prompts with Rich Cards, check out the sample code: BotBuilder-Samples/Node/cards-RichCards on GitHub.
I have LUIS app created, with 1 intent(Order Pizza) and 9 entities(Order,Kind, Signature, Size, Toppings, address, Time, Duration, Range).
I need to create a bot in Azure Bot Framework. I'm unable to understand and use the waterfall for all the entities. Please let me know how to go about it
Code :
dialog.matches('OrderPizza', [
function (session, args, next) {
var order = builder.EntityRecognizer.findEntity(args.entities, 'order');
var kind = builder.EntityRecognizer.findEntity(args.entities, 'Kind');
session.dialogData.intentScore = args.score;
if (!order) {
builder.Prompts.text(session, 'welcome please order pizza');
} else {
session.dialogData.order = order;
next();
}
},
function (session, results) {
var order = session.dialogData.order;
if (results.response) {
session.send('what kind?');
}else{
session.send(`we don't have that kind of pizza`);
}
}
]);
How to go further for other entities?
I'm not sure what you mean by only being able to write 2 functions; but if you're trying to call LUIS inside of your dialog you can follow this example. You'll call a LuisRecognizer inside your waterfall step on session.message.text. The snippet from the example is below:
builder.LuisRecognizer.recognize(session.message.text, '<model url>', function (err, intents, entities) {
if (entities) {
var entity = builder.EntityRecognizer.findEntity(entities, 'TYPE');
// do something with entity...
}
});
This would allow you to call LUIS inside a waterfall for recognizing your user's messages.
Regarding your code there is a number of issues:
// Original Code
dialog.matches('OrderPizza', [
function (session, args, next) {
var order = builder.EntityRecognizer.findEntity(args.entities, 'order');
var kind = builder.EntityRecognizer.findEntity(args.entities, 'Kind');
session.dialogData.intentScore = args.score;
if (!order) { // What about kind?
builder.Prompts.text(session, 'welcome please order pizza');
} else {
session.dialogData.order = order; // What about kind?
next();
}
},
function (session, results) {
var order = session.dialogData.order;
if (results.response) {
session.send('what kind?');
}else{
session.send(`we don't have that kind of pizza`);
}
}
]);
In your first waterfall step you're not saving the "Kind" entity to your dialogData.
function (session, args, next) {
var order = builder.EntityRecognizer.findEntity(args.entities, 'order');
var kind = builder.EntityRecognizer.findEntity(args.entities, 'Kind');
session.dialogData.intentScore = args.score;
if (kind) {
session.dialogData.kind = kind;
}
if (!order) {
builder.Prompts.text(session, 'welcome please order pizza');
} else {
session.dialogData.order = order;
next();
}
}
In your second waterfall step you are not calling next, nor are you sending a Prompt to the user, which results in you being stuck after two functions.
function (session, results, next) {
var order = session.dialogData.order ? session.dialogData.order : results.response;
var kind = session.dialogData.kind;
if (results.response && !kind) {
builder.Prompts.text(session, 'what kind?');
} else {
session.send('we don\'t have that kind of pizza');
next();
}
}