-
-
Save SeitaroShinagawa/46d130c8520b2bbd96327d51c78d23e6 to your computer and use it in GitHub Desktop.
ChatGPTをSlack botとして導入するためのGAS実装
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function doPost(e, _IsTest=false) { | |
//受信データをパース (doPost()のデバッグ時(_IsTest=true)は入力が異なる) | |
const params = (_IsTest) ? JSON.parse(e) : JSON.parse(e.postData.getDataAsString()); | |
//Challenge認証用 | |
if (params.type === 'url_verification') { | |
return ContentService.createTextOutput(params.challenge); | |
} | |
// イベント再送回避 | |
// キャッシュに積まれていれば処理済 | |
// 未処理の場合はキャッシュに積む(有効期間5m) | |
const event_id = params.event_id; | |
const cache = CacheService.getScriptCache(); | |
const isProcessed = cache.get(event_id); | |
if (isProcessed) { | |
return; | |
} | |
cache.put(event_id, true, 601); | |
//サブタイプが設定されたイベントは回避 | |
if('subtype' in params.event) { | |
return; | |
} | |
const botId = PropertiesService.getScriptProperties().getProperty('slackBotId'); | |
const channel = params.event.channel; | |
const text = params.event.text; | |
// 以下、メインの処理部分 | |
// 基本方針:botにメンションした時だけ返事をする、返事はスレッドに | |
// ユーザ設定 | |
const filler_message = '(考え中)'; // 生成文が長い場合は時間がかかるので、ユーザに返答可能を示すために先に送信するメッセージを設定 | |
const max_history_len = 10; // スレッドの履歴の読み込み制限 | |
//event.type.app_mentionがうまくいかないのでtext.includesで代用 | |
if (text.includes(`<@${botId}>`) && params.event.user !== botId) { | |
// スレッドが存在しているかどうかをチェック | |
// スレッドが存在しない場合はtime stamp(ts)を利用してsendSlackで新規作成 | |
const thread_ts = params.event.thread_ts || params.event.ts; | |
// スレッドが存在する場合は履歴を抽出、スレッドが存在しない場合は履歴なし | |
if (params.event.thread_ts) { | |
history = getThreadHistory(channel, thread_ts, filler_message, max_history_len) | |
} else { | |
history = [{role: 'user', content: text}] | |
}; | |
// 返答可能を示すための頭出しメッセージ送信 | |
sendSlack(params.token, channel, filler_message, thread_ts); | |
// 本メッセージ送信 | |
const message = requestChatGPT(history); // ChatGPTでテキスト生成 | |
const new_message = replace_mention_with_username(message); // テキスト中のメンション表現'<@\w+>'をすべて対応する「at:ユーザ名」に置換 (誤爆回避のため) | |
sendSlack(params.token, channel, new_message, thread_ts); | |
} else { | |
Logger.log('bot message'); | |
} | |
return; | |
} | |
// 指定されたスレッドの履歴を抽出する | |
// 抽出する履歴はbotのfiller_messageを除いた履歴とする | |
function getThreadHistory(channel, thread_ts, filler_message, max_history_len=10) { | |
const slackToken = PropertiesService.getScriptProperties().getProperty('slackBotToken'); | |
const params = { | |
'headers': { | |
'Authorization': 'Bearer ' + slackToken | |
} | |
}; | |
const url = 'https://slack.com/api/conversations.replies?ts='+thread_ts+'&channel='+channel; | |
const response = UrlFetchApp.fetch(url, params); | |
// Logger.log('getBotMentionedMessages response' + response); | |
const json = JSON.parse(response.getContentText('UTF-8')); | |
if (json.ok) { | |
const messages = json.messages; | |
const botId = PropertiesService.getScriptProperties().getProperty('slackBotId'); | |
const results = []; | |
for (let i = 0; i < messages.length; i++) { | |
const message = messages[i]; | |
// (option) botの発話(ただしfiller_messageは除く)とユーザがbotにメンションした発話を履歴として取得 | |
// if ((message.user == botId || message.text.includes(`<@${botId}>`)) && message.text !== filler_message) { | |
// filler_messageを除いた発話履歴の取得 | |
if (message.text !== filler_message) { | |
const role = message.user == botId ? '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) { | |
// メッセージテキスト中のメンション表現'<@\w+>'にマッチする文字列を全て取得し配列に格納 | |
const mentions = text.match(/<@\w+>/g); | |
// mentionsがnullではない場合は、mentionsの各要素に対してユーザ名を取得し、テキスト中のメンション表現を全てユーザ名に置き換える | |
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]; // メンション表現からユーザIDを取得 | |
const username = getUserName(userId); // ユーザIDからユーザ名を取得 | |
if (username !== null) { | |
const pattern = new RegExp(mention, "g"); // 正規表現オブジェクトを生成 | |
replaced_text = replaced_text.replace(pattern, `at:${username}`); // テキスト中のメンション表現を全て対応するユーザ名に置き換える | |
} | |
} | |
} | |
return replaced_text; | |
} | |
// ユーザIDから対応するユーザ名を取得 | |
function getUserName(userId) { | |
const slackToken = PropertiesService.getScriptProperties().getProperty('slackBotToken'); | |
const url = "https://slack.com/api/users.info?user=" + userId; | |
const params = { | |
'headers': { | |
'Authorization': 'Bearer ' + slackToken | |
} | |
}; | |
const response = JSON.parse(UrlFetchApp.fetch(url, params).getContentText('UTF-8')); | |
// Logger.log(response); | |
if (response.ok){ | |
return response.user.name; // Display name | |
// return response.user.real_name; // Full name | |
} else { | |
return null; | |
} | |
} | |
// SlackBotsを通してメッセージを指定されたスレッドに送信する | |
// スレッドが無ければ自動的に新しく作成する(thread_ts = params.event.thread_ts || params.event.ts) | |
function sendSlack(token, channel, message, thread_ts) { | |
const slackToken = PropertiesService.getScriptProperties().getProperty('slackBotToken'); | |
const url = "https://slack.com/api/chat.postMessage"; | |
var payload = { | |
"token" : slackToken, | |
"channel" : channel, | |
"thread_ts": thread_ts, | |
"text" : message | |
}; | |
params = { | |
"method": 'post', | |
'payload': payload | |
} | |
const response = UrlFetchApp.fetch(url, params); | |
return response.ok; | |
} | |
// ChatGPT APIによるテキスト生成 | |
function requestChatGPT(history) { | |
const apiKey = PropertiesService.getScriptProperties().getProperty('openApiKey'); | |
const apiUrl = 'https://api.openai.com/v1/chat/completions'; | |
//リクエストデータの設定 | |
const headers = { | |
'Authorization':'Bearer '+ apiKey, | |
'Content-type': 'application/json; charset=UTF-8' | |
}; | |
// history = [{'role': 'user', 'content': text}], | |
const params = { | |
'headers': headers, | |
'method': 'POST', | |
'muteHttpExceptions': true, | |
'payload': JSON.stringify({ | |
'model': 'gpt-3.5-turbo', | |
'messages': history, | |
'temperature': 0.0, | |
}) | |
}; | |
//リクエストを送信し、結果取得 | |
const response = JSON.parse(UrlFetchApp.fetch(apiUrl, params).getContentText('UTF-8')); | |
return response.choices[0].message.content; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment