<目次>
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

↓

↓