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

# やりたいこと

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1022607/1a434f6a-9165-6218-b301-35fc39d24e7b.png)

ただし、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, '')` でユーザーの入力が発生していない判定となってしまい、永遠とダイアログが次に進まなくなってしまうところでつまずきました。

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

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

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

# 備考

* AzureボットのWebチャットでテストのときは、上記のことをしなくても問題なかったですが、Teamsで動かすと動きが違うようで今回のような対応が必要になりました
* こちらの記事を参考にしました
    > [c\# \- What is the right way to retrieve data from Adaptive Card without displayed card again after submit? \- Stack Overflow](https://stackoverflow.com/questions/57536168/what-is-the-right-way-to-retrieve-data-from-adaptive-card-without-displayed-card)
* もともとユーザーに選択肢入力してもらう仕組みを `suggestedActions` を使って実装したんですが、これもTeamsで動作しないことが分かり、アダプティブカードを使うしか方法がありませんでした
    > [Bot Framework SDK でメッセージに推奨されるアクションを追加する \- Bot Service \| Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/bot-service/rest-api/bot-framework-rest-connector-add-suggested-actions?view=azure-bot-service-4.0)