上一篇([05]搞懂Bot的State Management - 怎麽儲存信息)看完了一堆理論了之後,相信對於整個V4的BotState有個比較清楚的概念 - 爲什麽要用Accessor,整體的串接以及需要哪些原件。
這一篇將在加深這個部分的印象,如果Bot能夠記得使用者的姓名將會給一個不同的使用者體驗。
看看怎麽在UserState
裡面儲存内容。
整個功能的流程
要開始寫Code之前要知道整個流程是如何才有辦法撰寫,這邊用文字簡單描述一下:
- 當使用者剛進來的時候,會先判斷是否知道它的姓名
- 如果不知道的話,就會詢問他的姓名
- 如果知道的話,就直接保留之前的echo功能,回傳姓名+得到的信息内容
大概知道流程是什麽了之後,接下來就是實際的撰寫,大概會分幾個階段:
- 建立一個POCO用來代表儲存使用者的信息
- 增加Accessor - 用來存取這個設定
- 網站啓動的時候注冊UserState用到的storage
- 調整信息處理的程式碼
- 測試
建立一個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
來注冊這些信息進去。
首先是用MemoryStorage
把UserState
建立出實例,并且注冊進去:
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他的姓名?
這也是ConversationState
和UserState
的差別,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就可以看到以下結果啦:
可以看到整個流程符合一開始的要求。
結語
這篇介紹了實際建立一個UserState
用來儲存目前使用者的信息。
透過上一篇的理論搭配這一篇的實作,相信對於BotState
的使用非常清楚了。
透過目前的知識其實就已經可以開始開發一個好的Chatbot,不過等等,透過conversation state然後if else來控制信息流程好像不太耶。不管從維護的角度還是撰寫的角度。
難道沒有更好控制流程的方式嗎?
下一篇([07]Dialog - 控制流程的元件介紹)來看看V4裡面另外一個重要的原件,看看怎麽讓流程控制變得簡單。