 Daiki Urata
 Daiki Urata Table of Contents
はじめに
react-native-vercel-ai というReact Native版のVercel AI SDK (非公式)を試してみました。
公式のVercel AI SDKのようにuseChatというカスタムフックが用意されていて、簡単にAIチャットアプリが作れます。
技術スタック
- Expo SDK 49
- React Native 0.72.6
- react-native-vercel-ai 0.1.0
- Next.js 13.5.6
- Vercel AI SDK 2.2.19
- OpenAI Node API Library 4.14.0
今回はOpenAI APIを呼び出すバックエンド側の構築も必要なのでNext.jsも使用しています。
バックエンド(Next.js)側の実装
Next.jsプロジェクト作成
まずはバックエンド側を実装していきます。
$ npx create-next-app@latest
ライブラリのインストール
OpenAI APIを使ってAIチャットアプリを作るため、OpenAI Node API LibraryとVercel AI SDKもインストールします。
$ npm install openai ai
APIエンドポイントの作成
基本的にreact-native-vercel-aiのREADME そのままで動きました。
// /api/chat
// ./app/api/chat/route.ts
import OpenAI from 'openai';
import { OpenAIStream, StreamingTextResponse } from 'ai';
import { NextResponse, userAgent } from 'next/server';
// Create an OpenAI API client (that's edge friendly!)
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY || '',
});
// IMPORTANT! Set the runtime to edge
export const runtime = 'edge';
export async function POST(req: Request, res: Response) {
  // Extract the `prompt` from the body of the request
  const { messages } = await req.json();
  const userAgentData = userAgent(req);
  // FIX: Seems not to work on Android
  const isNativeMobile = userAgentData.ua?.includes('Expo');
  if (!isNativeMobile) {
    console.log('Not native mobile');
    // Ask OpenAI for a streaming chat completion given the prompt
    const response = await openai.chat.completions.create({
      model: 'gpt-3.5-turbo',
      stream: true,
      messages,
    });
    // Convert the response into a friendly text-stream
    const stream = OpenAIStream(response);
    // Respond with the stream
    return new StreamingTextResponse(stream);
  } else {
    console.log('Native mobile');
    // Ask OpenAI for a streaming chat completion given the prompt
    const response = await openai.chat.completions.create({
      model: 'gpt-3.5-turbo',
      // Set your provider stream option to be `false` for native
      stream: false,
      messages,
    });
    return NextResponse.json({ data: response.choices[0].message });
  }
}
内容としてはユーザーエージェントでネイティブのモバイルアプリなのかWebアプリなのか判定して、Webアプリならストリームでレスポンスして、ネイティブモバイルアプリならJSONでOpenAI APIからの結果をレスポンスとして返すようにしています。
レスポンスを分けている理由としては、現段階ではReact Nativeのfetch APIではストリームに対応していないため、分けているのだそうです。
ただ、Androidで試したところ今のユーザーエージェントで判定するロジックではうまくネイティブのモバイルアプリだと判定されなかったので、改良が必要そうです。
// Androidではうまく判定されなかった...
const isNativeMobile = userAgentData.ua?.includes('Expo');
起動
準備ができたので、あとは起動してExpoアプリでエンドポイント /api/chat  に対してリクエストすればOKです。
$ npm run dev
Expo/React Native側の実装
Expoプロジェクト作成
次にExpoプロジェクトを作成します。
TypeScriptのBlankテンプレートを使用します。
$ npx create-expo-app -t expo-template-blank-typescript
ライブラリのインストール
react-native-vercel-ai をインストールします。
$ npm install react-native-vercel-ai
AIチャット画面の作成
こちらもREADME のサンプルコードをちょっと改良してチャット画面を作成してみました。
// App.tsx
import { StatusBar } from "expo-status-bar";
import {
  Button,
  Platform,
  ScrollView,
  StyleSheet,
  Text,
  TextInput,
  View,
} from "react-native";
import { useChat } from "react-native-vercel-ai";
import Constants from "expo-constants";
const uri = Constants.expoConfig?.hostUri?.split(":")?.shift();
const localEndpoint = `http://${uri}:3000/api/chat`;
const api = process.env.EXPO_PUBLIC_API_URL
  ? `${process.env.EXPO_PUBLIC_API_URL}/api/chat`
  : localEndpoint;
export default function App() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({
      api,
    });
  return (
    <View style={styles.container}>
      <ScrollView>
        {messages.length > 0
          ? messages.map((m) => (
              <Text
                style={{
                  marginBottom: 10,
                  borderWidth: 1,
                  paddingVertical: 10,
                  paddingHorizontal: 20,
                }}
                key={m.id}
              >
                {m.role === "user" ? "🧔 User: " : "🤖 AI: "}
                {m.content}
              </Text>
            ))
          : null}
        {isLoading && Platform.OS !== "web" && (
          <View>
            <Text>Loading...</Text>
          </View>
        )}
      </ScrollView>
      <View style={{ height: 240, width: "100%" }}>
        <View style={{ marginTop: 60 }}>
          <TextInput
            value={input}
            placeholder="Say something..."
            style={{
              borderWidth: 1,
              minHeight: 50,
              padding: 10,
              marginBottom: 10,
            }}
            onChangeText={(e) => {
              handleInputChange(
                Platform.OS === "web" ? { target: { value: e } } : e
              );
            }}
          />
          <View
            style={{
              backgroundColor: "#f2f2f2f2",
              borderWidth: 1,
              borderColor: "black",
            }}
          >
            <Button onPress={handleSubmit} title="Send" />
          </View>
        </View>
      </View>
      <StatusBar style="auto" />
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
    paddingHorizontal: 20,
    paddingTop: 60,
  },
});
起動
Expo側も起動して、Simulatorや実機(Expo Go)で確認してみます。
$ npm run start
実行結果
ストリームに対応していないのでChatGPTのような動きはできないですが、無事にAIチャットアプリができました。

おわりに
react-native-vercel-aiとVercel AI SDKを使うことで少ないコードで簡単にAIチャットアプリを作ることができました。
ただ、React Nativeのfetch APIがストリーム対応できていないのはちょっと残念なので、どうにか実現できる方法を探っていきたいです。