ReactNativeでGPTからのレスポンスをストリーミングで受け取る方法
ストリーミングを正式にサポートしているライブラリや方法がない...
メジャーな二つの方法はどちらも使えませんでした。
- Axiosはクライアント側でのストリーミング形式をサポートしていない
- fetchだと可能だが、出来るのはWebだけでReactNativeでは出来ない模様(普通のReact.jsと使われているJSのエンジンの違いによるものとのこと)
実際に試してみましたがエラーになりました。
他に方法はないのか探してみたところ一つだけありました。
react-native-communityから出しているreact-native-fetch-api
を使うことでストリーミングで取得ができるようです。
しかし3年以上更新されていない(今後もメンテはなさそう)ため極力使いたくないところですが、これしか方法がありません....
UI実装
1. 環境構築
プロジェクト作成
npx react-native init <your project name> --template react-native-template-typescript
プロジェクトフォルダに移動
cd <your project name>
アプリ起動
npx react-native run-ios
以下のようにアプリが起動されればOK
2. 見た目を作成
送るメッセージを入力するためのTextFormとチャット一覧を表示するUIを作ります。
その前に、メッセージの識別のために一意のidを作れるようにしたいのでuuidを導入します。
yarn add react-native-uuid
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/
import React, {useState} from 'react';
import {
KeyboardAvoidingView,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
useColorScheme,
View,
} from 'react-native';
import {Colors} from 'react-native/Libraries/NewAppScreen';
import uuid from 'react-native-uuid';
type ChatType = {
id: string;
role: 'ai' | 'user';
message: string;
};
function ChatView({role, message}: Omit<ChatType, 'id'>): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
return (
<View style={styles.sectionContainer}>
<Text
style={[
{
color: isDarkMode ? Colors.white : Colors.black,
},
]}>
{role}
</Text>
<Text
style={[
{
color: isDarkMode ? Colors.light : Colors.dark,
},
]}>
{message}
</Text>
</View>
);
}
function App(): React.JSX.Element {
const [inputText, setInputText] = useState<string>('');
const [chatList, setChatList] = useState<Array<ChatType>>([]);
const handleChangeInputText = (text: string) => {
setInputText(text);
};
const handleAddUserChat = () => {
const newChatList: Array<ChatType> = [
...chatList,
{id: uuid.v4().toString(), role: 'user', message: inputText},
];
setChatList(newChatList);
setInputText('');
};
return (
<SafeAreaView style={{flex: 1}}>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.sectionContainer}>
<View>
{chatList.map((e: ChatType) => (
<ChatView key={e.id} role={e.role} message={e.message} />
))}
</View>
</ScrollView>
<View style={styles.inputContainer}>
<TextInput
value={inputText}
onChangeText={handleChangeInputText}
onSubmitEditing={handleAddUserChat}
style={styles.textInput}
autoFocus={true}
placeholder="メッセージ"
returnKeyType="send"
/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
backgroundColor: '#E6ECF8',
},
inputContainer: {
height: 40,
margin: 20,
},
textInput: {
height: '100%',
padding: 10,
borderWidth: 1,
borderRadius: 20,
borderColor: '#878787',
},
});
export default App;
TextInputから文字を入力してEnter Keyを押した時にチャットに追加されたらOK
3. UIを一工夫
キーボードはアプリを立ち上げたら直ぐに入力できる状態にしたいです。
・ KeyboardAvoidngViewを使って、TextInputのコンポーネントを囲います。
・ TextInputコンポーネント内でautoFocus
をtrueにします。
...
<KeyboardAvoidingView behavior="padding">
<View style={styles.inputContainer}>
<TextInput
value={inputText}
onChangeText={handleChangeInputText}
onSubmitEditing={handleAddUserChat}
style={styles.textInput}
autoFocus={true}
placeholder="メッセージ"
returnKeyType="send"
/>
</View>
</KeyboardAvoidingView>
...
以下のようにアプリを立ち上げたタイミングでキーボードが表示されたらOK
ストリーミング処理の実装
1. 必要なパッケージを導入
fetchを使ってストリーミングで受け取れるようにするために、コミュニティから提供されている以下のパッケージをインストール
yarn add react-native-fetch-api
次にもともとReact Nativeがサポートしていない機能を使えるようにするために以下のパッケージをインストール
yarn addreact-native-polyfill-globals
依存関係があるパッケージも追加
yarn add text-encoding@^0.7.0 url-parser@^0.0.1 web-streams-polyfill@^3.3.3
2. polyfillを追加
index.js内に追記します。
require('react-native-polyfill-globals/src/fetch').polyfill();
require('react-native-polyfill-globals/src/encoding').polyfill();
require('react-native-polyfill-globals/src/readable-stream').polyfill();
AppRegistry.registerComponent(appName, () => App);
3. 追加の型定義
導入したパッケージはTypeScriptに対応してないため、このまま使うとエラーになってしまいます。
自分たちで型定義する必要があります。
// Decoder
type BufferSource = ArrayBufferView | ArrayBuffer;
interface TextDecoderCommon {
readonly encoding: string;
readonly fatal: boolean;
readonly ignoreBOM: boolean;
}
interface TextDecodeOptions {
stream?: boolean;
}
interface TextDecoder extends TextDecoderCommon {
decode(input?: BufferSource, options?: TextDecodeOptions): string;
}
declare const TextDecoder: {
prototype: TextDecoder;
new (label?: string, options?: TextDecoderOptions): TextDecoder;
};
// Fetch
interface RequestInit {
reactNative?: Partial<{textStreaming: boolean}>;
}
4. APIリクエスト処理を実装
APIにリクエストを送る関数を実装します。
APIは以下の記事で作ったFastAPI製のものを使います。
こちらではマルチモーダルに対応するためにmultipart形式でのリクエスト方法にしていますので注意してください。
import {TextDecoder} from 'text-encoding';
import {ReadableStream} from 'web-streams-polyfill';
...
const handleSubmitMessageToApi = async () => {
// ストリーミングで文字を受け取る際に検索で必要になるので定数で持っておく
const newAIChatId = uuid.v4().toString();
const newChatList: Array<ChatType> = [
...chatList,
{id: uuid.v4().toString(), role: 'user', message: inputText},
{id: newAIChatId, role: 'ai', message: ''},
];
setChatList(newChatList);
const endpoint = '<your endpoint>';
const headers = {
'Content-Type': 'multipart/form-data',
accept: 'application/json',
};
// FormData形式でリクエストボディを作成
const requestBody = new FormData();
requestBody.append('message', JSON.stringify(inputText));
const response = await fetch(endpoint, {
method: 'POST',
headers: headers,
body: requestBody,
// 本来は存在しないkeyだがパッケージを入れることで利用可能に
reactNative: {textStreaming: true},
});
setInputText('');
const stream = (await response.body) as ReadableStream<Uint8Array> | null;
if (stream) {
const reader = stream.getReader();
// doneになるまでループ処理で応答テキストを更新する
while (true) {
const {done, value} = await reader.read();
const decoder = new TextDecoder('utf-8');
if (done) {
break;
}
// 新しいテキストを受け取り次第、chatIdを検索して対象のmessageの値を更新する
setChatList(prev =>
prev.map(d => {
if (d.id === newAIChatId) {
const newMessage = d.message + decoder.decode(value);
return {
...d,
message: newMessage,
};
}
return d;
}),
);
}
}
};
動作確認
ストリーミングで返ってきました!
GPTを使ったアプリの開発をする際は注意
React Nativeで開発する場合はどうしたらいいんですかね...
正直今後のことを考えてもこのパッケージを使うのは良くないかなと思いますので、通常のfetchでサポートして欲しいのですが。
個人的には別の言語で作った方がいいかなと思いました。
Flutterだとストリーミングがサポートされているので、Flutterを使う。
もしくはSwiftとかKotlinといったネイティブ言語で開発をした方が安全かなと。
Discussion