見出し画像

【第435回】 Agentforce : アダプティブ応答形式(Apex 実装版)

前回および前々回の記事では、「カスタム Lightning タイプ」を使った Agentforce チャットの UI カスタマイズについて紹介しました。

カスタム Lightning タイプは LWC を用いたコードベースの実装となるため、非常に柔軟で表現力が高い一方、Apex に加えて JavaScript・HTML・CSS といったフロントエンド開発の知識が求められます。

そのハードルを補う形で登場したのが 「アダプティブ応答形式」 です。
この機能を利用すると、エージェントは返却されるデータ構造に応じて、会話内で最適なレスポンス表示(テキスト/リンク/画像など)を自動的に選択してくれます。

本記事を書いている時点では、サービスエージェント で利用可能です。 


アダプティブ応答形式の有効/無効について

「アダプティブ応答形式」は、新規エージェント作成時に デフォルトで有効化 されています。無効化したい場合は、エージェントビルダーの「接続」メニューから設定を行います。

なお、私の上記画面では新しい「接続」画面を有効化していますが、
アダプティブ応答形式は「拡張チャット v2」では動作せず、「メッセージング」チャネルでのみ利用可能です。


常に有効でよいのか?

表示がリッチになる機能なので、「常に有効で良いのでは?」と思われるかもしれません。
しかし、回答構成がシンプルで、テキストベースのやり取りのみを求める場合は、無効化をおすすめします。

例えば、

  • 画像 URL が存在しない

  • リンク URL のみが含まれる

といったケースでは、中途半端にリッチな表示となり、見た目があまり美しくならないことがあります。


提供されている応答形式(2 種類)

現在、以下の 2 種類が提供されています。

  • リッチ選択肢応答(Rich Choice Response)
    ボタン、カルーセル、リストによる質問表示

  • リッチリンク応答(Rich Link Response)
    文字タイトル+画像による Web ページリンク表示


今回の検証内容

今回は Salesforce 公式のサンプルデータを使って実装を試します。
データは ハードコード されているため、新たにオブジェクトやレコードを用意する必要はありません。

このアクションでは、「メニューには何がありますか?」という質問に
「ユーザーに料理の選択肢と、その詳細情報を提供する」
というシナリオを実装することが可能です。

それでは、設定手順を見ていきましょう。


設定手順

1. Apex クラスの作成

設定画面から Apex クラスを検索し、以下のコードを 新しい Apex クラスとして保存します。

public class GetFoodDetailsInvocable {

    @InvocableMethod(
        label='Get Food Details'
        description='Returns linkURL, linkTitle, image linkURL, image MIME Type, and description text for a given food name'
    )
    public static List<FoodDetailResponse> getFoodDetails(List<FoodDetailRequest> requests) {
        List<FoodDetailResponse> responses = new List<FoodDetailResponse>();

        for (FoodDetailRequest request : requests) {
            FoodDetailResponse response = new FoodDetailResponse();
            FoodDetail foodDetail = new FoodDetail();

            String foodName = (request.foodName == null) ? '' : request.foodName.toLowerCase();

            // Populate food details dynamically from selections
            if (foodName.contains('pizza')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/pizza';
                foodDetail.linkTitle = 'Delicious Pizza';
                foodDetail.linkImageURL = 'https://www.publicdomainpictures.net/pictures/240000/velka/pizza-1508086895mrm.jpg';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'Our pizza is made with the most flavorful tomato sauce and fresh cheese so that you can indulge all of your senses.';
            } else if (foodName.contains('pasta')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/pasta';
                foodDetail.linkTitle = 'Tasty Pasta';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1606761568499-5ddd45e43b32';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'Our pasta is topped with the most flavorful tomato sauce and fresh cheese so that you can indulge all of your senses.';
            } else if (foodName.contains('tiramisu')) {
                foodDetail.linkURL = 'https://tastesbetterfromscratch.com/easy-tiramisu/';
                foodDetail.linkTitle = 'Classic Tiramisu';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1571877227200-a0d98ea607e9';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'Our tiramisu is made with the freshest ingredients so that you can indulge all of your senses.';
            } else if (foodName.contains('tacos')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/tacos';
                foodDetail.linkTitle = 'Authentic Mexican Tacos';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1551504734-5ee1c4a127da';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'Our tacos are made with the freshest ingredients so that you can indulge all of your senses.';
            } else if (foodName.contains('quesadilla')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/quesadilla';
                foodDetail.linkTitle = 'Cheesy Quesadilla';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1613952273499-5de42c1789c7';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('nachos')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/nachos';
                foodDetail.linkTitle = 'Loaded Nachos';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1606755962773-b35f5d518c46';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('croissant')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/croissant';
                foodDetail.linkTitle = 'Fresh Croissant';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1587049352907-23d8ff0ceac3';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('ratatouille')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/ratatouille';
                foodDetail.linkTitle = 'Traditional Ratatouille';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1633527076476-f847fc6d16a2';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('creme brulee')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/creme-brulee';
                foodDetail.linkTitle = 'French Crème Brûlée';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1609767400465-45e06f28a680';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('sushi')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/sushi';
                foodDetail.linkTitle = 'Japanese Sushi';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1589308078050-cf38fe054d63';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('ramen')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/ramen';
                foodDetail.linkTitle = 'Delicious Ramen';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1602334872613-3ab02f3c95a7';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('tempura')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/tempura';
                foodDetail.linkTitle = 'Crispy Tempura';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1571771439241-89b31f5092c7';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('dumplings')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/dumplings';
                foodDetail.linkTitle = 'Chinese Dumplings';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1617196036851-20ce58a15b1b';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('peking duck')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/peking-duck';
                foodDetail.linkTitle = 'Peking Duck';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1643989946129-73e3a872ad80';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('spring rolls')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/spring-rolls';
                foodDetail.linkTitle = 'Fresh Spring Rolls';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1604328698692-80d9b81baf4d';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('burger')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/burger';
                foodDetail.linkTitle = 'Classic Burger';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1551782450-a2132b4ba21d';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('hot dog')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/hot-dog';
                foodDetail.linkTitle = 'Grilled Hot Dog';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1621248313959-d6c0e480c54e';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else if (foodName.contains('apple pie')) {
                foodDetail.linkURL = 'https://www.foodwebsite.com/apple-pie';
                foodDetail.linkTitle = 'Homemade Apple Pie';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1606760225157-145fe46c0b7f';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            } else {
                foodDetail.linkURL = 'https://www.foodwebsite.com/food';
                foodDetail.linkTitle = 'Unknown Food';
                foodDetail.linkImageURL = 'https://images.unsplash.com/photo-1606851093480-b24197b5fd6a';
                foodDetail.linkImageMimeType = 'image/jpeg';
                foodDetail.linkDescriptionText = 'All of our food is delicious.';
            }

            response.foodDetails.add(foodDetail);
            responses.add(response);
        }

        return responses;
    }

    // Request Wrapper Class
    public class FoodDetailRequest {
        @InvocableVariable(label='Food Name' description='The name of the food item' required=true)
        public String foodName;
    }

    // Food detail structure
    public class FoodDetail { 
        public String linkURL;
        public String linkTitle;
        public String linkImageURL;
        public String linkImageMimeType;
        public String linkDescriptionText;
    }

    // Response Wrapper Class
    public class FoodDetailResponse {
        @InvocableVariable(label='Food Details' description='Shows details, image, and link for the selected food' required=true)
        public List<FoodDetail> foodDetails;

        public FoodDetailResponse() {
            this.foodDetails = new List<FoodDetail>();
        }
    }
}

ここで重要なのは、以下のようなデータの構造 です。
名前は以下で設定する必要はありませんが、エージェントが理解しやすいように、分かりやすい名前で設定しておくことが望ましいです。

  • name(テキスト型)
    製品などのラベル名

  • imageUrl(テキスト型)
    製品などの画像 URL

  • linkUrl(テキスト型)
    製品などのリンク URL

  • description(テキスト型)(※ 任意)
    製品などの説明テキスト

  • mimeType(テキスト型)(※ 任意)
    画像の MIME タイプを指定する文字列

MIME タイプは、必須ではありませんが、クライアント側での描画や処理を効率化するため、パフォーマンスの観点から指定することが推奨されます。

<MIME タイプ>

  • image/apng:Animated Portable Network Graphics (APNG)

  • image/avif:AV1 Image File Format (AVIF)

  • image/gif:Graphics Interchange Format (GIF)

  • image/jpeg:Joint Photographic Expert Group image (JPEG)

  • image/png:Portable Network Graphics (PNG)

  • image/svg+xml:Scalable Vector Graphics (SVG)

  • image/webp:Web Picture format (WEBP)


2. Apex クラスへのアクセス権付与

続いて、この Apex クラスへのアクセス権を Service Agent ユーザー に付与します。

重要:このアクセス権がないと、エージェントは回答できません

  1. 新規権限セットを作成します。

    • 権限セット名:Agent Apex Actions (Custom)

  2. Apex Class Access を選択します。

3. 今回作成した GetFoodDetailsInvocable を選択して保存します。

4. 作成後、必ず「サービスエージェントユーザー」への 割り当て を行ってください。


3. 信頼済み URL の設定(Winter ’26 以降の注意点)

Winter ’26 の新機能 により、
信頼済み URL リストに登録されていないリンクは、エージェント会話内でブロックされます。

注意:未承認の URL は URL_Redacted に置き換えられます。

1. 設定から 「信頼済み URL」 を開いてください。

2. 今回は、選択肢の選択で「Classic Tiramisu」を使用するため、以下 2 件のみ登録します。必要に応じて他の URL を追加しても問題ありません。

リンク URL 用

  • API 名:tastesbetterfromscratch

  • URL:https://tastesbetterfromscratch.com

画像 URL 用

  • API 名:unsplash

  • URL:https://images.unsplash.com


サービスエージェントの設定

1. トピックの作成

1. 新しいトピックを作成します。

2. 以下を入力して開始します。

This topic provides the user a list of food options and details.
(このトピックでは、ユーザーに食品のオプションと詳細のリストを提供します)

3. トピックの詳細を以下で編集してください。

Classification Description

  • This topic provides the user a list of food options and details. For example, use this topic when the user says: "What's on the menu?"
    (このトピックは、ユーザーに料理の選択肢とその詳細を提供します。
    たとえば、ユーザーが「メニューには何がありますか?」と発言した場合に、このトピックを使用します。)

Scope

  • Your job is only to provide food options and details.
    (あなたの役割は、料理の選択肢とその詳細を提供することのみです。)

Instructions(3 つ)

  • Use Get_Food_Details action to get food options and details.
    (料理の選択肢と詳細を取得するために、Get_Food_Details アクションを使用します。)

  • When the user says: "What's on the menu?," always give a list of food options. For example, Classic Tiramisu, Homemade Apple Pie, and Delicious Ramen. Then the user is expected to select a food option. Using the food option name selected by user, pass the name to the Get_Food_Details action to get details about the item.
    (ユーザーが「メニューは何がありますか?」と言った場合は、必ず料理の一覧を提示します。
    例:Classic Tiramisu、Homemade Apple Pie、Delicious Ramen。
    その後、ユーザーが料理を 1 つ選択することを想定します。
    ユーザーが選択した料理名を使って、Get_Food_Details アクションに料理名を渡し、該当アイテムの詳細を取得します。)

  • When user selects a food option, use the Get_Food_Details action to give the user the link and image for the food option. For example, if user selects Delicious Ramen, then send Delicious Ramen as input to the Get_Food_Details action.
    (ユーザーが料理を選択したら、Get_Food_Details アクションを使用して、その料理のリンクと画像をユーザーに提供します。
    例:ユーザーが Delicious Ramen を選択した場合、Get_Food_Details アクションの入力として「Delicious Ramen」を渡します。)

4. アクションは追加せず、「完了」をクリックします。


2. アクションの作成

1. 作成したトピックを選択します。

2. 「このトピックのアクション」を開きます。

3.「新規アクションの作成」をクリックします。

4. 以下を選択します。

  • 参照アクション種別:Apex

  • 参照アクションカテゴリ:Invocable Method

5. 参照アクションで Get Food Details を選択します。

6. Agent Action Configuration では、以下を入力して保存します。

Agent Action Instructions

  • Returns linkURL, linkTitle, image linkURL, image MIME Type, and description text for a given food name
    (指定された料理名に対して、linkURL、linkTitle、画像の linkURL、画像の MIME Type、説明文を返します。)

Loading Text

  • Getting food details…
    (料理の詳細を取得しています…)

Food Name Instructions

  • The name of the food item
    (料理名を入力してください)
    ※ Require input のみチェック

Food Details Instructions

  • Shows details, image, and link for the selected food
    (選択された料理の詳細、画像、リンクを表示します)
    ※ Show in conversation のみチェック


動作確認

それでは、実際の動作を確認してみましょう。

① アダプティブ応答形式:無効の場合

1. まず、メニューから 「接続」 を選択し、「メッセージング」 を開きます。

2. ここで 「アダプティブ応答形式」無効 に設定します。

3. 続いて、エージェントを有効化します。

4. サービスエージェントを起動し、「What’s on the menu?」(メニューには何がありますか?) と尋ねてみます。

5. アダプティブ応答形式が無効なため、料理の選択肢はテキスト形式で表示され、ユーザーもテキスト入力で回答する必要があります。

ここでは Classic Tiramisu と入力して送信します。

6. すると、以下のように表示されます。

この場合でも画像は表示されるため、決して悪い見た目ではありませんが、
やり取りはあくまで 従来のテキストベースの体験 となっています。


② アダプティブ応答形式:有効の場合

1. 次に、再びエージェントビルダーの 「接続」 に戻り、「アダプティブ応答形式」有効 にして確認します。

2. 先ほどと同じように、「What’s on the menu?」 (メニューには何がありますか?) と質問します。

3. すると今度は、メニューがボタン形式で表示されました。

この表示形式の大きなメリットは、ユーザーがテキストを入力する必要がない点です。ボタンをクリックするだけで、選択内容が自動的に入力・送信されます。

4. 続いて表示されるのが、以下の画面です。

これが リッチリンク応答(リッチリンクレスポンス) で、
文字タイトルと画像を組み合わせた Web ページリンク表示 となります。

先ほどと比べると、無駄に画像も大きすぎず、URL リンクとしてクリックしやすい形になったかと思います。成功です。


補足

なお、このアダプティブ応答形式は、
LINE などの外部メッセージングチャネルでも利用可能です。


いかがでしたでしょうか。

アダプティブ応答形式は、LWC を用いたカスタム Lightning タイプほどの自由度はないものの、フロントエンド開発スキルを必要とせずに、会話体験をリッチにできる 点が大きな魅力です。

一方で、常に有効にしておくべき機能というわけではありません。
例えば、

  • テキスト中心のシンプルな回答を重視したい場合

  • 画像やリンクが十分に揃っていない場合

には、あえて無効化した方が、見た目やユーザー体験が自然になる ケースもあります。

今回のように、

  • ユーザーに選択肢を提示したい

  • 画像やリンク付きで、直感的に案内したい

といったシナリオでは、アダプティブ応答形式は非常に相性が良く、設定の切り替えだけで UX を大きく向上させられる ことが確認できました。

まずはシンプルな Apex 実装から試し、
必要に応じて アダプティブ応答形式 → カスタム Lightning タイプ へと段階的に使い分けていくのがおすすめです。
ぜひ、実際のユースケースで活用してみてください。

今回は以上です。


次の記事はこちら

前回の記事はこちら

私の note のトップページはこちら