SalesforceからChatGPTを使ってみよう

2023.03.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

Salesforce x ChatGPT に関しては、

Salesforce、世界初のCRM向け生成AI 「Einstein GPT」を発表 https://www.salesforce.com/jp/company/news-press/press-releases/2023/03/230309/

という発表もあるのですが、コーディングを辞さない覚悟でSalesforceからChatGPTを試してみたかったので、やってみました。

結構、サクッとできちゃいましたので、ご参考になれば幸いです。

この記事でわかること

  • SalesforceからChatGPT APIをコールする方法
  • AWS CDK を使って AWS Lambda から ChatGPT APIをコールする API Gateway を作る方法
  • コールした結果をSalesforceのChatterに投稿する方法

SalesforceからChatGPTを使ってみよう

OpenAIでは現在(2023/03/17現在)、 Python と Node.js のライブラリが公式にサポートされているようです。

https://platform.openai.com/docs/libraries

SalesforceからChatGPT APIをコールする方法として、大別して二つあると考えています。

  1. LWC(Lightning Web Component) から Node.js ライブラリを使ってコールする
  2. ChatGPT APIをコールするAPIを実装して、それをSalesforceのコールアウトを使って呼び出す

本エントリーでは後者のコールアウトを使う方法を使ってみたいと思います。

段取りは次の通りです。

  1. ChatGPT API をコールする AWS Lambda を実装する
  2. 1をコールアウトで呼び出すApexコードを記述する
  3. (おまけ)コールアウトの結果をChatterに投稿してみる

では、いってみましょう!

ChatGPT API をコールする AWS Lambda を実装する

CDKも利用しつつ、サクッと作りたいと思います。

まず、CDKプロジェクトを新規に作ります。ここではプロジェクト名をchatgpt-mediatorとしました。言語にはTypeScriptを選びました。

$ mkdir chatgpt-mediator
$ cd chatgpt-mediator
$ cdk init app --language=typescript

次に、リソースを定義します。lib/chatgpt-mediator-stack.tsを次のように書きました。

lib/chatgpt-mediator-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import { ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import * as dotenv from 'dotenv';

dotenv.config();

export class ChatgptMediatorStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const chatgptMediator = new lambda.Function(this, 'ChatgptMediatorHandler', {
      runtime: lambda.Runtime.NODEJS_16_X,
      code: new lambda.AssetCode('lambda'),
      handler: 'chatgptMediator.handler',
      timeout: cdk.Duration.seconds(120),
      environment: {
        OPENAI_ORG_ID: process.env.OPENAI_ORG_ID ?? '',
        OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? ''
      }
    });
    chatgptMediator.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com'));

    const api = new apigw.LambdaRestApi(this, 'chatgptMediator', {
      handler: chatgptMediator,
      proxy: false,
      deployOptions: {
        loggingLevel: apigw.MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        metricsEnabled: true,
      }
    });
    const mediator = api.root.addResource('mediator');
    mediator.addMethod('POST');
  }
}

Lambdaのタイムアウト設定を120秒に設定しました。デフォルトの3秒では応答が返ってこないことが多いので。

また、OpenAIの組織IDとAPIキーはdotenvモジュールを使って、.envファイルから読み取るようにしています。

$ npm i -D dotenv
$ cat > .env
OPENAI_ORG_ID=<OpenAIの組織IDを記述する>
OPENAI_API_KEY=<OpenAIのAPIキーを記述する>

.gitignore.envを追加しておきます。

+ .env

次に、Lambda本体を定義します。ES6モジュールを使いたいので、package.json{"type":"module"}を書き込んでいます。

$ mkdir lambda
$ cd lambda
$ cat > package.json
{"type":"module"}
$ npm install openai

最終的には、Salesforceから取得した取引先名を渡して、その会社ってどんな会社ですか?とChatGPTに質問するようにしたいので、次のように定義しました。

lambda/chatgptMediator.js

import { Configuration, OpenAIApi } from "openai";

export const handler = async (event) => {
  const body = JSON.parse(event.body);

  const configuration = new Configuration({
    organization: process.env.OPENAI_ORG_ID,
    apiKey: process.env.OPENAI_API_KEY
  });
  const openai = new OpenAIApi(configuration);

  const response = await openai.createChatCompletion({
    model: "gpt-3.5-turbo",
    messages: [{ role: "user", content: body.name + "はどんな会社ですか?"}]
  })

  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(response.data.choices[0].message, null, 2)
  };
};

デプロイして、実行してみます。

$ cdk deploy --profile=<デプロイ先のプロファイル>
:
Outputs:
ChatgptMediatorStack.chatgptMediatorEndpointXXXXXX = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
$ curl -X POST -d '{"name": "クラスメソッド"}' https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/mediator
{
  "role": "assistant",
  "content": "\n\nクラスメソッドは、クラウドコンピューティングやオープンソースソフトウェアの導入・活用を手掛けるテクノロジー企業です。主な業務として、AWSを中心としたクラウドサービスの導入・運用支援、システム開発・保守、セキュリティソリューションの提供などがあります。また、新しい技術や考え方の研究開発にも力を入れており、内外のイベントで登壇することもあります。2006年の創業以来、多くの大手企業やスタートアップ企業との取り組みによって、優れた実績を築いています。"
}

期待する結果が返ってきていますね。

作成したAPIをコールアウトで呼び出すApexコードを記述する

手前味噌ですが、私が過去に書いた次のエントリーを参照して作成したAPIをコールします。

Amazon API Gateway を Salesforce からコールアウトする

Salesforceでの指定ログイン情報の作成は、参照先記事の方法で実施済みとします(名前をAWS_APIGWとして定義したものとします)。

取引先を作成するタイミングで実行したいので、取引先(Account)に対するApex Triggerを作成します。
triggers/AccountTrigger.triggerを次のように実装してみました。

triggers/AccountTrigger.trigger

trigger AccountTrigger on Account (after insert) {
    new AccountTriggerHandler().execute();
}

処理の実体はAccountTriggerHandlerというApexクラスの方で行います。

classes/AccountTriggerHandler.cls

public with sharing class AccountTriggerHandler extends TriggerHandler {
    /**
     * 作成後処理
     * @param newMap 作成した取引先
     */
    public override void afterInsert(Map<Id, Sobject> newMap) {
        // 取引先名を取り出してChatGPT APIをコールする
        for (Id id : newMap.keySet()) {
            Account acc = (Account)newMap.get(id);
            ChatGPTMediatorCalloutAsync.execute('{"name": "' + acc.Name+ '"}');
        }
    }
}

TriggerHandlerはTrigger作成用の汎用抽象クラスです。作っておくと便利ですよ。

classes/TriggerHandler.cls

public abstract class TriggerHandler {
    public void execute() {
        switch on Trigger.operationType {
            when BEFORE_INSERT  { this.beforeInsert(Trigger.new); }
            when BEFORE_UPDATE  { this.beforeUpdate(Trigger.oldMap, Trigger.newMap); }
            when BEFORE_DELETE  { this.beforeDelete(Trigger.oldMap); }
            when AFTER_INSERT   { this.afterInsert(Trigger.newMap); }
            when AFTER_UPDATE   { this.afterUpdate(Trigger.oldMap, Trigger.newMap); }
            when AFTER_DELETE   { this.afterDelete(Trigger.oldMap); }
            when AFTER_UNDELETE { this.afterUndelete(Trigger.newMap); }
        }
    }

    protected virtual void beforeInsert(List<Sobject> newList) {}
    protected virtual void beforeUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {}
    protected virtual void beforeDelete(Map<Id, Sobject> oldMap) {}
    protected virtual void afterInsert(Map<Id, Sobject> newMap) {}
    protected virtual void afterUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {}
    protected virtual void afterDelete(Map<Id, Sobject> oldMap) {}
    protected virtual void afterUndelete(Map<Id, Sobject> newMap) {}
}

AccountTriggerHandlerの中から、実際のAPIのコールアウト処理はさらにChatGPTMediatorCalloutAsyncクラスで行っています。

classes/ChatGPTMediatorCalloutAsync.cls

global with sharing class ChatGPTMediatorCalloutAsync implements Database.AllowsCallouts {
    @future(callout=true)
    public static void execute(String body) {
        Http http = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:AWS_APIGW/prod/mediator');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Accept', 'application/json');
        req.setBody(body);
        req.setTimeout(120000); // こちらも120秒のタイムアウト時間を設定
        HttpResponse res = http.send(req);
        CalloutResponse cres = (CalloutResponse)JSON.deserializeStrict(res.getBody(), CalloutResponse.class);
        System.debug(cres.content);
    }

    class CalloutResponse {
        String role;
        String content;
    }
}

Salesforceで取引先を新規作成してみて、開発者コンソールのログにAPIの実行結果が記載されていることを確認します。ここでは取引先名クラスメソッドを指定してみました。

Salesforceで取引先「クラスメソッド」を作成

開発者コンソールのログを見てみます。

開発者コンソールのログに「クラスメソッド」について問い合わせたChatGPTの返答が記録されている

期待する結果が取れていますね。

(おまけ)コールアウトの結果をChatterに投稿してみる

取得した結果をChatterに投稿してみましょう。AccountTriggerHandlerChatGPTMediatorCalloutAsyncを次のように書き換えます(ハイライト箇所)。

classes/AccountTriggerHandler.cls

public with sharing class AccountTriggerHandler extends TriggerHandler {
    /**
     * 作成後処理
     * @param newMap 作成した取引先
     */
    public override void afterInsert(Map<Id, Sobject> newMap) {
        // 取引先名を取り出してChatGPT APIをコールする
        for (Id id : newMap.keySet()) {
            Account acc = (Account)newMap.get(id);
            ChatGPTMediatorCalloutAsync.execute('{"id": "' + acc.Id + '", "name": "' + acc.Name+ '"}');
        }
    }
}

classes/ChatGPTMediatorCalloutAsync.cls

global with sharing class ChatGPTMediatorCalloutAsync implements Database.AllowsCallouts {
    @future(callout=true)
    public static void execute(String body) {
        Http http = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:AWS_APIGW/prod/mediator');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Accept', 'application/json');
        req.setBody(body);
        req.setTimeout(120000);
        HttpResponse res = http.send(req);
        CalloutResponse cres = (CalloutResponse)JSON.deserializeStrict(res.getBody(), CalloutResponse.class);

        FeedItem post = new FeedItem();
        post.IsRichText = True;
        post.Body = cres.content;
        CalloutRequestBody creqBody = (CalloutRequestBody)JSON.deserializeStrict(body, CalloutRequestBody.class);    
        post.ParentId = creqBody.id;
        insert post;
    }

    class CalloutResponse {
        String role;
        String content;
    }

    class CalloutRequestBody {
        Id id;
        String name;
    }
}

Chatterを投稿するときに、投稿先のレコードIDが必要なので、AccountTriggerHandlerでリクエストボディに取引先のIDも渡すようにして、ChatGPTMediatorCalloutAsyncで取り出しています。

再度、取引先クラスメソッドを作ってみると、当該取引先のChatterにChatGPTの回答が投稿されることを確認できます。

ChatGPTの結果をchatterに投稿した結果

いい感じですね。

まとめ

取引先を作ると、その取引先についてChatGPTにたずねた結果がChatterで確認できるサンプルを作ってみました。
本エントリーで解説した方法を活用すれば、他にも、Slackに投稿したり、Salesforceの自動化処理機構であるフローから呼び出したり、色んなことができますので夢が広がります。
ChatGPTへの問い合わせ方で回答もぐっと変わりますので、CDKで作成したAPI側にも工夫の凝らしがいがありそうです。
ご参考になれば嬉しいです。