LINEとAgentforceをSalesforce Sitesで仲介するコードの生成に関する顛末

みなさんこんにちは。エンジニアの佐藤です。本日はLINEのボットをAgentforceで実装する投稿の第2弾となります。

前回、LINEボットをAgentforceで実現する方法を、Python / Flask / SQLiteを使った実装で、ChatGPTによるコード生成で書き起こすお話を紹介しました。

ところで、SalesforceにはSalesfore Sitesという機能があり、これは完全なWebサーバーとして機能します。(Agentforceとのデザインパターンについては、弊社竹田のブログでも紹介されています。)AgentforceがSalesforceの機能である以上、他に理由がなければ、Salesforce組織ひとつで完結するこの実装の方が良さそうです。Sitesのエンドポイントは無認証でアクセスでき、後述するApex RESTによってHTTPリクエストハンドリングの全てを調整することができます。(もちろん立派なHTTPS証明書も付いています。)ApexからのHTTPSコールアウトはもちろん実行可能で、Salesforceの各種ストレージが利用できますので、LINEボットの実装要件を全て満たすのです。

不安もあります。Apexで開発することになるので、前回使ったLINEボットPython SDKは使えません。特にWebhookの署名を検証する手順などの暗号処理が正確に移植できるのか、筆者は心配でした。また、その他にも、やっていくと最後になって決定的なSalesforce環境の制約に遭遇するのではないか?といういつもの不安がつきまといます。

などの不安はあるものの、なんとかなるだろうというのが筆者の予想でした。そして今回も、生成AIでなるべく完成品に近いコードを書き起こすことを目標に、この移植作業に取り掛かりました。

結論を先に申し上げますと、この目論見はうまくいきました。Agentforceで実装されたLINEボットは、Salesforce環境一つで実現可能だったのです。そして大半のコードはClaude 4 Sonnetが書き起こしてくれました。しかし様々な顛末がありましたので、ここに報告させていただきます。何かの参考にしていただければ幸いです。

なお、LINE - Agnetforce接続コードSalesforce Site版の調整済みコードが手っ取り早く知りたい方は、こちらをご覧ください。

移植の計画

生成AIがやってくれると言っても、「じゃあ良しなに」で丸投げというわけにはいかない、というのが筆者の予想でした。しっかりと計画を立て、プロンプトとして注入してやる必要があるでしょう。生成プロンプトは以下のような構成にしました。

Python版のコード
Apexのリクエストハンドリングに関する説明
定数、環境変数、一時ストレージ
LINEメッセージフォーマット
LINE Webhook署名検証手順

例によって、これらの情報は、表記について礼儀正しくある必要はないものの、ソフト開発に要求される厳密な情報提供となっていることが重要です。

以下に要点を説明させていただきます。プロンプト全体は、こちらのようになりました。(英文が稚拙な点はご容赦ください。)

Apexのリクエストハンドリングに関する説明

Python版ではHTTPサーバーの実装にFlaskを使いましたが、Salesforce Sitesの場合はWebサーバーを実装する必要はなく、Apex RESTという仕組みでハンドラを記述します。

@RestResource(urlMapping='/SiteTest01Handler02/*')
global with sharing class SiteTest01Handler02 {
...

この仕組みは古くからあるもので、LLMは学習済みだろうと考え、プロンプトでは概要を案内するだけにしました。

環境変数、一時ストレージ

ここが最も苦労したところです。以下のような移植方針を説明しました。

情報の種類 Pythonでの実装 Sitesでの実装
LINE接続情報 Flask環境変数 カスタム設定
Salesforce接続アプリケーションのコンシューマーキーとシークレット Flask環境変数 Named Credentials
AgentforceセッションID SQLite Salesforceカスタムオブジェクト

Salesforce接続アプリケーションの呼び出しは、特別なエンドポイント表記となりますので、事前にNamed Credential "SalesforceApi01"を登録し(この時コンシューマーキーとシークレットを登録)、プロンプトで以下のようなサンプルコードを案内しました。

HttpRequest req = new HttpRequest();
SiteTest01L2A01__c rec = SiteTest01L2A01__c.getOrgDefaults();
String agentId = rec.AfAgentId__c;
req.setEndpoint('callout:SalesforceApi01/einstein/ai-agent/v1/agents/' + agentId + '/sessions');
...

LINEメッセージフォーマット

PythonではLINEが提供したPython SDKが使えたので、JSONレベルでメッセージフォーマットを考慮する必要はありませんでした。しかしSDKが無いApexに対しては、JSONフォーマットを教えてあげる必要があるでしょう。

LLMが既に公開情報を学習しているかもしれませんが、情報が古い可能性もありますので、LINE Developers リファレンスから最新仕様をコピペしました。

LINE Webhook署名検証手順

この点については、事前検証したサンプルコードをそのまま貼り付けて例示しました。(このコードも実のところChatGPTが書き起こしたものを手元で検証したのです。。)

生成コードの品質は?

生成されたばかりのコードはこちらのようになりました。なお、予めお詫びですが、このコードは先ほどの調整版とだいぶ違っています。これは調整版コードの元になった生成結果を手元で保存し忘れたためです。そのためメソッドの生成順序などが異なり、diffで差分を見ることはできなくなっています。複数回の生成試行によってはこれだけ結果が違うんだ、という雰囲気を感じていただければと思います。以下は筆者が覚えている範囲での所感をまとめたものです。

全体的には、良く書き起こしてあります。計画的に情報注入した成果でしょうか。以下のような点が丁寧に作り込まれています。

  • JSONメッセージに対応するエンティティクラス
  • Python版ではSQLiteで実装されていた一時ストレージの、Salesforceカスタムオブジェクトへの移植
  • Agent APIの取り扱い

しかし、一箇所だけ、クリティカルな間違いがありました。それはSalesforce固有の「Apexコールアウトの前にDMLを実行できない」を考慮できていなかった点です。

生成コードではこうなっていましたが、

private static void handleMessageEvent(WebhookEvent event) {
    String lineUserId = event.source.userId;
    String incomingText = event.message.text.trim();
    String afReply;
    
    try {
        String contactSfid = getOrCreateContact(lineUserId);
        String sessionId = getAgentforceSession(lineUserId, contactSfid);
        afReply = sendToAgentforce(sessionId, incomingText, contactSfid); // <= NG
        ...

このままでは例外となってしまいます。

以下のように修正する必要がありました。

private static void handleTextMessage(LineEvent event) {
    String lineUserId = event.source.userId;
    String incomingText = event.message.text;
    String replyToken = event.replyToken;
    
    try {
        // 1. Get or create contact
        String contactSfid = getOrCreateContact(lineUserId);
        
        // 2. Ensure Agentforce session
        String sessionId = getAgentforceSession(lineUserId, contactSfid);
        
        handleAgentforceReply(sessionId, incomingText, contactSfid, replyToken);
        ...
}
@future(callout=true) // <= 非同期メソッドにする必要がある。
private static void handleAgentforceReply(
    String sessionId,
    String incomingText,
    String contactSfid,
    String replyToken
){
    // 3. Send user's text to Agentforce
    String afReply = sendToAgentforce(sessionId, incomingText, contactSfid);
    ...

これはSalesforceに昔からある制約なのですが、さすがのClaudeもここまではフォローできなかったようです。(筆者も例外を見るまでこのことは知りませんでした。)ただし、これもプロンプトでこの例を示せば、生成結果は改善されたかもしれませんね。

振り返って、どうか

移植コードの生成は、相当にLLMに向いている処理だと思います。移植元も動作するコードではあるので、入力情報としては高精度である点が理由の一つだと思います。移植先環境の要求事項を正確に入力することで、高い精度の変換が期待できると思います。

最後までお読みいただき、ありがとうございました。