diff --git a/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/README.md b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/README.md new file mode 100644 index 00000000..d97566ee --- /dev/null +++ b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/README.md @@ -0,0 +1,39 @@ +# Human in the loop (HITL) with Strands Agents on AgentCore Runtime with Anthropic Claude Sonnet 4.5 + +## Overview + +This tutorial will go over how to host a Strands agent with tools that require human approval, using Amazon Bedrock AgentCore Runtime & Anthropic Claude Sonnet 4.5. + +### Tutorial Details + + +| Information | Details | +|:--------------------|:---------------------------------------------------------------------------------| +| Tutorial type | Conversational | +| Agent type | Single | +| Agentic Framework | Strands Agents | +| LLM model | Anthropic Claude Sonnet 4.5 | +| Tutorial components | Hosting agent on AgentCore Runtime, using a Strands Agent with HITL | +| Tutorial vertical | Cross-vertical | +| Example complexity | Medium | +| SDK used | Amazon BedrockAgentCore Python SDK and boto3 | + +### Tutorial Architecture + +In this tutorial we will describe how to create a Strands agent with tools protected by human approval. This will be deployed to. AgentCore runtime. + +For demonstration purposes, we will use a Strands Agent using Anthropic Claude Sonnet 4.5. + +In our example we will use a very simple agent with two tools: `send_email` and `get_weather`. + +Strand's inherent tool `handoff_to_user` will be used to intercept tool calls in the agent loop. + +
+ +
+ +### Tutorial Key Features + +* Hosting Strands Agents on Amazon Bedrock AgentCore Runtime. +* Using Anthropic Claude Sonnet 4.5. +* Using Strands Agents built in "handoff_to_user" functionality for human-in-the-loop. diff --git a/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/architecture_runtime.jpg b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/architecture_runtime.jpg new file mode 100644 index 00000000..0e05405c Binary files /dev/null and b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/architecture_runtime.jpg differ diff --git a/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/configure.png b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/configure.png new file mode 100644 index 00000000..5b8e004a Binary files /dev/null and b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/configure.png differ diff --git a/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/invoke.png b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/invoke.png new file mode 100644 index 00000000..4026a950 Binary files /dev/null and b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/invoke.png differ diff --git a/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/launch.png b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/launch.png new file mode 100644 index 00000000..db613e8b Binary files /dev/null and b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/images/launch.png differ diff --git a/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/notebook_hitl.ipynb b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/notebook_hitl.ipynb new file mode 100644 index 00000000..a0c87de8 --- /dev/null +++ b/01-tutorials/01-fundamentals/09-human-in-the-loop-agentcore/notebook_hitl.ipynb @@ -0,0 +1,492 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8024df16", + "metadata": {}, + "source": [ + "# Human in the loop (HITL) with Strands Agents on AgentCore Runtime with Anthropic Claude Sonnet 4.5\n", + "\n", + "## Overview\n", + "\n", + "This tutorial will go over how to host an agent with tools that require human approval, using Amazon Bedrock AgentCore Runtime & Anthropic Claude Sonnet 4.5.\n", + "\n", + "### Tutorial Details\n", + "\n", + "\n", + "| Information | Details |\n", + "|:--------------------|:---------------------------------------------------------------------------------|\n", + "| Tutorial type | Conversational |\n", + "| Agent type | Single |\n", + "| Agentic Framework | Strands Agents |\n", + "| LLM model | Anthropic Claude Sonnet 4.5 |\n", + "| Tutorial components | Hosting agent on AgentCore Runtime, using a Strands Agent with HITL |\n", + "| Tutorial vertical | Cross-vertical |\n", + "| Example complexity | Medium |\n", + "| SDK used | Amazon BedrockAgentCore Python SDK and boto3 |\n", + "\n", + "### Tutorial Architecture\n", + "\n", + "In this tutorial we will describe how to create an Agent with tools protected by human approval. This will be deployed to AgentCore runtime. \n", + "\n", + "For demonstration purposes, we will use a Strands Agent using Anthropic Claude Sonnet 4.5.\n", + "\n", + "In our example we will use a very simple agent with two tools: `send_email` and `get_weather`. \n", + "\n", + "Strand's inherent tool `handoff_to_user` will be used to intercept tool calls in the agent loop. \n", + "\n", + "
\n", + " \n", + "
\n", + "\n", + "### Tutorial Key Features\n", + "\n", + "* Hosting Strands Agents on Amazon Bedrock AgentCore Runtime\n", + "* Using Anthropic Claude Sonnet 4.5\n", + "* Using Strands Agents built in \"handoff_to_user\" functionality for human-in-the-loop\n" + ] + }, + { + "cell_type": "markdown", + "id": "1f2eccbf", + "metadata": {}, + "source": [ + "Install dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fc01b8a", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -U --quiet boto3 bedrock-agentcore-starter-toolkit bedrock-agentcore strands-agents strands-agents-tools" + ] + }, + { + "cell_type": "markdown", + "id": "8b2ecbae", + "metadata": {}, + "source": [ + "Create requirements file for AgentCore Runtime deployment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4942f5a0", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile requirements.txt\n", + "boto3\n", + "bedrock-agentcore-starter-toolkit\n", + "bedrock-agentcore\n", + "strands-agents\n", + "strands-agents-tools" + ] + }, + { + "cell_type": "markdown", + "id": "18d6cbb4", + "metadata": {}, + "source": [ + "## Preparing our HITL Agent for Deployment on AgentCore Runtime\n", + "\n", + "Let's now deploy our **HITL** Strands Agent to AgentCore Runtime. \n", + "This agent demonstrates how to **pause tool execution** and **hand control back to a human** directly from within the agent loop.\n", + "\n", + "---\n", + "\n", + "### Key points for HITL implementation:\n", + "\n", + "- Use an **approval-aware proxy tool** (e.g., `send_email`) that checks if the action is approved before running. \n", + "- Maintain a **protected tools list** (`PROTECTED_TOOLS`) to define which actions need explicit approval. \n", + "- Read **approval flags** from the runtime payload (`payload[\"approvals\"]`). \n", + "- If approval is missing or denied, the tool **returns a `handoff_to_user`** response to pause execution. \n", + "- Unprotected tools (like `get_weather`) execute automatically without intervention. \n", + "\n", + "---\n", + "\n", + "## Inside the Strands Agent: Approval-Aware Tools\n", + "\n", + "This HITL pattern is implemented **directly inside the tool itself**, without relying on external hooks or helper functions.\n", + "\n", + "The `send_email` tool performs both the **approval check** and the **execution logic**:\n", + "- When approval exists (`approvals[\"send_email\"] == True`), it immediately performs the email action and returns a success message. \n", + "- When approval is missing or denied, it **returns a `handoff_to_user` response** that pauses execution and asks for human approval. \n", + "- The agent then surfaces this message verbatim to the user and waits for confirmation before retrying.\n", + "\n", + "---\n", + "\n", + "## Understanding Human-in-the-Loop in AgentCore Runtime\n", + "\n", + "When using HITL patterns with Strands + AgentCore, the following behaviors occur automatically:\n", + "\n", + "### Approval Flow\n", + "- The runtime payload can include an `approvals` object specifying tool permissions. \n", + "- Each proxy tool checks these flags before proceeding with execution. \n", + "\n", + "### Handoff to User\n", + "- If approval is not granted, the tool calls `handoff_to_user(message=..., reason=...)`. \n", + "- This **pauses execution inside the agent loop**, signaling the need for human review. \n", + "- The user can then re-invoke the same prompt with `approvals[\"tool_name\"] = true` to proceed. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17cf746f", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile hitl_strands_agentcore.py\n", + "#Imports\n", + "from typing import Any, Dict, Iterable, Callable, Mapping, Optional\n", + "from strands import Agent, tool\n", + "from strands.models import BedrockModel\n", + "from strands.hooks import HookProvider, HookRegistry\n", + "from strands.experimental.hooks import BeforeToolInvocationEvent\n", + "from strands_tools import handoff_to_user\n", + "from bedrock_agentcore.runtime import BedrockAgentCoreApp\n", + "\n", + "# AgentCore Runtime app\n", + "app = BedrockAgentCoreApp()\n", + "\n", + "# Choose your Bedrock model\n", + "MODEL_ID = \"us.anthropic.claude-sonnet-4-5-20250929-v1:0\"\n", + "\n", + "# Protected tool list. Any tool listed here requires explicit approval\n", + "PROTECTED_TOOLS = {\"send_email\"}\n", + "\n", + "# Keep the latest payload accessible to tools/hooks\n", + "last_payload: Dict[str, Any] = {}\n", + "\n", + "# Track payload for Agent loop (so it can decide to handoff to user or not)\n", + "def payload_get() -> Dict[str, Any]:\n", + " \"\"\"Return the most recent invocation payload.\"\"\"\n", + " return last_payload or {}\n", + "\n", + "@tool(description=\"Send a transactional email. Requires human approval before execution.\")\n", + "def send_email(to: str, subject: str, body: str) -> dict:\n", + " \"\"\"\n", + " This tool checks whether sending an email is approved.\n", + " If approved, it executes the action.\n", + " Otherwise, it returns a handoff_to_user event inside the agent loop.\n", + " \"\"\"\n", + " approvals = (payload_get().get(\"approvals\") or {})\n", + "\n", + " # Approved -> perform the real action\n", + " if approvals.get(\"send_email\") is True:\n", + " return {\"content\": [{\"text\": f\"Email successfully sent to {to!r} with subject {subject!r}\"}]}\n", + "\n", + " # Not approved -> hand back to user\n", + " reason = \"approval_required\" if \"send_email\" not in approvals else \"approval_denied\"\n", + " msg = (\n", + " \"Execution of 'send_email' requires human approval. \"\n", + " \"Re-invoke with approvals['send_email']=true to proceed.\"\n", + " if reason == \"approval_required\"\n", + " else \"The request to execute 'send_email' was denied by the user. No action was taken.\"\n", + " )\n", + " return handoff_to_user(message=msg, reason=reason)\n", + "\n", + "@tool(description=\"Return weather.\")\n", + "def get_weather() -> str:\n", + " # Demo only\n", + " return \"sunny\"\n", + "\n", + "# Create Strands Agent\n", + "model = BedrockModel(model_id=MODEL_ID)\n", + "agent = Agent(\n", + " model=model,\n", + " tools=[send_email, get_weather, handoff_to_user], # handoff_to_user (built into Strands) must be added to the tools list \n", + " system_prompt=( #Prompt engineering is an art & a science, ensure this is optimized for your usecase. This one is optimized for this notebook's purposes\n", + " \"You are a helpful assistant.\\n\"\n", + " \"Use tools to perform actions (sending email & checking weather).\\n\"\n", + " \"When a user asks to send an email, you MUST call the send_email tool.\\n\"\n", + " \"Do NOT claim an action occurred unless a tool_result confirms it.\\n\"\n", + " \"If 'handoff_to_user' is used, OUTPUT THE HANDOFF MESSAGE VERBATIM and STOP. \"\n", + " \"Do not apologize or call it a technical error.\\n\"\n", + " \"For protected actions without explicit approval, call 'handoff_to_user'.\"\n", + " )\n", + ")\n", + "\n", + "# Entrypoint\n", + "@app.entrypoint\n", + "def strands_agent_hitl(payload):\n", + " \"\"\"\n", + " Payload example:\n", + " {\n", + " \"prompt\": \"send an email to dev@example.com about launch features\",\n", + " \"approvals\": {\"send_email\": true} # optional per-tool approval flags\n", + " }\n", + " \"\"\"\n", + " global last_payload\n", + " last_payload = payload or {}\n", + "\n", + " user_input = (payload or {}).get(\"prompt\", \"\")\n", + " if not user_input:\n", + " return \"Please provide a 'prompt'.\"\n", + "\n", + " response = agent(user_input)\n", + "\n", + " # Extract text\n", + " try:\n", + " return response.message[\"content\"][0][\"text\"]\n", + " except Exception:\n", + " return str(getattr(response, \"text\", response))\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " app.run()\n" + ] + }, + { + "cell_type": "markdown", + "id": "254a19c7", + "metadata": {}, + "source": [ + "### Configure AgentCore Runtime deployment\n", + "\n", + "First we will use our starter toolkit to configure the AgentCore Runtime deployment with an entrypoint, the execution role we just created and a requirements file. We will also configure the starter kit to auto create the Amazon ECR repository on launch.\n", + "\n", + "During the configure step, your docker file will be generated based on your application code\n", + "\n", + "
\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56b4f42b", + "metadata": {}, + "outputs": [], + "source": [ + "from bedrock_agentcore_starter_toolkit import Runtime\n", + "from boto3.session import Session\n", + "\n", + "boto_sess = Session()\n", + "region = boto_sess.region_name\n", + "\n", + "agentcore_runtime = Runtime()\n", + "agent_name = \"hitl_strands_bedrock_demo\" # <-- Change as needed\n", + "\n", + "response = agentcore_runtime.configure(\n", + " entrypoint=\"hitl_strands_agentcore.py\", # <-- The file created above\n", + " auto_create_execution_role=True,\n", + " auto_create_ecr=True,\n", + " requirements_file=\"requirements.txt\", # <-- The requirements file created above\n", + " region=region,\n", + " agent_name=agent_name\n", + ")\n", + "response" + ] + }, + { + "cell_type": "markdown", + "id": "fe329639", + "metadata": {}, + "source": [ + "### Launching agent to AgentCore Runtime\n", + "\n", + "Now that we've got a docker file, let's launch the agent to the AgentCore Runtime. This will create the Amazon ECR repository and the AgentCore Runtime\n", + "\n", + "
\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41756d14", + "metadata": {}, + "outputs": [], + "source": [ + "#Launch the Runtime\n", + "launch_result = agentcore_runtime.launch()\n", + "launch_result" + ] + }, + { + "cell_type": "markdown", + "id": "07523e72", + "metadata": {}, + "source": [ + "### Checking for the AgentCore Runtime Status\n", + "Now that we've deployed the AgentCore Runtime, let's check for it's deployment status" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8260994", + "metadata": {}, + "outputs": [], + "source": [ + "#Wait for the Runtime status as READY\n", + "import time\n", + "\n", + "status_response = agentcore_runtime.status()\n", + "status = status_response.endpoint[\"status\"]\n", + "end_status = [\"READY\", \"CREATE_FAILED\", \"DELETE_FAILED\", \"UPDATE_FAILED\"]\n", + "\n", + "while status not in end_status:\n", + " time.sleep(10)\n", + " status_response = agentcore_runtime.status()\n", + " status = status_response.endpoint[\"status\"]\n", + " print(status)\n", + "\n", + "status" + ] + }, + { + "cell_type": "markdown", + "id": "5a618bf9", + "metadata": {}, + "source": [ + "### Invoking AgentCore Runtime\n", + "\n", + "Finally, we can invoke our AgentCore Runtime with a payload\n", + "\n", + "
\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16cbe061", + "metadata": {}, + "outputs": [], + "source": [ + "#Function to parse response text from AgentCore\n", + "#If you want to see the output metadata as well, just print \"invoke_response\" in the invocation cells\n", + "from IPython.display import Markdown, display\n", + "import json\n", + "def parse_response(invoke_response):\n", + " response_text = invoke_response['response'][0]\n", + " return display(Markdown(response_text))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8911c88", + "metadata": {}, + "outputs": [], + "source": [ + "#Invoke with no approval a False flag is optional, the Agent will handoff to user as long as the payload does not have \"True\"\n", + "invoke_response = agentcore_runtime.invoke({\n", + " \"prompt\": \"Can you send an email for me. I want to send it to dev@example.com asking about what features will be available at launch, draft it end to end before sending\",\n", + " #\"approvals\": {\"send_email\": False}\n", + "})\n", + "parse_response(invoke_response)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1cea7c4f", + "metadata": {}, + "outputs": [], + "source": [ + "#Invoke with approval \n", + "invoke_response = agentcore_runtime.invoke({\n", + " \"prompt\": \"The email is approved\",\n", + " \"approvals\": {\"send_email\": True}\n", + "})\n", + "parse_response(invoke_response)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b3918f7", + "metadata": {}, + "outputs": [], + "source": [ + "#Invoke unprotected tool no approvals needed\n", + "invoke_response = agentcore_runtime.invoke({\n", + " \"prompt\": \"What is the weather\"\n", + "})\n", + "parse_response(invoke_response)" + ] + }, + { + "cell_type": "markdown", + "id": "1f876162", + "metadata": {}, + "source": [ + "## Cleanup (Optional)\n", + "\n", + "Let's now clean up the AgentCore Runtime created" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fb0c4fe", + "metadata": {}, + "outputs": [], + "source": [ + "#Cleanup (Optional) -> Uncomment the rest of this cell to cleanup the resources created\n", + "# import boto3\n", + "\n", + "# agent_id = launch_result.agent_id\n", + "# ecr_uri = launch_result.ecr_uri\n", + "# repo_name = ecr_uri.split(\"/\")[1]\n", + "\n", + "# control = boto3.client(\"bedrock-agentcore-control\", region_name=region)\n", + "# ecr = boto3.client(\"ecr\", region_name=region)\n", + "\n", + "# tmp = control.delete_agent_runtime(agentRuntimeId=agent_id)\n", + "# tmp = ecr.delete_repository(repositoryName=repo_name, force=True)" + ] + }, + { + "cell_type": "markdown", + "id": "14ffec8a", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "In this notebook, we built an end-to-end **Human-in-the-Loop** agent using **Strands + Anthropic Claude Sonnet 4.5 + AgentCore Runtime**, demonstrating how AI-driven actions can remain transparent, safe, and human-approved.\n", + "\n", + "### Some Takeaways\n", + "\n", + "- **Embedded control logic:** By placing approval checks directly inside the tool, the agent ensures human consent is required for sensitive operations without needing external hooks. \n", + "- **Seamless user experience:** The use of `handoff_to_user` from Strands allows the model to pause gracefully.\n", + "- **Flexible runtime design:** AgentCore Runtime automatically manages session context and payloads, making it easy to pass approval metadata between invocations. \n", + "- **Scalable pattern:** This same structure can be extended to other sensitive tools (e.g., `delete_user`, `approve_invoice`, etc.) by following the same approval-aware proxy pattern. \n", + "\n", + "This foundation can be expanded into larger multi-agent systems, approval workflows, or enterprise-grade GenAI governance frameworks — ensuring **responsible autonomy** while maintaining **human oversight** at every step.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/01-tutorials/01-fundamentals/README.md b/01-tutorials/01-fundamentals/README.md index ce6b0d29..c807f650 100644 --- a/01-tutorials/01-fundamentals/README.md +++ b/01-tutorials/01-fundamentals/README.md @@ -16,3 +16,4 @@ This folder contains a series of tutorials covering the fundamental concepts of | F6 | [Integrating Bedrock Guardrail](./06-guardrail-integration) | Integrate an Amazon Bedrock Guardrail to your agent | | F7 | [Adding memory to your agent](./07-memory-persistent-agents) | Personal assistant using memory and websearch tools | | F8 | [Observability and Evaluation](./08-observability-and-evaluation) | Adding observability and evaluation to your agent | +| F9 | [Human in the loop](./09-human-in-the-loop-agentcore) | Human in the loop for protected tools with Agentcore | \ No newline at end of file