Cloud Functionsを利用して、SlackからChatGPTを呼び出すSlack Appをつくる

はじめに

ChatGPT APIの練習として、N番煎じだが、SlackからChatGPTを呼び出すSlack Appを実装した。

Appの概要は、メンションで受け取ったメッセージをChatGPT APIにリクエストし、返答を同チャンネルに書き込む。

ふるまいの前提となるようなキャラクター、文脈も設定できるので、あなた好みのChatBotを実装しよう!

キャラクター設定した返答例

シンディ

シンディ*1Waifu Labs - Magical Anime Portraits で作成した。生成AIに頼りきっている。

Cloud Functions

Cloud Functions  |  Google Cloud

CPU .167 vCPU
メモリ 256 MB
リージョン asia-northeast1
ランタイム  PHP 8.1

Slack Appをつくる

Guided tutorials | Slack

必要な権限は下記の2つ。 - app_mentions:read - chat:write

またCloud Functionデプロイ後にFunctionのURLをEvent SubscriptionsのRequest URLに設定する。

Subscribe to bot eventsにはapp_mentionsを設定する。

Slackからのイベントを受け取るCloud Functionの実装

function execute(ServerRequestInterface $request): ResponseInterface
{
    $params = json_decode((string)$request->getBody(), true);

    if ($_ENV['SLACK_VERIFY_TOKEN'] !== $params['token']) {
        return new Response(403, [], 'invalid token');
    }

    // Slackの初期設定で利用するバリデーションリクエスト対応
    if ($params['type'] === 'url_verification') {
        return new Response(200, [], $params['challenge']);
    }

    // SlackのEvents APIのタイムアウトになり、再リクエストがくることがある
    // 今回は再リクエストの場合はすぐに200返し、完了させ、再々リクエストがこないようにする
    if (isset($request->getHeader('X-Slack-Retry-Num')[0])) {
        return new Response(200, ['X-Slack-No-Retry' => 1], 'No need to resend.');
    }

    if ($params['type'] === 'event_callback' && $params['event']['type'] === 'app_mention') {
        $client = new Client();
        $message = preg_replace('/<@[A-Z0-9]+>/', '', $params['event']['text']);
        $chatGPTResponse = sendRequestToChatGPT($client, $message);
        postMessageToSlack($client, $params['event']['channel'], $chatGPTResponse);

        return new Response(200, [], 'OK');
    }

    return new Response(400, ['X-Slack-No-Retry' => 1], "type:{$params['type']} This type is not supported.");
}

We wait longer than 3 seconds to receive a valid response from your server.

Using the Slack Events API | Slack

Slackは上記のようにイベント処理を3秒でタイムアウトし、失敗とする仕様になっている。

このためChat GPTのAPIが少し時間がかかるとタイムアウトしてまい、リトライが発生する。

そうするとSlackに複数メッセージが書き込まれてしまうので、ここへの対応が必要である。

解決方法は非同期処理にしたり、色々とあるが、今回はすこし強引にリトライリクエストであれば、SlackにOKを伝えてしまうかたちにした。

    // SlackのEvents APIのタイムアウトになり、再リクエストがくることがある
    // 今回は再リクエストの場合はすぐに200返し、完了させ、再々リクエストがこないようにする
    if (isset($request->getHeader('X-Slack-Retry-Num')[0])) {
        return new Response(200, ['X-Slack-No-Retry' => 1], 'No need to resend.');
    }

より正確に行うには X-Slack-Retry-Reason にリトライ理由が記述されているので、タイムアウトのみOKを返すべきである。

ChatGPTへ質問を送る

function sendRequestToChatGPT(Client $client, string $message): string
{
    $url = 'https://api.openai.com/v1/chat/completions';
    $systemMessage = 'ふるまいの前提となるような設定、文脈を記入する';

    $payload = [
        'model' => 'gpt-3.5-turbo',
        'max_tokens' => 2048,
        'temperature' => 1.0,
        'messages' => [
            ['role' => 'system', 'content' => $systemMessage],
            ['role' => 'user', 'content' => $message]
        ]
    ];

    $response = $client->post($url, [
        'headers' => [
            'Authorization' => 'Bearer ' . $_ENV['CHAT_GPT_API_KEY'],
            'Content-Type' => 'application/json',
        ],
        'body' => json_encode($payload)
    ]);

    $result = json_decode((string)$response->getBody(), true);

    return $result['choices'][0]['message']['content'];
}

max_tokens は生成されるトークンの最大数、temperature はランダム度合いで0から2の範囲で設定する。

他のパラメータなどは下記を参照。

OpenAI API

Slackに対するレスポンスの生成

function postMessageToSlack(Client $client, string $channel, string $message): ResponseInterface
{
    $slackApiUrl = 'https://slack.com/api/chat.postMessage';
    $options = [
        'headers' => [
            'Authorization' => 'Bearer ' . $_ENV['SLACK_API_TOKEN'],
            'Content-Type' => 'application/json',
        ],
        'body' => json_encode([
            'channel' => $channel,
            'text' => $message,
        ]),
    ];

    return $client->post($slackApiUrl, $options);
}

Slack API経由で返答内容を送る。

SLACK_API_TOKENにはBot User OAuth Tokenを設定する。

デプロイ

gcloud functions deploy my-function \
--gen2 \
--region=asia-northeast1 \
--runtime=php81 \
--source=. \
--entry-point=execute \
--trigger-http \
--allow-unauthenticated \
--env-vars-file .env.yaml \
--timeout=600

Cloud Functionのデフォルトのタイムアウトは60秒で設定されており、ChatGPTのAPIを呼び出す関係で、60秒だと心もとないため、デプロイ時に設定する。

ChatGPTと対話しながらの実装

はじめはGASで手軽にと思いながら、ChatGPTと対話しながら、実装していた。

感覚としては60-70点くらいのコードを出してくれて、そこから修正していくかたちで実現できた。

その後、GASがリクエストのHeaderを取得できず、Slackのリトライへの対応がやりづらかったため、Cloud Functionsに移行した。

移行の際にGASのコードをCloud FunctionsのPHPで動作するようにChat GPTに書き換えてもらった。

こちらの書き換えは80点くらいで、こちらのが感覚的にはそのまま使えるコード比率が高かった。

これはChatGPTの得意不得意もあるかもしれないが、書き換えのが明確な指示だからなのかなと考えている。

最初のGASのコードも自分が適切な入力を行えば、おそらく同じレベルのコードを出力してもらうことはできただろう。

ただいきなり詳細の言語化を頑張って、一発でいいアウトプットを狙うより、

とりあえず雑な指示を送って、そこから詳細な指示を繰り返したり、自分で書き換えたりしていくのが、使い方としては正しいだろう。

*1:個人サービスはAからアルファベット順で始まる食べ物としていたが、今回はキャラクターとしたく、Cから始める人名となった。AはAsparagus 、BはBlueberry