タグアーカイブ openai

著者: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 – 文書生成 ROS ラッパー利用編(Completion API)

本シリーズ前回の記事 ChatGPT と ROS – 調査編 では ChatGPT の ROS を介した利用について少し調べてみたことをお伝えしました.

今回は OpenAI API の ROS ラッパーの中で Completion API を利用している ROS1 の Python ラッパ https://github.com/davesarmoury/openai_ros を使ってみた様子を紹介します.

実行環境

今回は次の環境で OpenAI API の ROS を介した実行を行っています.

  • Ubuntu 20.04
  • ROS Noetic
  • OpenAI API の利用が有効なアカウントを持っている
    • API Key を取得済

OpenAI API は新規登録後 3ヶ月 の期限がありますが 5ドル分 の無料クレジットが付与されるのでお試し利用することができます.(2023年8月中旬時点)

API Key の取得は OpenAI API の Web ページでログインした状態で下記リンク先の API keys のページから取得します.

インストールとビルド

実行環境の準備が整いましたらインストールとビルドを行います.

ROS のインストール

ROS は既にインストールされているようでしたら改めてインストールする必要はありません.

加えて下記の catkin ツール関係もインストールしておきます.

$ sudo apt install python3-osrf-pycommon python3-catkin-tools

OpenAI Python ライブラリのインストール

OpenAI の Python ライブラリが必要ですので pip からインストールします.

$ sudo apt install python3-pip
$ pip install --upgrade openai

ワークスペースの作成とビルド

今回は openai_ws という名前のワークスペースを作成してソースコードのクローンとビルドを行っています.

$ mkdir -p ~/openai_ws/src
$ cd ~/openai_ws/src/
$ git clone https://github.com/davesarmoury/openai_ros.git
$ cd ~/openai_ws/
$ rosdep install -y -r --from-paths src --ignore-src
$ catkin build
$ source ~/openai_ws/devel/setup.bash

openai_ros の実行

ワークスペースでビルドした openai_ros の ROS プロセスを実行します.

まず1つ目のターミナルで API Key を環境変数 OPENAI_API_KEY として export で設定しておきます.$ export OPENAI_API_KEY="sk-..."sk-... の部分は各自の OpenAI API アカウントで作成した API Key の内容に置き換えてください.

OpenAI の Completion API を利用するための ROS サービスサーバを実行するために openai.launch を起動します.

ターミナル 1

$ source ~/openai_ws/devel/setup.bash
$ export OPENAI_API_KEY="sk-..."
$ roslaunch openai_ros openai.launch max_tokens:=256

2つ目のターミナルから1つ目のターミナルで実行している OpenAI Completion API の ROS サービスにプロンプトを “Write a poem about OpenAI” としてサービスコールを行います.

ターミナル 2

$ source ~/openai_ws/devel/setup.bash
$ rosservice call /get_response '{prompt: "Write a poem about OpenAI"}'
finish_reason: "stop"
text: "\n\nOpenAI, a force of nature,\nA tool of the future,\nA way to explore the unknown,\n\
  A way to make the world better.\n\nA way to make machines smarter,\nA way to make\
  \ them think,\nA way to make them learn,\nA way to make them act.\n\nA way to make\
  \ them understand,\nA way to make them act,\nA way to make them do,\nA way to make\
  \ them react.\n\nOpenAI, a force of nature,\nA tool of the future,\nA way to explore\
  \ the unknown,\nA way to make the world better."
model: "text-davinci-003"
completion_tokens: 134
prompt_tokens: 6
total_tokens: 140

Completion API から ROS サービス経由で応答が帰ってきました.text: に応答内容があります.

text 部分の改行コードなどを除くと次のようになっています.

OpenAI, a force of nature,
A tool of the future,
A way to explore the unknown,
A way to make the world better.

A way to make machines smarter,
A way to make them think,
A way to make them learn,
A way to make them act.

A way to make them understand,
A way to make them act,
A way to make them do,
A way to make them react.

OpenAI, a force of nature,
A tool of the future,
A way to explore the unknown,
A way to make the world better.

…だそうです.

プロンプトを日本語で例えば '{prompt: "OpenAI についての40字以 内のポエムを書いてください"}' 記述しても応答はありますが rosservice のコールの応答をそのままコンソール出力した状態ですと text: 内は文字コード化されていて可読性がありませんでした.

注)コマンド全文は枠内を横スクロールして表示してください.

$ rosservice call /get_response '{prompt: "OpenAI についての40字以 内のポエムを書いてください"}'
finish_reason: "stop"
text: "\n\nOpenAI\u306F\u3001\u4EBA\u985E\u306E\u672A\u6765\u3092\u5B88\u308B\u305F\u3081\
  \u306B\u3001AI\u3092\u4F7F\u3063\u3066\u6280\u8853\u3092\u767A\u5C55\u3055\u305B\
  \u308B\u3002\u79C1\u305F\u3061\u306F\u3001AI\u3092\u4F7F\u3063\u3066\u3001\u3088\
  \u308A\u826F\u3044\u672A\u6765\u3092\u5275\u9020\u3057\u3088\u3046\u3002"
model: "text-davinci-003"
completion_tokens: 81
prompt_tokens: 31
total_tokens: 112
$

文字コード表示 解決法1 – ascii2uni を使う

文字コード表示を文字コード変換の ascii2uni で解決してみます.ascii2uni を使うため uni2ascii をインストールします.

$ sudo apt install uni2ascii

ターミナル 2

ROS サービスコールの結果に対して | ascii2uni -a U -q をパイプしてコード変換を行います.

注)コマンド全文は枠内を横スクロールして表示してください.

$ rosservice call /get_response '{prompt: "OpenAI についての40字以 内のポエムを書いてください"}' | ascii2uni -a U -q
finish_reason: "stop"
text: "\n\nOpenAIは,人類の未来を守るため\
  に𰀚Iを使って技術を発展させ\
  る.私たちは𰀚Iを使って,よ\
  り良い未来を創造しよう."
model: "text-davinci-003"
completion_tokens: 81
prompt_tokens: 31
total_tokens: 112
$ 

一部文字化けしてしまっているようです.おそらく \u3001(=読点「,」) + AI\u3001A + I と判断して違う文字を表示しようとしているようです.「文字コードの”読点”」+「平文英数字」の組み合わせ以外は大体 ascii2uni で表示できそうです.

文字コード表示 解決法2 – Python を使う

文字コード化されたものは Python の print() 内で解決されて可読性のある日本語の状態で出力されますので,今回の openai_ros の ROS サービスを Python からコールするプログラム openni_get_completion.py を書きました.

openni_get_completion.py

#!/usr/bin/env python3

import sys
import rospy

from openai_ros.srv import Completion, CompletionResponse


def get_response_client(prompt):
  request = '{prompt: ' + str(prompt) +'}'
  rospy.wait_for_service('get_response')
  try:
    get_response = rospy.ServiceProxy('get_response', Completion)
    response = get_response(request, 0)
    return response
  except rospy.ServiceException as e:
    print ("Service call failed: %s"%e)


if __name__ == "__main__":
  if len(sys.argv) == 2:
    prompt = str(sys.argv[1])
  else:
    prompt = "Write a poem about OpenAI"
  
  print("Prompt: %s\n" % (prompt))
  
  response = get_response_client(prompt)
  
  print("Response: \n%s\n" % (response))
  print("Text: %s\n" % (response.text))

先程の ターミナル1 で openai.launch を実行している状態で ターミナル2 から openni_get_completion.py を実行します.

ターミナル 2

$ source ~/openai_ws/devel/setup.bash
$ rosrun openai_ros openai_get_completion.py 
Prompt: Write a poem about OpenAI

Response: 
finish_reason: "stop"
text: "\n\nOpenAI, a force of nature,\nA powerful tool of creation,\nAble to learn and adapt,\n\
  Able to think and create.\n\nA tool of the future,\nA tool of the present,\nA tool\
  \ of the past,\nA tool of the ages.\n\nA tool of the people,\nA tool of the world,\n\
  A tool of the universe,\nA tool of the gods.\n\nOpenAI, a force of nature,\nA powerful\
  \ tool of creation,\nAble to learn and adapt,\nAble to think and create."
model: "text-davinci-003"
completion_tokens: 124
prompt_tokens: 11
total_tokens: 135

Text: 

OpenAI, a force of nature,
A powerful tool of creation,
Able to learn and adapt,
Able to think and create.

A tool of the future,
A tool of the present,
A tool of the past,
A tool of the ages.

A tool of the people,
A tool of the world,
A tool of the universe,
A tool of the gods.

OpenAI, a force of nature,
A powerful tool of creation,
Able to learn and adapt,
Able to think and create.

$ 

実行時にプロンプトの引数を渡していないのでプログラム内に書かれてるデフォルトのプロンプト “Write a poem about OpenAI” に対する英語のポエムが返ってきています.英語でも Python の print() で出力すると改行コードが見えなくなるので読みやすくなっています.

次は引数として日本語のプロンプト “OpenAI についての40字以内のポエムを書いてください.” を渡して openni_get_completion.py を実行します.

ターミナル 2

$ rosrun openai_ros openai_get_completion.py "OpenAI についての40字以内のポエムを書いてください."
Prompt: OpenAI についての40字以内のポエムを書いてください.

Response: 
finish_reason: "stop"
text: "\n\nOpenAI\u306F\u672A\u6765\u3092\u7167\u3089\u3059\u5149\u3001\u672A\u6765\u3092\
  \u5207\u308A\u958B\u304F\u92ED\u3044\u5203\u3002\u79C1\u305F\u3061\u306E\u672A\u6765\
  \u3092\u8C4A\u304B\u306B\u3057\u3066\u304F\u308C\u308B\u3002"
model: "text-davinci-003"
completion_tokens: 58
prompt_tokens: 39
total_tokens: 97

Text: 

OpenAIは未来を照らす光,未来を切り開く鋭い刃.私たちの未来を豊かにしてくれる.

$ 

日本語でも可読性のある整形された状態で読めました.そうなんですね…

ポエムはさておき,openai_ros パッケージを利用して OpenAI の Completion API を ROS から英語と日本語のどちらでも利用することができました.

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


本シリーズ次回の記事は OpenAI の Chat Completion API を ROS から利用する Python プログラムを OpenAI の ChatGPT に教えてもらいながら作成した様子を紹介する予定です.

著者: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 の記事でお伝えする予定です.

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