LoginSignup
3
2

プロンプトテンプレートでカスタム生成AIアプリ「AI商談レビュー」を作る

Last updated at Posted at 2024-04-30

Salesforceの Einstein 1 Platform 上で作る、カスタム生成AIアプリケーションのサンプル第一弾です。今回は、プロンプトテンプレートとApex、Lightning Web コンポーネント(LWC)を組み合わせ、AIに商談・活動状況を多角的に評価・フィードバックをしてもらうAIアプリ「AI商談レビュー」を作ります。

〜「AI商談レビュー」シリーズ 〜

カスタム生成AIアプリ 「AI商談レビュー」 コンセプト

  • ワンクリックで商談・活動状況をセルフチェック可能
  • 任意の評価指標でスコアリング(複数登録可)
  • スコアリング採点基準はフリーテキストで記述

image.png

前提

以下のSalesforceテクノロジーを使用します。

開発環境

  • プロンプトビルダーが有効化されたDeveloper Edition組織を取得 ※有効期限5日

構成

1. Salesforceオブジェクト

3つのカスタムオブジェクトを作ります。

  • 1-1. 商談レビュー
    • 商談レビューを商談レコードに紐付けて管理します
  • 1-2. 商談評価
    • 一回の商談レビューにつき、複数の評価指標に基づいて評価します
  • 1-3. 評価基準
    • 評価指標ごとの評価基準を管理します

1-1. 商談レビュー(DealReview__c)

項目 API名 データ型 備考
商談レビュー名 Name テキスト
商談 Opportunity__c 参照関係(商談) レビュー対象商談レコード
フェーズ Stage__c テキスト レビュー時の商談フェーズを記録
NextStep NextStep__c テキスト レビュー時の商談NextStepを記録
レビュー実施日時 ReviewDateTime__c 日付時間 レビュー実施日時を記録
総評 Summary__c ロングテキストエリア ※プロンプトテンプレートで生成する想定

1-2. 商談評価(DealScore__c)

項目 API名 データ型 備考
項番 No__c 数値
評価項目 Name テキスト 評価項目名
商談レビュー DealReview__c 参照関係(商談レビュー) 紐づく商談レビューレコード
スコア Score__c 数値 ※プロンプトテンプレートで生成する想定
詳細 Detail__c ロングテキストエリア ※プロンプトテンプレートで生成する想定
推奨Nextアクション RecommendedNextAction__c ロングテキストエリア ※プロンプトテンプレートで生成する想定

1-3. 評価基準(DealScoreCriteria__c)

項目 API名 データ型 備考
項番 No__c 数値
評価項目 Name テキスト 評価項目名
説明 Description__c ロングテキストエリア 評価項目の詳細
評価基準 Criteria__c ロングテキストエリア スコアリングの基準

2. プロンプトテンプレート

新しいノーコードツールであるプロンプトビルダーを使い、2つのプロンプトテンプレートを作成します。

2-1. 商談評価プロンプト (DealScoringPrompt)

評価基準に基づいて対象商談をスコアリングし、フィードバックコメントを生成します。
※登録された評価基準数分、本プロンプトが呼び出されます。

設定例

image.png

種類 項目
リソース定義 #1 オブジェクト 商談 (Opportunity)
名前 対象商談
API参照名 targetOpportunity
リソース定義 #2 オブジェクト 評価基準 (DealScoreCriteria__c)
名前 評価基準
API参照名 targetCriteria
プロンプト例

image.png

あなたは営業のエキスパートで、現在、営業担当者のAIアシスタントとして
商談レビューを担当しています。以下の商談情報、評価基準を参考に、対象
商談の"""{!$Input:targetCriteria.Name}"""に関するスコアを教え
てください。スコアは採点基準を満たすものの中で、もっとも高いスコアを
選択してください。
なお、指定のJSONフォーマットで応答してください。

商談情報: """
<商談名>
{!$Input:targetOpportunity.Name}
</商談名>
<フェーズ>
{!$Input:targetOpportunity.StageName}
</フェーズ>
<成約予定日>
{!$Input:targetOpportunity.CloseDate}
</成約予定日>
<営業活動履歴>
{!$Apex:GetActivityHistories.Prompt}
</営業活動履歴>
"""

評価基準: """
<評価項目>
{!$Input:targetCriteria.Name}
</評価項目>
<評価項目の説明>
{!$Input:targetCriteria.Description__c}
</評価項目の説明>
<評価基準>
{!$Input:targetCriteria.Criteria__c}
</評価基準>
"""

応答フォーマット: 
{
  "score":"スコア",
  "detail":"スコアの根拠を短く完結に記述してください(主に営業活動履歴から読み取って要約した内容を記述してください)",
  "nextStep":"本スコアをあげるために、次にどのような営業活動を行うべきか?を完結に記述してください。"
}

2-2. 総評フィードバックプロンプト (DealReviewFeedbackPrompt)

評価項目ごとの評価結果をまとめ、総評フィードバックコメントを生成します。

設定例

image.png

種類 項目
リソース定義 #1 オブジェクト 商談レビュー (DealReview__c)
名前 商談レビュー
API参照名 dealReview
プロンプト例

image.png

あなたは営業のエキスパートで、現在、営業担当者のAIアシスタントとして
商談レビューを担当しています。あなたはレビュー対象商談の営業担当に、
商談の確度が少しでも上がるよう、モチベーションが高まるかつ的確な示唆
を与えるのが得意です。あなたは既に様々な観点から対象商談を評価したものを、
商談評価メモとして記録しています。以下を参考にして、営業担当者に送る
フィードバックコメントを作成してください。
100文字以内で簡潔に記述してください。

商談情報: """
<商談名>
{!$Input:dealReview.Opportunity__r.Name}
</商談名>
<商談フェーズ>
{!$Input:dealReview.Opportunity__r.StageName}
</商談フェーズ>
<完了予定日>
{!$Input:dealReview.Opportunity__r.CloseDate}
</完了予定日>
<NextStep>
{!$Input:dealReview.Opportunity__r.NextStep}
</NextStep>
"""

商談評価メモ: """
{!$RelatedList:dealReview.scores__r.Records}
"""

営業担当者へのフィードバックコメント:

3. LWC & Apex

商談レコードページからワンクリックで商談レビューが開始され、レビュー完了時に商談レビューレコードページへ遷移するLWCを作ります。

3-1. レビュー開始ボタン Lightning Web コンポーネント(LWC)

image.png

aiDealReview.html
<template>
  <div class="slds-var-p-around_small slds-text-align_center">
    <div class="slds-var-p-around_x-small">AI商談レビューを開始します。</div>
    <lightning-button onclick={onClickStartDealReview} label="レビュー開始">
    </lightning-button>
    <template lwc:if={isLoading}>
      <lightning-spinner
        size="small"
        alternative-text="loading"
      ></lightning-spinner>
    </template>
  </div>
</template>
aiDealReview.js
import { LightningElement, wire, api, track } from "lwc";
import getDealScoreCriteria from "@salesforce/apex/AiDealReviewController.getDealScoreCriteria";
import doDealReview from "@salesforce/apex/AiDealReviewController.doDealReview";
import createDealReview from "@salesforce/apex/AiDealReviewController.createDealReview";
import { NavigationMixin } from "lightning/navigation";

export default class AiDealReview extends NavigationMixin(LightningElement) {
  @api recordId; // 商談レコードページの対象商談レコードID
  @wire(getDealScoreCriteria) criteriasData; // 登録された評価指標を取得
  @track reviewResult = {};
  isLoading = false;

  // レビュー開始ボタン押下時
  async onClickStartDealReview() {
    this.isLoading = true;
    this.initDealReview();
    await this.startDealScoring();
    const dealReview = await this.saveDealReview();
    this.isLoading = false;
    this.navigateToDealReviewRecord(dealReview.Id);
  }

  initDealReview() {
    this.criterias.forEach((criteria) => {
      this.reviewResult[criteria.Id] = {
        status: "inqueue",
        score: null,
        detail: null,
        nextStep: null
      };
    });
  }

  // スコアリングプロセスを開始
  async startDealScoring() {
    const criteriaIds = this.criterias.map((criteria) => criteria.Id);
    await this.executeDealScoring(criteriaIds);
  }

  // 評価項目ごとのスコアリング:再帰呼び出し
  async executeDealScoring(criteriaIds) {
    if (criteriaIds?.length > 0) {
      const [criteriaId, ...nextCriteriaIds] = criteriaIds;
      const opportunityId = this.recordId;
      this.reviewResult[criteriaId].status = "ongoing";
      const res = await doDealReview({ opportunityId, criteriaId });
      const { score, detail, nextStep } = JSON.parse(res);
      const status = "done";
      this.reviewResult[criteriaId] = {
        status,
        score,
        detail,
        nextStep
      };
      if (nextCriteriaIds?.length > 0) {
        await this.executeDealScoring(nextCriteriaIds);
      }
    }
  }

  // 商談レビューレコードを保存
  async saveDealReview() {
    const opportunityId = this.recordId;
    const dealScores = this.normalizedCriterias.map(
      ({ score, nextStep, Name, detail, No__c }) => {
        return {
          Name,
          No__c,
          Score__c: score,
          Detail__c: detail,
          RecommendedNextAction__c: nextStep
        };
      }
    );
    const dealReview = await createDealReview({ opportunityId, dealScores });
    return dealReview;
  }

  // 作成した商談レビューレコードページに画面遷移させる
  navigateToDealReviewRecord(recordId) {
    if (recordId) {
      this[NavigationMixin.Navigate]({
        type: "standard__recordPage",
        attributes: {
          actionName: "view",
          recordId
        }
      });
    }
  }

  get criterias() {
    if (this.criteriasData?.data?.length > 0) {
      return this.criteriasData.data;
    }
    return [];
  }

  get normalizedCriterias() {
    if (this.criterias?.length > 0) {
      return this.criterias.map(({ Id, Name, Description__c, No__c }) => {
        const result = this.reviewResult[Id];
        return {
          Id,
          Name,
          Description__c,
          No__c,
          status: result?.status,
          score: result?.score,
          detail: result?.detail,
          nextStep: result?.nextStep
        };
      });
    }
    return [];
  }
}
aiDealReview.js-meta.xml
<?xml version="1.0" encoding="UTF-8" ?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>60.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>AI商談レビュー</masterLabel>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <objects>
                <object>Opportunity</object>
            </objects>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

3-2. LWC 3-1. レビュー開始ボタン 用のApexクラス

AiDealReviewController.cls
public with sharing class AiDealReviewController {

  // 登録された評価基準を返す
  @AuraEnabled(cacheable=true)
  public static List<DealScoreCriteria__c> getDealScoreCriteria() {
    return [
      SELECT Id, Name, No__c, Description__c, Criteria__c
      FROM DealScoreCriteria__c
      WITH USER_MODE
      ORDER BY No__c
    ];
  }

  // 商談レビューレコードを新規作成 & 総評コメントを生成
  @AuraEnabled
  public static DealReview__c createDealReview(String opportunityId, List<DealScore__c> dealScores){
    DealReview__c dr = insertDealReview(opportunityId);
    insertDealScores(dr.Id, 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
    );
    insert dr;
    return dr;
  }
  
  private static void insertDealScores(String dealReviewId, List<DealScore__c> dealScores){
    List<DealScore__c> dss = new List<DealScore__c>();
    for (DealScore__c ds : dealScores) {
      ds.DealReview__c = dealReviewId;
      dss.add(ds);
    }
    insert (dss);
  }

  private static void feedbackToDealReview(DealReview__c dr) {
    dr.Summary__c = generateFeedbackForOverview(dr.Id);
    update dr;
  }

  private static String generateFeedbackForOverview(String dealReviewId) {
    List<Map<String, String>> params = new List<Map<String, String>>();
    params.add(
      new Map<String, String>{ 'id' => dealReviewId, 'key' => 'dealReview' }
    );
    return invokePromptTemplate(params, 'DealReviewFeedbackPrompt');
  }

  // 対象商談を評価指標ごとにスコアリング
  @AuraEnabled
  public static String doDealReview(String opportunityId, String criteriaId) {
    List<Map<String, String>> params = new List<Map<String, String>>();
    params.add(
      new Map<String, String>{
        'id' => opportunityId,
        'key' => 'targetOpportunity'
      }
    );
    params.add(
      new Map<String, String>{ 'id' => criteriaId, 'key' => 'targetCriteria' }
    );
    return invokePromptTemplate(params, 'DealScoringPrompt');
  }

  // プロンプトテンプレートを実行
  private static String invokePromptTemplate(List<Map<String, String>> params, String promptTemplateName) {
    Map<String, ConnectApi.WrappedValue> inputParams = getInputParams(params);
    ConnectApi.EinsteinPromptTemplateGenerationsInput promptGenerationsInput = new ConnectApi.EinsteinPromptTemplateGenerationsInput();
    promptGenerationsInput.additionalConfig = new ConnectApi.EinsteinLlmAdditionalConfigInput();
    promptGenerationsInput.additionalConfig.applicationName = 'PromptTemplateGenerationsInvocable';
    promptGenerationsInput.isPreview = false;
    promptGenerationsInput.inputParams = inputParams;
    ConnectApi.EinsteinPromptTemplateGenerationsRepresentation generationsOutput = ConnectApi.EinsteinLLM.generateMessagesForPromptTemplate(
      promptTemplateName,
      promptGenerationsInput
    );
    ConnectApi.EinsteinLLMGenerationItemOutput response = generationsOutput.generations[0];
    return response.text;
  }
  
  // プロンプトテンプレート用に入力パラメータを変換
  private static Map<String, ConnectApi.WrappedValue> getInputParams(List<Map<String, String>> params) {
    Map<String, ConnectApi.WrappedValue> inputParams = new Map<String, ConnectApi.WrappedValue>();
    for (Map<String, String> param : params) {
      Map<String, String> property = new Map<String, String>();
      property.put('id', param.get('id'));
      ConnectApi.WrappedValue propertyValue = new ConnectApi.WrappedValue();
      propertyValue.value = property;
      inputParams.put('Input:' + param.get('key'), propertyValue);
    }
    return inputParams;
  }
}

3-3. プロンプト 2-1. 商談評価プロンプト 用のApexクラス

GetActivityHistories.cls
public with sharing class GetActivityHistories {
  @InvocableMethod(CapabilityType='FlexTemplate://DealScoringPrompt' label='活動履歴の取得')
  public static List<Response> GetActivitiesForOpportunity(List<Request> requests) {
    Request input = requests[0];
    Opportunity opp = input.targetOpportunity;
    List<Response> responses = new List<Response>();
    Response res = new Response();

    List<Opportunity> opps = [
      SELECT
        (
          SELECT Subject, Description, ActivityType, ActivityDate
          FROM ActivityHistories
        )
      FROM Opportunity
      WHERE Id = :opp.Id
      WITH USER_MODE
      LIMIT 1
    ];

    res.Prompt = JSON.serialize(opps);
    responses.add(res);
    return responses;
  }
  public class Request {
    @InvocableVariable(required=true)
    public Opportunity targetOpportunity;
    @InvocableVariable(required=true)
    public DealScoreCriteria__c targetCriteria;
  }

  public class Response {
    @InvocableVariable
    public String Prompt;
  }
}

4. 商談レコードページ

商談レコード上にLWC 3-1. レビュー開始ボタン を配置します。

image.png

デモ

ダミーデータ準備

ChatGPTにデモ用ダミーデータを作ってもらいます。

評価基準レコード

5つ評価基準を作ってもらいます。

image.png

それぞれ評価基準レコードとして登録します。

image.png

商談・活動レコード

架空の製品

image.png

架空の取引先・商談

image.png

架空の活動履歴

image.png

以上をレコード登録します。

image.png

【デモ】 AI商談レビュー

登録した商談レコードページ上で、レビュー開始ボタンを押下します。

image.png

しばらくするとレビューが完了し、商談レビューレコードページに画面遷移されます。

image.png

以上、ほんの数秒で商談レビューが完了します。今回登録したダミーデータで得られたレビュー結果の全文は以下の通りです。
※AIが生成した文章なので、文言は毎回変わります

  • 総評
    • 顧客ニーズの明確化や行動計画の策定と実行は適切に行われています。一方で、競合分析と差別化ポイントの明確化、意思決定者との関係強化が必要となります。特に、競合との比較を明確に伝え、意思決定者への説明を深化させてください。
  • 詳細
    • 競合分析と差別化ポイント = スコア: 2
      • 営業活動履歴によれば、競合他社についての情報収集が行われており、その結果は提案内容の調整に利用されています。また、プレゼンテーションでは、プラットフォームの機能や利点について詳細に説明が行われており、これらは差別化ポイントとして強調されています。しかし、競合分析の詳細や、どのように差別化ポイントが競合と比較して優れているのかについての明確な言及は見られませんでした。
    • 意思決定者との関係構築 = スコア: 2
      • 営業部マネージャーの佐藤様とIT部門の鈴木様との関係が構築され、複数回の会議やプレゼンテーションなどを通じて信頼関係が築かれつつあります。しかし、まだ完全に信頼関係が確立しているとは言えないため、スコアは2とします。
    • 顧客ニーズの明確化 = スコア: 3
      • 商談履歴から読み取ると、営業担当者は顧客のニーズを詳細に把握しており、それに完全に沿った活動を行っています。具体的には、ニーズの最終確認、プレゼンテーション、見積もり提示などの活動を通じて、顧客の要件やカスタマイズの必要性、導入計画やトレーニングに関する詳細を確認しています。
    • 購買プロセスの把握 = スコア: 3
      • 営業活動履歴から、顧客のニーズの確認、見積もり提示、フォローアップなど、購買プロセスの各ステップが効果的に行われていることが確認できます。また、顧客の担当者とのアポイントメント取得や、プレゼンテーション準備など、顧客とのコミュニケーションも適切に行われていることから、詳細な購買プロセスが把握されていると評価できます。
    • 行動計画の策定と実行 = スコア: 3
      • 営業活動履歴を見る限り、具体的な行動計画が策定され、それが効果的に実行されていることが確認できます。具体的には、情報収集からアポイントメント取得、ディスカバリーコール、プレゼンテーション準備、プレゼンテーション、提案書送付、フォローアップ、ニーズの確認、見積もり提示といった一連の営業活動が行われています。

おわりに

本記事では、Salesforceの新しいテクノロジーであるプロンプトテンプレートを用いて、これまで難しかった、自然言語で記述した活動履歴を、同様に自然言語で記述した採点基準に基づいて定性評価する。というソリューションについて考察しました。

生成AIは、自然言語のフリーテキストでリクエストできるというシンプルさ、柔軟性が革新的です。しかし、本当に注目すべきは、Apexやフローとの連携により、より高度な条件分岐や複雑なロジックが実装でき、また、LWCと連携することでAIの出力をリッチなUIで表現できるなど、既存テクノロジーとの相乗効果によって大きな価値を創出できる点にあります。

プロンプトテンプレートには無限の可能性が秘められています。是非、開発環境で実際に触れて、その魅力を体感してみてください。


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

参考

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2