Fusic Tech Blog

Fusicエンジニアによる技術ブログ

Expo + react-native-vercel-ai + Next.jsでAIチャットアプリを作る
2024/02/19

Expo + react-native-vercel-ai + Next.jsでAIチャットアプリを作る

はじめに

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チャットアプリができました。

AI chat app demo

おわりに

react-native-vercel-aiとVercel AI SDKを使うことで少ないコードで簡単にAIチャットアプリを作ることができました。

ただ、React Nativeのfetch APIがストリーム対応できていないのはちょっと残念なので、どうにか実現できる方法を探っていきたいです。

Daiki Urata

Daiki Urata

フロントエンド/モバイルアプリなどを主に開発しています。