Front-End Web & Mobile
Creating a Generative AI Travel Assistant App with Amazon Bedrock and AWS Amplify
In this post, we will walk you through creating a travel assistant app. The app will provide a personalized experience by suggesting popular attractions, local experiences, and hidden gems for the user’s desired destination. We will build this app using AWS Amplify and Amazon Bedrock.
AWS Amplify Gen 2 uses a TypeScript-based, code-first developer experience (DX) for defining backends. The Gen 2 DX offers a unified Amplify developer experience with hosting, backend, and UI-building capabilities and a code-first approach. Amplify empowers frontend developers to deploy cloud infrastructure by simply expressing their app’s data model, business logic, authentication, and authorization rules completely in TypeScript. Amplify automatically configures the correct cloud resources and removes the requirement to stitch together underlying AWS services.
Amazon Bedrock is a fully managed service offering high-performing foundation models (FMs) from leading AI companies. These FMs are accessible through a single Application Programming Interface (API), along with capabilities for building secure, private, and responsible generative AI applications. The single API access allows flexibility in using different FMs and upgrading to the latest model versions with minimal code changes.
Prerequisites
- An AWS account. Note that Amplify is part of the AWS Free Tier.
- Node.js v18.17 or later
- npm v9 or later
- git v2.14.1 or later
- A text editor, for this guide we will use VSCode, but you can use your preferred IDE.
- Access to the Claude 3 Haiku model on Amazon Bedrock
Cloning the repo
Step 1: Navigate to the repository on AWS Samples and fork it to your GitHub repositories
Step 2: Clone the app by running the command below in your terminal
git clone https://github.com/<YOUR_GITHUB>/travel-personal-assistant.git
Step 3: Access the newly cloned repository in VSCode by executing the commands below in your terminal.
cd travel-personal-assistant
code . -r
VSCode will open the repository folder, including the Amplify folder, which contains the backend details that we’ll discuss in the next section.
Step 4: Install the required packages including the Amplify packages by running the command below:
npm i
The Amplify Backend
In the final app (as seen in the GIF at the beginning of the post), users will initiate the conversation by submitting their travel questions (e.g., “I want to go to Ireland in December for 7 days”). The code is in the repository you cloned. Here, we’ll go over the key steps to connect your Amplify app with Amazon Bedrock.
In the repository, you’ll find an amplify folder containing direcotores for auth, data and functions resources
By default, the auth resource configured in the amplify/auth/resource.ts
file to use the email as a sign up mechanism for the users.
import { defineAuth } from '@aws-amplify/backend';
/**
* Define and configure your auth resource
* @see https://docs.amplify.aws/gen2/build-a-backend/auth
*/
export const auth = defineAuth({
loginWith: {
email: true,
},
});
In the amplify/data/resource.ts
file, we’ve defined a GraphQL query chat
that returns a string and expects JSON string argument conversation
that will include the entire conversation from our interaction with the travel assistant. Authorization is setup using .authorization((allow) => [allow.authenticated()])
to only allow authenticated users to access this query. The .handler(a.handler.function(personalAssistantFunction))
line sets up personalAssistantFunction as a a custom handler for this query
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
import { personalAssistantFunction } from "../functions/personal-assistant/resource";
const schema = a.schema({
chat: a
.query()
.arguments({
conversation: a.json().required(),
})
.returns(a.string())
.authorization((allow) => [allow.authenticated()])
.handler(a.handler.function(personalAssistantFunction)),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: "userPool",
},
});
The personalAssistantFunction
lambda function is defined in and exported from the file amplify/functions/personal-assistant/resource.ts
Here we also set an environment variable MODEL_ID
to Claude 3 Haiku
import { defineFunction } from "@aws-amplify/backend";
export const MODEL_ID = "anthropic.claude-3-haiku-20240307-v1:0";
export const personalAssistantFunction = defineFunction({
entry: "./handler.ts",
environment: {
MODEL_ID,
},
timeoutSeconds: 30,
runtime: 20,
});
The file amplify/functions/personal-assistant/handler.ts
contains the implementation of the personalAssistantFunction
handler. Here we initialize a Bedrock run time client and utilizes the query’s input parameters, i.e., conversation
and a systemPrompt
constant (to provide context, instructions, and guidelines to Amazon Bedrock on how to respond.), to generate a input object and use it to create a ConverseCommand
, then send it to Amazon Bedrock using the run time client. We verify the response for errors and return it as JSON string
import {
BedrockRuntimeClient,
ConverseCommandInput,
ConverseCommand,
} from "@aws-sdk/client-bedrock-runtime";
import type { Handler } from "aws-lambda";
// Constants
const AWS_REGION = process.env.AWS_REGION;
const MODEL_ID = process.env.MODEL_ID;
// Configuration
const INFERENCE_CONFIG = {
maxTokens: 1000,
temperature: 0.5,
};
// Initialize Bedrock Runtime Client
const client = new BedrockRuntimeClient({ region: AWS_REGION });
export const handler: Handler = async (event) => {
const { conversation } = event.arguments;
const SYSTEM_PROMPT = `
To create a personalized travel planning experience, greet users warmly and inquire about their travel preferences
such as destination, dates, budget, and interests. Based on their input, suggest tailored itineraries that include
popular attractions, local experiences, and hidden gems, along with accommodation options across various price
ranges and styles. Provide transportation recommendations, including flights and car rentals, along with estimated
costs and travel times. Recommend dining experiences that align with dietary needs, and share insights on local
customs, necessary travel documents, and packing essentials. Highlight the importance of travel insurance, offer
real-time updates on weather and events, and allow users to save and modify their itineraries. Additionally,
provide a budget tracking feature and the option to book flights and accommodations directly or through trusted
platforms, all while maintaining a warm and approachable tone to enhance the excitement of trip planning.
`;
const input = {
modelId: MODEL_ID,
system: [{ text: SYSTEM_PROMPT }],
messages: conversation,
inferenceConfig: INFERENCE_CONFIG,
} as ConverseCommandInput;
try {
const command = new ConverseCommand(input);
const response = await client.send(command);
if (!response.output?.message) {
throw new Error("No message in the response output");
}
return JSON.stringify(response.output.message);
} catch (error) {
console.error("Error in chat handler:", error);
throw error; // Re-throw to be handled by AWS Lambda
}
};
In the amplify/backend.ts
file, we add a new policy to the principal of the personalAssistantFunction
function using the addToPrincipalPolicy
method. The policy statement specifies the allowed resources and actions. In this case, the resource is the AWS ARN (Amazon Resource Name) for the Claude 3 Haiku model, and the permitted action is bedrock:InvokeModel
.
import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
import { personalAssistantFunction, MODEL_ID } from "./functions/personal-assistant/resource";
export const backend = defineBackend({
auth,
data,
personalAssistantFunction,
});
backend.personalAssistantFunction.resources.lambda.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["bedrock:InvokeModel"],
resources: [
`arn:aws:bedrock:*::foundation-model/${MODEL_ID}`,
],
})
);
When you run the app (as demonstrated in the next section), a file named amplify_outputs.json
is generated automatically. This file holds your API’s endpoint details. In the src/app/amplify-utils.ts
, we initialize and configure the Amplify client library as shown below. Then, we create a data client to facilitate requests to the Amplify backend.
import { generateClient } from "aws-amplify/api";
import { Schema } from "../../amplify/data/resource";
import { Amplify } from "aws-amplify";
import outputs from "../../amplify_outputs.json";
Amplify.configure(outputs);
export const amplifyClient = generateClient<Schema>();
In the src/app/layout.tsx
file, we wrapped the app’s content in an AuthWrapper
component (src/app/components/AuthWrapper.tsx
) that utilizes the Amplify Authenticator. This component scaffolds a complete user authentication flow, enabling users to sign up, sign in, reset their passwords, and confirm sign-in for multi-factor authentication (MFA).
"use client";
import { Authenticator } from "@aws-amplify/ui-react";
import { ReactNode } from "react";
interface AuthWrapperProps {
children: ReactNode;
}
export function AuthWrapper({ children }: AuthWrapperProps) {
return <Authenticator>{children}</Authenticator>;
}
The app uses the src/app/page.tsx
file to present a component (src/app/components/Chat.tsx
) to users for chatting with the AI assistant.
"use client";
import { Button, View, Heading, Flex, Text } from "@aws-amplify/ui-react";
import Chat from "@/components/Chat";
import { useAuthenticator } from "@aws-amplify/ui-react";
export default function Home() {
const { user, signOut } = useAuthenticator();
return (
<View className="app-container">
<Flex
as="header"
justifyContent="space-between"
alignItems="center"
padding="1rem"
>
<Text fontWeight="bold">{user?.signInDetails?.loginId}</Text>
<Heading level={3}>Travel Personal Assistant</Heading>
<Button onClick={signOut} size="small" variation="destructive">
Sign out
</Button>
</Flex>
<View as="main">
<Chat />
</View>
</View>
);
}
In the src/app/components/Chat.tsx
file, we created a simple chat interface to facilitate the conversation with the AI assistant. We present a form to capture the user’s message. once submitted, we use the function fetchChatResponse
to invoke the chat query, passing the current conversation and the user’s new message as a parameter to retrieve the assistant response and update the conversation.
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { Button, Placeholder, View } from "@aws-amplify/ui-react";
import { amplifyClient } from "@/app/amplify-utils";
// Types
type Message = {
role: string;
content: { text: string }[];
};
type Conversation = Message[];
export function Chat() {
const [conversation, setConversation] = useState<Conversation>([]);
const [inputValue, setInputValue] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const messagesRef = useRef(null);
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setError("");
setInputValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (inputValue.trim()) {
const message = setNewUserMessage();
fetchChatResponse(message);
}
};
const fetchChatResponse = async (message: Message) => {
setInputValue("");
setIsLoading(true);
try {
const { data, errors } = await amplifyClient.queries.chat({
conversation: JSON.stringify([...conversation, message]),
});
if (!errors && data) {
setConversation((prevConversation) => [
...prevConversation,
JSON.parse(data),
]);
} else {
throw new Error(errors?.[0].message || "An unknown error occurred.");
}
} catch (err) {
setError((err as Error).message);
console.error("Error fetching chat response:", err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
const lastMessage = conversation[conversation.length - 1];
console.log("lastMessage", lastMessage);
(
messagesRef.current as HTMLDivElement | null
)?.lastElementChild?.scrollIntoView();
}, [conversation]);
const setNewUserMessage = (): Message => {
const newUserMessage: Message = {
role: "user",
content: [{ text: inputValue }],
};
setConversation((prevConversation) => [
...prevConversation,
newUserMessage,
]);
setInputValue("");
return newUserMessage;
};
return (
<View className="chat-container">
<View className="messages" ref={messagesRef}>
{conversation.map((msg, index) => (
<View key={index} className={`message ${msg.role}`}>
{msg.content[0].text}
</View>
))}
</View>
{isLoading && (
<View className="loader-container">
<p>Thinking...</p>
<Placeholder size="large" />
</View>
)}
<form onSubmit={handleSubmit} className="input-container">
<input
name="prompt"
value={inputValue}
onChange={handleInputChange}
placeholder="Type your message..."
className="input"
type="text"
/>
<Button
type="submit"
className="send-button"
isDisabled={isLoading}
loadingText="Sending..."
>
Send
</Button>
</form>
{error ? <View className="error-message">{error}</View> : null}
</View>
);
}
export default Chat;
Running the App
Step 1: Amplify provides each developer with a personal cloud sandbox environment, offering isolated development spaces for rapid building, testing, and iteration. To initiate a cloud sandbox environment, open a new terminal window and execute the following command:
npx amplify sandbox
Step 2: Execute the command below to start a localhost development server.
npm run dev
Step 3: After testing the app, you can terminate the sandbox session via Ctrl+c
. Choose Y
for the prompt to delete all the resources in the sandbox environment.
[Sandbox] Watching for file changes...
File written: amplify_outputs.json
? Would you like to delete all the resources in your sandbox environment (This cannot be undone)? (y/N) Y
Deploy the App
Now that your app is functioning correctly, let’s deploy and host it on Amplify. Amplify provides a fully managed hosting service with built-in CI/CD, simplifying the setup of production and staging environments using Git branches. In Gen 2, each Git branch in your repository corresponds directly to a fullstack branch in Amplify.
Step 1: Sign in to the AWS console and select your desired AWS Region. Open the AWS Amplify console and choose Create new app
Step 2: On the Start building with Amplify page, for Deploy your app, select GitHub, and select Next.
Step 3: When prompted, authenticate with GitHub. You will be automatically redirected back to the Amplify console. Choose the repository and main branch of the app. Then select Next.
Step 4: Review the settings and select Next.
Step 5: Lastly, click on the “Save and deploy” button to initiate the deployment process.
Step 6: Wait for the deployment process to finish, and the you can use the Visit deployed Url button to open the web app.
Clean up resources
Now that you’ve finished this walkthrough, you can delete the backend resources to prevent unexpected costs by deleting the app from the Amplify console, as shown below.
Conclusion
Congratulations! You’ve successfully used AWS Amplify Gen 2 and Amazon Bedrock to develop an AI-powered Travel Assistant App. Additionally, you’ve deployed the app on AWS using Amplify Hosting. To get started with Amplify Gen 2, try out the Quickstart tutorial, and join the Community Discord to leave any feedback or feature requests.
Author: