Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

[chatbot + AI = 下一代操作模式][09]使用IDialog來實現SoC

[chatbot + AI = 下一代操作模式][09]使用IDialog來實現SoC.jpg
圖片來源:https://pixabay.com/en/books-spine-colors-pastel-1099067/ 

在上一篇([08]如何微調FormFlow讓使用上更流暢)介紹完FormFlow之後,我們需要回來看一下目前最大的問題,也就是程式碼都寫在一隻RootDialog裡面。

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

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

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

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

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

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

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

  1. 讓使用者輸入姓名的頁面
  2. 讓使用者查飯店的頁面
  3. 讓使用者訂房的頁面

而這些頁面之間的跳轉我們是透過按鈕點下去之後出現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

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

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

建立出一個NameDialog

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

  1. 要加入[Serializable]
  2. 要實作IDialog<T> - 其中的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<IMessageActivity> result)
{
	var message = await result;

	context.Done(message.Text);
}
...
這邊發現,本來我們需要有個暫存來知道是不是問過名字這件事,但是透過Dialog這件事情就不需要暫存資料就可以做到了,因爲StartAsync自動分開了。
我們這個Dialog主要邏輯變成詢問名字了 - 當然問到了之後可以直接存起來,或者像我們這樣回傳,然後看呼叫的人怎麽處理。我個人傾向讓呼叫的人決定怎麽處理,這樣能夠更generic。

調整RootDialog - 決定什麽時候進入NameDialog

我們在RootDialog還是會判斷有沒有取得過名字,沒有的話就觸發我們NameDialog

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

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

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

private async Task GreetingAfterAsync(IDialogContext context,
            IAwaitable<string> 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
測試執行結果

從上面的測試結果可以看出,我們保持了和以前一樣的流程,但是内部邏輯拆開了。我們有一個專門詢問名字的NameDialog,至於取得的名字之後要做什麽,我們交由呼叫這個Dialog的程式去控制,達到SoC。

優化NameDialog

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

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

private async Task MessageReceivedAsync
            (IDialogContext context, IAwaitable<IMessageActivity> 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<IMessageActivity> 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概念都使用上。

下一篇([10]用IDialog全部重構 - 階段性總結)將是一個階段性的總結。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus