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

# はじめに

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64722/09f0d8a3-4ade-dd90-ab94-9316882494d1.png)

実際に動作させた例

## TL;DR

* Function callingを使って、ChatGPT Pluginsのソースコードをローカルで実行することができる
* しかもLangChainを使うより精緻に動作する
* ソースコードは[ここ](https://github.com/canada/function-calling-connects-plugins)

## 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については前回の記事をご覧ください

https://qiita.com/canadie/items/161aaa41a5d110470e8f

## 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を呼び出す

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

課題は以下の通りです。

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

しかし逆に言うと

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

この２つを解決すれば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の関数定義に変換するロジックを組みます。

```python
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はどうやって呼び出しましょう。これはソースコード上でインポートしてしまって、無理やり関数として実行させます。

```python
# プラグインのモジュール名を指定
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に聞きながら突破しました。やりたいことは、前回の

```python
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64722/d5e7ac37-673d-b260-97bd-03c9e3af8e2e.png)

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

## ソースコード

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

https://github.com/canada/function-calling-connects-plugins