はじめに
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
を次のように書きました。
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をコールします。
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の実行結果が記載されていることを確認します。ここでは取引先名クラスメソッド
を指定してみました。
開発者コンソールのログを見てみます。
期待する結果が取れていますね。
(おまけ)コールアウトの結果をChatterに投稿してみる
取得した結果をChatterに投稿してみましょう。AccountTriggerHandler
とChatGPTMediatorCalloutAsync
を次のように書き換えます(ハイライト箇所)。
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で確認できるサンプルを作ってみました。
本エントリーで解説した方法を活用すれば、他にも、Slack
に投稿したり、Salesforceの自動化処理機構であるフローから呼び出したり、色んなことができますので夢が広がります。
ChatGPTへの問い合わせ方で回答もぐっと変わりますので、CDKで作成したAPI側にも工夫の凝らしがいがありそうです。
ご参考になれば嬉しいです。