Un bot è intrinsecamente senza stato. Dopo la distribuzione, il bot potrebbe non essere eseguito nello stesso processo o nello stesso computer da un turno all'altro. È tuttavia possibile che il bot debba tenere traccia del contesto di una conversazione per poterne gestire il comportamento e ricordare le risposte alle domande precedenti. Le funzionalità di stato e archiviazione di Bot Framework SDK consentono di aggiungere lo stato al bot. I bot usano oggetti di gestione dello stato e archiviazione per gestire e rendere persistente lo stato. Il gestore dello stato offre un livello di astrazione che consente di accedere alle proprietà di stato usando funzioni di accesso alle proprietà indipendentemente dal tipo di archiviazione sottostante.
Nota
Gli SDK JavaScript, C# e Python di Bot Framework continueranno a essere supportati, ma Java SDK verrà ritirato con il supporto finale a lungo termine che termina a novembre 2023.
I bot esistenti creati con Java SDK continueranno a funzionare.
Il codice in questo articolo si basa sull'esempio di bot con gestione dello stato. È necessaria una copia dell'esempio in C#, JavaScript, Java o Python.
Informazioni sull'esempio
Dopo aver ricevuto l'input utente, l'esempio controlla lo stato della conversazione archiviato per verificare se all'utente è stato chiesto in precedenza il suo nome. Se non è stato fatto, viene richiesto il nome dell'utente e l'input viene archiviato nello stato dell'utente. In tal caso, il nome archiviato all'interno dello stato utente viene usato per conversare con l'utente e i relativi dati di input, insieme all'ora ricevuta e all'ID del canale di input, viene restituito all'utente. I valori relativi all'ora e all'ID canale vengono recuperati dai dati della conversazione utente e quindi salvati nello stato della conversazione. Il diagramma seguente illustra la relazione tra il bot, il profilo utente e le classi dei dati della conversazione.
Il primo passaggio per configurare la gestione dello stato consiste nel definire le classi che conterranno le informazioni da gestire nello stato dell'utente e della conversazione. L'esempio usato in questo articolo definisce le classi seguenti:
In UserProfile.cs si definisce una UserProfile classe per le informazioni utente raccolte dal bot.
In ConversationData.cs definisci una ConversationData classe per controllare lo stato della conversazione durante la raccolta delle informazioni utente.
Gli esempi di codice seguenti illustrano le definizioni per le classi 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;
}
Questo passaggio non è necessario in JavaScript.
Il primo passaggio per configurare la gestione dello stato consiste nel definire le classi che conterranno le informazioni da gestire nello stato dell'utente e della conversazione. L'esempio usato in questo articolo definisce le classi seguenti:
In UserProfile.java si definisce una UserProfile classe per le informazioni utente raccolte dal bot.
In ConversationData.java definisci una ConversationData classe per controllare lo stato della conversazione durante la raccolta delle informazioni utente.
Gli esempi di codice seguenti illustrano le definizioni per le classi 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) {
Il primo passaggio per configurare la gestione dello stato consiste nel definire le classi che conterranno le informazioni da gestire nello stato dell'utente e della conversazione. L'esempio usato in questo articolo definisce le classi seguenti:
Il user_profile.py contiene la UserProfile classe che archivia le informazioni utente raccolte dal bot.
Il conversation_data.py contiene la ConversationData classe che controlla lo stato della conversazione durante la raccolta delle informazioni utente.
Gli esempi di codice seguenti illustrano le definizioni per le classi UserProfile e ConversationData.
user_profile.py
class UserProfile:
def __init__(self, name: str = None):
self.name = name
Successivamente, si registra MemoryStorage che viene usato per creare UserState oggetti e ConversationState . Gli oggetti di stato dell'utente e della conversazione vengono creati in Startup e la dipendenza viene inserita nel costruttore del bot. Gli altri servizi registrati per un bot includono un provider di credenziali, un adattatore e l'implementazione 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 */
Successivamente, si registra MemoryStorage che viene quindi usato per creare UserState oggetti e ConversationState . Questi vengono creati in index.js e utilizzati durante la creazione del 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;
Successivamente, si registra in StateManagementBot Application.java. Sia ConversationState che UserState vengono forniti per impostazione predefinita dalla classe BotDependencyConfiguration e Spring li inserisce nel metodo getBot.
Application.java
@Bean
public Bot getBot(
ConversationState conversationState,
UserState userState
) {
return new StateManagementBot(conversationState, userState);
}
Successivamente, si registra MemoryStorage che viene usato per creare UserState oggetti e ConversationState . Questi vengono creati in app.py e utilizzati durante la creazione del 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")
Aggiungere le funzioni di accesso alle proprietà di stato
A questo punto si creano funzioni di accesso alle proprietà usando il CreateProperty metodo che fornisce un handle all'oggetto BotState . Ogni funzione di accesso alle proprietà di stato consente di ottenere o impostare il valore della proprietà di stato associata. Prima di usare le proprietà di stato, usare ogni funzione di accesso per caricare la proprietà dall'archiviazione e recuperarla dalla cache di stato. Per ottenere la chiave con ambito corretta associata alla proprietà di stato, chiamare il GetAsync metodo .
Bots/StateManagementBot.cs
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
Ora si creano funzioni di accesso alle proprietà per UserState e ConversationState. Ogni funzione di accesso alle proprietà di stato consente di ottenere o impostare il valore della proprietà di stato associata. È possibile usare ogni funzione di accesso per caricare la proprietà associata dall'archiviazione e recuperarne lo stato corrente dalla 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);
Ora si creano funzioni di accesso alle proprietà usando il createProperty metodo . Ogni funzione di accesso alle proprietà di stato consente di ottenere o impostare il valore della proprietà di stato associata. Prima di usare le proprietà di stato, usare ogni funzione di accesso per caricare la proprietà dall'archiviazione e recuperarla dalla cache di stato. Per ottenere la chiave con ambito corretta associata alla proprietà di stato, chiamare il get metodo .
Ora si creano funzioni di accesso alle proprietà per UserProfile e ConversationData. Ogni funzione di accesso alle proprietà di stato consente di ottenere o impostare il valore della proprietà di stato associata. È possibile usare ogni funzione di accesso per caricare la proprietà associata dall'archiviazione e recuperarne lo stato corrente dalla cache.
La sezione precedente descrive i passaggi in fase di inizializzazione per aggiungere le funzioni di accesso alle proprietà di stato al bot. A questo punto, è possibile usare queste funzioni di accesso in fase di esecuzione per leggere e scrivere informazioni sullo stato. Il codice di esempio usa il flusso logico seguente:
Se userProfile.Name è vuoto ed conversationData.PromptedUserForName è true, si recupera il nome utente specificato e lo si archivia all'interno dello stato utente.
Se userProfile.Name è vuoto ed conversationData.PromptedUserForName è false, viene richiesto il nome dell'utente.
Se userProfile.Name è stato archiviato in precedenza, si recuperano l'ora del messaggio e l'ID canale dall'input dell'utente, vengono restituiti tutti i dati all'utente e i dati recuperati all'interno dello stato della conversazione.
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}");
}
}
Prima di uscire dal gestore dei turni, usare il metodo SaveChangesAsync() degli oggetti di gestione dello stato per scrivere tutte le modifiche di stato nell'archiviazione.
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 è vuoto ed conversationData.PromptedUserForName è true, si recupera il nome utente specificato e lo si archivia all'interno dello stato utente.
Se userProfile.Name è vuoto ed conversationData.PromptedUserForName è false, viene richiesto il nome dell'utente.
Se userProfile.Name è stato archiviato in precedenza, si recuperano l'ora del messaggio e l'ID canale dall'input dell'utente, vengono restituiti tutti i dati all'utente e i dati recuperati all'interno dello stato della conversazione.
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();
});
Prima di uscire da ogni turno di dialogo, si usa il metodo saveChanges() degli oggetti di gestione dello stato per salvare in modo permanente tutte le modifiche scrivendo lo stato nella risorsa di archiviazione.
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() è vuoto ed conversationData.getPromptedUserForName() è true, si recupera il nome utente specificato e lo si archivia all'interno dello stato utente.
Se userProfile.getName() è vuoto ed conversationData.getPromptedUserForName() è false, viene richiesto il nome dell'utente.
Se userProfile.getName() è stato archiviato in precedenza, si recuperano l'ora del messaggio e l'ID canale dall'input dell'utente, vengono restituiti tutti i dati all'utente e i dati recuperati all'interno dello stato della conversazione.
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);
}
Prima di uscire dal gestore dei turni, usare il metodo saveChanges() degli oggetti di gestione dello stato per scrivere tutte le modifiche di stato nell'archiviazione.
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 è vuoto e conversation_data.prompted_for_user_name è true, il bot recupera il nome fornito dall'utente e lo archivia nel suo stato.
Se user_profile.name è vuoto e conversation_data.prompted_for_user_name è false, il bot chiede il nome dell'utente.
Se user_profile.name è stato archiviato in precedenza, il bot recupera l'ora del messaggio e l'ID canale dall'input dell'utente, restituisce i dati all'utente e archivia i dati recuperati nello stato della conversazione.
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 }"
)
Prima che ogni turno di dialogo termini, il bot usa il metodo degli save_changes oggetti di gestione dello stato per rendere persistenti tutte le modifiche scrivendo informazioni sullo stato nella risorsa di archiviazione.
Tutte le chiamate di gestione dello stato sono asincrone e la scrittura più recente ha la precedenza per impostazione predefinita. In pratica è necessario ottenere, impostare e salvare lo stato a distanza il più possibile ravvicinata nel bot. Per informazioni su come implementare il blocco ottimistico, vedere Implementare l'archiviazione personalizzata per il bot.
Dati aziendali critici
Usare lo stato del bot per archiviare le preferenze, il nome utente o l'ultima cosa ordinata, ma non usarla per archiviare i dati aziendali critici. Per i dati critici creare componenti di archiviazione personalizzati oppure scrivere i dati direttamente nell'archivio.
Riconoscimento del testo
L'esempio usa le librerie Microsoft/Recognizers-Text per analizzare e convalidare l'input dell'utente. Per ulteriori informazioni, vedere la Pagina Panoramica.
Passaggi successivi
Informazioni su come porre all'utente una serie di domande, convalidare le risposte e salvare l'input.