[Slack] ChatGPTをSlackで使えるようにする

Slack Events API、OpenAI API (ChatGPT)、GAS (Google Apps Script)を使用してSlackにGPTBotを導入して使えるようになります。業務でSlackを使用されているのであれば入れておくと便利かも。

Botの作成

Bot自体の作成はこちらを参考に進めてください、今回必要な権限は以下の通り。

  • OAuth & Permissions
    • app_mentions:read
    • channels:history
    • chat:write
  • Event Subscriptions
    • message.channels

こちらの手順を完了させて、チャンネル追加ができればBot自体の作成はOKです。

OpenAI API Keyの取得

OpenAIのアカウントが必要なので作成してない場合は先に作成を完了させてください。

https://platform.openai.com/api-keys

こちらにアクセスして、APIKeyを取得してください。

従量課金制からPre-Purchase制に?

2023年秋頃までは従量課金制でしたが、プリペイド式に変わった模様。

最低チャージ額が$5なのでこちら先に課金が必要です。

私の場合はガンガン使っても$5チャージで1年以上使えているので十分コスパが良いと思いました。

こちらでAPI Keyを取得したら忘れないように一旦どこかにメモをしておきましょう、以降のGASで使用します。

Google Apps Scriptの実装

こちらでSlackとChatGPTの橋渡しを担わせます。

こちらにアクセスして、新規プロジェクトの作成を行い、以下スクリプトを貼り付けてください。

const slackBotId = PropertiesService.getScriptProperties().getProperty('slackBotId');
const slackBotToken = PropertiesService.getScriptProperties().getProperty('slackBotToken');
const openAIApiKey = PropertiesService.getScriptProperties().getProperty('openAIApiKey');

function doPost(e, _IsTest = false) {
  const params = (_IsTest) ? JSON.parse(e) : JSON.parse(e.postData.getDataAsString());

  // Slackリクエスト検証
  if (params.type === 'url_verification') {
    return ContentService.createTextOutput(params.challenge);
  }

  const slackChannel = params.event.channel;
  const paramsText = params.event.text;

  // 無限ループ回避
  if ('subtype' in params.event) {
    return ContentService.createTextOutput('');
  }

  // @slackBotIdに反応してGPTからのレスをスレッドに返す
  if (paramsText.includes(`<@${slackBotId}>`) && params.event.user !== slackBotId) {
    // 3秒タイムアウトリトライ対策 (再送処理リクエストは200を返すだけ)
    let cache = CacheService.getScriptCache();
    if (cache.get(params.event.client_msg_id) == 'done') {
      return ContentService.createTextOutput();
    } else {
      cache.put(params.event.client_msg_id, 'done', 600);
    }

    let history;

    if (params.event.thread_ts) {
      history = getThreadHistory(slackChannel, params.event.thread_ts);
    } else {
      history = [{ role: 'user', content: paramsText }];
    }

    const message = requestToChatGPT(history);
    const new_message = replace_mention_with_username(message);
    sendMessagesToSlack(slackChannel, new_message, params.event.thread_ts || params.event.ts);
  }

  return ContentService.createTextOutput('');
}

function getThreadHistory(channel, thread_ts, max_history_len = 20) {
  const params = {
    'headers': {
      'Authorization': 'Bearer ' + slackBotToken
    }
  };

  const url = 'https://slack.com/api/conversations.replies?ts=' + thread_ts + '&channel=' + channel;
  const response = UrlFetchApp.fetch(url, params);

  const json = JSON.parse(response.getContentText('UTF-8'));
  if (json.ok) {
    const messages = json.messages;
    const results = [];
    for (let i = 0; i < messages.length; i++) {
      const message = messages[i];
      const role = message.user == slackBotId ? 'assistant' : 'user';
      results.push({ role: role, content: message.text });
      if (results.length >= max_history_len) {
        break;
      }
    }
    return results;
  } else {
    throw new Error('Failed to get thread: ' + json.error);
  }
}

function replace_mention_with_username(text) {
  const mentions = text.match(/<@\w+>/g);
  let replaced_text = text;
  if (mentions !== null) {
    for (let j = 0; j < mentions.length; j++) {
      const mention = mentions[j];
      const userId = mention.match(/<@(\w+)>/)[1];
      const username = getUserName(userId);
      if (username !== null) {
        const pattern = new RegExp(mention, "g");
        replaced_text = replaced_text.replace(pattern, `at:${username}`);
      }
    }
  }
  return replaced_text;
}

function getUserName(userId) {
  const url = "https://slack.com/api/users.info?user=" + userId;
  const params = {
    'headers': {
      'Authorization': 'Bearer ' + slackBotToken
    }
  };
  const response = JSON.parse(UrlFetchApp.fetch(url, params).getContentText('UTF-8'));
  Logger.log(response);
  if (response.ok) {
    return response.user.name;
  } else {
    return null;
  }
}

function sendMessagesToSlack(channel, message, thread_ts) {
  const url = "https://slack.com/api/chat.postMessage";
  var payload = {
    "token": slackBotToken,
    "channel": channel,
    "thread_ts": thread_ts,
    "text": message
  };
  params = {
    "method": 'post',
    'payload': payload
  }
  const response = UrlFetchApp.fetch(url, params);
  return response.ok;
}

function requestToChatGPT(history) {
  const apiUrl = 'https://api.openai.com/v1/chat/completions';
  const headers = {
    'Authorization': 'Bearer ' + openAIApiKey,
    'Content-type': 'application/json; charset=UTF-8'
  };
  const params = {
    'headers': headers,
    'method': 'POST',
    'muteHttpExceptions': true,
    'payload': JSON.stringify({
      'model': 'gpt-4o-mini',
      'messages': history,
      'temperature': 0.7,
    })
  };

  const response = JSON.parse(UrlFetchApp.fetch(apiUrl, params).getContentText('UTF-8'));
  return response.choices[0].message.content;
}

slackBotId, slackBotToken, openAiApiKeyはスクリプト プロパティに保存します。

  • openAIApiKey
    • OpenAI Console画面で取得したKeyをそのままここに貼り付けましょう
  • slackBotId
    • Botの識別ID、以下添付画像のCopy Member IDでコピーされたものをそのまま貼り付けでOK
  • slackBotToken
    • Slack Events APIコンソールから確認OAuth & Permissionsにあるxoxbから始まるものがTOKENです。

全て入れ終わったらGASをデプロイして、Slack上で動作確認をしましょう、レスポンスが正常に返ってこれば成功です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です