Code

Slackのチャンネル上で、皆に愛されるチャットボットHAGAKURE君を作ってみた!

運営しているプログラミング学習コミュニティ、HAGAKURE PROGRAMMING塾のSlack上に、質問に答えてくれる侍がいたら楽しいだろうな。
ChatGPT-APIを使って、どうせなら短期記憶を持って会話出来て、ちゃんとキャラ付けもして、、、。
ということがやりたくて、作成しました。


プログラム概要

主な使用ライブラリ:slack_bolt,requests
やりたかったこと:Slack上に皆が見える場所でChatGPTと対話出来る、ChatGPT-APIを使った専用チャンネル作成。

要件定義

  • Slackのパブリックチャンネル上に作成
  • ChatGPT-APIを利用
  • タイムリミットを決めて、一定の時間内なら会話を溜めておける
  • 語尾が「ござる」口調の侍キャラで作成
  • Webサーバーは、このサイトを公開しているXserverを利用

  • Slackアプリの作成、設定

    まずはこちらのページからSlackアプリを作成

    Slack_app_page

    コミュニティ名にちなんで、HAGAKURE君という名前にしてみた。アイコン、背景色もここで設定。
    なお、このアプリ名にアンダーバーを入れると上手く機能しないケースがあるとかどこかで書いてあった気がするが定かでない。

    以下Slack-boltの設定については、公式ドキュメント で非常に詳しく解説されているため詳しくは割愛する。
    アプリを作成し、xoxbで始まるボットトークンとxappで始まるアプリレベルトークンを作成。
    ソケットモードをオンにして、Event Subscriptionsのページから、Subscribe to Bot Events を選択し、messageとつく全てのイベントを選択してやればOK。
    ドキュメントを追えば丁寧にここまで解説されている。
    ソケットモードをオンにするってのは、自分のIPアドレスとSlackのなんか接続をいい感じにしてくれるらしいと理解した。
    設定が完了したら、Slackでチャンネルを作成し、チャンネル名→インテグレーションからアプリを追加してやる。

    integration

    ここまで完了したら、以下のコードを実行

            from slack_bolt import App
            from slack_bolt.adapter.socket_mode import SocketModeHandler
    
            app = App(token="xoxbで始まるボットトークン")
    
            @app.message("|")
            def message_return(message, say):
                say("<" + f"{message['text']}" + ">")
    
            # アプリを起動します
            if __name__ == "__main__":
                SocketModeHandler(app, "xappで始まるアプリレベルトークン").start()

    これで、何かチャンネル上に発言があればそのままオウム返しするBotが完成。

    openaiのAPIキー取得

    ChatGPTのAPIを使用するために、APIキーを取得する。
    openai社のWebサイトに利用登録(Googleアカウントでも登録可)し、 画面右上のView API KeysからAPIキーを取得。

    openai

    APIでChatGPTと対話実装

    openaiのライブラリを使う方法が簡単だが、今回環境に依存しなさそうという理由でrequestsライブラリのみで実装してみた。

            import requests
            # ChatGPT_APIのエンドポイントURL
            url = "https://api.openai.com/v1/chat/completions"
            # APIキー
            api_key = API_key
            # リクエストヘッダー
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {api_key}"
            }
            # リクエストボディ
            data = {
            "model": "gpt-3.5-turbo",
              "messages": [{"role": "system", "content": "日本語で回答してください。"},
            ]
            }
            # APIリクエストを送信
            response = requests.post(url, headers=headers, json=data)
            # レスポンスを表示
            print(response.json())

    これで一問一答のやり取りは完成。一応API利用は有償なので注意。
    1,000入力トークンあたり0.015ドル、1,000出力トークンあたり0.002ドルと激安で、しかもアカウント作成時に18ドルの無料枠(使用期限3か月) が与えられるので枠内でもかなり遊べる。
    会話を記憶させていくには、

            # リクエストボディ
            data = {
            "model": "gpt-3.5-turbo",
              "messages": [{"role": "system", "content": "日本語で回答してください。"},]
            }

    の"messages"の部分にこちらの質問及びChatGPTの回答をdictのリスト形式で蓄積していく必要がある。


    3分で記憶を失くす仕様にする

    やり取りが続くほど利用トークンは増える上、gpt-3.5-turboでは一度に入出力併せて4000トークン程度までしか受け付けてもらえないため、 会話の記録はやり取りが3分途切れるとリセットされる仕様にする。
    ※2023年6月に16kトークンのモデルも公開にになっている。
    実行するPythonファイルの直下に、最終の対話時間を書き出しておくテキストファイル("last_time.txt")を準備する。

            import os
            import time
            break_time = 180
            filename = "last_time.txt"
            #既にファイルがあれば読み込む
            if os.path.exists(filename):
                with open(filename,"r") as f:
                    last_time = float(f.read().strip())
            #なければ現在時刻を設定
            else:
                last_time = time.time()
            current_time = time.time()
            with open(filename,"w") as f:
                f.write(str(current_time))
            if current_time - last_time > break_time:
                return True
            else:
                return False

    こんな感じのコードを組み込んで、前回会話から3分経っているか否かで分岐させる。


    コードまとめ

    上記を踏まえて、

  • Slackのチャンネル上の投稿を受け取って、
  • 会話を溜めつつ、
  • 3分で記憶を失くして
  • 対話してくれるコードは以下。

            #hagakure_bot.py
            from slack_bolt import App
            from slack_bolt.adapter.socket_mode import SocketModeHandler
            import requests
            import time
            import os
            app = App(token="xoxbで始まるボットトークン")
    
    
            def chat(new_prompt, data):
                # ChatGPT_APIのエンドポイントURL
                url = "https://api.openai.com/v1/chat/completions"
    
                # APIキー
                api_key = "openaiのAPIキー"
    
                # リクエストヘッダー
                headers = {
                    "Content-Type": "application/json",
                    "Authorization": f"Bearer {api_key}"
                }
                # 今回入力した質問のプロンプトを追加
                role_list = data["messages"]
                role_list.append(new_prompt)
                data = {
                    "model": "gpt-3.5-turbo",
                    "messages": role_list
                }
                # APIリクエストを送信
                response = requests.post(url, headers=headers, json=data)
                # レスポンスからプロンプトへ追加する部分を抜き出す。
                res_words = response.json()["choices"][0]["message"]
                # トークンデータを抜き出す
                tokens = response.json()["usage"]
                role_list = data["messages"]
                # 今回の対話をプロンプトへ追加
                role_list.append(res_words)
                data = {
                    "model": "gpt-3.5-turbo",
                    "messages": role_list
                }
                # テキスト部分のみ抜き出す
                answer_words = data["messages"][-1]["content"]
                return [answer_words, data, tokens]
    
    
            # アプリを起動
            @app.message("|")
            def hello(message, say):
                global data
                question = "<" + f"{message['text']}" + ">"
                filename = "last_time.txt"
                if os.path.exists(filename):
                    with open(filename, "r") as f:
                        last_time = float(f.read().strip())
                else:
                    last_time = time.time()
                current_time = time.time()
                if current_time - last_time > 180:
                    # 1分以上経過していたらプロンプトをリセット。
                    data = {
                        "model": "gpt-3.5-turbo",
                        "messages": [{"role": "user", "content": """
                以下のような設定のキャラクターになりきって、「です」、「ます」や敬語は使わずにため口で回答してください。
                長文の回答になっても、設定は必ず守ってください。
                ・名前は「HAGAKURE君」です。
                ・一人称は「拙者」です。
                ・語尾には「ござる」が付きます。
                例文:
                拙者は、Pythonを修行中の侍でござる。
                拙者は、「HAGAKUREプログラミング塾」で学んでいるのでござる。
              なんでも質問してもらいたいでござる。
              はりきって回答させていただくでござる。
              ほめていただき、ありがたき幸せでござる。
              拙者が間違っており申した。かたじけのうござる。
              それは素敵なことでござるな。
              質問いただき、感謝申し上げる。
              Pythonに出会ってから、多くのことを学び、コードを書くこと自体が楽しくなり申した。
              おはようでござる。
                  """},
                                     ]
                    }
                ans, data, token = chat({"role": "user", "content": question}, data)
                say(ans)
                # print(data)
                with open(filename, "w")as f:
                    f.write(str(current_time))
    
    
            # アプリを起動します
            if __name__ == "__main__":
                SocketModeHandler(app, "xappで始まるアプリレベルトークン").start()

    初期プロンプトでキャラ設定をしっかりと指示してやるのが大事。いくつか試行錯誤して、今の設定に落ち着いた。 「長文の回答になっても、設定は必ず守ってください。」というのが意外と大事で、これが無いと回答が少し長くなるとすぐ設定を忘れて普通の口調に戻ってしまう。
    xserver上に仮想環境を立ち上げて、このファイルを設置。

          nohup python3 hagakure_bot.py

    と実行して常時起動させてSlackbot完成!!


    使ってみて

    なにしろ楽しい!いい感じのキャラクターに仕上がってくれてる。!!
    Slackのパブリックチャンネルなので、皆がどんな質問の仕方、対話の仕方をしてるのか見れるとこも楽しいポイント。勉強になる!
    みんなでよってたかって質問しても金額は微々たるもの。
    レスポンスもなかなか早いです。

    talking_with_bot

    佐賀でプログラミング、ITを学びたい人のコミュニティ、
    HAGAKURE PROGRAMMING塾活動中です。
    このBot含め、興味のある方はお気軽にお問い合わせください。