Firebase Genkit Functions 開発のすゝめ
はじめに
こんにちは、Firebase が大好きな cobo です。
アプリケーション開発の際、Google では Test Sizes の考え方が推奨されています。
これは、テストを Small / Medium / Large といった規模に応じて分類し、それぞれの目的に応じて効率的にテストを行う方法です。
特に重要なのが、Small Tests をローカル環境で高速に実行することです。
これにより、一般的な結合テストフェーズで動作を確認するよりも、個々の機能が正しく動作するかを迅速に確認でき、デプロイ前に問題を発見することが可能になります。
Genkit には、ローカルで動作を確認できる Genkit Emulator というツールがあり、Firebase Genkit Functions の開発を大幅に加速させます。
しかし、他の Firebase プロダクト(例えば Firestore や Cloud Storage)と連携する際には、Genkit Emulator だけでは不十分な場合があります。
そのため、今回は Firebase Local Emulator Suite を統合して、より高度なローカルテスト環境を実現する方法をご紹介します。
Firebase Genkit Emulator
Genkit には、独自の Genkit Emulator が存在します。
このツールを使うことで、Genkit Functions をデプロイする前に、その動作をローカル環境で検証することができるというものです。
(非常に UI がかっこよくて、使っていてとてもワクワクします!!)
Genkit Emulator
実は、この最高の開発者体験を提供する Genkit Emulator にも、課題点があります。
課題点
それは、他の Firebase プロダクトとの連携です。
例えば、前回執筆した Firebase & AI のオーケストレーションを実現!Genkit アーキテクチャ 8 選 の中にもあった、3. Firestore を生成 AI のコンテキストとした構成 の Genkit -> Firestore のような構成の際です。
まずは、イメージを沸かせるために下記の 2 種類のケースを確認します。
- Genkit Emulator のみで、ローカル検証ができる OK のケース
- Genkit Emulator のみで、ローカル検証ができない NG のケース
OK のケース
OK のケース
このケースでは、Firestore や Cloud Storage などの Firebase プロダクトを使用せず、Genkit 内の処理だけでローカルで検証が完結します。
export const noAuthFunction = genkitFunctions.onFlow(
{
name: `noAuthFunction`,
httpsOptions: {
cors: true,
secrets: [googleAIapiKey],
},
inputSchema: chatbotInputSchema,
outputSchema: chatbotOutputSchema,
authPolicy: firebaseAuth((user) => {
if (user.firebase?.sign_in_provider !== `anonymous`) {
throw new Error(
`Only anonymously authenticated users can access this function`
);
}
}),
},
async (input) => {
const chatbotPrompt = await prompt<z.infer<typeof chatbotInputSchema>>(
`chatbot`
);
const result = await chatbotPrompt.generate({ input });
return result.output();
}
);
NG のケース
NG のケース
このケースでは、async (input) =>
以降で Firestore のデータベースを参照しているため、Genkit Emulator だけでは Firestore データベースへのアクセスができず、ローカル環境での動作確認ができません。
ですので、実環境へデプロイした後に、エンドポイントへアクセスして Genkit Functions の動作を確認するという方法を採る方もいるのではないでしょうか。
export const generateChatMessage = genkitFunctions.onFlow(
{
name: `generateChatMessage`,
httpsOptions: {
cors: true,
secrets: [googleAIapiKey],
},
inputSchema: z.object({
userId: z.string(),
chatId: z.string(),
}),
authPolicy: firebaseAuth((user) => {
if (user.firebase?.sign_in_provider !== `anonymous`) {
throw new Error(
`Only anonymously authenticated users can access this function`
);
}
}),
},
async (input) => {
try {
const chatDoc = await db.collection(`chats`).doc(input.chatId).get();
if (!chatDoc.exists) throw new Error(`Chat document not found`);
const messagesSnapshot = await chatDoc.ref.collection(`messages`).get();
const messages = messagesSnapshot.docs.map((doc) => ({
createdAt: doc.data().createdAt.toDate().toString(),
isUser: doc.data().isUser,
text: doc.data().text,
}));
const chatbotPrompt = await prompt<z.infer<typeof chatInputSchema>>(
`chat`
);
const result = await chatbotPrompt.generate({ input: { messages } });
await chatDoc.ref.collection(`messages`).add({
createdAt: Timestamp.now(),
isUser: false,
text: result.output().response,
});
return result.output();
} catch (error) {
console.error(`Error in generateChatMessage:`, error);
throw error;
}
}
);
Firebase Local Emulator Suite との統合
上記の NG のケース
のように、Firestore などの Firebase プロダクトを使用する場合、Genkit Emulator だけではローカル検証ができないという課題があります。
このような場合、Firebase Local Emulator Suite を使用することで、Firestore や Cloud Storage をローカルでシミュレートし、Genkit Functions の動作を確認することが可能になります。
以降は、上記の NG なケースを OK なケースにするための手順を紹介します。
プログラムフローの確認
まず、NG のケース
の Genkit Functions のプログラムフローを確認しましょう。
この関数では、
- Firestore からデータを取得
- 生成 AI によってチャットメッセージを処理
- 処理後のメッセージを Firestore へ書き込み
というフローとなっており、Firestore の接続が必要なことがわかるかと思います。
また、補足として、Firestore のスキーマ関連図と、各種ポイントとなるコードを下記に示します。
Firestore スキーマ関連図
スキーマ関連図
Firestore Schemas
ポイントとなるコード群
1. Firestore からデータを取得するコード
const chatDoc = await db.collection(`chats`).doc(input.chatId).get();
const messages = messagesSnapshot.docs.map((doc) => ({
createdAt: doc.data().createdAt.toDate().toString(),
isUser: doc.data().isUser,
text: doc.data().text,
}));
2. 生成 AI によってチャットメッセージを処理するコード
const chatbotPrompt = await prompt<z.infer<typeof chatInputSchema>>(`chat`);
const result = await chatbotPrompt.generate({ input: { messages } });
3. 処理後のメッセージを Firestore へ書き込むコード
await chatDoc.ref.collection(`messages`).add({
createdAt: Timestamp.now(),
isUser: false,
text: result.output().response,
});
Firebase Local Emulator Suite の設定
Genkit Emulator の接続先を Firebase Local Emulator にするための設定を行なっていきます。
0. インストール
firebase init
1. 起動
下記コマンドによって、エミュレータ上でコードが実行され、開発モードで Genkit フレームワークが実行されます。
GENKIT_ENV=dev firebase emulators:start --inspect-functions
2. Genkit Emulator に接続
--attach オプションを指定することで、Firebase Emulator へ接続を行う、Genkit Emulator が起動します。
genkit start --attach http://localhost:3100 --port 4001
ローカル環境での Firebase Genkit Functions 検証
これで、Genkit Functions をローカル環境で検証するための準備が整ったので、動作を確認していきます。
1. Firestore Emulator でコレクション・ドキュメントデータの整備
今回は、Firestore 上のデータを使用するので、まずは、Firebase Local Emulator Suite の UI で Firestore を起動し、検証用のデータを追加していきます。
Chats Collection
Messages Sub Collection
2. Genkit Emulator で Genkit Functions の動作確認
ここでは、通常時の使用と同様、定義されたリクエストのスキーマに沿って、値を入力します。
Genkit Emulator
さらに、今回の例では、Genkit Functions の Auth Policy を 匿名認証済みのユーザー としているので、下記のように Auth JSON
タブにて認証プロバイダーを注入します。
Genkit Emulator > Auth JSON
参考: ユーザーを識別する
これで、今度こそ Genkit Functions をローカル環境で実行できるようになったので、Run ボタンを押下します。
すると、Firestore Emulator 上のコレクション・ドキュメントデータを参照して Genkit Functions から結果が返却されたことがわかります。
Response
さらに、今回の Genkit Functions では Firesotre へデータを書き込む処理も行われており、Firestore Emulator を見にいくと、データが追加されたことも確認できます。
Firestore Emulator
Discussion