Lanna
Lanna

Reputation: 115

How to handle state and configure cosmos db storage in a nested dialog? (bot framewrok v4)

I am trying to build a chatbot with nested dialogs which is supposed to gather information from the user and store it in Azure CosmosDB. The dialog worked fine until I implemented the Cosmos DB storage. Now, with the CosmosDB storage the dialog loops on the first task in the first dialog, instead of continuing. How can I solve this problem?

Beginning with the dialogs, and how it was before implementing CosmosDB storage. I basically followed the code in this sample 43.complex-dialog.

Then, implementing the storage I used this answer as a guide. I set up the cosmosDB storage in Startup.cs like this:

public class Startup
    {

        private const string CosmosServiceEndpoint = "MyCosmosServiceEndpoint";
        private const string CosmosDBKey = "MyCosmosDBKey";
        private const string CosmosDBDatabaseName = "MyCosmosDBDatabaseName";
        private const string CosmosDBCollectionName = "MyCosmosDBCollectionName";
        private const string CosmosDBPartitionKey = "MyCosmosDBPartitionKey";
        
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;    
        }
        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            var storage = new CosmosDbStorage(new CosmosDbStorageOptions
            {
                AuthKey = CosmosDBKey,
                CollectionId = CosmosDBCollectionName,
                CosmosDBEndpoint = new Uri(CosmosServiceEndpoint),
                DatabaseId = CosmosDBDatabaseName,
                PartitionKey = CosmosDBPartitionKey
            });

            var conversationState = new ConversationState(storage);
            var userState = new UserState(storage);

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
            services.AddSingleton<MainDialog>();
            services.AddTransient<IBot, WelcomeBot<MainDialog>>();
            services.AddSingleton<IStorage, MemoryStorage>();
            services.AddSingleton(userState);
            services.AddSingleton(conversationState);
            services.AddSingleton(userState.CreateProperty<UserProfile>("MyUserState"));
            services.AddSingleton(conversationState.CreateProperty<DialogState>("MyBotDialogState"));

        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseDefaultFiles();
            app.UseStaticFiles();

            app.UseMvc();
        }
    }
}

My main bot looks like this (don't mind the name "Echobot"):

public class EchoBot<T> : ActivityHandler where T : Dialog
    {
        private readonly BotState _userState;
        private readonly BotState _conversationState;
        private readonly Dialog _dialog;
        private readonly ILogger _logger;

        private readonly IStatePropertyAccessor<UserProfile> _userStateAccessor;
        private readonly IStatePropertyAccessor<DialogState> _conversationDialogStateAccessor;

        // Create cancellation token (used by Async Write operation).
        public CancellationToken CancellationToken { get; private set; }

        public EchoBot(ConversationState conversationState, UserState userState, T dialog, ILogger<EchoBot<T>> logger, IStatePropertyAccessor<UserProfile> userStatePropertyAccessor, IStatePropertyAccessor<DialogState> dialogStatePropertyAccessor) 
        {
            _conversationState = conversationState;
            _userState = userState;
            _dialog = dialog;
            _logger = logger;
            _userStateAccessor = userStatePropertyAccessor;
            _conversationDialogStateAccessor = dialogStatePropertyAccessor;

        }

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

            var currentUserState = await _userStateAccessor.GetAsync(turnContext, () => new UserProfile(), cancellationToken);
            var currentConversationDialogState = await _conversationDialogStateAccessor.GetAsync(turnContext, () => new DialogState(), cancellationToken);

            await _userStateAccessor.SetAsync(turnContext, currentUserState, cancellationToken);
            await _conversationDialogStateAccessor.SetAsync(turnContext, currentConversationDialogState, cancellationToken);

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

            
        }

        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken=default)
        {
            _logger.LogInformation("Running dialog with Message Activity.");
            //See DialogExtension.cs in the sample "complex-dialog" to see the Run method.
            await _dialog.Run(turnContext, _conversationDialogStateAccessor, cancellationToken);

        }
    }
}

Upvotes: 1

Views: 295

Answers (1)

mdrichardson
mdrichardson

Reputation: 7241

For now, it should work if you remove the PartitionKey parameter from CosmosDbStorageOptions. You will likely need to delete your Container or use a different name, since yours is currently partitioned. Easiest to just delete your Container and let the bot make one for you.

There's currently a bug in all the Bot Builder SDKs around reading from partitioned databases when the partitionKey is supplied. Tracking the issue here

Upvotes: 2

Related Questions