机器人框架破坏对话状态

16

我目前正在使用微软的Bot Framework制作聊天机器人。在我的流程中,我有一个最终对话框,告诉用户他们正在参与比赛。还有一个未知输入的错误处理方法。这两种方法如下所示:

[Serializable]
public class ConcertCityDialog : AbstractBasicDialog<DialogResult>
{
    private static FacebookService FacebookService => new FacebookService(new FacebookClient());

    [LuisIntent("ConcertCity")]
    public async Task ConcertCityIntent(IDialogContext context, LuisResult result)
    {
        var fbAccount = await FacebookService.GetAccountAsync(context.Activity.From.Id);

        var selectedCityName = result.Entities.FirstOrDefault()?.Entity;

        concert_city selectedCity;
        using (var concertCityService = new ConcertCityService())
        {
            selectedCity = concertCityService.FindConcertCity(selectedCityName);
        }

        if (selectedCity == null)
        {
            await NoneIntent(context, result);
            return;
        }

        user_interaction latestInteraction;
        using (var userService = new MessengerUserService())
        {
            var user = userService.FindByFacebookIdIncludeInteractions(context.Activity.From.Id);
            latestInteraction = user.user_interaction.MaxBy(e => e.created_at);
        }

        latestInteraction.preferred_city_id = selectedCity.id;
        latestInteraction.gif_created = true;

        using (var userInteractionService = new UserInteractionService())
        {
            userInteractionService.UpdateUserInteraction(latestInteraction);
        }

        var shareIntroReply = context.MakeMessage();
        shareIntroReply.Text = "Great choice! You are now participating in the competition. If you dare then pass your message \uD83D\uDE0E";

        await context.PostAsync(shareIntroReply);

        var reply = await MessageUtility.MakeShareMessageCard(context, fbAccount, latestInteraction, false);

        await context.PostAsync(reply);

        context.Done(DialogResult.Done);
    }

    [LuisIntent("")]
    [LuisIntent("None")]
    public async Task NoneIntent(IDialogContext context, LuisResult result)
    {
        messenger_user user;
        using (var userService = new MessengerUserService())
        {
            user = userService.FindByFacebookId(context.Activity.From.Id);
        }

        var phrase = CreateMisunderstoodPhrase(user, result.Query);

        using (var misunderstoodPhraseService = new MisunderstoodPhraseService())
        {
            misunderstoodPhraseService.CreatePhrase(phrase);
        }

        List<concert_city> concertCities;
        using (var concertCityService = new ConcertCityService())
        {
            concertCities = concertCityService.GetUpcomingConcertCities().ToList();
        }

        // Prompt city
        var reply = context.MakeMessage();
        reply.Text = "I'm not sure what you mean \uD83E\uDD14<br/>Which Grøn Koncert would you like to attend?";

        reply.SuggestedActions = new SuggestedActions
        {
            Actions = concertCities.Select(e => MessageUtility.MakeQuickAnswer(e.name)).ToList()
        };

        await context.PostAsync(reply);

        context.Wait(MessageReceived);
    }

    protected override void OnDeserializedCustom(StreamingContext context)
    {
    }
}

以下是 AbstractBasicDialog 的实现:

[Serializable]
public abstract class AbstractBasicDialog<T> : LuisDialog<T>
{
    protected AbstractBasicDialog() : base(new LuisService(new LuisModelAttribute(
        ConfigurationManager.AppSettings["LuisAppId"],
        ConfigurationManager.AppSettings["LuisAPIKey"],
        domain: ConfigurationManager.AppSettings["LuisAPIHostName"])))
    {
    }

    [LuisIntent("Cancel")]
    public virtual async Task CancelIntent(IDialogContext context, LuisResult result)
    {
        var randomQuotes = new List<string>
        {
            "If you say so, I'll leave you alone for now",
            "alright then, I'll leave you alone",
            "Okay then, I won't bother you anymore"
        };

        await context.PostAsync(MessageUtility.RandAnswer(randomQuotes));

        context.Done(DialogResult.Cancel);
    }

    [LuisIntent("Start")]
    public virtual async Task StartIntent(IDialogContext context, LuisResult result)
    {
        context.Done(DialogResult.Restart);
    }

    [LuisIntent("CustomerSupport")]
    public async Task CustomerSupportIntent(IDialogContext context, LuisResult result)
    {
        using (var userService = new MessengerUserService())
        {
            var user = userService.FindByFacebookId(context.Activity.From.Id);
            if (user != null)
            {
                user.receiving_support = true;
                userService.UpdateUser(user);
            }
        }

        await context.PostAsync("I'll let customer service know, that you want to talk to them. They will get back to you within 24 hours.<br/>If at any time you want to return to me, and start passing a message, just type \"Stop customer support\".");

        context.Call(new CustomerSupportDialog(), ResumeAfterCustomerSupport);
    }

    private async Task ResumeAfterCustomerSupport(IDialogContext context, IAwaitable<DialogResult> result)
    {
        context.Done(await result);
    }

    protected misunderstood_phrase CreateMisunderstoodPhrase(messenger_user user, string phrase)
    {
        return new misunderstood_phrase
        {
            phrase = phrase,
            dialog = GetType().Name,
            messenger_user_id = user.id
        };
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        OnDeserializedCustom(context);
    }

    protected abstract void OnDeserializedCustom(StreamingContext context);
}

调用链始于此对话框:

[Serializable]
public class BasicLuisDialog : LuisDialog<DialogResult>
{
    private static FacebookService FacebookService => new FacebookService(new FacebookClient());

    public BasicLuisDialog() : base(new LuisService(new LuisModelAttribute(
        ConfigurationManager.AppSettings["LuisAppId"],
        ConfigurationManager.AppSettings["LuisAPIKey"],
        domain: ConfigurationManager.AppSettings["LuisAPIHostName"])))
    {
    }

    [LuisIntent("")]
    [LuisIntent("None")]
    public async Task NoneIntent(IDialogContext context, LuisResult result)
    {
        var facebookAccount = await FacebookService.GetAccountAsync(context.Activity.From.Id);

        RegisterUser(facebookAccount, null, out var user);

        var phrase = CreateMisunderstoodPhrase(user, result.Query);
        using (var misunderstoodPhraseService = new MisunderstoodPhraseService())
        {
            misunderstoodPhraseService.CreatePhrase(phrase);
        }

        var reply = context.MakeMessage();
        reply.SuggestedActions = new SuggestedActions
        {
            Actions = new List<CardAction>
            {
                new CardAction { Title = "Get started", Type = ActionTypes.ImBack, Value = "Get started" },
                new CardAction { Title = "Customer support", Type = ActionTypes.ImBack, Value = "Customer support" }
            }
        };

        var name = string.IsNullOrEmpty(facebookAccount.FirstName) ? "" : $"{facebookAccount.FirstName} ";
        reply.Text = $"Hm, I'm not sure what you mean {name} \uD83E\uDD14 Here are some ways you can interact with me:";

        await context.PostAsync(reply);
        context.Wait(MessageReceived);
    }

    [LuisIntent("Greeting")]
    [LuisIntent("Positive")]
    [LuisIntent("Start")]
    public async Task GreetingIntent(IDialogContext context, LuisResult result)
    {
        var rnd = new Random();
        var facebookAccount = await FacebookService.GetAccountAsync(context.Activity.From.Id);

        // Initial Greeting
        var greetings = new List<string>
        {
            "Well hello there",
            "Hi there"
        };

        if (!string.IsNullOrEmpty(facebookAccount.FirstName))
        {
            greetings.Add("Hi {0}");
            greetings.Add("Hello {0}");
            greetings.Add("Welcome {0}");
        }

        if (facebookAccount.Gender == "male")
            greetings.Add("Hey handsome");
        else if (facebookAccount.Gender == "female")
            greetings.Add("Hi gorgeous");

        var randIndex = rnd.Next(greetings.Count);

        var greeting = string.Format(greetings[randIndex], facebookAccount.FirstName);

        await context.PostAsync(greeting);

        await MessageUtility.StartTyping(context, 300);

        country country;
        using (var countryService = new CountryService())
        {
            country = countryService.FindCountry(facebookAccount.Locale);
        }
        var userHasCountry = RegisterUser(facebookAccount, country, out var user);

        // If user contry not found prompt for answer
        if (!userHasCountry)
        {
            var countryReply = context.MakeMessage();
            countryReply.Text = "You are hard to keep track of - where are you from?";
            countryReply.SuggestedActions = new SuggestedActions
            {
                Actions = new List<CardAction>
                {
                    MessageUtility.MakeQuickAnswer("Denmark"),
                    MessageUtility.MakeQuickAnswer("Norway"),
                    MessageUtility.MakeQuickAnswer("Sweden"),
                    MessageUtility.MakeQuickAnswer("Other")
                }
            };

            await context.PostAsync(countryReply);

            context.Call(new CountryDialog(), AfterCountryDialog);
        }
        else
        {
            await FunPrompt(context, country);
        }
    }

    private async Task AfterCountryDialog(IDialogContext countryContext, IAwaitable<country> countryAwaitable)
    {
        var country = await countryAwaitable;

        var facebookAccount = await FacebookService.GetAccountAsync(countryContext.Activity.From.Id);

        using (var userService = new MessengerUserService())
        {
            var user = userService.FindByFacebookId(facebookAccount.Id);

            user.country = country;
            userService.UpdateUser(user);
        }

        var reply = countryContext.MakeMessage();
        reply.Text = "That's cool \uD83D\uDE0E";

        await countryContext.PostAsync(reply);

        await MessageUtility.StartTyping(countryContext, 350);

        await FunPrompt(countryContext, country);
    }

    private async Task FunPrompt(IDialogContext context, country country)
    {
        if (country?.name == "norway" && DateTime.Now < new DateTime(2018, 8, 13))
        {
            var reply = context.MakeMessage();
            reply.Text = "Unfortunately the competition isn't open in Norway yet. You can still talk to customer support if you want to";
            reply.SuggestedActions = new SuggestedActions
            {
                Actions = new List<CardAction>
                {
                    MessageUtility.MakeQuickAnswer("Customer support")
                }
            };

            await context.PostAsync(reply);

            context.Wait(MessageReceived);
        }
        else if ((country?.name == "denmark" && DateTime.Now >= new DateTime(2018, 7, 29)) ||
                 (country?.name == "norway" && DateTime.Now >= new DateTime(2018, 10, 21)))
        {
            var reply = context.MakeMessage();
            reply.Text = "The competition has ended. You can still talk to customer support if you want to";
            reply.SuggestedActions = new SuggestedActions
            {
                Actions = new List<CardAction>
                {
                    MessageUtility.MakeQuickAnswer("Customer support")
                }
            };

            await context.PostAsync(reply);

            context.Wait(MessageReceived);
        }
        else
        {
            await context.PostAsync("Are you up for some fun?");

            context.Call(new IntroductionDialog(), ResumeAfterDialog);
        }
    }

    [LuisIntent("CustomerSupport")]
    public async Task CustomerSupportIntent(IDialogContext context, LuisResult result)
    {
        using (var userService = new MessengerUserService())
        {
            var user = userService.FindByFacebookId(context.Activity.From.Id);
            if (user != null)
            {
                user.receiving_support = true;
                userService.UpdateUser(user);
            }
        }

        await context.PostAsync("I'll let customer support know, that you want to talk to them. They should be messaging you shortly.<br/>You can end your conversation with customer support at any time by typing \"Stop customer support\".");

        context.Call(new CustomerSupportDialog(), ResumeAfterDialog);
    }

    private async Task ResumeAfterDialog(IDialogContext context, IAwaitable<DialogResult> result)
    {
        var resultState = await result;
        if (resultState == DialogResult.Restart)
            await GreetingIntent(context, null);
        else if (resultState == DialogResult.CustomerSupport)
            await ResumeAfterCustomerSupport(context);
        else if (resultState == DialogResult.Done || resultState == DialogResult.Cancel)
            context.Done(resultState);
        else
            context.Wait(MessageReceived);
    }

    private async Task ResumeAfterCustomerSupport(IDialogContext context)
    {
        using (var userService = new MessengerUserService())
        {
            var user = userService.FindByFacebookId(context.Activity.From.Id);
            if (user != null)
            {
                user.receiving_support = false;
                userService.UpdateUser(user);
            }
        }

        await context.PostAsync("I hope you got the help you needed. Would you like to pass a message to a friend?");

        context.Call(new IntroductionDialog(), ResumeAfterDialog);
    }

    private bool RegisterUser(FacebookAccount fbAccount, country country, out messenger_user user)
    {
        if (string.IsNullOrEmpty(fbAccount?.Id))
        {
            user = null;
            return false;
        }

        using (var userService = new MessengerUserService())
        {
            user = userService.FindByFacebookId(fbAccount.Id);

            if (user != null)
                return user.country != null;

            user = new messenger_user
            {
                id = fbAccount.Id,
                country = country
            };

            userService.CreateUser(user);

            return user.country != null;
        }
    }

    protected misunderstood_phrase CreateMisunderstoodPhrase(messenger_user user, string phrase)
    {
        return new misunderstood_phrase
        {
            phrase = phrase,
            dialog = GetType().Name,
            messenger_user_id = user.id
        };
    }
}

这通常是有效的。用户被告知他们的注册成功并且流程通过context.Done()调用退出。然而,有时候聊天机器人没有将对话注册为已退出,就像在这里看到的那样:

enter image description here

如您所见,即使我调用了Done()方法,聊天机器人仍然处于同一个对话中。这是我的聊天机器人普遍存在的问题,因为在所有对话框中有时都会发生这种情况。
您有任何关于可能出错的输入吗?
编辑: 在调试时,我每次调用context.Call都添加了断点。当我的问题出现时,它停止命中这些断点。这可能是某个DI的副作用吗?这是我的DI代码:
Conversation.UpdateContainer(builder =>
{
    builder.RegisterModule(new DialogModule());
    builder.RegisterModule(new ReflectionSurrogateModule());
    builder.RegisterModule(new DialogModule_MakeRoot());
    builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));

    var store = new TableBotDataStore(ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString);

    builder.Register(c => store)
        .Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
        .AsSelf()
        .SingleInstance();

    builder.Register(c => new CachingBotDataStore(store,
            CachingBotDataStoreConsistencyPolicy
                .ETagBasedConsistency))
        .As<IBotDataStore<BotData>>()
        .AsSelf()
        .InstancePerLifetimeScope();

    builder.RegisterType<BasicLuisDialog>().As<LuisDialog<DialogResult>>().InstancePerDependency();
});

请添加您的父实现(调用对话框),因为在这里我们无法确定它是否真的是一个错误。 - Nicolas R
@NicolasR 我已经添加了完整的实现。 - Frederik Hansen
我认为调用你的逻辑的主要对话框丢失了,因此在 context.Done 之后我们仍然看不到你正在做什么。 - Nicolas R
@NicolasR 我已经添加了主要的对话框。这是启动链的初始对话框。链中的所有其他对话框都会回调“Done()”方法到主对话框。 - Frederik Hansen
@NicolasR 我已经添加了更多关于我的问题以及问题发生时会发生什么的信息。 - Frederik Hansen
3个回答

7
我想我终于找到了问题所在。在我的代码中,我实现了一个静态类中的帮助方法来发送一个打字响应并等待一定时间。由于上下文被传递到这个静态方法中,似乎这会引起一些问题。
将该方法更改为LuisDialog的扩展方法后,我不再有此问题。
如果有人能够详细说明可能出现的问题,我将不胜感激。
编辑:疑问中的方法:
public static async Task StartTyping(IDialogContext context, int sleep)
{
    var typingMsg = context.MakeMessage();
    typingMsg.Type = ActivityTypes.Typing;

    await context.PostAsync(typingMsg);
    await Task.Delay(sleep);
}

4
我也遇到了同样的问题,这个方法似乎也解决了我的问题,不过我不知道为什么。 - Péter Bozsó

2
我曾经遇到过一个非常类似的问题,当我将消息发送的输入移动到基类中(就像Frederik所做的那样),从静态辅助类中高度减少了出现问题的次数,但最终解决方案是这样的:https://github.com/Microsoft/BotBuilder/issues/4477 简而言之,我不得不将与机器人相关的NuGet包(Microsoft.Bot.Builder、Microsoft.Bot.Builder.History和Microsoft.Bot.Connector)降级到3.13.1版本,问题就消失了。

1

由于在[LuisIntent("ConcertCity")]中使用了context.Done(),所以当前对话已经从堆栈中退出。这就是为什么下一条消息将由先前的对话或消息控制器处理,其中调用了'None'意图并获得此响应。

reply.Text = "I'm not sure what you mean \uD83E\uDD14<br/>Which Grøn Koncert would you like to attend?";

您不应该在各个地方都使用context.Done(),这只有在必须返回到堆栈中的上一个对话框时才应调用。


但问题在于,在这个例子中,对话框没有从堆栈中移除。下一条消息会在同一个对话框中触发[LuisIntent("")],即使我已经调用了context.Done() - Frederik Hansen
1
在你的代码中,不清楚你是在哪里调用了ConcertCityDialog。我问这个的原因是当你执行context.Done<object>(null)时,它会返回给调用对话框。但是我在这里看不到实现。 此外,在仿真器详细信息中,我可以看到一个空的回复消息,然后是来自“None”意图的回复。 - Harsh Raj
ConcertCityDialog是从另一个对话框中调用的。总体上,它是大约8个对话框的链。这些对话框都通过“context.Done(await result)”将结果传回根“BasicLuisDialog”。通过调试,我可以看到调用确实带着正确的结果回到那里。此外,我的问题发生在随机对话框中,而不一定是“ConcertCityDialog”。即使是“BasicLuisDialog”有时也会出现这种情况。 - Frederik Hansen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接