[Microsoft Bot Framework] Dialog를 사용하여 대화의 흐름 만들기

Microsoft Bot Frame의 기본 개발 방법은 아래 시리즈에 자세히 설명되어 있다.

Microsoft Bot Framework 시작해보기 (1) Microsoft Bot Framework 시작해보기 (2) Hello Bot Framework Microsoft Bot Framework 시작해보기 (3) Bot Application 배포 Microsoft Bot Framework 시작해보기 (4) Bot 등록과 스카이프에서 테스트 하기 [Microsoft Bot Framework] Direct Line REST API 3.0 [Microsoft Bot Framework] Dialog를 사용하여 대화의 흐름 만들기 [Microsoft Bot Framework] 상태저장을 위한 Bot State Service 핀켓 CS ChatBot 만들기 HackFest 사례

대화에는 맥락이라는 것이 있다.  어떤 주제에 대해서 대화들은 같은 맥락 안에서 대화가 이어진다. Bot 을 만들 때도 같은 맥락 안에서 사용자와 Bot간의 대화가 이어져 나가는 방식으로 구현이 된다. 피자를 주문하는 Bot은 피자 주문이라는 맥락에서 크기, 토핑 종류, 음료 등을 물어보고 답을 할 것이다. 

Microsoft Bot Framework에서는 Bot과 사용자 사이의 대화를 만드는 기본 모델이 Dialog 이다. Dialog를 통해서 반복되는 특정 대화를 모듈화해서 재사용성을 높이고 여러 Dialog를 체인 형식으로 연결시켜서 맥락을 이어가도록 프로그래밍 할 수 있다.  Dialogs 에 대한 공식 문서는 docs.botframework.com 에 있다.

Bot framework builder에 이미 만들어져 있는 Dialog들이 있다. 이 Dialog들과 내가 만든 Dialog를 연결하면 좋은 구조를 만들 수 있다.

Custom Dialog 만들기

StartAsync라는 메서드가 정의되어 있는 IDialog를 구현하면 된다.

 public async Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);
        }

        public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
        {
            using (CommentService service = new CommentService(new KetBotContext()))
            {
                var activity = await argument;

                // get state 
                KetBotState state = null;
                context.ConversationData.TryGetValue("KetBotState", out state);

                var cat3 = await service.GetFormsAsync(state.Stage1Selection);

                bool checkflag = false;
                int selected;
                if (int.TryParse(activity.Text, out selected) && selected > 0 && selected <= cat3.Count)
                {
                    checkflag = true;
                }

                if (checkflag == true)
                {
                    // save Stage1 selection
                    state.Stage2Selection = activity.Text;
                    // save state
                    context.ConversationData.SetValue("KetBotState", state);

                    // final answers 
                    var answers = await service.GetAnswerAsync(state.Stage0Selection + state.Stage1Selection + state.Stage2Selection);
                    await context.PostAsync(string.Join("\n", answers.ToArray()));

                    var q = await service.GetCommentAsync("RCB02");
                    List<string> yesno = new List<string>() { "네, 맞아요!", "아닌데요?" };

                    PromptDialog.Choice(context, AfterChoiceAsync, yesno, q, promptStyle: PromptStyle.Keyboard);


                }
                else
                {
                    // Go back to stage 2
                    await context.PostAsync(await service.GetCommentAsync("REB01"));
                    await context.PostAsync(string.Join("\n", cat3.ToArray()));
                    context.Wait(MessageReceivedAsync);
                }
            }
        }

Dialog 안에서는 PINKET Bot의 Stage3Dialog.cs 코드처럼

  • context.PostAsync()로 사용자에게 메시지를 즉시 전송하거나
  • context.Wait(MessageReceivedAsync)로 사용자로 부터 메시지를 받을 때 까지 기다리거나
  • PromptDialog를 통해서 형식화된 메시지를 주고 받거나
  • context.Done("some text)를 호출하면 체인으로 연결된 다음 Dialog가 Active 된다. 

Dialog Chain

Dialog들을 체인으로 연결시켜서 또 다른 Dialog를 만들 수 있다. 내부적으로 DialogStack에 순서를 관리하며 연결을 위한 다양한 Chain Method를 Fluent API로 지원한다.

Dialog Chain 샘플코드

EchoChainBot의 코드를 살펴보면

 public static readonly IDialog<string> dialog = Chain.PostToChain()
            .Select(msg => msg.Text)
            .Switch(
                new Case<string, IDialog<string>>(text =>
                    {
                        var regex = new Regex("^reset");
                        return regex.Match(text).Success;
                    }, (context, txt) =>
                    {
                        return Chain.From(() => new PromptDialog.PromptConfirm("Are you sure you want to reset the count?",
                        "Didn't get that!", 3, PromptStyle.Keyboard)).ContinueWith<bool, string>(async (ctx, res) =>
                        {
                            string reply;
                            if (await res)
                            {
                                ctx.UserData.SetValue("count", 0);
                                reply = "Reset count.";
                            }
                            else
                            {
                                reply = "Did not reset count.";
                            }
                            return Chain.Return(reply);
                        });
                    }),
                new RegexCase<IDialog<string>>(new Regex("^help", RegexOptions.IgnoreCase), (context, txt) =>
                    {
                        return Chain.Return("I am a simple echo dialog with a counter! Reset my counter by typing \"reset\"!");
                    }),
                new DefaultCase<string, IDialog<string>>((context, txt) =>
                    {
                        int count;
                        context.UserData.TryGetValue("count", out count);
                        context.UserData.SetValue("count", ++count);
                        string reply = string.Format("{0}: You said {1}", count, txt);
                        return Chain.Return(reply);
                    }))
            .Unwrap()
            .PostToUser();
  • Chain.PostToChain() 로 사용자의 메시지로 부터 Chain이 시작되고
  • Select(msg => msg.Text) DialogContext에서 Message 부분만 Select 하고
  • .Switch(new Case<string, IDialog<string>> ...) 에서 메시지가 Match 되면 Callback 메서드가 실행된다.
  • Switch 에서  RegexCase<IDialog<string>>() 정규표현식을 사용할 수도 있고
  • 매칭이 없으면 DefaultCase가 실행된다.
  • 마지막으로 Unwarp()과 PostToUser()로 체인을 끝낸다.

이런식으로 Dialog를 연결해서 한 묶음의 대화를 완성한 예제가 PINKET Bot 이다. Dialog를 체인으로 엮어서 대화를 만드는 부분을 참조해 볼만하다.