日本語の話し言葉でWebAPIを呼べる時代到来。ChatGPTプラグイン開発入門

こんにちは、テリーです。11月にアップデートされたChatGPTの新機能はもう試しましたか? ChatGPT Builder、GPTsなど、カスタムのChatGPTを作成して共有できるようになりました。毎月のように新機能が追加、公開されているOpenAIの快進撃はしばらく続きそうです。

さて、ChatGPTのたくさんの新機能の紹介は公式サイトと各種YouTubeを見ていただくとして、個人的に一番興味があるのがアクションという機能です。これは自然言語でChatGPTとチャットしつつ、外部のWebAPIを正しいフォーマットで呼び出してくれる機能です。日本語で文字を打ってWebAPIを呼べるのってすごくないですか?

そこで本記事では、ChatGPTから外部WebAPIを呼ぶために必要なステップをご紹介します。

ChatGPT関連は進化が激しく、まだバグも多いので、本記事執筆時点から数ヶ月経つとサンプルコードが動作しなくなる可能性が高いのでご了承ください。

動作確認環境

  • macOS 13.5.2
  • ChatGPT Plusアカウント
  • ngrokアカウント
  • ngrok v3
  • python 3.10.13
  • quart 0.19.3 / quart-cors 0.7.0
  • pyngrok 7.0.0

ngrokの設定

ChatGPTからWebAPIを呼ぶには開発段階でもhttpsのサーバが必要です。レンタルサーバ等で開発する方法もありますが、今回はローカルパソコン1台で無料で開発できるようにngrokを使います。

ngrokの公式サイトからアカウントを開設します。

ログインすると50文字程度の英数字が表示されます。

今回の作業にはngrokのコマンドラインツールが必要ですが、後述のpyngrokが自動的に最新版をダウンロードしてインストールしてくれます。手動でインストールする場合は、サイトからzipファイルをダウンロードして解凍し、パスの通っている場所(/usr/local/bin など)にコピーしてください。

次にAuthtokenを登録します。

ngrok config add-authtoken (あなたのAuthtoken)

ngrokのサービスで比較的最近ついた機能にCloud Edge Domainsというものがあります。これを使うと、ngrokを実行した際に毎回固定のドメイン名が割り当てられ、WebAPIを呼ぶ側のソースコードを変更する必要がなくなります。ドメイン名はランダムで割り当てられるため選択できませんが、無料で1つ取得できます。

こちらのページから登録し、あなた専用のドメインをメモしてください。以降は "aaaa-bbbb-cccc.ngrok-free.app" を得たものとしてサンプルコードを書きます。

pyngrok、quartのインストールと確認

ngrokはコマンドラインを実行して呼び出すものですが、pythonのプログラムから気軽に呼べるようにしたpyngrokというラッパーライブラリがあります。下記のコマンドを実行してインストールしてください。

pip install pyngrok

動作確認をするにはローカルにWebサーバが必要です。ChatGPTの公式サンプルで使用されているWebアプリケーションフレームワーク quart をインストールします。quartは初めて知りましたが、PythonのWebアプリケーションフレームワークとして有名なFlaskを非同期のayncを多用して書き換えたもの、という説明がされています。2つ以上の関数が同時に呼ばれる場合でも、非同期型の関数はマルチプロセスやマルチスレッドを使わずに同時に処理をすることができます。

pip install quart quart-cors

ngrokの前にまずquartの動作確認をします。下記のコードをapp.pyなどの名前で作成し、実行します。(行頭の数字は行番号です)

  1 import quart, quart_cors
  2 
  3 app = quart_cors.cors(quart.Quart(__name__, static_url_path="/"))
  4 
  5 
  6 @app.get("/hello")
  7 async def hello():
  8     return "Hello, world!"
  9 
 10 
 11 def main():
 12     port = 5003
 13     app.run(debug=True, port=port)
 14 
 15 
 16 if __name__ == "__main__":
 17     main()

http://127.0.0.1:5003/hello にブラウザでアクセスすると、「Hello, world!」と表示されることを確認します。

quartの良いところの一つに自動リロード機能があります。上記Pythonのコードを修正して上書き保存すると自動的に再実行するので、「終了して再度実行」の手間が省けます。これはコードが頻繁に更新される開発初期段階では特に便利です。

次にngrokを使ってサーバを公開します。下記のように書き換えてください。5行目のNGROK_DOMAINは上述のあなた専用のドメイン名に書き換えてください。もしくは環境変数にNGROK_DOMAINというキーで登録してください。

  1 import quart, quart_cors
  2 from pyngrok import ngrok
  3 import os, subprocess, time
  4 
  5 NGROK_DOMAIN = os.environ.get("NGROK_DOMAIN", "aaaa-bbbb-cccc.ngrok-free.app")
  6 
  7 app = quart_cors.cors(quart.Quart(__name__, static_url_path="/"))
  8 
  9 
 10 @app.get("/hello")
 11 async def hello():
 12     return "Hello, world!"
 13 
 14 
 15 def main():
 16     port = 5003
 17     ret = subprocess.run("pkill ngrok".split())
 18     if ret.returncode == 0:
 19         time.sleep(1)
 20     ngrok.connect(port, hostname=NGROK_DOMAIN)
 21     app.run(debug=True, port=port)
 22 
 23 
 24 if __name__ == "__main__":
 25     main()

https://aaaa-bbbb-cccc.ngrok-free.app/hello にブラウザでアクセスし、先ほど同様に「Hello, world!」と表示されることを確認します。

ChatGPTから呼ばれるWebAPIの作成

何かしらのWebAPIを作成します。今回は公式サンプルプログラム https://github.com/openai/plugins-quickstart/blob/main/main.py を参考に、あるユーザー名のTODOリストを管理(読み込み、追加、削除)できるWebAPIを想定し、app.pyを下記のように書き換えます。

  1 import quart, quart_cors
  2 from pyngrok import ngrok
  3 import os, subprocess, time
  4 
  5 NGROK_DOMAIN = os.environ.get("NGROK_DOMAIN", "aaaa-bbbb-cccc.ngrok-free.app")
  6 
  7 app = quart_cors.cors(quart.Quart(__name__, static_url_path="/"))
  8 
  9 # Keep track of todo's. Does not persist if Python session is restarted.
 10 _TODOS = {}
 11 
 12 
 13 @app.post("/todos/<string:username>")
 14 async def add_todo(username):
 15     request = await quart.request.get_json(force=True)
 16     if username not in _TODOS:
 17         _TODOS[username] = []
 18     _TODOS[username].append(request["todo"])
 19     return "OK"
 20 
 21 
 22 @app.get("/todos/<string:username>")
 23 async def get_todos(username):
 24     return _TODOS.get(username, [])
 25 
 26 
 27 @app.delete("/todos/<string:username>")
 28 async def delete_todo(username):
 29     request = await quart.request.get_json(force=True)
 30     todo_idx = request["todo_idx"]
 31     # fail silently, it's a simple plugin
 32     if 0 <= todo_idx < len(_TODOS[username]):
 33         _TODOS[username].pop(todo_idx)
 34     return "OK"
 35 
 36 
 37 def main():
 38     port = 5003
 39     ret = subprocess.run("pkill ngrok".split())
 40     if ret.returncode == 0:
 41         time.sleep(1)
 42     ngrok.connect(port, hostname=NGROK_DOMAIN)
 43     app.run(debug=True, port=port)
 44 
 45 
 46 if __name__ == "__main__":
 47     main()

quartの関数は、オブジェクトを返せば application/json のContent-Type付きで、jsonにエンコードして返してくれるので、当面は難しいことはする必要ありません。ステータス200以外の値を明示的に返したい時に、return文の書き方をドキュメントを見て書き換えてください。

OpenAPIのスキーマ定義ファイルの作成

OpenAIとOpenAPIは、名前は似ていますがまったく別物です。OpenAIはChatGPTを開発した会社名、OpenAPIはWebAPIの仕様を記述する書式の名前です。

ChatGPTから自作のWebAPIとやり取りするためには、OpenAPIの書式で定義ファイルを書く必要があります。OpenAPIの書式は初見ではやや難易度が高いですが、他のものを見よう見まねで書きます。同じ構造であれば、yamlでもjsonでもどちらでも構いません。

こちらのURLをコピペし、app.pyと同じフォルダにstaticというフォルダを作り、そのフォルダ内にopenapi.yamlという名前で保存します。

  1 openapi: 3.0.1
  2 info:   
  3   title: TODO Plugin
  4   description: A plugin that allows the user to create and manage a TODO list using ChatGPT. If you do not know the user's username, ask them first before making queries to the plugin. Otherwise, use the username "global".
  5   version: 'v1'
  6 servers:  
  7   - url: http://localhost:5003
  8 paths:
  9   /todos/{username}:
 10     get:
 11       operationId: getTodos
 12       summary: Get the list of todos
 13       parameters:
 14       - in: path
 15         name: username
 16         schema:
 17             type: string
 18         required: true
 19         description: The name of the user.
 20       responses:
 21         "200":
 22           description: OK
 23           content:
 24             application/json:
 25               schema:
 26                 $ref: '#/components/schemas/getTodosResponse'
 27     post:
 28       operationId: addTodo
 29       summary: Add a todo to the list
 30       parameters:
 31       - in: path
 32         name: username
 33         schema:
 34             type: string
 35         required: true
 36         description: The name of the user.
 37       requestBody:
 38         required: true
 39         content:
 40           application/json:
 41             schema:
 42               $ref: '#/components/schemas/addTodoRequest'
 43       responses:
 44         "200":
 45           description: OK
 46     delete:
 47       operationId: deleteTodo
 48       summary: Delete a todo from the list
 49       parameters:
 50       - in: path
 51         name: username
 52         schema:
 53             type: string
 54         required: true
 55         description: The name of the user.
 56       requestBody:
 57         required: true
 58         content:
 59           application/json:
 60             schema:
 61               $ref: '#/components/schemas/deleteTodoRequest'
 62       responses:
 63         "200":
 64           description: OK
 65 
 66 components:
 67   schemas:
 68     getTodosResponse:
 69       type: object
 70       properties:
 71         todos:
 72           type: array
 73           items:
 74             type: string
 75           description: The list of todos.
 76     addTodoRequest:
 77       type: object
 78       required:
 79       - todo
 80       properties:
 81         todo:
 82           type: string
 83           description: The todo to add to the list.
 84           required: true
 85     deleteTodoRequest:
 86       type: object
 87       required:
 88       - todo_idx
 89       properties:
 90         todo_idx:
 91           type: integer
 92           description: The index of the todo to delete.
 93           required: true

修正するのは3箇所です。7行目、84行目、93行目です。

まず、7行目の「http://localhost:5003」を「https://aaaa-bbbb-cccc.ngrok-free.app」に書き換えます。これは上述のngrokで取得したあなた専用ドメインです。

それから、84行目、93行目の「required: true」を行ごと削除します。これはChatGPT側のバグの可能性もありますが、本記事執筆時点では削除しないと動作しません。

ファイルの置き場所が正しいことを確認するために、https://aaaa-bbbb-cccc.ngrok-free.app/openapi.yaml にブラウザでアクセスし、外部からhttpsでyamlファイルが読み込めることを確認します。

ChatGPTにインポート

ようやくChatGPT側の画面での作業です。左のメニューの2段目「Explore」をクリックし、右の画面2段目の「Create a GPT」をクリックします。

ChatGPT Builderは一旦スルーして「Configure」ボタンをクリックします。

一番下にある「Add actions」ボタンをクリックします。

画面をよく見て、4段目付近にある「Import from URL」というリンクをクリックします。

先ほどブラウザで確認したURL「https://aaaa-bbbb-cccc.ngrok-free.app/openapi.yaml」をペーストし、「Import」ボタンを押します。

すると、下記のようにスキーマが読み込まれます。この時点で赤い文字の警告が出ている場合は、書式が文法違反の可能性が高いです。赤い文字がスキーマの下に出ていなければ正常です。

以上でインポートは完了です。

MyGPTから呼び出し

ついにChatGPTから呼び出しです。インポートにエラーが出ていない状態で、右側のChatGPTのPreviewに何か文字を入れてみます。例えば、「terryのtodoを教えて」と入れてみましょう。「todoはありません」のような趣旨のメッセージが出るでしょう。

「terryのtodoに"旅行のホテル予約"を追加して」と入れてみましょう。「データを送りますけど、信用できるサイトですか?」と質問してくるので、「Allow」ボタンを押して了承します。postの場合は毎回許可を求めてきます。

すると、TODOリストに追加されたというメッセージが出ます。先ほどと同様に「terryのtodoを教えて」と入れてみると、追加されていることが確認できます。「Talked to ドメイン名」のところをクリックすると、送信したjsonが確認できます。

動作確認ができたら、このプラグインにお好みの名前をつけ、右上の「Save」ボタンをクリックし、「Only me」を選択して「Confirm」ボタンを押します。「Only me」以外の場合はプライバシーポリシーのURLを指定する必要があります。

My GPTsの先頭に追加されていることが確認できます。また画面左の2段目にもリンクが追加されています。

Pythonの実行ログを見ると、下図のようにGETやPOSTのリクエストが来ていることが確認できます。

まとめ

ChatGPTプラグインを最小構成で作成し、MyGPTから呼び出すステップを紹介しました。自社のAPIを持っている人はプラグインを利用者に提供することができるでしょう。また、ローカルでの動作を前提にすれば、ChatGPTではできないようなブラウザ外へのアクションをプラグインを介して指示、操作することができます。DALL-E3とは違う画像生成AIに、ChatGPTで作ったプロンプトを送信してから画像を表示することもできそうです。これからのユーザーインターフェース、マンマシンインターフェースがすべて自然言語になる時代が見えてきました。ぜひ読者の皆様もプラグイン作成をお試しください。