Livebook で Claude 3.5 Sonnet を Amazon Bedrock の基盤モデルとして呼び出す

## はじめに

AWS の生成 AI サービス Bedrock を Livebook から呼び出します

2023 年 12 月にもやっていますが、今回は Claude 3.5 Sonnet に画像を与え、内容を説明してもらいます

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

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

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

## 事前準備

以下の記事を参考に、 AWS コンソールから基盤モデルのアクセス要求を出しておきましょう

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

また、 AWS にアクセスするための IAM ユーザー、認証情報が必要です

## セットアップ

必要なモジュールをインストールします

以前はまだ AWS Elixir の Bedrock 対応版がリリースされていませんでしたが、 2024 年 8 月現在は対応済です

```elixir
Mix.install([
  {:aws, "~> 1.0"},
  {:hackney, "~> 1.20"},
  {:kino, "~> 0.13"}
])
```

## シークレットの登録

AWS の認証情報をシークレットに登録します

- ACCESS_KEY_ID
- SECRET_ACCESS_KEY

![スクリーンショット 2024-08-01 22.27.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2e9b89e8-4d93-c0ce-28cb-8ec6d90ae2f8.png)

## クライアントの作成

AWS の API にアクセスするためのクライアントを作成します

シークレットに登録した値には `LB_` を先頭につけて環境変数としてアクセスします

リージョンは適宜変更してください

```elixir
client =
  AWS.Client.create(
    System.get_env("LB_ACCESS_KEY_ID"),
    System.get_env("LB_SECRET_ACCESS_KEY"),
    "us-east-1"
  )
```

## 基盤モデル一覧の確認

`AWS.Bedrock.list_foundation_models` で基盤モデルの一覧が取得できます

`Kino.DataTable.new` でテーブル表示してみましょう

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

keys = [
  "modelId",
  "modelName",
  "providerName",
  "inputModalities",
  "outputModalities"
]

Kino.DataTable.new(models, keys: keys)
```

実行結果

![スクリーンショット 2024-08-01 22.38.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3e0eead0-cd17-87f2-a1b8-e4e059447635.png)

確かに Claude 3.5 Sonnet が存在します

## Claude 3.5 Sonnet の呼び出し

Claude 1.0 は `AWS.BedrockRuntime.invoke_model` で呼び出せていましたが、 Claude 3.5 Sonnet では以下のようなエラーが発生します

```
"claude-3-5-sonnet-20240620" is not supported on this API. Please use the Messages API instead.
```

`invoke_model` の代わりに `converse` を使いましょう

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

{:ok, body, _response} =
  client
  |> AWS.BedrockRuntime.converse(
    model_id_claude,
    %{
      "messages" => [%{
        "role" => "user",
        "content" => [%{"text" => input}]
      }]
    },
    recv_timeout: 60_000
  )
```

実行結果

```elixir
{:ok,
 %{
   "metrics" => %{"latencyMs" => 10208},
   "output" => %{
     "message" => %{
       "content" => [
         %{
           "text" => "Elixirで5の階乗を計算するコードをいくつかの方法で示します：\n\n1. 再帰を使用する方法：\n\n```elixir\ndefmodule Factorial do\n  def of(0), do: 1\n  def of(n) when n > 0, do: n * of(n - 1)\nend\n\nIO.puts Factorial.of(5)  # 120\n```\n\n2. Enumモジュールを使用する方法：\n\n```elixir\nfactorial = fn n ->\n  1..n |> Enum.reduce(&*/2)\nend\n\nIO.puts factorial.(5)  # 120\n```\n\n3. リスト内包表記を使用する方法：\n\n```elixir\nfactorial = fn n ->\n  for i <- 1..n, reduce: 1, do: (acc -> acc * i)\nend\n\nIO.puts factorial.(5)  # 120\n```\n\n4. パイプ演算子を使用する方法：\n\n```elixir\nfactorial = fn n ->\n  1..n\n  |> Enum.to_list()\n  |> Enum.reduce(&*/2)\nend\n\nIO.puts factorial.(5)  # 120\n```\n\nこれらの方法はすべて5の階乗（5!）を計算し、結果として120を出力します。再帰を使用する方法が最も一般的で、数学的な定義に近いアプローチです。他の方法は、Elixirの異なる機能を示しています。\n\n実際の使用では、パフォーマンスや可読性を考慮して、適切な方法を選択してください。小さな数の階乗を計算する場合は、これらの方法の間で大きな性能差はありません。"
         }
       ],
       "role" => "assistant"
     }
   },
   "stopReason" => "end_turn",
   "usage" => %{"inputTokens" => 29, "outputTokens" => 463, "totalTokens" => 492}
 },
 %{
   body: "{\"metrics\":{\"latencyMs\":10208},\"output\":{\"message\":{\"content\":[{\"text\":\"Elixirで5の階乗を計算するコードをいくつかの方法で示します：\\n\\n1. 再帰を使用する方法：\\n\\n```elixir\\ndefmodule Factorial do\\n  def of(0), do: 1\\n  def of(n) when n > 0, do: n * of(n - 1)\\nend\\n\\nIO.puts Factorial.of(5)  # 120\\n```\\n\\n2. Enumモジュールを使用する方法：\\n\\n```elixir\\nfactorial = fn n ->\\n  1..n |> Enum.reduce(&*/2)\\nend\\n\\nIO.puts factorial.(5)  # 120\\n```\\n\\n3. リスト内包表記を使用する方法：\\n\\n```elixir\\nfactorial = fn n ->\\n  for i <- 1..n, reduce: 1, do: (acc -> acc * i)\\nend\\n\\nIO.puts factorial.(5)  # 120\\n```\\n\\n4. パイプ演算子を使用する方法：\\n\\n```elixir\\nfactorial = fn n ->\\n  1..n\\n  |> Enum.to_list()\\n  |> Enum.reduce(&*/2)\\nend\\n\\nIO.puts factorial.(5)  # 120\\n```\\n\\nこれらの方法はすべて5の階乗（5!）を計算し、結果として120を出力します。再帰を使用する方法が最も一般的で、数学的な定義に近いアプローチです。他の方法は、Elixirの異なる機能を示しています。\\n\\n実際の使用では、パフォーマンスや可読性を考慮して、適切な方法を選択してください。小さな数の階乗を計算する場合は、これらの方法の間で大きな性能差はありません。\"}],\"role\":\"assistant\"}},\"stopReason\":\"end_turn\",\"usage\":{\"inputTokens\":29,\"outputTokens\":463,\"totalTokens\":492}}",
   headers: [
     {"Date", "Thu, 01 Aug 2024 13:48:39 GMT"},
     {"Content-Type", "application/json"},
     {"Content-Length", "1474"},
     {"Connection", "keep-alive"},
     {"x-amzn-RequestId", "0bbc2381-ca9b-4a8d-9c9c-94600010ea28"}
   ],
   status_code: 200
 }}
```

生成されたテキストを Markdown として解釈しましょう

```elixir
body
|> Map.get("output")
|> Map.get("message")
|> Map.get("content")
|> Enum.at(0)
|> Map.get("text")
|> Kino.Markdown.new()
```

実行結果

![スクリーンショット 2024-08-01 22.54.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ba233090-1abb-d5d6-48f0-001be2c43624.png)

非常に多くのパターンを生成してくれました

参考として、 Claude 1.0 の生成結果は以下のとおり

![スクリーンショット 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)

## 画像の説明

Claude に画像の説明をしてもらいましょう

任意の画像をバイナリとして読み込みます

```elixir
image = File.read!("/home/livebook/vix/puppies.png")
```

Claude を呼び出す関数を用意します

```elixir
invoke_claude = fn content ->
  client
  |> AWS.BedrockRuntime.converse(
    model_id_claude,
    %{
      "messages" => [%{
        "role" => "user",
        "content" => content
      }]
    },
    recv_timeout: 60_000
  )
  |> elem(1)
  |> Map.get("output")
  |> Map.get("message")
  |> Map.get("content")
  |> Enum.at(0)
  |> Map.get("text")
end
```

画像のバイナリデータを Base64 エンコードして Bedrock に送ります

画像と生成結果を並べて表示してみましょう

```elixir
result =
  invoke_claude.([
    %{
      "image" => %{
        "format" => "png",
        "source" => %{"bytes" => Base.encode64(image)}
      }
    },
    %{"text"=> "画像に写っているものを説明してください"},
  ])

[
  image,
  Kino.Markdown.new(result)
]
|> Kino.Layout.grid(columns: 2)
```

実行結果

![スクリーンショット 2024-08-01 23.01.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/122416e9-87eb-1873-c93f-72c63502e685.png)

かなり詳細で正確な説明ができています

## フォームから選択した画像の説明

画像説明用の関数を用意します

```elixir
describe_image = fn image ->
  invoke_claude.([
    %{
      "image" => %{
        "format" => "png",
        "source" => %{"bytes" => Base.encode64(image)}
      }
    },
    %{"text"=> "画像に写っているものを説明してください"},
  ])
end
```

入出力の UI とフォーム送信時の処理を用意します

画像入力に `format: :png` を指定しているのが肝です

指定が漏れていると画像として処理できません

```elixir
# 入力用フォーム
form =
  Kino.Control.form(
    [
      image: Kino.Input.image("IMAGE", format: :png)
    ],
    submit: "Submit"
  )

# 出力用フレーム
frame = Kino.Frame.new()

# フォーム送信時の処理
Kino.listen(form, fn event ->
  image =
    event.data.image.file_ref
    |> Kino.Input.file_path()
    |> File.read!()

  result = describe_image.(image)

  result
  |> Kino.Markdown.new()
  |> then(&Kino.Frame.render(frame, &1))
end)

# 入出力を並べて表示
Kino.Layout.grid([form, frame], columns: 2)
```

実行結果

![スクリーンショット 2024-08-01 23.08.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/558a3d7a-5219-9d4c-4ca9-e2800e690bdc.png)

左側の入力フォームで画像を選択し、 "Submit" ボタンをクリックすると、画像の説明が右側に表示されます

![スクリーンショット 2024-08-01 23.12.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ef53be59-0737-77ac-a48f-028506e4ec34.png)

## アプリ化

以下の記事で紹介しているように、簡単にアプリ化することも可能です

https://qiita.com/RyoWakabayashi/items/9cd0032d1bd4ddad5d6c

![bedrock.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9bae2e6a-c10d-c45d-5fff-83f2b527ad4c.gif)

## まとめ

Claude 3.5 Sonnet を使うことで、かなり性能の高い AI アプリが実装できそうですね