Alan Tsai 的學習筆記


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

[Bot Framework V4][09]使用waterfall建立表單式填寫

[Bot Framework V4][09]使用waterfall建立表單式填寫.jpg
圖片來源:https://pixabay.com/en/books-spine-colors-pastel-1099067/ 

在上一篇([08]改用TextPrompt詢問使用者姓名)使用TextPrompt來取得使用者的姓名,感覺起來好像和自己維護狀態沒什麽兩樣,因爲還是需要透過if else來呼叫。

這樣Dialogs還有意義嗎?

這篇將會介紹另外一種使用情景,有時候需要搜集使用者的資料,例如説他要訂房的話會需要搜集他要訂什麽時間,住幾個晚上等等,這個時候Dialog就變得更加方便。

這篇將介紹如何透過waterfall來達到這個效果。

在V3版本有所謂的FormFlow,waterfall dialog做出來有點類似一樣的概念,有興趣可以看看:[07]使用FormFlow讓Chatbot搜集表單資訊更容易
這篇的程式碼github頁面是alantsai-samples/mhat-hotelbotv4:blog/chapter-09

想增加什麽功能?

有了使用者的姓名,接下來就是看看如何協助訂房的輸入。

這邊需要幾個資訊:

  1. 從那天入住
  2. 要住幾個晚上
  3. 總共幾個人
  4. 床的大小

瞭解需求了之後,接下來就是開發。

調整什麽?

接下來就要看看要調整什麽。

首先,需要建立一個POCO的model代表這個訂房的信息。

再來,建立一個dialog用來啓動這個流程。

然後,暫時先把取得姓名那段拿掉。

最後就是測試。

整個拆接下來就是:

  1. 建立出RoomReservation的POCO
  2. 整合剛建立的POCO到CounterState
  3. 再來建立出一個新的DialogSet
  4. 定義每一個step要做的事情
  5. 修改bot邏輯呼叫DialogSet
  6. 測試

建立出RoomReservation的POCO

首先第一步是建立出POCO,因此在Model資料夾下面建立:RoomReservation

public class RoomReservation
{
	public DateTime StartDate { get; set; }
	public int NumberOfNightToStay { get; set; }
	public int NumberOfPepole { get; set; }
	public string BedSize { get; set; }

	public override string ToString()
	{
		return $"入住日期:{StartDate}{Environment.NewLine}" +
		$"入住幾晚:{NumberOfNightToStay}{Environment.NewLine}" +
		$"幾人:{NumberOfPepole}{Environment.NewLine}" +
		$"床型:{BedSize}{Environment.NewLine}";
	}
}

整合剛建立的POCO到CounterState

要把RoomReservation加入到Accessor,這邊偷懶用CounterState,因此在增加一個property進去:

public class CounterState
{
	...
	public RoomReservation RoomReservation { get; set; } = new RoomReservation();
}

再來建立出一個新的DialogSet

再來切換到EchoWithCounterBot.cs,首先是準備好DialogSet:

public class EchoWithCounterBot : IBot
{
	...
	private readonly DialogSet _dialogsWaterfall;

	public EchoWithCounterBot
		(EchoBotAccessors accessors, ILoggerFactory loggerFactory)
	{
		_dialogsWaterfall = new DialogSet(_accessors.DialogState);

		var waterfallSteps = new WaterfallStep[]
		{
			GetStartStayDateAsync,
			GetStayDayAsync,
			GetNumberOfOccupantAsync,
			GetBedSizeAsync,
			GetConfirmAsync,
			GetSummaryAsync,
		};

		_dialogsWaterfall.Add(new WaterfallDialog("formFlow", waterfallSteps));
		_dialogsWaterfall.Add(new DateTimePrompt("dateTime"));
		_dialogsWaterfall.Add(new NumberPrompt<int>("number"));
		_dialogsWaterfall.Add(new ChoicePrompt("choice"));
		_dialogsWaterfall.Add(new ConfirmPrompt("confirm"));
	}
	....
}

這邊定義了一個DialogSet,裡面第一個是一個Waterfalldialog,其中定義了好多個steps。

再來增加了一些prompt,在這些steps可以呼叫使用。

准備好了之後,接下來是定義每一個steps。

定義每一個step要做的事情

每一個steps都可以取得上一個steps結束的時候使用者輸入的内容。

因此,每一個steps,可以依照上一個steps的結果做出處理,然後觸發往下要執行的prompt或者waterfall結束。

接下來看看每一個step的定義:

private async Task<DialogTurnResult> GetStartStayDateAsync
	(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
	return await stepContext.PromptAsync("dateTime",
		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> 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> 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> 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> 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);
}
這邊有重構出一個GetCounterState,目的是爲了取得CounterState
有些prompt有特別的用法,包含資料驗證等。這篇沒有機會介紹,未來有機會在介紹。

到這裡Dialog就准備好了。

修改bot邏輯呼叫DialogSet

OnTurnAsync會加入以下方式來啓動waterflow dialog:

...
var dialogWaterfallContext = await _dialogsWaterfall.CreateContextAsync(turnContext, cancellationToken);
var waterfallResult = await dialogWaterfallContext.ContinueDialogAsync(cancellationToken);

if(turnContext.Activity.Text == "訂房")
{
	await dialogWaterfallContext.BeginDialogAsync("formFlow",
		null, cancellationToken);
}
else if(waterfallResult.Status != DialogTurnStatus.Empty)
{

}
...
爲了簡化功能,因此暫時把取得使用者姓名的部分拿掉
有一個空的if是爲了避免原本的echo功能觸發 - 這邊還有一個最後的else主要用來保留原本echo的功能。

測試

最後測試下來就會變成:

Bot Framework Emulator_2018-10-25_23-41-46.png
2018-10-25_23-42-08.png
測試最後結果

結語

這篇介紹了waterfall這個dialog,可以看到透過step的方式可以自己定義每一個階段要做什麽。

不過,這裡也有注意到一件事情,那就是取得姓名的部分被暫時拿掉了,原因是控制誰在執行透過目前方式有點麻煩。

可是沒辦法解決嗎?畢竟好幾個不同的類型模組是很重要。

下一篇([10]在Dialog裡面做Branching以及Looping把不同功能更加模組化)看看解決方式,怎麽把兩個功能(取得姓名以及訂房)整合在一起同時存在。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付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