LoginSignup
0
0

Teamsのアダプティブカードの応答をBot Frameworkでキャッチできるようにする

Last updated at Posted at 2024-05-14

やりたいこと

Bot FrameworkからTeamsへアダプティブカードを送信したときにアダプティブカードの Aciton.Submit で返される値をBot Frameworkのプロンプト入力として反応できるようにしたい

何がうまくいかなかったか

Bot FrameworkでTeamsに向けてアダプティブカードを送信したときに、ユーザーの選択をキャッチできるようにしたい。送信するアダプティブカードは下記のもので、4つのボタンそれぞれが Action.Submit で、 data プロパティには選択肢の日本語と同じ値を文字列で指定しているため、ボタンをクリックするとその値がBot Frameworkに帰ってくる想定です。

image.png

ただし、Teamsで動作確認したところ、アダプティブカードのボタンを押したときに返されるデータ(アダプティブカードの data プロパティに設定した値)は、ユーザーがテキストを送信したときに値が格納される context.activity.text ではなく、 context.activity.value に入ってくるようでした。 ( context.activity.textundefined になる)

context.activity.textundefined になっていると、下記 FeedbackDialog クラスの askFeedback メソッドの return await stepContext.prompt(FEEDBACK_PROMPT_ID, '') でユーザーの入力が発生していない判定となってしまい、永遠とダイアログが次に進まなくなってしまうところでつまずきました。

feedbackDialog.ts
// ユーザーにフィードバックを求めるアダプティブカードの送信と、回答内容のログ保存処理を行っています

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.tsyo 等を使ってボットアプリを作成していれば初めから作成されているファイルだと思います) に少し変更を加えます。

run メソッド(このメソッドはユーザーの何かしら入力があったときに必ず毎回実行される)の前半に context.activity.value の値を context.activity.text に代入する処理を追記しておきます。
こうすれば FeedbackDialog クラスの askFeedback メソッドでユーザーのテキスト入力があった判定として処理が継続できるようになります。
context.activity.value の値が文字列でなかった場合、正しく処理が進まないので、文字列に変換するか、 data プロパティの値をはじめから文字列にしておきます。

mainDialog.ts

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について質問してください。' });
    }
}

備考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0