🍣

【Flutter x OpenAI API】ChatをStreamで受け取る💬

2023/12/09に公開

Flutter Advent Calendar 2023 11日目の記事です。

概要

OpneAI APIからのレスポンスをStreamで受け取り、
リアルタイムに文字が出力されていくデモアプリをFlutterで作ってみました。

本記事では実装時に考えたこと、個人的に躓いたところをまとめようと思います。

OpenAI APIの登録、SSEの知見が必要となりますが、
記事後半に載せた参考にさせていただいた記事がめちゃわかりやすかったです!

まずコード

何やってるかにフォーカスしたかったので、
処理分けたりエラーハンドリングしたりはすっ飛ばしてますがご容赦ください🙇

Gifの送信ボタンを押した後に走らせてるのが以下です。
Future<StreamedResponse>で受け取ったデータをlistenし、表示しているChatGPTのテキストに追加しています。

import 'package:http/http.dart' as http;

~~~

void sendChatCompletionRequest() {
  final client = http.Client();
  
  var request = http.Request(
    'POST',
    Uri.parse('https://api.openai.com/v1/chat/completions'),
  );
  
  Map<String, String> header = {
    'accept': 'text/event-stream',
    'Authorization': 'Bearer $apiKey',
    'Content-Type': 'application/json'
  };
  
  header.forEach((key, value) {
    request.headers[key] = value;
  });
  
  Map<String, dynamic> body = {
    "model": "gpt-3.5-turbo",
    "messages": [
      {"role": "user", "content": inputText.value}
    ],
    "stream": true
  };
  
  request.body = jsonEncode(body);
  
  Future<http.StreamedResponse> response = client.send(request);
  
  response.asStream().listen((data) {
    // ByteStreamをStringに変換し、改行で分割して1行ずつ処理する
    // httpでなくdioを使う場合でも同じような処理が必要になる
    // https://github.com/cfug/dio/issues/1279#issuecomment-1150634592
    data.stream
        .transform(const Utf8Decoder())
        .transform(const LineSplitter())
        .listen(
      (dataLine) {
        // dataLineが空の場合や、[DONE]が返ってきた場合は早期リターン
        if (dataLine.isEmpty || dataLine == 'data: [DONE]') {
          return;
        }
	
        // dataLineの中身は'data: 'で始まっているので、それを削除してからMapに変換
        final map = dataLine.replaceAll('data: ', '');
        Map<String, dynamic> data = json.decode(map);
        // finish_reasonがstopの場合は早期リターン
        if (data['choices'][0]['finish_reason'] == 'stop') {
          return;
        }
	
        // 'content'を取得して、aiMessageに文字列を追加していく
        List<dynamic> choices = data["choices"];
        Map<String, dynamic> choice = choices[0];
        Map<String, dynamic> delta = choice["delta"];
        String content = delta["content"];
        aiMessage.value = aiMessage.value += content;
      },
    );
  });
}

このコードになった経緯

Streamで受け取るためのSSEを実装するためにflutter_client_sseパッケージの使用を考え、
Exampleに記載されている通り実装すれば簡単に実装可能だったのですが、2点問題がありました。

なのでflutter_client_sseは使わず、
要所でやっていることを参考に実装しています。
https://github.com/pratikbaid3/flutter_client_sse/blob/9433e1e49e8dca302a2e4362640355aa4e4f634a/lib/flutter_client_sse.dart

要所

以下を押さえればhttpでもdioでも似たような実装になります。

  • headerで'accept': 'text/event-stream',を指定
  • bodyで"stream": trueを指定
  • responseのByteStreamをStringに変換する

本記事には書いてないですがdioで実装したとき参考になったissue
https://github.com/cfug/dio/issues/1279

まとめ

意外とサクッとできるので、リアルタイムにメッセージを出すのが適している機能には積極的にSSEを取り入れたいと思いました!

参考にさせていただいた記事

https://zenn.dev/chot/articles/a089c203adad74
https://qiita.com/NaoSekig/items/69dc7a0d24149cf4fc04
https://zenn.dev/k9i/articles/25c310b2dad300

Documentation

https://platform.openai.com/docs/api-reference/chat/create
https://platform.openai.com/docs/api-reference/streaming

あとがき

今回初めてAPI課金し利用しましたが、
せっかくならバックエンドも学んで良い感じに書きたい〜!と思いました...!!

来年はフロント以外も多少触れるようになりたい🎄

Discussion