Amazon Web Services ブログ

React Native と AWS Amplify、Amazon Bedrock Knowledge Base を利用したトラベルプランナーの構築

本記事は、2024 年 11 月 22 日に公開された Build a Travel Planner with React Native, AWS Amplify, and Amazon Bedrock Knowledge Base を翻訳したものです。翻訳は Solutions Architect の 吉村 が担当しました。

Cover image with image of phone

Amplify AI kit の発表により、カスタム UI コンポーネント会話の履歴会話の流れに外部データを追加する方法を学びました。この記事では、React Native を使用してトラベルプランアプリケーションを構築する方法を学びます。このアプリケーションは、 Knowledge Base に基づいて、Retrieval Augmented Generation (RAG) および Large Language Models (LLM) を使用して応答を生成します。

大規模言語モデル (LLM) に最新の独自情報を付与するには、RAG という手法を使用できます。これは、企業のデータソースからデータを取得し、プロンプトを強化することで、より関連性が高く正確な応答を提供できるようになります。Amazon Bedrock Knowledge Bases を使えば、会社のプライベートデータソースから FM とエージェントにコンテキスト情報を提供し、RAG がより関連性があり、正確で、カスタマイズされた応答を提供できます。

この記事のバックエンドの構築、Knowledge Base の作成、RAG の部分は、どのウェブフレームワークでも使用できます。 ただし、このチュートリアルでは React Native でアプリケーションを構築することを想定しており、それに応じてフロントエンドのコードを説明します。

Amplify アプリの構築

Amplify アプリケーションを作成するには、アプリケーションのルートフォルダで create-amplify コマンドを実行する必要があります。

npm create amplify@latest -y
Bash

これにより、AWS Amplify に対してプロジェクトに必要な依存関係がインストールされます。IDE でプロジェクトを開くと、新しい amplify フォルダが表示されます。

Structure of the Amplify project with auth and data.

このフォルダには、メール認証付きのシンプルな ToDo アプリケーションが含まれています。関連するカテゴリのリソースは、それぞれ専用のフォルダの下に定義されています。たとえば、認証については auth/resource.ts ファイルを更新します。

ユーザーにパーソナライズされた体験のためのサインアップ認証フローを追加しましょう。まず auth/resource.ts ファイルを開き、次のように更新します。

import { defineAuth } from "@aws-amplify/backend";

export const auth = defineAuth({
  loginWith: {
    email: {
      verificationEmailSubject: "Welcome to Travel Advisor! Verify your email!",
      verificationEmailBody: (code) => `Here is your verification code: ${code()}`,
      verificationEmailStyle: "CODE",
    },
  },
  userAttributes: {
    preferredUsername: {
      mutable: true,
      required: true,
    },
  },
});
TypeScript

これにより、ユーザー確認メールがカスタマイズされ、登録時にユーザー名の作成が求められます。次に、Amplify サンドボックスを介して、初期デプロイを行います。個人用のクラウドサンドボックス環境は、フルスタックアプリを迅速に構築、テストしていてレーションを回すことができる、分離された開発スペースを提供します。チームの各開発者は、クラウドリソースに接続された使い捨てのサンドボックス環境を使用できます。それでは最初のデプロイを行いましょう。その前に、backend.ts を次のように更新してください。

import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";

/**
  * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more
  */
defineBackend({
  auth,
});
TypeScript

データリソースをコメントアウトしておくことで、データリソースをデプロイしないでおくこともできます。次のコマンドを実行して、認証用のサンドボックス環境を起動します。

npx ampx sandbox
Bash

次のステップはフロントエンドの実装です。これには、Amplify UI コンポーネントを使用します。Amplify UI とは、アクセシビリティが高く、テーマ設定が可能で、高パフォーマンスなコンポーネントの集合体で、クラウドに直接接続することができます。わずか数行のコードで、複雑なタスクを些細なタスクに変えることができます。

まず、UI ライブラリを使うために必要なライブラリをインストールします。

npm install --force @aws-amplify/ui-react-native aws-amplify @aws-amplify/react-native react-native-safe-area-context @react-native-community/netinfo @react-native-async-storage/async-storage react-native-get-random-values
Bash

React Native の最新バージョンと UI ライブラリが競合するため、forceフラグを追加しています。

iOS 向けに Pod をインストールして、ネイティブライブラリにライブラリをバインドします。

npx pod-install
Bash

次に、App.tsx 内の App コンポーネントを次のように更新してください:

import outputs from "./amplify_outputs.json";
Amplify.configure(outputs);

const SignOutButton = () => {
  const { signOut } = useAuthenticator();
  return (
    <TouchableOpacity onPress={signOut}>
      <MaterialIcons name="exit-to-app" size={32} />
    </TouchableOpacity>
  );
};

export default function App() {
  const [username, setUsername] = useState("");
  useEffect(() => {
    const fetchUsername = async () => {
      const attributes = await fetchUserAttributes();
      const username = attributes.preferred_username ?? "";
      setUsername(username);
    };
    fetchUsername();
  }, []);
  return (
    <Authenticator.Provider>
      <Authenticator>
        <SafeAreaView style={styles.container}>
          <KeyboardAvoidingView behavior={"height"} style={styles.container}>
            <View style={styles.header}>
              <Text style={styles.headerIcon}>✈️</Text>
              <Text style={styles.headerText}>Travel Advisor</Text>
              <SignOutButton />
            </View>
          </KeyboardAvoidingView>
        </SafeAreaView>
      </Authenticator>
    </Authenticator.Provider>
  );
}
TypeScript

以下は主な変更点です。

  • fetchUserAttributes を介して、定義した追加属性を取得しています。
  • useAuthenticator フックを使用して、signOut ボタンを呼び出しています。
  • Authenticator コンポーネントと Authenticator.Provider コンポーネントで、認証用の UI を作成し、認証フローを制御します。

認証フローはテストする準備ができました。次のステップは AI 機能を実装することです。

Amplify の新しい AI 機能により、生成 AI を使いやすくなりました。たとえばコンポーネントを生成する場合は、生成機能を使ってみると、どのようになるかを確認できます。data/resource.ts ファイルを開き、次のように更新してください:

import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  generateDestination: a
    .generation({
      aiModel: a.ai.model("Claude 3.5 Sonnet"),
      systemPrompt: `
You are an advanced travel assistant with comprehensive knowledge about global destinations, including their geography, climate patterns, historical significance, tourist attractions, and cost of living. Your task is to analyze the following information: {{input}}
Based solely on this input, determine the single best city on Earth for the user. Your response should be concise and definitive, presenting only the chosen city along with comprehensive information about their geography, climate patterns, historical significance, tourist attractions, and cost of living and why it's the ideal match. Do not ask for additional information or clarification. Provide your recommendation based exclusively on the given details.
      `,
    })
    .arguments({
      input: a.string().required(),
    })
    .returns(a.string().required())
    .authorization((allow) => [allow.authenticated()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
  },
});
TypeScript

このおかげで、質問に対する回答を生成できます。App.tsx ファイルを開き、どこからでも以下のように generateDestination を呼び出せば、目的地を生成できるようになりました。

const { data, errors } = await client.generations.generateDestination({
    input: inputText,
});
TypeScript

Knowledge Base の作成

(LLM に与える) 情報は、プロンプトの強さに大きく左右されます。また、情報は AI モデルとその特性に関するものです。しかし、Amazon Bedrock Knowledge Base を使えば、プロンプトにさらに情報を与えることができます。

以下のような基本的な Knowledge Base を作成します。

都市 人口 説明 金融ハブ 首都? ランキング
東京 日本 973 万人 東京は日本の首都です。超近代的な高層ビルと歴史的な寺院と庭園が共存しています。独自のポップカルチャー、先進的な技術、そして絶品の料理で知られています。世界最大の魚市場と最も混雑した横断歩道があります。 はい はい 1
イスタンブール トルコ 1,546 万人 イスタンブールはボスポラス海峡に跨っており、ヨーロッパとアジアの境目に位置しています。歴史に富み、ビザンティンとオスマンの建築物が見られます。バザール、ハンマーム、様々な料理で有名です。アヤソフィアとブルーモスクは象徴的な建造物です。 はい いいえ 5
ベルリン ドイツ 370 万人 ベルリンはドイツの首都で、活気ある芸術シーンと現代的な建築物、複雑な歴史で知られています。世界トップクラスの博物館、多様な地区、ベルリンの壁の残骸があります。テクノクラブ、ストリートアート、多文化な料理で有名です。 いいえ はい 2
ニューヨーク アメリカ 880 万人 ニューヨーク市は金融、芸術、文化の世界的ハブです。自由の女神像やエンパイアステートビルなど有名な観光スポットがあります。多様な地区、ブロードウェイの劇場、世界トップクラスの博物館、食の多様性で知られています。植民地時代から現代までの豊かな歴史があります。 はい いいえ 4
プラハ チェコ共和国 130 万人 プラハはチェコ共和国の首都で、「百塔の都市」として知られています。プラハ城やカレル橋など中世の建築物で有名です。ビール文化、クラシック音楽の遺産、よく保存された旧市街広場で名高いです。 いいえ はい 3

Amazon S3 コンソールに移動し、Create bucket ボタンをクリックします。

Creating an S3 bucket from console

バケットに一意の名前を選び、他はデフォルトの選択のままにします。次に、ファイルを S3 バケットにアップロードします。

Uploaded file is indicated through S3

それでは Knowledge Base を作成しましょう。まず AWS コンソールを開き、Amazon Bedrock ページへ移動します。ページにアクセスしたら、左側のメニューから Knowledge bases を選択してください。

Create Knowledge Base button over AWS Console

Create knowledge base ボタンをクリックしてください。すべてのデフォルト値のままにして (S3 が選択されていることを確認) 次へをクリックします。次のページで、S3 バケットからデータソースを選択してください。

Selecting the correct S3 bucket

データを変換するための埋め込みモデルを選択してください。

List of embedding models

お客様に代わって Amazon が Vector Store を作成するか、以前作成したストアを選択して、Bedrock がデータの埋め込みを格納、更新、管理できるようにします。これで Knowledge Base を作成する準備が整いました。

Amazon Bedrock Knowledge Base のデフォルトのセットアップは OpenSearch Serverless で、これは使っていない間もデフォルトのコストがかかります。注意していないと AWS から請求が来るかもしれません。もしこれをテストするだけなら、終了後に必ず OpenSearch Serverless インスタンスをオフにしてください。

既にコンソール上で Knowledge Base をテストし、データが期待通りに動作するかどうかを確認できます。

result of the knowledge base

Knowledge Base をアプリケーションで使用する時が来ました。

作成した Knowledge Base の利用

まず、いくつか片付けをしましょう。 Knowledge Base に対応した会話を作成し、そのデータベースと通信するための AppSync リゾルバーに接続する必要があります。data/resource.ts ファイルを次のように更新してください。

import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  chat: a 
    .conversation({
      aiModel: a.ai.model("Claude 3 Haiku"),
      systemPrompt: `You are a helpful assistant.`,
      tools: [ 
        a.ai.dataTool({
          name: "DestinationKnowledgeBase",
          description:
            "A knowledge base to be checked about everything related to the cities.",
          query: a.ref("searchDestination"),
        }),
      ],
    })
    .authorization((allow) => allow.owner()),
  searchDestination: a 
    .query()
    .arguments({ input: a.string() })
    .handler(
      a.handler.custom({
        dataSource: "DestinationKnowledgeBaseDataSource",
        entry: "./bedrockresolver.js",
      })
    )
    .returns(a.string())
    .authorization((allow) => [allow.authenticated()]),
});

export type Schema = ClientSchema;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
  },
});
TypeScript

これにより、先ほど作成した Knowledge Base がツールとして conversation に追加されます。description は、LLM が Knowledge Base とやり取りするための説明テキストになります。js リゾルバーを追加するには、bedrockresolver.js というファイルを作成し、次のコードを貼り付けてください。

export function request(ctx) {
  const { input } = ctx.args;
  return {
    resourcePath: "/knowledgebases/<knowledge-base-id>/retrieve",
    method: "POST",
    params: {
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        retrievalQuery: {
          text: input,
        },
      }),
    },
  };
}

export function response(ctx) {
  return JSON.stringify(ctx.result.body);
}
JavaScript

これにより、AppSync を通じて会話のコンテキストに指定した ID の Knowledge Base が取得されます。最後に、backend.ts ファイルの Lambda からデータソースへのポリシーを更新する必要があります。

import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";
import * as iam from "aws-cdk-lib/aws-iam";

const backend = defineBackend({
  auth,
  data,
});

const KnowledgeBaseDataSource =
  backend.data.resources.graphqlApi.addHttpDataSource(
    "DestinationKnowledgeBaseDataSource",
    "https://bedrock-agent-runtime.<region>.amazonaws.com",
    {
      authorizationConfig: {
        signingRegion: "<region>",
        signingServiceName: "bedrock",
      },
    }
  );

KnowledgeBaseDataSource.grantPrincipal.addToPrincipalPolicy(
  new iam.PolicyStatement({
    resources: [
      "arn:aws:bedrock:<region>:<user-id>:knowledge-base/<knowledge-base-id>",
    ],
    actions: ["bedrock:Retrieve"],
  })
);
TypeScript

最後に、UI を次のように更新してストリーミングを処理します。

import React, { useEffect, useState } from "react";
import {
  StyleSheet,
  Text,
  View,
  TextInput,
  TouchableOpacity,
  SafeAreaView,
  KeyboardAvoidingView,
  FlatList,
  ActivityIndicator,
} from "react-native";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { Authenticator, useAuthenticator } from "@aws-amplify/ui-react-native";
import { fetchUserAttributes } from "aws-amplify/auth";
import { Amplify } from "aws-amplify";
import { Schema } from "./amplify/data/resource";
import { generateClient } from "aws-amplify/data";

import outputs from "./amplify_outputs.json";
import { createAIHooks } from "@aws-amplify/ui-react-ai";

Amplify.configure(outputs);
const client = generateClient<Schema>();
const { useAIConversation } = createAIHooks(client);

const HomePage = () => {
  const [username, setUsername] = useState("");
  const [inputText, setInputText] = useState("");

  const [
    {
      data: { messages },
      isLoading,
    },
    handleSendMessage,
  ] = useAIConversation("chat");

  const handleSend = () => {
    handleSendMessage({
      content: [{ text: inputText }],
    });
    setInputText("");
  };
  useEffect(() => {
    const fetchUsername = async () => {
      const attributes = await fetchUserAttributes();
      const fetchedUsername = attributes.preferred_username ?? "";
      setUsername(fetchedUsername);
    };
    fetchUsername();
  }, []);
  return (
    <SafeAreaView style={styles.container}>
      <KeyboardAvoidingView behavior={"height"} style={styles.container}>
        <View style={styles.header}>
          <Text style={styles.headerIcon}>✈️</Text>
          <Text style={styles.headerText}>Travel Advisor</Text>
          <TouchableOpacity
            onPress={() => {
              const { signOut } = useAuthenticator();
              signOut();
            }}
          >
            <MaterialIcons name="exit-to-app" size={32} />
          </TouchableOpacity>
        </View>
        <FlatList
          data={messages}
          keyExtractor={(message) => message.id}
          renderItem={({ item }) =>
            item.content
              .map((content) => content.text)
              .join("")
              .trim().length == 0 ? (
              <View style={styles.loadingContainer}>
                <ActivityIndicator />
              </View>
            ) : (
              <ChatMessage
                text={item.content
                  .map((content) => content.text)
                  .join("")
                  .trim()}
                isUser={item.role == "user"}
                userName={item.role == "user" ? username : "Travel Advisor"}
              />
            )
          }
          contentContainerStyle={styles.chatContainer}
          ListEmptyComponent={() => (
            <View style={styles.emptyContainer}>
              <Text style={styles.emptyText}>
                Start a conversation by sending a message below!
              </Text>
            </View>
          )}
        />
        <View style={styles.inputContainer}>
          <TextInput
            style={styles.input}
            value={inputText}
            onChangeText={setInputText}
            placeholder="Describe your dream travel..."
            multiline={true}
            numberOfLines={3}
          />
          <TouchableOpacity
            style={[styles.sendButton, isLoading && styles.sendButtonDisabled]}
            onPress={handleSend}
            disabled={isLoading}
          >
            <Text style={styles.sendButtonText}>Send</Text>
          </TouchableOpacity>
        </View>
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
};

interface Message {
  text: string;
  isUser: boolean;
  userName: string;
}

const ChatMessage = ({ text, isUser, userName }: Message) => (
  <View>
    <View
      style={[
        styles.messageBubble,
        isUser ? styles.userMessage : styles.aiMessage,
      ]}
    >
      <Text style={styles.messageText}>{text}</Text>
    </View>
    <Text style={[styles.nameText, isUser ? styles.userName : styles.aiName]}>
      {userName}
    </Text>
  </View>
);

export default function App() {
  return (
    <Authenticator.Provider>
      <Authenticator>
        <HomePage />
      </Authenticator>
    </Authenticator.Provider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#FFFFFF",
  },
  header: {
    flexDirection: "row",
    alignItems: "center",
    padding: 15,
    borderBottomWidth: 1,
    borderBottomColor: "#E0E0E0",
  },
  headerIcon: {
    fontSize: 24,
    marginRight: 10,
  },
  headerText: {
    fontSize: 20,
    fontWeight: "bold",
    flex: 1,
  },
  signOutIcon: {
    fontSize: 24,
  },
  chatContainer: {
    padding: 10,
  },
  messageBubble: {
    maxWidth: "80%",
    padding: 10,
    borderRadius: 20,
    marginBottom: 5,
  },
  aiMessage: {
    alignSelf: "flex-start",
    backgroundColor: "#F0F0F0",
  },
  userMessage: {
    alignSelf: "flex-end",
    backgroundColor: "#DCF8C6",
  },
  messageText: {
    fontSize: 16,
  },
  nameText: {
    fontSize: 12,
    marginBottom: 10,
  },
  userName: {
    alignSelf: "flex-end",
    color: "#4CAF50",
  },
  aiName: {
    alignSelf: "flex-start",
    color: "#666666",
  },
  inputContainer: {
    flexDirection: "row",
    padding: 10,
    borderTopWidth: 1,
    borderTopColor: "#E0E0E0",
  },
  input: {
    flex: 1,
    backgroundColor: "#F0F0F0",
    borderRadius: 20,
    paddingHorizontal: 15,
    paddingVertical: 10,
    fontSize: 16,
  },
  sendButton: {
    backgroundColor: "#4CAF50",
    paddingHorizontal: 12,
    borderRadius: 20,
    justifyContent: "center",
    alignItems: "center",
    marginLeft: 10,
  },
  sendButtonDisabled: {
    backgroundColor: "#A5D6A7",
  },
  sendButtonText: {
    color: "#FFFFFF",
    fontSize: 24,
  },
  loadingContainer: {
    alignSelf: "flex-start",
    marginBottom: 10,
  },
  loadingText: {
    fontSize: 24,
    color: "#666666",
  },
  emptyContainer: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    height: 500,
  },
  emptyText: {
    fontSize: 16,
    color: "#666666",
    textAlign: "center",
  },
});
TypeScript

上記のコードで最も重要な部分は以下の通りです。

  • const { useAIConversation } = createAIHooks(client);
    • 会話情報を取得し、メッセージを受信・送信するための React フックを作成します
  • const [
      {
        data: { messages },
        isLoading,
      },
      handleSendMessage,
    ] = useAIConversation("chat");
    TypeScript
    • メッセージを受信し、メッセージを送信します

全体として、このアプリは取得した情報をリアルタイムに取得して、画面に表示します。

サンドボックスを再デプロイすると、アプリケーションが提供された情報を使用して Knowledge Base を呼び出すことがわかります。それではアプリケーションを実行して、その様子を確認しましょう。

クリーンアップ

このブログ記事では、Amazon Bedrock Knowledge Base を通して LLM を呼び出す方法を学びました。最後に、次のコマンドを実行して Amplify サンドボックス内のリソースを削除することを忘れずに行ってください。

npx ampx sandbox delete -y
Bash

また、ベクトルデータベースは高価な場合があるので、アプリケーションのテストが終わったら、必ず Amazon OpenSearch Serverless ダッシュボードからインスタンスを削除してください。

おわりに

このブログ記事では、 Knowledge Base の作成方法とアプリケーションでの利用方法を学びました。さらに詳しく知りたい場合は、AI 入門ガイドをご覧ください。Amplify AI kit の使い方について詳しく説明しています。