蒙古タンメン中本について答えるスプレッドシートの関数をGoogle Apps Script(GAS)とOpenAI Assistants APIで作ってみた

こんにちは。
penguinmanと申します。

2023年11月6日（火）にOpenAIから[大きな発表](https://www.youtube.com/watch?v=U9mJuUkhUzk)がありましたね。
あまりにも大きな発表が多すぎて私もまだ噛み砕き切れておりませんが、今からもっと面白いことができるかもとワクワクしています！

今日は先日発表されたAssistants APIとGoogle Apps Script（以降GASと略記します）を使って蒙古タンメン中本（以降中本と略記します）について答えてくれる関数を作りましたのでレシピを共有したいと思います🍜

# 目次
[何を作ったか？](#何を作ったか)
[どうやって作ったか？](#どうやって作ったか)
[作成手順](#作成手順)
- [Playgroundでの実験](#playgroundでの実験)
- [必要情報の取得](#必要情報の取得)
- [GASの事前設定](#gasの事前設定)
- [コードをコピペ](#コードをコピペ)

[Done](#done)

# 何を作ったか？
[Wikipediaの中本の記事](https://ja.wikipedia.org/wiki/%E8%92%99%E5%8F%A4%E3%82%BF%E3%83%B3%E3%83%A1%E3%83%B3%E4%B8%AD%E6%9C%AC)を取り込んで、中本について答えてくれるようなスプレッドシートの関数（post_assistant関数とよびます）をGoogle Apps Script（GAS）とAssistants APIを使って作成しました。今回は中本の記事を取り込ませましたが、ここで会社の社内規定やプロダクトのFAQを取り込めば、それについて答える関数ができるので汎用性は◎だと思っています。

![記事3-1. 関数画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/db5c8a54-de07-ad2a-5f58-05570c823462.png)


# どうやって作ったか？
基本的に以下の4つの関数をGAS上で組んでいます。
各関数はAssistants APIのエンドポイントにHTTPリクエストを投げています。

![記事3-2. 処理の流れ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/b90f6c26-d822-b830-afc2-e15eb4926bff.png)



メインで動くコードはmain.gsであり、上記4つの関数を実行しています。
GASで実行する際には以下のコードをコピペした上で、スクリプトプロパティにOpenAIのAPI keyを設定し（[後述](#gasの事前設定)）、main.gsの2行目のところにassistant_idを貼り付ける（[後述](#コードをコピペ)）必要があります。GASがわかる方はこれで大丈夫です。よくわからない方も安心してください😊 この後説明していきます💪

```javascript:main.gs
function post_assistant(message){
  // プロパティからAPIキーを取得 （要設定）
  const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty('API_KEY'); # スクリプトプロパティにOpenAI API keyを貼り付ける（後述）
  // 実行するアシスタントid　（コード書き換え必要）
  let assistant_id = "asst_*****************" # assistant_idを貼り付ける（後述）

  // httpリクエストのベースの情報
  let options = {
    'contentType': 'application/json',
    'headers': {
      'Authorization': 'Bearer ' + OPENAI_API_KEY,
      'OpenAI-Beta': 'assistants=v1'
    },
    'muteHttpExceptions': true // HTTP例外をミュートにする
  };
  // threadを新規作成
  let thread_id = create_thread(options);
  Logger.log(thread_id);
  // メッセージを送る
  send_message(options, thread_id, message);
  // アシスタントを実行する
  run_assistant(options, thread_id, assistant_id);
  // アシスタントの実行終了まで20秒待機
  Utilities.sleep(20000);
  // メッセージを取得する
  let res_message = get_message(options, thread_id);
  Logger.log(res_message)
  return res_message
}
```

```javascript:create_thread.gs
function create_thread(options) {
  let options_c = JSON.parse(JSON.stringify(options));
  let url = 'https://api.openai.com/v1/threads';
  options_c['method'] = 'post';

  try {
    let response = UrlFetchApp.fetch(url, options_c);
    let responseCode = response.getResponseCode();
    let content = response.getContentText();
    
    if (responseCode == 200) {
      Logger.log('Success: ' + content);
      return JSON.parse(content)["id"];
    } else {
      Logger.log('Error: ' + responseCode + '\n' + content);
    }
  } catch (e) {
    Logger.log('Exception: ' + e.toString());
  }
}
```

```javascript:send_message.gs
function send_message(options, thread_id, message) {
  let options_c = JSON.parse(JSON.stringify(options));
  let url = 'https://api.openai.com/v1/threads/'+thread_id+'/messages';
  options_c['method'] = 'post';
  // threadに送るメッセージの定義
  let data = {
    "role": "user",
    "content": message
  };
  options_c['payload'] = JSON.stringify(data);

  try {
    let response = UrlFetchApp.fetch(url, options_c);
    let responseCode = response.getResponseCode();
    let content = response.getContentText();
    
    if (responseCode == 200) {
      Logger.log('Success: ' + content);
    } else {
      Logger.log('Error: ' + responseCode + '\n' + content);
    }
  } catch (e) {
    Logger.log('Exception: ' + e.toString());
  }
  
}
```

```javascript:run_assistant.gs
function run_assistant(options, thread_id, assistant_id) {
  let options_c = JSON.parse(JSON.stringify(options));
  let url = 'https://api.openai.com/v1/threads/' + thread_id + '/runs';
  options['method'] = 'post';
  // 起動させるアシスタントの定義
  var data = {
    "assistant_id": assistant_id
  }
  options_c['payload'] = JSON.stringify(data);

  try {
    let response = UrlFetchApp.fetch(url, options_c);
    let responseCode = response.getResponseCode();
    let content = response.getContentText();
    
    if (responseCode == 200) {
      Logger.log('Success: ' + content);
    } else {
      Logger.log('Error: ' + responseCode + '\n' + content);
    }
  } catch (e) {
    Logger.log('Exception: ' + e.toString());
  }
  
}
```

```javascript:get_message.gs
function get_message(options, thread_id) {
  let options_c = JSON.parse(JSON.stringify(options));
  let url = 'https://api.openai.com/v1/threads/'+thread_id+'/messages';
  options_c['method'] = 'get';

  try {
    let response = UrlFetchApp.fetch(url, options_c);
    let responseCode = response.getResponseCode();
    let content = response.getContentText();
    
    if (responseCode == 200) {
      Logger.log('Success: ' + content);
      return JSON.parse(content)["data"][0]["content"][0]["text"]["value"];
    } else {
      Logger.log('Error: ' + responseCode + '\n' + content);
    }
  } catch (e) {
    Logger.log('Exception: ' + e.toString());
  }
}
```


# 作成手順
それでは早速作成手順を詳しくみていきたいと思います✨
概略を先にお示しするとOpenAI側の作業が2ステップ、GAS側の作業が2ステップで合計4ステップになります。

![記事3-3. 作業の概略図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/a2370bec-f4aa-85c0-70b0-4fccc49a2163.png)

## Playgroundでの実験
まずはOpenAIのAsssitants APIのPlaygroundで中本の情報を読み込ませたChatGPT（以降中本GPTと略記します）の精度を確認しましょう。まずはOpenAIの[Assistantsの画面](https://platform.openai.com/assistants)にログインします。

![記事3-3.5. 初期画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/7efc5282-e92b-d174-2ee9-2d74cc8dbdf3.png)



最初は上記のように何もない画面が表示されると思いますので、真ん中の「+Create」ボタンを押します。

![記事3-4. 初期設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/ad5b6305-adea-fa3a-7485-6e11c0c96d55.png)

すると今度は上記ような画面が出るので画像のように設定していきます。Instructionの部分はひとまず「必ずアップロードされた資料をもとに回答してください。」としましたが、こちらの指示はもっと細かく工夫しても良いかと思います。[Retrieval機能](https://platform.openai.com/docs/assistants/tools/knowledge-retrieval)（Chatgptにあらかじめ資料を読み込ませる機能）の場合はModelの部分はgpt-4-1106-previewかgpt-3.5-turbo-1106のどちらかが選べるようですが、gpt-4-1106-previewだと1回のやり取りで大体10円程度の費用が発生するので最初は費用が少額で収まるgpt-3.5-turbo-1106を設定することをお勧めします。[Wikipediaの中本の記事](https://ja.wikipedia.org/wiki/%E8%92%99%E5%8F%A4%E3%82%BF%E3%83%B3%E3%83%A1%E3%83%B3%E4%B8%AD%E6%9C%AC)をテキストファイルでアップロードしていますが、こちらは自分が取り込ませたいテキストをアップロードしてください。すべての設定を終えて「save」ボタンを押すとAssistantが追加されます。

![記事3-5. Assistant追加後画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/5ced6560-f79e-79a5-1b70-a42c5ffa5e65.png)

![記事3-6. テストボタン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/a730dc81-8f2d-792a-8e34-9436d918e1ff.png)

追加されたAssistantを再度クリックすると上記の画像のような設定画面がもう一度出てくるので右上の「Test」ボタンをクリックしてください。

![記事3-7. Playground画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/7814085a-8bcb-3f90-9d15-ee449f352b47.png)


上記のような画面に遷移するので、テキストボックス内に文章を入れて「Add and run」ボタンを押すと先ほどアップロードしたテキストをもとに回答していることがわかります😀
この画面の左側の部分で資料を追加でアップロードしたり、削除したりできるので資料を変えつつ、自分が望む回答精度になるように調整してみてください。お金の余裕がある方はModelでgpt-4-1106-previewを選んでみるのも良いかもです。中本GPTでは精度がかなり上がりました。

何度がチャットをしたら必ず[こちら](https://platform.openai.com/usage)から費用を確認し、思いがけず多額の請求がこないようにしましょう。

## 必要情報の取得
Playgroundで十分な精度が出ることを確認したら以下の2つの情報をメモしておいてください。

1. assistant_id
Playgroudの下記の画像の位置にある「asst_＊＊＊＊＊＊＊＊＊」というidです。

![記事3-8. Assistant_id.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/6a61ae30-ebff-1e74-5d92-6784737c5edb.png)




2. OpenAI API key
[こちらの画面](https://platform.openai.com/api-keys)から発行できます。流出すると他の人に使われてしまうため流出しないように注意してください⚠️

## GASの事前設定
適当なスプレッドシートを開いて以下の「Apps Script」部分からGASのエディタに入ってください。

![記事3-9. GAS入り方.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/8895b923-5ef6-a5b5-4016-9862d6d70505.png)
![記事3-10. GAS設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/f571f3d5-4747-f244-119e-aa9418c74657.png)


スクリプトプロパティを設定する画面を見つけたら、上記の画像のようにプロパティの欄には「API_KEY」、値の欄には必要情報の取得の(2) OpenAI API keyを入力してください。

![記事3-11. スクリプトプロパティ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/15646277-c997-09f9-31c6-bd30fd3851cb.png)


入力が終わったら後少しで完成です💪


## コードをコピペ
さて、最後のステップです。
下記の画像のように「エディタ」をクリックしてエディタ画面に移動します。

![記事3-12. エディタへの戻り方.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/5df359e1-251f-5cb5-2416-ae5bdb63f519.png)

一番最初にお示ししたmain.gsのコードの2行目の部分に必要情報の取得の(1) assistant_idを貼り付けます。

```javascript:main.gs
    // 実行するアシスタントid　（コード書き換え必要）
    let assistant_id = "asst_*****************" # assistant_idを貼り付ける（後述）
```


後はmain.gs~get_message.gsまでの5つの関数をひたすらエディタにコピペし、最後に右上のデプロイボタンを押して完成です✨
仕組みとしてはHTTPリクエストを送っているだけなのですがそれぞれの関数のAssistants APIとの対応関係を載せておきます。

create_thread関数 - [Create thread](https://platform.openai.com/docs/api-reference/threads/createThread)
send_message関数 - [Create message](https://platform.openai.com/docs/api-reference/messages/createMessage)
run_assistant関数 - [Create run](https://platform.openai.com/docs/api-reference/runs/createRun)
get_message関数 - [List messages](https://platform.openai.com/docs/api-reference/messages/listMessages)

注意点を2点挙げておきます。

(1) main.gsでrun_assistant関数を実行した後に20秒の待ち時間を設けています。これはすぐにget_message関数を実行してしまうと言語モデルからの応答がない状態でメッセージを取得してしまうため、言語モデルのアウトプットが出るまで20秒待たせています。こちらページを見るとおそらくrunの状態（実行中か終了したか）を識別する方法があるのでそれをもとに組むともっと良いかもです。

(2) get_message関数はcreate_thread関数で作成したスレッドにあるメッセージを全て取得し、その最新のメッセージのみを抜き出してくる関数になっています。そのため、もし言語モデルのアウトプットがない状態で実行するとsend_messageで投げたメッセージが返ってくる可能性があります。


# Done
後は「何を作ったか？」で共有した通り、スプレッドシート上で中本に関する質問をpost_assistant関数に通すだけです。

![記事3-1. 関数画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619737/db5c8a54-de07-ad2a-5f58-05570c823462.png)

この仕組みを使って是非自分オリジナルのGPT関数を作ってみてください！この記事が皆様のお役に立てれば幸いでございます🙇

それではまた！！