Function callingで複雑なJson形式を抽出する

背景

OpenAI APIのChat APIにFunction calling機能がリリースされました。
名称的にもサンプルコード的にも、Chat APIでPluginsのようなツールを使うための方法のようです。

ですが、「Jsonを安定して出せる」ことが何よりの価値だと感じます。

この記事でもテキストからJson形式で抽出する方法について書きましたが、
安定してJsonを出力する部分で少し苦労しています。
これをアップデートしたいなということで、まずは勉強しました。

本日リリースですでにいくつも使い方の記事がみつかります。(本当にスピードの早い世の中。。)
ただ、見える範囲では、ネストされたJsonを出力するものは見つからなかったので、試してみました。

実装

Import & Load API key

まずはいつも通り必要なパッケージをインポートして、APIキーをセットします。

import openai,os,json
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())
openai.api_key = os.environ['OPENAI_API_KEY']

Example from OpenAI Document

まずはOpenAIのドキュメントにある例を動作確認します。

def get_current_weather(location, unit="fahrenheit"):
    weather_info = {
        "location": location,
        "temperature": "72",
        "unit": unit,
        "forecast": ["sunny", "windy"],
    }
    return json.dumps(weather_info)

`get_current_weather(location)`で`location`の天気を出力するダミー関数を定義しておきます。

def run_conversation(input):
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[{"role": "user", "content": input}],
        functions=[
            {
                "name": "get_current_weather",
                "description": "指定した場所の現在の天気を取得",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "都市と州",
                        },
                        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                    },
                    "required": ["location"],
                },
            }
        ],
        function_call="auto",
    )
    message = response["choices"][0]["message"]
    print("message>>>\n", message, "\n\n")

    if message.get("function_call"):
        function_name = message["function_call"]["name"]

        function_response = get_current_weather(
            location=message.get("location"),
            unit=message.get("unit"),
        )

        second_response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            messages=[
                {"role": "user", "content": input},
                message,
                {
                    "role": "function",
                    "name": function_name,
                    "content": function_response,
                },
            ],
        )
        return second_response

print("response>>>\n", run_conversation("ボストンの天気はどうですか?")["choices"][0]["message"]["content"], "\n\n")

先程の関数とその入力を定義し、`functions`に入力するという仕様です。
最初の入力の応答に、`function_call`があった場合、関数名と入力を受け取って関数を実行し、次の入力にします。
計2回のAPI呼び出しで、応答を得られます。

    message>>>
     {
      "role": "assistant",
      "content": null,
      "function_call": {
        "name": "get_current_weather",
        "arguments": "{\n  \"location\": \"Boston\"\n}"
      }
    } 
    
    
    response>>>
     ボストンの現在の天気は晴れで、風も強いようです。気温は72度です。 

結果を見ると、いい感じにJsonが得られ、それをもとに自然な回答を得られています。

デモ

次に、リストや辞書が含まれる、より複雑なJsonの場合を試してみます。(Json的にはarrayとobject?)

まずは、ChatGPTのUI版で適当なレシピテキストを作ってもらいました。ここからレシピを抽出します。

recipe_text = """\
シンプルなトマトパスタのレシピです。
材料: パスタ(お好みの種類): 100g、トマト缶: 60g、にんにく: 1かけ、オリーブオイル: 大さじ1、塩: 適量、こしょう: 適量、パルメザンチーズ(お好みで): 適量
鍋にお湯を沸かし、パスタを袋の表示通りに茹でます。
別のフライパンにオリーブオイルを熱し、みじん切りにしたにんにくを加えて弱火で炒めます。
トマト缶を加え、塩とこしょうで味を調えます。
トマトソースを弱火で加熱し、少しとろみがつくまで煮込みます。
茹で上がったパスタをソースに加え、全体がよく絡まるように混ぜます。
お皿に盛り付け、お好みでパルメザンチーズをかけて完成です。\
"""

以下のようなフォーマットを定義しました。リストや辞書を含む形です。

{
    "name": "string: レシピ名。",
    "appliances": ["string: 使用機器"],
    "ingredients": [
        {"name": "string: 材料名","amount": "string: 分量"}
    ],
    "instructions": ["string: 手順"]
}

このフォーマットを`parameters`に入力すると、以下のようになりました。
ドキュメントにあった以下のリンクをもとにJsonを勉強しました。

Understanding JSON Schema — Understanding JSON Schema 2020-12 documentation

かなり長くなりますね。。

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[{"role": "user", "content": recipe_text}],
    functions=[
        {
            "name": "get_recipe",
            "description": "レシピデータをJson形式で返す",
            "parameters": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string", "description": "レシピ名"
                    },
                    "appliances": {
                        "type": "array", "description": "使用機器のリスト",
                        "items": {
                            "type": "string", "description": "使用機器"
                        }
                    },
                    "ingredients": {
                        "type": "object", "description": "材料のリスト",
                        "properties": {
                            "name": {
                                "type": "string", "description": "材料名"
                            },
                            "amount": {
                                "type": "string", "description": "材料の分量。例:200g, 大さじ1, 2個, etc",
                            }
                        }
                    },
                    "instructions": {
                        "type": "array", "description": "調理手順のリスト",
                        "items": {
                            "type": "string", "description": "調理手順"
                        }
                    },
                },
                "required": ["name","appliances","ingredients","instructions"],
            },
        }
    ],
    function_call="auto",
)
message = response["choices"][0]["message"]
print(json.dumps(json.loads(message['function_call']['arguments']),indent=2,ensure_ascii=False))

出力がこちらです。いい感じでJson抽出できました。

{
  "name": "トマトパスタ",
  "appliances": [
    "鍋",
    "フライパン"
  ],
  "ingredients": [
    {
      "name": "パスタ",
      "amount": "100g"
    },
    {
	  "name": "トマト缶",
	  "amount": "60g"
	},
	{
	  "name": "にんにく",
	  "amount": "1かけ"
	},
	{
	  "name": "オリーブオイル",
	  "amount": "大さじ1"
	},
	{
	  "name": "塩",
	  "amount": "適量"
	},
	{
	  "name": "こしょう",
	  "amount": "適量"
	},
	{
	  "name": "パルメザンチーズ(お好みで)",
	  "amount": "適量"
	}
  ],
  "instructions": [
    "鍋にお湯を沸かし、パスタを袋の表示通りに茹でます。",
	"別のフライパンにオリーブオイルを熱し、みじん切りにしたにんにくを加えて弱火で炒めます。",
	"トマト缶を加え、塩とこしょうで味を調えます。",
	"トマトソースを弱火で加熱し、少しとろみがつくまで煮込みます。",
	"茹で上がったパスタをソースに加え、全体がよく絡まるように混ぜます。",
	"お皿に盛り付け、お好みでパルメザンチーズをかけて完成です。"
  ]
}

疑問

まだ、`required`の扱いがわかりません。

こちらで使うには、`required`を初期から使うわけにはいかないが、なにか問題が起こるのか。。今後試してみます。

また、Function callingという名称について、
普通に考えると、「Output Parserです」って出したほうが汎用性的にもいいと思うんですが、なぜFunction calling(「関数の入力を作成し、関数を使えるようにしました」)だったんでしょう。

本当に関数を使うだけの機能なら、API呼び出しを2回に分けないほうが自然ですし。

用途を限定してわかりやすく、インパクトを大きくするという意図なのでしょうか。それとも何か他に意図や願望があるのか。うーん。

まとめ

新しく出たFunction callsについてサンプルコードより複雑なJsonの出力方法をお試しました。

少し前に試していたLangChainのOutput Parserだと、ネストされたJsonのスキーマを与える方法がわからず、仕方なく自分でプロンプトを書いていました。(Pydanticだとdescriptionを日本語で書くと文字化けするんです。回避方法もありそうですが。。)

これで、より色々なところで使えそうですね。
LangChainを使うのかopenaiのみを使って自前実装するのか、難しくなっていきそうですね。「LangChainの実装はわかっておきながら自前実装する」が最適解ですかね。

参考

ChatGPTでURLから任意のJson形式でデータ抽出を行う|harukary
GPT function calling - OpenAI API
Understanding JSON Schema — Understanding JSON Schema 2020-12 documentation

サンプルコード

https://github.com/harukary/llm_samples/blob/main/OpenAI/funtion_calling.ipynb

この記事が気に入ったらサポートをしてみませんか?

コメント

1

とても分かりやすいです! 勉強になります😆

ログイン または 会員登録 するとコメントできます。
Function callingで複雑なJson形式を抽出する|harukary
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1