Um bot é inerentemente sem estado. Após a implantação do seu bot, talvez ele não seja executado no mesmo processo ou no mesmo computador de um turno para outro. Porém, talvez seu bot precise controlar o contexto de uma conversa, para que ele possa gerenciar seu comportamento e lembrar-se das respostas às perguntas anteriores. Os recursos de armazenamento e estado do SDK do Bot Framework permitem que você adicione um estado ao seu bot. Os bots usam o gerenciamento de estado e os objetos de armazenamento para gerenciar e manter o estado. O gerenciador de estado fornece uma camada de abstração que permite acessar propriedades de estado usando acessadores de propriedades, independentemente do tipo de armazenamento subjacente.
Observação
Os SDKs JavaScript, C# e Python do Bot Framework continuarão a ser compatíveis. No entanto, o SDK Java está sendo desativado, com o suporte final de longo prazo terminando em novembro de 2023.
Os bots existentes criados com o SDK para Java continuarão a funcionar.
O código neste artigo baseia-se no exemplo de bot de gerenciamento de estado. Você precisará de uma cópia do exemplo em C#, JavaScript, Java ou Python.
Sobre este exemplo
Ao receber entrada do usuário, este exemplo verifica o estado da conversa armazenada para ver se o nome foi solicitado a esse usuário anteriormente. Se não foi, o nome do usuário será solicitado e essa entrada será armazenada no estado do usuário. Se foi, o nome armazenado no estado do usuário será usado para conversar com ele, e os respectivos dados de entrada, bem como a hora de recebimento e a ID do canal de entrada, serão retornados ao usuário. Os valores de hora e ID do canal são recuperados dos dados da conversa do usuário e, em seguida, salvos no estado da conversa. O diagrama a seguir mostra a relação entre o bot, o perfil do usuário e as classes de dados da conversa.
A primeira etapa na configuração de gerenciamento de estado é definir as classes que contêm as informações a serem gerenciadas no estado do usuário e da conversa. O exemplo usado neste artigo define as seguintes classes:
Em UserProfile.cs, você define uma classe UserProfile para as informações do usuário que o bot coletará.
Em ConversationData.cs, você define uma classe ConversationData para controlar o estado da nossa conversa durante a coleta de informações do usuário.
Os exemplos de código a seguir mostram as definições para as classes UserProfile e ConversationData.
UserProfile.cs
public class UserProfile
{
public string Name { get; set; }
}
ConversationData.cs
public class ConversationData
{
// The time-stamp of the most recent incoming message.
public string Timestamp { get; set; }
// The ID of the user's channel.
public string ChannelId { get; set; }
// Track whether we have already asked the user's name
public bool PromptedUserForName { get; set; } = false;
}
Essa etapa não é necessária no JavaScript.
A primeira etapa na configuração de gerenciamento de estado é definir as classes que contêm as informações a serem gerenciadas no estado do usuário e da conversa. O exemplo usado nesse artigo define as seguintes classes:
Em UserProfile.java, você define uma classe UserProfile para as informações do usuário que o bot coletará.
Em ConversationData.java, você define uma classe ConversationData para controlar o estado da nossa conversa durante a coleta de informações do usuário.
Os exemplos de código a seguir mostram as definições para as classes UserProfile e ConversationData.
UserProfile.java
public class UserProfile {
private String name;
public String getName() {
return name;
}
public void setName(String withName) {
name = withName;
}
}
ConversationData.java
public class ConversationData {
// The time-stamp of the most recent incoming message.
private String timestamp;
// The ID of the user's channel.
private String channelId;
// Track whether we have already asked the user's name.
private boolean promptedUserForName = false;
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String withTimestamp) {
timestamp = withTimestamp;
}
public String getChannelId() {
return channelId;
}
public void setChannelId(String withChannelId) {
channelId = withChannelId;
}
public boolean getPromptedUserForName() {
return promptedUserForName;
}
public void setPromptedUserForName(boolean withPromptedUserForName) {
A primeira etapa na configuração de gerenciamento de estado é definir as classes que contêm as informações a serem gerenciadas no estado do usuário e da conversa. O exemplo usado neste artigo define as seguintes classes:
O user_profile.py contém a classe UserProfile que armazena as informações do usuário coletadas pelo bot.
O conversation_data.py contém a classe ConversationData que controla o estado da conversa durante a coleta de informações do usuário.
Os exemplos de código a seguir mostram as definições para as classes UserProfile e ConversationData.
user_profile.py
class UserProfile:
def __init__(self, name: str = None):
self.name = name
Em seguida, você registra o MemoryStorage, que é usado para criar os objetos UserState e ConversationState. Os objetos de estado da conversa e do usuário são criados em Startup e a dependência é injetada no construtor de bot. Outros serviços de um bot que são registrados: um provedor de credenciais, um adaptador e a implementação do bot.
Startup.cs
// {
// TypeNameHandling = TypeNameHandling.All,
// var storage = new BlobsStorage("<blob-storage-connection-string>", "bot-state");
// With a custom JSON SERIALIZER, use this instead.
// var storage = new BlobsStorage("<blob-storage-connection-string>", "bot-state", jsonSerializer);
/* END AZURE BLOB STORAGE */
Em seguida, você registra o MemoryStorage, que é então usado para criar os objetos UserState e ConversationState. Eles são criados em index.js e consumidos quando o bot é criado.
index.js
const memoryStorage = new MemoryStorage();
// Create conversation and user state with in-memory storage provider.
const conversationState = new ConversationState(memoryStorage);
const userState = new UserState(memoryStorage);
// Create the bot.
const bot = new StateManagementBot(conversationState, userState);
bots/stateManagementBot.js
const CONVERSATION_DATA_PROPERTY = 'conversationData';
const USER_PROFILE_PROPERTY = 'userProfile';
class StateManagementBot extends ActivityHandler {
constructor(conversationState, userState) {
super();
// Create the state property accessors for the conversation data and user profile.
this.conversationDataAccessor = conversationState.createProperty(CONVERSATION_DATA_PROPERTY);
this.userProfileAccessor = userState.createProperty(USER_PROFILE_PROPERTY);
// The state management objects for the conversation and user state.
this.conversationState = conversationState;
this.userState = userState;
Em seguida, você registra o StateManagementBot em Application.java. ConversationState e UserState são fornecidos por padrão pela classe BBotDependencyConfiguration, e o Spring os injetará no método getBot.
Application.java
@Bean
public Bot getBot(
ConversationState conversationState,
UserState userState
) {
return new StateManagementBot(conversationState, userState);
}
Em seguida, você registra o MemoryStorage, que é usado para criar os objetos UserState e ConversationState. Eles são criados em app.py e consumidos quando o bot é criado.
app.py
CONVERSATION_STATE = ConversationState(MEMORY)
# Create Bot
BOT = StateManagementBot(CONVERSATION_STATE, USER_STATE)
# Listen for incoming requests on /api/messages.
bots/state_management_bot.py
def __init__(self, conversation_state: ConversationState, user_state: UserState):
if conversation_state is None:
raise TypeError(
"[StateManagementBot]: Missing parameter. conversation_state is required but None was given"
)
if user_state is None:
raise TypeError(
"[StateManagementBot]: Missing parameter. user_state is required but None was given"
)
self.conversation_state = conversation_state
self.user_state = user_state
self.conversation_data_accessor = self.conversation_state.create_property(
"ConversationData"
)
self.user_profile_accessor = self.user_state.create_property("UserProfile")
Agora você pode criar acessadores de propriedade usando o método CreateProperty, que fornece um identificador ao objeto BotState. Cada acessador de propriedade de estado permite que você obtenha ou defina o valor da propriedade de estado associada. Antes de usar as propriedades de estado, use cada acessador para carregar a propriedade do armazenamento e obtê-la no cache de estado. Para obter a chave de escopo corretamente associada à propriedade do estado, você pode chamar o método GetAsync.
Bots/StateManagementBot.cs
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
Agora, você pode criar acessadores de propriedade para UserState e ConversationState. Cada acessador de propriedade de estado permite que você obtenha ou defina o valor da propriedade de estado associada. Você usa cada acessador para carregar a propriedade associada no armazenamento e recuperar o seu estado atual contido no cache.
bots/stateManagementBot.js
// Create the state property accessors for the conversation data and user profile.
this.conversationDataAccessor = conversationState.createProperty(CONVERSATION_DATA_PROPERTY);
this.userProfileAccessor = userState.createProperty(USER_PROFILE_PROPERTY);
Agora você cria acessadores de propriedade usando o método createProperty. Cada acessador de propriedade de estado permite que você obtenha ou defina o valor da propriedade de estado associada. Antes de usar as propriedades de estado, use cada acessador para carregar a propriedade do armazenamento e obtê-la no cache de estado. Para obter a chave de escopo corretamente associada à propriedade do estado, você pode chamar o método get.
Agora, você pode criar acessadores de propriedade para UserProfile e ConversationData. Cada acessador de propriedade de estado permite que você obtenha ou defina o valor da propriedade de estado associada. Você usa cada acessador para carregar a propriedade associada no armazenamento e recuperar o seu estado atual contido no cache.
As seções anteriores abordam as etapas de tempo de inicialização para adição dos acessadores de propriedade do estado ao nosso bot. Agora, você pode usar esses acessadores em tempo de execução para ler e gravar informações de estado. O código de exemplo abaixo usa o seguinte fluxo lógico:
Se userProfile.Name estiver vazio e conversationData.PromptedUserForName for true, você poderá recuperar o nome de usuário fornecido e armazená-lo no estado do usuário.
Se userProfile.Name estiver vazio e conversationData.PromptedUserForName for false, você poderá solicitar o nome de usuário.
Se userProfile.Name foi armazenado anteriormente, você recupera a hora da mensagem e a ID do canal da entrada do usuário, ecoa todos os dados de volta para o usuário e armazena os dados recuperados no estado da conversa.
Bots/StateManagementBot.cs
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// Get the state properties from the turn context.
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData());
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile());
if (string.IsNullOrEmpty(userProfile.Name))
{
// First time around this is set to false, so we will prompt user for name.
if (conversationData.PromptedUserForName)
{
// Set the name to what the user provided.
userProfile.Name = turnContext.Activity.Text?.Trim();
// Acknowledge that we got their name.
await turnContext.SendActivityAsync($"Thanks {userProfile.Name}. To see conversation data, type anything.");
// Reset the flag to allow the bot to go through the cycle again.
conversationData.PromptedUserForName = false;
}
else
{
// Prompt the user for their name.
await turnContext.SendActivityAsync($"What is your name?");
// Set the flag to true, so we don't prompt in the next turn.
conversationData.PromptedUserForName = true;
}
}
else
{
// Add message details to the conversation data.
// Convert saved Timestamp to local DateTimeOffset, then to string for display.
var messageTimeOffset = (DateTimeOffset)turnContext.Activity.Timestamp;
var localMessageTime = messageTimeOffset.ToLocalTime();
conversationData.Timestamp = localMessageTime.ToString();
conversationData.ChannelId = turnContext.Activity.ChannelId.ToString();
// Display state data.
await turnContext.SendActivityAsync($"{userProfile.Name} sent: {turnContext.Activity.Text}");
await turnContext.SendActivityAsync($"Message received at: {conversationData.Timestamp}");
await turnContext.SendActivityAsync($"Message received from: {conversationData.ChannelId}");
}
}
Antes de encerrar o manipulador de turno, você pode usar o método SaveChangesAsync() dos objetos de gerenciamento de estado para gravar todas as alterações no estado de volta no armazenamento.
Bots/StateManagementBot.cs
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
// Save any state changes that might have occurred during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
}
Se userProfile.Name estiver vazio e conversationData.PromptedUserForName for true, você poderá recuperar o nome de usuário fornecido e armazená-lo no estado do usuário.
Se userProfile.Name estiver vazio e conversationData.PromptedUserForName for false, você poderá solicitar o nome de usuário.
Se userProfile.Name foi armazenado anteriormente, você recupera a hora da mensagem e a ID do canal da entrada do usuário, ecoa todos os dados de volta para o usuário e armazena os dados recuperados no estado da conversa.
bots/stateManagementBot.js
this.onMessage(async (turnContext, next) => {
// Get the state properties from the turn context.
const userProfile = await this.userProfileAccessor.get(turnContext, {});
const conversationData = await this.conversationDataAccessor.get(
turnContext, { promptedForUserName: false });
if (!userProfile.name) {
// First time around this is undefined, so we will prompt user for name.
if (conversationData.promptedForUserName) {
// Set the name to what the user provided.
userProfile.name = turnContext.activity.text;
// Acknowledge that we got their name.
await turnContext.sendActivity(`Thanks ${ userProfile.name }. To see conversation data, type anything.`);
// Reset the flag to allow the bot to go though the cycle again.
conversationData.promptedForUserName = false;
} else {
// Prompt the user for their name.
await turnContext.sendActivity('What is your name?');
// Set the flag to true, so we don't prompt in the next turn.
conversationData.promptedForUserName = true;
}
} else {
// Add message details to the conversation data.
conversationData.timestamp = turnContext.activity.timestamp.toLocaleString();
conversationData.channelId = turnContext.activity.channelId;
// Display state data.
await turnContext.sendActivity(`${ userProfile.name } sent: ${ turnContext.activity.text }`);
await turnContext.sendActivity(`Message received at: ${ conversationData.timestamp }`);
await turnContext.sendActivity(`Message received from: ${ conversationData.channelId }`);
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
Antes de encerrar cada turno de diálogo, você pode usar o método saveChanges() dos objetos de gerenciamento de estado para persistir todas as alterações gravando o estado de volta no armazenamento.
bots/stateManagementBot.js
/**
* Override the ActivityHandler.run() method to save state changes after the bot logic completes.
*/
async run(context) {
await super.run(context);
// 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);
}
Se userProfile.getName() estiver vazio e conversationData.getPromptedUserForName() for true, você poderá recuperar o nome de usuário fornecido e armazená-lo no estado do usuário.
Se userProfile.getName() estiver vazio e conversationData.getPromptedUserForName() for false, você poderá solicitar o nome de usuário.
Se userProfile.getName() foi armazenado anteriormente, você recupera a hora da mensagem e a ID do canal da entrada do usuário, ecoa todos os dados de volta para o usuário e armazena os dados recuperados no estado da conversa.
StateManagementBot.java
@Override
protected CompletableFuture<Void> onMessageActivity(TurnContext turnContext) {
// Get state data from ConversationState.
StatePropertyAccessor<ConversationData> dataAccessor =
conversationState.createProperty("data");
CompletableFuture<ConversationData> dataFuture =
dataAccessor.get(turnContext, ConversationData::new);
// Get profile from UserState.
StatePropertyAccessor<UserProfile> profileAccessor = userState.createProperty("profile");
CompletableFuture<UserProfile> profileFuture =
profileAccessor.get(turnContext, UserProfile::new);
return dataFuture.thenCombine(profileFuture, (conversationData, userProfile) -> {
if (StringUtils.isBlank(userProfile.getName())) {
// First time around this is set to false, so we will prompt user for name.
if (conversationData.getPromptedUserForName()) {
// Reset the flag to allow the bot to go though the cycle again.
conversationData.setPromptedUserForName(false);
// Set the name to what the user provided and reply.
userProfile.setName(turnContext.getActivity().getText());
// Acknowledge that we got their name.
return turnContext.sendActivity(
MessageFactory.text(
"Thanks " + userProfile.getName()
+ ". To see conversation data, type anything."
)
);
} else {
// Set the flag to true, so we don't prompt in the next turn.
conversationData.setPromptedUserForName(true);
// Prompt the user for their name.
return turnContext.sendActivity(MessageFactory.text("What is your name?"));
}
} else {
OffsetDateTime messageTimeOffset = turnContext.getActivity().getTimestamp();
LocalDateTime localMessageTime = messageTimeOffset.toLocalDateTime();
//Displaying current date and time in 12 hour format with AM/PM
DateTimeFormatter dateTimeAMPMFormat = DateTimeFormatter.ofPattern("MM/dd/yyyy, hh:mm:ss a");
conversationData.setTimestamp(dateTimeAMPMFormat.format(localMessageTime));
conversationData.setChannelId(turnContext.getActivity().getChannelId());
List<Activity> sendToUser = new ArrayList<>();
sendToUser.add(
MessageFactory.text(
userProfile.getName() + " sent: " + turnContext.getActivity().getText()
)
);
sendToUser.add(
MessageFactory.text("Message received at: " + conversationData.getTimestamp()
)
);
sendToUser.add(
MessageFactory.text("Message received from: " + conversationData.getChannelId()
)
);
return turnContext.sendActivities(sendToUser);
}
})
// make the return value happy.
.thenApply(resourceResponse -> null);
}
Antes de encerrar o manipulador de turno, você pode usar o método saveChanges() dos objetos de gerenciamento de estado para gravar todas as alterações no estado de volta no armazenamento.
StateManagementBot.java
@Override
public CompletableFuture<Void> onTurn(TurnContext turnContext) {
return super.onTurn(turnContext)
// Save any state changes that might have occurred during the turn.
.thenCompose(turnResult -> conversationState.saveChanges(turnContext))
.thenCompose(saveResult -> userState.saveChanges(turnContext));
}
Se user_profile.name estiver vazio e conversation_data.prompted_for_user_name for true, o bot vai recuperar o nome fornecido pelo usuário e vai armazená-lo no estado do usuário.
Se user_profile.name estiver vazio e conversation_data.prompted_for_user_name for false, o bot solicitará o nome de usuário.
Se user_profile.name tiver sido armazenado anteriormente, o bot vai recuperar a hora da mensagem e a ID do canal da entrada do usuário, vai ecoar os dados de volta para o usuário e vai armazenar os dados recuperados no estado da conversa.
bots/state_management_bot.py
async def on_message_activity(self, turn_context: TurnContext):
# Get the state properties from the turn context.
user_profile = await self.user_profile_accessor.get(turn_context, UserProfile)
conversation_data = await self.conversation_data_accessor.get(
turn_context, ConversationData
)
if user_profile.name is None:
# First time around this is undefined, so we will prompt user for name.
if conversation_data.prompted_for_user_name:
# Set the name to what the user provided.
user_profile.name = turn_context.activity.text
# Acknowledge that we got their name.
await turn_context.send_activity(
f"Thanks { user_profile.name }. To see conversation data, type anything."
)
# Reset the flag to allow the bot to go though the cycle again.
conversation_data.prompted_for_user_name = False
else:
# Prompt the user for their name.
await turn_context.send_activity("What is your name?")
# Set the flag to true, so we don't prompt in the next turn.
conversation_data.prompted_for_user_name = True
else:
# Add message details to the conversation data.
conversation_data.timestamp = self.__datetime_from_utc_to_local(
turn_context.activity.timestamp
)
conversation_data.channel_id = turn_context.activity.channel_id
# Display state data.
await turn_context.send_activity(
f"{ user_profile.name } sent: { turn_context.activity.text }"
)
await turn_context.send_activity(
f"Message received at: { conversation_data.timestamp }"
)
await turn_context.send_activity(
f"Message received from: { conversation_data.channel_id }"
)
Antes de cada final de turno do diálogo, o bot utiliza o método save_changes dos objetos de gerenciamento de estado para persistir todas as alterações, gravando informações de estado no armazenamento.
todas as chamadas de gerenciamento de estado são assíncronas e last-writer-wins por padrão. Na prática, você deve obter, definir e salvar o estado o mais próximo possível em seu bot. Para uma discussão sobre como implementar o bloqueio otimista, confira Implementar armazenamento personalizado para seu bot.
Dados críticos de negócios
Use o estado do bot para armazenar preferências, nome de usuário ou o último pedido feito, mas não o use para armazenar dados críticos de negócios. Para dados críticos, crie seus próprios componentes de armazenamento ou grave diretamente no armazenamento.
Recognizer-Text
o exemplo usa as bibliotecas Microsoft/Recognizer-Text para analisar e validar a entrada do usuário. Para saber mais, confira a página visão geral.
Próximas etapas
Saiba como fazer uma série de perguntas ao usuário, validar suas respostas e salvar suas entradas.