【コード開発】続編: Einstein Copilotで「AI商談レビュー」の対話型AIアシスタントアプリを実現

前記事の続編として、Salesforceの Einstein 1 Platform 上で作る、カスタム生成AIアプリケーションのサンプル第二弾になります。今回は、前記事にて作成したAIに商談・活動状況を多角的に評価・フィードバックをしてもらうカスタムアプリをベースに、Einstein Copilot カスタムアクションを使用し、「AI商談レビュー」の対話型AIアシスタントアプリを構築します。

 〜「AI商談レビュー」シリーズ 〜
 - [第一弾 プロンプトテンプレートでカスタム生成AIアプリ「AI商談レビュー」を作る](https://qiita.com/misu007/items/feb70c7e49a6382ab7c0) 
 - **第二弾 Einstein Copilotで「AI商談レビュー」の対話型AIアシスタントアプリを実現（本記事）**


![Pasted Graphic 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/3ce1d265-58c4-c520-0093-8db6f5e80ab4.png)

:::note warn
Einstein Copilotは米国での一般提供を開始してますが、2024/5/8 現在、日本での提供開始時期は未定です。
:::

## 前提
以下のSalesforceテクノロジーを使用します。
 - プロンプトテンプレート
     - https://trailhead.salesforce.com/ja/content/learn/projects/quick-start-prompt-builder
 - Einstein Copilot カスタムアクション
     - https://trailhead.salesforce.com/ja/content/learn/projects/quick-start-einstein-copilot
 - Apex
     - https://trailhead.salesforce.com/ja/content/learn/projects/quickstart-apex
 - Queueable Apex
     - https://trailhead.salesforce.com/ja/content/learn/modules/asynchronous_apex

### 開発環境 
 - プロンプトビルダー / Einstein Copilotが有効化されたDeveloper Edition組織を取得  
※有効期限5日

https://trailhead.salesforce.com/content/learn/projects/quick-start-prompt-builder

## 構成
前記事からの続きの手順になります。

### 1. Salesforceオブジェクト
商談レビューのステータスを管理できるよう、作成済みのカスタムオブジェクトにカスタム項目を一つ追加します。

#### 1-1. 商談レビュー(DealReview__c)
|項目|API名|データ型|備考|
|:--|:--|:--|:--|
|ステータス|Status__c|選択リスト|３つの状態を管理<br/> <b>レビュー前 => レビュー中 => 完了</b>|


### 2. Apex
Plannerから商談レビュー用Copilotアクションが呼ばれる際、一部時間のかかる処理を非同期ジョブにオフロードする構成で、いくつかApexクラスを作成します。

 - **Copilotアクション用Apex**
 - **非同期ジョブ用Apex**

```mermaid
sequenceDiagram
    autonumber
    participant Planner
    participant Copilotアクション用Apex
    participant 非同期ジョブ用Apex
    participant Platform
    Planner->>Copilotアクション用Apex: Copilotアクション呼び出し
    activate Copilotアクション用Apex
    Copilotアクション用Apex->>Platform: insert 商談レビュー
    activate Platform
    Platform-->>Copilotアクション用Apex: 商談レビューレコード
    deactivate Platform
    Copilotアクション用Apex-->>非同期ジョブ用Apex: 非同期ジョブ呼び出し
    Copilotアクション用Apex->>Planner: 商談レビューレコード
    deactivate Copilotアクション用Apex
    loop 評価基準ごとの処理
        activate 非同期ジョブ用Apex
        非同期ジョブ用Apex->>Platform: プロンプト呼び出し
        activate Platform
        Note left of Platform: 商談評価プロンプト
        Platform-->>非同期ジョブ用Apex: スコアリング結果
        deactivate Platform
        非同期ジョブ用Apex->>非同期ジョブ用Apex: すべての評価基準[]
        deactivate 非同期ジョブ用Apex
    end
    activate 非同期ジョブ用Apex
    非同期ジョブ用Apex->>Platform: insert 商談評価[]
    非同期ジョブ用Apex->>Platform: プロンプト呼び出し
    activate Platform
    Note left of Platform: 総評フィードバックプロンプト
    Platform-->>非同期ジョブ用Apex: 総評フィードバック
    deactivate Platform
    非同期ジョブ用Apex->>Platform: update 商談レビュー
    deactivate 非同期ジョブ用Apex
```

#### Copilotアクション用Apex

```apex:AiDealReviewCopilotAction.cls
public with sharing class AiDealReviewCopilotAction {
  @InvocableMethod(
    label='商談レビューの実施'
    description='Conduct a deal review for a specific Opportunity record'
  )
  public static List<Output> startDealReview(List<Input> inputs) {
    List<Output> outputs = new List<Output>();
    try {
      Input input = inputs[0];
      Id opptyId = input.OpportunityId;

      Output output = new Output();

      DealReview__c dr = AiDealReviewController.insertDealReview(opptyId);
      List<DealScoreCriteria__c> dscs = AiDealReviewController.getDealScoreCriteria();
      List<DealScore__c> dss = new List<DealScore__c>();
      AsyncOptions asyncOptions = new AsyncOptions();
      asyncOptions.MaximumQueueableStackDepth = dscs.size() + 1;
      System.enqueueJob(new AiDealReviewAsyncJob(opptyId, dr.Id, dscs, dss), asyncOptions);
      output.dealReview = dr;
      output.annotationMessage = 'レビューを開始しました。（レビュー完了まで時間がかかる場合があります）';
      outputs.add(output);
    } catch (Exception e) {
      Output output = new Output();
      output.dealReview = null;
      output.annotationMessage = 'もう一度実行してください';
      outputs.add(output);
    }
    return outputs;
  }

  public class Input {
    @InvocableVariable(required=true, description='The ID of the Opportunity record to be reviewed')
    public Id OpportunityId;
  }

  public class Output {
    @InvocableVariable(description='Display this message as a response')
    public String annotationMessage;
    @InvocableVariable(description='Display this created Deal Review record (Object name is DealReview__c)')
    public DealReview__c dealReview;
  }
}
```

#### 非同期ジョブ用Apex
`Copilotアクション用Apex` から呼び出される非同期ジョブをQueueable Apexで作ります。チェーニングしてジョブを連続して実行させます。

```apex:AiDealReviewAsyncJob.cls
public with sharing class AiDealReviewAsyncJob implements Queueable {
  String opptyId;
  String dealReviewId;
  List<DealScoreCriteria__c> dealScoreCriterias;
  List<DealScore__c> dealScores;

  public AiDealReviewAsyncJob(String opptyId, String dealReviewId, List<DealScoreCriteria__c> dealScoreCriterias, List<DealScore__c> dealScores
  ) {
    this.opptyId = opptyId;
    this.dealReviewId = dealReviewId;
    this.dealScoreCriterias = dealScoreCriterias;
    this.dealScores = dealScores;
  }
  public void execute(QueueableContext qc) {
    if (dealScoreCriterias.isEmpty()) {
      getSummaryFeedback();
    } else {
      getDealScore();
    }
  }

  private void getDealScore() {
    DealScoreCriteria__c dsc = dealScoreCriterias.remove(0);
    DealScore__c ds = new DealScore__c(
      No__c = dsc.No__c,
      Name = dsc.Name,
      DealReview__c = dealReviewId
    );
    try {
      String jsont = AiDealReviewController.doDealReview(opptyId, dsc.Id);
      DealScoringResponse dsr = (DealScoringResponse) JSON.deserialize(
        jsont,
        DealScoringResponse.class
      );
      ds.Score__c = Integer.valueOf(dsr.score);
      ds.Detail__c = dsr.detail;
      ds.RecommendedNextAction__c = dsr.nextStep;
    } catch (Exception ex) {
      ds.Score__c = null;
      ds.Detail__c = ex.getMessage();
      ds.RecommendedNextAction__c = null;
    } finally {
      dealScores.add(ds);
      System.enqueueJob(new AiDealReviewAsyncJob(
          opptyId,
          dealReviewId,
          dealScoreCriterias,
          dealScores
      ));
    }
  }

  private void getSummaryFeedback() {
    AiDealReviewController.updateDealReview(dealReviewId, dealScores);
  }

  private class DealScoringResponse {
    private String score;
    private String detail;
    private String nextStep;
  }
}

```


#### 既存Apex `AiReviewController` の変更
`非同期ジョブ用Apex` 用に一部変更します。
```apex:AiDealReviewController.cls
public with sharing class AiDealReviewController {

  //
  // 以下を追加します
  //  
  public static DealReview__c updateDealReview(
    String dealReviewId,
    List<DealScore__c> dealScores
  ) {
    DealReview__c dr = [
      SELECT Id
      FROM DealReview__c
      WHERE Id = :dealReviewId
      WITH USER_MODE
      LIMIT 1
    ];
    insertDealScores(dealReviewId, dealScores);
    feedbackToDealReview(dr);
    return dr;
  }

  //
  // 以下を更新します
  //
　public static DealReview__c insertDealReview(String opportunityId) {
    Datetime dt = Datetime.now();
    Opportunity opp = [
      SELECT Id, Name, NextStep, StageName
      FROM Opportunity
      WHERE Id = :opportunityId
      WITH USER_MODE
      LIMIT 1
    ];
    String dealReviewName = opp.Name + '_商談レビュー_' + dt.format('yyyy-MM-dd HH:mm');
    DealReview__c dr = new DealReview__c(
      Opportunity__c = opportunityId,
      Name = dealReviewName,
      NextStep__c = opp.NextStep,
      Stage__c = opp.StageName,
      Status__c = 'レビュー中' // この行を追加
    );
    insert dr;
    return dr;
  }

  public static void feedbackToDealReview(DealReview__c dr) {
    dr.Summary__c = generateFeedbackForOverview(dr.Id);
    dr.Status__c = '完了'; // この行を追加
    update dr;
  }

}

```

### Einstein Copilot カスタムアクション

商談レビューを実施するEinstein Copilot カスタムアクショを一つ作成し、有効化します。作成するカスタムアクションの実体は上記で作成したApexです。カスタムアクションを準備するだけで、Plannerが会話の中で適切なタイミングでアクションを呼び出し、結果を処理してくれます。

#### Copilotアクションの定義
設定 > Einstein 生成AI > Einstein Copilot アクション > "+ 新規 Copilot アクション"ボタンを押下し、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/6944ecbd-dcf3-5e97-5772-e83803a16986.png)

以下のようにアクションを定義します。
 - 参照アクション種別
     - Apex
 - 参照アクション
     - 商談レビューの実施
 - Copilotアクション表示ラベル
     - 商談レビューの実施

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/6bc2047f-a46d-8ff6-72af-d64777ec4156.png)



続いて、入力と出力について以下のように設定します。
 - Copilot アクション手順
     - Conduct a deal review for a specific Opportunity record
 - 入力
     - OpportunityId
         - Instructions
             - The ID of the Opportunity record to be reviewed
         - データ型
             - lightning__recordIdType
         - 入力が必要
             - チェック
 - 出力
     - annotationMessage
         - Instructions
             - Display this message to user as a response
         - データ型
             - lightning__textType
         - 会話に表示
             - チェック
     - dealReview
         - Instructions
             - Dsplay this created Deal Review record (Object name is DealReview__c)
         - データ型
             - lightning__recordInfoType
         - 会話に表示
             - チェック


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/e8d2ba7b-5a22-925d-fec4-7b3bf198780f.png)

> CopilotアクションはPlannerにより、まるで人間が文章を読んで判断するかのように選択・実行されますので、対話型AIアシスタントアプリの構築では以下要素の**自然言語での記述**がとても重要になります。
> - CopilotアクションのInstrruction
> - 入力パラメータのInstrruction
> - 出力パラメータのInstrruction

#### Copilotアクションの有効化

Copilotビルダーで作成した Copilotアクション `商談レビューの実施` を有効化し、CopilotをActivateします。


![Pasted Graphic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/f7b32af1-91d3-528a-b431-d7579aaa2b50.png)

以上で準備が整いました。

## デモ

### 【シーン①】 商談名を指定して実行

まずは既存商談
 - データナビゲーション・テクノロジーズ_ユニバーサル・データネット

に対してレビューを依頼してみましょう。


Einstein Copilotを開き、「Please review my opportunity "データナビゲーション・テクノロジーズ_ユニバーサル・データネット"」とお願いしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/7dacc0d3-a948-8193-2af1-7edf43345ed4.png)

すると、「レビューを開始しました」というメッセージと共に、商談レビューレコードが表示されます。しばらくしてからクリックすると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/ee36bf85-d93b-c08d-b68a-93aaf3ff2bb9.png)


商談レビュー結果が表示され、以下内容がスムーズに確認できました。

 - 総評コメント
 - 各評価項目ごとのスコア・コメント


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/f7788fbc-a45a-404b-0ae8-38da26293eba.png)



### 【シーン②】 商談名を指定せずに実行

今度は敢えて、商談名を指定せずに商談レビューをお願いしてみましょう。商談名を指定しないとどうなるでしょう？Einstein Copilotに、「Please review the opportunity」と聞いてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/a22e554c-e05a-84c0-9e0a-53e99f6f161f.png)

すると、今後は成功するわけでもエラーになるわけでもなく、なんとEinstein Copilotは、不足している情報を補うために、レビュー対象商談の商談名を入力するよう、聞き返してくれました。  

商談名を入力し、"Submit"を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/1a2e87fd-a20b-323e-a997-a3b70c21372a.png)

これで無事、商談レビューを実施してくれました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/a41457ea-3197-1b36-82f6-b4f5b42f93cd.png)



### 【シーン③】 会話の流れから実行

自分の担当商談を確認するという別の会話の流れから、表示された商談に対してレビューをお願いすることもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/9d8dd5fb-a141-3e43-4b1b-f6ae9f2888c9.png)

> 「Show my Opportunities」に対するEinsteinの回答は、デフォルトで利用できる標準アクションが呼び出された結果となります。

## おわりに

本記事では、Einstein Copilotカスタムアクションを活用して、「AI商談レビュー」の対話型AIアシスタントアプリを構築する方法を紹介しました。基本、既存Salesforceテクノロジーで対話型AIアシスタントアプリを構築でき、また、開発者は対話型AIアシスタントそのものの開発ではなく、どのようなソリューションを提供し価値創出できるか？の部分に多くの時間を割くことが可能です。

従来テクノロジーで実現できなかったことが、もしかしたらEinstein 1 Platformで実現できる可能性があります。是非、開発環境で実際にお試しください。

---


[【前記事】プロンプトテンプレートでカスタム生成AIアプリ「AI商談レビュー」を作る](https://qiita.com/misu007/items/feb70c7e49a6382ab7c0)

https://qiita.com/misu007/items/feb70c7e49a6382ab7c0


## ご参考

https://developer.salesforce.com/blogs/2024/03/build-custom-copilot-actions-using-apex

https://trailhead.salesforce.com/content/learn/trails/build-ai-assistants-with-einstein-copilot