<目次>
OpenAIで返答するSlackボットで「会話履歴」を加味する方法
やりたいこと
前提条件
概要
サンプルプログラム
動かし方
OpenAIで返答するSlackボットで「会話履歴」を加味する方法
やりたいこと
前提条件
概要
●【ポイント】「会話履歴」を加味した応答を実現するために、以下の実装を行った
サンプルプログラム
●メイン処理:0774_app.py
from slack_bolt import App from slack_sdk import WebClient # Flaskクラスのインポート from flask import Flask, request from slack_bolt.adapter.flask import SlackRequestHandler #ソケットモード用 from slack_bolt.adapter.socket_mode import SocketModeHandler # 環境変数読み込み import env # OpenAI import openai import pkg_resources import respond_to_message # モードに応じて書き換え BOT_USER_ID = env.get_env_variable("BOT_USER_ID") # Botトークン(Flask) WEBAPPS_SLACK_TOKEN = env.get_env_variable("WEBAPPS_SLACK_TOKEN") WEBAPPS_SIGNING_SECRET = env.get_env_variable("WEBAPPS_SIGNING_SECRET") # Botトークン(ソケットモード) SOCK_SLACK_BOT_TOKEN = env.get_env_variable("SOCK_SLACK_BOT_TOKEN") SOCK_SLACK_APP_TOKEN = env.get_env_variable("SOCK_SLACK_APP_TOKEN") # OpenAIのAPI設定 openai.api_key = env.get_env_variable('OPEN_AI_KEY') openai_version = pkg_resources.get_distribution("openai").version # モード入れ替え(WebAPサーバ実行=Flask/ローカル実行=ソケットモード) def app_mode_change(i_name): if i_name == "__main__": return App(token=SOCK_SLACK_BOT_TOKEN) else: return App(token=WEBAPPS_SLACK_TOKEN, signing_secret=WEBAPPS_SIGNING_SECRET) # グローバルオブジェクト s_app = app_mode_change(__name__) # Flaskクラスのインスタンス生成 app = Flask(__name__) #app.config['JSON_AS_ASCII'] = False handler_flask, handler_socket = None,None #ソケットーモードの場合のハンドラ設定 if __name__ == "__main__": handler_socket = SocketModeHandler(app=s_app, app_token=SOCK_SLACK_APP_TOKEN, trace_enabled=True) #Flaskでのハンドラー設定 else: handler_flask = SlackRequestHandler(s_app) # Flask httpエンドポイント # 疎通確認用1 @app.route('/', methods=['GET', 'POST']) def home(): return "Hello World Rainbow 2!!" # 疎通確認用2 @app.route("/test", methods=['GET', 'POST']) def hello_test(): return "Hello, This is test.2!!" #イベント登録されたリクエストを受け付けるエンドポイント @app.route("/slack/events", methods=["POST"]) def slack_events(): # ------------------------------------ # Challenge用 # ------------------------------------ # # Slackから送られてくるPOSTリクエストのBodyの内容を取得 # json = request.json # print(json) # # レスポンス用のJSONデータを作成 # # 受け取ったchallengeのKey/Valueをそのまま返却する # d = {'challenge' : json["challenge"]} # # レスポンスとしてJSON化して返却 # return jsonify(d) # ------------------------------------ # 本番用 # ------------------------------------ return handler_flask.handle(request) @s_app.event("message") @s_app.event("app_mention") def respondToRequestMsg(body, client:WebClient, ack): ack() type = body["event"].get("type", None) print("-------------"+str(type)) # 二重で応答するのを防ぐため、メンションの時のイベントのみ応答対象とする if type == 'app_mention': respond_to_message.respond_to_message(body=body,client=client) # __name__はPythonにおいて特別な意味を持つ変数です。 # 具体的にはスクリプトの名前を値として保持します。 # この記述により、Flaskがmainモジュールとして実行された時のみ起動する事を保証します。 # (それ以外の、例えば他モジュールから呼ばれた時などは起動しない) if __name__ == '__main__': EXEC_MODE = "SLACK_SOCKET_MODE" # Slack ソケットモード実行 if EXEC_MODE == "SLACK_SOCKET_MODE": handler_socket.start() # Flask Web/APサーバ 実行 elif EXEC_MODE == "FLASK_WEB_API": # Flaskアプリの起動 # →Webサーバが起動して、所定のURLからアクセス可能になります。 # →hostはFlaskが起動するサーバを指定しています(今回はローカル端末) # →portは起動するポートを指定しています(デフォルト5000) app.run(port=8000, debug=True)
●Slackイベント解析&応答処理:respond_to_message.py
from slack_sdk import WebClient import conversation_util import env import re import time # OpenAI import openai openai.api_key = env.get_env_variable('OPEN_AI_KEY') model=env.get_env_variable('MODEL') import pkg_resources openai_version = pkg_resources.get_distribution("openai").version # langchain from langchain import schema from langchain.chat_models import ChatOpenAI # from langchain.chat_models import AzureChatOpenAI from langchain.schema import HumanMessage # ロギング import traceback def respond_to_message(body, client: WebClient): try: #----------------------------------- # Slackのイベント情報から各種パラメータを取得 bot_user_id = env.get_env_variable('BOT_USER_ID') ts = body["event"]["ts"] thread_ts = body["event"].get("thread_ts", None) channel = body["event"]["channel"] user = body["event"]["user"] input_text = schema.HumanMessage(content=body["event"].get("text", None)) attachment_files = body["event"].get("files", None) system_message = schema.SystemMessage(content=env.get_env_variable('SYSTEM_MESSAGE')) # やり取りに関するインスタンス生成 conversation_info = conversation_util.ConversationInfoSlack( client=client, bot_user_id=bot_user_id, ts=ts, thread_ts=thread_ts, channel=channel, user=user, human_message_latest=input_text, messages=None, system_message=system_message ) # メッセージ情報の構築 conversation_info.build_messages() print("==============="+str(conversation_info._messages)) # OpenAIからの返答を生成 output_text = generate_response_v2(str(conversation_info._messages)) time.sleep(1) # n秒待機 (実施しないと「The server responded with: {'ok': False, 'error': 'no_text'}」になる) # # Slackに返答 client.chat_postMessage(channel=channel, text=output_text ,thread_ts=ts) except Exception as e: print("-------------------------------------------------") print("======== react_to_msg 例外発生:"+str(e)) traceback.print_exc() def generate_response_v2(prompt) ->str: print("============ generate_response : TOTAL_PROMPT(過去分含む):"+prompt) # 言語モデル(OpenAIのチャットモデル)のラッパークラスをインスタンス化 llm = ChatOpenAI( model = "gpt-3.5-turbo", openai_api_key=env.get_env_variable('OPEN_AI_KEY'), max_tokens=500, temperature=0.5 ) # APIを使用して、応答を生成します # モデルにPrompt(入力)を与えCompletion(出力)を取得する # SystemMessage: OpenAIに事前に連携したい情報。キャラ設定や前提知識など。 # HumanMessage: OpenAIに聞きたい質問 response = llm(messages=[HumanMessage(content=prompt)]) # 文字列から必要なSystemMessageのみを抽出(以下例で言うaaaaaの部分のみ) # [SystemMessage(content='aaaaa', additional_kwargs={}), HumanMessage(content='bbbbb', additional_kwargs={}, example=False)] pattern = r"SystemMessage\(content='([^']*)'" matches = re.findall(pattern, response.content) content = "" if matches: content = matches[0] else: content = response.content print("============ generate_response : COMPLETION:"+str(response.content)) return content
●やり取りに関する情報を保持するクラス:conversation_util.py
import re from langchain import schema from slack_sdk import WebClient class ConversationInfo: """ 親クラス 入力データ(会話履歴+System Prompt+Human Prompt)をクラス 開発者は当クラスを継承し、状況に応じたInput情報生成を行う。 """ def __init__( self, messages:list[schema.BaseMessage]=None, **kwargs) -> None: # 「_変数」は非公開orプライベート変数 self._messages = messages # 以降、@property(Decorator)で、ゲッター(getter)メソッドを定義。 # 以降、@messages.setter(Decorator)で、セッター(setter)メソッドを定義。 @property def messages(self) -> list[schema.BaseMessage]: """ メッセージ履歴のgetter Returns: list: メッセージ履歴のリスト """ return self._messages @messages.setter def messages(self, value:list[schema.BaseMessage]): """ メッセージ履歴のsetter Args: value (list[schema.BaseMessage]): メッセージ履歴 """ self._messages = value def build_messages(self, **kwargs): """ messagesを構築するための処理を実装するメソッド。 必須でなく、メンバ変数「messages」を直接セットする事も可能 """ pass def remove_mention_str(remove_user_id:str, target_text:str) -> str: """ 文頭/文末のボットへのメンション文字列"<@Uxxxx>"を除外する Args: remove_user_id (str): 除外したいボットのユーザーID target_text (str): 除外を行いたいテキスト文字列 Returns: str: メンション文字列が除外された新しいテキスト文字列 """ # 例:^<@UAAAAAAAAAA.*?>|<@UAAAAAAAAAA.*?>$ pattern = f'^<@{remove_user_id}.*?>|<@{remove_user_id}.*?>$' # target_textのうち、パターンにマッチした部分を除去 ret = re.sub(pattern, '', target_text.strip()) return ret def get_thread_history( client:WebClient, bot_user_id:str, ts: str, thread_ts:str, channel:str) ->list: """ スレッドメッセージの履歴も含めた、一連のやり取りの履歴を取得する。 (親メッセージ+conversation_repliesのlimitの件数まで取得) Args: bot_user_id (str): ボットのユーザーID。メッセージが"ai" or "human"を判定するために必要。 client (WebClient) :Slackとインタラクトする為のWebClientクラスのインスタンス bot_user_id (str) :ボットのユーザーID ts (str) :ユーザーメッセージのタイムスタンプ thread_ts (str) :ユーザーメッセージがスレッドの一部である場合の親メッセージのtimestamp channel (str) :チャンネルID Returns: list: schema.HumanMessage または schema.SystemMessageが格納されたlist """ history = [] # 会話履歴を取得 resp = client.conversations_replies( channel=channel, inclusive=False, latest=ts, ts=thread_ts) messages = resp.get("messages") # ボットの応答はAIMessageに、人からの回答はHumanMessageに格納 for message in messages: if message.get("user", "-") == bot_user_id: content=message.get("text", "**** メッセージの取得に失敗しました ****") history.append(schema.AIMessage(content=content)) else: content=message.get("text", "**** メッセージの取得に失敗しました ****") history.append(schema.HumanMessage(content=content)) return history class ConversationInfoSlack(ConversationInfo): """ TaskInputChatを継承した子クラス Slackからのメッセージ(処理依頼)に対して、メッセージ情報を保持するためのクラス """ def __init__(self, client:WebClient, bot_user_id:str, ts:str, thread_ts:str, channel:str, user:str, human_message_latest:schema.HumanMessage=None, messages:list[schema.BaseMessage]=None, system_message:schema.SystemMessage=None, **kwargs) -> None: """ コンストラクタ。 Args: client (WebClient) :Slackとインタラクトする為のWebClientクラスのインスタンス bot_user_id (str) :ボットのユーザーID ts (str) :ユーザーメッセージのタイムスタンプ thread_ts (str) :ユーザーメッセージがスレッドの一部である場合の親メッセージのtimestamp channel (str) :チャンネルID user (str) :投稿したユーザーのSlackユーザーID human_message_latest (schema.HumanMessage) : 最新のユーザーメッセージ (省略可能) messages (list[schema.BaseMessage]) :メッセージ履歴 (省略された場合は、後からsetterから格納するか、build_messages()から構築) schema.BaseMessageクラスのインスタンスのlistを受け取ることができます(Noneのため省略可能) system_message (schema.SystemMessage) : システムメッセージ (省略可能) Notes: - system_message、human_message_latestを設定する事で、build_messages()を呼び、過去スレッド履歴も含めたmessagesが構築できる。 - build_messages()を使用せずに、直接メンバ変数messagesに値を格納する事も可能。 """ # 親クラスのコンストラクタを呼ぶ super().__init__(messages=messages,**kwargs) self.client = client self.bot_user_id = bot_user_id self.ts = ts self.thread_ts = thread_ts self.channel = channel self.user = user self.human_message_latest = human_message_latest self.system_message = system_message def build_messages(self, **kwargs): """ messagesを構築するための処理を実装するメソッド。 必須でなく、メンバ変数「messages」を直接セットする事も可能 """ ret = [] # システムメッセージを追加 if not self.system_message is None: ret.append(self.system_message) # スレッド履歴を追加 if self.thread_ts is not None: thread_history = get_thread_history( client=self.client, bot_user_id=self.bot_user_id, ts=self.ts, thread_ts=self.thread_ts, channel=self.channel ) ret.extend(thread_history) # 最新のメッセージを追加 ret.append(self.human_message_latest) # ボットへの文頭/文末のメンションを除去する # Before例: <@U03U3JE514N>\n私は赤色の携帯が欲しいです。 # After例: '私は赤色の携帯が欲しいです。' for message in ret: message.content = remove_mention_str(self.bot_user_id, message.content) self._messages = ret
●環境変数:env.py
def get_env_variable(key): env_variable_dict = { # ------------------------------------ # Azure Form Recognizer(RDL) # ------------------------------------ "FR_ENDPOINT" : "", "FR_KEY" : "", # ------------------------------------ # OpenAI # ------------------------------------ "OPEN_AI_KEY" : "s", "MODEL" : "text-davinci-003", "SYSTEM_MESSAGE" : "あなたはxxxトです。xxxxしてください。", # ------------------------------------ # App名:Slack_Python_Flask # ------------------------------------ # BotユーザーID "BOT_USER_ID" : "", # Botトークン(Flask) "WEBAPPS_SLACK_TOKEN" : "", "WEBAPPS_SIGNING_SECRET" : "", # Botトークン(ソケットモード) "SOCK_SLACK_BOT_TOKEN" : "", "SOCK_SLACK_APP_TOKEN" : "" } ret_val = env_variable_dict.get(key, None) return ret_val
動かし方
> python -m venv .venv
↓
> .venv/Scripts/activate
↓
> pip install -r requirements.txt
↓
↓