Cloudflare Workers + HonoでChatGPT Pluginを作る
ChatGPT PluginとはChatGPTとサードパーティのアプリケーションやリソースを接続するための機能。例えば通常ChatGPTに「池袋でランチにおすすめのイタリアンのお店を教えて」と聞くと大体存在しない店を平然と回答してくる。これはChatGPTはGoogle検索で調べられるような事実やニュース、即時性のある事柄に対する回答が仕組み上苦手だからだ。
ChatGPT Pluginではこうした点を補うために、先の例で言えば食べログPluginを使うとユーザーからの質問を元に食べログのAPIを叩きおすすめのお店を取得、そのお店の情報を利用してユーザーへの回答を作る。そうすると実在する店の情報を自然な日本語で回答できるようになる。
ChatGPT Pluginは現在ベータ版ではあるが課金してるユーザーはもちろんfreeユーザーでも利用できるようになってきていて、少しコードが読み書きできれば誰でも自分が開発したPluginを実際に組み込んで試すこともできるようになっている。
今回はそんなChatGPT Pluginの開発方法を調べて試したのでその手順を書く。
何を作るか
サンプルとして用意されているようなTODOリストを作ったりしてもいいのだが、DBを用意したり認証周りの整備をするのが面倒なので今回は現在時刻を返してくれるプラグインを作ることにする。
ChatGPTは上記のスクショのようにその特性上「今何時?」と聞いても何も答えることができないが、今回作ったプラグインを使えばリアルタイムに何時かを答えてくれるようにできる。
まぁ実際時計を見ればいい話なので実用性は全くない。
必要なもの
ChatGPT Pluginを作るのはとても簡単である。というのも必要なものは、下記の3つだけだからだ。
- プラグインのマニフェストファイル
- OpenAPI Specificationファイル
- ChatGPTから呼び出されるAPI
今回は静的ファイルのサービングとAPIの提供をCloudflare Worker + Honoを使って作ることにした。実はローカルにサーバーを建てるだけでもPluginを試すことはできるが、あえて今回はRemoteにコードを置いて動かしてみる。
とりあえずhono with Cloudflare Workerのcreateコマンドを使って雛形を作成する。
% npm create hono@latest chatgpt-plugin-demo
% npm i
% npm i --save-dev @cloudflare/workers-types
% tree
.
├── README.md
├── package-lock.json
├── package.json
├── src
│ └── index.ts
├── tsconfig.json
└── wrangler.toml
Honoはご丁寧にCloudflare Workers用のGetting Startedページもあるので簡単にdiveできて良い。
プラグインのマニフェストファイル
${scheme}://${host}/.well-know/ai-plugin.json
という形式でファイルにアクセスできるようにしたいのでまずはルートディレクトリにassets/.well-known/ai-plugin.json
を作る。
.
├── assets
│ └── .well-known
│ └── ai-plugin.json <- 今回追加したファイル
├── README.md
├── package-lock.json
├── package.json
├── src
│ └── index.ts
├── tsconfig.json
└── wrangler.toml
そしてWorkersで静的ファイルをホスティングするための設定としてwrangler.toml
にWorkers Siteのbucket
の設定を加える。
...
[site]
bucket = "./assets"
さて、ai-plugin.json
の中身についてだが、下記のような記述になっている。
{
"schema_version": "v1",
"name_for_human": "Time Plugin (no auth)",
"name_for_model": "time",
"description_for_model": "Plugin for getting a current time in the current location from the user's saying (such as 'What time is it now?' or 'Im living in Tokyo, What time?'). Use it whenever you have a question about the current time.",
"description_for_human": "Get what time it is now in your location.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "https://chatgpt-plugin-demo.razokulover.workers.dev/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "https://chatgpt-plugin-demo.razokulover.workers.dev/logo.png",
"contact_email": "legal@example.com",
"legal_info_url": "http://example.com/legal"
}
schema_version
, name_for_human
, name_for_model
, description_for_human
, description_for_model
, auth
, api
, logo_url
, contact_email
, legal_info_url
は必須の項目になっている。各項目の詳細はここを参照してほしい。
今回は楽するためにAPIの認証は不要にしたいのでauth
はtype: none
になっている。
description_for_model
がこの中で一番重要なフィールド。ChatGPTはユーザー入力からここに書かれている内容を見てPluginを起動するかどうか決める。このプラグインには何ができてどのように使われることを想定しているのかを簡潔に、しかし情報量多く、を心がけて書いておくことが好まれる。
書き方のコツは公式ドキュメントのWrinting descriptionやopenai/chatgpt-retrieval-pluginを参考にすると良い。
OpenAPI Specificationのファイル
OpenAPI Specification(OpenAIではない。ややこしい。)はHTTP APIの仕様を共通のフォーマットでドキュメント化するためのものだ。ChatGPTはこのOpenAPI Specificationを読んでどのAPIにクエリを投げるのかを決める。ここにはどのエンドポイントがどのような機能を持つのか、そのエンドポイントを叩くと何が返ってくるのか等を詳細に記述する必要がある。
.
├── assets
│ └── .well-known
│ └── ai-plugin.json
│ └── openapi.yaml <- 今回追加したファイル
├── README.md
├── package-lock.json
├── package.json
├── src
│ └── index.ts
├── tsconfig.json
└── wrangler.toml
今回の場合は上記のようにassets
配下にopenapi.yaml
を作った。中身は以下の通り。
openapi: 3.0.1
info:
title: Current Time API
description: A plugin that allows the user to get what time it is now in their location using ChatGPT. If you do not know where the user lives, always ask back where they live before querying the API. And always be sure to run the API when asked something to get latest time.
version: "v1"
servers:
- url: https://chatgpt-plugin-demo.razokulover.workers.dev
paths:
/time:
get:
operationId: getTime
summary: Get the current time in UTC
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/getTimeResponse"
components:
schemas:
getTimeResponse:
type: object
properties:
now:
type: string
description: current time
重要なフィールドはinfo
のdescription
と各paths
のsummary
。ここもai-plugin.json
のdescription_for_model
同様、ChatGPTに参照される部分なので良い感じに書く必要がある。
ChatGPTから呼び出されるAPIの実装
あとはAPIを実装するだけ。今回の例で言えば、下記の3つのエンドポイントへのリクエストが200で正しく動けばOK。
/.well-known/ai-plugin.json
/openapi.yaml
/time
上記2つは静的ファイルですでに準備済みなのであとは/time
を実装する。と言っても先のOpenAPI Specificationに書いた通り現在時刻をUTCで返すだけ。コードは下記の通り。逐一コメントを入れたので気になったら読んでおいてほしい。
import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers";
import { cors } from "hono/cors";
const app = new Hono();
// CORS設定。ChatGPTにプラグイン登録するときにやっておかないと警告が出たので追加した。
app.use("*", cors());
// `./assets`が静的ファイルのルートディレクトリになっているのでserveStaticに指定するpathは`./assets`配下から
app.use(
"/.well-known/ai-plugin.json",
serveStatic({
path: "./.well-known/ai-plugin.json",
})
);
app.use(
"/openapi.yaml",
serveStatic({
path: "./openapi.yaml",
})
);
// logoファイルも一応必要なので適当に
app.use(
"/logo.png",
serveStatic({
path: "./logo.png",
})
);
// これはなくてもOK。ルートにリクエストして何も返ってこないと気持ち悪いので追加しておいた。
app.get("/", (c) => {
return c.json({
message: "What time is it now?",
});
});
// UTC時間で現在時刻を返すだけのコード
app.get("/time", (c) => {
const now = new Date();
const year = now.getUTCFullYear();
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
const day = String(now.getUTCDate()).padStart(2, "0");
const hours = String(now.getUTCHours()).padStart(2, "0");
const minutes = String(now.getUTCMinutes()).padStart(2, "0");
const seconds = String(now.getUTCSeconds()).padStart(2, "0");
return c.json({
now: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`,
});
});
export default app;
ChatGPTに組み込む
ChatGPTのPlugin設定のPlugin storeを開くとDevelop your own pluginというリンクが右下に出てくるのでそれをクリック。
するとマニフェストファイルやAPIのエンドポイントのホストされているhost名を入力するように促されるので言われるがままに入力して進んでいく。
するとプラグインがインストールされる。
以上。
実際に使ってみる
現在は東京のタイムゾーンで17:45である。
まずは何も細かい指定をせずに今何時か聞いてみる。
UTC時間で返答してきた。
では東京に住んでいることを明示して改めて時間を聞いてみる。
するとタイムゾーンを考慮して17:45と回答してきた。偉い。
それではifにも対応できるのか実験。パリに住んでいるとしたら今何時か?を聞いてみる。
パリのタイムゾーンを考慮して10:46と回答してきた。サマータイムを考慮できているのが地味にすごい。しかもちゃんと1分経過している事実も反映されている。リアルタイムに時間が取得できているようだ。
ではもっと意地悪な質問にしてみる。タイムゾーンが複数ある場合にどのように対応するか?を実験するために「アメリカに住んでいます。今何時ですか?」と聞いてみた。
ちゃんとタイムゾーンが複数あることが理解できている。さらにどこに住んでいるのか聞き返してきた。これはopenapi.yaml
のinfo
で聞き返すようにプロンプトを記述していたからだろう。言われた通りに住んでいる都市名を返すとタイムゾーンを考慮した現在時刻を返してきた。
想定通り動いている。
試行錯誤ポイント
locationの自動判定
-
最初、ユーザーがどこに住んでいるのかをユーザーが回答せずとも知ることはできないか?と思いいくつか試行錯誤してみた。Cloudflare WorkersではAPIへのリクエスト元に関するGeo Locationの情報をrequestの中に含んでくれるのだけど、ChatGPT PluginはAPIへのリクエストをクライアントではなくOpenAIのサーバー側で実行するみたいなのでどのクライアントであろうとアメリカの西海岸がTimezoneになってしまった...
https://blog.cloudflare.com/location-based-personalization-using-workers/ -
ローカルだとうまく動くけど、これは本番だとどうしようもないのでとりあえずAPIはUTCの時間を返すだけにして、ユーザーがどこに住んでいるか教えてくれたらそのタイムゾーンにおける現在時刻に変換して返してくれるようにmodel向けのdescriptionを変更したりした
環境の切り替え
- プラグインを開発してる時はlocalhostでホストしているが、Workersにdeployした状態ではworkers.devでホストされる
- ai-plugin.jsonとopenapi.yamlファイルの中にエンドポイントのホスト名がハードコードされている
- ハードコードされているので実行環境ごとに切り替えどうするか...という問題がある
- openai/chatgpt-retrieval-pluginでは静的ファイル配信ではなく、ローカルファイルとしてyaml/jsonを読み込み、テキスト内のホスト部分をユーザーからのリクエストヘッダーのHostで置換してレスポンスを返すようにして解決している
- Workersだとローカルファイルへのfetchができないし、かといって別のストレージにjsonとyamlだけおくのもだるい。また同一リポジトリの別のエンドポイントでjson/yamlにアクセスできるようにし(https://~.worker.dev/tmp/openapi.json みたいな感じ)、これにWorker内部からfetchすればいけるか?と思ったら同一Worker内からのfetchリクエストは禁止されてるっぽいのでだめ
- 結局どうすると良いかは思いつかなくて保留してる...
まとめ
Cloudflare Workers + Honoでリアルタイムに現在時刻を返すChatGPT Pluginを作ってみた。予想以上に簡単にできるので社内文書をバックエンドに据えたプラグインなんかも簡単に作れるんじゃないかと思う。
今回は1つしかエンドポイントがなかったが複数あるとOpenAPIのファイルを作るのはだるそうだなと感じた。Honoの実装からOpenAPIのファイルが自動で作れたりしたら楽そう。
Workersで動かしてるのでD1やKVなんかと連携してもっとwebアプリっぽいプラグインも作れるかもしれない。色々遊べそう。
ソースコードは下記に置いてあるのでご自由にどうぞ。
リンク
Discussion