Google Apps ScriptでOpenAIのASSISTANT APIを試す|LINEBOT

# Google Apps ScriptでOpenAIのASSISTANT APIを試す
ASSISTANT APIが公開されました。
本記事では、Google Apps Script(GAS)を使用してOpenAI APIを介してLINEボットの応答を行うプロセスを紹介します。以下では、特定のユーザーメッセージに反応し、OpenAIのアシスタントを活用して応答するスクリプトの主要な機能に焦点を当てて解説します。

https://platform.openai.com/docs/assistants/overview

![1699489903714.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486826/6128cd50-7ecb-dfd1-6600-e2cbc180a588.png)




## 前提条件
- ASSISTANTとTHREADはplatform上で作成し、IDをGUI上でコピペします。
- ユーザーごとのスレッド管理はまだしてません。
- LINEボットの作り方は以下を参照(GASのソースは全部書き換え。無料版でOK)
- `gpt-3.5-turbo-1106`を使用しています

![スクリーンショット 2023-11-09 094339.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486826/fb8ae4f9-28ad-7dcf-dc92-4fdff6b21c34.png)

https://chatgpt-lab.com/n/n55257c082a9d

## 環境変数の設定

スクリプトで利用する各種トークンやAPIキーは、Google Apps Scriptの`PropertiesService`を使ってスクリプトプロパティから取得します。これにより、セキュリティを確保しつつ、必要な認証情報へのアクセスを可能にしています。


## メイン処理の流れ

`doPost`関数はLINEプラットフォームからのWebhookリクエストをトリガーとして実行される主要なエントリポイントです。ここで受け取ったイベントデータを解析し、適切な応答を生成してLINEプラットフォームに返送します。

1. イベントデータのパース
2. 応答先の設定
3. ユーザーメッセージの前処理
4. 応答の生成と送信
5. エラー時のハンドリング

## 応答の生成

ユーザーから受け取ったメッセージに基づき、OpenAIのアシスタントAPIを使って応答を生成します。応答の処理は`handleMainResponse`関数にて行われ、スレッドを作成し、メッセージの送信及びスレッドの実行状況を確認した後、最終的な応答をLINEプラットフォームに送信します。

### 引数
- `event`: LINEのプラットフォームから送信されるWebhookイベントオブジェクト。
- `processedMessage`: 前処理を行った後のユーザーのメッセージ。
- `flags`: メッセージ処理に関するフラグの配列（例: メッセージを処理するかどうかなどを指定するフラグ）。
- `url`: LINEのメッセージ送信エンドポイントURL。
- `replyToken`: 応答メッセージをLINEプラットフォームに送り返すためのトークン。

### 処理の流れ
1. `DEFAULT_THREAD`定数からスレッドIDを取得します。
2. ユーザーのメッセージをOpenAI APIに送信するための新しいメッセージを作成します（`createMessage`関数）。
3. OpenAI APIによるスレッドの実行要求を行います（`runThread`関数）。
4. OpenAI APIからの応答が完了するまで待機します（`checkRunCompletion`関数）。
5. スレッドに投稿されたメッセージのリストを取得します（`listMessages`関数）。
6. 取得したメッセージリストから、応答メッセージを取得してLINEユーザーに送信します（`postLineMessage`関数）。
7. 正常に応答を送信した後、「post ok」という内容でテキスト出力をビルドします（`buildTextOutput`関数）。

### エラーハンドリング
- `try`ブロック内で何らかのエラーが発生した場合は、これをキャッチし、エラーメッセージを含んだ応答をLINEユーザーに送信する（`handleErrors`関数）。
- `buildTextOutput`関数は、エラー発生時にもJSON形式でのレスポンスを生成しています。

## エラーハンドリング

処理中に発生する可能性のあるエラーは、`handleErrors`関数を介して適切にハンドリングされます。エラーが発生した場合、ユーザーにはエラーメッセージが含まれた応答が返されます。

## 全文
```javascript:全文
const LINE_ACCESS_TOKEN = "ここにLINEアクセストークンをいれる";
const OPENAI_APIKEY = "ここにOPENAIのAPIキーを入れる";

const URL_ASSISTANT_API = 'https://api.openai.com/v1/assistants';
const URL_OPENAI_API = 'https://api.openai.com/v1/';
const DEFAULT_THREAD = "ここにスレッドIDをいれる";
const ASSISTANT_ID = "ここにアシスタントIDをいれる";

function doPost(e) {
  // JSONデータのパース
  const event = parseEvent(e);
  const replyToken = event.replyToken;
  // URL設定
  const url = 'https://api.line.me/v2/bot/message/reply';

  try {
    // ユーザーメッセージの処理
    const userMessage = event.message.text;

    // ユーザーIDに基づく特定のチェック
    const userId = event.source.userId;
    if (shouldStopProcessing(userId, userMessage)) {
      return buildTextOutput('post ok');
    }

    // 特定のキーワードに基づくメッセージの変更
    var processedMessage = userMessage;
    var flags = [true,true];



    // 他のパターンマッチング処理...

    // メインの応答ロジック
    return handleMainResponse(event, processedMessage, flags, url,replyToken);
    
  } catch (error) {
    // エラーハンドリング
    handleErrors(userId, error, url, event.replyToken, userMessage);
    return buildTextOutput('post ok');
  }
}

function parseEvent(e) {
  return JSON.parse(e.postData.contents).events[0];
}


function shouldStopProcessing(userId, userMessage) {
  // ユーザーIDやメッセージ内容に基づく早期リターンの条件
  // ...
}

function processKeywords(userMessage) {
  // キーワードに基づいてメッセージを変更し、フラグを設定
  // ...
}

function handleMainResponse(event, processedMessage, flags, url,replyToken) {
  // メインの応答処理
  // ...
  var threadId = DEFAULT_THREAD;
  createMessage(threadId,processedMessage);
  var resultRunThread = runThread(threadId);
  checkRunCompletion(threadId,resultRunThread.id);
  var resultListMessage = listMessages(threadId);
  // Logger.log(toMessageResult(resultListMessage))
  postLineMessage(url,replyToken,toMessageResult(resultListMessage));
  return buildTextOutput("post ok");
}

function handleErrors(userId, error, url, replyToken, userMessage) {
  // エラーハンドリングのロジック
  // ...
  postLineMessage(url,replyToken,userMessage + "¥n---¥n" + error.message)
}

function buildTextOutput(content) {
  return ContentService.createTextOutput(JSON.stringify({ 'content': content }))
         .setMimeType(ContentService.MimeType.JSON);
}

function postLineMessage(url,replyToken,messageText){
  if (messageText === null) {
    const errorMessage = "messageText cannot be null";
    console.error(errorMessage);
    messageText = errorMessage;
  }

  if(typeof messageText !== 'string'){
    const errorMessage = "messageText must be a string";
    console.error(errorMessage);
    messageText = errorMessage;
  }

  UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
        'type': 'text',
        'text': messageText
      }]
    })
  });
}

function toMessageResult(result){
  return result.data[0].content[0].text.value;
}



function createMessage(threadId,userMessage){
  var data = {
    "role":"user",
    "content":userMessage
  };

  var options = {
    'method': 'post',
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + OPENAI_APIKEY,
      'OpenAI-Beta': 'assistants=v1'
    },
    'payload': JSON.stringify(data)
  };

  try {
    var response = UrlFetchApp.fetch(URL_OPENAI_API+"/threads/" + threadId  + "/messages", options);
    var result = JSON.parse(response.getContentText());
    Logger.log(result.id); // 応答をログに記録します
    return result;
  } catch (e) {
    Logger.log(e.toString());
  }
}
function retrieveThread(threadId){
  var options = {
    'method': 'get',
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + OPENAI_APIKEY,
      'OpenAI-Beta': 'assistants=v1'
    },
  };

  try {
    var response = UrlFetchApp.fetch(URL_OPENAI_API+"/threads/" + threadId,options);
    var result = JSON.parse(response.getContentText());
    Logger.log(result); // 応答をログに記録します
    return result;
  } catch (e) {
    Logger.log(e.toString());
  }
}

function runThread(threadId){
  var data = {
    "assistant_id":ASSISTANT_ID,
  };

  var options = {
    'method': 'post',
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + OPENAI_APIKEY,
      'OpenAI-Beta': 'assistants=v1'
    },
    'payload': JSON.stringify(data)
  };

  try {
    var response = UrlFetchApp.fetch(URL_OPENAI_API+"/threads/" + threadId + "/runs", options);
    var result = JSON.parse(response.getContentText());
    Logger.log(result.id); // 応答をログに記録します
    return result;
  } catch (e) {
    Logger.log(e.toString());
  }
}
function retrieveRun(threadId,runId){
  var options = {
    'method': 'get',
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + OPENAI_APIKEY,
      'OpenAI-Beta': 'assistants=v1'
    },
  };

  try {
    var response = UrlFetchApp.fetch(URL_OPENAI_API+"/threads/" + threadId + "/runs/" + runId, options);
    var result = JSON.parse(response.getContentText());
    Logger.log(result.status); // 応答をログに記録します
    return result;
  } catch (e) {
    Logger.log(e.toString());
  }
}

function listMessages(threadId){

  var options = {
    'method': 'get',
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + OPENAI_APIKEY,
      'OpenAI-Beta': 'assistants=v1'
    },
  };

  try {
    var response = UrlFetchApp.fetch(URL_OPENAI_API+"/threads/" + threadId  + "/messages", options);
    var result = JSON.parse(response.getContentText());
    Logger.log(result.data[0].content[0].text.value); // 応答をログに記録します
    return result;
  } catch (e) {
    Logger.log(e.toString());
  }
}

function listRuns(threadId){

  var options = {
    'method': 'get',
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + OPENAI_APIKEY,
      'OpenAI-Beta': 'assistants=v1'
    },
  };

  try {
    var response = UrlFetchApp.fetch(URL_OPENAI_API+"/threads/" + threadId  + "/messages", options);
    var result = JSON.parse(response.getContentText());
    Logger.log(result.data[0].content[0].text.value); // 応答をログに記録します
    return result;
  } catch (e) {
    Logger.log(e.toString());
  }
}
function checkRunCompletion(threadId,runId) {
  var status = "";
  var maxAttempts = 10; // 最大試行回数を設定（ここでは30回とする）
  var attempts = 0;

  // ステータスが "completed" になるか、最大試行回数に達するまでループ
  do {
    Utilities.sleep(500); 
    result = retrieveRun(threadId,runId); // ステータスを取得
    attempts++;

    // ステータスが "completed" の場合はループを抜ける
    if (result.status === "completed") {
      break;
    }
    
    // 最大試行回数に達した場合はエラーメッセージを表示
    if (attempts >= maxAttempts) {
      throw new Error("最大試行回数に達しました。");
    }
  } while (result.status !== "completed");

  // "completed" ステータス時の次の処理
  // ...
}
```
![1699489903714.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486826/e9b1af30-aa47-d883-4bd8-bba266c06127.png)
![1699489903714.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486826/96f761f2-4251-caf9-0ddf-17e8af91c0c6.png)


このように、Google Apps ScriptとASSISTANT APIを連携させることで、簡単な設定とコードでインテリジェントなチャットボットを作成することが可能です。開発者は、この基本的なフレームワークをカスタマイズして、異なるユースケースに合わせたチャットボットを設計することができます。