はじめに
ChatGPT Pluginsは非常に強力な機能である一方、基本的にOpenAIアプリからしか使えないことがボトルネックとなっています。いちおうLangChainなどからも呼び出せるものの、パラメタ(Action)の生成に失敗したり、トークンが足りなくなったりといろいろと厳しい。そこでFunction callingから呼び出すことで簡単に、高精度なChain of Thoughtを実現させてみます。
実際に動作させた例
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のメソッドに渡してやれば完成です。
動作サンプル
それっぽい動きが出来ていますね!バックグラウンドではきちんと必要な関数が呼び出されています。
ソースコード
動作するものを以下で公開しています。LangChainから呼び出すサンプルコードもつけてあります。