やりたいこと
Bot FrameworkからTeamsへアダプティブカードを送信したときにアダプティブカードの Aciton.Submit
で返される値をBot Frameworkのプロンプト入力として反応できるようにしたい
何がうまくいかなかったか
Bot FrameworkでTeamsに向けてアダプティブカードを送信したときに、ユーザーの選択をキャッチできるようにしたい。送信するアダプティブカードは下記のもので、4つのボタンそれぞれが Action.Submit
で、 data
プロパティには選択肢の日本語と同じ値を文字列で指定しているため、ボタンをクリックするとその値がBot Frameworkに帰ってくる想定です。
ただし、Teamsで動作確認したところ、アダプティブカードのボタンを押したときに返されるデータ(アダプティブカードの data
プロパティに設定した値)は、ユーザーがテキストを送信したときに値が格納される context.activity.text
ではなく、 context.activity.value
に入ってくるようでした。 ( context.activity.text
は undefined
になる)
context.activity.text
が undefined
になっていると、下記 FeedbackDialog
クラスの askFeedback
メソッドの return await stepContext.prompt(FEEDBACK_PROMPT_ID, '')
でユーザーの入力が発生していない判定となってしまい、永遠とダイアログが次に進まなくなってしまうところでつまずきました。
// ユーザーにフィードバックを求めるアダプティブカードの送信と、回答内容のログ保存処理を行っています
export class FeedbackDialog extends ComponentDialog {
// searchDialogから渡される会話UUID
instanceUUID: string
constructor() {
super(FEEDBACK_DIALOG_ID)
this.addDialog(new TextPrompt(FEEDBACK_PROMPT_ID))
this.addDialog(new TextPrompt(OTHER_FEEDBACK_PROMPT_ID))
this.addDialog(new WaterfallDialog(FEEDBACK_WATERFALL_DIALOG_ID, [
this.askFeedback.bind(this),
this.askOtherFeedback.bind(this),
this.saveOtherFeedback.bind(this),
this.endGetFeedbackDialog.bind(this),
]))
this.initialDialogId = FEEDBACK_WATERFALL_DIALOG_ID
}
/**
* フィードバックを選択肢でユーザーに入力依頼します。
*/
private async askFeedback (stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
// searchDialogから渡される会話UUIDをクラスプロパティに保管し、
// このクラスのどこからでも参照できるようにしている
this.instanceUUID = stepContext.context.turnState.get('instanceUUID')
// フィードバックをユーザーに依頼
const feedbackAdaptiveCard = CardFactory.adaptiveCard(adaptiveCards.feedbackCard)
await stepContext.context.sendActivity({ attachments: [feedbackAdaptiveCard] })
// アダプティブカードの選択を受け付けるために意図的に promptメソッドでユーザー入力を待っている
return await stepContext.prompt(FEEDBACK_PROMPT_ID, '')
}
/**
* 「その他」のフィールドバックをユーザーに入力依頼します。
*/
private async askOtherFeedback (stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
const userInput = stepContext.context.activity.text
if (userInput == 'ほしい情報だった') {
await upsertLog(this.instanceUUID, 'userFeedback', 'AccurateAnswer')
} else if (userInput == '回答になっていない') {
await upsertLog(this.instanceUUID, 'userFeedback', 'CouldNotAnswer')
} else if (userInput == '不正確な回答だった') {
await upsertLog(this.instanceUUID, 'userFeedback', 'InaccurateAnswer')
} else if (userInput == '情報が古い') {
await upsertLog(this.instanceUUID, 'userFeedback', 'OutdatedInformation')
} else {
// フィードバックを提供している選択肢ではなく、テキストで直接入力したときは、その他の判定として、入力内容をログに残す
await upsertLog(this.instanceUUID, 'userFeedback', 'Other')
}
return await stepContext.continueDialog()
}
/**
* 「その他」のフィードバックをユーザーからテキストで受け付けます。
* 'q'が入力されたときは、入力キャンセルとして扱います。
*/
private async saveOtherFeedback (stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
// 最後にユーザーが選択・入力した値が保持されているため、askOtherFeedbackメソッドでユーザーの入力がなかった場合は
// askFeedbackメソッドのユーザーの回答結果が引き続き参照されます
const userInput = stepContext.context.activity.text
await stepContext.context.sendActivity('フィードバックの入力にご協力いただき、ありがとうございます。')
if (!['ほしい情報だった', '回答になっていない', '不正確な回答だった', '情報が古い'].includes(userInput)) {
// その他のフィードバックをログに残しているのはここ
await upsertLog(this.instanceUUID, 'userFeedbackOther', userInput)
}
return await stepContext.continueDialog()
}
/**
* feedbackDialogを終了し、呼び出し元のsearchDialogクラスに戻ります
*/
private async endGetFeedbackDialog (stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
return await stepContext.continueDialog()
}
}
対応方法
ユーザーがアダプティブカードでボタンクリックしたときに、Bot Framework側の context.activity.text
に値が入るようになってさえいれば、アダプティブカードの値を拾ってダイアログの処理を進めることができるので、 mainDialog.ts
( yo
等を使ってボットアプリを作成していれば初めから作成されているファイルだと思います) に少し変更を加えます。
run
メソッド(このメソッドはユーザーの何かしら入力があったときに必ず毎回実行される)の前半に context.activity.value
の値を context.activity.text
に代入する処理を追記しておきます。
こうすれば FeedbackDialog
クラスの askFeedback
メソッドでユーザーのテキスト入力があった判定として処理が継続できるようになります。
context.activity.value
の値が文字列でなかった場合、正しく処理が進まないので、文字列に変換するか、 data
プロパティの値をはじめから文字列にしておきます。
export class MainDialog extends ComponentDialog {
public onboarding: boolean;
constructor() {
super(MAIN_DIALOG_ID);
this.addDialog(new WaterfallDialog(MAIN_WATERFALL_DIALOG_ID, [
this.introStep.bind(this),
this.actStep.bind(this),
this.finalStep.bind(this)
]))
this.addDialog(new TextPrompt('TextPrompt'))
this.addDialog(new TeamsInfoDialog())
this.addDialog(new HelpDialog())
this.addDialog(new MentionUserDialog())
this.addDialog(new SearchDialog())
this.initialDialogId = MAIN_WATERFALL_DIALOG_ID;
this.onboarding = false;
}
public async run(context: TurnContext, accessor: StatePropertyAccessor<DialogState>) {
// ユーザー入力がPOSTで来たときは無理やりテキスト入力として認識できるようにする
const userInputText = context.activity.text
const userInputBody = context.activity.value
if (userInputText == undefined && userInputBody) {
context.activity.text = String(userInputBody)
}
const dialogSet = new DialogSet(accessor);
dialogSet.add(this);
const dialogContext = await dialogSet.createContext(context);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(this.id);
}
}
private async introStep(stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
if ((stepContext.options as any).restartMsg) {
const messageText = (stepContext.options as any).restartMsg ? (stepContext.options as any).restartMsg : 'What can I help you with today?';
const promptMessage = MessageFactory.text(messageText, messageText, InputHints.ExpectingInput);
return await stepContext.prompt('TextPrompt', { prompt: promptMessage });
} else {
this.onboarding = true;
return await stepContext.next();
}
}
private async actStep(stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
// ユーザーの入力を取得
let userInput: string | undefined
if (stepContext.result) {
userInput = stepContext.result.trim().toLocaleLowerCase();
} else if (this.onboarding) {
userInput = stepContext.context.activity.text
}
if (!userInput) return await stepContext.next()
// ヘルプのダイアログを実行
if (['help', 'Help', 'ヘルプ', 'へるぷ'].includes(userInput)) {
return await stepContext.beginDialog('helpDialog')
}
// 検索を投げるダイアログを実行
return await stepContext.beginDialog('searchDialog')
}
private async finalStep(stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
return await stepContext.replaceDialog(this.initialDialogId, { restartMsg: 'XXXXXXXXについて質問してください。' });
}
}
備考
- AzureボットのWebチャットでテストのときは、上記のことをしなくても問題なかったですが、Teamsで動かすと動きが違うようで今回のような対応が必要になりました
- こちらの記事を参考にしました
- もともとユーザーに選択肢入力してもらう仕組みを
suggestedActions
を使って実装したんですが、これもTeamsで動作しないことが分かり、アダプティブカードを使うしか方法がありませんでしたBot Framework SDK でメッセージに推奨されるアクションを追加する - Bot Service | Microsoft Learn