動けばよし、の精神でやっています。もし誤り・補足があればコメントください。
はじめに
LangGraphで作った(作ってしまった)マルチエージェントをAmazon Bedrock AgentCore(以下、AgentCore)にデプロイし、
その応答を、GenUで表示できるようにしました。
少し工夫が必要だったので紹介します。
Amazon Bedrock AgentCoreとは: AWSの提供する、AIエージェント開発プラットフォームです。AIエージェント開発に必要なあらゆる機能があります。2025年7月に発表された新サービスで、執筆時点ではまだプレビューです。
https://aws.amazon.com/jp/blogs/news/introducing-amazon-bedrock-agentcore-securely-deploy-and-operate-ai-agents-at-any-scale/
まだ触ってない方は、こちらの記事を参考にされると良いかもしれません。
Strands & AgentCoreハンズオン! MCPマルチエージェントをAWSに簡単デプロイ
GenUとは: 『生成AIを安全に業務活用するための、ビジネスユースケース集を備えたアプリケーション実装』です。AWS公式のプロジェクトとして公開されています。
https://aws-samples.github.io/generative-ai-use-cases/ja/index.html
StrandsとLangGraph: どちらも、AIエージェント開発のフレームワークです。
LangGraphは、LangChainが開発、StrandsはAWSが提供していて、LangGraphの方が歴史の長いフレームワークです。
LangGraphは、ノード(処理ステップ)とエッジ(ノード間の遷移)を組み合わせたグラフ構造でワークフローを表現し、状態管理や条件分岐を細かく制御できる、というのが売りでした。
一方、Strandsは最近リリースされたばかりですが、開発が目覚ましく、今ではLangGraphとほぼ同等の機能を持つと言って良いでしょう。今、イチからやるなら、Strandsの方がお手軽で構築しやすいかと思います。
(その後、少しLangGraph等を勉強したところ、私の知らない機能がたくさんあることが分かったため撤回します。 が、それでも手軽に作るならStrandsをお勧めします)
背景
公式のAgentCoreサンプルの中に、SRE Agentがありました。2025年7月のAmazon Bedrock AgentCore発表後、自システムに対応するようにこれをベースにAIエージェントの開発を進めました。
そのままでは必要以上に複雑だったので、AgentCore Gatewayは無くしたり、私の担当システムがOCIだったので、OCI用にプロンプトやToolを書き換えました。
一方、普段の業務では GenU を使っており、2025年8月のアップデートでAgentCore対応しました。
サンプルのSRE Agent概要
以下、GitHubページからの抜粋になります
エージェントグラフ
次図のように、複数のエージェントからなるマルチエージェント構成で、
supervisorエージェントが、kubernetes/logs/metrics/runbooksエージェントから情報を収集または対処を実行し、AWS上のインフラの問題に対処します。
便利そうだし興味が湧きますよね?
構成図
課題
GenUをアップデートし、作成していたエージェントを適用しました、が動作しませんでした。
422エラーの原因は、
自作したagentcoreが受けられる型と要求の型が異なることです。
さらに、表示するには応答の値も異なるのでこれも合わせる必要があります。
解決案
概要
1. 要求の型を合わせる
まず、422エラーは、要求が異なること、とあります。
参考:https://docs.aws.amazon.com/ja_jp/bedrock-agentcore/latest/devguide/runtime-troubleshooting.html#common-error-codes
422 Unprocessable Entity
This happens when the container encounters validation issues with the input payload.
Common causes:
- Missing required fields in the payload (e.g., missing "input" field)
- Incorrect data types for fields
- Invalid format for the payload
GenUでは、次のフォーマットをpayloadに入れ込んで要求を出しています。なのでまず、自作エージェントもこれに応答できるよう合わせましょう。
以下のような感じで、GenUのpayloadを受け取ることができました。(重要な部分をハイライトしています)
from fastapi import Request
@app.post("/invocations")
+ async def invoke_agent(request: Request):
"""Main agent invocation endpoint."""
global agent_graph, tools
try:
+ body = await request.body()
+ body_str = body.decode()
+ request_data = json.loads(body_str)
if "input" in request_data and isinstance(request_data["input"], dict):
request_data = request_data["input"]
+ # messages = request_data.get("messages", [])
+ # system_prompt = request_data.get("systemPrompt", "")
+ # 私のAgenttCoreでは使わないので処理していません。
+ user_prompt_data = request_data.get("prompt", [])
+ model = request_data.get("model", {})
+ model_id = model.get("modelId", "")
+ provider = model.get("type", "bedrock").lower()
# Handle prompt format - extract text from array format
if isinstance(user_prompt_data, list) and len(user_prompt_data) > 0:
user_prompt = user_prompt_data[0].get("text", "")
elif isinstance(user_prompt_data, str):
user_prompt = user_prompt_data
else:
user_prompt = ""
...略
2. stream応答にする
json形式をyieldで返すようにします。ポイントは以下です。
-
yield json.dumps(return_json, ensure_ascii=False) + "\n"で応答するevent_generator()を定義する -
fastapiのStreamingResponseを使い、
StreamingResponse(event_generator(), media_type="text/event-stream")で返却 -
event_generator()の中で、graph.astream()の引数に、stream_mode=["updates"]を指定する(後述のように、他の値でも良いが簡単に制御したい場合はupdatesがお勧め)
from fastapi.responses import StreamingResponse
async def initialize_agent(
provider: str = "bedrock",
model_id: str = DEFAULT_MODEL_ID,
):
global agent_graph, tools
agent_graph, tools = ...略(agent_graphを構築)
@app.post("/invocations")
async def invoke_agent(request_data: dict):
global agent_graph, tools
await initialize_agent(provider=provider, model_id=model_id)
async def event_generator():
try:
async for event in agent_graph.astream(
initial_state,
stream_mode=["updates"],
):
return_json={...} #Strandsと同じ形式を定義(次の章を参照)
yield json.dumps(return_json, ensure_ascii=False) + "\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")
3. 応答の型を合わせる
これで、エラーは出なくなりましたが、応答は出ませんでした。GenUだと応答の処理はこのあたりです。
StrandsエージェントとLangGraphエージェントで応答の型が異なるので合わせます。
普通の出力と、traceへの出力でjsonの形式を分けます。
text = "ユーザ情報は..."
json = {
"event": {
"contentBlockDelta": { # 👈普通の出力は"contentBlockDelta"
"delta": {"text": text},
"contentBlockIndex": 0,
}
}
}
# traceのコードブロックへの出力を開始する。
tool_start_event = {
"event": {
"contentBlockStart": { # 👈traceのstartは"contentBlockStart"
"start": {
"toolUse": {
"toolUseId": tool_id,
"name": f"{node_name}==>{tool_name}",# 👈traceのtitle
}
},
"contentBlockIndex": 1,
}
}
}
# traceのコードブロックへの出力を更新
tool_delta_event = {
"event": {
"contentBlockDelta": { # 👈traceの差分更新は"contentBlockDelta"
"delta": {
"toolUse": {
"input": str(tool_args) # 👈tools_argsには、toolに渡した引数を格納しています
}
},
"contentBlockIndex": 1,
}
}
}
# traceのコードブロックの終了
tool_end_event = {
"event": {
"contentBlockStop": { # 👈traceの区切りは"contentBlockStop"
"contentBlockIndex": 1
}
}
}
# yieldで返す
yield json.dumps(tool_start_event, ensure_ascii=False) + "\n"
yield json.dumps(tool_delta_event, ensure_ascii=False) + "\n"
yield json.dumps(tool_end_event, ensure_ascii=False) + "\n"
補足
2. LangGraphのstream応答について
LangGraph - Stream outputs によると、graph.astream()の引数stream_modeは以下があります。
| モード | 説明 |
|---|---|
| values | グラフの各ステップの後に状態の完全な値をストリーミングします。 |
| updates | グラフの各ステップの後に状態の更新をストリーミングします。同じステップで複数の更新が行われた場合(例:複数のノードが実行される場合)、それらの更新は個別にストリーミングされます。 |
| custom | グラフノード内からカスタムデータをストリーミングします。 |
| messages | LLMが呼び出される任意のグラフノードから2タプル (LLMトークン、メタデータ) をストリームします。 |
| debug | グラフの実行全体を通じて可能な限り多くの情報をストリーミングします。 |
せっかくなので、これらのうち、values, updates, messages で応答の型を調べてみました。
今回構築するエージェントにおいては次のようになっていることが分かりました。
- updates
- サブエージェントの応答毎にまとめて値を返す
- ある程度のストリーミングが確保できればよいならこれで十分
- 今回のエージェントの場合、valuesとした時と、応答の差は見られなかった
- values
- updatesと中身はほぼ同じだが、キーが異なる
- messages
- サブエージェントの出力およびtoolの引数や実行結果を、よりリアルタイムに応答
- よりユーザフレンドリーにするなら、これが適切。ただし制御は複雑になりそう
以下に、具体的に示します。シナリオは次の通り。
東京は今何時?と聞く。
→ supervisorエージェントは、エージェントを1つ(時々2つ)選び時刻を尋ねる。
→ ルーティングされたエージェントは、`get_current_time`ツールを使用して時刻を回答。
→ supervisorエージェントが回答をまとめる
以下が応答です、長いので折りたたんでいます。
updates
updatesでのevent
(
"updates",
{
"infra_websearch_agent": {
"agent_results": {
"infra_websearch_agent": "現在の東京の時間は2025年9月19日 00時10分38秒です。\n\nget_current_timeツールの出力によると、東京(Asia/Tokyo)のタイムゾーンでの現在時刻は2025年9月19日の午前0時10分38秒(日本標準時 JST、UTC+9)となっています。"
},
"agents_invoked": ["infra_websearch_agent"],
"messages": [
HumanMessage(
content="東京は今何時?",
additional_kwargs={},
response_metadata={},
id="5968e2a2-478f-417b-b16e-b43ebae7c183",
),
AIMessage(
content="現在の東京の時間を確認します。",
additional_kwargs={
"usage": {
"prompt_tokens": 3458,
"completion_tokens": 72,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3530,
},
"stop_reason": "tool_use",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
response_metadata={
"usage": {
"prompt_tokens": 3458,
"completion_tokens": 72,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3530,
},
"stop_reason": "tool_use",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
id="run--76cef62d-53ac-4757-9e59-3ff75e67d092-0",
tool_calls=[
{
"name": "get_current_time",
"args": {"timezone": "Asia/Tokyo"},
"id": "toolu_bdrk_012zCQ7kgoQxh4FAsnBnvqgQ",
"type": "tool_call",
}
],
usage_metadata={
"input_tokens": 3458,
"output_tokens": 72,
"total_tokens": 3530,
"input_token_details": {"cache_creation": 0, "cache_read": 0},
},
),
ToolMessage(
content='{\n "timezone": "Asia/Tokyo",\n "datetime": "2025-09-19T00:10:38+09:00",\n "is_dst": false\n}',
name="get_current_time",
id="b3a74749-a04d-4bbb-940c-83bfa5596fef",
tool_call_id="toolu_bdrk_012zCQ7kgoQxh4FAsnBnvqgQ",
),
AIMessage(
content="現在の東京の時間は2025年9月19日 00時10分38秒です。\n\nget_current_timeツールの出力によると、東京(Asia/Tokyo)のタイムゾーンでの現在時刻は2025年9月19日の午前0時10分38秒(日本標準時 JST、UTC+9)となっています。",
additional_kwargs={
"usage": {
"prompt_tokens": 3583,
"completion_tokens": 102,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3685,
},
"stop_reason": "end_turn",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
response_metadata={
"usage": {
"prompt_tokens": 3583,
"completion_tokens": 102,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3685,
},
"stop_reason": "end_turn",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
id="run--370cec31-d727-4aa9-8088-a9abfbfce7f4-0",
usage_metadata={
"input_tokens": 3583,
"output_tokens": 102,
"total_tokens": 3685,
"input_token_details": {"cache_creation": 0, "cache_read": 0},
},
),
],
"metadata": {
"investigation_plan": {
"steps": [
"infra_websearch_agentを使用して東京の現在時刻を検索する",
"検索結果から正確な時刻情報を抽出して提示する",
],
"agents_sequence": ["infra_websearch_agent"],
"complexity": "simple",
"auto_execute": True,
"reasoning": "この質問はOCIインフラストラクチャやアプリケーションの問題ではなく、単純な時刻情報の検索です。infra_websearch_agentを使用して東京の現在時刻を検索するのが最も適切です。複雑な調査は必要なく、単一のエージェントで対応可能なシンプルなクエリです。",
},
"routing_reasoning": "Executing plan step 1: infra_websearch_agentを使用して東京の現在時刻を検索する",
"plan_step": 0,
"plan_text": "## 🔍 Investigation Plan\n\n**1.** infra_websearch_agentを使用して東京の現在時刻を検索する\n\n**2.** 検索結果から正確な時刻情報を抽出して提示する\n\n**📊 Complexity:** Simple\n**🤖 Auto-execute:** Yes\n**💭 Reasoning:** この質問はOCIインフラストラクチャやアプリケーションの問題ではなく、単純な時刻情報の検索です。infra_websearch_agentを使用して東京の現在時刻を検索するのが最も適切です。複雑な調査は必要なく、単一のエージェントで対応可能なシンプルなクエリです。\n**👥 Agents involved:** Infra Websearch Agent\n",
"show_plan": True,
"infra_websearch_agent_trace": [
AIMessage(
content="現在の東京の時間を確認します。",
additional_kwargs={
"usage": {
"prompt_tokens": 3458,
"completion_tokens": 72,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3530,
},
"stop_reason": "tool_use",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
response_metadata={
"usage": {
"prompt_tokens": 3458,
"completion_tokens": 72,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3530,
},
"stop_reason": "tool_use",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
id="run--76cef62d-53ac-4757-9e59-3ff75e67d092-0",
tool_calls=[
{
"name": "get_current_time",
"args": {"timezone": "Asia/Tokyo"},
"id": "toolu_bdrk_012zCQ7kgoQxh4FAsnBnvqgQ",
"type": "tool_call",
}
],
usage_metadata={
"input_tokens": 3458,
"output_tokens": 72,
"total_tokens": 3530,
"input_token_details": {
"cache_creation": 0,
"cache_read": 0,
},
},
),
ToolMessage(
content='{\n "timezone": "Asia/Tokyo",\n "datetime": "2025-09-19T00:10:38+09:00",\n "is_dst": false\n}',
name="get_current_time",
id="b3a74749-a04d-4bbb-940c-83bfa5596fef",
tool_call_id="toolu_bdrk_012zCQ7kgoQxh4FAsnBnvqgQ",
),
AIMessage(
content="現在の東京の時間は2025年9月19日 00時10分38秒です。\n\nget_current_timeツールの出力によると、東京(Asia/Tokyo)のタイムゾーンでの現在時刻は2025年9月19日の午前0時10分38秒(日本標準時 JST、UTC+9)となっています。",
additional_kwargs={
"usage": {
"prompt_tokens": 3583,
"completion_tokens": 102,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3685,
},
"stop_reason": "end_turn",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
response_metadata={
"usage": {
"prompt_tokens": 3583,
"completion_tokens": 102,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3685,
},
"stop_reason": "end_turn",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
id="run--370cec31-d727-4aa9-8088-a9abfbfce7f4-0",
usage_metadata={
"input_tokens": 3583,
"output_tokens": 102,
"total_tokens": 3685,
"input_token_details": {
"cache_creation": 0,
"cache_read": 0,
},
},
),
],
},
}
},
)
values
valuesでのevent
(
"values",
{
"messages": [
HumanMessage(
content="東京は今何時?",
additional_kwargs={},
response_metadata={},
id="cbc43ca4-8a68-4486-933b-4dda1cc04b83",
),
AIMessage(
content="東京の現在時刻をお調べします。",
additional_kwargs={
"usage": {
"prompt_tokens": 3458,
"completion_tokens": 73,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3531,
},
"stop_reason": "tool_use",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
response_metadata={
"usage": {
"prompt_tokens": 3458,
"completion_tokens": 73,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3531,
},
"stop_reason": "tool_use",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
id="run--1aaed91e-2a45-4cf4-b6e4-71293f5864ac-0",
tool_calls=[
{
"name": "get_current_time",
"args": {"timezone": "Asia/Tokyo"},
"id": "toolu_bdrk_01Q7ZmWhHVjXehT4b9ufDaZp",
"type": "tool_call",
}
],
usage_metadata={
"input_tokens": 3458,
"output_tokens": 73,
"total_tokens": 3531,
"input_token_details": {"cache_creation": 0, "cache_read": 0},
},
),
ToolMessage(
content='{\n "timezone": "Asia/Tokyo",\n "datetime": "2025-09-18T23:28:26+09:00",\n "is_dst": false\n}',
name="get_current_time",
id="77eae437-94fd-4086-a6b2-7a7a30427316",
tool_call_id="toolu_bdrk_01Q7ZmWhHVjXehT4b9ufDaZp",
),
AIMessage(
content="東京の現在時刻は2025年9月18日 23時28分26秒です。\n\nこの情報は`get_current_time`ツールの出力に基づいています。日本標準時(JST)はUTC+9時間のタイムゾーンです。",
additional_kwargs={
"usage": {
"prompt_tokens": 3584,
"completion_tokens": 80,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3664,
},
"stop_reason": "end_turn",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
response_metadata={
"usage": {
"prompt_tokens": 3584,
"completion_tokens": 80,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3664,
},
"stop_reason": "end_turn",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
id="run--d759e5cb-223d-49a7-bffa-b463db630f37-0",
usage_metadata={
"input_tokens": 3584,
"output_tokens": 80,
"total_tokens": 3664,
"input_token_details": {"cache_creation": 0, "cache_read": 0},
},
),
],
"next": "FINISH",
"agent_results": {
"infra_websearch_agent": "東京の現在時刻は2025年9月18日 23時28分26秒です。\n\nこの情報は`get_current_time`ツールの出力に基づいています。日本標準時(JST)はUTC+9時間のタイムゾーンです。"
},
"current_query": "東京は今何時?",
"metadata": {
"investigation_plan": {
"steps": [
"infra_websearch_agentを使用して東京の現在時刻を検索する",
"検索結果から正確な時刻情報を抽出して提示する",
],
"agents_sequence": ["infra_websearch_agent"],
"complexity": "simple",
"auto_execute": True,
"reasoning": "この質問はOCIインフラやアプリケーションの問題ではなく、単純な時刻情報の検索です。infra_websearch_agentが外部情報を検索できるため、東京の現在時刻を調べるのに最適です。単一のエージェントで完結する簡単なタスクなので、シンプルな計画として自動実行します。",
},
"routing_reasoning": "Investigation plan completed. Presenting results.",
"plan_step": 1,
"plan_text": "## 🔍 Investigation Plan\n\n**1.** infra_websearch_agentを使用して東京の現在時刻を検索する\n\n**2.** 検索結果から正確な時刻情報を抽出して提示する\n\n**📊 Complexity:** Simple\n**🤖 Auto-execute:** Yes\n**💭 Reasoning:** この質問はOCIインフラやアプリケーションの問題ではなく、単純な時刻情報の検索です。infra_websearch_agentが外部情報を検索できるため、東京の現在時刻を調べるのに最適です。単一のエージェントで完結する簡単なタスクなので、シンプルな計画として自動実行します。\n**👥 Agents involved:** Infra Websearch Agent\n",
"show_plan": True,
"infra_websearch_agent_trace": [
AIMessage(
content="東京の現在時刻をお調べします。",
additional_kwargs={
"usage": {
"prompt_tokens": 3458,
"completion_tokens": 73,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3531,
},
"stop_reason": "tool_use",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
response_metadata={
"usage": {
"prompt_tokens": 3458,
"completion_tokens": 73,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3531,
},
"stop_reason": "tool_use",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
id="run--1aaed91e-2a45-4cf4-b6e4-71293f5864ac-0",
tool_calls=[
{
"name": "get_current_time",
"args": {"timezone": "Asia/Tokyo"},
"id": "toolu_bdrk_01Q7ZmWhHVjXehT4b9ufDaZp",
"type": "tool_call",
}
],
usage_metadata={
"input_tokens": 3458,
"output_tokens": 73,
"total_tokens": 3531,
"input_token_details": {"cache_creation": 0, "cache_read": 0},
},
),
ToolMessage(
content='{\n "timezone": "Asia/Tokyo",\n "datetime": "2025-09-18T23:28:26+09:00",\n "is_dst": false\n}',
name="get_current_time",
id="77eae437-94fd-4086-a6b2-7a7a30427316",
tool_call_id="toolu_bdrk_01Q7ZmWhHVjXehT4b9ufDaZp",
),
AIMessage(
content="東京の現在時刻は2025年9月18日 23時28分26秒です。\n\nこの情報は`get_current_time`ツールの出力に基づいています。日本標準時(JST)はUTC+9時間のタイムゾーンです。",
additional_kwargs={
"usage": {
"prompt_tokens": 3584,
"completion_tokens": 80,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3664,
},
"stop_reason": "end_turn",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
response_metadata={
"usage": {
"prompt_tokens": 3584,
"completion_tokens": 80,
"cache_read_input_tokens": 0,
"cache_write_input_tokens": 0,
"total_tokens": 3664,
},
"stop_reason": "end_turn",
"thinking": {},
"model_id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
id="run--d759e5cb-223d-49a7-bffa-xxxxx-0",
usage_metadata={
"input_tokens": 3584,
"output_tokens": 80,
"total_tokens": 3664,
"input_token_details": {"cache_creation": 0, "cache_read": 0},
},
),
],
},
"requires_collaboration": False,
"agents_invoked": ["infra_websearch_agent"],
"final_response": "# 🔍 調査結果...", #👈最終回答
"auto_approve_plan": True,
},
)
messsages
messagesでのevent
# supervisorの思考。ルーティングを考えているところ
(
"messages",
(
AIMessageChunk(
content=[
{
"type": "tool_use",
"partial_json": "東京の現在時刻を知りたい",
"index": 0,
}
],
additional_kwargs={},
response_metadata={},
id="run--bae05ce1-c5c2-435e-a6c1-f6db8764009a",
invalid_tool_calls=[
{
"name": None,
"args": "東京の現在時刻を知りたい",
"id": None,
"error": None,
"type": "invalid_tool_call",
}
],
tool_call_chunks=[
{
"name": None,
"args": "東京の現在時刻を知りたい",
"id": None,
"index": 0,
"type": "tool_call_chunk",
}
],
),
{
"langgraph_step": 2,
"langgraph_node": "supervisor",
"langgraph_triggers": ("branch:to:supervisor",),
"langgraph_path": ("__pregel_pull", "supervisor"),
"langgraph_checkpoint_ns": "supervisor:f95c3db7-798a-6414-4ce3-9c95d26e240b",
"checkpoint_ns": "supervisor:f95c3db7-798a-6414-4ce3-9c95d26e240b",
"ls_provider": "amazon_bedrock",
"ls_model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"ls_model_type": "chat",
"ls_temperature": 0.1,
"ls_max_tokens": 4096,
},
),
)
# サブエージェントがツールを実行しようとしているところ
(
"messages",
(
AIMessage(
content=[
{"type": "text", "text": "現在の東京の時間を確認します。", "index": 0},
{
"type": "tool_use",
"id": "toolu_bdrk_01WYRJEnxJ3L8LZHzRDEizTv",
"name": "get_current_time",
"input": {},
"index": 1,
"partial_json": '{"timezone": "Asia/Tokyo"}',
},
],
additional_kwargs={},
response_metadata={
"stop_reason": "tool_use",
"stop_sequence": None,
"model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
},
id="run--d4304bb1-e078-421a-b9e1-8649fa790486",
tool_calls=[
{
"name": "get_current_time",
"args": {"timezone": "Asia/Tokyo"},
"id": "toolu_bdrk_01WYRJEnxJ3L8LZHzRDEizTv",
"type": "tool_call",
}
],
usage_metadata={
"input_tokens": 3164,
"output_tokens": 72,
"total_tokens": 3236,
"input_token_details": {"cache_creation": 0, "cache_read": 0},
},
),
{
"langgraph_step": 5,
"langgraph_node": "infra_oci_document_agent",
"langgraph_triggers": ("branch:to:infra_oci_document_agent",),
"langgraph_path": ("__pregel_pull", "infra_oci_document_agent"),
"langgraph_checkpoint_ns": "infra_oci_document_agent:ba6e1561-27b6-7f1a-e279-8608536acb37",
},
),
)
# サブエージェントがツールを実行しているところ
(
"messages",
(
ToolMessage(
content='{\n "timezone": "Asia/Tokyo",\n "datetime": "2025-09-19T00:28:20+09:00",\n "is_dst": false\n}',
name="get_current_time",
id="53726e5d-e88f-44c1-a1e4-3507a5135ef4",
tool_call_id="toolu_bdrk_017Qw1AHJKEaULfZXLKwU844",
),
{
"langgraph_step": 3,
"langgraph_node": "infra_websearch_agent",
"langgraph_triggers": ("branch:to:infra_websearch_agent",),
"langgraph_path": ("__pregel_pull", "infra_websearch_agent"),
"langgraph_checkpoint_ns": "infra_websearch_agent:3402f9aa-4080-cec8-4964-66813a7092e3",
},
),
)
# supervisorエージェントが回答をまとめているところ
(
"messages",
(
AIMessageChunk(
content="に返しています。",
additional_kwargs={},
response_metadata={},
id="run--13f45356-201a-4f2a-97d1-dbd7e4277745",
),
{
"langgraph_step": 7,
"langgraph_node": "aggregate",
"langgraph_triggers": ("branch:to:aggregate",),
"langgraph_path": ("__pregel_pull", "aggregate"),
"langgraph_checkpoint_ns": "aggregate:79481a25-d7a8-a079-21ac-90a86d2c09c5",
"checkpoint_ns": "aggregate:79481a25-d7a8-a079-21ac-90a86d2c09c5",
"ls_provider": "amazon_bedrock",
"ls_model_name": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"ls_model_type": "chat",
"ls_temperature": 0.1,
"ls_max_tokens": 1000,
},
),
)
詳細
以上を元に、次のように実装しました。
- 制御を複雑にしたくなかったので、
stream_mode=["updates"]にしています - また、ここまでの話に加え、どのように考えてツールを実行したのかを表示するようにしたり、エージェントの種別によって出力を制御しています
コードを折りたたんでいます、必要に応じて参照してください
#!/usr/bin/env python3
import asyncio
import json
import logging
import os
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import StreamingResponse
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.tools import BaseTool
from .agent_state import AgentState
from .constants import DEFAULT_MODEL_ID
# Import logging config
from .logging_config import configure_logging
from .multi_agent_langgraph import create_multi_agent_system
# Configure logging based on DEBUG environment variable
# This ensures debug mode works even when not run via __main__
if not logging.getLogger().handlers:
# Check if DEBUG is already set in environment
debug_from_env = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
configure_logging(debug_from_env)
# Disable uvicorn access logs for /ping endpoint
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
# Simple FastAPI app
app = FastAPI(title="SRE Agent Runtime", version="1.0.0")
# Global variables for agent state
agent_graph = None
tools: list[BaseTool] = []
async def initialize_agent(
provider: str = "bedrock",
model_id: str = DEFAULT_MODEL_ID,
):
"""Initialize the SRE agent system using the same method as CLI."""
global agent_graph, tools
# If already initialized with same parameters, skip
if agent_graph is not None:
return # Already initialized
try:
logger.info("Initializing SRE Agent system...")
# Get provider from environment variable with bedrock as default
provider = os.getenv("LLM_PROVIDER", "bedrock").lower()
# Validate provider
if provider not in ["anthropic", "bedrock"]:
logger.error("Invalid provider specified. Using default: bedrock.")
provider = "bedrock"
logger.info(f"Environment LLM_PROVIDER: {os.getenv('LLM_PROVIDER', 'NOT_SET')}")
logger.info(f"Using LLM provider: {provider}, Using model ID: {model_id}")
# Create multi-agent system using the same function as CLI
# Pass model_id if provided through llm_kwargs
llm_kwargs = {}
llm_kwargs["model_id"] = model_id
agent_graph, tools = await create_multi_agent_system(provider, **llm_kwargs)
logger.info(
f"SRE Agent system initialized successfully with {len(tools)} tools"
)
except Exception as e:
logger.error(f"Failed to initialize SRE Agent system: {str(e)}")
raise
@app.on_event("startup")
async def startup_event():
"""Initialize agent on startup."""
await initialize_agent()
def _generate_tool_message_events(node_name: str, ai_message: AIMessage):
"""ツールメッセージのイベントストリームを生成するヘルパー関数"""
if ai_message.tool_calls is None:
return
if not hasattr(ai_message, "tool_calls"):
return
content = ai_message.content
for tc in ai_message.tool_calls:
tool_name = tc.get("name", "unknown")
tool_args = tc.get("args", {})
tool_id = tc.get("id", "unknown")
# ツールメッセージ開始イベント
tool_start_event = {
"event": {
"contentBlockStart": {
"start": {
"toolUse": {
"toolUseId": tool_id,
"name": f"{node_name}===>{tool_name}",
}
},
"contentBlockIndex": 1,
}
}
}
# 思考過程出力
tool_delta_event_comment = None
if isinstance(content, str):
tool_delta_event_comment = {
"event": {
"contentBlockDelta": {
"delta": {"toolUse": {"input": "\n" + str(content)}},
"contentBlockIndex": 1,
}
}
}
# ツールメッセージデルタイベント
tool_delta_event = {
"event": {
"contentBlockDelta": {
# strにしないとGenUの処理でNGになる
"delta": {"toolUse": {"input": str(tool_args)}},
"contentBlockIndex": 1,
}
}
}
# ツールメッセージ終了イベント
tool_end_event = {"event": {"contentBlockStop": {"contentBlockIndex": 1}}}
# JSON文字列として順次yield
yield json.dumps(tool_start_event, ensure_ascii=False) + "\n"
yield json.dumps(tool_delta_event, ensure_ascii=False) + "\n"
if tool_delta_event_comment is not None:
yield json.dumps(tool_delta_event_comment, ensure_ascii=False) + "\n"
yield json.dumps(tool_end_event, ensure_ascii=False) + "\n"
@app.post("/invocations")
async def invoke_agent(request: Request):
"""Main agent invocation endpoint."""
global agent_graph, tools
logger.info("Received invocation request")
try:
body = await request.body()
body_str = body.decode()
request_data = json.loads(body_str)
# Handle input field if present (AWS Lambda integration format)
if "input" in request_data and isinstance(request_data["input"], dict):
request_data = request_data["input"]
# Extract required fields
# messages = request_data.get("messages", [])
# system_prompt = request_data.get("systemPrompt", "")
user_prompt_data = request_data.get("prompt", [])
model = request_data.get("model", {})
model_id = model.get("modelId", "")
provider = model.get("type", "bedrock").lower()
# Handle prompt format - extract text from array format
if isinstance(user_prompt_data, list) and len(user_prompt_data) > 0:
user_prompt = user_prompt_data[0].get("text", "")
elif isinstance(user_prompt_data, str):
user_prompt = user_prompt_data
else:
user_prompt = ""
if not user_prompt:
raise HTTPException(
status_code=400,
detail="No prompt found in request.",
)
logger.info(f"Processing query: {user_prompt}")
logger.info(f"Using model: {model_id}")
# Initialize agent with specific model if provided
await initialize_agent(provider=provider, model_id=model_id)
# Create initial state exactly like the CLI does
initial_state: AgentState = {
"messages": [HumanMessage(content=user_prompt)],
"next": "supervisor",
"agent_results": {},
"current_query": user_prompt,
"metadata": {},
"requires_collaboration": False,
"agents_invoked": [],
"final_response": None,
"auto_approve_plan": True, # Always auto-approve plans in runtime mode
}
logger.info("Starting agent graph execution")
async def event_generator():
has_returned_plan = False
try:
async for event in agent_graph.astream(
initial_state,
stream_mode=["updates"],
):
if not event:
continue
for node_name, node_output in event[1].items():
# Log key events from each node
if node_name == "supervisor" and not has_returned_plan:
next_agent = node_output.get("next", "")
metadata = node_output.get("metadata", {})
logger.info(f"Supervisor routing to: {next_agent}")
if metadata.get("routing_reasoning"):
logger.info(
f"Routing reasoning: {metadata['routing_reasoning']}"
)
plan_text = metadata.get("plan_text", "計画の作成に失敗。")
plan_text += metadata.get("routing_reasoning", "")
plan_text += "\n\n"
return_json = {
"event": {
"contentBlockDelta": {
"delta": {"text": plan_text},
"contentBlockIndex": 0,
}
}
}
has_returned_plan = True
yield json.dumps(return_json, ensure_ascii=False) + "\n"
continue
# Capture final response from aggregate node
elif node_name == "aggregate":
final_response = "---\n\n"
final_response += node_output.get("final_response", "")
logger.info(
"Aggregate node completed, final response captured"
)
logger.info(f"Final response: {final_response}")
fr_json = {
"event": {
"contentBlockDelta": {
"delta": {"text": final_response},
"contentBlockIndex": 0,
}
}
}
yield json.dumps(fr_json, ensure_ascii=False) + "\n"
continue
elif node_name == "prepare":
continue
else:
agent_results = node_output.get("agent_results", {})
s = f"{node_name}: {agent_results.get(node_name, '')}"
logger.info(f"{node_name} completed with results: {s}")
if messages := node_output.get("messages"):
for m in messages:
if isinstance(m, AIMessage):
if hasattr(m, "tool_calls") and m.tool_calls:
# ツールメッセージのイベントストリームを生成
for event in _generate_tool_message_events(
node_name, m
):
yield event
except Exception as e:
logger.error(f"agent_processing_failed: {e}")
raise
return StreamingResponse(event_generator(), media_type="text/event-stream")
except Exception as e:
logger.error(f"agent_processing_failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/ping")
async def ping():
"""Health check endpoint."""
return {"status": "healthy"}
おわりに
少し強引ですが、自前のLangGraphエージェントをGenU対応しました。GenUでも使えれば便利さは100倍です。
自分と同じように、LangGraphを使ってしまった人や、LangChain系の人に届けば嬉しいです。
自分だけのエージェントを、どんどん開発しましょう!




