タグアーカイブ api

著者:yamamoto.yosuke

SwitchBot を ROS から利用する – データ取得編

本シリーズ前回の記事 SwitchBot を ROS から利用する – コマンド操作編2 では SwitchBot を ROS から利用する switchbot_ros のサンプルのソースコードで扱われていた SwitchBot デバイス以外のものを ROS から操作するために SwitchBot API のコマンドセットを調べて control_switchbot.py に実装する過程について紹介しました.

今回は SwitchBot デバイスのステータスデータの取得と ROS トピックへのパブリッシュを行ってみます.

switchbot_ros の更新・ビルド

前回の記事 SwitchBot を ROS から利用する – コマンド操作編2 を公開した後に GitHub 上の switchbot_ros が更新されて SwitchBot デバイスのステータスデータの取得とパブリッシュを行うソフトウェアソースコードが追加されました.

更新された switchbot_ros を実際に動作させる Ubuntu PC 内の switchbot_ros に適用してビルドします.

今回初めて switchbot_ros を使う場合は前々回の記事 SwitchBot を ROS から利用する – コマンド操作編1switchbot_ros のビルド の項目を参考にワークスペースの作成やクローン・ビルドを実行してください.このように新規にクローンしてきた場合は次の更新作業は不要です.

既に前回の記事の時点の switchbot_ros を含む jsk_3rdparty をクローンして利用している場合は次の手順で更新された GitHub 上の jsk_3rdparty を git でプル(ダウンロード更新)してビルドします.

switchbot_ros を含む jsk_3rdparty の更新とビルド

$ source ~/switchbot_ws/devel/setup.bash
$ cd ~/switchbot_ws/src/jsk_3rdparty
$ git checkout master
$ git pull origin master
$ catkin build
$ source ~/switchbot_ws/devel/setup.bash

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

ターミナル 1 : switchbot_ros の実行

前回記事と同じですがユーザの 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/87b6e5c8-c1a2-11ee-bce7-1d89a9d14e1f/roslaunch-robotuser-PC-62866.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:40731/

SUMMARY
========

PARAMETERS
 * /rosdistro: noetic
 * /rosversion: 1.16.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 [62874]
ROS_MASTER_URI=http://localhost:11311

setting /run_id to 87b6e5c8-c1a2-11ee-bce7-1d89a9d14e1f
process[rosout-1]: started with pid [62884]
started core service [/rosout]
process[switchbot_ros-2]: started with pid [62891]
[INFO] [1706861436.195243]: Switchbot API Client initialized.
[INFO] [1706861436.199678]: Using SwitchBot API v1.1
[INFO] [1706861436.204957]: Switchbot Device List:
6 Item(s)
deviceName: bot74a, deviceID: (固有のID番号が表示), deviceType: Bot
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

[INFO] [1706861436.208853]: Switchbot Remote List:
2 Item(s)
deviceName: air-conditioner, deviceID: (固有のID番号が表示), remoteType: Air Conditioner
deviceName: pendant-light, deviceID: (固有のID番号が表示), remoteType: DIY Light

[INFO] [1706861436.214168]: Switchbot Scene List:
3 Item(s)
sceneName: turnoff-all-lights, sceneID: (固有のID番号が表示)
sceneName: turnon-all-lights, sceneID: (固有のID番号が表示)
sceneName: turnon-all-lights, sceneID: (固有のID番号が表示)

[INFO] [1706861436.254126]: Ready.

利用可能なデバイス名がコンソール出力されたので一旦 Ctrl-C にて switchbot.launch を終了します.

上記の switchbot.launch 実行出力例にある SwitchBot デバイスのうち取得するステータスがない Remote 以外の次のデバイスタイプは switchbot_ros にてステータスデータを取得することができます.

  • Bot
  • Hub 2
  • Meter
  • Plug Mini (JP)
  • Strip Light

また上記リスト以外のデータ取得 API 提供がされている SwitchBot デバイスについては switchbot_ros のコードに組み込まれていませんが適宜情報をコードに加えれば switchbot_ros からもデータ取得できるようになると思います.

SwitchBot デバイスのステータスデータの取得と確認

実行例として今回は SwitchBot の温湿度計(デバイスタイプ Meter)のステータスデータを取得してパブリッシュされている ROS トピックを表示してみます.先述の switchbot.launch の実行出力例から読み取ると,該当するデバイス名が thermo-hygrometer-f7a となっています.

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

  • pub_status:=true ステータスを取得・パブリッシュを実行するオプション true/false
  • pub_device_name:=thermo-hygrometer-f7a デバイス名の指定(本例では thermo-hygrometer-f7a)

ターミナル 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:=thermo-hygrometer-f7a
  • 注1) 上記テキストボックスの横スクロールで全 launch オプションが表示されます.
  • 注2) 各 launch オプションについて
    • YOUR_TOKENYOUR_SECRET は各々の SwitchBot アカウントのトークンとシークレットに置き換えて実行してください.
    • pub_status:=true でステータスを取得・パブリッシュを実行します.
    • pub_device_name:=thermo-hygrometer-f7athermo-hygrometer-f7a
      は各ユーザ利用のデバイス名に変更してください.

switchbot.launch 実行出力例

... logging to /home/robotuser/.ros/log/81bc64b6-faf2-11ee-8dad-e57ee950b51d/roslaunch-robotuser-PC-28197.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:35371/

SUMMARY
========

PARAMETERS
 * /rosdistro: noetic
 * /rosversion: 1.16.0
 * /switchbot_ros/secret: (シークレットの上位数桁が表示)...
 * /switchbot_ros/token: (トークンの上位数桁が表示)...
 * /switchbot_status_publisher/device_name: thermo-hygrometer...
 * /switchbot_status_publisher/rate: 0.1
 * /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 [28205]
ROS_MASTER_URI=http://localhost:11311

setting /run_id to 81bc64b6-faf2-11ee-8dad-e57ee950b51d
process[rosout-1]: started with pid [28215]
started core service [/rosout]
process[switchbot_ros-2]: started with pid [28222]
process[switchbot_status_publisher-3]: started with pid [28223]
[INFO] [1713163000.937913]: Switchbot API Client initialized.
[INFO] [1713163000.938005]: Switchbot API Client initialized.
[INFO] [1713163000.940084]: Using SwitchBot API v1.1
[INFO] [1713163000.940382]: Using SwitchBot API v1.1
[INFO] [1713163000.942545]: Switchbot Device List:
6 Item(s)
deviceName: bot74a, deviceID: (固有のID番号が表示), deviceType: Bot
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

[INFO] [1713163000.944131]: Switchbot Remote List:
2 Item(s)
deviceName: air-conditioner, deviceID: (固有のID番号が表示), remoteType: Air Conditioner
deviceName: pendant-light, deviceID: (固有のID番号が表示), remoteType: DIY Light

[INFO] [1713163000.944268]: Rate: 0.1
[INFO] [1713163000.945732]: Switchbot Scene List:
2 Item(s)
sceneName: turnoff-all-lights, sceneID: (固有のID番号が表示)
sceneName: turnon-all-lights, sceneID: (固有のID番号が表示)

[INFO] [1713163000.947428]: deviceName: thermo-hygrometer-f7a / deviceType: Meter
[INFO] [1713163000.951801]: Ready: SwitchBot Status Publisher for thermo-hygrometer-f7a
[INFO] [1713163000.966800]: Ready.

ターミナル 2 : ROS トピックの確認

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/thermo_hygrometer_f7a

rostopic echo 実行入力

$ rostopic echo /switchbot_status_publisher/thermo_hygrometer_f7a

rostopic echo の実行出力例

header: 
  seq: 1
  stamp: 
    secs: 1713163093
    nsecs: 412018775
  frame_id: ''
temperature: 26.9
humidity: 36.0
battery: 100.0
---
header: 
  seq: 2
  stamp: 
    secs: 1713163103
    nsecs: 447003364
  frame_id: ''
temperature: 26.9
humidity: 36.0
battery: 100.0
---
header: 
  seq: 3
  stamp: 
    secs: 1713163113
    nsecs: 380291700
  frame_id: ''
temperature: 26.9
humidity: 36.0
battery: 100.0

SwitchBot 温湿度計 Meter のステータスデータとして温度・湿度などが取得されて ROS トピックにパブリッシュされている様子がわかるかと思います.

ステータスの取得とパブリッシュの間隔は switchbot.launch のデフォルト設定で 0.1 [Hz] = 10秒間隔 になっています.これを変更する場合には switchbot.launch のオプションで pub_status_rate:=0.05 のように追加します.温湿度のように急に変化しなさそうなデータの場合はもっと長めの間隔でも良いかもしれません.

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

著者:yamamoto.yosuke

SwitchBot を ROS から利用する – コマンド操作編2

本シリーズ前回の記事 SwitchBot を ROS から利用する – コマンド操作編1 では SwitchBot を ROS から利用する switchbot_ros の導入とサンプル Python コードの実行の様子を紹介しました.

今回は前回の記事の続きとしてサンプルのソースコードで扱われていた SwitchBot デバイス以外のものを ROS から操作するために SwitchBot API のコマンドセットを調べて control_switchbot.py に実装する過程について紹介します.

  1. 利用可能な SwitchBot デバイス情報の取得
  2. SwitchBot デバイス API コマンドセットの調査
  3. SwitchBot デバイスコマンドのソースコード追記

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

switchbot_ros のサンプル Python コード control_switchbot.py においてボット(スイッチ)をオンにする命令は次のようになっていて「デバイス名」とそれに対する「コマンド」の2つを指定する必要があります.

client.control_device('bot74a', 'turnOn')
client.control_device('デバイス名', 'コマンド')

このうち「コマンド」は SwitchBot API にてデバイスタイプごとに設定されているので「デバイスタイプ」が何かを知る必要があります.

ユーザのアカウントで登録されているデバイスの「デバイス名」と「デバイスタイプ」は switchbot.launch を実行すると表示されます.

(下記 launch オプションの YOUR_TOKEN と YOUR_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/87b6e5c8-c1a2-11ee-bce7-1d89a9d14e1f/roslaunch-robotuser-PC-62866.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:40731/

SUMMARY
========

PARAMETERS
 * /rosdistro: noetic
 * /rosversion: 1.16.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 [62874]
ROS_MASTER_URI=http://localhost:11311

setting /run_id to 87b6e5c8-c1a2-11ee-bce7-1d89a9d14e1f
process[rosout-1]: started with pid [62884]
started core service [/rosout]
process[switchbot_ros-2]: started with pid [62891]
[INFO] [1706861436.195243]: Switchbot API Client initialized.
[INFO] [1706861436.199678]: Using SwitchBot API v1.1
[INFO] [1706861436.204957]: Switchbot Device List:
6 Item(s)
deviceName: bot74a, deviceID: (固有のID番号が表示), deviceType: Bot
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

[INFO] [1706861436.208853]: Switchbot Remote List:
2 Item(s)
deviceName: air-conditioner, deviceID: (固有のID番号が表示), remoteType: Air Conditioner
deviceName: pendant-light, deviceID: (固有のID番号が表示), remoteType: DIY Light

[INFO] [1706861436.214168]: Switchbot Scene List:
3 Item(s)
sceneName: turnoff-all-lights, sceneID: (固有のID番号が表示)
sceneName: turnon-all-lights, sceneID: (固有のID番号が表示)
sceneName: turnon-all-lights, sceneID: (固有のID番号が表示)

[INFO] [1706861436.254126]: Ready.

switchbot.launch 実行出力から次の1行を例にとると「デバイス名」が plugmini7a1 で「デバイスタイプ」が Plug Mini (JP) です.

deviceName: plugmini7a1, deviceID: (固有のID番号が表示), deviceType: Plug Mini (JP)

SwitchBot デバイス API コマンドセットの調査

操作したい SwitchBot デバイスタイプが分かればそのコマンドセットを調べます.SwitchBot API のコマンドセットは下記の Web ページで知ることができます.

今回はデバイスタイプ Plug Mini (JP) と Strip Light のデバイスを操作したいのでそれらのコマンドセットについて調べます.

Plug Mini (JP) のコマンドセット

電源プラグの On/Off を行う SwitchBot デバイスである Plug Mini (JP) のコマンドセットの説明は次のリンク先にあります.

上記 Web ページの Plug Mini (JP) コマンドセットの表をそのまま貼り付けたものが次の表です.

deviceType commandType Command command parameter Description
Plug Mini (JP) command turnOn default set to ON state
Plug Mini (JP) command turnOff default set to OFF state
Plug Mini (JP) command toggle default toggle state

デバイスの機能どおりに電源入 turnOn,電源切 turnOff,電源入切の切替 toggle の3つのコマンドにより構成されています.

Strip Light のコマンドセット

テープライト形状の SwitchBot デバイスである Strip Light のコマンドセットの説明は次のリンク先にあります.

上記 Web ページの Strip Light コマンドセットの表をそのまま貼り付けたものが次の表です.

deviceType commandType Command command parameter Description
Strip Light command turnOn default set to ON state
Strip Light command turnOff default set to OFF state
Strip Light command toggle default toggle state
Strip Light command setBrightness {1-100} set brightness
Strip Light command setColor "{0-255}:{0-255}:{0-255}" set RGB color value

点灯 turnOn,消灯 turnOff,明滅切替 toggle,輝度設定 setBrightness,色設定 setColor の5つのコマンドにより構成されていて,そのうち輝度設定では{1-100} の範囲で輝度設定, "{0-255}:{0-255}:{0-255}" の値で RGB 色設定を行います.

SwitchBot デバイスコマンドのソースコード追記

Plug Mini (JP) を操作するソースコード追記と実行

デバイス名 plugmini7a1 の Plug Mini (JP) を操作します. ROS からコマンドを送って On/Off の切り替え toggle をしてみます.

サンプルコード control_switchbot.py に client.control_device('plugmini7a1', 'toggle') を追加します.(下記ソースコードの 16行目)

control_switchbot.py

#!/usr/bin/env python

import rospy
from switchbot_ros.switchbot_ros_client import SwitchBotROSClient

rospy.init_node('controler_node')
client = SwitchBotROSClient()

devices = client.get_devices()
print(devices)

# client.control_device('pendant-light', 'turnOn')

# client.control_device('bot74a', 'turnOn')

client.control_device('plugmini7a1', 'toggle')

ここでは元々サンプルコードにあったペンダントライトとボット(スイッチ)の操作をする行(上記ソースコードの12,14行目)は行頭に # を入れてコメントアウトして実行されないようにしています.

変更を加えた control_switchbot.py ファイルを保存してから実行します.

ターミナル 1 : switchbot.launch 実行入力

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

ターミナル 2 : control_switchbot.py 実行入力

$ source ~/switchbot_ws/devel/setup.bash
$ rosrun switchbot_ros control_switchbot.py 

Strip Light を操作するソースコード追記と実行

デバイス名 tapelight7a1 の Strip Light (テープライト)を操作します. ROS からコマンドを送って次の動作をしてみます.

  1. 消灯
  2. 点灯
  3. 輝度を 100% に設定
  4. 色を白 '255:255:255' に設定
  5. 色を赤 '255:0:0' に設定
  6. 色を緑 '0:255:0' に設定
  7. 色を青 '0:0:255' に設定
  8. 輝度を 1% に設定
  9. 消灯

サンプルコード control_switchbot.py に下記ソースコードの18行目以降を追加します.

値を設定する setBrightnesssetColor といったコマンドでは各数値を control_device() の引数 parameter に文字列として渡します.

また control_device() の中ではコマンドを Action サーバにゴールとして送っているので新しいコマンドが前のコマンドに置き換わらないように1つ1つのコマンド実行を終えるのを待つように引数 waitTrue を渡しています.

control_switchbot.py

#!/usr/bin/env python

import rospy
from switchbot_ros.switchbot_ros_client import SwitchBotROSClient

rospy.init_node('controler_node')
client = SwitchBotROSClient()

devices = client.get_devices()
print(devices)

# client.control_device('pendant-light', 'turnOn')

# client.control_device('bot74a', 'turnOn')

# client.control_device('plugmini7a1', 'toggle')

client.control_device('tapelight7a1', 'turnOff', wait=True)
client.control_device('tapelight7a1', 'turnOn', wait=True)
client.control_device('tapelight7a1', 'setBrightness', parameter='100', wait=True)
client.control_device('tapelight7a1', 'setColor', parameter='255:255:255', wait=True)
client.control_device('tapelight7a1', 'setColor', parameter='255:0:0', wait=True)
client.control_device('tapelight7a1', 'setColor', parameter='0:255:0', wait=True)
client.control_device('tapelight7a1', 'setColor', parameter='0:0:255', wait=True)
client.control_device('tapelight7a1', 'setBrightness', parameter='1', wait=True)
client.control_device('tapelight7a1', 'turnOff', wait=True)

前述の Plug Mini (JP) のときと同様に変更を加えた control_switchbot.py ファイルを保存してから実行します.

補足ソースコード変更

筆者が試した範囲では時々 client.control_device() が実行されない不具合が見受けられ,それが SwitchBotROSClient インスタンス作成時 client = SwitchBotROSClient() に Action サーバの起動やサーバへの接続が不十分であることが原因のように思われました.

そこで下記の switchbot_ros_client.py の21行目のように self.action_client.wait_for_server() を入れて Action サーバが起動して接続されるのを待つようにしたところ,現状では安定して client.control_device() が実行されているように感じます.

switchbot_ros_client.py

import rospy
import actionlib
from switchbot_ros.msg import SwitchBotCommandAction
from switchbot_ros.msg import SwitchBotCommandGoal
from switchbot_ros.msg import DeviceArray


class SwitchBotROSClient(object):

    def __init__(self,
                 actionname='switchbot_ros/switch',
                 topicname='switchbot_ros/devices'):

        self.actionname = actionname
        self.topicname = topicname
        self.action_client = actionlib.SimpleActionClient(
                actionname,
                SwitchBotCommandAction
                )
        rospy.loginfo("Waiting for action server to start.")
        self.action_client.wait_for_server()

    def get_devices(self, timeout=None):

        return rospy.wait_for_message(
                self.topicname,
                DeviceArray,
                timeout=timeout
                )

    def control_device(self,
                       device_name,
                       command,
                       parameter='',
                       command_type='',
                       wait=False
                       ):

        goal = SwitchBotCommandGoal()
        goal.device_name = device_name
        goal.command = command
        goal.parameter = parameter
        goal.command_type = command_type
        self.action_client.send_goal(goal)
        if wait:
            self.action_client.wait_for_result()
            return self.action_client.get_result()

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

著者: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

#!/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 を介したチャットプログラムのどちらを実行するかを切り替えるようにしています.

<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 を次のように変更しました.

<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型にしています.

string messages
---
string response

scripts / openai_chat_server.py

ROS Service 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

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

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

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

<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 の定義ファイルを記述して使えるようにします.

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

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

著者:yamamoto.yosuke

ChatGPT と ROS – 文書生成 ROS ラッパー生成編(Chat Completion API)

本シリーズ前回の記事 ChatGPT と ROS – 文書生成 ROS ラッパー利用編(Completion API) では OpenAI API の ROS ラッパーの中で Completion API を利用している ROS1 の Python ラッパ https://github.com/davesarmoury/openai_ros を使ってみました.

今回は OpenAI の Chat Completion API を利用した ROS ラッパ Python プログラムを Web サービスの方の ChatGPT の助けをかりながら作成した様子を紹介します.

OpenAI の文書生成 API 概要

本シリーズ第1回の記事 ChatGPT と ROS – 調査編 でもふれましたが OpenAI の文書生成 API について簡単にまとめますと,「1問1答形式」の Completion API と,「対話した文脈を含むチャット対話形式」の Chat Completion API の2つがあります.

  • OpenAI の文書生成 API
    • Completion API : 1問1答形式
    • Chat Completion API : 文脈を含むチャット対話形式

前回の記事ではこの2つのうち「1問1答」形式の Completion API を利用ました.今回の記事ではもう一方の「チャット対話」形式のインタフェースである Chat Completion API を ROS から利用してみます.

開発・実行環境

今回は Web サービスの ChatGPT に Chat Completion API を使ったプログラムを生成してもらいながら進めましたのでそれも含めて開発・実行環境の構成は次のようになっています.

  • Ubuntu 20.04
  • ROS Noetic
  • OpenAI ChatGPT のアカウントを持っている(今回筆者は ChatGPT-4 を利用)
  • OpenAI API の利用が有効なアカウントを持っている
    • API Key を取得済

ChatGPT でのプログラム生成

まずは ChatGPT の Web サービスに ChatGPT の Python API にアクセスするための Python プログラムを書いてもらいました.

User

ChatGPT の Python API を用いたアクセスプログラムを書いてください.


ChatGPT

まず,OpenAIのPythonクライアントライブラリをインストールする必要があります.次のコマンドでインストールできます.

pip install openai

そして,以下はOpenAIのGPTを使用するための基本的なPythonコードです.この例では,プロンプトとして”Translate the following English text to French: ‘{}'”を使用し,その'{}’の部分に翻訳したい英文を挿入しています.

import openai

openai.api_key = 'your-api-key'

response = openai.ChatCompletion.create(
  model="gpt-4.0-turbo",
  messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Translate the following English text to French: '{}'"}
    ]
)

print(response['choices'][0]['message']['content'])

上記のコードは,APIキーを設定し,特定のモデル(この場合は”gpt-4.0-turbo”)を指定して,ChatCompletion APIを呼び出す基本的な構造を示しています.messagesパラメータは会話の履歴を表し,最初のメッセージは通常,モデルに役割(この場合は”helpful assistant”)を設定するためのシステムメッセージです.ユーザーメッセージはその後に続きます.

注意: ‘your-api-key’の部分は,OpenAIから提供される実際のAPIキーに置き換える必要があります.また,APIの使用は料金が発生しますので,公式ドキュメンテーションを確認して使用制限とコストを理解することが重要です.

なお,GPT-4のリリース後もAPIの基本的な使い方はこれまでと同様ですが,詳細な情報や更新内容についてはOpenAIの公式ドキュメンテーションをご覧ください.

次にこの ChatGPT にアクセスする Python コードを ROS の Service に対応させたプログラムを作成してもらいました.

User

この Python プログラムから ROS の service プログラムラッパーを作成してください.


ChatGPT

ROS (Robot Operating System)は,ロボットのためのフレームワークで,様々なハードウェアやソフトウェアの要素を組み合わせて使うことができます.ここでは,PythonのOpenAI GPT-4クライアントをROSサービスとしてラップする基本的な例を示します.

まず,適切なメッセージタイプを使用するためのsrvファイルを作成します.この例では,リクエストとレスポンスが共に一つの文字列を持つシンプルなサービスを定義します.

# GptService.srv
string input
---
string output

次に,このサービスを実装するPythonのROSノードを作成します.このノードは,指定された文字列を翻訳するサービスを提供します.

#!/usr/bin/env python
import rospy
from openai_ros.srv import GptService, GptServiceResponse
import openai

def handle_gpt_request(req):
    openai.api_key = 'your-api-key'
    
    response = openai.ChatCompletion.create(
        model="gpt-4.0-turbo",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "Translate the following English text to French: '{}'".format(req.input)}
        ]
    )

    return GptServiceResponse(response['choices'][0]['message']['content'])

def gpt_service():
    rospy.init_node('gpt_service')
    s = rospy.Service('gpt_service', GptService, handle_gpt_request)
    print("Ready to translate English text to French.")
    rospy.spin()

if __name__ == "__main__":
    gpt_service()

このPythonスクリプトは,ROSノードgpt_serviceを作成し,それがGptServiceサービスを提供するようにします.サービスリクエストが来ると,handle_gpt_request関数が呼び出され,その中でOpenAIのGPT-4クライアントが使われてリクエストされたテキストを翻訳します.

このスクリプトをROSノードとして実行するためには,そのファイルに実行可能なパーミッションを与える必要があります.以下のコマンドで実行可能なパーミッションを与えることができます:

chmod +x gpt_service.py

また,OpenAIのAPIキーを設定する部分では,実際のキーに’your-api-key’を置き換える必要があります.そして,APIの使用は料金が発生しますので,公式ドキュメンテーションを確認して使用制限とコストを理解することが重要です.

なお,上記のコードはあくまで一例であり,具体的な使用シーンや要件によりますので,適宜修正や拡張を行ってください.

ChatGPT に最初に生成してもらった Python プログラムにおいてプロンプトの内容が固定されているため,それを基に生成した ROS ラッパープログラムも固定プロンプトへの応答結果を出すだけになってしまいました.

そこでプロンプトを変更可能なようにプログラムを変更してもらいました.

User

ChatGPT に問うプロンプトを変えられるようにプログラムを変更してください.


ChatGPT

もちろんです.以下に,GPTサービスリクエストにプロンプトを含めるように改変したROSサービスの例を示します.

まず,新たなsrvファイルを作成します.この例では,リクエストにプロンプトとインプットの2つの文字列を持つサービスを定義します.

# GptService.srv
string prompt
string input
---
string output

次に,このサービスを実装するPythonのROSノードを作成します.

#!/usr/bin/env python
import rospy
from openai_ros.srv import GptService, GptServiceResponse
import openai

def handle_gpt_request(req):
    openai.api_key = 'your-api-key'
    
    response = openai.ChatCompletion.create(
        model="gpt-4.0-turbo",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "{} '{}'".format(req.prompt, req.input)}
        ]
    )

    return GptServiceResponse(response['choices'][0]['message']['content'])

def gpt_service():
    rospy.init_node('gpt_service')
    s = rospy.Service('gpt_service', GptService, handle_gpt_request)
    print("Ready to handle GPT-4 requests.")
    rospy.spin()

if __name__ == "__main__":
    gpt_service()

このPythonスクリプトは,ROSノードgpt_serviceを作成し,それがGptServiceサービスを提供するようにします.サービスリクエストが来ると,handle_gpt_request関数が呼び出され,その中でOpenAIのGPT-4クライアントが使われてリクエストされたプロンプトとテキストを処理します.

上記のコードを実行する前に,Pythonスクリプトに実行可能なパーミッションを与えることを忘れないでください.

ここまでの ChatGPT の応答で ROS の Python プログラムとして動きそうな感じのソースコードが生成されたように思いましたので実際に実行しながら修正してみることにしました.

ChatGPT が生成したプログラムの修正

今回は ChatGPT と ROS – 文書生成 ROS ラッパー利用編(Completion API) で利用した ROS パッケージ https://github.com/davesarmoury/openai_ros をベースに Chat Completion API を利用する機能を付け加えるかたちで進めました.

ChatGPT が生成した Chat Completion API を利用する ROS Python プログラムを使って Chat Completion API を利用できるよう ROS パッケージに変更を加えた箇所をまとめると次のようになります.

  • openai_chat_node.py の追加
    • ChatGPT が生成したコードからの修正点
      • #!/usr/bin/env python
        • → Python3 に #!/usr/bin/env python3
      • openai.api_key = 'your-api-key'
        • → ROS パラメータから取得する方法に変更
          openai.api_key = rospy.get_param('~key')
      • model="gpt-4.0-turbo",
        • → GPT 3.5 に変更 model="gpt-3.5-turbo",
      • print("Ready to handle GPT-4 requests.")
        • → GPT 3.5 に変更 print("Ready to handle GPT-3.5 requests.")
  • GptService.srv の追加
    • ChatGPT が生成したコードをそのまま利用
  • CMakeLists.txs 内に GptService.srv の記述追加
  • openai_chat.launch の追加
    • 主に OpenAI API Key を ROS パラメータとするために作成

openai_chat_node.py

#!/usr/bin/env python3
import rospy
from openai_ros.srv import GptService, GptServiceResponse
import openai

def handle_gpt_request(req):
#    openai.api_key = 'your-api-key'
    openai.api_key = rospy.get_param('~key')
    
    response = openai.ChatCompletion.create(
#        model="gpt-4.0-turbo",
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "{} '{}'".format(req.prompt, req.input)}
        ]
    )

    return GptServiceResponse(response['choices'][0]['message']['content'])

def gpt_service():
    rospy.init_node('gpt_service')
    s = rospy.Service('gpt_service', GptService, handle_gpt_request)
#    print("Ready to handle GPT-4 requests.")
    print("Ready to handle GPT-3.5 requests.")
    rospy.spin()

if __name__ == "__main__":
    gpt_service()

プログラムの本筋の部分は ChatGPT が生成したコードから修正の必要はありませんでした.

GptService.srv

# GptService.srv
string prompt
string input
---
string output

CMakeLists.txt

add_service_files(
  FILES
  Completion.srv
  GptService.srv
)

GptService.srv のサービスが利用できるように CMakeLists.txt に加筆しました.このあたりの修正箇所の洗い出しも ChatGPT に問うてみるのも修正規模が大きい場合にはありかもしれません.

openai_chat.launch

<launch>
  <arg name="key" default="$(env OPENAI_API_KEY)" />
  <arg name="max_tokens" default="256" />
  <arg name="model" default="gpt-4.0-turbo" />

  <node pkg="openai_ros" type="openai_chat_node.py" name="openai_chat" output="screen">
    <param name="key" value="$(arg key)" />
    <param name="max_tokens" value="$(arg max_tokens)" />
    <param name="model" value="$(arg model)" />
  </node>  
</launch>

モデルを GPT-3.5 と GPT-4 で launch オプションで切り替えて使おうかと思っていたのですが,Web と API への課金は別らしく今回は API では GPT-3.5 のみ利用可能な状況でしたので openai_chat_node.py にモデル名を直書きしたまま使ってしまいました.

修正したプログラム実行の様子

roslaunch openai_ros openai_chat.launch を起動してからもう1つのターミナルで ROS サービスで rosservice call /gpt_service '{prompt: "(プロンプト)", input: "(内容)"}'
output: "Mon nom est Robotuser."
のように利用します.

ターミナル1

robotuser@robotuser-PC:~/openai_ws$ source ~/openai_ws/devel/setup.bash
robotuser@robotuser-PC:~/openai_ws$ roslaunch openai_ros openai_chat.launch 
... logging to /home/robotuser/.ros/log/9d61ced2-f54d-11ed-b5ca-c10df8d90fa9/roslaunch-robotuser-PC-41157.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:35595/

SUMMARY
========

PARAMETERS
 * /openai_chat/key: sk-3JDluBbxsNuIhi...
 * /openai_chat/max_tokens: 256
 * /openai_chat/model: gpt-4.0-turbo
 * /rosdistro: noetic
 * /rosversion: 1.16.0

NODES
  /
    openai_chat (openai_ros/openai_chat_node.py)

auto-starting new master
process[master]: started with pid [41165]
ROS_MASTER_URI=http://localhost:11311

setting /run_id to 9d61ced2-f54d-11ed-b5ca-c10df8d90fa9
process[rosout-1]: started with pid [41175]
started core service [/rosout]
process[openai_chat-2]: started with pid [41182]
Ready to handle GPT-3.5 requests.

ターミナル2

robotuser@robotuser-PC:~/openai_ws$ source ~/openai_ws/devel/setup.bash
robotuser@robotuser-PC:~/openai_ws$ rosservice call /gpt_service '{prompt: "Translate following to French:", input: "My name is Robotuser."}'
output: "Mon nom est Robotuser."
robotuser@robotuser-PC:~/openai_ws$ rosservice call /gpt_service '{prompt: "Translate following to Spanish:", input: "My name is Robotuser."}'
output: "Mi nombre es Robotuser."
robotuser@robotuser-PC:~/openai_ws$ rosservice call /gpt_service '{prompt: "Translate following to Japanese:", input: "My name is Robotuser."}'
output: "\u79C1\u306E\u540D\u524D\u306F\u30ED\u30DC\u30C3\u30C8\u30E6\u30FC\u30B6\u30FC\u3067\
  \u3059\u3002"
robotuser@robotuser-PC:~/openai_ws$ 
  • 注)コマンド全体は横スクロールにて表示してください.

このサービス利用例では “My name is Robotuser.” をフランス語,スペイン語,日本語に翻訳するよう各プロンプトを送りました.
日本語への翻訳指示した output が文字コード化していたので Unicode 変換すると次のようになりました.

私の名前はロボットユーザーです.

このように ChatGPT の Web サービスを利用してコードを生成してもらい,OpenAI の Chat Completion API を ROS から利用できるようになりました.

しかし今回のプログラムは Chat Completion API を用いているものの「1問1答」形式の使い方をしていて,それは Completion API を利用している場合と大きく変わらず, “文脈” をふまえた「チャット」形式ではありませんでした.

本シリーズ次回の記事では今回の Chat Completion API を利用する ROS サービスプログラムを文脈をふまえた「チャット」をする ROS プログラムに改造した様子をお伝えする予定です.

著者:yamamoto.yosuke

ChatGPT と ROS – 調査編

文書生成などで何かと話題の ChatGPT の ROS を介した利用について少し調べてみました.

本記事の筆者は AI に興味はあるものの利用したことがほとんどない立場ですが,ChatGPT などについて調べてみると AI 初心者でも OpenAI の API と ROS を介して文章を生成することができました.加えて ChatGPT の文章生成機能の力を借りてその API を使った ROS ラッパープログラムも作ることができました.その様子を次のようなトピックで複数回に分けて記事にします.

  1. 調査編(本記事)
  2. 文書生成 ROS ラッパー利用編(Completion API)
  3. 文書生成 ROS ラッパー生成編(Chat Completion API)

OpenAI/ChatGPT の API

ChatGPT を提供しているのは OpenAI 社ですので “OpenAI” や “ChatGPT” でネット検索してみると OpenAI Gym の利用だったり,ChatGPT だったりといろいろとあるようでした.全く体系的・網羅的ではないと思うのですが,今回調査してみて得られた情報を大まかに分類してみると次のような構成になりました.

  • 強化学習を行うための OpenAI Gym
  • 文書生成系の OpenAI API も複数
    • Chat Completion API
    • Completion API

まず OpenAI Gym というのは強化学習を行うためのものでロボットの動作経路の学習などに用いられていて,今回の目的としていた文書生成とは異なる機能を提供しているようです.文書生成 API も Completion API と Chat Completion API と複数に分かれていました.

Web ページから ChatGPT-4 に Completion と Chat Completion の API の違いを聞いてみました.

OpenAI API の Completion API と Chat Completion API の違いを教えてください.

Completion APIとChat Completion APIは,OpenAIのGPT(Generative Pretrained Transformer)をベースとしたテキスト生成APIです.しかし,それぞれの使用方法や特性は少し異なります.

Completion API:

  • Completion APIは,提供されたプロンプトに対する続きのテキストを生成します.つまり,モデルに入力を1つ与え,それに対する出力を1つ得ます.
  • このAPIは,多くの一般的なテキスト生成タスクに使用できます.例えば,文章の完成,質問への答え,電子メールのドラフト作成などです.

Chat Completion API:

  • Chat Completion APIは,会話形式のテキスト生成に最適化されています.複数のメッセージを入力として受け取り,それに基づいて応答を生成します.
  • それぞれのメッセージには,「role」(’system’,’user’,’assistant’)と「content」があります.’system’ メッセージは,チャット全体のコンテキストを設定します.’user’ メッセージはモデルに指示を出し,’assistant’ メッセージはモデルの先行の応答を提供します.
  • このAPIは,対話型のタスクやマルチターンの会話に特に適しています.

使用するAPIは,あなたがどのようなタスクを実行したいか,またはどのような出力を期待しているかによって異なります.

Chat Completion は名前の「チャット」のとおり文脈を含む対話形式で,Completion は1問1答形式のようです.

Completion と Chat Completion の違いについて対比的に解説されている記事もあり参考になりました.

OpenAI/ChatGPT と ROS

ROS では OpenAI/ChatGPT がどのように使われているのか?ということで調べてみると,まず ROS Wiki にある openai_ros は強化学習の OpenAI Gym の機能を利用するパッケージのようでした.

また GitHub 内で “openai” と “ros” を組み合わせて検索してみると https://github.com/search?q=openai+ros&type=repositories&p=1 かなりヒットします.そのうち文書生成に関するリポジトリをピックアップしてリストにします.

これらの既存の OpenAI API の ROS ラッパーの中で Completion API を利用している ROS1 の Python ラッパ https://github.com/davesarmoury/openai_ros を使ってみた様子を次回の OpenAI と ROS の記事でお伝えする予定です.

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