⛳
Flutter WebでSSEを扱う際の注意点
はじめに
これはFlutter Webの話です。
Webをターゲットにしない場合、こんなことを考えなくてもよいはずです。
結論
http.post(request)
でストリームを取得し、読み取ろうとしても意図した動作になりません。
サーバーサイドでのデータ送信が全て完了した後、一括での読み取りとなってしまいます。(意味ない)
これはFlutter Webの仕様のようです。[1]
都度データを読み出すためには、fetch_client
を使ってあげる必要があります。
Flutterサンプルコード
Future<ByteStream>
からストリームを読み取るサンプルになります。
このissueのポストがズバリ解決方法になっています。
リクエストからレスポンスストリームを取得
sse_stream_web.dart
import 'package:fetch_client/fetch_client.dart';
import 'package:http/http.dart';
Future<ByteStream> getStream(Request request) async {
final FetchClient fetchClient = FetchClient(mode: RequestMode.cors);
final FetchResponse response = await fetchClient.send(request);
return response.stream;
}
リクエストを投げ、レスポンスストリームから読み取り
main.dart
import 'package:XXXXX/sse_stream_web.dart';
final endpointUri = Uri.http(apiServerUrl, "/getTopicStream/");
final body = {
"apikey": "",
"prompt": prompt,
"picture_base64": pictureBase64,
"dry_run": isDryRun,
};
final header = {
'accept': 'text/event-stream',
"Content-Type": "application/json",
};
var request = http.Request(
"POST",
endpointUri,
);
header.forEach((key, value) {
request.headers[key] = value;
});
request.body = jsonEncode(body);
// [NOTE] 64KiB以上のデータを送信するためにfalseを設定する!
// これがないとfetch errorで死ぬほど悩むことになります
request.persistentConnection = false;
var stream = getStream(request);
stream.asStream().listen((event) {
event
.transform(const Utf8Decoder())
.transform(const LineSplitter())
.listen(
(dataLine) {
// ストリーム中身
print(dataLine);
})
});
(参考)FastAPI サーバーサイド処理
OpenAIのAPIにstream=True
を指定してリクエストを投げ、その中身を返しているだけです。
プロンプト・base64エンコードした画像の文字列を投げ、レスポンスストリームを返しています。
@app.post("/getTopicStream/")
async def get_topic_stream(item:GetTopicRequestItem) -> EventSourceResponse:
# validate the input
if item.prompt == "":
raise HTTPException(status_code=400, detail="prompt is required")
if item.dry_run:
return EventSourceResponse(get_topic_stream_test())
else:
def get_stream():
try:
print("------------------")
print("start")
response = chatter.chat_stream(memory_id=item.apikey,message=item.prompt,base64_str=item.picture_base64)
for chunk in response:
if chunk is not None:
content = chunk.choices[0].delta.content
data = {"content":f"{content}"}
if content is not None:
yield content
yield f"\n\n\n(使用モデル : {chatter.model})"
print("end")
print("------------------")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return EventSourceResponse(get_stream())
ついでの注意点
fetch_client
を使う場合、64KiB以上のデータを送信するためにはrequest.persistentConnection=false
としなければなりません!
ドキュメントにも書いてありますが、見落としました。
デカい画像をアップロードしたときにエラーになってしまい、かなりハマってしまいました。
背景(読まなくてよい)
覚えたてのFlutterを使って、イケてるWEBサイト上でAIとおしゃべりしたいと思っていた時にハマりました。
ストリーミング応答するAPIを作るのも初めてだったため、Flutter・APIのどちらに問題があるかで切り分けに時間がかかったのも原因の1つです。
FlutterはWEBとWEB以外のプラットフォームでは動作が異なる可能性が多分にあるので気を付けましょう。
上記情報で皆さんの3日間が救われることを願います。
Discussion