どうもこんにちは。今回の記事では、前回の記事で構築したMemoryに保存した記憶をWebアプリ側へ呼び出して画面へレンダリングしてみようと思います。
AIエージェント側へのMemory実装は以下の記事をご参照ください。
今回は何をする?
- AIエージェントとのやりとり(Memory)をAWS SDKから呼び出す
- 呼び出したMemoryを画面へ表示する
RailsアプリケーションとAIエージェントの関係
以下のように、AWS SDKを通じて、RailsアプリケーションからAIエージェントを呼び出しています。
また、AIチャット画面はチャットルームの閲覧画面のパスに実装をしています。ルーティングは以下のようになっています。
get '/ai_chat_rooms' => 'ai_chat_rooms#index'
get '/ai_chat_rooms/new' => 'ai_chat_rooms#new'
post '/ai_chat_rooms' => 'ai_chat_rooms#create'
get '/ai_chat_rooms/:id' => 'ai_chat_rooms#show'
get '/ai_chat_rooms/:id/edit' => 'ai_chat_rooms#edit'
patch '/ai_chat_rooms/:id' => 'ai_chat_rooms#update'
delete '/ai_chat_rooms/:id' => 'ai_chat_rooms#destroy'
get '/ai_chat_rooms/:id/chat' => 'ai_chat_rooms#chat' # ユーザからメッセージを送信 → 受信
get '/ai_chat_rooms/:id/get_chat_history' => 'ai_chat_rooms#get_chat_history' # 履歴を受信
# 上記を簡潔に記載すると、以下のようになります。
resources :ai_chat_rooms do
member do
get :chat
get :get_chat_history
end
end
Railsアプリケーション側のDB構成
スキーマファイルは以下のようになっています。
session_idがnullを許容しているのは、チャットルームを新規作成したタイミングではsession_idを発行せず、チャット開始時に発行されたsession_idを格納するように設計したためです。
create_table "ai_chat_rooms", charset: "utf8", force: :cascade do |t|
t.string "name", null: false
t.integer "user_id", null: false
t.string "session_id"
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["session_id"], name: "index_ai_chat_rooms_on_session_id", unique: true
t.index ["user_id"], name: "index_ai_chat_rooms_on_user_id"
end
RailsアプリでAIチャットを実装するために
RailsアプリでAIチャットを実装するために、ActionController::Liveを採用しています。
これをincludeすることによって、ストリーミングに対応することができます。
class AiChatController < ApplicationController
include ActionController::Live
#...中略...
リクエスト&レスポンスの処理の流れ
処理は以下の流れで行います。
- ユーザがテキストボックスにリクエスト文を入力し、送信ボタンをクリックする
- クリックイベントが発火し、非同期でRailsサーバへ通知される
- RailsサーバからAWS SDK経由でBedrock AgentCore Runtime APIが呼ばれる
- (Bedrock AgentCore Runtime側でやりとり履歴がMemoryに保存される)
- Railsサーバがレスポンスを受け取る
- 画面にレスポンスを表示する
APIリクエストを送信する際に、「どのユーザのやりとりなのか」「どのセッションのやりとりなのか」をパラメータに含める必要があります。
どのユーザのやりとりなのかは、ログインユーザのusernameを使用します。どのセッションのやりとりなのかは、チャットルームに保存しているsession_idを使用します。初回リクエストの場合は、その場でsesison_idを発行します。
これを踏まえると、以下のようなコードでAPIリクエストを送信することになります。
# チャットルームIDを取得
room_id = params[:room_id]
# チャットルームObjectを取得
room_obj = Room.find(room_id)
# セッションIDを取得
session_id = if room_obj.session_id.present?
room_obj.session_id
else
SecureRandom.hex(24)
end
# ログインユーザのusernameを取得
user_name = current_user.username
# リクエスト文を取得
text = params.require(:text)
# AWS Bedrock AgentCore Runtime クライアントの初期化
region = ENV.fetch('BEDROCK_AGENTCORE_AWS_REGION')
client = if Rails.env.development?
creds = Aws::Credentials.new(
ENV['BEDROCK_AGENTCORE_AWS_ACCESS_KEY'],
ENV['BEDROCK_AGENTCORE_AWS_SECRET_ACCESS_KEY']
)
Aws::BedrockAgentCore::Client.new(
region: region,
credentials: creds,
http_read_timeout: 900,
http_open_timeout: 30
)
else
Aws::BedrockAgentCore::Client.new(
region: region,
http_read_timeout: 900,
http_open_timeout: 30
)
end
# ペイロードを生成
payload = {
input: {
prompt: text
},
session_data: {
session_id: session_id,
actor_id: user_name
}
}.to_json
# リクエストを送信
resp = client.invoke_agent_runtime(
agent_runtime_arn: ENV['BEDROCK_AGENTCORE_AGENT_RUNTIME_ARN'],
runtime_session_id: session_id,
payload: payload,
qualifier: 'DEFAULT',
content_type: 'application/json',
accept: 'application/json'
)
上記のようなコードでAPIリクエストを送信します。AIエージェント側でペイロードを受け取れるように実装をしておいてください。
画面が更新されたり、他の画面から遷移してきた時の処理の流れ
これに加えて、画面が更新された時に、過去のやりとりも画面に表示されている必要があります。その場合は以下の流れで処理を行います。
- 画面を更新する
- RailsサーバからAWS SDK経由でBedrock AgentCore Memory APIが呼ばれる
- Railsサーバがレスポンス(履歴データ)を受け取る
- 画面にレスポンス(履歴データ)を表示する
今回は、画面が更新されたタイミングで、JS側からRailsサーバへ履歴取得のアクションを実行します。
const getChatHistory = async () => {
try {
// 下に記載している`get_chat_history`を呼び出し
const response = await fetch(`/ai_chat_rooms/${roomId}/get_chat_history`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (data.messages && Array.isArray(data.messages)) {
data.messages.forEach(entry => {
if (entry.role === 'USER') {
appendUserMessage(entry.text); // ユーザのメッセージを描画するための関数
} else if (entry.role === 'ASSISTANT') {
appendAssistantMessage(entry.text); // AIのメッセージを描画するための関数
}
});
}
} catch (err) {
console.error('[AI_CHAT] Failed to fetch chat history:', err);
}
};
getChatHistory();
before_action :set_ai_chat_room, only: %i[show edit update destroy get_chat_history]
def get_chat_history
session_id = @ai_chat_room.session_id
return if session_id.blank?
# AWS SDKで履歴を取得(Short-term memory)
res = @client.list_events({
memory_id: "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
actor_id: current_user.username,
session_id: session_id
})
# 時系列順にソート
events = res.events.sort_by(&:event_timestamp)
# 受信した履歴を整理
messages = []
events.each do |event|
event.payload.each do |payload|
if payload.conversational
messages << {
role: payload.conversational.role,
text: payload.conversational.content.text
}
end
end
end
# JSへ返却
respond_to do |format|
format.json { render json: { status: 'success', messages: messages } }
end
end
private
def set_ai_chat_room
@ai_chat_room = current_user.ai_chat_rooms.find(params[:id])
end
上記のように処理を行うことで、「画面を更新した時になかなか画面が表示されない」という問題は解消されます。
自分が初心者エンジニアの時は、showアクションに履歴取得のコードを書いていたと思います。
showに書いてしまうと、「大量の履歴を取得してHTMLの生成が完了するまで画面が表示されない」という問題が発生します。
そうならないように、非同期で実装することをお勧めします。
list_eventsで履歴の取得ができない場合
以下の点を確認してみてください。
- AWS SDKのAPI呼び出しで失敗していないか
- この場合、Rails側のシンタックスエラーなどではないか確認する
- Rails側でない場合、AIエージェント側のエラーである可能性が高い
- AIエージェント側のエラーである場合
- ペイロードの受け取りはできているか
- Memoryの保存はできているか
AIエージェント側のエラーであるかは、Bedrock AgentCore Observabilityの設定ができていないと難しかったりします。もしくは、AIエージェントのagent.pyなどのprintにflush=trueを設定していないことが原因だったりします。
まとめ
履歴を保存するためのテーブルを作成しなくて良いのが楽!
画面表示が遅くなるデメリットも非同期処理にすればひとまずOKかなぁ。
