Loading [MathJax]/extensions/tex2jax.js

Rainbow Engine

IT技術を分かりやすく簡潔にまとめることによる学習の効率化、また日常の気付きを記録に残すことを目指します。

OpenAI Python

OpenAIで返答するSlackボットで「会話履歴」を加味する方法

投稿日:2023年9月4日 更新日:

 

<目次>

OpenAIで返答するSlackボットで「会話履歴」を加味する方法
 やりたいこと
 前提条件
 概要
 サンプルプログラム
 動かし方

OpenAIで返答するSlackボットで「会話履歴」を加味する方法

やりたいこと

・SlackボットでOpenAIのAPIから回答を行い、尚且つその会話履歴を保持する
 
(図100)イメージ

前提条件

概要

基本的なSlackボットの考え方は「(手順)Slackでボットを開発する手順をご紹介(Python)」で紹介しているので割愛しますが、今回のポイントとしては以下です。

●【ポイント】「会話履歴」を加味した応答を実現するために、以下の実装を行った

・①スレッド履歴を保持する「メッセージ情報保持クラス」を用意
(図112)

・②それらをLangchainを使って、「人からのメッセージ」と「ボットからの応答メッセージ」を識別して配列に格納
(図113)
 

サンプルプログラム

(図111)3つのPythonモジュールがあります。

●メイン処理:0774_app.py

  1. from slack_bolt import App
  2. from slack_sdk import WebClient
  3. # Flaskクラスのインポート
  4. from flask import Flask, request
  5. from slack_bolt.adapter.flask import SlackRequestHandler
  6. #ソケットモード用
  7. from slack_bolt.adapter.socket_mode import SocketModeHandler
  8. # 環境変数読み込み
  9. import env
  10.  
  11. # OpenAI
  12. import openai
  13. import pkg_resources
  14. import respond_to_message
  15.  
  16. # モードに応じて書き換え
  17. BOT_USER_ID = env.get_env_variable("BOT_USER_ID")
  18. # Botトークン(Flask)
  19. WEBAPPS_SLACK_TOKEN = env.get_env_variable("WEBAPPS_SLACK_TOKEN")
  20. WEBAPPS_SIGNING_SECRET = env.get_env_variable("WEBAPPS_SIGNING_SECRET")
  21. # Botトークン(ソケットモード)
  22. SOCK_SLACK_BOT_TOKEN = env.get_env_variable("SOCK_SLACK_BOT_TOKEN")
  23. SOCK_SLACK_APP_TOKEN = env.get_env_variable("SOCK_SLACK_APP_TOKEN")
  24. # OpenAIのAPI設定
  25. openai.api_key = env.get_env_variable('OPEN_AI_KEY')
  26. openai_version = pkg_resources.get_distribution("openai").version
  27.  
  28. # モード入れ替え(WebAPサーバ実行=Flask/ローカル実行=ソケットモード)
  29. def app_mode_change(i_name):
  30. if i_name == "__main__":
  31. return App(token=SOCK_SLACK_BOT_TOKEN)
  32. else:
  33. return App(token=WEBAPPS_SLACK_TOKEN, signing_secret=WEBAPPS_SIGNING_SECRET)
  34.  
  35. # グローバルオブジェクト
  36. s_app = app_mode_change(__name__)
  37. # Flaskクラスのインスタンス生成
  38. app = Flask(__name__)
  39. #app.config['JSON_AS_ASCII'] = False
  40. handler_flask, handler_socket = None,None
  41.  
  42. #ソケットーモードの場合のハンドラ設定
  43. if __name__ == "__main__":
  44. handler_socket = SocketModeHandler(app=s_app, app_token=SOCK_SLACK_APP_TOKEN, trace_enabled=True)
  45. #Flaskでのハンドラー設定
  46. else:
  47. handler_flask = SlackRequestHandler(s_app)
  48.  
  49. # Flask httpエンドポイント
  50. # 疎通確認用1
  51. @app.route('/', methods=['GET', 'POST'])
  52. def home():
  53. return "Hello World Rainbow 2!!"
  54. # 疎通確認用2
  55. @app.route("/test", methods=['GET', 'POST'])
  56. def hello_test():
  57. return "Hello, This is test.2!!"
  58.  
  59. #イベント登録されたリクエストを受け付けるエンドポイント
  60. @app.route("/slack/events", methods=["POST"])
  61. def slack_events():
  62.  
  63. # ------------------------------------
  64. # Challenge用
  65. # ------------------------------------
  66. # # Slackから送られてくるPOSTリクエストのBodyの内容を取得
  67. # json = request.json
  68. # print(json)
  69. # # レスポンス用のJSONデータを作成
  70. # # 受け取ったchallengeのKey/Valueをそのまま返却する
  71. # d = {'challenge' : json["challenge"]}
  72. # # レスポンスとしてJSON化して返却
  73. # return jsonify(d)
  74.  
  75. # ------------------------------------
  76. # 本番用
  77. # ------------------------------------
  78. return handler_flask.handle(request)
  79.  
  80. @s_app.event("message")
  81. @s_app.event("app_mention")
  82. def respondToRequestMsg(body, client:WebClient, ack):
  83. ack()
  84. type = body["event"].get("type", None)
  85. print("-------------"+str(type))
  86. # 二重で応答するのを防ぐため、メンションの時のイベントのみ応答対象とする
  87. if type == 'app_mention':
  88. respond_to_message.respond_to_message(body=body,client=client)
  89.  
  90. # __name__はPythonにおいて特別な意味を持つ変数です。
  91. # 具体的にはスクリプトの名前を値として保持します。
  92. # この記述により、Flaskがmainモジュールとして実行された時のみ起動する事を保証します。
  93. # (それ以外の、例えば他モジュールから呼ばれた時などは起動しない)
  94. if __name__ == '__main__':
  95. EXEC_MODE = "SLACK_SOCKET_MODE"
  96. # Slack ソケットモード実行
  97. if EXEC_MODE == "SLACK_SOCKET_MODE":
  98. handler_socket.start()
  99. # Flask Web/APサーバ 実行
  100. elif EXEC_MODE == "FLASK_WEB_API":
  101. # Flaskアプリの起動
  102. # →Webサーバが起動して、所定のURLからアクセス可能になります。
  103. # →hostはFlaskが起動するサーバを指定しています(今回はローカル端末)
  104. # →portは起動するポートを指定しています(デフォルト5000)
  105. app.run(port=8000, debug=True)

●Slackイベント解析&応答処理:respond_to_message.py

  1. from slack_sdk import WebClient
  2. import conversation_util
  3. import env
  4. import re
  5. import time
  6. # OpenAI
  7. import openai
  8. openai.api_key = env.get_env_variable('OPEN_AI_KEY')
  9. model=env.get_env_variable('MODEL')
  10. import pkg_resources
  11. openai_version = pkg_resources.get_distribution("openai").version
  12. # langchain
  13. from langchain import schema
  14. from langchain.chat_models import ChatOpenAI
  15. # from langchain.chat_models import AzureChatOpenAI
  16. from langchain.schema import HumanMessage
  17.  
  18. # ロギング
  19. import traceback
  20.  
  21. def respond_to_message(body, client: WebClient):
  22. try:
  23. #-----------------------------------
  24. # Slackのイベント情報から各種パラメータを取得
  25. bot_user_id = env.get_env_variable('BOT_USER_ID')
  26. ts = body["event"]["ts"]
  27. thread_ts = body["event"].get("thread_ts", None)
  28. channel = body["event"]["channel"]
  29. user = body["event"]["user"]
  30. input_text = schema.HumanMessage(content=body["event"].get("text", None))
  31. attachment_files = body["event"].get("files", None)
  32. system_message = schema.SystemMessage(content=env.get_env_variable('SYSTEM_MESSAGE'))
  33.  
  34. # やり取りに関するインスタンス生成
  35. conversation_info = conversation_util.ConversationInfoSlack(
  36. client=client,
  37. bot_user_id=bot_user_id,
  38. ts=ts,
  39. thread_ts=thread_ts,
  40. channel=channel,
  41. user=user,
  42. human_message_latest=input_text,
  43. messages=None,
  44. system_message=system_message
  45. )
  46. # メッセージ情報の構築
  47. conversation_info.build_messages()
  48.  
  49. print("==============="+str(conversation_info._messages))
  50. # OpenAIからの返答を生成
  51. output_text = generate_response_v2(str(conversation_info._messages))
  52. time.sleep(1) # n秒待機 (実施しないと「The server responded with: {'ok': False, 'error': 'no_text'}」になる)
  53. # # Slackに返答
  54. client.chat_postMessage(channel=channel, text=output_text ,thread_ts=ts)
  55.  
  56. except Exception as e:
  57. print("-------------------------------------------------")
  58. print("======== react_to_msg 例外発生:"+str(e))
  59. traceback.print_exc()
  60.  
  61. def generate_response_v2(prompt) ->str:
  62.  
  63. print("============ generate_response : TOTAL_PROMPT(過去分含む):"+prompt)
  64. # 言語モデル(OpenAIのチャットモデル)のラッパークラスをインスタンス化
  65. llm = ChatOpenAI(
  66. model = "gpt-3.5-turbo",
  67. openai_api_key=env.get_env_variable('OPEN_AI_KEY'),
  68. max_tokens=500,
  69. temperature=0.5
  70. )
  71.  
  72. # APIを使用して、応答を生成します
  73. # モデルにPrompt(入力)を与えCompletion(出力)を取得する
  74. # SystemMessage: OpenAIに事前に連携したい情報。キャラ設定や前提知識など。
  75. # HumanMessage: OpenAIに聞きたい質問
  76. response = llm(messages=[HumanMessage(content=prompt)])
  77. # 文字列から必要なSystemMessageのみを抽出(以下例で言うaaaaaの部分のみ)
  78. # [SystemMessage(content='aaaaa', additional_kwargs={}), HumanMessage(content='bbbbb', additional_kwargs={}, example=False)]
  79. pattern = r"SystemMessage\(content='([^']*)'"
  80. matches = re.findall(pattern, response.content)
  81. content = ""
  82. if matches:
  83. content = matches[0]
  84. else:
  85. content = response.content
  86. print("============ generate_response : COMPLETION:"+str(response.content))
  87. return content

●やり取りに関する情報を保持するクラス:conversation_util.py

  1. import re
  2. from langchain import schema
  3. from slack_sdk import WebClient
  4.  
  5. class ConversationInfo:
  6. """
  7. 親クラス
  8. 入力データ(会話履歴+System Prompt+Human Prompt)をクラス
  9. 開発者は当クラスを継承し、状況に応じたInput情報生成を行う。
  10. """
  11. def __init__(
  12. self,
  13. messages:list[schema.BaseMessage]=None,
  14. **kwargs) -> None:
  15. # 「_変数」は非公開orプライベート変数
  16. self._messages = messages
  17. # 以降、@property(Decorator)で、ゲッター(getter)メソッドを定義。
  18. # 以降、@messages.setter(Decorator)で、セッター(setter)メソッドを定義。
  19.  
  20. @property
  21. def messages(self) -> list[schema.BaseMessage]:
  22. """
  23. メッセージ履歴のgetter
  24. Returns:
  25. list: メッセージ履歴のリスト
  26. """
  27. return self._messages
  28. @messages.setter
  29. def messages(self, value:list[schema.BaseMessage]):
  30. """
  31. メッセージ履歴のsetter
  32. Args:
  33. value (list[schema.BaseMessage]): メッセージ履歴
  34. """
  35. self._messages = value
  36. def build_messages(self, **kwargs):
  37. """
  38. messagesを構築するための処理を実装するメソッド。
  39. 必須でなく、メンバ変数「messages」を直接セットする事も可能
  40. """
  41. pass
  42.  
  43. def remove_mention_str(remove_user_id:str, target_text:str) -> str:
  44. """
  45. 文頭/文末のボットへのメンション文字列"<@Uxxxx>"を除外する
  46.  
  47. Args:
  48. remove_user_id (str): 除外したいボットのユーザーID
  49. target_text (str): 除外を行いたいテキスト文字列
  50.  
  51. Returns:
  52. str: メンション文字列が除外された新しいテキスト文字列
  53. """
  54. # 例:^<@UAAAAAAAAAA.*?>|<@UAAAAAAAAAA.*?>$
  55. pattern = f'^<@{remove_user_id}.*?>|<@{remove_user_id}.*?>$'
  56. # target_textのうち、パターンにマッチした部分を除去
  57. ret = re.sub(pattern, '', target_text.strip())
  58. return ret
  59.  
  60. def get_thread_history(
  61. client:WebClient,
  62. bot_user_id:str,
  63. ts: str,
  64. thread_ts:str,
  65. channel:str) ->list:
  66. """
  67. スレッドメッセージの履歴も含めた、一連のやり取りの履歴を取得する。
  68. (親メッセージ+conversation_repliesのlimitの件数まで取得)
  69. Args:
  70. bot_user_id (str): ボットのユーザーID。メッセージが"ai" or "human"を判定するために必要。
  71.  
  72. client (WebClient) :Slackとインタラクトする為のWebClientクラスのインスタンス
  73. bot_user_id (str) :ボットのユーザーID
  74. ts (str) :ユーザーメッセージのタイムスタンプ
  75. thread_ts (str) :ユーザーメッセージがスレッドの一部である場合の親メッセージのtimestamp
  76. channel (str) :チャンネルID
  77.  
  78. Returns:
  79. list: schema.HumanMessage または schema.SystemMessageが格納されたlist
  80.  
  81. """
  82. history = []
  83. # 会話履歴を取得
  84. resp = client.conversations_replies(
  85. channel=channel, inclusive=False, latest=ts, ts=thread_ts)
  86. messages = resp.get("messages")
  87.  
  88. # ボットの応答はAIMessageに、人からの回答はHumanMessageに格納
  89. for message in messages:
  90. if message.get("user", "-") == bot_user_id:
  91. content=message.get("text", "**** メッセージの取得に失敗しました ****")
  92. history.append(schema.AIMessage(content=content))
  93. else:
  94. content=message.get("text", "**** メッセージの取得に失敗しました ****")
  95. history.append(schema.HumanMessage(content=content))
  96. return history
  97.  
  98. class ConversationInfoSlack(ConversationInfo):
  99. """
  100. TaskInputChatを継承した子クラス
  101. Slackからのメッセージ(処理依頼)に対して、メッセージ情報を保持するためのクラス
  102. """
  103. def __init__(self,
  104. client:WebClient,
  105. bot_user_id:str,
  106. ts:str,
  107. thread_ts:str,
  108. channel:str,
  109. user:str,
  110. human_message_latest:schema.HumanMessage=None,
  111. messages:list[schema.BaseMessage]=None,
  112. system_message:schema.SystemMessage=None,
  113. **kwargs) -> None:
  114. """
  115. コンストラクタ。
  116. Args:
  117. client (WebClient) :Slackとインタラクトする為のWebClientクラスのインスタンス
  118. bot_user_id (str) :ボットのユーザーID
  119. ts (str) :ユーザーメッセージのタイムスタンプ
  120. thread_ts (str) :ユーザーメッセージがスレッドの一部である場合の親メッセージのtimestamp
  121. channel (str) :チャンネルID
  122. user (str) :投稿したユーザーのSlackユーザーID
  123. human_message_latest (schema.HumanMessage) : 最新のユーザーメッセージ (省略可能)
  124. messages (list[schema.BaseMessage])
  125. :メッセージ履歴 (省略された場合は、後からsetterから格納するか、build_messages()から構築)
  126. schema.BaseMessageクラスのインスタンスのlistを受け取ることができます(Noneのため省略可能)
  127. system_message (schema.SystemMessage) : システムメッセージ (省略可能)
  128. Notes:
  129. - system_message、human_message_latestを設定する事で、build_messages()を呼び、過去スレッド履歴も含めたmessagesが構築できる。
  130. - build_messages()を使用せずに、直接メンバ変数messagesに値を格納する事も可能。
  131. """
  132.  
  133. # 親クラスのコンストラクタを呼ぶ
  134. super().__init__(messages=messages,**kwargs)
  135.  
  136. self.client = client
  137. self.bot_user_id = bot_user_id
  138. self.ts = ts
  139. self.thread_ts = thread_ts
  140. self.channel = channel
  141. self.user = user
  142. self.human_message_latest = human_message_latest
  143. self.system_message = system_message
  144. def build_messages(self, **kwargs):
  145. """
  146. messagesを構築するための処理を実装するメソッド。
  147. 必須でなく、メンバ変数「messages」を直接セットする事も可能
  148. """
  149. ret = []
  150.  
  151. # システムメッセージを追加
  152. if not self.system_message is None:
  153. ret.append(self.system_message)
  154.  
  155. # スレッド履歴を追加
  156. if self.thread_ts is not None:
  157. thread_history = get_thread_history(
  158. client=self.client,
  159. bot_user_id=self.bot_user_id,
  160. ts=self.ts,
  161. thread_ts=self.thread_ts,
  162. channel=self.channel
  163. )
  164. ret.extend(thread_history)
  165.  
  166. # 最新のメッセージを追加
  167. ret.append(self.human_message_latest)
  168.  
  169. # ボットへの文頭/文末のメンションを除去する
  170. # Before例: <@U03U3JE514N>\n私は赤色の携帯が欲しいです。
  171. # After例: '私は赤色の携帯が欲しいです。'
  172. for message in ret:
  173. message.content = remove_mention_str(self.bot_user_id, message.content)
  174. self._messages = ret

●環境変数:env.py

  1. def get_env_variable(key):
  2.  
  3. env_variable_dict = {
  4. # ------------------------------------
  5. # Azure Form Recognizer(RDL)
  6. # ------------------------------------
  7. "FR_ENDPOINT" : "",
  8. "FR_KEY" : "",
  9. # ------------------------------------
  10. # OpenAI
  11. # ------------------------------------
  12. "OPEN_AI_KEY" : "s",
  13. "MODEL" : "text-davinci-003",
  14. "SYSTEM_MESSAGE" : "あなたはxxxトです。xxxxしてください。",
  15.  
  16. # ------------------------------------
  17. # App名:Slack_Python_Flask
  18. # ------------------------------------
  19. # BotユーザーID
  20. "BOT_USER_ID" : "",
  21. # Botトークン(Flask)
  22. "WEBAPPS_SLACK_TOKEN" : "",
  23. "WEBAPPS_SIGNING_SECRET" : "",
  24.  
  25. # Botトークン(ソケットモード)
  26. "SOCK_SLACK_BOT_TOKEN" : "",
  27. "SOCK_SLACK_APP_TOKEN" : ""
  28. }
  29. ret_val = env_variable_dict.get(key, None)
  30. return ret_val

動かし方

・①venvを作る
  1. > python -m venv .venv
(図211)

・②venvのアクティベート
  1. > .venv/Scripts/activate
(図212)

・③パッケージのインストール(requirements.txt)
  1. > pip install -r requirements.txt
(図213)

・④メインモジュール実行(0774_app.py)
(図214)

・⑤Slackに投稿
(図100)

Adsense審査用広告コード


Adsense審査用広告コード


-OpenAI, Python
-

執筆者:


comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

関連記事

PythonでSQLAlchemyを使ってOracleDBに接続する際に遭遇したエラーとその対処方法

本記事は次の記事の続編です。 (下記記事の手順を実行する際に遭遇したエラーについての備忘録) PythonでSQLAlchemyを使ってOracleDBに接続する方法 (0) 目次 (4) エラー対応 …

Pythonのdatapackage学習中に遭遇したエラー「StopIteration」と「AttributeError」の対応

(0)目次&概説 (1) 記事の目的 (2) エラー1:AttributeError: ‘generator’ object has no attribute ‘n …

PythonのPandas使用時に発生した「UnicodeEncodeError: ‘ascii’ codec can’t encode characters~」エラーの対処方法について

(0)目次&概説 (1) エラー対応1:UnicodeEncodeError  (1-1) 発生状況・エラーメッセージ   (1-1-1) エラーメッセージ   (1-1-2) エラーとなったソース …

脳波をPythonプログラムで取得して、解析できるようにしたい

  <目次> 脳波をPythonプログラムで取得して、解析できるようにしたい  やりたいこと  STEP1:前提条件(事前準備)  STEP2:デバイス(MindWave Mobile 2) …

Python開発環境にPandasライブラリをインストールする手順

(0)目次&概説 (1) Pandasの導入  (1-1) Pandasとは? (2) オフラインインストール  (2-1) インストール資源の入手  (2-2) インストール時の諸注意  (2-3) …

  • English (United States)
  • 日本語
S