【Gemini×GAS】契約書をフォルダに入れるだけ!自動でリスクチェックして一覧化するAIシステムを作ってみた

Gemini
スポンサーリンク

みなさん、こんにちは!業務ハックLabのようです。

最近、寒くなってきましたね。 (布団から出るのが毎日戦いですw)

さて、今回は「AIを使った業務自動化」のネタを持ってきました!

これ、わけわからなくなりますよね…

みなさん、契約書のチェックってどうしてますか? 細かい条文を目を皿のようにして読んで、リスクがないか確認する……。

正直なところ、めんどくさいですよね?

もちろん、最終的には人間の目で見る必要がありますが、「まずはざっくりリスクが高い箇所だけ教えてほしい!」と思うこと、ありませんか?

ということで、今回は「フォルダに契約書を入れるだけで、自動でスプレッドシートにリスク分析結果が書き込まれ、ファイルが整理されるシステム」を作っていきたいと思います!

実は「Power Automate × Azure OpenAI × SharePoint構成版」の記事もあります

「あれ?このネタ見たことあるぞ?」と思った方、鋭いですねw 実は以前、Power Automate × Azure OpenAI × SharePoint という「企業向けガチ構成」で同じようなシステムを作る連載記事を書いています。

会社で本格的にセキュリティを固めて導入するならAzure版がおすすめなので、興味がある方はこちらもぜひチェックしてみてください。

しかし、記事を読んだ方の中にはこう思った方もいるかもしれません。

「うちはGoogle Workspaceを使っとるんじゃ!Power Automate × Azure OpenAI × SharePointは使えないんじゃあ!!」

そんな方向けに、今回は Google Workspace × Gemini でも同じ仕組み を考えてみました! これならGoogle派の皆さんも安心ですね。

今回作る仕組みの全体像

いきなりコードを書く前に、まずはどういう仕組みにするか、ロジックを整理しましょう。 ここをサボると、後でコードを書く時に迷子になりますからね。(自戒)

やりたいことはシンプルです。

  1. 入口: 特定のフォルダに契約書(PDFや画像)を入れる。
  2. 処理: GASがファイルを検知して、テキストを抜き出す。
  3. 頭脳: Gemini APIにテキストを投げて、「リスク分析して!」と頼む。
  4. 出口: 返ってきた結果をスプレッドシートに書き込み、ファイルを「処理済」フォルダへ移動する。

この流れをGoogle Apps Script (GAS) で実装していきます。

ステップ1:Googleドライブとスプレッドシートの準備

まずはデータの「入り口」と「出口」を作ります。

フォルダの作成

Googleドライブに、以下の2つのフォルダを作ってください。

  • 契約書_未処理 (ここが入口になります)
  • 契約書_処理済 (処理が終わったらここに移動させます)

今回は共有ドライブにこのフォルダを作成しました!

フォルダIDの取得

プログラムからフォルダを特定するために「ID」が必要です。 作成した「契約書_未処理」フォルダを開き、URLバーを見てみましょう。

drive.google.com/drive/folders/ の後ろにある、長い英数字の羅列。 これがフォルダIDです。これをコピーして、メモ帳などに控えておいてください。 (「契約書_処理済」のIDも同じように控えてくださいね!)

スプレッドシートの作成

次に、結果を書き出すためのスプレッドシートを新規作成します。 名前は 契約書レビュー管理表 としておきましょう。 こちらもURLの /d//edit の間にある文字列(スプレッドシートID)を控えておきます。

ステップ2:Gemini APIキーの取得

次は、AIの頭脳を使うためのパスポートを取得します。

  1. Google AI Studio にアクセスします。
  2. 左下の 「Get API key」 をクリック。
  3. 「API キーを作成」をクリック
  4. 「キー名の設定」にキーの名前を入力し
  5. 「インポートしたプロジェクトを選択」「プロジェクトを作成」 を選択しプロジェクト名を入力。
  6. 最後に「キーを作成」をクリック

画面に AIza から始まるキーが表示されるので、これをコピーして控えてください。

⚠️ 【超重要】実運用するなら「有償枠」への切り替えが必須!

ここで一つ、めちゃくちゃ大事な話をします。 (これを知らずに会社で使うと、情シスに怒られる案件ですw)

今回の手順で取得したAPIキーは、デフォルトだと「無料枠」の状態です。 個人の学習用ならこれで全く問題ないのですが、「会社の契約書」を扱う場合は、必ずGoogle Cloudプロジェクトと紐付けた「有償枠(Pay-as-you-go)」に切り替えてください。

理由は以下の2点です。

理由①:入力データがAIの学習に使われないようにするため

これが最大の理由です。 Googleの規約上、無料枠のデータは「モデルの改善(学習)」に使われる可能性があります。 つまり、あなたの会社の秘密保持契約書の内容が、巡り巡ってGemini賢くなるための餌になってしまうかもしれないのです。 有償枠に切り替えると、「入力データは学習に使われない」という規約が適用されるため、セキュリティ的に安心です。

理由②:APIの回数制限(レートリミット)を回避するため

無料枠は、1分間にリクエストできる回数に厳しい制限があります。 「月末に契約書を50件まとめて処理したい!」と思っても、無料枠だと途中で「回数制限オーバーです」とエラーが出て止まってしまう可能性があります。

【結論】 まずは無料枠で試してみて、「これは使える!」となったら、必ずGoogle Cloudの請求先アカウントを設定して、有償枠に切り替えてから運用してくださいね。

ステップ3:Google Apps Script (GAS) の設定

はい、ここからが本番です。

スクリプトエディタを開く

スプレッドシートのメニューから 「拡張機能」 > 「Apps Script」 をクリックします。

【重要】Drive API サービスの追加

ここ、一番のハマりポイントです! PDFから文字を読み取る機能を使うために、APIサービスを追加する必要があります。

  1. エディタ左側、「サービス」の横の 「+」 をクリック。
  2. 「Drive API」 を選択して 「追加」
  3. 【重要】バージョンは必ず「v2」を選択してください。 (今回のコードはv2の仕様で書かれているため、v3だと動きません!)
  4. 「追加」 をクリック。

コードの記述

以下のコードをコピーして貼り付けてください。
ちなみにMODEL_NAMEに関しては僕は「gemini-flash-latest」を使用しました。

// ==========================================
// ★設定エリア(ここだけ書き換えてください)
// ==========================================
const API_KEY = "ここに取得したAPIキーを貼り付け"; 
const FOLDER_ID_INPUT = "契約書_未処理のフォルダIDを貼り付け";
const FOLDER_ID_PROCESSED = "契約書_処理済のフォルダIDを貼り付け";
const SPREADSHEET_ID = "スプレッドシートIDを貼り付け"; 
const MODEL_NAME = "あなたの環境で動作確認済みのモデル名"; 

// ==========================================
// メイン処理
// ==========================================
function main() {
  const inputFolder = DriveApp.getFolderById(FOLDER_ID_INPUT);
  const processedFolder = DriveApp.getFolderById(FOLDER_ID_PROCESSED);
  
  // 1. ファイル処理開始
  processFiles_(inputFolder, processedFolder);

  // 2. 一時ファイルのお掃除
  Logger.log("フォルダの清掃を行っています...");
  cleanUpTempFiles_(inputFolder);
  
  Logger.log("すべての処理が完了しました。");
}

// ==========================================
// ファイル処理ループ
// ==========================================
function processFiles_(inputFolder, processedFolder) {
  const files = inputFolder.getFiles();
  
  while (files.hasNext()) {
    const file = files.next();
    const fileName = file.getName();
    
    // OCR用の一時ファイルなどはスキップ
    if (fileName.includes("temp_doc_for_text_extraction")) continue;

    Logger.log(`[処理開始] ${fileName}`);

    try {
      // ★ポイント1:PDFからテキストを引っこ抜く
      const text = extractTextFromFile_(file.getId());
      if (!text || text.length < 50) {
        writeToSheet_(fileName, "エラー(テキスト抽出不可)", []);
        Logger.log(`[スキップ] テキスト抽出不可: ${fileName}`);
        continue;
      }

      // ★ポイント2:Geminiに分析してもらう
      Logger.log("Geminiに分析を依頼中...");
      const rawResponse = callGemini_(text);
      // AIがたまにMarkdown記法(```json ... ```)を含めるので掃除する
      const cleanedJson = cleanJson_(rawResponse);
      
      let riskItems = [];
      try {
        riskItems = JSON.parse(cleanedJson);
      } catch (e) {
        writeToSheet_(fileName, "エラー(JSON解析失敗)", []);
        Logger.log("JSONパースエラー");
        continue;
      }

      // 結果の書き込み
      if (!riskItems || riskItems.length === 0) {
        writeToSheet_(fileName, "完了(リスクなし)", []);
      } else {
        writeToSheet_(fileName, "完了(リスクあり)", riskItems);
      }
      
      // 処理済フォルダへ移動
      file.moveTo(processedFolder);
      Logger.log(`[完了] 処理済フォルダへ移動しました`);
      
      // 連続実行でAPI制限にかからないよう少し休憩
      Utilities.sleep(2000); 

    } catch (e) {
      writeToSheet_(fileName, `システムエラー: ${e.toString()}`, []);
      Logger.log(`[エラー] ${e.toString()}`);
    }
  }
}

// ==========================================
// スプレッドシート書き込み(指定の列構成)
// ==========================================
function writeToSheet_(fileName, status, riskItems) {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getActiveSheet();
  
  // ヘッダー作成(初回のみ)
  if (sheet.getLastRow() === 0) {
    sheet.appendRow([
      "ID", "分析ステータス", "対象条項", "危険度", "リスク理由", "修正案", "処理日時"
    ]);
    sheet.setFrozenRows(1);
    sheet.getRange("1:1").setFontWeight("bold");
  }

  const timestamp = new Date();
  
  // リスクがない、またはエラーの場合
  if (!riskItems || riskItems.length === 0) {
    sheet.appendRow([fileName, status, "-", "-", "-", "-", timestamp]);
    return;
  }

  // リスク項目がある場合、行を分けて書き込み
  riskItems.forEach(item => {
    sheet.appendRow([
      fileName,
      status,
      item.clause || "",
      item.risk_level || "",
      item.reason || "",
      item.suggestion || "",
      timestamp
    ]);
  });
}

// ==========================================
// 共通機能(ここが裏側のキモ!)
// ==========================================

// ゴミ掃除関数
function cleanUpTempFiles_(folder) {
  const files = folder.getFiles();
  while (files.hasNext()) {
    const file = files.next();
    if (file.getName().includes("temp_doc_for_text_extraction")) {
      try { file.setTrashed(true); } catch (e) {}
    }
  }
}

// ★OCR処理の核心部
function extractTextFromFile_(fileId) {
  let tempFile;
  try {
    // Drive APIを使って、PDFをGoogleドキュメントとしてコピー(これでOCRがかかる!)
    const resource = { title: "temp_doc_for_text_extraction", mimeType: MimeType.GOOGLE_DOCS };
    const optionalArgs = {ocr: true, supportsTeamDrives: true, supportsAllDrives: true};
    tempFile = Drive.Files.copy(resource, fileId, optionalArgs);
    
    // ドキュメントを開いてテキストを取得
    const doc = DocumentApp.openById(tempFile.id);
    const text = doc.getBody().getText();
    return text;
  } catch (e) { return null; } finally {
    // 終わったら一時ファイルはゴミ箱へ(これ大事)
    if (tempFile) {
      try { Drive.Files.patch({trashed: true}, tempFile.id, {supportsTeamDrives: true, supportsAllDrives: true}); } catch (e) {}
    }
  }
}

// Gemini呼び出し関数
function callGemini_(text) {
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL_NAME}:generateContent?key=${API_KEY}`;
  
  // プロンプト:ここで「どう振る舞うか」を定義
  const systemPrompt = `
あなたは優秀な企業法務の専門家です。
ユーザーから提供される契約書のテキストをレビューし、自社(契約を受ける側)にとって不利な条項やリスクのある条項を抽出してください。
以下の4点を特に注意してチェックしてください:
1. 損害賠償の範囲と上限
2. 契約の解除条件
3. 知的財産権の帰属
4. 支払条件と遅延損害金
出力は以下のJSON形式のみで行ってください。
[{"clause":"条項名","risk_level":"高/中/低","reason":"理由","suggestion":"修正案"}]
`;

  const payload = {
    "contents": [{"parts": [{"text": systemPrompt}, {"text": `契約書内容:\n${text}`}]}],
    // レスポンスをJSONに固定する設定
    "generationConfig": {"response_mime_type": "application/json"}
  };
  
  const options = { "method": "post", "contentType": "application/json", "payload": JSON.stringify(payload), "muteHttpExceptions": true };
  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());
  
  if (json.error) throw new Error(`Gemini API Error: ${json.error.message}`);
  return json.candidates[0].content.parts[0].text;
}

function cleanJson_(text) { return text.replace(/```json/g, "").replace(/```/g, "").trim(); }

設定値の入力

コード冒頭の API_KEY などを、ステップ1・2で控えた値に書き換えてください。

💻 コードの詳細解説:何をしているの?

「コピペで動くのはわかったけど、中身はどうなってるの?」 という勉強熱心な方のために、今回のコードのキモとなる部分を解説します!

① 目を作る:PDFから文字を読む裏技

実はGAS標準の機能には「PDFの文字を読む」機能はありません。 そこで、このコードではDrive APIの裏技を使っています。

const resource = { mimeType: MimeType.GOOGLE_DOCS };
const optionalArgs = {ocr: true};
tempFile = Drive.Files.copy(resource, fileId, optionalArgs);

これは、「PDFファイルをGoogleドキュメントとしてコピーを作成する」という処理なんですが、その時に ocr: true(光学文字認識をオン)にすることで、コピーされたドキュメントの中身がテキスト化されるんです。 このテキストを取得して、最後に用済みになったドキュメントをゴミ箱に捨てています。 (これをしないと、フォルダが謎のドキュメントだらけになりますw)

② 脳を作る:Geminiへの指示出し

AIを使う上で一番大事なのが「プロンプト」と「出力形式」です。

const systemPrompt = `
...
出力は以下のJSON形式のみで行ってください。
[{"clause":"条項名","risk_level":"高/中/低" ... }]
`;

ここで「法務の専門家になりきって」と指示しつつ、「JSON形式以外は認めない」という強い制約をかけています。 さらに、APIのリクエスト設定でも:

"generationConfig": {"response_mime_type": "application/json"}

と指定することで、Gemini側にも「プログラムが読みやすい形式で返してね」と念押ししています。これにより、GAS側でのデータ処理(JSON.parse)がエラーになりにくくなるんです。

③ 手を動かす:結果の書き込み

riskItems.forEach(item => {
  sheet.appendRow([ ... ]);
});

Geminiから返ってきたリスク一覧(配列データ)を、forEach で一つずつ取り出して、スプレッドシートの行に追加しています。 こうすることで、「一つの契約書にリスクが3つあったら、3行分追加する」という柔軟な動きを実現しています。

ステップ4:自動実行(トリガー)の設定

コードの理解が深まったところで、最後に自動実行の設定をしましょう。

  1. エディタ画面左側の 「時計のマーク(トリガー)」 をクリック。
  2. 右下の 「+ トリガーを追加」 をクリック。
  3. 以下のように設定して「保存」します。
    • 実行する関数: main
    • イベントのソース: 時間主導型
    • 時間ベースのトリガーのタイプ: 分ベースのタイマー
    • 時間の間隔: 1分おき(※テスト用。本番なら10分おきとかでもOK)

⚠️ トリガー設定の注意点(やりすぎ注意!)

今回はテストなのですぐに動いてほしいから「1分おき」にしていますが、これをずっと動かし続けると危険です!

  • GASの実行時間制限: 1日の総実行時間(無料版だと90分/日)を超えてしまい、止まる可能性があります。
  • APIのスロットリング: 短時間に何度もアクセスしすぎると、Gemini API側から「落ち着け!」と怒られてエラーになることがあります。

本番運用する時は、「10分おき」「1時間おき」など、業務のペースに合わせて余裕を持った設定にすることをおすすめします。

完成!使い方の確認

さあ、動かしてみましょう!

「契約書_未処理」フォルダに、PDFファイル(または画像)をドラッグ&ドロップでポイッと入れます。 (今回はテストなので、ダミーの契約書などを入れてみてくださいね)

〜1分後〜

スプレッドシートを開いてみましょう。

…どうですか?

リスク分析結果が自動的に行として追加されていませんか? そして、「契約書_未処理」フォルダを見ると空になっていて、ファイルは「契約書_処理済」フォルダに移動されているはずです。

最後に

はい、いかがでしたでしょうか?

今回はGeminiとGASを組み合わせて、「契約書レビューの自動化」に挑戦してみました。 コードの解説も挟みましたが、この仕組みを応用すれば「請求書の読み取り」や「アンケートの自動集計」なんかもできちゃいます。

ぜひ、皆さんの業務に合わせてカスタマイズしてみてください。

それでは皆さん、良い業務ハックライフを~

コメント