ABEJA Tech Blog

中の人の興味のある情報を発信していきます

忘れっぽい人にオススメ!AIが応援してくれるテンションの上がるリマインダーボットを作る (GAS+Notion+OpenAI)


初めに

こんにちは、ABEJAでデータサイエンティストを務めている嘉藤です。今日から始まるABEJAアドベントカレンダ2023の記事シリーズ、皆さんお楽しみにしていただけていますか?記念すべき1日目の記事では、私が社内のアドベントカレンダー企画のために作ったGoogle Apps Script(GAS)を用いて、投稿者が気持ちよく記事を書けるよう、最近流行りのLLMで生成した応援の言葉も添えてslackに自動でリマインドするボットを作成したお話をしたいと思います!

全体の構成

今回はGAS上でNotion、Slack、Open AIのAPIを用いてボットを作成しました。

大まかな処理の流れとしては以下の通りです。

  1. GASのトリガー機能でmain関数を起動
  2. NotionDBからデータを抽出
  3. Open AIのAPIを用いてメッセージを作成
  4. Slack APIでメッセージを送信

コードはこちらで公開しております。 github.com

APIの準備

ここでは各APIの設定や必要な準備についてお話しします。(Open AIのAPIについてはありふれているので割愛)

notionの準備

integrationの作成

まずこちらから新しくintegrationの作成を行います。 必要な権限を聞かれますが、今回はSlackへのリマインダーにメンションをつけるためにメールアドレスを使用するため、メールアドレス含む個人情報を取得します。 integrationの作成が完了すると、seacretsからAPIを叩く際に必要となるintegration tokenが取得できます。

connectionの設定

次に、情報を取り出したいDBに先ほど作成したintegrationを繋げます。 DBをページとして開き、右上の三点リーダからコネクションの追加を選択し、先ほど作成したintegratoinを追加します。 これでNotionの準備は完了です。

Slackの準備

Appの作成

SlackのAPIを使用するためにはまずこちらからAppを作成します。

Scopesの設定

Appを作成するとAppの設定へ遷移しますので、OAuth & PermissionsタブのScopesからchat:write、users:read、users:read.emailを有効にします。 chat:writeはSlackの投稿に、users:readとusers:read.emailは後述するメンションするために必要となるuser idをメールアドレスから取得するために必要となります。

ワークスペースへのインストール

最後に先ほどScopesを作成したタブから上にスクロールし、OAuth Tokens for Your Workspaceからinstall to workspaceを選択し、 作成したAppをワークスペースへインストールします。 すると、Bot User OAuth Tokenの欄からAPIを叩く際に必要となる認証用のtokenが取得できます。

GASにおけるAPI keyの扱い

前述の通りSlackとNotionのAPIを叩く際には認証のためのtokenが必要となります。 その際にコードの中に直接tokenをベタ書きするのはセキュリティ面から考えて非常によろしくないです。 GASではスクリプトプロパティを利用することでこれを避けられます。 スクリプトプロパティはGASの設定から(key,value)の形で登録することができ、スクリプトの中ではPropertiesService.getScriptProperties().getProperty("KEY")とすることで取得することができます。

GASでモチベーターボットを作る

さて、これでAPIの準備は完了したので早速モチベーターボットの中身についてお話ししたいと思います。

トリガーの設定

今回のボットは営業日の午前中に数営業日後に記事投稿予定の方にリマインドを送ります。 しかし、GASのトリガーは毎日や毎週などの大雑把な単位でしか設定することができません。 そのため以下のような営業日か否かを判別する関数を用い、毎日main関数を起動させることで営業日だけにリマインドを送っています。

function main() {
  const today=new Date()

  if(is_buisinessday(today)){
    const calender_info=get_calender_info()

    //3,5,7営業日前の人にリマインダーを送る
    remind(calender_info)
  }
}
function is_buisinessday(_date){
  //土日の判断
  const week = _date.getDay();
  if (week == 0 || 6 == week) {
    return false;
  }
  //日本の祝日だけが入ったカレンダー
  const japanese_holiday_callender_id='ja.japanese#holiday@group.v.calendar.google.com'
  const calendar = CalendarApp.getCalendarById(japanese_holiday_callender_id);
  const holiday_events = calendar.getEventsForDay(_date);
  //予定が入っている日は祝日
  if (holiday_events.length > 0) {
    return false;
  }
  return true;
}

祝日の判定は日本の祝日のGoogle Calender(ja.japanese#holiday@group.v.calendar.google.com)に予定が入っていた場合、祝日(営業日でない)としています。 日本の祝日がカレンダーに登録されていないとエラーになります(英語版はen.japanese#holiday@group.v.calendar.google.com)。

NotionのAPIを叩く

NotionのAPIで以下のようなDBから投稿予定日、投稿者のメールアドレス、記事タイトルの三つの情報を抽出します。

APIを呼び出すコードは以下の通りです。 以下の関数を実行することでjson形式でDBのデータが取得できるため、ここから目的の情報を抜き出して来ればOKです。

function get_notion_db(){
    const token = PropertiesService.getScriptProperties().getProperty("NOTION_KEY");
    const db_id = PropertiesService.getScriptProperties().getProperty("NOTION_DB_ID");
    const url = "https://api.notion.com/v1/databases/" + db_id + "/query";

    const headers = {
        'content-type' : 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + token,
        'Notion-Version': '2022-06-28',
      };
    const _payload = payload==null ? null : JSON.stringify(payload);
    const options ={
        "method": method,
        "headers": headers,
        "payload": _payload
      };

      const notion_data = UrlFetchApp.fetch(url, options);
      const notion_data_json = JSON.parse(notion_data);
      return notion_data_json;

スクリプトプロパティのNOTION_KEYに先ほどintegrationを作成する際に取得したtokenを、 NotionDBのURLがhttps://www.notion.so/XXXX/YYYY?v=ZZZのような形になっていると思うのですが、YYYYの部分をNOTION_DB_IDとして追加すれば動きます。

応援コメントをGPTで生成する

応援コメントの生成には以下の3つのルールを与えています。

  • 文中に記事タイトルと投稿予定日までの営業日数を必ず含めてください。
  • タイトルが「未定」「なんか書く」などの際は、タイトルを決めるように催促してください。
  • 3営業日前には記事をレビューに上げられそうかを聞いてください。

応援コメントの生成は具体的には以下のようなコードで行なっています。

function generate_encouragement_message(title,days_remaining) {
  const api_key = PropertiesService.getScriptProperties().getProperty("OPENAI_KEY");
  const model='gpt-4-1106-preview'
  const endpoint = 'https://api.openai.com/v1/chat/completions'; // GPT-4のエンドポイント

  const system_prompt='あなたは記事執筆を応援するモチベーターAIです。記事が書きたくなるように記事のタイトルを考慮して執筆者のモチベーションを向上させてください'
  const prompt = `「記事執筆応援メッセージを一文で出力してください。
  ただし以下のルールに従ってください。
  ・文中に記事タイトルと投稿予定日までの営業日数を必ず含めてください。
  ・タイトルが「未定」「なんか書く」などの際は、タイトルを決めるように催促してください。
  ・3営業日前には記事をレビューに上げられそうかを聞いてください。

  記事タイトル:${title}
  投稿予定日までの営業日数:${days_remaining}日`;

  const content=[
    {
      'role':'system',
      'content':system_prompt
    },
    {
      'role':'user',
      'content':prompt
    }
  ]
  const payload = {
    "model":model,
    "messages": content,
    "max_tokens": 1000,
    "temperature":0.8
  };

  const options = {

    'method' : 'post',
    'contentType': 'application/json',
    'headers': {
      'Authorization': 'Bearer ' + api_key
    },
    'payload': JSON.stringify(payload)
  };

  for (const attempts = 0; attempts < 5; attempts++) {
    try {
      const response = UrlFetchApp.fetch(endpoint, options);
      const json = response.getContentText();
      const data = JSON.parse(json);
      return data.choices[0]['message']['content'];
    } catch (e) {
      console.log('retry message generation ' + e);
      Utilities.sleep(1000); // 1秒待機してからリトライ
    }
  }
  console.log('5 times failed generating message')
}

このプロンプトで「アドベントカレンダーのモチベーターボットを作ってみた」というタイトルの記事の投稿3営業日前という条件で以下のようなメッセージが生成されます。

「アドベントカレンダーのモチベーターボットを作ってみた」という興味深いタイトルの記事、投稿予定まであと3営業日ですね!その革新的なボットに触れる日が待ち遠しいです。レビューに上げる準備は進んでいますか?最後のブラッシュアップで読者の心を掴む内容に仕上げましょう!

SlackのAPIを叩く

SlackのAPIではメールアドレスからのuser idの取得とメッセージの送信を行なっています。 Slackでメンションを行う方法はいくつかあるのですが、今回はメールアドレスからAPIを用いてuser idを取得し、メッセージに'<@'+user_id+'>'を追加することでメンションしています。

function call_slack_api(token, api_method, payload) {
  const params = {};
  Object.assign(params, payload);
  for (const [key, value] of Object.entries(params)) {
    if (typeof value === "object") {
      params[key] = JSON.stringify(value);
    }
  } 
  const response = UrlFetchApp.fetch(
    `https://www.slack.com/api/${api_method}`,
    {
      method: "post",
      contentType: "application/x-www-form-urlencoded",
      headers: { "Authorization": `Bearer ${token}` },
      payload: params,
    }
  );
  return response;
}
function get_userid_from_email(email){
  const token=PropertiesService.getScriptProperties().getProperty("SLACK_OAUTH_TOKEN");
  const payload={
    'email':email
  }
  const response=call_slack_api(token,'users.lookupByEmail',payload)
  const response_json = JSON.parse(response.getContentText());
  const user_id=response_json['user']['id']
  return user_id
}

function send_to_slack(message){
  const token=PropertiesService.getScriptProperties().getProperty("SLACK_OAUTH_TOKEN");
  const channel_id=PropertiesService.getScriptProperties().getProperty("CHANNEL_ID");
  const api_response = call_slack_api(token, "chat.postMessage", {
    channel: channel_id,
    text: message
  });
  return api_response
}

こちらのコードはSLACK_OAUTH_TOKENとしてSlackのAppを作成した際に取得したTokenを、メッセージを送りたいチャンネルのチャンネル IDをCHANNEL_IDとしてスクリプトプロパティに追加すると動きます。

おわりに

この記事ではアドベントカレンダーの実施に伴なったリマインド業務を代行してくれるボットの作成についてお話ししました。 このボットを作成したおかげでリマインドのことに頭のリソースを割かなくても良いようになり、ついでにアドベントカレンダーの記事が一つ作成できました! 面倒で忘れやすいような業務を自動化したい方のお役に立てれば幸いです!