向 qnamaker bot 添加对话框的最有效方法
Most efficient way to add dialog to qnamaker bot
QnA maker 机器人易于实施并提供巨大价值。在某些情况下,我需要向 QnaMaker 机器人添加对话框。我正在努力寻找最好的方法来做到这一点。我试过的示例都以非 QnAmaker 主对话框开始。
我的目标是在 QnA 服务 (#contact) 的特定回答后开始对话(以获取联系方式)。一些指导表示赞赏。
我创建了一个对话框组件来检索用户配置文件。我使用多提示示例作为指导。该对话框确实是在 QnAMaker 查询的某个结果后启动的。
// user requests to be contacted
case '#Contact': {
await this.dialog.run(turnContext, this.dialogState);
break;
对话集的第一步开始。输入响应后,该过程失败。答案将再次发送到 QnA 服务,而不是用作对话组件下一步的输入(结果)。
我预计原因是所有结果都由 onTurn 处理程序发送到 QnA 服务。
我的问题:
这还能做吗。我是否能够(无需大量重构)从 QnA 机器人启动一个简单的对话。
有没有办法检查是否有活动的对话框。如果是这样,我也许可以通过它来解决它。
我正在考虑这样的事情:
this.onMessage(async (context, next) => {
console.log('Any active Dialog we need to finish?');
AciveDialog ? ResumeDialog : const qnaResults = await this.qnaMaker.getAnswers(context);
文档和示例不是很有帮助,因此非常感谢任何帮助。
到目前为止我的机器人代码。我没有 link 对话框组件,因为我希望这不会成为问题的一部分。
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Microsoft Bot Framework components
const { AttachmentLayoutTypes, ActivityTypes, ActivityHandler, CardFactory } = require('botbuilder');
const { QnAMaker } = require('botbuilder-ai');
// Making sure the time is mentioned correctly
const moment = require('moment-timezone');
require('moment/locale/nl');
// Helper funtions (forecast, welcome-message, cards, storage)
const helper = require('./helper');
// Introcard for welcome message
const IntroCard = require('./resources/IntroCard.json');
class QnAMakerBot extends ActivityHandler {
constructor(endpoint, qnaOptions, conversationState, userState, dialog) {
super();
this.qnaMaker = new QnAMaker(endpoint, qnaOptions);
this.conversationState = conversationState;
this.userState = userState;
this.dialog = dialog;
this.dialogState = this.conversationState.createProperty('DialogState');
}
async onTurn(turnContext) {
// First check if a new user joined the webchat, if so, send a greeting message to the user.
if (turnContext.activity.name === 'webchat/join') {
await turnContext.sendActivity({ type: 'typing' });
await turnContext.sendActivity({
attachments: [CardFactory.adaptiveCard(IntroCard)]
});
};
// if a user sent a message, show some response (1) and construct an answer (2).
if (turnContext.activity.type === ActivityTypes.Message) {
// (1)typing indicator with a short delay to improve user experience
await turnContext.sendActivity({ type: 'typing' });
// (2) Perform a call to the QnA Maker service to retrieve matching Question and Answer pairs.
const qnaResults = await this.qnaMaker.getAnswers(turnContext);
// for learning purposes store all questions with qnaMaker score.
if (turnContext.activity.name !== 'webchat/join') {
let score = (qnaResults[0] != null) ? qnaResults[0].score : 'No answer found';
helper.storeQuestions(turnContext, score);
};
// If QnAMaker found an answer that might be correct, first check for responses that need additional work
// If so, do the additional work, otherwise (default) send the QnA answer to the user
if (qnaResults[0] && qnaResults[0].score > 0.5) {
switch (qnaResults[0].answer) {
// user requests a weatherforecast
case '#Weather': {
var weatherForecast = await helper.getWeatherForecast(turnContext);
await turnContext.sendActivity({
attachments: [CardFactory.adaptiveCard(weatherForecast)]
});
break;
}
// user requests current date and/or time
case '#DateTime': {
await turnContext.sendActivity(moment().tz('Europe/Amsterdam').format('[Today is ]LL[ and the time is ] LT'));
break;
}
// user requests help or a startmenu
case '#Help': {
await turnContext.sendActivity({
attachments: [CardFactory.adaptiveCard(IntroCard)]
});
break;
}
// user requests an overview of current bots
case '#Bots': {
await turnContext.sendActivity({
attachments: helper.createBotsGallery(turnContext),
attachmentLayout: AttachmentLayoutTypes.Carousel
});
break;
}
// user requests to be contacted. This is were the magic should happen ;-)
case '#Contact': {
await this.dialog.run(turnContext, this.dialogState);
break;
}
// if no 'special' requests, send the answer found in QnaMaker
default: {
await turnContext.sendActivity(qnaResults[0].answer);
break;
}
}
// QnAmaker did not find an answer with a high probability
} else {
await turnContext.sendActivity('Some response');
}
}
}
async onMessage(turnContext, next) {
// Run the Dialog with the new message Activity.
await this.dialog.run(turnContext, this.dialogState);
await next();
};
async onDialog(turnContext, next) {
// Save any state changes. The load happened during the execution of the Dialog.
await this.conversationState.saveChanges(turnContext, false);
await this.userState.saveChanges(turnContext, false);
await next();
};
}
module.exports.QnAMakerBot = QnAMakerBot;
最简单的方法是使用 botbuilder-dialogs 库 https://github.com/microsoft/botbuilder-js/tree/master/libraries/botbuilder-dialogs
使用预先打包的 libraries/dialog 类 botbuilder 比尝试从头开始更容易。简单提示之类的东西一应俱全
Botbuilder-Samples 存储库具有特定功能的示例,因此您不会因浏览大量机器人代码或阅读 Microsoft 令人困惑的文档试图找到您需要的内容而不知所措。
您似乎只是想提示输入,所以这将非常适合您的需要
https://github.com/microsoft/BotBuilder-Samples/tree/master/samples/javascript_nodejs/44.prompt-for-user-input
您可以通过使用 component dialogs 来实现。
在下面的示例中,我有一个组件对话框,'listens' 用于用户输入。在这种情况下,让用户输入与获取用户名相关的内容。如果匹配,它会调用 QnA 来检索 answer/response。检索并显示答案后,机器人会在 return 返回主对话之前开始中间(子)对话。
首先,创建要在任何成功的 QnA 响应之后路由到的组件对话框。我已将此文件命名为 'getUserNameDialog.js'.
const {
TextPrompt,
ComponentDialog,
WaterfallDialog
} = require('botbuilder-dialogs');
const GET_USER_NAME_DIALOG = 'GET_USER_NAME_DIALOG';
const TEXT_PROMPT = 'TEXT_PROMPT';
const WATERFALL_DIALOG = 'WATERFALL_DIALOG';
class GetUserNameDialog extends ComponentDialog {
constructor() {
super(GET_USER_NAME_DIALOG);
this.addDialog(new TextPrompt(TEXT_PROMPT));
this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.getNameStep.bind(this),
this.displayNameStep.bind(this)
]));
this.initialDialogId = WATERFALL_DIALOG;
}
async getNameStep(stepContext) {
return await stepContext.prompt(TEXT_PROMPT, "Let's makeup a user name for fun. Enter something.");
// return stepContext.next();
}
async displayNameStep(stepContext) {
const stepResults = stepContext.result;
await stepContext.context.sendActivity(`${ stepResults } is a fine name!`);
return stepContext.endDialog();
}
}
module.exports.GetUserNameDialog = GetUserNameDialog;
module.exports.GET_USER_NAME_DIALOG = GET_USER_NAME_DIALOG;
接下来,创建 QnA 对话框(我将其命名为 qnaResponseDialog.js)。我的 QnA 凭据存储在一个 .env 文件中,从中检索它们。请注意,我需要我在上面创建的 'getUserNameDialog' 文件。
当有来自 QnA 的 match/response 时(我正在寻找对 'user name' 的一些引用),然后我调用 beginDialog() 来启动子对话框。我通过在 QnA 响应中映射 returned 问题并匹配用户输入来实现这一点。如果 'user' and/or 'name' 在任何一个问题中,那么我 return 是正确的。如果为真,那么我 return QnA 响应并开始子对话。
这个匹配过程非常简单,更多的是为了演示,但如果它对你有用,那就太好了。但是,我建议您考虑使用 LUIS 来匹配用户意图。它将使这个过程变得更干净,更易于维护。
const { ComponentDialog } = require('botbuilder-dialogs');
const { QnAMaker } = require('botbuilder-ai');
const { GetUserNameDialog, GET_USER_NAME_DIALOG } = require('./getUserNameDialog');
class QnAResponseDialog extends ComponentDialog {
constructor() {
super(GET_USER_NAME_DIALOG);
this.addDialog(new GetUserNameDialog());
try {
this.qnaMaker = new QnAMaker({
knowledgeBaseId: process.env.QnAKnowledgebaseId,
endpointKey: process.env.QnAAuthKey,
host: process.env.QnAEndpointHostName
});
} catch (err) {
console.warn(`QnAMaker Exception: ${ err } Check your QnAMaker configuration in .env`);
}
}
async onBeginDialog(innerDc, options) {
const result = await this.interrupt(innerDc);
if (result) {
return result;
}
return await super.onBeginDialog(innerDc, options);
}
async onContinueDialog(innerDc) {
const result = await this.interrupt(innerDc);
if (result) {
return result;
}
return await super.onContinueDialog(innerDc);
}
async interrupt(innerDc) {
if (innerDc.context.activity.type === 'message') {
const text = innerDc.context.activity.text.toLowerCase();
const stepResults = innerDc.context;
let qnaResults = await this.qnaMaker.getAnswers(stepResults);
console.log(qnaResults[0]);
stepResults.qna = qnaResults[0];
if (qnaResults[0]) {
let mappedResult = null;
const includesText = qnaResults[0].questions.map((question) => {
if (text.includes('user') || text.includes('name')) {
mappedResult = true;
} else {
mappedResult = false;
}
console.log('RESULTS: ', mappedResult);
});
console.log('MAPPED: ', mappedResult);
switch (mappedResult) {
case true:
let answer = stepResults.qna.answer;
await innerDc.context.sendActivity(answer);
return await innerDc.beginDialog(GET_USER_NAME_DIALOG);
}
}
}
}
}
module.exports.QnAResponseDialog = QnAResponseDialog;
最后,在您的主对话框或顶级对话框中,包括以下内容:
const { QnAResponseDialog } = require('./qnaResponseDialog');
class MainDialg extends QnAResponseDialog {
[...]
}
此时,如果所有配置都正确,当用户键入一个短语时,QnA 会识别并接受它应该中断当前对话,显示 QnA 响应,开始子组件对话,一旦完成,return 返回父对话框。
QnA maker 机器人易于实施并提供巨大价值。在某些情况下,我需要向 QnaMaker 机器人添加对话框。我正在努力寻找最好的方法来做到这一点。我试过的示例都以非 QnAmaker 主对话框开始。
我的目标是在 QnA 服务 (#contact) 的特定回答后开始对话(以获取联系方式)。一些指导表示赞赏。
我创建了一个对话框组件来检索用户配置文件。我使用多提示示例作为指导。该对话框确实是在 QnAMaker 查询的某个结果后启动的。
// user requests to be contacted
case '#Contact': {
await this.dialog.run(turnContext, this.dialogState);
break;
对话集的第一步开始。输入响应后,该过程失败。答案将再次发送到 QnA 服务,而不是用作对话组件下一步的输入(结果)。
我预计原因是所有结果都由 onTurn 处理程序发送到 QnA 服务。
我的问题:
这还能做吗。我是否能够(无需大量重构)从 QnA 机器人启动一个简单的对话。
有没有办法检查是否有活动的对话框。如果是这样,我也许可以通过它来解决它。
我正在考虑这样的事情:
this.onMessage(async (context, next) => {
console.log('Any active Dialog we need to finish?');
AciveDialog ? ResumeDialog : const qnaResults = await this.qnaMaker.getAnswers(context);
文档和示例不是很有帮助,因此非常感谢任何帮助。
到目前为止我的机器人代码。我没有 link 对话框组件,因为我希望这不会成为问题的一部分。
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Microsoft Bot Framework components
const { AttachmentLayoutTypes, ActivityTypes, ActivityHandler, CardFactory } = require('botbuilder');
const { QnAMaker } = require('botbuilder-ai');
// Making sure the time is mentioned correctly
const moment = require('moment-timezone');
require('moment/locale/nl');
// Helper funtions (forecast, welcome-message, cards, storage)
const helper = require('./helper');
// Introcard for welcome message
const IntroCard = require('./resources/IntroCard.json');
class QnAMakerBot extends ActivityHandler {
constructor(endpoint, qnaOptions, conversationState, userState, dialog) {
super();
this.qnaMaker = new QnAMaker(endpoint, qnaOptions);
this.conversationState = conversationState;
this.userState = userState;
this.dialog = dialog;
this.dialogState = this.conversationState.createProperty('DialogState');
}
async onTurn(turnContext) {
// First check if a new user joined the webchat, if so, send a greeting message to the user.
if (turnContext.activity.name === 'webchat/join') {
await turnContext.sendActivity({ type: 'typing' });
await turnContext.sendActivity({
attachments: [CardFactory.adaptiveCard(IntroCard)]
});
};
// if a user sent a message, show some response (1) and construct an answer (2).
if (turnContext.activity.type === ActivityTypes.Message) {
// (1)typing indicator with a short delay to improve user experience
await turnContext.sendActivity({ type: 'typing' });
// (2) Perform a call to the QnA Maker service to retrieve matching Question and Answer pairs.
const qnaResults = await this.qnaMaker.getAnswers(turnContext);
// for learning purposes store all questions with qnaMaker score.
if (turnContext.activity.name !== 'webchat/join') {
let score = (qnaResults[0] != null) ? qnaResults[0].score : 'No answer found';
helper.storeQuestions(turnContext, score);
};
// If QnAMaker found an answer that might be correct, first check for responses that need additional work
// If so, do the additional work, otherwise (default) send the QnA answer to the user
if (qnaResults[0] && qnaResults[0].score > 0.5) {
switch (qnaResults[0].answer) {
// user requests a weatherforecast
case '#Weather': {
var weatherForecast = await helper.getWeatherForecast(turnContext);
await turnContext.sendActivity({
attachments: [CardFactory.adaptiveCard(weatherForecast)]
});
break;
}
// user requests current date and/or time
case '#DateTime': {
await turnContext.sendActivity(moment().tz('Europe/Amsterdam').format('[Today is ]LL[ and the time is ] LT'));
break;
}
// user requests help or a startmenu
case '#Help': {
await turnContext.sendActivity({
attachments: [CardFactory.adaptiveCard(IntroCard)]
});
break;
}
// user requests an overview of current bots
case '#Bots': {
await turnContext.sendActivity({
attachments: helper.createBotsGallery(turnContext),
attachmentLayout: AttachmentLayoutTypes.Carousel
});
break;
}
// user requests to be contacted. This is were the magic should happen ;-)
case '#Contact': {
await this.dialog.run(turnContext, this.dialogState);
break;
}
// if no 'special' requests, send the answer found in QnaMaker
default: {
await turnContext.sendActivity(qnaResults[0].answer);
break;
}
}
// QnAmaker did not find an answer with a high probability
} else {
await turnContext.sendActivity('Some response');
}
}
}
async onMessage(turnContext, next) {
// Run the Dialog with the new message Activity.
await this.dialog.run(turnContext, this.dialogState);
await next();
};
async onDialog(turnContext, next) {
// Save any state changes. The load happened during the execution of the Dialog.
await this.conversationState.saveChanges(turnContext, false);
await this.userState.saveChanges(turnContext, false);
await next();
};
}
module.exports.QnAMakerBot = QnAMakerBot;
最简单的方法是使用 botbuilder-dialogs 库 https://github.com/microsoft/botbuilder-js/tree/master/libraries/botbuilder-dialogs
使用预先打包的 libraries/dialog 类 botbuilder 比尝试从头开始更容易。简单提示之类的东西一应俱全
Botbuilder-Samples 存储库具有特定功能的示例,因此您不会因浏览大量机器人代码或阅读 Microsoft 令人困惑的文档试图找到您需要的内容而不知所措。
您似乎只是想提示输入,所以这将非常适合您的需要 https://github.com/microsoft/BotBuilder-Samples/tree/master/samples/javascript_nodejs/44.prompt-for-user-input
您可以通过使用 component dialogs 来实现。
在下面的示例中,我有一个组件对话框,'listens' 用于用户输入。在这种情况下,让用户输入与获取用户名相关的内容。如果匹配,它会调用 QnA 来检索 answer/response。检索并显示答案后,机器人会在 return 返回主对话之前开始中间(子)对话。
首先,创建要在任何成功的 QnA 响应之后路由到的组件对话框。我已将此文件命名为 'getUserNameDialog.js'.
const {
TextPrompt,
ComponentDialog,
WaterfallDialog
} = require('botbuilder-dialogs');
const GET_USER_NAME_DIALOG = 'GET_USER_NAME_DIALOG';
const TEXT_PROMPT = 'TEXT_PROMPT';
const WATERFALL_DIALOG = 'WATERFALL_DIALOG';
class GetUserNameDialog extends ComponentDialog {
constructor() {
super(GET_USER_NAME_DIALOG);
this.addDialog(new TextPrompt(TEXT_PROMPT));
this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.getNameStep.bind(this),
this.displayNameStep.bind(this)
]));
this.initialDialogId = WATERFALL_DIALOG;
}
async getNameStep(stepContext) {
return await stepContext.prompt(TEXT_PROMPT, "Let's makeup a user name for fun. Enter something.");
// return stepContext.next();
}
async displayNameStep(stepContext) {
const stepResults = stepContext.result;
await stepContext.context.sendActivity(`${ stepResults } is a fine name!`);
return stepContext.endDialog();
}
}
module.exports.GetUserNameDialog = GetUserNameDialog;
module.exports.GET_USER_NAME_DIALOG = GET_USER_NAME_DIALOG;
接下来,创建 QnA 对话框(我将其命名为 qnaResponseDialog.js)。我的 QnA 凭据存储在一个 .env 文件中,从中检索它们。请注意,我需要我在上面创建的 'getUserNameDialog' 文件。
当有来自 QnA 的 match/response 时(我正在寻找对 'user name' 的一些引用),然后我调用 beginDialog() 来启动子对话框。我通过在 QnA 响应中映射 returned 问题并匹配用户输入来实现这一点。如果 'user' and/or 'name' 在任何一个问题中,那么我 return 是正确的。如果为真,那么我 return QnA 响应并开始子对话。
这个匹配过程非常简单,更多的是为了演示,但如果它对你有用,那就太好了。但是,我建议您考虑使用 LUIS 来匹配用户意图。它将使这个过程变得更干净,更易于维护。
const { ComponentDialog } = require('botbuilder-dialogs');
const { QnAMaker } = require('botbuilder-ai');
const { GetUserNameDialog, GET_USER_NAME_DIALOG } = require('./getUserNameDialog');
class QnAResponseDialog extends ComponentDialog {
constructor() {
super(GET_USER_NAME_DIALOG);
this.addDialog(new GetUserNameDialog());
try {
this.qnaMaker = new QnAMaker({
knowledgeBaseId: process.env.QnAKnowledgebaseId,
endpointKey: process.env.QnAAuthKey,
host: process.env.QnAEndpointHostName
});
} catch (err) {
console.warn(`QnAMaker Exception: ${ err } Check your QnAMaker configuration in .env`);
}
}
async onBeginDialog(innerDc, options) {
const result = await this.interrupt(innerDc);
if (result) {
return result;
}
return await super.onBeginDialog(innerDc, options);
}
async onContinueDialog(innerDc) {
const result = await this.interrupt(innerDc);
if (result) {
return result;
}
return await super.onContinueDialog(innerDc);
}
async interrupt(innerDc) {
if (innerDc.context.activity.type === 'message') {
const text = innerDc.context.activity.text.toLowerCase();
const stepResults = innerDc.context;
let qnaResults = await this.qnaMaker.getAnswers(stepResults);
console.log(qnaResults[0]);
stepResults.qna = qnaResults[0];
if (qnaResults[0]) {
let mappedResult = null;
const includesText = qnaResults[0].questions.map((question) => {
if (text.includes('user') || text.includes('name')) {
mappedResult = true;
} else {
mappedResult = false;
}
console.log('RESULTS: ', mappedResult);
});
console.log('MAPPED: ', mappedResult);
switch (mappedResult) {
case true:
let answer = stepResults.qna.answer;
await innerDc.context.sendActivity(answer);
return await innerDc.beginDialog(GET_USER_NAME_DIALOG);
}
}
}
}
}
module.exports.QnAResponseDialog = QnAResponseDialog;
最后,在您的主对话框或顶级对话框中,包括以下内容:
const { QnAResponseDialog } = require('./qnaResponseDialog');
class MainDialg extends QnAResponseDialog {
[...]
}
此时,如果所有配置都正确,当用户键入一个短语时,QnA 会识别并接受它应该中断当前对话,显示 QnA 响应,开始子组件对话,一旦完成,return 返回父对话框。