一個連續對話的例子,像是「請假」。

     

每道問題的構成

一道問題包含「Order(提問順序)」、「XXXQuestion(問句)」、「Answer(預期答案的過濾法則, 答非所問的抱怨)」。

using OpenLineBot.Service;

namespace OpenLineBot.Models.Conversation.Entity.Custom
{
    public class LeaveApply : ConversationEntity
    {
        public LeaveApply(BotService bot) : base (bot) { }

        [Order(1)]
        [TextQuestion("給我員工編號?")]
        [Answer(typeof(EmployIdFilter), "是4個數字好嗎!")]
        public string EmployId { get; set; }

        [Order(2)]
        [TextPickerQuestion("請哪種假?", new string[] {"公假", "事假", "病假"})]
        [Answer(typeof(LeaveCateFilter), "用選的,不要自己亂回!")]
        public string LeaveCate { get; set; }

        [Order(3)]
        [DateTimePickerQuestion("何時開始?")]
        [Answer(typeof(LeaveStartFilter), "用選的,不要自己亂回!")]
        public string LeaveStart { get; set; }

        [Order(4)]
        [TextQuestion("請幾天?")]
        [Answer(typeof(LeaveDaysFilter), "給個數目好嗎!")]
        public string LeaveDays { get; set; }

        [Order(5)]
        [TextQuestion("請幾小時?")]
        [Answer(typeof(LeaveHoursdFilter), "0到8小時!")]
        public string LeaveHours { get; set; }
        
        [Order(6)]
        [ConfirmQuestion("要提交了嗎?")]
        [Answer(typeof(SubmitFilter), "用選的,不要自己亂回!")]
        public string Submit { get; set; }
        
    }
}

機器人在想什麼?

運作的核心說穿就是if-else分支樹(我用Chain of Responsibility Pattern包裝,如果任何人有其他建議,請在GitHub發Issue或是寄信讓我知道…)。

using OpenLineBot.Models.Conversation.Entity.Custom;
using OpenLineBot.Models.Conversation.Handler.Custom;
using OpenLineBot.Service;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Http;

namespace OpenLineBot.Controllers
{
    public class DefaultController : ApiController
    {
        [HttpPost]
        public IHttpActionResult POST()
        {

            BotService bot = null;

            try
            {
                string postData = Request.Content.ReadAsStringAsync().Result;
                var receivedMessage = isRock.LineBot.Utility.Parsing(postData);
                var evt = receivedMessage.events.FirstOrDefault();

                SecretInfo secret = Load();
                bot = new BotService(secret.ChannelAccessToken, secret.AdminId, evt);

                var handlerSubmit = new SubmitHandler<LeaveApply>(bot);
                var handlerByeBye = new ByeByeHandler<LeaveApply>(bot);
                var handlerNoKeyWord = new NoKeyWordHandler<LeaveApply>(bot);
                var handlerComplaint = new ComplaintHandler<LeaveApply>(bot);
                var handlerFirstQuestion = new FirstQuestionHandler<LeaveApply>(bot);
                var handlerNextQuestion = new NextQuestionHandler<LeaveApply>(bot);
                var handlerFinalQuestion = new FinalQuestionHandler<LeaveApply>(bot);
                var handlerNotFinalQuestion = new NotFinalQuestionHandler<LeaveApply>(bot);
                var handlerRecorded = new RecordedHandler<LeaveApply>(bot);
                var handlerNotRecorded = new NotRecordedHandler<LeaveApply>(bot);
                var handlerNotBye = new NotByeHandler<LeaveApply>(bot);
                var handlerText = new TextHandler<LeaveApply>(bot);
                var handlerLineEvent = new LineEventHandler<LeaveApply>(bot);

                // Set seccessors
                handlerSubmit.SetSeccessor(handlerSubmit.SuccessorDic["ByeBye"], handlerByeBye);

                handlerFinalQuestion.SetSeccessor(handlerFinalQuestion.SuccessorDic["Submit"], handlerSubmit);
                handlerFinalQuestion.SetSeccessor(handlerFinalQuestion.SuccessorDic["ByeBye"], handlerByeBye);

                handlerNotFinalQuestion.SetSeccessor(handlerNotFinalQuestion.SuccessorDic["NextQuestion"], handlerNextQuestion);
                handlerNotFinalQuestion.SetSeccessor(handlerNotFinalQuestion.SuccessorDic["Complaint"], handlerComplaint);

                handlerRecorded.SetSeccessor(handlerRecorded.SuccessorDic["FinalQuestion"], handlerFinalQuestion);
                handlerRecorded.SetSeccessor(handlerRecorded.SuccessorDic["NotFinalQuestion"], handlerNotFinalQuestion);

                handlerNotRecorded.SetSeccessor(handlerNotRecorded.SuccessorDic["FirstQuestion"], handlerFirstQuestion);
                handlerNotRecorded.SetSeccessor(handlerNotRecorded.SuccessorDic["NoKeyWord"], handlerNoKeyWord);

                handlerNotBye.SetSeccessor(handlerNotBye.SuccessorDic["Recorded"], handlerRecorded);
                handlerNotBye.SetSeccessor(handlerNotBye.SuccessorDic["NotRecorded"], handlerNotRecorded);

                handlerText.SetSeccessor(handlerText.SuccessorDic["ByeBye"], handlerByeBye);
                handlerText.SetSeccessor(handlerText.SuccessorDic["NotBye"], handlerNotBye);

                handlerLineEvent.SetSeccessor(handlerLineEvent.SuccessorDic["Text"], handlerText);

                handlerLineEvent.HandleRequest();

                return Ok();
            }
            catch (Exception ex)
            {
                bot.Notify(ex);
                return Ok();
            }
        }

        SecretInfo Load()
        {
            string tokenPath = HttpContext.Current.Server.MapPath(@"../App_Data/secret.token");
            using (StreamReader r = new StreamReader(tokenPath))
            {
                string json = r.ReadToEnd();
                return JsonConvert.DeserializeObject<SecretInfo>(json);
            }
        }

    }

    internal class SecretInfo
    {
        public string AdminId { get; set; }
        public string ChannelAccessToken { get; set; }
    }

}

如何使用?

  • 務必新增資料庫檔案
    請預先單獨執行DbStarter/Programs.cs以生成OpenLineBot/App_Data/LineBotDb.sqlite
  • 務必在App_Data目錄新增檔案secret.token
    {
    "AdminId":"YourLineUserID",
    "ChannelAccessToken":"YourChannelAccessToken"
    }
    
  • 如欲變更問題的順序、問句、抱怨
    請實作一個如LeaveApply能繼承ConversationEntity的類別。
  • 如欲設計問題的過濾法則
    請參考ConcreteFilters.cs裡面的類別,實作IFilter
  • 如欲擴充機器人的提問模板(點選時間日期或是純文字提問,就是採用不同模板)
    請參考Questions.cs裡面的類別,實作IQuestion,並且ConversationEntity的方法PushQuestion也得跟著增加case。
  • 如欲改變機器人的分支流程
    請參考ConcreteHandlers.cs裡面的類別,以XXXHandler命名,實作Handler<T>,留意T會實作IConversationEntity;若XXXHandler有下個分支YYYHandler,請務必做SuccessorDic.Add(“YYY”, 編號)。最後在DefaultController串接好每個實例Handler的Successor,並呼叫第一個實例Handler的方法HandleRequest。

程式碼放在哪裡?

OpenLineBot

沒做完的部份

  • 要保留多長的連續對話時間?
  • 機器人沒有辨別用戶是否為公司員工?