在上一篇介紹了怎麽使用IDialog來拆分邏輯,并且一步一步的用取得名字的邏輯拆成為一個NameDialog。
在這一篇我們將會把所有的邏輯重構成爲IDialog,并且對於目前學習到的BotBuilder做一個階段性的總結。
上篇重構完了取得名字的邏輯之後,我們還剩下幾個部分需要重構:
查飯店
取得訂房的費用明細
訂房
重上一篇的重構方式我們可以看出,整個重構的步奏分爲:
建立一個Dialog
把邏輯從root放到dialog裡面
修正root為呼叫dialog來觸發
測試
接下來我們就快速看一下這幾個邏輯拆分的程式部分。
首先我們建立一個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);
});
}
...
接下來測試一下,發現結果一樣。
還記得我們之前爲了顯示AdaptiveCard有做了一個v2版本的查飯店。
我們把他和SearchHotelDialog整合,改成一次回傳兩家飯店,一個用HeroCard,一個用AdaptiveCard。
首先,我們調整SearchHotelDialog的回傳内容,從HeroCard改成List
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);
});
}
最後,我們可以看到,輸入了查飯店,可以看到兩個版本的飯店:
最後輸出結果
我們的訂房使用的是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的建立有了概念,并且這些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);
}
最後我們看到輸入完表單了之後,會有個確認的訊息:
最後確認畫面
由這個範例看出,我們可以透過重構IDialog讓我們邏輯能夠重複使用,這樣就不用一直寫這些utility功能。
到目前爲止,我們已經把整個BotBuilder的主要component瞭解的7788了。我們從訊息格式瞭解起,知道用Activity作爲統一的格式,然後透過Rich Card的輸出内容讓我們可以讓輸出變得更加漂亮。
再來我們看了FormFlow,用它來快速建立表單輸入的方式。
最後介紹了用Dialog來拆分邏輯,并且介紹了其中一個内建的Dialog,PromptDialog。
BotBuilder還有一些細節,例如透過Scoreable來處理一些特殊關鍵字,不過這個以後有機會在介紹。
對於開發一個Chatbot目前的知識量已經足夠了,并且我們透過這些知識做出了一個訂房的chatbot,接下來我們就可以考慮把他散佈出去讓大家來給我們一些feedback。
因此,在下一篇,我們來看看如何部署我們的chatbot。