本シリーズ前回の記事 1. ROS Service プログラムの文脈をふまえたチャット対応 では OpenAI の Chat Completion API を利用して過去のチャット履歴もふまえたチャットを行える ROS Service プログラムを作成した様子をお伝えしました.
今回の記事では Chat Completion API を利用した「文脈をふまえたチャット」をする ROS ソフトウェアを実装してみた2つ目の方法「2. ROS Topic を介した ChatGPT チャットプログラム」を作成した様子を紹介します.
前回,比較的短文のチャットを扱う Chat Completion API へのアクセスであれば ROS Service よりも ROS Topic を介したメッセージのやり取りの方が ROS ノード内でのチャット会話に限られず,より ROS に親和的でよりシンプルな構成になるのでは?という反省がありました.
今回の ROS Topic を用いたチャットプログラムの作成方針は次のようにしました.
/request
を購読してユーザの発言を得る/response
としてパブリッシュする/request
にパブリッシュする/response
を購読して得るROS Service プログラムの場合はチャット履歴をふまえたとしてもチャット機能提供側とユーザとの1者対1者でのやり取りでしたが,ROS Topic にすることでチャット機能提供側と複数のユーザの1者対他者でのやり取りも可能になる利点もあります.
ROS Topic を介した文脈をふまえたチャットプログラムで追加したファイルは次の2つです.
サービスの定義など考慮しなくて良いので非常にシンプルです.
以下,それぞれのファイル内のコードを記載して少し説明をします.
scripts / openai_chat_rostopic.py
#!/usr/bin/env python3 import rospy import openai from std_msgs.msg import String class Chatter: """ Chat with ChatGPT on ROS topics """ def __init__(self): # Get ROS parameters prompt = rospy.get_param('~prompt') self.model = rospy.get_param('~model') openai.api_key = rospy.get_param('~key') rospy.loginfo("For \'system\': %s" % (prompt)) # Set initial message with a prompt self.messages = [] self.messages.append({"role": "system", "content": str(prompt)}) self.sub = rospy.Subscriber('request', String, self.callback) self.pub = rospy.Publisher('response', String, queue_size=10) rospy.spin() def callback(self, data): rospy.loginfo("request: %s", data.data) # Add user's input to the history self.messages.append({"role": "user", "content": str(data.data)}) response = openai.ChatCompletion.create( model=self.model, messages=self.messages ) content = response["choices"][0]["message"]["content"] role = response["choices"][0]["message"]["role"] token = response["usage"]["total_tokens"] # Add GPT's response to the history self.messages.append({"role": str(role), "content": str(content)}) rospy.loginfo("%s(token:%d): %s" % (role, token, content)) self.pub.publish(content) if __name__ == "__main__": rospy.init_node('chat_rostopic', anonymous=True) chatter = Chatter()
/request
を購読(Subscribe)してトピックを受け取ったら callback()
メソッドを呼び出すcallback()
メソッド内で新たなリクエストをチャット履歴に追加/response
としてパブリッシュlaunch / openai_chat.launch
launch オプション service
を用いて前回の記事で紹介した ROS Service によるチャットプログラムと今回の ROS Topic を介したチャットプログラムのどちらを実行するかを切り替えるようにしています.
<launch> <arg name="key" default="$(env OPENAI_API_KEY)" /> <arg name="model" default="gpt-3.5-turbo" /> <arg name="service" default="false" /> <arg name="prompt" default="You are a helpful assistant." /> <node if="$(arg service)" pkg="openai_ros" type="openai_chat_server.py" name="openai_chat_service" output="screen"> <param name="key" value="$(arg key)" /> <param name="model" value="$(arg model)" /> </node> <node unless="$(arg service)" pkg="openai_ros" type="openai_chat_rostopic.py" name="openai_chat_topic" output="screen"> <param name="key" value="$(arg key)" /> <param name="model" value="$(arg model)" /> <param name="prompt" value="$(arg prompt)" /> </node> </launch>
文脈をふまえた ROS Topic を介したチャットプログラムを実行した例を以下に記載します.
ターミナル 1 : チャットノードの起動
Chat Completion API にアクセスして ROS Topic でやり取りする ROS Node を openai_chat.launch で起動しています.
robotuser@robotuser-PC:~$ source ~/openai_ws/devel/setup.bash robotuser@robotuser-PC:~$ export OPENAI_API_KEY="sk-..." robotuser@robotuser-PC:~$ roslaunch openai_ros openai_chat.launch ... logging to /home/robotuser/.ros/log/609f8d12-52cd-11ee-9968-6b3ff6703622/roslaunch-robotuser-PC-5035.log Checking log directory for disk usage. This may take a while. Press Ctrl-C to interrupt Done checking log file disk usage. Usage is <1GB. started roslaunch server http://robotuser-PC:40257/ SUMMARY ======== PARAMETERS * /openai_chat/key: sk-3JDluBbxsNuIhi... * /openai_chat/model: gpt-3.5-turbo * /openai_chat/prompt: You are a helpful... * /rosdistro: noetic * /rosversion: 1.16.0 NODES / openai_chat (openai_ros/openai_chat_rostopic.py) auto-starting new master process[master]: started with pid [5043] ROS_MASTER_URI=http://localhost:11311 setting /run_id to 609f8d12-52cd-11ee-9968-6b3ff6703622 process[rosout-1]: started with pid [5053] started core service [/rosout] process[openai_chat-2]: started with pid [5060] [INFO] [1694675254.662674]: For 'system': You are a helpful assistant. [INFO] [1694675266.017788]: request: Hello [INFO] [1694675267.401076]: assistant(token:27): Hello! How can I assist you today? [INFO] [1694675292.897892]: request: インターネットはどこにありますか? [INFO] [1694675300.168180]: assistant(token:183): インターネットは物理的な場所にあるのではなく,世界中のコンピューターがネットワークで結ばれている仮想的な空間です.インターネットは,データを送信し受け取るためのプロトコルとパケット交換技術を使用しています.つまり,インターネットは世界中のコンピューターネットワークの集合体です. [INFO] [1694675347.975610]: request: 世界中とは地球のことですか? [INFO] [1694675354.647098]: assistant(token:324): はい,正確に言えば,インターネットは地球上のコンピューターネットワークの集合体です.このネットワークには,地球上の様々な場所に設置されたサーバーやルーターなどの機器が含まれます.インターネットのデータ通信は,これらの機器を介して行われます. [INFO] [1694675387.510775]: request: あなたは誰ですか? [INFO] [1694675391.409440]: assistant(token:407): 私はAI(人工知能)ベースのヘルプフルなアシスタントです.質問や問題解決,情報の提供など,お手伝いできることがありましたらお知らせください. [INFO] [1694675436.047036]: request: どのようにヘルプフルですか? [INFO] [1694675447.019485]: assistant(token:645): 私は多くの情報と知識を持つAIですので,様々な質問や問題に対してお答えすることができます.例えば,旅行の予約や天気予報の確認,翻訳や研究のサポート,一般的な知識の提供など,さまざまな分野でお手伝いすることができます.また,自然言語処理の技術を利用して,あなたの質問や要求を理解し,最適な回答やソリューションを提供することも可能です.お困りのことや疑問があれば,いつでもお気軽にお知らせください. [INFO] [1694675485.687087]: request: 東京の明日の天気もわかりますか? [INFO] [1694675507.944942]: assistant(token:1064): もちろんです!東京の天気予報を調べてみましょう. 私はリアルタイムのデータにアクセスできるわけではありませんが,一般的に天気予報を提供する公式のウェブサイトやアプリを利用して,詳細な天気予報を確認することができます.天気予報は頻繁に更新されるため,事前に確認することをおすすめします.以下は一般的な天気予報サービスの利用方法です. - インターネット検索エンジンで「東京の天気予報」と検索すると,現在の天気情報と明日の天気予報を含む結果が表示されます. - スマートフォンやタブレットを使用している場合は,天気予報を提供するアプリをダウンロードしてインストールすることもできます.定番のアプリには「Weather」や「Weather Underground」などがあります. これらの方法を使用して,明日の東京の天気を確認してみてください.天気予報に関する詳細な情報を入手するためには,地元の気象庁や天気予報サービスの公式ウェブサイトを参照することもおすすめです. ^C[openai_chat-2] killing on exit [rosout-1] killing on exit [master] killing on exit shutting down processing monitor... ... shutting down processing monitor complete done robotuser@robotuser-PC:~$
ターミナル 2 : ROS Topic に発言をパブリッシュ
1つ目のターミナルで openai_chat.launch を起動したままの状態で2つ目のターミナルから ROS Topic をパブリッシュします.
robotuser@robotuser-PC:~$ rostopic pub -1 /request std_msgs/String "Hello" publishing and latching message for 3.0 seconds robotuser@robotuser-PC:~$ rostopic pub -1 /request std_msgs/String "インターネットはどこにありますか ?" publishing and latching message for 3.0 seconds robotuser@robotuser-PC:~$ rostopic pub -1 /request std_msgs/String "世界中とは地球のことですか?" publishing and latching message for 3.0 seconds robotuser@robotuser-PC:~$ rostopic pub -1 /request std_msgs/String "あなたは誰ですか?" publishing and latching message for 3.0 seconds robotuser@robotuser-PC:~$ rostopic pub -1 /request std_msgs/String "どのようにヘルプフルですか?" publishing and latching message for 3.0 seconds robotuser@robotuser-PC:~$ rostopic pub -1 /request std_msgs/String "東京の明日の天気もわかりますか?" publishing and latching message for 3.0 seconds robotuser@robotuser-PC:~$
ターミナル 3 : チャットノードからの応答の ROS Topic を確認
3つ目のターミナルで ROS Topic /response
に Chat Completion API からの応答がパブリッシュされているかを確認します.コンソール出力では文字コード化して可読性がないですが Python で print()
や rospy.loginfo()
で出力すると ターミナル 1 のような読める日本語で表示されます.
robotuser@robotuser-PC:~$ rostopic echo /response data: "\u79C1\u306FAI\uFF08\u4EBA\u5DE5\u77E5\u80FD\uFF09\u30D9\u30FC\u30B9\u306E\u30D8\u30EB\ \u30D7\u30D5\u30EB\u306A\u30A2\u30B7\u30B9\u30BF\u30F3\u30C8\u3067\u3059\u3002\u8CEA\ \u554F\u3084\u554F\u984C\u89E3\u6C7A\u3001\u60C5\u5831\u306E\u63D0\u4F9B\u306A\u3069\ \u3001\u304A\u624B\u4F1D\u3044\u3067\u304D\u308B\u3053\u3068\u304C\u3042\u308A\u307E\ \u3057\u305F\u3089\u304A\u77E5\u3089\u305B\u304F\u3060\u3055\u3044\u3002" --- data: "\u79C1\u306F\u591A\u304F\u306E\u60C5\u5831\u3068\u77E5\u8B58\u3092\u6301\u3064AI\u3067\ \u3059\u306E\u3067\u3001\u69D8\u3005\u306A\u8CEA\u554F\u3084\u554F\u984C\u306B\u5BFE\ \u3057\u3066\u304A\u7B54\u3048\u3059\u308B\u3053\u3068\u304C\u3067\u304D\u307E\u3059\ \u3002\u4F8B\u3048\u3070\u3001\u65C5\u884C\u306E\u4E88\u7D04\u3084\u5929\u6C17\u4E88\ \u5831\u306E\u78BA\u8A8D\u3001\u7FFB\u8A33\u3084\u7814\u7A76\u306E\u30B5\u30DD\u30FC\ \u30C8\u3001\u4E00\u822C\u7684\u306A\u77E5\u8B58\u306E\u63D0\u4F9B\u306A\u3069\u3001\ \u3055\u307E\u3056\u307E\u306A\u5206\u91CE\u3067\u304A\u624B\u4F1D\u3044\u3059\u308B\ \u3053\u3068\u304C\u3067\u304D\u307E\u3059\u3002\u307E\u305F\u3001\u81EA\u7136\u8A00\ \u8A9E\u51E6\u7406\u306E\u6280\u8853\u3092\u5229\u7528\u3057\u3066\u3001\u3042\u306A\ \u305F\u306E\u8CEA\u554F\u3084\u8981\u6C42\u3092\u7406\u89E3\u3057\u3001\u6700\u9069\ \u306A\u56DE\u7B54\u3084\u30BD\u30EA\u30E5\u30FC\u30B7\u30E7\u30F3\u3092\u63D0\u4F9B\ \u3059\u308B\u3053\u3068\u3082\u53EF\u80FD\u3067\u3059\u3002\u304A\u56F0\u308A\u306E\ \u3053\u3068\u3084\u7591\u554F\u304C\u3042\u308C\u3070\u3001\u3044\u3064\u3067\u3082\ \u304A\u6C17\u8EFD\u306B\u304A\u77E5\u3089\u305B\u304F\u3060\u3055\u3044\u3002" --- data: "\u3082\u3061\u308D\u3093\u3067\u3059\uFF01\u6771\u4EAC\u306E\u5929\u6C17\u4E88\u5831\ \u3092\u8ABF\u3079\u3066\u307F\u307E\u3057\u3087\u3046\u3002\n\n\u79C1\u306F\u30EA\ \u30A2\u30EB\u30BF\u30A4\u30E0\u306E\u30C7\u30FC\u30BF\u306B\u30A2\u30AF\u30BB\u30B9\ \u3067\u304D\u308B\u308F\u3051\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\ \u4E00\u822C\u7684\u306B\u5929\u6C17\u4E88\u5831\u3092\u63D0\u4F9B\u3059\u308B\u516C\ \u5F0F\u306E\u30A6\u30A7\u30D6\u30B5\u30A4\u30C8\u3084\u30A2\u30D7\u30EA\u3092\u5229\ \u7528\u3057\u3066\u3001\u8A73\u7D30\u306A\u5929\u6C17\u4E88\u5831\u3092\u78BA\u8A8D\ \u3059\u308B\u3053\u3068\u304C\u3067\u304D\u307E\u3059\u3002\u5929\u6C17\u4E88\u5831\ \u306F\u983B\u7E41\u306B\u66F4\u65B0\u3055\u308C\u308B\u305F\u3081\u3001\u4E8B\u524D\ \u306B\u78BA\u8A8D\u3059\u308B\u3053\u3068\u3092\u304A\u3059\u3059\u3081\u3057\u307E\ \u3059\u3002\u4EE5\u4E0B\u306F\u4E00\u822C\u7684\u306A\u5929\u6C17\u4E88\u5831\u30B5\ \u30FC\u30D3\u30B9\u306E\u5229\u7528\u65B9\u6CD5\u3067\u3059\u3002\n\n- \u30A4\u30F3\ \u30BF\u30FC\u30CD\u30C3\u30C8\u691C\u7D22\u30A8\u30F3\u30B8\u30F3\u3067\u300C\u6771\ \u4EAC\u306E\u5929\u6C17\u4E88\u5831\u300D\u3068\u691C\u7D22\u3059\u308B\u3068\u3001\ \u73FE\u5728\u306E\u5929\u6C17\u60C5\u5831\u3068\u660E\u65E5\u306E\u5929\u6C17\u4E88\ \u5831\u3092\u542B\u3080\u7D50\u679C\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002\ \n- \u30B9\u30DE\u30FC\u30C8\u30D5\u30A9\u30F3\u3084\u30BF\u30D6\u30EC\u30C3\u30C8\ \u3092\u4F7F\u7528\u3057\u3066\u3044\u308B\u5834\u5408\u306F\u3001\u5929\u6C17\u4E88\ \u5831\u3092\u63D0\u4F9B\u3059\u308B\u30A2\u30D7\u30EA\u3092\u30C0\u30A6\u30F3\u30ED\ \u30FC\u30C9\u3057\u3066\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3059\u308B\u3053\u3068\ \u3082\u3067\u304D\u307E\u3059\u3002\u5B9A\u756A\u306E\u30A2\u30D7\u30EA\u306B\u306F\ \u300CWeather\u300D\u3084\u300CWeather Underground\u300D\u306A\u3069\u304C\u3042\ \u308A\u307E\u3059\u3002\n\n\u3053\u308C\u3089\u306E\u65B9\u6CD5\u3092\u4F7F\u7528\ \u3057\u3066\u3001\u660E\u65E5\u306E\u6771\u4EAC\u306E\u5929\u6C17\u3092\u78BA\u8A8D\ \u3057\u3066\u307F\u3066\u304F\u3060\u3055\u3044\u3002\u5929\u6C17\u4E88\u5831\u306B\ \u95A2\u3059\u308B\u8A73\u7D30\u306A\u60C5\u5831\u3092\u5165\u624B\u3059\u308B\u305F\ \u3081\u306B\u306F\u3001\u5730\u5143\u306E\u6C17\u8C61\u5E81\u3084\u5929\u6C17\u4E88\ \u5831\u30B5\u30FC\u30D3\u30B9\u306E\u516C\u5F0F\u30A6\u30A7\u30D6\u30B5\u30A4\u30C8\ \u3092\u53C2\u7167\u3059\u308B\u3053\u3068\u3082\u304A\u3059\u3059\u3081\u3067\u3059\ \u3002" ---
ChatGPT に対して問い合わせる側が人間であれば応答から自分で文脈をふまえて次の会話をすると思いますが,クライアントプログラムの場合は文脈をふまえた会話をしたければクライアント側のソフトウェアも ROS Topic を拾って自分で文脈を記録して解釈する必要があります.
Chat Completion API との応答を ROS Topic を介してやり取りしているので,複数の Chat ノード(openai_chat_rostopic.py)を実行してトピックの remap
をして互いのノードの応答を自らのノードの入力にすれば ChatGPT 同士で会話を続けるようにすることも簡単にできます.
そのために openai_chat.launch を次のように変更しました.
<launch> <arg name="key" default="$(env OPENAI_API_KEY)" /> <arg name="model" default="gpt-3.5-turbo" /> <arg name="service" default="false" /> <arg name="opponent" default="false" /> <arg name="prompt" default="You are a helpful assistant." /> <node if="$(arg service)" pkg="openai_ros" type="openai_chat_server.py" name="openai_chat_service" output="screen"> <param name="key" value="$(arg key)" /> <param name="model" value="$(arg model)" /> </node> <group unless="$(arg service)"> <node pkg="openai_ros" type="openai_chat_rostopic.py" name="openai_chat_topic" output="screen"> <param name="key" value="$(arg key)" /> <param name="model" value="$(arg model)" /> <param name="prompt" value="$(arg prompt)" /> </node> <group ns="opponent" if="$(arg opponent)"> <node pkg="openai_ros" type="openai_chat_rostopic.py" name="openai_chat_topic"> <param name="key" value="$(arg key)" /> <param name="model" value="$(arg model)" /> <param name="prompt" value="You are a good talker." /> <remap from="/opponent/request" to="/response" /> <remap from="/opponent/response" to="/request" /> </node> </group> </group> </launch>
opponent
で ChatGPT 同士の会話にするかを指定
output="screen"
はなしprompt
の設定でアシスタント同士だと会話が不自然な感じがしたので(とりあえず)2つ目のプロンプトは “You are a good talker.” としてみたremap
で /request
と /response
を入れ替えターミナル 1 : 2つのチャットノードの起動
openai_chat.launch の起動オプション opponent:=true
で2つのチャットノード実行とトピックの remap
を行います.
起動した状態では応答は何もないですが ターミナル 2 から ROS トピック /request
に最初のリクエストを1つパブリッシュすることで以後 ChatGPT 同士の会話が始まります.
robotuser@robotuser-PC:~$ roslaunch openai_ros openai_chat.launch opponent:=true ... logging to /home/robotuser/.ros/log/9cc9a1be-5dc8-11ee-9968-6b3ff6703622/roslaunch-robotuser-PC-35705.log Checking log directory for disk usage. This may take a while. Press Ctrl-C to interrupt Done checking log file disk usage. Usage is <1GB. started roslaunch server http://robotuser-PC:42615/ SUMMARY ======== PARAMETERS * /openai_chat_topic/key: sk-3JDluBbxsNuIhi... * /openai_chat_topic/model: gpt-3.5-turbo * /openai_chat_topic/prompt: You are a helpful... * /opponent/openai_chat_topic/key: sk-3JDluBbxsNuIhi... * /opponent/openai_chat_topic/model: gpt-3.5-turbo * /opponent/openai_chat_topic/prompt: You are a good ta... * /rosdistro: noetic * /rosversion: 1.16.0 NODES / openai_chat_topic (openai_ros/openai_chat_rostopic.py) /opponent/ openai_chat_topic (openai_ros/openai_chat_rostopic.py) auto-starting new master process[master]: started with pid [35714] ROS_MASTER_URI=http://localhost:11311 setting /run_id to 9cc9a1be-5dc8-11ee-9968-6b3ff6703622 process[rosout-1]: started with pid [35724] started core service [/rosout] process[openai_chat_topic-2]: started with pid [35731] process[opponent/openai_chat_topic-3]: started with pid [35732] [INFO] [1695882670.941923]: For 'system': You are a helpful assistant. [INFO] [1695882677.451265]: request: サッカーの盛んな国を1つ挙げてください. [INFO] [1695882679.335665]: assistant(token:60): ブラジルはサッカーの盛んな国として知られています. [INFO] [1695882705.653403]: request: そうですね,ブラジルは世界でも有名なサッカーの強豪国として知られています.ブラジルではサッカーは国民的なスポーツであり,多くの人々が熱狂的に応援しています. ブラジル代表チームは過去に5回のワールドカップ優勝を果たし,サッカーの歴史においても最も成功した国の一つです.有名な選手も多く輩出しており,ペレやジーコ,ロナウド,ロナウジーニョ,ネイマールなど,数々の伝説的なプレーヤーがブラジルから生まれています. ブラジルではサッカーの試合が行われると,町中が一体となって応援に熱が入ります.カラフルな応援旗やドラム,歌声,そして華麗なサンバの踊りなど,独特のエネルギーと情熱が試合会場を包みます. また,ブラジルには多くの有名なサッカークラブがあります.サンパウロのサンパウロFC,リオデジャネイロのフラメンゴ,サントス,リオグランデ・ド・スールのグレミオ,コリンチャンスなど,これらのクラブは強豪として名高いだけでなく,ファンの熱心さも有名です. ブラジルのサッカーは単なるスポーツ以上のものであり,国民の誇りやアイデンティティの一部となっています.サッカーを通じて,ブラジルの文化や人々の情熱を感じることができるでしょう. [INFO] [1695882713.741564]: assistant(token:754): その通りです.ブラジルのサッカーは国民の誇りであり,文化の一部として重要な役割を果たしています.多くの人々がサッカーに情熱を注ぎ,試合を熱狂的に応援する様子は見る価値があります.ブラジルのサッカーは世界中で愛され,その魅力は他の国にも広まっています.それだけに,ブラジルはサッカーの盛んな国として有名です. [INFO] [1695882744.670979]: request: そうですね,ブラジルのサッカーの魅力は世界中に広まっており,多くの人々がその情熱に共感しています.ブラジル代表チームやクラブチームの試合は,テレビやインターネットを通じて世界中に配信されており,多くのサッカーファンがその魅力に触れることができます. さらに,ブラジルのサッカー文化は技術,創造性,スピード,そしてリズム感を特徴としています.ブラジルのサッカー選手は驚くほど優れたテクニックを持ち,美しいプレーを見せることで知られています.彼らのキレのあるドリブル,正確なパス,そして豪快なシュートは,多くの人々に感動を与えます. ブラジルのサッカーの成功は,その国の熱狂的なサッカーカルチャーとも関連しています.子供たちは幼い頃からサッカーボールを蹴り,街角やビーチでプレーする様子をよく見かけます.サッカースクールやアカデミーも充実しており,若い才能は早いうちから育成されています. ブラジルのサッカーは単なるスポーツの一環ではなく,国民の誇りやアイデンティティの一部です.多くの人々が試合を通じて喜びや感動を共有し,サッカーを通じて結びついています.ブラジルのサッカー文化は他の国々にも影響を与え,彼らのスタイルやプレースタイルが憧れとなっています. ブラジルのサッカーは確かに盛んな国であり,その魅力は世界中に広がっています.それはブラジルの人々の情熱と才能,そしてサッカー文化の豊かさによるものです. [INFO] [1695882752.038028]: assistant(token:1519): 完全に同意します.ブラジルのサッカーカルチャーは,国民の情熱と才能,そして豊かなサッカー文化によって支えられています.その魅力は世界中に広まり,多くの人々がブラジルのサッカーに感動を覚えています.ブラジルのサッカーは間違いなく世界的な影響力を持っており,多くの国々で愛される存在です. [INFO] [1695882760.235372]: request: ありがとうございます.ブラジルのサッカーは確かに世界的な影響力を持っており,多くの人々に愛されています.その独特のスタイルと情熱は,他の国々のサッカーカルチャーにも大きな影響を与えています.ブラジルのサッカーは常に進化し,新たな才能が次々に生まれることで,さらなる魅力と成功を築いていくでしょう. ^C[opponent/openai_chat_topic-3] killing on exit [openai_chat_topic-2] killing on exit [rosout-1] killing on exit [master] killing on exit shutting down processing monitor... ... shutting down processing monitor complete done robotuser@robotuser-PC:~$
ターミナル 2 : 最初の話題投下
robotuser@robotuser-PC:~$ rostopic pub -1 /request std_msgs/String "サッカーの盛んな国を1つ挙げてください." publishing and latching message for 3.0 seconds robotuser@robotuser-PC:~$
ROS ノードグラフ
rqt の ROS Node Graph でノードとトピックの様子を確認してみると,2つのノード /openai_chat_topic
と /opponent/openai_chat_topic
とが互いの応答トピックを参照して循環していることが見て取れます.
ターミナル 1 の出力にも現れていますが段々と応答の文字数が互いに多くなる傾向があります.ChatGPT の token 数の上限に達して終わったりしますが,そうでない限りはずっと ChatGPT 同士で応答を続けるので終わらせたい場合は Ctrl-C で終わらせます.
文脈のデータを蓄積して多くなると Chat Completion API の token を消費してしまいますし,応答に時間がかかったりもします.一定時間会話がなかった場合やそれまでの文脈からがらりと話題を変える場合のために文脈を含めたメッセージデータを初期化するメソッドも運用上は必要かもしれません.
また,OpenAI 以外の大規模言語モデル(LLM)の API の ROS Topic ラッパがあれば(or を作れば)異なる LLM 間での会話も可能であろうと思います.
今回の記事はここまでです.
本シリーズ前回の記事 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 チャットプログラム」を作成した様子を紹介する予定です.
今回の記事はここまでです.