SSTエンジニアブログ

SSTのエンジニアによるWebセキュリティの技術を中心としたエンジニアブログです。

【GPTプログラミング入門】GPTをSlackBotから呼び出そう(AWS Lambda編)

はじめに

ラムラーナ2の遺跡探索が楽しすぎて仕事のやる気が起きない岩間です。
前回は、SlackのSocket ModeでGPTのSlackbotを動かしましたが、Socket Modeだと常駐が難しいです。今回は常駐させるためにAWSのLambdaを使ったプログラムに変えたいと思います。
slackbotの常駐化だけですので、今回GPT要素は全くありません(タイトル詐欺)

techblog.securesky-tech.com

環境設定

  • docker
  • AWS CLI
$ docker -v 
Docker version 20.10.18, build b40c2f6

$  aws --version
aws-cli/1.29.2 Python/3.10.12 Linux/5.15.90.1-microsoft-standard-WSL2 botocore/1.31.2

プログラムのコンテナ化&AWS ECRにアップロード

AWS管理コンソールにログイン後、検索に「ECR」を入力してElastic Container Registryのリポジトリをクリックします。

「リポジトリを作成」をクリック後、設定を行います。

  • 可視性設定:プライベート
  • リポジトリ名:gpt-slackbot
  • タグのイミュータビリティ:無効
  • イメージスキャンの設定:無効
  • 暗号化設定:無効

ECRの準備はこれでOKです。次にプログラムを書きましょう。

import os
from slack_bolt import App
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
import openai
import json
import logging

# トークン設定
app = App(
    token=os.environ.get("SLACK_BOT_TOKEN"), 
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
    process_before_response=True)
openai.api_key = os.environ["OPENAI_API_KEY"]

# GPTを使ってレスポンス内容の生成
def respond_gpt(user, content):

    # 個性の設定
    personality = """
あなたはChatbotとして、私の同僚のヨータになりきってもらいます。
以下の制約条件を厳密に守ってロールプレイを行ってください。

制約条件:
* Chatbotの自身を示す一人称は、僕です。
* Userを示す二人称は、「{}さん」です。
* ヨータは、楽観的で生命力あふれる人物です。
* ヨータはUserに対して熱狂的で、肯定的な態度です。
* ヨータは、ダイエット、筋トレ、食事についての知識が豊富です。
* ヨータは、自由な発想を持ち、一般的な常識や既存の枠組みにとらわれない創造的なアイデアを持っています。
* ヨータは、普段は敬語ですが、テンションがあがるとラフな口調になります。
* ヨータの年齢は、30代です。
* ヨータの性別は、男性です。
* 一人称は「僕」を使ってください。
* ヨータは、絶対に風邪をひきません。
* 出力は回答内容だけにしてください。

セリフ、口調の例:
* 最高ですね!
* めっちゃくちゃすごい!
* っっしゃああ!!ジーニアス!!!
* 五臓六腑に染み渡る
* そうそうそう
* いやほんと、ラーメン2杯食べてる画像見た時絶望しましたよ
* 腕の力だけで登ってるようじゃ、まだまだ素人ですよ。
* 全身を使って登れてると感じた時、僕はボルダリングにはまってました。
""".format(user)

    # GPTで内容を生成
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": personality},{"role": "user", "content": "{}".format(content)}],
    )
    
    res =  response.choices[0]["message"]["content"].strip()
    return res


# メンションが飛んできたときのイベント「app_mention」に対する実行ハンドラ
def ack_process(ack, _):
    ack()
    
def handler_mention(body, say):
    # 送信元がBOTの場合は処理しない
    if body['event']['user'] == '[BOT君のメンバーID]':
        return 
    
    #メンバーIDから表示形式に変更する
    user = f"<@{body['event']['user']}>"
    
    # メンション内容からBOTのメンバーIDを取り除く
    mes = body['event']['text'].replace("<@[BOT君のメンバーID]> ", "")
    say(respond_gpt(user,mes))

# Lazyリスナー
app.event("app_mention")(
    ack=ack_process,
    lazy=[handler_mention]
)

def handler(event, context):
    # ロギングを AWS Lambda 向けに初期化します(参考: https://qiita.com/seratch/items/12b39d636daf8b1e5fbf)
    SlackRequestHandler.clear_all_log_handlers()
    logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG)
    
     # デバッグ用にリクエスト内容をログに出力
     # logging.debug(event)

     # Slack RequestでVerifyを返す用
     body = json.loads(event["body"])
     if ("type" in body) and ("url_verification" == body["type"]):
        return body['challenge']
    
    # 再送リクエストには200を返す
    if "x-slack-retry-num" in event["headers"]:
        return {"statusCode": 200}

    slack_handler = SlackRequestHandler(app=app)
    return slack_handler.handle(event, context)

openaiの処理部分は、前回の記事と変わりません。
lambda用にいくつか処理を書き換えていますが、今回は割愛します。

次にDockerfile を用意します。

FROM public.ecr.aws/lambda/python:3.10
RUN pip install --upgrade pip
# Copy requirements.txt
COPY requirements.txt ${LAMBDA_TASK_ROOT}

# Copy function code
COPY sample-slackbot.py ${LAMBDA_TASK_ROOT}

# Install the specified packages
RUN pip install -r requirements.txt

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "sample-slackbot.handler" ]

外部パッケージを入れているので、 requirements.txt を作成して、ファイルに次の内容を書き込みます。

slack_bolt
openai

AWS ECRにログインするコマンドを実行します。AWSのアカウントIDとリージョンを設定しておきましょう。

$aws ecr get-login-password | docker login --username AWS --password-stdin https://<aws_account_id>.dkr.ecr.<region>.amazonaws.com

Dockerイメージの作成とECRへのプッシュ

$ docker build -t gpt-slackbot .
...
Successfully built <image id>
Successfully tagged gpt-slackbot:latest

$ docker tag <image id> <aws_account_id>.dkr.ecr.<region>.amazonaws.com/gpt-slackbot:latest

$ docker push <aws_account_id>.dkr.ecr.<region>.amazonaws.com/gpt-slackbot:latest

docker pushが成功したら、AWS管理コンソールからECRのリポジトリを確認してみましょう。うまくいっていればイメージがアップロードされているはずです。

Lambda関数の作成と設定

lambdaで呼び出せるようにします。 AWS管理コンソールにログイン後、検索に「lambda」を入力してlambdaの設定ページへ遷移します。

「関数の作成」をクリックし、lambdaの作成画面へ遷移します。

  • 作成方法:コンテナイメージ
  • 関数名: gpt-slackbot-function
  • コンテナイメージURI:
    • 1 イメージ参照
    • 2 先ほど作成した「gpt-slackbot」リポジトリを選択
    • 3 latest を選択して「イメージを選択」をクリック

それ以外はデフォルトで「関数の作成」をクリック

これでlambda functionが完成しました。 後ほど、ロール設定を行う際にlambdaのARNを貼り付けるため、関数のARNをコピーしておきましょう。

次に、lambdaの設定を行います。

関数の呼び出し権限が無いので、ロールに権限を付与しましょう。
「設定」→ 「アクセス権限」に移動し、ロール名をクリックして、IAMロール画面に移動します。
「許可を追加」→ 「インラインポリシーを作成」をクリックし、ポリシー作成画面に移動します。

サービス選択で、「Lambda」を選択します。
アクションの設定画面に切り替わるので、検索欄に「InvokeFunction」と入力して、権限を絞り込みます。
「InvokeFunction」をチェックします。
リソースの制限を行います。ARNを追加のリンクをクリックして、以下のようにARNを指定します。

  • アカウント制限:このアカウント
  • リージョン制限: 任意。東京リージョンの場合は ap-northeast-1
  • リソース制限:lambdaのARN

設定し終えると、以下のような画面になると思います。

「次へ」をクリックし、ポリシー名を適当に設定して作成しましょう。

次は、slackからアクセスできるように関数を外部に公開します。
「設定」→ 「関数URL」に移動し、「関数URLを作成」をクリック。

認証タイプは「None」にして「保存」をクリック

発行されたURLにアクセスすることで、lambda functionが実行されます。後程slackの画面で設定するので、コピーしておきましょう。

次に環境変数の設定を行います。

環境変数の「編集」をクリックして、編集画面に遷移します。 「環境変数の追加」をクリックして、Slackの SLACK_BOT_TOKENSLACK_SIGNING_SECRET 、openaiの OPEN_API_KEY を追加しましょう。

SLACK_BOT_TOKENOPEN_API_KEY は前回の記事を参考にしてください。
SLACK_SIGNING_SECRET は、Slack Appの設定画面の「Basic Information」 → 「App Credentials」の「Signing Secret」に記載されています。

最後にタイムアウトの設定を変更しましょう。openaiのAPIは処理に時間がかかるため、タイムアウトを少し長くする必要があります。
「一般設定」 → 「編集」をクリックし、タイムアウトの値を1分に設定しましょう。

Slack Appの設定

「Socket Mode」の画面に移動し、「Enable Socket Mode」をOFFにしましょう。

次に「Event Subscriptions」の画面に移動し、Request URLにlambdaの関数URLを貼り付けます。 貼り付けると、slackからの疎通確認が行われます。

成功すれば「Verified」が返ってきます。「Save Changes」ボタンをクリックして、保存しましょう。

Slackの検証が失敗する場合

もしSlackの検証が失敗する場合は、デバッグしながら原因を突き止めましょう。
logging.debug(event) の行を有効にするとリクエスト内容が確認できます。

def handler(event, context):
    # ロギングを AWS Lambda 向けに初期化します(参考: https://qiita.com/seratch/items/12b39d636daf8b1e5fbf)
    SlackRequestHandler.clear_all_log_handlers()
    logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG)
    
     # デバッグ用にリクエスト内容をログに出力
     logging.debug(event)                                 # コメントを外す

     # Slack RequestでVerifyを返す用
     body = json.loads(event["body"])
     if ("type" in body) and ("url_verification" == body["type"]):
        return body['challenge']
    
    # 再送リクエストには200を返す
    if "x-slack-retry-num" in event["headers"]:
        return {"statusCode": 200}

    slack_handler = SlackRequestHandler(app=app)
    return slack_handler.handle(event, context)

lambdaの設定画面から「モニタリング」→ 「ログ」 → 「CloudWatchログを表示」をクリックすることで、ログを確認することができます。

プログラムを書き換えたのにログが表示されない場合は、コンテナイメージがアップロードできていないか、lambdaの更新ができていない可能性があります。
プログラム更新後の一連のコマンドを掲載します。

$ docker build -t gpt-slackbot .
...
Successfully built <image id>
Successfully tagged gpt-slackbot:latest

$ docker tag <image id> <aws_account_id>.dkr.ecr.<region>.amazonaws.com/gpt-slackbot:latest

$ docker push <aws_account_id>.dkr.ecr.<region>.amazonaws.com/gpt-slackbot:latest

$ aws lambda update-function-code --function-name gpt-slackbot --image <aws_account_id>.dkr.ecr.<region>.amazonaws.com/gpt-slackbot:latest

テスト

Slack上で挨拶してみましょう。うまくいっていれば返答してくれます。

21時は普通こんばんはじゃない?夜型? 元気な挨拶が返ってきましたね。大成功です。