Às vezes, um bot precisa reiniciar uma conversa desde o início. Por exemplo, se o usuário não responder após um determinado período. Este artigo descreve dois métodos para expirar uma conversa:
O código de exemplo deste artigo começa com a estrutura de um bot de várias rodadas e estende a funcionalidade deste bot ao adicionar códigos (fornecidos nas seções a seguir). Esse código estendido demonstra como limpar o estado da conversa após um determinado período.
appsettings.json
Primeiro, adicione uma configuração ExpireAfterSeconds
a appsettings.json:
{
"MicrosoftAppId": "",
"MicrosoftAppPassword": "",
"ExpireAfterSeconds": 30
}
Bots\DialogBot.cs
Em seguida, adicione campos ExpireAfterSeconds
, LastAccessedTimeProperty
e DialogStateProperty
à classe do bot e inicialize-os no construtor do bot. Adicione também um parâmetro IConfiguration
ao construtor com o qual valor ExpireAfterSeconds
será recuperado.
Em vez de criar o acessador embutido da propriedade de estado do diálogo no método OnMessageActivityAsync
, você o está criando e registrando no momento da inicialização. O bot precisará do acessador da propriedade de estado não apenas para executar o diálogo, mas também para limpar o estado do diálogo.
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));
}
Por fim, adicione código ao método OnTurnAsync
do bot para limpar o estado do diálogo se a conversa for muito antiga.
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);
}
.env
Primeiro, adicione uma configuração ExpireAfterSeconds
a .env:
MicrosoftAppId=
MicrosoftAppPassword=
ExpireAfterSeconds=30
bots\dialogBot.js
Em seguida, adicione campos a DialogBot
e atualize o construtor. Adicione campos locais para expireAfterSeconds
e lastAccessedTimeProperty
.
Adicione expireAfterSeconds
como parâmetro ao construtor e crie o StatePropertyAccessor
necessário:
constructor(expireAfterSeconds, conversationState, userState, dialog) {
// Existing code omitted...
this.lastAccessedTimeProperty = this.conversationState.createProperty('LastAccessedTime');
this.expireAfterSeconds = expireAfterSeconds;
// Existing code omitted...
}
Adicione código ao método run
do bot:
async run(context) {
// Retrieve the property value, and compare it to the current time.
const now = new Date();
const lastAccess = new Date(await this.lastAccessedTimeProperty.get(context, now.toISOString()));
if (now !== lastAccess && ((now.getTime() - lastAccess.getTime()) / 1000) >= this.expireAfterSeconds) {
// Notify the user that the conversation is being restarted.
await context.sendActivity("Welcome back! Let's start over from the beginning.");
// Clear state.
await this.conversationState.clear(context);
}
await super.run(context);
// Set LastAccessedTime to the current time.
await this.lastAccessedTimeProperty.set(context, now.toISOString());
// Save any state changes. The load happened during the execution of the Dialog.
await this.conversationState.saveChanges(context, false);
await this.userState.saveChanges(context, false);
}
index.js
Por fim, atualize index.js
para enviar o ExpireAfterSeconds
parâmetro para DialogBot
:
const bot = new DialogBot(process.env.ExpireAfterSeconds, conversationState, userState, dialog);
application.properties
Primeiro, adicione uma configuração ExpireAfterSeconds
a application.properties:
MicrosoftAppId=
MicrosoftAppPassword=
server.port=3978
ExpireAfterSeconds=30
DialogBot.java
Em seguida, adicione campos expireAfterSeconds
, lastAccessedTimeProperty
e dialogStateProperty
à classe do bot e inicialize-os no construtor do bot. Adicione também um parâmetro Configuration
ao construtor para recuperar o valor ExpireAfterSeconds
.
Em vez de criar o acessador embutido da propriedade de estado do diálogo no método onMessageActivity
, você o cria e registra no momento da inicialização. O bot precisará do acessador da propriedade de estado não apenas para executar o diálogo, mas também para limpar o estado do diálogo.
protected final int expireAfterSeconds;
protected final StatePropertyAccessor<LocalTime> lastAccessedTimeProperty;
protected final StatePropertyAccessor<DialogState> dialogStateProperty;
// Existing fields omitted...
public DialogBot(
Configuration configuration,
ConversationState withConversationState,
UserState withUserState,
Dialog withDialog
) {
dialog = withDialog;
conversationState = withConversationState;
userState = withUserState;
expireAfterSeconds = configuration.getProperty("ExpireAfterSeconds") != null ?
Integer.parseInt(configuration.getProperty("ExpireAfterSeconds")) :
30;
lastAccessedTimeProperty = conversationState.createProperty("LastAccessedTimeProperty");
dialogStateProperty = conversationState.createProperty("DialogStateProperty");
}
Por fim, adicione código ao método onTurn
do bot para limpar o estado do diálogo se a conversa for muito antiga.
@Override
public CompletableFuture<Void> onTurn(
TurnContext turnContext
) {
LocalTime lastAccess = lastAccessedTimeProperty.get(turnContext).join();
if (lastAccess != null
&& (java.time.temporal.ChronoUnit.SECONDS.between(lastAccess, LocalTime.now()) >= expireAfterSeconds)) {
turnContext.sendActivity("Welcome back! Let's start over from the beginning.").join();
conversationState.clearState(turnContext).join();
}
return lastAccessedTimeProperty.set(turnContext, LocalTime.now()).thenCompose(setResult -> {
return super.onTurn(turnContext)
.thenCompose(result -> conversationState.saveChanges(turnContext))
// Save any state changes that might have occurred during the turn.
.thenCompose(result -> userState.saveChanges(turnContext));
});
}
config.py
Primeiro, adicione uma configuração ExpireAfterSeconds
a config.py:
PORT = 3978
APP_ID = os.environ.get("MicrosoftAppId", "")
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
EXPIRE_AFTER_SECONDS = os.environ.get("ExpireAfterSeconds", 30)
bots\dialog_bot.py
Em seguida, adicione campos a DialogBot
e atualize o construtor. Adicione campos locais para expire_after_seconds
e last_accessed_time_property
.
Adicione expire_after_seconds
como parâmetro ao construtor e crie o StatePropertyAccessor
necessário:
def __init__(
self,
expire_after_seconds: int,
conversation_state: ConversationState,
user_state: UserState,
dialog: Dialog,
):
# Existing code omitted...
self.expire_after_seconds = expire_after_seconds
self.dialog_state_property = conversation_state.create_property("DialogState")
self.last_accessed_time_property = conversation_state.create_property("LastAccessedTime")
self.conversation_state = conversation_state
self.user_state = user_state
self.dialog = dialog
Mude on_message_activity
para que ele use o dialog_state_property
:
async def on_message_activity(self, turn_context: TurnContext):
await DialogHelper.run_dialog(
self.dialog,
turn_context,
self.dialog_state_property,
)
Adicione código ao método on_turn
do bot:
async def on_turn(self, turn_context: TurnContext):
# Retrieve the property value, and compare it to the current time.
now_seconds = int(time.time())
last_access = int(
await self.last_accessed_time_property.get(turn_context, now_seconds)
)
if now_seconds != last_access and (
now_seconds - last_access >= self.expire_after_seconds
):
# Notify the user that the conversation is being restarted.
await turn_context.send_activity(
"Welcome back! Let's start over from the beginning."
)
# Clear state.
await self.conversation_state.clear_state(turn_context)
await self.conversation_state.save_changes(turn_context, True)
await super().on_turn(turn_context)
# Set LastAccessedTime to the current time.
await self.last_accessed_time_property.set(turn_context, now_seconds)
# Save any state changes that might have occurred during the turn.
await self.conversation_state.save_changes(turn_context)
await self.user_state.save_changes(turn_context)
app.py
Por fim, atualize app.py
para enviar o EXPIRE_AFTER_SECONDS
parâmetro para DialogBot
:
BOT = DialogBot(CONFIG.EXPIRE_AFTER_SECONDS, CONVERSATION_STATE, USER_STATE, DIALOG)
O Cosmos DB fornece um recurso de vida útil (TTL) que permite que você exclua itens automaticamente de um contêiner após um determinado período. Isso pode ser configurado de dentro do portal do Azure ou durante a criação do contêiner (usando os SDKs do Cosmos DB específicos da linguagem).
O SDK do Bot Framework não expõe a definição de configuração de um TTL. No entanto, a inicialização do contêiner pode ser substituída e o SDK do Cosmos DB pode ser usado para configurar a TTL antes da inicialização do armazenamento do Bot Framework.
Comece com uma nova cópia do exemplo de solicitação de várias rodadas e adicione o pacote NuGet Microsoft.Bot.Builder.Azure
ao projeto.
appsettings.json
Atualize appsettings.json para incluir opções de armazenamento do Cosmos DB:
{
"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>"
}
Observe os dois ContainerIds, um para UserState
e outro para ConversationState
. A TTL padrão é definida no contêiner ConversationState
, mas não no UserState
.
CosmosDbStorageInitializerHostedService.cs
Em seguida, crie uma classe CosmosDbStorageInitializerHostedService
, que criará o contêiner com a vida útil configurada.
// 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
Por fim, atualize Startup.cs
para usar o inicializador de armazenamento e o Cosmos DB para estado:
// 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...
Comece com uma nova cópia do exemplo de solicitação de várias rodadas.
.env
Atualize .env para incluir as opções de armazenamento do Cosmos DB:
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>
Observe os dois ContainerIds, um para UserState
e outro para ConversationState
. A TTL padrão é definida no contêiner ConversationState
, mas não UserState
.
project.json
Em seguida, adicione o pacote npm botbuilder-azure
a project.json.
"dependencies": {
"botbuilder": "~4.9.2",
"botbuilder-dialogs": "~4.9.2",
"botbuilder-azure": "~4.9.2",
"dotenv": "^8.2.0",
"path": "^0.12.7",
"restify": "~8.5.1"
},
index.js
Adicione as instruções de requisitos necessárias ao índice.js:
const { CosmosDbPartitionedStorage } = require('botbuilder-azure');
const { CosmosClient } = require('@azure/cosmos');
Substitua a criação de MemoryStorage
, ConversationState
e UserState
por:
// const memoryStorage = new MemoryStorage();
// Storage options for Conversation State
const conversationStorageOptions = {
cosmosDbEndpoint: process.env.CosmosDbEndpoint,
authKey: process.env.CosmosDbAuthKey,
databaseId: process.env.CosmosDbDatabaseId,
containerId: process.env.CosmosDbConversationStateContainerId
};
// Create a cosmosClient, and set defaultTtl (with other properties)
var cosmosClient = new CosmosClient({
endpoint: conversationStorageOptions.cosmosDbEndpoint,
key: conversationStorageOptions.authKey,
...conversationStorageOptions.cosmosClientOptions,
});
// Create the container with the provided TTL.
Promise.resolve(cosmosClient
.database(conversationStorageOptions.databaseId)
.containers.createIfNotExists({
id: conversationStorageOptions.containerId,
partitionKey: {
paths: ['/id']
},
defaultTtl: parseInt(process.env.CosmosDbTimeToLive, 10)
}));
// Storage options for User State
const userStorageOptions = {
cosmosDbEndpoint: process.env.CosmosDbEndpoint,
authKey: process.env.CosmosDbAuthKey,
databaseId: process.env.CosmosDbDatabaseId,
containerId: process.env.CosmosDbUserStateContainerId
};
// Create state instances.
const conversationState = new ConversationState(new CosmosDbPartitionedStorage(conversationStorageOptions));
const userState = new UserState(new CosmosDbPartitionedStorage(userStorageOptions));
Por fim, execute npm install antes de iniciar o bot.
npm install
Comece com uma nova cópia do exemplo de solicitação de várias rodadas e adicione as seguintes dependências ao arquivo pom.xml:
<dependency>
<groupId>com.microsoft.bot</groupId>
<artifactId>bot-azure</artifactId>
<version>4.13.0</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-cosmos</artifactId>
</dependency>
application.properties
Atualize application.properties para incluir opções de armazenamento do Cosmos DB:
MicrosoftAppId=
MicrosoftAppPassword=
server.port=3978
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>
Observe os dois ContainerIds, um para UserState
e outro para ConversationState
. A TTL padrão é definida no contêiner ConversationState
, mas não no UserState
.
CosmosDbStorageInitializer.java
Em seguida, crie uma classe CosmosDbStorageInitializer
, que criará o contêiner com a vida útil configurada.
package com.microsoft.bot.sample.multiturnprompt;
import com.azure.cosmos.CosmosAsyncClient;
import com.azure.cosmos.CosmosClientBuilder;
import com.azure.cosmos.models.CosmosContainerProperties;
import com.microsoft.bot.azure.CosmosDbPartitionedStorageOptions;
import com.microsoft.bot.integration.Configuration;
public class CosmosDbStorageInitializer {
final CosmosDbPartitionedStorageOptions storageOptions;
final int cosmosDbTimeToLive;
public CosmosDbStorageInitializer(Configuration configuration) {
storageOptions = new CosmosDbPartitionedStorageOptions();
storageOptions.setCosmosDbEndpoint(configuration.getProperty("CosmosDbEndpoint"));
storageOptions.setAuthKey(configuration.getProperty("CosmosDbAuthKey"));
storageOptions.setDatabaseId(configuration.getProperty("CosmosDbDatabaseId"));
storageOptions.setContainerId(configuration.getProperty("CosmosDbConversationStateContainerId"));
cosmosDbTimeToLive = configuration.getProperty("CosmosDbTimeToLive") != null
? Integer.parseInt(configuration.getProperty("CosmosDbTimeToLive"))
: 30;
}
public void initialize() {
CosmosAsyncClient client = new CosmosClientBuilder().endpoint(storageOptions.getCosmosDbEndpoint())
.key(storageOptions.getAuthKey()).buildAsyncClient();
client.createDatabaseIfNotExists(storageOptions.getDatabaseId()).block();
CosmosContainerProperties cosmosContainerProperties = new CosmosContainerProperties(
storageOptions.getContainerId(), "/id");
cosmosContainerProperties.setDefaultTimeToLiveInSeconds(cosmosDbTimeToLive);
client.getDatabase(storageOptions.getDatabaseId()).createContainerIfNotExists(cosmosContainerProperties)
.block();
client.close();
}
}
Application.java
Por fim, atualize Application.java
para usar o inicializador de armazenamento e o Cosmos DB para estado:
// Existing code omitted...
@Override
public ConversationState getConversationState(Storage storage) {
Configuration configuration = getConfiguration();
CosmosDbStorageInitializer initializer = new CosmosDbStorageInitializer(configuration);
initializer.initialize();
CosmosDbPartitionedStorageOptions storageOptions = new CosmosDbPartitionedStorageOptions();
storageOptions.setCosmosDbEndpoint(configuration.getProperty("CosmosDbEndpoint"));
storageOptions.setAuthKey(configuration.getProperty("CosmosDbAuthKey"));
storageOptions.setDatabaseId(configuration.getProperty("CosmosDbDatabaseId"));
storageOptions.setContainerId(configuration.getProperty("CosmosDbConversationStateContainerId"));
return new ConversationState(new CosmosDbPartitionedStorage(storageOptions));
}
/**
* Returns a UserState object. Default scope of Singleton.
*
* @param storage The Storage object to use.
* @return A UserState object.
*/
@Override
public UserState getUserState(Storage storage) {
Configuration configuration = getConfiguration();
CosmosDbPartitionedStorageOptions storageOptions = new CosmosDbPartitionedStorageOptions();
storageOptions.setCosmosDbEndpoint(configuration.getProperty("CosmosDbEndpoint"));
storageOptions.setAuthKey(configuration.getProperty("CosmosDbAuthKey"));
storageOptions.setDatabaseId(configuration.getProperty("CosmosDbDatabaseId"));
storageOptions.setContainerId(configuration.getProperty("CosmosDbUserStateContainerId"));
return new UserState(new CosmosDbPartitionedStorage(storageOptions));
}
Comece com uma nova cópia do exemplo de solicitação de várias rodadas.
config.py
Atualize config.py
para incluir as opções de armazenamento do Cosmos DB:
PORT = 3978
APP_ID = os.environ.get("MicrosoftAppId", "")
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
COSMOSDB_TTL = os.environ.get("CosmosDbTimeToLive", 30)
COSMOSDB_ENDPOINT = os.environ.get("CosmosDbEndpoint", "<endpoint-for-your-cosmosdb-instance>")
COSMOSDB_AUTH_KEY = os.environ.get("CosmosDbAuthKey", "<your-cosmosdb-auth-key>")
COSMOSDB_DATABASE_ID = os.environ.get("CosmosDbDatabaseId", "<your-database-id>")
COSMOSDB_USER_STATE_CONTAINER_ID = os.environ.get("CosmosDbUserStateContainerId", "<no-ttl-container-id>")
COSMOSDB_CONVERSATION_STATE_CONTAINER_ID = os.environ.get("CosmosDbConversationStateContainerId", "<ttl-container-id>")
Observe os dois ContainerIds, um para UserState
e outro para ConversationState
. A TTL padrão é definida no contêiner ConversationState
, mas não UserState
.
requirements.txt
Em seguida, adicione o pacote botbuilder-azure
a requisitos.txt.
botbuilder-integration-aiohttp>=4.10.0
botbuilder-dialogs>=4.10.0
botbuilder-ai>=4.10.0
botbuilder-azure>=4.10.0
Em seguida, execute pip install:
pip install -r requirements.txt
app.py
client = cosmos_client.CosmosClient(
CONFIG.COSMOSDB_ENDPOINT, {"masterKey": CONFIG.COSMOSDB_AUTH_KEY},
)
containers = list(client.QueryContainers("dbs/" + CONFIG.COSMOSDB_DATABASE_ID, {
"query": "SELECT * FROM r WHERE r.id=@id",
"parameters": [
{"name": "@id", "value": CONFIG.COSMOSDB_CONVERSATION_STATE_CONTAINER_ID}
],
}))
if len(containers) < 1:
new_container = client.CreateContainer(
"dbs/" + CONFIG.COSMOSDB_DATABASE_ID,
{
"defaultTtl": CONFIG.COSMOSDB_TTL,
"id": CONFIG.COSMOSDB_CONVERSATION_STATE_CONTAINER_ID,
"partitionKey": {"paths": ["/id"], "kind": "Hash",},
},
)
Agora, o Cosmos DB excluirá automaticamente os registros de estado da conversa após 30 segundos de inatividade.