Chatbot - 사용자 및 대화 데이터 저장


사용자 및 대화 데이터 저장

봇은 기본적으로 상태를 저장하지 않는다. 하지만 봇은 대화의 문맥을 파악해서 동작을 관리하고 이전 질문에 대한 사용자의 답변을 기억해야 할 경우가 생긴다. 이것을 ** Bot Framework SDK의 상태 및 스토리지 ** 기능을 사용하면 상태를 봇에 저장할 수 있다. 봇은 이 상태 관리 및 스토리지 개체를 사용하여 상태를 관리하고 유지한다. 속성 접근자를 사용하여 상태 속성에 접근할 수 있는 추상화 레이어를 제공한다고 한다.

공식 문서에서 제공하는 샘플 봇은 저장된 대화 상태를 확인해서 사용자에게 이름을 물어보는 메세지를 표시했는지 확인한다. 만약 메세지를 보내지 않았다면 사용자에게 이름을 물어보고 이 입력은 사용자 상태에 저장된다.

이름을 물어본 경우 사용자 상태에 저장된 이름을 사용해서 사용자랑 대화하고, 사용자의 입력 데이터를 수신 시간과 입력 채널 ID와 함께 사용자에게 다시 반환한다. 이 시간과 채널 ID 값은 사용자 대화 데이터에서 찾아진 후 대화 상태에 저장된다.

밑에 있는 다이어그램이 봇, 사용자 프로필, 대화 데이터 클래스간의 관계를 표현한 것이다.

statebotsample-overview

클래스 정의

상태 관리를 위해 하는 첫 번째 일은 관리하고 싶은 정보를 포함하는 클래스를 만드는 것이다.

  • UserProfile.cs : 사용자 정보에 대한 “UserProfile” 클래스를 정의한다.
  • ConversationData.cs : 사용자 정보를 수집하는 동안 대화 상태를 제어하기 위한 “ConversationData” 클래스를 정의한다.

UserProfile.cs

// Defines a state property used to track information about the user.
public class UserProfile
{
    public string Name { get; set; }
}

UserProfile 클래스를 만들고 사용자의 이름을 받기 위해 Name 이라는 String을 두었다.

ConversationData.cs

// Defines a state property used to track conversation data.
public class ConversationData
{
    // The time-stamp of the most recent incoming message.
    public string Timestamp { get; set; }

    // The ID of the user's channel.
    public string ChannelId { get; set; }

    // Track whether we have already asked the user's name
    public bool PromptedUserForName { get; set; } = false;
}

ConversationData 클래스를 만들고 Timestamp, ChannelId, 유저 이름을 받았는지 확인할 PromptedUserforname을 설정한다.

대화 및 사용자 상태 개체 만들기

다음으로 UserState와 ConversationState, 즉 상태 객체를 만드는 데 사용한 MemoryStorage를 등록한다.

** Startup.cs **

// Create the storage we'll be using for User and Conversation state.
// (Memory is great for testing purposes - examples of implementing storage with
// Azure Blob Storage or Cosmos DB are below).
var storage = new MemoryStorage();

// Create the User state passing in the storage layer.
var userState = new UserState(storage);
services.AddSingleton(userState);

// Create the Conversation state passing in the storage layer.
var conversationState = new ConversationState(storage);
services.AddSingleton(conversationState);

추가로 microsoft의 공식 github sample을 확인하니

이 storage를 azure blob storage나 cosmosdb storage로 설정할 수도 있는 것 같았다.

그런데 Azure blob과 cosmosdb를 같이 사용할 수 있는건지, 아니면 따로따로 써야하는 건지는 확실하지 않아 더 공부해야 할 듯 하다.

Azure Blob Storage

var storage = new AzureBlobStorage("<blob-storage-connection-string>", "bot-state");

Cosmosdb Storage

var cosmosDbStorageOptions = new CosmosDbPartitionedStorageOptions()
             {
                 CosmosDbEndpoint = "<endpoint-for-your-cosmosdb-instance>",
                 AuthKey = "<your-cosmosdb-auth-key>",
                 DatabaseId = "<your-database-id>",
                 ContainerId = "<cosmosdb-container-id>"
             };
             var storage = new CosmosDbPartitionedStorage(cosmosDbStorageOptions);

메모리를 등록하고, 이 메모리를 이용해서 사용자 상태와 대화 상태 객체를 만들어 addsingleton으로 등록한 것 같다.

Bots/(사용자가 만든봇).cs

private BotState _conversationState;
private BotState _userState;

public StateManagementBot(ConversationState conversationState, UserState userState)
{
    _conversationState = conversationState;
    _userState = userState;
}

생성자를 설정한다. 여기서 사용자가 만든 봇의 이름이 StateManagementBot이다.

상태 속성 접근자 추가

이제 아까 위에서 만든 BotState 타입으로 생성한 개체를 handle할 CreateProperty method를 사용해서 속성 접근자를 만든다.

각 상태 속성 접근자를 사용하면 연결된 상태 속성의 값을 가져오거나 설정할 수 있다.

상태 속성을 사용하기 전에 각 접근자를 사용해서 스토리지에서 속성을 로드하고 상태 캐시에서 이걸 가져온다.

상태 속성과 연결된 올바른 범위의 키를 가져오기 위해 GetAsync 메서드를 호출한다.

Bots/사용자가 만든봇.cs

var conversationStateAccessors =  _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));

위에서 만든 state 변수에 접근하기 위한 접근자를 만든 것으로 이해했다.

봇에서 상태 액세스

이제 초기화를 어느정도 마쳤으니 실행시 위에서 만든 접근자를 사용해서 상태 정보를 읽고 쓰는 것을 해볼 예정이다.

예제 봇에서는

  • userProfile.Name이 비어 있고 conversationData.PromptedUserForName이 True => 제공된 사용자 이름을 검색하고 사용자 상태에 저장
  • userProfile.Name이 비어 있고 conversationData.PromptedUserForName이 False => 사용자 이름 요청
  • userProfile.Name이 저장된 경우 => 사용자 입력에서 메세지 시간, 채널 ID를 검색하고 모든 데이터를 사용자에게 다시 보여주고 검색된 데이터를 대화 상태에 저장한다.

Bots/사용자가 만든봇.cs

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    // Get the state properties from the turn context.

    //아까 만든 대화 상태 접근자 (Conversation은 위에서 만든 데이터를 담을 클래스였다)
    var conversationStateAccessors =  _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
    //GetAsync 메서드를 호출해서 상태 속성과 연결된 올바른 범위의 키를 가져온다
    var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData());

    //아까 만든 사용자 상태 접근자 (UserProfile은 위에서 만든 데이터를 담을 클래스였다)
    var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
    //GetAsync 메서드를 호출해서 상태 속성과 연결된 올바른 범위의 키를 가져온다.
    var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile());

    if (string.IsNullOrEmpty(userProfile.Name))
    {
        // First time around this is set to false, so we will prompt user for name.
        if (conversationData.PromptedUserForName)
        {
            // Set the name to what the user provided.
            userProfile.Name = turnContext.Activity.Text?.Trim();

            // Acknowledge that we got their name.
            await turnContext.SendActivityAsync($"Thanks {userProfile.Name}. To see conversation data, type anything.");

            // Reset the flag to allow the bot to go through the cycle again.
            conversationData.PromptedUserForName = false;
        }
        else
        {
            // Prompt the user for their name.
            await turnContext.SendActivityAsync($"What is your name?");

            // Set the flag to true, so we don't prompt in the next turn.
            conversationData.PromptedUserForName = true;
        }
    }
    else
    {
        // Add message details to the conversation data.
        // Convert saved Timestamp to local DateTimeOffset, then to string for display.
        var messageTimeOffset = (DateTimeOffset) turnContext.Activity.Timestamp;
        var localMessageTime = messageTimeOffset.ToLocalTime();
        conversationData.Timestamp = localMessageTime.ToString();
        conversationData.ChannelId = turnContext.Activity.ChannelId.ToString();

        // Display state data.
        await turnContext.SendActivityAsync($"{userProfile.Name} sent: {turnContext.Activity.Text}");
        await turnContext.SendActivityAsync($"Message received at: {conversationData.Timestamp}");
        await turnContext.SendActivityAsync($"Message received from: {conversationData.ChannelId}");
    }
}

마지막으로 SaveChangesAsync() method를 사용해서 모든 상태 변경 내용을 스토리지에 쓴다.

**Bots/사용자가 만든 봇.cs **

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
    await base.OnTurnAsync(turnContext, cancellationToken);

    // Save any state changes that might have occurred during the turn.
    await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
}

보니까 위에서 만든 state 변수에 SaveChangesAsync라는 메서드를 사용하면 되나보다. ConversationState랑 UserState 도 azure bot에서 제공하는 타입일까?