[機能]AIエージェントと連携

できること

・お客様からメッセージに対して、AIエージェントから自動応答する。
・お客様から質問に対して、AIエージェントから回答生成する。
・会話メッセージの要約およびTODOの作成する。
・定期的に会話メッセージを分析し、顧客満足度や顧客ランク評価など任意情報抽出する

設定方法

準備  必要なプロンプトを事前に登録し、以下各機能にて選択する。※利用箇所以下②以後にて参照する。

DX-LINE→ホーム→設定・登録→すべて表示→プロンプト
選択する。

準備 ② AIエージェントの接続情報をDX-LINEへ設定する

① DX-LINE設定にて、「生成AI利用」有効化し、「生成AI関連設定」タブへ遷移する

➁ 有効化後に、「認証情報」
Edit押下し、接続入力を入力する。

※ChatGPTキーの取得方法はこちらへ参照する

③要約生成機能有効化
有効することによって、チャット画面の要約生成ボタン利用可能になる。
登録済みのプロンプト選択して、TODO自動作成有効の場合、TODOのデフォルト件名、期日、紐付け先を指定する

④定期的にチャットメッセージから関心情報を分析
「定期処理有効」選択し、対象メッセージと友だち、実施頻度指定し、抽出した情報を友だち項目へ更新する。

活用① AIエージェントから自動応答する

以下の自動応答登録すれだけで、AIによる自動応答できる。

マッチタイプ:
  AIボット
プロンプト:
  作成済みプロンプト
・有効:選択

活用② AIエージェントから会話内容による回答を生成する

チャット画面のアインシュタインアインコをクリックし、AIアシスタント画面開く

ユーザの内容をここで編集しても利用可能です。

生成内容をダブルクリックしたらそのまま編集可能です。

活用③ AIエージェントでチャットの内容を要約生成し、TODOを自動作成も可能です。

AgentForce連携実装例:

/**
 * Salesforce Einstein Models API (aiplatform.ModelsAPI) 実装クラス
 *
 * - aiplatform.ModelsAPI.createGenerations を呼び出してLLM応答を取得
 * - HTTPコールアウトを使わない(リモートサイト/Named Credential 不要)
 * - Einstein Trust Layer (PIIマスキング/監査) 自動適用
 * - ステートレスAPIのため、会話履歴はプロンプトに連結して送信
 * - callAIApi() の戻り値はAI応答テキスト(エラー時は 'Error:...' 形式)
 *
 * 【カスタム設定に追加が必要なフィールド】
 *   DX_LINE_SETTING__c.AIPLATFORM_MODEL_NAME__c
 *     = 'sfdc_ai__DefaultGPT4Omni'  などのモデル名
 *
 * 【前提条件】
 *   - 組織で Einstein Generative AI / Models API が有効
 *   - 実行ユーザーに必要な権限セット(Prompt Template User 等)が付与
 */
global with sharing class FmlAIAgentforce extends bfml.FmlExternalIF {

    // カスタム設定から取得(デフォルトモデル)
    private static final String DEFAULT_MODEL = 'sfdc_ai__DefaultGPT4Omni';//DX_LINE_SETTING__c.getOrgDefaults().AIPLATFORM_MODEL_NAME__c;

    // =============================================
    // callAIApi 実装
    // =============================================

    /**
     * Models API を呼び出してAI応答を返す
     *
     * @param  bean リクエスト情報
     * @return      AI応答テキスト(エラー時は 'Error:...' 形式)
     */
    override global String callAIApi(bfml.FmlExternalIF.IntentBean bean) {

        if (bean == null) {
            return 'Error:IntentBean null ERROR';
        }
        String modelName = String.isBlank(bean.model) ? DEFAULT_MODEL : bean.model;
        if (String.isBlank(modelName)) {
            return 'Error:モデル名が未設定です';
        }

        // ── プロンプト構築(会話履歴を連結) ──────
        String prompt = buildPrompt(bean);
        if (String.isBlank(prompt)) {
            return 'Error:プロンプトが空です';
        }

        String requestJson = '';
        String responseJson = '';

        try {
            // ── リクエスト構築 ────────────────────────
            aiplatform.ModelsAPI.createGenerations_Request request =
                new aiplatform.ModelsAPI.createGenerations_Request();
            request.modelName = modelName;

            aiplatform.ModelsAPI_GenerationRequest body =
                new aiplatform.ModelsAPI_GenerationRequest();
            body.prompt = prompt;

            // 追加パラメータ(bean.parameters 経由で上書き可能)
            applyOptionalParameters(body, bean);

            request.body = body;

            // ログ用JSON(body + 元のbean設定を併記して可観測性を確保)
            requestJson = JSON.serialize(new Map<String, Object> {
                'modelName'  => modelName,
                'body'       => body,
                'beanParams' => new Map<String, Object> {
                    'temperature'       => bean.temperature,
                    'max_tokens'        => bean.max_tokens,
                    'top_p'             => bean.top_p,
                    'frequency_penalty' => bean.frequency_penalty,
                    'presence_penalty'  => bean.presence_penalty,
                    'stream'            => bean.stream
                }
            });

            // ── API呼び出し ───────────────────────────
            aiplatform.ModelsAPI api = new aiplatform.ModelsAPI();
            aiplatform.ModelsAPI.createGenerations_Response response =
                api.createGenerations(request);

            // ── レスポンス処理 ────────────────────────
            String outputText = extractOutputText(response);
            Decimal totalTokens = extractTokenCount(response);
            responseJson = JSON.serialize(response.Code200);

            // ── ログ記録 ──────────────────────────────
            saveLog(bean, requestJson, responseJson, modelName, totalTokens);

            if (String.isBlank(outputText)) {
                return 'Error:応答テキストが空です';
            }
            return outputText;

        } catch (Exception e) {
            // Models API 例外 / その他例外をまとめて捕捉
            String errMsg = e.getTypeName() + ': ' + e.getMessage();
            saveLog(bean, requestJson, errMsg, modelName, 0);
            return 'Error:' + errMsg;
        }
    }

    // =============================================
    // private: プロンプト構築
    // =============================================

    /**
     * IntentBean からプロンプト文字列を組み立て
     *
     *  優先順位:
     *   1) bean.messages (List<MessageBean>) が存在 → role別に整形連結
     *   2) bean.systemtext / usertext / assistanttext から組み立て
     */
    private String buildPrompt(bfml.FmlExternalIF.IntentBean bean) {

        List<String> lines = new List<String>();

        // ── (1) messages 優先 ────────────────────────
        if (bean.messages != null && !bean.messages.isEmpty()) {
            for (bfml.FmlExternalIF.MessageBean msg : bean.messages) {
                if (msg == null) continue;
                String text = contentToString(msg.content);
                if (String.isBlank(text)) continue;
                lines.add(formatRoleLine(msg.role, text));
            }
        } else {
            // ── (2) systemtext / usertext / assistanttext フォールバック ──
            if (String.isNotBlank(bean.systemtext)) {
                lines.add(formatRoleLine('system', bean.systemtext));
            }
            if (String.isNotBlank(bean.assistanttext)) {
                lines.add(formatRoleLine('assistant', bean.assistanttext));
            }
            if (String.isNotBlank(bean.usertext)) {
                lines.add(formatRoleLine('user', bean.usertext));
            }
        }

        if (lines.isEmpty()) return null;

        // 最後にAssistantの応答を促す
        lines.add('Assistant:');
        return String.join(lines, '\n');
    }

    /**
     * MessageBean.content (Object型) を安全に文字列化
     *  - String     → そのまま
     *  - List/Map   → JSON文字列
     *  - その他      → String.valueOf
     */
    private String contentToString(Object content) {
        if (content == null) return null;
        if (content instanceof String) return (String) content;
        try {
            return JSON.serialize(content);
        } catch (Exception e) {
            return String.valueOf(content);
        }
    }

    /**
     * role に応じてプロンプト1行を整形
     */
    private String formatRoleLine(String role, String text) {
        if ('system'.equalsIgnoreCase(role)) {
            return '[Instruction]\n' + text;
        } else if ('assistant'.equalsIgnoreCase(role)) {
            return 'Assistant: ' + text;
        }
        return 'User: ' + text;
    }

    /**
     * bean.parameters (JSON文字列) から任意パラメータをbodyに反映
     *  例: {"localization": {...}, "tags": {...}}
     *  ※ ModelsAPI_GenerationRequest が公開しているのは prompt / localization / tags のみ
     */
    private void applyOptionalParameters(aiplatform.ModelsAPI_GenerationRequest body,
                                         bfml.FmlExternalIF.IntentBean bean) {
        if (String.isBlank(bean.parameters)) return;
        try {
            // ModelsAPI_Localization / ModelsAPI_Tags の構造はSDKに依存するため、
            // JSON経由で安全にデシリアライズして反映
            Map<String, Object> p = (Map<String, Object>) JSON.deserializeUntyped(bean.parameters);

            if (p.containsKey('localization')) {
                body.localization = (aiplatform.ModelsAPI_Localization)
                    JSON.deserialize(JSON.serialize(p.get('localization')),
                                     aiplatform.ModelsAPI_Localization.class);
            }
            if (p.containsKey('tags')) {
                body.tags = (aiplatform.ModelsAPI_Tags)
                    JSON.deserialize(JSON.serialize(p.get('tags')),
                                     aiplatform.ModelsAPI_Tags.class);
            }
        } catch (Exception e) {
            System.debug('applyOptionalParameters error: ' + e.getMessage());
        }
    }

    // =============================================
    // private: レスポンス処理
    // =============================================

    /**
     * 生成結果テキストを取り出し
     *  response.Code200.generation.generatedText
     */
    private String extractOutputText(aiplatform.ModelsAPI.createGenerations_Response response) {
        if (response == null || response.Code200 == null) return null;

        aiplatform.ModelsAPI_GenerationResponse res = response.Code200;
        if (res.generation == null) return null;

        return res.generation.generatedText;
    }

    /**
     * トークン使用数を取得
     * ※ ModelsAPI_GenerationResponse には標準でusage情報がない場合があるため、
     *   JSON経由で動的に取得を試みる
     */
    private Decimal extractTokenCount(aiplatform.ModelsAPI.createGenerations_Response response) {
        try {
            if (response == null || response.Code200 == null) return 0;
            String resJson = JSON.serialize(response.Code200);
            Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(resJson);

            Object gen = m.get('generation');
            if (gen instanceof Map<String, Object>) {
                Object usage = ((Map<String, Object>) gen).get('parameters');
                if (usage instanceof Map<String, Object>) {
                    Object usageMap = ((Map<String, Object>) usage).get('usage');
                    if (usageMap instanceof Map<String, Object>) {
                        Map<String, Object> u = (Map<String, Object>) usageMap;
                        Object inp = u.get('prompt_tokens');
                        Object out = u.get('completion_tokens');
                        return (inp != null ? (Integer) inp : 0)
                             + (out != null ? (Integer) out : 0);
                    }
                }
            }
        } catch (Exception e) {
            System.debug('extractTokenCount error: ' + e.getMessage());
        }
        return 0;
    }

    // =============================================
    // private: ログ保存
    // =============================================
    private bfml__FmlChatGPTLog__c saveLog(bfml.FmlExternalIF.IntentBean bean, String requestJson,
                                           String responseJson, String modelName, Decimal totalTokens) {
        try {
            // userkeyが友だち以外のレコードIDなら除去
            try {
                String objName = Id.valueOf(bean.userkey).getsobjecttype().getDescribe().getName();
                if (!bfml__FmlLineMember__c.getSObjectType().getDescribe().getName().equals(objName)) {
                    bean.userkey = null;
                }
            } catch (Exception e) {
                bean.userkey = null;
                System.debug(e.getMessage());
            }

            bfml__FmlChatGPTLog__c log = new bfml__FmlChatGPTLog__c(
                bfml__Model__c        = 'ModelsAPI:' + modelName,
                bfml__Request__c      = requestJson,
                bfml__Response__c     = responseJson,
                bfml__Total_tokens__c = totalTokens,
                bfml__LineMemberID__c = bean.userkey
            );

            if (Schema.sObjectType.bfml__FmlChatGPTLog__c.isCreateable()) {
                insert log;
                return log;
            }
        } catch (Exception e) {
            System.debug(e.getMessage());
            return null;
        }
        return null;
    }
}
タイトルとURLをコピーしました