見出し画像

アドベントカレンダー2023#22:LINE Botとの動作確認(疑似的な非同期処理:トリガー)

LINE Botの結合完了

今日の成果は、LINE Botの設定がうまくいき、DialogflowやGASとの統合が実現したことです。初めてLINEから想定通りの処理が動作した瞬間は、非常にうれしいものでした。

主な作業は、タイムアウトの回避と疑似的な非同期処理の導入

作成する主処理には、チャート図の生成、OpenAI APIの実行があることから、同期処理ではタイムアウトが発生します。そのため、タイムアウトを回避するために、主処理を「処理の受付と実際の処理」に分離しました。「実際の処理」の非同期処理には、トリガーを使用していますので説明します。

doPostによる処理の実行

まずは、「処理の受付」の内容となります。Lineからのリクエストに対して同期処理で回答を返します。

処理の依頼受付の動作画面

受付までは、同期処理で動作。

処理の依頼受付フロー

ChatBotに、特定のメッセージ(例: 「サービス別の見通しは?」)を送ると、処理依頼が開始されます。

doPost(e) で受け付けたイベントオブジェクトからリクエスト内容を取得し、スプレッドシート(オーダーリストという名称のシート)に、依頼を追加します。
ここで重要なのは、JSON.stringify() を使ってリクエスト内容を文字列化すること。JSON.parse()で復元できるようにすることです。

受付部分の抜粋(GASのコード)

doPost(e) で、Webhookイベント(e)を受け取り、スプレッドシート(オーダーリストという名称のシート)に、リクエストを登録し、登録完了の返信をする処理です。※コードは部分掲載のため、この部分だけでは動作しませんので、あらかじめご容赦ください。処理の流れを共有します。

//----------------------------------------------------------------------------------------
// WEBサービスイベント
//----------------------------------------------------------------------------------------
function doPost(e) {
  requestLogger(e.postData.contents, e);

  // 検索依頼のリクエストをオーダーリスト(Sheet)に登録
  var orderNumber = setOrder(e);
  var body = JSON.parse(e.postData.contents);
  var intentName = body.queryResult.intent.displayName;
  
  try {
    if (intentName == "Order") {
      requestLogger(intentName, "doing");
      return OrderResponse(orderNumber); // 検索依頼のリクエストを受け付けたことを返信
    } else {
      requestLogger("else", "doing");
      return NoEventResponse(body);
    }
  } catch (error) {
    // JSON解析エラーまたはその他のエラー
    return ContentService.createTextOutput(JSON.stringify({ "error": error.message })).setMimeType(ContentService.MimeType.JSON);
  }
}

//----------------------------------------------------------------------------------------
// リクエストの保存
//----------------------------------------------------------------------------------------
function setOrder(e) {
  const spreadsheetManager = new SpreadsheetManager();
  const logSheet = spreadsheetManager.getOrderSheet();
  
  var date = new Date();
  var formattedDate = Utilities.formatDate(date, "JST", " HH:mm:ss");
  var jsonData = typeof e.postData.contents === 'string' ? e.postData.contents : JSON.stringify(e.postData.contents);
  var lastRow = logSheet.getLastRow();
  var nextRowNumber = lastRow + 1;

  logSheet.appendRow([nextRowNumber, formattedDate, jsonData]);
  return nextRowNumber;
}

//----------------------------------------------------------------------------------------
// WEBサービス処理:受付回答
//----------------------------------------------------------------------------------------
function OrderResponse(inumber) {
  var fulfillmentText = inumber + '番で受け付けました。しばらくお待ちください。';
  var response = {
    "fulfillmentText": fulfillmentText
  };
  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}

トリガーによる処理の実行

「実際の処理」ですが、処理の内容は、チャート作成、OPENAI APIの実行です。トリガーで実行され、Lineへは非同期にWebhookで回答を返します。

実際の処理の動作

受付後、約90秒後にメッセージが送られてきた。(非同期処理)

実際の処理

設定したトリガーが時間になると処理を起動し、オーダーリスト内の未処理項目をパラメーターとして、処理を開始します。

この処理の工夫は、トリガーによる処理の多重起動を防ぐために、LockService.getScriptLock() と lock.tryLock(5000) を使用し、一種のシングルトン処理を実現したことです。

実際の処理部分の抜粋(GASのコード)

この例では、トリガーを使ってオーダーリストの未処理のリクエストを順に処理する機能を実装しています。

//----------------------------------------------------------------------------------------
// 非同期処理(トリガー起動)
//----------------------------------------------------------------------------------------
function execOrders() {
  var lock = LockService.getScriptLock();
  try {
    if (lock.tryLock(5000)) { // 簡易的な多重起動防止
      var body = callGetOrderWithFirstEmptyRow();
      while (body !== null) {
        Logger.log(JSON.stringify(body));
        var intentName = body.queryResult.intent.displayName;
        Logger.log(intentName);

        try {
          if (intentName == "Order") {
            // 処理の実施(検索、チャート作成、OPENAI実行、LineWebhook)
            SerchResponce(body);
          } else {
            Logger.log(intentName);
          }
        } catch (error) {
          Logger.log("エラー: " + error.toString());
        }
        body = callGetOrderWithFirstEmptyRow(); // 次の行のデータを取得
      }
    } else {
      Logger.log("重複起動のため処理せず終了。");
    }
  } catch (e) {
    Logger.log("エラー: " + e.toString());
  } finally {
    lock.releaseLock(); // ロックを解放
  }      
}

//----------------------------------------------------------------------------------------
// リクエストの復元
//----------------------------------------------------------------------------------------
function getOrder(rowNumber) {
  const spreadsheetManager = new SpreadsheetManager();
  const logSheet = spreadsheetManager.getOrderSheet();
  var jsonString = logSheet.getRange(rowNumber, 3).getValue();

  if (jsonString == "") {
    return "";
  } else {
    logSheet.getRange(rowNumber, 4).setValue('executed'); // 実行済みのサインを入れる
    return JSON.parse(jsonString);
  }
}

//----------------------------------------------------------------------------------------
// リクエストの復元:未処理の最終行の取得
//----------------------------------------------------------------------------------------
function callGetOrderWithFirstEmptyRow() {
  var rowNumber = getFirstEmptyRowInColumn();
  return rowNumber !== null ? getOrder(rowNumber) : null;
}

function getFirstEmptyRowInColumn() {
  const spreadsheetManager = new SpreadsheetManager();
  const logSheet = spreadsheetManager.getOrderSheet();
  var columnDData = logSheet.getRange("D:D").getValues();

  for (var i = 0; i < columnDData.length; i++) {
    if (!columnDData[i][0]) return i + 1;
  }  
  return null;
}

まとめ

今回の実装には、まだ多くの改善余地がありますが、全て無料ツールを使用して、ChatBotを作成できたことはいい経験になりました。今後はお手軽に簡単なBot(モックアップレベル)が作れるようになったと思う。今後は、当初の計画どおりサーバーサイドをGoogle FunctionやBigQueryに変更し、より堅牢で高品質なシステムを作ってみたい。
正月までに、アドベントカレンダー2023は、終わるのだろうか・・・

この記事が気に入ったらサポートをしてみませんか?