Expire a conversation

APPLIES TO: SDK v4

A bot sometimes needs to restart a conversation from the beginning. For instance, if a user doesn't respond after a certain period of time. This article describes two methods for expiring a conversation:

  • Track the last time a message was received from a user, and clear state if the time is greater than a preconfigured length upon receiving the next message from the user. For more information, see the user interaction expiration section.
  • Use a storage layer feature, such as Cosmos DB Time To Live (TTL), to automatically clear state after a preconfigured length of time. For more information, see the storage expiration section.

Note

The Bot Framework Python and Java SDKs are being retired with final long-term support ending in November 2023. Only critical security and bug fixes within this repository will be undertaken. Existing bots built with these SDKs will continue to function.

For new bot development, consider using Power Virtual Agents. For more information, see The future of bot building.

Prerequisites

About this sample

The sample code in this article begins with the structure of a multi-turn bot, and extends that bot's functionality by adding additional code (provided in the following sections). This extended code demonstrates how to clear conversation state after a certain time period has passed.

User Interaction Expiration

This type of expiring conversation is accomplished by adding a last accessed time property to the bot's conversation state. This property value is then compared to the current time within the activity handler before processing activities.

Note

This example uses a 30 second timeout for ease of testing this pattern.

appsettings.json

First, add an ExpireAfterSeconds setting to appsettings.json:

{
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "ExpireAfterSeconds": 30
}

Bots\DialogBot.cs

Next, add ExpireAfterSeconds, LastAccessedTimeProperty, and DialogStateProperty fields to the bot class and initialize them in the bot's constructor. Also add an IConfiguration parameter to the constructor with which to retrieve the ExpireAfterSeconds value.

Instead of creating the dialog state property accessor inline in the OnMessageActivityAsync method, you're creating and recording it at initialization time. The bot will need the state property accessor not only to run the dialog, but also to clear the dialog state.

protected readonly int ExpireAfterSeconds;
protected readonly IStatePropertyAccessor<DateTime> LastAccessedTimeProperty;
protected readonly IStatePropertyAccessor<DialogState> DialogStateProperty;

// Existing fields omitted...

public DialogBot(IConfiguration configuration, ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger)
{
    ConversationState = conversationState;
    UserState = userState;
    Dialog = dialog;
    Logger = logger;

    ExpireAfterSeconds = configuration.GetValue<int>("ExpireAfterSeconds");
    DialogStateProperty = ConversationState.CreateProperty<DialogState>(nameof(DialogState));
    LastAccessedTimeProperty = ConversationState.CreateProperty<DateTime>(nameof(LastAccessedTimeProperty));
}

Finally, add code to the bot's OnTurnAsync method to clear the dialog state if the conversation is too old.

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
    // Retrieve the property value, and compare it to the current time.
    var lastAccess = await LastAccessedTimeProperty.GetAsync(turnContext, () => DateTime.UtcNow, cancellationToken).ConfigureAwait(false);
    if ((DateTime.UtcNow - lastAccess) >= TimeSpan.FromSeconds(ExpireAfterSeconds))
    {
        // Notify the user that the conversation is being restarted.
        await turnContext.SendActivityAsync("Welcome back!  Let's start over from the beginning.").ConfigureAwait(false);

        // Clear state.
        await ConversationState.ClearStateAsync(turnContext, cancellationToken).ConfigureAwait(false);
    }

    await base.OnTurnAsync(turnContext, cancellationToken).ConfigureAwait(false);

    // Set LastAccessedTime to the current time.
    await LastAccessedTimeProperty.SetAsync(turnContext, DateTime.UtcNow, cancellationToken).ConfigureAwait(false);

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

Storage Expiration

Cosmos DB provides a Time To Live (TTL) feature that allows you to delete items automatically from a container after a certain time period. This can be configured from within the Azure portal or during container creation (using the language-specific Cosmos DB SDKs).

The Bot Framework SDK doesn't expose a TTL configuration setting. However, container initialization can be overridden and the Cosmos DB SDK can be used to configure TTL prior to Bot Framework storage initialization.

Start with a fresh copy of the multi-turn prompt sample, and add the Microsoft.Bot.Builder.Azure NuGet package to the project.

appsettings.json

Update appsettings.json to include Cosmos DB storage options:

{
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",

  "CosmosDbTimeToLive": 30,
  "CosmosDbEndpoint": "<endpoint-for-your-cosmosdb-instance>",
  "CosmosDbAuthKey": "<your-cosmosdb-auth-key>",
  "CosmosDbDatabaseId": "<your-database-id>",
  "CosmosDbUserStateContainerId": "<no-ttl-container-id>",
  "CosmosDbConversationStateContainerId": "<ttl-container-id>"
}

Notice the two ContainerIds, one for UserState and one for ConversationState. The default TTL is set on the ConversationState container, but not on UserState.

CosmosDbStorageInitializerHostedService.cs

Next, create a CosmosDbStorageInitializerHostedService class, which will create the container with the configured Time To Live.

// Add required using statements...

public class CosmosDbStorageInitializerHostedService : IHostedService
{
    readonly CosmosDbPartitionedStorageOptions _storageOptions;
    readonly int _cosmosDbTimeToLive;

    public CosmosDbStorageInitializerHostedService(IConfiguration config)
    {
        _storageOptions = new CosmosDbPartitionedStorageOptions()
        {
            CosmosDbEndpoint = config["CosmosDbEndpoint"],
            AuthKey = config["CosmosDbAuthKey"],
            DatabaseId = config["CosmosDbDatabaseId"],
            ContainerId = config["CosmosDbConversationStateContainerId"]
        };

        _cosmosDbTimeToLive = config.GetValue<int>("CosmosDbTimeToLive");
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        using (var client = new CosmosClient(
            _storageOptions.CosmosDbEndpoint,
            _storageOptions.AuthKey,
            _storageOptions.CosmosClientOptions ?? new CosmosClientOptions()))
        {
            // Create the contaier with the provided TTL
            var containerResponse = await client
                .GetDatabase(_storageOptions.DatabaseId)
                .DefineContainer(_storageOptions.ContainerId, "/id")
                .WithDefaultTimeToLive(_cosmosDbTimeToLive)
                .WithIndexingPolicy().WithAutomaticIndexing(false).Attach()
                .CreateIfNotExistsAsync(_storageOptions.ContainerThroughput)
                .ConfigureAwait(false);
        }
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

Startup.cs

Lastly, update Startup.cs to use the storage initializer, and Cosmos DB for state:

// Existing code omitted...

// commented out MemoryStorage, since we are using CosmosDbPartitionedStorage instead
// services.AddSingleton<IStorage, MemoryStorage>();

// Add the Initializer as a HostedService (so it's called during the app service startup)
services.AddHostedService<CosmosDbStorageInitializerHostedService>();

// Create the storage options for User state
var userStorageOptions = new CosmosDbPartitionedStorageOptions()
{
    CosmosDbEndpoint = Configuration["CosmosDbEndpoint"],
    AuthKey = Configuration["CosmosDbAuthKey"],
    DatabaseId = Configuration["CosmosDbDatabaseId"],
    ContainerId = Configuration["CosmosDbUserStateContainerId"]
};

// Create the User state. (Used in this bot's Dialog implementation.)
services.AddSingleton(new UserState(new CosmosDbPartitionedStorage(userStorageOptions)));

// Create the storage options for Conversation state
var conversationStorageOptions = new CosmosDbPartitionedStorageOptions()
{
    CosmosDbEndpoint = Configuration["CosmosDbEndpoint"],
    AuthKey = Configuration["CosmosDbAuthKey"],
    DatabaseId = Configuration["CosmosDbDatabaseId"],
    ContainerId = Configuration["CosmosDbConversationStateContainerId"]
};

// Create the Conversation state. (Used by the Dialog system itself.)
services.AddSingleton(new ConversationState(new CosmosDbPartitionedStorage(conversationStorageOptions)));

// Existing code omitted...

Cosmos DB will now automatically delete conversation state records after 30 seconds of inactivity.

For more information, see Configure time to live in Azure Cosmos DB

To test the bot

  1. If you haven't done so already, install the Bot Framework Emulator.
  2. Run the sample locally on your machine.
  3. Start the Emulator, connect to your bot, and send a message to it.
  4. After one of the prompts, wait 30 seconds before responding.