Skip to content

Instantly share code, notes, and snippets.

@SeitaroShinagawa
Created April 22, 2023 21:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SeitaroShinagawa/46d130c8520b2bbd96327d51c78d23e6 to your computer and use it in GitHub Desktop.
Save SeitaroShinagawa/46d130c8520b2bbd96327d51c78d23e6 to your computer and use it in GitHub Desktop.
ChatGPTをSlack botとして導入するためのGAS実装
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