Alan Tsai 的學習筆記


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

[Bot Framework V4][06]使用BotState儲存使用者的相關信息

[Bot Framework V4][06]使用BotState儲存使用者的相關信息.jpg
圖片來源:https://pixabay.com/en/books-spine-colors-pastel-1099067/ 

上一篇([05]搞懂Bot的State Management - 怎麽儲存信息)看完了一堆理論了之後,相信對於整個V4的BotState有個比較清楚的概念 - 爲什麽要用Accessor,整體的串接以及需要哪些原件。

這一篇將在加深這個部分的印象,如果Bot能夠記得使用者的姓名將會給一個不同的使用者體驗。

看看怎麽在UserState裡面儲存内容。

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

整個功能的流程

要開始寫Code之前要知道整個流程是如何才有辦法撰寫,這邊用文字簡單描述一下:

  1. 當使用者剛進來的時候,會先判斷是否知道它的姓名
  2. 如果不知道的話,就會詢問他的姓名
  3. 如果知道的話,就直接保留之前的echo功能,回傳姓名+得到的信息内容

大概知道流程是什麽了之後,接下來就是實際的撰寫,大概會分幾個階段:

  1. 建立一個POCO用來代表儲存使用者的信息
  2. 增加Accessor - 用來存取這個設定
  3. 網站啓動的時候注冊UserState用到的storage
  4. 調整信息處理的程式碼
  5. 測試

建立一個POCO用來代表儲存使用者的信息

首先第一步是需要儲存信息的POCO - 有了這個POCO在整個程式撰寫才能夠使用强行別。

因此,首先加一個Model的資料夾 - 這個資料夾能夠把POCO都放在裡面比較好區隔。

然後建立一個POCO叫做UserInfo.cs的class。

public class UserInfo
{
	public string Name { get; set; }
}

增加Accessor

有了Model之後,就要建立一個可以在BotState存取的Accessor。這邊將沿用EchoBot裡面的EchoBotAccessors.cs去做修改。

首先,要有UserState的物件。還記得如果是User相關信息要儲存在UserState,因此先調整Constructor加入UserState的注入:

public UserState UserState { get; }

public EchoBotAccessors(ConversationState conversationState,
 UserState userState)
{
	ConversationState = conversationState ??
		throw new ArgumentNullException(nameof(conversationState));
		
	UserState = userState ??
		throw new ArgumentNullException(nameof(userState));
}

再來記得Accessors需要一個字串用來作爲實際儲存的時候區別的key,因此建立一個代表要儲存的字串,以及實際的Accessor:

public static string UserInfoName { get; } = 
	$"{nameof(EchoBotAccessors)}.UserInfoName";

public IStatePropertyAccessor<UserInfo> UserInfo { get; set; }

到這邊Accessor就准備好了。

網站啓動的時候注冊UserState用到的storage

Accessor准備好了,但是實際上沒有設定UserState因此執行起來會有問題。

這個時候就要回去Startup.cs來注冊這些信息進去。

首先是用MemoryStorageUserState建立出實例,并且注冊進去:

services.AddBot<EchoWithCounterBot>(options =>
{
	....

	IStorage dataStore = new MemoryStorage();

	var userState = new UserState(dataStore);
	options.State.Add(userState);
});

再來是注冊EchoBotAccessors為Singleton的時候,要調整new的參數:

services.AddSingleton<EchoBotAccessors>(sp =>
{
	...
	var userState = options.State.OfType<UserState>().FirstOrDefault();

	var accessors = new EchoBotAccessors(conversationState, userState)
	{
		...
		UserInfo = userState.CreateProperty<UserInfo>(EchoBotAccessors.UserInfoName)
	};

	return accessors;
});

到這邊,UserState的建立以及Accessor也都準備好了,接下來就是實際code的部分了。

調整信息處理的程式碼

首先,在信息來的時候,看看有沒有姓名,如果有的話依照之前的邏輯(加上姓名作爲輸出的一部分),沒有的話詢問他的尊姓大名:

public async Task OnTurnAsync(ITurnContext turnContext,
	CancellationToken cancellationToken = default(CancellationToken))
{
	if (turnContext.Activity.Type == ActivityTypes.Message)
	{
		var state = await _accessors.CounterState
		.GetAsync(turnContext, () => new CounterState());

		var userInfo = await _accessors.UserInfo
		.GetAsync(turnContext, () => new Model.UserInfo());

		if (string.IsNullOrEmpty(userInfo.Name))
		{
			await turnContext.SendActivityAsync("請問尊姓大名?");
		}
		else
		{
			// Bump the turn count for this conversation.
			state.TurnCount++;

			// Set the property using the accessor.
			await _accessors.CounterState.SetAsync(turnContext, state);

			// Save the new turn count into the conversation state.
			await _accessors.ConversationState.SaveChangesAsync(turnContext);

			// Echo back to the user whatever they typed.
			var responseMessage = $"Name: {userInfo.Name} Turn {state.TurnCount}: You sent '{turnContext.Activity.Text}'\n";
			await turnContext.SendActivityAsync(responseMessage);
		}
	}
	....
}

這個時候會遇到一個問題,怎麽知道使用者的回答是對應到詢問姓名這個部分?,換句話説,怎麽知道目前使用者發出的信息是告訴bot他的姓名?

這也是ConversationStateUserState的差別,ConversationState可以用來儲存這種和信息有關的狀態欄位。

這邊偷懶一下,直接使用CounterState.cs作爲儲存的POCO,直接增加一個property:

public class CounterState
{
	...
	public string CurrentConversationFlow { get; set; } = "askName";
}

有了這個Property之後,調整一下剛剛的if判斷:

if (string.IsNullOrEmpty(userInfo.Name) 
		&& state.CurrentConversationFlow == "askName")
{
	state.CurrentConversationFlow = "getName";

	await _accessors.CounterState.SetAsync(turnContext, state);
	await _accessors.ConversationState.SaveChangesAsync(turnContext);

	await turnContext.SendActivityAsync("請問尊姓大名?");
}
else if(state.CurrentConversationFlow == "getName")
{
	userInfo.Name = turnContext.Activity.Text;
	state.CurrentConversationFlow = "done";

	await _accessors.UserInfo.SetAsync(turnContext, userInfo);
	await _accessors.UserState.SaveChangesAsync(turnContext);

	await _accessors.CounterState.SetAsync(turnContext, state);
	await _accessors.ConversationState.SaveChangesAsync(turnContext);

	await turnContext.SendActivityAsync($"{userInfo.Name} 您好");
}
else
{
......
}

到這邊就修改完了,最後就是測試啦。

測試

透過Ctrl+F5 run起來了之後,在透過Emulator就可以看到以下結果啦:

Bot Framework Emulator_2018-10-22_20-03-37.png
測試結果

可以看到整個流程符合一開始的要求。

結語

這篇介紹了實際建立一個UserState用來儲存目前使用者的信息。

透過上一篇的理論搭配這一篇的實作,相信對於BotState的使用非常清楚了。

透過目前的知識其實就已經可以開始開發一個好的Chatbot,不過等等,透過conversation state然後if else來控制信息流程好像不太耶。不管從維護的角度還是撰寫的角度。

難道沒有更好控制流程的方式嗎?

下一篇([07]Dialog - 控制流程的元件介紹)來看看V4裡面另外一個重要的原件,看看怎麽讓流程控制變得簡單。


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