Store conversation to Cosmos DB Using Azure Bot Framework SDK V4

The Bot Framework SDK supports the preservation of the user input as it is received. Here, we will store each transaction into the memory object. The same object will be saved to the storage at the end. 
 
In this tutorial, you will learn the following.
  1. Setup a Cosmos DB.
  2. Create a basic bot.
  3. Setup configuration information to the Azure bot.
  4. Implement Cosmos DB storage to Azure bot.
  5. Build and test your bot with Emulator.
Below are the prerequisites.
 
The bot created in the previous article will be used here to help you understand how to build a basic bot.
 
Let's understand the information flow with respect to this article.


You can read and write directly to your storage object without using middleware or context object. This can be appropriate for data your bot uses to preserve a conversation or data that comes from a source outside your bot's conversation flow. In this data storage model, data is read directly from storage instead of using a state manager.
 
Step 1 - Set up Cosmos DB
  • Sign in to the Azure portal with your Azure credentials.
  • Click "Create a resource".
  • Click “Azure Cosmos DB".

  • Click Create a resource > Databases > Azure Cosmos DB
  • On the New account page, 
    • provide Subscription, 
    • Resource group information. 
    • Create a unique name for your Account Name field - this eventually becomes part of your data access URL name. 
    • For API, select Core(SQL), 
    • Provide a nearby Location to improve data access times.
  • Then click Review + Create.

Once validation has been successful, click Create.

The account creation takes a few minutes and Your Azure Cosmos DB account created.
  • Search for setting
  • Click Settings
  • Click the New Container.
  • Add Container area is displayed on the far right.
  • Fill new database id i.e. bot-cosmos-sql-db
  • Container id i.e. bot-storage
  • Partition key i.e. /id
  • Click Ok to create.

Cosmos DB container created with name bot-cosmos-sql-db


Keys to access new database collection, "bot-cosmos-sql-db" with a container id of "bot-storage." 


Step 2 - Create a basic bot
  • Browse Visual Studio 2017.
  • Select Bot Framework.
  • Select Echo Bot (Bot Framework V4).
  • Give the appropriate name and click ok to proceed.

Sample Echo Bot project will be created with basic functionality.
 
Step 3 - Setup configuration information to the Azure bot
  •  Firstly, install the NuGet package solution.

  • Add Cosmos DB configuration to Visual Studio solution. 
  1. private const string CosmosServiceEndpoint = "https://bot-cosmos-sql-db.documents.azure.com:443/";  
  2.         private const string CosmosDBKey = "vr1psMExluqGtp45XbIu4q1w2gmwfFksLr8GX4TDE5AkdRBiLE1fGmPsEoVpDXrM6qtOfLEGS25SzCO9G5XORg==";  
  3.         private const string CosmosDBDatabaseName = "bot-cosmos-sql-db";  
  4.         private const string CosmosDBCollectionName = "bot-storage";  
  5.   
  6.         // Create Cosmos DB  Storage.  
  7.         private static readonly CosmosDbStorage query = new CosmosDbStorage(new CosmosDbStorageOptions  
  8.         {  
  9.             AuthKey = CosmosDBKey,  
  10.             CollectionId = CosmosDBCollectionName,  
  11.             CosmosDBEndpoint = new Uri(CosmosServiceEndpoint),  
  12.             DatabaseId = CosmosDBDatabaseName,  
  13.         });  

Step 4 - Implement Cosmos DB storage to azure bot
 
To preserve conversation to Azure Cosmos DB, I am going to create an object which will hold properties of the same conversation, it will concatenate and store all at one instance. To get started, Create one class with basic properties. 
  • Create UtteranceLog.cs file and below properties 
  1. public class UtteranceLog : IStoreItem  
  2.    {  
  3.        // A list of things that users have said to the bot  
  4.        public List<string> UtteranceList { get; } = new List<string>();  
  5.   
  6.        // The number of conversational turns that have occurred          
  7.        public int TurnNumber { get; set; } = 0;  
  8.   
  9.        // Create concurrency control where this is used.  
  10.        public string ETag { get; set; } = "*";  
  11.    }  
It's just a screen shot of above code placement.

  • Replace OnMessageActivityAsyn method with below code section
  1. protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)  
  2.       {  
  3.           // preserve user input.  
  4.           var utterance = turnContext.Activity.Text;  
  5.           // make empty local logitems list.  
  6.           UtteranceLog logItems = null;  
  7.   
  8.           // see if there are previous messages saved in storage.  
  9.           try  
  10.           {  
  11.               string[] utteranceList = { "UtteranceLog" };  
  12.               logItems = query.ReadAsync<UtteranceLog>(utteranceList).Result?.FirstOrDefault().Value;  
  13.           }  
  14.           catch  
  15.           {  
  16.               // Inform the user an error occured.  
  17.               await turnContext.SendActivityAsync("Sorry, something went wrong reading your stored messages!");  
  18.           }  
  19.   
  20.           // If no stored messages were found, create and store a new entry.  
  21.           if (logItems is null)  
  22.           {  
  23.               // add the current utterance to a new object.  
  24.               logItems = new UtteranceLog();  
  25.               logItems.UtteranceList.Add(utterance);  
  26.               // set initial turn counter to 1.  
  27.               logItems.TurnNumber++;  
  28.   
  29.               // Show user new user message.  
  30.               await turnContext.SendActivityAsync($"Echo" + turnContext.Activity.Text);  
  31.   
  32.               // Create Dictionary object to hold received user messages.  
  33.               var changes = new Dictionary<string, object>();  
  34.               {  
  35.                   changes.Add("UtteranceLog", logItems);  
  36.               }  
  37.               try  
  38.               {  
  39.                   // Save the user message to your Storage.  
  40.                   await query.WriteAsync(changes, cancellationToken);  
  41.               }  
  42.               catch  
  43.               {  
  44.                   // Inform the user an error occured.  
  45.                   await turnContext.SendActivityAsync("Sorry, something went wrong storing your message!");  
  46.               }  
  47.           }  
  48.           // Else, our Storage already contained saved user messages, add new one to the list.  
  49.           else  
  50.           {  
  51.               // add new message to list of messages to display.  
  52.               logItems.UtteranceList.Add(utterance);  
  53.               // increment turn counter.  
  54.               logItems.TurnNumber++;  
  55.   
  56.               // show user new list of saved messages.  
  57.               await turnContext.SendActivityAsync($"Echo " + turnContext.Activity.Text);  
  58.   
  59.               // Create Dictionary object to hold new list of messages.  
  60.               var changes = new Dictionary<string, object>();  
  61.               {  
  62.                   changes.Add("UtteranceLog", logItems);  
  63.               };  
  64.   
  65.               try  
  66.               {  
  67.                   // Save new list to your Storage.  
  68.                   await query.WriteAsync(changes, cancellationToken);  
  69.               }  
  70.               catch  
  71.               {  
  72.                   // Inform the user an error occured.  
  73.                   await turnContext.SendActivityAsync("Sorry, something went wrong storing your message!");  
  74.               }  
  75.           }  
  76.       }  
Step 5 - Build and Test your bot with Emulator.
  • Press F5 to run the Visual Studio solution locally.
  • Start the bot emulator and then connect to your bot in the emulator:
  • Click the Create new bot configuration link in the emulator "Welcome" tab.
  • Fill in fields to connect to your bot, given the information on the webpage displayed when you started your bot.
  • Interact with your bot
  • Send a message to your bot. The bot will list the messages it has received

OutPut
  • Navigate to Azure Cosmos DB instance and expand the items under bot-storage.
  • Select the utterances log,
  • All asked queries turn to storage.


I hope you have enjoyed and learned something new in this article. Thanks for reading and stay tuned for the next article.

State Management with Azure Bot Framework SDKV4

In this article, I am going to talk about User State Management with Azure Bot Framework SDK V4.
 

Let's understand, what is State?

 
State is like an activity. It may not run on the same process. Bot is inherently stateless. Bot Framework SDK allows you to use state management and storage objects to manage and persist the state. State Manager provides the abstraction layer and follows the same paradigms as a modern web application.
 

Why we need state?

 
Maintaining state allows your bot to have more meaningful conversations by remembering certain things about a user or conversation. For example, if you've talked to a user previously, you can save previous information about them, so that you don't have to ask for it again.
 

What we are going to explore in this article

 
Upon receiving user input, this sample checks the stored conversation state to see if this user has previously been prompted to provide their name. If not, the user's name is requested and that input is stored within the user state. If so, the name stored within the user state is used to converse with the user and their input data, along with the time received and input channel Id, is returned back to the user.



Let’s get started with a solution.
 
Step 1 - Create Bot using VS2017 (Bot framework SDK V4 template)
  • Launch Visual Studio 2017, create a new project, and select Bot Framework.
  • Select the Echo Bot (Bot Framework V4) template.
  • Select the location and give the bot name.
  • Click OK to create the project.

Step 2 - Create User Profile and Conversation Data Class
 
Define the classes which can maintain the user information and conversation data
  1. User Profile class for the user information.
  2. Conversation Data class to gather user information.
  1. public class UserProfile  
  2. {  
  3.    public string Name { getset; }  
  4. }  


Add ConversationData.cs
  1. public class ConversationData  
  2.    {  
  3.        // The time-stamp of the most recent incoming message.  
  4.        public string Timestamp { get; set; }  
  5.   
  6.        // The ID of the user's channel.  
  7.        public string ChannelId { get; set; }  
  8.   
  9.        // Track whether we have already asked the user's name  
  10.        public bool PromptedUserForName { get; set; } = false;  
  11.    }  


Step 3 - Create User and Conversation State Object
 
Register Memory Storage that is used to create User and Conversation objects. The user and conversation state objects are created at Startup and dependency injected into the bot constructor. Other services for a bot that are registered are - a credential provider, an adapter, and the bot implementation.
  1. public void ConfigureServices(IServiceCollection services)  
  2.        {  
  3.            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);  
  4.            // Create the credential provider to be used with the Bot Framework Adapter.  
  5.            services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();  
  6.           // Create the Bot Framework Adapter.  
  7.            services.AddSingleton<IBotFrameworkHttpAdapter, BotFrameworkHttpAdapter>();  
  8.            // Create the bot as a transient. In this case the ASP Controller is expecting an IBot.  
  9.            services.AddTransient<IBot, EchoBot>();  
  10.            // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)  
  11.            services.AddSingleton<IStorage, MemoryStorage>();  
  12.            // Create the User state.  
  13.            services.AddSingleton<UserState>();             
  14.            // Create the Conversation state.  
  15.            services.AddSingleton<ConversationState>();  
  16.        }  

  1. private BotState _conversationState;  
  2.         private BotState _userState;  
  3.   
  4.         public EchoBot(ConversationState conversationState, UserState userState)  
  5.         {  
  6.             _conversationState = conversationState;  
  7.             _userState = userState;  
  8.         } 


Step 4 – Add State Property
 
Create Property method that provides a handle to the BotState object. Each state property accessor allows you to get or set the value of the associated state property. Accessor loads the property from storage and get it from the state cache.
 
Get Async to get the properly scoped key associated with the state property,
  1. var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));  
  2. var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData());  
  3.   
  4. var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));  
  5. var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile());  
Step 5 – Get State from Bot
  1. userProfile.Name is empty and conversationData.PromptedUserForName is true, we retrieve the user name provided and store this within user state.
  2. If userProfile.Name is empty and conversationData.PromptedUserForName is false, we ask for the user's name.
  3. If userProfile.Name was previously stored, we retrieve message time and channel Id from the user input, echo all data back to the user, and store the retrieved data within conversation state.
Change in EchoBot.cs add OnMessage Activity Async
  1. if (string.IsNullOrEmpty(userProfile.Name))  
  2.            {  
  3.                // First time around this is set to false, so we will prompt user for name.  
  4.                if (conversationData.PromptedUserForName)  
  5.                {  
  6.                    // Set the name to what the user provided.  
  7.                    userProfile.Name = turnContext.Activity.Text?.Trim();  
  8.   
  9.                    // Acknowledge that we got their name.  
  10.                    await turnContext.SendActivityAsync($"Thanks {userProfile.Name}. To see conversation data, type anything.");  
  11.   
  12.                    // Reset the flag to allow the bot to go though the cycle again.  
  13.                    conversationData.PromptedUserForName = false;  
  14.                }  
  15.                else  
  16.                {  
  17.                    // Prompt the user for their name.  
  18.                    await turnContext.SendActivityAsync($"What is your name?");  
  19.   
  20.                    // Set the flag to true, so we don't prompt in the next turn.  
  21.                    conversationData.PromptedUserForName = true;  
  22.                }  
  23.            }  
  24.            else  
  25.            {  
  26.                // Add message details to the conversation data.  
  27.                // Convert saved Timestamp to local DateTimeOffset, then to string for display.  
  28.                var messageTimeOffset = (DateTimeOffset)turnContext.Activity.Timestamp;  
  29.                var localMessageTime = messageTimeOffset.ToLocalTime();  
  30.                conversationData.Timestamp = localMessageTime.ToString();  
  31.                conversationData.ChannelId = turnContext.Activity.ChannelId.ToString();  
  32.   
  33.                // Display state data.  
  34.                await turnContext.SendActivityAsync($"{userProfile.Name} sent: {turnContext.Activity.Text}");  
  35.                await turnContext.SendActivityAsync($"Message received at: {conversationData.Timestamp}");  
  36.                await turnContext.SendActivityAsync($"Message received from: {conversationData.ChannelId}");  
  37.            }  
Step 6 – To save the changes async
 
Add function to save the changes in EchoBot.cs
  1. public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))  
  2.         {  
  3.             await base.OnTurnAsync(turnContext, cancellationToken);  
  4.   
  5.             // Save any state changes that might have occured during the turn.  
  6.             await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);  
  7.             await _userState.SaveChangesAsync(turnContext, false, cancellationToken);  
  8.         }  
Step 7 – Test your Bot Locally
 
Press F5 to Visual Studio Solution and Launch with Bot Emulator V4,


I hope you have enjoyed and learned something new in this article. Thanks for reading and stay tuned for the next article.