Front-End Web & Mobile

Build a Travel Planner with React Native, AWS Amplify, and Amazon Bedrock Knowledge Base

Cover image with image of phone

With the announcement of the Amplify AI kit, we learned how to build custom UI components, conversation history and add external data to the conversation flow. In this blog post, we will learn how to build a travel planner application using React Native. The application will generate responses using Retrieval Augmented Generation (RAG) and Large Language Models (LLMs) based on knowledge bases.

To equip large language models (LLMs) with up-to-date and proprietary information, you can use RAG, a technique that fetches data from company data sources and enriches the prompt to provide more relevant and accurate responses. With Amazon Bedrock Knowledge Bases, you can give FMs and agents contextual information from your company’s private data sources for RAG to deliver more relevant, accurate, and customized responses.

You can use the backend building and creating Knowledge Bases and RAG parts of this article with any web framework. However, this tutorial assumes you are building your applications with React Native and will explain the frontend code accordingly.

Building an Amplify App

For creating an Amplify application, we have to run the create-amplify command at the root folder of your application:

npm create amplify@latest -y

This will install the necessary dependencies for our project on the AWS Amplify. If we open our project now on our IDE, we can see a new folder called amplify:

Structure of the Amplify project with auth and data.

The folder has a simple to-do application with email authentication. We will define the resources for the related categories under its own folders. E.g. For authentication we will update the auth/resource.ts file.

Let’s add the authentication flow for our users to sign up for a personalized experience. First open the auth/resource.ts file and update it like the following:

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,
    },
  },
});

This will customize the user confirmation email and ask users to create a username for registration. Now we will do the initial deployment through an Amplify sandbox. You can use a personal cloud sandbox environment that provides an isolated development space to rapidly build, test, and iterate on a fullstack app. Each developer on your team can use their own disposable sandbox environment connected to cloud resources. Let’s do our first deployment. Before we do that, update the backend.ts with the following:

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,
});

Commenting out the data resource it will prevent it from deploying for now. Run the following command to start a sandbox environment for auth:

npx ampx sandbox

Next step is to implement the frontend. For this, we will be using Amplify UI Components. Amplify UI is a collection of accessible, themeable, performant components that can connect directly to the cloud. With a few lines of code, we can turn complicated tasks to trivial tasks.

First install the necessary libraries for using the UI libraries.

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

The force flag is added due to the conflict of the ui library with latest version of react native.

Install the pods for iOS to bind the libraries to native libraries.

npx pod-install

After that update the App component in the App.tsx with the following:

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>
  );
}

Here are the major changes that happened:

  • Through the fetchUserAttributes we are getting the extra attributes we defined.
  • With the useAuthenticator hook, we are calling the signOut button.
  • Authenticator and Authenticator.Provider components will create the UI for authentication and control the authentication flow.

Now the authentication flow is ready to be tested. The next step is to implement the AI capabilities.

Amplify’s new AI capabilities make it easier to work with generative AI. For example, if we want to generate our components, let’s use the generation capability to see what it would look like. Open the data/resource.ts file and update it with the following:

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",
  },
});

Thanks to this, we can generate an answer for our questions. If we open our App.tsx file and call the generateDestination like the following from anywhere we can now generate a destination:

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

Creating a Knowledge Base

The information is highly depending on the strength of our prompt. Also the information is around the AI Model and it’s properties. However, Amazon Bedrock Knowledge Bases can help us give more information to our prompt.

We will create a basic knowledge base like the following:

City Country Population Description Financial Hub Is Capital? Ranking
Tokyo Japan 9.73 Million Tokyo is the capital of Japan. It blends ultramodern skyscrapers with historic temples and gardens. Known for its unique pop culture, advanced technology, and exquisite cuisine. Home to the world’s largest fish market and busiest pedestrian crossing. Yes Yes 1
Istanbul Turkey 15.46 Million Istanbul straddles Europe and Asia across the Bosphorus Strait. Rich in history, it showcases Byzantine and Ottoman architecture. Famous for its bazaars, Turkish baths, and diverse culinary scene. The Hagia Sophia and Blue Mosque are iconic landmarks. Yes No 5
Berlin Germany 3.7 Million Berlin is Germany’s capital, known for its vibrant arts scene, modern architecture, and complex history. Home to world-class museums, diverse neighborhoods, and remnants of the Berlin Wall. Famous for its techno clubs, street art, and multicultural cuisine. No Yes 2
New York USA 8.8 Million New York City is a global hub for finance, arts, and culture. Home to iconic landmarks like the Statue of Liberty and Empire State Building. Known for its diverse neighborhoods, Broadway theaters, world-class museums, and culinary diversity. Rich in history from colonial times to present. Yes No 4
Prague Czech Republic 1.3 Million Prague is the capital of the Czech Republic, known as “The City of a Hundred Spires.” Famous for its medieval architecture, including Prague Castle and Charles Bridge. Renowned for its beer culture, classical music heritage, and well-preserved Old Town Square. No Yes 3

Go to Amazon S3 Console now and click the Create bucket button:

Creating an S3 bucket from console

Select a unique name for our bucket and leave all as default selection. Next upload our file to our S3 bucket:

Uploaded file is indicated through S3

Now we can create our knowledge bases. First, open the AWS Console and go to Amazon Bedrock page. Once we land on the page, select the Knowledge bases from the left menu.

Create Knowledge Base button over AWS Console

Click on the Create knowledge base button. Leave all of the default values (double check that S3 is selected) and click on next. In the next page, select the data source from our S3 buckets:

Selecting the correct S3 bucket

Select an embedding model to convert our data:

List of embedding models

We will also let Amazon create a vector store on our behalf or select a previously created store to allow Bedrock to store, update and manage embeddings of our data. Now we can start creating our knowledge base.

The default setup for an Amazon Bedrock Knowledge Base is OpenSearch Serverless which has a default cost whether or not you use it. You can get an AWS bill if you are not careful. If you are just testing this out make sure to turn off the OpenSearch Serverless instance when you are done.

We can test our knowledge base already in the console and see if the data is working as expected.

result of the knowledge base

Now it is time to use the knowledge base in our application.

Using the Created Knowledge Base

First let’s do some clean-up. We have to create our knowledge base aware conversation and connect that to a AppSync Resolver to communicate with the database. Update the data/resource.ts file with the following:

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<typeof schema>;

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

This will add the earlier created knowledge base to the conversation as tool. The description will be the explanatory text for LLM to interact with the knowledge base. For adding a js resolver, create a file called bedrockresolver.js and paste the following:

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);
}

This will get the knowledge base with the provided id to the conversation’s context through AppSync. Lastly, we have to update the policies from lambda to data source in the backend.ts file:

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"],
  })
);

Lastly update your UI like the following to handle the streaming:

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",
  },
});

The most important part of the code above is the following:

  • const { useAIConversation } = createAIHooks(client);
    • Creates a react hook to fetch conversation information, listen to messages and send messages to your conversation
  • const [
      {
        data: { messages },
        isLoading,
      },
      handleSendMessage,
    ] = useAIConversation("chat");
    • Listens to the messages and sends messages

Overall the app will use the retrieved information and show it on the screen as it is retrieved real-time.

Now if you deploy your sandbox again, you will see that your application will call the knowledge base with the provided information. Now run the application and see how it looks:

Cleanup

In this blog post you have learned how you can call an LLM through an Amazon Bedrock Knowledge Base. Before wrapping things up, make sure you delete your resources in the Amplify sandbox by running:

npx ampx sandbox delete -y

Also, the vector databases can be expensive, once you are done testing the application, make sure to delete your instances in the Amazon OpenSearch Serverless dashboard.

Wrapping Up

In this blog post, you learned how you can create your knowledge base and use it with your application. If you would like to learn more take a look at our starting guide for AI. In it we go over in detail how to get started with Amplify AI kit.