티스토리 뷰

Bot Framework

Bot Framework V3 - Dialogs

kaki104 2016. 7. 22. 22:56
반응형

30도가 넘는 불타는 금요일 저녁 봇과의 전쟁을 시작하려고 합니다. 이전 V1 버전 포스팅을 했을 때 다루지 않았던 LUIS를 좀더 심층적으로 다루어 보려고 합니다. 비록 아직 한글지원은 요원한 일이지만, 어떻게든 짧은 영어로 비벼 보겠습니다.

 

 

0. 관련 페이지

https://docs.botframework.com/en-us/csharp/builder/sdkreference/dialogs.html#Overview

 

다이얼로그 관련 페이지로, 중간에 핵심 내용 중 하나는 LUIS를 이용해서 자언어를 이해하고 그에 맞는 결과를 반화는 것입니다.

 

Language Understanding Intelligent Service(LUIS)

https://docs.botframework.com/en-us/node/builder/guides/understanding-natural-language/

 

이 곳에 내용이 오늘의 핵심 내용입니다.

 

 

1. LUIS

 

마이크로소프트의 LUIS는 어플리케이션이 빠르고 효과적으로 언어를 이해할 수 있도록 지원하는 서비스 입니다. LUIS를 이용하기 위해서는 모델이 필요한데, 이미 Bind이나 Cortana에서 만들어 놓은 모델 중 당신의 목적에 맞는 모델을 사용해서 빠르게 만들 수 있습니다.

 

LUIS는 대화형 개념 학습(PICL) 서비스이며, 마이크로소프트 리서치의 플랫폼을 기반으로 하며, 마이크로소프트 프로젝트 옥스포드의 일부분이기도 합니다.

 

Bot Builder를 사용하면 LuisDialog 클래스를 통해 서비스에 자동으로 접근 할 수 있으며, 자연 언어 이해를 통한 처리를 bot에 추가할 수 있습니다. 당신은 공개 언어 모델을 참조하는 LuisDialog의 인스턴스를 추가하고, 사용자의 대화에 응답하는 처리기를 만들 수 있습니다.

 

LUIS Tutorial & Pre-built application v2

https://www.luis.ai/Help/Index#PreBuiltApp

 

비디오와 설명 자료와 이미 만들어져있는 LUIS 모델을 사용할 수 있도록 해주는 참고 문서 입니다. 이 참고 문서를 이용해서 Alarm Bot을 만들 수 있습니다.

 

 

2. Intents, Entities, and Model Training (의지, 실재, 모델 학습)

 

인간과 컴퓨터간 상호 작용에서 중요한 문제 중 하나는 사용자가 원하는 것을 이해하고, 사용자의 의도에 맞는 정보를 찾을 수 있느냐 하는 것입니다. LUIS는 응용 프로그램과 관련된 객체의 집합을 쉽고 빠르게 디자인을 할 수 있어서 원하는 정보를 찾고, 사용하는데 용이 합니다.

 

또한, 당신의 어플리케이션이 배포되어 데이터가 발생하여 시스템에 흐르게 되면, LUIS를 이용해서 학습을 진행하여 더 좋은 결과를 만들 수도 있습니다. 학습 진행 과정에서 LUIS는 비교적 확실한 상호 작용을 식별하여 intent와 entities를 구분하도록 당신에게 요청을 할 수 있습니다. LUIS는 불안정한 요소를 파악해서 당신에게 시스템의 성능을 향상 시킬 수 있는 방법을 제공하고 도움을 줄 것입니다. 그리고, 중요한 경우에 대해 초점을 맞춤으로써, LUIS는 가능한 빨리 학습하여 당신의 소중한 시간을 낭비하지 않을 것 입니다.

 

 

3. Create Your Model

 

당신의 bot에 자언 언어 지원을 추가하는 첫번째 단계는 LUIS Model을 생성하는 것 입니다. LUIS에 로그인을 하여 Bot을 위한 새로운 LUIS 응용프로그램을 만들면 됩니다.

 

https://www.luis.ai/

 

사이트에 가입하기 위해서는 Microsoft Account가 필요하며, 현재 LUIS는 beta 버전으로 사용료는 무료입니다.

 

 

로그인을 하고 들어가면 아래와 비슷한 화면이 나옵니다. 저는 이전에 MSHealthBot이라는 어플리케이션을 만들어 놓은 것이 있어서 해당 내용이 화면에 표시 됩니다.

 

 

처음 사용하기 위해서는 응용프로그램을 만든 후 LUIS가 당신의 bot 모델을 훈련하는데 사용할 Intents와 Entities를 추가 한 후에 문장을 입력해서 학습을 시킬 수 있습니다. 하지만, 시간이 오래 걸릴 수 있겠죠? 그래서 미리 만들어진  모델을 사용하는 하나의 추가 적인 옵션이 있습니다. 이는 튜토리얼을 위해 만들어진 것으로 Cortana pre-built apps를 클릭하면 5개의 언어에 대한 지원을 선택하 실 수 있습니다. 영어, 중국어, 프랑스어, 스페인어, 이탈리어 입니다.

 

화면에 출력되는 팝업에 표시된 URL에 포함된 id와 subscription-key를 복사해서 LuisDialog class에 사용하실 수 있습니다. 또한 URL은 당신이 만든 LUIS bot app을 가리 키는 것입니다. 그래서, 당신은 이 기본 LUIS 모델을 훈련시켜서 원하는 방향으로 성장시켜 사용할 수 있습니다.

 

이번에는 기존에 제공되는 모델을 이용해서 만들어 본 후 좀더 익숙해지면 진짜 LUIS 모델을 만들어서 이용해 보도록 하겠습니다.

 

 

 

 

URL을 복사해서 보관한 후 Alarm Bot 만들기로 이동합니다.(Dialog의 중간..)

 

 

4. Alarm Bot

 

프로젝트 탐색기에서 클래스를 추가하고 이름을 SimpleAlarmDialog라고 입력합니다. 아래 내용을 입력합니다.

이렇게 만들어진 클래스는 자체적으로 LUIS 서비스와 통신을 수행 합니다.

 

    [LuisModel("c413b2ef-382c-45bd-8ff0-f76d60e2a821", "6d0966209c6e4f6b835ce34492f3e6d9")]
    [Serializable]
    public class SimpleAlarmDialog : LuisDialog<object>
    {
    }

 

이제 내부에 메소드를 하나씩 만들어 나갑니다. 사용자가 입력한 내용이 LUIS의 intent로 구분이 가능하다면, LuisIntent라는 속성으로 연결된 메소드가 실행이 됩니다.

 

        [LuisIntent("builtin.intent.alarm.turn_off_alarm")]
        public async Task TurnOffAlarm(IDialogContext context, LuisResult result)
        {
            if (TryFindAlarm(result, out this.turnOff))
            {
                PromptDialog.Confirm(context, AfterConfirming_TurnOffAlarm, "Are you sure?",
                    promptStyle: PromptStyle.None);
            }
            else
            {
                await context.PostAsync("did not find alarm");
                context.Wait(MessageReceived);
            }
        }

 

위의 예를 보면 사용자가 "turn off my 7am alarm", "turn off my wake up alarm"이라고 입력을 하게 되면, 위의 메소드가 실행되는 구조 입니다. TryFindAlarm은 내부 메소드를 호출하는 곳으로 삭제할 알람을 찾는 부분이

 

고 찾게되면 정말 삭제할 것인지를 물어보는 sub-dialog를 이용해서 사용자 확인을 받고, 확인이 되면 AfterConfirming_TurnOffAlarm이라는 이름의 내부 메소드를 호출해서 최종적으로 처리를 하게 됩니다.

 

전체 소스를 확인합니다.

 

    [LuisModel("c413b2ef-382c-45bd-8ff0-f76d60e2a821", "6d0966209c6e4f6b835ce34492f3e6d9")]
    [Serializable]
    public class SimpleAlarmDialog : LuisDialog<object>

    {
        /// <summary>
        ///     기본 알람 이름
        /// </summary>
        public const string DefaultAlarmWhat = "default";

        /// <summary>
        ///     entity로 알람 제목
        /// </summary>
        public const string Entity_Alarm_Title = "builtin.alarm.title";

        /// <summary>
        ///     entity 시작 시간
        /// </summary>
        public const string Entity_Alarm_Start_Time = "builtin.alarm.start_time";

        /// <summary>
        ///     entity 시작 일자
        /// </summary>
        public const string Entity_Alarm_Start_Date = "builtin.alarm.start_date";

        /// <summary>
        ///     저장된 알람 정보
        /// </summary>
        private readonly Dictionary<string, Alarm> alarmByWhat = new Dictionary<string, Alarm>();

        /// <summary>
        ///     알람끄기?
        /// </summary>
        private Alarm turnOff;

        /// <summary>
        ///     기본 생성자
        /// </summary>
        public SimpleAlarmDialog()
        {
        }

        /// <summary>
        ///     루이스 서비스 생성자
        /// </summary>
        /// <param name="service"></param>
        public SimpleAlarmDialog(ILuisService service)
            : base(service)
        {
        }

        /// <summary>
        ///     알람 찾기
        /// </summary>
        /// <param name="result"></param>
        /// <param name="alarm"></param>
        /// <returns></returns>
        public bool TryFindAlarm(LuisResult result, out Alarm alarm)
        {
            //엔티티
            EntityRecommendation title;
            //루이스 결과 중 알람 제목을 찾아보고, 있으면, 그녀석의 값(entity)를 없으면 기본 알람 이름을 what에 입력
            var what = result.TryFindEntity(Entity_Alarm_Title, out title) ? title.Entity : DefaultAlarmWhat;
            //저장되어있는 알람 정보 중에 해당 알람을 반환
            return alarmByWhat.TryGetValue(what, out alarm);
        }

        /// <summary>
        ///     아무것도 찾지 못한 경우
        /// </summary>
        /// <param name="context"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        [LuisIntent("")]
        public async Task None(IDialogContext context, LuisResult result)
        {
            var message = "Sorry I did not understand: " + string.Join(", ", result.Intents.Select(i => i.Intent));
            await context.PostAsync(message);
            context.Wait(MessageReceived);
        }

        /// <summary>
        ///     알람 삭제 delete an alarm, delete my alarm "wake up"
        /// </summary>
        /// <param name="context"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        [LuisIntent("builtin.intent.alarm.delete_alarm")]
        public async Task DeleteAlarm(IDialogContext context, LuisResult result)
        {
            Alarm alarm;

            if (TryFindAlarm(result, out alarm))
            {
                alarmByWhat.Remove(alarm.What);
                await context.PostAsync($"alarm {alarm} deleted");
            }
            else
            {
                await context.PostAsync("did not find alarm");
            }
            context.Wait(MessageReceived);
        }

        /// <summary>
        ///     알람 검색 what time is my wake-up alarm set for?, is my wake-up alarm on?
        /// </summary>
        /// <param name="context"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        [LuisIntent("builtin.intent.alarm.find_alarm")]
        public async Task FindAlarm(IDialogContext context, LuisResult result)
        {
            Alarm alarm;

            if (TryFindAlarm(result, out alarm))
            {
                await context.PostAsync($"found alarm {alarm}");
            }
            else
            {
                await context.PostAsync("did not find alarm");
            }
            context.Wait(MessageReceived);
        }

        /// <summary>
        ///     알람 설정 turn on my wake up alarm, can you set an alarm for 12 called take antibiotics?
        /// </summary>
        /// <param name="context"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        [LuisIntent("builtin.intent.alarm.set_alarm")]
        public async Task SetAlarm(IDialogContext context, LuisResult result)
        {
            EntityRecommendation title;

            //제목 찾기
            if (!result.TryFindEntity(Entity_Alarm_Title, out title))
            {
                title = new EntityRecommendation(type: Entity_Alarm_Title) {Entity = DefaultAlarmWhat};
            }
            //날짜 찾기
            EntityRecommendation date;
            if (!result.TryFindEntity(Entity_Alarm_Start_Date, out date))
            {
                date = new EntityRecommendation(type: Entity_Alarm_Start_Date) {Entity = string.Empty};
            }
            //시간 찾기
            EntityRecommendation time;
            if (!result.TryFindEntity(Entity_Alarm_Start_Time, out time))
            {
                time = new EntityRecommendation(type: Entity_Alarm_Start_Time) {Entity = string.Empty};
            }
            //파서를 이용해서 날짜와 시간을 결합
            var parser = new Parser();
            var span = parser.Parse(date.Entity + " " + time.Entity);
            //날짜와 시간 값이 있다면
            if (span != null)
            {
                //시작과 종료 확인?
                var when = span.Start ?? span.End;
                //알람 생성
                var alarm = new Alarm {What = title.Entity, When = when.Value};
                //알람 추가
                alarmByWhat[alarm.What] = alarm;

                string reply = $"alarm {alarm} created";
                await context.PostAsync(reply);
            }
            else
            {
                await context.PostAsync("could not find time for alarm");
            }

            context.Wait(MessageReceived);
        }

        /// <summary>
        ///     알람 일시 정지 snooze alarm for 5 minutes, snooze alarm
        /// </summary>
        /// <param name="context"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        [LuisIntent("builtin.intent.alarm.snooze")]
        public async Task AlarmSnooze(IDialogContext context, LuisResult result)
        {
            Alarm alarm;
            if (TryFindAlarm(result, out alarm))
            {
                //todo : 시간이 입력되었다면 해당 시간 값을 찾아서 그걸 사용하도록 수정
                //현재는 7분 후에 다시 알람이 발생하도록 되어있음
                alarm.When = alarm.When.Add(TimeSpan.FromMinutes(7));
                await context.PostAsync($"alarm {alarm} snoozed!");
            }
            else
            {
                await context.PostAsync("did not find alarm");
            }
            context.Wait(MessageReceived);
        }

        /// <summary>
        ///     알람 시간 확인 how much longer do i have until "wake-up"?, how much time until my next alarm?
        /// </summary>
        /// <param name="context"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        [LuisIntent("builtin.intent.alarm.time_remaining")]
        public async Task TimeRemaining(IDialogContext context, LuisResult result)
        {
            Alarm alarm;
            if (TryFindAlarm(result, out alarm))
            {
                var now = DateTime.UtcNow;
                //알람의 시간과 현재 시간 비교
                if (alarm.When > now)
                {
                    //알람 시간에서 현재시간을 빼고 남은 시간을 알림
                    var remaining = alarm.When.Subtract(DateTime.UtcNow);
                    await context.PostAsync($"There is {remaining} remaining for alarm {alarm}.");
                }
                else
                {
                    await context.PostAsync($"The alarm {alarm} expired already.");
                }
            }
            else
            {
                await context.PostAsync("did not find alarm");
            }

            context.Wait(MessageReceived);
        }

        /// <summary>
        ///     알람 끄기 turn off my 7am alarm, turn off my wake up alarm
        /// </summary>
        /// <param name="context"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        [LuisIntent("builtin.intent.alarm.turn_off_alarm")]
        public async Task TurnOffAlarm(IDialogContext context, LuisResult result)
        {
            if (TryFindAlarm(result, out turnOff))
            {
                PromptDialog.Confirm(context, AfterConfirming_TurnOffAlarm, $"Are you sure? {turnOff} right?",
                    promptStyle: PromptStyle.None);
            }
            else
            {
                await context.PostAsync("did not find alarm");
                context.Wait(MessageReceived);
            }
        }

        /// <summary>
        ///     알람 끄기 - 처리
        /// </summary>
        /// <param name="context"></param>
        /// <param name="confirmation"></param>
        /// <returns></returns>
        public async Task AfterConfirming_TurnOffAlarm(IDialogContext context, IAwaitable<bool> confirmation)
        {
            if (await confirmation)
            {
                alarmByWhat.Remove(turnOff.What);
                await context.PostAsync($"Ok, alarm {turnOff} disabled.");
            }
            else
            {
                await context.PostAsync("Ok! We haven't modified your alarms!");
            }
            context.Wait(MessageReceived);
        }

        /// <summary>
        ///     알람 이외.. update my 7:30 alarm to be eight o'clock, change my alarm from 8am to 9am
        /// </summary>
        /// <param name="context"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        [LuisIntent("builtin.intent.alarm.alarm_other")]
        public async Task AlarmOther(IDialogContext context, LuisResult result)
        {
            //todo : 알람 시간을 변경하는 로직을 구현
            await context.PostAsync("what ?");
            context.Wait(MessageReceived);
        }

        /// <summary>
        ///     알람 클래스
        /// </summary>
        [Serializable]
        public sealed class Alarm : IEquatable<Alarm>
        {
            /// <summary>
            ///     시간
            /// </summary>
            public DateTime When { get; set; }

            /// <summary>
            ///     뭐?
            /// </summary>
            public string What { get; set; }

            /// <summary>
            ///     다른 알람 인스턴스와 비교
            /// </summary>
            /// <param name="other"></param>
            /// <returns></returns>
            public bool Equals(Alarm other)
            {
                return other != null
                       && When == other.When
                       && What == other.What;
            }

            /// <summary>
            ///     문자열 변환
            /// </summary>
            /// <returns></returns>
            public override string ToString()
            {
                return $"[{What} at {When}]";
            }

            /// <summary>
            ///     다른 오브젝트와 비교
            /// </summary>
            /// <param name="other"></param>
            /// <returns></returns>
            public override bool Equals(object other)
            {
                return Equals(other as Alarm);
            }

            /// <summary>
            ///     해쉬코드 반환
            /// </summary>
            /// <returns></returns>
            public override int GetHashCode()
            {
                return What.GetHashCode();
            }
        }
    }

 

 

MessageController.cs 파일도 수정합니다.

 

        [ResponseType(typeof(void))]
        public virtual async Task<HttpResponseMessage> Post([FromBody] Activity activity)
        {
            if (activity != null)
            {
                // one of these will have an interface and process it
                switch (activity.GetActivityType())
                {
                    case ActivityTypes.Message:
                        await Conversation.SendAsync(activity, () => new SimpleAlarmDialog());
                        break;

                    case ActivityTypes.ConversationUpdate:
                    case ActivityTypes.ContactRelationUpdate:
                    case ActivityTypes.Typing:
                    case ActivityTypes.DeleteUserData:
                    default:
                        Trace.TraceError($"Unknown activity type ignored: {activity.GetActivityType()}");
                        break;
                }
            }
            return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted);
        }

 

 

그리고 실행해서 하나씩 살펴 보도록 하겠습니다.

 

 

5. 실행

 

 

set alarlm을 입력했더니, 무슨 소리인지 모르겠다고 하는군요.. ㅋ 그리고 set alarm이라고 입력했더니 시간을 찾을 수 없어서 등록할 수 없다고 합니다. 알람을 등록할 때는 반드시 시간을 알려줘야 합니다.

 

1) 알람 설정용 기본 문장

turn on my wake up alarm 6am, -> alarm [wake up at 7/25/2016 6:00:00 AM] created
오전 6시 알람을 wake up이란 이름으로 등록합니다.

 

can you set an alarm for 12 called take antibiotics? -> alarm [take antibiotics at 7/25/2016 12:00:00 PM] created
오후 12시에 약 먹는 시간을 take antibiotics라는 이름의 알람으로 등록합니다.

 

 

2) 등록된 알람 검색

what time is my wake up alarm set for? -> found alarm [wake up at 7/25/2016 6:00:00 AM]
wake up알람의 시간이 어떻게 되니?

 

is my wake up alarm on? -> found alarm [wake up at 7/25/2016 6:00:00 AM]

wake up 알람이 켜져있니?...인데..시간을 반환하고 있군요 흐흐.. 이 부분은 개선이 필요할 듯합니다.

 

 

3) 알람 끄기

 

turn off my 7am alarm -> did not find alarm

7시 알람 꺼줘 -> 그런 알람 없는데요;; 우리가 등록한 알람은 2개 뿐인데 하나는 오후 12시고 또 다른 하나는 오전 6시 알람이니..찾지 못했겠죠?

 

 

6. 간단하게 몇가지 내용을 입력해서 결과를 살펴 봤습니다.

 

이 심플 알람은 개선할 사항이 아주 많이 있습니다.

1) 알람 시간이 되었을 때 알려주는 기능;;

2) 알람 켜기/끄기

3) 알람 시간 변경

등이 있을 수 있습니다.


그리고, 여기서 다루지 않은 LUIS 모델들이 정말 많이 있으니 하나씩 찾아보면서 자신만의 채팅 봇을 만들어 보면 좋을 것 같습니다.

 

 

 

반응형
댓글