Alan Tsai 的學習筆記


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