Inherentemente, un bot no tiene estado. Una vez implementado el bot, no se puede ejecutar en el mismo proceso o en el mismo equipo de turno a otro. Sin embargo, el bot debe poder hacer un seguimiento del contexto de una conversación, para que pueda controlar su comportamiento y recordar las respuestas a las preguntas anteriores. Las características de almacenamiento y estado de Bot Framework SDK permiten agregar un estado al bot. Los bots usan objetos de almacenamiento y administración de estado para administrar y conservar el estado. El administrador de estados proporciona una capa de abstracción que permite acceder a las propiedades de estado mediante descriptores de acceso de las propiedades, independientemente del tipo de almacenamiento subyacente.
Nota:
Los SDK de JavaScript, C# y Python de Bot Framework seguirán siendo compatibles, pero el SDK de Java se va a retirar con la compatibilidad final a largo plazo que finaliza en noviembre de 2023.
Los bots existentes creados con el SDK de Java seguirán funcionando.
El código de este artículo se basa en el ejemplo de bot de administración de estados. Necesitará una copia del ejemplo en C#, JavaScript, Java, o Python.
Acerca de este ejemplo
Al recibir la entrada del usuario, este ejemplo comprueba el estado de la conversación almacenada para ver si se le ha pedido a este usuario que proporcione su nombre anteriormente. Si no es así, se solicita el nombre del usuario y se almacena esa entrada en el estado del usuario. Si ya se le ha pedido, se usará el nombre almacenado en el estado del usuario para conversar con este y se devolverán sus datos de entrada junto con la hora de recepción y el identificador del canal de entrada, al usuario. Los valores de hora y de identificador del canal se recuperan de los datos de conversación del usuario y se guardan en el estado de conversación. El siguiente diagrama muestra la relación entre el bot, el perfil de usuario y las clases de datos de conversación.
El primer paso para configurar la administración de estados es definir las clases que contendrán la información que queremos administrar del usuario y el estado de la conversación. En el ejemplo que se usa en este artículo se definen las clases siguientes:
En UserProfile.cs, se define una clase UserProfile para la información de usuario que recopilará el bot.
En ConversationData.cs, se define una clase ConversationData para controlar el estado de la conversación mientras se recopila información del usuario.
En los ejemplos de código siguientes se muestran las definiciones de las clases UserProfile y 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;
}
Este paso no es necesario en JavaScript.
El primer paso para configurar la administración de estados es definir las clases que contendrán la información que queremos administrar del usuario y el estado de la conversación. En el ejemplo que se usa en este artículo se definen las clases siguientes:
En UserProfile.java, se define una clase UserProfile para la información de usuario que recopilará el bot.
En ConversationData.java, se define una clase ConversationData para controlar el estado de la conversación mientras se recopila información del usuario.
En los ejemplos de código siguientes se muestran las definiciones de las clases UserProfile y 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) {
El primer paso para configurar la administración de estados es definir las clases que contendrán la información que queremos administrar del usuario y el estado de la conversación. En el ejemplo que se usa en este artículo se definen las clases siguientes:
user-profile.py contiene la clase UserProfile, que almacena la información del usuario recopilada por el bot.
conversation_data.py contiene la clase ConversationData, que controla el estado de la conversación al recopilar información del usuario.
En los ejemplos de código siguientes se muestran las definiciones de las clases UserProfile y ConversationData.
user_profile.py
class UserProfile:
def __init__(self, name: str = None):
self.name = name
A continuación, se registra MemoryStorage que se usa para crear objetos UserState y ConversationState. Los objetos de estado de usuario y conversación se crean en Startup y se inserta la dependencia en el constructor del bot. Otros servicios que se registran para un bot son: un proveedor de credenciales, un adaptador y la implementación del 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 */
A continuación, se registra MemoryStorage que se usa para crear objetos UserState y ConversationState. Estos se crean en index.js y se consumen cuando se crea el bot.
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;
A continuación, registre StateManagementBot en Application.java. ConversationState y UserState se proporcionan de forma predeterminada desde la clase BotDependencyConfiguration y Spring los insertará en el método getBot.
Application.java
@Bean
public Bot getBot(
ConversationState conversationState,
UserState userState
) {
return new StateManagementBot(conversationState, userState);
}
A continuación, se registra MemoryStorage que se usa para crear objetos UserState y ConversationState. Estos se crean en app.py y se consumen cuando se crea el bot.
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")
Incorporación de descriptores de acceso de propiedad de estado
Ahora se crearán descriptores de acceso de propiedad mediante el método CreateProperty que proporciona un control sobre el objeto BotState. Cada descriptor de acceso a una propiedad de estado permite obtener o establecer el valor de la propiedad de estado correspondiente. Antes de usar nuestras propiedades de estado, usamos cada descriptor de acceso para cargar la propiedad desde el almacenamiento y obtenerla de la caché de estado. Para obtener la clave de ámbito correcto asociada con la propiedad de estado, el método se llamará GetAsync.
Bots/StateManagementBot.cs
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
A continuación, se crearán los descriptores de acceso de UserState y ConversationState. Cada descriptor de acceso a una propiedad de estado permite obtener o establecer el valor de la propiedad de estado correspondiente. El descriptor de acceso se usa para cargar la propiedad asociada del almacenamiento y para recuperar su estado actual de la caché.
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);
Ahora se crean descriptores de acceso de propiedad mediante el método createProperty. Cada descriptor de acceso a una propiedad de estado permite obtener o establecer el valor de la propiedad de estado correspondiente. Antes de usar nuestras propiedades de estado, usamos cada descriptor de acceso para cargar la propiedad desde el almacenamiento y obtenerla de la caché de estado. Para obtener la clave de ámbito correcto asociada con la propiedad de estado, el método se llamará get.
A continuación, se crearán los descriptores de acceso de UserProfile y ConversationData. Cada descriptor de acceso a una propiedad de estado permite obtener o establecer el valor de la propiedad de estado correspondiente. El descriptor de acceso se usa para cargar la propiedad asociada del almacenamiento y para recuperar su estado actual de la caché.
En las secciones anteriores se describen los pasos en tiempo de inicialización para agregar a nuestro bot los descriptores de acceso de las propiedades de estado. Ahora, podemos usar los descriptores de acceso a las propiedades de estado para leer y escribir la información sobre el estado en tiempo de ejecución. El siguiente ejemplo de código usa este flujo de lógica:
Si userProfile.Name está vacío y conversationData.PromptedUserForName es true, recupera el nombre de usuario proporcionado y lo almacena dentro del estado de usuario.
Si userProfile.Name está vacío y conversationData.PromptedUserForName es false, se pregunta por el nombre del usuario.
Si ya se almacenó userProfile.Name anteriormente, se recuperará la hora del mensaje y el identificador del canal a partir de la entrada del usuario, se enviarán de nuevo todos los datos al usuario y se almacenarán los datos recuperados en el estado de la conversación.
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 salir del controlador de turnos, se usará el método SaveChangesAsync() de los objetos de administración de estados para escribir todos los cambios de estados de nuevo en el almacenamiento.
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);
}
Si userProfile.Name está vacío y conversationData.PromptedUserForName es true, recupera el nombre de usuario proporcionado y lo almacena dentro del estado de usuario.
Si userProfile.Name está vacío y conversationData.PromptedUserForName es false, se pregunta por el nombre del usuario.
Si ya se almacenó userProfile.Name anteriormente, se recuperará la hora del mensaje y el identificador del canal a partir de la entrada del usuario, se enviarán de nuevo todos los datos al usuario y se almacenarán los datos recuperados en el estado de la conversación.
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 salir de cada turno de diálogo, se usará el método saveChanges() de los objetos de administración de estados para conservar todos los cambios escribiendo el estado en el almacenamiento.
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);
}
Si userProfile.getName() está vacío y conversationData.getPromptedUserForName() es true, recupera el nombre de usuario proporcionado y lo almacena dentro del estado de usuario.
Si userProfile.getName() está vacío y conversationData.getPromptedUserForName() es false, se pregunta por el nombre del usuario.
Si ya se almacenó userProfile.getName() anteriormente, se recuperará la hora del mensaje y el identificador del canal a partir de la entrada del usuario, se enviarán de nuevo todos los datos al usuario y se almacenarán los datos recuperados en el estado de la conversación.
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 salir del controlador de turnos, se usará el método saveChanges() de los objetos de administración de estados para escribir todos los cambios de estados de nuevo en el almacenamiento.
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));
}
Si user_profile.name está vacío y conversation_data.prompted_for_user_name es true, el bot recupera el nombre proporcionado por el usuario y lo almacena en el estado del usuario.
Si user_profile.name está vacío y conversation_data.prompted_for_user_name es false, el bot solicita el nombre del usuario.
Si ya se almacenó user_profile.name anteriormente, el bot recupera la hora del mensaje y el identificador del canal a partir de la entrada del usuario, envía de nuevo los datos al usuario y almacena los datos recuperados en el estado de la conversación.
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 que finalice cada turno de diálogo, el bot usa el método save_changes de los objetos de administración de estados para conservar todos los cambios escribiendo la información de estado en el almacenamiento.
Todas las llamadas de administración de estado son asincrónicas y prevalece el último en escribir de forma predeterminada. En la práctica, debe obtener, establecer y guardar el estado lo más próximos en el bot como sea posible. Para obtener una explicación sobre cómo implementar el bloqueo optimista, consulte Implementación del almacenamiento personalizado para el bot.
Datos empresariales críticos:
Utilice el estado del bot para almacenar las preferencias, el nombre de usuario o lo último que haya solicitado, pero no lo utilice para almacenar datos empresariales críticos. Para los datos críticos, cree sus propios componentes de almacenamiento o escríbalos directamente en el almacenamiento.
Recognizer-Text:
El ejemplo usa las bibliotecas Microsoft/Recognizers-Text para analizar y validar la entrada del usuario. Para más información, consulte la página de información general.
Pasos siguientes
Aprenda a plantear al usuario una serie de preguntas, validar sus respuestas y guardar sus datos.