Compass ソリューション

生成 AI を使用して、エージェント主導の旅行計画アプリを構築する

次の図は、最高水準の Google デベロッパー サービスを組み合わせて AI ドリブン アプリケーションを構築する方法の概要を示しています。

モバイルとウェブ向けに、AI を活用した見栄えの良いアプリを作成

Flutter と Firebase Genkit を使用して、AI とシームレスに統合できるマルチプラットフォーム アプリを構築できます。
Genkit を使用して LLM の出力を検証できるスキーマを定義することで、アプリで LLM からのデータを確実に消費できるようになります。Flutter では、Dart を使用してリクエストをシリアル化し、Genkit スキーマに適合するように標準の HTTP リクエストを使ってレスポンスをシリアル化解除できます。
IDX を使用すると、ソフトウェアをインストールせずに Flutter アプリを作成できます。これにより、ブラウザで Android アプリやウェブアプリを開発し、テストできます。
Firebase Data Connect は、モバイルアプリとウェブアプリ向けのリレーショナル データベース サービスで、Cloud SQL を活用したフルマネージドの PostgreSQL データベースを使用して構築とスケーリングを行うことができます。Firebase Authentication と緊密に統合された GraphQL を使用して、安全なスキーマ、クエリ、ミューテーション管理を実現します。Data Connect には、Kotlin Android とウェブの SDK サポートが含まれています。

Firebase Genkit を使用してエージェントを構築する

エージェントは、AI オーケストレーション フレームワークを使用してユーザー入力を受け取り、レスポンスを生成します。
---
model: googleai/gemini-1.5-flash-latest
config:
  temperature: 1.0
  safetySettings:
    - category: HARM_CATEGORY_HATE_SPEECH
      threshold: BLOCK_LOW_AND_ABOVE
    - category: HARM_CATEGORY_DANGEROUS_CONTENT
      threshold: BLOCK_ONLY_HIGH
    - category: HARM_CATEGORY_HARASSMENT
      threshold: BLOCK_LOW_AND_ABOVE
    - category: HARM_CATEGORY_SEXUALLY_EXPLICIT
      threshold: BLOCK_LOW_AND_ABOVE
input:
  schema:
    request: string, The users request for where they want to travel to.
    place: string, The place that closely represents the users request.
    placeDescription: string, A description of that place.
    activities(array, a stringify list of activities that can be found at the specified place): string
    restaurants?(array, a stringify list of all the restaurants found at that location): string
output:
  schema:
    place: string, The place the user is traveling to.
    itineraryName: string, a catchy itinerary name that encapsulates the spirit of the trip and includes the place name
    startDate: string, the start date of the trip
    endDate: string, the end date of the trip
    tags(array, relevant tags for the trip): string
    itinerary(array):
      day: number
      date: string
      planForDay(array):
        activityRef: string, the reference value for the activity - this comes from the available activities JSON. If no value is present use a ref value of restaurant.
        activityTitle: string, a catchy title for the activity
        activityDesc: string, a six word description of the activity
        photoUri?: string, set the photo uri value for restaurants only.
        googleMapsUri?: string, if this is a restaurant include the googleMapsUri
---

Generate an itinerary for a tourist planning on traveling to the location specified based in their request.
If there is something that does not exist within the list of activities, do not include it in your answer.
Feel free to relate the activitiy to the request in a meaningful way.
In the plan for day array, put activities as a travel brouchure might do.
Come up with a catchy name for the itinerary.

Pick three activities per day, minimum of three day trip unless otherwise specified in the request.

Output schema should not include the properties type or object.

Pick a date after 2024-05-14 but before 2024-12-31.

The output date must be in the format year-month-day.

Give each activity a unique title and description.

Limit activity descriptions to 6 words.

If no restaurants are supplied, do not recommend any restaurants to eat at.

{{#if restaurants}}
Find a restaurant to eat at each day.

Include a restaurant to visit in the itinerary for each day from the available restaurants.
The restaurant should be the only activity with a photoUri.
The photoUri for the restaurant should be from the photoUri property from the restaurant array.
If there are no restaurants to pick from, do not include it in the list.

The photoUri from the restaurantFinder should be in the format of places/${placeId}/photos/${photoId}

Each restaurant should be unique to the overall itinerary.
Each restaurant must contain a photoUri in their output JSON schema.
Each restaurant must also include  an activitiyRef, activityTitle, and activityDesc in their output
{{/if}}
Output must be in JSON format.

REQUEST : {{request}}
PLACE : {{place}}
PLACE DESCRIPTION : {{placeDescription}}
AVAILABLE ACTIVITIES : {{activities}}
RESTAURANTS : {{restaurants}}
生成 AI を使用する際には、モデルが高品質のレスポンスを返すように効果的なプロンプトを作成することが重要です。Firebase Genkit には、生成 AI のプロンプトの作成と整理に役立つ Dotprompt プラグインとテキスト形式が用意されています。この形式は、プロンプト、入力と出力のスキーマ、モデル、構成をすべて 1 つのファイルにカプセル化します。

次のコードサンプルは、旅行アプリで使用される Dotprompt ファイルを示しています。スキーマは、ユーザーが夢の旅行について記述する際に提供する情報に基づいています。
Dotprompt は、プロンプトがコードであることを前提として設計されています。プロンプトは、dotprompt ファイルと呼ばれる特別な形式で記述して維持し、コードに使用するのと同じバージョン管理システムを使用して変更を追跡し、生成 AI モデルを呼び出すコードとともにデプロイします。
フローは、厳密に型指定された、ストリーミング可能で、ローカルにもリモートからも呼び出し可能で、完全に監視可能な関数です。Firebase Genkit は、フローの実行やデバッグなど、フローを操作するためのコマンドライン インターフェースとデベロッパー UI を提供します。
import {defineTool} from '@genkit-ai/ai/tool';
...
{
  name: 'restaurantFinder',
  description: `Used when needing to find a restaurant based on a users location.
  The location should be used to find nearby restaurants to a place. You can also
  selectively find restaurants based on the users preferences, but you should default
  to 'Local' if there are no indications of restaurant types in the users request.
  `,
  inputSchema: z.object({
    place: z.string(),
    typeOfRestaurant: z.string().optional() }),
    outputSchema: z.unknown(),
},
...
async (input) => {
  if (input.typeOfRestaurant == undefined) {
    input.typeOfRestaurant = "Local";
  }
  const geocodeEndpoint = "https://places.googleapis.com/v1/places:searchText";
  const textQuery = {textQuery: `${input.typeOfRestaurant} restaurants in ${input.place}`};

  const  response = await axios.post(
    geocodeEndpoint,
    JSON.stringify(textQuery),
    {
      headers: {
        "Content-Type": "application/json",
        "X-Goog-Api-Key": MAPS_API_KEY,
        "X-Goog-FieldMask": "places.displayName,places.formattedAddress,places.priceLevel,places.photos.name,places.editorialSummary,places.googleMapsUri"
      }
    }
  );
  console.log(response.data);
  let data = (response.data as PlaceResponse);
  for(let i = 0; i < data.places.length; i++) {
    if (data.places[i].photos) {
      data.places[i].photos = [data.places[i].photos[0]];
    }
  }
  return data as PlaceResponse;
}
Genkit の関数呼び出しを使用してエージェントの機能を拡張することで、エージェントはレスポンスをさらに絞り込んで追加のタスクを完了できます。この旅行アプリでは、ユーザーが希望するルートに基づいて、Places API からレストラン情報を返すツールを定義します。このコードは、クエリ結果を検証できるように、Zod を使用して入力スキーマと出力スキーマを定義します。
...
export const textRefinement = defineFlow(
{
  name: 'textRefinement',
  inputSchema: z.string(),
  outputSchema: z.unknown(),
},
async (userRequest) => {
  const refinementPrompt = await prompt('textRefinement')
  const result = await refinementPrompt.generate({
      input: {
          request: userRequest
      },
  });
  return result.output();
});
Gemini は、よりカスタマイズされた検索エクスペリエンスを提供するため、ユーザーが夢の旅行について述べた後、旅行アプリが提示するプロンプトに基づいて追加情報が必要かどうかを判断し、より多くの情報が必要であるとアプリに通知します。次に、アプリはユーザーにこの情報の入力を求め、バックエンドのリクエストに追加します。
import 'package:http:http.dart' as http;
...
Future<List<Trip>> generateTrips(String description, List<Image> images) async {
  final uri = Uri.parse('.../generateTrips');
  final request = http.MultipartRequest('POST', uri);
  request.fields['description'] = description;
  request.files.add(http.MultipartFile.fromData(
      images.name, images.bytes,
      contentType: MediaType('image', 'png'),
  ));
  var response = await request.send();
  if (response.statusCode == 200) {
      final body = await response.body.text();
      final items = jsonDecode(body) as List<dynamic>;
      return items.map(Trip.fromJson).toList();
  }
  ...
  import { imagen2, geminiProVision } from '@genkit-ai/vertexai';
  import { generate } from '@genkit-ai/ai';

  const imageResult = await generate({
    model: imagen2,
    prompt: 'Generate an image of a very specific historical time and place.',
  });
  const generatedImage = imageResult.media();

  const descriptionResult = await generate({
    model: geminiProVision,
    prompt: [
      {
        text: 'What is the historical time and place represented in this picture?',
      },
      { media: generatedImage },
    ],
  });
  console.log(descriptionResult.text());
  }
旅行アプリが、テキスト入力を使用するか、マイクボタンをタップして音声入力を有効にして、夢の旅行を定義するようユーザーに求めます。ユーザーはオプションで画像をアップロードすることもできます。

アプリは pub.dev の Dart パッケージを利用して、各プラットフォームのネイティブ Speech-to-Text 機能と統合し、Firebase Genkit 内の Gemini API を使用して画像や動画などのマルチモーダル入力を処理します。Gemini API は、検索拡張生成(RAG)を使用して、Firebase Data Connect とエンベディングを使用して最近傍探索を行い、提案されたルートのセットを返します。

本番環境用にアプリをスケーリングする

Firebase Hosting は、Flutter などのよく使われている最新のウェブ フレームワークと統合されています。これらのフレームワークで Firebase Hosting と Cloud Functions for Firebase を使用すると、好みのフレームワーク環境でアプリとマイクロサービスを開発し、マネージド型の安全なサーバー環境にデプロイできます。本番環境に移行する前に、アプリに含まれるすべてのサービスのセキュリティとパフォーマンスを把握します。詳細については、Firebase のリリース チェックリストをご覧ください。
この旅行アプリは、Google AI を使用してテストデータを迅速に反復処理するため、スケーリングを必要としない最小限の AI ユースケースを持つアプリに適しています。Vertex AI では、本番環境アプリケーションのスケーリングに役立つ高い割り当てと、ユーザーデータを保護するための強固なプライバシー ポリシーが用意されています。Genkit にはモデルを簡単に切り替えられる機能が組み込まれているため、プロンプトや API 呼び出しを書き直す必要はありません。