iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 9
1
Everything on Azure

使用 Microsoft Conversational AI Tools - 打造新时代的UI界面系列 第 9

[09]使用IDialog來實現SoC

在上一篇介紹完FormFlow之後,我們需要回來看一下目前最大的問題,也就是程式碼都寫在一隻RootDialog裡面。

BotBuilder有考慮到這件事情,因此内建用IDialog來解決這個問題。

這篇來看看IDialog怎麽做到SoC (Seperation of Concern)。

這篇的程式碼github頁面是alantsai-samples/mhat-hotelbot:blog/chapter-09

重新思考一下bot的操作模式

在進入IDialog之前,我們先來想一下目前的bot和以前的desktop程式或者web程式有什麽不同。

想象一下,如果今天我們現有功能是寫成一個Desktop的App。

每一個功能我們就會有一個頁面,因此,我們會有:

讓使用者輸入姓名的頁面
讓使用者查飯店的頁面
讓使用者訂房的頁面
而這些頁面之間的跳轉我們是透過按鈕點下去之後出現pop up的方式做完輸入之後,可能按下一個確認鍵把資料儲存起來然後又回到了程式的主頁。

剛剛上面提到的是傳統的app概念,那回到chatbot呢?其實是一樣的概念,只不過我們不是透過按下按鈕來切換功能,而是透過輸入文字、圖片和語音來跳轉(多媒體輸入之後在介紹)

剛剛是使用者面的感受,那從程式的角度呢?不管是desktop還是web,一個頁面就會有一隻獨立的cs檔案,除非你把所有功能都寫在一個頁面上面,要不然多多少少邏輯還是會拆分出來,不會造成一個頁面好多個邏輯,無形之中做到了 SoC。

但是在我們目前的chatbot來説,全部都還是在一起,那麽類似desktop或web每一種頁面一個邏輯的對應東西是什麽?那就是IDialog

dialogs-screens.png
傳統desktop和chatbot的對應,來源:https://docs.microsoft.com/en-us/azure/bot-service/dotnet/bot-builder-dotnet-manage-conversation-flow?view=azure-bot-service-3.0

重構詢問姓名的邏輯到IDialog

我們用一個例子來學習怎麽使用IDialog來切分邏輯。

我們第一個功能是取得使用者的名字,這個我們可以重構到另外一個IDialog,我稱之爲NameDialog

我們會經歷以下幾個步奏:

建立出一個NameDialog
把邏輯從RootDialog搬到NameDialog
調整RootDialog - 決定什麽時候進入NameDialog

建立出一個NameDialog

要建立一個IDialog需要兩件事情:

要加入[Serializable]
要實作IDialog - 其中的T代表會回傳的内容形態

[Serializable]
public class NameDialog : IDialog<string>
{
	public Task StartAsync(IDialogContext context)
	{
		throw new NotImplementedException();
	}
}
``
### 把邏輯從RootDialog搬到NameDialog
邏輯很簡單,當這個Dialog觸發的時候,會詢問使用者的名稱,然後把使用者輸入的内容回傳回來。程式碼變成:

...
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("您的名字是?");

context.Wait(MessageReceivedAsync);

}

private async Task MessageReceivedAsync
(IDialogContext context, IAwaitable result)
{
var message = await result;

context.Done(message.Text);

}
...

這邊發現,本來我們需要有個暫存來知道是不是問過名字這件事,但是透過Dialog這件事情就不需要暫存資料就可以做到了,因爲StartAsync自動分開了。
我們這個Dialog主要邏輯變成詢問名字了 - 當然問到了之後可以直接存起來,或者像我們這樣回傳,然後看呼叫的人怎麽處理。我個人傾向讓呼叫的人決定怎麽處理,這樣能夠更generic。
### 調整RootDialog - 決定什麽時候進入NameDialog
我們在RootDialog還是會判斷有沒有取得過名字,沒有的話就觸發我們NameDialog

...
context.UserData.TryGetValue("Name", out string name);

if(string.IsNullOrEmpty(name))
{
context.Call(new NameDialog(), GreetingAfterAsync);
}
...

我們可以看到,整個判斷問過名字了沒的部分都沒了,全部由NameDialog處理了,然後我們會在那邊取得使用者輸入的姓名,將會在GreetingAfterAsync的callback裡面做處理。

private async Task GreetingAfterAsync(IDialogContext context,
IAwaitable result)
{
var name = await result;

context.UserData.SetValue<string>("Name", name);

await context.PostAsync($"{name} 您好,能夠幫助您什麽");

context.Wait(MessageReceivedAsync);

}

可以看到,NameDialog取得了名稱,在這個callback裡面得到了之後,把這個名稱寫到UserData裡面。

![botframework-emulator_2018-07-13_00-43-05.png](https://d33wubrfki0l68.cloudfront.net/b6a94b69c80fb0b1cf4b94f21a1028f4caf7c042/d22f2/posts/2018/07/2018-07-13-bot-framework-with-ai-cognitive-service-9-refactor-using-idialog-for-better-soc/5bf419d9-bd4f-4244-a0e3-d4aab9a0fc18.png)
測試執行結果
從上面的測試結果可以看出,我們保持了和以前一樣的流程,但是内部邏輯拆開了。我們有一個專門詢問名字的NameDialog,至於取得的名字之後要做什麽,我們交由呼叫這個Dialog的程式去控制,達到SoC。

### 優化NameDialog
到目前爲止,NameDialog的邏輯和未重構前一樣,不過這邊少了一個防呆,也就是如果使用者沒有輸入任何名字怎麽辦?

我們應該要有個邏輯,做這個檢查:

private async Task MessageReceivedAsync
(IDialogContext context, IAwaitable result)
{
var message = await result;

if(string.IsNullOrEmpty(message.Text))
{
	await context.PostAsync("請您輸入您的名字");

	context.Wait(MessageReceivedAsync);
}

context.Done(message.Text);

}

不過這個時候會遇到另外一個問題,如果使用者一直輸入不符合條件的姓名,他會無限回圈在這裡面。因此,這種類型的重試,都記得要做所謂的只嘗試幾次,避免使用者卡在這個Dialog。

private int Attempt = 3;

private async Task MessageReceivedAsync
(IDialogContext context, IAwaitable result)
{
var message = await result;

if(string.IsNullOrEmpty(message.Text))
{
	Attempt = Attempt - 1;

	if (Attempt > 0)
	{
		await context.PostAsync("請您輸入您的名字");

		context.Wait(MessageReceivedAsync);
	}
	else
	{
		context.Fail(new TooManyAttemptsException("取不到名字"));
	}
}

context.Done(message.Text);

}

從這裡可以看出,拆IDialog是很有必要的,想象一下全部寫在RootDialog,不止邏輯看起來複雜,無法做單元測試,同樣邏輯還不能夠重複使用。因此,好好使用Dialog對整個chatbot開發是關鍵。
## 結語
程式設計師最需要注意的就是保持程式碼的“乾净”,而好好使用IDialog就是讓chatbot程式碼乾净的好幫手。

到目前爲止,chatbot的重要觀念都已經介紹完了,細節的部分還有很多,不過以目前所知要開發出一個chatbot已經不是什麽太難的事情了。

在要轉換文章重心之前,要對目前的code做一次大重構,把這篇所學的IDialog概念都使用上。

下一篇將是一個階段性的總結。

上一篇
[08]如何微調FormFlow讓使用上更流暢
下一篇
[10]用IDialog全部重構 - 階段性總結
系列文
使用 Microsoft Conversational AI Tools - 打造新时代的UI界面30

尚未有邦友留言

立即登入留言