[活用]DX-LINE×Agentforce 連携

ChatGPT連携

できること

Agentforceを呼び出して、自動応答、回答生成、チャット内容の要約など生成する。

実現方法

方式1 DX-LINEにて定義済みの「プロンプト」 + Salesforce Einstein Models API 呼び出す
方式2 Agentforceテンプレート呼び出す(EinsteinLLM.generateMessagesForPromptTemplate 呼び出す)

方法① DX-LINEのプロンプト + AgentForce モデル

/**
 * Salesforce Einstein Models API (aiplatform.ModelsAPI) 実装クラス
 *
 * - aiplatform.ModelsAPI.createGenerations を呼び出してLLM応答を取得
 * - HTTPコールアウトを使わない(リモートサイト/Named Credential 不要)
 * - Einstein Trust Layer (PIIマスキング/監査) 自動適用
 * - ステートレスAPIのため、会話履歴はプロンプトに連結して送信
 * - callAIApi() の戻り値はAI応答テキスト(エラー時は 'Error:...' 形式)
 *
 * 【前提条件】
 *   - 組織で 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;
    }
}

方法② Agentforce テンプレート利用

DX-LINEのプロンプトレコードの「モデル名」項目にて、AgentforceテンプレートのAPI名を指定する。

テンプレート例:

テンプレート例2 (Salesforceのナレッジから回答作成する)

テンプレート利用のApexコードサンプル:

/**
 *
 * - ConnectApi.EinsteinLLM.generateMessagesForPromptTemplate を呼び出してLLM応答を取得
 * - callAIApi() の戻り値はAI応答テキスト(エラー時は 'Error:...' 形式)
 *
 * 【前提条件】
 *   - 実行ユーザーに必要な権限セット(Prompt Template User 等)が付与
 */
global with sharing class FmlAIAgentforceTpl extends bfml.FmlExternalIF {

    /**
     * Execute an Agentforce / Prompt Builder prompt template.
     *
     * The input format follows bfml.FmlExternalIF.IntentBean.
     * - bean.id is the bfml__FmlIntent__c record Id.
     * - bean.model is the Prompt Template API Name.
     * - template inputParams contains only Input:conversationMsgs.
     *
     * Template input example:
     * {
     *   "Input:conversationMsgs": [
     *       {"role": "user", "content": "I need to return an item..."}
     *   ]
     * }
     *
     * @param bean
     *        Request information in bfml.FmlExternalIF.IntentBean format.
     *
     * @return
     *        Generated response text, or Error:... when execution fails.
     */
    override global String callAIApi(bfml.FmlExternalIF.IntentBean bean) {

        if (bean == null) {
            return 'Error:IntentBean null ERROR';
        }

        if (String.isBlank(bean.model)) {
            return 'Error:Prompt template API name is required in bean.model';
        }

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

        try {
            promptTemplateApiName = bean.model;

            if (String.isBlank(promptTemplateApiName)) {
                return 'Error:Prompt template API name is required in bean.model';
            }

            Map<String, Object> rawInputParams = buildRawInputParams(bean);

            if (rawInputParams == null || rawInputParams.isEmpty()) {
                return 'Error:Prompt Template input is empty';
            }

            Map<String, ConnectApi.WrappedValue> inputParams =
                new Map<String, ConnectApi.WrappedValue>();

            for (String inputName : rawInputParams.keySet()) {
                ConnectApi.WrappedValue wrappedValue =
                    new ConnectApi.WrappedValue();

                wrappedValue.value = rawInputParams.get(inputName);
                inputParams.put(inputName, wrappedValue);
            }

            ConnectApi.EinsteinPromptTemplateGenerationsInput templateInput =
                new ConnectApi.EinsteinPromptTemplateGenerationsInput();

            templateInput.inputParams = inputParams;
            templateInput.isPreview = false;
            templateInput.additionalConfig =
                new ConnectApi.EinsteinLlmAdditionalConfigInput();

            requestJson = JSON.serialize(rawInputParams);

            ConnectApi.EinsteinPromptTemplateGenerationsRepresentation result =
                ConnectApi.EinsteinLLM.generateMessagesForPromptTemplate(
                    promptTemplateApiName,
                    templateInput
                );

            if (
                result == null ||
                result.generations == null ||
                result.generations.isEmpty()
            ) {
                responseJson = JSON.serialize(new Map<String, Object> {
                    'status' => 'No content generated'
                });
                saveLog(bean, requestJson, responseJson, promptTemplateApiName, 0);
                return 'Error:No content generated';
            }

            ConnectApi.EinsteinLLMGenerationItemOutput generation =
                result.generations[0];

            if (generation == null || generation.text == null) {
                responseJson = JSON.serialize(new Map<String, Object> {
                    'status' => 'No content generated'
                });
                saveLog(bean, requestJson, responseJson, promptTemplateApiName, 0);
                return 'Error:No content generated';
            }

            responseJson = JSON.serialize(new Map<String, Object> {
                'text' => generation.text
            });

            saveLog(bean, requestJson, responseJson, promptTemplateApiName, 0);
            return generation.text;

        } catch (Exception e) {
            String errMsg = e.getTypeName() + ': ' + e.getMessage();
            saveLog(bean, requestJson, errMsg, promptTemplateApiName, 0);
            return 'Error:' + errMsg;
        }
    }

    // =============================================
    // private: Prompt Builder input construction
    // =============================================

    /**
     * Build Prompt Builder inputParams from an IntentBean.
     */
    private Map<String, Object> buildRawInputParams(bfml.FmlExternalIF.IntentBean bean) {

        Map<String, Object> inputParams = new Map<String, Object>();
        List<Object> conversationMsgs = buildConversationMsgs(bean);

        System.debug(LoggingLevel.INFO, 'bean.messages raw: ' + String.valueOf(bean.messages));
        System.debug(LoggingLevel.INFO, 'Input:conversationMsgs JSON-safe: ' + JSON.serialize(conversationMsgs));

        inputParams.put('Input:conversationMsgs', conversationMsgs);

        return inputParams;
    }

    /**
     * Convert bean.messages into JSON-safe prompt template input data.
     */
    private List<Object> buildConversationMsgs(bfml.FmlExternalIF.IntentBean bean) {
        List<Object> conversationMsgs = new List<Object>();

        if (bean.messages == null || bean.messages.isEmpty()) {
            System.debug(LoggingLevel.INFO, 'bean.messages is null or empty.');
            return conversationMsgs;
        }

        System.debug(LoggingLevel.INFO, 'bean.messages size: ' + bean.messages.size());

        Integer messageIndex = 0;
        for (bfml.FmlExternalIF.MessageBean msg : bean.messages) {
            if (msg == null) {
                System.debug(LoggingLevel.INFO, 'bean.messages[' + messageIndex + '] is null.');
                messageIndex++;
                continue;
            }

            String content = contentToString(msg.content);

            System.debug(
                LoggingLevel.INFO,
                'bean.messages[' + messageIndex + '] role=' + msg.role + ', content=' + content
            );

            conversationMsgs.add(new Map<String, Object> {
                'role'    => msg.role,
                'content' => content
            });

            messageIndex++;
        }

        return conversationMsgs;
    }

    /**
     * MessageBean.content (Object type) to String.
     */
    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);
        }
    }

    // =============================================
    // private: Log save
    // =============================================
    private bfml__FmlChatGPTLog__c saveLog(bfml.FmlExternalIF.IntentBean bean, String requestJson,
                                           String responseJson, String promptTemplateApiName,
                                           Decimal totalTokens) {
        try {
            if (bean != null) {
                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        = 'PromptTemplate:' + promptTemplateApiName,
                bfml__Request__c      = requestJson,
                bfml__Response__c     = responseJson,
                bfml__Total_tokens__c = totalTokens,
                bfml__LineMemberID__c = bean == null ? null : 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をコピーしました