🌎

Dart EdgeとSupabaseでOpenAI APIを叩いてみた

2023/05/02に公開

背景

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 で書けると嬉しいなと思っています。現に、ServerpodShelfDart 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+以上が必要です。
https://docs.dartedge.dev/

https://github.com/invertase/dart_edge

現在対応しているプラットフォームについては example も置いてあるので、クローンして実行するのがもっとも早く手元で試せます。
https://github.com/invertase/dart_edge/tree/main/examples

Supabase で動かしてみる

ドキュメント通りなので、詳細は割愛しますが以下を実行していくと Supabase のローカル環境(Docker)で実行を確認できます。1つ目が Supabase のドキュメントで、2つ目が Dart Edge のドキュメントとなっていますが、前者の方が詳しく書かれていてオススメです(タイラーさん の YouTube も分かりやすいです)。
https://supabase.com/docs/guides/functions/dart-edge
https://docs.dartedge.dev/platform/supabase

# 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 である必要はないです。
https://supabase.com/docs/guides/functions/examples/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 を参照します。FetchHTTP Package を使った2つの方法が紹介されています。
https://docs.dartedge.dev/guides/fetch

基本的に Edge Runtime では XMLHttpRequest を利用できないため、Deno や Node.js で HTTP リクエストを投げる場合に Fetch API を利用します。ただし、Flutter/Dart で馴染みのある HTTP パッケージの httpdio などは 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 が必要があります。

https://pub.dev/packages/dart_openai

ただ、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?"}'

大したソースコードではないですが一応こちらに置いておきます。

https://github.com/htsuruo/dart-edge-supabase-playground

まとめ

本記事では、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