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

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

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

ChatGPT と ROS - 文書生成 ROS ラッパー生成編(Chat Completion API)
ChatGPT と ROS – ROS Topic を介した ChatGPT チャットプログラム

著者について

yamamoto.yosuke administrator

コメントを投稿するにはログインしてください。