AIを活用したGoogleスプレッドシートの関数整合性チェック

## はじめに
業務の中で、Excelやスプレッドシートでレポートなどを作成することがあると思います。その際、関数を組んで集計を自動化することはよくあるかと思いますが、行や列をずらしたときに参照がずれて集計結果がおかしくなってしまうという経験をされた方は多いのではないでしょうか。

弊社でも同様の課題があり、最近生成AIを活用して整合性チェックを行う試みをしたので紹介します。

## 前提
Google Apps Script（以降GAS）は特定のスプレッドシートに紐づく[コンテナバインド型](https://developers.google.com/apps-script/guides/bound?hl=ja)ではなく、[スタンドアロン型](https://developers.google.com/apps-script/guides/standalone?hl=ja)でデプロイしています。

また、今回はOpenAIのo4-miniをAPIで使用しています。APIキーは`OPENAI_API_KEY`というプロパティ名で、[スクリプトプロパティ](https://developers.google.com/apps-script/guides/properties?hl=ja#manage_script_properties_manually)に登録してください。

## 実装
### サンプルコード
以下のファイルを作成します。
- main.gs
- const.gs
    - 定数をまとめたもの
- spreadsheet.gs
    - スプレッドシートからデータを取得する処理をまとめたもの
- submitAI.gs
    - AI送信処理をまとめたもの


:::note warn
実際にはGASのWebアプリケーションとして開発していますが、本記事ではフロント側の解説はせずバックエンド側のみ紹介します。
:::

完成版のコードは以下を展開してご覧ください。
<details><summary>main.gs</summary>

```js:main.gs
// スプレッドシートからデータを取得してリクエストを投げる処理
function main() {
  const response = submitOpenAi(SHEET_URL, SHEET_NAMES);
  console.log(response.summary);
}

```
</details>

<details><summary>const.gs</summary>

```js:const.gs
const OPENAI_URL = 'https://api.openai.com/v1/chat/completions';
const OPENAI_MODEL = 'o4-mini';
const SHEET_URL = 'xxx';
const SHEET_NAMES = [
  'シート1',
  'シート2'
];
const PROMPT_OPENAI = `
## 前提
あなたはGoogleスプレッドシートのプロフェッショナルです。
スプレッドシートの全てのセルを詳細にチェックすることができます。

## 対応事項
以下のスプレッドシートの内容をもとに、関数の整合性・設定漏れをチェックして下さい。
加えて、項目の意味から値や関数が合っているかチェックをお願いします。例えば週単位で集計しているものについては、その範囲が適切に設定されているか等、意味まで踏まえて確認して下さい。
シートが複数ある場合は、関連するシート間でもデータの整合性があっているか確認して下さい。

問題がない場合は「問題なし！」と返して下さい。問題がある場合は、そのセル番号と内容を端的に教えて下さい。

## 補足
取得しているデータはGoogle Apps ScriptのgetDisplayValues,getFormulas関数を使用して取得しております。
セルに関数が設定されている場合は関数を、そうでない場合は値を取得しています。

データは次のようなJSON形式で渡されます。
{
  "シート名": [["データ（2次元配列）"]]
}
`;
```
</details>

<details><summary>spreadsheet.gs</summary>

```js:spreadsheet.gs
function getSheetData(url, sheetNames) {
    const data = {}; // シート名をキーにしたデータのオブジェクト
    const ranges = {};
    const spreadsheet = SpreadsheetApp.openByUrl(url);
    sheetNames.forEach(sheetName => {
        const sheet = spreadsheet.getSheetByName(sheetName);
        if (sheet) {
            const [rangeText, values] = getValuesOrFormulas(sheet.getDataRange());
            data[sheetName] = values; // シート名をキーにしてデータを格納
            ranges[sheetName] = rangeText;
        }
    });
    return [JSON.stringify(ranges), JSON.stringify(data)];
}

function getValuesOrFormulas(range) {
    let valueOrFormulas = range.getDisplayValues();
    let tempFormulas = range.getFormulas();
    const rangeText = range.getA1Notation();
    for (let column = 0; column < valueOrFormulas[0].length; column++) {
        for (let row = 0; row < valueOrFormulas.length; row++) {
            if (tempFormulas[row][column].length !== 0) {
                valueOrFormulas[row][column] = tempFormulas[row][column];
            }
        }
    }
    return [rangeText, valueOrFormulas];
}

```
</details>

<details><summary>submitAI.gs</summary>

```js:submitAI.gs
function submitOpenAi(url, selectedSheets) {
    const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');
    const backQuote = "```";
    const [ranges, spreadsheetData] = getSheetData(url, selectedSheets);
    const submitPrompt = `
  ${PROMPT_OPENAI}

  なお、取得範囲は以下の通りです。
  ${ranges}

  ## データ
  ${backQuote}json
  ${spreadsheetData}
  ${backQuote}
  `;
    const payload = {
        'model': OPENAI_MODEL,
        'messages': [
            {
                'role': 'user',
                'content': submitPrompt
            }
        ]
    };
    const response = UrlFetchApp.fetch(OPENAI_URL, {
        method: 'post',
        contentType: 'application/json',
        headers: { 'Authorization': `Bearer ${OPENAI_API_KEY}` },
        payload: JSON.stringify(payload)
    });
    const responseBody = response.getContentText();
    const data = JSON.parse(responseBody);
    const responseText = data.choices[0].message.content;
    return { summary: responseText };
}
```
</details>

### 解説
- const.gs
  このファイルで定数を管理しています。プログラム実行前に`SHEET_URL`と`SHEET_NAMES`に対象のスプレッドシートURL、使用したいシート名を指定してからmain.gsのmain関数を実行してください。

- spreadsheet.gs
  スプレッドシートに関連する処理をまとめています。今回は主にスプレッドシートの**関数**の整合性チェックに重きを置いているため、関数が設定されている箇所については**関数をそのまま**取得するために`getValuesOrFormulas`関数を作成しています。
  データの取得はGASで用意されている関数を組み合わせて使用していますが、その都合で取得したデータは二次元配列となるので、JSON形式でシート名と紐づけてプロンプトに埋め込んでいます。
  
- submitAI.gs
  スプレッドシートのデータと合わせてプロンプトを生成し、OpenAIのAPIを叩いている処理です。どのようなプロンプトを使用しているかは`const.gs`でご確認いただければと思いますが、工夫している点としては`spreadsheet.gs`の`getSheetData`関数でシートからデータを取得した範囲（"A1:Z10"のような形式）を取得してプロンプトに渡していることです。
  プロンプトにデータを載せる際にはJSON形式で貼り付けているのですが、その状態だと正確なセル番号が分からないため、取得した範囲も一緒に渡すことで回答時にどのセルに問題があるかを指摘しやすくなります。

## 実行結果
整合性に問題がなければ、以下画像のように「問題なし！」と返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2598043/b5fa7bb1-381f-4a49-a567-24d1705bb210.png)

関数に誤りがある場合、以下画像のようにどのセルにどのような問題があるかを回答してくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2598043/433353d0-4319-4fcf-b18d-f2cd30ad1007.png)

目視では気付きづらい箇所も見つけてくれますし、周辺のテキストからどのような関数が入るべきかの意味を考えて回答してくれるのでダブルチェックで大きな威力を発揮すると思います。

## まとめ
今回の記事では、生成AIを活用したスプレッドシートの関数整合性チェックの方法を紹介しました。
少し特殊な活用方法かもしれませんが、最近のo3、o4-miniなどの推論モデルは今回の用途に対して相性が良いと感じました。

なお、弊社ではGoogleのGeminiも使用していますが、今回の用途ではGeminiよりもo3-miniやo4-miniのほうが高精度で検知できたため、o4-miniを採用しました。

:::note warn
スプレッドシートのデータを丸ごと送信するという特性上、Geminiの方が大規模トークンを受け付けるので精度が良ければGeminiを使いたかったのですが、今回の用途ではOpenAIの方が高精度でした。

OpenAIから100万トークン扱えるGPT-4.1なども出てきましたので、今後もどのモデルが良いかは検証してみたいと思います。
:::

同じような悩みを抱えている人の参考になれば幸いです。
ここまでお読みいただきありがとうございました。