本シリーズ前回の記事 ChatGPT と ROS – 文書生成 ROS ラッパー生成編(Chat Completion API) では OpenAI の Chat Completion API を利用した ROS Service ラッパ Python プログラムを Web の方の ChatGPT の助けを借りて作成した様子を紹介しました.
しかし,前回のプログラムは Chat Completion API を用いているものの使い方は「1問1答」形式で,それは Completion API を利用しているのと大きく変わらず,「文脈をふまえたチャット」形式ではありませんでした.
そこで前回から発展させて Chat Completion API を利用した「文脈をふまえたチャット」をする ROS ソフトウェアを次の2つの方法で実装してみました.
今回の記事ではこれらのうち「1. ROS Service プログラムの文脈をふまえたチャット対応」を行った様子をお伝えします.
前回作成した Chat Completion API を利用しながらも「1問1答」形式だった ROS Service プログラムを「チャット」形式に対応させるて会話ができるように作り直します.
Chat Completion API を用いたチャット機能の実装については下記の Web 記事が ROS ではない Python プログラムですが大変参考になりました.
この記事によると Chat Completion API の応答を「チャット」の結果として機能させるためには「新しい問い」に一連の「チャットの全履歴」も含めて新たなメッセージとしたものを API に送ってリクエストする必要があります.
前回作成した ROS Service プログラムを発展させて,Chat Completion API を利用し履歴をふまえた ROS チャットソフトウェアを作成する場合,チャットの履歴を ROS Service のサーバ側で保持するか,サービスの呼び出し側で保持するか,システム的な最適解はさておき,プログラム的にはどちらの方法もあるかと思います.
今回作成した文脈をふまえたチャットに対応する ROS Service プログラムは Chat Completion API のシンプルな ROS ラッパーとしての位置づけは変えずにそのサービスを呼び出す ROS Node 側で履歴を保持して ROS Service にリクエストを送り Chat Completion API にアクセスする方法を採ってみました.
今回も Web サービスの ChatGPT の助けを借りてプログラムを作成したのですが,まるごと Python コードを作成するための問いの仕方が下手だったのか,ChatGPT の応答でしっくり来るようなソースコードは得られませんでした.そこで Python コードを記述する際の個別具体的な問題について ChatGPT の力を借りました.
例えば Chat Completion API へのメッセージやレスポンスを文字列(string)にしてやり取りする方法を聞きました.
User
Chat Completion API Python における引数 messages に渡すのは json 形式の text 文字列であっていますか?
ChatGPT
Chat Completion APIのmessages
引数に渡すものは,Pythonのリストとしてのデータ構造です.このリストは辞書のオブジェクトを要素として持ちます.通常,直接JSON形式のテキスト文字列を渡すのではなく,Pythonのデータ構造を使用します.
以下は,APIに渡すためのmessages
の例です:
messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Who won the world series in 2020?"} ]
もし,このリストがJSON形式の文字列として提供されている場合,Pythonのjson
モジュールを使ってデコードすることで,上記のようなPythonのデータ構造に変換することができます:
import json json_string = '[{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Who won the world series in 2020?"}]' messages = json.loads(json_string)
OpenAIのAPIを直接使用する際や,HTTPリクエストを行う場合,リクエストのボディとしてJSON文字列を送信することがあります.その際には,上記のようにPythonのデータ構造をJSON文字列に変換(エンコード)したり,逆にJSON文字列からPythonのデータ構造に変換(デコード)したりする必要があります.しかし,多くのライブラリやSDKは,この変換を自動的に行ってくれるので,ユーザーが直接JSON文字列を扱う必要は少ないです.
User
json の python list を string に変換する場合はどうしますか?
ChatGPT
PythonのリストをJSON形式の文字列に変換するには,Pythonの標準ライブラリであるjson
モジュールのdumps
関数を使用します.
以下に具体的な手順を示します.
1. json
モジュールをインポートします.
import json
1. json.dumps
関数を使用してPythonのリストをJSON形式の文字列に変換します.
data = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Who won the world series in 2020?"} ] json_string = json.dumps(data) print(json_string)
この例では,data
という名前のPythonリストをJSON形式の文字列に変換し,それをjson_string
という変数に保存しています.
また,json.dumps
関数にはいくつかのオプション引数があります.例えば,indent
を指定することで,整形された(インデントされた)JSON文字列を取得することができます:
json_string_pretty = json.dumps(data, indent=4) print(json_string_pretty)
これにより,出力は読みやすい形式で表示されます.
User
単純に str(data) とするのと異なりますか?
ChatGPT
str(data)
とjson.dumps(data)
は,多くの場合において似た結果を返すことがありますが,実際には異なる振る舞いを持ち,それぞれ異なる目的に最適化されています.以下にその主な違いをいくつか示します.
1. Unicode Handling:
json.dumps()
は,Unicode文字列を適切にエンコードしてJSON互換のエスケープシーケンスにします.一方,str()
はPythonのUnicode文字列の内部表現をそのまま返すことがあります.
data = {"key": "あいうえお"} print(str(data)) # Python 2では "{'key': u'\u3042\u3044\u3046\u3048\u304a'}" print(json.dumps(data)) # '{"key": "あいうえお"}'
2. Boolean Values:
json.dumps()
は,PythonのTrue
とFalse
をそれぞれtrue
とfalse
としてJSONに変換します.str()
は,Pythonの表現をそのまま返します.
data = {"key": True} print(str(data)) # "{'key': True}" print(json.dumps(data)) # '{"key": true}'
3. Single vs Double Quotes:
Pythonの辞書を文字列として表現するとき,str()
はシングルクォート(‘)を使用します.しかし,JSON標準はプロパティ名や文字列値にダブルクォート(“)を使用するため,json.dumps()
はダブルクォートを使用します.
data = {"key": "value"} print(str(data)) # "{'key': 'value'}" print(json.dumps(data)) # '{"key": "value"}'
これらの違いから,PythonのデータをJSONとして扱いたい場合や,他のシステムとの互換性を確保したい場合は,json.dumps()
を使用するのが最適です.一方,デバッグ目的などでPythonの内部表現を文字列として見たいだけの場合は,str()
を使用することができます.
また,Chat Completion API へ送るメッセージデータの中のrole
の使い方についても ChatGPT に質問してみました.
Chat Completion API の渡し値の role における system と assistant の違いをおしえて?
ChatGPT
Chat Completion APIの文脈におけるrole
の値system
とassistant
は,入力メッセージのコンテキストを形成するためのものです.各role
は以下のような意味を持ちます:
1. system:
system
の役割は,環境やセッションの全体的な指示やコンテキストを設定するためのものです.{"role": "system", "content": "You are an assistant that speaks like Shakespeare."}
2. assistant:
assistant
の役割は,モデル自体の前の発言やレスポンスを示すためのものです.各役割は,APIを通じてモデルとの対話を形成するためのコンテキストを提供する役割を果たします.たとえば,モデルにシェイクスピアのように話すように指示するsystem
メッセージの後,ユーザーの質問に対するモデルの回答はシェイクスピアのスタイルで形成される可能性があります.
このように,role
の値を使用して,モデルとの対話のコンテキストや動作を微調整することができます.
文脈をふまえたチャット対応を行った ROS Service プログラムの作成や変更を行ったファイルは次のとおりです.
以下,それぞれのファイル内のコードを記載して少し説明をします.
srv / ChatCompletion.srv
messages
,受取るデータをresponse
というstring
型にしています.
string messages --- string response
scripts / openai_chat_server.py
chat_service
としてデータを JSON 形式の文字列として授受し,Chat Completion API へのアクセスを行っています.
#!/usr/bin/env python3 import rospy import openai import json from openai_ros.srv import ChatCompletion, ChatCompletionResponse def handle_chat_request(req): openai.api_key = rospy.get_param('~key') model = rospy.get_param('~model') res = ChatCompletionResponse() messages = json.loads(req.messages) response = openai.ChatCompletion.create( model=model, messages=messages ) return json.dumps(response) def chat_service(): rospy.init_node('chat_service') rospy.Service('chat_service', ChatCompletion, handle_chat_request) rospy.spin() if __name__ == "__main__": chat_service()
scripts / openai_chat_client.py
role
の「環境やセッションの全体的な指示やコンテキストを設定する」 system
に prompt
の内容を設定します.
ROS Node 内でユーザからのインプットを受け取り,これまでのメッセージ履歴に追加して,メッセージデータを JSON 文字列として ROS Service chat_service
に送っています.サービスからの応答データは JSON 文字列から Python リストに変換されて含まれていた返答メッセージはprint()
してメッセージ履歴にも追加することを繰り返しています.
#!/usr/bin/env python3 import sys, json import rospy from openai_ros.srv import ChatCompletion, ChatCompletionResponse def chat_client(prompt="You are a helpful assistant."): messages = [] messages.append({"role": "system", "content": str(prompt)}) rospy.wait_for_service('chat_service') try: chat_service_client = rospy.ServiceProxy('chat_service', ChatCompletion) except rospy.ServiceException as e: print ("Service call failed: %s" % e) while not rospy.is_shutdown(): # Get input from the user user_input = input("You: ") if user_input == "quit": exit() # Add user's input to the history messages.append({"role": "user", "content": user_input}) msg_string = json.dumps(messages) # Call the service try: res_msg = chat_service_client(msg_string) except rospy.ServiceException as e: print ("Service call failed: %s" % e) response = json.loads(res_msg.response) content = response["choices"][0]["message"]["content"] role = response["choices"][0]["message"]["role"] token = response["usage"]["total_tokens"] print("\n%s(token:%d): %s\n" % (role, token, content)) # Add GPT's response to the history messages.append({"role": str(role), "content": str(content)}) if __name__ == "__main__": if len(sys.argv) == 2: prompt = str(sys.argv[1]) else: prompt = "You are a helpful assistant." print("Type \'quit\' to quit.\n") print("For \'system\': %s\n" % (prompt)) rospy.init_node('chat_client') chat_client(prompt=prompt)
launch / openai_chat.launch
<launch> <arg name="key" default="$(env OPENAI_API_KEY)" /> <arg name="model" default="gpt-3.5-turbo" /> <node pkg="openai_ros" type="openai_chat_server.py" name="openai_chat" output="screen"> <param name="key" value="$(arg key)" /> <param name="model" value="$(arg model)" /> </node> </launch>
CMakeLists.txt
add_service_files( FILES Completion.srv ChatCompletion.srv ) generate_messages(
文脈をふまえたチャットへ対応させた ROS Service プログラムの実行例が次になります.
ターミナル1 : ROS Service サーバの起動
robotuser@robotuser-PC:~/openai_ws$ source ~/openai_ws/devel/setup.bash robotuser@robotuser-PC:~/openai_ws$ export OPENAI_API_KEY="sk-..." robotuser@robotuser-PC:~/openai_ws$ roslaunch openai_ros openai_chat.launch
ターミナル2 : ROS チャットノードの実行
robotuser@robotuser-PC:~/openai_ws$ source ~/openai_ws/devel/setup.bash robotuser@robotuser-PC:~/openai_ws$ rosrun openai_ros openai_chat_client.py Type 'quit' to quit. For 'system': You are a helpful assistant. You: hello assistant(token:27): Hello! How can I assist you today? You: ハロー assistant(token:56): こんにちは!どのようにお手伝いできますか? You: ボン・ニュイ assistant(token:115): ボン・ニュイ!夜にちょっと早い挨拶ですね.何かお手伝いできることはありますか? You: こんばんは をフランス語で言うと? assistant(token:163): 「こんばんは」をフランス語で言うと「Bonsoir」となります. You: bon nuit は日本語では? assistant(token:224): 「bon nuit」はフランス語で「良い夜」を意味しますが,日本語では「おやすみなさい」と訳されます. You: 間違えていました.ボンソワール assistant(token:338): 間違いありません.「ボンソワール」はフランス語で「こんばんは」という意味です.おやすみなさいの言い方は「ボンヌィ」となります.ごめんなさい,混乱を招いてしまいました.どうかお許しください. You: quit robotuser@robotuser-PC:~/openai_ws$
このように今回は ROS Node 内でのチャット会話になっています.
前回ベースにしている https://github.com/davesarmoury/openai_ros が1問1答形式の Completion API にアクセスする ROS Service を提供していたことから Chat Completion API にアクセスする ROS ソフトウェアも ROS Service 形式で作成してみました.
比較的長文で1問1答を行う Completion API であれば ROS Service や ROS Action (actionlib) のソフトウェアにするのも一理あるように思えます.しかし短文のチャットを扱う Chat Completion API へのアクセスであれば ROS Topic を介したメッセージのやり取りの方が ROS ノード内のチャット会話に限られず,より ROS 親和的かつよりシンプルな構成になるのでは?と思いました.
本シリーズ次回の記事では冒頭で述べました文脈をふまえたチャットを実現する2つの方法のもう一方の「ChatGPT と ROS – ROS Topic を介した ChatGPT チャットプログラム」を作成した様子を紹介する予定です.
今回の記事はここまでです.