Enregistrer les données d’utilisateur et de conversation
Article
S'APPLIQUE À : SDK v4
Un bot est par nature sans état. Une fois que votre bot est déployé, il ne peut pas s’exécuter dans le même processus ni sur le même ordinateur d’un tour à l’autre. Votre bot peut toutefois peut avoir besoin de suivre le contexte d’une conversation pour pouvoir gérer son comportement et se rappeler les réponses aux questions précédentes. Les fonctionnalités d’état et de stockage du SDK Bot Framework permettent d’ajouter un état à votre bot. Les bots utilisent la gestion de l’état et les objets de stockage pour gérer et conserver l’état. Le gestionnaire d’état fournit une couche d’abstraction qui vous permet d’accéder aux propriétés d’état à l’aide d’accesseurs de propriété, indépendamment du type de stockage sous-jacent.
Remarque
Les kits SDK JavaScript, C# et Python Bot Framework continueront d’être pris en charge. Toutefois, le kit de développement logiciel (SDK) Java est mis hors service avec une prise en charge finale à long terme se terminant en novembre 2023.
Les bots existants créés avec le kit de développement logiciel (SDK) Java continueront de fonctionner.
Le code présenté dans cet article est basé sur l’exemple de bot de gestion d’état. Vous aurez besoin d'une copie de l'exemple en C#, JavaScript, Java, ou Python.
À propos de cet exemple
Quand il reçoit l’entrée utilisateur, cet exemple vérifie l’état de conversation stocké pour voir si cet utilisateur a précédemment été invité à fournir son nom. Si ce n’est pas le cas, le nom de l’utilisateur est demandé et cette entrée est stockée dans l’état utilisateur. Dans l'affirmative, le nom stocké dans l'état utilisateur sert à communiquer avec l'utilisateur. Par ailleurs, ses données d'entrée ainsi que l'heure de réception et l'ID du canal d'entrée sont renvoyés à l'utilisateur. Les valeurs d'heure et d'ID de canal sont récupérées à partir des données de conversation utilisateur, puis enregistrées dans l'état de conversation. Le diagramme suivant montre la relation entre le bot, le profil utilisateur et les classes de données de conversation.
La première étape de la configuration de la gestion d’état consiste à définir les classes qui contiennent les informations à gérer dans l’état utilisateur et l’état de conversation. L’exemple utilisé dans cet article définit les classes suivantes :
Dans UserProfile.cs, vous définissez une classe UserProfile pour les informations utilisateur qui seront collectées par le bot.
Dans ConversationData.cs, vous définissez une classe ConversationData pour contrôler l'état de votre conversation pendant la collecte des informations utilisateur.
Les exemples de code suivants illustrent les définitions des classes UserProfile et 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;
}
Cette étape n'est pas nécessaire dans JavaScript.
La première étape de la configuration de la gestion d’état consiste à définir les classes qui contiennent les informations à gérer dans l’état utilisateur et l’état de conversation. L'exemple utilisé dans cet article définit les classes suivantes :
Dans UserProfile.java, vous définissez une classe UserProfile pour les informations utilisateur qui seront collectées par le bot.
Dans ConversationData.java, vous définissez une classe ConversationData pour contrôler l'état de votre conversation pendant la collecte des informations utilisateur.
Les exemples de code suivants illustrent les définitions des classes UserProfile et 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) {
La première étape de la configuration de la gestion d’état consiste à définir les classes qui contiennent les informations à gérer dans l’état utilisateur et l’état de conversation. L’exemple utilisé dans cet article définit les classes suivantes :
user_profile.py contient la classe UserProfile, qui stocke les informations utilisateur collectées par le bot.
conversation_data.py contient la classe ConversationData, qui contrôle l'état de la conversation pendant la collecte des informations utilisateur.
Les exemples de code suivants illustrent les définitions des classes UserProfile et ConversationData.
user_profile.py
class UserProfile:
def __init__(self, name: str = None):
self.name = name
Ensuite, vous inscrivez MemoryStorage qui est utilisé pour créer les objets UserState et ConversationState. Les objets d’état utilisateur et de conversation sont créés lors de Startup et la dépendance est injectée dans le constructeur de bot. Les autres services inscrits qui disponibles pour un bot sont les suivants : un fournisseur d’informations d’identification, un adaptateur et l’implémentation de 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 */
Ensuite, vous inscrivez MemoryStorage qui est alors utilisé pour créer les objets UserState et ConversationState. Ceux-ci sont créés dans index. js et sont consommés lors de la création du 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;
Ensuite, vous inscrivez StateManagementBot dans Application.java. ConversationState et UserState sont fournis par défaut à partir de la classe BotDependencyConfiguration, et Spring les injecte dans la méthode getBot.
Application.java
@Bean
public Bot getBot(
ConversationState conversationState,
UserState userState
) {
return new StateManagementBot(conversationState, userState);
}
Ensuite, vous inscrivez MemoryStorage qui est utilisé pour créer les objets UserState et ConversationState. Ceux-ci sont créés dans app.py et sont consommés lors de la création du 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")
Vous pouvez à présent créer des accesseurs de propriété à l'aide de la méthode CreateProperty qui fournit un gestionnaire à l'objet BotState. Chaque accesseur de propriété d’état vous permet d’obtenir ou de définir la valeur de la propriété d’état associée. Avant d'utiliser les propriétés de l'état, utilisez chaque accesseur pour charger la propriété à partir du stockage et la récupérer depuis le cache de l'état. Pour obtenir la clé incluse dans l'étendue appropriée et associée à la propriété d'état, vous appelez la méthode GetAsync.
Bots/StateManagementBot.cs
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
Vous devez à présent créer des accesseurs de propriété pour UserState et ConversationState. Chaque accesseur de propriété d’état vous permet d’obtenir ou de définir la valeur de la propriété d’état associée. Vous utilisez chaque accesseur pour charger la propriété associée à partir du stockage et récupérer son état actuel à partir du 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);
Vous créez à présent des accesseurs de propriétés à l'aide de la méthode createProperty. Chaque accesseur de propriété d’état vous permet d’obtenir ou de définir la valeur de la propriété d’état associée. Avant d'utiliser les propriétés de l'état, utilisez chaque accesseur pour charger la propriété à partir du stockage et la récupérer depuis le cache de l'état. Pour obtenir la clé incluse dans l'étendue appropriée et associée à la propriété d'état, vous appelez la méthode get.
Vous devez à présent créer des accesseurs de propriété pour UserProfile et ConversationData. Chaque accesseur de propriété d’état vous permet d’obtenir ou de définir la valeur de la propriété d’état associée. Vous utilisez chaque accesseur pour charger la propriété associée à partir du stockage et récupérer son état actuel à partir du cache.
La section précédente présente les étapes nécessaires au moment de l’initialisation pour ajouter des accesseurs de propriété d’état à notre bot. Vous pouvez à présent utiliser ces accesseurs au moment de l'exécution pour lire et écrire des informations d'état. L’exemple de code ci-dessous utilise le flux logique suivant :
Si userProfile.Name est vide et conversationData.PromptedUserForName a la valeur true, vous récupérez le nom d'utilisateur fourni et le stockez dans l'état de l'utilisateur.
Si userProfile.Name est vide et conversationData.PromptedUserForName a la valeur false, vous demandez le nom de l'utilisateur.
Si userProfile.Name a été stocké, vous récupérez l'heure du message et l'ID du canal à partir de l'entrée de l'utilisateur, vous renvoyez toutes les données à l'utilisateur et vous stockez les données récupérées dans l'état de la conversation.
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}");
}
}
Avant de quitter le gestionnaire de tours, vous utilisez la méthode SaveChangesAsync() des objets de gestion de l'état pour écrire toutes les modifications de l'état dans le stockage.
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 vide et conversationData.PromptedUserForName a la valeur true, vous récupérez le nom d'utilisateur fourni et le stockez dans l'état de l'utilisateur.
Si userProfile.Name est vide et conversationData.PromptedUserForName a la valeur false, vous demandez le nom de l'utilisateur.
Si userProfile.Name a été stocké, vous récupérez l'heure du message et l'ID du canal à partir de l'entrée de l'utilisateur, vous renvoyez toutes les données à l'utilisateur et vous stockez les données récupérées dans l'état de la conversation.
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();
});
Avant de quitter chaque tour de dialogue, vous utilisez la méthode saveChanges() des objets de gestion d'état pour conserver tous les changements en récrivant l'état dans le stockage.
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 vide et conversationData.getPromptedUserForName() a la valeur true, vous récupérez le nom d'utilisateur fourni et le stockez dans l'état de l'utilisateur.
Si userProfile.getName() est vide et conversationData.getPromptedUserForName() a la valeur false, vous demandez le nom de l'utilisateur.
Si userProfile.getName() a été stocké, vous récupérez l'heure du message et l'ID du canal à partir de l'entrée de l'utilisateur, vous renvoyez toutes les données à l'utilisateur et vous stockez les données récupérées dans l'état de la conversation.
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);
}
Avant de quitter le gestionnaire de tours, vous utilisez la méthode saveChanges() des objets de gestion d'état pour écrire tous les changements d'état dans le stockage.
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 vide et si conversation_data.prompted_for_user_name a la valeur true, le bot récupère le nom fourni par l’utilisateur et le stocke dans l’état de l’utilisateur.
Si user_profile.name est vide et si conversation_data.prompted_for_user_name a la valeur false, le bot demande le nom de l'utilisateur.
Si user_profile.name a été stocké, le bot récupère l'heure du message et l'ID de canal à partir de l'entrée utilisateur, retourne les données à l'utilisateur, puis stocke les données récupérées dans l'état de conversation.
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 }"
)
Avant la fin de chaque tour de dialogue, le bot utilise la méthode save_changes des objets de gestion de l'état pour conserver tous les changements en écrivant des informations sur l'état dans la stockage.
Tous les appels de gestion d’état sont asynchrones et, par défaut, le dernier qui écrit gagne (last-writer-wins). Dans la pratique, vous devez obtenir, définir et enregistrer l’état dans votre bot de la façon la plus rapprochée possible. Pour une discussion sur la façon d'implémenter le verrouillage optimiste, consultez Implémenter un stockage personnalisé pour votre bot.
Données critique sur l'activité
Utilisez l'état du bot pour stocker les préférences, le nom d'utilisateur ou le dernier élément commandé. En revanche, ne l'utilisez pas pour stocker des données critiques. Pour les données critiques, créez vos propres composants de stockage ou écrivez directement dans le stockage.
Texte de reconnaissance
L’exemple utilise les bibliothèques Microsoft/Recognizers-Text pour analyser et valider les entrées utilisateur. Pour plus d’informations, consultez la page de présentation.
Étapes suivantes
Découvrez comment poser une série de questions à l'utilisateur, valider ses réponses et enregistrer ses données.