Dart EdgeとSupabaseでOpenAI APIを叩いてみた
背景
Dart Edge は、Edge Functions 上で Dart コードを実行することを目的としたプロジェクトで、FlutterFire や Melos で有名な Invertase 社 が開発しています。現在、Cloudflare Workers、Vercel Edge Functions、Supabase Edge Functions をサポートしており、Supabase に関しては つい最近追加されました。
今後もプラットフォーム的には増えていく予定とのこと(packages
を見るだけでも、Deno Deploy や Netlify Edge などが開発されています)で、現時点で非対応のプラットフォームだったとしても、メジャーどころが対応されるのは時間の問題だと思います。
普段、Flutter/Dart でアプリケーションを開発をしている方は、一度は「Dart ですべてのアプリケーションを作れると良いのにな」と思ったことがあるのではないでしょうか。私もその一人で、Flutter に限らずバックエンドの実装など、すべて Dart で書けると嬉しいなと思っています。現に、Serverpod や Shelf、Dart Frog などのサーバサイドフレームワークも進化していますし、Prisma ORM などの O/R マッパーも Dart で書けるように環境が整ってきています。
また、そろそろ stable にも来るであろう Dart 3 といった、言語仕様の進化もありますし、ますます Dart を使った開発環境が増えていくことにワクワクしています。
今回はその中で最近追加された Supabase の Edge Functions を軽く触ってみたので執筆しました。
Dart Edge
ドキュメントとリポジトリはこちらです。Dart Edge は Edge Functions 上で動くコードを Dart で書くことができるのは確かですが、ランタイムはないので Dart コードそのものが動いているわけではありません。一度 JavaScript にコンパイルしてから(将来的には WASM も対応予定)動かしているのが実態で、利用するには Node.js v18.14.0+以上が必要です。
現在対応しているプラットフォームについては example
も置いてあるので、クローンして実行するのがもっとも早く手元で試せます。
Supabase で動かしてみる
ドキュメント通りなので、詳細は割愛しますが以下を実行していくと Supabase のローカル環境(Docker)で実行を確認できます。1つ目が Supabase のドキュメントで、2つ目が Dart Edge のドキュメントとなっていますが、前者の方が詳しく書かれていてオススメです(タイラーさん の YouTube も分かりやすいです)。
# Supabaseの開発環境(Docker)立ち上げる
# ※初回はイメージのダウンロードがあるので時間かかる
❯ supabase start
# SupabaseのEdge Functions用プロジェクトを作る
❯ edge new supabase_functions new_project
❯ dart pub get
❯ supabase init
開発する
以下で Dart ファイルの変更を検知して JavaScript へ再コンパイルする Watcher を起動します。--dev
をつけると自動で再コンパイルしてくれます(Flutter の Hot Reload の感覚に近い)。
❯ edge build supabase_functions --dev
上記 Watcher を起動した状態で、以下の serve
コマンドを実行すると http://localhost:54321/functions/v1/dart_edge にて関数が実行されるはずです。
❯ supabase functions serve dart_edge --no-verify-jwt
これで、Dart コードを変更する度に再コンパイルされた JavaScript がロードされて、「Dart で Edge Functions を開発する」ができるようになります。
ちなみに、手元で観測した範囲ではサンプルコードでコンパイルに3秒程度、反映に2秒程度の計5秒程度かかり、Flutter の Hot Reload のような即時的な UI 反映と比べるとやや待機が必要な感覚でした。
✓ Compiling Dart entry file (4.3s)
✓ Compiling Dart entry file (3.1s)
✓ Compiling Dart entry file (3.1s)
デプロイする
以下のコマンドだけで完了です。
# リリース用にビルド(注意後述)
❯ edge build supabase_functions
# Edge Functionsへデプロイ
❯ supabase functions deploy dart_edge
---
# アクセストークンやプロジェクトRefを入力するとデプロイされます
You can generate an access token from https://app.supabase.com/account/tokens
Enter your access token: [YOUR_ACCESS_TOKEN] # `supabse login`していれば不要
You can find your project ref from the project's dashboard home page, e.g. https://app.supabase.com/project/<project-ref>.
Enter your project ref: [YOUR_PROJECT_REF]
Bundling dart_edge
Deploying dart_edge (script size: 112.6kB)
Deployed Function dart_edge on project [YOUR_PROJECT_REF]
You can inspect your deployment in the Dashboard: https://app.supabase.com/project/[YOUR_PROJECT_REF]/functions/dart_edge/details
適当に、http://httpbin.org/ip
を叩いて Edge Functions の IP アドレスを取得してみたら以下が返ってきました。
{"origin":"34.84.50.63"}
IP アドレス検索 などで調べてみると、西新宿あたりの Google Cloud 上で実行されているようです。私は関東圏からアクセスしたので、Edge 上でちゃんと実行されていそうですね。
OpenAI API を叩いてみる
Deno で OpenAI API を叩くサンプルがあったので、せっかくなのでこれを Dart Edge で動かしてみます。HTTP リクエストを投げることが主題なので、キーが手元になければ別に OpenAI である必要はないです。
ちなみに、Dart Edge の example としては SupabaseClient
を使って DB からデータを取得する があるので、先にそちらを動かしてみるのも良いと思います。
以下、実行環境です。
❯ fvm dart --version
Dart SDK version: 3.0.0-417.3.beta (beta)
dependencies:
edge: ^0.0.6
edge_http_client: ^0.0.1+1
http: ^0.13.5
supabase: ^1.6.4
supabase_functions: ^0.0.2+1
公式ドキュメントの Fetching Data を参照します。Fetch
と HTTP Package
を使った2つの方法が紹介されています。
基本的に Edge Runtime では XMLHttpRequest を利用できないため、Deno や Node.js で HTTP リクエストを投げる場合に Fetch API を利用します。ただし、Flutter/Dart で馴染みのある HTTP パッケージの http
や dio
などは XMLHttpRequest ベースで作られているため、Edge Runtime では利用できません。
これを解決するために、edge_http_client
という Edge 上で http
を実行できるようなパッケージが用意されているので、これを利用します。
ちなみに、Fetch
に関しては edge_runtime
パッケージにて実装されています。以下は内部実装ですが、JavaScript の fetch API を直接利用していることが分かります。
.JS('fetch')
external Promise<interop.Response> fetch(
interop.Request request, [
interop.RequestInit? init,
]);
HTTP リクエストを投げる
本来ならば、下記のようなパッケージを利用してさくっと済ませたいところですが、内部で http
を利用しているため Edge Functinos での実行には前述の通り edge_http_client
が必要があります。
ただ、example
には Supabase と組み合わせた実装がなく、試しに以下の実装をしてみたら UnsupprtedError
が返ってきてしまいリクエストを投げられませんでした。まだ Supabase 上で http
パッケージは使えないのかもしれません。
http.runWithClient(() async {
SupabaseFunctions(fetch: (request) async {
// `http`でリクエストを投げる
});
}, () => EdgeHttpClient());
`http` での UnsupportedError
UnsupportedError {
message: "Cannot create a client without dart:html or dart:io.",
"$thrownJsError": Unsupported operation: Cannot create a client without dart:html or dart:io.
at Object.wrapException (file:///home/deno/functions/dart_edge/main.dart.js:1051:17)
at Object.throwExpression (file:///home/deno/functions/dart_edge/main.dart.js:1065:15)
at Object.createClient (file:///home/deno/functions/dart_edge/main.dart.js:11473:16)
at file:///home/deno/functions/dart_edge/main.dart.js:7067:44
at _wrapJsFunctionForAsync_closure.$protected (file:///home/deno/functions/dart_edge/main.dart.js:3609:15)
at _wrapJsFunctionForAsync_closure.call$2 (file:///home/deno/functions/dart_edge/main.dart.js:14042:12)
at Object._asyncStartSync (file:///home/deno/functions/dart_edge/main.dart.js:3573:20)
at Object._withClient$body (file:///home/deno/functions/dart_edge/main.dart.js:7103:16)
at Object._withClient (file:///home/deno/functions/dart_edge/main.dart.js:7050:16)
at Object.get (file:///home/deno/functions/dart_edge/main.dart.js:7047:16)
}
仕方ないので今回は fetch
で愚直に実装します。何の変哲もないコードで恐縮ですが、以下で実行できます。
void main() {
// ref. https://supabase.com/docs/guides/functions/examples/openai
SupabaseFunctions(fetch: (request) async {
final req = (await request.json()) as Map<String, dynamic>?;
print('req: $req');
if (req == null) {
return Response.error();
}
return fetch(
Resource.uri(Uri.parse('https://api.openai.com/v1/completions')),
method: 'POST',
headers: Headers(
{
'Authorization': 'Bearer ${Deno.env.get('OPENAI_API_KEY')}',
'OpenAI-Organization': '${Deno.env.get('ORGANIZATION_ID')}',
'Content-Type': 'application/json'
},
),
// TODO: replase Map<String,dynamic> to Type-Safe class.
body: jsonEncode(<String, dynamic>{
'model': 'text-davinci-003',
'prompt': req['query'],
'max_tokens': 256,
'temperature': 0,
'stream': false,
}),
);
});
環境変数は /supabase
直下に .env.local
を配置して記載しておけば、Deno.env.get('XXX')
で読み込めます。起動時に --env-file
オプションを付けるのを忘れないようにしましょう。
❯ supabase functions serve dart_edge --env-file ./supabase/.env.local --no-verify-jwt
curl -X POST http://localhost:54321/functions/v1/dart_edge \
-H "Content-Type: application/json" \
-d '{"query": "What is Flutter?"}'
大したソースコードではないですが一応こちらに置いておきます。
まとめ
本記事では、Dart Edge と Supabase を使って HTTP リクエスト(OpenAI API)を投げるところまでをやってみました。
Dart Edge is an experimental project - it's probably not ready for production usage unless you're okay with living on the 'edge'.
繰り返しになりますが、まだ experimental
なので未熟な部分は多々あると認識しつつ、現状開発してみてツライと思ったことを書いておきます(いずれ解消されると思います)。
-
http
パッケージが Supabase では現状使えない(?) - ブレークポイントが止められない
- Router が現状 Vercel Edge のみ
-
Shelf
の軽量ラッパーとしてVercelEdgeShelf
しかまだ対応されていないので、それ以外ではルーティングできない。 - ref. https://docs.dartedge.dev/guides/shelf
-
とは言いつつも、これらも含めて対応が進んでいくと思いますし今後の動向に注目です。
Supabase の Edge Functions は Deno で構成されているため、その点だけでも十分に開発者体験が良く書きやすいとは思います。しばらくは Deno で開発することになるとは思いますが、Dart Edge の動向はウォッチしつつ良きタイミングでまた触ってみようと思います。
Discussion