見出し画像

Gemini × GASの最終形態!?1つのスプレッドシートで全タスクを操る方法【コード公開!】

こんにちは。Edocです。
note公式の投稿コンテストがあったので初めて参加してみました!

開発経緯

スプレッドシートで扱っているテキストをAIに投げて、要約したり分類したり──。
そんな作業をしていると、毎回こう思っていました。

「いちいちコピペしてGeminiに貼り付けるの、面倒すぎる…!」

特に、大量の行データを扱う時は手間が何倍にも膨れます。
「じゃあ、スプレッドシート上から直接Geminiを呼び出せるようにすればいいじゃないか」と思い立ったのが開発のきっかけです。


GASとは?

Google Apps Script(GAS)は、Googleスプレッドシートやドキュメント、GmailなどGoogleサービスを自動化・拡張できるスクリプト言語です。
Excelのマクロ(VBA)のGoogle版のようなもの、と言えばイメージしやすいでしょう。


Geminiとの統合の親和性

GASとGemini(Googleの生成AI)を組み合わせるメリットは大きく3つあります。

  1. Google製サービス同士のスムーズな連携
    スプレッドシート内のデータをGAS経由で簡単に取得でき、そのままGemini APIに渡せます。

  2. インターフェース不要
    Webアプリやフォームをわざわざ作らなくても、スプレッドシート上のボタンやメニューから直接実行できます。

  3. 柔軟なタスク切り替え
    タスク内容をセルで指定するだけで、要約・分類・翻訳など、複数種類の処理を一つの仕組みで回せます。


実装内容

今回のシート構成は以下の通りです。

  • content列:AIに渡したい元データ

  • task列:Geminiに実行してほしいタスク(例:「要約」「分類」)

  • target列:実行対象かどうかを指定

  • result列:Geminiの実行結果を出力

さらに、GASで「Gemini操作」マクロボタンを作り、クリックすると
task列とcontent列、target列をセットでGeminiに送り、result列に結果を自動で書き込みます。


試してみた結果

横に長いため、見づらくてすみません…

実行前
作成したマクロ

resultに結果が格納!
  • 要約タスク:長文もスッキリと簡潔にまとめてくれた。

  • 分類タスク:ユーザーコメント(サンプル)の感情分類ができた。

  • 翻訳タスク:和訳も問題なくできた。

シートから直接Geminiを操作できるのは想像以上に快適で、日常の小さなタスクを一瞬で片付けられるようになりました。


活用例

  • マーケティング:SNS投稿案の要約・タグ付け

  • 業務改善:問い合わせ内容の分類・テンプレ自動提案

  • 学習支援:長文資料の要約、難解な文の簡易化

  • 多言語対応:文章を一括翻訳して海外チームと共有

「一つの機能だけ」ではなく、task列を書き換えるだけで用途が変わるのが魅力です。


失敗談

最初はセルに直接 =Gemini_Call(args) と書く関数型の方式にしましたが、これが大失敗。
改行やセル編集のたびにAPIが走り、利用料金が加速度的に増加…。
心臓に悪かったので、今回の「ボタンクリック方式」に切り替えました。


まとめ

GASとGeminiを組み合わせれば、スプレッドシートが「AIタスク実行マシン」に早変わりします。
普段の業務で「AIに頼みたい作業」があるなら、わざわざブラウザや別ツールを開かずに、手元のシートからポチッと実行できる仕組みを作ってみると、作業効率は確実に上がります。

参考)実装コード

今回紹介したGASの実装コードです!
スプレッドシートを立ち上げて"拡張機能"→"Apps Script"に、
以下記載のコードを貼り付けてください。

スプレッドシートは1行目を列名、2行目以降から実行したい内容を記載すれば、記載コードのまま使用可能となります!

APIキーについて

GeminiAPIは自分で取得したAPIkeyをGASのスクリプトプロパティに登録して使ってください。
■スクリプトプロパティ
 プロパティ:GEMINI_API_KEY
 値:皆さんが取得したAPIkey

※APIは絶対に公開してはいけないので、上記スクリプトプロパティで実行してください!!!

お願い

※無断での転載はご遠慮いただきますようお願いいたします。
※GeminiAPIは設定によっては無料利用枠を超えると従量課金に移行します。コードの実行や利用によって生じたいかなる問題についても、当方は一切の責任を負いかねます。 ご自身の責任においてご利用ください。m(_ _)m

可変推奨パラメータ

1.モデル
今回は簡易タスクだったため安価なgemini-1.5-flashを利用しました。必要に応じてモデルを変更してください。

const model = 'gemini-1.5-flash';

↓モデルリスト

2.出力トークン数
test用のため出力トークン数を50に制限しています。
必要に応じて変更してください。

"maxOutputTokens": 50

コード

#JavaScript
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Gemini操作')
    .addItem('タスクを実行する', 'executeTasks')
    .addToUi();
}

function executeTasks() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRow = sheet.getLastRow();
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  const ui = SpreadsheetApp.getUi();

  if (!apiKey) {
    ui.alert('GEMINI_API_KEYが設定されていません。');
    return;
  }

  // 列の指定(必要に応じて変えてください)
  const taskCol = 2;     // B列 タスク
  const targetCol = 3;   // C列 実行対象セルアドレス
  const resultCol = 4;   // D列 結果

  const model = 'gemini-1.5-flash';
  const endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' + model + ':generateContent?key=' + apiKey;

  let processedCount = 0;

  for (let row = 2; row <= lastRow; row++) { // 1行目はヘッダー想定
    const prompt = sheet.getRange(row, taskCol).getValue();
    const targetCellAddress = sheet.getRange(row, targetCol).getValue();

    if (!prompt || !targetCellAddress) {
      // どちらか空ならスキップ
      continue;
    }

    let inputText = '';
    try {
      inputText = sheet.getRange(targetCellAddress).getValue();
    } catch (e) {
      sheet.getRange(row, resultCol).setValue('対象セル指定エラー');
      continue;
    }

    const fullPrompt = prompt + ":" + inputText;

    const requestBody = {
      "contents": [
        {
          "parts": [
            {"text": fullPrompt}
          ]
        }
      ],
      "generationConfig": {
        "maxOutputTokens": 50,
        "temperature": 1.0
      }
    };

    const options = {
      'method': 'post',
      'contentType': 'application/json',
      'payload': JSON.stringify(requestBody),
      'muteHttpExceptions': true
    };

    try {
      const response = UrlFetchApp.fetch(endpoint, options);
      const code = response.getResponseCode();
      const text = response.getContentText();

      if (code < 200 || code >= 300) {
        sheet.getRange(row, resultCol).setValue("APIエラー: HTTP " + code);
        continue;
      }

      const result = JSON.parse(text);
      let out = "";
      if (result.candidates && result.candidates.length > 0) {
        const parts = result.candidates[0].content && result.candidates[0].content.parts;
        if (parts && parts.length > 0) {
          out = parts.map(p => p.text || "").join("");
        }
      }

      sheet.getRange(row, resultCol).setValue(out.trim());
      processedCount++;

      // API負荷軽減のために少し待つ(必要に応じて調整)
      Utilities.sleep(300);

    } catch (e) {
      sheet.getRange(row, resultCol).setValue("例外: " + e.message);
    }
  }

  ui.alert('処理完了。' + processedCount + ' 件実行されました。');
}


いいなと思ったら応援しよう!