SalesforceからChatGPTを使ってみよう
はじめに
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をコールする方法として、大別して二つあると考えています。
- LWC(Lightning Web Component) から Node.js ライブラリを使ってコールする
- ChatGPT APIをコールするAPIを実装して、それをSalesforceのコールアウトを使って呼び出す
本エントリーでは後者のコールアウトを使う方法を使ってみたいと思います。
段取りは次の通りです。
- ChatGPT API をコールする AWS Lambda を実装する
- 1をコールアウトで呼び出すApexコードを記述する
- (おまけ)コールアウトの結果を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
を次のように書きました。
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に質問するようにしたいので、次のように定義しました。
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をコールします。
Salesforceでの指定ログイン情報の作成は、参照先記事の方法で実施済みとします(名前をAWS_APIGW
として定義したものとします)。
取引先を作成するタイミングで実行したいので、取引先(Account)に対するApex Triggerを作成します。
triggers/AccountTrigger.trigger
を次のように実装してみました。
trigger AccountTrigger on Account (after insert) { new AccountTriggerHandler().execute(); }
処理の実体はAccountTriggerHandler
というApexクラスの方で行います。
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作成用の汎用抽象クラスです。作っておくと便利ですよ。
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
クラスで行っています。
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の実行結果が記載されていることを確認します。ここでは取引先名クラスメソッド
を指定してみました。
開発者コンソールのログを見てみます。
期待する結果が取れていますね。
(おまけ)コールアウトの結果をChatterに投稿してみる
取得した結果をChatterに投稿してみましょう。AccountTriggerHandler
とChatGPTMediatorCalloutAsync
を次のように書き換えます(ハイライト箇所)。
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+ '"}'); } } }
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で確認できるサンプルを作ってみました。
本エントリーで解説した方法を活用すれば、他にも、Slack
に投稿したり、Salesforceの自動化処理機構であるフローから呼び出したり、色んなことができますので夢が広がります。
ChatGPTへの問い合わせ方で回答もぐっと変わりますので、CDKで作成したAPI側にも工夫の凝らしがいがありそうです。
ご参考になれば嬉しいです。