選單
GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
5 分鐘閱讀時間 (911 個字)

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

woman-working-on-the-microsoft-surface-laptop-mockup

前言

在 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 快速打造 ChatbotStrategyAutofac Named Services
使用 IoC 進行專案的客製
簡化申請、更新流程的免費網站憑證

相關文章

 

評論

尚無評論
已經注冊了? 這裡登入
Guest
2024/04/27, 週六

Captcha 圖像