Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 記錄軟體開發的點點滴滴 著重於微軟技術、網頁開發、DevOps、C#, Asp .net Mvc、Azure、AI、Chatbot、Docker、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裡面。

BotBuilder有考慮到這件事情,因此内建用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全部重構 - 階段性總結)將是一個階段性的總結。

comments powered by Disqus