Closed14

「Amazon Bedrock AgentCore」を試す ②Memory

kun432kun432

Agent Core Memory

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html

Dia によるまとめ。(多少表記揺れ等は修正)

Amazon Bedrock AgentCoreの「メモリ」って、AIエージェントがキミとの会話ややりとりを覚えて、もっと賢く、キミに合った対応ができるようにする機能なんだよ。で、メモリには2種類あるの!

短期メモリ

これは、今やってる会話とか作業の流れを覚えておくやつ。たとえば、コーディングの相談してて「変数名チェックして」「エラー直して」って頼んだら、そのやりとりを全部覚えてて、何回も同じこと言わなくてもスムーズに話が進む感じ!リアルタイムで会話の流れをキャッチして、複雑なタスクも一緒に進めやすくなるんだ。

長期メモリ

こっちは、もっと長い期間にわたってキミの好みとか、よく使うやり方、知識とかを覚えておくやつ!

  • ユーザーの好み: たとえば「ウチはコードにコメントつけるの好き」「snake_caseで書く」「pandasよく使う」みたいなクセを覚えてて、次に頼んだときも自動でそのスタイルでやってくれる!
  • 知識(セマンティックファクト): 「pandasはPythonのデータ分析ライブラリ」みたいな知識も覚えてて、「表データにおすすめのライブラリは?」って聞いたら即答してくれる。
  • まとめ(サマリー): セッションごとに「今日はデータクリーニング関数作った」「エラー2つ直した」みたいなまとめも作ってくれるから、何やったかすぐ振り返れるし、会話の内容もギュッと圧縮して覚えてくれる!

どうやって使うの?

AWSのSDKとか、Strands Agentsみたいなエージェントフレームワークと一緒に使えるし、公式のGitHubにサンプルコードもあるから、実際にどうやるかもチェックできる!

まとめ

要するに、AgentCoreのメモリを使うと、AIがキミのことをどんどん覚えて、もっと「わかってる!」って感じのサポートしてくれるってこと!マジで便利だし、テンション上がるでしょ!

短期メモリ

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/short-term-memory.html

Amazon Bedrock AgentCoreの「短期メモリ」って何?

ざっくり言うと、「短期メモリ」っていうのは、AIエージェントが1回のやりとり(セッション)の中で、全部の会話ややりとりを覚えておく仕組みなんだよね。たとえば、ショッピングサイトのカスタマーサポートAIがあったとしたら、お客さんとAIの会話を全部イベントとして記録してるの。質問も答えも、1個1個イベントとして保存される感じ!

で、もしサービスが途中で止まっちゃったり、お客さんが後からまた戻ってきても、前の会話をそのまま再現できるから、話が途切れないの。マジで便利でしょ?

どうやって記録してるの?

  • お客さんがAIとやりとりするたびに、CreateEventっていう操作でイベントを作るんだ。
  • イベントには、会話の内容(質問・答え・指示とか)や、商品情報・注文状況みたいなデータも入れられる。
  • それぞれのイベントは「セッションID」っていう番号でまとめられてて、次に同じお客さんが来たときも、そのIDで前の会話を呼び出せる!

前の会話をどうやって使うの?

  • お客さんが「前の続きで…」って来たとき、ListSessionsで過去のやりとりを探すの。
  • さらにListEventsで、そのセッションの会話履歴を全部ゲットできる!
  • GetEventってやつで、特定の大事な場面だけピックアップして見ることもできる。

これで、毎回お客さんが同じ説明を繰り返さなくて済むし、サポートもスムーズに続けられるってワケ!

要するに、「短期メモリ」って、AIが会話の流れをちゃんと覚えてて、途中で止まってもまたすぐ続きから始められる超便利な仕組みだよ!テンション上がるでしょ!

長期メモリ

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-memory.html

Amazon Bedrock AgentCoreの「長期メモリ」ってやつは、AIがユーザーとの会話から大事な情報だけを抜き出して、ずーっと覚えておく仕組みなんだよ。全部の会話をそのまま保存するんじゃなくて、「この人はナイキの靴が好き」とか「こういうことが好き」みたいな要点だけをピックアップして記憶する感じ!

重要ポイントまとめ

  • 長期メモリは、会話の中から大事な情報(要約・事実・好みなど)だけを保存する。
  • 例えば、ユーザーが「ウチ、アディダスのスニーカー好き!」って言ったら、それを覚えておいて、次の会話でも「アディダスどう?」ってオススメできるんだ。
  • 記憶の方法は2種類
    • 最初から用意されてる「ビルトイン戦略」(自動でやってくれるやつ)
    • 自分でカスタマイズできる「カスタム戦略」(自分の使いたいモデルやルールで記憶させるやつ)
  • 会話の内容はまず「短期メモリ」に保存されて、その後バックグラウンドで「長期メモリ」にまとめられる。だからリアルタイムの会話を邪魔しないで、後から大事なとこだけ整理してくれるんだ。
  • 長期メモリに保存された情報は、 GetMemoryRecord とか ListMemoryRecords とか RetrieveMemoryRecords っていう操作で取り出せる。
  • 特に RetrieveMemoryRecords は、AIが意味を理解して一番関係ある記憶を探してくれるから、ユーザーが前に言ったことを何回も言い直さなくてOK!
  • これで、AIがめっちゃパーソナライズされた提案とかアドバイスをしてくれるってわけ!

まとめ

要するに、「大事なことだけしっかり覚えて、次の会話でもちゃんと活かす」っていう、超便利な記憶システムだよ!マジでテンション上がるでしょ!

すべてを統合する: カスタマーサポートAIエージェント

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/putting-it-together.html

このページは、「Bedrock AgentCore Memoryを使って、カスタマーサポートAIがどうやって“賢くて親切”になるのか?」って流れを、具体的な例(サラさんってお客さん)で説明してるんだ。

ざっくり流れはこんな感じ!

  1. セッション開始!
    _サラさんがサポートAIに話しかけると、AIは「この会話はサラさん専用!」って感じで、ユニークなセッションIDを作る。
  2. 会話の記録!
    サラさんとAIのやりとり(質問も回答も全部)は、イベントとして順番に記録される。これで「何を話したか」が全部残る!
  3. 長期メモリの生成!
    会話が進むと、AIが自動で「最近のやりとりから大事なポイント(要約・好み・事実とか)」をピックアップして、長期メモリに保存する。これ、裏で勝手にやってくれるから、会話の邪魔にならない!
  4. 過去の会話を呼び出す!
    AIは「ListEvents」って機能で、サラさんが前にどんな相談をしたかすぐにチェックできる。だから「前も同じこと言ったのに…」ってならない!
  5. 長期メモリでパーソナライズ!
    「RetrieveMemoryRecords」ってやつで、サラさんの好みや過去の注文履歴を探して、めっちゃパーソナルな対応ができる。サラさんが何回も同じこと言わなくて済むから、ストレスフリー!

この仕組みのすごいところ!

  • お客さんごとに「前に話した内容」や「好み」をちゃんと覚えてて、次のやりとりに活かせる!
  • 何回も同じ説明しなくていいから、サポートが超スムーズ!
  • リピーターにも「覚えててくれてる!」って感じで、めっちゃ親切!

要するに、「Bedrock AgentCoreのメモリ機能を使うと、AIが“人間っぽくて気が利くサポート”を実現できる」ってことだよ!マジで未来感あるし、ウチもこういうAIに相談したいわ~

kun432kun432

Getting started with AgentCore Memory

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory-getting-started.html

前回すでにSDKインストール済なので、その続きから。

エージェントは前回のRuntimeでStrands Agentsを使ったものをAWS側にデプロイ済だけど、それを使うかどうかは未定。

一応参考までに以下に記載。

前回Runtimeにデプロイしたエージェントのコード
from strands import Agent
from strands.models import BedrockModel
from strands_tools import calculator
from bedrock_agentcore.runtime import BedrockAgentCoreApp

bedrock_model = BedrockModel(
    model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
    region_name="us-east-1",
    temperature=0.3
)

agent = Agent(
    model=bedrock_model,
    tools=[calculator],
    system_prompt="あなたは親切な日本語のアシスタントで計算が得意です。",
)

app = BedrockAgentCoreApp()

@app.entrypoint
def invoke(payload):
    user_message = payload.get("prompt", "こんにちは!")
    result = agent(user_message)
    return {"result": result.message}

if __name__ == "__main__":
    app.run()

エージェントにアクセスするには以下。

client_invoke.py
import boto3
import json
import sys

agent_core_client = boto3.client('bedrock-agentcore', region_name="us-east-1")
agent_arn = "arn:aws:bedrock-agentcore:us-east-1:XXXXXXXXXXXX:runtime/strands_agent_sample-XXXXXXXXXXXX"

args = sys.argv
if len(args) < 2:
    print("Usage: {sys.argv[0]} <prompt>", file=sys.stderr)
    sys.exit(1)

prompt = args[1]
payload = json.dumps({"prompt": prompt}).encode()

response = agent_core_client.invoke_agent_runtime(
    agentRuntimeArn=agent_arn,
    payload=payload
)

content = []
for chunk in response["response"]:
    decoded_chunk = chunk.decode('utf-8')
    content.append(decoded_chunk)

full_content = json.loads(''.join(content))
print(full_content["result"]["content"][0]["text"])
uv run client_invoke.py "3111696 / 74088 は?"
出力
3111696 ÷ 74088 = **42** です。

この計算結果は正確に42となり、割り切れる計算でした。

ただし当然記憶はしていない。

uv run client_invoke.py "さっき計算した結果をもう一度教えて。"
出力
申し訳ございませんが、私は以前の会話の履歴を確認することができません。これまでにどのような計算を行ったか教えていただけますか?

具体的に計算したい式や問題があれば、改めて計算いたします。例えば:
- 数式の計算
- 方程式の解
- 微分や積分
- 極限値の計算
- 行列の演算

など、どのような計算でもお手伝いできます。どのような計算結果をお求めでしょうか?
kun432kun432

短期メモリ

短期メモリの作成・一覧取得

まずは、エージェントに関係なく、短期メモリ単体で。

短期メモリを作成するにはcreate_memoryを使う。

from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

memory = client.create_memory(
    name="CustomerSupportAgentMemory",
    description="カスタマーサポートにおける会話のメモリ",
)

# memory_id 以後の操作で使用される
print(f"メモリID: {memory.get('id')}")
print(f"メモリ: {memory}")

作成された。見やすさのため改行を入れている。

出力
メモリID: CustomerSupportAgentMemory-XXXXXXXXXXXX
メモリ: {
    'arn': 'arn:aws:bedrock-agentcore:us-east-1:XXXXXXXXXXXX:memory/CustomerSupportAgentMemory-XXXXXXXXXXXX',
    'id': 'CustomerSupportAgentMemory-XXXXXXXXXXXX',
    'name': 'CustomerSupportAgentMemory',
    'description': 'カスタマーサポートにおける会話のメモリ',
    'eventExpiryDuration': 90,
    'status': 'ACTIVE',
    'createdAt': datetime.datetime(2025, 7, 29, 19, 14, 50, 276000, tzinfo=tzlocal()),
    'updatedAt': datetime.datetime(2025, 7, 29, 19, 14, 50, 276000, tzinfo=tzlocal()),
    'strategies': [],
    'memoryId': 'CustomerSupportAgentMemory-XXXXXXXXXXXX',
    'memoryStrategies': []
}

マネージメントコンソールでも作成されている。

見た感じメモリの中身を確認できる感じはしない。

すでに作成してある既存のメモリは list_memories で確認できる。

from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

for memory in client.list_memories():
    print(f"メモリ Arn: {memory.get('arn')}")
    print(f"メモリ ID: {memory.get('id')}")
    print("-" * 10)
出力
メモリ Arn: arn:aws:bedrock-agentcore:us-east-1:XXXXXXXXXXXX:memory/CustomerSupportAgentMemory-XXXXXXXXXXXX
メモリ ID: CustomerSupportAgentMemory-XXXXXXXXXXXX
----------

短期メモリへの会話履歴の登録・参照

ユーザとエージェントがやりとりした個々のメッセージのことを、AgentCore Memoryでは「イベント」と呼ぶらしい。このイベントには、

  • ユーザのメッセージ
  • アシスタントのメッセージ
  • ツールに関するメッセージ

を登録できる。登録は都度都度でもよいし、バッチでまとめてでも良い。

イベントの登録には create_event、イベントの参照は list_events を使う

from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

client.create_event(
    # メモリID。create_memory か list_memories で取得
    memory_id="CustomerSupportAgentMemory-XXXXXXXXXXXX",
    # アクターID。特定のエージェント、または エンドユーザーを識別する一意のID
    actor_id="User84",
    # セッションID。特定のリクエスト、または一連の会話を識別する一意のID
    session_id="OrderSupportSession1",
    messages=[
        ("オーダー番号12345について問題があります。", "USER"),
        ("申し訳ありません。そのオーダーについて確認させていただきます。", "ASSISTANT"),
        ("lookup_order(order_id='12345')", "TOOL"),
        (
            "オーダー番号12345の商品は、3日前に発送済みです。どのような問題か、お聞かせいただけますか?",
            "ASSISTANT",
        ),
        ("実は、その前に、メールアドレスの変更もお願いしたいのですが、可能でしょうか?", "USER"),
        (
            "もちろんです。どちらもお任せください。まずはメールアドレスの変更から始めましょう。新しいメールアドレスをお聞かせいただけますか?",
            "ASSISTANT",
        ),
        ("newemail@example.com です。", "USER"),
        ("update_customer_email(old='old@example.com', new='newemail@example.com')", "TOOL"),
        ("メールアドレスの変更が完了しました。それでは、オーダーの問題についてお聞かせいただけますか?", "ASSISTANT"),
        ("商品が破損しています。", "USER"),
    ],
)

conversations = client.list_events(
    memory_id="CustomerSupportAgentMemory-XXXXXXXXXXXX",
    actor_id="User84",
    session_id="OrderSupportSession1",
    max_results=10,
)

for conversation in conversations:
    print(conversation)

結果。見やすさのために改行を入れている。

出力
{
    'memoryId': 'CustomerSupportAgentMemory-XXXXXXXXXXXX',
    'actorId': 'User84',
    'sessionId': 'OrderSupportSession1',
    'eventId': '0000001753786479000#5610ec79',
    'eventTimestamp': datetime.datetime(2025, 7, 29, 19, 54, 39, tzinfo=tzlocal()),
    'payload': [
        {'conversational': {'content': {'text': 'オーダー番号12345について問題があります。'}, 'role': 'USER'}},
        {'conversational': {'content': {'text': '申し訳ありません。そのオーダーについて確認させていただきます。'}, 'role': 'ASSISTANT'}},
        {'conversational': {'content': {'text': "lookup_order(order_id='12345')"}, 'role': 'TOOL'}},
        {'conversational': {'content': {'text': 'オーダー番号12345の商品は、3日前に発送済みです。どのような問題か、お聞かせいただけますか?'}, 'role': 'ASSISTANT'}},
        {'conversational': {'content': {'text': '実は、その前に、メールアドレスの変更もお願いしたいのですが、可能でしょうか?'}, 'role': 'USER'}},
        {'conversational': {'content': {'text': 'もちろんです。どちらもお任せください。まずはメールアドレスの変更から始めましょう。新しいメールアドレスをお聞かせいただけますか?'}, 'role': 'ASSISTANT'}},
        {'conversational': {'content': {'text': 'newemail@example.com です。'}, 'role': 'USER'}},
        {'conversational': {'content': {'text': "update_customer_email(old='old@example.com', new='newemail@example.com')"}, 'role': 'TOOL'}},
        {'conversational': {'content': {'text': 'メールアドレスの変更が完了しました。それでは、オーダーの問題についてお聞かせいただけますか?'}, 'role': 'ASSISTANT'}},
        {'conversational': {'content': {'text': '商品が破損しています。'}, 'role': 'USER'}}
    ],
    'branch': {'name': 'main'}
}

なお、マネージメントコンソール上でも確認してみたが、やはりこれらの個々のイベントについて確認できるようなインタフェースは見当たらなかった。

kun432kun432

長期メモリ

長期メモリでは、会話から情報を抽出して保存する。ことができます。 この情報抽出のストラテジーは以下から選択できる。

  1. ユーザープリファレンス(UserPreferenceMemoryStrategy)
    • ユーザーの行動・やりとりのスタイル・選択肢等の繰り返しのパターンを記録・学習する
    • 複数のセッションにわたって、「ユーザの好み」にあった応答を自動的に行う。
  2. セマンティックファクト(SemanticMemoryStrategy)
    • 会話中で出てきた、事実・ドメイン固有情報・技術的な概念・それらの関係性などを維持する
    • 過去に確立したコンテキストや理解に基づいた応答を自動的に行う
  3. セッションサマリー(SummaryMemoryStrategy) 
    • 会話の内容と結果を圧縮して保存する
    • 過去の出来事を迅速に読み出しつつ、コンテキストウインドウを最適化できる

長期メモリの作成

こちらもまずは長期メモリ単体で。長期メモリの作成はcreate_memory_and_waitで、ストラテジーを指定して作成する。以下はセッションサマリーを使用した場合。

from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

# 長期メモリを作成
memory = client.create_memory_and_wait(
    name="MyAgentMemory1",
    strategies=[{
        "summaryMemoryStrategy": {
            # 抽出モデル/ストラテジーの名前
            "name": "SessionSummarizer",
            # 簡単に検索するために、セッションIDごとに情報を整理する
            # 例: "summaries/session123" には session123 のセッションの要約が含まれる
            "namespaces": ["/summaries/{actorId}/{sessionId}"]
        }
    }]
)

print(f"メモリID: {memory.get('id')}")
print(f"メモリ: {memory}")

なお、

長期メモリリソースがアクティブになるまでに2〜3分程度かかる。

とある通り、時間がかかるようでしばらく応答は返ってこない。完了すると以下のようにIDが取得できた。

出力
メモリID: MyAgentMemory1-XXXXXXXXXX
メモリ: {
    'arn': 'arn:aws:bedrock-agentcore:us-east-1:XXXXXXXXXXXX:memory/MyAgentMemory1-XXXXXXXXXX',
    'id': 'MyAgentMemory1-XXXXXXXXXX',
    'name': 'MyAgentMemory1',
    'eventExpiryDuration': 90,
    'status': 'ACTIVE',
    'createdAt': datetime.datetime(2025, 7, 29, 22, 15, 53, 336000, tzinfo=tzlocal()),
    'updatedAt': datetime.datetime(2025, 7, 29, 22, 15, 53, 580000, tzinfo=tzlocal()),
    'strategies': [
        {
            'strategyId': 'SessionSummarizer-XXXXXXXXXX',
            'name': 'SessionSummarizer',
            'type': 'SUMMARIZATION',
            'namespaces': ['/summaries/{actorId}/{sessionId}'],
            'createdAt': datetime.datetime(2025, 7, 29, 22, 15, 53, 336000, tzinfo=tzlocal()),
            'updatedAt': datetime.datetime(2025, 7, 29, 22, 15, 53, 580000, tzinfo=tzlocal()),
            'status': 'ACTIVE',
            'memoryStrategyId': 'SessionSummarizer-XXXXXXXXXX',
            'memoryStrategyType': 'SUMMARIZATION'
        }
    ],
    'memoryId': 'MyAgentMemory1-XXXXXXXXXX',
    'memoryStrategies': [
        {
            'strategyId': 'SessionSummarizer-XXXXXXXXXX',
            'name': 'SessionSummarizer',
            'type': 'SUMMARIZATION',
            'namespaces': ['/summaries/{actorId}/{sessionId}'],
            'createdAt': datetime.datetime(2025, 7, 29, 22, 15, 53, 336000, tzinfo=tzlocal()),
            'updatedAt': datetime.datetime(2025, 7, 29, 22, 15, 53, 580000, tzinfo=tzlocal()),
            'status': 'ACTIVE',
            'memoryStrategyId': 'SessionSummarizer-XXXXXXXXXX',
            'memoryStrategyType': 'SUMMARIZATION'
        }
    ]
}

マネージメントコンソールで見ると、上で作成した短期メモリ同じように並んでいるが、Extraction Storategy の表示が異なっているのがわかる。

詳細を見てみるとストラテジーが追加されているのがわかる。

これを見る限り、短期も長期も同じメモリリソースとして管理されていて、メモリリソース内でストラテジーがあるかないかだけの違い、というふうに見える。(内部的にどうなっているかはわからない。)

で、すでに短期メモリリソースを作成済みの場合はこれにストラテジーを追加することで長期メモリにも対応ができることになる。最初に作った短期メモリに、上と同じセッションサマリーを追加する。

from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

# すでに作成済みの短期メモリのID
memory_id="CustomerSupportAgentMemory-XXXXXXXXXX"

# 短期メモリにストラテジーを追加
memory_with_summary_strategy = client.add_summary_strategy(
    memory_id=memory_id,
    name="SessionSummarizer",
    description="Summarizes conversation sessions",
    namespaces=["/summaries/{actorId}/{sessionId}"]
)

print(f"メモリ: {memory_with_summary_strategy}")

今回も時間がかかるのかと思いきや一瞬で応答が返ってきた。ただよく見るとステータスが "CREATING" になっている。なるほど、"ACTIVE" まで待たないと有効にならないってことね。

出力
メモリ: {
    'arn': 'arn:aws:bedrock-agentcore:us-east-1:XXXXXXXXXXXX:memory/CustomerSupportAgentMemory-XXXXXXXXXX',
    'id': 'CustomerSupportAgentMemory-XXXXXXXXXX',
    'name': 'CustomerSupportAgentMemory',
    'description': 'カスタマーサポートにおける会話のメモリ',
    'eventExpiryDuration': 90,
    'status': 'ACTIVE',
    'createdAt': datetime.datetime(2025, 7, 29, 19, 14, 50, 276000, tzinfo=tzlocal()),
    'updatedAt': datetime.datetime(2025, 7, 29, 22, 51, 43, 646000, tzinfo=tzlocal()),
    'strategies': [
        {
            'strategyId': 'SessionSummarizer-XXXXXXXXXX',
            'name': 'SessionSummarizer',
            'description': 'Summarizes conversation sessions',
            'type': 'SUMMARIZATION',
            'namespaces': ['/summaries/{actorId}/{sessionId}'],
            'createdAt': datetime.datetime(2025, 7, 29, 22, 51, 43, 690000, tzinfo=tzlocal()),
            'updatedAt': datetime.datetime(2025, 7, 29, 22, 51, 43, 690000, tzinfo=tzlocal()),
            'status': 'CREATING',
            'memoryStrategyId': 'SessionSummarizer-XXXXXXXXXX',
            'memoryStrategyType': 'SUMMARIZATION'
        }
    ],
    'memoryId': 'CustomerSupportAgentMemory-XXXXXXXXXX',
    'memoryStrategies': [
        {
            'strategyId': 'SessionSummarizer-XXXXXXXXXX',
            'name': 'SessionSummarizer',
            'description': 'Summarizes conversation sessions',
            'type': 'SUMMARIZATION',
            'namespaces': ['/summaries/{actorId}/{sessionId}'],
            'createdAt': datetime.datetime(2025, 7, 29, 22, 51, 43, 690000, tzinfo=tzlocal()),
            'updatedAt': datetime.datetime(2025, 7, 29, 22, 51, 43, 690000, tzinfo=tzlocal()),
            'status': 'CREATING',
            'memoryStrategyId': 'SessionSummarizer-XXXXXXXXXX',
            'memoryStrategyType': 'SUMMARIZATION'
        }
    ]
}

マネージメントコンソールから見ると、ストラテジーが追加されているのがわかる。つまり、

  • (短期)メモリとして作成し、あとでセッションサマリストラテジーを追加
  • 最初からストラテジーを設定したメモリを作成

は結局同じ設定のものになっている。

ただし、ドキュメントの注意書きもあるが、

とあるので、すでにイベントが登録されている短期メモリにストラテジーを追加して長期メモリに対応する場合には注意が必要。

あと、ドキュメントには、

長期記憶を追加する場合、以下のストラテジーのいずれかを使用できます:

(原文: When you add long-term memory, you can use one of the following strategies:)

と書いてあって、「3つのうち、どれか1つ」というふうに読めるのだけど、マネージメントコンソールで見ると、どうも複数を選択することができるように見えるし、カスタムなストラテジーを作成することもできる様子。

カスタムなストラテジーについては別途紹介されているので、後ほど合わせて確認したいと思う。

メモリへの会話の保存と、抽出されたメモリの取得

上にもある通り、長期メモリはストラテジー有効後に追加された会話にのみ適用される。ということで、ストラテジーを指定して作成したメモリに会話履歴を追加する。

from bedrock_agentcore.memory import MemoryClient
import time

client = MemoryClient(region_name="us-east-1")

memory_id = "MyAgentMemory1-XXXXXXXXXX"
user_id = "User84"
session_id = "OrderSupportSession1"

client.create_event(
    memory_id = memory_id,
    actor_id=user_id,
    session_id=session_id,
    messages=[
        ("オーダー番号12345について問題があります。", "USER"),
        ("申し訳ありません。そのオーダーについて確認させていただきます。", "ASSISTANT"),
        ("lookup_order(order_id='12345')", "TOOL"),
        (
            "オーダー番号12345の商品は、3日前に発送済みです。どのような問題か、お聞かせいただけますか?",
            "ASSISTANT",
        ),
        ("実は、その前に、メールアドレスの変更もお願いしたいのですが、可能でしょうか?", "USER"),
        (
            "もちろんです。どちらもお任せください。まずはメールアドレスの変更から始めましょう。新しいメールアドレスをお聞かせいただけますか?",
            "ASSISTANT",
        ),
        ("newemail@example.com です。", "USER"),
        ("update_customer_email(old='old@example.com', new='newemail@example.com')", "TOOL"),
        ("メールアドレスの変更が完了しました。それでは、オーダーの問題についてお聞かせいただけますか?", "ASSISTANT"),
        ("商品が破損しています。", "USER"),
    ],
)

# 会話から意味のあるメモリを抽出するために、60秒待つ。
time.sleep(60)

# 上記のストラテジーで設定した名前空間を使用して、問題の要約を検索します。
memories = client.retrieve_memories(
    memory_id=memory_id,
    namespace=f"/summaries/{user_id}/{session_id}",
    query="サポートの問題を要約して。"
)

print(memories)
出力
[
    {
        'memoryRecordId': 'mem-ef6843aaaeafa8d0c99d3cdf2780ff0e01f8',
        'content': {
            'text': '<summary>\n    <topic name="Customer Service">\n        The customer contacted support regarding order #12345 and reported two issues. First, they requested to change their email address, which was updated from old@example.com to newemail@example.com. Second, they reported that the product they received was damaged. The assistant confirmed that order #12345 had been shipped three days prior to the conversation.\n    </topic>\n</summary>'
        },
        'memoryStrategyId': 'SessionSummarizer-XXXXXXXXXX',
        'namespaces': ['/summaries/User84/OrderSupportSession1'],
        'createdAt': datetime.datetime(2025, 7, 29, 23, 34, 20, tzinfo=tzlocal()),
        'score': 0.3644892
    }
]

要約部分だけを抽出するとこんな感じ。

<summary>
<topic name="Customer Service">
The customer contacted support regarding order #12345 and reported two issues. First, they requested to change their email address, which was updated from old@example.com to newemail@example.com. Second, they reported that the product they received was damaged. The assistant confirmed that order #12345 had been shipped three days prior to the conversation.
</topic>
</summary>

日本語訳

<summary>
<topic name="Customer Service">
お客様はサポートに連絡し、注文番号#12345に関する2つの問題を報告しました。まず、メールアドレスの変更を依頼され、old@example.comからnewemail@example.comに変更されました。次に、受け取った商品が破損しているとの報告がありました。アシスタントは、注文番号#12345が会話の3日前に出荷済みであることを確認しました。
</topic>
</summary>

会話の内容が要約されて取得できているのがわかる。

kun432kun432

エージェントへのメモリの組み込み

ではエージェントにメモリを組み込んでみる。Strands Agents SDKも前回インストール済なのでその続き。

ドキュメントのコードは、長期メモリとしてユーザプリファレンスストラテジーを設定したメモリを新規作成しつつ、それを使ったエージェントを作成するものとなっている。いろいろ出力を追加してみた。

from strands import Agent
from strands.models import BedrockModel
from strands_tools.agent_core_memory import AgentCoreMemoryToolProvider
import time
from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

# ユーザ・セッション情報
actor_id = "CaliforniaPerson"
session_id = "TalkingAboutFood"

# ユーザプリファレンスを指定したメモリを作成
memory = client.create_memory_and_wait(
    name="MyAgentMemory",
    strategies=[{
        "userPreferenceMemoryStrategy": {
            "name": "UserPreference",
            "namespaces": ["/users/{actorId}"]
        }
    }]
)
memory_id = memory.get("id")

print(f"メモリID: {memory_id}")
print(f"メモリ: {memory}")
print()

# メモリをツールとして使用する
strands_provider = AgentCoreMemoryToolProvider(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    namespace=f"/users/{actor_id}",
    region="us-east-1"
)

# モデルの定義
bedrock_model = BedrockModel(
    model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
    region_name="us-east-1",
)

# エージェントを作成して、ツールを渡す
agent = Agent(
    model=bedrock_model,
    tools=strands_provider.tools
)

# エージェントにメモリを使用してコミュニケーションを開始
agent("私はベジタリアンで、静かな雰囲気のレストランが好きです。")
agent("イタリアンの料理が食べたいです。")
agent("値段は中程度で、ダウンタウンの近くがいいです。")
agent("私はロサンゼルスに住んでいます。")

time.sleep(60)

# 長期メモリをツールとして使う
agent("どんな料理が食べたかったか、もう忘れちゃったけど、あなたは覚えてる?")


# エージェントが持っている会話履歴
print("-" * 20)
print("エージェントが持っている会話履歴:")
print(agent.messages)

# 短期メモリ
print("-" * 20)
print("短期メモリ:")
conversations = client.list_events(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    max_results=10,
)
for conversation in conversations:
    print(conversation)

# 長期メモリ(ユーザプリファレンス)
print("-" * 20)
print("長期メモリ(ユーザプリファレンス):")
memories = client.retrieve_memories(
    memory_id=memory_id,
    namespace=f"/users/{actor_id}",
    query="ユーザの好みは?"
)
print(memories)

コードを見ると、Strands Agentsで作成したエージェントにAgentCore Memoryを追加する場合はなんかツールっぽく追加するみたい。

実行結果。見やすさのため適宜改行を入れている。まず個々のやり取り。

出力
メモリID: MyAgentMemory-aEVzisDCPH
メモリ: {
	'arn': 'arn:aws:bedrock-agentcore:us-east-1:XXXXXXXXXXXX:memory/MyAgentMemory-XXXXXXXXXX',
	'id': 'MyAgentMemory-XXXXXXXXXX',
	'name': 'MyAgentMemory',
	'eventExpiryDuration': 90,
	'status': 'ACTIVE',
	'createdAt': datetime.datetime(2025, 7, 30, 12, 1, 38, 880000, tzinfo=tzlocal()),
	'updatedAt': datetime.datetime(2025, 7, 30, 12, 1, 43, 741000, tzinfo=tzlocal()),
	'strategies': [
		{
			'strategyId': 'UserPreference-XXXXXXXXXX',
			'name': 'UserPreference',
			'type': 'USER_PREFERENCE',
			'namespaces': ['/users/{actorId}'],
			'createdAt': datetime.datetime(2025, 7, 30, 12, 1, 38, 880000, tzinfo=tzlocal()),
			'updatedAt': datetime.datetime(2025, 7, 30, 12, 1, 43, 741000, tzinfo=tzlocal()),
			'status': 'ACTIVE',
			'memoryStrategyId': 'UserPreference-XXXXXXXXXX',
			'memoryStrategyType': 'USER_PREFERENCE'
		}
	],
	'memoryId': 'MyAgentMemory-aEVzisDCPH',
	'memoryStrategies': [
		{
			'strategyId': 'UserPreference-XXXXXXXXXX',
			'name': 'UserPreference',
			'type': 'USER_PREFERENCE',
			'namespaces': ['/users/{actorId}'],
			'createdAt': datetime.datetime(2025, 7, 30, 12, 1, 38, 880000, tzinfo=tzlocal()),
			'updatedAt': datetime.datetime(2025, 7, 30, 12, 1, 43, 741000, tzinfo=tzlocal()),
			'status': 'ACTIVE',
			'memoryStrategyId': 'UserPreference-XXXXXXXXXX',
			'memoryStrategyType': 'USER_PREFERENCE'
		}
	]
}

あなたの好みを記録させていただきますね。
Tool #1: agent_core_memory
承知いたしました。あなたがベジタリアンで静かな雰囲気のレストランがお好きということを記録しました。今後レストランの提案や食事に関する会話をする際に、この情報を参考にさせていただきますね。

何か他にご質問やお手伝いできることがありましたら、お気軽にお声かけください。
Tool #2: agent_core_memory
イタリアン料理がお食べになりたいのですね!あなたがベジタリアンで静かな雰囲気のレストランがお好みということを考慮すると、以下のようなイタリアン料理がおすすめです:

**ベジタリアン向けイタリアン料理:**
- マルゲリータピザ(トマト、モッツァレラ、バジル)
- ペンネアラビアータ(トマトベースの辛味パスタ)
- リゾット(キノコやほうれん草入り)
- カプレーゼサラダ(トマトとモッツァレラのサラダ)
- ラザニア(野菜やチーズを使ったもの)

静かで落ち着いた雰囲気のイタリアンレストランを探すのがよろしいでしょうね。特定の地域のレストラン情報が必要でしたら、お住まいの地域を教えていただければ、より具体的な提案ができますよ。

何か特定のイタリアン料理で気になるものはありますか?
Tool #3: agent_core_memory
承知いたしました!あなたの希望をまとめると:

**ご希望の条件:**
- ベジタリアン対応
- 静かな雰囲気
- イタリアン料理
- 中程度の価格帯
- ダウンタウン近く

これらの条件に合うレストランを探すのに最適ですね。ダウンタウン周辺には多くのイタリアンレストランがありますが、ベジタリアン向けのメニューが豊富で、落ち着いた雰囲気のお店を選ぶことが重要です。

**おすすめの探し方:**
- レストラン検索サイトで「ベジタリアン対応」「静か」「イタリアン」でフィルター
- 口コミで雰囲気について確認
- 事前に電話でベジタリアンメニューについて問い合わせ

どちらの都市のダウンタウンをお探しでしょうか?具体的な都市名が分かれば、より詳しい提案ができます。
Tool #4: agent_core_memory
ロサンゼルスでしたら素晴らしい選択肢がたくさんありますね!ダウンタウンLAには、あなたの条件に合うイタリアンレストランがいくつかあります。

**ロサンゼルスのダウンタウン周辺でおすすめのイタリアンレストラン:**

1. **Bottega Louie** (Grand Central Market近く)
   - 中程度の価格帯
   - エレガントで落ち着いた雰囲気
   - ベジタリアンパスタやピザが豊富

2. **Bestia** (Arts District)
   - モダンな雰囲気
   - 野菜料理やベジタリアンオプションあり
   - 少し賑やかですが人気店

3. **Republique** (Mid-City、ダウンタウンから少し離れるが価値あり)
   - フレンチ・イタリアン
   - 落ち着いた雰囲気
   - 素晴らしいベジタリアンメニュー

**探すときのポイント:**
- OpenTableやYelpでレビューをチェック
- 予約時にベジタリアンメニューについて確認
- 平日の早めの時間帯は静かな場合が多い

どのエリアが一番アクセスしやすいですか?
Tool #5: agent_core_memory
はい、覚えていますよ!あなたは**イタリアン料理**が食べたいとおっしゃっていました。

具体的には、あなたの希望をまとめると:
- **イタリアン料理**が食べたい
- **ベジタリアン**対応
- **静かな雰囲気**のレストラン
- **中程度の価格帯**
- **ダウンタウン**(ロサンゼルス)の近く

そして、マルゲリータピザやペンネアラビアータ、キノコのリゾットなどのベジタリアン向けイタリアン料理についてもお話ししていましたね。

まだイタリアン料理を食べに行かれていないのでしたら、先ほどご提案したBottega LouieやBestiaなどのレストランはいかがでしょうか?

まずは一連の会話を見る限り、会話の流れが継続しているのがわかる。都度都度ツールとしてAgentCore Memoryが呼び出されているのも見える。

ただし、そもそもStrands Agentsはエージェントそのものが会話履歴を状態として管理しているはずであり、この回答がAgentCore Memoryがなくても回答は可能なはず。エージェントが持っている会話履歴を見てみる。

出力
--------------------
エージェントが持っている会話履歴:
[
	{
		'role': 'user',
		'content': [
			{
				'text': '私はベジタリアンで、静かな雰囲気のレストランが好きです。'
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'text': 'あなたの好みを記録させていただきますね。'
			},
			{
				'toolUse': {
					'toolUseId': 'tooluse_LR0V5Ti2T02T57Pi3jJ0_Q',
					'name': 'agent_core_memory',
					'input': {
						'action': 'record',
						'content': 'ユーザーはベジタリアンで、静かな雰囲気のレストランを好む'
					}
				}
			}
		]
	},
	{
		'role': 'user',
		'content': [
			{
				'toolResult': {
					'status': 'success',
					'content': [
						{
							'text': 'Memory created successfully: {"memoryId": "MyAgentMemory-XXXXXXXXXX", "actorId": "CaliforniaPerson", "sessionId": "TalkingAboutFood", "eventId": "0000001753844669000#4ca48d12", "eventTimestamp": "2025-07-30 12:04:29+09:00", "branch": {"name": "main"}}'
						}
					],
					'toolUseId': 'tooluse_LR0V5Ti2T02T57Pi3jJ0_Q'
				}
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'text': '承知いたしました。あなたがベジタリアンで静かな雰囲気のレストランがお好きということを記録しました。今後レストランの提案や食事に関する会話をする際に、この情報を参考にさせていただきますね。\n\n何か他にご質問やお手伝いできることがありましたら、お気軽にお声かけください。'
			}
		]
	},
	{
		'role': 'user',
		'content': [
			{
				'text': 'イタリアンの料理が食べたいです。'
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'toolUse': {
					'toolUseId': 'tooluse_Qqxt_QXlRvipX6asKR_6aQ',
					'name': 'agent_core_memory',
					'input': {
						'action': 'record',
						'content': 'ユーザーはイタリアン料理が食べたいと希望している'
					}
				}
			}
		]
	},
	{
		'role': 'user',
		'content': [
			{
				'toolResult': {
					'status': 'success',
					'content': [
						{
							'text': 'Memory created successfully: {"memoryId": "MyAgentMemory-XXXXXXXXXX", "actorId": "CaliforniaPerson", "sessionId": "TalkingAboutFood", "eventId": "0000001753844699000#99224dba", "eventTimestamp": "2025-07-30 12:04:59+09:00", "branch": {"name": "main"}}'
						}
					],
					'toolUseId': 'tooluse_Qqxt_QXlRvipX6asKR_6aQ'
				}
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'text': 'イタリアン料理がお食べになりたいのですね!あなたがベジタリアンで静かな雰囲気のレストランがお好みということを考慮すると、以下のようなイタリアン料理がおすすめです:\n\n**ベジタリアン向けイタリアン料理:**\n- マルゲリータピザ(トマト、モッツァレラ、バジル)\n- ペンネアラビアータ(トマトベースの辛味パスタ)\n- リゾット(キノコやほうれん草入り)\n- カプレーゼサラダ(トマトとモッツァレラのサラダ)\n- ラザニア(野菜やチーズを使ったもの)\n\n静かで落ち着いた雰囲気のイタリアンレストランを探すのがよろしいでしょうね。特定の地域のレストラン情報が必要でしたら、お住まいの地域を教えていただければ、より具体的な提案ができますよ。\n\n何か特定のイタリアン料理で気になるものはありますか?'
			}
		]
	},
	{
		'role': 'user',
		'content': [
			{
				'text': '値段は中程度で、ダウンタウンの近くがいいです。'
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'toolUse': {
					'toolUseId': 'tooluse_MHO0UZzvQSO6_zgnn39aAQ',
					'name': 'agent_core_memory',
					'input': {
						'action': 'record',
						'content': 'ユーザーは中程度の価格帯で、ダウンタウン近くのレストランを希望している'
					}
				}
			}
		]
	},
	{
		'role': 'user',
		'content': [
			{
				'toolResult': {
					'status': 'success',
					'content': [
						{
							'text': 'Memory created successfully: {"memoryId": "MyAgentMemory-XXXXXXXXXX", "actorId": "CaliforniaPerson", "sessionId": "TalkingAboutFood", "eventId": "0000001753844758000#ad614b90", "eventTimestamp": "2025-07-30 12:05:58+09:00", "branch": {"name": "main"}}'
						}
					],
					'toolUseId': 'tooluse_MHO0UZzvQSO6_zgnn39aAQ'
				}
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'text': '承知いたしました!あなたの希望をまとめると:\n\n**ご希望の条件:**\n- ベジタリアン対応\n- 静かな雰囲気\n- イタリアン料理\n- 中程度の価格帯\n- ダウンタウン近く\n\nこれらの条件に合うレストランを探すのに最適ですね。ダウンタウン周辺には多くのイタリアンレストランがありますが、ベジタリアン向けのメニューが豊富で、落ち着いた雰囲気のお店を選ぶことが重要です。\n\n**おすすめの探し方:**\n- レストラン検索サイトで「ベジタリアン対応」「静か」「イタリアン」でフィルター\n- 口コミで雰囲気について確認\n- 事前に電話でベジタリアンメニューについて問い合わせ\n\nどちらの都市のダウンタウンをお探しでしょうか?具体的な都市名が分かれば、より詳しい提案ができます。'
			}
		]
	},
	{
		'role': 'user',
		'content': [
			{
				'text': '私はロサンゼルスに住んでいます。'
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'toolUse': {
					'toolUseId': 'tooluse_2dHHiqSgTUGQ2H7eow-EAg',
					'name': 'agent_core_memory',
					'input': {
						'action': 'record', 'content': 'ユーザーはロサンゼルスに住んでいる'
					}
				}
			}
		]
	},
	{
		'role': 'user',
		'content': [
			{
				'toolResult': {
					'status': 'success',
					'content': [
						{
							'text': 'Memory created successfully: {"memoryId": "MyAgentMemory-XXXXXXXXXX", "actorId": "CaliforniaPerson", "sessionId": "TalkingAboutFood", "eventId": "0000001753844819000#ac7f451b", "eventTimestamp": "2025-07-30 12:06:59+09:00", "branch": {"name": "main"}}'
						}
					],
					'toolUseId': 'tooluse_2dHHiqSgTUGQ2H7eow-EAg'
				}
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'text': 'ロサンゼルスでしたら素晴らしい選択肢がたくさんありますね!ダウンタウンLAには、あなたの条件に合うイタリアンレストランがいくつかあります。\n\n**ロサンゼルスのダウンタウン周辺でおすすめのイタリアンレストラン:**\n\n1. **Bottega Louie** (Grand Central Market近く)\n   - 中程度の価格帯\n   - エレガントで落ち着いた雰囲気\n   - ベジタリアンパスタやピザが豊富\n\n2. **Bestia** (Arts District)\n   - モダンな雰囲気\n   - 野菜料理やベジタリアンオプションあり\n   - 少し賑やかですが人気店\n\n3. **Republique** (Mid-City、ダウンタウンから少し離れるが価値あり)\n   - フレンチ・イタリアン\n   - 落ち着いた雰囲気\n   - 素晴らしいベジタリアンメニュー\n\n**探すときのポイント:**\n- OpenTableやYelpでレビューをチェック\n- 予約時にベジタリアンメニューについて確認\n- 平日の早めの時間帯は静かな場合が多い\n\nどのエリアが一番アクセスしやすいですか?'
			}
		]
	},
	{
		'role': 'user',
		'content': [
			{
				'text': 'どんな料理が食べたかったか、もう忘れちゃったけど、あなたは覚えてる?'
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'toolUse': {
					'toolUseId': 'tooluse_M11UQy6sRFaJtbGed2PIpg',
					'name': 'agent_core_memory',
					'input': {
						'action': 'retrieve',
						'query': 'イタリアン料理 食べたい'
					}
				}
			}
		]
	},
	{
		'role': 'user',
		'content': [
			{
				'toolResult': {
					'status': 'success',
					'content': [
						{
							'text': 'Memories retrieved successfully: {"memoryRecordSummaries": [{"memoryRecordId": "mem-c83bdecd-5926-435c-b6b1-dddeedeb50d1", "content": {"text": "{\\"context\\":\\"The assistant mentioned that the user wants to eat Italian cuisine and has shown repeated interest in it\\",\\"preference\\":\\"Enjoys Italian cuisine\\",\\"categories\\":[\\"food\\",\\"cuisine\\"]}"}, "memoryStrategyId": "UserPreference-XXXXXXXXXX", "namespaces": ["/users/CaliforniaPerson"], "createdAt": "2025-07-30 12:06:59+09:00", "score": 0.66473913}, {"memoryRecordId": "mem-438b1d8a-fc1a-4bcc-a150-fae02b4fa00a", "content": {"text": "{\\"context\\":\\"The system mentioned the user\'s dietary preference\\",\\"preference\\":\\"Vegetarian diet\\",\\"categories\\":[\\"food\\",\\"diet\\"]}"}, "memoryStrategyId": "UserPreference-XXXXXXXXXX", "namespaces": ["/users/CaliforniaPerson"], "createdAt": "2025-07-30 12:04:29+09:00", "score": 0.37793505}, {"memoryRecordId": "mem-8c1611dc-5531-4a70-9ad6-174f63ba18ba", "content": {"text": "{\\"context\\":\\"The system noted the user\'s restaurant atmosphere preference\\",\\"preference\\":\\"Prefers quiet restaurant atmosphere\\",\\"categories\\":[\\"dining\\",\\"atmosphere\\",\\"ambiance\\"]}"}, "memoryStrategyId": "UserPreference-XXXXXXXXXX", "namespaces": ["/users/CaliforniaPerson"], "createdAt": "2025-07-30 12:06:59+09:00", "score": 0.3478774}, {"memoryRecordId": "mem-45383b7b-8414-41a4-951b-0a5bcec14c64", "content": {"text": "{\\"context\\":\\"The assistant suggested a restaurant in downtown with a moderate price range, indicating the user\'s current preference\\",\\"preference\\":\\"Prefers mid-range price restaurants near downtown\\",\\"categories\\":[\\"food\\",\\"dining\\",\\"location\\",\\"budget\\"]}"}, "memoryStrategyId": "UserPreference-XXXXXXXXXX", "namespaces": ["/users/CaliforniaPerson"], "createdAt": "2025-07-30 12:06:59+09:00", "score": 0.3472069}]}'
						}
					],
					'toolUseId': 'tooluse_M11UQy6sRFaJtbGed2PIpg'
				}
			}
		]
	},
	{
		'role': 'assistant',
		'content': [
			{
				'text': 'はい、覚えていますよ!あなたは**イタリアン料理**が食べたいとおっしゃっていました。\n\n具体的には、あなたの希望をまとめると:\n- **イタリアン料理**が食べたい\n- **ベジタリアン**対応\n- **静かな雰囲気**のレストラン\n- **中程度の価格帯**\n- **ダウンタウン**(ロサンゼルス)の近く\n\nそして、マルゲリータピザやペンネアラビアータ、キノコのリゾットなどのベジタリアン向けイタリアン料理についてもお話ししていましたね。\n\nまだイタリアン料理を食べに行かれていないのでしたら、先ほどご提案したBottega LouieやBestiaなどのレストランはいかがでしょうか?'
			}
		]
	}
]

ユーザとエージェントの個々のやりとりが記録されているとともに、ユーザの入力後にツールが呼び出されて、どうやらユーザの入力からプリファレンスを抽出しているように見える。自分は、AgentCoreの長期メモリの説明を読んだとき、「クラウド側」で「非同期」に行われるもの、つまりユーザインタラクションのレイテンシー等には影響しないもの、と思っていたが、この挙動だけを見ると果たしてそうなのだろうか?という気がちょっとしてきた。ただし、途中で長期メモリの抽出のためのsleepを入れているので、その前後で見比べてみないとわからない。

次にAgentCore側の短期メモリを見てみる。

出力
--------------------
短期メモリ:
{
	'memoryId': 'MyAgentMemory-XXXXXXXXXX',
	'actorId': 'CaliforniaPerson',
	'sessionId': 'TalkingAboutFood',
	'eventId': '0000001753844819000#ac7f451b',
	'eventTimestamp': datetime.datetime(2025, 7, 30, 12, 6, 59, tzinfo=tzlocal()),
	'payload': [{'conversational': {'content': {'text': 'ユーザーはロサンゼルスに住んでいる'}, 'role': 'ASSISTANT'}}],
	'branch': {'name': 'main'}
}
{
	'memoryId': 'MyAgentMemory-XXXXXXXXXX',
	'actorId': 'CaliforniaPerson',
	'sessionId': 'TalkingAboutFood',
	'eventId': '0000001753844758000#ad614b90',
	'eventTimestamp': datetime.datetime(2025, 7, 30, 12, 5, 58, tzinfo=tzlocal()),
	'payload': [{'conversational': {'content': {'text': 'ユーザーは中程度の価格帯で、ダウンタウン近くのレストランを希望している'}, 'role': 'ASSISTANT'}}], 'branch': {'name': 'main'}
}
{
	'memoryId': 'MyAgentMemory-XXXXXXXXXX',
	'actorId': 'CaliforniaPerson',
	'sessionId': 'TalkingAboutFood',
	'eventId': '0000001753844699000#99224dba',
	'eventTimestamp': datetime.datetime(2025, 7, 30, 12, 4, 59, tzinfo=tzlocal()),
	'payload': [{'conversational': {'content': {'text': 'ユーザーはイタリアン料理が食べたいと希望している'}, 'role': 'ASSISTANT'}}],
	'branch': {'name': 'main'}
}
{
	'memoryId': 'MyAgentMemory-XXXXXXXXXX',
	'actorId': 'CaliforniaPerson',
	'sessionId': 'TalkingAboutFood',
	'eventId': '0000001753844669000#4ca48d12',
	'eventTimestamp': datetime.datetime(2025, 7, 30, 12, 4, 29, tzinfo=tzlocal()),
	'payload': [{'conversational': {'content': {'text': 'ユーザーはベジタリアンで、静かな雰囲気のレストランを好む'}, 'role': 'ASSISTANT'}}],
	'branch': {'name': 'main'}
}

上の方で短期メモリを単体で試した際は、単純に全部のやり取りがイベントとして保存されていたが、今回はプリファレンス抽出後のデータだけとなっている・・・この時点で生のやり取りが圧縮されていることになるし、アシスタント側の発話も残ってないのだけど、果たしてこれで短期記憶として機能するのだろうか?ちょっとここはlist_eventsのオプションなども確認してみたい。

最後に長期メモリ。

出力
--------------------
長期メモリ(ユーザプリファレンス):
[
	{
		'memoryRecordId': 'mem-438b1d8a-fc1a-4bcc-a150-fae02b4fa00a',
		'content': {
			'text': '{"context":"The system mentioned the user\'s dietary preference","preference":"Vegetarian diet","categories":["food","diet"]}'
		},
		'memoryStrategyId': 'UserPreference-XXXXXXXXXX',
		'namespaces': ['/users/CaliforniaPerson'],
		'createdAt': datetime.datetime(2025, 7, 30, 12, 4, 29, tzinfo=tzlocal()),
		'score': 0.4038627
	},
	{
		'memoryRecordId': 'mem-8c1611dc-5531-4a70-9ad6-174f63ba18ba',
		'content': {
			'text': '{"context":"The system noted the user\'s restaurant atmosphere preference","preference":"Prefers quiet restaurant atmosphere","categories":["dining","atmosphere","ambiance"]}'
		},
		'memoryStrategyId': 'UserPreference-XXXXXXXXXX',
		'namespaces': ['/users/CaliforniaPerson'],
		'createdAt': datetime.datetime(2025, 7, 30, 12, 6, 59, tzinfo=tzlocal()),
		'score': 0.37398234
	},
	{
		'memoryRecordId': 'mem-c83bdecd-5926-435c-b6b1-dddeedeb50d1',
		'content': {
			'text': '{"context":"The assistant mentioned that the user wants to eat Italian cuisine and has shown repeated interest in it","preference":"Enjoys Italian cuisine","categories":["food","cuisine"]}'
		},
		'memoryStrategyId': 'UserPreference-XXXXXXXXXX',
		'namespaces': ['/users/CaliforniaPerson'],
		'createdAt': datetime.datetime(2025, 7, 30, 12, 6, 59, tzinfo=tzlocal()),
		'score': 0.37103364
	}
]

ここは抽出したプリファレンスに対して、カテゴリーなどが付与されているのが見える。おそらく長期記憶からの検索時等にフィルタ的に使用されるのだろうと思われる。

気になったのは

  • 長期記憶のための抽出処理がインタラクションのレイテンシーに影響するか?
  • 長期記憶有効時に、生のやり取りはどこまでAgentCore Memoryに保存されているか?
  • ストラテジーの違いによって、振る舞いやデータ構造などがどう異なるか?

というところ。

kun432kun432

上の方で短期メモリを単体で試した際は、単純に全部のやり取りがイベントとして保存されていたが、今回はプリファレンス抽出後のデータだけとなっている・・・この時点で生のやり取りが圧縮されていることになるし、アシスタント側の発話も残ってないのだけど、果たしてこれで短期記憶として機能するのだろうか?ちょっとここは list_events のオプションなども確認してみたい。

長期記憶有効時に、生のやり取りはどこまでAgentCore Memoryに保存されているか?

ということで list_events のオプションを見てみる。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/short-term-memory-operations.html#list-events

いくつかオプションがあるようだが簡単な説明だけなので、SDKのAPIリファレンスを見たいなー、どこにあるかわからないのでIDEで見ている。

(method) def list_events(
    memory_id: str,
    actor_id: str,
    session_id: str,
    branch_name: str | None = None,
    include_parent_events: bool = False,
    max_results: int = 100,
    include_payload: bool = True
) -> List[Dict[str, Any]]

セッション内のすべてのイベントをページネーション機能付きで一覧表示します。

このメソッドは生のイベントAPIへの直接アクセスを提供し、開発者が get_last_k_turns のターングループ化ロジックを使用すること無く、すべてのイベントを取得できます。

引数:
    memory_id: メモリリソースID
    actor_id: アクター識別子
    session_id: セッション識別子
    branch_name: イベントをフィルタリングするためのオプションのブランチ名(すべてのブランチを対象とする場合はNone)
    include_parent_events: 親ブランチのイベントを含めるかどうか(branch_nameが指定されている場合のみ適用)
    max_results: 返すイベントの最大数
    include_payload: レスポンスにイベントのペイロードを含めるかどうか

返り値:
    時系列順に並べたイベント辞書の一覧
    
例:
    # 全てのイベントを取得
    events = client.list_events(memory_id, actor_id, session_id)

    # メインブランチのイベントを取得
    main_events = client.list_events(memory_id, actor_id, session_id, branch_name="main")

    # 特定のブランチのイベントを取得
    branch_events = client.list_events(memory_id, actor_id, session_id, branch_name="test-branch")

関係ありそうなのは includePayloadsinclude_parent_events あたり。ただ、説明を読む限りはどちらも意味がなさそうに思えるが、とりあえず有効にして確認してみた。

from strands import Agent
from strands.models import BedrockModel
from strands_tools.agent_core_memory import AgentCoreMemoryToolProvider
import time
from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

# 同じユーザ・セッションを使う
actor_id = "CaliforniaPerson"
session_id = "TalkingAboutFood"
# 同じメモリIDを使う
memory_id = "MyAgentMemory-XXXXXXXXXX"

# 短期メモリ
print("短期メモリ:")
conversations = client.list_events(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    max_results=10,
    include_parent_events=True,
    include_payload=True,
)
for conversation in conversations:
    print(conversation)
出力
短期メモリ:
{
	'memoryId': 'MyAgentMemory-XXXXXXXXXX',
	'actorId': 'CaliforniaPerson',
	'sessionId': 'TalkingAboutFood',
	'eventId': '0000001753844819000#ac7f451b',
	'eventTimestamp': datetime.datetime(2025, 7, 30, 12, 6, 59, tzinfo=tzlocal()),
	'payload': [{'conversational': {'content': {'text': 'ユーザーはロサンゼルスに住んでいる'}, 'role': 'ASSISTANT'}}],
	'branch': {'name': 'main'}
}
{
	'memoryId': 'MyAgentMemory-XXXXXXXXXX',
	'actorId': 'CaliforniaPerson',
	'sessionId': 'TalkingAboutFood',
	'eventId': '0000001753844758000#ad614b90',
	'eventTimestamp': datetime.datetime(2025, 7, 30, 12, 5, 58, tzinfo=tzlocal()),
	'payload': [{'conversational': {'content': {'text': 'ユーザーは中程度の価格帯で、ダウンタウン近くのレストランを希望している'}, 'role': 'ASSISTANT'}}], 'branch': {'name': 'main'}
}
{
	'memoryId': 'MyAgentMemory-XXXXXXXXXX',
	'actorId': 'CaliforniaPerson',
	'sessionId': 'TalkingAboutFood',
	'eventId': '0000001753844699000#99224dba',
	'eventTimestamp': datetime.datetime(2025, 7, 30, 12, 4, 59, tzinfo=tzlocal()),
	'payload': [{'conversational': {'content': {'text': 'ユーザーはイタリアン料理が食べたいと希望している'}, 'role': 'ASSISTANT'}}],
	'branch': {'name': 'main'}
}
{
	'memoryId': 'MyAgentMemory-XXXXXXXXXX',
	'actorId': 'CaliforniaPerson',
	'sessionId': 'TalkingAboutFood',
	'eventId': '0000001753844669000#4ca48d12',
	'eventTimestamp': datetime.datetime(2025, 7, 30, 12, 4, 29, tzinfo=tzlocal()),
	'payload': [{'conversational': {'content': {'text': 'ユーザーはベジタリアンで、静かな雰囲気のレストランを好む'}, 'role': 'ASSISTANT'}}],
	'branch': {'name': 'main'}
}

うーん、結果は変わらず。

短期メモリには、ユーザのプリファレンスのみ・アシスタントの回答は含まれていない、ように見えるので、アシスタントの応答内容について再度エージェントに聞いてみる。エージェントは再度初期化されるのでエージェント内の会話履歴は消えて、AgentCore Memoryからのみ確認されるはず。

from strands import Agent
from strands.models import BedrockModel
from strands_tools.agent_core_memory import AgentCoreMemoryToolProvider
import time
from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

# 同じユーザ・セッションを使う
actor_id = "CaliforniaPerson"
session_id = "TalkingAboutFood"
# 同じメモリIDを使う
memory_id = "MyAgentMemory-XXXXXXXXXX"

strands_provider = AgentCoreMemoryToolProvider(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    namespace=f"/users/{actor_id}",
    region="us-east-1"
)

bedrock_model = BedrockModel(
    model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
    region_name="us-east-1",
)

agent = Agent(
    model=bedrock_model,
    tools=strands_provider.tools
)

agent("さっき教えてくれた、おすすめのイタリアンレストランをもう一度教えて。")
出力
申し訳ございませんが、過去の会話でおすすめしたイタリアンレストランについて記憶を検索してみますね。
Tool #1: agent_core_memory
お探しの具体的なイタリアンレストランの名前については、記憶に詳細な情報が残っていないようです。ただし、あなたがイタリア料理を好まれることと、以下のような好みがあることは覚えています:

- イタリア料理がお好き
- ベジタリアン料理を好まれる
- ダウンタウン近くの中価格帯のレストラン
- 静かな雰囲気のお店

もう一度、より詳しい記憶を検索してみますね。
Tool #2: agent_core_memory
申し訳ございません。私の記憶には具体的なイタリアンレストランの店名が残っていないようです。

もしかすると、以前の会話で具体的な店名をお伝えしていたかもしれませんが、その詳細な情報が記録されていない状態です。

お手数をおかけして申し訳ありませんが、どのような特徴のイタリアンレストランをお探しでしたでしょうか?あなたの好みに合わせて、改めておすすめのお店をご提案させていただきます:

- ベジタリアン料理が充実している
- ダウンタウン近く
- 中価格帯
- 静かな雰囲気

これらの条件に合うお店を探してご提案いたしますが、よろしいでしょうか?

なるほど、やはり確認した通り、ユーザ発話からのプリファレンスの情報しか残っておらず、短期メモリを確認した結果の通り。

まだまだ確認が必要だとは思うが、とりあえず現時点での自分の認識は、

  • 短期メモリは生のやり取りを残すもの、長期記憶はそれらから特定の要素を抽出したもの、というほどシンプルではなさそう。
  • 長期メモリを有効にした場合、短期メモリの内容に影響するものがありそう。長期メモリを有効にする場合は、実際にどういった内容が短期メモリ・長期メモリに保存されているか?欠落するコンテキストはないのか?を確認したほうが良さそう。
  • ストラテジーの種類により上記が異なるかどうかを確認したほうが良さそう。
  • ストラテジーは「どれか1つ」ではなく「併用」できるようにも思える。自分が試した限りは、少なくとも長期メモリでユーザプリファレンス「だけ」を指定する使い方は基本的になしに思える。このあたりの使い方のプラクティスを確認したほうが良さそう。

という感じ。これが、こういう仕様なのか、プレビュー時点だからなのか、それともバグなのか、は現時点ではわからない。

kun432kun432

他のストラテジーもそれぞれ単体でエージェントに組み込んでみて試してみたけど、上と同じ結果になった。

切り分けるために、エージェントは忘れて、メモリ単体、つまりAgentCore SDKだけで確認してみる。試行錯誤がやりやすいように今回はJupyterで。

事前準備

作業ディレクトリ作成。

mkdir agentcore-memory-work && $_

使い捨てJupyterを起動。AWS CLIのコンフィグなどのディレクトリをマウントしておく。

docker run --rm \
    -p 8888:8888 \
    -u root \
    -e GRANT_SUDO=yes \
    -v "$(pwd):/home/jovyan/work:cached" \
    -v "$HOME/.aws:/home/jovyan/.aws:cached" \
    quay.io/jupyter/minimal-notebook:latest

Jupyterにアクセス。

まずターミナルを立ち上げてAWS CLIをインストール

curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip" && \
    unzip awscliv2.zip && \
    sudo ./aws/install --update && \
    rm -rf awscliv2.zip aws/

jqインストール

sudo apt update && sudo apt install -y jq

確認

aws bedrock-runtime converse \
    --model-id 'us.anthropic.claude-3-5-haiku-20241022-v1:0' \
    --messages '[{"role": "user", "content": [{"text": "こんにちは!"}]}]' \
    --region us-east-1 \
    | jq -r .output.message.content[0].text
出力
こんにちは!お手伝いできることがありましたら、お気軽にお聞きください。どんなことでもサポートさせていただきます。

準備完了。 workフォルダに移動して、ノートブックを開く。以降はノートブック上での作業。

AgentCore SDKとStrands Agents SDKをインストール

!pip install bedrock-agentcore strands-agents strands-agents-tools
!pip freeze | grep -i agent
出力
bedrock-agentcore==0.1.1
strands-agents==1.1.0
strands-agents-tools==0.2.2

セッションサマリ

長期メモリとしてセッションサマリを有効にしたメモリを作成。(余談だが、actor_idじゃなくてuser_idとして定義したことをあとで後悔した・・・)

from bedrock_agentcore.memory import MemoryClient
import time

client = MemoryClient(region_name="us-east-1")

actor_id = "user_001"
session_id = "session_001"

memory = client.create_memory_and_wait(
    name="SessionSummaryMemoryTest",
    strategies=[{
        "summaryMemoryStrategy": {
            "name": "SessionSummarizer",
            "namespaces": ["/summaries/{actorId}/{sessionId}"]
        }
    }]
)
memory_id = memory.get("id")
print(memory_id)
出力
SessionSummaryMemoryTest

メモリにイベントを登録

client.create_event(
    memory_id = memory_id,
    actor_id=actor_id,
    session_id=session_id,
    messages=[
        ("私はベジタリアンで、静かな雰囲気のレストランが好きです。", "USER"), 
        ("それは素敵な好みですね!ベジタリアン対応で、静かで落ち着いた雰囲気のレストランをお探しとのことですが、もう少し詳しく教えていただけますか?", "ASSISTANT"), 
        ("イタリアンの料理が食べたいです。", "USER"), 
        ("ベジタリアン対応で静かな雰囲気のイタリアンレストランをお探しですね。とても素敵な組み合わせです。予算やエリアなどのご希望はありますか?", "ASSISTANT"), 
        ("値段は中程度で、ダウンタウンの近くがいいです。", "USER"), 
        ("ありがとうございます!もしよろしければ、都市名を教えて下さい。それに合わせて具体的なレストランをご紹介いたします!", "ASSISTANT"), 
        ("私はロサンゼルスに住んでいます。", "USER"), 
    ]
)

# 会話から意味のあるメモリを抽出するために、60秒待つ。
time.sleep(60)

短期メモリを参照

import json

conversations = client.list_events(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    max_results=10,
)

for conversation in conversations:
    for event in conversation["payload"]:
        print(
            event["conversational"]["role"],
            ":",
            event["conversational"]["content"]["text"],
        )
出力
USER : 私はベジタリアンで、静かな雰囲気のレストランが好きです。
ASSISTANT : それは素敵な好みですね!ベジタリアン対応で、静かで落ち着いた雰囲気のレストランをお探しとのことですが、もう少し詳しく教えていただけますか?
USER : イタリアンの料理が食べたいです。
ASSISTANT : ベジタリアン対応で静かな雰囲気のイタリアンレストランをお探しですね。とても素敵な組み合わせです。予算やエリアなどのご希望はありますか?
USER : 値段は中程度で、ダウンタウンの近くがいいです。
ASSISTANT : ありがとうございます!もしよろしければ、都市名を教えて下さい。それに合わせて具体的なレストランをご紹介いたします!
USER : 私はロサンゼルスに住んでいます。

長期メモリを出力

memories = client.retrieve_memories(
    memory_id=memory_id,
    namespace=f"/summaries/{actor_id}/{session_id}",
    query="要約して。"
)

for memory in memories:
    print(memory["content"]["text"])
出力
<summary>
    <topic name="Restaurant Preferences">
        The user is looking for a vegetarian restaurant with a quiet atmosphere. They specifically want Italian cuisine. They prefer a mid-range price point and a location near downtown Los Angeles.
    </topic>
    <topic name="User Location">
        The user lives in Los Angeles.
    </topic>
    <topic name="Conversation Context">
        The assistant is helping the user find a restaurant that matches their preferences. The assistant asked for more details about budget, location preferences, and city to provide specific restaurant recommendations.
    </topic>
</summary>

問題なし

セマンティックファクト

from bedrock_agentcore.memory import MemoryClient
import time

client = MemoryClient(region_name="us-east-1")

actor_id = "user_001"
session_id = "session_001"

memory = client.create_memory_and_wait(
    name="SemanticMemoryTest",
    strategies=[{
        "semanticMemoryStrategy": {
            "name": "FactExtractor",
            "namespaces": ["/semantic/{actorId}/{sessionId}"]
        }
    }]
)
memory_id = memory.get("id")
print(memory_id)
出力
SemanticMemoryTest-XXXXXXXXXX
client.create_event(
    memory_id = memory_id,
    actor_id=actor_id,
    session_id=session_id,
    messages=[
        ("私はベジタリアンで、静かな雰囲気のレストランが好きです。", "USER"), 
        ("それは素敵な好みですね!ベジタリアン対応で、静かで落ち着いた雰囲気のレストランをお探しとのことですが、もう少し詳しく教えていただけますか?", "ASSISTANT"), 
        ("イタリアンの料理が食べたいです。", "USER"), 
        ("ベジタリアン対応で静かな雰囲気のイタリアンレストランをお探しですね。とても素敵な組み合わせです。予算やエリアなどのご希望はありますか?", "ASSISTANT"), 
        ("値段は中程度で、ダウンタウンの近くがいいです。", "USER"), 
        ("ありがとうございます!もしよろしければ、都市名を教えて下さい。それに合わせて具体的なレストランをご紹介いたします!", "ASSISTANT"), 
        ("私はロサンゼルスに住んでいます。", "USER"), 
    ]
)

time.sleep(60)

短期メモリ

import json

conversations = client.list_events(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    max_results=10,
)

for conversation in conversations:
    for event in conversation["payload"]:
        print(
            event["conversational"]["role"],
            ":",
            event["conversational"]["content"]["text"],
        )
出力
USER : 私はベジタリアンで、静かな雰囲気のレストランが好きです。
ASSISTANT : それは素敵な好みですね!ベジタリアン対応で、静かで落ち着いた雰囲気のレストランをお探しとのことですが、もう少し詳しく教えていただけますか?
USER : イタリアンの料理が食べたいです。
ASSISTANT : ベジタリアン対応で静かな雰囲気のイタリアンレストランをお探しですね。とても素敵な組み合わせです。予算やエリアなどのご希望はありますか?
USER : 値段は中程度で、ダウンタウンの近くがいいです。
ASSISTANT : ありがとうございます!もしよろしければ、都市名を教えて下さい。それに合わせて具体的なレストランをご紹介いたします!
USER : 私はロサンゼルスに住んでいます。

長期メモリ

memories = client.retrieve_memories(
    memory_id=memory_id,
    namespace=f"/semantic/{actor_id}/{session_id}",
    query="野菜が食べたい。"
)

for memory in memories:
    print(memory["content"]["text"])
出力
The user likes Italian cuisine and is interested in finding a vegetarian-friendly Italian restaurant.
The user is a vegetarian who enjoys quiet restaurants.
The user lives in Los Angeles and prefers mid-range priced restaurants near downtown.

問題なし

ユーザプリファレンス

長期メモリの名前空間の付け方はまだちょっとよくわかってないが、以下がAgentCore SDKのデフォルト

https://github.com/aws/bedrock-agentcore-sdk-python/blob/4f5c80d874f8ae0dcce9fcf91e5d08f5ff023b55/src/bedrock_agentcore/memory/constants.py#L68-L73

これを見る限り、ユーザプリファレンスはセッションIDが不要に思える(ユーザの好みなので、まあセッションごとに管理する意味合いはそれほどなさそう)

from bedrock_agentcore.memory import MemoryClient
import time

client = MemoryClient(region_name="us-east-1")

actor_id = "user_001"
session_id = "session_001"

memory = client.create_memory_and_wait(
    name="UserPreferenceMemoryTest",
    strategies=[{
        "userPreferenceMemoryStrategy": {
            "name": "UserPreference",
            "namespaces": ["/preferences/{actorId}"]
        }
    }]
)
memory_id = memory.get("id")
print(memory_id)
出力
UserPreferenceMemoryTest-XXXXXXXXXX
出力
client.create_event(
    memory_id = memory_id,
    actor_id=actor_id,
    session_id=session_id,
    messages=[
        ("私はベジタリアンで、静かな雰囲気のレストランが好きです。", "USER"), 
        ("それは素敵な好みですね!ベジタリアン対応で、静かで落ち着いた雰囲気のレストランをお探しとのことですが、もう少し詳しく教えていただけますか?", "ASSISTANT"), 
        ("イタリアンの料理が食べたいです。", "USER"), 
        ("ベジタリアン対応で静かな雰囲気のイタリアンレストランをお探しですね。とても素敵な組み合わせです。予算やエリアなどのご希望はありますか?", "ASSISTANT"), 
        ("値段は中程度で、ダウンタウンの近くがいいです。", "USER"), 
        ("ありがとうございます!もしよろしければ、都市名を教えて下さい。それに合わせて具体的なレストランをご紹介いたします!", "ASSISTANT"), 
        ("私はロサンゼルスに住んでいます。", "USER"), 
    ]
)

time.sleep(60)

短期メモリ

import json

conversations = client.list_events(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    max_results=10,
)

for conversation in conversations:
    for event in conversation["payload"]:
        print(
            event["conversational"]["role"],
            ":",
            event["conversational"]["content"]["text"],
        )
出力
USER : 私はベジタリアンで、静かな雰囲気のレストランが好きです。
ASSISTANT : それは素敵な好みですね!ベジタリアン対応で、静かで落ち着いた雰囲気のレストランをお探しとのことですが、もう少し詳しく教えていただけますか?
USER : イタリアンの料理が食べたいです。
ASSISTANT : ベジタリアン対応で静かな雰囲気のイタリアンレストランをお探しですね。とても素敵な組み合わせです。予算やエリアなどのご希望はありますか?
USER : 値段は中程度で、ダウンタウンの近くがいいです。
ASSISTANT : ありがとうございます!もしよろしければ、都市名を教えて下さい。それに合わせて具体的なレストランをご紹介いたします!
USER : 私はロサンゼルスに住んでいます。

長期メモリ

memories = client.retrieve_memories(
    memory_id=memory_id,
    namespace=f"/preferences/{actor_id}",
    query="ユーザの食べ物の好みは?"
)

for memory in memories:
    print(memory["content"]["text"])
出力
{"context":"User explicitly mentioned being a vegetarian","preference":"Vegetarian diet","categories":["food","diet"]}
{"context":"User mentioned wanting to eat Italian cuisine","preference":"Enjoys Italian food","categories":["food","cuisine"]}
{"context":"User indicated preference for mid-range priced restaurants near downtown","preference":"Prefers mid-range priced restaurants located near downtown","categories":["dining","budget","location"]}

問題なし

うーん、これはもしかして・・・

StrandsAgents

再度Strands Agentsでやってみる。セッションサマリで。(actor_idで統一した、最初からそうしとけばよかった・・・)

from strands import Agent
from strands.models import BedrockModel
from strands_tools.agent_core_memory import AgentCoreMemoryToolProvider
import time
from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

actor_id = "user_001"
session_id = "session_001"

# セッションサマリー
memory = client.create_memory_and_wait(
    name="AgentSessionSummaryMemoryTest",
    strategies=[{
        "summaryMemoryStrategy": {
            "name": "SessionSummarizer",
            "namespaces": ["/users/{actorId}/{sessionId}"]
        }
    }]
)
memory_id = memory.get("id")
print(f"メモリID: {memory_id}")

strands_provider = AgentCoreMemoryToolProvider(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    namespace=f"/users/{actor_id}/{session_id}",
    region="us-east-1"
)

bedrock_model = BedrockModel(
    model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
    region_name="us-east-1",
)

agent = Agent(
    model=bedrock_model,
    tools=strands_provider.tools
)

agent("私はベジタリアンで、静かな雰囲気のレストランが好きです。")
agent("イタリアンの料理が食べたいです。")
agent("値段は中程度で、ダウンタウンの近くがいいです。")
agent("私はロサンゼルスに住んでいます。")

print("-" * 20)
print("短期メモリ(エージェントとのやりとりの直後):")
conversations = client.list_events(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    max_results=10,
)
for conversation in conversations:
    for event in conversation["payload"]:
        print(
            event["conversational"]["role"],
            ":",
            event["conversational"]["content"]["text"],
        )

time.sleep(60)

print()

# 短期メモリ
print("-" * 20)
print("短期メモリ(60秒経過後):")
conversations = client.list_events(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    max_results=10,
)
for conversation in conversations:
    for event in conversation["payload"]:
        print(
            event["conversational"]["role"],
            ":",
            event["conversational"]["content"]["text"],
        )

# 長期メモリ
print("-" * 20)
print("長期メモリ(セッションサマリー):")
memories = client.retrieve_memories(
    memory_id=memory_id,
    namespace=f"/users/{actor_id}/{session_id}",
    query="要約して"
)
for memory in memories:
    print(memory["content"]["text"])
出力
メモリID: AgentSessionSummaryMemoryTest-AVBzU99si5
承知いたしました。あなたがベジタリアンで、静かな雰囲気のレストランを好まれるということを記録させていただきますね。
Tool #1: agent_core_memory
記録しました!今後レストランをお勧めする際は、ベジタリアン対応メニューがあり、静かで落ち着いた雰囲気のお店を優先してご提案いたします。他にも何かご希望や好みがございましたら、お気軽にお聞かせください。イタリアン料理がお食べになりたいということですね。その情報も記録させていただきます。
Tool #2: agent_core_memory
記録いたしました!あなたの好みを整理すると:
- ベジタリアン対応
- 静かな雰囲気
- イタリアン料理

この条件に合うレストランをお探しでしょうか?具体的な場所や予算、お食事の時間帯などがあれば、より詳しいご提案ができるかもしれません。また、パスタ、ピザ、リゾットなど、特にお好みのイタリアン料理はございますか?値段は中程度で、ダウンタウンの近くをご希望ということですね。その情報も記録させていただきます。
Tool #3: agent_core_memory
承知いたしました!現在までのあなたのご希望をまとめると:

- ベジタリアン対応
- 静かな雰囲気
- イタリアン料理
- 中程度の価格帯
- ダウンタウン近く

これらの条件に合うレストランの情報をお探しいただければと思います。私はレストランの検索機能を持っていないため、具体的なお店をご提案することはできませんが、これらの条件で検索していただくと、きっと素敵なレストランが見つかると思います。

お食事を楽しまれてくださいね!ロサンゼルスにお住まいということですね。その情報も記録させていただきます。
Tool #4: agent_core_memory
ロサンゼルスでしたら、ダウンタウンLAには素晴らしいイタリアンレストランがたくさんありますね!あなたのご希望条件を改めてまとめると:

- ロサンゼルス(ダウンタウン近く)
- ベジタリアン対応
- 静かな雰囲気
- イタリアン料理
- 中程度の価格帯

ロサンゼルスのダウンタウン周辺には、Little Italy地区やArts District、さらにはWest Hollywood方面にも、ベジタリアンメニューが充実した静かなイタリアンレストランが多数あると思います。Yelp や Google Maps などで「vegetarian Italian restaurants downtown LA」で検索されると、きっと理想的なお店が見つかるでしょう。

素敵なお食事をお楽しみください!
--------------------
短期メモリ(エージェントとのやりとりの直後):
ASSISTANT : ユーザーはロサンゼルスに住んでいる
ASSISTANT : ユーザーは中程度の値段で、ダウンタウンの近くのレストランを希望している
ASSISTANT : ユーザーはイタリアン料理を食べたいと希望している
ASSISTANT : ユーザーはベジタリアンで、静かな雰囲気のレストランを好む

--------------------
短期メモリ(60秒経過後):
ASSISTANT : ユーザーはロサンゼルスに住んでいる
ASSISTANT : ユーザーは中程度の値段で、ダウンタウンの近くのレストランを希望している
ASSISTANT : ユーザーはイタリアン料理を食べたいと希望している
ASSISTANT : ユーザーはベジタリアンで、静かな雰囲気のレストランを好む
--------------------
長期メモリ(セッションサマリー):
<summary>
    <topic name="User Preferences">
        The user is a vegetarian and prefers restaurants with a quiet atmosphere as noted on March 30, 2025. The user also wants to eat Italian food, as indicated on March 30, 2025. The user is looking for a restaurant with moderate pricing and located near downtown, as mentioned on March 30, 2025.
    </topic>
    <topic name="User Location">
        The user lives in Los Angeles, as noted on March 30, 2025.
    </topic>
</summary>

あぁ、これはAgentCoreMemoryToolProviderだな、これが短期メモリの書き方を変えてしまっている可能性が高い・・・

多分このあたりが参考になりそう

https://github.com/strands-agents/sdk-python/issues/486

んー、なんとなくだけど、自分的には、AgentCore Memory向けにStrands Agents側でSession Manager作るべきだと思うけどなぁ・・・今のツールベースのアプローチは開発者側がやらないといけないことが多くなると思うんだけども・・・あと、せっかくAgentCore Memory側で短期→長期を非同期でやってくれるのに、こういうツール的アプローチの使い方だとそのメリットが活かせないのではなかろうか・・・

とりあえず現状は手動でやる必要がありそう。AgentCoreはプレビューだしね。ただどちらもAWS公式なので、もうちょっと連携してほしい感。

kun432kun432

上のIssueを見ていたのだけど、Strands Agentsで現時点でやる場合には、短期と長期で使い方を分けたほうがよくて、ちょろっと書いてあるとおりHookを使って

  • AgentCoreでメモリを作る、長期も設定しておく
  • 短期: Strands AgentsのHookを使ってイベントをAgentCore Memoryに登録
  • 長期: Strands AgentsのToolを使って、長期メモリを検索できるツールとして、エージェントに渡し、必要な時にAgentCore Memoryを検索

という感じにすればとりあえず今の形でもできそうな気がする。

kun432kun432

その他

  • カスタムストラテジー
  • 複数のストラテジーの同時適用
  • ストラテジーの名前空間
  • ブランチの使い方

など、まだ確認すべき点は多いが、ちょっと色々調べるのに疲れたので、一旦ここまで。

とりあえずAgentCore Memory単体だと確かに便利に使えそうではあるが、現状はインテグレーションが少ないし、いろいろ考えないといけないことも多そうではある。

kun432kun432

次はBuilt-in Tools。Identityに興味があるのだけど、そのためにもTools / Gatewayを先にやらないと。

kun432kun432

上の方でメモリへのイベント登録は以下のように行っていた。再掲。

イベントの登録には create_event、イベントの参照は list_events を使う

from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

client.create_event(
   # メモリID。create_memory か list_memories で取得
   memory_id="CustomerSupportAgentMemory-XXXXXXXXXXXX",
   # アクターID。特定のエージェント、または エンドユーザーを識別する一意のID
   actor_id="User84",
   # セッションID。特定のリクエスト、または一連の会話を識別する一意のID
   session_id="OrderSupportSession1",
   messages=[
       ("オーダー番号12345について問題があります。", "USER"),
       ("申し訳ありません。そのオーダーについて確認させていただきます。", "ASSISTANT"),
       ("lookup_order(order_id='12345')", "TOOL"),
       (
           "オーダー番号12345の商品は、3日前に発送済みです。どのような問題か、お聞かせいただけますか?",
           "ASSISTANT",
       ),
       ("実は、その前に、メールアドレスの変更もお願いしたいのですが、可能でしょうか?", "USER"),
       (
           "もちろんです。どちらもお任せください。まずはメールアドレスの変更から始めましょう。新しいメールアドレスをお聞かせいただけますか?",
           "ASSISTANT",
       ),
       ("newemail@example.com です。", "USER"),
       ("update_customer_email(old='old@example.com', new='newemail@example.com')", "TOOL"),
       ("メールアドレスの変更が完了しました。それでは、オーダーの問題についてお聞かせいただけますか?", "ASSISTANT"),
       ("商品が破損しています。", "USER"),
   ],
)

conversations = client.list_events(
   memory_id="CustomerSupportAgentMemory-XXXXXXXXXXXX",
   actor_id="User84",
   session_id="OrderSupportSession1",
   max_results=10,
)

for conversation in conversations:
   print(conversation)

結果。見やすさのために改行を入れている。

出力
{
   'memoryId': 'CustomerSupportAgentMemory-XXXXXXXXXXXX',
   'actorId': 'User84',
   'sessionId': 'OrderSupportSession1',
   'eventId': '0000001753786479000#5610ec79',
   'eventTimestamp': datetime.datetime(2025, 7, 29, 19, 54, 39, tzinfo=tzlocal()),
   'payload': [
       {'conversational': {'content': {'text': 'オーダー番号12345について問題があります。'}, 'role': 'USER'}},
       {'conversational': {'content': {'text': '申し訳ありません。そのオーダーについて確認させていただきます。'}, 'role': 'ASSISTANT'}},
       {'conversational': {'content': {'text': "lookup_order(order_id='12345')"}, 'role': 'TOOL'}},
       {'conversational': {'content': {'text': 'オーダー番号12345の商品は、3日前に発送済みです。どのような問題か、お聞かせいただけますか?'}, 'role': 'ASSISTANT'}},
       {'conversational': {'content': {'text': '実は、その前に、メールアドレスの変更もお願いしたいのですが、可能でしょうか?'}, 'role': 'USER'}},
       {'conversational': {'content': {'text': 'もちろんです。どちらもお任せください。まずはメールアドレスの変更から始めましょう。新しいメールアドレスをお聞かせいただけますか?'}, 'role': 'ASSISTANT'}},
       {'conversational': {'content': {'text': 'newemail@example.com です。'}, 'role': 'USER'}},
       {'conversational': {'content': {'text': "update_customer_email(old='old@example.com', new='newemail@example.com')"}, 'role': 'TOOL'}},
       {'conversational': {'content': {'text': 'メールアドレスの変更が完了しました。それでは、オーダーの問題についてお聞かせいただけますか?'}, 'role': 'ASSISTANT'}},
       {'conversational': {'content': {'text': '商品が破損しています。'}, 'role': 'USER'}}
   ],
   'branch': {'name': 'main'}
}

create_event() を使うと、よくある会話履歴のフォーマットでイベントを登録ができるのだけども、このフォーマットは以下のようになっている。

https://github.com/aws/bedrock-agentcore-sdk-python/blob/1a85ebe91f148330799202c1920e948fa8131a32/src/bedrock_agentcore/memory/client.py#L330-L338

https://github.com/aws/bedrock-agentcore-sdk-python/blob/1a85ebe91f148330799202c1920e948fa8131a32/src/bedrock_agentcore/memory/client.py#L349

で、何かしらのモデルベンダーのAPI・SDKと連携させて、AgentCore Memoryを短期メモリ・ストレージとして使う場合、このメッセージフォーマットを相互変換する必要が出てくる。例えば、OpenAIの場合だと、

  • ツール呼び出しメッセージ以外に、ツール実行結果を含めたメッセージがある。
  • ツール呼び出しメッセージとツール実行結果を含めたメッセージは共通のID(tool_call_id)を保つ必要がある。
  • ユーザメッセージにオプション的な name パラメータがある

など、どうしてもフォーマットの差異があるので、ここは開発者が吸収しなければいけないのだが、上記の通り create_event() で扱えるペイロードはconversationalというオブジェクトのリストで、このオブジェクトはrolecontent.textのタプルしか含めることができず、非常につらい。

AWS公式のドキュメントを見ると、実はこのイベントのペイロードタイプ、conversational 以外に blob というのがある

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/short-term-memory-operations.html

イベントペイロードタイプ

ペイロードパラメータはペイロード項目のリストを受け付け、単一イベント内に異なるタイプのデータを保存できます。一般的なペイロードタイプには以下が含まれます:

  • Conversational: 役割(例: 「ユーザー」、「アシスタント」)とコンテンツを含む会話メッセージを保存します。
  • Blob: 画像や文書などのバイナリ形式データ、またはエージェント固有のデータ(例: JSON形式で保存されたデータ)を保存する場合。

これを使えば、API・SDKのメッセージフォーマットの違いに関係なく、マルッとメッセージを登録できるはず。これはAgentCoreのAPIでは用意されている(つまりBoto3だと使える)のだが、AgentCore SDKではまだ実装されていなかった。

が、先日の v0.1.3 でこれに対応している。

https://github.com/aws/bedrock-agentcore-sdk-python/pull/61

create_blob_event() というメソッドが生えているのでこれを使えばいいみたい。

https://github.com/akshseh/bedrock-agentcore-sdk-python/blob/c69f51d0e4793c476409d5fc1b3e0549265ebc42/src/bedrock_agentcore/memory/client.py#L379-L436

ということでやってみる。

まず短期メモリ作成。長期メモリは一旦なしで。

from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")
memory = client.create_memory(name="blob_event_test")

print(f"メモリID: {memory.get('id')}")
出力
メモリID: blob_event_test-XXXXXXXXXX

適当なオブジェクトを入れてみる。ここではよくあるメッセージの辞書を渡してみた。

client.create_blob_event(
    memory_id=memory.get('id'),
    actor_id="user_123",
    session_id="session_123",
    blob_data={"role": "user", "content": "おっはー!。"}
)

ではこのイベントを読み出してみる。

conversations = acm_client.list_events(
    memory_id=memory.get('id'),
    actor_id="user_123",
    session_id="session_123",
)
for conversation in conversations:
    print(conversation)

見やすさのため手動で改行を入れている。

出力
{
    'memoryId': 'blob_event_test-XXXXXXXXXX',
    'actorId': 'user_123',
    'sessionId': 'session_123',
    'eventId': '0000001757603296000#3caf97ab',
    'eventTimestamp': datetime.datetime(2025, 9, 11, 15, 8, 16, tzinfo=tzlocal()),
    'payload': [
        {'blob': '{content=おっはー!。, role=user}'}
    ],
    'branch': {'name': 'main'}
}

blobの中身は {k=v} のフォーマットかつ文字列で返ってきている・・・んー、これは果たして期待される結果なのかな?

なお、Boto3のドキュメントを見ると、

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agentcore/client/create_event.html

Response Syntax

{
   'event': {
       'memoryId': 'string',
       'actorId': 'string',
       'sessionId': 'string',
       'eventId': 'string',
       'eventTimestamp': datetime(2015, 1, 1),
       'payload': [
           {
               'conversational': {
                   'content': {
                       'text': 'string'
                   },
                   'role': 'ASSISTANT'|'USER'|'TOOL'|'OTHER'
               },
               'blob': {...}|[...]|123|123.4|'string'|True|None
           },
       ],
       'branch': {
           'rootEventId': 'string',
           'name': 'string'
       }
   }
}

とあるし、aws-cliのcreate-eventのドキュメントを見ると、

https://docs.aws.amazon.com/cli/latest/reference/bedrock-agentcore/create-event.html

create-event uses document type values. Document types follow the JSON data model where valid values are: strings, numbers, booleans, null, arrays, and objects. For command input, options and nested parameters that are labeled with the type document must be provided as JSON. Shorthand syntax does not support document types.

とあって、list-events の戻りを見ても

https://docs.aws.amazon.com/cli/latest/reference/bedrock-agentcore/list-events.html

blob -> (document)

The binary content of the payload.

とあるので、多分これはAPIが返す型がおかしいのではないだろうか?

とりあえず、今の挙動を踏まえると、辞書を渡したい場合はJSON文字列としてシリアライズ、読み出すときにデシリアライズするのが良さそう。

メモリを作り直して再度トライ。

from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")
memory = client.create_memory(name="blob_event_test2")

print(f"メモリID: {memory.get('id')}")
client.create_blob_event(
    memory_id=memory.get('id'),
    actor_id="user_123",
    session_id="session_123",
    blob_data=json.dumps({"role": "user", "content": "おっはー!。"}, ensure_ascii=False)
)
conversations = acm_client.list_events(
    memory_id=memory.get('id'),
    actor_id="user_123",
    session_id="session_123",
)
for conversation in conversations:
    if conversation.get('payload'):
        for event in conversation.get('payload'):
            print(json.loads(event["blob"]))
出力
{'role': 'user', 'content': 'おっはー!。'}

これは期待する動作ではないような気がするのでとりあえずIssue上げた。

https://github.com/boto/boto3/issues/4614

2025/09/18追記

回答があって、Bedrock側でAPIとドキュメントの動きがあってないので、Bedrockサービスチームに確認してくれるみたい。

このスクラップは2ヶ月前にクローズされました