michimani.net

OpenAI API の Function Calling を使って自然言語で AWS リソースを作成してみる

2023-06-18

OpenAI API にアップデートがあり、Function Calling という機能が追加されました。その名前からして任意の関数を実行できるのかなと思いましたが、どうやらそうではないみたいだったので実際に使ってみて機能の雰囲気を掴んでみます。

実際に動くコードはこちら。

misc/llm/openai/get-started-to-use-function-calling | michimani/misc · GitHub

Function Calling とは

Function Calling とは、 OpenAI の ChatCompletion API に対して、そのレスポンス内に任意のスキーマを持つ JSON を含めるようにすることができる機能です。 Function Calling という名前から、 OpenAI API と任意の関数を統合して実行までしてくれるような機能を想像しましたが、そうではありません。

The Chat Completions API does not call the function; instead, the model generates JSON that you can use to call the function in your code.

Function calling | GPT - OpenAI API

ChatCompletion API はモデルが生成した JSON を返すだけで、関数の実行自体は我々が書くコードで実施します。つまり、 Function Calling という機能の本質は、 ChatCompletion API のレスポンスに任意のスキーマの JSON を含めることができるようになった ということです。

実際に使ってみる

今回は、この 任意のスキーマの JSON を生成できるようになった ことを利用して、ユーザからの自然言語による入力から AWS のリソースを作成するコードを書いてみます。具体的には、 CloudWatch Evidently の Feature (および Project) を自然言語で作成できるようにしてみます。

例えば、

「寿司を提供するかどうかを判定する feature を作りたいです。名前は sushi で、プロジェクト名は food。force-sushi という entity に対しては必ず true を返して、デフォルトでは false を返すようにしたいです。」

といった入力を与えることで、food という名前の Project と、 True/False を返す sushi という Feature をオーバーライドルールありで作成されるようなイメージです。

Function Calling リリースまでは、上記の入力からどの部分が Project 名でどの部分が Feature 名で、オーバーライドルールはあるのか、あるならどういう指定なのか、などを判定していい感じの JSON を返してくれるようにするためのプロンプトを調整する必要がありました。それが、 Function Calling を使うことで予め指定した任意の JSON スキーマに従ってユーザの入力から値を書くフィールドに割り当てた JSON をモデル側が生成してくれるようになります。

ChatCompletion API 実行時のパラメータ

ChatCompletion API のリクエストに functions および function_call パラメータを指定できるようになりました。

functions

functions パラメータには、 ChatCompletion API のレスポンスに含める任意のスキーマを JSON で指定します。今回は、以下のように指定します。

functions=[
    {
        "name": "create_evidently_boolean_feature",
        "description": "Create a new boolean feature in evidently",
        "parameters": {
            "type": "object",
            "properties": {
                "project_name": {
                    "type": "string",
                    "description": "Project name of CloudWatch Evidently.",
                },
                "feature_name": {
                    "type": "string",
                    "description": "Feature name of CloudWatch Evidently.",
                },
                "default_value": {
                    "type": "boolean",
                    "description": "Default value of CloudWatch Evidently.",
                    "default": False,
                },
                "override_rules": {
                    "type": "array",
                    "items": {
                        "type": "array",
                        "items": {
                            "type": "string",
                        },
                    },
                    "description": """Override rules of CloudWatch Evidently.
                        This value is list of tuple that has two elements.
                        First element is the name of entity,
                        second element is the variation 'True' or 'False'.""",
                    "default": [],
                },
            },
            "required": [
                "project_name",
                "feature_name",
                "default_value",
                "override_rules",
            ],
        },
    }
]

functions 自体は array で、複数の関数を指定することができます。今回は、1つの関数を指定しており、各設定値については下記のとおりです。

name

このレスポンスを受け取ったあとに自分が書いたプログラム側で実行する予定の関数の名前です。この値は実際に実行する関数の名前と一致している必要はありませんが、複数の関数を指定したりする場合はややこしくなるので基本的に一致させておいたほうが良さそうです。

description

実行する予定の関数の説明です。この値によってモデル側での JSON 生成の精度に影響があるのかもしれませんが、今回は検証していません。

parameters

モデルに生成してもらう JSON スキーマの設定です。今回は下記のような JSON を生成してもらうような設定にしています。

{
  "project_name": "food",
  "feature_name": "sushi",
  "default_value": false,
  "override_rules": [
    [
      "force-sushi",
      "True"
    ]
  ]
}

各 parameter (JSON のフィールド) に対して型やデフォルト値、また今回は使っていませんが Enum による値の制限などを指定することができます。 どのような値が入ってほしいかを description で指定することで、ユーザの入力からよしなに値を割り当ててくれます。

型の指定方法については JSON Schema Reference に従います。

required

生成される JSON に含まれてくるフィールドを指定します。今回はすべてのフィールドが含まれるようにしています。

function_call

function_call パラメータには、 functions で指定した関数のうち、どの関数用の JSON を生成してほしいかを指定します。明示的に指定したい場合は下記のように指定します。

function_call={"name": "create_evidently_boolean_feature"}

ユーザの入力によっては無理やり JSON を生成してほしくない場合があるので、その場合は

function_call="auto"

と指定します。今回はこの設定で実行します。

デフォルトは、 JSON 生成を行わない function_call=None です。

ChatCompletion API のレスポンス

ChatCompletion API のレスポンスには、 functions で指定した関数のうち、 function_call で指定した関数用の JSON が含まれます。今回は functions は一つだけで、かつ function_call="auto" で実行するので、 JSON が生成される場合には下記のようなレスポンスが返ってきます。

{
  "id": "chatcmpl-7SgH1Taqoxxxxxxxxxxxxxxxxxx",
  "object": "chat.completion",
  "created": 1687070039,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "function_call": {
          "name": "create_evidently_boolean_feature",
          "arguments": "{\n  \"project_name\": \"food\",\n  \"feature_name\": \"sushi\",\n  \"default_value\": false,\n  \"override_rules\": [[\"force-sushi\", \"True\"]]\n}"
        }
      },
      "finish_reason": "function_call"
    }
  ],
  "usage": {
    "prompt_tokens": 234,
    "completion_tokens": 49,
    "total_tokens": 283
  }
}

choices[0].messagefunction_call フィールドが含まれていることがわかります。

function_call.name には、リクエスト時に指定した functionsname が含まれています。 そして、 function_call.arguments には、 parameters で指定した JSON スキーマに従った JSON が文字列で含まれています。この JSON 文字列をパースすることで、次に実行予定の関数への引数設定が容易になります。

ちなみに、ユーザの入力から関数用の JSON を生成できない場合は、通常の ChatCompletion API のレスポンスになります。例えば、今回の場合に お腹が空きました といった入力が渡された場合は、下記のようなレスポンスが返ってきます。

{
  "id": "chatcmpl-7SgMT1loxxxxxxxxxxxxxxxxxx",
  "object": "chat.completion",
  "created": 1687070377,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "\u304a\u8179\u304c\u7a7a\u3044\u305f\u306a\u3089\u3001\u4f55\u304b\u98df\u3079\u308b\u3053\u3068\u3092\u304a\u3059\u3059\u3081\u3057\u307e\u3059\u3002\u4f55\u304b\u7279\u5b9a\u306e\u98df\u3079\u7269\u304c\u6b32\u3057\u3044\u3067\u3059\u304b\uff1f"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 159,
    "completion_tokens": 46,
    "total_tokens": 205
  }
}
\u304a\u8179\u304c\u7a7a\u3044\u305f\u306a\u3089\u3001\u4f55\u304b\u98df\u3079\u308b\u3053\u3068\u3092\u304a\u3059\u3059\u3081\u3057\u307e\u3059\u3002\u4f55\u304b\u7279\u5b9a\u306e\u98df\u3079\u7269\u304c\u6b32\u3057\u3044\u3067\u3059\u304b\uff1f
お腹が空いたなら、何か食べることをおすすめします。何か特定の食べ物が欲しいですか?

あとは関数の実装を頑張る

ChatCompletion API がユーザの入力から関数実行用のパラメータを JSON 形式で生成してくれたので、あとは関数の実装を頑張るだけです。ここは Function Calling のリリースとは直接関係ないので、詳細は割愛します。が、今回であれば下記のような関数を作っていました。

@dataclass
class CreateBooleanFeatureInput:
    """Input for create_evidently_boolean_feature function."""

    project_name: str
    feature_name: str
    default_value: bool = False
    override_rules: Optional[list[Tuple[str, str]]] = None
    
  
def create_evidently_boolean_feature(params: CreateBooleanFeatureInput) -> bool:
    """Create a new boolean feature in evidently.

    Args:
        params (CreateBooleanFeatureInput): The input parameters.

    Returns:
        bool: True if successful, False otherwise.
    """

    if not evidently.project_exists(client, params.project_name):
        project_arn = evidently.create_project(client, params.project_name)
        if project_arn is None:
            print("Failed to create project")
            return False

        print(f"Created project {params.project_name} with ARN {project_arn}")

    feature_arn = evidently.create_boolean_feature(
        client,
        params.project_name,
        params.feature_name,
        default_value=params.default_value,
        override_rules=params.override_rules,
    )

    if feature_arn is None:
        print("Failed to create feature")
        return False

    print(f"Created feature {params.feature_name} with ARN {feature_arn}")
    return True

先ほど例に挙げた下記の入力を与えてみます。

寿司を提供するかどうかを判定する feature を作りたいです。名前は sushi で、プロジェクト名は food。force-sushi という entity に対しては必ず true を返して、デフォルトでは false を返すようにしたいです。

すると、下記のような Feature が作成されます。

$ aws evidently get-feature --project 'food' --feature 'sushi'

{
    "feature": {
        "arn": "arn:aws:evidently:ap-northeast-1:000000000000:project/food/feature/sushi",
        "createdTime": "2023-06-18T01:48:39.219000+09:00",
        "defaultVariation": "False",
        "description": "",
        "entityOverrides": {
            "force-sushi": "True"
        },
        "evaluationRules": [],
        "evaluationStrategy": "ALL_RULES",
        "lastUpdatedTime": "2023-06-18T01:48:39.219000+09:00",
        "name": "sushi",
        "project": "arn:aws:evidently:ap-northeast-1:000000000000:project/food",
        "status": "AVAILABLE",
        "tags": {},
        "valueType": "BOOLEAN",
        "variations": [
            {
                "name": "True",
                "value": {
                    "boolValue": true
                }
            },
            {
                "name": "False",
                "value": {
                    "boolValue": false
                }
            }
        ]
    }
}

今回の実装の詳細についてはこちらのコードを参照してください。 お手元でも試していただけます。

misc/llm/openai/get-started-to-use-function-calling | michimani/misc · GitHub

まとめ

OpenAI API の新機能 Function Calling を試してみた話でした。
機能の名前と実際にできることとは似ているようで直接的な説明ではないので混乱しがちですが、 OpenAI API へのリクエスト結果を元になにか別の関数あるいは別の API を実行するような実装をしている場面では、その関数および API 実行用のパラメータ生成を OpenAI のモデル側でやってくれるようになったのは非常に便利だと思います。


comments powered by Disqus