봇은 기본적으로 상태 비지정입니다. 봇이 배포되면 동일한 프로세스 또는 동일한 컴퓨터에서 한 턴에서 다음 턴으로 실행되지 않을 수 있습니다. 그러나 봇은 대화의 동작을 관리하고 이전 질문에 대한 답변을 기억할 수 있도록 대화의 컨텍스트를 추적해야 할 수 있습니다. Bot Framework SDK의 상태 및 스토리지 기능을 사용하면 봇에 상태를 추가할 수 있습니다. 봇은 상태 관리 및 스토리지 개체를 사용하여 상태를 관리하고 유지합니다. 상태 관리자는 기본 스토리지 유형과 관계없이 속성 접근자를 사용하여 상태 속성에 액세스할 수 있는 추상화 계층을 제공합니다.
참고 항목
Bot Framework JavaScript, C#및 Python SDK는 계속 지원되지만 Java SDK는 2023년 11월에 종료되는 최종 장기 지원으로 사용 중지됩니다.
이 문서의 코드는 상태 관리 봇 샘플을 기반으로 합니다. C#, JavaScript, Java 또는 Python에서 샘플의 복사본이 필요합니다.
이 샘플 정보
사용자 입력을 수신할 때 이 샘플은 저장된 대화 상태를 확인하여 이 사용자에게 해당 사용자의 이름을 제공하라는 프롬프트를 표시했는지 확인합니다. 프롬프트를 표시하지 않은 경우 사용자의 이름을 요청하며 해당 입력은 사용자 상태 내에 저장됩니다. 이 경우 사용자 상태 내에 저장된 이름은 수신된 시간 및 입력 채널 ID와 함께 사용자 및 해당 입력 데이터와 대화하는 데 사용됩니다. 시간 및 채널 ID 값은 사용자 대화 데이터에서 검색된 다음 대화 상태로 저장됩니다. 다음 다이어그램은 봇, 사용자 프로필 및 대화 데이터 클래스 간의 관계를 보여줍니다.
상태 관리를 설정하는 첫 번째 단계는 사용자 및 대화 상태에서 관리하려는 모든 정보를 포함하는 클래스를 정의하는 것입니다. 이 문서에 사용된 예제에서는 다음 클래스를 정의합니다.
UserProfile.cs 봇이 수집할 사용자 정보에 대한 클래스를 정의 UserProfile 합니다.
ConversationData.cs 사용자 정보를 수집하는 동안 대화 상태를 제어하는 클래스를 정의 ConversationData 합니다.
다음 코드 예제에서는 클래스 및 ConversationData 클래스에 UserProfile 대한 정의를 보여 줍니다.
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;
}
JavaScript에서는 이 단계가 필요하지 않습니다.
상태 관리를 설정하는 첫 번째 단계는 사용자 및 대화 상태에서 관리하려는 모든 정보를 포함하는 클래스를 정의하는 것입니다. 이 문서에서 사용되는 예제는 다음 클래스를 정의합니다.
UserProfile.java 봇이 수집할 사용자 정보에 대한 클래스를 정의 UserProfile 합니다.
ConversationData.java 사용자 정보를 수집하는 동안 대화 상태를 제어하는 클래스를 정의 ConversationData 합니다.
다음 코드 예제에서는 클래스 및 ConversationData 클래스에 UserProfile 대한 정의를 보여 줍니다.
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) {
상태 관리를 설정하는 첫 번째 단계는 사용자 및 대화 상태에서 관리하려는 모든 정보를 포함하는 클래스를 정의하는 것입니다. 이 문서에 사용된 예제에서는 다음 클래스를 정의합니다.
user_profile.py 봇에서 UserProfile 수집한 사용자 정보를 저장하는 클래스를 포함합니다.
conversation_data.py 사용자 정보를 수집하는 동안 대화 상태를 제어하는 클래스를 포함합니다ConversationData.
다음 코드 예제에서는 클래스 및 ConversationData 클래스에 UserProfile 대한 정의를 보여 줍니다.
user_profile.py
class UserProfile:
def __init__(self, name: str = None):
self.name = name
다음으로, 만들고 개체를 만드는 UserStateConversationState 데 사용되는 등록 MemoryStorage 을 합니다. 사용자 및 대화 상태 개체가 생성 Startup 되고 종속성이 봇 생성자에 삽입됩니다. 등록된 봇에 대한 다른 서비스는 자격 증명 공급자, 어댑터 및 봇 구현입니다.
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 */
다음으로, 만들고 개체를 만드는 UserStateConversationState 데 사용되는 등록 MemoryStorage 을 합니다. index.js 만들어 지고 봇을 만들 때 사용됩니다.
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;
다음으로, Application.java 등록 StateManagementBot 합니다. ConversationState와 UserState는 기본적으로 BotDependencyConfiguration 클래스에서 제공되며 Spring은 getBot 메서드에 삽입합니다.
Application.java
@Bean
public Bot getBot(
ConversationState conversationState,
UserState userState
) {
return new StateManagementBot(conversationState, userState);
}
다음으로, 만들고 개체를 만드는 UserStateConversationState 데 사용되는 등록 MemoryStorage 을 합니다. app.py 만들어 지고 봇을 만들 때 사용됩니다.
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")
이제 개체에 대한 핸들 BotState 을 CreateProperty 제공하는 메서드를 사용하여 속성 접근자를 만듭니다. 각 상태 속성 접근자를 사용하면 연결된 상태 속성의 값을 가져오거나 설정할 수 있습니다. 상태 속성을 사용하기 전에 각 접근자를 사용하여 스토리지에서 속성을 로드하고 상태 캐시에서 가져옵니다. 상태 속성과 연결된 적절한 범위의 키를 얻으려면 메서드를 호출합니다 GetAsync .
Bots/StateManagementBot.cs
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
이제 및 에 대한 UserStateConversationState속성 접근자를 만듭니다. 각 상태 속성 접근자를 사용하면 연결된 상태 속성의 값을 가져오거나 설정할 수 있습니다. 각 접근자를 사용하여 스토리지에서 연결된 속성을 로드하고 캐시에서 현재 상태를 검색합니다.
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);
이제 메서드를 사용하여 속성 접근자를 만듭니다 createProperty . 각 상태 속성 접근자를 사용하면 연결된 상태 속성의 값을 가져오거나 설정할 수 있습니다. 상태 속성을 사용하기 전에 각 접근자를 사용하여 스토리지에서 속성을 로드하고 상태 캐시에서 가져옵니다. 상태 속성과 연결된 적절한 범위의 키를 얻으려면 메서드를 호출합니다 get .
이제 및 에 대한 UserProfileConversationData속성 접근자를 만듭니다. 각 상태 속성 접근자를 사용하면 연결된 상태 속성의 값을 가져오거나 설정할 수 있습니다. 각 접근자를 사용하여 스토리지에서 연결된 속성을 로드하고 캐시에서 현재 상태를 검색합니다.
비어 있고 conversationData.PromptedUserForName true이면 userProfile.Name 제공된 사용자 이름을 검색하고 사용자 상태 내에 저장합니다.
비어 있고 conversationData.PromptedUserForName false이면 userProfile.Name 사용자의 이름을 요청합니다.
이전에 저장된 경우 userProfile.Name 사용자 입력에서 메시지 시간 및 채널 ID를 검색하고, 모든 데이터를 사용자에게 다시 에코하고, 검색된 데이터를 대화 상태 내에 저장합니다.
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}");
}
}
턴 처리기를 종료하기 전에 상태 관리 개체의 SaveChangesAsync() 메서드를 사용하여 모든 상태 변경 내용을 스토리지에 다시 씁니다.
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);
}
비어 있고 conversationData.PromptedUserForName true이면 userProfile.Name 제공된 사용자 이름을 검색하고 사용자 상태 내에 저장합니다.
비어 있고 conversationData.PromptedUserForName false이면 userProfile.Name 사용자의 이름을 요청합니다.
이전에 저장된 경우 userProfile.Name 사용자 입력에서 메시지 시간 및 채널 ID를 검색하고, 모든 데이터를 사용자에게 다시 에코하고, 검색된 데이터를 대화 상태 내에 저장합니다.
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();
});
각 대화 상자를 종료하기 전에 상태 관리 개체의 saveChanges() 메서드를 사용하여 상태를 스토리지에 다시 작성하여 모든 변경 내용을 유지합니다.
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);
}
비어 있고 conversationData.getPromptedUserForName() true이면 userProfile.getName() 제공된 사용자 이름을 검색하고 사용자 상태 내에 저장합니다.
비어 있고 conversationData.getPromptedUserForName() false이면 userProfile.getName() 사용자의 이름을 요청합니다.
이전에 저장된 경우 userProfile.getName() 사용자 입력에서 메시지 시간 및 채널 ID를 검색하고, 모든 데이터를 사용자에게 다시 에코하고, 검색된 데이터를 대화 상태 내에 저장합니다.
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);
}
턴 처리기를 종료하기 전에 상태 관리 개체의 saveChanges() 메서드를 사용하여 모든 상태 변경 내용을 스토리지에 다시 씁니다.
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));
}
user_profile.name이 비어 있고 conversation_data.prompted_for_user_name이 true인 경우 봇은 사용자가 제공한 이름을 검색하여 사용자의 상태에 저장합니다.
비어 있고 conversation_data.prompted_for_user_name false이면 user_profile.name 봇은 사용자의 이름을 요청합니다.
이전에 저장된 경우 user_profile.name 봇은 사용자 입력에서 메시지 시간 및 채널 ID를 검색하고, 데이터를 사용자에게 다시 에코하고, 검색된 데이터를 대화 상태에 저장합니다.
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 }"
)
각 대화 상자가 끝나기 전에 봇은 상태 관리 개체의 save_changes 메서드를 사용하여 스토리지에 상태 정보를 작성하여 모든 변경 내용을 유지합니다.
상태 관리 호출은 모두 비동기적이며, 기본적으로 최종 작성자 인정(last-writer-wins)입니다. 실제로 봇에서 상태를 가능한 한 가깝게 설정하고 저장해야 합니다. 낙관적 잠금을 구현하는 방법에 대한 자세한 내용은 봇에 대한 사용자 지정 스토리지 구현을 참조하세요.
중요한 비즈니스 데이터
봇 상태를 사용하여 기본 설정, 사용자 이름 또는 마지막으로 주문한 것을 저장하지만 중요한 비즈니스 데이터를 저장하는 데는 사용하지 마세요. 중요한 데이터의 경우 고유한 스토리지 구성 요소를 만들거나 스토리지에 직접 씁니다.
인식기 텍스트
이 샘플에서는 Microsoft/Recognizers-Text 라이브러리를 사용하여 사용자 입력을 구문 분석하고 유효성을 검사합니다. 자세한 내용은 개요 페이지를 참조하세요.
다음 단계
사용자에게 일련의 질문을 하고, 답변의 유효성을 검사하고, 입력을 저장하는 방법을 알아봅니다.