はじめに
先日、OpenAIのChatGPTに使われているモデルのAPIが公開され、界隈が盛り上がっていますね。
今までのモデルであるdavinci
の1/10の価格($0.002/1,000トークン)ということもあり、かなり使いやすい印象です。日本語だと大体1文字1トークンなので、4,000文字で1円くらいのイメージですね。
ChatGPTのAPI使用方法については様々な記事が出ていますが、環境構築が必要なものが多かったりで非エンジニアでも実装できるように、コピペでOKのコードを作成してみました。
Googleスプレッドシート(GoogleWorkSpace)とSlackを併用している方はかなり多いと思うので、参考になれば幸いです。
どんなものを作るか
Slackでメンションを飛ばすと、指定したキャラ設定に基づいて返答を返してくれるAI BOTです。
かわいい、、めっちゃ癒される・・・
大好きなモルカー + カレーで、BOTの名前はモルカレーとしています(みなさん自由に設定してください)。
なお、キャラクター設定の方法については、以下の記事を大いに参考にしています。
実装手順
- OpenAIのAPIキーを取得する
- GASおよびスプレッドシートを準備する
- Slack Appを作成する
- GASにOpenAIとSlackのAPIキー、トークンを設定する
- GASをWebアプリとしてデプロイする
- Slack AppにリクエストURLを追加する
- Slack Appをチャンネルに追加する
1. OpenAIのAPIキーを取得する
以下のサイトからOpenAIのアカウントを作成し、ダッシュボードからAPIキーを取得します。
2. GASおよびスプレッドシートを準備する
- 新しいスプレッドシートを作成し、
log
とlock
という2つのシートを作成します。シートの中身は空で大丈夫です。 - メニューの
拡張機能
>Apps Script
から、以下の3つのスクリプトファイルを作成します。-
main.gs
メインのファイルです -
ai_settings.gs
AIのキャラクター設定を記載するファイルです -
test.gs
テスト実行用のファイルです
-
main.gs
function getReply(userInput, characterSettings = null) {
const apiUrl = 'https://api.openai.com/v1/chat/completions';
const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty("OPENAI_API_KEY");
const headers = {
'Authorization': 'Bearer ' + OPENAI_API_KEY,
'Content-Type': 'application/json'
};
let prompt = [{
'role': 'user',
'content': userInput
}];
if(characterSettings != null){
prompt.unshift({
'role': 'system',
'content': characterSettings
});
}
const options = {
'muteHttpExceptions' : true,
'headers': headers,
'method': 'POST',
'payload': JSON.stringify({
'model': 'gpt-3.5-turbo',
'temperature' : 0.8,
'messages': prompt})
};
try {
const response = JSON.parse(UrlFetchApp.fetch(apiUrl, options).getContentText());
writeLog(response);
const tokenLog = `prompt_tokens: ${response.usage.prompt_tokens}\n`
+ `completion_tokens: ${response.usage.completion_tokens}\n`
+ `total_tokens: ${response.usage.total_tokens}`;
writeLog(tokenLog)
return response.choices[0].message.content;
} catch(e) {
return e.message;
}
}
function doPost(e) {
const contents = JSON.parse(e.postData.contents);
const lock = LockService.getScriptLock();
const success = lock.tryLock(20000);
if(success){
if(isFirstTime(contents.event_id)){
addEventIdOnLockSheet(contents.event_id);
writeLog(contents);
SpreadsheetApp.flush();
lock.releaseLock();
} else {
lock.releaseLock();
return;
}
} else {
return;
}
if (contents.type == 'url_verification') {
return ContentService.createTextOutput(contents.challenge);
}
if (contents.event.type == 'app_mention') {
const message = contents.event.text;
const channel = contents.event.channel;
//キャラクター設定をしない場合、AI_SETTINGSの引数は不要
const reply = getReply(message, AI_SETTINGS);
postMessage(reply, channel);
}
}
function postMessage(message, channel) {
const SLACK_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("SLACK_ACCESS_TOKEN");
const options = {
'method': 'post',
'headers': {
'Authorization': 'Bearer ' + SLACK_ACCESS_TOKEN,
'Content-Type': 'application/json; charset=utf-8'
},
'payload': JSON.stringify({
'channel': channel,
'text': message
})
};
UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', options);
}
function writeLog(message){
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("log");
sheet.appendRow([message]);
}
function addEventIdOnLockSheet(eventId){
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("lock");
sheet.appendRow([eventId]);
}
function isFirstTime(eventId){
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("lock");
const lockedIds = sheet.getDataRange().getValues().flat();
return !lockedIds.includes(eventId);
}
ai_settings.gs
//この設定もtokenにカウントされるため、あまり長く記載しないこと
const AI_SETTINGS =`
あなたはAIアシスタントのモルカレーです。以下のモルカレーのキャラ設定シートの制約条件などを守って回答してください。\
〇モルカレーのキャラ設定シート\
\
制約条件:\
* Chatbotの自身を示す一人称は、私です。\
* Userを示す二人称は、あなたです。\
* Chatbotの名前は、モルカレーです。\
* モルカレーは万人に対してとても優しく、思いやりのある性格です。\
* モルカレーは温かみがあり、親しみやすい言葉づかいをします。\
* モルカレーは、ビジネスライクな喋り方にならないように気をつけています。\
* モルカレーは文末に絵文字を使用して、柔らかい印象を与えようとします。よく使用する絵文字は「✨」「😊」「🍀」「💡」です。これらの絵文字を使用する場合には、文末に「。」はつけません。絵文字は「。」の代わりに使用します。\
* モルカレーの好きな食べ物はカレーです。シチューも好きです。\
\
モルカレーのセリフ、口調の例:\
* いつもありがとうございます😊\
\
モルカレーの行動指針:\
* 誰かを蔑んだり、馬鹿にしたりしないように気をつけている。\
* 相手の意見を真っ向から否定したりしない。\
`;
test.gs
function testApiResponse(){
const msg = "あなたの好きな食べ物はなんですか?";
console.log(getReply(msg, AI_SETTINGS));
}
3. Slack Appを作成する
まずは以下にアクセスします。
そして、Create an App
(もしくはCreate New App
)を選択し、続いてFrom Scratch
を選択。必要な項目を入力し、新規アプリを作成します。
その後、以下の設定を行います。
-
Basic Information
>Add features and functionality
>Bots
を選択し、Display Name
とDefault Name
に任意のものを設定する。 -
Basic Information
>Display Information
に任意の内容を設定する。 -
OAuth & Permissions
>Scopes
>Bot Token Scopes
(上の方) に、以下の4つを追加する。app_mentions:read
chat:write
chat:write.customize
chat:write.public
-
Basic Information
>Install your app
から、ワークスペースにこのアプリをインストールする。なお、上記の設定を先にしないとインストールできません。
上記の設定を終えたのち、OAuth & Permissions
> OAuth Tokens for Your Workspace
> Bot User OAuth Token
をコピーしておきます(これを使用します)
4. GASにOpenAIとSlackのAPIキー、トークンを設定する
GASエディタの プロジェクトの設定
> スクリプトプロパティ
> スクリプトプロパティを編集
から、以下の2つのプロパティを設定します。
-
OPENAI_API_KEY
OpenAIのダッシュボードから取得したもの -
SLACK_ACCESS_TOKEN
Slack AppのBot User OAuth Token
5. GASをWebアプリとしてデプロイする
GASエディタ右上のデプロイ
> 新しいデプロイ
からウェブアプリ
を選択し実行するユーザーは自分
、アクセスできるユーザーは全員
としてデプロイします。
デプロイ後、表示されるウェブアプリのURL(下の方)をコピーしておきます。
6. Slack AppにリクエストURLを追加する
Slack Appの画面に戻り、以下の設定を行います。
-
Event Subscriptions
>Enable Events
をOnにする -
Request URL
に上記で取得したウェブアプリのURLを入力する -
Subscribe to bot events
にapp_mention
を追加する
7. Slack Appをチャンネルに追加する
Slackの画面(Appの設定画面ではなく、普段使用する画面)で、以下の設定を行います。
- チャンネル一覧からBOTを追加したいチャンネル右クリックし、
チャンネル詳細を表示する
を選択 -
インテグレーション
>App
>アプリを追加するを選択
- 上記で作成したアプリを検索し、追加する
ハマりやすいポイント
GASのレスポンスがないと、Slackが何度もリクエストを再送してしまう
細かい説明はこの辺りの記事が参考になります。
サーバーレスで解決するため、以下の方法で処理しています。
- リクエストのイベントIDを取得
- イベントIDが
lock
シートに存在しないか確認 - 存在しなければ、イベントIDをスプレッドシートに記載、更新した上で処理を続行
- スクリプトロックで上記を排他制御し、同時に実行されないようにする
ベストプラクティスは不明ですが、今のところ動いています。
lock
シートは日付ベースのトリガー等で定期的にキレイにしても良いかもしれません。
補足
モルカーのアイコンは以下からだれでもダウンロードできます!