LangChain(のチャットモデル)からBedrockのClaude3 Sonnet、Haikuが呼び出せるようになったので、画像チャットアプリをLCELで作成します。
よく読むとハルシネーションしてますね。Sonnetだとこんなものでしょうか。
以下をフュージョンしてLangChainの記法でClaude3を呼び出すように変更しました。
Haikuを使用する場合はモデルIDをanthropic.claude-3-haiku-20240307-v1:0
に変更してください
前提作業
- DynamoDBに
SessionTable
を作成してPython実行環境からの権限を付与しておく - 必要ライブラリのインストール(上記の両方を合わせた感じ)
pip install -U boto3 langchain langchain-community streamlit streamlit_authenticator numpy
プログラム
from langchain.globals import set_debug
set_debug(False) # debug時はTrue
import base64
import copy
from PIL import Image
import numpy as np
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage
from langchain_community.chat_message_histories import DynamoDBChatMessageHistory
from langchain_community.chat_models import BedrockChat
import streamlit as st
import streamlit_authenticator as sa
# 認証情報を作成
authenticator = sa.Authenticate(
credentials={"usernames":{
"user1":{"name":"user1","password":"pass"},
"user2":{"name":"user2","password":"pass"},
"user3":{"name":"user3","password":"pass"}}},
cookie_name="streamlit_cookie",
key="signature_key",
cookie_expiry_days=1
)
# ログイン画面描画
authenticator.login()
if st.session_state["authentication_status"] is False: #ログイン失敗
st.error('Username/password is incorrect')
elif st.session_state["authentication_status"] is None: #未入力
st.warning('Please enter your username and password')
else: #ログイン成功
st.sidebar.title("LangChain(LCEL) x Bedrock(Claude3 Sonnet)")
authenticator.logout(location="sidebar") #ログアウトボタンをサイドバーに表示
# ログイン直後っぽい時は過去のメッセージをクリアする
if "FormSubmitter:Login-Login" in st.session_state:
if "messages" in st.session_state:
st.session_state["messages"].clear()
# usernameをセッションIDのKeyとする
session_id = st.session_state["username"]
# DynamoDBの会話履歴(テーブル名"SessionTable"、TTL=3600秒)
message_history = DynamoDBChatMessageHistory(table_name="SessionTable", session_id=session_id, ttl=3600)
# プロンプトテンプレートを作成
prompt = ChatPromptTemplate.from_messages(
[
("system","あなたはAIチャットボットです"),
MessagesPlaceholder(variable_name="messages"), #ここにDynamoDBから取得した会話履歴を入れる
MessagesPlaceholder(variable_name="human_message")
]
)
# LLMの定義
LLM = BedrockChat(
model_id="anthropic.claude-3-sonnet-20240229-v1:0",
model_kwargs={"max_tokens": 1000},
)
# chainの定義
chain = prompt | LLM
# 画面表示用チャット履歴を初期化する
if "messages" not in st.session_state:
# 辞書形式で定義
st.session_state["messages"] = []
# これまでのチャット履歴を全て表示する
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# サイドバーに画像アップロード欄の作成
img_file = st.sidebar.file_uploader("Jpegファイルアップロード", type='jpg')
if img_file: # 画像がアップロードされている場合
img_file_copy = copy.copy(img_file) # コピーしてからサムネイル表示
image = Image.open(img_file_copy)
img_array = np.array(image)
st.sidebar.image(img_array,width = 300)
# 入力を求める
if input_text := st.chat_input("会話しましょう"):
# ユーザの入力を表示する
with st.chat_message("user"):
st.write(input_text)
# HumanMessageの組み立て
content = []
if img_file: # 画像がアップロードされている場合
image = img_file.read() # 読み込み
b64 = base64.b64encode(image).decode("utf-8") # Base64変換
# プロンプトの形式にしてcontent配列に追加
content_image={"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,"+b64}}
content.append(content_image)
# 入力テキストをcontent配列に追加
content_text = {"type": "text", "text": input_text}
content.append(content_text)
# chainの実行
result = chain.invoke({"messages": message_history.messages, "human_message": [HumanMessage(content=content)]})
# ChatBotの返答を表示する
with st.chat_message("assistant"):
st.write(result.content)
# セッション上のチャット履歴の更新
st.session_state.messages.append({"role": "user", "content": input_text})
st.session_state.messages.append({"role": "assistant", "content": result.content})
# DynamoDBのチャット履歴の更新(画像は登録しない)
message_history.add_user_message(input_text)
message_history.add_ai_message(result.content)
画像をJSONに設定している部分ですが、上記の記法でも、boto3の記法(Claude3のプロンプト仕様)でもどちらでも通ります。
あと補足すると、チャット履歴をDynamoDBに登録する際には画像は登録していません。チャット履歴はテキストのみで、最新の問い合わせにのみ画像のBase64形式を挿入しています。同じ画像でチャットを続ける場合は、常に最新の問い合わせに同じ画像が入ります。(この為、一度アップロードした画像を削除すると、Bedrockからは画像が見えない状態になる)
履歴に入れる事も出来そうなんですが、それをやると同じ画像でチャットを続けるとプロンプトサイズがえらいことになるのでこうしています。履歴に1件のみ入れる案もありますがStreamlitだと作りにくいので試していません。
他の部分の説明は過去のエントリを確認ください。
起動する
python -m streamlit run hogehoge.py
user1/passかuser2/passかuser3/passでログインしてください。
プロンプトを確認する
まずは画像無しでLangChainがどのようなプロンプトを生成しているか確認します。
上記の会話のプロンプトをClaudeWatch Logsで確認します。
"operation": "InvokeModel",
"modelId": "anthropic.claude-3-sonnet-20240229-v1:0",
"input": {
"inputContentType": "application/json",
"inputBodyJson": {
"max_tokens": 1000,
"anthropic_version": "bedrock-2023-05-31",
"messages": [
{
"role": "user",
"content": "初めまして"
},
{
"role": "assistant",
"content": "はい、初めまして。私はClaudeと申します。どのようなお話ができればよいでしょうか?あなたの質問にお答えしたり、話を楽しくしたりするのが私の役割です。ぜひ気軽に質問や要望を話してくださいね。"
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "アップロードする画像に関する会話をしたいです"
}
]
}
],
"system": "あなたはAIチャットボットです"
},
"inputTokenCount": 135
},
"output": {
"outputContentType": "application/json",
"outputBodyJson": {
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "はい、画像に関する会話をさせていただきます。画像をアップロードしていただければ、その画像の内容を分析し、詳しく説明することができます。物体の認識、シーンの理解、テキストの読み取りなど、画像から得られる情報を活用して、的確なフィードバックを心がけます。画像のアップロードをお待ちしております。"
}
],
"model": "claude-3-sonnet-28k-20240229",
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 135,
"output_tokens": 121
}
},
"outputTokenCount": 121
}
ChatPromptTemplate
に与えた("system","あなたはAIチャットボットです")
が"system"
とシステムプロンプトとして与えられています。その他、"anthropic_version"
など、プロンプトの仕様に従って設定されている事が分かります。
ロールの部分がどうなっているかを確認すると、以下のようにこちらもプロンプトの仕様に従って展開・設定されています。
"messages": [
{"role": "user", "content": "初めまして"},
{"role": "assistant", "content": "はい、初めまして。私はClaudeと申します。どのようなお話ができればよいでしょうか?あなたの質問にお答えしたり、話を楽しくしたりするのが私の役割です。ぜひ気軽に質問や要望を話してくださいね。"},
{"role": "user", "content": [{"type": "text","text": "アップロードする画像に関する会話をしたいです"}]}
最後のcontent
を配列にしているのは、画像を設定する場合にimageのみ追加すれば良いようにする為です。
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "/9j/4…(Base64エンコードされた画像ファイル)"
}
},
{
"type": "text",
"text": "画像の説明をしてください"
}
]
}
],
"system": "あなたはAIチャットボットです"
},
"inputTokenCount": 494
},
こちらもプロンプトの仕様に従って展開・設定されています。
余談ですがJpegファイルをBase64エンコードすると先頭は必ず/9j/4
になるそうです。
画像ファイルも入力トークン数としてカウントされているように見えますが、そこのカウント方法はちょっと分かりません。
入力が大きくなると(?)CloudWatch Logsでは確認できない事があるので、その場合はデバッグモードである程度は確認する事が出来ます。他に方法無いのかな。
[{
'type': 'image_url',
'image_url': {
'url': 'data:image/jpeg;base64,/9j/4…(Base64エンコードされた画像ファイル)
}
},
{
'type': 'text', 'text': '画像の説明をしてください'
}]"
この段階だとClaude3のプロンプト仕様には変換されていないので、そこは注意が必要です。
インターネットに公開したい場合
自己責任で以下