LoginSignup
3
7

Function callingを利用して、高精度なChatGPT Pluginsの呼び出しを実現

Last updated at Posted at 2023-07-11

はじめに

ChatGPT Pluginsは非常に強力な機能である一方、基本的にOpenAIアプリからしか使えないことがボトルネックとなっています。いちおうLangChainなどからも呼び出せるものの、パラメタ(Action)の生成に失敗したり、トークンが足りなくなったりといろいろと厳しい。そこでFunction callingから呼び出すことで簡単に、高精度なChain of Thoughtを実現させてみます。

image.png

実際に動作させた例

TL;DR

  • Function callingを使って、ChatGPT Pluginsのソースコードをローカルで実行することができる
  • しかもLangChainを使うより精緻に動作する
  • ソースコードはここ

Pluginsが実行できると何が嬉しいのか

MicrosoftはCopilot戦略にあたり、OpenAI社のPluginsと互換性をもたせることを発表しています。つまり今後、ChatGPTのみならず、Copilotなどサードパーティーの拡張機能としてChatGPT Pluginsがデファクト・スタンダードとして普及していくことが考えられるのです。

となると、新たに拡張機能を作る場合、Pluginとして提供しなくとも、同様なフォーマットを使った方が仕様が統一され、開発効率の向上が見込まれます。もちろんプラグインへの再利用も容易です。

しかし現状はChatGPTアプリ以外でPluginsを動かすのはなかなか難しいため、より精緻に動くFunction callingで実装してみました。

前提

  • OpenAI社のAPIであるgpt-3.5-turbo-0613が利用できること(Azure OpenAIのgpt-35-turbo-0613はFunction calling未対応)
  • Function callingとChatGPT Pluginsについてふんわり理解していること
  • 仕様するPluginsがFlaskで書かれていること

Function callingについては前回の記事をご覧ください

Function callingとChatGPT Pluginsの違い

ところで、Function callingとChatGPT Pluginsは機能が似ている点を前回述べましたが、ここでもう一度機能を見てみましょう。

  • Function calling
    • ChatGPTを呼び出す
    • Functionの仕様をChatGPTに読ませる
    • ChatGPTの指示で外部APIを呼び出すことができる
  • ChatGPT Plugins
    • ChatGPTから呼び出される
    • openapi.yamlのAPI仕様を用意してそれをChatGPTに読んでもらう
    • ChatGPTに呼び出されるAPIを実装する

何か気づくことがないでしょうか?

Function callingからChatGPT Pluginsを呼び出す

そうです。この2つの機能はちょうど構造が逆ですね。ということは、Function calling側で頑張ればLangChainを使わずともChain of Thoughtができそうです。

課題は以下の通りです。

  • Function callingの関数仕様と、openapi.yamlのフォーマットが全然違う
  • ChatGPT PluginsはWebアプリであるため、Function callingから呼ばれる前提にない

しかし逆に言うと

  • openapi.yamlを無理やりFunction callingの関数仕様に変換する
  • OpenAPI PluginsのAPI用に定義されたメソッドを無理やりFunction callingから関数として呼び出す

この2つを解決すればOpenAPI Pluginsの仕様そのままにFunction callingから叩けそうです。LangChainから利用する場合は、ChatGPT Pluginsを起動しなければなりませんが、Function callingから無理やりPluginsのソースコードをインポートすればWebアプリの起動も必要ありません。

Function callingを利用するメリット

LangChain版で実行すると、Actionの生成に失敗してChain of Thoughtが途中で止まってしまうことがよくあります。Function callingはChatGPTがネイティブで関数の呼び出しに対応しているので、アクションの生成に失敗して実行が途中で止まってしまうということがありません。

また、LangChainではトークンを使い切って止まってしまうことが多いですが、Function callingの方が消費トークンは少なくて済むようです。

実装

ではFunction callingからPluginsを呼び出す実装をやっていきましょう。

まずはopenapi.yamlをFunction callingの関数定義に変換するロジックを組みます。

import yaml

class OpenApiToFunctionConverter:

    openapi_definition: str

    # コンストラクタ
    def __init__(self, yaml_file_path: str):
        # YAMLファイルを読み込む
        with open(yaml_file_path) as f:
            self.openapi_definition = f.read()

    # YAMLスキーマ中の$refを展開する
    @staticmethod
    def _resolve_schema(schema: str, components_schemas: dict) -> dict:
        if "$ref" in schema:
            ref_name = schema["$ref"].split("/")[-1]
            return OpenApiToFunctionConverter._resolve_schema(components_schemas[ref_name], components_schemas)
        elif "properties" in schema:
            resolved_properties = {}
            for prop_name, prop_schema in schema["properties"].items():
                resolved_properties[prop_name] = OpenApiToFunctionConverter._resolve_schema(prop_schema, components_schemas)
            schema["properties"] = resolved_properties
        elif "items" in schema:
            schema["items"] = OpenApiToFunctionConverter._resolve_schema(schema["items"], components_schemas)
        return schema

    # OpenAPI定義を関数定義に変換する
    @staticmethod
    def _convert_to_functions(openapi_definition: str) -> list:
        openapi_data = yaml.safe_load(openapi_definition)
        paths = openapi_data["paths"]
        components_schemas = openapi_data["components"]["schemas"]
        functions = []

        for path, methods in paths.items():
            for _, details in methods.items():
                function = {
                    "name": path.strip("/"),
                    "description": details["summary"],
                    "parameters": {
                        "type": "object",
                        "properties": {},
                        "required": [],
                    },
                    "responses": {},
                }

                for parameter in details["parameters"]:
                    param_name = parameter["name"]
                    function["parameters"]["properties"][param_name] = {
                        "type": parameter["schema"]["type"],
                        "description": parameter["description"],
                    }
                    if parameter["required"]:
                        function["parameters"]["required"].append(param_name)

                for response_code, response_details in details["responses"].items():
                    response_schema = response_details["content"]["application/json"]["schema"]
                    resolved_schema = OpenApiToFunctionConverter._resolve_schema(response_schema, components_schemas)
                    function["responses"][response_code] = {
                        "description": response_details["description"],
                        "schema": resolved_schema,
                    }

                functions.append(function)

        return functions

    # openapi.yamlを関数定義に変換する
    def get(self) -> list:
        return self._convert_to_functions(self.openapi_definition)

30分ほどでできてしまいました。これはGPT-4に、openapi.yamlとFunction callingの仕様を両方食わせて「コンバータを書いて」とお願いして出力されたものを、さらにGPT-4に投げてクラス化してもらったものです。

PluginsのAPIはどうやって呼び出しましょう。これはソースコード上でインポートしてしまって、無理やり関数として実行させます。

# プラグインのモジュール名を指定
MODULE_NAME = "plugin.app"

# プラグインの読み込み
plugin_module = importlib.import_module(MODULE_NAME)

# Flaskのappコンテキストの中の関数を無理やり呼び出す
from flask import Flask
app = Flask(__name__)
with app.app_context():
    function_response = getattr(plugin_module, f_call["name"])(f_call["arguments"])

ここは難儀しましたが、GPT-4に聞きながら突破しました。やりたいことは、前回の

function_response = globals()[f_call["name"]](f_call["arguments"])

と同じですが、インポートしたplugin.appの関数を呼びたい点、plugin/app.pyがFlaskアプリになっている点などで一筋縄ではいきませんでした。

難しいところはこの2か所だけ。インポートしたopenapi.yamlの定義をFunction callingに渡し、Function callingから返ってきた関数と引数をPluginsのメソッドに渡してやれば完成です。

動作サンプル

image.png

それっぽい動きが出来ていますね!バックグラウンドではきちんと必要な関数が呼び出されています。

ソースコード

動作するものを以下で公開しています。LangChainから呼び出すサンプルコードもつけてあります。

3
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
7