対カルト戦-回想録
対カルト戦-回想録

AIを作成する(余談4:エージェント風)

【用意するもの】

Google Apps Script(GAS)

Google AI Studio(推論AI用APIキー)

Google Driveフォルダ

Googleスプレッドシート(PC使用)

Gemini

メモ帳



PCでGoogleスプレッドシートを開いて、上部のファイルや編集や表示の並びにある「拡張機能」を選択して「Apps Script」を表示させる

↓

画面最上部右上などに「Geminiに相談」があれば、GeminiにGASコードを書くように頼んで、GASコードをコピー&ペーストする

↓

丸見えにならないようにする為にGASの左にある設定マークからスクリプトプロパティに次を追加登録する

  • スプレッドシートID
  • DriveフォルダID
  • 推論AI用APIキー

プロパティ

GASコードにある同じ文字列

スプレッドシートやDriveフォルダごとにリンクをコピーしてメモ帳にペーストすると、スプレッドシートIDやDriveフォルダIDがわかる

  • スプレッドシートなら間の文字列がスプレッドシートID
  • DriveフォルダならURLの後方の文字列がDriveフォルダID
  • GASコードにある推論AIの文字列

値

IDやAPIキーの文字列

↓

GASの左にある設定から次の項目にチェックを入れる

「appsscript.json」マニフェスト ファイルをエディタで表示する

↓

GASコードの画面と左の設定マークの間にある+ファイルの項目にappsscript.jsonが表示されるので、appsscript.jsonをGeminiに書くように頼んでコピー&ペーストする

↓

GASのデプロイの横をonEditにする

↓

連動させる為にGASの左にある時計マークから次を選択する

トリガーを追加をタッチしてから

実行する関数

onEdit

実行するデプロイ

Head

イベントのソース

フォームから

イベントの種類

編集時、フォーム送信時

↓

スプレッドシートで入力と出力を行なう為に、登録したスプレッドシートを開く

A1のセルに質問

B1のセルにAI回答

と入力してA2に質問を入力して「改行」すると、A2にAIが回答するか確認する

  • GASで実行してもエラーが表示される
  • スプレッドシートの改行で実行する

↓

スプレッドシートの下にタブがあるので、タブ1とタブ2で関連付けることができる

↓

エージェント風(自立型)になる


例:スプレッドシートのタブ1をAI対話、タブ2を理論株価計算とした場合

タブ1 質問を入力して改行する

タブ2 独自理論株価の計算式や GOOGLEFINANCE 関数、各種指標・算出結果の一覧を配置。AIが参照するための「データハブ」として機能させる。

↓

質問を入力後に改行して、AI回答にエラーっぽく表示されたらAI回答も改行する


/**
 * スプレッドシートが編集されたときに自動で発動する関数
 */
function onEdit(e) {
  if (!e) return;

  const sheet = e.source.getActiveSheet();
  const range = e.range;
  const row = range.getRow();
  const col = range.getColumn();

  // A列(1列目)の2行目以降に文字が入力された場合のみ実行
  if (col === 1 && row >= 2 && range.getValue() !== "") {
    const userQuery = range.getValue();
    
    // 処理中の仮表示をB列(2列目)に出力
    sheet.getRange(row, 2).setValue("AI思考中...");

    try {
      const scriptProperties = PropertiesService.getScriptProperties();
      const apiKey = scriptProperties.getProperty('GEMINI_API_KEY');
      const spreadsheetId = scriptProperties.getProperty('DATA_SPREADSHEET_ID') || e.source.getId();
      const folderId = scriptProperties.getProperty('TARGET_DRIVE_FOLDER_ID');

      if (!apiKey) {
        throw new Error("スクリプトプロパティに 'GEMINI_API_KEY' が設定されていません。");
      }

      // メインのAIワークフローを実行
      const finalAnswer = mainAIWorkflowCore(userQuery, apiKey, spreadsheetId, folderId);

      // B列(2列目)に最終回答を書き込む
      sheet.getRange(row, 2).setValue(finalAnswer);

    } catch (error) {
      sheet.getRange(row, 2).setValue("エラー: " + error.message);
    }
  }
}

/**
 * AIの処理をまとめたコア関数
 */
function mainAIWorkflowCore(userQuery, apiKey, spreadsheetId, folderId) {
  // ステップ1: セレクター&リスター(データベースとドライブから関連データを抽出)
  const workingMemory = retrieveAndFilterData_(userQuery, spreadsheetId, folderId);

  // ステップ2: ゼネラリストAIによる方向性決定
  const generalPlan = callGeminiFlash_(
    apiKey, 
    "あなたは全体を統括するゼネラリストです。以下の作業台(ワーキングメモリ)のデータに基づき、今回の質問に対するアプローチを整理してください。", 
    `質問: ${userQuery}\n\n作業台データ: ${workingMemory}`
  );

  // ステップ3: 並列・スペシャリストAIによる多角的検証
  const expertDataResult = callGeminiFlash_(
    apiKey, 
    "あなたはデータ分析のスペシャリストです。数字や事実関係に矛盾がないか検証してください。", 
    `計画: ${generalPlan}\nデータ: ${workingMemory}`
  );

  const expertActionResult = callGeminiFlash_(
    apiKey, 
    "あなたは構成・実用化のスペシャリストです。実行手順や実用性の観点から検証してください。", 
    `計画: ${generalPlan}\nデータ: ${workingMemory}`
  );

  // ステップ4: 最終統合
  const finalAnswer = callGeminiFlash_(
    apiKey, 
    "あなたは最高責任者のAIです。これまでの専門家の意見を統合し、お世辞や前置きなしで、結論から分かりやすく箇条書きで回答してください。", 
    `質問: ${userQuery}\nスペシャリストAの意見: ${expertDataResult}\nスペシャリストBの意見: ${expertActionResult}`
  );

  return finalAnswer;
}

/**
 * データ抽出・中間メモリ構築関数(リスター&セレクター)
 */
function retrieveAndFilterData_(userQuery, spreadsheetId, folderId) {
  let matchedData = [];

  try {
    const sheet = SpreadsheetApp.openById(spreadsheetId).getSheets()[0];
    const rows = sheet.getDataRange().getValues();
    for (let i = 1; i < rows.length; i++) {
      let rowText = rows[i].join(" ");
      if (userQuery.split(" ").some(keyword => keyword !== "" && rowText.includes(keyword))) {
        matchedData.push(rowText);
      }
    }
  } catch (e) {
    Logger.log('スプレッドシート読み込みエラー: ' + e.message);
  }

  if (folderId) {
    try {
      const folder = DriveApp.getFolderById(folderId);
      const files = folder.getFiles();
      while (files.hasNext()) {
        let file = files.next();
        if (file.getMimeType() === MimeType.GOOGLE_DOCS) {
          let content = DocumentApp.openById(file.getId()).getBody().getText();
          if (userQuery.split(" ").some(keyword => keyword !== "" && content.includes(keyword))) {
            matchedData.push(`[Driveファイル: ${file.getName()}] ${content.substring(0, 500)}...`);
          }
        }
      }
    } catch (e) {
      Logger.log('ドライブ読み込みエラー: ' + e.message);
    }
  }

  if (matchedData.length === 0) {
    matchedData.push("※直接一致する特記事項なし。一般知識ベースで推論します。");
  }

  return matchedData.join("\n---\n");
}

/**
 * Gemini Flash API呼び出し共通関数
 */
function callGeminiFlash_(apiKey, systemPrompt, userContent) {
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`;
  
  const payload = {
    contents: [
      {
        role: "user",
        parts: [{ text: systemPrompt + "\n\n" + userContent }]
      }
    ]
  };

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

  try {
    const response = UrlFetchApp.fetch(url, options);
    const json = JSON.parse(response.getContentText());
    if (json.candidates && json.candidates[0].content) {
      return json.candidates[0].content.parts[0].text;
    } else {
      return "AIからの応答を取得できませんでした。";
    }
  } catch (e) {
    return "API通信エラー: " + e.message;
  }
}