diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/README.md b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/README.md new file mode 100644 index 000000000..a9c38cbc9 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/README.md @@ -0,0 +1,37 @@ +# Streaming Responses with Strands Agents and Amazon Bedrock models in Amazon Bedrock AgentCore Runtime + +## Overview + +In this tutorial we will learn how to implement streaming responses using Amazon Bedrock AgentCore Runtime with your existing agents. + +We will focus on a Strands Agents with Amazon Bedrock model example that demonstrates real-time streaming capabilities. + +### Tutorial Details + +|Information| Details| +|:--------------------|:---------------------------------------------------------------------------------| +| Tutorial type | Conversational with Streaming| +| Agent type | Single | +| Agentic Framework | Strands Agents | +| LLM model | Anthropic Claude Sonnet 4 | +| Tutorial components | Custom Strands Agent Container with Streaming responses and Amazon Bedrock Model | +| Tutorial vertical | Cross-vertical | +| Example complexity | Intermediate | +| SDK used | Amazon BedrockAgentCore Python SDK and boto3| + +### Tutorial Architecture + +In this tutorial we will describe how to deploy a streaming agent to AgentCore runtime. + +For demonstration purposes, we will use a custom Strands Agent container using Amazon Bedrock models with streaming capabilities. + +In our example we will use a simple agent with two tools in separate python modules: `get_weather` and `get_time`, but with streaming response capabilities. + + +### Tutorial Key Features + +* Custom Agent API Container +* Streaming responses from agents on Amazon Bedrock AgentCore Runtime +* Real-time partial result deliver +* Using Amazon Bedrock models with streaming +* Using Strands Agents and FastAPI with async streaming support \ No newline at end of file diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/byo_container_streaming_agent.ipynb b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/byo_container_streaming_agent.ipynb new file mode 100644 index 000000000..947ff06c0 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/byo_container_streaming_agent.ipynb @@ -0,0 +1,680 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "52dc9b17-1182-44b3-bebf-ae2f508675d3", + "metadata": {}, + "source": [ + "# Bring Your Container with Streaming Reponse in Amazon Bedrock AgentCore Runtime\n", + "\n", + "## Overview\n", + "\n", + "In this tutorial we will learn how to host your own custom agent with Streaming response in Amazon Bedrock AgentCore Runtime. This example demonstrates how to build a FastAPI using StrandsAgents adequate to Amazon Bedrock AgentCore Runtime standards and make a requests with correct processing of the streaming response. \n", + "\n", + "### Tutorial Details\n", + "\n", + "|Information| Details|\n", + "|:--------------------|:---------------------------------------------------------------------------------|\n", + "| Tutorial type | Conversational with Streaming|\n", + "| Agent type | Single |\n", + "| Agentic Framework | Strands Agents |\n", + "| LLM model | Anthropic Claude Sonnet 4 |\n", + "| Tutorial components | Custom Strands Agent Container with Streaming responses and Amazon Bedrock Model |\n", + "| Tutorial vertical | Cross-vertical |\n", + "| Example complexity | Intermediate |\n", + "| SDK used | Amazon BedrockAgentCore Python SDK and boto3|\n", + "\n", + "### Tutorial Architecture\n", + "\n", + "In this tutorial we will describe how to build and deploy your own agent container into Amazon Bedrock AgentCore Runtime with streaming response.\n", + "\n", + "For demonstration purposes, we will use a Strands Agent using Amazon Bedrock models with streaming capabilities.\n", + "\n", + "In our example we will use a simple agent with two tools in separate python modules: `get_weather` and `get_time`, but with streaming response capabilities.\n", + "\n", + "### Tutorial Key Features\n", + "\n", + "* Custom Agent API Container\n", + "* Streaming responses from agents on Amazon Bedrock AgentCore Runtime\n", + "* Real-time partial result deliver\n", + "* Using Amazon Bedrock models with streaming\n", + "* Using Strands Agents and FastAPI with async streaming support" + ] + }, + { + "cell_type": "markdown", + "id": "3a676f58ecf52b42", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "To execute this tutorial you will need:\n", + "* Python 3.11+\n", + "* AWS credentials\n", + "* Amazon Bedrock AgentCore SDK\n", + "* Strands Agents\n", + "* Docker or podman running" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da79d12f-8595-457b-b5f7-8a51a65ba217", + "metadata": {}, + "outputs": [], + "source": [ + "!uv init" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "jupyter": { + "is_executing": true + } + }, + "outputs": [], + "source": [ + "!uv add -r requirements.txt --active" + ] + }, + { + "cell_type": "markdown", + "id": "932110e6-fca6-47b6-b7c5-c4714a866a80", + "metadata": {}, + "source": [ + "## Preparing your streaming agent custom container\n", + "\n", + "To get closer to a real case scenario, we will build an agent that has it's core functionality distributed in different modules. \n", + "\n", + "For this tutorial we will build a chess evaluator agent. This agent will be capable to answer general questions about chess and, given a chess position with a FEN string (FEN - Forsyth-Edwards Notation - is a standard notation for describing chess positions), it will be able to evaluate the position using chess-api.com API or search Lichess users and Masters database for statistics on the position.\n", + "\n", + "We will have the agent in the `api.py` file, important configurations in the `config.py` file and the available tools in the `tools.py` file.\n", + "\n", + "Let's start with the `api.py` file. Here, we will build a simple FastAPI engine that will run inside the container. Notice that:\n", + "- We are using `async def` for your entrypoint function\n", + "- We are using `yield` to stream chunks as they become available\n", + "- Clients will receive Content-Type: text/event-stream responses\n", + "- The endpoints follow Amazon Bedrock AgentCore's endpoint standards: a post endpoint `/invocations` and a get endpoint `/ping` for healthchecks.\n", + "- For streaming, the `return` statement for the POST endpoint should return a `StreamingResponse` FastAPI class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c70e4179", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile api.py\n", + "from fastapi import FastAPI, HTTPException\n", + "from fastapi.responses import StreamingResponse\n", + "from fastapi.middleware.cors import CORSMiddleware\n", + "from pydantic import BaseModel\n", + "from typing import Dict, Any\n", + "from strands import Agent\n", + "from strands.models import BedrockModel\n", + "\n", + "from config import MODEL_CONFIG, AWS_REGION, MODEL_PARAMS\n", + "\n", + "from tools import (\n", + " get_position_evaluation,\n", + " get_masters_games,\n", + " get_lichess_games\n", + ")\n", + "\n", + "\n", + "modelinstance = BedrockModel(model_id=MODEL_CONFIG[\"chess-agent\"], region_name=AWS_REGION, **MODEL_PARAMS)\n", + "\n", + "SYSTEM_PROMPT = \"\"\"You are a friendly Chess AI Agent. You main goal is to help users improve at chess and have a better\n", + "understanding of the game.\n", + "\n", + "To do that, you will analyze given positions, make comments on a position and answer general chess related questions from the user.\n", + "\n", + "You have access to these tools:\n", + "\n", + "1. get_position_evaluation: send a FEN string to get analysis information on that given position.\n", + "2. get_masters_games: get information on masters games with the same given FEN string position, get overall data, what is the opening, possible next moves and top games (from lichess database)\n", + "3. get_lichess_games: get information on lichess users games with the same given FEN string position, get overall data, what is the opening, possible next moves and top games (from lichess database)\n", + "\n", + "Don't give long answers. Engage in a chatty style conversation.\n", + "\"\"\"\n", + "\n", + "agent = Agent(\n", + " model=modelinstance,\n", + " system_prompt=SYSTEM_PROMPT,\n", + " tools=[\n", + " get_position_evaluation,\n", + " get_masters_games,\n", + " get_lichess_games\n", + " ],\n", + " callback_handler=None,\n", + ")\n", + "\n", + "app = FastAPI(title=\"Chess Evaluator Agent\", version=\"1.0.0\")\n", + "\n", + "class InvocationRequest(BaseModel):\n", + " input: Dict[str, Any]\n", + "\n", + "\n", + "@app.post(\"/invocations\")\n", + "async def stream_response(request: InvocationRequest):\n", + " async def generate(agent=agent):\n", + " try:\n", + " async for event in agent.stream_async(request.input.get(\"prompt\", \"\")):\n", + " if \"data\" in event:\n", + " # Only stream text chunks to the client\n", + " yield event[\"data\"]\n", + " except Exception as e:\n", + " yield f\"Error: {str(e)}\"\n", + "\n", + " return StreamingResponse(\n", + " generate(),\n", + " media_type=\"text/event-stream\"\n", + " )\n", + "\n", + "\n", + "@app.get(\"/ping\")\n", + "async def ping():\n", + " return {\"status\": \"healthy\"}\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " import uvicorn\n", + " uvicorn.run(app, host=\"0.0.0.0\", port=8080)" + ] + }, + { + "cell_type": "markdown", + "id": "e490f439", + "metadata": {}, + "source": [ + "Next, in the `tools.py` file, we will set up functions to call the external APIs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afc051d9", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile tools.py\n", + "import json\n", + "from strands import Agent, tool\n", + "import requests\n", + "\n", + "@tool\n", + "def get_position_evaluation(fen: str) -> dict:\n", + " response = requests.post(\n", + " \"https://chess-api.com/v1\", \n", + " json={\n", + " \"fen\": fen,\n", + " \"depth\": 18\n", + " }\n", + " )\n", + " return json.loads(response.text)\n", + "\n", + "\n", + "@tool\n", + "def get_masters_games(fen: str) -> dict:\n", + " response = requests.get(\n", + " \"https://explorer.lichess.ovh/masters\",\n", + " params = {\"fen\": fen}\n", + " )\n", + " return json.loads(response.text)\n", + "\n", + "\n", + "@tool\n", + "def get_lichess_games(fen: str) -> dict:\n", + " response = requests.get(\n", + " \"https://explorer.lichess.ovh/lichess\",\n", + " params = {\"fen\": fen}\n", + " )\n", + " return json.loads(response.text)" + ] + }, + { + "cell_type": "markdown", + "id": "739f92f2", + "metadata": {}, + "source": [ + "Next, let's put together our `config.py` file with some configurations (simplified for educational purposes.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb2dcd04", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile config.py\n", + "\"\"\"\n", + "Configuration module for the GenAI Strategy Agent system.\n", + "Contains settings for AI models and knowledge base IDs.\n", + "\"\"\"\n", + "\n", + "# Default model for all agents\n", + "DEFAULT_MODEL = \"us.anthropic.claude-sonnet-4-20250514-v1:0\"\n", + "\n", + "# Model configuration for specific agents\n", + "# Override DEFAULT_MODEL for specific agents if needed\n", + "MODEL_CONFIG = {\n", + " \"chess-agent\": DEFAULT_MODEL\n", + "}\n", + "\n", + "# AWS region for Bedrock models\n", + "AWS_REGION = \"us-east-1\"\n", + "\n", + "# Model parameters\n", + "MODEL_PARAMS = {\n", + " \"temperature\": 0.7,\n", + " \"max_tokens\": 4096\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "bc8bd389", + "metadata": {}, + "source": [ + "Now, we will build the container for Amazon Bedrock AgentCore Runtime. For the container, notice that:\n", + "- the base image should be built using linux/arm64 architecture to be compatible with AgentCore Runtime (better performance, more efficient resources utilization)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c430e71e", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile Dockerfile\n", + "FROM --platform=linux/arm64 ghcr.io/astral-sh/uv:python3.11-bookworm-slim\n", + "\n", + "WORKDIR /app\n", + "\n", + "# Copy uv files\n", + "COPY pyproject.toml uv.lock ./\n", + "\n", + "# Install dependencies\n", + "RUN uv sync --frozen --no-cache\n", + "\n", + "# Copy agent file\n", + "COPY api.py config.py tools.py ./\n", + "\n", + "# Expose port\n", + "EXPOSE 8080\n", + "\n", + "CMD [\"uv\", \"run\", \"uvicorn\", \"api:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8080\"]" + ] + }, + { + "cell_type": "markdown", + "id": "4f58eca5", + "metadata": {}, + "source": [ + "Next, we will build the container image and upload it to an ECR repository." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d90e747", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile build.sh\n", + "#!/bin/bash\n", + "set -e\n", + "\n", + "# If the ECR repository is not created, create it\n", + "aws ecr describe-repositories --repository-names \"chess-agent/runtime/api\" --region us-east-1 &>/dev/null || (echo \"Creating ECR repository...\" && aws ecr create-repository --repository-name \"chess-agent/runtime/api\" --region us-east-1)\n", + "\n", + "# AWS Account Number - CHANGE TO YOUR ACTUAL ACCOUNT NUMBER\n", + "ACCOUNT_NUMBER=\"123456789101\"\n", + "# ECR repository\n", + "ECR_REPO=$ACCOUNT_NUMBER\".dkr.ecr.us-east-1.amazonaws.com/chess-agent/runtime/api\"\n", + "# VERSION\n", + "VERSION=\"v1\"\n", + "\n", + "# Login to ECR\n", + "aws ecr get-login-password --region us-east-1 | podman login --username AWS --password-stdin $ACCOUNT_NUMBER.dkr.ecr.us-east-1.amazonaws.com\n", + "\n", + "# Build the Docker image for Intel platform\n", + "podman build --platform=linux/arm64 -t chess-agent-runtime-api:$VERSION .\n", + "\n", + "# Tag the image\n", + "podman tag chess-agent-runtime-api:$VERSION $ECR_REPO:$VERSION\n", + "\n", + "# Push the image to ECR\n", + "podman push $ECR_REPO:$VERSION\n", + "\n", + "echo \"Successfully built and pushed image to $ECR_REPO:$VERSION\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d94e6fe8", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "sh build.sh" + ] + }, + { + "cell_type": "markdown", + "id": "7be5062a", + "metadata": {}, + "source": [ + "## Deploy AgentCore Runtime\n", + "\n", + "To deploy an Amazon Bedrock AgentCore Runtime using our custom container image, we will need an IAM Role. The code below creates one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74f738d8", + "metadata": {}, + "outputs": [], + "source": [ + "from utils import create_agentcore_role\n", + "\n", + "agent_name=\"chess_agent_runtime_demo\"\n", + "agentcore_iam_role = create_agentcore_role(agent_name=agent_name)" + ] + }, + { + "cell_type": "markdown", + "id": "a361bb96", + "metadata": {}, + "source": [ + "Let's now deploy an Amazon Bedrock AgentCore Runtime using our custom container image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f54b915", + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "\n", + "client = boto3.client('bedrock-agentcore-control')\n", + "\n", + "response = client.create_agent_runtime(\n", + " agentRuntimeName=agent_name,\n", + " agentRuntimeArtifact={\n", + " 'containerConfiguration': {\n", + " 'containerUri': '123456789101.dkr.ecr.us-east-1.amazonaws.com/chess-agent/runtime/api:v1' # Change to your actual container URI\n", + " }\n", + " },\n", + " networkConfiguration={\"networkMode\": \"PUBLIC\"},\n", + " roleArn=agentcore_iam_role['Role']['Arn']\n", + ")\n", + "\n", + "print(f\"Agent Runtime created successfully!\")\n", + "print(f\"Agent Runtime ARN: {response['agentRuntimeArn']}\")\n", + "print(f\"Status: {response['status']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "39172762", + "metadata": {}, + "source": [ + "### Streaming request responses\n", + "\n", + "Now, we will make a request to our agent and stream its response.\n", + "\n", + "Get your Agent Runtime ARN printed in the last code chunk and paste it accordingly." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "3b845b32-a03e-45c2-a2f0-2afba8069f47", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Streaming response:\n", + "\n", + "The Italian Opening is one of the oldest and most classical chess openings! It starts with:\n", + "\n", + "1. e4 e5\n", + "2. Nf3 Nc6 \n", + "3. Bc4\n", + "\n", + "The key idea is that White develops the bishop to c4, targeting Black's f7 square - which is often considered Black's weakest point in the opening since it's only defended by the king.\n", + "\n", + "This opening leads to open, tactical games with lots of piece activity. It's great for beginners because it follows solid opening principles: control the center, develop pieces quickly, and create threats.\n", + "\n", + "The most common continuation is 3...Bc5, leading to the Italian Game proper, though Black has other good options like 3...Be7 (Hungarian Defense) or 3...f5 (Rousseau Gambit, though that's quite risky!).\n", + "\n", + "It's named after Italian chess players who analyzed it extensively in the 16th century. Even today, it's popular at all levels - from club players to world champions!\n", + "\n", + "Are you thinking of adding it to your opening repertoire? 😊" + ] + } + ], + "source": [ + "import boto3\n", + "import json\n", + "import uuid\n", + "\n", + "agent_core_client = boto3.client('bedrock-agentcore', region_name='us-east-1')\n", + "\n", + "my_prompt = \"Can you briefly explain what is the Italian Opening?\"\n", + "\n", + "payload = json.dumps({\n", + " \"input\": {\"prompt\": my_prompt},\n", + "})\n", + "\n", + "# sessionId = uuid.uuid4()\n", + "sessionId = 'f4510a4c-3e5f-4511-a8cc-eb9ef5c8a9ad'\n", + "\n", + "response = agent_core_client.invoke_agent_runtime(\n", + " agentRuntimeArn=response['agentRuntimeArn'],\n", + " runtimeSessionId=sessionId,\n", + " payload=payload,\n", + " qualifier=\"DEFAULT\"\n", + ")\n", + "\n", + "print(\"Streaming response:\\n\")\n", + "\n", + "try:\n", + " if \"text/event-stream\" in response.get(\"contentType\", \"\"):\n", + " streaming_body = response[\"response\"]\n", + " buffer = b'' # Buffer to handle incomplete UTF-8 sequences\n", + " \n", + " while True:\n", + " # Use larger chunks for simpler approach but still handle UTF-8 properly\n", + " chunk = streaming_body.read(8) # 16 bytes - larger chunks, less frequent processing\n", + " if not chunk:\n", + " # End of stream - decode any remaining buffer\n", + " if buffer:\n", + " try:\n", + " decoded_chunk = buffer.decode(\"utf-8\")\n", + " print(decoded_chunk, end='', flush=True)\n", + " except UnicodeDecodeError:\n", + " pass # Skip invalid bytes\n", + " break\n", + " \n", + " buffer += chunk\n", + " \n", + " # Try to decode the buffer\n", + " try:\n", + " decoded_chunk = buffer.decode(\"utf-8\")\n", + " print(decoded_chunk, end='', flush=True)\n", + " buffer = b'' # Clear buffer after successful decode\n", + " except UnicodeDecodeError:\n", + " # Incomplete UTF-8 sequence at end of chunk\n", + " # Find the last complete UTF-8 character boundary\n", + " for i in range(len(buffer) - 1, max(0, len(buffer) - 4), -1):\n", + " try:\n", + " # Try to decode up to position i\n", + " decoded_chunk = buffer[:i].decode(\"utf-8\")\n", + " print(decoded_chunk, end='', flush=True)\n", + " buffer = buffer[i:] # Keep remaining bytes for next iteration\n", + " break\n", + " except UnicodeDecodeError:\n", + " continue\n", + " else:\n", + " # If we can't find a valid boundary, skip the first byte\n", + " buffer = buffer[1:]\n", + " else:\n", + " streaming_body = response[\"response\"]\n", + " data = streaming_body.read()\n", + " print(data.decode('utf-8'))\n", + " \n", + "except Exception as e:\n", + " print(f\"An error occurred: {e}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "e296abc4", + "metadata": {}, + "source": [ + "---\n", + "\n", + "*Et voilà!*. Here is your streaming response from your custom agent deployed into AgentCore Runtime. \n", + "\n", + "Play a little bit with some different chess questions or develop an agent example of your own." + ] + }, + { + "cell_type": "markdown", + "id": "streaming_benefits", + "metadata": {}, + "source": [ + "## Benefits of Streaming Responses\n", + "\n", + "Streaming responses provide several key advantages:\n", + "\n", + "### User Experience\n", + "* **Immediate Feedback**: Users see partial results as they become available\n", + "* **Perceived Performance**: Responses feel faster even if total time is the same\n", + "* **Progressive Display**: Long responses can be displayed incrementally\n", + "\n", + "### Technical Benefits\n", + "* **Memory Efficient**: Process large responses without loading everything into memory\n", + "* **Timeout Prevention**: Avoid timeouts on long-running operations\n", + "* **Real-time Processing**: Handle real-time data as it becomes available\n", + "\n", + "### Use Cases\n", + "* **Content Generation**: Long-form writing, reports, documentation\n", + "* **Data Analysis**: Progressive results from complex calculations\n", + "* **Multi-step Workflows**: Show progress through complex agent reasoning\n", + "* **Real-time Monitoring**: Live updates from monitoring agents" + ] + }, + { + "cell_type": "markdown", + "id": "7d3fdfe404469632", + "metadata": {}, + "source": [ + "## Cleanup (Optional)\n", + "\n", + "Let's now clean up the AgentCore Runtime created" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76a6cf1416830a54", + "metadata": {}, + "outputs": [], + "source": [ + "agentcore_control_client = boto3.client(\n", + " 'bedrock-agentcore-control',\n", + " region_name=region\n", + ")\n", + "ecr_client = boto3.client(\n", + " 'ecr',\n", + " region_name=region\n", + " \n", + ")\n", + "\n", + "iam_client = boto3.client('iam')\n", + "\n", + "runtime_delete_response = agentcore_control_client.delete_agent_runtime(\n", + " agentRuntimeId='',\n", + " \n", + ")\n", + "\n", + "response = ecr_client.delete_repository(\n", + " repositoryName='',\n", + " force=True\n", + ")\n", + "\n", + "policies = iam_client.list_role_policies(\n", + " RoleName=agentcore_iam_role['Role']['RoleName'],\n", + " MaxItems=100\n", + ")\n", + "\n", + "for policy_name in policies['PolicyNames']:\n", + " iam_client.delete_role_policy(\n", + " RoleName=agentcore_iam_role['Role']['RoleName'],\n", + " PolicyName=policy_name\n", + " )\n", + "iam_response = iam_client.delete_role(\n", + " RoleName=agentcore_iam_role['Role']['RoleName']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "b118ad38-feeb-4d1d-9d57-e5c845becc56", + "metadata": {}, + "source": [ + "# Congratulations!\n", + "\n", + "You have successfully implemented and deployed a custom streaming agent using Amazon Bedrock AgentCore Runtime! \n", + "\n", + "## What you've learned:\n", + "* How to implement streaming responses using async generators and StreamingResponse\n", + "* How to build a custom container image with several python modules and a custom agent\n", + "* How to process streaming responses on the client side\n", + "* The benefits of streaming for user experience and performance\n", + "\n", + "## Next steps:\n", + "* Experiment with different streaming patterns for your use cases\n", + "* Implement custom streaming logic for complex multi-step workflows\n", + "* Explore combining streaming with other AgentCore features like Memory and Gateway\n", + "* Consider implementing client-side streaming visualization for better UX" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/requirements.txt b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/requirements.txt new file mode 100644 index 000000000..1551bab09 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/requirements.txt @@ -0,0 +1,8 @@ +bedrock-agentcore +bedrock-agentcore-starter-toolkit +fastapi>=0.116.1 +pydantic>=2.11.7 +requests>=2.32.3 +strands-agents>=0.1.0 +strands-agents-tools>=0.1.0 +uvicorn>=0.34.2 \ No newline at end of file diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/utils.py b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/utils.py new file mode 100644 index 000000000..34d8813b8 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/05-byo-container-streaming-agent/utils.py @@ -0,0 +1,253 @@ +import boto3 +import json +import time +from boto3.session import Session + + +def setup_cognito_user_pool(): + boto_session = Session() + region = boto_session.region_name + + # Initialize Cognito client + cognito_client = boto3.client('cognito-idp', region_name=region) + + try: + # Create User Pool + user_pool_response = cognito_client.create_user_pool( + PoolName='MCPServerPool', + Policies={ + 'PasswordPolicy': { + 'MinimumLength': 8 + } + } + ) + pool_id = user_pool_response['UserPool']['Id'] + + # Create App Client + app_client_response = cognito_client.create_user_pool_client( + UserPoolId=pool_id, + ClientName='MCPServerPoolClient', + GenerateSecret=False, + ExplicitAuthFlows=[ + 'ALLOW_USER_PASSWORD_AUTH', + 'ALLOW_REFRESH_TOKEN_AUTH' + ] + ) + client_id = app_client_response['UserPoolClient']['ClientId'] + + # Create User + cognito_client.admin_create_user( + UserPoolId=pool_id, + Username='testuser', + TemporaryPassword='Temp123!', + MessageAction='SUPPRESS' + ) + + # Set Permanent Password + cognito_client.admin_set_user_password( + UserPoolId=pool_id, + Username='testuser', + Password='MyPassword123!', + Permanent=True + ) + + # Authenticate User and get Access Token + auth_response = cognito_client.initiate_auth( + ClientId=client_id, + AuthFlow='USER_PASSWORD_AUTH', + AuthParameters={ + 'USERNAME': 'testuser', + 'PASSWORD': 'MyPassword123!' + } + ) + bearer_token = auth_response['AuthenticationResult']['AccessToken'] + + # Output the required values + print(f"Pool id: {pool_id}") + print(f"Discovery URL: https://cognito-idp.{region}.amazonaws.com/{pool_id}/.well-known/openid-configuration") + print(f"Client ID: {client_id}") + print(f"Bearer Token: {bearer_token}") + + # Return values if needed for further processing + return { + 'pool_id': pool_id, + 'client_id': client_id, + 'bearer_token': bearer_token, + 'discovery_url':f"https://cognito-idp.{region}.amazonaws.com/{pool_id}/.well-known/openid-configuration" + } + + except Exception as e: + print(f"Error: {e}") + return None + + +def create_agentcore_role(agent_name): + iam_client = boto3.client('iam') + agentcore_role_name = f'agentcore-{agent_name}-role' + boto_session = Session() + region = boto_session.region_name + account_id = boto3.client("sts").get_caller_identity()["Account"] + role_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BedrockPermissions", + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ], + "Resource": "*" + }, + { + "Sid": "ECRImageAccess", + "Effect": "Allow", + "Action": [ + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ], + "Resource": [ + f"arn:aws:ecr:{region}:{account_id}:repository/*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "logs:DescribeLogStreams", + "logs:CreateLogGroup" + ], + "Resource": [ + f"arn:aws:logs:{region}:{account_id}:log-group:/aws/bedrock-agentcore/runtimes/*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "logs:DescribeLogGroups" + ], + "Resource": [ + f"arn:aws:logs:{region}:{account_id}:log-group:*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + f"arn:aws:logs:{region}:{account_id}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*" + ] + }, + { + "Sid": "ECRTokenAccess", + "Effect": "Allow", + "Action": [ + "ecr:GetAuthorizationToken" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + "xray:GetSamplingRules", + "xray:GetSamplingTargets" + ], + "Resource": [ "*" ] + }, + { + "Effect": "Allow", + "Resource": "*", + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "bedrock-agentcore" + } + } + }, + { + "Sid": "GetAgentAccessToken", + "Effect": "Allow", + "Action": [ + "bedrock-agentcore:GetWorkloadAccessToken", + "bedrock-agentcore:GetWorkloadAccessTokenForJWT", + "bedrock-agentcore:GetWorkloadAccessTokenForUserId" + ], + "Resource": [ + f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default", + f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default/workload-identity/{agent_name}-*" + ] + } + ] + } + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AssumeRolePolicy", + "Effect": "Allow", + "Principal": { + "Service": "bedrock-agentcore.amazonaws.com" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "aws:SourceAccount": f"{account_id}" + }, + "ArnLike": { + "aws:SourceArn": f"arn:aws:bedrock-agentcore:{region}:{account_id}:*" + } + } + } + ] + } + + assume_role_policy_document_json = json.dumps( + assume_role_policy_document + ) + role_policy_document = json.dumps(role_policy) + # Create IAM Role for the Lambda function + try: + agentcore_iam_role = iam_client.create_role( + RoleName=agentcore_role_name, + AssumeRolePolicyDocument=assume_role_policy_document_json + ) + + # Pause to make sure role is created + time.sleep(10) + except iam_client.exceptions.EntityAlreadyExistsException: + print("Role already exists -- deleting and creating it again") + policies = iam_client.list_role_policies( + RoleName=agentcore_role_name, + MaxItems=100 + ) + print("policies:", policies) + for policy_name in policies['PolicyNames']: + iam_client.delete_role_policy( + RoleName=agentcore_role_name, + PolicyName=policy_name + ) + print(f"deleting {agentcore_role_name}") + iam_client.delete_role( + RoleName=agentcore_role_name + ) + print(f"recreating {agentcore_role_name}") + agentcore_iam_role = iam_client.create_role( + RoleName=agentcore_role_name, + AssumeRolePolicyDocument=assume_role_policy_document_json + ) + + # Attach the AWSLambdaBasicExecutionRole policy + print(f"attaching role policy {agentcore_role_name}") + try: + iam_client.put_role_policy( + PolicyDocument=role_policy_document, + PolicyName="AgentCorePolicy", + RoleName=agentcore_role_name + ) + except Exception as e: + print(e) + + return agentcore_iam_role \ No newline at end of file diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 9d6180a4c..510ed952b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -27,3 +27,4 @@ - w601sxs - erezweinstein5 - HardikThakkar94 +- neylsoncrepalde