Amazon Bedrock の「生成AIによるチャット、画像生成」を Livebook から実行する

## はじめに

Amazon Bedrock は AWS から各種生成 AI を呼び出し、テキスト生成や画像生成を実行するサービスです

ブラウザからお手軽に試す場合、以下の記事を参考にしてください

https://qiita.com/RyoWakabayashi/private/5680a4bac246a1a0c532

本記事では Elixir の Livebook から Bedrock を使い、生成AIによるチャット、画像生成を実装します

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/aws/bedrock.livemd

## Livebook とは

Livebook は Elixir　のコードをブラウザから実行し、結果を表示してくれるツールです

https://livebook.dev/

Python における Jupyter のようなもので、 Elixir 入門や Elixir を使ったデータ分析、データの視覚化などに適しています

はじめ方は以下の記事を参考にしてください

https://qiita.com/RyoWakabayashi/items/dc82ad3ed866d58a8acf

## セットアップ

Livebook で新しいノートブックを開いたら、先頭のセットアップセルに以下のコードを入力し、実行してください

```elixir
Mix.install([
  {:aws, git: "https://github.com/aws-beam/aws-elixir/"},
  {:hackney, "~> 1.20"},
  {:kino, "~> 0.11"}
])
```

AWS Elixir というモジュールをインストールしています

https://github.com/aws-beam/aws-elixir/tree/master

まだリリースされているバージョンには Bedrock 用のモジュールが含まれていないため、 GitHub から最新版をインストールしています

## クライアントの準備

AWS の認証情報を入力するためのテキストボックスを準備します

```elixir
access_key_id_input = Kino.Input.password("ACCESS_KEY_ID")
secret_access_key_input = Kino.Input.password("SECRET_ACCESS_KEY")
region_input = Kino.Input.text("REGION")

[
  access_key_id_input,
  secret_access_key_input,
  region_input
]
|> Kino.Layout.grid(columns: 3)
```

セルを実行して表示されたテキストボックスにそれぞれ入力してください

リージョンは Bedrock を使用するリージョンなので、東京リージョン（ap-northeast-1）で使えないモデルを使いたい場合、バージニア北部（us-east-1）などを指定してください

![スクリーンショット 2023-12-03 23.53.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ddaeb2f0-6469-ead9-5815-2ec34dfbe88b.png)

AWS の API と通信するためのクライアントを用意します
この際、先ほどの認証情報を受け渡します

```elixir
client =
  AWS.Client.create(
    Kino.Input.read(access_key_id_input),
    Kino.Input.read(secret_access_key_input),
    Kino.Input.read(region_input)
  )
```

## 基盤モデル一覧の取得

入力したリージョンで使用可能な基盤モデルの一覧を表示します

```elixir
models =
  client
  |> AWS.Bedrock.list_foundation_models()
  |> elem(1)
  |> Map.get("modelSummaries")

Kino.DataTable.new(models)
```

`Kino.DataTable` を使うことで、結果が検索やソートも可能なテーブル表示になります

![table.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3dfa0c77-8db3-72ec-4348-db7591e6f911.gif)

テーブル表示した情報を参考にしながら、使いたい基盤モデルの情報を取得します
例えば Titan Text を使う場合、以下のように `providerName` が `Amazon` かつ `modelName` が `Titan Text G1 - Express` かつ 
`inferenceTypesSupported` が `["ON_DEMAND"]` の条件を指定して検索できます

同じモデルでも課金体系によって `ON_DEMAND` と `PROVISIONED` が存在する点に注意してください
考え方は DynamoDB と同じように、完全に呼ばれる毎に課金されるのが `ON_DEMAND` で、一定の性能を確保して時間毎固定料金になる `PROVISIONED` です

本記事では数回しか呼ばないため、 `ON_DEMAND` を使用します

```elixir
titan_express_model =
  models
  |> Enum.find(fn model ->
    model["providerName"] == "Amazon" and
    model["modelName"] == "Titan Text G1 - Express" and
    model["inferenceTypesSupported"] == ["ON_DEMAND"]
  end)
```

実行結果
```elixir
%{
  "customizationsSupported" => [],
  "inferenceTypesSupported" => ["ON_DEMAND"],
  "inputModalities" => ["TEXT"],
  "modelArn" => "arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1",
  "modelId" => "amazon.titan-text-express-v1",
  "modelLifecycle" => %{"status" => "ACTIVE"},
  "modelName" => "Titan Text G1 - Express",
  "outputModalities" => ["TEXT"],
  "providerName" => "Amazon",
  "responseStreamingSupported" => true
}
```

この情報のうち、基盤モデルを API 経由で呼び出す場合には `modelId` の値が必要になるため、取得しておきます

```elixir
titan_express_model_id = titan_express_model["modelId"]
```

実行結果
```elixir
"amazon.titan-text-express-v1"
```

日本語対応テキスト生成の Claude も使いたいのでモデル ID を取得しておきます

```elixir
claude_model_id =
  models
  |> Enum.find(fn model ->
    model["modelName"] == "Claude" and
    model["inferenceTypesSupported"] == ["ON_DEMAND"]
  end)
  |> Map.get("modelId")
```

実行結果
```elixir
"anthropic.claude-v1"
```

画像生成用のモデル ID も取得します

```elixir
titan_image_id =
  models
  |> Enum.find(fn model ->
    model["modelName"] == "Titan Image Generator G1" and
    model["inferenceTypesSupported"] == ["ON_DEMAND"]
  end)
  |> Map.get("modelId")
```

実行結果
```elixir
"amazon.titan-image-generator-v1"
```

## テキスト生成（チャット）

テキスト生成によるチャットを実装してみます

まず、生成 AI に投げる質問を用意します

Titan は日本語非対応なので、英語で「Elixir での　Hello, World のコードを教えて」と聞いてみましょう

```elixir
input = "Please show me the code to display \"Hello, World\" in Elixir."
```

以下のようにして Bedrock の基盤モデルを呼び出します

```elixir
results =
  client
  |> AWS.BedrockRuntime.invoke_model(
    titan_express_model_id,
    %{
      "accept" => "application/json",
      "contentType" => "application/json",
      "inputText" => input
    }
  )
  |> elem(1)
  |> Map.get("results")
```

第1引数が用意しておいたクライアント、第2引数がモデル ID 、第3引数が生成 AI に渡す入力です

第3引数の `"accept" => "application/json"` と `"contentType" => "application/json"` は API リクエスト時のヘッダーとして指定するため、固定で必ず必要です

それ以外の項目と、出力形式については基盤モデル毎にバラバラな仕様になっているため、個別に仕様を確認してください

https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters.html

Titan の場合、以下のような形式で結果が返ってきます

実行結果
```elixir
[
  %{
    "completionReason" => "LENGTH",
    "outputText" => "\nTo display \"Hello, World\" in Elixir, you can use the \"IO.puts\" function. Here is an example:\n```Elixir\nIO.puts \"Hello, World\"\n```\nIn Elixir, the \"IO.puts\" function is used to print messages to the console. In this case, it will output \"Hello, World\" to the console.\nYou can also use formatting options to customize the output. For example, you can use \"IO.inspect\" to pretty-print the message:\n```Elixir\nIO.inspect \"Hello, World\"",
    "tokenCount" => 128
  }
]
```

生成 AI の出力は Markdown になっているため、ノートブック上で Markdown として表示してみましょう

```elixir
results
|> Enum.at(0)
|> Map.get("outputText")
|> Kino.Markdown.new()
```

実行結果
![スクリーンショット 2023-12-04 0.22.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f4e88799-9add-c33d-3caa-eead9297d4d2.png)

ちゃんと Hello, World のコードを解説付きで生成してくれました

## 日本語チャット

次に Claude による日本語チャットを実装します

今度は少し難しい問題として、 Elixir で 5 の階乗を計算するコードを生成してもらいます

```elixir
input = "Elixirで5の階乗を計算するコードを教えてください。"

result =
  client
  |> AWS.BedrockRuntime.invoke_model(
    claude_model_id,
    %{
      "accept" => "application/json",
      "contentType" => "application/json",
      "prompt" => "\n\nHuman: #{input}\n\nAssistant:",
      "max_tokens_to_sample" => 300
    },
    recv_timeout: 60_000
  )
  |> elem(1)
  |> Map.get("completion")
```

Claude の場合、入力テキストを `prompt` に　`\n\nHuman: <入力テキスト>\n\nAssistant:` という形式で入れなければならない、という点に注意してください
`\n\nHuman: ` で始まり、 `\n\nAssistant:` で終わる、という条件を満たしていないとエラーが返ります
`max_tokens_to_sample` も必須項目なので指定してください

処理に少し時間がかかるため、第4引数のオプションに `recv_timeout: 60_000` を指定し、タイムアウトを 1 分まで延長しています
これを指定しないとタイムアウトエラーが発生します

Titan の場合、結果は `results` に入っていましたが、 Claude の場合は `completion` に入っている点も要注意です

結果を表示してみましょう

```elixir
result
|> Kino.Markdown.new()
```

実行結果
![スクリーンショット 2023-12-04 0.31.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9e68217e-7afa-afdb-7027-10f258dda1c9.png)

しっかり Elixir らしい実装を日本語の解説とともに返してくれました

## 画像生成

最後に画像生成です

Titan に Elixir の初心者向けコミュニティ piyopiyo.ex の画像を生成してもらいましょう

https://piyopiyoex.connpass.com/

さすがに名前だけだと厳しいと思うので、「Elixir の赤ちゃんが卵から生まれている」「Elixir はかわいい顔をした紫色の雫」とヒントを与えます。

```elixir
input = "piyopiyo.ex Elixir babies are being born from eggs. Elixir is a purple drop with a pretty face."

results =
  client
  |> AWS.BedrockRuntime.invoke_model(
    titan_image_id,
    %{
      "accept" => "application/json",
      "contentType" => "application/json",
      "taskType" => "TEXT_IMAGE",
      "textToImageParams" => %{
        "text" => input
      },
      "imageGenerationConfig" => %{
        "numberOfImages" => 1,
        "quality" => "standard",
        "height" => 512,
        "width" => 512,
        "cfgScale" => 2.0,
        "seed" => 999
      }
    },
    recv_timeout: 60_000
  )
  |> elem(1)
  |> Map.get("images")
```

実行結果
```elixir
["iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOt...
```

結果を見て「何のこっちゃ」と思うかもしれませんが、これはバイナリデータを文字列で表現した Base64 という形式です

Base64 を PNG 画像として表示してみます

```elixir
results
|> Enum.at(0)
|> then(&"""
<img src="data:image/png;base64,#{&1}" />
""")
|> Kino.HTML.new()
```

実行結果
![piyopiyo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/da70fc64-5907-bdb7-312a-3021dc355d56.png)

当たらずも遠からず、といったところでしょうか

## まとめ

Elixir から Amazon Bedrock を呼び出し、生成 AI によるチャット、画像生成が実装できました

Bedrock は今後もモデルが増えるでしょうし、しかも自分のデータでモデルをカスタマイズすることも可能です

当然 AWS 上の Lambda などから呼び出したりなど、 AWS の各種サービスとの連携も容易になっています

今後、 Bedrock を使ったサービスがどんどん増えそうですね