Bun & Honoで始めるAmazon Bedrock入門
はじめに
AWSで利用できる生成AIのサービス、BedrockがGAになりました。
コンソールで触ってみた系、PythonのSDK(boto3)で触ってみた系は沢山あるので、Typescriptに加えて私の好きな技術スタックで遊んでみようと思います。
環境
前提としてCloud9(Ubuntu 22.04 LTS)で動かしています。
Bun
BunはTypescriptが簡単に使える爆速ランタイム。
こうやって簡単にインストールできます。
curl -fsSL https://bun.sh/install | bash
source /home/ubuntu/.bashrc
bun --version
1.0.3
Hono
HonoはCDNエッジをはじめ、爆速、軽量、かつポータビリティが高いWebフレームワーク。
OpenAPIやstream Responseにも対応しており、AI時代にも使いやすいです。
bun create hono bedrock-api
create-hono version 0.2.6
✔ Which template do you want to use? › bun
cloned honojs/starter#main to /home/ubuntu/environment/bedrock-api
✔ Copied project files
cd bedrock-api
bun install
これでサーバが起動して、疎通が確認できます。
bun run dev
curl http://localhost:8080/
Hello Hono!
コード
それではHonoでBedrockをプロキシするAPIを作っていきましょう。
今回はBedrockで使いやすくなった「Claude instant」モデルを使っていきます。
前提となる必要なライブラリをあらかじめ入れておきます。
bun i @aws-sdk/client-bedrock-runtime @hono/zod-validator zod
1. InvokeModel
こうやって書きます。ZodのValidationは趣味です。
const claudeInstantSchema = z.object({
prompt: z.string(),
max_tokens_to_sample: z.number().int().positive(),
temperature: z.number().min(0).max(1),
top_k: z.number().int().positive(),
top_p: z.number().min(0).max(1),
stop_sequences: z.array(z.string()),
});
const bedrockClient = new BedrockRuntimeClient({ region: "us-east-1" });
const app = new Hono();
app.get("/", (c) => c.text("Hello Hono!"));
app.post(
"/bedrock/claude/instant",
zValidator("json", claudeInstantSchema),
async (c) => {
const body = c.req.valid("json");
const params = {
modelId: "anthropic.claude-instant-v1",
body: JSON.stringify(body),
contentType: "application/json",
};
const command = new InvokeModelCommand(params);
const response = await bedrockClient.send(command);
return c.text(
JSON.parse(Buffer.from(response.body).toString("utf-8")).completion
);
},
);
export default app;
起動すると、こうやって結果が返ってきます。
curl -X POST http://localhost:8080/bedrock/claude/instant -H "Content-Type: application/json" -d '{
"prompt": "Human: ランタイムのBunについて詳しく悦明して\\n\\nAssistant:",
"max_tokens_to_sample": 500,
"temperature": 0.5,
"top_k": 250,
"top_p": 1,
"stop_sequences": ["\\n\\nHuman:"]
}'
"Bunは、Rustで作成されたWebアプリケーションフレームワークです。\\n\\n主な特徴は以下のとおりです:\\n\\n- Rustで書かれているため、高速で安全性が高い\\n- シングルバイナリ構成で、 デプロイが簡単\\n- 非同期処理に対応している\\n- テンプレートエンジンとしてTeraテンプレートをサポート\\n- RESTful APIの作成に適している\\n- コンポーネントベースのアーキテクチャで構成可能\\n- 簡単 なルーティングシステム\\n- 静的ファイルのサービング機能\\n- 環境変数などの設定ファイルに対応\\n\\nBunのランタイムはRustで実装されており、WebサーバーとしてHyperを使用しています。\\nアプリケーショ ンコードはRustで記述しますが、テンプレートエンジンとしてTeraテンプレートを利用できるため、HTMLテンプレートもRust+Teraのシンタックスで記述できます。\\n\\n非同期処理にも対応しているため、データベースアクセスなどブロッキング処理を非同期に行うことができ、スケーラビリティに優れています。\\n\\n以上がBunのランタイムに関する主な点だと思います。Rust製Webフレームワークとして性能と安全性に優れてい ます。\"
それっぽい日本語が返ってきました! ちなみにBunはZig言語で作られています!!
2. InvokeModelWithResponseStream
あの結果をヌルヌル返すやつも作りたいですよね。その場合はInvokeModel
でなくInvokeModelWithResponseStream
を使います。
コードはこんな感じ!
app.post(
"/stream/bedrock/claude/instant",
zValidator("json", claudeInstantSchema),
async (c) => {
const body = c.req.valid("json");
const params = {
modelId: "anthropic.claude-instant-v1",
body: JSON.stringify(body),
contentType: "application/json",
};
const command = new InvokeModelWithResponseStreamCommand(params);
try {
const response = await bedrockClient.send(command);
return c.streamText(async (stream) => {
for await (const event of response.body) {
if (event.chunk) {
const completion = JSON.parse(
Buffer.from(event.chunk.bytes).toString("utf-8")
).completion;
await stream.write(completion.trim());
} else if (
event.internalServerException ||
event.modelStreamErrorException ||
event.throttlingException ||
event.validationException
) {
console.error("Error event received:", event);
await stream.write("Error occurred.");
break;
}
}
});
} catch (error) {
console.error("Error handling request:", error);
return c.text("Error occurred.");
}
},
);
書けました。こんな感じで結果がヌルヌル返ってきます。
curl -X POST http://localhost:8080/stream/bedrock/claude/instant -H "Content-Type: application/json" -d '{
"prompt": "Human: WebフレームワークのHonoについて詳しく悦明して\\n\\nAssistant:",
"max_tokens_to_sample": 500,
"temperature": 0.5,
"top_k": 250,
"top_p": 1,
"stop_sequences": ["\\n\\nHuman:"]
}'
Honoは、オープンソースのJavaベースのWebアプリケーションフレームワークです。
主な特徴は以下の通りです:
- MVCアーキテクチャに基づいており、View(HTMLなど)とController(Javaコード)を明確に分離しています。
- デフォルトでJSPをViewの技術としてサポートしていますが、FreemarkerやThymeleafなど他のテンプレートエンジンにも対応できます。
- コントローラはAnnotationベースで開発でき、クラスパススキャンで自動マッピングされます。
- データバインディング機能でViewとモデルのデータの同期を容易に行えます。
- Spring Frameworkと統合されており、DIコンテナやAOPなどの機能を利用できます。
-セキュリティ機能として認証/認可機能、CSRF対策などが組み込まれています。
- JPAやMyBatisなどのORMフレームワークと組み合わせることができます。
- 他のWebコンポーネントとの連携性が高く、RESTfulなWebサービスの開発にも適しています。
なんかびっくりするぐらいハルシネーションしてますが、1行ずつ返ってくることが確認できました!
まとめ
というわけでBedrockをBun+Honoで作ったAPI越しに操作することができました。
HonoはCloudflare workersだけではなく、今回のBunや、NodejsでもAWS LambdaでもLambda@Edge
でも数行のコード追加で動くので、AWSユーザ的にも覚えておくと便利です。
最後にコード全文も貼っておきます。それでは!
import { Hono } from "hono";
import {
BedrockRuntimeClient,
InvokeModelCommand,
InvokeModelWithResponseStreamCommand,
} from "@aws-sdk/client-bedrock-runtime";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
const claudeInstantSchema = z.object({
prompt: z.string(),
max_tokens_to_sample: z.number().int().positive(),
temperature: z.number().min(0).max(1),
top_k: z.number().int().positive(),
top_p: z.number().min(0).max(1),
stop_sequences: z.array(z.string()),
});
const bedrockClient = new BedrockRuntimeClient({ region: "us-east-1" });
const app = new Hono();
app.get("/", (c) => c.text("Hello Hono!"));
app.post(
"/bedrock/claude/instant",
zValidator("json", claudeInstantSchema),
async (c) => {
const body = c.req.valid("json");
const params = {
modelId: "anthropic.claude-instant-v1",
body: JSON.stringify(body),
contentType: "application/json",
};
const command = new InvokeModelCommand(params);
const response = await bedrockClient.send(command);
return c.text(
JSON.parse(Buffer.from(response.body).toString("utf-8")).completion
);
},
);
app.post(
"/stream/bedrock/claude/instant",
zValidator("json", claudeInstantSchema),
async (c) => {
const body = c.req.valid("json");
const params = {
modelId: "anthropic.claude-instant-v1",
body: JSON.stringify(body),
contentType: "application/json",
};
const command = new InvokeModelWithResponseStreamCommand(params);
try {
const response = await bedrockClient.send(command);
return c.streamText(async (stream) => {
for await (const event of response.body) {
if (event.chunk) {
const completion = JSON.parse(
Buffer.from(event.chunk.bytes).toString("utf-8")
).completion;
await stream.write(completion.trim());
} else if (
event.internalServerException ||
event.modelStreamErrorException ||
event.throttlingException ||
event.validationException
) {
console.error("Error event received:", event);
await stream.write("Error occurred.");
break;
}
}
});
} catch (error) {
console.error("Error handling request:", error);
return c.text("Error occurred.");
}
},
);
export default app;
Discussion