🦆

【GAS】タグの解説情報作成を自動化したい

に公開

なぜ作成したのか

  • 情報分類のためにタグ付けして情報ストックするようにしたらタグが無限増殖をはじめたので、解説文をイチイチまとめるのが面倒になってきたので、タグと親タグから解説文をGeminiくんに作ってもらおうという試み

やりたいこと

  • タグ名称に対して、解説文未設定のレコードに対して、IT企業の情シス担当者が抑えておくべき解説文章を自動作成する

既存の環境

シート「db」

column description
feed_id RSSFeed取得元情報源を示すID(別シートのプライマリKey)
title RSSFeedで取得した記事タイトル
timestamp RSSFeedから取得した公開日
link RSSFeedから取得した記事URL
content RSSFeedから取得した記事概要
create_date 取得処理を実行した日
exclude 表示除外フラグ
bookmark ブックマークフラグ

シート「tags」

column description
id タグを一意に識別するID
tag タグ文字列
description タグの解説文
parent-tag 親子関係にある親タグのID(同一シート内IDを参照)

シート「tagRelations」

column description
id タグと記事の関連性を識別する一意のID
tag tagsのidを参照
db dbのlinkを参照

処理としてやりたいことの記述

  • 処理対象とするスコープ

    • シート「tags」のうち、descriptionに値が設定されていないレコード
  • 処理概要

    • 処理はGASで実装する
    • 対象レコードのtag、設定されている場合は文脈の方向付けとしてparent-tagの文字列を基に、tagに対する解説文を生成して、descriptionにセットする。
      • 解説文の生成には gemini-2.0-flash APIを使用する
    • 処理がタイムアウトにより中断されることを考慮し、繰り返し実行に耐えられるよう処理の一貫性を担保する

GAS


/**
 * ──────────────────────────────────────────────
 *   tags.description が空の行に Gemini で解説文を生成して入力
 *   - Google AI Gemini 2.0 Flash を REST API 経由で呼び出し
 *   - 1 実行あたり GM_BATCH_SIZE 行だけ処理してタイムアウトを回避
 *   - 処理進捗は ScriptProperties に保存(idempotent)
 * ──────────────────────────────────────────────
 */
const SHEET_TAGS        = 'tags';
const API_KEY_PROP      = 'GEMINI_API_KEY';       // スクリプトプロパティ名
const PROGRESS_PROP     = 'tagdesc_last_row';
const MODEL_URL         = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
const GM_BATCH_SIZE        = 15;                     // 1 実行で処理する行数
const GM_TIMEOUT_MS        = 20 * 1000;              // 1 リクエスト最大待機 20 秒

/**
 * メイントリガー
 */
function fillTagDescriptions() {
  const ss       = SpreadsheetApp.getActiveSpreadsheet();
  const tagSh    = ss.getSheetByName(SHEET_TAGS);
  if (!tagSh) throw new Error(`シート「${SHEET_TAGS}」がありません`);

  const apiKey = PropertiesService.getScriptProperties().getProperty(API_KEY_PROP);
  if (!apiKey) throw new Error(`スクリプトプロパティ「${API_KEY_PROP}」に Gemini API キーを設定してください`);

  // --- シート全行読み出し ---
  const data      = tagSh.getDataRange().getValues();  // 0:ヘッダー行
  const header    = data.shift();
  const COL_ID    = header.indexOf('id');
  const COL_TAG   = header.indexOf('tag');
  const COL_DESC  = header.indexOf('description');
  const COL_PARENT= header.indexOf('parent-tag');

  // --- parent-tag 用マップ作成(id → tag 文字列)---
  const id2tag = new Map(data.map(r => [String(r[COL_ID]), String(r[COL_TAG])]));

  // --- 進捗読み込み ---
  const prop      = PropertiesService.getScriptProperties();
  let   startRow  = Math.max(Number(prop.getProperty(PROGRESS_PROP) || 2), 2); // 2 行目から
  if (startRow > data.length + 1) startRow = 2;                                // 終了したらリセット
  const endRow    = Math.min(startRow + GM_BATCH_SIZE - 1, data.length + 1);

  const updates = [];   // {row, text}

  for (let i = startRow; i <= endRow; i++) {
    const idx   = i - 2;                // data 配列のインデックス
    const row   = data[idx];
    if (row[COL_DESC]) continue;        // 既に説明あり

    const tagStr      = String(row[COL_TAG]);
    const parentId    = String(row[COL_PARENT] || '');
    const parentName  = id2tag.get(parentId) || '';

    try {
      const prompt = buildPrompt(tagStr, parentName);
      const desc   = callGemini(apiKey, prompt);
      if (desc) updates.push({row: i, text: desc});
    } catch (e) {
      console.error(`Gemini 生成失敗 (row ${i}): ${e}`);
    }
  }

  // --- シート更新(まとめて setValue)---
  for (const {row, text} of updates) {
    tagSh.getRange(row, COL_DESC + 1).setValue(text);
  }

  // --- 進捗保存 ---
  const nextStart = (endRow >= data.length + 1) ? 2 : endRow + 1;
  prop.setProperty(PROGRESS_PROP, String(nextStart));

  console.log(`Rows ${startRow}-${endRow} をチェック。生成 ${updates.length} 行。次回開始行: ${nextStart}`);
}

/**
 * 解説文生成用のプロンプト
 * - IT 企業の情シス担当者が読者
 * - 2〜3 文で簡潔に
 */
function buildPrompt(tag, parent) {
  const parentPart = parent ? `(上位概念: ${parent}` : '';
  return `あなたは IT 企業の情シス担当者向けに、技術用語を簡潔に説明する専門家です。
次のタグに対して 2〜3 文の日本語解説を書いてください。専門用語はかみ砕き、要点のみまとめてください。タイトルや前置きを含めず、解説文のみを回答してください。
タグ: 「${tag}${parentPart}`;
}

/**
 * Gemini 2.0 Flash API 呼び出し
 * 返値: 生成テキスト or ''(失敗時)
 */
function callGemini(apiKey, prompt) {
  const payload = {
    contents: [{parts: [{text: prompt}]}],
    generationConfig: {maxOutputTokens: 120, temperature: 0.3}
  };
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
    followRedirects: true,
    escaping: false,
    deadline: GM_TIMEOUT_MS / 1000
  };
  const url   = `${MODEL_URL}?key=${encodeURIComponent(apiKey)}`;
  const resp  = UrlFetchApp.fetch(url, options);
  const code  = resp.getResponseCode();
  if (code !== 200) throw new Error(`Gemini API error ${code}: ${resp.getContentText()}`);

  const json  = JSON.parse(resp.getContentText());
  return json?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || '';
}

所感

  • 処理は10秒くらいで完了してるのでタイムアウトは今のところ大丈夫そう
  • 初期データはひとまずデータセットできたけれど、内部の進捗変数も先頭に戻る
  • この処理をどのような頻度で起動するか?
  • タグは自動生成ではなく手動追加だし、行追加トリガーで起動しつつ、情報更新を意識して毎時トリガーを仕込んでおくかなあ

GitHubで編集を提案

Discussion