メインコンテンツへスキップ
見出し画像

【Dify×Salesforce×Claude】PDF転記作業を卒業!PDF解析からSalesforce登録まで自動化してみた

    皆さんこんにちは!
    PDFからの情報コピペ作業、もううんざりしていませんか?職務経歴書から候補者情報を転記したり、請求書の内容をシステムに入力したり...単純作業に時間を費やしている方も多いのではないでしょうか。

    採用・営業・法務・経理など、どの部署でも共通して抱えているこの課題。PDFの情報を手作業でCRMや管理システムに転記する作業は、時間がかかる上にミスも起こりやすいですよね。

    そこで今回は、Difyのチャットフロー機能を使って、この面倒な作業を完全自動化するアプリを作ってみました!PDFをアップロードしてチャットで「情報を抽出して」と話しかけるだけで、必要な情報を綺麗に整理してくれます。さらに「Salesforceに登録して」と言えば、自動でリードレコードまで作成してくれます!

    このアプリがあれば、従来30分かかっていた作業が3分で完了するのも夢じゃないかも!
    今回はこのアプリの仕組みや構築方法を詳しく解説していきます!
    最後にはDMLファイルもご用意しています!

    今回の担当

    画像

    完成イメージ

    今回例として作成するのは、職務経歴書ファイルを元に求職者情報を抽出し、Salesforceのリードオブジェクトにレコードを登録するアプリケーションです!
    以下のような流れを想定しています!

    画像

    1. ユーザーがPDFファイルをアップロードし「求職者の情報を抽出して」と入力
    2. システムがPDFを解析し、名前・職務経歴・スキル・自己PRを構造化データとして抽出・出力
    3. 抽出された情報をユーザーが確認し、「この情報を元にSalesforceに登録して」と入力
    4. システムがSalesforce APIを通じて自動的にリードレコードを作成

    アプリの全体像はこちらです!

    画像

    アーキテクチャ概要

    システム構成は以下の通りです!
    抽出した情報を登録する先をSalesforceにしていますが、API連携できるものであれば、他サービスへの連携にも応用可能です!

    • Dify: チャットフロー全体の制御とユーザーインターフェース

    • Claude Sonnet 4: PDF内容の解析とパラメータ抽出

    • Python: 構造化データへの変換

    • Salesforce REST API: リードレコードの作成

    Difyチャットフローの実装

    今回は抽出した内容を確認したり必要に応じて編集してからSalesforceに登録したかったので、やり取りが可能なチャットフローを選んでいます!
    また、ユーザーが求めていることに応じてフローを分岐し、PDFの解析・Salesforceへの登録の処理を分けるよう設計しています。

    画像

    では、テンプレートから作成>チャットフローを選択し作成していきましょう!

    開始
    開始の入力フィールドを追加します。フィールドタイプは単一ファイルにし、ファイルタイプはドキュメントと画像を選択しておきます。(PDFとPNGで動作確認済みです!他はサポートされていない場合がありますのでご了承ください)
    ※「アップロードされたファイルのタイプ」は、ローカルがおすすめです。
    URLだと取り込みエラーになる場合があります。

    画像

    質問分類器
    開始した時のチャット内容からユーザの目的を判断し、これによってフローを分岐させます!

    画像

    クラス1に当てはまる場合は求職者情報の抽出、クラス2に当てはまる場合はSalesforceへの登録というように分岐させます。

    ここからは、まず求職者情報の抽出機能、その後Salesforceへの登録機能についてご説明します!

    実装🔧 求職者情報の抽出機能

    IF/ELSE
    IF/ELSEにて添付ファイルがあるかを確認し、ない場合は「ファイルを送信してください」というメッセージを返します。

    画像

    LLM
    ファイルが見つかった場合は、Claude Sonnet4.0で文字起こししてもらいます!

    画像

    ※今回はLLMノードでClaudeを使用していますが、Claudeが使用できない場合は「テキスト抽出ノード」でPDFやtxtファイルの文字起こしが可能です!(詳細はDify ドキュメントをご参照ください。)

    パラメータ抽出
    文字起こしした内容から、欲しい情報を抽出するための設定をします!
    今回は氏名・自己PR・スキル・職務経歴を取得したいため、
    パラメータ抽出ノードにて、name・selfPromotion・skill・workExperienceという4つのパラメータを設定しました。

    画像

    コード実行
    抽出した情報をわかりやすく出力し、次のSalesforce登録へ使用できるようにするため、コード実行ノードでJSON形式にフォーマットしておきます。

    画像

    使用したPythonコードはこちらです!

    import json
    
    def main(name: str, selfPromotion: str, skill: str, workExperience: str) -> dict:
        inner_dict = {
            "name": name,
            "selfPromotion": selfPromotion,
            "skill": skill,
            "workExperience": workExperience
        }
        return {
            "result": json.dumps(inner_dict, ensure_ascii=False)
        }

    LLM
    JSONに変換したものをそのまま出力しても良いのですが、読みにくいので、人間が読みやすい形に変換してもらいます!
    ここのモデルは何でもOKです!

    画像

    回答
    最後に、情報を読みやすく整えたものを回答して終了です!

    画像

    おまけ(会話の開始 を設定)
    ユーザーに使い方を説明したいので、会話開始時にボットからユーザーに最初に送信されるメッセージを設定しておきます!

    画像

    以下のように設定しておきました!

    画像

    実行🔍 求職者情報の抽出機能

    求職者情報の抽出機能が完成したので、プレビューで動かしてみます!
    画面右上のプレビューボタンをクリックすると、ファイルアップロード画面と、先ほど設定したメッセージが表示されることが確認できます!

    画像

    では実際に、PDFをアップロードして試してみます!
    今回はこちらのお試し用PDFを使用しています。

    このように出力されました!
    PDFから、4つのパラメータ(氏名・自己PR・スキル・職務経歴)について出力してくれています!

    ここまでのDMLファイルは、こちらからダウンロードいただけます!


    実装🔧 Salesforceへの登録機能

    では続いて、Salesforceへの登録機能を実装していきます!
    まずは事前準備をしていきましょう!

    ーーーーーーーーーーー事前準備ーーーーーーーーーーーーー

    Salesforce側の準備 - 外部クライアントアプリケーション作成
    Salesforceにて、外部クライアントアプリケーションを作成します!
    設定 > 外部クライアントアプリケーションマネージャー > 新規外部クライアントアプリケーションボタンをクリック。

    アプリケーション名と、取引先責任者メールアドレス(自分のアドレスでOK)を入力します!

    画像

    「OAuthを有効化」にチェックを入れ、コールバックURLとOAuth範囲を選択します!コールバックURLは今回は使用しないので、適当なものでOKです!
    また今回はクライアントログイン情報フローを使用したいので、こちらを有効化します!

    画像

    ここまで設定できたら、作成ボタンをクリックします!

    作成後、ポリシータブの編集ボタンをクリックし、クライアントログイン情報フローの実行者を設定します!
    本番環境ではAPI専用ユーザーを作成するのがベストですが、今回はPlaygroundで実装しているので自身のユーザー名を入力しました。

    画像

    保存後、設定タブのコンシューマー鍵と秘密をクリックしてコンシューマーキーとコンシューマーシークレットを取得します!

    画像

    これらの値はDifyに設定するのでメモしておきます!

    画像

    これで、SalesforceにAPIを叩いてレコードを作成する準備ができました!

    また今回はリードオブジェクトを使用するので、自己PR・スキル・職務経歴のカスタム項目を作成しておきました!(割愛します)

    ではDifyに戻ります!

    環境変数の設定
    先ほど取得したコンシューマーキーとコンシューマーシークレットをDifyに環境変数として設定します!
    右上のENVボタン > 環境変数を追加をクリック。下記の3つを登録します!
    CLIENT_IDにコンシューマーキー、CLIENT_SECRETにコンシューマーシークレットを設定。TOKEN_URLには、設定 > 私のドメイン で確認したURL に「/services/oauth2/token」を追加したURLを設定します。
    https://〇〇.my.salesforce.com/services/oauth2/token のような形式です!

    画像

    ーーーーーーーーーーー事前準備ここまでーーーーーーーーーーーーーーー

    ここから、チャットフローの実装を進めていきます!

    これから実装していくのは、質問分類器のクラス2に当てはまる場合の機能です!

    画像

    パラメータ抽出
    ここでは、ユーザーから登録したい求職者情報をメッセージで送信されることを想定しています。
    先ほどの求職者情報抽出機能で出力した情報をコピペしてメッセージで送ってもらうイメージです。

    画像

    IF/ELSE
    全てのパラメータに値があるかを確認します!

    画像

    パラメータが不足している場合は、情報が不足している旨のメッセージを送信して終了します。

    画像

    回答

    画像

    コード実行
    全てのパラメータが揃っている場合、SalesforceのREST APIを使って、
    リードオブジェクトにレコードを作成します!

    画像

    コード内容はこちらです!
    ⚠️ 下記コードには、リードオブジェクトに作成したカスタム項目SelfPromotion__c、Skill__c、WorkExperience__cが含まれています。
    ご自身の環境に合わせて変更してご使用ください☺️

    import requests
    import json
    import os
    
    # --- メイン関数 ---
    def main(name: str, selfPromotion: str, skill: str, workExperience: str, CLIENT_ID: str, CLIENT_SECRET: str, TOKEN_URL: str) -> dict:
    
        sobject_api_name = "Lead" # SObject名を指定
    
        last_name_val = name
        company_val = name 
        selfPromotion_val = selfPromotion
        status_val = "Open - Not Contacted"
        skill_val = skill
        workExperience_val = workExperience
    
        # Salesforceアクセストークン取得時に、main関数の引数として受け取った接続情報を使用
        access_token, instance_url, token_error = _get_salesforce_access_token(
            CLIENT_ID, 
            CLIENT_SECRET, 
            TOKEN_URL
        )
        if token_error:
            current_result = {
                "status": "error",
                "message": f"Salesforceアクセストークン取得失敗: {token_error}",
                "salesforce_response": None
            }
            return {"result": json.dumps(current_result, ensure_ascii=False)}
    
        if not access_token or not instance_url: # instance_urlもチェック
            current_result = {
                "status": "error",
                "message": "SalesforceアクセストークンまたはインスタンスURLが取得できませんでした。",
                "salesforce_response": None
            }
            return {"result": json.dumps(current_result, ensure_ascii=False)}
            
        record_payload = {
            "LastName": last_name_val,
            "Company": company_val,
            "SelfPromotion__c": selfPromotion_val,
            "Skill__c": skill_val,
            "WorkExperience__c": workExperience_val,
        }
        if sobject_api_name == "Lead":
            record_payload["Status"] = status_val
        
        sf_response, creation_error = _create_salesforce_record(
            access_token, instance_url, sobject_api_name, record_payload
        )
    
        current_result_dict = {}
        if creation_error:
            current_result_dict = {
                "status": "error",
                "message": f"Salesforceへのレコード作成失敗 ({sobject_api_name}): {creation_error}",
                "salesforce_response": sf_response
            }
        elif sf_response and sf_response.get("success"):
            record_id = sf_response.get("id")
            record_url = None
            if record_id:
                # ユーザー指定のURL形式で構築
                # 注意: ドメイン部分は固定です。instance_url から動的に取得する方がより堅牢ですが、
                #       ユーザーの指定に合わせています。
                record_url = f"https://cunning-otter-muyevc-dev-ed.trailblaze.lightning.force.com/lightning/r/{sobject_api_name}/{record_id}/view"
            
            current_result_dict = {
                "status": "success",
                "message": f"Salesforceにレコードが正常に作成されました (SObject: {sobject_api_name})。",
                "salesforce_response": sf_response,
                "record_url": record_url # 生成したURLを追加
            }
        else:
            current_result_dict = {
                "status": "error",
                "message": f"Salesforceでのレコード作成処理中にエラーが発生しました (SObject: {sobject_api_name})。",
                "salesforce_response": sf_response if sf_response else {"errors": ["Unknown error during record creation."]}
            }
        
        return {"result": json.dumps(current_result_dict, ensure_ascii=False)}
    
    # --- ヘルパー関数 ---
    def _get_salesforce_access_token(CLIENT_ID: str, CLIENT_SECRET: str, TOKEN_URL: str) -> tuple[str | None, str | None, str | None]:
        payload = {
            'grant_type': 'client_credentials',
            'client_id': CLIENT_ID,
            'client_secret': CLIENT_SECRET
        }
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
        
        try:
            response = requests.post(TOKEN_URL, headers=headers, data=payload, timeout=10)
            response.raise_for_status()
            token_data = response.json()
            access_token = token_data.get('access_token')
            instance_url = token_data.get('instance_url')
            if not access_token or not instance_url:
                return None, None, f"エラー: アクセストークンまたはインスタンスURLをレスポンスから取得できませんでした。 レスポンス: {token_data}"
            return access_token, instance_url, None
        except requests.exceptions.HTTPError as http_err:
            return None, None, f"HTTPエラーが発生しました: {http_err} - レスポンスボディ: {getattr(http_err.response, 'text', 'N/A')}"
        except requests.exceptions.RequestException as req_err:
            return None, None, f"リクエストエラーが発生しました: {req_err}"
        except json.JSONDecodeError:
            return None, None, f"レスポンスのJSONデコードに失敗しました。レスポンス: {getattr(response, 'text', 'N/A')}"
    
    def _create_salesforce_record(access_token: str, instance_url: str, sobject_name: str, record_data: dict) -> tuple[dict | None, str | None]:
        api_endpoint = f"{instance_url}/services/data/v59.0/sobjects/{sobject_name}/"
        headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'}
        try:
            response = requests.post(api_endpoint, headers=headers, json=record_data, timeout=10)
            response.raise_for_status()
            return response.json(), None
        except requests.exceptions.HTTPError as http_err:
            error_msg = f"HTTPエラーが発生しました: {http_err} - レスポンスボディ: {getattr(http_err.response, 'text', 'N/A')}"
            try:
                error_details = http_err.response.json()
                error_msg += f" Salesforceからのエラー詳細: {json.dumps(error_details, ensure_ascii=False)}"
            except json.JSONDecodeError:
                pass
            return None, error_msg
        except requests.exceptions.RequestException as req_err:
            return None, f"リクエストエラーが発生しました: {req_err}"


    パラメータ抽出
    レコード作成はコード実行ノードで完了していますが、作成されたレコードのURLをチャットで回答させたいので、コード実行のresultからrecord_urlを取得します!

    画像

    回答
    下記のように設定して、作成したレコードURLを回答させます!

    画像

    おまけ(会話の開始 を編集)
    必要に応じて編集しておきます!

    これでSalesforceへの登録機能が完成です!

    実行🔍 Salesforceへの登録機能

    では動作確認をしてみます!
    プレビューから、「Salesforceへ登録して」という文言と一緒に、名前・自己PR・スキル・職務経歴を送信します!

    Salesforceへレコードを作成し、URLを返すことができました!
    今回はレコードの作成のみの機能にしていますが、外部キーを用意して更新機能も付けるとより実用的になりますね!

    ここまでのDMLファイルは、こちらからダウンロードいただけます!


    アプリケーションを公開

    最後に公開して、Dify外でも実行できるようにします!

    画像

    「アプリを実行」をクリックすると、下記のような画面からアプリを実行できます!URLを共有して使用することも可能です。

    画像

    ⚠️ 実際に運用する場合には、個人情報の取り扱いには十分ご注意ください

    まとめ

    今回は、PDFから情報の抽出+Salesforceへ連携するアプリケーションを作成してみました!
    DifyでLLMの自然言語処理能力と外部システムのAPI連携を組み合わせることで、従来は複雑なプログラミングが必要だった業務自動化が、驚くほど簡単に実現できるようになります!

    Difyに限った話ではないですが、重要なのは、完璧を求めすぎずに「まず動くものを作る」こと!そして実際に使いながら改善していくことです。

    皆さんも、ぜひご自身の業務に合わせてカスタマイズして、日々の作業を効率化してみてくださいね!

    SAMURAIでは、DifyやPythonについて学べるレッスンを提供しています。
    初心者でもわかりやすく、必要な知識を段階的に習得できるので、是非チェックしてみてください!


     
     
    累計指導実績5万名以上のプログラミングスクールSAMURAI ENGINEER(運営会社:株式会社オープンアップITエンジニア)が提供💻 業務に活かせる生成AI関連の情報をお届け💪 導入活用支援も行っております↓ https://www.sejuku.net/biz/

    あなたへのおすすめ