Twitter
Facebook
Hatena
Salesforce × AgentforceでAI議事録要約を自動化してみた!

営業現場の情報共有を効率化することは、企業の競争力を高める重要な要素です。特に、商談や会議で作成される議事録の迅速な把握は、営業チームの生産性向上に直結します。今回は、Salesforceと連携可能なAIエージェント「Agentforce」を活用し、Microsoft Teamsで作成した議事録を自動要約してSalesforceの商談オブジェクトに取り込む仕組みを構築しました。営業DXに関心のある方はぜひご覧ください。

Salesforceとはクラウド型CRM(顧客管理)プラットフォームで、営業・マーケティング・カスタマーサービスなどの業務を効率化します。
AgentforceとはSalesforceのAIエージェント機能で、業務を自律的に実行するAIを構築・運用できる包括的で拡張性に優れたオープンプラットフォームです。

実現させたいこと

AgentforceとSalesforceを連携してMicrosoft Teamsのトランスクリプト機能で作成した議事録ファイル(PDF)をAgentforceで要約し、その結果をSalesforceの商談オブジェクトに自動的に取り込む仕組みを作ります。今まで議事録作成、レビュー依頼にかかっていた作業が大幅に短縮でき、コスト削減ができると考えます。

  1. Microsoft Teamsのトランスクリプト機能で作成した議事録(PDF)を取込む
  2. Agentforceで要約
  3. Salesforceの商談オブジェクトに要約結果を保存

プロンプトビルダーの設定
プロンプトを効果的に作成することで、生成AIの応答の精度を高め、業務の効率化を図ることができます。 作成時に工夫した内容は以下となります。

出力項目の共通ルール
作成したプロンプトの共通ルールを設定します。

出力項目別の要件、文字数制限
出力項目別に要件を設定し制限事項を細かく指定します。

選択リスト項目
選択リスト項目は選択肢から最も適切なもの選択できるよう指示します。

連動選択リスト項目
連動選択リスト項目は選択肢に応じて選択可能な値が制限されるよう指示します。

関連リスト項目
CombinedAttachmentsを使用し議事録(PDF)の読込みをしています。
関連情報: CombinedAttachment | Salesforce プラットフォームのオブジェクトリファレンス | Salesforce Developers

実装概要

商談オブジェクトに追加したカスタム項目

以下の13項目を商談オブジェクトに追加しました。

  • 引合発生経緯
  • 競合有無内容
  • スケジュール
  • アクションプラン
  • 状況
  • 担当役員
  • 案件状況
  • 競合有無
  • 競合先価格差
  • 人脈
  • 提案合意状況(金額)
  • 提案合意状況(対応範囲)
  • 提案合意状況(スケジュール)

フロー(画面フロー)の構成

以下の6要素を画面フローに作成しました。

  • 画面(議事録要約)
    - PDFファイルのアップロードと要約実行のトリガー
  • レコード取得(商談レコード取得)
    - 更新対象となるレコードを取得する
  • アクション(議事録要約プロンプトを呼び出す)
    - Agentforceに渡すプロンプトを構築
  • Apexアクション(議事録要約を正規化する)
    - 要約結果の整形・エラーチェック
  • 画面(要約結果表示)
    - ユーザーに要約結果を確認させる
  • レコード更新(商談オブジェクト更新)
    - 要約結果を商談オブジェクトに反映

Apexの処理概要

以下の6構成を処理に作成しました。

  • 入力項目の定義
  • 出力項目の定義
  • Agentforceで要約した内容をJSON形式で取得
  • 取得した内容のエラーチェック、正規化
  • 取得した内容を配列に格納
  • 結果出力
public class summary_normalization {


	// 入力項目を定義
    public class Input {
        @InvocableVariable(label='議事録')
        public String input;
    }

    // 出力項目を定義
    public class Output {
        @InvocableVariable(label='引合発生経緯')
        public String InquiryBirthDetails;
        @InvocableVariable(label='競合有無内容')
        public String CompetingPresenceDetail;
        @InvocableVariable(label='スケジュール')
        public String OrderSchedule;
        @InvocableVariable(label='アクションプラン')
        public String ActionPlan;
        @InvocableVariable(label='状況')
        public String OkuenStatusReport;
        @InvocableVariable(label='担当役員')
        public String ResponsibleOfficer;
        @InvocableVariable(label='案件状況')
        public String OpportunityStatus;
        @InvocableVariable(label='競合有無')
        public String CompetingPresence;
        @InvocableVariable(label='競合先価格差')
        public String ConflictPriceDiff;
        @InvocableVariable(label='人脈')
        public String PersonalConection;
        @InvocableVariable(label='提案合意状況(金額)')
        public String ProposedAgreementSituationPrice;
        @InvocableVariable(label='提案合意状況(対応範囲)')
        public String ProposedAgreementSituationRange;
        @InvocableVariable(label='提案合意状況(スケジュール)')
        public String ProposedAgreementSituationSchedule;
    }



    //ここのInvocableMethodラベルを設定するとフローからアクセスできる
    @InvocableMethod(label='議事録正規化')
    public static List<Output> run(List<Input> inputs) {
        List<Output> results = new List<Output>();		// 戻り値
        
        for (Input i : inputs) {
            try{
                // JSONが正しく記載されているか確認
                String check_json = extractJson((String)i.input);
                
                // JSONをMapに格納
                Map<String, Object> jsonMap = (Map<String, Object>)JSON.deserializeUntyped(check_json);

                // output用の宣言
                Output o = new Output();
                
                // outputに値を格納
                o.InquiryBirthDetails = convertSpace((String)jsonMap.get('InquiryBirthDetails'));					// 引合発生経緯
                o.CompetingPresenceDetail = convertSpace((String)jsonMap.get('CompetingPresenceDetail'));			// 競合有無内容
                o.OrderSchedule = convertSpace((String)jsonMap.get('OrderSchedule'));								// スケジュール
                o.ActionPlan = convertSpace((String)jsonMap.get('ActionPlan'));										// アクションプラン
                o.OkuenStatusReport = convertSpace((String)jsonMap.get('OkuenStatusReport'));						// 状況
                o.ResponsibleOfficer = convertSpace((String)jsonMap.get('ResponsibleOfficer'));						// 担当役員
                o.OpportunityStatus = convertSpace((String)jsonMap.get('OpportunityStatus'));						// 案件状況
                o.CompetingPresence = convertSpace((String)jsonMap.get('CompetingPresence'));						// 競合有無
                o.ConflictPriceDiff = convertSpace((String)jsonMap.get('ConflictPriceDiff'));						// 競合先価格差
                o.PersonalConection = convertSpace((String)jsonMap.get('PersonalConection'));						// 人脈
                o.ProposedAgreementSituationPrice = convertSpace((String)jsonMap.get('ProposedAgreementSituationPrice'));		// 提案合意状況(金額)
                o.ProposedAgreementSituationRange = convertSpace((String)jsonMap.get('ProposedAgreementSituationRange'));		// 提案合意状況(対応範囲)
                o.ProposedAgreementSituationSchedule = convertSpace((String)jsonMap.get('ProposedAgreementSituationSchedule'));	// 提案合意状況(スケジュール)
                
                // 結果を格納
                results.add(o);
            }catch(Exception e){
                System.debug('エラー:' + e.getMessage());
            }
        }
        system.debug(results);
        return results;
    }
    
	// JSONの正規化
	public static String extractJson(String input_json) {
        // 最初の { と最後の } の位置を取得
        Integer start_text = input_json.indexOf('{');
        Integer end_text = input_json.lastIndexOf('}');

        try{
            // 両方見つかった場合のみ抽出
            if (start_text != -1 && end_text != -1 && end_text > start_text) {
                return input_json.substring(start_text, end_text + 1);
            }else{
            	return null; // JSON部分が見つからない場合
            }
        }catch(Exception e){
            System.debug('エラー:' + e.getMessage());
        }
        return null;
    }
    
    // 出力項目の正規化
    public static String convertSpace(String input_space) {
        String result;		// 戻り値
        
        // 改行コードの挿入
        try{
            if (!(String.isBlank(input_space))) {
                // 「。」を「。\n」に変換
                result = input_space.replace('。', '。\n');
                // 「・」を「・\n」に変換
                result = result.replace('・', '\n・');
            }else{}
        }catch(Exception e){
            System.debug('エラー:' + e.getMessage());
        }
        
        // 文字列先頭の改行コード削除
        try{   
            if (String.isBlank(result)) {
                return result;
            }else{
                // 正規表現で先頭の改行コードを削除
                result = result.replaceFirst('^(\\r\\n|\\n)+', '');
            }
        }catch(Exception e){
            System.debug('エラー:' + e.getMessage());
        }
        
		return result;
    }
}

実際の操作手順

1. 議事録要約に議事録PDFファイルをアップロードします

2. 開始ボタンをクリックすると議事録PDFファイルの内容が要約され表示されます

3. 保存ボタンをクリックすると追加したカスタム項目に保存されます

実装時の注意すべきポイント
フローの「アクション設定」時に、「入力値を設定」欄で前段の「レコード取得」要素が選択できず、反映されないという現象が発生しました。何度かリトライすることで解消されましたが、原因は不明…。Salesforceのフロー設定は繊細ですね。

技術ポイント

  • Apexは、サーバーでフローとトランザクションの制御ステートメントをAPIへのコールと組み合わせて実行できるようにしたオブジェクト指向のプログラミング言語です。
  • InvocableMethod を使用して、フローからApexを呼び出し
  • PDFファイルアップロード時の制約事項があります。公式ヘルプ「ファイルのサイズおよび共有の制限」でご確認お願いします。

関連情報: Salesforceファイルサイズ制限に関するヘルプ記事

最後に

SalesforceとAgentforceの連携は、業務効率化の可能性を大きく広げてくれます。今後も業務効率化に向けた取り組みを紹介していきます。ぜひご期待ください!

この記事の執筆者

加藤 英雄Hideo Kato

ソリューション事業本部
情報ソリューション事業部
DXソリューション部
担当

Agentforce