Claude + マルチモーダルのサンプルコード

アップロードしたAWS構成図をClaude V3に解説してもらうマルチモーダルなサンプルコードを書く必要があったのだが、とても苦労したので、後のどなたかのために貼っておく。

なお一番苦戦した`HumanMessage`の部分は、同僚S氏のサンプルコードを利用させていただいた。この場を借りて御礼申し上げます。

## 最終作成物

適当に作ったアーキテクチャー図を読み込ませ、Claudeに解説させる。

![スクリーンショット 2024-05-19 9.41.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/10811ecc-e7ab-992c-e574-5da121b04370.png)


## 使ったもの

| モデル/ライブラリ | バージョン |
| ---- | ---- |
| Claude | anthropic.claude-3-haiku-20240307-v1:0" |
| LangChain | 0.1.19 |
| langchain-aws | 0.1.0 |
| langchain-core | 0.1.52 |
| boto3 | 1.34.89 |
| Python | 3.11.9 |
| Streamlit | 1.34.0 |

## コード

> 2024/05/17 20:06
> 一部動作しない箇所があったため、修正しました。

```python:claude_multimodal.py
import streamlit as st
import base64
import boto3
from botocore.exceptions import ClientError
from PIL import Image
from langchain_aws import ChatBedrock
from langchain_core.messages import HumanMessage
from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate
)

# S3の設定
s3_client = boto3.client('s3')
bucket_name = '<サンプルコードなのでとりあえずバケット名をベタ打ち>'  # ご自身のバケットに変更して下さい
folder_name = 'Diagram'  # フォルダ名を指定

# プロンプトの定義
def get_prompt_templates():
    system_message_template = """
    ### システムプロンプト
    あなたはAWSおよびIT技術全般に精通しているエンジニアです。

    与えられたAWSアーキテクチャー図を分析し、なるべく詳細な技術情報を取り出します。
    その後、AWSの公式情報に基づき、技術情報を具体的に説明します。

    ### 構造的: マークダウンを使用して構造的に表現して下さい。タイトルに相当する行は、###を使用して強調表示して下さい。
    ### 誠実: 分からないことについては、分からない旨を明言して下さい。存在しない情報を捏造してはいけません。
    ### 要求: より詳細な説明のために追加の情報が必要な場合は、どのような情報が必要であるかを具体的に示して下さい。
    ### 言語: 関西弁で回答して下さい。
    """

    human_message_template = """
    以下はAWSアーキテクチャ図です。この図を見て、以下の4点を説明してください。

    1. そのアーキテクチャで何を実現しようとしているか（100文字程度）
    2. 使っているAWSサービスの一覧
    3. 当該アーキテクチャのポイント
    4. 改善点

    AWS サービスの一覧は、箇条書きで示して下さい。
    図の説明に当たっては、適切な解釈を加えて、簡潔かつ分かりやすく説明してください。
    """

    return system_message_template, human_message_template

# S3へのファイルアップロード
def upload_file_to_s3(file, bucket_name, folder_name, object_key):
    try:
        full_object_key = f"{folder_name}/{object_key}"
        s3_client.upload_fileobj(file, bucket_name, full_object_key)
        st.write(f"S3 バケット '{bucket_name}' に正常にアップロードされました。")
        return full_object_key
    except ClientError as e:
        print(f"S3へのアップロード中にエラーが発生しました: {e}")
        return False

# S3からのダウンロード
def download_image_from_s3(bucket_name, full_object_key):
    s3_client.download_file(bucket_name, full_object_key, full_object_key.split('/')[-1])
    return full_object_key.split('/')[-1]

# 画像のbase64エンコード
def encode_image_to_base64(image_path):
    with open(image_path, 'rb') as f:
        encoded_string = base64.b64encode(f.read()).decode('utf-8')
    return encoded_string

# Bedrockモデルの初期化
def initialize_bedrock():
    return ChatBedrock(
        model_id="anthropic.claude-3-haiku-20240307-v1:0",
        region_name="us-east-1",
        verbose=True,
        model_kwargs={
            "anthropic_version": "bedrock-2023-05-31",
            "temperature": 0.1,
            "top_p": 0.9,
            "top_k": 50,
            "max_tokens": 1000,
        },
    )

# プロンプト生成
def compose_prompt(system_message_template, human_message_template, encoded_string):
    system_message = SystemMessagePromptTemplate.from_template(system_message_template)
    human_message = HumanMessage(
        content=[
            {
                'type': 'image_url',
                'image_url': {"url": "data:image/png;base64,"+encoded_string}
            },
            {
                'type': 'text',
                'text': human_message_template,
            }
        ]
    )
    prompt = ChatPromptTemplate.from_messages([system_message, human_message])
    return prompt

# main
def main():
    st.title('関西弁アーキ図解説')

    # 1. アーキテクチャ図の画像アップロード
    uploaded_file = st.file_uploader("AWS アーキテクチャー図をアップロードして下さい。 (png, jpg, or jpeg)", type=["png", "jpg", "jpeg"])

    if uploaded_file is not None:
        # 2. S3に画像をアップロード
        st.write("S3 バケットにアップロードしています...")
        object_key = uploaded_file.name
        full_object_key = upload_file_to_s3(uploaded_file, bucket_name, folder_name, object_key)
        st.info("回答を生成中...")

        # 3. LLMを初期化
        bedrock = initialize_bedrock()

        # 4. S3から画像をローカルにダウンロード
        local_image_path = download_image_from_s3(bucket_name, full_object_key)

        # 5. ダウンロードした画像をbase64エンコード
        encoded_string = encode_image_to_base64(local_image_path)

        # 6. プロンプト用のテキストを取得
        system_message_tempate, human_message_template = get_prompt_templates()

        # 7. プロンプトを構成
        prompt = compose_prompt(system_message_tempate, human_message_template, encoded_string)

        # 8. アーキテクチャー概要を生成（位置変数を求められるため"dummy"という文字列を渡している）
        chain = prompt | bedrock 
        summary = chain.invoke({"dummy_message": "dummy"})

        # 9. 概要説明の表示
        st.success("完了！")
        st.write("## アーキテクチャーの概要")
        image = Image.open(local_image_path)
        st.image(image)
        st.write(summary.content)

    else:
        st.write("ユーザーの操作を待っています...")

if __name__ == "__main__":
    main()

```

## 実行方法

```
streamlit run claude_multimodal.py
```

## 苦労した箇所

### その1：画像の渡し方
画像を読ませて解説させたいので、プロンプトに「画像」と「テキスト」を渡す必要があったが、ここのやり方が分からず苦労した。
Claudeにも書かせてみたのだが、マルチモーダルは苦手なのか、なかなか動くコードにならない。
同僚S氏から、base64エンコードした変数をHumanMessageの二つ目の引数として渡すやり方を教わって、ようやく解決した。

```python
human_message = HumanMessage(
    content=[
        {
            'type': 'text',
            'text': human_message_template,
        },
        {
            'type': 'image_url',
            'image_url': {"url": "data:image/png;base64," + encoded_string}
        }
    ]
)
```

### その2:`ChatPromptTemplate`を使ったプロンプトの渡し方

前回のポストで同様のことはやっていたのだが、Claudeに任せていてよく腹落ちしていなかったので、今回あれこれと試してみた。

https://qiita.com/itsuki3/items/c41014c8bd2e76145ebc

最初は、以下のように呼び出そうとしていたのだが、あっさりエラーになった。

```python
summary = bedrock.invoke(prompt)
```
```
ValueError: Invalid input type <class 'langchain_core.prompts.chat.ChatPromptTemplate'>. Must be a PromptValue, str, or list of BaseMessages.
```

promptが`str`型の場合（先程のHumanMessageをそのまま渡す時）はこれで動作するのだが、システムプロンプトをくっつけて`ChatPromptTemplate`型にしていると、どうもこの呼び方ではダメらしい。


次に試したのが以下。

```python
chain = prompt | bedrock 
summary = chain.invoke()
```

位置変数を寄越せと言われる。

```
TypeError: RunnableSequence.invoke() missing 1 required positional argument: 'input'
```

最終的に以下の形にしてようやく動作した。
ダミーを渡すのが正しいのかは分からないが、位置変数問題は一応回避できた。
なお、実際のプロンプトはChatPromptTemplateにシステムプロンプトとヒューマンプロンプトの組み合わせとしてpromptに格納されており、そちらが使われている。

```python
chain = prompt | bedrock 
summary = chain.invoke({"dummy_message": "dummy"})
```

## まとめ

とにかく練習あるのみ。
LangChainは変化が速すぎて、LLM側の知識も陳腐化していっている気がする。。。結局、公式ドキュメントや先人のコード、ブログが頼り。皆様ありがとうございます。

先週ChatGPT-4oが発表されて、マルチモーダルも次の段階に入った感がある。
Claudeの進化を待つ必要があるが、また時間を見つけて試してみたい。