• 4 min read

Bot conversation history with Azure Cosmos DB

The Bot Framework provides a service for tracking the context of a conversation, called Bot Framework State. It enables you to store and retrieve data associated with a user, conversation or a specific user within the context of a conversation.

The Bot Framework provides a service for tracking the context of a conversation, called Bot Framework State. It enables you to store and retrieve data associated with a user, conversation or a specific user within the context of a conversation.

In this article, it is assumed you have previous experience with developing bots using Bot Builder SDK (C# or Node.js), so we will not get into the details in terms of bot implementation.

Before using Cosmos DB, it’s helpful to understand the importance of storing conversation data and how it is stored in Bot Framework State.

Why store conversation data?

Some scenarios where conversation data can be useful:

  • Analytics: When you want to analyze user data and conversations in near real time. You can also apply Machine Learning models and tools (like Microsoft Cognitive Services APIs).
    Some examples:

    • Sentiment analysis to track the quality of a conversation  
    • Funnel analysis of messages in bots to identify where the Natural Language Processing (as LUIS) has failed or can be improved to better handle input messages
    • Metrics: Number of active or new users and message counts (to determine the level of engagement the bot has with users)
  • Audit: When you have to store data of all users for audit purposes. It can even be a requirement, depending on your solution.

How conversation data is stored

The conversation data is stored in three different structures (in JSON format), known as property bags:

  • UserData: It's the user property bag, where the ID is the user ID. It stores user data globally across all conversations. It's useful for storing data about the user that isn't dependent on a specific conversation. For example, you can track all conversations of a specific user and get additional information (username, birth date and so on):
{
     "id": "emulator:userdefault-user",
     "botId": "",
     "channelId": "emulator",
     "conversationId": "",
     "userId": "",
     "data": {
         "username": "Fernando de Oliveira"
     },
     "_rid": "9G5GANrnJQADAAAAAAAAAA==",
     "_self": "dbs/9G5GAA==/colls/9G5GANrnJQA=/docs/9G5GANrnJQADAAAAAAAAAA==/",
     "_etag": ""01008737-0000-0000-0000-5993a11d0000"",
     "_attachments": "attachments/",
     "_ts": 1502847257
}

This is an example of how you can save data to UserData in C#:

private bool userWelcomed;

public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable result)
{
         var message = await result;

        string userName;

        if (!context.UserData.TryGetValue("username", out userName))
         {
             PromptDialog.Text(context, ResumeAfterPrompt, "Before get started, please tell me your name?");
             return;
         }

        if (!userWelcomed)
         {
             userWelcomed = true;
             await context.PostAsync($"Welcome back {userName}!");

            context.Wait(MessageReceivedAsync);
         }
}

private async Task ResumeAfterPrompt(IDialogContext context, IAwaitable result)
{
     try
     {
         var userName = await result;
         userWelcomed = true;

        await context.PostAsync($"Welcome {userName}!");

        context.UserData.SetValue("username", userName);
     }
     catch (TooManyAttemptsException ex)
     {
         await context.PostAsync($"Oops! Something went wrong :( Technical Details: {ex}");
     }

    context.Wait(MessageReceivedAsync);
}
  • ConversationData: It's the conversation property bag, where the ID is the conversation ID. It stores data related to a single conversation globally. This data is visible to all users within the conversation. For example, a group conversation where you can set a default language to your bot (the language your bot will understand and interact with group members):
{
     "id": "emulator:conversation",
     "botId": "",
     "channelId": "emulator",
     "conversationId": "",
     "userId": "default-user",
     "data": {
         "defaultLanguage": "pt-BR"
     },
     "_rid": "9G5GANrnJQAEAAAAAAAAAA==",
     "_self": "dbs/9G5GAA==/colls/9G5GANrnJQA=/docs/9G5GANrnJQAEAAAAAAAAAA==/",
     "_etag": ""0800357b-0000-0000-0000-598b52060000"",
     "_attachments": "attachments/",
     "_ts": 1502302725
}

This is an example of how you can save data to ConversationData:

public async Task StartAsync(IDialogContext context)
{
     string language;

    if (!context.ConversationData.TryGetValue("defaultLanguage", out language))
     {
         language = "pt-BR";
         context.ConversationData.SetValue("defaultLanguage", country);
     }

    await context.PostAsync($"Hi! I'm currently configured for {language} language.");

    context.Wait(MessageReceivedAsync);
}
  • PrivateConversationData: It's the private conversation property bag, where the ID is a merge of user ID and conversation ID. It stores data related to a single conversation globally, where the data is visible only to the current user within the conversation. It's useful for storing temporary data that you want to be cleaned up when a conversation ends (like a browser cache). For example a bot for online purchases, where you can save an order ID:
{
     "id": "emulator:private:default-user",
     "botId": "",
     "channelId": "emulator",
     "conversationId": "",
     "userId": "default-user",
     "data": {
         "ResumptionContext": {
             "locale": pt-BR",
             "isTrustedServiceUrl": false
         },
         "DialogState": "",
         "orderId": ""
     },
     "_rid": "9G5GANrnJQAXAAAAAAAAAA==",
     "_self": "dbs/9G5GAA==/colls/9G5GANrnJQA=/docs/9G5GANrnJQAXAAAAAAAAAA==/",
     "_etag": ""0100f938-0000-0000-0000-5993ab090000"",
     "_attachments": "attachments/",
     "_ts": 1502849796
}  

This is an example of how you can save data to PrivateConversationData:

string orderId;

if (!context.PrivateConversationData.TryGetValue("orderId", out orderId))
{
     // Generic method to generate an order ID
     orderId = await GetOrderIdAsync();

    context.PrivateConversationData.SetValue("orderId", orderId);

    await context.PostAsync($"{userName}, this is your order ID: {orderId}");
}

Cosmos DB rather than Bot Framework State

By default, Bot Framework uses the Bot Framework State to store conversation data. It is designed for prototyping, and is useful for development and testing environments. At the time of this writing, it has only a size limit of 32KB. Learn more about data management.

For production environments, it’s highly recommended to use a NoSQL database to store data as documents, such as Azure Cosmos DB. It's a multi-model database (like document, graph, key-value, table and column-family models) that can offer some key benefits, including:

  • Global distribution: It's possible to distribute your data across different Azure Regions, ensuring low latency to users.
  • Horizontal Scalability: You can easily scale your database at a per second granularity and scale storage size up and down automatically according to your needs.
  • Availability: You can ensure your database will have at least 99.99% availability in a single region.

For document models, there are options like Azure DocumentDB and MongoDB. In this article we are going to use the DocumentDB API.

Storing Conversation Data

To customize your bot conversation data storage, you can use the Bot Builder SDK Azure Extensions. If you are developing your bot with Bot Builder SDK in C#, you have to edit the Global.asax file:

protected void Application_Start()
{
     // Adding DocumentDB endpoint and primary key
     var docDbServiceEndpoint = new Uri("");
     var docDbKey = "";

    // Creating a data store based on DocumentDB
     var store = new DocumentDbBotDataStore(docDbServiceEndpoint, docDbKey);

    // Adding Azure dependencies to your bot (documentDB data store and Azure module)
     var builder = new ContainerBuilder();

    builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));           
    
     // Key_DataStore is the key for data store register with the container
     builder.Register(c => store)
         .Keyed>(AzureModule.Key_DataStore)
         .AsSelf()
         .SingleInstance();

    // After adding new dependencies, update the container
     builder.Update(Conversation.Container);

    GlobalConfiguration.Configure(WebApiConfig.Register);
}

If you run your bot and open your Cosmos DB service on Azure Portal, you can see all stored documents (clicking on Data Explorer).

data-explorer

References