#botframework
#botframework
Вопрос:
TL; DR
Когда мой бот ожидает приглашения (например, TextPrompt) и заканчивается другое диалоговое окно (поскольку пользовательский ввод вызвал действие прерывания, такое как «справка», которое запустило диалоговое окно справки, которое просто выводит текст справки), OnPromptAsync
вызывается метод этого приглашения и снова запрашивает текст подсказки. Я этого не хочу. Я хочу, чтобы диалоговое окно запроса ожидало ввода пользователем после завершения диалога справки.
Подробная
У меня есть бот, который запрашивает что-то с помощью TextPrompt, а затем ожидает ответа пользователя. Я реализовал пользовательские прерывания, как описано здесь, чтобы перехватывать запросы о помощи . Если пользователь ввел «справка», бот должен вывести некоторый текст справки (в ExampleDialog, см. Ниже), а затем снова ждать ввода пользователя.
Основной каталог
public class MainDialog : ComponentDialog
{
public MainDialog() : base("Main")
{
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new ExampleDialog(nameof(ExampleDialog)));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
PromptStep,
EvaluationStep
}));
InitialDialogId = nameof(WaterfallDialog);
}
protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(innerDc.Context.Activity.Text))
{
// Check for interruptions
var result = await InterruptAsync(innerDc, cancellationToken);
if (result != null)
{
return resu<
}
}
return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}
private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken)
{
if (innerDc.Context.Activity.Type == ActivityTypes.Message)
{
string text = innerDc.Context.Activity.Text;
// Catch request for help
if (text == "help")
{
await innerDc.BeginDialogAsync(nameof(ExampleDialog), null, cancellationToken);
return Dialog.EndOfTurn;
}
}
return null;
}
private async Task<DialogTurnResult> PromptStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions()
{
Prompt = MessageFactory.Text("Please enter some text. Type 'help' if you need some examples."),
});
}
private async Task<DialogTurnResult> EvaluationStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("You typed: " stepContext.Result as string));
return await stepContext.EndDialogAsync(null, cancellationToken);
}
}
ExampleDialog
public class ExampleDialog : ComponentDialog
{
public ExampleDialog(string dialogId) : base(dialogId)
{
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
ExampleStep
}));
InitialDialogId = nameof(WaterfallDialog);
}
private async Task<DialogTurnResult> ExampleStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Example: bla bla"));
return await stepContext.NextAsync(null, cancellationToken);
// ExampleDialog ends here
}
}
Проблема в том, что, когда ExampleDialog завершается после вывода текста справки, TextPrompt возобновляется и снова запрашивает свое сообщение. Это приводит к этому разговору:
Bot: Hello world!
Bot: Please enter some text. Type ‘help’ if you need some examples.
User: help
Bot: Example: bla bla
Bot: Please enter some text. Type ‘help’ if you need some examples.
I don’t want this last line to be reprompted by the bot. How can I fix this?
Заранее спасибо
РЕДАКТИРОВАТЬ 1: (не совсем удовлетворительный) обходной путь
Я нашел решение, которое на самом деле меня не удовлетворяет. Я создал свой собственный класс TextPrompt с именем MyTextPrompt и перезаписал ResumeDialogAsync:
public class MyTextPrompt : TextPrompt
{
public MyTextPrompt(string id) : base(id)
{
}
public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext dc, DialogReason reason, object result = null, CancellationToken cancellationToken = default)
{
return Dialog.EndOfTurn;
}
}
В MainDialog я просто заменил TextPrompt на MyTextPrompt в конструкторе
AddDialog(new MyTextPrompt(nameof(MyTextPrompt)));
и используйте правильный идентификатор диалогового окна в PromptStep
private async Task<DialogTurnResult> PromptStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.PromptAsync(nameof(MyTextPrompt), new PromptOptions()
{
Prompt = MessageFactory.Text("Please enter some text. Type 'help' if you need some examples."),
});
}
Результатом является этот разговор:
Bot: Hello world!
Bot: Please enter some text. Type ‘help’ if you need some examples.
User: help
Bot: Example: bla bla
/* bot now waits at this point of the conversation */
User: bla bla
Bot: You typed: bla bla
Хорошо, отлично. Это то, чего я хотел, не так ли?
Да, это так, но есть некоторые недостатки:
- Это должно быть сделано для каждого отдельного типа диалогового окна запроса.
- Если вы уже перезаписали метод ResumeDialogAsync в пользовательском классе prompt, каким-то образом вам нужно отслеживать, что вызывает вызов ResumeDailogAsync .
Как это можно решить элегантным способом?
Комментарии:
1. Вот интересный анекдот. Много размышлений было уделено вопросу о том, должно ли приглашение использовать свое приглашение повторить попытку или обычное приглашение при его возобновлении. Это считалось мелкой проблемой , потому что приглашения, как правило, являются конечными узлами в стеке, поэтому они никогда не возобновляются. Ваш случай демонстрирует, что действительно есть боты, которым необходимо возобновить запросы. В любом случае, похоже, что ваш диалог справки является однооборотным и поэтому не обязательно должен быть диалогом. Вы были бы удовлетворены просто отправкой справочного сообщения без диалога?
2. В этом случае можно отправлять сообщение без диалога. Но вопрос остается актуальным для других вариантов использования. Так что, если у кого-нибудь есть элегантный способ, я был бы очень заинтересован.
3. Можете ли вы объяснить, что вы подразумеваете под элегантным, чтобы мы могли предоставить удовлетворительный ответ, который вы примете? Вы просто ищете решение, которое устраняет две проблемы, которые вы отметили в своем собственном решении?
4. Под «элегантным» я подразумеваю лучшее решение, чем мое, надеюсь, без двух недостатков.
Ответ №1:
Я могу придумать три основные возможности для получения приглашения не перепрофилировать при его возобновлении.
1. Аннулируйте приглашение
Единственный способ фактически остановить перепрофилирование встроенных подсказок при их возобновлении — установить Prompt
для свойства значение null. Я упомянул в комментариях, что возник некоторый вопрос о том, следует ли считать возобновление запроса повторной попыткой запроса, и вам повезло, потому что возобновление запроса не считается повторной попыткой. Это означает, что вы можете оставить RetryPrompt
свойство нетронутым, чтобы оно по-прежнему автоматически перепрофилировалось при неверном вводе, как обычно. Все, о чем вам нужно позаботиться, это сделать так, чтобы начальное приглашение отображалось без свойства prompt , и это достаточно просто, потому что вы можете просто отправить сообщение, сформулированное как приглашение, прежде чем вызывать фактическое приглашение.
2. Используйте промежуточное программное обеспечение
Даже если приглашение пытается перепрофилировать, оно делает это через контекст turn, и поэтому действие, которое оно пытается отправить пользователю, будет проходить через конвейер промежуточного программного обеспечения. Это означает, что вы можете перехватить действие в пользовательском промежуточном программном обеспечении и предотвратить его фактическую отправку пользователю.
Для того, чтобы ваше пользовательское промежуточное программное обеспечение распознало действие запроса как нечто, что оно должно перехватить, для действия потребуется какой-то тег в одном из его свойств, для поиска которого запрограммировано промежуточное программное обеспечение. Действия содержат много скрытых метаданных, которые вы могли бы использовать для чего-то подобного, например ChannelData
, или Entities
или Value
или Properties
. Вы не хотите, чтобы промежуточное программное обеспечение постоянно блокировало действие, потому что большую часть времени вы действительно хотите, чтобы отображалось приглашение, поэтому также должен быть переключатель в состоянии поворота, который вы можете активировать в диалоговом окне справки и который промежуточное программное обеспечение может проверить.
Для того, чтобы ваше пользовательское промежуточное программное обеспечение фактически остановило выполнение действия, вы можете либо замкнуть конвейер, либо заменить действие чем-то, что не будет отображаться в диалоге.
3. Создайте класс-оболочку
Эта возможность основана на идее, которую вы уже придумали, но вместо того, чтобы извлекать из каждого существующего приглашения, вы можете просто создать один класс, который может обернуть любое приглашение. Я не буду делать всю реализацию для вас, но подпись класса может выглядеть примерно так:
class NonResumingPrompt<TPrompt, TValue> : Prompt<TValue> where TPrompt : Prompt<TValue>
Вы могли бы создать экземпляр NonResumingPrompt
, например, так:
new NonResumingPrompt<TextPrompt, string>()
Затем код может внутренне создать экземпляр TPrompt
и определить NonResumingPrompt.OnPromptAsync
, NonResumingPrompt.OnRecognizeAsync
, и NonResumingPrompt.OnPreBubbleEventAsync
вызвать соответствующие методы в обернутом TPrompt
экземпляре.
Это решило бы проблему необходимости создавать собственную версию каждого приглашения, но для этого потребовалось бы переопределить еще несколько методов, чем в вашем решении. Я должен упомянуть, что создание собственной версии каждого приглашения не сильно отличается от того, что произошло при создании библиотеки адаптивных диалоговых окон. Были созданы новые «адаптивные» версии (называемые входными данными) каждого приглашения, и, к сожалению, большая часть кода оказалась дублированной. Вы можете смело опробовать адаптивные диалоги, если хотите.
Комментарии:
1. Большое спасибо. Номер 1 кажется самым простым способом, и я думаю, что это лучше, чем мое решение.