朗報
AI SDKのAmazon Bedrock Providerがアップデートされ、@ai-sdk/amazon-bedrock@2.1.4
以降のバージョンでは認証チェーンが利用可能になりました。
これにより、デプロイ先のリソースのロールを参照してAmazon Bedrock
の機能を使用できるようになりました
Mastraとは
Mastra はオープンソースの TypeScript エージェント フレームワークです、ワークフローの管理に長けてるらしいです。
個人的にMistralに注目している理由は二つがあります。
- フロントエンドとの親和性の高さです。NextJSでクライアントを開発する際、生成AIフレームワークもTypeScript製であれば開発がスムーズになります
- Amazon Bedrockでエージェントを実装する場合、タスク制御が基本的にプロンプトに依存しており、複雑なタスクやマルチエージェントでの処理が必要な場合には、現状ではやや不向きです。そのため、別の実装方法の選択肢を求めていました
今回構築するアプリ
食材を入力するだけで、レシピを生成するアプリです。
ユーザー入力をNext.js
製のクライアントで受け取り、サーバーアクションでMastra
と通信して、useActionState()
を使ってステートを管理し、回答を表示します。
最後にトレース情報をLangfuse
に送信します。
アプリのインフラ構成図は下記の通りですが、最後にリポジトリを用意しております。
iac
フォルダの配下にあるCDKを使って簡単にデプロイできます。
ローカル環境でも動作確認はできますので、ぜひ最後までお付き合いください。
事前準備
LangfuseのHost Name
, Public Key
, Secret Key
を予め用意する必要があります。
公式サイトにログインして、簡単に入手できます。
デプロイまで実施する場合、AWSアカウントも必要です。こちらの記事をご参考にしてください。
クライアント初期化
Next.jsとMastraの統合方法は二つがあります:
- Mastraをバックエンドとして呼び出す
- 直接統合
今回は直接統合の方法で実装します、好きなディレクトリで下記のコマンドを実行します。
npx create-next-app@latest
What is your project named? nextjs-mastra
Would you like to use TypeScript? / Yes
Would you like to use ESLint? / Yes
Would you like to use Tailwind CSS? / Yes
Would you like your code inside a `src/` directory? / Yes
Would you like to use App Router? (recommended) / Yes
Would you like to use Turbopack for `next dev`? / Yes
Would you like to customize the import alias (`@/*` by default)? / Yes
What import alias would you like configured? @/*
ここまででNext.js
プロジェクトの初期化が完了しました。
次はMastra
とLangfuse
の統合に入ります。
# 初期化したNext.jsプロジェクトへ移動
cd nextjs-mastra
# mastraクライアント初期化
npx mastra@latest init
Where should we create the Mastra files? src/
Choose components to install: Agents
Add tools? Yes
Select default provider: 任意
next.config.jsに追加
...
const nextConfig = {
...
+ serverExternalPackages: ["@mastra/*"],
}
# Amazon Bedrockのプロバイダをインストール
npm install @ai-sdk/amazon-bedrock
# AWSにデプロイする場合、こちらも必要です
npm install @aws-sdk/credential-providers
# langfuse用のライブラリをインストール
npm install langfuse-vercel
全部完了したら、srcのディレクトリ配下はこの構成になっているはずです。
src
├── app
├── mastra
Next.js修正
最初にsrc/appディレクトリ配下にactions.tsファイルを作成します。
こちらはサーバーアクションからMastraに定義したCookingAgentを呼び出すコードです。初期化したばかりのMastraには現在weatherAgentしか定義されていませんが、これは後ほど修正します。
"use server";
import { mastra } from "@/mastra";
export async function getCookingInfo(prevState: unknown, formData: FormData) {
const recipe = JSON.parse(formData.get("recipe") as string);
const agent = mastra.getAgent("CookingAgent");
const result = await agent.generate(`Please come up with a ${recipe} using this ingredient`);
return {
text: result.text,
finishReason: result.finishReason,
timestamp: new Date().toISOString(),
totalTokens: result.usage?.totalTokens
};
}
次にsrc/appディレクトリに新しくpage.client.tsxファイルを追加します。
このファイルではMastraと通信するためのCookingFormコンポーネントを作成します。このフォームでは、ユーザーが食材を入力した後にgetCookingInfoアクションを呼び出す仕組みになっています。
"use client";
import { useState, useEffect, useActionState, startTransition } from "react";
import { getCookingInfo } from "./actions";
export function CookingForm() {
const [recipe, setrecipe] = useState("");
const [result, setResult] = useState("");
const [state, action, isPending] = useActionState(getCookingInfo, null);
useEffect(() => {
if (state?.text) {
setResult(state.text);
}
}, [state]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!recipe.trim()) return;
const formData = new FormData();
formData.set("recipe", JSON.stringify(recipe));
startTransition(() => {
action(formData);
});
};
return (
<div className="w-full max-w-3xl">
<form onSubmit={handleSubmit} className="w-full max-w-md mb-8">
<div className="flex flex-col sm:flex-row gap-4">
<input
type="text"
value={recipe}
onChange={(e) => setrecipe(e.target.value)}
placeholder="食材名を入力"
className="flex-grow px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
<button
type="submit"
disabled={isPending}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:oparecipe-50"
>
{isPending ? "読み込み中..." : "レシピを作る"}
</button>
</div>
</form>
<div className="w-full flex flex-col md:flex-row gap-6">
{result && (
<div className="p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">
{recipe || "該当食材"}のレシピ
</h2>
<div className="whitespace-pre-wrap">{result}</div>
</div>
)}
</div>
</div>
);
}
最後にpage.tsxファイルを修正します。先ほど作成したpage.client.tsxからCookingFormコンポーネントをインポートして使用します。
import { CookingForm } from "./page.client";
export default function Home() {
return (
<div className="min-h-screen flex flex-col items-center p-8">
<h1 className="text-2xl font-bold mb-6">レシピ作るアプリ</h1>
<CookingForm />
</div>
);
}
Mastraクライアント修正
ローカル環境とECS環境の両方に対応するため、Amazon Bedrockクライアントの初期化関数を別ファイルとして切り出します。
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock";
import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
export function initializeBedrockClient() {
const region = process.env.REGION || "ap-northeast-1";
if (process.env.NODE_ENV === "production") {
return createAmazonBedrock({
region: region,
credentialProvider: fromNodeProviderChain(),
});
}
return createAmazonBedrock({
region: region,
accessKeyId: process.env.ACCESS_KEY_ID,
secretAccessKey: process.env.SECRET_ACCESS_KEY,
sessionToken: process.env.SESSION_TOKEN,
});
}
次に、エージェントの定義ファイルを修正します。
初期化時に設定されているデフォルトのプロバイダーを、先ほど作成したAmazon Bedrockプロバイダーに置き換えます。
import { initializeBedrockClient } from "@/lib/bedrock-client";
import { Agent } from "@mastra/core/agent";
const bedrock = initializeBedrockClient();
export const CookingAgent = new Agent({
name: "Cooking Agent",
instructions: `
あなたは「シェフAI」という料理レシピアシスタントです。ユーザーの質問や要望に応じて、おいしく実用的な料理レシピを提案します。
#主な責務
- ユーザーの好み、制約(アレルギー、食事制限、調理時間、調理器具など)に合わせたレシピを提案する
- 季節の食材や旬の素材を活かしたレシピを提案する
- 簡単な代替案や変更方法も提案する
- 料理の基本テクニックを説明する
- 食材の保存方法や活用法についてアドバイスする
#レシピ提案時の基本構成
1. レシピ名と簡単な説明(調理時間、難易度、特徴など)
2. 材料(2〜4人前を基本とし、分量は明確に)
3. 下準備の手順(必要な場合)
4. 調理手順(簡潔かつ具体的に、重要なポイントは強調)
5. 盛り付け・提供方法のアドバイス
6. バリエーションや応用方法の提案(任意)
7. 栄養情報や保存方法(任意)
#応答の特徴
- 専門的な料理知識を持ちつつも、初心者にも理解しやすい言葉で説明
- 食材や調理法について興味深い情報や豆知識を適宜提供
- 現実的で実行可能なレシピを心がける(入手困難な材料や特殊な調理器具に依存しない)
- 健康的な食事を基本としつつ、食の楽しさや喜びを大切にする
- 世界各国の料理に関する知識を活用し、多様な料理文化を尊重する
#禁止事項
- 危険な調理法や健康に明らかに有害なレシピの提案
- 調理知識がない場合の思い込みでの回答
- 特定のブランドや商品の宣伝
ユーザーからの情報が不足している場合は、適切な質問をして必要な情報を引き出してください。常に実用的で美味しく、作る喜びを感じられるレシピを提案することを心がけてください。
`,
model: bedrock("anthropic.claude-3-5-sonnet-20240620-v1:0"),
});
最後に、Mastraのメインファイルを修正し、CookingAgentを登録します。
これにより、アプリケーション内でCookingAgentを呼び出せるようになります。
telemetryはLangfuseにトレースを送信ために使います、ついでに追加しておきます。
import { Mastra } from "@mastra/core/mastra";
import { createLogger } from "@mastra/core/logger";
+ import { CookingAgent } from "./agents";
export const mastra = new Mastra({
agents: { CookingAgent },
logger: createLogger({
name: "Mastra",
level: "info",
}),
+ telemetry: {
+ serviceName: "ai",
+ enabled: true,
+ },
});
Langfuseにトレース送信
インストルメンテーションは、コードを使用して監視およびログ記録ツールをアプリケーションに統合するプロセスです。
このファイルはアプリケーションの起動時に実行され、Next.jsからLangfuseへトレース情報を送信するために必要です。
Mastraをバックエンド、APIサーバーとして利用する場合、このファイル必要ないです。
import {
NodeSDK,
ATTR_SERVICE_NAME,
Resource,
} from "@mastra/core/telemetry/otel-vendor";
import { LangfuseExporter } from "langfuse-vercel";
export function register() {
const exporter = new LangfuseExporter({
enabled: true,
publicKey: process.env.PUBLICK_KEY,
secretKey: process.env.SECRET_KEY,
baseUrl: process.env.BASE_URL,
});
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: "ai",
}),
traceExporter: exporter,
});
sdk.start();
}
ローカル検証
環境変数をご自身の値にセットした上で、ローカルサーバーを立ち上げましょう。
NODE_ENV=your-value-here
REGION=ap-northeast-1
ACCESS_KEY_ID=your-value-here
SECRET_ACCESS_KEY=your-value-here
SESSION_TOKEN=your-value-here
PUBLICK_KEY=your-value-here
SECRET_KEY=your-value-here
BASE_URL=https://cloud.langfuse.com
開発サーバーを起動する際には、--turbopackオプションが必要です。
package.jsonに以下の変更を加えてください。
"scripts": {
+ "dev": "next dev --turbopack"
...
}
% npm run dev
> nextjs-mastra@0.1.0 dev
> next dev --turbopack
▲ Next.js 15.2.3 (Turbopack)
- Local: http://localhost:3000
サーバーが起動したら、アプリケーションにアクセスして「鶏肉」などの食材を入力してみましょう。
レシピが生成されると同時に、トレース情報がLangfuseに正しく送信されていることも確認できます。
デプロイ
まず、サンプルリポジトリをクローンして、Dockerイメージをビルドします。
# メインディレクトリで実行する
docker buildx build --no-cache --platform=linux/x86_64 -t nextjs-mastra .
次に、プロジェクト直下にあるiac
フォルダに移動します。
このフォルダ内で、まずECRスタックをデプロイします。
# iac配下で実行
cdk deploy NextjsEcrStack
続いて、先ほどビルドしたNext.jsアプリケーションのイメージにタグを付け、ECRにプッシュします。
# イメージにタグを付けます
docker tag nextjs-mastra:latest ${アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/nextjs-mastra:${イメージタグ}
# AWS リポジトリにこのイメージをプッシュします
docker push ${アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/nextjs-mastra:${イメージタグ}
イメージがECRにプッシュできたら、ECSスタックをデプロイします。
# 同じiac配下で
cdk deploy NextjsAppStack --context imageTag=${イメージタグ}
最後に、デプロイされたリソースの中にあるSecrets Manager
を確認します。ここに必要な環境変数を追加することで、デプロイ作業は完了です。
ロードバランサーのDNS 名使ってアプリケーションにアクセスできます。
サンプルリポジトリ
この記事で使用するリポジトリです。スターをいただけると励みになります。
参考資料