Alan Tsai 的學習筆記


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

[chatbot + AI = 下一代操作模式][10]用IDialog全部重構 - 階段性總結

[chatbot + AI = 下一代操作模式][10]用IDialog全部重構 - 階段性總結.jpg
圖片來源:https://pixabay.com/en/books-spine-colors-pastel-1099067/ 

在上一篇([09]使用IDialog來實現SoC)介紹了怎麽使用IDialog來拆分邏輯,并且一步一步的用取得名字的邏輯拆成為一個NameDialog

在這一篇我們將會把所有的邏輯重構成爲IDialog,并且對於目前學習到的Bot Builder SDK做一個階段性的總結。

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

重構剩下來的邏輯

上篇重構完了取得名字的邏輯之後,我們還剩下幾個部分需要重構:

  1. 查飯店
  2. 取得訂房的費用明細
  3. 訂房

重上一篇的重構方式我們可以看出,整個重構的步奏分爲:

  1. 建立一個Dialog
  2. 把邏輯從root放到dialog裡面
  3. 修正root為呼叫dialog來觸發
  4. 測試

接下來我們就快速看一下這幾個邏輯拆分的程式部分。

查飯店

首先我們建立一個IDialog叫做SearchHotelDialog。這個SearchHotelDialog回回傳一個HeroCard

[Serializable]
public class SearchHotelDialog : IDialog<HeroCard>
{
	public Task StartAsync(IDialogContext context)
	{
		throw new NotImplementedException();
	}
}

在來我們把HeroCard的產生放在Dialog裡面:

public Task StartAsync(IDialogContext context)
{
	// 返回飯店的圖片以及可以打開官網的按鈕

	// 建立一個HeroCard
	var herocard = new HeroCard()
	{
		Title = "xxx飯店",
		Text = "5星級高級大飯店",
		Images = new List<CardImage>()
				{
					   new CardImage("https://cdn.pixabay.com/photo/2016/02/10/13/32/hotel-1191709_1280.jpg")
				},
		Buttons = new List<CardAction>()
				{
					new CardAction("openUrl", "官網", value: "http://www.google.com")
				}
	};

	context.Done(herocard);

	return Task.CompletedTask;
}

這邊我們沒有真的搜索資料庫,但是可以想象一下,這邊可以詢問使用者條件然後把結果回傳,甚至回傳多筆讓使用者選擇那筆。不過,這個部分就給各位去發揮啦。

最後我們要在RootDialog做一些調整:

...
if (activity.Text == "查飯店")
{
	context.Call(new SearchHotelDialog(), async (ctx, r) =>
	{
		var returnMessage = activity.CreateReply();
		var heroCardResult = await r;
		returnMessage.Attachments = new List<Attachment>(){ heroCardResult.ToAttachment() };

		await context.PostAsync(returnMessage);
		context.Wait(MessageReceivedAsync);
	});
}
...

最後我們測試,發現結果和之前一樣。

取得訂房的費用明細

再來我們重構取得費用明細,先建立一個GetReciptDialog,這個Dialog會回傳一個Attachment

[Serializable]
public class GetReceiptDialog : IDialog<Attachment>
{
	public Task StartAsync(IDialogContext context)
	{
		throw new NotImplementedException();
	}
}

再來把邏輯搬進去:

public Task StartAsync(IDialogContext context)
{
	var receiptCard = new ReceiptCard()
			{
				Title = "訂房費用",
				Total = "NT$ 120",
				Tax = "NT$ 20",
				Items = new List<ReceiptItem>()
				   {
					   new ReceiptItem()
					   {
							Title = "1大房",
							Price = "90",
							Quantity = "1",
							Image = new CardImage("https://cdn.pixabay.com/photo/2014/08/11/21/40/wall-416062__180.jpg")
					   },
					   new ReceiptItem()
					   {
						   Title = "飲料",
						   Price = "10",
						   Quantity = "1",
						   Image = new CardImage("https://cdn.pixabay.com/photo/2014/09/26/19/51/coca-cola-462776_1280.jpg")
					   }
				   }
			};

	context.Done(receiptCard.ToAttachment());

	return Task.CompletedTask;
}

這次我回傳不是ReceiptItem而是一個Attachment,因爲Attachment是更加generic的形態,我們剛剛那個查飯店其實應該也要調整,這個我們等一下處理。

接下來我們調整RootDialog,這邊我們把關鍵字改成明細比較清楚:

...
else if(activity.Text == "明細")
{
	context.Call(new GetReceiptDialog(), async (ctx, r) =>
	{
		var returnMessage = activity.CreateReply();
		var attachmentResult = await r;
		returnMessage.Attachments = new List<Attachment>() { attachmentResult };

		await context.PostAsync(returnMessage);
		context.Wait(MessageReceivedAsync);
	});
}
...

接下來測試一下,發現結果一樣。

查飯店v2調整

還記得我們之前爲了顯示AdaptiveCard有做了一個v2版本的查飯店。

我們把他和SearchHotelDialog整合,改成一次回傳兩家飯店,一個用HeroCard,一個用AdaptiveCard

首先,我們調整SearchHotelDialog的回傳内容,從HeroCard改成List <Attachment>

public class SearchHotelDialog : IDialog<List<Attachment>>

再來調整dialog裡面的邏輯:

public Task StartAsync(IDialogContext context)
{
	// 返回飯店的圖片以及可以打開官網的按鈕

	// 建立一個HeroCard
	HeroCard herocard = GetHeroCard();

	AdaptiveCard adCard = GetAdaptiveCard();

	context.Done(new List<Attachment>() { herocard.ToAttachment(),
			new Attachment()
			{
				Content = adCard,
				ContentType = AdaptiveCard.ContentType
			}
	 });

	return Task.CompletedTask;
}

最後調整RootDialog,把查飯店v2刪掉,然後調整查飯店取得的回傳形態:

if (activity.Text == "查飯店")
{
	context.Call(new SearchHotelDialog(), async (ctx, r) =>
	{
		var returnMessage = activity.CreateReply();
		var attachments = await r;
		returnMessage.Attachments = attachments;

		await context.PostAsync(returnMessage);
		context.Wait(MessageReceivedAsync);
	});
}

最後,我們可以看到,輸入了查飯店,可以看到兩個版本的飯店:

Bot Framework Emulator_2018-07-14_23-00-18.png
最後輸出結果

訂房調整

我們的訂房使用的是FormFlow來產生,這一整個邏輯也可以重構到dialog裡面,做法和其他一樣,先建立一個ReserveRoomDialog

[Serializable]
public class ReserveRoomDialog : IDialog<RoomReservation>
{
	public Task StartAsync(IDialogContext context)
	{
		throw new NotImplementedException();
	}
}

再來,我們把邏輯放到了Dialog裡面:

public Task StartAsync(IDialogContext context)
{
	var reserveRoomForm =
				FormDialog.FromForm(RoomReservation.BuildForm,
					FormOptions.PromptInStart);

	context.Call(reserveRoomForm, AfterReserveRoomAsync);

	return Task.CompletedTask;
}

private async Task AfterReserveRoomAsync(IDialogContext context
   , IAwaitable<RoomReservation> result)
{
	RoomReservation reservation = null;

	try
	{
		reservation = await result;

		//await context.PostAsync($"得到的結果:{Environment.NewLine} {JsonConvert.SerializeObject(reservation)}");
	}
	catch (FormCanceledException<RoomReservation> ex)
	{
		string reply;

		if (ex.InnerException == null)
		{
			reply = $"您在 {ex.Last} 的時候退出了 -- 如果有遇到任何問題請告訴我們";
		}
		else
		{
			reply = "機器人暫時罷工了,請稍後嘗試";
		}

		await context.PostAsync(reply);
	}
	finally
	{
		context.Done(reservation);
	}
}
由於回傳的是RoomReservation,因此當使用者退出的時候,直接在這邊輸出内容了。但是,既然回傳的是一個物件,那麽更好的做法應該是一個包含RoomReservation的物件,裡面有資訊可以記錄 像是退出錯誤的訊息。這個就留給大家嘗試。

最後,調整Rootdialog:

...
else if(activity.Text == "訂房")
{
	context.Call(new ReserveRoomDialog(), ReserverRoomAfterAsync);
}

private async Task ReserverRoomAfterAsync(IDialogContext context,
   IAwaitable<RoomReservation> result)
{
	var roomReserved = await result;

	if (roomReserved != null)
	{
		await context.PostAsync($"您的訂單資訊:{Environment.NewLine}" +
			$"{JsonConvert.SerializeObject(roomReserved, Formatting.Indented)}");
	}
	else
	{
		await context.PostAsync($"訂單取得失敗");
	}

	context.Wait(MessageReceivedAsync);
}
這邊把關鍵字也調整變成訂房

内建的Dialog - 範例 Prompt

透過這幾次的重構,相信對於Dialog的建立有了概念,并且這些Dialog是可以簡單重複使用。

這邊舉個簡單例子,内建有個Dialog能夠讓我們方便取得使用者的回復,舉例來説,訂房最後應該有個確認輸入,這個時候我們可以使用内建的Dialog,Prompt

private async Task ReserverRoomAfterAsync(IDialogContext context,
   IAwaitable<RoomReservation> result)
{
	var roomReserved = await result;

	if (roomReserved != null)
	{
		await context.PostAsync($"您的訂單資訊:{Environment.NewLine}" +
			$"{JsonConvert.SerializeObject(roomReserved, Formatting.Indented)}");

		PromptDialog.Confirm(context, ConfirmReservation, "請確認訂房資訊");
	}
	else
	{
		await context.PostAsync($"訂單取得失敗");

		context.Wait(MessageReceivedAsync);
	}
}

private async Task ConfirmReservation(IDialogContext context, IAwaitable<bool> result)
{
	var confirmResult = await result;

	if(confirmResult)
	{
		await context.PostAsync($"訂單完成。訂單號:{DateTime.Now.Ticks}");
	}
	else
	{
		await context.PostAsync("訂房取消");
	}

	context.Wait(MessageReceivedAsync);
}

最後我們看到輸入完表單了之後,會有個確認的訊息:

Bot Framework Emulator_2018-07-14_23-54-36.png
最後確認畫面

由這個範例看出,我們可以透過重構IDialog讓我們邏輯能夠重複使用,這樣就不用一直寫這些utility功能。

結語

到目前爲止,我們已經把整個Bot Builder SDK的主要component瞭解的7788了。我們從訊息格式瞭解起,知道用Activity作爲統一的格式,然後透過Rich Card的輸出内容讓我們可以讓輸出變得更加漂亮。

再來我們看了FormFlow,用它來快速建立表單輸入的方式。

最後介紹了用Dialog來拆分邏輯,并且介紹了其中一個内建的Dialog,PromptDialog

Bot Builder SDK還有一些細節,例如透過Scoreable來處理一些特殊關鍵字,不過這個以後有機會在介紹。

對於開發一個Chatbot目前的知識量已經足夠了,并且我們透過這些知識做出了一個訂房的chatbot,接下來我們就可以考慮把他散佈出去讓大家來給我們一些feedback。

因此,在下一篇(),我們來看看如何部署我們的chatbot。


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