0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Strands AgentとBedrock AgentCoreに入門。マイカーの故障で彼女がメンブレしていたので、「お悩み受け止めエージェント」を作って全部受け止める。

Posted at

はじめに

最近Jr.ChampionsのMeetUpでStrands×AgentCoreのワークショップがありました。

お恥ずかしながらこれまでここら辺まったく触ってこず、
さすがにやばいと思って少し座学でAgentCoreの機能だけ簡単に抑えていったものの爆死。
(プログラミングがそもそも出来なさすぎるのは一旦置いておいたとしても)

流石にヤバイなって思って勉強を始めました



というタイミングで、彼女の車がバッテリー上がりで動かなくなってJAFを呼ぶことになるという事件が発生。
彼女はひどく落ち込んでいます。

丁度良い?タイミングなので、
勉強がてらStrands×AgentCoreでお悩み受け止めエージェントを作って全部受け止めることにしました

構成

ざっくり以下のような構成で作成しました。
バックエンドをAgentCore Runtimeにデプロイ、フロントエンドはStremalitで実装しました。
また、MCPとしてLambda関数を利用しました。

image.png

工夫したい点は以下です。

「前話したこと覚えてくれてうれしい!」

これまでに相談した際の悩みや、対話の傾向は抑えておかなければなりません。
AgentCore Memoryを使って、STM、LTMを実装します。
STMではセッション内の会話を記憶して会話に反映できるように。
LTMでは、長期記憶戦略のうち、「Semantic」と「Preference」を設定して、ユーザの名前・職業・過去の悩みを踏まえて対話に反映出来るようにしました。

アラート通知機能で必要な時は察しの良い男に。

彼女が深刻な悩みを抱えていたら、暗に察して「どしたん?」ってしたいですよね。
彼女に内緒で、入力された悩みにバックエンドで重大度判定を行い、
重大と判定された場合はAmazon SNS経由でメール通知を飛ばせるようにしました。
これで、メール来て即察しの良い良い男の道化を演じることが出来ます。

SNSへの送信機能はLambdaで実装。
Lambda関数をAgentCore Gatewayに登録し、
今後の機能拡張も踏まえて単一のGatewayエンドポイント経由で呼び出せるようにしました。

Cognitoと統合して、自分の悩みが他の人にバレないように。

フロントエンドの認証機能でCognitoを利用(InboundAuth)。
ユーザとセッションごとにエージェントとの会話履歴を保持できるようにしました。
これによって、私がエージェントに「生きるのがつらい、、、」と相談しているところに彼女が対話を始めて、「私生きるのがつらいなんて言ってないのに、、、、😡」とならないようにしました。

バックエンド(Runtime→Lambda)の認証(OutboundAuth)はIAMロールで実施し、Lambdaの実行権限を付与しています。

悩みは「受け止める」だけ。

誰かに悩みを打ち明けるとき、具体的な解決策は求められないことが往々にしてあります。
なので、このエージェントは悩みを受け止めることだけに特化させていて、具体的な解決策は何も提示しません。

前提

  • 今回はStrandsとAgentCoreの凄いところを一通り体感したい!が目的(お悩みを受け止めるのは副次的な目的)なので、プログラミング経験ほぼゼロの私は、Kiroにコーディングをしてもらいました。
    (最低限動かすことが目的なので、プログラムの質は担保されていません。)
    といいつつ、プログラミングが全く出来ないのでここは触りながら学んでいきたい所存です、、、、
  • Strandsの実装部分やAWS基盤の構築は自身で実施しています。

実装

AWSサービス基盤設定

AgentCore

以下記事(本記事末尾参照)を参考に、AgentCore Runtimeを実装していきます。
(今回の構築の前に一つ一つAgentCoreの各機能に触れて今回使う機能の設定項目は抑えていたので、初期構築ではStarter Tool Kitを使いました。)

Starter Tool Kitをデフォルト設定で作成後、以下を追加設定しました。

①AgentCore Memoryの長期記憶戦略
今回は長期記憶戦略として、以下を設定します。

  • セマンティック:ユーザ入力に基づく事実情報
  • プリファレンス(ユーザ設定):ユーザの傾向・好み

Memory自体は作成済みなので、以下通りMemoryの編集画面から有効化してあげるだけです。
image.png

②AgentCore Gatewayの作成
今回はRuntimeからLambda関数を呼び出すので、Gatewayを作成してターゲットでLambda ARNを登録します。
image.png

スキーマ・認証方式を定義します。
このスキーマで定義したnameとターゲット名を、Strands側で{ターゲット名}___{nameの値}という形で呼び出すので注意です。(私はここでハマりました)
今回はRuntimeに付与したIAMロールでバックエンドの実行に対する認証を行うので、認証方式はIAMロールを選択します。
image.png

その他

  • SNSにて、自身のメアドでサブスクリプションを作成して、関連付けたSNSトピックを作ります。
  • 作成したSNSトピック宛てにメールを送信するLambda関数を作成します。
    (本筋から逸れるので細かい処理内容は割愛)
  • Cognitoユーザプールを作成し、それに紐づける形でPython用のアプリケーションクライアントを作成します。ユーザプールID及びクライアントID・クライアントシークレットを控えておきます。(上記AgentCore Runtimeとフロントエンドの設定で使う)

バックエンド

以下、今回作成したRuntimeのポイントとなる箇所を記載します。

今回使うLambda関数を登録したGatewayを呼び出すToolを定義します。
ユーザ入力(お悩み内容・ユーザIDとセッションIDをGateway経由でLambdaに渡します。

@tool
def send_critical_notification(worry_content: str, user_id: str = None, session_id: str = None) -> dict:
    uid = user_id or current_actor_id.get()
    sid = session_id or current_session_id.get()
    return mcp_tools_call(GATEWAY_TOOL_NAME, {"worry_content": worry_content, "user_id": uid, "session_id": sid})

エージェントを定義します。
歌舞伎町ホストになってもらい、乱暴にだけど優しく悩みを受け止めてもらいます。
重大な悩みの時は、Toolに登録したLambda関数を使ってメール通知を行います。

agent = Agent(
    tools=[send_critical_notification],
    system_prompt=(
        "あなたは温かく優しい歌舞伎町のホスト。\n"
        "傾聴と肯定が最優先。\n"
        "ユーザが重要度を指定していない場合は、文脈から重要度を判断し、"
        "重大(自殺願望・自傷他害の恐れ等)と判断したら send_critical_notification を必ず使って通知して。\n"
        "口調は少しオラオラ、丁寧語は避ける。"
    ),
)

長期記憶の呼び出し戦略を定義します。
今回はセマンティック(事実情報)とプリファレンス(ユーザの好みや傾向)を抽出します。
プリファレンスに関してはユーザの口調の好みを、セマンティックについてはユーザの名前や職業、あとは悩みについて抽出します。

def pref_namespace(actor_id: str) -> str:
    return f"/strategies/{PREFERENCE_STRATEGY_ID}/actors/{actor_id}"


def retrieve_preferences(actor_id: str) -> dict:
    client = boto3.client("bedrock-agentcore", region_name=REGION)
    return client.retrieve_memory_records(
        memoryId=MEMORY_ID,
        namespace=pref_namespace(actor_id),
        searchCriteria={"searchQuery": "このユーザーの好み(話し方、口調、トーン、敬語/砕けた等)"},
        maxResults=5,
    )

def semantic_namespace(actor_id: str) -> str:
    return f"/strategies/{SEMANTIC_STRATEGY_ID}/actors/{actor_id}"


def retrieve_semantic(actor_id: str) -> dict:
    client = boto3.client("bedrock-agentcore", region_name=REGION)
    return client.retrieve_memory_records(
        memoryId=MEMORY_ID,
        namespace=semantic_namespace(actor_id),
        searchCriteria={"searchQuery": "このユーザの名前、職業、年齢、ユーザの悩み)"},
        maxResults=5,
    )

エントリーポイントを定義します。
エントリーポイントの中でMemoryのセッションマネージャーを作成します。
前の関数定義を用いて長期記憶についても抽出し、プロンプトに反映します。

@app.entrypoint
def handler(event: dict):
    user_prompt = event["prompt"]
    actor_id = event["user_id"]
    session_id = event["session_id"]

    token_actor = current_actor_id.set(actor_id)
    token_sess = current_session_id.set(session_id)

    try:
        worry_content, explicit_sev = parse_explicit_severity(user_prompt)

        # STM(このセッション)
        session = MemorySessionManager(memory_id=MEMORY_ID, region_name=REGION).create_memory_session(
            actor_id=actor_id, session_id=session_id
        )
        history = "\n".join(map(str, session.get_last_k_turns(k=6)))

        # LTM
        pref_obj = retrieve_preferences(actor_id)
        semantic_obj = retrieve_semantic(actor_id)

        sev_block = (
            f"【ユーザ指定の重要度】\n{explicit_sev}\n※必ず従って。\n\n"
            if explicit_sev
            else "【重要度】\nユーザ指定なし。君が判断して、重大なら通知して。\n\n"
        )

        ltm_block = json.dumps(
            {
                "preference": pref_obj,
                "semantic": semantic_obj
            },
            ensure_ascii=False,
            indent=2,
            default= str
        )

        prompt = (
            "【長期記憶 raw(Preference)】\n"
            f"{ltm_block}\n\n"
            "【抽出指示】\n"
            "長期記憶rawからユーザの好み、年齢、職業、名前、過去の悩みを取得し、会話に自然に反映して。\n"
            "※抽出内容をユーザにそのまま貼らない。\n\n"
            + (f"【短期記憶】\n{history}\n\n" if history else "")
            + f"【今回の入力】\n{worry_content}\n\n{sev_block}"
        )

        assistant_text = str(agent(prompt))

        session.add_turns(
            messages=[
                ConversationalMessage(user_prompt, MessageRole.USER),
                ConversationalMessage(assistant_text, MessageRole.ASSISTANT),
            ]
        )

        return {"output": assistant_text, "user_id": actor_id, "session_id": session_id}

    finally:
        current_actor_id.reset(token_actor)
        current_session_id.reset(token_sess)

ユーザやセッション情報はプロンプトと一緒にフロントエンドからRuntime Invoke時に引数として渡します。

      payload = {"prompt": prompt, "user_id": user_id, "session_id": session_id}
    r = requests.post(url, headers=headers, data=json.dumps(payload), timeout=60)

フロントエンド

Cognitoとの連携ポイントはだいぶ詰まりました。
長くなるので、別の記事にまとめようと思います!良ければ参照してみてください。
【執筆後URL記載予定】

動作確認

Cognitoの認証画面にてユーザ・PWを入力後、
以下のようにエージェントとのチャット画面に遷移します。

image.png

こんな感じで自分の悩みのレベル感とお悩みを入力して、送信。
image.png

エージェントが受け止めてくれます。
(歌舞伎町ホストの設定にしたからか、怪しげな手続きをしている、、、)
image.png

今回は深刻度を重大に設定したので、やばいと感じたエージェントがアラートメールを送ってくれました。想定通りです。
image.png

名前と職業を伝えた後に会話(セッション)をリセットしても名前・職業を憶えてくれています!
(プロンプトがあからさますぎたのか、客の前で「この客だったよな、、」と確認するようにプロフィールを詠唱する最悪のホストになっています。

image.png

セッション内で直前にした話(お腹が空いた)も覚えてくれてます!
image.png

効果

さあ、動作確認が出来たのでいよいよお披露目。
「なんでも悩み相談出来るエージェント作ったから、辛いときはこれ使いや。」



「Chat GPTで充足している」
「私の悩みに重大度を付けんな」
「オラオラ口調いやだ」
「エージェントに任せないでお前が聞け」

とのことで、継続利用には繋がりませんでした。
ってことでエージェントからの通知に頼らずに自力で察しの良い男になるしかありません。

まとめ

今回はStrandsとAgentCoreに入門したってことで、お悩み相談エージェントを作ってみました。
まだまだ理解しきれていない&活用しきれていない機能があるので、もっと勉強して活用出来るよう励みます!

おわり

参考にした記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?