Microsoft Botframework + Adaptive Cards 快速打造 Chatbot 之 2

前言

在 Microsoft Botframework + Adaptive Cards 快速打造 Chatbot 一篇中,
我們說明如何透過 Adaptive Cards 將所有的動作透過 Action 再依它的 Action Type 來轉換成對應的 Action 物件及處理該 Action 物件的 Strategy 物件。

但是當我們的 Action 逾來逾多時,原本使用 switch 勢必會造成相對應的複雜度,依 Strategy 的方式是建立對應表,以下將介紹使用 Dictionary<string, Func<T>> 及 Autofac DI 這2種方式。

另外,如果 Strategy 物件 中有使用到 Prompt dialogs 時,原本在執行完 Strategy 物件的 Method 後,如果直接呼叫 context.Done(“”); 將會造成 botframework Dialog Stack 運行上的錯誤,我們也將修改 Strategy 物件實作的 interface ,增加最後是否要自動執行 context.Done 。

實作

調整 STRATEGY 物件的建立方式

因為對應的 Strategy 物件變成了4個,如果用 switch 判斷的話,程式如下,

/// <summary>
/// 依不同的 Type 決定要用那個 Class
/// </summary>

/// <param name="type"></param>
/// <returns></returns>
[Obsolete]
public static IActionStrategy ResolveActionStrategy(BotAction botAction)
{
    IActionStrategy result = null;
    switch (botAction.ActionType)
    {
        case ActionTypes.ShowAcountAction:
            result = new ShowAccountActionStrategy();
            break;
        case ActionTypes.AccountAction:
            result = new AccountActionStrategy();
            break;
        case ActionTypes.ShowMenuAction:
            result = new ShowMenuActionStrategy();
            break;
        case ActionTypes.OrderAction:
            result = new OrderActionStrategy();
            break;
        default:
            result = new NoneActionStrategy();
            break;
    }

    return result;
}

因為 botframework SDK中是使用 Autofac ,所以我們可以透過 Autofac Named Services 方式來建立對應表,所以在 ActionStrategyResolver 物件中建立 ResolveByActionTypes Method 然後在 Global.asax.cs Application_Start Method 中去呼叫 ResolveByActionTypes Method。如下,

/// <summary>
/// 設定 ACTION_TYPE 對應要建立的物件
/// </summary>

/// <param name="builder"></param>
public static void ResolveByActionTypes(ContainerBuilder builder)
{
    builder
        .RegisterType<ShowAccountActionStrategy>()
        .Named<IActionStrategy>(ActionTypes.ShowAcountAction.ToString())
        .InstancePerLifetimeScope();

    builder
        .RegisterType<AccountActionStrategy>()
        .Named<IActionStrategy>(ActionTypes.AccountAction.ToString())
        .InstancePerLifetimeScope();

    builder
        .RegisterType<ShowMenuActionStrategy>()
        .Named<IActionStrategy>(ActionTypes.ShowMenuAction.ToString())
        .InstancePerLifetimeScope();

    builder
        .RegisterType<OrderActionStrategy>()
        .Named<IActionStrategy>(ActionTypes.OrderAction.ToString())
        .InstancePerLifetimeScope();
}

而在取得對應的 Strategy 物件時,只要給 Action Type 值 ( var actionStrategy =scope.ResolveNamed(botAction.ActionType.ToString()) ),就可以了哦! 如下,

/// <summary>
/// 透過 ACTION_TYPE 來建立對應的 Strategy 物件
/// </summary>

/// <param name="botAction"></param>
/// <param name="context"></param>
/// <returns></returns>
public async static Task DoActionAsync(BotAction botAction, IDialogContext context)
{
    using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, context.Activity.AsMessageActivity()))
    {
        var actionStrategy = scope.ResolveNamed<IActionStrategy>(botAction.ActionType.ToString());
        await actionStrategy.DoActionAsync(botAction, context);
        //註:如果您的 strategy 有用到 PromptDialog Or Other ResumeAfter 你就不能在這裡 CALL context.Done()
        if (actionStrategy.IsContextDone)
        {
            context.Done("");
        }
    }
}

調整 ACTION 物件的建立方式

之前在 JsonConverter 中依 ActionType 透過 switch 來建立物件,我們可以建立一個 Dictionary 來對應生成的Function,因為會需要 jtoken 所以建立的 Dictionary為,Dictionary<string, Func<JToken, BotAction>> ,最後要回傳一個 BotAction Base 類別物件,然後在 static construct 時建立它 (ActionConverter是我們的 class name),如下,

// 設定依 ActionType 來建立物件的 Dictionary
static Dictionary<string, Func<JToken, BotAction>> BotActionMapper;
static ActionConverter()
{
    ResolveByActionTypes();
}

/// <summary>
/// 依 ActionType 來決定要建立那個 Action 及初始化處理
/// </summary>

private static void ResolveByActionTypes()
{
    Func<JToken, BotAction> acountActionFun = jtoken => new AccountAction();
    Func<JToken, BotAction> orderActionFun = jtoken =>
    {
        var result = new OrderAction();
        //建立物件後,如果還有其他要處理的事,可以接著寫下去...
        //AssignProducts(result as OrderAction, jtoken as JObject);
        return result;
    };

    BotActionMapper = new Dictionary<string, Func<JToken, BotAction>>(){
        {ActionTypes.AccountAction.ToString(), acountActionFun },
        {ActionTypes.OrderAction.ToString(), orderActionFun }
    };
}

所以原本在 ReadJson Method 裡面的 switch 改成依 ActionType 當成 BotActionMapper 的 Key ,取出生成的 Function ,然後呼叫它( BotAction result = BotActionMappertype.ToString(); ),
如下,

/// <summary>
/// 依 ActionType 來決定要轉回什麼物件
/// </summary>
/// <param name="reader"></param>
/// <param name="objectType"></param>
/// <param name="existingValue"></param>
/// <param name="serializer"></param>
/// <returns></returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    var jtoken = JToken.ReadFrom(reader);
    var type = jtoken["ActionType"].ToObject<ActionTypes>();
    BotAction result = BotActionMapper[type.ToString()](jtoken);
    serializer.Populate(jtoken.CreateReader(), result);
    return result;
}

設定是否呼叫 CONTEXT.DONE(“”)

之前我們執行完 Strategy 物件的 Method 後,會直接呼叫 context.Done(“”) ,這表示這個 Strategy 物件的事已做好了,再進來的訊息又是會對應到新的 Strategy 物件。

但有時,Strategy 物件會需要再從使用者那取得回應,例如用了 PromptDialog 相關的 Method, 它需要傳入一個 resume 的 Callback Function,在整個結束才會呼叫 contex.Done(“”),所以這時候,就不可以直接呼叫 context.Done 。

所以 Strategy 物件的 interface 多加入一個設定是否呼叫 contex.Done(“”) 的屬性,或是改成不呼叫,全都交由 Strategy 物件自行決定。如下,

/// <summary>
/// 定義 Action 共通的 interface 
/// </summary>

public interface IActionStrategy
{
    Task DoActionAsync(BotAction botAction, IDialogContext context);
    bool IsContextDone { get;}
}

所以在上面的 DoActionAsync Method 中有依 IsContextDone 屬性來判斷是否執行 contex.Done(“”) 。

經過將 switch 改以 Dictionary or Autofac DI 方式來建立物件,改善了複雜度,也提升了可讀性。
希望對大家以 Microsoft Botframework 來開發 Chatbot 有所幫助。

參考資料

Microsoft Botframework + Adaptive Cards 快速打造 Chatbot
Strategy
Autofac Named Services

作者: 亂馬客

亂馬客 @叡揚資訊 rainmaker_ho@gss.com.tw https://rainmakerho.github.io/ https://www.slideshare.net/rainmakerho

發表迴響

你的電子郵件位址並不會被公開。