📢

Bun & Honoで始めるAmazon Bedrock入門

2023/09/30に公開

はじめに

AWSで利用できる生成AIのサービス、BedrockがGAになりました。

https://aws.amazon.com/jp/blogs/aws/amazon-bedrock-is-now-generally-available-build-and-scale-generative-ai-applications-with-foundation-models/

コンソールで触ってみた系、PythonのSDK(boto3)で触ってみた系は沢山あるので、Typescriptに加えて私の好きな技術スタックで遊んでみようと思います。

環境

前提としてCloud9(Ubuntu 22.04 LTS)で動かしています。

Bun

BunはTypescriptが簡単に使える爆速ランタイム。
https://bun.sh/

こうやって簡単にインストールできます。

curl -fsSL https://bun.sh/install | bash 
source /home/ubuntu/.bashrc
bun --version
1.0.3

Hono

HonoはCDNエッジをはじめ、爆速、軽量、かつポータビリティが高いWebフレームワーク。

https://hono.dev/getting-started/bun

OpenAPIやstream Responseにも対応しており、AI時代にも使いやすいです。

https://blog.yusu.ke/hono-ai-ready/

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」モデルを使っていきます。

https://aws.amazon.com/marketplace/pp/prodview-mwa5sjvsopoku

前提となる必要なライブラリをあらかじめ入れておきます。

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を使います。

https://docs.aws.amazon.com/bedrock/latest/APIReference/welcome.html

コードはこんな感じ!

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ユーザ的にも覚えておくと便利です。

最後にコード全文も貼っておきます。それでは!

src/index.ts
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