📚

Raspberry PiとChatGPTでつくるボイス・アシスタント・ロボット #9

2023/09/24に公開

thunbnail2
ChatGPTにFunction callingを送るロボットのイメージ 複数のマニュアルを指示に沿って行動します

Function calling

https://openai.com/blog/function-calling-and-other-api-updates

Function callingは2023年6月13日にOpenAIが導入した新機能です。これにより、開発者はChatGPTモデルに対して関数を記述し、モデルが必要な場面でそれを呼び出すことが可能になりました。

Function callingは、GPTモデルの機能を外部ツールやAPIと連携させるための革新的な方法です。開発者は、自然言語のクエリをAPI呼び出しやデータベースクエリに変換したり、チャットボットを作成したりする際に多くの利点を享受できます。

開発者向けドキュメント

https://platform.openai.com/docs/guides/gpt/function-calling

ChatGPTに翻訳してもらったドキュメントは、以下のリンク PonDad/function_calling_guide.md から確認できます。ドキュメントによる基本的な手順は以下のようになります。

  1. ユーザー入力、関数が指定されたモデルを呼び出します。
  2. モデルは必要に応じて関数の呼び出しを選択します。その内容は開発者が設計したJSONオブジェクトです。
  3. テキストをJSON形式に変換し、提供された引数があればそれを使って関数呼び出しを行います。
  4. モデルを再度呼び出し、関数呼び出し結果を新しいメッセージとして追加し、それらをモデルに要約させてユーザーに返します。

モデルを2回呼び出していることに注目してください。ドキュメントを元にサンプルプログラムを作成し、仕組みを確認していきます。

日時を取得する関数を呼び出す

前章で作成した、テキストベースのチャットテンプレートを使用し、日時を取得するを関数を呼び出すサンプルプログラムを作成しました。.envの読み込みは省略します。プログラムの実行内容は以下のステップで行われます。

  1. 「会話」と「利用可能な関数」をGPTに送信します ---(※4)
  2. GPTが関数を呼び出すことを希望しているか確認します ---(※8)
  3. 関数を呼び出します ---(※9)
  4. 関数呼び出しと関数の応答情報をGPTに送信します ---(※12)
ex_bot_gpt_analyzer_3.py
# ライブラリのインポート ---(※1)
import openai, os, dotenv, json, datetime

# .envファイルから環境変数をロード ---(※2)
dotenv.load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")

# 現在時刻を取得する関数を定義 ---(※3)
def get_date_time():
    """datetime関数をつかい、「現在時刻」「今日の日付」を返します"""
    day_now = datetime.datetime.today().strftime("%-Y年%-m月%-d日")
    time_now = datetime.datetime.now().strftime("%-H時%-M分")
    date_time_data = {
        "day_now": day_now,
        "time_now": time_now,
    }
    return json.dumps(date_time_data)

def chat_with_gpt(text):

    # ステップ1: 「会話」と「利用可能な関数」をGPTに送信 ---(※4)

    # 会話を管理する変数 ---(※5)
    messages = [
        {
            "role": "system",
            "content": """
                あなたは優秀なボイス・アシスタント・ロボット。名前は「ゆっくり霊夢」です。
            """
        },{                                             
            "role": "user",
            "content": text
        }
        ]
    # 利用可能な関数を指定 ---(※6)
    functions = [
        {
            "name": "get_date_time",
            "description": "「現在時刻」「今日の日付」を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "time": {
                        "type": "string", 
                        "description": "いつの時刻か 例: 今何時"
                    },
                    "day": {
                        "type": "string", 
                        "description": "いつの日付か 例: 今日の日付"
                    },
                },
            },
            "required": ["time","day"],
        },
    ]
    # GPTの応答を取得します  ---(※7) 
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
        functions=functions,
        function_call="auto",  # auto がデフォルトですが、明示的に説明します
    )
    response_message = response["choices"][0]["message"]
    #print("response: ", response)

    # ステップ2: GPTが関数を呼び出すことを希望しているか確認します ---(※8)

    if response_message.get("function_call"):

        # ステップ3: 関数を呼び出します  ---(※9)

        # 注意: JSONの応答が常に有効でない場合があるため、エラーを処理することを確認してください
        available_functions = {
            "get_date_time": get_date_time,
        } 
        # この例では関数が1つだけですが、複数持つことができます  ---(※10) 
        function_name = response_message["function_call"]["name"]
        fuction_to_call = available_functions[function_name]

        if function_name  == "get_date_time":
            # 関数の実行結果を取得します  ---(※11)
            function_response = fuction_to_call()
            #print("function_response: ", function_response)
        else:
            pass

        # ステップ4: 関数呼び出しと関数の応答情報をGPTに送信します  ---(※12) 

        # アシスタントの応答で会話を延長する ---(※13) 
        messages.append(response_message) 
        messages.append(
            {
                "role": "function",
                "name": function_name,
                "content": function_response,
            }
        )  
        # 関数応答で会話を拡張する  ---(※14) 
        second_response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            messages=messages,
        ) 

        # 関数の応答を確認できる GPT から新しい応答を取得します  ---(※15) 
        #print("second_response: ", second_response)
        return second_response["choices"][0]["message"]['content']
    else:
        return response["choices"][0]["message"]['content']

if __name__ == "__main__":
    print("🖥️. SYSTEM: チャットを開始します。終了するには '/exit' を入力してください。")

    # ターミナルで連続して対話するループ ---(※16)
    while True:
        user_input = input("😀 USER: ")
        if user_input == "/exit":
            print("🖥️. SYSTEM: チャットを終了します。")
            break

        # ChatGPTによる応答を取得 ---(※17)
        assistant_reply = chat_with_gpt(user_input)

        # ChatGPTの応答を表示
        print("🤖 GPT: " + assistant_reply)

(※1)で必要なライブラリをインポートし、(※2)で.envファイルからChatGPTのAPIキーををロードします。ここでは読み込みにdotenvモジュールを使用します。

(※3)で日時を取得する関数を定義します。この関数は引数を指定していませんが、自然言語から引数を生成することもできます。関数の戻り値はJSON形式で明示します。これにより前章で問題になった出力が安定しないという問題を解決できます。

(※4)(※5)でChatGPTに送信するパラメータのひとつ、チャットメッセージリストをmessagesを定義します。

(※6)で新たにパラメータを定義します。呼び出す関数をfunctionsで定義します。複数形で表されているように、このパラメータは複数の要素を入れた配列で定義され、関数呼び出し機能はこの要素を選択します。(※3)で定義した関数はこのパラメータのnameで指定します。関数呼び出しのヒントになるキーワードなどをここで指定することで精度を上げる仕組みになっています。

(※7)でChatGPTのAPIを呼び出しの際、パラメータにfunctionsを指定します。

最初の送信を終え、戻り値を得たら次のステップへ進みます。関数呼び出しの指定がされているか確認し、もし指定があれば関数呼び出し機能を実行します(※8)(※9)(※10)。

実行結果から条件分岐を使用し、もし事前に登録した関数があればその戻り値を取得します(※11)。

次のステップは再度ChatGPTへ送信する準備です(※12) 。

チャットメッセージのパラメータfunctionに、関数の応答情報を追加し(※13)、再度ChatGPTへ送信します(※14)。

その後の応答処理は、サンプルテンプレートプログラムと同様になります(※15)(※16)(※17)。

実行結果は以下のようになります。

terminal
🖥️ SYSTEM: チャットを開始します。終了するには '/exit' を入力してください。
😀 USER: こんにちは
🤖 GPT: こんにちは!私はゆっくり霊夢です。何かお手伝いできますか?
😀 USER: 今日は何日ですか
🤖 GPT: 今日は2023年9月9日です。
😀 USER: 今何時ですか
🤖 GPT: 現在の時刻は、10時13分です。
😀 USER: /exit
🖥️ SYSTEM: チャットを終了します。

サーボモーター角度を取得する関数を呼び出す

次に、水平・垂直の角度を出力する関数を呼び出すサンプルプログラムを作成します。

上記サンプルプログラムは関数に引数がありませんでしたが、今回はpan(水平)・tilt(垂直)2つの角度を引数として設定します。

ドキュメンテーションや類似サンプルが存在しなかったため、プログラムの作成は非常に困難でした。なぜなら、関数の引数を、関数内のプロンプトで生成するプログラムであるためです。

通常、この様な処理はエラーを引き起こしますが、何度かプロンプトの記述を変更しながら実験を行った結果、最終的に意図通りの実行結果を得ることができました。

複数の関数を指定する方法が分かるように、前節のサンプルプログラムに追記した内容で解説します。

ex_bot_gpt_analyzer_4.py
# ライブラリのインポート ---(※1)
import openai, os, dotenv, json, datetime

# .envファイルから環境変数をロード ---(※2)
dotenv.load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")

# 日時を取得する関数を定義 ---(※3)
def get_date_time():
    """datetime関数をつかい、「現在時刻」「今日の日付」を返します"""
    day_now = datetime.datetime.today().strftime("%-Y年%-m月%-d日")
    time_now = datetime.datetime.now().strftime("%-H時%-M分")
    date_time_data = {
        "day_now": day_now,
        "time_now": time_now,
    }
    return json.dumps(date_time_data)

# pan、tilt角度を生成する関数を定義 ---(※4)
def turn_pan_tilt(pan, tilt):
    """
        ###目的###
        テキストで方向を指示された場合
        パラメータ "pan"(水平)、"tilt"(垂直)を数値化し、その値を返します

        ###数値化するパラメータ###
        - "pan": -90 < pan < 90
        - "tilt": -90 < tilt < 90

        ###出力の例###
        Q: "右を向いて"
        A: "pan": -90,"tilt": 0

        Q: "左を向いて"
        A: "pan": 90,"tilt": 0
        
        Q: "上を向いて"
        A: "pan": 0, "tilt": -90

        Q: "下を向いて"
        A: "pan": 0, "tilt": 90

        Q: "右上を向いて"
        A: "pan": -90, "tilt": -90
        
        Q: 左下を向いて
        A:
    """
    turn_degree = {
        "pan": pan,
        "tilt": tilt,
    }
    return json.dumps(turn_degree)

def chat_with_gpt(text):

    # ステップ1: 「会話」と「利用可能な関数」をGPTに送信 ---(※5)

    # 会話を管理する変数 ---(※6)
    messages = [
        {
            "role": "system", 
            "content": """
                あなたは垂直方向と水平方向に移動するカメラを搭載した音声チャットロボットです。
                名前は「ゆっくり霊夢」です。
            """
        },{                                             
            "role": "user",
            "content": text
        }
        ]
    # 利用可能な関数を指定 ---(※7)
    functions = [
        {
            "name": "get_date_time",
            "description": "現在の時刻を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "time": {
                        "type": "string", 
                        "description": "いつの時刻か 例: 今何時"
                    },
                    "day": {
                        "type": "string", 
                        "description": "いつの日付か 例: 今日の日付"
                    },
                },
            },
            "required": ["time","day"],
        },
        {
            "name": "turn_pan_tilt",
            "description": "指示された方向のpan,tiltを取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "pan": {
                        "type": "number",
                        "description": "水平移動 例:右は−90 左は90"
                    },
                    "tilt": {
                        "type": "number", 
                        "description": "垂直移動 例:上は−90 下は90"
                    },
                },
                 "required": ["pan","tilt"],
            },
        },
    ]
    # GPTの応答を取得します  ---(※8) 
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
        functions=functions,
        function_call="auto",  # auto がデフォルトですが、明示的に説明します
    )
    response_message = response["choices"][0]["message"]
    print("response: ", response)

    # ステップ2: GPTが関数を呼び出すことを希望しているか確認します ---(※9)

    if response_message.get("function_call"):

        # ステップ3: 関数を呼び出します  ---(※10) 

        # 注意: JSONの応答が常に有効でない場合があるため、エラーを処理することを確認してください
        available_functions = {
            "get_date_time": get_date_time,
            "turn_pan_tilt": turn_pan_tilt,
        } 
        # この例では関数を2つ持っています  ---(※11)
        function_name = response_message["function_call"]["name"]
        fuction_to_call = available_functions[function_name]
        function_args = json.loads(response_message["function_call"]["arguments"])

        # 複数の関数それぞれの実行結果を取得します  ---(※12)
        if function_name  == "get_date_time":
            # ひとつめの関数の実行結果を取得します  ---(※13)
            function_response = fuction_to_call()
            print("function_response: ", function_response)
        
        elif function_name  == "turn_pan_tilt":
             # ふたつめの関数の実行結果を取得します  ---(※14)
            function_response = fuction_to_call(
                pan = function_args.get("pan"),
                tilt = function_args.get("tilt"),
            )
            print("function_response: ", function_response)
        else:
            pass

        # ステップ4: 関数呼び出しと関数の応答情報をGPTに送信します  ---(※15)

        # アシスタントの応答で会話を延長する ---(※16) 
        messages.append(response_message)
        messages.append(
            {
                "role": "function",
                "name": function_name,
                "content": function_response,
            }
        ) 
        # 関数応答で会話を拡張する  ---(※17)
        second_response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            messages=messages,
        ) 
        # 関数の応答を確認できる GPT から新しい応答を取得します  ---(※18) 
        #print("second_response: ", second_response)
        return second_response["choices"][0]["message"]['content']
    else:
        return response["choices"][0]["message"]['content']

if __name__ == "__main__":
    print("🖥️. SYSTEM: チャットを開始します。終了するには '/exit' を入力してください。")

    # ターミナルで連続して対話するループ ---(※19)
    while True:
        user_input = input("😀 USER: ")
        if user_input == "/exit":
            print("🖥️. SYSTEM: チャットを終了します。")
            break

        # ChatGPTによる応答を取得 ---(※20)
        assistant_reply = chat_with_gpt(user_input)

        # ChatGPTの応答を表示
        print("🤖 GPT: " + assistant_reply)

(※4)はpantilt角度を生成する関数です。引数に生成するpantiltを指定し、関数内のプロンプトに得たい結果を記述します。通常、関数の戻り値は計算結果や値ですが、ここではプロンプトにより生成される値を戻り値として指定しています。

複数の関数を指定する場合、(※7)のfunctions配列の要素としてそれらを記述します。関数名"turn_pan_tilt"を記述し、関数呼び出しの参考になるキーワードなども記述します。
このようにして、方向からpantiltを生成する方法をChatGPTに説明します。

そして、(※10)で関数を呼び出す際には、有効な関数を明示的に指定します。関数の実行結果を取得した場合、それを2回目のチャットメッセージに含めることが(※14)で行われます。

実行結果は以下のようになります。

terminal
🖥️ SYSTEM: チャットを開始します。終了するには '/exit' を入力してください。
😀 USER: こんにちは
🤖 GPT: こんにちは!どのようなお手伝いができますか?
😀 USER: 右を向いて
function_response:  {"pan": -90, "tilt": null}
🤖 GPT: 右を向きました。
😀 USER: 少しだけ下を向いてください
function_response:  {"pan": 0, "tilt": 10}
🤖 GPT: 了解です。少しだけ下を向きます。
😀 USER: /exit
🖥️ SYSTEM: チャットを終了します。

Discussion