beginDialogAction 不重新提示

beginDialogAction doesn't reprompt

我目前正在使用带有 node.js 的 Microsoft Bot Framework 来编写一个通过 API 遍历决策树的机器人。我想允许用户在对话框中的任何时候取消树,这样他就不必遍历整个(可能是巨大的)树来退出。因为如果会话关闭,我需要向 API 发送消息,所以我使用 Dialog.beginDialogAction() 来启动 "cancel"-Dialog,而不是 Dialog.cancelAction(),这样我可以提示用户确认以及关闭会话。

现在我已经准备好了,取消树就可以了,但是如果用户选择对确认提示说 "no" 并且机器人实际上应该重新提示最后一个问题,它有时使用 "no" 自动回答上一个问题(或抛出错误)。这仅在问题的 valueType 为 "string" 且显示 Prompts.choice 对话框时出现,对于 Prompts.numberPrompts.time 会产生预期的行为。

我搜索了所有我能找到的文档,但没有关于某些不支持 DialogActions 或类似内容的提示的信息。我只使用 session.beginDialogsession.endDialogsession.endConversationbuilder.Prompts 来控制对话框堆栈。

代码如下所示:

//This dialog gets the next question node of the tree passed in args[1].
bot.dialog('select', [
  function(session, args, next) {
    //Depending on the value type of the question, different prompts are used.
    switch (args[1].valueType) {
      case "string":
        builder.Prompts.choice(session, args[1].text, args[1].answerValues, {
          listStyle: builder.ListStyle.button,
          maxRetries: 0
        });
        break;
      case "number":
        builder.Prompts.number(session, args[1].text, {
          integerOnly: true,
          maxValue: 100,
          minValue: 0
        })
        break;
      case "datetime":
        builder.Prompts.time(session, message + "\nPlease enter a date in the format 'yyyy-mm-dd'.");
        break;
    }
  },
  function(session, results) {
    //Use the answer to proceed the tree.
    //...
    //With the new question node start over.
    session.beginDialog('select', [sessionId, questionNode]);
  }
]).beginDialogAction('cancelSelect', 'cancel', {
  matches: /^stop$/i
});

//This dialog is used for cancelling.
bot.dialog('cancel', [
  function(session, next) {
    builder.Prompts.confirm(session, 'Do you really want to quit?');
  },
  function(session, results) {
    if (results.response) {
      finishSession();
      session.endConversation('Session was closed.')
    } else {
      //Here the bot should stop this cancel-Dialog and reprompt the previous question
      session.endDialog();
    }
  }
])

但机器人并没有重新提示,而是跳转到 'select'-Dialog 中的 function (session, results),在那里它尝试解析答案 "no",但显然失败了。

这里是我的 app.js 的完整副本。如果不模拟我们产品的 esc-API,您将无法 运行 它,但它表明我只使用 session.beginDialogsession.endDialogsession.endConversationbuilder.Prompts。我所做的唯一更改是删除私人信息并将消息翻译成英文。

/*---------
-- Setup --
---------*/

//esc and esc_auth are product specific, so obviously i cant share them. They handle the API of our product.
const esc = require("./esc");
const esc_auth = require("./esc_auth");
var restify = require("restify");
var builder = require("botbuilder");
var server = restify.createServer();

server.listen(process.env.PORT || process.env.port || 3978, function() {
    console.log(`${server.name} listening to ${server.url}`);
});

var connector = new builder.ChatConnector({
    //Cant share these as well
    appId: "",
    appPassword: ""
});

server.post("/api/messages", connector.listen());

var esc_header;
var esc_session;
var esc_attribs = {};
var treeNames = [];

/*---------
-- Start --
---------*/

//This function is called when no other dialog is currently running.
//It gets the authorization token from the API, reads concepts from the input and searches for matching decision trees.
//If not concepts or trees were found, a text search on the API is cone.
var bot = new builder.UniversalBot(connector, [
    function(session) {
        var esc_token;
        esc_attribs = {};
        console.log("Getting token...");
        esc.escAccessToken(esc_auth.esc_system, esc_auth.esc_apiUser)
        .then(function(result) {
            esc_token = result;
            esc_header = {
                "Content-Type": "application/json",
                "Authorization": "Bearer " + esc_token
            };
            console.log("Got token.");
            //Look for concepts in the message.
            esc.escAnnotateQuery(esc_header, session.message.text)
            .then(function(result) {
                for(i in result.concepts) {
                    for(j in result.concepts[i]) {
                        esc_attribs[i] = j;
                    }
                }
                //If concepts were found, look for trees and solutions with them
                if(Object.keys(esc_attribs).length > 0) {
                    esc.escSearchIndexWithConcepts(esc_header, esc_attribs)
                    .then(function(result) {
                        var treeIds = [];
                        treeNames = [];
                        result = result;
                        result.records.forEach(function(entry) {
                            //Check which tree the found tree is or found solution is in.
                            if(entry.DecisionTree && !treeIds.includes(entry.DecisionTree)) {
                            treeIds.push(entry.DecisionTree);
                            }
                        })
                        if(treeIds.length != 0) {
                            esc.escSearchTrees(esc_header)
                            .then(function(result) {
                                console.log("Trees found.");
                                result.records.forEach(function(tree) {
                                    if(treeIds.includes(tree.DecisionTree)) {
                                        treeNames.push({id:tree.DecisionTree, name: tree.Label})
                                        console.log("Tree: ", tree.DecisionTree, tree.Label);
                                    }
                                })
                                session.beginDialog("tree", treeNames);
                            })
                        } else {
                            console.log("No tree found for " + session.message.text);
                            treeNames = [];
                            session.beginDialog("textSearch");
                            return;
                        }
                    })
                } else {
                    console.log("No concepts found.");
                    session.beginDialog("textSearch");
                    return;
                }
            })
        })
    },
    function(session, results) {
        session.endConversation("You may now start a new search.");
    }
]);

//Searches for trees by text.
bot.dialog("textSearch", [
    function(session) {
        session.send("No concepts were found in your input.");
        builder.Prompts.confirm(session, "Start a text search instead?", {"listStyle": builder.ListStyle.button});
    },
    function(session, results) {
        if(results.response) {
            builder.Prompts.text(session, "Please enter your new search prompt in keywords.")
        } else {
            session.endDialog("Ok, back to concept search.")
        }
    },
    function(session) {
        //Search gives better results without mutated vowels
        esc.escSearchIndex(esc_header, undoMutation(session.message.text))
        .then(function(result) {
            var treeIds = [];
            treeNames = [];
            result.records.forEach(function(entry) {
                //Check which tree the found document is in.
                if(entry.DecisionTree && !treeIds.includes(entry.DecisionTree)) {
                treeIds.push(entry.DecisionTree);
                }
            })
            if(treeIds.length != 0) {
                esc.escSearchTrees(esc_header)
                .then(function(result) {
                    console.log("Trees found.");
                    result.records.forEach(function(tree) {
                        if(treeIds.includes(tree.DecisionTree)) {
                            treeNames.push({id:tree.DecisionTree, name: tree.Label})
                            console.log("Tree: ", tree.DecisionTree, tree.Label);
                        }
                    })
                    session.beginDialog("tree", treeNames);
                })
            } else {
                console.log("No tree found for " + session.message.text);
                treeNames = [];
                session.endConversation("No trees were found for this search.");
            }
        })
    }
])

//The cancel dialog.
bot.dialog("cancel", [
    function(session) {
        builder.Prompts.confirm(session, "Do you really want to cancel?", {"listStyle": builder.ListStyle.button});
    },
    function(session, results) {
        if(results.response) {
            if(esc_session) {
                esc.escFinishSession(esc_header, esc_session.sessionId)
                .then(function(result) {
                    esc_session = undefined;
                    session.endConversation("Session was cancelled.")
                })
            } else {
                session.endConversation("Session was cancelled.")
            }
        } else {
            session.endDialog();
        }
    }
])

/*-------------------------
-- Decision tree dialogs --
-------------------------*/

//This dialog presents the found decision trees and lets the user select one.
bot.dialog("tree", [
    function(session, treeArray) {
        var opts = [];
        treeArray.forEach(function(t) {
            opts.push(t.name);
        });
        builder.Prompts.choice(session, "Following trees were found:", opts, {listStyle: builder.ListStyle.button})
    },
    function(session, results) {
        let answer = results.response.entity;
        console.log("Tree selected:", answer);
        let id;
        treeNames.forEach(function(t) {
            if(t.name === answer && !id) {
                id = t.id;
            }
        })
        console.log("Starting session...");
        esc.escStartSession(esc_header, id, esc_attribs)
        .then(function(result) {
            esc_session = result;
            for(i in esc_session.concepts) {
                for(j in esc_session.concepts[i]) {
                    esc_attribs[i] = j;
                }
            }
            console.log("Started session.");
            session.beginDialog(esc_session.questions[0].nodeType,[esc_session.sessionId, esc_session.questions[0]]);
        })
        .catch(function(err) {
            console.log("Error starting ESC session.");
            console.log(err);
        })
    }
]).beginDialogAction("cancelTree", "cancel", {matches: /^cancel$|^end$|^stop$|^halt/i});

//This dialog is for selection answers on a question node. It also saves recognized concepts within the answer.
bot.dialog("select", [
    function(session, args) {
        console.log("Select");
        message = args[1].text;

        attach(args[1].memo["Memo_URL"]);
        session.userData = args;
        var opts = new Array();
        switch(args[1].valueType) {
            case "string":
                for(var i = 0; i < args[1].answerValues.length; i++) {
                    opts[i] = args[1].answerValues[i].value;
                }
                builder.Prompts.choice(session, message, opts, {listStyle: builder.ListStyle.button, maxRetries: 0});
                break;
            case "number":
                for(var i = 0; i < args[1].answerIntervals.length; i++) {
                    opts[i] = args[1].answerIntervals[i].value;
                }
                builder.Prompts.number(session, message, {integerOnly: true, maxValue: 100, minValue: 0});
                break;
            case "datetime":
                for(var i = 0; i < args[1].answerIntervals.length; i++) {
                   opts[i] = args[1].answerIntervals[i].value;
                }
                builder.Prompts.time(session, message + "\nPlease enter a date in format 'yyyy-mm-dd'.");
                break;
        }
    },
    function(session, results) {
        let args = session.userData;
        let answer;
        //An answer was given.
        if(results.response != null && results.response.entity != null) {
            answer = results.response.entity;
        } else if (results.response != null) {
            answer = results.response;
        } else {
            //No answer (to a choice prompt) was given, check if concepts were recognized and try again.
        }
        esc.escAnnotateQuery(esc_header, session.message.text)
        .then(function(result) {
            for(i in result.concepts) {
                for(j in result.concepts[i]) {
                    esc_attribs[i] = j;
                }
            }
            console.log("Proceeding tree with answer %s", answer);
            esc.escProceedTree(esc_header, args[0], args[1].nodeId, args[1].treeId, answer, esc_attribs)
            .then(function(result) {
                if(result.questions[0].nodeType === "error") {
                    //If no concept answers the question, ask again.
                    session.send("No answer was given.")
                    session.beginDialog("select", [esc_session.sessionId, esc_session.questions[0]])
                } else {
                    esc_session = result;
                    console.log("Initiating new Dialog %s", esc_session.questions[0].nodeType);
                    //the nodeType is either "select", "info" or "solution"
                    session.beginDialog(esc_session.questions[0].nodeType, [esc_session.sessionId, esc_session.questions[0]])
                }
            })
            .catch(function(err) {
                console.log("Error proceeding tree.");
                console.log(err);
            });
        })
    }
]).beginDialogAction("cancelSelect", "cancel", {matches: /^abbrechen$|^beenden$|^stop$|^halt/i});

//This dialog is for showing hint nodes. It then automatically proceeds the tree.
bot.dialog("info", [
    function(session, args) {
        console.log("Info");
        message = args[1].text;

        attach(args[1].memo["Memo_URL"]);

        session.send(message);
        console.log("Proceeding tree without answer.");
        esc.escProceedTree(esc_header, args[0], args[1].nodeId, args[1].treeId, "OK", esc_attribs)
        .then(function(result) {
            esc_session = result;
            console.log("Initiating new Dialog %s", esc_session.questions[0].nodeType);
            session.beginDialog(esc_session.questions[0].nodeType, [esc_session.sessionId, esc_session.questions[0]]);
        })
        .catch(function(err) {
            console.log("Error proceeding tree.");
            console.log(err);
        });
    }
])

//This dialog displays the reached solution. It then ends the dialog, erasing the concepts of this session.
bot.dialog("solution", [
    function(session, args) {
        console.log("Solution");
        message = args[1].text;

        attach(args[1].memo["Memo_URL"]);

        session.send(message);
        esc.escFinishSession(esc_header, args[0])
        .then(function(result) {
            console.log("Finished Session " + args[0]);
            esc_session = undefined;
        })
        .catch(function(err) {
            console.log("Error finishing session.");
            console.log(err);
        })      

        console.log("Ending dialog.");
        session.endDialog("I hope i could help you.");
    }
])


/*-----------
-- Manners --
-----------*/

// Greetings
bot.on("conversationUpdate", function (message) {
    if (message.membersAdded && message.membersAdded.length > 0) {
        // Don"t greet yourself
        if(message.membersAdded[0].id !== message.address.bot.id) {
            // Say hello
            var reply = new builder.Message()
                .address(message.address)
                .text("Welcome to the Chatbot. Please enter your search.");
            bot.send(reply);
        }

    } else if (message.membersRemoved) {
        // Say goodbye
        var reply = new builder.Message()
            .address(message.address)
            .text("Goodbye.");
        bot.send(reply);
    }
});


/*---------------------
-- Utility functions --
---------------------*/

//function for attached images
var attach = function(p) {
    if(typeof p != undefined && p != null) {
        console.log("Found attachment: %s", p);
        session.send({
            attachments:[{
                contentUrl: p,
                contentType: "image/jpeg"
            }]
        })
    }
}

var undoMutation = function(s) {
    while(s.indexOf("ä") !== -1) {
        s = s.replace("ä", "ae");
    }
    while(s.indexOf("ö") !== -1) {
        s = s.replace("ö", "oe");
    }
    while(s.indexOf("ü") !== -1) {
        s = s.replace("ü", "ue");
    }
    return s;
}

可能是嵌套字符串提示对话框的问题。目前我只找到一些解决方法来快速解决问题。

  1. 最快:扩大提示对话框的距离,如果Bot输入无效,会重新提示问题。

    builder.Prompts.choice(session, args[1].text, args[1].answerValues, {
          listStyle: builder.ListStyle.button,
          maxRetries: 2
        });
    
  2. 正如我所见,您会将 treeNote 传递给 select 对话框,因此您可以尝试使用 replaceDialog() 而不是 beginDialog() 来清除嵌套对话框堆栈。在 cancel 对话框中,也将 endDialog() 替换为 replaceDialog()