Chatbot - 분기 및 루프를 사용하여 고급 대화 흐름 만들기(multi-dialog)


공식 문서에서 나온 샘플은 최대 2개의 회사를 검토하기 위해 사용자가 가입할 수 있는 봇을 보여준다.

봇은 3개의 구성 요소 대화르 사용하여 대화 흐름을 관리한다.

각 구성 요소 대화(아마 dialog를 말하는 것 같다)에는 폭포 대화와 사용자 입력을 수집하는 데 필요한 프롬프트가 포함된다.

대화 상태를 사용하여 해당 대화를 관리하고 사용자 상태를 사용해서 사용자, 사용자가 검토하려는 회사에 대한 정보를 저장한다.

complex-conversation-flow

사용자 프로필 정의

사용자 프로필에는 대화, 사용자의 이름, 나이와 검토하도록 선택한 회사에서 수집한 정보가 포함된다.

UserProfile.cs

/// <summary>Contains information about a user.</summary>
public class UserProfile
{
    public string Name { get; set; }
    public int Age { get; set; }

    // The list of companies the user wants to review.
    public List<string> CompaniesToReview { get; set; } = new List<string>();
}

대화 만들기

이 봇에는 3개의 대화가 포함되어 있다.

  • 기본 대화는 전체 프로세스를 시작한 다음, 수집된 정보를 요약합니다.
  • 최상위 대화는 사용자 정보를 수집하고 사용자의 나이를 기준으로 분기 논리를 포함합니다.
  • 검토 선택 대화를 통해 사용자는 검토할 회사를 반복적으로 선택할 수 있습니다. 이 작업에는 반복되는 논리가 사용됩니다.

기본 대화

기본 대화는 다음 2단계로 이루어진다.

  1. 최상위 대화를 시작한다.
  2. 최상위 대화에서 수집한 사용자 프로필을 검색, 요약하고 사용자 상태에 해당 정보를 저장한 다음, 기본 대화의 끝을 알린다.

Dialogs/MainDialog.cs

private async Task<DialogTurnResult> InitialStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    return await stepContext.BeginDialogAsync(nameof(TopLevelDialog), null, cancellationToken);
}

private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var userInfo = (UserProfile)stepContext.Result;

    string status = "You are signed up to review "
        + (userInfo.CompaniesToReview.Count is 0 ? "no companies" : string.Join(" and ", userInfo.CompaniesToReview))
        + ".";

    await stepContext.Context.SendActivityAsync(status);

    var accessor = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
    await accessor.SetAsync(stepContext.Context, userInfo, cancellationToken);

    return await stepContext.EndDialogAsync(null, cancellationToken);
}

최상위 대화

최상위 대화는 다음 4단계로 이루어진다.

  1. 사용자의 이름을 요청합니다.
  2. 사용자의 나이를 요청합니다.
  3. 사용자의 나이를 기준으로 검토 선택 대화를 시작하거나 다음 단계로 진행합니다.
  4. 마지막으로 사용자의 참여에 대한 감사 인사를 보내고, 수집된 정보를 반환합니다.

첫번째 단계에서는 대화 상태의 일부로 빈 사용자 프로필을 만든다. 빈 프로필로 대화가 시작되고 진행되면서 프로필에 정보가 추가된다.

대화가 끝나면 마지막 단계가 수집된 정보를 반환한다.

세번째 단계(선택 시작)에서는 사용자의 나이에 따라 대화 흐름이 나눠진다.

Dialogs/TopLevelDialog.cs

private static async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Create an object in which to collect the user's information within the dialog.
    stepContext.Values[UserInfo] = new UserProfile();

    var promptOptions = new PromptOptions { Prompt = MessageFactory.Text("Please enter your name.") };

    // Ask the user to enter their name.
    return await stepContext.PromptAsync(nameof(TextPrompt), promptOptions, cancellationToken);
}

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Set the user's name to what they entered in response to the name prompt.
    var userProfile = (UserProfile)stepContext.Values[UserInfo];
    userProfile.Name = (string)stepContext.Result;

    var promptOptions = new PromptOptions { Prompt = MessageFactory.Text("Please enter your age.") };

    // Ask the user to enter their age.
    return await stepContext.PromptAsync(nameof(NumberPrompt<int>), promptOptions, cancellationToken);
}

private async Task<DialogTurnResult> StartSelectionStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Set the user's age to what they entered in response to the age prompt.
    var userProfile = (UserProfile)stepContext.Values[UserInfo];
    userProfile.Age = (int)stepContext.Result;

    //나이를 물어보는 부분
    if (userProfile.Age < 25)
    {
        // If they are too young, skip the review selection dialog, and pass an empty list to the next step.
        await stepContext.Context.SendActivityAsync(
            MessageFactory.Text("You must be 25 or older to participate."),
            cancellationToken);
        return await stepContext.NextAsync(new List<string>(), cancellationToken);
    }
    else
    {
        // Otherwise, start the review selection dialog.
        return await stepContext.BeginDialogAsync(nameof(ReviewSelectionDialog), null, cancellationToken);
    }
    //여기까지
}

private async Task<DialogTurnResult> AcknowledgementStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Set the user's company selection to what they entered in the review-selection dialog.
    var userProfile = (UserProfile)stepContext.Values[UserInfo];
    userProfile.CompaniesToReview = stepContext.Result as List<string> ?? new List<string>();

    // Thank them for participating.
    await stepContext.Context.SendActivityAsync(
        MessageFactory.Text($"Thanks for participating, {((UserProfile)stepContext.Values[UserInfo]).Name}."),
        cancellationToken);

    // Exit the dialog, returning the collected user information.
    return await stepContext.EndDialogAsync(stepContext.Values[UserInfo], cancellationToken);
}

검토 선택 대화

검토 선택 대화는 다음 2단계로 이루어진다.

  1. 사용자에게 검토할 회사를 선택하도록 요청하거나 done을 선택하여 완료한다.
    • 대화가 초기 정보로 시작된 경우 폭포 단계 컨텍스트의 옵션 속성을 통해 정보를 사용할 수 있습니다. 검토 선택 대화가 다시 시작될 수 있으며 이는 사용자가 검토할 회사를 여러 개 선택할 수 있도록 하는 데 사용됩니다.
    • 사용자가 검토할 회사를 이미 선택한 경우 해당 회사는 사용 가능한 선택 항목에서 제거됩니다.
    • 사용자가 루프를 일찍 종료할 수 있도록 done 선택 항목이 추가됩니다.
  2. 이 대화를 반복하거나 적절하게 종료합니다.
    • 사용자가 검토할 회사를 선택한 경우 목록에 추가합니다.
    • 사용자가 2개의 회사를 선택했거나 종료하도록 선택한 경우 대화를 끝내고 수집된 목록을 반환합니다.
    • 그렇지 않으면 대화를 다시 시작하여 목록의 내용으로 초기화합니다.

Dialogs/ReviewSelectionDialog.cs

private async Task<DialogTurnResult> SelectionStepAsync(
    WaterfallStepContext stepContext,
    CancellationToken cancellationToken)
{
    // Continue using the same selection list, if any, from the previous iteration of this dialog.
    var list = stepContext.Options as List<string> ?? new List<string>();
    stepContext.Values[CompaniesSelected] = list;

    // Create a prompt message.
    string message;
    if (list.Count is 0)
    {
        message = $"Please choose a company to review, or `{DoneOption}` to finish.";
    }
    else
    {
        message = $"You have selected **{list[0]}**. You can review an additional company, " +
            $"or choose `{DoneOption}` to finish.";
    }

    // Create the list of options to choose from.
    var options = _companyOptions.ToList();
    options.Add(DoneOption);
    if (list.Count > 0)
    {
        options.Remove(list[0]);
    }

    var promptOptions = new PromptOptions
    {
        Prompt = MessageFactory.Text(message),
        RetryPrompt = MessageFactory.Text("Please choose an option from the list."),
        Choices = ChoiceFactory.ToChoices(options),
    };

    // Prompt the user for a choice.
    return await stepContext.PromptAsync(nameof(ChoicePrompt), promptOptions, cancellationToken);
}

private async Task<DialogTurnResult> LoopStepAsync(
    WaterfallStepContext stepContext,
    CancellationToken cancellationToken)
{
    // Retrieve their selection list, the choice they made, and whether they chose to finish.
    var list = stepContext.Values[CompaniesSelected] as List<string>;
    var choice = (FoundChoice)stepContext.Result;
    var done = choice.Value == DoneOption;

    if (!done)
    {
        // If they chose a company, add it to the list.
        list.Add(choice.Value);
    }

    //이부분부터
    if (done || list.Count >= 2)
    {
        // If they're done, exit and return their list.
        return await stepContext.EndDialogAsync(list, cancellationToken);
    }
    else
    {
        // Otherwise, repeat this dialog, passing in the list from this iteration.
        return await stepContext.ReplaceDialogAsync(nameof(ReviewSelectionDialog), list, cancellationToken);
    }
    //여기까지
}

대화 실행

대화 봇 클래스는 작업 처리기를 확장하며 대화 실행을 위한 논리를 포함한다. 대화 및 환 영 봇 클래스는 대화 봇을 확장하여 대화 참여 시 사용자를 환영한다.

봇의 순서 처리기는 3개의 대화로 정의되는 대화 흐름을 반복한다. 사용자로부터 메세지를 받는 경우

  1. 기본 대화를 실행합니다.
    • 대화 스택이 비어 있으면 기본 대화가 시작됩니다.
    • 그렇지 않으면 대화가 아직 진행 중인 것이므로 활성 대화가 계속됩니다.
  2. 사용자, 대화 및 대화 상태에 대한 모든 업데이트가 유지되도록 상태가 저장됩니다.

Bots/DialogBot.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);
}

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    Logger.LogInformation("Running dialog with Message Activity.");

    // Run the Dialog with the new message Activity.
    await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}

봇용 서비스 등록

필요에 따라 서비스를 만들고 등록한다.

  • 봇에 대한 기본 서비스 : 어댑터 및 봇 구현
  • 상태 관리 서비스 : 스토리지, 사용자 상태 및 대화 상태
  • 봇이 사용할 루트 대회

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddNewtonsoftJson();

    // Create the Bot Framework Adapter with error handling enabled.
    services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

    // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)
    services.AddSingleton<IStorage, MemoryStorage>();

    // Create the User state. (Used in this bot's Dialog implementation.)
    services.AddSingleton<UserState>();

    // Create the Conversation state. (Used by the Dialog system itself.)
    services.AddSingleton<ConversationState>();

    services.AddSingleton<MainDialog>();
    // Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
    services.AddTransient<IBot, DialogAndWelcomeBot<MainDialog>>();
}