在上一篇([09]使用waterfall建立表單式填寫)介紹了使用watfall
的方式達到建立一個表單式搜集的chatbot。
裡面爲了簡化把取得姓名的部分暫時拿掉了,但是在實務上不同邏輯的dialog可能會存在,那怎麽辦呢?
這篇將介紹透過Dialog來做Branching以及Looping。
什麽是Branching和Looping
Branching就和名稱一樣的意思,分叉出去。
舉例來説,假設我的waterfall啓動的時候,因爲收到了什麽訊息這個時候啓動另外一個waterfall, 這個時候除非另外一個(剛啓動那個)waterfall結束,要不然不會繼續往原本的繼續執行下去。
這個就是所謂的Branching,先分叉出去,然後執行完了在分叉回來。
Branching是透過呼叫BeginDialogAsync
達到。
Looping和字面上面的意思也一樣,當waterfall執行到最後一步的時候,不結束,重新又new一個同樣的waterfall,這樣就延續執行下去。
換而言之,就達到了一直loop。
Looping是透過呼叫ReplaceDialogAsync
來達到。
加入Branching和Looping的概念到目前的範例
對到目前的範例來説:
- 需要有branching分支出去執行取得姓名以及取得訂房這兩個flow
- 需要有loop,這樣整個waterfall永遠不會結束
因此整個流程概念如下:
上圖有幾個重要的部分:
- 藍色的框框代表3個大flow
有3個大藍色的框框:
- root - 這個是整個流程的起始點。root裡面有個echo - 當另外兩個branching條件不符合的時候,echo會觸發。
- askNameWaterfall - 當使用者沒有輸入過姓名的時候,將透過branching的方式觸發這段的waterfall
- bookRoom - 當使用者輸入
訂房
的時候,將透過branching的方式觸發
- 橘紅色的綫
-
代表是branching的部分,從root這個waterfall切換到另外兩個branching。
這邊
echo
有點特別,因爲他不屬於branching因此屬於root的一部分 - 綠色的綫
- 代表是looping的部分。當其他watefall結束的時候,都會回到root waterfall。
修改現行的程式碼
上面有了概念之後,來看看如何調整現行的程式碼來達到使用branching以及looping。
整個修改會分爲幾個部分:
- 建立一個
HotelDialogSet
- 完成詢問姓名的waterfall
- 完成訂房的waterfall
- 完成root waterfall
- 整合到bot呼叫HotelDialogSet
建立一個HotelDialogSet
首先建立出一個class叫做HotelDialogSet
,然後讓這個class繼承DialogSet
,透過這個方式讓邏輯整合到這個class就好。
由於之後會使用到Accessor來取得儲存的值,因此會用建構子傳入來,最後整個class:
public class HotelDialogSet : DialogSet
{
private EchoBotAccessors _accessors;
public HotelDialogSet(IStatePropertyAccessor<DialogState> dialogState,
EchoBotAccessors accessors)
: base(dialogState)
{
_accessors = accessors;
}
}
完成詢問姓名的waterfall
首先來完成詢問姓名的waterfall,整個的邏輯和之前篇幅看到的一樣,定義出一個WaterfallDialog,用來取得使用者姓名:
public class HotelDialogSet : DialogSet
{
....
public string askNameWaterfall { get; } = "askNameWaterfall";
public HotelDialogSet
(IStatePropertyAccessor<DialogState> dialogState, EchoBotAccessors accessors)
: base(dialogState)
{
...
var askNameDialogSet = new WaterfallStep[]
{
StartPromptName,
ProcessPromptName,
};
Add(new WaterfallDialog(askNameWaterfall, askNameDialogSet));
Add(new TextPrompt("textPrompt"));
}
private async Task<DialogTurnResult> ProcessPromptName
(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var userInfo = await _accessors.UserInfo.GetAsync
(stepContext.Context, () => new Model.UserInfo());
userInfo.Name = stepContext.Result.ToString();
await _accessors.UserInfo.SetAsync(stepContext.Context, userInfo);
await _accessors.UserState.SaveChangesAsync(stepContext.Context);
await stepContext.Context.SendActivityAsync($"{userInfo.Name} 您好");
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
private async Task<DialogTurnResult> StartPromptName
(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.PromptAsync("textPrompt", new PromptOptions()
{
Prompt = MessageFactory.Text("請問尊姓大名?"),
},
cancellationToken);
}
}
上面程式碼應該不太需要介紹,定義了了一個Waterfall,裡面有兩個step用來取得姓名以及儲存在Accessor。
完成訂房的waterfall
看過了上面詢問名字的部分,相信對於訂房的做法也就很清楚了 - 一樣是建立一個waterfall,裡面定義出完成訂房需要的step。
首先是在建構子的時候建立出waterfall的step:
public HotelDialogSet
(IStatePropertyAccessor<DialogState> dialogState, EchoBotAccessors accessors)
: base(dialogState)
{
...
var waterfallSteps = new WaterfallStep[]
{
GetStartStayDateAsync,
GetStayDayAsync,
GetNumberOfOccupantAsync,
GetBedSizeAsync,
GetConfirmAsync,
GetSummaryAsync,
};
Add(new WaterfallDialog("bookRoom", waterfallSteps));
Add(new DateTimePrompt("dateTime"));
Add(new NumberPrompt<int>("number"));
Add(new ChoicePrompt("choice"));
Add(new ConfirmPrompt("confirm"));
}
再來就是看看每一個step的實際動作:
...
#region bookRoom
private async Task<DialogTurnResult> GetSummaryAsync
(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
if ((bool)stepContext.Result)
{
await stepContext.Context.SendActivityAsync
($"訂單下定完成,訂單號:{DateTime.Now.Ticks}");
}
else
{
await stepContext.Context.SendActivityAsync("已經取消訂單");
}
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
private async Task<DialogTurnResult> GetConfirmAsync
(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var roomReservation = (await GetCounterState(stepContext.Context))
.RoomReservation;
roomReservation.BedSize = ((FoundChoice)stepContext.Result).Value;
return await stepContext.PromptAsync("confirm", new PromptOptions()
{
Prompt = MessageFactory.Text($"請確認您的訂房條件:{Environment.NewLine}" +
$"{roomReservation}")
});
}
private async Task<DialogTurnResult> GetBedSizeAsync
(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
(await GetCounterState(stepContext.Context))
.RoomReservation.NumberOfPepole = (int)stepContext.Result;
var choices = new List<Choice>()
{
new Choice("單人床"),
new Choice("雙人床"),
};
return await stepContext.PromptAsync("choice",
new PromptOptions()
{
Prompt = MessageFactory.Text("請選擇床型"),
Choices = choices,
},
cancellationToken);
}
private async Task<DialogTurnResult> GetNumberOfOccupantAsync
(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
(await GetCounterState(stepContext.Context))
.RoomReservation.NumberOfNightToStay = (int)stepContext.Result - 1;
return await stepContext.PromptAsync("number",
new PromptOptions()
{
Prompt = MessageFactory.Text("幾人入住"),
},
cancellationToken);
}
private async Task<DialogTurnResult> GetStayDayAsync
(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
(await GetCounterState(stepContext.Context))
.RoomReservation.StartDate =
DateTime.Parse(((List<DateTimeResolution>)stepContext.Result).First().Value);
return await stepContext.PromptAsync("number", new PromptOptions()
{
Prompt = MessageFactory.Text("請輸入要住幾天"),
},
cancellationToken);
}
private async Task<DialogTurnResult> GetStartStayDateAsync
(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.PromptAsync("dateTime",
new PromptOptions()
{
Prompt = MessageFactory.Text("請輸入入住日期"),
},
cancellationToken);
}
#endregion
完成root waterfall
剩下最後一個waterfall了,也就是所有的起點,root
的waterfall。
一樣就是先在建構子定義出這個waterfall的step:
...
var rootSteps = new WaterfallStep[]
{
StartRootAsync,
ProcessRootAsync,
LoopRootAsync,
};
Add(new WaterfallDialog("root", rootSteps));
再來看看實際step裡面執行的内容:
private async Task<DialogTurnResult> LoopRootAsync
(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.ReplaceDialogAsync("root", null, cancellationToken);
}
private async Task<DialogTurnResult> ProcessRootAsync(WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
var userInfo = await _accessors.UserInfo.GetAsync(
stepContext.Context, () => new Model.UserInfo());
if (string.IsNullOrEmpty(userInfo.Name))
{
return await stepContext.BeginDialogAsync(
askNameWaterfall, null, cancellationToken);
}
else if (stepContext.Result.ToString() == "訂房")
{
return await stepContext.BeginDialogAsync(
"bookRoom", null, cancellationToken);
}
else
{
CounterState state = await GetCounterState(stepContext.Context);
state.TurnCount++;
// Set the property using the accessor.
await _accessors.CounterState.SetAsync(stepContext.Context, state);
// Save the new turn count into the conversation state.
// Echo back to the user whatever they typed.
var responseMessage = $"Name: {userInfo.Name} Turn {state.TurnCount}: You sent '{stepContext.Result}'\n";
await stepContext.Context.SendActivityAsync(responseMessage);
return await stepContext.ContinueDialogAsync(cancellationToken);
}
}
private async Task<DialogTurnResult> StartRootAsync(WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
return await stepContext.PromptAsync("textPrompt", new PromptOptions()
{
Prompt = MessageFactory.Text("您好,能夠幫到您什麽?"),
},
cancellationToken);
}
這邊最重要的就是ProcessRootAsync
以及LoopRootAsync
。
在ProcessRootAsync
裡面,透過呼叫BeginDialogAsync
來做到branching - 依照輸入内容不同branch到不同的waterfall。
然後在LoopRootAsunc
裡面呼叫ReplaceDialogAsync
來做到looping。
整合到bot呼叫HotelDialogSet
最後切換到EchoWithCounterBot
來設定把整個流程啓動起來。
首先把HotelDialogSet
在建構子的時候建立出來:
public class EchoWithCounterBot : IBot
{
private readonly HotelDialogSet _dialogs;
public EchoWithCounterBot(EchoBotAccessors accessors,
ILoggerFactory loggerFactory)
{
...
_dialogs = new HotelDialogSet(_accessors.DialogState, accessors);
}
}
最後要把整個啓動起來:
public async Task OnTurnAsync(ITurnContext turnContext,
CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext.Activity.Type == ActivityTypes.Message)
{
var dc = await _dialogs.CreateContextAsync(
turnContext, cancellationToken);
await dc.ContinueDialogAsync(cancellationToken);
if (!turnContext.Responded)
{
await dc.BeginDialogAsync
("root", null, cancellationToken);
}
}
else
{
await turnContext.SendActivityAsync
($"{turnContext.Activity.Type} event detected");
}
await _accessors.ConversationState.SaveChangesAsync(turnContext);
}
EchoWithCounterBot
裡面刪除原本code的部分處理 - 如果在處理上有遇到問題,歡迎參考範例程式碼。
測試結果
首先是看取得姓名的部分:
再來使用關鍵字訂房
:
最後如果輸入其他任何内容,都變成echo模式:
結語
這篇透過建立出一個自己的DialogSet并且透過branching以及looping的方式讓整個組合運作起來。
透過邏輯整合到自己的DialogSet,在呼叫端(EchoWithCounterBot
)變得非常的乾净,并且邏輯也分別出去了。
可是還是產生了別的問題,現在所有邏輯都卡在了DialogSet裡面,尤其是2個waterfall明明是不同的東西難道不能夠抽出去嗎?可不可以抽到一個獨立module然後需要的時候整合使用呢?
這就是composite dialogs的作用,下一篇再來介紹。