Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 記錄軟體開發的點點滴滴 著重於微軟技術、網頁開發、DevOps、C#, Asp .net Mvc、Azure、AI、Chatbot、Docker、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
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus