タグアーカイブ topic

著者:yamamoto.yosuke

SwitchBot を ROS から利用する – データ取得デバイスの追加方法

本シリーズ前回の記事 SwitchBot を ROS から利用する – データ取得編 掲載以降,手元にある SwitchBot デバイスの種類が増えてきました.

そこで今回の記事では switchbot_ros でまだ対応していないデバイスのデータを取得・パブリッシュするためにコードを追加する様子を紹介します.追加例としてデバイス「SwitchBot CO2センサー(温湿度計)」のデータを取得して ROS Topic としてパブリッシュできるようにします.

SwitchBot API の確認

追加したいデバイスが SwitchBot API で対応していないと switchbot_ros 側でも対応はできないので最初に SwitchBot API のドキュメントを調べます.

SwitchBot API v1.1 ドキュメント にデバイスのリスト取得とステータス取得の項目に「Meter Pro CO2」の記述があり,これが今回追加してみたデバイス「SwitchBot CO2センサー(温湿度計)」に対応したものとなっていました.

制御コマンドの一覧には CO2 センサーらしき記述が(少なくとも本記事執筆時には)ありませんので「SwitchBot CO2センサー(温湿度計)」には取得系コマンドのみが対応していて,制御系コマンドは無いようです.

SwitchBot API ドキュメントにある「SwitchBot CO2センサー(温湿度計)」のステータス取得で返されるデータの表を下に抜粋しましたので確認してみます.

Meter Pro CO2

Key Value Type Description
deviceId String device ID
deviceType String device type. MeterPro(CO2)
hubDeviceId String device’s parent Hub ID
battery Integer the current battery level, 0-100
version String the current firmware version, e.g. V4.2
temperature Float temperature in celsius
humidity Integer humidity percentage
CO2 Integer CO2 ppm value, 0-9999

二酸化炭素濃度が単位 [ppm](百万分率)で範囲 0〜9999 の整数型データとして返されることが分かります.他は SwitchBot の温湿度計と同じように各測定値が得られるようです.

switchbot_ros への機能追加

switchbot_ros に機能追加したいデバイス「SwitchBot CO2センサー(温湿度計)」について SwitchBot API が対応していることと API により取得できる測定値が分かりましたので実装してゆきます.もう少し具体化すると「SwitchBot CO2センサー(温湿度計)」から API 経由で得られた

  • 温度
  • 湿度
  • バッテリーレベル
  • CO2濃度

の4つの値を ROS Topic としてパブリッシュします.

SwitchBot API によるデータの取得と ROS Topic のパブリッシュプロセスは switchbot_ros にある既存のルーチンをそのまま利用できますので今回新たに実装する部分は大まかには次の2つです.

  • 「SwitchBot CO2センサー(温湿度計)」に対応したメッセージ型の定義
  • 「SwitchBot CO2センサー(温湿度計)」メッセージ型に取得データを代入

先に結論としてコードの追加部分を下に記載してしまいます.ファイルとしては3点,追加箇所としては5点です.

  1. switchbot_ros / msg / MeterProCO2.msg (ファイル追加)
    • 「SwitchBot CO2センサー(温湿度計)」に対応したメッセージ型の定義
  2. switchbot_ros / CMakeLists.txt (記述追加)
    • MeterProCO2.msg の add_message_files() への追加
  3. switchbot_ros / scripts / switchbot_status_publisher.py (記述追加)
    • インポートクラス MeterProCO2 の追加
    • パブリッシャーのメッセージクラス設定においてデバイスタイプ MeterPro(CO2) の場合のメッセージクラス MeterProCO2 を追加
    • MeterProCO2 の場合のメッセージクラス各要素への取得データ代入処理の追加

追加デバイス用メッセージの定義

今回追加する「SwitchBot CO2センサー(温湿度計)」に対応したメッセージ型 MeterProCO2.msg を定義します.

パブリッシュする4つのデータ「温度」「湿度」「バッテリーレベル」「CO2濃度」のうち3つ 温度,湿度,バッテリーレベル は Meter.msg と同じなので Meter.msg を複製してファイル名を MeterProCO2.msg に変更して CO2濃度 のデータ型を追加する方法で作成しました.

switchbot_ros / msg / MeterProCO2.msg

1
2
3
4
5
6
7
Header  header       # timestamp
 
float64 temperature  # temperature in celsius
float64 humidity     # humidity percentage
float64 battery      # the current battery level, 0-100
 
int64   co2ppm       # CO2 ppm value, 0-9999

新しいメッセージ型を定義した場合はパッケージの CMakeLists.txt 内の add_message_files() に追記する必要があります.

switchbot_ros / CMakeLists.txt

13
14
15
16
17
18
19
20
21
22
23
add_message_files(
  FILES
  Device.msg
  DeviceArray.msg
  Meter.msg
  MeterProCO2.msg
  PlugMini.msg
  Hub2.msg
  Bot.msg
  StripLight.msg
)

追加デバイス対応プログラムコード更新

新しいメッセージ型 MeterProCO2.msg の準備ができましたので取得データの代入などの機能をパブリッシャーの Python のプログラムファイル switchbot_ros / scripts / switchbot_status_publisher.py に追加記述します.

追加した MeterProCO2.msg を switchbot_status_publisher.py 内で利用するためにメッセージクラス MeterProCO2import しておきます.

switchbot_ros / scripts / switchbot_status_publisher.py

1
2
3
4
5
6
7
8
#!/usr/bin/env python
 
import os.path
from requests import ConnectionError
import rospy
from switchbot_ros.switchbot import SwitchBotAPIClient
from switchbot_ros.switchbot import DeviceError, SwitchBotAPIError
from switchbot_ros.msg import Meter, PlugMini, Hub2, Bot, StripLight, MeterProCO2

初期化関数定義 def __init__(self): 内のパブリッシャーのメッセージクラス設定を行う部分にデバイスタイプ MeterPro(CO2) の場合の条件分岐を追加して MeterProCO2 をメッセージクラスとして設定します.

switchbot_ros / scripts / switchbot_status_publisher.py

66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# Publisher Message Class for each device type
if self.device_type == 'Remote':
    rospy.logerr('Device Type: "' + self.device_type + '" has no status in specifications.')
    return
else:
    if self.device_type == 'Meter':
        self.msg_class = Meter
    elif self.device_type == 'MeterPlus':
        self.msg_class = Meter
    elif self.device_type == 'WoIOSensor':
        self.msg_class = Meter
    elif self.device_type == 'Hub 2':
        self.msg_class = Hub2
    elif self.device_type == 'Plug Mini (JP)':
        self.msg_class = PlugMini
    elif self.device_type == 'Plug Mini (US)':
        self.msg_class = PlugMini
    elif self.device_type == 'Bot':
        self.msg_class = Bot
    elif self.device_type == 'Strip Light':
        self.msg_class = StripLight
    elif self.device_type == 'MeterPro(CO2)':
        self.msg_class = MeterProCO2
    else:
        rospy.logerr('No publisher process for "' + self.device_type + '" in switchbot_status_publisher.py')
        return

繰り返し関数定義 def spin(self): 内のパブリッシュするメッセージクラスごとの場合分けに MeterProCO2 を追加して SwitchBot API により取得した各データを MeterProCO2 メッセージの各要素に代入します.

switchbot_ros / scripts / switchbot_status_publisher.py

120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
if status:
    time = rospy.get_rostime()
    if self.msg_class == Meter:
        msg = Meter()
        msg.header.stamp = time
        msg.temperature  = status['temperature']
        msg.humidity     = status['humidity']
        msg.battery      = status['battery']
    elif self.msg_class == Hub2:
        msg = Hub2()
        msg.header.stamp = time
        msg.temperature  = status['temperature']
        msg.humidity     = status['humidity']
        msg.light_level  = status['lightLevel']
    elif self.msg_class == PlugMini:
        msg = PlugMini()
        msg.header.stamp = time
        msg.voltage      = status['voltage']
        msg.weight       = status['weight']
        msg.current      = status['electricCurrent']
        msg.minutes_day  = status['electricityOfDay']
    elif self.msg_class == Bot:
        msg = Bot()
        msg.header.stamp = time
        msg.battery      = status['battery']
        if status['power'] == 'on':
            msg.power    = True
        else:
            msg.power    = False
        msg.device_mode  = status['deviceMode']
    elif self.msg_class == StripLight:
        msg = StripLight()
        msg.header.stamp = time
        if status['power'] == 'on':
            msg.power    = True
        else:
            msg.power    = False
        msg.brightness   = status['brightness']
        rgb_string       = status['color']
        r, g, b = map(int, rgb_string.split(':'))
        msg.color_r      = r
        msg.color_g      = g
        msg.color_b      = b
    elif self.msg_class == MeterProCO2:
        msg = MeterProCO2()
        msg.header.stamp = time
        msg.temperature  = status['temperature']
        msg.humidity     = status['humidity']
        msg.battery      = status['battery']
        msg.co2ppm       = status['CO2']
    else:
        return

今回の「SwitchBot CO2センサー(温湿度計)」のデータ取得とその ROS Topic へのパブリッシュに関するファイルの追加・コード変更は以上です.

ビルドと環境設定

今回の変更で新しいメッセージ型を定義して導入していますのでパッケージのビルドを行い,環境設定を改めて行います.

switchbot_ros を含む jsk_3rdparty のビルド

$ source /opt/ros/noetic/setup.bash
$ cd ~/switchbot_ws
$ catkin build
$ source ~/switchbot_ws/devel/setup.bash

switchbot_ros に追加した機能の動作確認

まず利用可能な SwitchBot デバイス名リストを取得・確認してから,得られた新規追加したデバイス名を指定して SwitchBot デバイスのステータスデータの取得と ROS Topic へのパブリッシュを行い,実際にパブリッシュされているかを ROS Topic を表示して確認します.

実行方法は今回機能追加するより前の方法と同じで,launch 時にオプション指定するデバイス名を変えるだけです.

利用可能な SwitchBot デバイス名の取得

ターミナル 1 : switchbot_ros の実行

SwitchBot API 経由で今回追加した「SwitchBot CO2センサー(温湿度計)」の「デバイス名」と「デバイスタイプ」が取得できることを確認します.ユーザの SwitchBot アカウントで登録されているデバイスの「デバイス名」と「デバイスタイプ」は switchbot.launch を実行すると表示されます.

(下記 launch オプションの YOUR_TOKENYOUR_SECRET をそれぞれユーザアカウントのトークンとシークレットに置き換えて実行)

switchbot.launch 実行入力

$ source ~/switchbot_ws/devel/setup.bash
$ roslaunch switchbot_ros switchbot.launch token:=YOUR_TOKEN secret:=YOUR_SECRET

switchbot.launch 実行出力例

... logging to /home/robotuser/.ros/log/999f113c-b07d-11ef-9b82-a11b8fd4bdd2/roslaunch-robotuser-PC-7718.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:43087/
 
SUMMARY
========
 
PARAMETERS
 * /rosdistro: noetic
 * /rosversion: 1.17.0
 * /switchbot_ros/secret: (シークレットの上位数桁が表示)...
 * /switchbot_ros/token: (トークンの上位数桁が表示)...
 
NODES
  /
    switchbot_ros (switchbot_ros/switchbot_ros_server.py)
 
auto-starting new master
process[master]: started with pid [7726]
ROS_MASTER_URI=http://localhost:11311
 
setting /run_id to 999f113c-b07d-11ef-9b82-a11b8fd4bdd2
process[rosout-1]: started with pid [7736]
started core service [/rosout]
process[switchbot_ros-2]: started with pid [7743]
[INFO] [1733123900.102425]: Switchbot API Client initialized.
[INFO] [1733123900.104175]: Using SwitchBot API v1.1
[INFO] [1733123900.106131]: Switchbot Device List:
9 Item(s)
deviceName: bot74a, deviceID: (固有のID番号が表示), deviceType: Bot
deviceName: cam-entrance01, deviceID: (固有のID番号が表示), deviceType: None
deviceName: co2sensor-ba1, deviceID: (固有のID番号が表示), deviceType: MeterPro(CO2)
deviceName: hub2a, deviceID: (固有のID番号が表示), deviceType: Hub 2
deviceName: plugmini7a1, deviceID: (固有のID番号が表示), deviceType: Plug Mini (JP)
deviceName: remote-button10a, deviceID: (固有のID番号が表示), deviceType: Remote
deviceName: tapelight7a1, deviceID: (固有のID番号が表示), deviceType: Strip Light
deviceName: thermo-hygrometer-f7a, deviceID: (固有のID番号が表示), deviceType: Meter
deviceName: trackcard01, deviceID: (固有のID番号が表示), deviceType: None
 
[INFO] [1733123900.107799]: Switchbot Remote List:
2 Item(s)
deviceName: air-conditioner, deviceID: (固有のID番号が表示), remoteType: Air Conditioner
deviceName: pendant-light, deviceID: (固有のID番号が表示), remoteType: DIY Light
 
[INFO] [1733123900.109488]: Switchbot Scene List:
2 Item(s)
sceneName: turnoff-all-lights, sceneID: (固有のID番号が表示)
sceneName: turnon-all-lights, sceneID: (固有のID番号が表示)
 
[INFO] [1733123900.139015]: Ready.

追加した「SwitchBot CO2センサー(温湿度計)」のデバイス名 co2sensor-ba1 がコンソール出力されたので一旦 Ctrl-C にて switchbot.launch を終了します.

SwitchBot デバイスのステータスデータの取得・パブリッシュ

「SwitchBot CO2センサー(温湿度計)」のステータスデータを取得してパブリッシュされている ROS Topic を表示して確認してみます.前述の switchbot.launch の実行出力例からデバイス名が co2sensor-ba1 となっています.

ステータスデータを取得する場合は switchbot.launch 実行時に次の2つのオプションを追加します.

  • pub_status:=true ステータスを取得・パブリッシュを実行するオプション true/false
  • pub_device_name:= デバイス名の指定(今回の例 co2sensor-ba1

ターミナル 1 : switchbot_ros の実行

switchbot.launch 実行入力

$ source ~/switchbot_ws/devel/setup.bash
$ roslaunch switchbot_ros switchbot.launch token:=YOUR_TOKEN secret:=YOUR_SECRET pub_status:=true pub_device_name:=co2sensor-ba1 pub_status_rate:=0.016666
  • 注1) 上記テキストボックスの横スクロールで全 launch オプションが表示されます.
  • 注2) 各 launch オプションについて
    • YOUR_TOKENYOUR_SECRET は各々の SwitchBot アカウントのトークンとシークレットに置き換えて実行してください.
    • pub_status:=true でステータスを取得・パブリッシュを実行します.
    • pub_device_name:=co2sensor-ba1co2sensor-ba1 は各ユーザ利用のデバイス名に変更してください.
    • pub_status_rate:=0.016666 でステータスを取得・パブリッシュする周波数 [Hz] を設定します.(本例では 0.016666[Hz] = 約60秒に1回)

switchbot.launch 実行出力例

... logging to /home/robotuser/.ros/log/e454822a-b07d-11ef-9b82-a11b8fd4bdd2/roslaunch-robotuser-PC-7804.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:36557/
 
SUMMARY
========
 
PARAMETERS
 * /rosdistro: noetic
 * /rosversion: 1.17.0
 * /switchbot_ros/secret: (シークレットの上位数桁が表示)...
 * /switchbot_ros/token: (トークンの上位数桁が表示)...
 * /switchbot_status_publisher/device_name: co2sensor-ba1
 * /switchbot_status_publisher/rate: 0.016666
 * /switchbot_status_publisher/secret: (シークレットの上位数桁が表示)...
 * /switchbot_status_publisher/token: (トークンの上位数桁が表示)...
 
NODES
  /
    switchbot_ros (switchbot_ros/switchbot_ros_server.py)
    switchbot_status_publisher (switchbot_ros/switchbot_status_publisher.py)
 
auto-starting new master
process[master]: started with pid [7812]
ROS_MASTER_URI=http://localhost:11311
 
setting /run_id to e454822a-b07d-11ef-9b82-a11b8fd4bdd2
process[rosout-1]: started with pid [7822]
started core service [/rosout]
process[switchbot_ros-2]: started with pid [7829]
process[switchbot_status_publisher-3]: started with pid [7830]
[INFO] [1733124025.952044]: Switchbot API Client initialized.
[INFO] [1733124025.953872]: Using SwitchBot API v1.1
[INFO] [1733124025.955817]: Switchbot Device List:
9 Item(s)
deviceName: bot74a, deviceID: (固有のID番号が表示), deviceType: Bot
deviceName: cam-entrance01, deviceID: (固有のID番号が表示), deviceType: None
deviceName: co2sensor-ba1, deviceID: (固有のID番号が表示), deviceType: MeterPro(CO2)
deviceName: hub2a, deviceID: (固有のID番号が表示), deviceType: Hub 2
deviceName: plugmini7a1, deviceID: (固有のID番号が表示), deviceType: Plug Mini (JP)
deviceName: remote-button10a, deviceID: (固有のID番号が表示), deviceType: Remote
deviceName: tapelight7a1, deviceID: (固有のID番号が表示), deviceType: Strip Light
deviceName: thermo-hygrometer-f7a, deviceID: (固有のID番号が表示), deviceType: Meter
deviceName: trackcard01, deviceID: (固有のID番号が表示), deviceType: None
 
[INFO] [1733124025.957387]: Switchbot Remote List:
2 Item(s)
deviceName: air-conditioner, deviceID: (固有のID番号が表示), remoteType: Air Conditioner
deviceName: pendant-light, deviceID: (固有のID番号が表示), remoteType: DIY Light
 
[INFO] [1733124025.959085]: Switchbot Scene List:
2 Item(s)
sceneName: turnoff-all-lights, sceneID: (固有のID番号が表示)
sceneName: turnon-all-lights, sceneID: (固有のID番号が表示)
 
[INFO] [1733124025.981926]: Ready.
[INFO] [1733124026.191206]: Switchbot API Client initialized.
[INFO] [1733124026.192948]: Using SwitchBot API v1.1
[INFO] [1733124026.195299]: Rate: 0.016666
[INFO] [1733124026.197702]: deviceName: co2sensor-ba1 / deviceType: MeterPro(CO2)
[INFO] [1733124026.200537]: Ready: SwitchBot Status Publisher for co2sensor-ba1

ターミナル 2 : ROS Topic の確認

rostopic list の実行入力

$ source ~/switchbot_ws/devel/setup.bash
$ rostopic list

rostopic list の実行出力例

/rosout
/rosout_agg
/switchbot_ros/devices
/switchbot_ros/switch/cancel
/switchbot_ros/switch/feedback
/switchbot_ros/switch/goal
/switchbot_ros/switch/result
/switchbot_ros/switch/status
/switchbot_status_publisher/co2sensor_ba1

rostopic echo 実行入力

$ rostopic echo /switchbot_status_publisher/co2sensor_ba1

rostopic echo の実行出力例

header:
  seq: 1
  stamp:
    secs: 1733124088
    nsecs:   5677223
  frame_id: ''
temperature: 25.4
humidity: 35.0
battery: 100.0
co2ppm: 562
---
header:
  seq: 2
  stamp:
    secs: 1733124147
    nsecs: 910016298
  frame_id: ''
temperature: 25.4
humidity: 35.0
battery: 100.0
co2ppm: 562
---

「SwitchBot CO2センサー(温湿度計)」のステータスデータとして CO2濃度 などが取得されて ROS Topic にパブリッシュされている様子が見て取れるかと思います.

今回の記事はここまでです.

著者:yamamoto.yosuke

ChatGPT と ROS – ROS Topic を介した ChatGPT チャットプログラム

本シリーズ前回の記事 1. ROS Service プログラムの文脈をふまえたチャット対応 では OpenAI の Chat Completion API を利用して過去のチャット履歴もふまえたチャットを行える ROS Service プログラムを作成した様子をお伝えしました.

今回の記事では Chat Completion API を利用した「文脈をふまえたチャット」をする ROS ソフトウェアを実装してみた2つ目の方法「2. ROS Topic を介した ChatGPT チャットプログラム」を作成した様子を紹介します.

  1. ROS Service プログラムの文脈をふまえたチャット対応
    • 「1問1答」形式 →「チャット」形式
  2. ROS Topic を介した ChatGPT チャットプログラム
    • ROS Service の応答 → ROS Topic のやり取りによる ChatGPT とのチャット

ROS Topic を介した ChatGPT のチャットプログラム

前回,比較的短文のチャットを扱う Chat Completion API へのアクセスであれば ROS Service よりも ROS Topic を介したメッセージのやり取りの方が ROS ノード内でのチャット会話に限られず,より ROS に親和的でよりシンプルな構成になるのでは?という反省がありました.

今回の ROS Topic を用いたチャットプログラムの作成方針は次のようにしました.

  • Chat Completion API にアクセスする ROS Node
    • ROS Topic /request を購読してユーザの発言を得る
    • ユーザの発言をふまえて Chat Completion API にアクセスして応答を ROS Topic /response としてパブリッシュする
    • チャットの履歴データを蓄積する
  • チャットユーザ側
    • 質問を ROS Topic /request にパブリッシュする
    • 返答は ROS Topic /response を購読して得る

ROS Service プログラムの場合はチャット履歴をふまえたとしてもチャット機能提供側とユーザとの1者対1者でのやり取りでしたが,ROS Topic にすることでチャット機能提供側と複数のユーザの1者対他者でのやり取りも可能になる利点もあります.

ソースコード

ROS Topic を介した文脈をふまえたチャットプログラムで追加したファイルは次の2つです.

  • scripts / openai_chat_rostopic.py
  • launch / openai_chat.launch

サービスの定義など考慮しなくて良いので非常にシンプルです.

以下,それぞれのファイル内のコードを記載して少し説明をします.

scripts / openai_chat_rostopic.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/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()
    
  • L25,31: ROS Topic /request を購読(Subscribe)してトピックを受け取ったら callback() メソッドを呼び出す
  • L35: callback() メソッド内で新たなリクエストをチャット履歴に追加
  • L37-40: Chat Completion API に投げる
  • L47: Chat Completion API からの返答をチャット履歴に追加
  • ROS Topic /response としてパブリッシュ

launch / openai_chat.launch

launch オプション service を用いて前回の記事で紹介した ROS Service によるチャットプログラムと今回の ROS Topic を介したチャットプログラムのどちらを実行するかを切り替えるようにしています.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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 を拾って自分で文脈を記録して解釈する必要があります.

ChatGPT vs ChatGPT

Chat Completion API との応答を ROS Topic を介してやり取りしているので,複数の Chat ノード(openai_chat_rostopic.py)を実行してトピックの remap をして互いのノードの応答を自らのノードの入力にすれば ChatGPT 同士で会話を続けるようにすることも簡単にできます.

そのために openai_chat.launch を次のように変更しました.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<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>
  • L6: launch オプシション opponent で ChatGPT 同士の会話にするかを指定
  • L22: 2つ目のチャットノードは別のネームスペースとして区別
  • L23: 2つ目のチャットノードのコンソール出力も表示すると内容が重複するので output="screen" はなし
  • L26: prompt の設定でアシスタント同士だと会話が不自然な感じがしたので(とりあえず)2つ目のプロンプトは “You are a good talker.” としてみた
  • L27-28: 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 間での会話も可能であろうと思います.


今回の記事はここまでです.

著者:yamamoto.yosuke

ChatGPT と ROS – ROS Service プログラムの文脈をふまえたチャット対応

本シリーズ前回の記事 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 プログラムの文脈をふまえたチャット対応
    • 「1問1答」形式 →「チャット」形式
  2. ROS Topic を介した ChatGPT チャットプログラム
    • ROS Service の応答 → ROS Topic のやり取りによる ChatGPT とのチャット

今回の記事ではこれらのうち「1. ROS Service プログラムの文脈をふまえたチャット対応」を行った様子をお伝えします.

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 にアクセスする方法を採ってみました.

  • チャット対応 ROS Service プログラム概要
    • サービスサーバ側で Chat Completion API にアクセス(シンプルなラッパー)
    • サービスを呼び出す側でチャットの履歴を保持してチャットメッセージを作成
    • サービスのメッセージ型
      • JSON 形式の文字列として Chat Completion API へのメッセージおよびレスポンスのデータをやりとり

プログラム作成時の ChatGPT の活用

今回も Web サービスの ChatGPT の助けを借りてプログラムを作成したのですが,まるごと Python コードを作成するための問いの仕方が下手だったのか,ChatGPT の応答でしっくり来るようなソースコードは得られませんでした.そこで Python コードを記述する際の個別具体的な問題について ChatGPT の力を借りました.

例えば Chat Completion API へのメッセージやレスポンスを文字列(string)にしてやり取りする方法を聞きました.

これも聞いた時の少し前の時期に Web API を扱っていたことで Python API と Web API を混同していて,聞き方も JSON データ(文字列)と Python リストを混同していてかなりあやしいですが辻褄を合わせて何とか答えてくれるのも ChatGPT ですね.(小文字)

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のTrueFalseをそれぞれtruefalseとして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 に質問してみました.

User

Chat Completion API の渡し値の role における system と assistant の違いをおしえて?


ChatGPT

Chat Completion APIの文脈におけるroleの値systemassistantは,入力メッセージのコンテキストを形成するためのものです.各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
  • scripts / openai_chat_server.py
  • scripts / openai_chat_client.py
  • launch / openai_chat.launch

以下,それぞれのファイル内のコードを記載して少し説明をします.

srv / ChatCompletion.srv

先述したように Chat Completion API とやり取りする Python データ構造データを JSON 形式の文字列にして ROS Service との通信を行います.Chat Completion API に渡すデータをmessages,受取るデータをresponseというstring型にしています.

1
2
3
string messages
---
string response

scripts / openai_chat_server.py

ROS Service chat_service としてデータを JSON 形式の文字列として授受し,Chat Completion API へのアクセスを行っています.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/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

ChatGPT に教えてもらったように role の「環境やセッションの全体的な指示やコンテキストを設定する」 systemprompt の内容を設定します.

ROS Node 内でユーザからのインプットを受け取り,これまでのメッセージ履歴に追加して,メッセージデータを JSON 文字列として ROS Service chat_service に送っています.サービスからの応答データは JSON 文字列から Python リストに変換されて含まれていた返答メッセージはprint()してメッセージ履歴にも追加することを繰り返しています.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/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

環境変数から OpenAI API Key を読み取って ROS Parameter に設定し, Chat Completion API にアクセスする ROS Service を実行します.

1
2
3
4
5
6
7
8
9
10
11
<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

今回の ROS Service の定義ファイルを記述して使えるようにします.

18
19
20
21
22
23
24
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 チャットプログラム」を作成した様子を紹介する予定です.

今回の記事はここまでです.