Next.jsでClaude3 Sonnet (Bedrock) の実行環境をつくる (Stream版)
こんにちは!@Ryo54388667です!☺️
普段は都内でフロントエンドエンジニアとして業務をしてます!
主にTypeScriptやNext.jsといった技術を触っています。
今回はNext.jsでClaude3 Sonnet(Bedrock)の実行環境をつくる方法を紹介したいと思います。
自分はChatGPTには課金していますが、Claude Proに課金するかどうかは悩み中の身です。この実行環境で触ってみて検討したいと思っています。
📌 準備
いくつか準備が必要です。
- AWS IAMユーザーの準備
- BedrockのClaude3 Sonnetモデルの利用申請
- Bedrockを操作できるように、IAMユーザーにポリシーをアタッチする
これらの準備が済んでいる人は先に進んでください〜
AWS IAMユーザーの準備
既存の開発者IAMユーザーを作成済みの方はスキップして大丈夫です。
コード上でBedrockを操作できるように権限の箱のようなものを準備します。こちらに必要に応じて様々な権限を付与していきます。
今回の詳細を話していくと冗長になりそうなので、こちらの記事が参考になるかと思います。
AWS IAMユーザーのページにユーザーが作成されればひとまずOKです。
ここで、コードで実行できるようにアクセスキーを作成しておきます。下記の記事が親切で分かりやすいです〜
後ほど、環境変数のAWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
の値になります。コピーしておきます。
BedrockのClaude3 Sonnetモデルの利用申請
僕は、こちらを忘れていて、通らないリクエストを何度も投げていました。。😇お忘れなく。
利用するために申請が必要です!特に何時間もかかるものではありません。簡単なアンケートに答えるものです。
僕の場合は5分くらいでアクセス可能になりました。
詳しくはこちらのページの「Anthropic モデルを初めて使用する場合は~」を参考に進めるのが良いかと思います!
緑色のチェックマークが表示されれば、アクセス可能になります👍
Bedrockを操作できるように、IAMユーザーにポリシーをアタッチする
先ほど権限の箱であるIAMユーザーを作成したので、そこにアタッチするポリシーを作成します。
ポリシー名は任意のもので大丈夫です👌
JSON形式で下記のものを貼り付けてください。リージョンはus-east-1
であることに注意です。今回、Actionの箇所にはstream対応のリクエストと通常のリクエストの2種類のアクションを入れました。streamのみでも全く構いません。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0"
}
]
}
こちらのポリシーが作成できたら、今度はIAMユーザーのページに遷移し、先ほど作成したポリシーをアタッチすれば準備OKです👌
なかなか大変ですね。。😅
📌 コードについて
あとはコードを書いて準備していきます。
Package
name | version |
---|---|
Next | 14.0.0 |
React | 18.2.0 |
Tailwindcss | ^3 |
@aws-sdk/client-bedrock-runtime | ^3.529.1 |
ディレクトリ構造
├── app
│ ├── api
│ │ └── chat
│ │ └── route.ts
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── lib
└── bedrock.ts
Next.jsのプロジェクトは下記のページを参考に作成すると良いかと思います。
大まかな流れとしては、
- promptの入力
- 送信ボタンの押下時に
/api/chat
のAPIコール(route.tsに関わる内容) - Stream形式のレスポンスをその都度画面に表示させる
Bedrockに関わる内容
ライブラリをインストールします。
npm i @aws-sdk/client-bedrock-runtime
続いて、準備のセクションでコピーした環境変数を用意します。
プロジェクトのルートにenv.local
を作成し、ファイル内に以下のコードを書きます。
/env.local
AWS_ACCESS_KEY_ID=ここにコピーした値を貼ります(key)
AWS_SECRET_ACCESS_KEY=ここにコピーした値を貼ります(secret)
蛇足ではありますが、こちらも簡易的な環境変数の設定方法なのでデプロイする際にはセキュリティに配慮した方法を選択する必要があるかと思います。
/lib/bedrock.ts
import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } from '@aws-sdk/client-bedrock-runtime';
const bedrock = new BedrockRuntimeClient({
region: 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',
}
});
export const postMessageWithRiouteHandler = async (prompt: string) => {
const payload = {
anthropic_version: "bedrock-2023-05-31",
max_tokens: 1000,
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
};
const response = await bedrock.send(
new InvokeModelWithResponseStreamCommand({
modelId: 'anthropic.claude-3-sonnet-20240229-v1:0',
contentType: 'application/json',
body: JSON.stringify(payload)
})
);
return response;
}
Route Handlerに関わる内容
/api/chat
のエンドポイントにアクセスした時に処理されます。リクエストした時ですね。
/app/api/chat/route.ts
import { postMessageWithRiouteHandler } from "@/lib/bedrock"
export const runtime = 'edge'
export async function POST(request: Request) {
const data = await request.json()
const readableStream = new ReadableStream({
async start(controller) {
const response = await postMessageWithRiouteHandler(data.prompt)
if (response.body) {
for await (const stream of response.body) {
controller.enqueue(stream.chunk?.bytes)
}
controller.close()
}
}
})
return new Response(readableStream, { headers: { "Content-Type": "text/plain" } })
}
UIに関わる内容
あとはリクエストする部分とレスポンスを受け取る箇所を実装します。
/app/layout.tsx
import "./globals.css"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja" className='max-w-[768px] mx-auto'>
<body>{children}</body>
</html>
)
}
/app/page.tsx
"use client"
import { useState, useTransition } from "react"
export default function Home() {
const [isPending, startTransition] = useTransition()
const [prompt, setPrompt] = useState("")
const [userChat, setUserChat] = useState("")
const [botChat, setBotChat] = useState("")
const onSend = async (e: any) => {
e.preventDefault()
const _prompt = e.target[0].value
setUserChat(_prompt)
setPrompt("")
startTransition(async () => {
const response = await fetch(`/api/chat`, {
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify({ "prompt": _prompt })
})
if (response.body) {
const reader = response.body.getReader()
const decoder = new TextDecoder()
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
if (value) {
const chunk = JSON.parse(decoder.decode(value, { stream: true }))
const chunk_type = chunk.type;
switch (chunk_type) {
case "message_start":
console.log(chunk["message"]["id"]);
console.log(chunk["message"]["model"]);
break;
case "content_block_delta":
const currentText = chunk["delta"]["text"]
setBotChat(prev => prev + currentText)
if (chunk["delta"]["stop_reason"] === "max_tokens") {
return
}
break;
case "message_delta":
if (chunk["delta"]["stop_reason"] === "end_turn") {
return
}
break;
case "message_stop":
const metrics = chunk["amazon-bedrock-invocationMetrics"];
console.log(metrics);
break;
default:
null
}
}
}
} finally {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
reader.releaseLock()
}
}
})
}
return (
<div className="h-screen flex flex-col gap-4">
<header className="p-4 grid place-items-center">
<h1 className="text-2xl font-semibold">Chat</h1>
</header>
<main className="flex-1 flex flex-col p-4">
<div className="grid gap-4">
{userChat &&
<div className="flex flex-col items-end gap-1">
<div className="flex flex-col max-w-[75%] rounded-lg p-4 bg-gray-100">
<div className="flex items-center gap-2 text-sm">
<div className="font-medium bg-slate-500 text-white px-2 py-1 rounded-full">You</div>
<time className="opacity-70">{new Date().getHours()}</time>
</div>
<div className="mt-2">{userChat}</div>
</div>
</div>
}
{botChat &&
<div className="flex flex-col items-start gap-1">
<div className="flex flex-col max-w-[75%] rounded-lg p-4 bg-gray-100">
<div className="flex items-center gap-2 text-sm">
<div className="font-medium bg-green-600 text-white px-2 py-1 rounded-full">Bed Rock</div>
<time className="opacity-70">{new Date().getHours()}</time>
</div>
<div className="mt-2 whitespace-pre-wrap">{botChat}</div>
</div>
</div>
}
{isPending && botChat.length === 0 && <div className="flex flex-col items-start gap-1">
<div className="flex flex-col max-w-[75%] rounded-lg p-4 bg-gray-100">
<div className="flex items-center gap-2 text-sm">
<div className="font-medium">Bed Rock</div>
</div>
<div className="mt-2">考え中...</div>
</div>
</div>}
</div>
</main>
<div className="border-t p-4">
<form className="flex gap-4" onSubmit={onSend}>
<input
placeholder="Type a message"
className="flex-1 p-3 rounded-lg border border-gray-300 focus:outline-none focus:ring focus:ring-gray-400"
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
<button type="submit" className="px-6 py-3 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors">Send</button>
</form>
</div>
</div>
)
}
これでひとまず、実行環境はできました!✨
ただ、連続でチャットができないので、コードを書き足す必要があります。今回は最小限の実行環境を作成しました。
今回は手弁当でStreamの部分を作成しましたが、Vercelから便利がライブラリが出ていました。こちらも活用すると、より早く作成できるのではないかと思います!
📌 最後に
今回はNext.jsでClaude3 Sonnet(Bedrock)の実行環境をつくる方法をまとめました。
なにせ準備が大変です笑
いろいろと雑に作ってしまったので、これからリファクタリングしていこうと思います!
素振りしたリポジトリを少しずつ更新していきますので、参考になれば幸いです。
より良い方法があれば教えてください〜
最後まで読んでいただきありがとうございます!
気ままにつぶやいているので、気軽にフォローをお願いします!🥺
Discussion