Amazon Web Services ブログ

Amazon Bedrock と AWS Amplify を使った生成 AI トラベルアシスタントアプリの作成

本記事では、トラベルアシスタントアプリの作成方法について説明します。このアプリは、ユーザーの希望する目的地における人気の観光スポット、地域の体験、隠れた名所などをお勧めすることで、パーソナライズされた体験を提供します。このアプリの構築には、AWS AmplifyAmazon Bedrock を使用します。

AWS Amplify Gen 2 は、バックエンド定義に TypeScript ベースのコードファーストの開発者体験(DX)を採用しています。Gen 2 の DX は、ホスティング、バックエンド、UI ビルド機能を備えた統合された Amplify 開発者体験と、コードファーストのアプローチを提供します。Amplify は、フロントエンド開発者がアプリのデータモデル、ビジネスロジック、認証、認可ルールの全てを TypeScript で表現するだけで、クラウドインフラストラクチャをデプロイできるようにします。Amplify は適切なクラウドリソースを自動的に設定し、基盤となる AWS サービスを手作業で統合する必要性を排除します。

Amazon Bedrock は、主要な AI 企業が提供する高性能な基盤モデル(FM)をフルマネージド型で提供するサービスです。これらの FM は単一のアプリケーションプログラミングインターフェース(API)を通じてアクセス可能で、安全で、プライベートで、責任ある生成 AI アプリケーションを構築するための機能も併せて提供されます。単一の API アクセスにより、異なる FM を柔軟に使用でき、最小限のコード変更で最新のモデルバージョンにアップグレードすることができます。

The Travel Planner AI App running in the browser

前提条件

  • AWS アカウント(Amplify は AWS 無料枠の対象です)
  • Node.js v18.17 以降
  • npm v9 以降
  • git v2.14.1 以降
  • テキストエディタ(このガイドでは VSCode を使用しますが、お好みの IDE を使用可能です)
  • Amazon Bedrock での Claude 3 Haiku モデルへのアクセス権

リポジトリのクローン

ステップ 1: AWS Samples のリポジトリに移動し、あなたの GitHub リポジトリにフォークします。

Fork the repository on AWS samples

ステップ 2: 以下のコマンドをターミナルで実行して、アプリをクローンします。


git clone https://github.com/<YOUR_GITHUB>/travel-personal-assistant.git

Bash

ステップ 3: 以下のコマンドをターミナルで実行して、新しくクローンしたリポジトリに VSCode でアクセスします。


cd travel-personal-assistant
code . -r
Bash

VSCode が、次のセクションで説明するバックエンドの詳細を含む Amplify フォルダを含むリポジトリフォルダを開きます。

Open the cloned repository using VSCode

ステップ 4: 以下のコマンドを実行して、Amplify パッケージを含む必要なパッケージをインストールします。


npm i 

Bash

Amplify バックエンド

完成したアプリ(冒頭の GIF で示したもの)では、ユーザーは旅行に関する質問(例:「12 月にアイルランドに 7 日間行きたい」)を送信することで会話を開始します。コードはクローンしたリポジトリにあります。ここでは、Amplify アプリを Amazon Bedrock と接続するための主要なステップを説明します。

リポジトリには、auth(認証)、data(データ)、functions(関数)リソース用のディレクトリを含む amplify フォルダがあります。
The Amplify folder in the project

デフォルトでは、amplify/auth/resource.ts ファイルで設定された認証リソースは、ユーザーのサインアップメカニズムとしてメールを使用するように設定されています。

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

amplify/data/resource.ts ファイルでは、文字列を返し、会話全体を含む JSON 文字列引数 conversation を引数にとる GraphQL クエリ chat を定義しています。認証は .authorization((allow) => [allow.authenticated()]) を使用して、認証されたユーザーのみがこのクエリにアクセスできるように設定されています。.handler(a.handler.function(personalAssistantFunction)) の行は、このクエリのカスタムハンドラーとして personalAssistantFunction を設定します。

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

personalAssistantFunction Lambda 関数は、amplify/functions/personal-assistant/resource.ts ファイルで定義・エクスポートされています。ここでは、環境変数 MODEL_ID を 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,
});
TypeScript

amplify/functions/personal-assistant/handler.ts ファイルには、personalAssistantFunction ハンドラーの実装が含まれています。ここでは、Bedrock ランタイムクライアントを初期化し、クエリの入力パラメータ conversationsystemPrompt 定数(応答方法に関するコンテキスト、指示、ガイドラインを Amazon Bedrock への提供するため)を使用して入力オブジェクトを生成し、それを使用して ConverseCommand を作成し、ランタイムクライアントを使用して Amazon Bedrock に送信します。応答のエラーを確認し、JSON 文字列として返します。

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

amplify/backend.ts ファイルでは、addToRolePolicy メソッドを使用して、personalAssistantFunction 関数のプリンシパルに新しいポリシーを追加します。ポリシーステートメントは、許可されたリソースとアクションを指定します。この場合、リソースは Claude 3 Haiku モデルの AWS ARN(Amazon Resource Name)で、許可されたアクションは 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}`,
    ],
  })
);
TypeScript

アプリを実行すると(次のセクションで説明)、amplify_outputs.json という名前のファイルが自動的に生成されます。このファイルには API のエンドポイントの詳細が含まれています。src/app/amplify-utils.ts では、以下のように Amplify クライアントライブラリを初期化・設定します。そして、Amplify バックエンドへのリクエストを容易にするためのデータクライアントを作成します。

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>();
TypeScript

src/app/layout.tsx ファイルでは、アプリのコンテンツを AuthWrapper コンポーネント(src/app/components/AuthWrapper.tsx)でラップしています。このコンポーネントは Amplify Authenticator を利用して、ユーザーがサインアップ、サインイン、パスワードリセット、多要素認証(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>;
}
TypeScript

アプリは、src/app/page.tsx ファイルを使用して、AI アシスタントとチャットするためのコンポーネント(src/app/components/Chat.tsx)をユーザーに提供します。

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

src/app/components/Chat.tsx ファイルでは、AI アシスタントとの会話を促進するためのシンプルなチャットインターフェースを作成しています。ユーザーのメッセージを取得するためのフォームを表示し、送信されると、fetchChatResponse 関数を使用してチャットクエリを呼び出します。この際、現在の会話とユーザーの新しいメッセージをパラメータとして渡し、アシスタントの応答を取得して会話を更新します。

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;
TypeScript

アプリの実行

ステップ 1: Amplify は各開発者に個人のクラウドサンドボックス環境を提供し、迅速な構築、テスト、反復のための独立した開発スペースを提供します。クラウドサンドボックス環境を開始するには、新しいターミナルウィンドウを開き、以下のコマンドを実行します。


npx amplify sandbox
Bash

ステップ 2: 以下のコマンドを実行して、ローカルホストの開発サーバーを起動します。


npm run dev
Bash

The Travel Planner AI App running in the browser

ステップ 3: アプリのテスト後、Ctrl+c でサンドボックスセッションを終了できます。サンドボックス環境のすべてのリソースを削除するプロンプトで Y を選択してください。


[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
Bash

アプリのデプロイ

アプリが正しく機能することを確認したら、Amplify でデプロイしてホストしましょう。Amplify は、Git ブランチを使用した本番環境とステージング環境の設定を簡素化する、組み込みの CI/CD を備えたフルマネージド型のホスティングサービスを提供します。Gen 2 では、リポジトリの各 Git ブランチが、Amplify のフルスタックブランチに直接対応します。

ステップ 1: AWS コンソールにサインインし、希望する AWS リージョンを選択します。AWS Amplify コンソールを開き、“Create new app” を選択します。

Amplify homepage on AWS console

ステップ 2: “Start building with Amplify“ ページで、“Deploy your app” から GitHub を選択し、“Next“ を選択します。

select GitHub, and select Next.

ステップ 3: プロンプトが表示されたら、GitHub で認証を行います。自動的に Amplify コンソールにリダイレクトされます。アプリのリポジトリとメインブランチを選択し、“Next” を選択します。

Choose the repository and the branch from the dropdown lists

ステップ 4: 設定を確認し、“Next” を選択します。

Review the settings

ステップ 5: 最後に、“Save and Deploy” ボタンをクリックしてデプロイプロセスを開始します。

click on the

ステップ 6: デプロイプロセスが完了するのを待ち、“Visit deployed URL” ボタンをクリックして Web アプリを開くことができます。

Click the Visit deployed Url button to open the web app

The Travel Planner AI App running in the browser

リソースのクリーンアップ

このチュートリアルが終了したら、予期しないコストを防ぐために、下図のように Amplify コンソールからアプリを削除してバックエンドリソースを削除することができます。

Click the Delete app button

まとめ

おめでとうございます!AWS Amplify Gen 2 と Amazon Bedrock を使用して、AI 搭載のトラベルアシスタントアプリの開発に成功しました。さらに、Amplify Hosting を使用して AWS 上にアプリをデプロイしました。Amplify Gen 2 を開始するには、クイックスタートチュートリアルを試してみてください。また、フィードバックや機能リクエストを残すために、コミュニティ Discord に参加することもできます。

本記事は、Creating a Generative AI Travel Assistant App with Amazon Bedrock and AWS Amplify を翻訳したものです。翻訳は Solutions Architect の 都築 が担当しました。

著者:

Mo Malaka

Mo Malaka is a Senior Solution Architect on the AWS Amplify Team. The Solution Architecture team educates developers regarding products and offerings, and acts as the primary point of contact for assistance and feedback. Mo enjoys using technology to solve problems and making people’s lives easier. You can find Mo on YouTube or on Twitter.