From c2fe5cd9a7d85782ff318abda4998bf691785b0e Mon Sep 17 00:00:00 2001 From: Mike Hume Date: Wed, 8 Jul 2026 11:41:32 +0100 Subject: [PATCH 1/2] New serverless pattern - strands-agentcore-smithy --- strands-agentcore-smithy/.gitignore | 15 + strands-agentcore-smithy/Makefile | 31 + strands-agentcore-smithy/README.md | 184 + .../architecture/target-smithy.png | Bin 0 -> 69771 bytes strands-agentcore-smithy/example-pattern.json | 86 + .../bedrock-runtime-2023-09-30.json | 7561 +++++++++++++++++ .../infrastructure/template.yaml | 219 + strands-agentcore-smithy/requirements.txt | 4 + strands-agentcore-smithy/scripts/deploy.sh | 240 + strands-agentcore-smithy/scripts/test.sh | 78 + .../src/agent/__init__.py | 4 + .../src/agent/agent_processor.py | 77 + strands-agentcore-smithy/src/agent/handler.py | 108 + .../src/agent/strands_client.py | 71 + .../src/shared/__init__.py | 36 + .../src/shared/error_utils.py | 75 + .../src/shared/jwt_utils.py | 96 + .../src/shared/logging_utils.py | 74 + strands-agentcore-smithy/src/shared/models.py | 29 + strands-agentcore-smithy/tests/__init__.py | 1 + strands-agentcore-smithy/tests/conftest.py | 140 + .../tests/property/__init__.py | 1 + .../property/test_agnostic_properties.py | 82 + .../test_cloudformation_properties.py | 252 + .../property/test_deploy_script_properties.py | 50 + .../tests/property/test_jwt_properties.py | 120 + .../tests/unit/__init__.py | 1 + .../tests/unit/test_agent_processor.py | 143 + .../unit/test_cloudformation_template.py | 363 + .../tests/unit/test_deploy_script.py | 192 + .../tests/unit/test_error_utils.py | 60 + .../tests/unit/test_handler.py | 131 + .../tests/unit/test_jwt_utils.py | 98 + .../tests/unit/test_logging_utils.py | 56 + .../tests/unit/test_models.py | 54 + .../tests/unit/test_strands_client.py | 115 + .../tests/unit/test_test_script.py | 142 + 37 files changed, 10989 insertions(+) create mode 100644 strands-agentcore-smithy/.gitignore create mode 100644 strands-agentcore-smithy/Makefile create mode 100644 strands-agentcore-smithy/README.md create mode 100644 strands-agentcore-smithy/architecture/target-smithy.png create mode 100644 strands-agentcore-smithy/example-pattern.json create mode 100644 strands-agentcore-smithy/infrastructure/bedrock-runtime-2023-09-30.json create mode 100644 strands-agentcore-smithy/infrastructure/template.yaml create mode 100644 strands-agentcore-smithy/requirements.txt create mode 100755 strands-agentcore-smithy/scripts/deploy.sh create mode 100755 strands-agentcore-smithy/scripts/test.sh create mode 100644 strands-agentcore-smithy/src/agent/__init__.py create mode 100644 strands-agentcore-smithy/src/agent/agent_processor.py create mode 100644 strands-agentcore-smithy/src/agent/handler.py create mode 100644 strands-agentcore-smithy/src/agent/strands_client.py create mode 100644 strands-agentcore-smithy/src/shared/__init__.py create mode 100644 strands-agentcore-smithy/src/shared/error_utils.py create mode 100644 strands-agentcore-smithy/src/shared/jwt_utils.py create mode 100644 strands-agentcore-smithy/src/shared/logging_utils.py create mode 100644 strands-agentcore-smithy/src/shared/models.py create mode 100644 strands-agentcore-smithy/tests/__init__.py create mode 100644 strands-agentcore-smithy/tests/conftest.py create mode 100644 strands-agentcore-smithy/tests/property/__init__.py create mode 100644 strands-agentcore-smithy/tests/property/test_agnostic_properties.py create mode 100644 strands-agentcore-smithy/tests/property/test_cloudformation_properties.py create mode 100644 strands-agentcore-smithy/tests/property/test_deploy_script_properties.py create mode 100644 strands-agentcore-smithy/tests/property/test_jwt_properties.py create mode 100644 strands-agentcore-smithy/tests/unit/__init__.py create mode 100644 strands-agentcore-smithy/tests/unit/test_agent_processor.py create mode 100644 strands-agentcore-smithy/tests/unit/test_cloudformation_template.py create mode 100644 strands-agentcore-smithy/tests/unit/test_deploy_script.py create mode 100644 strands-agentcore-smithy/tests/unit/test_error_utils.py create mode 100644 strands-agentcore-smithy/tests/unit/test_handler.py create mode 100644 strands-agentcore-smithy/tests/unit/test_jwt_utils.py create mode 100644 strands-agentcore-smithy/tests/unit/test_logging_utils.py create mode 100644 strands-agentcore-smithy/tests/unit/test_models.py create mode 100644 strands-agentcore-smithy/tests/unit/test_strands_client.py create mode 100644 strands-agentcore-smithy/tests/unit/test_test_script.py diff --git a/strands-agentcore-smithy/.gitignore b/strands-agentcore-smithy/.gitignore new file mode 100644 index 0000000000..6475b62f13 --- /dev/null +++ b/strands-agentcore-smithy/.gitignore @@ -0,0 +1,15 @@ +*-deps/ +*-package/ +*.zip +.aws-sam/ +samconfig.toml +.kiro/ +.venv/ +venv/ +__pycache__/ +*.pyc +*.so +*.egg-info/ +.pytest_cache/ +.hypothesis/ +.DS_Store diff --git a/strands-agentcore-smithy/Makefile b/strands-agentcore-smithy/Makefile new file mode 100644 index 0000000000..0a8a6589d6 --- /dev/null +++ b/strands-agentcore-smithy/Makefile @@ -0,0 +1,31 @@ +# ============================================================ +# AgentCore Smithy Bedrock - SAM Makefile build target +# ============================================================ +# Used by `sam build` via the AgentLambdaFunction resource's +# `Metadata: BuildMethod: makefile`. This replaces the previous +# manual packaging in deploy.sh and requires NO Docker. +# +# SAM invokes `make build-AgentLambdaFunction` with ARTIFACTS_DIR +# set to the directory SAM expects the built artifacts in. +# +# Two-step install reproduces the original packaging behaviour: +# 1. Install all dependencies as manylinux2014 x86_64 wheels +# 2. Re-install pure-Python packages with --no-deps so nothing +# is skipped by the binary-only constraint +# ============================================================ + +build-AgentLambdaFunction: + pip3 install \ + --target "$(ARTIFACTS_DIR)" \ + --platform manylinux2014_x86_64 \ + --python-version 3.12 \ + --only-binary=:all: \ + -r requirements.txt + pip3 install \ + --target "$(ARTIFACTS_DIR)" \ + --platform manylinux2014_x86_64 \ + --python-version 3.12 \ + --only-binary=:all: \ + --no-deps \ + requests urllib3 charset-normalizer idna certifi PyJWT cryptography cffi + cp -r src "$(ARTIFACTS_DIR)/" diff --git a/strands-agentcore-smithy/README.md b/strands-agentcore-smithy/README.md new file mode 100644 index 0000000000..d6e8b71889 --- /dev/null +++ b/strands-agentcore-smithy/README.md @@ -0,0 +1,184 @@ +# Strands Agent on Lambda with AgentCore Smithy Bedrock Target + +A serverless AI agent that uses AWS Bedrock AgentCore Gateway with a Smithy model target to call Bedrock Runtime. The agent (Claude Sonnet 4.6) calls another model (Claude Haiku 4.5) as a tool via the gateway's Converse API. + +## Architecture + +![Architecture Diagram](architecture/target-smithy.png) + +``` +User → test.sh → Agent Lambda ──────────────→ AgentCore Gateway (MCP) ──────────→ Bedrock Runtime + │ │ │ + Strands Agent CUSTOM_JWT Auth GATEWAY_IAM_ROLE + + BedrockModel (Cognito JWKS) (IAM role signs + (Sonnet 4.6) + MCP Protocol requests — no + + MCPClient + Tool Discovery API key needed) + │ + SmithyModel target │ + Cognito JWT (loaded from S3) Converse API call + validated → Claude Haiku 4.5 +``` + +## How It Works + +1. User authenticates with Cognito and invokes the Lambda with a natural language prompt +2. The Lambda runs a Strands SDK agent with Claude Sonnet 4.6 as the LLM +3. The agent connects to AgentCore Gateway via MCP and discovers available tools +4. The gateway reads the official Bedrock Runtime Smithy model (from S3) and exposes operations as MCP tools +5. Sonnet decides to call the Converse tool, passing the user's prompt to Claude Haiku 4.5 +6. The gateway assumes its IAM execution role, signs the request, and calls Bedrock Runtime +7. Haiku's response flows back through the gateway → MCP → agent → user + +## Deploy + +### Step 1: Prerequisites + +- AWS CLI v2 +- AWS SAM CLI +- Python 3.12+ +- pip3 +- `make` (used by the SAM Makefile build — no Docker required) +- AWS account with Bedrock and AgentCore enabled in `us-east-1` +- Bedrock model access enabled for Claude Sonnet 4.6 and Claude Haiku 4.5 + +### Step 2: Open a Terminal + +Open a terminal on your machine and navigate to where you want to clone the project. + +### Step 3: Clone the Repository + +```bash +git clone https://github.com/aws-samples/serverless-patterns +cd serverless-patterns/strands-agentcore-smithy +``` + +### Step 4: Deploy + +One command deploys everything: + +```bash +bash scripts/deploy.sh +``` + +This will: +1. Validate the SAM template (`sam validate`) +2. Download the official Bedrock Runtime Smithy model and upload to S3 +3. Build the Lambda with `sam build` (Makefile build — two-step pip3 install, no Docker) +4. Deploy the stack with `sam deploy` (Gateway, Cognito, Lambda, IAM roles) +5. Create a Cognito test user +6. Generate `scripts/test.sh` with baked-in config values + +## Test + +After deployment, test with: + +```bash +./scripts/test.sh "Ask Haiku to write a short poem about the Beatles" +``` + +Or use the default prompt: + +```bash +./scripts/test.sh +``` + +## Project Structure + +``` +├── infrastructure/ +│ ├── template.yaml # SAM template (all AWS resources) +│ └── bedrock-runtime-2023-09-30.json # Official Smithy model (for tests) +├── Makefile # SAM Makefile build (no Docker) +├── scripts/ +│ ├── deploy.sh # Full deployment script (SAM) +│ └── test.sh # Generated by deploy.sh +├── src/ +│ ├── agent/ +│ │ ├── handler.py # Lambda entry point +│ │ ├── agent_processor.py # Strands Agent + MCP lifecycle +│ │ └── strands_client.py # MCP client + Bedrock model factories +│ └── shared/ +│ ├── models.py # UserContext, AgentRequest, AgentResponse +│ ├── jwt_utils.py # JWT validation (Cognito ID tokens) +│ ├── error_utils.py # Error response formatting +│ └── logging_utils.py # Structured logging +├── tests/ +│ ├── unit/ # Unit tests +│ └── property/ # Property-based tests (Hypothesis) +├── requirements.txt # Lambda runtime dependencies +└── README.md +``` + +## Changing the Outer Agent Model + +The outer agent model is set in `src/agent/strands_client.py`: + +```python +# Default Bedrock model ID for Claude Sonnet 4.6 +DEFAULT_MODEL_ID = "us.anthropic.claude-sonnet-4-6" +``` + +To use a different model, update `DEFAULT_MODEL_ID` to any Bedrock cross-region inference profile ID you have access to. For example: + +```python +DEFAULT_MODEL_ID = "us.anthropic.claude-3-5-sonnet-20241022-v2:0" +``` + +Make sure the model is enabled in your AWS account under **Bedrock → Model access** before deploying. + +> **Note:** The inner model (the one the agent calls as a tool) is separate — it's configured in the `SYSTEM_PROMPT` string in `src/agent/agent_processor.py`. See [Changing the Inner Model](#changing-the-inner-model) and [Key Design Decisions](#key-design-decisions) for more detail. + +## Changing the Inner Model + +The inner model is the one the outer agent calls *as a tool* via the AgentCore Gateway. It's configured inside the `SYSTEM_PROMPT` string in `src/agent/agent_processor.py`: + +```python +SYSTEM_PROMPT = """You have access to Bedrock Runtime tools via MCP. Use the Converse tool (not InvokeModel) to call another model. + +When using bedrock-runtime-target___Converse: +- Set modelId to: anthropic.claude-haiku-4-5-20251001-v1:0 +... +``` + +To use a different model, replace the `modelId` value in that instruction: + +```python +- Set modelId to: anthropic.claude-haiku-4-5-20251001-v1:0 +``` + +A few things to keep in mind: + +- The model ID here is passed as a parameter in the MCP tool call, not used directly by the Lambda — so it must be a valid Bedrock `modelId` (not a cross-region inference profile). +- Make sure the model supports the **Converse API** (most Claude models do, but check the [Bedrock docs](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html)). +- Enable the model in your AWS account under **Bedrock → Model access** before deploying. + +## Key Design Decisions + +### Official Smithy Model (not custom) +Custom Smithy models don't work for AWS service targets — the gateway needs the full endpoint resolution metadata (`aws.api#service` trait, partition data, FIPS/DualStack logic) that only official models have. The deploy script downloads the official model from the [AWS API Models repo](https://github.com/aws/api-models-aws). + +### Converse API (not InvokeModel) +The `InvokeModel` operation uses a `Blob` with `httpPayload` trait for the request body, which doesn't map to MCP tool input parameters. `Converse` has structured `messages` and `modelId` fields that map cleanly. + +### GATEWAY_IAM_ROLE (not API_KEY) +Since the target is an AWS service (Bedrock Runtime), the gateway uses its IAM execution role to sign requests. No API keys, no Secrets Manager secrets, no credential provider CLI commands. + +### Agent code is target-agnostic +All code in `src/agent/` and `src/shared/` connects to the gateway via MCP and doesn't reference Bedrock, DynamoDB, or any specific target. The same code works with any gateway target type. + +## Teardown + +```bash +aws cloudformation delete-stack --stack-name agentcore-smithy-bedrock --region us-east-1 +aws s3 rb s3://agentcore-smithy-bedrock-smithy-models --force --region us-east-1 +``` + +## Run Tests + +```bash +python -m pytest tests/ -v +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 diff --git a/strands-agentcore-smithy/architecture/target-smithy.png b/strands-agentcore-smithy/architecture/target-smithy.png new file mode 100644 index 0000000000000000000000000000000000000000..227e1291575a6edd2be370912c42c69943103d02 GIT binary patch literal 69771 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

Apv{JQh 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 hS<2K(kjAGJZ0xkJ2 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 PfIoF*?I*>LEMNXFD!$XA literal 0 HcmV?d00001 diff --git a/strands-agentcore-smithy/example-pattern.json b/strands-agentcore-smithy/example-pattern.json new file mode 100644 index 0000000000..714e25a40c --- /dev/null +++ b/strands-agentcore-smithy/example-pattern.json @@ -0,0 +1,86 @@ +{ + "title": "Strands Agent on Lambda with AgentCore Smithy Bedrock Target", + "description": "Strands SDK agent on Lambda calls Bedrock Runtime via AgentCore Gateway using a Smithy model target, with Cognito JWT auth and IAM role signing.", + "language": "Python", + "level": "300", + "framework": "SAM", + "introBox": { + "headline": "How it works", + "text": [ + "The user authenticates with Amazon Cognito and receives a JWT token.", + "The JWT is passed to an Agent Lambda which uses the Strands Agents SDK to create an AI agent backed by Amazon Bedrock (Claude Sonnet 4.6 cross-region inference profile).", + "The Strands Agent connects to an AgentCore Gateway MCP endpoint, dynamically discovering available Bedrock Runtime operations via the MCP tools/list protocol.", + "The AgentCore Gateway validates the JWT token using a CUSTOM_JWT authorizer backed by Cognito, then routes MCP tool calls to the Smithy model target.", + "The gateway target uses the official Bedrock Runtime Smithy model (loaded from S3) to expose Bedrock Runtime operations as MCP tools.", + "The gateway uses its IAM execution role (GATEWAY_IAM_ROLE) to sign requests to Bedrock Runtime — no API keys or Secrets Manager secrets are required.", + "The outer agent (Claude Sonnet 4.6) calls the Converse tool, which invokes Claude Haiku 4.5 as the inner model to generate the response.", + "The Strands SDK handles the full agentic loop: tool discovery, Claude tool selection, MCP tool execution, and response formatting — all in a single agent() call." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/strands-agentcore-smithy", + "templateURL": "serverless-patterns/strands-agentcore-smithy", + "projectFolder": "strands-agentcore-smithy", + "templateFile": "infrastructure/template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "Strands Agents SDK", + "link": "https://github.com/strands-agents/sdk-python" + }, + { + "text": "Amazon Bedrock AgentCore Gateway", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html" + }, + { + "text": "AgentCore Gateway Smithy Model Target", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-building-smithy-targets.html" + }, + { + "text": "Model Context Protocol (MCP)", + "link": "https://modelcontextprotocol.io/" + }, + { + "text": "Amazon Cognito JWT Authentication", + "link": "https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html" + }, + { + "text": "Amazon Bedrock Converse API", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html" + }, + { + "text": "Amazon Bedrock Cross-Region Inference", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html" + } + ] + }, + "deploy": { + "text": [ + "bash scripts/deploy.sh" + ] + }, + "testing": { + "text": [ + "./scripts/test.sh", + "./scripts/test.sh 'Ask Haiku to write a short poem about the Beatles'" + ] + }, + "cleanup": { + "text": [ + "aws cloudformation delete-stack --stack-name agentcore-smithy-bedrock --region us-east-1", + "aws s3 rb s3://agentcore-smithy-bedrock-smithy-models --force --region us-east-1" + ] + }, + "authors": [ + { + "name": "Mike Hume", + "image": "https://serverlessland.com/assets/images/contributors/mike-hume.jpg", + "bio": "AWS Senior Solutions Architect & UKPS Serverless Lead.", + "linkedin": "michael-hume-4663bb64", + "twitter": "" + } + ] +} diff --git a/strands-agentcore-smithy/infrastructure/bedrock-runtime-2023-09-30.json b/strands-agentcore-smithy/infrastructure/bedrock-runtime-2023-09-30.json new file mode 100644 index 0000000000..1ca7dafc55 --- /dev/null +++ b/strands-agentcore-smithy/infrastructure/bedrock-runtime-2023-09-30.json @@ -0,0 +1,7561 @@ +{ + "smithy": "2.0", + "shapes": { + "com.amazonaws.bedrockruntime#AccessDeniedException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

The request is denied because you do not have sufficient permissions to perform the requested action. For troubleshooting this error, see AccessDeniedException in the Amazon Bedrock User Guide

", + "smithy.api#error": "client", + "smithy.api#httpError": 403 + } + }, + "com.amazonaws.bedrockruntime#AccountId": { + "type": "string", + "traits": { + "smithy.api#pattern": "^[0-9]{12}$" + } + }, + "com.amazonaws.bedrockruntime#AdditionalModelResponseFieldPaths": { + "type": "list", + "member": { + "target": "smithy.api#String", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 256 + } + } + }, + "traits": { + "smithy.api#length": { + "max": 10 + } + } + }, + "com.amazonaws.bedrockruntime#AmazonBedrockFrontendService": { + "type": "service", + "version": "2023-09-30", + "resources": [ + { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeResource" + }, + { + "target": "com.amazonaws.bedrockruntime#GuardrailResource" + }, + { + "target": "com.amazonaws.bedrockruntime#InferenceResource" + }, + { + "target": "com.amazonaws.bedrockruntime#TokenizerResource" + } + ], + "traits": { + "aws.api#service": { + "sdkId": "Bedrock Runtime", + "endpointPrefix": "bedrock-runtime", + "cloudTrailEventSource": "bedrock.amazonaws.com" + }, + "aws.auth#sigv4": { + "name": "bedrock" + }, + "aws.protocols#restJson1": { + "http": [ + "h2", + "http/1.1" + ], + "eventStreamHttp": [ + "h2", + "http/1.1" + ] + }, + "smithy.api#auth": [ + "aws.auth#sigv4", + "smithy.api#httpBearerAuth" + ], + "smithy.api#documentation": "

Describes the API operations for running inference using Amazon Bedrock models.

", + "smithy.api#httpBearerAuth": {}, + "smithy.api#title": "Amazon Bedrock Runtime", + "smithy.rules#endpointRuleSet": { + "version": "1.0", + "parameters": { + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "string" + }, + "UseDualStack": { + "builtIn": "AWS::UseDualStack", + "required": true, + "default": false, + "documentation": "When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.", + "type": "boolean" + }, + "UseFIPS": { + "builtIn": "AWS::UseFIPS", + "required": true, + "default": false, + "documentation": "When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.", + "type": "boolean" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "string" + } + }, + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "error": "Invalid Configuration: FIPS and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": { + "ref": "Endpoint" + }, + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + } + ], + "type": "tree" + }, + { + "conditions": [], + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "PartitionResult" + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + } + ] + } + ], + "rules": [ + { + "conditions": [], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://bedrock-runtime-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS and DualStack are enabled, but this partition does not support one or both", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://bedrock-runtime-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + } + ] + } + ], + "rules": [ + { + "conditions": [], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://bedrock-runtime.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://bedrock-runtime.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + } + ], + "type": "tree" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "Invalid Configuration: Missing Region", + "type": "error" + } + ], + "type": "tree" + } + ] + }, + "smithy.rules#endpointTests": { + "testCases": [ + { + "documentation": "For region us-east-1 with FIPS enabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime-fips.us-east-1.api.aws" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region us-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime-fips.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-east-1 with FIPS disabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime.us-east-1.api.aws" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region us-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region cn-north-1 with FIPS enabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime-fips.cn-north-1.api.amazonwebservices.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region cn-north-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime-fips.cn-north-1.amazonaws.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region cn-north-1 with FIPS disabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime.cn-north-1.api.amazonwebservices.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region cn-north-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime.cn-north-1.amazonaws.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS enabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime-fips.us-gov-east-1.api.aws" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime-fips.us-gov-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS disabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime.us-gov-east-1.api.aws" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime.us-gov-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-iso-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime-fips.us-iso-east-1.c2s.ic.gov" + } + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-iso-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime.us-iso-east-1.c2s.ic.gov" + } + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-isob-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime-fips.us-isob-east-1.sc2s.sgov.gov" + } + }, + "params": { + "Region": "us-isob-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-isob-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://bedrock-runtime.us-isob-east-1.sc2s.sgov.gov" + } + }, + "params": { + "Region": "us-isob-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For custom endpoint with region set and fips disabled and dualstack disabled", + "expect": { + "endpoint": { + "url": "https://example.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "For custom endpoint with region not set and fips disabled and dualstack disabled", + "expect": { + "endpoint": { + "url": "https://example.com" + } + }, + "params": { + "UseFIPS": false, + "UseDualStack": false, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "For custom endpoint with fips enabled and dualstack disabled", + "expect": { + "error": "Invalid Configuration: FIPS and custom endpoint are not supported" + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": false, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "For custom endpoint with fips disabled and dualstack enabled", + "expect": { + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported" + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": true, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "Missing region", + "expect": { + "error": "Invalid Configuration: Missing Region" + } + } + ], + "version": "1.0" + } + } + }, + "com.amazonaws.bedrockruntime#AnyToolChoice": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "

The model must request at least one tool (no text is generated). For example, {\"any\" : {}}. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#AppliedGuardrailDetails": { + "type": "structure", + "members": { + "guardrailId": { + "target": "com.amazonaws.bedrockruntime#GuardrailId", + "traits": { + "smithy.api#documentation": "

The unique ID of the guardrail that was applied.

" + } + }, + "guardrailVersion": { + "target": "com.amazonaws.bedrockruntime#GuardrailVersion", + "traits": { + "smithy.api#documentation": "

The version of the guardrail that was applied.

" + } + }, + "guardrailArn": { + "target": "com.amazonaws.bedrockruntime#GuardrailArn", + "traits": { + "smithy.api#documentation": "

The ARN of the guardrail that was applied.

" + } + }, + "guardrailOrigin": { + "target": "com.amazonaws.bedrockruntime#GuardrailOriginList", + "traits": { + "smithy.api#documentation": "

The origin of how the guardrail was applied. This can be either requested at the API level or enforced at the account or organization level as a default guardrail.

" + } + }, + "guardrailOwnership": { + "target": "com.amazonaws.bedrockruntime#GuardrailOwnership", + "traits": { + "smithy.api#documentation": "

The ownership type of the guardrail, indicating whether it is owned by the requesting account or is a cross-account guardrail shared from another AWS account.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Details about the specific guardrail that was applied during this assessment, including its identifier, version, ARN, origin, and ownership information.

" + } + }, + "com.amazonaws.bedrockruntime#ApplyGuardrail": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#ApplyGuardrailRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#ApplyGuardrailResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceQuotaExceededException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

The action to apply a guardrail.

For troubleshooting some of the common errors you might encounter when using the ApplyGuardrail API, see Troubleshooting Amazon Bedrock API Error Codes in the Amazon Bedrock User Guide

", + "smithy.api#http": { + "code": 200, + "method": "POST", + "uri": "/guardrail/{guardrailIdentifier}/version/{guardrailVersion}/apply" + } + } + }, + "com.amazonaws.bedrockruntime#ApplyGuardrailRequest": { + "type": "structure", + "members": { + "guardrailIdentifier": { + "target": "com.amazonaws.bedrockruntime#GuardrailIdentifier", + "traits": { + "smithy.api#documentation": "

The guardrail identifier used in the request to apply the guardrail.

", + "smithy.api#httpLabel": {}, + "smithy.api#required": {} + } + }, + "guardrailVersion": { + "target": "com.amazonaws.bedrockruntime#GuardrailVersion", + "traits": { + "smithy.api#documentation": "

The guardrail version used in the request to apply the guardrail.

", + "smithy.api#httpLabel": {}, + "smithy.api#required": {} + } + }, + "source": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentSource", + "traits": { + "smithy.api#documentation": "

The source of data used in the request to apply the guardrail.

", + "smithy.api#required": {} + } + }, + "content": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentBlockList", + "traits": { + "smithy.api#documentation": "

The content details used in the request to apply the guardrail.

", + "smithy.api#required": {} + } + }, + "outputScope": { + "target": "com.amazonaws.bedrockruntime#GuardrailOutputScope", + "traits": { + "smithy.api#documentation": "

Specifies the scope of the output that you get in the response. Set to FULL to return the entire output, including any detected and non-detected entries in the response for enhanced debugging.

Note that the full output scope doesn't apply to word filters or regex in sensitive information filters. It does apply to all other filtering policies, including sensitive information with filters that can detect personally identifiable information (PII).

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#ApplyGuardrailResponse": { + "type": "structure", + "members": { + "usage": { + "target": "com.amazonaws.bedrockruntime#GuardrailUsage", + "traits": { + "smithy.api#documentation": "

The usage details in the response from the guardrail.

", + "smithy.api#required": {} + } + }, + "action": { + "target": "com.amazonaws.bedrockruntime#GuardrailAction", + "traits": { + "smithy.api#documentation": "

The action taken in the response from the guardrail.

", + "smithy.api#required": {} + } + }, + "actionReason": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The reason for the action taken when harmful content is detected.

" + } + }, + "outputs": { + "target": "com.amazonaws.bedrockruntime#GuardrailOutputContentList", + "traits": { + "smithy.api#documentation": "

The output details in the response from the guardrail.

", + "smithy.api#required": {} + } + }, + "assessments": { + "target": "com.amazonaws.bedrockruntime#GuardrailAssessmentList", + "traits": { + "smithy.api#documentation": "

The assessment details in the response from the guardrail.

", + "smithy.api#required": {} + } + }, + "guardrailCoverage": { + "target": "com.amazonaws.bedrockruntime#GuardrailCoverage", + "traits": { + "smithy.api#documentation": "

The guardrail coverage details in the apply guardrail response.

" + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#AsyncInvokeArn": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^arn:[a-z0-9\\-]+:bedrock:[a-z0-9\\-]*:[0-9]*:(provisioned-model|foundation-model)/.+$" + } + }, + "com.amazonaws.bedrockruntime#AsyncInvokeIdempotencyToken": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 256 + }, + "smithy.api#pattern": "^[!-~]*$" + } + }, + "com.amazonaws.bedrockruntime#AsyncInvokeIdentifier": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 256 + }, + "smithy.api#pattern": "^[a-zA-Z_\\.\\-/0-9:]+$" + } + }, + "com.amazonaws.bedrockruntime#AsyncInvokeMessage": { + "type": "string", + "traits": { + "smithy.api#length": { + "max": 2048 + }, + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#AsyncInvokeOutputDataConfig": { + "type": "union", + "members": { + "s3OutputDataConfig": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeS3OutputDataConfig", + "traits": { + "smithy.api#documentation": "

A storage location for the output data in an S3 bucket

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Asynchronous invocation output data settings.

" + } + }, + "com.amazonaws.bedrockruntime#AsyncInvokeResource": { + "type": "resource", + "operations": [ + { + "target": "com.amazonaws.bedrockruntime#GetAsyncInvoke" + }, + { + "target": "com.amazonaws.bedrockruntime#ListAsyncInvokes" + }, + { + "target": "com.amazonaws.bedrockruntime#StartAsyncInvoke" + } + ] + }, + "com.amazonaws.bedrockruntime#AsyncInvokeS3OutputDataConfig": { + "type": "structure", + "members": { + "s3Uri": { + "target": "com.amazonaws.bedrockruntime#S3Uri", + "traits": { + "smithy.api#documentation": "

An object URI starting with s3://.

", + "smithy.api#required": {} + } + }, + "kmsKeyId": { + "target": "com.amazonaws.bedrockruntime#KmsKeyId", + "traits": { + "smithy.api#documentation": "

A KMS encryption key ID.

" + } + }, + "bucketOwner": { + "target": "com.amazonaws.bedrockruntime#AccountId", + "traits": { + "smithy.api#documentation": "

If the bucket belongs to another AWS account, specify that account's ID.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Asynchronous invocation output data settings.

" + } + }, + "com.amazonaws.bedrockruntime#AsyncInvokeStatus": { + "type": "enum", + "members": { + "IN_PROGRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "InProgress" + } + }, + "COMPLETED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Completed" + } + }, + "FAILED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Failed" + } + } + } + }, + "com.amazonaws.bedrockruntime#AsyncInvokeSummaries": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeSummary" + } + }, + "com.amazonaws.bedrockruntime#AsyncInvokeSummary": { + "type": "structure", + "members": { + "invocationArn": { + "target": "com.amazonaws.bedrockruntime#InvocationArn", + "traits": { + "smithy.api#documentation": "

The invocation's ARN.

", + "smithy.api#required": {} + } + }, + "modelArn": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeArn", + "traits": { + "smithy.api#documentation": "

The invoked model's ARN.

", + "smithy.api#required": {} + } + }, + "clientRequestToken": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeIdempotencyToken", + "traits": { + "smithy.api#documentation": "

The invocation's idempotency token.

" + } + }, + "status": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeStatus", + "traits": { + "smithy.api#documentation": "

The invocation's status.

" + } + }, + "failureMessage": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeMessage", + "traits": { + "smithy.api#documentation": "

An error message.

" + } + }, + "submitTime": { + "target": "com.amazonaws.bedrockruntime#Timestamp", + "traits": { + "smithy.api#documentation": "

When the invocation was submitted.

", + "smithy.api#required": {} + } + }, + "lastModifiedTime": { + "target": "com.amazonaws.bedrockruntime#Timestamp", + "traits": { + "smithy.api#documentation": "

When the invocation was last modified.

" + } + }, + "endTime": { + "target": "com.amazonaws.bedrockruntime#Timestamp", + "traits": { + "smithy.api#documentation": "

When the invocation ended.

" + } + }, + "outputDataConfig": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeOutputDataConfig", + "traits": { + "smithy.api#documentation": "

The invocation's output data settings.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A summary of an asynchronous invocation.

" + } + }, + "com.amazonaws.bedrockruntime#AudioBlock": { + "type": "structure", + "members": { + "format": { + "target": "com.amazonaws.bedrockruntime#AudioFormat", + "traits": { + "smithy.api#documentation": "

The format of the audio data, such as MP3, WAV, FLAC, or other supported audio formats.

", + "smithy.api#required": {} + } + }, + "source": { + "target": "com.amazonaws.bedrockruntime#AudioSource", + "traits": { + "smithy.api#documentation": "

The source of the audio data, which can be provided as raw bytes or an S3 location.

", + "smithy.api#required": {} + } + }, + "error": { + "target": "com.amazonaws.bedrockruntime#ErrorBlock", + "traits": { + "smithy.api#documentation": "

Error information if the audio block could not be processed or contains invalid data.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

An audio content block that contains audio data in various supported formats.

" + } + }, + "com.amazonaws.bedrockruntime#AudioFormat": { + "type": "enum", + "members": { + "MP3": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mp3" + } + }, + "OPUS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "opus" + } + }, + "WAV": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "wav" + } + }, + "AAC": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "aac" + } + }, + "FLAC": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "flac" + } + }, + "MP4": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mp4" + } + }, + "OGG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ogg" + } + }, + "MKV": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mkv" + } + }, + "MKA": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mka" + } + }, + "X_AAC": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "x-aac" + } + }, + "M4A": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "m4a" + } + }, + "MPEG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mpeg" + } + }, + "MPGA": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mpga" + } + }, + "PCM": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "pcm" + } + }, + "WEBM": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "webm" + } + } + } + }, + "com.amazonaws.bedrockruntime#AudioSource": { + "type": "union", + "members": { + "bytes": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#documentation": "

Audio data encoded in base64.

", + "smithy.api#length": { + "min": 1 + } + } + }, + "s3Location": { + "target": "com.amazonaws.bedrockruntime#S3Location", + "traits": { + "smithy.api#documentation": "

A reference to audio data stored in an Amazon S3 bucket. To see which models support S3 uploads, see Supported models and features for Converse.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The source of audio data, which can be provided either as raw bytes or a reference to an S3 location.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#AutoToolChoice": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "

The Model automatically decides if a tool should be called or whether to generate text instead. For example, {\"auto\" : {}}. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide

" + } + }, + "com.amazonaws.bedrockruntime#AutomatedReasoningRuleIdentifier": { + "type": "string", + "traits": { + "smithy.api#length": { + "max": 12 + }, + "smithy.api#pattern": "^[a-z0-9]{12}$" + } + }, + "com.amazonaws.bedrockruntime#BidirectionalInputPayloadPart": { + "type": "structure", + "members": { + "bytes": { + "target": "com.amazonaws.bedrockruntime#PartBody", + "traits": { + "smithy.api#documentation": "

The audio content for the bidirectional input.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Payload content for the bidirectional input. The input is an audio stream.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#BidirectionalOutputPayloadPart": { + "type": "structure", + "members": { + "bytes": { + "target": "com.amazonaws.bedrockruntime#PartBody", + "traits": { + "smithy.api#documentation": "

The speech output of the bidirectional stream.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Output from the bidirectional stream. The output is speech and a text transcription.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#Body": { + "type": "blob", + "traits": { + "smithy.api#length": { + "max": 25000000 + }, + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#CacheDetail": { + "type": "structure", + "members": { + "ttl": { + "target": "com.amazonaws.bedrockruntime#CacheTTL", + "traits": { + "smithy.api#documentation": "

TTL duration for these cached tokens

", + "smithy.api#required": {} + } + }, + "inputTokens": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

Number of tokens written to cache with this TTL (cache creation tokens)

", + "smithy.api#range": { + "min": 0 + }, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Cache creation metrics for a specific TTL duration

" + } + }, + "com.amazonaws.bedrockruntime#CacheDetailsList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#CacheDetail" + }, + "traits": { + "smithy.api#documentation": "

List of cache details by TTL

" + } + }, + "com.amazonaws.bedrockruntime#CachePointBlock": { + "type": "structure", + "members": { + "type": { + "target": "com.amazonaws.bedrockruntime#CachePointType", + "traits": { + "smithy.api#documentation": "

Specifies the type of cache point within the CachePointBlock.

", + "smithy.api#required": {} + } + }, + "ttl": { + "target": "com.amazonaws.bedrockruntime#CacheTTL", + "traits": { + "smithy.api#documentation": "

Optional TTL duration for cache entries. When specified, enables extended TTL caching with the specified duration. When omitted, uses type value for caching behavior.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Defines a section of content to be cached for reuse in subsequent API calls.

" + } + }, + "com.amazonaws.bedrockruntime#CachePointType": { + "type": "enum", + "members": { + "DEFAULT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "default" + } + } + } + }, + "com.amazonaws.bedrockruntime#CacheTTL": { + "type": "enum", + "members": { + "FIVE_MINUTES": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "5m" + } + }, + "ONE_HOUR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "1h" + } + } + }, + "traits": { + "smithy.api#documentation": "

Time-to-live duration for ephemeral cache entries

" + } + }, + "com.amazonaws.bedrockruntime#Citation": { + "type": "structure", + "members": { + "title": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The title or identifier of the source document being cited.

" + } + }, + "source": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The source from the original search result that provided the cited content.

" + } + }, + "sourceContent": { + "target": "com.amazonaws.bedrockruntime#CitationSourceContentList", + "traits": { + "smithy.api#documentation": "

The specific content from the source document that was referenced or cited in the generated response.

" + } + }, + "location": { + "target": "com.amazonaws.bedrockruntime#CitationLocation", + "traits": { + "smithy.api#documentation": "

The precise location within the source document where the cited content can be found, including character positions, page numbers, or chunk identifiers.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains information about a citation that references a specific source document. Citations provide traceability between the model's generated response and the source documents that informed that response.

" + } + }, + "com.amazonaws.bedrockruntime#CitationGeneratedContent": { + "type": "union", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The text content that was generated by the model and is supported by the associated citation.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains the generated text content that corresponds to or is supported by a citation from a source document.

" + } + }, + "com.amazonaws.bedrockruntime#CitationGeneratedContentList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#CitationGeneratedContent" + } + }, + "com.amazonaws.bedrockruntime#CitationLocation": { + "type": "union", + "members": { + "web": { + "target": "com.amazonaws.bedrockruntime#WebLocation", + "traits": { + "smithy.api#documentation": "

The web URL that was cited for this reference.

" + } + }, + "documentChar": { + "target": "com.amazonaws.bedrockruntime#DocumentCharLocation", + "traits": { + "smithy.api#documentation": "

The character-level location within the document where the cited content is found.

" + } + }, + "documentPage": { + "target": "com.amazonaws.bedrockruntime#DocumentPageLocation", + "traits": { + "smithy.api#documentation": "

The page-level location within the document where the cited content is found.

" + } + }, + "documentChunk": { + "target": "com.amazonaws.bedrockruntime#DocumentChunkLocation", + "traits": { + "smithy.api#documentation": "

The chunk-level location within the document where the cited content is found, typically used for documents that have been segmented into logical chunks.

" + } + }, + "searchResultLocation": { + "target": "com.amazonaws.bedrockruntime#SearchResultLocation", + "traits": { + "smithy.api#documentation": "

The search result location where the cited content is found, including the search result index and block positions within the content array.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies the precise location within a source document where cited content can be found. This can include character-level positions, page numbers, or document chunks depending on the document type and indexing method.

" + } + }, + "com.amazonaws.bedrockruntime#CitationSourceContent": { + "type": "union", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The text content from the source document that is being cited.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains the actual text content from a source document that is being cited or referenced in the model's response.

" + } + }, + "com.amazonaws.bedrockruntime#CitationSourceContentDelta": { + "type": "structure", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

An incremental update to the text content from the source document that is being cited.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains incremental updates to the source content text during streaming responses, allowing clients to build up the cited content progressively.

" + } + }, + "com.amazonaws.bedrockruntime#CitationSourceContentList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#CitationSourceContent" + } + }, + "com.amazonaws.bedrockruntime#CitationSourceContentListDelta": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#CitationSourceContentDelta" + } + }, + "com.amazonaws.bedrockruntime#Citations": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#Citation" + } + }, + "com.amazonaws.bedrockruntime#CitationsConfig": { + "type": "structure", + "members": { + "enabled": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "

Specifies whether citations from the selected document should be used in the model's response. When set to true, the model can generate citations that reference the source documents used to inform the response.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Configuration settings for enabling and controlling document citations in Converse API responses. When enabled, the model can include citation information that links generated content back to specific source documents.

" + } + }, + "com.amazonaws.bedrockruntime#CitationsContentBlock": { + "type": "structure", + "members": { + "content": { + "target": "com.amazonaws.bedrockruntime#CitationGeneratedContentList", + "traits": { + "smithy.api#documentation": "

The generated content that is supported by the associated citations.

" + } + }, + "citations": { + "target": "com.amazonaws.bedrockruntime#Citations", + "traits": { + "smithy.api#documentation": "

An array of citations that reference the source documents used to generate the associated content.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A content block that contains both generated text and associated citation information. This block type is returned when document citations are enabled, providing traceability between the generated content and the source documents that informed the response.

" + } + }, + "com.amazonaws.bedrockruntime#CitationsDelta": { + "type": "structure", + "members": { + "title": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The title or identifier of the source document being cited.

" + } + }, + "source": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The source from the original search result that provided the cited content.

" + } + }, + "sourceContent": { + "target": "com.amazonaws.bedrockruntime#CitationSourceContentListDelta", + "traits": { + "smithy.api#documentation": "

The specific content from the source document that was referenced or cited in the generated response.

" + } + }, + "location": { + "target": "com.amazonaws.bedrockruntime#CitationLocation", + "traits": { + "smithy.api#documentation": "

Specifies the precise location within a source document where cited content can be found. This can include character-level positions, page numbers, or document chunks depending on the document type and indexing method.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains incremental updates to citation information during streaming responses. This allows clients to build up citation data progressively as the response is generated.

" + } + }, + "com.amazonaws.bedrockruntime#ConflictException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

Error occurred because of a conflict while performing an operation.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.bedrockruntime#ContentBlock": { + "type": "union", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

Text to include in the message.

" + } + }, + "image": { + "target": "com.amazonaws.bedrockruntime#ImageBlock", + "traits": { + "smithy.api#documentation": "

Image to include in the message.

This field is only supported by Anthropic Claude 3 models.

" + } + }, + "document": { + "target": "com.amazonaws.bedrockruntime#DocumentBlock", + "traits": { + "smithy.api#documentation": "

A document to include in the message.

" + } + }, + "video": { + "target": "com.amazonaws.bedrockruntime#VideoBlock", + "traits": { + "smithy.api#documentation": "

Video to include in the message.

" + } + }, + "audio": { + "target": "com.amazonaws.bedrockruntime#AudioBlock", + "traits": { + "smithy.api#documentation": "

An audio content block containing audio data in the conversation.

" + } + }, + "toolUse": { + "target": "com.amazonaws.bedrockruntime#ToolUseBlock", + "traits": { + "smithy.api#documentation": "

Information about a tool use request from a model.

" + } + }, + "toolResult": { + "target": "com.amazonaws.bedrockruntime#ToolResultBlock", + "traits": { + "smithy.api#documentation": "

The result for a tool request that a model makes.

" + } + }, + "guardContent": { + "target": "com.amazonaws.bedrockruntime#GuardrailConverseContentBlock", + "traits": { + "smithy.api#documentation": "

Contains the content to assess with the guardrail. If you don't specify guardContent in a call to the Converse API, the guardrail (if passed in the Converse API) assesses the entire message.

For more information, see Use a guardrail with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "cachePoint": { + "target": "com.amazonaws.bedrockruntime#CachePointBlock", + "traits": { + "smithy.api#documentation": "

CachePoint to include in the message.

" + } + }, + "reasoningContent": { + "target": "com.amazonaws.bedrockruntime#ReasoningContentBlock", + "traits": { + "smithy.api#documentation": "

Contains content regarding the reasoning that is carried out by the model. Reasoning refers to a Chain of Thought (CoT) that the model generates to enhance the accuracy of its final response.

" + } + }, + "citationsContent": { + "target": "com.amazonaws.bedrockruntime#CitationsContentBlock", + "traits": { + "smithy.api#documentation": "

A content block that contains both generated text and associated citation information, providing traceability between the response and source documents.

" + } + }, + "searchResult": { + "target": "com.amazonaws.bedrockruntime#SearchResultBlock", + "traits": { + "smithy.api#documentation": "

Search result to include in the message.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A block of content for a message that you pass to, or receive from, a model with the Converse or ConverseStream API operations.

" + } + }, + "com.amazonaws.bedrockruntime#ContentBlockDelta": { + "type": "union", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The content text.

" + } + }, + "toolUse": { + "target": "com.amazonaws.bedrockruntime#ToolUseBlockDelta", + "traits": { + "smithy.api#documentation": "

Information about a tool that the model is requesting to use.

" + } + }, + "toolResult": { + "target": "com.amazonaws.bedrockruntime#ToolResultBlocksDelta", + "traits": { + "smithy.api#documentation": "

An incremental update that contains the results from a tool call.

" + } + }, + "reasoningContent": { + "target": "com.amazonaws.bedrockruntime#ReasoningContentBlockDelta", + "traits": { + "smithy.api#documentation": "

Contains content regarding the reasoning that is carried out by the model. Reasoning refers to a Chain of Thought (CoT) that the model generates to enhance the accuracy of its final response.

" + } + }, + "citation": { + "target": "com.amazonaws.bedrockruntime#CitationsDelta", + "traits": { + "smithy.api#documentation": "

Incremental citation information that is streamed as part of the response generation process.

" + } + }, + "image": { + "target": "com.amazonaws.bedrockruntime#ImageBlockDelta", + "traits": { + "smithy.api#documentation": "

A streaming delta event containing incremental image data.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A block of content in a streaming response.

" + } + }, + "com.amazonaws.bedrockruntime#ContentBlockDeltaEvent": { + "type": "structure", + "members": { + "delta": { + "target": "com.amazonaws.bedrockruntime#ContentBlockDelta", + "traits": { + "smithy.api#documentation": "

The delta for a content block delta event.

", + "smithy.api#required": {} + } + }, + "contentBlockIndex": { + "target": "com.amazonaws.bedrockruntime#NonNegativeInteger", + "traits": { + "smithy.api#documentation": "

The block index for a content block delta event.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The content block delta event.

" + } + }, + "com.amazonaws.bedrockruntime#ContentBlockStart": { + "type": "union", + "members": { + "toolUse": { + "target": "com.amazonaws.bedrockruntime#ToolUseBlockStart", + "traits": { + "smithy.api#documentation": "

Information about a tool that the model is requesting to use.

" + } + }, + "toolResult": { + "target": "com.amazonaws.bedrockruntime#ToolResultBlockStart", + "traits": { + "smithy.api#documentation": "

The

" + } + }, + "image": { + "target": "com.amazonaws.bedrockruntime#ImageBlockStart", + "traits": { + "smithy.api#documentation": "

The initial event indicating the start of a streaming image block.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Content block start information.

" + } + }, + "com.amazonaws.bedrockruntime#ContentBlockStartEvent": { + "type": "structure", + "members": { + "start": { + "target": "com.amazonaws.bedrockruntime#ContentBlockStart", + "traits": { + "smithy.api#documentation": "

Start information about a content block start event.

", + "smithy.api#required": {} + } + }, + "contentBlockIndex": { + "target": "com.amazonaws.bedrockruntime#NonNegativeInteger", + "traits": { + "smithy.api#documentation": "

The index for a content block start event.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Content block start event.

" + } + }, + "com.amazonaws.bedrockruntime#ContentBlockStopEvent": { + "type": "structure", + "members": { + "contentBlockIndex": { + "target": "com.amazonaws.bedrockruntime#NonNegativeInteger", + "traits": { + "smithy.api#documentation": "

The index for a content block.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A content block stop event.

" + } + }, + "com.amazonaws.bedrockruntime#ContentBlocks": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#ContentBlock" + } + }, + "com.amazonaws.bedrockruntime#ConversationRole": { + "type": "enum", + "members": { + "USER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "user" + } + }, + "ASSISTANT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "assistant" + } + } + } + }, + "com.amazonaws.bedrockruntime#ConversationalModelId": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:(([0-9]{12}:custom-model/[a-z0-9-]{1,63}[.]{1}[a-z0-9-]{1,63}/[a-z0-9]{12})|(:foundation-model/[a-z0-9-]{1,63}[.]{1}[a-z0-9-]{1,63}([.:]?[a-z0-9-]{1,63}))|([0-9]{12}:imported-model/[a-z0-9]{12})|([0-9]{12}:provisioned-model/[a-z0-9]{12})|([0-9]{12}:custom-model-deployment/[a-z0-9]{12})|([0-9]{12}:(inference-profile|application-inference-profile)/[a-zA-Z0-9-:.]+)))|([a-z0-9-]{1,63}[.]{1}[a-z0-9-]{1,63}([.:]?[a-z0-9-]{1,63}))|(([0-9a-zA-Z][_-]?)+)|([a-zA-Z0-9-:.]+)|(^(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:[0-9]{12}:prompt/[0-9a-zA-Z]{10}(?::[0-9]{1,5})?))$|(^arn:aws:sagemaker:[a-z0-9-]+:[0-9]{12}:endpoint/[a-zA-Z0-9-]+$)|(^arn:aws(-[^:]+)?:bedrock:([0-9a-z-]{1,20}):([0-9]{12}):(default-)?prompt-router/[a-zA-Z0-9-:.]+$)$" + } + }, + "com.amazonaws.bedrockruntime#Converse": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#ConverseRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#ConverseResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelErrorException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelNotReadyException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelTimeoutException" + }, + { + "target": "com.amazonaws.bedrockruntime#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Sends messages to the specified Amazon Bedrock model. Converse provides a consistent interface that works with all models that support messages. This allows you to write code once and use it with different models. If a model has unique inference parameters, you can also pass those unique parameters to the model.

Amazon Bedrock doesn't store any text, images, or documents that you provide as content. The data is only used to generate the response.

You can submit a prompt by including it in the messages field, specifying the modelId of a foundation model or inference profile to run inference on it, and including any other fields that are relevant to your use case.

You can also submit a prompt from Prompt management by specifying the ARN of the prompt version and including a map of variables to values in the promptVariables field. You can append more messages to the prompt by using the messages field. If you use a prompt from Prompt management, you can't include the following fields in the request: additionalModelRequestFields, inferenceConfig, system, or toolConfig. Instead, these fields must be defined through Prompt management. For more information, see Use a prompt from Prompt management.

For information about the Converse API, see Use the Converse API in the Amazon Bedrock User Guide. To use a guardrail, see Use a guardrail with the Converse API in the Amazon Bedrock User Guide. To use a tool with a model, see Tool use (Function calling) in the Amazon Bedrock User Guide

For example code, see Converse API examples in the Amazon Bedrock User Guide.

This operation requires permission for the bedrock:InvokeModel action.

To deny all inference access to resources that you specify in the modelId field, you need to deny access to the bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream actions. Doing this also denies access to the resource through the base inference actions (InvokeModel and InvokeModelWithResponseStream). For more information see Deny access for inference on specific models.

For troubleshooting some of the common errors you might encounter when using the Converse API, see Troubleshooting Amazon Bedrock API Error Codes in the Amazon Bedrock User Guide

", + "smithy.api#http": { + "code": 200, + "method": "POST", + "uri": "/model/{modelId}/converse" + } + } + }, + "com.amazonaws.bedrockruntime#ConverseMetrics": { + "type": "structure", + "members": { + "latencyMs": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#documentation": "

The latency of the call to Converse, in milliseconds.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Metrics for a call to Converse.

" + } + }, + "com.amazonaws.bedrockruntime#ConverseOutput": { + "type": "union", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#Message", + "traits": { + "smithy.api#documentation": "

The message that the model generates.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The output from a call to Converse.

" + } + }, + "com.amazonaws.bedrockruntime#ConverseRequest": { + "type": "structure", + "members": { + "modelId": { + "target": "com.amazonaws.bedrockruntime#ConversationalModelId", + "traits": { + "smithy.api#documentation": "

Specifies the model or throughput with which to run inference, or the prompt resource to use in inference. The value depends on the resource that you use:

The Converse API doesn't support imported models.

", + "smithy.api#httpLabel": {}, + "smithy.api#required": {} + } + }, + "messages": { + "target": "com.amazonaws.bedrockruntime#Messages", + "traits": { + "smithy.api#documentation": "

The messages that you want to send to the model.

" + } + }, + "system": { + "target": "com.amazonaws.bedrockruntime#SystemContentBlocks", + "traits": { + "smithy.api#documentation": "

A prompt that provides instructions or context to the model about the task it should perform, or the persona it should adopt during the conversation.

" + } + }, + "inferenceConfig": { + "target": "com.amazonaws.bedrockruntime#InferenceConfiguration", + "traits": { + "smithy.api#documentation": "

Inference parameters to pass to the model. Converse and ConverseStream support a base set of inference parameters. If you need to pass additional parameters that the model supports, use the additionalModelRequestFields request field.

" + } + }, + "toolConfig": { + "target": "com.amazonaws.bedrockruntime#ToolConfiguration", + "traits": { + "smithy.api#documentation": "

Configuration information for the tools that the model can use when generating a response.

For information about models that support tool use, see Supported models and model features.

" + } + }, + "guardrailConfig": { + "target": "com.amazonaws.bedrockruntime#GuardrailConfiguration", + "traits": { + "smithy.api#documentation": "

Configuration information for a guardrail that you want to use in the request. If you include guardContent blocks in the content field in the messages field, the guardrail operates only on those messages. If you include no guardContent blocks, the guardrail operates on all messages in the request body and in any included prompt resource.

" + } + }, + "additionalModelRequestFields": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "

Additional inference parameters that the model supports, beyond the base set of inference parameters that Converse and ConverseStream support in the inferenceConfig field. For more information, see Model parameters.

" + } + }, + "promptVariables": { + "target": "com.amazonaws.bedrockruntime#PromptVariableMap", + "traits": { + "smithy.api#documentation": "

Contains a map of variables in a prompt from Prompt management to objects containing the values to fill in for them when running model invocation. This field is ignored if you don't specify a prompt resource in the modelId field.

" + } + }, + "additionalModelResponseFieldPaths": { + "target": "com.amazonaws.bedrockruntime#AdditionalModelResponseFieldPaths", + "traits": { + "smithy.api#documentation": "

Additional model parameters field paths to return in the response. Converse and ConverseStream return the requested fields as a JSON Pointer object in the additionalModelResponseFields field. The following is example JSON for additionalModelResponseFieldPaths.

[ \"/stop_sequence\" ]

For information about the JSON Pointer syntax, see the Internet Engineering Task Force (IETF) documentation.

Converse and ConverseStream reject an empty JSON Pointer or incorrectly structured JSON Pointer with a 400 error code. if the JSON Pointer is valid, but the requested field is not in the model response, it is ignored by Converse.

", + "smithy.api#length": { + "max": 10 + } + } + }, + "requestMetadata": { + "target": "com.amazonaws.bedrockruntime#RequestMetadata", + "traits": { + "smithy.api#documentation": "

Key-value pairs that you can use to filter invocation logs.

" + } + }, + "performanceConfig": { + "target": "com.amazonaws.bedrockruntime#PerformanceConfiguration", + "traits": { + "smithy.api#documentation": "

Model performance settings for the request.

" + } + }, + "serviceTier": { + "target": "com.amazonaws.bedrockruntime#ServiceTier", + "traits": { + "smithy.api#documentation": "

Specifies the processing tier configuration used for serving the request.

" + } + }, + "outputConfig": { + "target": "com.amazonaws.bedrockruntime#OutputConfig", + "traits": { + "smithy.api#documentation": "

Output configuration for a model response.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#ConverseResponse": { + "type": "structure", + "members": { + "output": { + "target": "com.amazonaws.bedrockruntime#ConverseOutput", + "traits": { + "smithy.api#documentation": "

The result from the call to Converse.

", + "smithy.api#required": {} + } + }, + "stopReason": { + "target": "com.amazonaws.bedrockruntime#StopReason", + "traits": { + "smithy.api#documentation": "

The reason why the model stopped generating output.

", + "smithy.api#required": {} + } + }, + "usage": { + "target": "com.amazonaws.bedrockruntime#TokenUsage", + "traits": { + "smithy.api#documentation": "

The total number of tokens used in the call to Converse. The total includes the tokens input to the model and the tokens generated by the model.

", + "smithy.api#required": {} + } + }, + "metrics": { + "target": "com.amazonaws.bedrockruntime#ConverseMetrics", + "traits": { + "smithy.api#documentation": "

Metrics for the call to Converse.

", + "smithy.api#required": {} + } + }, + "additionalModelResponseFields": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "

Additional fields in the response that are unique to the model.

" + } + }, + "trace": { + "target": "com.amazonaws.bedrockruntime#ConverseTrace", + "traits": { + "smithy.api#documentation": "

A trace object that contains information about the Guardrail behavior.

" + } + }, + "performanceConfig": { + "target": "com.amazonaws.bedrockruntime#PerformanceConfiguration", + "traits": { + "smithy.api#documentation": "

Model performance settings for the request.

" + } + }, + "serviceTier": { + "target": "com.amazonaws.bedrockruntime#ServiceTier", + "traits": { + "smithy.api#documentation": "

Specifies the processing tier configuration used for serving the request.

" + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#ConverseStream": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#ConverseStreamRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#ConverseStreamResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelErrorException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelNotReadyException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelTimeoutException" + }, + { + "target": "com.amazonaws.bedrockruntime#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Sends messages to the specified Amazon Bedrock model and returns the response in a stream. ConverseStream provides a consistent API that works with all Amazon Bedrock models that support messages. This allows you to write code once and use it with different models. Should a model have unique inference parameters, you can also pass those unique parameters to the model.

To find out if a model supports streaming, call GetFoundationModel and check the responseStreamingSupported field in the response.

The CLI doesn't support streaming operations in Amazon Bedrock, including ConverseStream.

Amazon Bedrock doesn't store any text, images, or documents that you provide as content. The data is only used to generate the response.

You can submit a prompt by including it in the messages field, specifying the modelId of a foundation model or inference profile to run inference on it, and including any other fields that are relevant to your use case.

You can also submit a prompt from Prompt management by specifying the ARN of the prompt version and including a map of variables to values in the promptVariables field. You can append more messages to the prompt by using the messages field. If you use a prompt from Prompt management, you can't include the following fields in the request: additionalModelRequestFields, inferenceConfig, system, or toolConfig. Instead, these fields must be defined through Prompt management. For more information, see Use a prompt from Prompt management.

For information about the Converse API, see Use the Converse API in the Amazon Bedrock User Guide. To use a guardrail, see Use a guardrail with the Converse API in the Amazon Bedrock User Guide. To use a tool with a model, see Tool use (Function calling) in the Amazon Bedrock User Guide

For example code, see Conversation streaming example in the Amazon Bedrock User Guide.

This operation requires permission for the bedrock:InvokeModelWithResponseStream action.

To deny all inference access to resources that you specify in the modelId field, you need to deny access to the bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream actions. Doing this also denies access to the resource through the base inference actions (InvokeModel and InvokeModelWithResponseStream). For more information see Deny access for inference on specific models.

For troubleshooting some of the common errors you might encounter when using the ConverseStream API, see Troubleshooting Amazon Bedrock API Error Codes in the Amazon Bedrock User Guide

", + "smithy.api#http": { + "code": 200, + "method": "POST", + "uri": "/model/{modelId}/converse-stream" + } + } + }, + "com.amazonaws.bedrockruntime#ConverseStreamMetadataEvent": { + "type": "structure", + "members": { + "usage": { + "target": "com.amazonaws.bedrockruntime#TokenUsage", + "traits": { + "smithy.api#documentation": "

Usage information for the conversation stream event.

", + "smithy.api#required": {} + } + }, + "metrics": { + "target": "com.amazonaws.bedrockruntime#ConverseStreamMetrics", + "traits": { + "smithy.api#documentation": "

The metrics for the conversation stream metadata event.

", + "smithy.api#required": {} + } + }, + "trace": { + "target": "com.amazonaws.bedrockruntime#ConverseStreamTrace", + "traits": { + "smithy.api#documentation": "

The trace object in the response from ConverseStream that contains information about the guardrail behavior.

" + } + }, + "performanceConfig": { + "target": "com.amazonaws.bedrockruntime#PerformanceConfiguration", + "traits": { + "smithy.api#documentation": "

Model performance configuration metadata for the conversation stream event.

" + } + }, + "serviceTier": { + "target": "com.amazonaws.bedrockruntime#ServiceTier", + "traits": { + "smithy.api#documentation": "

Specifies the processing tier configuration used for serving the request.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A conversation stream metadata event.

" + } + }, + "com.amazonaws.bedrockruntime#ConverseStreamMetrics": { + "type": "structure", + "members": { + "latencyMs": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#documentation": "

The latency for the streaming request, in milliseconds.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Metrics for the stream.

" + } + }, + "com.amazonaws.bedrockruntime#ConverseStreamOutput": { + "type": "union", + "members": { + "messageStart": { + "target": "com.amazonaws.bedrockruntime#MessageStartEvent", + "traits": { + "smithy.api#documentation": "

Message start information.

" + } + }, + "contentBlockStart": { + "target": "com.amazonaws.bedrockruntime#ContentBlockStartEvent", + "traits": { + "smithy.api#documentation": "

Start information for a content block.

" + } + }, + "contentBlockDelta": { + "target": "com.amazonaws.bedrockruntime#ContentBlockDeltaEvent", + "traits": { + "smithy.api#documentation": "

The messages output content block delta.

" + } + }, + "contentBlockStop": { + "target": "com.amazonaws.bedrockruntime#ContentBlockStopEvent", + "traits": { + "smithy.api#documentation": "

Stop information for a content block.

" + } + }, + "messageStop": { + "target": "com.amazonaws.bedrockruntime#MessageStopEvent", + "traits": { + "smithy.api#documentation": "

Message stop information.

" + } + }, + "metadata": { + "target": "com.amazonaws.bedrockruntime#ConverseStreamMetadataEvent", + "traits": { + "smithy.api#documentation": "

Metadata for the converse output stream.

" + } + }, + "internalServerException": { + "target": "com.amazonaws.bedrockruntime#InternalServerException", + "traits": { + "smithy.api#documentation": "

An internal server error occurred. Retry your request.

" + } + }, + "modelStreamErrorException": { + "target": "com.amazonaws.bedrockruntime#ModelStreamErrorException", + "traits": { + "smithy.api#documentation": "

A streaming error occurred. Retry your request.

" + } + }, + "validationException": { + "target": "com.amazonaws.bedrockruntime#ValidationException", + "traits": { + "smithy.api#documentation": "

The input fails to satisfy the constraints specified by Amazon Bedrock. For troubleshooting this error, see ValidationError in the Amazon Bedrock User Guide.

" + } + }, + "throttlingException": { + "target": "com.amazonaws.bedrockruntime#ThrottlingException", + "traits": { + "smithy.api#documentation": "

Your request was denied due to exceeding the account quotas for Amazon Bedrock. For troubleshooting this error, see ThrottlingException in the Amazon Bedrock User Guide.

" + } + }, + "serviceUnavailableException": { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException", + "traits": { + "smithy.api#documentation": "

The service isn't currently available. For troubleshooting this error, see ServiceUnavailable in the Amazon Bedrock User Guide

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The messages output stream

", + "smithy.api#streaming": {} + } + }, + "com.amazonaws.bedrockruntime#ConverseStreamRequest": { + "type": "structure", + "members": { + "modelId": { + "target": "com.amazonaws.bedrockruntime#ConversationalModelId", + "traits": { + "smithy.api#documentation": "

Specifies the model or throughput with which to run inference, or the prompt resource to use in inference. The value depends on the resource that you use:

The Converse API doesn't support imported models.

", + "smithy.api#httpLabel": {}, + "smithy.api#required": {} + } + }, + "messages": { + "target": "com.amazonaws.bedrockruntime#Messages", + "traits": { + "smithy.api#documentation": "

The messages that you want to send to the model.

" + } + }, + "system": { + "target": "com.amazonaws.bedrockruntime#SystemContentBlocks", + "traits": { + "smithy.api#documentation": "

A prompt that provides instructions or context to the model about the task it should perform, or the persona it should adopt during the conversation.

" + } + }, + "inferenceConfig": { + "target": "com.amazonaws.bedrockruntime#InferenceConfiguration", + "traits": { + "smithy.api#documentation": "

Inference parameters to pass to the model. Converse and ConverseStream support a base set of inference parameters. If you need to pass additional parameters that the model supports, use the additionalModelRequestFields request field.

" + } + }, + "toolConfig": { + "target": "com.amazonaws.bedrockruntime#ToolConfiguration", + "traits": { + "smithy.api#documentation": "

Configuration information for the tools that the model can use when generating a response.

For information about models that support streaming tool use, see Supported models and model features.

" + } + }, + "guardrailConfig": { + "target": "com.amazonaws.bedrockruntime#GuardrailStreamConfiguration", + "traits": { + "smithy.api#documentation": "

Configuration information for a guardrail that you want to use in the request. If you include guardContent blocks in the content field in the messages field, the guardrail operates only on those messages. If you include no guardContent blocks, the guardrail operates on all messages in the request body and in any included prompt resource.

" + } + }, + "additionalModelRequestFields": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "

Additional inference parameters that the model supports, beyond the base set of inference parameters that Converse and ConverseStream support in the inferenceConfig field. For more information, see Model parameters.

" + } + }, + "promptVariables": { + "target": "com.amazonaws.bedrockruntime#PromptVariableMap", + "traits": { + "smithy.api#documentation": "

Contains a map of variables in a prompt from Prompt management to objects containing the values to fill in for them when running model invocation. This field is ignored if you don't specify a prompt resource in the modelId field.

" + } + }, + "additionalModelResponseFieldPaths": { + "target": "com.amazonaws.bedrockruntime#AdditionalModelResponseFieldPaths", + "traits": { + "smithy.api#documentation": "

Additional model parameters field paths to return in the response. Converse and ConverseStream return the requested fields as a JSON Pointer object in the additionalModelResponseFields field. The following is example JSON for additionalModelResponseFieldPaths.

[ \"/stop_sequence\" ]

For information about the JSON Pointer syntax, see the Internet Engineering Task Force (IETF) documentation.

Converse and ConverseStream reject an empty JSON Pointer or incorrectly structured JSON Pointer with a 400 error code. if the JSON Pointer is valid, but the requested field is not in the model response, it is ignored by Converse.

", + "smithy.api#length": { + "max": 10 + } + } + }, + "requestMetadata": { + "target": "com.amazonaws.bedrockruntime#RequestMetadata", + "traits": { + "smithy.api#documentation": "

Key-value pairs that you can use to filter invocation logs.

" + } + }, + "performanceConfig": { + "target": "com.amazonaws.bedrockruntime#PerformanceConfiguration", + "traits": { + "smithy.api#documentation": "

Model performance settings for the request.

" + } + }, + "serviceTier": { + "target": "com.amazonaws.bedrockruntime#ServiceTier", + "traits": { + "smithy.api#documentation": "

Specifies the processing tier configuration used for serving the request.

" + } + }, + "outputConfig": { + "target": "com.amazonaws.bedrockruntime#OutputConfig", + "traits": { + "smithy.api#documentation": "

Output configuration for a model response.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#ConverseStreamResponse": { + "type": "structure", + "members": { + "stream": { + "target": "com.amazonaws.bedrockruntime#ConverseStreamOutput", + "traits": { + "smithy.api#documentation": "

The output stream that the model generated.

", + "smithy.api#httpPayload": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#ConverseStreamTrace": { + "type": "structure", + "members": { + "guardrail": { + "target": "com.amazonaws.bedrockruntime#GuardrailTraceAssessment", + "traits": { + "smithy.api#documentation": "

The guardrail trace object.

" + } + }, + "promptRouter": { + "target": "com.amazonaws.bedrockruntime#PromptRouterTrace", + "traits": { + "smithy.api#documentation": "

The request's prompt router.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The trace object in a response from ConverseStream.

" + } + }, + "com.amazonaws.bedrockruntime#ConverseTokensRequest": { + "type": "structure", + "members": { + "messages": { + "target": "com.amazonaws.bedrockruntime#Messages", + "traits": { + "smithy.api#documentation": "

An array of messages to count tokens for.

" + } + }, + "system": { + "target": "com.amazonaws.bedrockruntime#SystemContentBlocks", + "traits": { + "smithy.api#documentation": "

The system content blocks to count tokens for. System content provides instructions or context to the model about how it should behave or respond. The token count will include any system content provided.

" + } + }, + "toolConfig": { + "target": "com.amazonaws.bedrockruntime#ToolConfiguration", + "traits": { + "smithy.api#documentation": "

The toolConfig of Converse input request to count tokens for. Configuration information for the tools that the model can use when generating a response.

" + } + }, + "additionalModelRequestFields": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "

The additionalModelRequestFields of Converse input request to count tokens for. Use this field when you want to pass additional parameters that the model supports.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The inputs from a Converse API request for token counting.

This structure mirrors the input format for the Converse operation, allowing you to count tokens for conversation-based inference requests.

" + } + }, + "com.amazonaws.bedrockruntime#ConverseTrace": { + "type": "structure", + "members": { + "guardrail": { + "target": "com.amazonaws.bedrockruntime#GuardrailTraceAssessment", + "traits": { + "smithy.api#documentation": "

The guardrail trace object.

" + } + }, + "promptRouter": { + "target": "com.amazonaws.bedrockruntime#PromptRouterTrace", + "traits": { + "smithy.api#documentation": "

The request's prompt router.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The trace object in a response from Converse.

" + } + }, + "com.amazonaws.bedrockruntime#CountTokens": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#CountTokensRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#CountTokensResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Returns the token count for a given inference request. This operation helps you estimate token usage before sending requests to foundation models by returning the token count that would be used if the same input were sent to the model in an inference request.

Token counting is model-specific because different models use different tokenization strategies. The token count returned by this operation will match the token count that would be charged if the same input were sent to the model in an InvokeModel or Converse request.

You can use this operation to:

  • Estimate costs before sending inference requests.

  • Optimize prompts to fit within token limits.

  • Plan for token usage in your applications.

This operation accepts the same input formats as InvokeModel and Converse, allowing you to count tokens for both raw text inputs and structured conversation formats.

The following operations are related to CountTokens:

  • InvokeModel - Sends inference requests to foundation models

  • Converse - Sends conversation-based inference requests to foundation models

", + "smithy.api#http": { + "code": 200, + "method": "POST", + "uri": "/model/{modelId}/count-tokens" + }, + "smithy.api#readonly": {} + } + }, + "com.amazonaws.bedrockruntime#CountTokensInput": { + "type": "union", + "members": { + "invokeModel": { + "target": "com.amazonaws.bedrockruntime#InvokeModelTokensRequest", + "traits": { + "smithy.api#documentation": "

An InvokeModel request for which to count tokens. Use this field when you want to count tokens for a raw text input that would be sent to the InvokeModel operation.

" + } + }, + "converse": { + "target": "com.amazonaws.bedrockruntime#ConverseTokensRequest", + "traits": { + "smithy.api#documentation": "

A Converse request for which to count tokens. Use this field when you want to count tokens for a conversation-based input that would be sent to the Converse operation.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The input value for token counting. The value should be either an InvokeModel or Converse request body.

" + } + }, + "com.amazonaws.bedrockruntime#CountTokensRequest": { + "type": "structure", + "members": { + "modelId": { + "target": "com.amazonaws.bedrockruntime#FoundationModelVersionIdentifier", + "traits": { + "smithy.api#documentation": "

The unique identifier or ARN of the foundation model to use for token counting. Each model processes tokens differently, so the token count is specific to the model you specify.

", + "smithy.api#httpLabel": {}, + "smithy.api#required": {} + } + }, + "input": { + "target": "com.amazonaws.bedrockruntime#CountTokensInput", + "traits": { + "smithy.api#documentation": "

The input for which to count tokens. The structure of this parameter depends on whether you're counting tokens for an InvokeModel or Converse request:

  • For InvokeModel requests, provide the request body in the invokeModel field

  • For Converse requests, provide the messages and system content in the converse field

The input format must be compatible with the model specified in the modelId parameter.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#CountTokensResponse": { + "type": "structure", + "members": { + "inputTokens": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The number of tokens in the provided input according to the specified model's tokenization rules. This count represents the number of input tokens that would be processed if the same input were sent to the model in an inference request. Use this value to estimate costs and ensure your inputs stay within model token limits.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#DocumentBlock": { + "type": "structure", + "members": { + "format": { + "target": "com.amazonaws.bedrockruntime#DocumentFormat", + "traits": { + "smithy.api#addedDefault": {}, + "smithy.api#default": "txt", + "smithy.api#documentation": "

The format of a document, or its extension.

" + } + }, + "name": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

A name for the document. The name can only contain the following characters:

  • Alphanumeric characters

  • Whitespace characters (no more than one in a row)

  • Hyphens

  • Parentheses

  • Square brackets

This field is vulnerable to prompt injections, because the model might inadvertently interpret it as instructions. Therefore, we recommend that you specify a neutral name.

", + "smithy.api#length": { + "min": 1, + "max": 200 + }, + "smithy.api#required": {} + } + }, + "source": { + "target": "com.amazonaws.bedrockruntime#DocumentSource", + "traits": { + "smithy.api#documentation": "

Contains the content of the document.

", + "smithy.api#required": {} + } + }, + "context": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

Contextual information about how the document should be processed or interpreted by the model when generating citations.

" + } + }, + "citations": { + "target": "com.amazonaws.bedrockruntime#CitationsConfig", + "traits": { + "smithy.api#documentation": "

Configuration settings that control how citations should be generated for this specific document.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A document to include in a message.

" + } + }, + "com.amazonaws.bedrockruntime#DocumentCharLocation": { + "type": "structure", + "members": { + "documentIndex": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The index of the document within the array of documents provided in the request.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "start": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The starting character position of the cited content within the document.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "end": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The ending character position of the cited content within the document.

", + "smithy.api#range": { + "min": 0 + } + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies a character-level location within a document, providing precise positioning information for cited content using start and end character indices.

" + } + }, + "com.amazonaws.bedrockruntime#DocumentChunkLocation": { + "type": "structure", + "members": { + "documentIndex": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The index of the document within the array of documents provided in the request.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "start": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The starting chunk identifier or index of the cited content within the document.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "end": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The ending chunk identifier or index of the cited content within the document.

", + "smithy.api#range": { + "min": 0 + } + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies a chunk-level location within a document, providing positioning information for cited content using logical document segments or chunks.

" + } + }, + "com.amazonaws.bedrockruntime#DocumentContentBlock": { + "type": "union", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The text content of the document.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains the actual content of a document that can be processed by the model and potentially cited in the response.

" + } + }, + "com.amazonaws.bedrockruntime#DocumentContentBlocks": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#DocumentContentBlock" + } + }, + "com.amazonaws.bedrockruntime#DocumentFormat": { + "type": "enum", + "members": { + "PDF": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "pdf" + } + }, + "CSV": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "csv" + } + }, + "DOC": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "doc" + } + }, + "DOCX": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "docx" + } + }, + "XLS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "xls" + } + }, + "XLSX": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "xlsx" + } + }, + "HTML": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "html" + } + }, + "TXT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "txt" + } + }, + "MD": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "md" + } + } + } + }, + "com.amazonaws.bedrockruntime#DocumentPageLocation": { + "type": "structure", + "members": { + "documentIndex": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The index of the document within the array of documents provided in the request.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "start": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The starting page number of the cited content within the document.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "end": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The ending page number of the cited content within the document.

", + "smithy.api#range": { + "min": 0 + } + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies a page-level location within a document, providing positioning information for cited content using page numbers.

" + } + }, + "com.amazonaws.bedrockruntime#DocumentSource": { + "type": "union", + "members": { + "bytes": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#documentation": "

The raw bytes for the document. If you use an Amazon Web Services SDK, you don't need to encode the bytes in base64.

", + "smithy.api#length": { + "min": 1 + } + } + }, + "s3Location": { + "target": "com.amazonaws.bedrockruntime#S3Location", + "traits": { + "smithy.api#documentation": "

The location of a document object in an Amazon S3 bucket. To see which models support S3 uploads, see Supported models and features for Converse.

" + } + }, + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The text content of the document source.

" + } + }, + "content": { + "target": "com.amazonaws.bedrockruntime#DocumentContentBlocks", + "traits": { + "smithy.api#documentation": "

The structured content of the document source, which may include various content blocks such as text, images, or other document elements.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains the content of a document.

" + } + }, + "com.amazonaws.bedrockruntime#ErrorBlock": { + "type": "structure", + "members": { + "message": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

A human-readable error message describing what went wrong during content processing.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A block containing error information when content processing fails.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#FoundationModelVersionIdentifier": { + "type": "string", + "traits": { + "smithy.api#documentation": "ARN or ID of a Bedrock model", + "smithy.api#length": { + "min": 1, + "max": 256 + }, + "smithy.api#pattern": "^[a-zA-Z_\\.\\-/0-9:]+$" + } + }, + "com.amazonaws.bedrockruntime#GetAsyncInvoke": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#GetAsyncInvokeRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#GetAsyncInvokeResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Retrieve information about an asynchronous invocation.

", + "smithy.api#http": { + "code": 200, + "method": "GET", + "uri": "/async-invoke/{invocationArn}" + }, + "smithy.api#readonly": {} + } + }, + "com.amazonaws.bedrockruntime#GetAsyncInvokeRequest": { + "type": "structure", + "members": { + "invocationArn": { + "target": "com.amazonaws.bedrockruntime#InvocationArn", + "traits": { + "smithy.api#documentation": "

The invocation's ARN.

", + "smithy.api#httpLabel": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#GetAsyncInvokeResponse": { + "type": "structure", + "members": { + "invocationArn": { + "target": "com.amazonaws.bedrockruntime#InvocationArn", + "traits": { + "smithy.api#documentation": "

The invocation's ARN.

", + "smithy.api#required": {} + } + }, + "modelArn": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeArn", + "traits": { + "smithy.api#documentation": "

The invocation's model ARN.

", + "smithy.api#required": {} + } + }, + "clientRequestToken": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeIdempotencyToken", + "traits": { + "smithy.api#documentation": "

The invocation's idempotency token.

" + } + }, + "status": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeStatus", + "traits": { + "smithy.api#documentation": "

The invocation's status.

", + "smithy.api#required": {} + } + }, + "failureMessage": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeMessage", + "traits": { + "smithy.api#documentation": "

An error message.

" + } + }, + "submitTime": { + "target": "com.amazonaws.bedrockruntime#Timestamp", + "traits": { + "smithy.api#documentation": "

When the invocation request was submitted.

", + "smithy.api#required": {} + } + }, + "lastModifiedTime": { + "target": "com.amazonaws.bedrockruntime#Timestamp", + "traits": { + "smithy.api#documentation": "

The invocation's last modified time.

" + } + }, + "endTime": { + "target": "com.amazonaws.bedrockruntime#Timestamp", + "traits": { + "smithy.api#documentation": "

When the invocation ended.

" + } + }, + "outputDataConfig": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeOutputDataConfig", + "traits": { + "smithy.api#documentation": "

Output data settings.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#GuardrailAction": { + "type": "enum", + "members": { + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + }, + "GUARDRAIL_INTERVENED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "GUARDRAIL_INTERVENED" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailArn": { + "type": "string", + "traits": { + "smithy.api#length": { + "max": 2048 + }, + "smithy.api#pattern": "^(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:[0-9]{12}:guardrail/[a-z0-9]+)$" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAssessment": { + "type": "structure", + "members": { + "topicPolicy": { + "target": "com.amazonaws.bedrockruntime#GuardrailTopicPolicyAssessment", + "traits": { + "smithy.api#documentation": "

The topic policy.

" + } + }, + "contentPolicy": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentPolicyAssessment", + "traits": { + "smithy.api#documentation": "

The content policy.

" + } + }, + "wordPolicy": { + "target": "com.amazonaws.bedrockruntime#GuardrailWordPolicyAssessment", + "traits": { + "smithy.api#documentation": "

The word policy.

" + } + }, + "sensitiveInformationPolicy": { + "target": "com.amazonaws.bedrockruntime#GuardrailSensitiveInformationPolicyAssessment", + "traits": { + "smithy.api#documentation": "

The sensitive information policy.

" + } + }, + "contextualGroundingPolicy": { + "target": "com.amazonaws.bedrockruntime#GuardrailContextualGroundingPolicyAssessment", + "traits": { + "smithy.api#documentation": "

The contextual grounding policy used for the guardrail assessment.

" + } + }, + "automatedReasoningPolicy": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningPolicyAssessment", + "traits": { + "smithy.api#documentation": "

The automated reasoning policy assessment results, including logical validation findings for the input content.

" + } + }, + "invocationMetrics": { + "target": "com.amazonaws.bedrockruntime#GuardrailInvocationMetrics", + "traits": { + "smithy.api#documentation": "

The invocation metrics for the guardrail assessment.

" + } + }, + "appliedGuardrailDetails": { + "target": "com.amazonaws.bedrockruntime#AppliedGuardrailDetails" + } + }, + "traits": { + "smithy.api#documentation": "

A behavior assessment of the guardrail policies used in a call to the Converse API.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAssessmentList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailAssessment" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAssessmentListMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "com.amazonaws.bedrockruntime#GuardrailAssessmentList" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAssessmentMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "com.amazonaws.bedrockruntime#GuardrailAssessment" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningDifferenceScenarioList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningScenario" + }, + "traits": { + "smithy.api#length": { + "max": 2 + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningFinding": { + "type": "union", + "members": { + "valid": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningValidFinding", + "traits": { + "smithy.api#documentation": "

Contains the result when the automated reasoning evaluation determines that the claims in the input are logically valid and definitively true based on the provided premises and policy rules.

" + } + }, + "invalid": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningInvalidFinding", + "traits": { + "smithy.api#documentation": "

Contains the result when the automated reasoning evaluation determines that the claims in the input are logically invalid and contradict the established premises or policy rules.

" + } + }, + "satisfiable": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningSatisfiableFinding", + "traits": { + "smithy.api#documentation": "

Contains the result when the automated reasoning evaluation determines that the claims in the input could be either true or false depending on additional assumptions not provided in the input context.

" + } + }, + "impossible": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningImpossibleFinding", + "traits": { + "smithy.api#documentation": "

Contains the result when the automated reasoning evaluation determines that no valid logical conclusions can be drawn due to contradictions in the premises or policy rules themselves.

" + } + }, + "translationAmbiguous": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationAmbiguousFinding", + "traits": { + "smithy.api#documentation": "

Contains the result when the automated reasoning evaluation detects that the input has multiple valid logical interpretations, requiring additional context or clarification to proceed with validation.

" + } + }, + "tooComplex": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTooComplexFinding", + "traits": { + "smithy.api#documentation": "

Contains the result when the automated reasoning evaluation cannot process the input due to its complexity or volume exceeding the system's processing capacity for logical analysis.

" + } + }, + "noTranslations": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningNoTranslationsFinding", + "traits": { + "smithy.api#documentation": "

Contains the result when the automated reasoning evaluation cannot extract any relevant logical information from the input that can be validated against the policy rules.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents a logical validation result from automated reasoning policy evaluation. The finding indicates whether claims in the input are logically valid, invalid, satisfiable, impossible, or have other logical issues.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningFindingList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningFinding" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningImpossibleFinding": { + "type": "structure", + "members": { + "translation": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslation", + "traits": { + "smithy.api#documentation": "

The logical translation of the input that this finding evaluates.

" + } + }, + "contradictingRules": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningRuleList", + "traits": { + "smithy.api#documentation": "

The automated reasoning policy rules that contradict the claims and/or premises in the input.

" + } + }, + "logicWarning": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningLogicWarning", + "traits": { + "smithy.api#documentation": "

Indication of a logic issue with the translation without needing to consider the automated reasoning policy rules.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that no valid claims can be made due to logical contradictions in the premises or rules.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningInputTextReference": { + "type": "structure", + "members": { + "text": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementNaturalLanguageContent", + "traits": { + "smithy.api#documentation": "

The specific text from the original input that this reference points to.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

References a portion of the original input text that corresponds to logical elements.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningInputTextReferenceList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningInputTextReference" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningInvalidFinding": { + "type": "structure", + "members": { + "translation": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslation", + "traits": { + "smithy.api#documentation": "

The logical translation of the input that this finding invalidates.

" + } + }, + "contradictingRules": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningRuleList", + "traits": { + "smithy.api#documentation": "

The automated reasoning policy rules that contradict the claims in the input.

" + } + }, + "logicWarning": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningLogicWarning", + "traits": { + "smithy.api#documentation": "

Indication of a logic issue with the translation without needing to consider the automated reasoning policy rules.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the claims are logically false and contradictory to the established rules or premises.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningLogicWarning": { + "type": "structure", + "members": { + "type": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningLogicWarningType", + "traits": { + "smithy.api#documentation": "

The category of the detected logical issue, such as statements that are always true or always false.

" + } + }, + "premises": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementList", + "traits": { + "smithy.api#documentation": "

The logical statements that serve as premises under which the claims are validated.

" + } + }, + "claims": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementList", + "traits": { + "smithy.api#documentation": "

The logical statements that are validated while assuming the policy and premises.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Identifies logical issues in the translated statements that exist independent of any policy rules, such as statements that are always true or always false.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningLogicWarningType": { + "type": "enum", + "members": { + "ALWAYS_FALSE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ALWAYS_FALSE" + } + }, + "ALWAYS_TRUE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ALWAYS_TRUE" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningNoTranslationsFinding": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "

Indicates that no relevant logical information could be extracted from the input for validation.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningPoliciesProcessed": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningPolicyAssessment": { + "type": "structure", + "members": { + "findings": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningFindingList", + "traits": { + "smithy.api#documentation": "

List of logical validation results produced by evaluating the input content against automated reasoning policies.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains the results of automated reasoning policy evaluation, including logical findings about the validity of claims made in the input content.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningPolicyUnitsProcessed": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningPolicyVersionArn": { + "type": "string", + "traits": { + "smithy.api#length": { + "max": 2048 + }, + "smithy.api#pattern": "^arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:[0-9]{12}:automated-reasoning-policy/[a-z0-9]{12}(:([1-9][0-9]{0,11}))?$" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningRule": { + "type": "structure", + "members": { + "identifier": { + "target": "com.amazonaws.bedrockruntime#AutomatedReasoningRuleIdentifier", + "traits": { + "smithy.api#documentation": "

The unique identifier of the automated reasoning rule.

" + } + }, + "policyVersionArn": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningPolicyVersionArn", + "traits": { + "smithy.api#documentation": "

The ARN of the automated reasoning policy version that contains this rule.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

References a specific automated reasoning policy rule that was applied during evaluation.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningRuleList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningRule" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningSatisfiableFinding": { + "type": "structure", + "members": { + "translation": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslation", + "traits": { + "smithy.api#documentation": "

The logical translation of the input that this finding evaluates.

" + } + }, + "claimsTrueScenario": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningScenario", + "traits": { + "smithy.api#documentation": "

An example scenario demonstrating how the claims could be logically true.

" + } + }, + "claimsFalseScenario": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningScenario", + "traits": { + "smithy.api#documentation": "

An example scenario demonstrating how the claims could be logically false.

" + } + }, + "logicWarning": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningLogicWarning", + "traits": { + "smithy.api#documentation": "

Indication of a logic issue with the translation without needing to consider the automated reasoning policy rules.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the claims could be either true or false depending on additional assumptions not provided in the input.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningScenario": { + "type": "structure", + "members": { + "statements": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementList", + "traits": { + "smithy.api#documentation": "

List of logical assignments and statements that define this scenario.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents a logical scenario where claims can be evaluated as true or false, containing specific logical assignments.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatement": { + "type": "structure", + "members": { + "logic": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementLogicContent", + "traits": { + "smithy.api#documentation": "

The formal logical representation of the statement.

" + } + }, + "naturalLanguage": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementNaturalLanguageContent", + "traits": { + "smithy.api#documentation": "

The natural language explanation of the logical statement.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A logical statement that includes both formal logic representation and natural language explanation.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatement" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementLogicContent": { + "type": "string", + "traits": { + "smithy.api#length": { + "max": 1000 + }, + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementNaturalLanguageContent": { + "type": "string", + "traits": { + "smithy.api#length": { + "max": 1000 + }, + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTooComplexFinding": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "

Indicates that the input exceeds the processing capacity due to the volume or complexity of the logical information.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslation": { + "type": "structure", + "members": { + "premises": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementList", + "traits": { + "smithy.api#documentation": "

The logical statements that serve as the foundation or assumptions for the claims.

" + } + }, + "claims": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningStatementList", + "traits": { + "smithy.api#documentation": "

The logical statements that are being validated against the premises and policy rules.

" + } + }, + "untranslatedPremises": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningInputTextReferenceList", + "traits": { + "smithy.api#documentation": "

References to portions of the original input text that correspond to the premises but could not be fully translated.

" + } + }, + "untranslatedClaims": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningInputTextReferenceList", + "traits": { + "smithy.api#documentation": "

References to portions of the original input text that correspond to the claims but could not be fully translated.

" + } + }, + "confidence": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationConfidence", + "traits": { + "smithy.api#documentation": "

A confidence score between 0 and 1 indicating how certain the system is about the logical translation.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains the logical translation of natural language input into formal logical statements, including premises, claims, and confidence scores.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationAmbiguousFinding": { + "type": "structure", + "members": { + "options": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationOptionList", + "traits": { + "smithy.api#documentation": "

Different logical interpretations that were detected during translation of the input.

" + } + }, + "differenceScenarios": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningDifferenceScenarioList", + "traits": { + "smithy.api#documentation": "

Scenarios showing how the different translation options differ in meaning.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the input has multiple valid logical interpretations, requiring additional context or clarification.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationConfidence": { + "type": "double", + "traits": { + "smithy.api#range": { + "min": 0, + "max": 1 + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslation" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationOption": { + "type": "structure", + "members": { + "translations": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationList", + "traits": { + "smithy.api#documentation": "

Example translations that provide this possible interpretation of the input.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents one possible logical interpretation of ambiguous input content.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationOptionList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslationOption" + }, + "traits": { + "smithy.api#length": { + "max": 2 + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningValidFinding": { + "type": "structure", + "members": { + "translation": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningTranslation", + "traits": { + "smithy.api#documentation": "

The logical translation of the input that this finding validates.

" + } + }, + "claimsTrueScenario": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningScenario", + "traits": { + "smithy.api#documentation": "

An example scenario demonstrating how the claims are logically true.

" + } + }, + "supportingRules": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningRuleList", + "traits": { + "smithy.api#documentation": "

The automated reasoning policy rules that support why this result is considered valid.

" + } + }, + "logicWarning": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningLogicWarning", + "traits": { + "smithy.api#documentation": "

Indication of a logic issue with the translation without needing to consider the automated reasoning policy rules.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the claims are definitively true and logically implied by the premises, with no possible alternative interpretations.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailConfiguration": { + "type": "structure", + "members": { + "guardrailIdentifier": { + "target": "com.amazonaws.bedrockruntime#GuardrailIdentifier", + "traits": { + "smithy.api#addedDefault": {}, + "smithy.api#default": "", + "smithy.api#documentation": "

The identifier for the guardrail.

" + } + }, + "guardrailVersion": { + "target": "com.amazonaws.bedrockruntime#GuardrailVersion", + "traits": { + "smithy.api#addedDefault": {}, + "smithy.api#default": "", + "smithy.api#documentation": "

The version of the guardrail.

" + } + }, + "trace": { + "target": "com.amazonaws.bedrockruntime#GuardrailTrace", + "traits": { + "smithy.api#default": "disabled", + "smithy.api#documentation": "

The trace behavior for the guardrail.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Configuration information for a guardrail that you use with the Converse operation.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentBlock": { + "type": "union", + "members": { + "text": { + "target": "com.amazonaws.bedrockruntime#GuardrailTextBlock", + "traits": { + "smithy.api#documentation": "

Text within content block to be evaluated by the guardrail.

" + } + }, + "image": { + "target": "com.amazonaws.bedrockruntime#GuardrailImageBlock", + "traits": { + "smithy.api#documentation": "

Image within guardrail content block to be evaluated by the guardrail.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The content block to be evaluated by the guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentBlockList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentBlock" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentFilter": { + "type": "structure", + "members": { + "type": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentFilterType", + "traits": { + "smithy.api#documentation": "

The guardrail type.

", + "smithy.api#required": {} + } + }, + "confidence": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentFilterConfidence", + "traits": { + "smithy.api#documentation": "

The guardrail confidence.

", + "smithy.api#required": {} + } + }, + "filterStrength": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentFilterStrength", + "traits": { + "smithy.api#documentation": "

The filter strength setting for the guardrail content filter.

" + } + }, + "action": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentPolicyAction", + "traits": { + "smithy.api#documentation": "

The guardrail action.

", + "smithy.api#required": {} + } + }, + "detected": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "

Indicates whether content that breaches the guardrail configuration is detected.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The content filter for a guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentFilterConfidence": { + "type": "enum", + "members": { + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + }, + "LOW": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "LOW" + } + }, + "MEDIUM": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MEDIUM" + } + }, + "HIGH": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "HIGH" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentFilterList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentFilter" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentFilterStrength": { + "type": "enum", + "members": { + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + }, + "LOW": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "LOW" + } + }, + "MEDIUM": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MEDIUM" + } + }, + "HIGH": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "HIGH" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentFilterType": { + "type": "enum", + "members": { + "INSULTS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "INSULTS" + } + }, + "HATE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "HATE" + } + }, + "SEXUAL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "SEXUAL" + } + }, + "VIOLENCE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "VIOLENCE" + } + }, + "MISCONDUCT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MISCONDUCT" + } + }, + "PROMPT_ATTACK": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PROMPT_ATTACK" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentPolicyAction": { + "type": "enum", + "members": { + "BLOCKED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BLOCKED" + } + }, + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentPolicyAssessment": { + "type": "structure", + "members": { + "filters": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentFilterList", + "traits": { + "smithy.api#documentation": "

The content policy filters.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

An assessment of a content policy for a guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentPolicyImageUnitsProcessed": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#GuardrailContentPolicyUnitsProcessed": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#GuardrailContentQualifier": { + "type": "enum", + "members": { + "GROUNDING_SOURCE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "grounding_source" + } + }, + "QUERY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "query" + } + }, + "GUARD_CONTENT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "guard_content" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentQualifierList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentQualifier" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContentSource": { + "type": "enum", + "members": { + "INPUT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "INPUT" + } + }, + "OUTPUT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "OUTPUT" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailContextualGroundingFilter": { + "type": "structure", + "members": { + "type": { + "target": "com.amazonaws.bedrockruntime#GuardrailContextualGroundingFilterType", + "traits": { + "smithy.api#documentation": "

The contextual grounding filter type.

", + "smithy.api#required": {} + } + }, + "threshold": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#documentation": "

The threshold used by contextual grounding filter to determine whether the content is grounded or not.

", + "smithy.api#range": { + "min": 0, + "max": 1 + }, + "smithy.api#required": {} + } + }, + "score": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#documentation": "

The score generated by contextual grounding filter.

", + "smithy.api#range": { + "min": 0, + "max": 1 + }, + "smithy.api#required": {} + } + }, + "action": { + "target": "com.amazonaws.bedrockruntime#GuardrailContextualGroundingPolicyAction", + "traits": { + "smithy.api#documentation": "

The action performed by the guardrails contextual grounding filter.

", + "smithy.api#required": {} + } + }, + "detected": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "

Indicates whether content that fails the contextual grounding evaluation (grounding or relevance score less than the corresponding threshold) was detected.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The details for the guardrails contextual grounding filter.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContextualGroundingFilterType": { + "type": "enum", + "members": { + "GROUNDING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "GROUNDING" + } + }, + "RELEVANCE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "RELEVANCE" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailContextualGroundingFilters": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailContextualGroundingFilter" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContextualGroundingPolicyAction": { + "type": "enum", + "members": { + "BLOCKED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BLOCKED" + } + }, + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailContextualGroundingPolicyAssessment": { + "type": "structure", + "members": { + "filters": { + "target": "com.amazonaws.bedrockruntime#GuardrailContextualGroundingFilters", + "traits": { + "smithy.api#documentation": "

The filter details for the guardrails contextual grounding filter.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The policy assessment details for the guardrails contextual grounding filter.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailContextualGroundingPolicyUnitsProcessed": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#GuardrailConverseContentBlock": { + "type": "union", + "members": { + "text": { + "target": "com.amazonaws.bedrockruntime#GuardrailConverseTextBlock", + "traits": { + "smithy.api#documentation": "

The text to guard.

" + } + }, + "image": { + "target": "com.amazonaws.bedrockruntime#GuardrailConverseImageBlock", + "traits": { + "smithy.api#documentation": "

Image within converse content block to be evaluated by the guardrail.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A content block for selective guarding with the Converse or ConverseStream API operations.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailConverseContentQualifier": { + "type": "enum", + "members": { + "GROUNDING_SOURCE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "grounding_source" + } + }, + "QUERY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "query" + } + }, + "GUARD_CONTENT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "guard_content" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailConverseContentQualifierList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailConverseContentQualifier" + } + }, + "com.amazonaws.bedrockruntime#GuardrailConverseImageBlock": { + "type": "structure", + "members": { + "format": { + "target": "com.amazonaws.bedrockruntime#GuardrailConverseImageFormat", + "traits": { + "smithy.api#documentation": "

The format details for the image type of the guardrail converse image block.

", + "smithy.api#required": {} + } + }, + "source": { + "target": "com.amazonaws.bedrockruntime#GuardrailConverseImageSource", + "traits": { + "smithy.api#documentation": "

The image source (image bytes) of the guardrail converse image block.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

An image block that contains images that you want to assess with a guardrail.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#GuardrailConverseImageFormat": { + "type": "enum", + "members": { + "PNG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "png" + } + }, + "JPEG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "jpeg" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailConverseImageSource": { + "type": "union", + "members": { + "bytes": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#documentation": "

The raw image bytes for the image.

", + "smithy.api#length": { + "min": 1 + } + } + } + }, + "traits": { + "smithy.api#documentation": "

The image source (image bytes) of the guardrail converse image source.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#GuardrailConverseTextBlock": { + "type": "structure", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The text that you want to guard.

", + "smithy.api#required": {} + } + }, + "qualifiers": { + "target": "com.amazonaws.bedrockruntime#GuardrailConverseContentQualifierList", + "traits": { + "smithy.api#documentation": "

The qualifier details for the guardrails contextual grounding filter.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A text block that contains text that you want to assess with a guardrail. For more information, see GuardrailConverseContentBlock.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailCoverage": { + "type": "structure", + "members": { + "textCharacters": { + "target": "com.amazonaws.bedrockruntime#GuardrailTextCharactersCoverage", + "traits": { + "smithy.api#documentation": "

The text characters of the guardrail coverage details.

" + } + }, + "images": { + "target": "com.amazonaws.bedrockruntime#GuardrailImageCoverage", + "traits": { + "smithy.api#documentation": "

The guardrail coverage for images (the number of images that guardrails guarded).

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The action of the guardrail coverage details.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailCustomWord": { + "type": "structure", + "members": { + "match": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The match for the custom word.

", + "smithy.api#required": {} + } + }, + "action": { + "target": "com.amazonaws.bedrockruntime#GuardrailWordPolicyAction", + "traits": { + "smithy.api#documentation": "

The action for the custom word.

", + "smithy.api#required": {} + } + }, + "detected": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "

Indicates whether custom word content that breaches the guardrail configuration is detected.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A custom word configured in a guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailCustomWordList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailCustomWord" + } + }, + "com.amazonaws.bedrockruntime#GuardrailId": { + "type": "string", + "traits": { + "smithy.api#length": { + "max": 2048 + }, + "smithy.api#pattern": "^([a-z0-9]+)$" + } + }, + "com.amazonaws.bedrockruntime#GuardrailIdentifier": { + "type": "string", + "traits": { + "smithy.api#length": { + "max": 2048 + }, + "smithy.api#pattern": "^(|([a-z0-9]+)|(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:[0-9]{12}:guardrail/[a-z0-9]+))$" + } + }, + "com.amazonaws.bedrockruntime#GuardrailImageBlock": { + "type": "structure", + "members": { + "format": { + "target": "com.amazonaws.bedrockruntime#GuardrailImageFormat", + "traits": { + "smithy.api#documentation": "

The format details for the file type of the image blocked by the guardrail.

", + "smithy.api#required": {} + } + }, + "source": { + "target": "com.amazonaws.bedrockruntime#GuardrailImageSource", + "traits": { + "smithy.api#documentation": "

The image source (image bytes) details of the image blocked by the guardrail.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Contain an image which user wants guarded. This block is accepted by the guardrails independent API.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#GuardrailImageCoverage": { + "type": "structure", + "members": { + "guarded": { + "target": "com.amazonaws.bedrockruntime#ImagesGuarded", + "traits": { + "smithy.api#documentation": "

The count (integer) of images guardrails guarded.

" + } + }, + "total": { + "target": "com.amazonaws.bedrockruntime#ImagesTotal", + "traits": { + "smithy.api#documentation": "

Represents the total number of images (integer) that were in the request (guarded and unguarded).

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The details of the guardrail image coverage.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailImageFormat": { + "type": "enum", + "members": { + "PNG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "png" + } + }, + "JPEG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "jpeg" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailImageSource": { + "type": "union", + "members": { + "bytes": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#documentation": "

The bytes details of the guardrail image source. Object used in independent api.

", + "smithy.api#length": { + "min": 1 + } + } + } + }, + "traits": { + "smithy.api#documentation": "

The image source (image bytes) of the guardrail image source. Object used in independent api.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#GuardrailInvocationMetrics": { + "type": "structure", + "members": { + "guardrailProcessingLatency": { + "target": "com.amazonaws.bedrockruntime#GuardrailProcessingLatency", + "traits": { + "smithy.api#documentation": "

The processing latency details for the guardrail invocation metrics.

" + } + }, + "usage": { + "target": "com.amazonaws.bedrockruntime#GuardrailUsage", + "traits": { + "smithy.api#documentation": "

The usage details for the guardrail invocation metrics.

" + } + }, + "guardrailCoverage": { + "target": "com.amazonaws.bedrockruntime#GuardrailCoverage", + "traits": { + "smithy.api#documentation": "

The coverage details for the guardrail invocation metrics.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The invocation metrics for the guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailManagedWord": { + "type": "structure", + "members": { + "match": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The match for the managed word.

", + "smithy.api#required": {} + } + }, + "type": { + "target": "com.amazonaws.bedrockruntime#GuardrailManagedWordType", + "traits": { + "smithy.api#documentation": "

The type for the managed word.

", + "smithy.api#required": {} + } + }, + "action": { + "target": "com.amazonaws.bedrockruntime#GuardrailWordPolicyAction", + "traits": { + "smithy.api#documentation": "

The action for the managed word.

", + "smithy.api#required": {} + } + }, + "detected": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "

Indicates whether managed word content that breaches the guardrail configuration is detected.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A managed word configured in a guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailManagedWordList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailManagedWord" + } + }, + "com.amazonaws.bedrockruntime#GuardrailManagedWordType": { + "type": "enum", + "members": { + "PROFANITY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PROFANITY" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailOrigin": { + "type": "enum", + "members": { + "REQUEST": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "REQUEST" + } + }, + "ACCOUNT_ENFORCED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ACCOUNT_ENFORCED" + } + }, + "ORGANIZATION_ENFORCED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ORGANIZATION_ENFORCED" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailOriginList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailOrigin" + } + }, + "com.amazonaws.bedrockruntime#GuardrailOutputContent": { + "type": "structure", + "members": { + "text": { + "target": "com.amazonaws.bedrockruntime#GuardrailOutputText", + "traits": { + "smithy.api#documentation": "

The specific text for the output content produced by the guardrail.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The output content produced by the guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailOutputContentList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailOutputContent" + } + }, + "com.amazonaws.bedrockruntime#GuardrailOutputScope": { + "type": "enum", + "members": { + "INTERVENTIONS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "INTERVENTIONS" + } + }, + "FULL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "FULL" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailOutputText": { + "type": "string" + }, + "com.amazonaws.bedrockruntime#GuardrailOwnership": { + "type": "enum", + "members": { + "SELF": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "SELF" + } + }, + "CROSS_ACCOUNT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CROSS_ACCOUNT" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailPiiEntityFilter": { + "type": "structure", + "members": { + "match": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The PII entity filter match.

", + "smithy.api#required": {} + } + }, + "type": { + "target": "com.amazonaws.bedrockruntime#GuardrailPiiEntityType", + "traits": { + "smithy.api#documentation": "

The PII entity filter type.

", + "smithy.api#required": {} + } + }, + "action": { + "target": "com.amazonaws.bedrockruntime#GuardrailSensitiveInformationPolicyAction", + "traits": { + "smithy.api#documentation": "

The PII entity filter action.

", + "smithy.api#required": {} + } + }, + "detected": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "

Indicates whether personally identifiable information (PII) that breaches the guardrail configuration is detected.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A Personally Identifiable Information (PII) entity configured in a guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailPiiEntityFilterList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailPiiEntityFilter" + } + }, + "com.amazonaws.bedrockruntime#GuardrailPiiEntityType": { + "type": "enum", + "members": { + "ADDRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ADDRESS" + } + }, + "AGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AGE" + } + }, + "AWS_ACCESS_KEY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AWS_ACCESS_KEY" + } + }, + "AWS_SECRET_KEY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AWS_SECRET_KEY" + } + }, + "CA_HEALTH_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CA_HEALTH_NUMBER" + } + }, + "CA_SOCIAL_INSURANCE_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CA_SOCIAL_INSURANCE_NUMBER" + } + }, + "CREDIT_DEBIT_CARD_CVV": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CREDIT_DEBIT_CARD_CVV" + } + }, + "CREDIT_DEBIT_CARD_EXPIRY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CREDIT_DEBIT_CARD_EXPIRY" + } + }, + "CREDIT_DEBIT_CARD_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CREDIT_DEBIT_CARD_NUMBER" + } + }, + "DRIVER_ID": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DRIVER_ID" + } + }, + "EMAIL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "EMAIL" + } + }, + "INTERNATIONAL_BANK_ACCOUNT_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "INTERNATIONAL_BANK_ACCOUNT_NUMBER" + } + }, + "IP_ADDRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "IP_ADDRESS" + } + }, + "LICENSE_PLATE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "LICENSE_PLATE" + } + }, + "MAC_ADDRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MAC_ADDRESS" + } + }, + "NAME": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NAME" + } + }, + "PASSWORD": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PASSWORD" + } + }, + "PHONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PHONE" + } + }, + "PIN": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PIN" + } + }, + "SWIFT_CODE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "SWIFT_CODE" + } + }, + "UK_NATIONAL_HEALTH_SERVICE_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "UK_NATIONAL_HEALTH_SERVICE_NUMBER" + } + }, + "UK_NATIONAL_INSURANCE_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "UK_NATIONAL_INSURANCE_NUMBER" + } + }, + "UK_UNIQUE_TAXPAYER_REFERENCE_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "UK_UNIQUE_TAXPAYER_REFERENCE_NUMBER" + } + }, + "URL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "URL" + } + }, + "USERNAME": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "USERNAME" + } + }, + "US_BANK_ACCOUNT_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "US_BANK_ACCOUNT_NUMBER" + } + }, + "US_BANK_ROUTING_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "US_BANK_ROUTING_NUMBER" + } + }, + "US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER" + } + }, + "US_PASSPORT_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "US_PASSPORT_NUMBER" + } + }, + "US_SOCIAL_SECURITY_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "US_SOCIAL_SECURITY_NUMBER" + } + }, + "VEHICLE_IDENTIFICATION_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "VEHICLE_IDENTIFICATION_NUMBER" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailProcessingLatency": { + "type": "long" + }, + "com.amazonaws.bedrockruntime#GuardrailRegexFilter": { + "type": "structure", + "members": { + "name": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The regex filter name.

" + } + }, + "match": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The regesx filter match.

" + } + }, + "regex": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The regex query.

" + } + }, + "action": { + "target": "com.amazonaws.bedrockruntime#GuardrailSensitiveInformationPolicyAction", + "traits": { + "smithy.api#documentation": "

The region filter action.

", + "smithy.api#required": {} + } + }, + "detected": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "

Indicates whether custom regex entities that breach the guardrail configuration are detected.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A Regex filter configured in a guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailRegexFilterList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailRegexFilter" + } + }, + "com.amazonaws.bedrockruntime#GuardrailResource": { + "type": "resource", + "operations": [ + { + "target": "com.amazonaws.bedrockruntime#ApplyGuardrail" + } + ] + }, + "com.amazonaws.bedrockruntime#GuardrailSensitiveInformationPolicyAction": { + "type": "enum", + "members": { + "ANONYMIZED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ANONYMIZED" + } + }, + "BLOCKED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BLOCKED" + } + }, + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailSensitiveInformationPolicyAssessment": { + "type": "structure", + "members": { + "piiEntities": { + "target": "com.amazonaws.bedrockruntime#GuardrailPiiEntityFilterList", + "traits": { + "smithy.api#documentation": "

The PII entities in the assessment.

", + "smithy.api#required": {} + } + }, + "regexes": { + "target": "com.amazonaws.bedrockruntime#GuardrailRegexFilterList", + "traits": { + "smithy.api#documentation": "

The regex queries in the assessment.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The assessment for a Personally Identifiable Information (PII) policy.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailSensitiveInformationPolicyFreeUnitsProcessed": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#GuardrailSensitiveInformationPolicyUnitsProcessed": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#GuardrailStreamConfiguration": { + "type": "structure", + "members": { + "guardrailIdentifier": { + "target": "com.amazonaws.bedrockruntime#GuardrailIdentifier", + "traits": { + "smithy.api#addedDefault": {}, + "smithy.api#default": "", + "smithy.api#documentation": "

The identifier for the guardrail.

" + } + }, + "guardrailVersion": { + "target": "com.amazonaws.bedrockruntime#GuardrailVersion", + "traits": { + "smithy.api#addedDefault": {}, + "smithy.api#default": "", + "smithy.api#documentation": "

The version of the guardrail.

" + } + }, + "trace": { + "target": "com.amazonaws.bedrockruntime#GuardrailTrace", + "traits": { + "smithy.api#default": "disabled", + "smithy.api#documentation": "

The trace behavior for the guardrail.

" + } + }, + "streamProcessingMode": { + "target": "com.amazonaws.bedrockruntime#GuardrailStreamProcessingMode", + "traits": { + "smithy.api#default": "sync", + "smithy.api#documentation": "

The processing mode.

The processing mode. For more information, see Configure streaming response behavior in the Amazon Bedrock User Guide.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Configuration information for a guardrail that you use with the ConverseStream action.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailStreamProcessingMode": { + "type": "enum", + "members": { + "SYNC": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "sync" + } + }, + "ASYNC": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "async" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailTextBlock": { + "type": "structure", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The input text details to be evaluated by the guardrail.

", + "smithy.api#required": {} + } + }, + "qualifiers": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentQualifierList", + "traits": { + "smithy.api#documentation": "

The qualifiers describing the text block.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The text block to be evaluated by the guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailTextCharactersCoverage": { + "type": "structure", + "members": { + "guarded": { + "target": "com.amazonaws.bedrockruntime#TextCharactersGuarded", + "traits": { + "smithy.api#documentation": "

The text characters that were guarded by the guardrail coverage.

" + } + }, + "total": { + "target": "com.amazonaws.bedrockruntime#TextCharactersTotal", + "traits": { + "smithy.api#documentation": "

The total text characters by the guardrail coverage.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The guardrail coverage for the text characters.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailTopic": { + "type": "structure", + "members": { + "name": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The name for the guardrail.

", + "smithy.api#required": {} + } + }, + "type": { + "target": "com.amazonaws.bedrockruntime#GuardrailTopicType", + "traits": { + "smithy.api#documentation": "

The type behavior that the guardrail should perform when the model detects the topic.

", + "smithy.api#required": {} + } + }, + "action": { + "target": "com.amazonaws.bedrockruntime#GuardrailTopicPolicyAction", + "traits": { + "smithy.api#documentation": "

The action the guardrail should take when it intervenes on a topic.

", + "smithy.api#required": {} + } + }, + "detected": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "

Indicates whether topic content that breaches the guardrail configuration is detected.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Information about a topic guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailTopicList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailTopic" + } + }, + "com.amazonaws.bedrockruntime#GuardrailTopicPolicyAction": { + "type": "enum", + "members": { + "BLOCKED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BLOCKED" + } + }, + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailTopicPolicyAssessment": { + "type": "structure", + "members": { + "topics": { + "target": "com.amazonaws.bedrockruntime#GuardrailTopicList", + "traits": { + "smithy.api#documentation": "

The topics in the assessment.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A behavior assessment of a topic policy.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailTopicPolicyUnitsProcessed": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#GuardrailTopicType": { + "type": "enum", + "members": { + "DENY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DENY" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailTrace": { + "type": "enum", + "members": { + "ENABLED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "enabled" + } + }, + "DISABLED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "disabled" + } + }, + "ENABLED_FULL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "enabled_full" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailTraceAssessment": { + "type": "structure", + "members": { + "modelOutput": { + "target": "com.amazonaws.bedrockruntime#ModelOutputs", + "traits": { + "smithy.api#documentation": "

The output from the model.

" + } + }, + "inputAssessment": { + "target": "com.amazonaws.bedrockruntime#GuardrailAssessmentMap", + "traits": { + "smithy.api#documentation": "

The input assessment.

" + } + }, + "outputAssessments": { + "target": "com.amazonaws.bedrockruntime#GuardrailAssessmentListMap", + "traits": { + "smithy.api#documentation": "

the output assessments.

" + } + }, + "actionReason": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

Provides the reason for the action taken when harmful content is detected.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A Top level guardrail trace object. For more information, see ConverseTrace.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailUsage": { + "type": "structure", + "members": { + "topicPolicyUnits": { + "target": "com.amazonaws.bedrockruntime#GuardrailTopicPolicyUnitsProcessed", + "traits": { + "smithy.api#documentation": "

The topic policy units processed by the guardrail.

", + "smithy.api#required": {} + } + }, + "contentPolicyUnits": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentPolicyUnitsProcessed", + "traits": { + "smithy.api#documentation": "

The content policy units processed by the guardrail.

", + "smithy.api#required": {} + } + }, + "wordPolicyUnits": { + "target": "com.amazonaws.bedrockruntime#GuardrailWordPolicyUnitsProcessed", + "traits": { + "smithy.api#documentation": "

The word policy units processed by the guardrail.

", + "smithy.api#required": {} + } + }, + "sensitiveInformationPolicyUnits": { + "target": "com.amazonaws.bedrockruntime#GuardrailSensitiveInformationPolicyUnitsProcessed", + "traits": { + "smithy.api#documentation": "

The sensitive information policy units processed by the guardrail.

", + "smithy.api#required": {} + } + }, + "sensitiveInformationPolicyFreeUnits": { + "target": "com.amazonaws.bedrockruntime#GuardrailSensitiveInformationPolicyFreeUnitsProcessed", + "traits": { + "smithy.api#documentation": "

The sensitive information policy free units processed by the guardrail.

", + "smithy.api#required": {} + } + }, + "contextualGroundingPolicyUnits": { + "target": "com.amazonaws.bedrockruntime#GuardrailContextualGroundingPolicyUnitsProcessed", + "traits": { + "smithy.api#documentation": "

The contextual grounding policy units processed by the guardrail.

", + "smithy.api#required": {} + } + }, + "contentPolicyImageUnits": { + "target": "com.amazonaws.bedrockruntime#GuardrailContentPolicyImageUnitsProcessed", + "traits": { + "smithy.api#documentation": "

The content policy image units processed by the guardrail.

" + } + }, + "automatedReasoningPolicyUnits": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningPolicyUnitsProcessed", + "traits": { + "smithy.api#documentation": "

The number of text units processed by the automated reasoning policy.

" + } + }, + "automatedReasoningPolicies": { + "target": "com.amazonaws.bedrockruntime#GuardrailAutomatedReasoningPoliciesProcessed", + "traits": { + "smithy.api#documentation": "

The number of automated reasoning policies that were processed during the guardrail evaluation.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The details on the use of the guardrail.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailVersion": { + "type": "string", + "traits": { + "smithy.api#pattern": "^(|([1-9][0-9]{0,7})|(DRAFT))$" + } + }, + "com.amazonaws.bedrockruntime#GuardrailWordPolicyAction": { + "type": "enum", + "members": { + "BLOCKED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BLOCKED" + } + }, + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + } + } + }, + "com.amazonaws.bedrockruntime#GuardrailWordPolicyAssessment": { + "type": "structure", + "members": { + "customWords": { + "target": "com.amazonaws.bedrockruntime#GuardrailCustomWordList", + "traits": { + "smithy.api#documentation": "

Custom words in the assessment.

", + "smithy.api#required": {} + } + }, + "managedWordLists": { + "target": "com.amazonaws.bedrockruntime#GuardrailManagedWordList", + "traits": { + "smithy.api#documentation": "

Managed word lists in the assessment.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The word policy assessment.

" + } + }, + "com.amazonaws.bedrockruntime#GuardrailWordPolicyUnitsProcessed": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#ImageBlock": { + "type": "structure", + "members": { + "format": { + "target": "com.amazonaws.bedrockruntime#ImageFormat", + "traits": { + "smithy.api#documentation": "

The format of the image.

", + "smithy.api#required": {} + } + }, + "source": { + "target": "com.amazonaws.bedrockruntime#ImageSource", + "traits": { + "smithy.api#documentation": "

The source for the image.

", + "smithy.api#required": {} + } + }, + "error": { + "target": "com.amazonaws.bedrockruntime#ErrorBlock", + "traits": { + "smithy.api#documentation": "

Error information if the image block could not be processed or contains invalid data.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Image content for a message.

" + } + }, + "com.amazonaws.bedrockruntime#ImageBlockDelta": { + "type": "structure", + "members": { + "source": { + "target": "com.amazonaws.bedrockruntime#ImageSource", + "traits": { + "smithy.api#documentation": "

The incremental image source data for this delta event.

" + } + }, + "error": { + "target": "com.amazonaws.bedrockruntime#ErrorBlock", + "traits": { + "smithy.api#documentation": "

Error information if this image delta could not be processed.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A streaming delta event that contains incremental image data during streaming responses.

" + } + }, + "com.amazonaws.bedrockruntime#ImageBlockStart": { + "type": "structure", + "members": { + "format": { + "target": "com.amazonaws.bedrockruntime#ImageFormat", + "traits": { + "smithy.api#documentation": "

The format of the image data that will be streamed in subsequent delta events.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The initial event in a streaming image block that indicates the start of image content.

" + } + }, + "com.amazonaws.bedrockruntime#ImageFormat": { + "type": "enum", + "members": { + "PNG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "png" + } + }, + "JPEG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "jpeg" + } + }, + "GIF": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "gif" + } + }, + "WEBP": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "webp" + } + } + } + }, + "com.amazonaws.bedrockruntime#ImageSource": { + "type": "union", + "members": { + "bytes": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#documentation": "

The raw image bytes for the image. If you use an AWS SDK, you don't need to encode the image bytes in base64.

", + "smithy.api#length": { + "min": 1 + } + } + }, + "s3Location": { + "target": "com.amazonaws.bedrockruntime#S3Location", + "traits": { + "smithy.api#documentation": "

The location of an image object in an Amazon S3 bucket. To see which models support S3 uploads, see Supported models and features for Converse.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The source for an image.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#ImagesGuarded": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#ImagesTotal": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#InferenceConfiguration": { + "type": "structure", + "members": { + "maxTokens": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The maximum number of tokens to allow in the generated response. The default value is the maximum allowed value for the model that you are using. For more information, see Inference parameters for foundation models.

", + "smithy.api#range": { + "min": 1 + } + } + }, + "temperature": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#documentation": "

The likelihood of the model selecting higher-probability options while generating a response. A lower value makes the model more likely to choose higher-probability options, while a higher value makes the model more likely to choose lower-probability options.

The default value is the default value for the model that you are using. For more information, see Inference parameters for foundation models.

", + "smithy.api#range": { + "min": 0, + "max": 1 + } + } + }, + "topP": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#documentation": "

The percentage of most-likely candidates that the model considers for the next token. For example, if you choose a value of 0.8 for topP, the model selects from the top 80% of the probability distribution of tokens that could be next in the sequence.

The default value is the default value for the model that you are using. For more information, see Inference parameters for foundation models.

", + "smithy.api#range": { + "min": 0, + "max": 1 + } + } + }, + "stopSequences": { + "target": "com.amazonaws.bedrockruntime#NonEmptyStringList", + "traits": { + "smithy.api#documentation": "

A list of stop sequences. A stop sequence is a sequence of characters that causes the model to stop generating the response.

", + "smithy.api#length": { + "max": 2500 + } + } + } + }, + "traits": { + "smithy.api#documentation": "

Base inference parameters to pass to a model in a call to Converse or ConverseStream. For more information, see Inference parameters for foundation models.

If you need to pass additional parameters that the model supports, use the additionalModelRequestFields request field in the call to Converse or ConverseStream. For more information, see Model parameters.

" + } + }, + "com.amazonaws.bedrockruntime#InferenceResource": { + "type": "resource", + "operations": [ + { + "target": "com.amazonaws.bedrockruntime#Converse" + }, + { + "target": "com.amazonaws.bedrockruntime#ConverseStream" + }, + { + "target": "com.amazonaws.bedrockruntime#InvokeModel" + }, + { + "target": "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStream" + }, + { + "target": "com.amazonaws.bedrockruntime#InvokeModelWithResponseStream" + } + ] + }, + "com.amazonaws.bedrockruntime#InternalServerException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

An internal server error occurred. For troubleshooting this error, see InternalFailure in the Amazon Bedrock User Guide

", + "smithy.api#error": "server", + "smithy.api#httpError": 500 + } + }, + "com.amazonaws.bedrockruntime#InvocationArn": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:[0-9]{12}:async-invoke/[a-z0-9]{12}$" + } + }, + "com.amazonaws.bedrockruntime#InvokeModel": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#InvokeModelRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#InvokeModelResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelErrorException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelNotReadyException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelTimeoutException" + }, + { + "target": "com.amazonaws.bedrockruntime#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceQuotaExceededException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Invokes the specified Amazon Bedrock model to run inference using the prompt and inference parameters provided in the request body. You use model inference to generate text, images, and embeddings.

For example code, see Invoke model code examples in the Amazon Bedrock User Guide.

This operation requires permission for the bedrock:InvokeModel action.

To deny all inference access to resources that you specify in the modelId field, you need to deny access to the bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream actions. Doing this also denies access to the resource through the Converse API actions (Converse and ConverseStream). For more information see Deny access for inference on specific models.

For troubleshooting some of the common errors you might encounter when using the InvokeModel API, see Troubleshooting Amazon Bedrock API Error Codes in the Amazon Bedrock User Guide

", + "smithy.api#http": { + "code": 200, + "method": "POST", + "uri": "/model/{modelId}/invoke" + } + } + }, + "com.amazonaws.bedrockruntime#InvokeModelIdentifier": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:(([0-9]{12}:custom-model/[a-z0-9-]{1,63}[.]{1}[a-z0-9-]{1,63}/[a-z0-9]{12})|(:foundation-model/[a-z0-9-]{1,63}[.]{1}[a-z0-9-]{1,63}([.:]?[a-z0-9-]{1,63}))|([0-9]{12}:imported-model/[a-z0-9]{12})|([0-9]{12}:provisioned-model/[a-z0-9]{12})|([0-9]{12}:custom-model-deployment/[a-z0-9]{12})|([0-9]{12}:(inference-profile|application-inference-profile)/[a-zA-Z0-9-:.]+)))|([a-z0-9-]{1,63}[.]{1}[a-z0-9-]{1,63}([.:]?[a-z0-9-]{1,63}))|(([0-9a-zA-Z][_-]?)+)|([a-zA-Z0-9-:.]+)$|(^(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:[0-9]{12}:prompt/[0-9a-zA-Z]{10}(?::[0-9]{1,5})?))$|(^arn:aws:sagemaker:[a-z0-9-]+:[0-9]{12}:endpoint/[a-zA-Z0-9-]+$)|(^arn:aws(-[^:]+)?:bedrock:([0-9a-z-]{1,20}):([0-9]{12}):(default-)?prompt-router/[a-zA-Z0-9-:.]+$)$" + } + }, + "com.amazonaws.bedrockruntime#InvokeModelRequest": { + "type": "structure", + "members": { + "body": { + "target": "com.amazonaws.bedrockruntime#Body", + "traits": { + "smithy.api#documentation": "

The prompt and inference parameters in the format specified in the contentType in the header. You must provide the body in JSON format. To see the format and content of the request and response bodies for different models, refer to Inference parameters. For more information, see Run inference in the Bedrock User Guide.

", + "smithy.api#httpPayload": {} + } + }, + "contentType": { + "target": "com.amazonaws.bedrockruntime#MimeType", + "traits": { + "smithy.api#documentation": "

The MIME type of the input data in the request. You must specify application/json.

", + "smithy.api#httpHeader": "Content-Type" + } + }, + "accept": { + "target": "com.amazonaws.bedrockruntime#MimeType", + "traits": { + "smithy.api#documentation": "

The desired MIME type of the inference body in the response. The default value is application/json.

", + "smithy.api#httpHeader": "Accept" + } + }, + "modelId": { + "target": "com.amazonaws.bedrockruntime#InvokeModelIdentifier", + "traits": { + "smithy.api#documentation": "

The unique identifier of the model to invoke to run inference.

The modelId to provide depends on the type of model or throughput that you use:

", + "smithy.api#httpLabel": {}, + "smithy.api#required": {} + } + }, + "trace": { + "target": "com.amazonaws.bedrockruntime#Trace", + "traits": { + "smithy.api#documentation": "

Specifies whether to enable or disable the Bedrock trace. If enabled, you can see the full Bedrock trace.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-Trace" + } + }, + "guardrailIdentifier": { + "target": "com.amazonaws.bedrockruntime#GuardrailIdentifier", + "traits": { + "smithy.api#documentation": "

The unique identifier of the guardrail that you want to use. If you don't provide a value, no guardrail is applied to the invocation.

An error will be thrown in the following situations.

  • You don't provide a guardrail identifier but you specify the amazon-bedrock-guardrailConfig field in the request body.

  • You enable the guardrail but the contentType isn't application/json.

  • You provide a guardrail identifier, but guardrailVersion isn't specified.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-GuardrailIdentifier" + } + }, + "guardrailVersion": { + "target": "com.amazonaws.bedrockruntime#GuardrailVersion", + "traits": { + "smithy.api#documentation": "

The version number for the guardrail. The value can also be DRAFT.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-GuardrailVersion" + } + }, + "performanceConfigLatency": { + "target": "com.amazonaws.bedrockruntime#PerformanceConfigLatency", + "traits": { + "smithy.api#default": "standard", + "smithy.api#documentation": "

Model performance settings for the request.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-PerformanceConfig-Latency" + } + }, + "serviceTier": { + "target": "com.amazonaws.bedrockruntime#ServiceTierType", + "traits": { + "smithy.api#documentation": "

Specifies the processing tier type used for serving the request.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-Service-Tier" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#InvokeModelResponse": { + "type": "structure", + "members": { + "body": { + "target": "com.amazonaws.bedrockruntime#Body", + "traits": { + "smithy.api#documentation": "

Inference response from the model in the format specified in the contentType header. To see the format and content of the request and response bodies for different models, refer to Inference parameters.

", + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + }, + "contentType": { + "target": "com.amazonaws.bedrockruntime#MimeType", + "traits": { + "smithy.api#documentation": "

The MIME type of the inference result.

", + "smithy.api#httpHeader": "Content-Type", + "smithy.api#required": {} + } + }, + "performanceConfigLatency": { + "target": "com.amazonaws.bedrockruntime#PerformanceConfigLatency", + "traits": { + "smithy.api#documentation": "

Model performance settings for the request.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-PerformanceConfig-Latency" + } + }, + "serviceTier": { + "target": "com.amazonaws.bedrockruntime#ServiceTierType", + "traits": { + "smithy.api#documentation": "

Specifies the processing tier type used for serving the request.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-Service-Tier" + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#InvokeModelTokensRequest": { + "type": "structure", + "members": { + "body": { + "target": "com.amazonaws.bedrockruntime#Body", + "traits": { + "smithy.api#documentation": "

The request body to count tokens for, formatted according to the model's expected input format. To learn about the input format for different models, see Model inference parameters and responses.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The body of an InvokeModel API request for token counting. This structure mirrors the input format for the InvokeModel operation, allowing you to count tokens for raw text inference requests.

" + } + }, + "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStream": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStreamRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStreamResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelErrorException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelNotReadyException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelStreamErrorException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelTimeoutException" + }, + { + "target": "com.amazonaws.bedrockruntime#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceQuotaExceededException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Invoke the specified Amazon Bedrock model to run inference using the bidirectional stream. The response is returned in a stream that remains open for 8 minutes. A single session can contain multiple prompts and responses from the model. The prompts to the model are provided as audio files and the model's responses are spoken back to the user and transcribed.

It is possible for users to interrupt the model's response with a new prompt, which will halt the response speech. The model will retain contextual awareness of the conversation while pivoting to respond to the new prompt.

", + "smithy.api#http": { + "code": 200, + "method": "POST", + "uri": "/model/{modelId}/invoke-with-bidirectional-stream" + } + } + }, + "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStreamInput": { + "type": "union", + "members": { + "chunk": { + "target": "com.amazonaws.bedrockruntime#BidirectionalInputPayloadPart", + "traits": { + "smithy.api#documentation": "

The audio chunk that is used as input for the invocation step.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Payload content, the speech chunk, for the bidirectional input of the invocation step.

", + "smithy.api#streaming": {} + } + }, + "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStreamOutput": { + "type": "union", + "members": { + "chunk": { + "target": "com.amazonaws.bedrockruntime#BidirectionalOutputPayloadPart", + "traits": { + "smithy.api#documentation": "

The speech chunk that was provided as output from the invocation step.

" + } + }, + "internalServerException": { + "target": "com.amazonaws.bedrockruntime#InternalServerException", + "traits": { + "smithy.api#documentation": "

The request encountered an unknown internal error.

" + } + }, + "modelStreamErrorException": { + "target": "com.amazonaws.bedrockruntime#ModelStreamErrorException", + "traits": { + "smithy.api#documentation": "

The request encountered an error with the model stream.

" + } + }, + "validationException": { + "target": "com.amazonaws.bedrockruntime#ValidationException", + "traits": { + "smithy.api#documentation": "

The input fails to satisfy the constraints specified by an Amazon Web Services service.

" + } + }, + "throttlingException": { + "target": "com.amazonaws.bedrockruntime#ThrottlingException", + "traits": { + "smithy.api#documentation": "

The request was denied due to request throttling.

" + } + }, + "modelTimeoutException": { + "target": "com.amazonaws.bedrockruntime#ModelTimeoutException", + "traits": { + "smithy.api#documentation": "

The connection was closed because a request was not received within the timeout period.

" + } + }, + "serviceUnavailableException": { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException", + "traits": { + "smithy.api#documentation": "

The request has failed due to a temporary failure of the server.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Output from the bidirectional stream that was used for model invocation.

", + "smithy.api#streaming": {} + } + }, + "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStreamRequest": { + "type": "structure", + "members": { + "modelId": { + "target": "com.amazonaws.bedrockruntime#InvokeModelIdentifier", + "traits": { + "smithy.api#documentation": "

The model ID or ARN of the model ID to use. Currently, only amazon.nova-sonic-v1:0 is supported.

", + "smithy.api#httpLabel": {}, + "smithy.api#required": {} + } + }, + "body": { + "target": "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStreamInput", + "traits": { + "smithy.api#documentation": "

The prompt and inference parameters in the format specified in the BidirectionalInputPayloadPart in the header. You must provide the body in JSON format. To see the format and content of the request and response bodies for different models, refer to Inference parameters. For more information, see Run inference in the Bedrock User Guide.

", + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStreamResponse": { + "type": "structure", + "members": { + "body": { + "target": "com.amazonaws.bedrockruntime#InvokeModelWithBidirectionalStreamOutput", + "traits": { + "smithy.api#documentation": "

Streaming response from the model in the format specified by the BidirectionalOutputPayloadPart header.

", + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#InvokeModelWithResponseStream": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#InvokeModelWithResponseStreamRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#InvokeModelWithResponseStreamResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelErrorException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelNotReadyException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelStreamErrorException" + }, + { + "target": "com.amazonaws.bedrockruntime#ModelTimeoutException" + }, + { + "target": "com.amazonaws.bedrockruntime#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceQuotaExceededException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Invoke the specified Amazon Bedrock model to run inference using the prompt and inference parameters provided in the request body. The response is returned in a stream.

To see if a model supports streaming, call GetFoundationModel and check the responseStreamingSupported field in the response.

The CLI doesn't support streaming operations in Amazon Bedrock, including InvokeModelWithResponseStream.

For example code, see Invoke model with streaming code example in the Amazon Bedrock User Guide.

This operation requires permissions to perform the bedrock:InvokeModelWithResponseStream action.

To deny all inference access to resources that you specify in the modelId field, you need to deny access to the bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream actions. Doing this also denies access to the resource through the Converse API actions (Converse and ConverseStream). For more information see Deny access for inference on specific models.

For troubleshooting some of the common errors you might encounter when using the InvokeModelWithResponseStream API, see Troubleshooting Amazon Bedrock API Error Codes in the Amazon Bedrock User Guide

", + "smithy.api#http": { + "code": 200, + "method": "POST", + "uri": "/model/{modelId}/invoke-with-response-stream" + } + } + }, + "com.amazonaws.bedrockruntime#InvokeModelWithResponseStreamRequest": { + "type": "structure", + "members": { + "body": { + "target": "com.amazonaws.bedrockruntime#Body", + "traits": { + "smithy.api#documentation": "

The prompt and inference parameters in the format specified in the contentType in the header. You must provide the body in JSON format. To see the format and content of the request and response bodies for different models, refer to Inference parameters. For more information, see Run inference in the Bedrock User Guide.

", + "smithy.api#httpPayload": {} + } + }, + "contentType": { + "target": "com.amazonaws.bedrockruntime#MimeType", + "traits": { + "smithy.api#documentation": "

The MIME type of the input data in the request. You must specify application/json.

", + "smithy.api#httpHeader": "Content-Type" + } + }, + "accept": { + "target": "com.amazonaws.bedrockruntime#MimeType", + "traits": { + "smithy.api#documentation": "

The desired MIME type of the inference body in the response. The default value is application/json.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-Accept" + } + }, + "modelId": { + "target": "com.amazonaws.bedrockruntime#InvokeModelIdentifier", + "traits": { + "smithy.api#documentation": "

The unique identifier of the model to invoke to run inference.

The modelId to provide depends on the type of model or throughput that you use:

", + "smithy.api#httpLabel": {}, + "smithy.api#required": {} + } + }, + "trace": { + "target": "com.amazonaws.bedrockruntime#Trace", + "traits": { + "smithy.api#documentation": "

Specifies whether to enable or disable the Bedrock trace. If enabled, you can see the full Bedrock trace.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-Trace" + } + }, + "guardrailIdentifier": { + "target": "com.amazonaws.bedrockruntime#GuardrailIdentifier", + "traits": { + "smithy.api#documentation": "

The unique identifier of the guardrail that you want to use. If you don't provide a value, no guardrail is applied to the invocation.

An error is thrown in the following situations.

  • You don't provide a guardrail identifier but you specify the amazon-bedrock-guardrailConfig field in the request body.

  • You enable the guardrail but the contentType isn't application/json.

  • You provide a guardrail identifier, but guardrailVersion isn't specified.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-GuardrailIdentifier" + } + }, + "guardrailVersion": { + "target": "com.amazonaws.bedrockruntime#GuardrailVersion", + "traits": { + "smithy.api#documentation": "

The version number for the guardrail. The value can also be DRAFT.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-GuardrailVersion" + } + }, + "performanceConfigLatency": { + "target": "com.amazonaws.bedrockruntime#PerformanceConfigLatency", + "traits": { + "smithy.api#default": "standard", + "smithy.api#documentation": "

Model performance settings for the request.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-PerformanceConfig-Latency" + } + }, + "serviceTier": { + "target": "com.amazonaws.bedrockruntime#ServiceTierType", + "traits": { + "smithy.api#documentation": "

Specifies the processing tier type used for serving the request.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-Service-Tier" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#InvokeModelWithResponseStreamResponse": { + "type": "structure", + "members": { + "body": { + "target": "com.amazonaws.bedrockruntime#ResponseStream", + "traits": { + "smithy.api#documentation": "

Inference response from the model in the format specified by the contentType header. To see the format and content of this field for different models, refer to Inference parameters.

", + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + }, + "contentType": { + "target": "com.amazonaws.bedrockruntime#MimeType", + "traits": { + "smithy.api#documentation": "

The MIME type of the inference result.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-Content-Type", + "smithy.api#required": {} + } + }, + "performanceConfigLatency": { + "target": "com.amazonaws.bedrockruntime#PerformanceConfigLatency", + "traits": { + "smithy.api#documentation": "

Model performance settings for the request.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-PerformanceConfig-Latency" + } + }, + "serviceTier": { + "target": "com.amazonaws.bedrockruntime#ServiceTierType", + "traits": { + "smithy.api#documentation": "

Specifies the processing tier type used for serving the request.

", + "smithy.api#httpHeader": "X-Amzn-Bedrock-Service-Tier" + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#InvokedModelId": { + "type": "string", + "traits": { + "smithy.api#pattern": "^(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}::foundation-model/[a-z0-9-]{1,63}[.]{1}[a-z0-9-]{1,63}([a-z0-9-]{1,63}[.]){0,2}[a-z0-9-]{1,63}([:][a-z0-9-]{1,63}){0,2})|(arn:aws(|-us-gov|-cn|-iso|-iso-b):bedrock:(|[0-9a-z-]{1,20}):(|[0-9]{12}):inference-profile/[a-zA-Z0-9-:.]+)$" + } + }, + "com.amazonaws.bedrockruntime#JsonSchemaDefinition": { + "type": "structure", + "members": { + "schema": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The JSON schema to constrain the model's output. For more information, see JSON Schema Reference.

", + "smithy.api#required": {} + } + }, + "name": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The name of the JSON schema.

" + } + }, + "description": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

A description of the JSON schema.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

JSON schema structured output format options.

" + } + }, + "com.amazonaws.bedrockruntime#KmsKeyId": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^arn:aws(-[^:]+)?:kms:[a-zA-Z0-9-]*:[0-9]{12}:((key/[a-zA-Z0-9-]{36})|(alias/[a-zA-Z0-9-_/]+))$" + } + }, + "com.amazonaws.bedrockruntime#ListAsyncInvokes": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#ListAsyncInvokesRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#ListAsyncInvokesResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Lists asynchronous invocations.

", + "smithy.api#http": { + "code": 200, + "method": "GET", + "uri": "/async-invoke" + }, + "smithy.api#paginated": { + "inputToken": "nextToken", + "outputToken": "nextToken", + "pageSize": "maxResults", + "items": "asyncInvokeSummaries" + }, + "smithy.api#readonly": {} + } + }, + "com.amazonaws.bedrockruntime#ListAsyncInvokesRequest": { + "type": "structure", + "members": { + "submitTimeAfter": { + "target": "com.amazonaws.bedrockruntime#Timestamp", + "traits": { + "smithy.api#documentation": "

Include invocations submitted after this time.

", + "smithy.api#httpQuery": "submitTimeAfter" + } + }, + "submitTimeBefore": { + "target": "com.amazonaws.bedrockruntime#Timestamp", + "traits": { + "smithy.api#documentation": "

Include invocations submitted before this time.

", + "smithy.api#httpQuery": "submitTimeBefore" + } + }, + "statusEquals": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeStatus", + "traits": { + "smithy.api#documentation": "

Filter invocations by status.

", + "smithy.api#httpQuery": "statusEquals" + } + }, + "maxResults": { + "target": "com.amazonaws.bedrockruntime#MaxResults", + "traits": { + "smithy.api#documentation": "

The maximum number of invocations to return in one page of results.

", + "smithy.api#httpQuery": "maxResults" + } + }, + "nextToken": { + "target": "com.amazonaws.bedrockruntime#PaginationToken", + "traits": { + "smithy.api#documentation": "

Specify the pagination token from a previous request to retrieve the next page of results.

", + "smithy.api#httpQuery": "nextToken" + } + }, + "sortBy": { + "target": "com.amazonaws.bedrockruntime#SortAsyncInvocationBy", + "traits": { + "smithy.api#default": "SubmissionTime", + "smithy.api#documentation": "

How to sort the response.

", + "smithy.api#httpQuery": "sortBy" + } + }, + "sortOrder": { + "target": "com.amazonaws.bedrockruntime#SortOrder", + "traits": { + "smithy.api#default": "Descending", + "smithy.api#documentation": "

The sorting order for the response.

", + "smithy.api#httpQuery": "sortOrder" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#ListAsyncInvokesResponse": { + "type": "structure", + "members": { + "nextToken": { + "target": "com.amazonaws.bedrockruntime#PaginationToken", + "traits": { + "smithy.api#documentation": "

Specify the pagination token from a previous request to retrieve the next page of results.

" + } + }, + "asyncInvokeSummaries": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeSummaries", + "traits": { + "smithy.api#documentation": "

A list of invocation summaries.

" + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#MaxResults": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 1, + "max": 1000 + } + } + }, + "com.amazonaws.bedrockruntime#Message": { + "type": "structure", + "members": { + "role": { + "target": "com.amazonaws.bedrockruntime#ConversationRole", + "traits": { + "smithy.api#documentation": "

The role that the message plays in the message.

", + "smithy.api#required": {} + } + }, + "content": { + "target": "com.amazonaws.bedrockruntime#ContentBlocks", + "traits": { + "smithy.api#documentation": "

The message content. Note the following restrictions:

  • You can include up to 20 images. Each image's size, height, and width must be no more than 3.75 MB, 8000 px, and 8000 px, respectively.

  • You can include up to five documents. Each document's size must be no more than 4.5 MB.

  • If you include a ContentBlock with a document field in the array, you must also include a ContentBlock with a text field.

  • You can only include images and documents if the role is user.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A message input, or returned from, a call to Converse or ConverseStream.

" + } + }, + "com.amazonaws.bedrockruntime#MessageStartEvent": { + "type": "structure", + "members": { + "role": { + "target": "com.amazonaws.bedrockruntime#ConversationRole", + "traits": { + "smithy.api#documentation": "

The role for the message.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The start of a message.

" + } + }, + "com.amazonaws.bedrockruntime#MessageStopEvent": { + "type": "structure", + "members": { + "stopReason": { + "target": "com.amazonaws.bedrockruntime#StopReason", + "traits": { + "smithy.api#documentation": "

The reason why the model stopped generating output.

", + "smithy.api#required": {} + } + }, + "additionalModelResponseFields": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "

The additional model response fields.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The stop event for a message.

" + } + }, + "com.amazonaws.bedrockruntime#Messages": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#Message" + } + }, + "com.amazonaws.bedrockruntime#MimeType": { + "type": "string" + }, + "com.amazonaws.bedrockruntime#ModelErrorException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + }, + "originalStatusCode": { + "target": "com.amazonaws.bedrockruntime#StatusCode", + "traits": { + "smithy.api#documentation": "

The original status code.

" + } + }, + "resourceName": { + "target": "com.amazonaws.bedrockruntime#NonBlankString", + "traits": { + "smithy.api#documentation": "

The resource name.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The request failed due to an error while processing the model.

", + "smithy.api#error": "client", + "smithy.api#httpError": 424 + } + }, + "com.amazonaws.bedrockruntime#ModelInputPayload": { + "type": "document", + "traits": { + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#ModelNotReadyException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

The model specified in the request is not ready to serve inference requests. The AWS SDK will automatically retry the operation up to 5 times. For information about configuring automatic retries, see Retry behavior in the AWS SDKs and Tools reference guide.

", + "smithy.api#error": "client", + "smithy.api#httpError": 429, + "smithy.api#retryable": {} + } + }, + "com.amazonaws.bedrockruntime#ModelOutputs": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#GuardrailOutputText" + } + }, + "com.amazonaws.bedrockruntime#ModelStreamErrorException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + }, + "originalStatusCode": { + "target": "com.amazonaws.bedrockruntime#StatusCode", + "traits": { + "smithy.api#documentation": "

The original status code.

" + } + }, + "originalMessage": { + "target": "com.amazonaws.bedrockruntime#NonBlankString", + "traits": { + "smithy.api#documentation": "

The original message.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

An error occurred while streaming the response. Retry your request.

", + "smithy.api#error": "client", + "smithy.api#httpError": 424 + } + }, + "com.amazonaws.bedrockruntime#ModelTimeoutException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

The request took too long to process. Processing time exceeded the model timeout length.

", + "smithy.api#error": "client", + "smithy.api#httpError": 408 + } + }, + "com.amazonaws.bedrockruntime#NonBlankString": { + "type": "string", + "traits": { + "smithy.api#pattern": "^[\\s\\S]*$" + } + }, + "com.amazonaws.bedrockruntime#NonEmptyString": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1 + } + } + }, + "com.amazonaws.bedrockruntime#NonEmptyStringList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#NonEmptyString" + } + }, + "com.amazonaws.bedrockruntime#NonNegativeInteger": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 0 + } + } + }, + "com.amazonaws.bedrockruntime#OutputConfig": { + "type": "structure", + "members": { + "textFormat": { + "target": "com.amazonaws.bedrockruntime#OutputFormat", + "traits": { + "smithy.api#documentation": "

Structured output parameters to control the model's text response.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Output configuration for a model response in a call to Converse or ConverseStream.

" + } + }, + "com.amazonaws.bedrockruntime#OutputFormat": { + "type": "structure", + "members": { + "type": { + "target": "com.amazonaws.bedrockruntime#OutputFormatType", + "traits": { + "smithy.api#documentation": "

The type of structured output format.

", + "smithy.api#required": {} + } + }, + "structure": { + "target": "com.amazonaws.bedrockruntime#OutputFormatStructure", + "traits": { + "smithy.api#documentation": "

The structure that the model's output must adhere to.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Structured output parameters to control the model's response.

" + } + }, + "com.amazonaws.bedrockruntime#OutputFormatStructure": { + "type": "union", + "members": { + "jsonSchema": { + "target": "com.amazonaws.bedrockruntime#JsonSchemaDefinition", + "traits": { + "smithy.api#documentation": "

A JSON schema structure that the model's output must adhere to.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The structure that the model's output must adhere to.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#OutputFormatType": { + "type": "enum", + "members": { + "JSON_SCHEMA": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "json_schema" + } + } + }, + "traits": { + "smithy.api#documentation": "

The type of structured output format. Available options are: json_schema.

" + } + }, + "com.amazonaws.bedrockruntime#PaginationToken": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^\\S*$" + } + }, + "com.amazonaws.bedrockruntime#PartBody": { + "type": "blob", + "traits": { + "smithy.api#length": { + "max": 1000000 + }, + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#PayloadPart": { + "type": "structure", + "members": { + "bytes": { + "target": "com.amazonaws.bedrockruntime#PartBody", + "traits": { + "smithy.api#documentation": "

Base64-encoded bytes of payload data.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Payload content included in the response.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#PerformanceConfigLatency": { + "type": "enum", + "members": { + "STANDARD": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "standard" + } + }, + "OPTIMIZED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "optimized" + } + } + } + }, + "com.amazonaws.bedrockruntime#PerformanceConfiguration": { + "type": "structure", + "members": { + "latency": { + "target": "com.amazonaws.bedrockruntime#PerformanceConfigLatency", + "traits": { + "smithy.api#default": "standard", + "smithy.api#documentation": "

To use a latency-optimized version of the model, set to optimized.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Performance settings for a model.

" + } + }, + "com.amazonaws.bedrockruntime#PromptRouterTrace": { + "type": "structure", + "members": { + "invokedModelId": { + "target": "com.amazonaws.bedrockruntime#InvokedModelId", + "traits": { + "smithy.api#documentation": "

The ID of the invoked model.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A prompt router trace.

" + } + }, + "com.amazonaws.bedrockruntime#PromptVariableMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "com.amazonaws.bedrockruntime#PromptVariableValues" + }, + "traits": { + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#PromptVariableValues": { + "type": "union", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The text value that the variable maps to.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains a map of variables in a prompt from Prompt management to an object containing the values to fill in for them when running model invocation. For more information, see How Prompt management works.

" + } + }, + "com.amazonaws.bedrockruntime#ReasoningContentBlock": { + "type": "union", + "members": { + "reasoningText": { + "target": "com.amazonaws.bedrockruntime#ReasoningTextBlock", + "traits": { + "smithy.api#documentation": "

The reasoning that the model used to return the output.

" + } + }, + "redactedContent": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#documentation": "

The content in the reasoning that was encrypted by the model provider for safety reasons. The encryption doesn't affect the quality of responses.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains content regarding the reasoning that is carried out by the model with respect to the content in the content block. Reasoning refers to a Chain of Thought (CoT) that the model generates to enhance the accuracy of its final response.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#ReasoningContentBlockDelta": { + "type": "union", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The reasoning that the model used to return the output.

" + } + }, + "redactedContent": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#documentation": "

The content in the reasoning that was encrypted by the model provider for safety reasons. The encryption doesn't affect the quality of responses.

" + } + }, + "signature": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

A token that verifies that the reasoning text was generated by the model. If you pass a reasoning block back to the API in a multi-turn conversation, include the text and its signature unmodified.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains content regarding the reasoning that is carried out by the model with respect to the content in the content block. Reasoning refers to a Chain of Thought (CoT) that the model generates to enhance the accuracy of its final response.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#ReasoningTextBlock": { + "type": "structure", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The reasoning that the model used to return the output.

", + "smithy.api#required": {} + } + }, + "signature": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

A token that verifies that the reasoning text was generated by the model. If you pass a reasoning block back to the API in a multi-turn conversation, include the text and its signature unmodified.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains the reasoning that the model used to return the output.

", + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#RequestMetadata": { + "type": "map", + "key": { + "target": "smithy.api#String", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 256 + }, + "smithy.api#pattern": "^[a-zA-Z0-9\\s:_@$#=/+,-.]{1,256}$" + } + }, + "value": { + "target": "smithy.api#String", + "traits": { + "smithy.api#length": { + "min": 0, + "max": 256 + }, + "smithy.api#pattern": "^[a-zA-Z0-9\\s:_@$#=/+,-.]{0,256}$" + } + }, + "traits": { + "smithy.api#length": { + "min": 1, + "max": 16 + }, + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.bedrockruntime#ResourceNotFoundException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

The specified resource ARN was not found. For troubleshooting this error, see ResourceNotFound in the Amazon Bedrock User Guide

", + "smithy.api#error": "client", + "smithy.api#httpError": 404 + } + }, + "com.amazonaws.bedrockruntime#ResponseStream": { + "type": "union", + "members": { + "chunk": { + "target": "com.amazonaws.bedrockruntime#PayloadPart", + "traits": { + "smithy.api#documentation": "

Content included in the response.

" + } + }, + "internalServerException": { + "target": "com.amazonaws.bedrockruntime#InternalServerException", + "traits": { + "smithy.api#documentation": "

An internal server error occurred. Retry your request.

" + } + }, + "modelStreamErrorException": { + "target": "com.amazonaws.bedrockruntime#ModelStreamErrorException", + "traits": { + "smithy.api#documentation": "

An error occurred while streaming the response. Retry your request.

" + } + }, + "validationException": { + "target": "com.amazonaws.bedrockruntime#ValidationException", + "traits": { + "smithy.api#documentation": "

Input validation failed. Check your request parameters and retry the request.

" + } + }, + "throttlingException": { + "target": "com.amazonaws.bedrockruntime#ThrottlingException", + "traits": { + "smithy.api#documentation": "

Your request was throttled because of service-wide limitations. Resubmit your request later or in a different region. You can also purchase Provisioned Throughput to increase the rate or number of tokens you can process.

" + } + }, + "modelTimeoutException": { + "target": "com.amazonaws.bedrockruntime#ModelTimeoutException", + "traits": { + "smithy.api#documentation": "

The request took too long to process. Processing time exceeded the model timeout length.

" + } + }, + "serviceUnavailableException": { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException", + "traits": { + "smithy.api#documentation": "

The service isn't available. Try again later.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Definition of content in the response stream.

", + "smithy.api#streaming": {} + } + }, + "com.amazonaws.bedrockruntime#S3Location": { + "type": "structure", + "members": { + "uri": { + "target": "com.amazonaws.bedrockruntime#S3Uri", + "traits": { + "smithy.api#documentation": "

An object URI starting with s3://.

", + "smithy.api#required": {} + } + }, + "bucketOwner": { + "target": "com.amazonaws.bedrockruntime#AccountId", + "traits": { + "smithy.api#documentation": "

If the bucket belongs to another AWS account, specify that account's ID.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A storage location in an Amazon S3 bucket.

" + } + }, + "com.amazonaws.bedrockruntime#S3Uri": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 1024 + }, + "smithy.api#pattern": "^s3://[a-z0-9][\\.\\-a-z0-9]{1,61}[a-z0-9](/.*)?$" + } + }, + "com.amazonaws.bedrockruntime#SearchResultBlock": { + "type": "structure", + "members": { + "source": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The source URL or identifier for the content.

", + "smithy.api#required": {} + } + }, + "title": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

A descriptive title for the search result.

", + "smithy.api#required": {} + } + }, + "content": { + "target": "com.amazonaws.bedrockruntime#SearchResultContentBlocks", + "traits": { + "smithy.api#documentation": "

An array of search result content block.

", + "smithy.api#required": {} + } + }, + "citations": { + "target": "com.amazonaws.bedrockruntime#CitationsConfig", + "traits": { + "smithy.api#documentation": "

Configuration setting for citations

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A search result block that enables natural citations with proper source attribution for retrieved content.

This field is only supported by Anthropic Claude Opus 4.1, Opus 4, Sonnet 4.5, Sonnet 4, Sonnet 3.7, and 3.5 Haiku models.

" + } + }, + "com.amazonaws.bedrockruntime#SearchResultContentBlock": { + "type": "structure", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The actual text content

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A block within a search result that contains the content.

" + } + }, + "com.amazonaws.bedrockruntime#SearchResultContentBlocks": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#SearchResultContentBlock" + } + }, + "com.amazonaws.bedrockruntime#SearchResultLocation": { + "type": "structure", + "members": { + "searchResultIndex": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The index of the search result content block where the cited content is found.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "start": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The starting position in the content array where the cited content begins.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "end": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The ending position in the content array where the cited content ends.

", + "smithy.api#range": { + "min": 0 + } + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies a search result location within the content array, providing positioning information for cited content using search result index and block positions.

" + } + }, + "com.amazonaws.bedrockruntime#ServiceQuotaExceededException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

Your request exceeds the service quota for your account. You can view your quotas at Viewing service quotas. You can resubmit your request later.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.bedrockruntime#ServiceTier": { + "type": "structure", + "members": { + "type": { + "target": "com.amazonaws.bedrockruntime#ServiceTierType", + "traits": { + "smithy.api#documentation": "

Specifies the processing tier type used for serving the request.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies the processing tier configuration used for serving the request.

" + } + }, + "com.amazonaws.bedrockruntime#ServiceTierType": { + "type": "enum", + "members": { + "PRIORITY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "priority" + } + }, + "DEFAULT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "default" + } + }, + "FLEX": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "flex" + } + }, + "RESERVED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "reserved" + } + } + } + }, + "com.amazonaws.bedrockruntime#ServiceUnavailableException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

The service isn't currently available. For troubleshooting this error, see ServiceUnavailable in the Amazon Bedrock User Guide

", + "smithy.api#error": "server", + "smithy.api#httpError": 503 + } + }, + "com.amazonaws.bedrockruntime#SortAsyncInvocationBy": { + "type": "enum", + "members": { + "SUBMISSION_TIME": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "SubmissionTime" + } + } + } + }, + "com.amazonaws.bedrockruntime#SortOrder": { + "type": "enum", + "members": { + "ASCENDING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Ascending" + } + }, + "DESCENDING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Descending" + } + } + } + }, + "com.amazonaws.bedrockruntime#SpecificToolChoice": { + "type": "structure", + "members": { + "name": { + "target": "com.amazonaws.bedrockruntime#ToolName", + "traits": { + "smithy.api#documentation": "

The name of the tool that the model must request.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The model must request a specific tool. For example, {\"tool\" : {\"name\" : \"Your tool name\"}}. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide

This field is only supported by Anthropic Claude 3 models.

" + } + }, + "com.amazonaws.bedrockruntime#StartAsyncInvoke": { + "type": "operation", + "input": { + "target": "com.amazonaws.bedrockruntime#StartAsyncInvokeRequest" + }, + "output": { + "target": "com.amazonaws.bedrockruntime#StartAsyncInvokeResponse" + }, + "errors": [ + { + "target": "com.amazonaws.bedrockruntime#AccessDeniedException" + }, + { + "target": "com.amazonaws.bedrockruntime#ConflictException" + }, + { + "target": "com.amazonaws.bedrockruntime#InternalServerException" + }, + { + "target": "com.amazonaws.bedrockruntime#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceQuotaExceededException" + }, + { + "target": "com.amazonaws.bedrockruntime#ServiceUnavailableException" + }, + { + "target": "com.amazonaws.bedrockruntime#ThrottlingException" + }, + { + "target": "com.amazonaws.bedrockruntime#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Starts an asynchronous invocation.

This operation requires permission for the bedrock:InvokeModel action.

To deny all inference access to resources that you specify in the modelId field, you need to deny access to the bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream actions. Doing this also denies access to the resource through the Converse API actions (Converse and ConverseStream). For more information see Deny access for inference on specific models.

", + "smithy.api#http": { + "code": 200, + "method": "POST", + "uri": "/async-invoke" + }, + "smithy.api#idempotent": {} + } + }, + "com.amazonaws.bedrockruntime#StartAsyncInvokeRequest": { + "type": "structure", + "members": { + "clientRequestToken": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeIdempotencyToken", + "traits": { + "smithy.api#documentation": "

Specify idempotency token to ensure that requests are not duplicated.

", + "smithy.api#idempotencyToken": {} + } + }, + "modelId": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeIdentifier", + "traits": { + "smithy.api#documentation": "

The model to invoke.

", + "smithy.api#required": {} + } + }, + "modelInput": { + "target": "com.amazonaws.bedrockruntime#ModelInputPayload", + "traits": { + "smithy.api#documentation": "

Input to send to the model.

", + "smithy.api#required": {} + } + }, + "outputDataConfig": { + "target": "com.amazonaws.bedrockruntime#AsyncInvokeOutputDataConfig", + "traits": { + "smithy.api#documentation": "

Where to store the output.

", + "smithy.api#required": {} + } + }, + "tags": { + "target": "com.amazonaws.bedrockruntime#TagList", + "traits": { + "smithy.api#documentation": "

Tags to apply to the invocation.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.bedrockruntime#StartAsyncInvokeResponse": { + "type": "structure", + "members": { + "invocationArn": { + "target": "com.amazonaws.bedrockruntime#InvocationArn", + "traits": { + "smithy.api#documentation": "

The ARN of the invocation.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.bedrockruntime#StatusCode": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 100, + "max": 599 + } + } + }, + "com.amazonaws.bedrockruntime#StopReason": { + "type": "enum", + "members": { + "END_TURN": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "end_turn" + } + }, + "TOOL_USE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "tool_use" + } + }, + "MAX_TOKENS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "max_tokens" + } + }, + "STOP_SEQUENCE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "stop_sequence" + } + }, + "GUARDRAIL_INTERVENED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "guardrail_intervened" + } + }, + "CONTENT_FILTERED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "content_filtered" + } + }, + "MALFORMED_MODEL_OUTPUT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "malformed_model_output" + } + }, + "MALFORMED_TOOL_USE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "malformed_tool_use" + } + }, + "MODEL_CONTEXT_WINDOW_EXCEEDED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "model_context_window_exceeded" + } + } + } + }, + "com.amazonaws.bedrockruntime#SystemContentBlock": { + "type": "union", + "members": { + "text": { + "target": "com.amazonaws.bedrockruntime#NonEmptyString", + "traits": { + "smithy.api#documentation": "

A system prompt for the model.

" + } + }, + "guardContent": { + "target": "com.amazonaws.bedrockruntime#GuardrailConverseContentBlock", + "traits": { + "smithy.api#documentation": "

A content block to assess with the guardrail. Use with the Converse or ConverseStream API operations.

For more information, see Use a guardrail with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "cachePoint": { + "target": "com.amazonaws.bedrockruntime#CachePointBlock", + "traits": { + "smithy.api#documentation": "

CachePoint to include in the system prompt.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains configurations for instructions to provide the model for how to handle input. To learn more, see Using the Converse API.

" + } + }, + "com.amazonaws.bedrockruntime#SystemContentBlocks": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#SystemContentBlock" + } + }, + "com.amazonaws.bedrockruntime#SystemTool": { + "type": "structure", + "members": { + "name": { + "target": "com.amazonaws.bedrockruntime#ToolName", + "traits": { + "smithy.api#documentation": "

The name of the system-defined tool that you want to call.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies a system-defined tool for the model to use. System-defined tools are tools that are created and provided by the model provider.

" + } + }, + "com.amazonaws.bedrockruntime#Tag": { + "type": "structure", + "members": { + "key": { + "target": "com.amazonaws.bedrockruntime#TagKey", + "traits": { + "smithy.api#documentation": "

The tag's key.

", + "smithy.api#required": {} + } + }, + "value": { + "target": "com.amazonaws.bedrockruntime#TagValue", + "traits": { + "smithy.api#documentation": "

The tag's value.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A tag.

" + } + }, + "com.amazonaws.bedrockruntime#TagKey": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 128 + }, + "smithy.api#pattern": "^[a-zA-Z0-9\\s._:/=+@-]*$" + } + }, + "com.amazonaws.bedrockruntime#TagList": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#Tag" + }, + "traits": { + "smithy.api#length": { + "min": 0, + "max": 200 + } + } + }, + "com.amazonaws.bedrockruntime#TagValue": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 0, + "max": 256 + }, + "smithy.api#pattern": "^[a-zA-Z0-9\\s._:/=+@-]*$" + } + }, + "com.amazonaws.bedrockruntime#TextCharactersGuarded": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#TextCharactersTotal": { + "type": "integer" + }, + "com.amazonaws.bedrockruntime#ThrottlingException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

Your request was denied due to exceeding the account quotas for Amazon Bedrock. For troubleshooting this error, see ThrottlingException in the Amazon Bedrock User Guide

", + "smithy.api#error": "client", + "smithy.api#httpError": 429 + } + }, + "com.amazonaws.bedrockruntime#Timestamp": { + "type": "timestamp", + "traits": { + "smithy.api#timestampFormat": "date-time" + } + }, + "com.amazonaws.bedrockruntime#TokenUsage": { + "type": "structure", + "members": { + "inputTokens": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The number of tokens sent in the request to the model.

", + "smithy.api#range": { + "min": 0 + }, + "smithy.api#required": {} + } + }, + "outputTokens": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The number of tokens that the model generated for the request.

", + "smithy.api#range": { + "min": 0 + }, + "smithy.api#required": {} + } + }, + "totalTokens": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The total of input tokens and tokens generated by the model.

", + "smithy.api#range": { + "min": 0 + }, + "smithy.api#required": {} + } + }, + "cacheReadInputTokens": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The number of input tokens read from the cache for the request.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "cacheWriteInputTokens": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "

The number of input tokens written to the cache for the request.

", + "smithy.api#range": { + "min": 0 + } + } + }, + "cacheDetails": { + "target": "com.amazonaws.bedrockruntime#CacheDetailsList", + "traits": { + "smithy.api#documentation": "

Detailed breakdown of cache writes by TTL. Empty if no cache creation occurred. Sorted by TTL duration (1h before 5m).

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The tokens used in a message API inference call.

" + } + }, + "com.amazonaws.bedrockruntime#TokenizerResource": { + "type": "resource", + "operations": [ + { + "target": "com.amazonaws.bedrockruntime#CountTokens" + } + ] + }, + "com.amazonaws.bedrockruntime#Tool": { + "type": "union", + "members": { + "toolSpec": { + "target": "com.amazonaws.bedrockruntime#ToolSpecification", + "traits": { + "smithy.api#documentation": "

The specfication for the tool.

" + } + }, + "systemTool": { + "target": "com.amazonaws.bedrockruntime#SystemTool", + "traits": { + "smithy.api#documentation": "

Specifies the system-defined tool that you want use.

" + } + }, + "cachePoint": { + "target": "com.amazonaws.bedrockruntime#CachePointBlock", + "traits": { + "smithy.api#documentation": "

CachePoint to include in the tool configuration.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Information about a tool that you can use with the Converse API. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolChoice": { + "type": "union", + "members": { + "auto": { + "target": "com.amazonaws.bedrockruntime#AutoToolChoice", + "traits": { + "smithy.api#documentation": "

(Default). The Model automatically decides if a tool should be called or whether to generate text instead.

" + } + }, + "any": { + "target": "com.amazonaws.bedrockruntime#AnyToolChoice", + "traits": { + "smithy.api#documentation": "

The model must request at least one tool (no text is generated).

" + } + }, + "tool": { + "target": "com.amazonaws.bedrockruntime#SpecificToolChoice", + "traits": { + "smithy.api#documentation": "

The Model must request the specified tool. Only supported by Anthropic Claude 3 and Amazon Nova models.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Determines which tools the model should request in a call to Converse or ConverseStream. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolConfiguration": { + "type": "structure", + "members": { + "tools": { + "target": "com.amazonaws.bedrockruntime#Tools", + "traits": { + "smithy.api#documentation": "

An array of tools that you want to pass to a model.

", + "smithy.api#length": { + "min": 1 + }, + "smithy.api#required": {} + } + }, + "toolChoice": { + "target": "com.amazonaws.bedrockruntime#ToolChoice", + "traits": { + "smithy.api#documentation": "

If supported by model, forces the model to request a tool.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Configuration information for the tools that you pass to a model. For more information, see Tool use (function calling) in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolInputSchema": { + "type": "union", + "members": { + "json": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "

The JSON schema for the tool. For more information, see JSON Schema Reference.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The schema for the tool. The top level schema type must be object. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolName": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 64 + }, + "smithy.api#pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "com.amazonaws.bedrockruntime#ToolResultBlock": { + "type": "structure", + "members": { + "toolUseId": { + "target": "com.amazonaws.bedrockruntime#ToolUseId", + "traits": { + "smithy.api#documentation": "

The ID of the tool request that this is the result for.

", + "smithy.api#required": {} + } + }, + "content": { + "target": "com.amazonaws.bedrockruntime#ToolResultContentBlocks", + "traits": { + "smithy.api#documentation": "

The content for tool result content block.

", + "smithy.api#required": {} + } + }, + "status": { + "target": "com.amazonaws.bedrockruntime#ToolResultStatus", + "traits": { + "smithy.api#documentation": "

The status for the tool result content block.

This field is only supported by Amazon Nova and Anthropic Claude 3 and 4 models.

" + } + }, + "type": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The type for the tool result content block.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A tool result block that contains the results for a tool request that the model previously made. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolResultBlockDelta": { + "type": "union", + "members": { + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The reasoning the model used to return the output.

" + } + }, + "json": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "

The JSON schema for the tool result content block. see JSON Schema Reference.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Contains incremental updates to tool results information during streaming responses. This allows clients to build up tool results data progressively as the response is generated.

" + } + }, + "com.amazonaws.bedrockruntime#ToolResultBlockStart": { + "type": "structure", + "members": { + "toolUseId": { + "target": "com.amazonaws.bedrockruntime#ToolUseId", + "traits": { + "smithy.api#documentation": "

The ID of the tool that was used to generate this tool result block.

", + "smithy.api#required": {} + } + }, + "type": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The type for the tool that was used to generate this tool result block.

" + } + }, + "status": { + "target": "com.amazonaws.bedrockruntime#ToolResultStatus", + "traits": { + "smithy.api#documentation": "

The status of the tool result block.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The start of a tool result block. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolResultBlocksDelta": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#ToolResultBlockDelta" + } + }, + "com.amazonaws.bedrockruntime#ToolResultContentBlock": { + "type": "union", + "members": { + "json": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "

A tool result that is JSON format data.

" + } + }, + "text": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

A tool result that is text.

" + } + }, + "image": { + "target": "com.amazonaws.bedrockruntime#ImageBlock", + "traits": { + "smithy.api#documentation": "

A tool result that is an image.

This field is only supported by Amazon Nova and Anthropic Claude 3 and 4 models.

" + } + }, + "document": { + "target": "com.amazonaws.bedrockruntime#DocumentBlock", + "traits": { + "smithy.api#documentation": "

A tool result that is a document.

" + } + }, + "video": { + "target": "com.amazonaws.bedrockruntime#VideoBlock", + "traits": { + "smithy.api#documentation": "

A tool result that is video.

" + } + }, + "searchResult": { + "target": "com.amazonaws.bedrockruntime#SearchResultBlock", + "traits": { + "smithy.api#documentation": "

A tool result that is a search result.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The tool result content block. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolResultContentBlocks": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#ToolResultContentBlock" + } + }, + "com.amazonaws.bedrockruntime#ToolResultStatus": { + "type": "enum", + "members": { + "SUCCESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "success" + } + }, + "ERROR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "error" + } + } + } + }, + "com.amazonaws.bedrockruntime#ToolSpecification": { + "type": "structure", + "members": { + "name": { + "target": "com.amazonaws.bedrockruntime#ToolName", + "traits": { + "smithy.api#documentation": "

The name for the tool.

", + "smithy.api#required": {} + } + }, + "description": { + "target": "com.amazonaws.bedrockruntime#NonEmptyString", + "traits": { + "smithy.api#documentation": "

The description for the tool.

" + } + }, + "inputSchema": { + "target": "com.amazonaws.bedrockruntime#ToolInputSchema", + "traits": { + "smithy.api#documentation": "

The input schema for the tool in JSON format.

", + "smithy.api#required": {} + } + }, + "strict": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "

Flag to enable structured output enforcement on a tool usage response.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The specification for the tool. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolUseBlock": { + "type": "structure", + "members": { + "toolUseId": { + "target": "com.amazonaws.bedrockruntime#ToolUseId", + "traits": { + "smithy.api#documentation": "

The ID for the tool request.

", + "smithy.api#required": {} + } + }, + "name": { + "target": "com.amazonaws.bedrockruntime#ToolName", + "traits": { + "smithy.api#documentation": "

The name of the tool that the model wants to use.

", + "smithy.api#required": {} + } + }, + "input": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "

The input to pass to the tool.

", + "smithy.api#required": {} + } + }, + "type": { + "target": "com.amazonaws.bedrockruntime#ToolUseType", + "traits": { + "smithy.api#documentation": "

The type for the tool request.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A tool use content block. Contains information about a tool that the model is requesting be run., The model uses the result from the tool to generate a response. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolUseBlockDelta": { + "type": "structure", + "members": { + "input": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The input for a requested tool.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The delta for a tool use block.

" + } + }, + "com.amazonaws.bedrockruntime#ToolUseBlockStart": { + "type": "structure", + "members": { + "toolUseId": { + "target": "com.amazonaws.bedrockruntime#ToolUseId", + "traits": { + "smithy.api#documentation": "

The ID for the tool request.

", + "smithy.api#required": {} + } + }, + "name": { + "target": "com.amazonaws.bedrockruntime#ToolName", + "traits": { + "smithy.api#documentation": "

The name of the tool that the model is requesting to use.

", + "smithy.api#required": {} + } + }, + "type": { + "target": "com.amazonaws.bedrockruntime#ToolUseType", + "traits": { + "smithy.api#documentation": "

The type for the tool request.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The start of a tool use block. For more information, see Call a tool with the Converse API in the Amazon Bedrock User Guide.

" + } + }, + "com.amazonaws.bedrockruntime#ToolUseId": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 64 + }, + "smithy.api#pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "com.amazonaws.bedrockruntime#ToolUseType": { + "type": "enum", + "members": { + "SERVER_TOOL_USE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "server_tool_use" + } + } + } + }, + "com.amazonaws.bedrockruntime#Tools": { + "type": "list", + "member": { + "target": "com.amazonaws.bedrockruntime#Tool" + } + }, + "com.amazonaws.bedrockruntime#Trace": { + "type": "enum", + "members": { + "ENABLED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ENABLED" + } + }, + "DISABLED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DISABLED" + } + }, + "ENABLED_FULL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ENABLED_FULL" + } + } + } + }, + "com.amazonaws.bedrockruntime#ValidationException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.bedrockruntime#NonBlankString" + } + }, + "traits": { + "smithy.api#documentation": "

The input fails to satisfy the constraints specified by Amazon Bedrock. For troubleshooting this error, see ValidationError in the Amazon Bedrock User Guide

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.bedrockruntime#VideoBlock": { + "type": "structure", + "members": { + "format": { + "target": "com.amazonaws.bedrockruntime#VideoFormat", + "traits": { + "smithy.api#documentation": "

The block's format.

", + "smithy.api#required": {} + } + }, + "source": { + "target": "com.amazonaws.bedrockruntime#VideoSource", + "traits": { + "smithy.api#documentation": "

The block's source.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A video block.

" + } + }, + "com.amazonaws.bedrockruntime#VideoFormat": { + "type": "enum", + "members": { + "MKV": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mkv" + } + }, + "MOV": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mov" + } + }, + "MP4": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mp4" + } + }, + "WEBM": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "webm" + } + }, + "FLV": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "flv" + } + }, + "MPEG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mpeg" + } + }, + "MPG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "mpg" + } + }, + "WMV": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "wmv" + } + }, + "THREE_GP": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "three_gp" + } + } + } + }, + "com.amazonaws.bedrockruntime#VideoSource": { + "type": "union", + "members": { + "bytes": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#documentation": "

Video content encoded in base64.

", + "smithy.api#length": { + "min": 1 + } + } + }, + "s3Location": { + "target": "com.amazonaws.bedrockruntime#S3Location", + "traits": { + "smithy.api#documentation": "

The location of a video object in an Amazon S3 bucket. To see which models support S3 uploads, see Supported models and features for Converse.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A video source. You can upload a smaller video as a base64-encoded string as long as the encoded file is less than 25MB. You can also transfer videos up to 1GB in size from an S3 bucket.

" + } + }, + "com.amazonaws.bedrockruntime#WebLocation": { + "type": "structure", + "members": { + "url": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The URL that was cited when performing a web search.

" + } + }, + "domain": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "

The domain that was cited when performing a web search.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Provides the URL and domain information for the website that was cited when performing a web search.

" + } + } + } +} \ No newline at end of file diff --git a/strands-agentcore-smithy/infrastructure/template.yaml b/strands-agentcore-smithy/infrastructure/template.yaml new file mode 100644 index 0000000000..8192d20cf0 --- /dev/null +++ b/strands-agentcore-smithy/infrastructure/template.yaml @@ -0,0 +1,219 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + AgentCore Smithy Bedrock Runtime - Serverless AI agent exposing Bedrock Runtime + operations through AWS Bedrock AgentCore Gateway using a Smithy model target. + Built and deployed with AWS SAM (Makefile build, no Docker required). + +Parameters: + GatewayName: + Type: String + Default: smithy-bedrock-gateway + Description: Name for the AgentCore Gateway + +Resources: + # ============================================================ + # Cognito User Pool and Client + # ============================================================ + CognitoUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: !Sub '${AWS::StackName}-UserPool' + AutoVerifiedAttributes: + - email + Schema: + - Name: email + Required: true + Mutable: true + + CognitoUserPoolClient: + Type: AWS::Cognito::UserPoolClient + Properties: + ClientName: !Sub '${AWS::StackName}-Client' + UserPoolId: !Ref CognitoUserPool + ExplicitAuthFlows: + - USER_PASSWORD_AUTH + GenerateSecret: false + + # ============================================================ + # Gateway Execution Role + # ============================================================ + GatewayExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${AWS::StackName}-GatewayExecutionRole' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: bedrock-agentcore.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: AgentCoreAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - bedrock-agentcore:* + Resource: + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:token-vault/default' + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:token-vault/default/apikeycredentialprovider/*' + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default' + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default/workload-identity/${GatewayName}-*' + - PolicyName: BedrockRuntimeAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + - bedrock:Converse + - bedrock:ConverseStream + Resource: + - !Sub 'arn:aws:bedrock:${AWS::Region}::foundation-model/*' + - !Sub 'arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/*' + - '*' + - PolicyName: SmithyModelS3Access + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - s3:GetObject + Resource: + - !Sub 'arn:aws:s3:::${AWS::StackName}-smithy-models/*' + + # ============================================================ + # AgentCore Gateway + # ============================================================ + AgentCoreGateway: + Type: AWS::BedrockAgentCore::Gateway + Properties: + Name: !Ref GatewayName + ProtocolType: MCP + ExceptionLevel: DEBUG + AuthorizerType: CUSTOM_JWT + AuthorizerConfiguration: + CustomJWTAuthorizer: + DiscoveryUrl: !Sub 'https://cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}/.well-known/openid-configuration' + AllowedAudience: + - !Ref CognitoUserPoolClient + RoleArn: !GetAtt GatewayExecutionRole.Arn + + # ============================================================ + # AgentCore Gateway Target (Smithy Model) + # ============================================================ + SmithyTarget: + Type: AWS::BedrockAgentCore::GatewayTarget + Properties: + GatewayIdentifier: !Ref AgentCoreGateway + Name: bedrock-runtime-target + CredentialProviderConfigurations: + - CredentialProviderType: GATEWAY_IAM_ROLE + TargetConfiguration: + Mcp: + SmithyModel: + S3: + Uri: !Sub 's3://${AWS::StackName}-smithy-models/bedrock-runtime-2023-09-30.json' + + # ============================================================ + # Agent Lambda Execution Role + # ============================================================ + AgentLambdaRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${AWS::StackName}-AgentLambdaRole' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: BedrockAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + - bedrock:Converse + - bedrock:ConverseStream + Resource: '*' + - PolicyName: AgentCoreAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - bedrock-agentcore:InvokeGateway + Resource: + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:gateway/${AgentCoreGateway}' + - PolicyName: CloudWatchLogsAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: '*' + + # ============================================================ + # Agent Lambda Function (SAM Serverless Function, Makefile build) + # ============================================================ + AgentLambdaFunction: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: makefile + Properties: + FunctionName: !Sub '${AWS::StackName}-AgentFunction' + CodeUri: ../ + Handler: src.agent.handler.lambda_handler + Runtime: python3.12 + Architectures: + - x86_64 + Role: !GetAtt AgentLambdaRole.Arn + Timeout: 120 + MemorySize: 1024 + Environment: + Variables: + GATEWAY_ID: !Ref AgentCoreGateway + GATEWAY_URL: !GetAtt AgentCoreGateway.GatewayUrl + COGNITO_USER_POOL_ID: !Ref CognitoUserPool + COGNITO_CLIENT_ID: !Ref CognitoUserPoolClient + AWS_REGION_NAME: !Ref AWS::Region + +# ============================================================ +# Stack Outputs +# ============================================================ +Outputs: + GatewayId: + Description: AgentCore Gateway ID + Value: !Ref AgentCoreGateway + Export: + Name: !Sub '${AWS::StackName}-GatewayId' + + CognitoUserPoolId: + Description: Cognito User Pool ID + Value: !Ref CognitoUserPool + Export: + Name: !Sub '${AWS::StackName}-CognitoUserPoolId' + + CognitoClientId: + Description: Cognito User Pool Client ID + Value: !Ref CognitoUserPoolClient + Export: + Name: !Sub '${AWS::StackName}-CognitoClientId' + + LambdaFunctionName: + Description: Agent Lambda Function Name + Value: !Ref AgentLambdaFunction + Export: + Name: !Sub '${AWS::StackName}-LambdaFunctionName' diff --git a/strands-agentcore-smithy/requirements.txt b/strands-agentcore-smithy/requirements.txt new file mode 100644 index 0000000000..d4a19fa8e1 --- /dev/null +++ b/strands-agentcore-smithy/requirements.txt @@ -0,0 +1,4 @@ +strands-agents +mcp +requests +PyJWT[crypto] diff --git a/strands-agentcore-smithy/scripts/deploy.sh b/strands-agentcore-smithy/scripts/deploy.sh new file mode 100755 index 0000000000..9d17a4df28 --- /dev/null +++ b/strands-agentcore-smithy/scripts/deploy.sh @@ -0,0 +1,240 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================ +# AgentCore Smithy Bedrock - Deployment Script (AWS SAM) +# ============================================================ +# Orchestrates the full deployment lifecycle with AWS SAM: +# 1. Validate the SAM template +# 2. Upload the official Smithy model to S3 +# 3. Build the Lambda with SAM (Makefile build — no Docker) +# 4. Deploy the stack with SAM +# 5. Create a Cognito test user +# 6. Generate scripts/test.sh +# ============================================================ + +STACK_NAME="${STACK_NAME:-agentcore-smithy-bedrock}" +REGION="${AWS_REGION:-us-east-1}" +TEMPLATE_PATH="infrastructure/template.yaml" +TEST_USERNAME="testuser" +TEST_PASSWORD="TestPass123!" + +echo "============================================================" +echo "Deploying stack: ${STACK_NAME} in region: ${REGION}" +echo "============================================================" + +# ============================================================ +# Step 1: Validate the SAM template +# ============================================================ +echo "" +echo ">>> Step 1: Validating SAM template..." +sam validate \ + --template-file "${TEMPLATE_PATH}" \ + --region "${REGION}" > /dev/null +echo "Template validation successful." + +# ============================================================ +# Step 2: Upload official Smithy model to S3 +# ============================================================ +echo "" +echo ">>> Step 2: Uploading Bedrock Runtime Smithy model to S3..." +SMITHY_BUCKET="${STACK_NAME}-smithy-models" +SMITHY_MODEL_URL="https://raw.githubusercontent.com/aws/api-models-aws/main/models/bedrock-runtime/service/2023-09-30/bedrock-runtime-2023-09-30.json" +SMITHY_MODEL_FILE="/tmp/bedrock-runtime-2023-09-30.json" + +curl -sL "${SMITHY_MODEL_URL}" -o "${SMITHY_MODEL_FILE}" +aws s3 mb "s3://${SMITHY_BUCKET}" --region "${REGION}" 2>/dev/null || true +aws s3 cp "${SMITHY_MODEL_FILE}" "s3://${SMITHY_BUCKET}/bedrock-runtime-2023-09-30.json" --region "${REGION}" +echo "Smithy model uploaded to s3://${SMITHY_BUCKET}/bedrock-runtime-2023-09-30.json" + +# ============================================================ +# Step 3: Build the Lambda with SAM (Makefile build, no Docker) +# ============================================================ +echo "" +echo ">>> Step 3: Building Lambda with SAM (Makefile build)..." +sam build \ + --template-file "${TEMPLATE_PATH}" +echo "SAM build complete." + +# ============================================================ +# Step 4: Deploy the stack with SAM +# ============================================================ +echo "" +echo ">>> Step 4: Deploying stack with SAM..." +sam deploy \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" \ + --capabilities CAPABILITY_NAMED_IAM \ + --resolve-s3 \ + --no-confirm-changeset \ + --no-fail-on-empty-changeset \ + --parameter-overrides "GatewayName=smithy-bedrock-gateway" +echo "SAM deploy complete." + +# ============================================================ +# Retrieve stack outputs +# ============================================================ +echo "" +echo ">>> Retrieving stack outputs..." + +GATEWAY_ID=$(aws cloudformation describe-stacks \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" \ + --query "Stacks[0].Outputs[?OutputKey=='GatewayId'].OutputValue" \ + --output text) + +COGNITO_USER_POOL_ID=$(aws cloudformation describe-stacks \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" \ + --query "Stacks[0].Outputs[?OutputKey=='CognitoUserPoolId'].OutputValue" \ + --output text) + +COGNITO_CLIENT_ID=$(aws cloudformation describe-stacks \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" \ + --query "Stacks[0].Outputs[?OutputKey=='CognitoClientId'].OutputValue" \ + --output text) + +LAMBDA_FUNCTION_NAME=$(aws cloudformation describe-stacks \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" \ + --query "Stacks[0].Outputs[?OutputKey=='LambdaFunctionName'].OutputValue" \ + --output text) + +echo "Gateway ID: ${GATEWAY_ID}" +echo "Cognito User Pool ID: ${COGNITO_USER_POOL_ID}" +echo "Cognito Client ID: ${COGNITO_CLIENT_ID}" +echo "Lambda Function: ${LAMBDA_FUNCTION_NAME}" + +# ============================================================ +# Step 5: Create Cognito test user +# ============================================================ +echo "" +echo ">>> Step 5: Creating Cognito test user..." + +aws cognito-idp admin-create-user \ + --user-pool-id "${COGNITO_USER_POOL_ID}" \ + --username "${TEST_USERNAME}" \ + --temporary-password "${TEST_PASSWORD}" \ + --message-action SUPPRESS \ + --region "${REGION}" 2>/dev/null || echo "User may already exist, continuing..." + +aws cognito-idp admin-set-user-password \ + --user-pool-id "${COGNITO_USER_POOL_ID}" \ + --username "${TEST_USERNAME}" \ + --password "${TEST_PASSWORD}" \ + --permanent \ + --region "${REGION}" + +echo "Cognito test user created (username: ${TEST_USERNAME})." + +# ============================================================ +# Step 6: Generate test script (scripts/test.sh) +# ============================================================ +echo "" +echo ">>> Step 6: Generating test script..." + +cat > scripts/test.sh << 'TESTSCRIPT_EOF' +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================ +# AgentCore Smithy Bedrock - Test Script +# Generated by deploy.sh — do not edit manually +# ============================================================ + +TESTSCRIPT_EOF + +# Append baked-in configuration values (not inside heredoc to allow variable expansion) +cat >> scripts/test.sh << BAKED_VALUES_EOF +REGION="${REGION}" +GATEWAY_ID="${GATEWAY_ID}" +COGNITO_USER_POOL_ID="${COGNITO_USER_POOL_ID}" +COGNITO_CLIENT_ID="${COGNITO_CLIENT_ID}" +LAMBDA_FUNCTION_NAME="${LAMBDA_FUNCTION_NAME}" +TEST_USERNAME="${TEST_USERNAME}" +TEST_PASSWORD="${TEST_PASSWORD}" +BAKED_VALUES_EOF + +# Append the rest of the script logic using a non-expanding heredoc +cat >> scripts/test.sh << 'TESTSCRIPT_LOGIC_EOF' + +PROMPT="${1:-Use the invoke model tool to ask Claude Haiku to write a short poem about the Beatles}" + +echo "============================================================" +echo "Testing AgentCore Smithy Bedrock Agent" +echo "============================================================" +echo "Region: ${REGION}" +echo "Lambda: ${LAMBDA_FUNCTION_NAME}" +echo "Prompt: ${PROMPT}" +echo "" + +# Authenticate with Cognito +echo ">>> Authenticating with Cognito..." +AUTH_RESULT=$(aws cognito-idp initiate-auth \ + --auth-flow USER_PASSWORD_AUTH \ + --client-id "${COGNITO_CLIENT_ID}" \ + --auth-parameters USERNAME="${TEST_USERNAME}",PASSWORD="${TEST_PASSWORD}" \ + --region "${REGION}" \ + --query "AuthenticationResult.IdToken" \ + --output text) + +ID_TOKEN="${AUTH_RESULT}" +echo "Authentication successful." + +# Build the Lambda payload +PAYLOAD_FILE="/tmp/test-payload-$$.json" +rm -f "${PAYLOAD_FILE}" +cat > "${PAYLOAD_FILE}" << PAYLOAD_INNER_EOF +{ + "headers": { + "Authorization": "Bearer ${ID_TOKEN}" + }, + "body": { + "prompt": "${PROMPT}" + } +} +PAYLOAD_INNER_EOF + +# Invoke the Lambda function +echo "" +echo ">>> Invoking Lambda function..." +OUTPUT_FILE="/tmp/test-output-$$.json" +rm -f "${OUTPUT_FILE}" + +aws lambda invoke \ + --function-name "${LAMBDA_FUNCTION_NAME}" \ + --payload "file://${PAYLOAD_FILE}" \ + --region "${REGION}" \ + --cli-binary-format raw-in-base64-out \ + "${OUTPUT_FILE}" > /dev/null + +echo "" +echo ">>> Response:" +cat "${OUTPUT_FILE}" +echo "" + +# Cleanup +rm -f "${PAYLOAD_FILE}" "${OUTPUT_FILE}" + +echo "" +echo "============================================================" +echo "Test complete." +echo "============================================================" +TESTSCRIPT_LOGIC_EOF + +chmod +x scripts/test.sh +echo "Test script generated at scripts/test.sh" + +# ============================================================ +# Deployment complete +# ============================================================ +echo "" +echo "============================================================" +echo "Deployment complete!" +echo "============================================================" +echo "" +echo "To test the agent, run:" +echo " ./scripts/test.sh" +echo " ./scripts/test.sh \"Use the invoke model tool to ask Haiku to write a short poem about the Beatles\"" +echo "" diff --git a/strands-agentcore-smithy/scripts/test.sh b/strands-agentcore-smithy/scripts/test.sh new file mode 100755 index 0000000000..bed5ae17b4 --- /dev/null +++ b/strands-agentcore-smithy/scripts/test.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================ +# AgentCore Smithy Bedrock - Test Script +# Generated by deploy.sh — do not edit manually +# ============================================================ + +REGION="us-east-1" +GATEWAY_ID="smithy-bedrock-gateway-2icnnwzik5" +COGNITO_USER_POOL_ID="us-east-1_d0B5LdMQN" +COGNITO_CLIENT_ID="c2ns12guk72f41kvsqs9oqs6u" +LAMBDA_FUNCTION_NAME="agentcore-smithy-bedrock-AgentFunction" +TEST_USERNAME="testuser" +TEST_PASSWORD="TestPass123!" + +PROMPT="${1:-Use the invoke model tool to ask Claude Haiku to write a short poem about the Beatles}" + +echo "============================================================" +echo "Testing AgentCore Smithy Bedrock Agent" +echo "============================================================" +echo "Region: ${REGION}" +echo "Lambda: ${LAMBDA_FUNCTION_NAME}" +echo "Prompt: ${PROMPT}" +echo "" + +# Authenticate with Cognito +echo ">>> Authenticating with Cognito..." +AUTH_RESULT=$(aws cognito-idp initiate-auth \ + --auth-flow USER_PASSWORD_AUTH \ + --client-id "${COGNITO_CLIENT_ID}" \ + --auth-parameters USERNAME="${TEST_USERNAME}",PASSWORD="${TEST_PASSWORD}" \ + --region "${REGION}" \ + --query "AuthenticationResult.IdToken" \ + --output text) + +ID_TOKEN="${AUTH_RESULT}" +echo "Authentication successful." + +# Build the Lambda payload +PAYLOAD_FILE="/tmp/test-payload-$$.json" +rm -f "${PAYLOAD_FILE}" +cat > "${PAYLOAD_FILE}" << PAYLOAD_INNER_EOF +{ + "headers": { + "Authorization": "Bearer ${ID_TOKEN}" + }, + "body": { + "prompt": "${PROMPT}" + } +} +PAYLOAD_INNER_EOF + +# Invoke the Lambda function +echo "" +echo ">>> Invoking Lambda function..." +OUTPUT_FILE="/tmp/test-output-$$.json" +rm -f "${OUTPUT_FILE}" + +aws lambda invoke \ + --function-name "${LAMBDA_FUNCTION_NAME}" \ + --payload "file://${PAYLOAD_FILE}" \ + --region "${REGION}" \ + --cli-binary-format raw-in-base64-out \ + "${OUTPUT_FILE}" > /dev/null + +echo "" +echo ">>> Response:" +cat "${OUTPUT_FILE}" +echo "" + +# Cleanup +rm -f "${PAYLOAD_FILE}" "${OUTPUT_FILE}" + +echo "" +echo "============================================================" +echo "Test complete." +echo "============================================================" diff --git a/strands-agentcore-smithy/src/agent/__init__.py b/strands-agentcore-smithy/src/agent/__init__.py new file mode 100644 index 0000000000..2330772da6 --- /dev/null +++ b/strands-agentcore-smithy/src/agent/__init__.py @@ -0,0 +1,4 @@ +"""Agent modules for the Lambda function. + +Provides target-type agnostic handler, processor, and client factories. +""" diff --git a/strands-agentcore-smithy/src/agent/agent_processor.py b/strands-agentcore-smithy/src/agent/agent_processor.py new file mode 100644 index 0000000000..6ae57949df --- /dev/null +++ b/strands-agentcore-smithy/src/agent/agent_processor.py @@ -0,0 +1,77 @@ +"""Agent request processor with MCP session lifecycle management. + +Target-type agnostic processor that creates a Strands Agent with an MCP +client connected to AgentCore Gateway. Manages the MCP session in a +try/finally block to ensure cleanup on both success and failure paths. +""" + +import os + +from strands import Agent + +from src.agent.strands_client import create_bedrock_model, create_mcp_client +from src.shared.error_utils import format_internal_error_response +from src.shared.logging_utils import get_logger +from src.shared.models import AgentRequest, AgentResponse + +logger = get_logger(__name__) + +# Environment variable for the gateway URL +GATEWAY_URL_ENV = "GATEWAY_URL" + +SYSTEM_PROMPT = """You have access to Bedrock Runtime tools via MCP. Use the Converse tool (not InvokeModel) to call another model. + +When using bedrock-runtime-target___Converse: +- Set modelId to: anthropic.claude-haiku-4-5-20251001-v1:0 +- Set messages to an array with one object: [{"role": "user", "content": [{"text": "YOUR PROMPT HERE"}]}] +- The content field inside each message must be an array of objects with a "text" key +- Do NOT set Region, UseFIPS, UseDualStack, or Endpoint +- Only try ONCE — do not retry if it fails""" + + +def process_request(request: AgentRequest, gateway_url: str | None = None) -> AgentResponse: + """Process an agent request by invoking the Strands Agent with MCP tools. + + Creates an MCP client connected to the AgentCore Gateway, builds a Strands + Agent with a Bedrock model, and invokes the agent with the user's prompt. + The MCP session lifecycle is managed in a try/finally block — NOT a with + context manager — to ensure cleanup occurs even on exceptions. + + Args: + request: The agent request containing the prompt and user context. + gateway_url: Optional gateway URL override. If not provided, reads + from the GATEWAY_URL environment variable. + + Returns: + An AgentResponse with the agent's response on success, or an error + response on failure. + """ + url = gateway_url or os.environ.get(GATEWAY_URL_ENV) + if not url: + logger.error("Gateway URL not configured") + return format_internal_error_response("Gateway URL not configured") + + token = request.user_context.token + prompt = request.prompt + + logger.info("Processing request for user: %s", request.user_context.username) + + mcp_client = create_mcp_client(url, token) + try: + bedrock_model = create_bedrock_model() + agent = Agent(model=bedrock_model, tools=[mcp_client], system_prompt=SYSTEM_PROMPT) + result = agent(prompt) + logger.info("Agent completed successfully for user: %s", request.user_context.username) + return AgentResponse(success=True, response=str(result)) + except Exception as e: + logger.error("Agent processing failed: %s", e) + return format_internal_error_response("Agent processing error", exception=e) + finally: + logger.info("Cleaning up MCP client") + try: + mcp_client.cleanup() + except AttributeError: + try: + mcp_client.close() + except AttributeError: + pass diff --git a/strands-agentcore-smithy/src/agent/handler.py b/strands-agentcore-smithy/src/agent/handler.py new file mode 100644 index 0000000000..3db24815ae --- /dev/null +++ b/strands-agentcore-smithy/src/agent/handler.py @@ -0,0 +1,108 @@ +"""Lambda entry point for the agent function. + +Target-type agnostic handler that extracts JWT tokens from the Lambda event, +validates them, creates a UserContext, and delegates processing to the +agent_processor module. Returns AgentResponse as a dict suitable for +Lambda response serialization. +""" + +from dataclasses import asdict + +from src.agent.agent_processor import process_request +from src.shared.error_utils import ( + format_internal_error_response, + format_unauthorized_response, +) +from src.shared.jwt_utils import JWTValidationError, extract_username, validate_token +from src.shared.logging_utils import configure_logging, get_correlation_id +from src.shared.models import AgentRequest, UserContext + + +def _extract_token(event: dict) -> str | None: + """Extract JWT token from the Lambda event. + + Checks for the token in two locations: + 1. event["token"] — direct token field + 2. event["headers"]["Authorization"] — Bearer token from headers + + Args: + event: The Lambda event dictionary. + + Returns: + The JWT token string, or None if not found. + """ + token = event.get("token") + if token: + return token + + headers = event.get("headers", {}) + auth_header = headers.get("Authorization", "") + if auth_header.startswith("Bearer "): + return auth_header[len("Bearer "):] + + # Return raw Authorization header if present (non-Bearer) + return auth_header or None + + +def lambda_handler(event: dict, context) -> dict: + """Lambda entry point for agent invocations. + + Extracts and validates the JWT token from the event, builds an + AgentRequest with the user's prompt and context, and delegates + to the agent processor. Returns the AgentResponse as a dict. + + Args: + event: Lambda event containing token/headers and prompt. + context: Lambda context object (unused). + + Returns: + A dict representation of AgentResponse. + """ + correlation_id = get_correlation_id(event) + logger = configure_logging(correlation_id=correlation_id) + + logger.info("Agent handler invoked") + + # Extract JWT token + token = _extract_token(event) + if not token: + logger.warning("Missing JWT token in request") + return asdict(format_unauthorized_response("Missing authentication token")) + + # Validate JWT and extract username + try: + claims = validate_token(token) + username = extract_username(claims) + except JWTValidationError as e: + logger.warning("JWT validation failed: %s", e) + return asdict(format_unauthorized_response(str(e))) + + # Build request and process + user_context = UserContext(username=username, token=token) + prompt = event.get("prompt", "") + + # Also check nested body.prompt (from test script payloads) + if not prompt: + body = event.get("body", {}) + if isinstance(body, dict): + prompt = body.get("prompt", "") + elif isinstance(body, str): + import json + try: + body = json.loads(body) + prompt = body.get("prompt", "") + except (json.JSONDecodeError, AttributeError): + pass + + if not prompt: + logger.warning("Empty prompt received from user: %s", username) + return asdict(format_unauthorized_response("Missing prompt")) + + request = AgentRequest(prompt=prompt, user_context=user_context) + + try: + response = process_request(request) + return asdict(response) + except Exception as e: + logger.error("Unexpected error processing request: %s", e) + return asdict(format_internal_error_response("Unexpected error", exception=e)) diff --git a/strands-agentcore-smithy/src/agent/strands_client.py b/strands-agentcore-smithy/src/agent/strands_client.py new file mode 100644 index 0000000000..ccf4e73729 --- /dev/null +++ b/strands-agentcore-smithy/src/agent/strands_client.py @@ -0,0 +1,71 @@ +"""Factory functions for Strands SDK client configuration. + +Target-type agnostic factories for creating MCP clients and Bedrock model +instances. These functions connect to AgentCore Gateway via MCP protocol +and are unaware of the target type behind the gateway. +""" + +import logging + +from mcp.client.streamable_http import streamablehttp_client +from strands.models.bedrock import BedrockModel +from strands.tools.mcp import MCPClient + +logger = logging.getLogger(__name__) + +# Default Bedrock model ID for Claude Sonnet 4.6 +DEFAULT_MODEL_ID = "us.anthropic.claude-sonnet-4-6" + +# Default AWS region for Bedrock +DEFAULT_REGION = "us-east-1" + + +def create_mcp_client(gateway_url: str, token: str) -> MCPClient: + """Create an MCP client configured to connect to AgentCore Gateway. + + Uses streamable HTTP transport with a Bearer token for authentication. + The returned client is NOT used as a context manager — the Agent manages + the MCP session lifecycle, and cleanup should occur in a finally block. + + Args: + gateway_url: The AgentCore Gateway endpoint URL. + token: JWT token for gateway authentication. + + Returns: + An MCPClient instance configured for the gateway. + """ + logger.info("Creating MCP client for gateway: %s", gateway_url) + + mcp_client = MCPClient( + lambda: streamablehttp_client( + gateway_url, + headers={"Authorization": f"Bearer {token}"}, + ) + ) + + return mcp_client + + +def create_bedrock_model( + model_id: str = DEFAULT_MODEL_ID, + region_name: str = DEFAULT_REGION, +) -> BedrockModel: + """Create a Bedrock model instance for the Strands Agent. + + Configures Claude 3 Sonnet as the LLM provider via AWS Bedrock. + + Args: + model_id: Bedrock model identifier. Defaults to Claude 3 Sonnet. + region_name: AWS region for Bedrock API calls. Defaults to us-east-1. + + Returns: + A BedrockModel instance ready for use with a Strands Agent. + """ + logger.info("Creating Bedrock model: %s in %s", model_id, region_name) + + model = BedrockModel( + model_id=model_id, + region_name=region_name, + ) + + return model diff --git a/strands-agentcore-smithy/src/shared/__init__.py b/strands-agentcore-smithy/src/shared/__init__.py new file mode 100644 index 0000000000..9853d4aab7 --- /dev/null +++ b/strands-agentcore-smithy/src/shared/__init__.py @@ -0,0 +1,36 @@ +"""Shared modules for the agent application. + +Provides target-type agnostic data models, utilities, and helpers. +""" + +from src.shared.models import AgentRequest, AgentResponse, UserContext +from src.shared.jwt_utils import ( + JWTValidationError, + extract_username, + validate_token, +) +from src.shared.error_utils import ( + format_error_response, + format_internal_error_response, + format_unauthorized_response, +) +from src.shared.logging_utils import ( + configure_logging, + get_correlation_id, + get_logger, +) + +__all__ = [ + "UserContext", + "AgentRequest", + "AgentResponse", + "JWTValidationError", + "extract_username", + "validate_token", + "format_error_response", + "format_internal_error_response", + "format_unauthorized_response", + "configure_logging", + "get_correlation_id", + "get_logger", +] diff --git a/strands-agentcore-smithy/src/shared/error_utils.py b/strands-agentcore-smithy/src/shared/error_utils.py new file mode 100644 index 0000000000..ed45210270 --- /dev/null +++ b/strands-agentcore-smithy/src/shared/error_utils.py @@ -0,0 +1,75 @@ +"""Standardized error response formatting. + +Target-type agnostic error handling utilities that produce consistent +AgentResponse objects for error conditions across the agent application. +""" + +import logging +from typing import Optional + +from src.shared.models import AgentResponse + +logger = logging.getLogger(__name__) + + +def format_error_response( + error_message: str, + status_code: int = 500, + log_message: Optional[str] = None, +) -> AgentResponse: + """Create a standardized error AgentResponse. + + Logs the error and returns a consistent AgentResponse with success=False. + + Args: + error_message: User-facing error description. + status_code: HTTP-style status code for categorization (default 500). + log_message: Optional detailed message for logging. If not provided, + the error_message is logged. + + Returns: + An AgentResponse with success=False and the error details. + """ + log_msg = log_message or error_message + if status_code >= 500: + logger.error("Error [%d]: %s", status_code, log_msg) + else: + logger.warning("Error [%d]: %s", status_code, log_msg) + + return AgentResponse( + success=False, + response="", + error=error_message, + ) + + +def format_unauthorized_response(detail: str = "Unauthorized") -> AgentResponse: + """Create a 401 Unauthorized error response. + + Args: + detail: Specific unauthorized reason (default "Unauthorized"). + + Returns: + An AgentResponse for authentication failures. + """ + return format_error_response(detail, status_code=401) + + +def format_internal_error_response( + detail: str = "Internal server error", + exception: Optional[Exception] = None, +) -> AgentResponse: + """Create a 500 Internal Server Error response. + + Optionally logs the full exception for debugging while returning + a safe user-facing message. + + Args: + detail: User-facing error description. + exception: Optional exception to log for debugging. + + Returns: + An AgentResponse for internal server errors. + """ + log_msg = f"{detail}: {exception}" if exception else detail + return format_error_response(detail, status_code=500, log_message=log_msg) diff --git a/strands-agentcore-smithy/src/shared/jwt_utils.py b/strands-agentcore-smithy/src/shared/jwt_utils.py new file mode 100644 index 0000000000..f03dcc28de --- /dev/null +++ b/strands-agentcore-smithy/src/shared/jwt_utils.py @@ -0,0 +1,96 @@ +"""JWT validation utilities for Cognito token handling. + +Target-type agnostic JWT validation that works with any AgentCore Gateway +target type. Accepts both access and ID tokens, disables audience verification +(gateway handles it via AllowedAudience), and extracts the cognito:username claim. +""" + +import logging +from typing import Any, Dict + +import jwt + +logger = logging.getLogger(__name__) + +# Valid token_use claim values accepted by the validator +VALID_TOKEN_USE_VALUES = {"access", "id"} + + +class JWTValidationError(Exception): + """Raised when JWT validation fails.""" + pass + + +def validate_token(token: str, options: Dict[str, Any] | None = None) -> Dict[str, Any]: + """Decode and validate a JWT token. + + Decodes the token without signature verification (gateway handles full + verification via CUSTOM_JWT authorizer). Validates that the token_use + claim is either 'access' or 'id'. Disables audience verification since + the gateway validates audience via AllowedAudience. + + Args: + token: The JWT token string to validate. + options: Optional PyJWT decode options override. + + Returns: + The decoded JWT claims dictionary. + + Raises: + JWTValidationError: If the token is invalid, expired, or has an + unsupported token_use claim. + """ + if not token: + raise JWTValidationError("Token is empty or missing") + + decode_options = options or { + "verify_signature": False, + "verify_aud": False, + "verify_exp": True, + } + + try: + claims = jwt.decode( + token, + algorithms=["RS256"], + options=decode_options, + ) + except jwt.ExpiredSignatureError: + logger.warning("JWT token has expired") + raise JWTValidationError("Token has expired") + except jwt.DecodeError as e: + logger.warning("JWT decode error: %s", str(e)) + raise JWTValidationError(f"Invalid token: {e}") + except jwt.InvalidTokenError as e: + logger.warning("JWT validation error: %s", str(e)) + raise JWTValidationError(f"Token validation failed: {e}") + + # Validate token_use claim — accept both 'access' and 'id' + token_use = claims.get("token_use") + if token_use is not None and token_use not in VALID_TOKEN_USE_VALUES: + raise JWTValidationError( + f"Unsupported token_use: {token_use}. Must be one of: {VALID_TOKEN_USE_VALUES}" + ) + + return claims + + +def extract_username(claims: Dict[str, Any]) -> str: + """Extract the username from JWT claims. + + Uses the 'cognito:username' claim, which is the correct claim for + Cognito ID tokens. Does NOT fall back to 'username' or 'sub' claims. + + Args: + claims: Decoded JWT claims dictionary. + + Returns: + The username string from the cognito:username claim. + + Raises: + JWTValidationError: If the cognito:username claim is missing. + """ + username = claims.get("cognito:username") + if not username: + raise JWTValidationError("Missing 'cognito:username' claim in token") + return username diff --git a/strands-agentcore-smithy/src/shared/logging_utils.py b/strands-agentcore-smithy/src/shared/logging_utils.py new file mode 100644 index 0000000000..73199b4e74 --- /dev/null +++ b/strands-agentcore-smithy/src/shared/logging_utils.py @@ -0,0 +1,74 @@ +"""Structured logging with correlation IDs. + +Target-type agnostic logging utilities that provide consistent structured +logging across the agent application. Supports correlation IDs for request +tracing through Lambda invocations. +""" + +import logging +import uuid +from typing import Any, Dict, Optional + + +def get_correlation_id(event: Optional[Dict[str, Any]] = None) -> str: + """Extract or generate a correlation ID for request tracing. + + Attempts to extract a correlation ID from the Lambda event's request + context. Falls back to generating a new UUID if none is found. + + Args: + event: Optional Lambda event dictionary. + + Returns: + A correlation ID string. + """ + if event: + # Check for AWS request ID in the event context + request_context = event.get("requestContext", {}) + request_id = request_context.get("requestId") + if request_id: + return request_id + + return str(uuid.uuid4()) + + +def configure_logging( + level: int = logging.INFO, + correlation_id: Optional[str] = None, +) -> logging.Logger: + """Configure structured logging for the application. + + Sets up the root logger with a structured format that includes + the correlation ID for request tracing. + + Args: + level: Logging level (default INFO). + correlation_id: Optional correlation ID to include in log format. + + Returns: + The configured root logger. + """ + log_format = "[%(levelname)s]" + if correlation_id: + log_format += f" [{correlation_id}]" + log_format += " %(name)s - %(message)s" + + logging.basicConfig(level=level, format=log_format, force=True) + # Enable debug logging for strands to see tool call details + logging.getLogger("strands").setLevel(logging.DEBUG) + return logging.getLogger() + + +def get_logger(name: str) -> logging.Logger: + """Get a named logger instance. + + Convenience wrapper around logging.getLogger for consistent + logger creation across modules. + + Args: + name: Logger name, typically __name__ of the calling module. + + Returns: + A named Logger instance. + """ + return logging.getLogger(name) diff --git a/strands-agentcore-smithy/src/shared/models.py b/strands-agentcore-smithy/src/shared/models.py new file mode 100644 index 0000000000..5f02724b5c --- /dev/null +++ b/strands-agentcore-smithy/src/shared/models.py @@ -0,0 +1,29 @@ +"""Shared data models for the agent application. + +Target-type agnostic data classes used across agent and shared modules. +""" + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class UserContext: + """Represents an authenticated user's context.""" + username: str + token: str + + +@dataclass +class AgentRequest: + """Represents a request to the agent.""" + prompt: str + user_context: UserContext + + +@dataclass +class AgentResponse: + """Represents a response from the agent.""" + success: bool + response: str + error: Optional[str] = None diff --git a/strands-agentcore-smithy/tests/__init__.py b/strands-agentcore-smithy/tests/__init__.py new file mode 100644 index 0000000000..65140f2e39 --- /dev/null +++ b/strands-agentcore-smithy/tests/__init__.py @@ -0,0 +1 @@ +# tests package diff --git a/strands-agentcore-smithy/tests/conftest.py b/strands-agentcore-smithy/tests/conftest.py new file mode 100644 index 0000000000..aec84a94fe --- /dev/null +++ b/strands-agentcore-smithy/tests/conftest.py @@ -0,0 +1,140 @@ +"""Shared test fixtures for the agentcore-smithy-bedrock project.""" + +import json +import os +import sys +import time + +import pytest +import jwt +import yaml + +# Add src to path so shared modules are importable +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +_ROOT = os.path.join(os.path.dirname(__file__), "..") + +# --------------------------------------------------------------------------- +# Custom YAML loader for CloudFormation intrinsic functions +# --------------------------------------------------------------------------- + +class _CfnLoader(yaml.SafeLoader): + pass + + +def _cfn_tag_constructor(loader, tag_suffix, node): + if isinstance(node, yaml.ScalarNode): + return loader.construct_scalar(node) + if isinstance(node, yaml.SequenceNode): + return loader.construct_sequence(node) + if isinstance(node, yaml.MappingNode): + return loader.construct_mapping(node) + return None + + +_CfnLoader.add_multi_constructor("!", _cfn_tag_constructor) + +# --------------------------------------------------------------------------- +# CloudFormation template fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture(scope="session") +def cfn_template(): + """Load and return the parsed SAM/CloudFormation template.""" + path = os.path.join(_ROOT, "infrastructure", "template.yaml") + with open(path) as fh: + return yaml.load(fh, Loader=_CfnLoader) + + +@pytest.fixture(scope="session") +def cfn_resources(cfn_template): + """Return the Resources section of the CloudFormation template.""" + return cfn_template["Resources"] + + +@pytest.fixture(scope="session") +def cfn_outputs(cfn_template): + """Return the Outputs section of the CloudFormation template.""" + return cfn_template.get("Outputs", {}) + + +# --------------------------------------------------------------------------- +# Smithy model fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture(scope="session") +def smithy_model(cfn_resources): + """Parse and return the official AWS Smithy model.""" + model_path = os.path.join( + os.path.dirname(__file__), "..", "infrastructure", "bedrock-runtime-2023-09-30.json" + ) + if not os.path.exists(model_path): + model_path = "/tmp/bedrock-runtime-2023-09-30.json" + with open(model_path) as f: + return json.load(f) + + +@pytest.fixture(scope="session") +def smithy_shapes(smithy_model): + """Return the shapes dict from the Smithy model.""" + return smithy_model["shapes"] + + +# --------------------------------------------------------------------------- +# Deploy script fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture(scope="session") +def deploy_script_content(): + """Return the raw content of scripts/deploy.sh.""" + path = os.path.join(_ROOT, "scripts", "deploy.sh") + with open(path) as fh: + return fh.read() + + +# --------------------------------------------------------------------------- +# JWT / user context fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def make_jwt_token(): + """Factory fixture that creates JWT tokens with given claims.""" + + def _make(claims: dict, exp_offset: int = 3600) -> str: + payload = { + "iat": int(time.time()), + "exp": int(time.time()) + exp_offset, + **claims, + } + return jwt.encode(payload, "test-secret", algorithm="HS256") + + return _make + + +@pytest.fixture +def sample_id_token(make_jwt_token): + """A sample Cognito ID token with cognito:username claim.""" + return make_jwt_token({ + "token_use": "id", + "cognito:username": "testuser", + "sub": "abc-123", + }) + + +@pytest.fixture +def sample_access_token(make_jwt_token): + """A sample Cognito access token.""" + return make_jwt_token({ + "token_use": "access", + "username": "testuser", + "sub": "abc-123", + }) + + +@pytest.fixture +def sample_user_context(sample_id_token): + """A sample UserContext dict.""" + return { + "username": "testuser", + "token": sample_id_token, + } diff --git a/strands-agentcore-smithy/tests/property/__init__.py b/strands-agentcore-smithy/tests/property/__init__.py new file mode 100644 index 0000000000..b622bf8d9d --- /dev/null +++ b/strands-agentcore-smithy/tests/property/__init__.py @@ -0,0 +1 @@ +# property tests package diff --git a/strands-agentcore-smithy/tests/property/test_agnostic_properties.py b/strands-agentcore-smithy/tests/property/test_agnostic_properties.py new file mode 100644 index 0000000000..195c40fd6f --- /dev/null +++ b/strands-agentcore-smithy/tests/property/test_agnostic_properties.py @@ -0,0 +1,82 @@ +"""Property-based tests for agent code target-type agnosticism (Property 8). + +Feature: agentcore-smithy-bedrock +Property 8: Agent code is target-type agnostic + Validates: Requirements 11.1 + +Scans all Python source files in src/agent/ and src/shared/ to verify that +none contain DynamoDB-specific or Smithy-specific identifiers. +""" + +import os +import re + +from hypothesis import given, settings, assume +from hypothesis import strategies as st + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +_SRC_ROOT = os.path.join(os.path.dirname(__file__), "..", "..", "src") + +# Identifiers that must NOT appear in agent/shared code. +# Case-insensitive patterns for DynamoDB and Smithy references. +_FORBIDDEN_PATTERNS: list[re.Pattern] = [ + re.compile(r"\bdynamodb\b", re.IGNORECASE), + re.compile(r"\bGetItem\b"), + re.compile(r"\bPutItem\b"), + re.compile(r"\bQuery\b"), + re.compile(r"\bTableName\b"), + re.compile(r"\bsmithy\b", re.IGNORECASE), + re.compile(r"\bSmithyModel\b"), + re.compile(r"\bInlinePayload\b"), +] + +_SOURCE_DIRS = ["agent", "shared"] + + +def _collect_source_files() -> list[str]: + """Return all .py files under src/agent/ and src/shared/.""" + files: list[str] = [] + for subdir in _SOURCE_DIRS: + dirpath = os.path.join(_SRC_ROOT, subdir) + if not os.path.isdir(dirpath): + continue + for fname in os.listdir(dirpath): + if fname.endswith(".py"): + files.append(os.path.join(dirpath, fname)) + return files + + +_SOURCE_FILES = _collect_source_files() +_SOURCE_CONTENTS: dict[str, str] = {} +for _f in _SOURCE_FILES: + with open(_f) as _fh: + _SOURCE_CONTENTS[_f] = _fh.read() + + +# --------------------------------------------------------------------------- +# Property 8: Agent code is target-type agnostic +# Tag: Feature: agentcore-smithy-bedrock, Property 8: Agent code is +# target-type agnostic +# --------------------------------------------------------------------------- + +# Strategy: pick a random source file and a random forbidden pattern, +# then assert the pattern does not appear in that file. + +source_file_st = st.sampled_from(list(_SOURCE_CONTENTS.keys())) +forbidden_pattern_st = st.sampled_from(_FORBIDDEN_PATTERNS) + + +@given(filepath=source_file_st, pattern=forbidden_pattern_st) +@settings(max_examples=100) +def test_property8_no_dynamodb_or_smithy_identifiers(filepath, pattern): + """For all source files in src/agent/ and src/shared/, none shall + contain DynamoDB-specific or Smithy-specific identifiers.""" + content = _SOURCE_CONTENTS[filepath] + matches = pattern.findall(content) + rel = os.path.relpath(filepath, _SRC_ROOT) + assert not matches, ( + f"{rel} contains forbidden identifier matching /{pattern.pattern}/: {matches}" + ) diff --git a/strands-agentcore-smithy/tests/property/test_cloudformation_properties.py b/strands-agentcore-smithy/tests/property/test_cloudformation_properties.py new file mode 100644 index 0000000000..fe5b75cf69 --- /dev/null +++ b/strands-agentcore-smithy/tests/property/test_cloudformation_properties.py @@ -0,0 +1,252 @@ +"""Property-based tests for CloudFormation template (Properties 1, 2, 3, 6). + +Feature: agentcore-smithy-bedrock +Property 1: Smithy operation structure completeness + Validates: Requirements 1.5, 1.6 +Property 2: Smithy documentation coverage + Validates: Requirements 1.7 +Property 3: No streaming or custom protocols in Smithy model + Validates: Requirements 1.9 +Property 6: No API Gateway resources in CloudFormation template + Validates: Requirements 7.4 +""" + +import json +import os + +import yaml +from hypothesis import given, settings +from hypothesis import strategies as st + +# --------------------------------------------------------------------------- +# Custom YAML loader for CloudFormation intrinsic functions +# --------------------------------------------------------------------------- + + +class _CfnLoader(yaml.SafeLoader): + """YAML loader that handles CloudFormation intrinsic function tags.""" + pass + + +def _cfn_tag_constructor(loader, tag_suffix, node): + """Generic constructor that returns the tag value as-is.""" + if isinstance(node, yaml.ScalarNode): + return loader.construct_scalar(node) + if isinstance(node, yaml.SequenceNode): + return loader.construct_sequence(node) + if isinstance(node, yaml.MappingNode): + return loader.construct_mapping(node) + return None + + +_CfnLoader.add_multi_constructor("!", _cfn_tag_constructor) + +# --------------------------------------------------------------------------- +# Load CloudFormation template and extract Smithy model +# --------------------------------------------------------------------------- + +_TEMPLATE_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "infrastructure", "template.yaml" +) + +with open(_TEMPLATE_PATH) as _fh: + _TEMPLATE = yaml.load(_fh, Loader=_CfnLoader) + +_RESOURCES = _TEMPLATE["Resources"] + +# Load the official AWS Smithy model from local file +_SMITHY_MODEL_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "infrastructure", "bedrock-runtime-2023-09-30.json" +) +if not os.path.exists(_SMITHY_MODEL_PATH): + _SMITHY_MODEL_PATH = "/tmp/bedrock-runtime-2023-09-30.json" +with open(_SMITHY_MODEL_PATH) as _smithy_fh: + _SMITHY_PAYLOAD = _smithy_fh.read() +_SMITHY_MODEL = json.loads(_SMITHY_PAYLOAD) +_SHAPES = _SMITHY_MODEL["shapes"] + +# Identify the service shape and collect all operations (direct + from resources) +_SERVICE_SHAPE = None +_SERVICE_KEY = None +for _key, _shape in _SHAPES.items(): + if _shape.get("type") == "service": + _SERVICE_SHAPE = _shape + _SERVICE_KEY = _key + break + +_OPERATION_TARGETS = [op["target"] for op in _SERVICE_SHAPE.get("operations", [])] +# Also collect operations from resources +for _res in _SERVICE_SHAPE.get("resources", []): + _res_shape = _SHAPES.get(_res["target"], {}) + for _op in _res_shape.get("operations", []): + _OPERATION_TARGETS.append(_op["target"]) + for _op in _res_shape.get("collectionOperations", []): + _OPERATION_TARGETS.append(_op["target"]) +_OPERATIONS = {t: _SHAPES[t] for t in _OPERATION_TARGETS if t in _SHAPES} + +# Collect all shape keys for documentation checks +_ALL_SHAPE_KEYS = list(_SHAPES.keys()) + +# Collect all CFN resource types +_RESOURCE_TYPES = [ + res.get("Type", "") for res in _RESOURCES.values() if isinstance(res, dict) +] + +# --------------------------------------------------------------------------- +# Property 1: Smithy operation structure completeness +# Tag: Feature: agentcore-smithy-bedrock, Property 1: Smithy operation +# structure completeness +# --------------------------------------------------------------------------- + +operation_st = st.sampled_from(list(_OPERATIONS.keys())) + + +@given(op_key=operation_st) +@settings(max_examples=100) +def test_property1_operation_has_valid_input_with_required_traits(op_key): + """For all operations in the Smithy model, each must have a valid input + structure with mandatory members marked with smithy.api#required.""" + op = _OPERATIONS[op_key] + input_target = op["input"]["target"] + assert input_target in _SHAPES, f"Input shape {input_target} not found" + + input_shape = _SHAPES[input_target] + assert input_shape["type"] == "structure", f"{input_target} is not a structure" + assert "members" in input_shape, f"{input_target} has no members" + + # Check if any members are marked required (not all operations require this) + required_members = [ + name + for name, member in input_shape["members"].items() + if "smithy.api#required" in member.get("traits", {}) + ] + # Official AWS models may have operations with no required members (e.g. list operations) + # Just verify the structure is valid + assert isinstance(input_shape["members"], dict) + + +@given(op_key=operation_st) +@settings(max_examples=100) +def test_property1_operation_has_valid_output_structure(op_key): + """For all operations in the Smithy model, each must have a valid output + structure that captures the DynamoDB response shape.""" + op = _OPERATIONS[op_key] + output_target = op["output"]["target"] + assert output_target in _SHAPES, f"Output shape {output_target} not found" + + output_shape = _SHAPES[output_target] + assert output_shape["type"] == "structure", f"{output_target} is not a structure" + assert "members" in output_shape, f"{output_target} has no members" + assert len(output_shape["members"]) > 0, f"{output_target} has no response members" + + +# --------------------------------------------------------------------------- +# Property 2: Smithy documentation coverage +# Tag: Feature: agentcore-smithy-bedrock, Property 2: Smithy documentation +# coverage +# --------------------------------------------------------------------------- + +shape_key_st = st.sampled_from(_ALL_SHAPE_KEYS) + + +@given(shape_key=shape_key_st) +@settings(max_examples=100) +def test_property2_all_shapes_have_documentation(shape_key): + """For service and operation shapes in the Smithy model, each should include a + smithy.api#documentation trait. (Not all helper shapes in official models have docs.)""" + shape = _SHAPES[shape_key] + # Only check service and operation shapes for documentation + if shape.get("type") not in ("service", "operation"): + return + traits = shape.get("traits", {}) + doc = traits.get("smithy.api#documentation") + assert doc is not None, f"Shape {shape_key} missing smithy.api#documentation trait" + assert isinstance(doc, str) and len(doc.strip()) > 0, ( + f"Shape {shape_key} has empty documentation" + ) + + +@given(op_key=operation_st) +@settings(max_examples=100) +def test_property2_operation_members_have_documentation(op_key): + """For all operations, every member in input and output structures must + have a smithy.api#documentation trait.""" + op = _OPERATIONS[op_key] + for io_key in ("input", "output"): + target = op[io_key]["target"] + shape = _SHAPES[target] + for member_name, member in shape.get("members", {}).items(): + traits = member.get("traits", {}) + doc = traits.get("smithy.api#documentation") + assert doc is not None, ( + f"{target}.{member_name} missing smithy.api#documentation" + ) + assert isinstance(doc, str) and len(doc.strip()) > 0, ( + f"{target}.{member_name} has empty documentation" + ) + + +# --------------------------------------------------------------------------- +# Property 3: No streaming or custom protocols in Smithy model +# Tag: Feature: agentcore-smithy-bedrock, Property 3: No streaming or +# custom protocols in Smithy model +# --------------------------------------------------------------------------- + +_ALLOWED_PROTOCOL = "aws.protocols#restJson1" + + +@given(op_key=operation_st) +@settings(max_examples=100) +def test_property3_no_streaming_traits(op_key): + """For all operations in the Smithy model, none shall have streaming + traits (smithy.api#streaming).""" + op = _OPERATIONS[op_key] + op_traits = op.get("traits", {}) + assert "smithy.api#streaming" not in op_traits, ( + f"Operation {op_key} has a streaming trait" + ) + + # Also check input/output shapes for streaming + for io_key in ("input", "output"): + target = op[io_key]["target"] + shape = _SHAPES[target] + shape_traits = shape.get("traits", {}) + assert "smithy.api#streaming" not in shape_traits, ( + f"{target} has a streaming trait" + ) + + +@given(shape_key=shape_key_st) +@settings(max_examples=100) +def test_property3_only_restjson1_protocol(shape_key): + """For all shapes, any protocol trait must be aws.protocols#restJson1. + No custom or alternative protocols are allowed.""" + shape = _SHAPES[shape_key] + traits = shape.get("traits", {}) + for trait_key in traits: + if "protocol" in trait_key.lower(): + assert trait_key == _ALLOWED_PROTOCOL, ( + f"Shape {shape_key} uses disallowed protocol trait: {trait_key}" + ) + + +# --------------------------------------------------------------------------- +# Property 6: No API Gateway resources in CloudFormation template +# Tag: Feature: agentcore-smithy-bedrock, Property 6: No API Gateway +# resources in CloudFormation template +# --------------------------------------------------------------------------- + +resource_type_st = st.sampled_from(_RESOURCE_TYPES) + + +@given(resource_type=resource_type_st) +@settings(max_examples=100) +def test_property6_no_api_gateway_resources(resource_type): + """For all resources in the CloudFormation template, none shall have a + type matching AWS::ApiGateway::* or AWS::ApiGatewayV2::*.""" + assert not resource_type.startswith("AWS::ApiGateway::"), ( + f"Found API Gateway v1 resource: {resource_type}" + ) + assert not resource_type.startswith("AWS::ApiGatewayV2::"), ( + f"Found API Gateway v2 resource: {resource_type}" + ) diff --git a/strands-agentcore-smithy/tests/property/test_deploy_script_properties.py b/strands-agentcore-smithy/tests/property/test_deploy_script_properties.py new file mode 100644 index 0000000000..f1f489ddf6 --- /dev/null +++ b/strands-agentcore-smithy/tests/property/test_deploy_script_properties.py @@ -0,0 +1,50 @@ +"""Property-based tests for the Makefile build (Property 7). + +Feature: agentcore-smithy-bedrock +Property 7: Lambda packaging uses pip3 exclusively + Validates: pip3-only packaging + +After the migration to AWS SAM, Lambda packaging (the pip install steps) +lives in the root Makefile, invoked by `sam build` via BuildMethod: makefile. +This property verifies every pip install invocation uses pip3, not bare pip. +""" + +import os +import re + +from hypothesis import given, settings +from hypothesis import strategies as st + +# --------------------------------------------------------------------------- +# Load Makefile content (packaging now lives here, not in deploy.sh) +# --------------------------------------------------------------------------- + +_MAKEFILE_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "Makefile" +) + +with open(_MAKEFILE_PATH) as _fh: + _MAKEFILE_CONTENT = _fh.read() + +# Find all pip/pip3 install invocations +_PIP_INVOCATIONS = re.findall(r"\b(pip3?)\s+install\b", _MAKEFILE_CONTENT) + +assert len(_PIP_INVOCATIONS) > 0, "No pip install commands found in Makefile" + +pip_invocation_st = st.sampled_from(_PIP_INVOCATIONS) + +# --------------------------------------------------------------------------- +# Property 7: Lambda packaging uses pip3 exclusively +# Tag: Feature: agentcore-smithy-bedrock, Property 7: Lambda packaging uses +# pip3 exclusively +# --------------------------------------------------------------------------- + + +@given(invocation=pip_invocation_st) +@settings(max_examples=100) +def test_property7_all_pip_commands_use_pip3(invocation): + """For all pip install commands in the Makefile, each command shall + use pip3 (not pip) as the executable name.""" + assert invocation == "pip3", ( + "Found bare 'pip' instead of 'pip3' in Makefile" + ) diff --git a/strands-agentcore-smithy/tests/property/test_jwt_properties.py b/strands-agentcore-smithy/tests/property/test_jwt_properties.py new file mode 100644 index 0000000000..dab5feb774 --- /dev/null +++ b/strands-agentcore-smithy/tests/property/test_jwt_properties.py @@ -0,0 +1,120 @@ +"""Property-based tests for JWT validation (Properties 4 and 5). + +Feature: agentcore-smithy-bedrock +Property 4: JWT validation accepts both token_use values + Validates: Requirements 4.4 +Property 5: Username extraction from cognito:username claim + Validates: Requirements 4.6 +""" + +import time + +import jwt +import pytest +from hypothesis import given, settings, assume +from hypothesis import strategies as st + +from shared.jwt_utils import validate_token, extract_username, JWTValidationError + + +# --------------------------------------------------------------------------- +# Strategies +# --------------------------------------------------------------------------- + +# Printable text that won't be empty, for usernames +username_st = st.text( + alphabet=st.characters(whitelist_categories=("L", "N", "P")), + min_size=1, + max_size=64, +).filter(lambda s: s.strip() != "") + +token_use_st = st.sampled_from(["access", "id"]) + + +def _encode(claims: dict, exp_offset: int = 3600) -> str: + """Encode a JWT with HS256 for testing (no signature verification).""" + payload = { + "iat": int(time.time()), + "exp": int(time.time()) + exp_offset, + **claims, + } + return jwt.encode(payload, "test-secret", algorithm="HS256") + + +# --------------------------------------------------------------------------- +# Property 4: JWT validation accepts both token_use values +# Tag: Feature: agentcore-smithy-bedrock, Property 4: JWT validation accepts +# both token_use values +# --------------------------------------------------------------------------- + + +@given(token_use=token_use_st) +@settings(max_examples=100) +def test_property4_validate_token_accepts_access_and_id(token_use): + """For any valid JWT with token_use set to 'access' or 'id', + validate_token shall accept the token without raising an error.""" + token = _encode({"token_use": token_use, "cognito:username": "testuser"}) + claims = validate_token(token) + assert claims["token_use"] == token_use + + +@given( + token_use=token_use_st, + extra_claims=st.fixed_dictionaries( + {}, + optional={ + "sub": st.uuids().map(str), + "email": st.emails(), + "custom:role": st.sampled_from(["admin", "user", "viewer"]), + }, + ), +) +@settings(max_examples=100) +def test_property4_validate_token_with_extra_claims(token_use, extra_claims): + """Acceptance of token_use holds regardless of other claims present.""" + all_claims = { + "token_use": token_use, + "cognito:username": "anyuser", + **extra_claims, + } + token = _encode(all_claims) + claims = validate_token(token) + assert claims["token_use"] == token_use + + +# --------------------------------------------------------------------------- +# Property 5: Username extraction from cognito:username claim +# Tag: Feature: agentcore-smithy-bedrock, Property 5: Username extraction +# from cognito:username claim +# --------------------------------------------------------------------------- + + +@given(cognito_username=username_st) +@settings(max_examples=100) +def test_property5_extracts_cognito_username(cognito_username): + """For any JWT containing a cognito:username claim, extract_username + shall return that claim's value.""" + claims = {"cognito:username": cognito_username} + assert extract_username(claims) == cognito_username + + +@given( + cognito_username=username_st, + other_username=username_st, + sub_value=st.uuids().map(str), +) +@settings(max_examples=100) +def test_property5_cognito_username_preferred_over_others( + cognito_username, other_username, sub_value +): + """extract_username returns cognito:username even when 'username' and + 'sub' claims are also present with different values.""" + assume(cognito_username != other_username) + claims = { + "cognito:username": cognito_username, + "username": other_username, + "sub": sub_value, + } + result = extract_username(claims) + assert result == cognito_username + assert result != other_username diff --git a/strands-agentcore-smithy/tests/unit/__init__.py b/strands-agentcore-smithy/tests/unit/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/__init__.py @@ -0,0 +1 @@ + diff --git a/strands-agentcore-smithy/tests/unit/test_agent_processor.py b/strands-agentcore-smithy/tests/unit/test_agent_processor.py new file mode 100644 index 0000000000..d812ea8379 --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_agent_processor.py @@ -0,0 +1,143 @@ +"""Unit tests for the agent processor module. + +Tests MCP session lifecycle (try/finally), gateway URL resolution, +and error handling during agent invocation. + +Requirements: 5.4, 5.5 +""" + +import sys +from types import ModuleType +from unittest.mock import patch, MagicMock + +import pytest + +# Mock third-party modules not installed in test env +_strands_mod = ModuleType("strands") +_strands_mod.Agent = MagicMock() +_mcp_mod = ModuleType("mcp") +_mcp_client_mod = ModuleType("mcp.client") +_mcp_http_mod = ModuleType("mcp.client.streamable_http") +_mcp_http_mod.streamablehttp_client = MagicMock() +_bedrock_mod = ModuleType("strands.models") +_bedrock_bedrock_mod = ModuleType("strands.models.bedrock") +_bedrock_bedrock_mod.BedrockModel = MagicMock() +_strands_tools_mod = ModuleType("strands.tools") +_strands_tools_mcp_mod = ModuleType("strands.tools.mcp") +_strands_tools_mcp_mod.MCPClient = MagicMock() + +sys.modules.setdefault("strands", _strands_mod) +sys.modules.setdefault("strands.models", _bedrock_mod) +sys.modules.setdefault("strands.models.bedrock", _bedrock_bedrock_mod) +sys.modules.setdefault("strands.tools", _strands_tools_mod) +sys.modules.setdefault("strands.tools.mcp", _strands_tools_mcp_mod) +sys.modules.setdefault("mcp", _mcp_mod) +sys.modules.setdefault("mcp.client", _mcp_client_mod) +sys.modules.setdefault("mcp.client.streamable_http", _mcp_http_mod) + +from shared.models import UserContext, AgentRequest + + +def _make_request(prompt="test prompt", username="alice", token="jwt-tok"): + ctx = UserContext(username=username, token=token) + return AgentRequest(prompt=prompt, user_context=ctx) + + +class TestProcessRequest: + """Tests for process_request function.""" + + @patch("agent.agent_processor.Agent") + @patch("agent.agent_processor.create_bedrock_model") + @patch("agent.agent_processor.create_mcp_client") + def test_successful_invocation(self, mock_mcp, mock_model, mock_agent_cls): + agent_instance = MagicMock() + agent_instance.return_value = "Agent says hello" + mock_agent_cls.return_value = agent_instance + mock_mcp_client = MagicMock() + mock_mcp.return_value = mock_mcp_client + + from agent.agent_processor import process_request + result = process_request(_make_request(), gateway_url="https://gw.example.com") + + assert result.success is True + assert result.response == "Agent says hello" + mock_mcp.assert_called_once_with("https://gw.example.com", "jwt-tok") + mock_mcp_client.cleanup.assert_called_once() + + @patch("agent.agent_processor.Agent") + @patch("agent.agent_processor.create_bedrock_model") + @patch("agent.agent_processor.create_mcp_client") + def test_cleanup_called_on_agent_exception(self, mock_mcp, mock_model, mock_agent_cls): + """Verify MCP client cleanup happens even when the agent raises.""" + agent_instance = MagicMock() + agent_instance.side_effect = RuntimeError("LLM failed") + mock_agent_cls.return_value = agent_instance + mock_mcp_client = MagicMock() + mock_mcp.return_value = mock_mcp_client + + from agent.agent_processor import process_request + result = process_request(_make_request(), gateway_url="https://gw.example.com") + + assert result.success is False + assert result.error is not None + # The critical assertion: cleanup MUST be called even on failure + mock_mcp_client.cleanup.assert_called_once() + + @patch("agent.agent_processor.Agent") + @patch("agent.agent_processor.create_bedrock_model") + @patch("agent.agent_processor.create_mcp_client") + def test_cleanup_called_on_model_creation_exception(self, mock_mcp, mock_model, mock_agent_cls): + """Verify cleanup even if create_bedrock_model raises.""" + mock_model.side_effect = RuntimeError("Bedrock config error") + mock_mcp_client = MagicMock() + mock_mcp.return_value = mock_mcp_client + + from agent.agent_processor import process_request + result = process_request(_make_request(), gateway_url="https://gw.example.com") + + assert result.success is False + mock_mcp_client.cleanup.assert_called_once() + + def test_missing_gateway_url_returns_error(self): + """When no gateway URL is provided and env var is unset, return error.""" + from agent.agent_processor import process_request + with patch.dict("os.environ", {}, clear=True): + result = process_request(_make_request()) + + assert result.success is False + assert "gateway" in result.error.lower() + + @patch("agent.agent_processor.Agent") + @patch("agent.agent_processor.create_bedrock_model") + @patch("agent.agent_processor.create_mcp_client") + def test_reads_gateway_url_from_env(self, mock_mcp, mock_model, mock_agent_cls): + """Falls back to GATEWAY_URL env var when no explicit URL given.""" + agent_instance = MagicMock() + agent_instance.return_value = "ok" + mock_agent_cls.return_value = agent_instance + mock_mcp_client = MagicMock() + mock_mcp.return_value = mock_mcp_client + + from agent.agent_processor import process_request + with patch.dict("os.environ", {"GATEWAY_URL": "https://env-gw.example.com"}): + result = process_request(_make_request()) + + assert result.success is True + mock_mcp.assert_called_once_with("https://env-gw.example.com", "jwt-tok") + mock_mcp_client.cleanup.assert_called_once() + + @patch("agent.agent_processor.Agent") + @patch("agent.agent_processor.create_bedrock_model") + @patch("agent.agent_processor.create_mcp_client") + def test_explicit_url_overrides_env(self, mock_mcp, mock_model, mock_agent_cls): + agent_instance = MagicMock() + agent_instance.return_value = "ok" + mock_agent_cls.return_value = agent_instance + mock_mcp_client = MagicMock() + mock_mcp.return_value = mock_mcp_client + + from agent.agent_processor import process_request + with patch.dict("os.environ", {"GATEWAY_URL": "https://env-gw.example.com"}): + result = process_request(_make_request(), gateway_url="https://explicit.example.com") + + mock_mcp.assert_called_once_with("https://explicit.example.com", "jwt-tok") diff --git a/strands-agentcore-smithy/tests/unit/test_cloudformation_template.py b/strands-agentcore-smithy/tests/unit/test_cloudformation_template.py new file mode 100644 index 0000000000..883d26b10f --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_cloudformation_template.py @@ -0,0 +1,363 @@ +"""Unit tests for CloudFormation template structure. + +Requirements: 1.1–1.9, 2.1–2.7, 3.1–3.6, 4.1–4.2, 5.1–5.2, 5.6–5.7, 7.1–7.6 +""" + +import json +import os + +import yaml + + +# --------------------------------------------------------------------------- +# Custom YAML loader for CloudFormation intrinsic functions +# --------------------------------------------------------------------------- + +class _CfnLoader(yaml.SafeLoader): + pass + + +def _cfn_tag_constructor(loader, tag_suffix, node): + if isinstance(node, yaml.ScalarNode): + return loader.construct_scalar(node) + if isinstance(node, yaml.SequenceNode): + return loader.construct_sequence(node) + if isinstance(node, yaml.MappingNode): + return loader.construct_mapping(node) + return None + + +_CfnLoader.add_multi_constructor("!", _cfn_tag_constructor) + +# --------------------------------------------------------------------------- +# Load template once +# --------------------------------------------------------------------------- + +_TEMPLATE_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "infrastructure", "template.yaml" +) + +with open(_TEMPLATE_PATH) as _fh: + TEMPLATE = yaml.load(_fh, Loader=_CfnLoader) + +RESOURCES = TEMPLATE["Resources"] +OUTPUTS = TEMPLATE.get("Outputs", {}) + +# Parse Smithy model from the official AWS model file (downloaded by deploy script) +_SMITHY_MODEL_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "infrastructure", "bedrock-runtime-2023-09-30.json" +) +# Fall back to /tmp if not in infrastructure/ +if not os.path.exists(_SMITHY_MODEL_PATH): + _SMITHY_MODEL_PATH = "/tmp/bedrock-runtime-2023-09-30.json" +with open(_SMITHY_MODEL_PATH) as _smithy_fh: + _SMITHY_PAYLOAD = _smithy_fh.read() +SMITHY_MODEL = json.loads(_SMITHY_PAYLOAD) + + +# =========================================================================== +# Gateway configuration tests (Requirements 2.1–2.7) +# =========================================================================== + +class TestGatewayConfiguration: + """Verify AgentCore Gateway resource configuration.""" + + def setup_method(self): + self.gateway = RESOURCES["AgentCoreGateway"]["Properties"] + + def test_protocol_type_is_mcp(self): + """Req 2.1: ProtocolType must be MCP.""" + assert self.gateway["ProtocolType"] == "MCP" + + def test_authorizer_type_is_custom_jwt(self): + """Req 2.2: AuthorizerType must be CUSTOM_JWT.""" + assert self.gateway["AuthorizerType"] == "CUSTOM_JWT" + + def test_custom_jwt_authorizer_key_casing(self): + """Req 2.2: Config block must be nested under AuthorizerConfiguration.CustomJWTAuthorizer.""" + assert "AuthorizerConfiguration" in self.gateway + auth_config = self.gateway["AuthorizerConfiguration"] + assert "CustomJWTAuthorizer" in auth_config + + def test_discovery_url_suffix(self): + """Req 2.3: DiscoveryUrl must end with /.well-known/openid-configuration.""" + url = self.gateway["AuthorizerConfiguration"]["CustomJWTAuthorizer"]["DiscoveryUrl"] + assert url.endswith("/.well-known/openid-configuration") + + def test_allowed_audience_not_audience(self): + """Req 2.4: Must use AllowedAudience, not Audience.""" + authorizer = self.gateway["AuthorizerConfiguration"]["CustomJWTAuthorizer"] + assert "AllowedAudience" in authorizer + assert "Audience" not in authorizer + + def test_role_arn_not_execution_role_arn(self): + """Req 2.5: Must use RoleArn, not ExecutionRoleArn.""" + assert "RoleArn" in self.gateway + assert "ExecutionRoleArn" not in self.gateway + + def test_gateway_target_credential_provider_type(self): + """Req 2.7: GatewayTarget must use GATEWAY_IAM_ROLE.""" + target = RESOURCES["SmithyTarget"]["Properties"] + cred_configs = target["CredentialProviderConfigurations"] + assert isinstance(cred_configs, list) + assert len(cred_configs) >= 1 + assert cred_configs[0]["CredentialProviderType"] == "GATEWAY_IAM_ROLE" + + def test_target_config_nesting(self): + """Req 2.6: TargetConfiguration nested as Mcp.SmithyModel with S3.""" + target_config = RESOURCES["SmithyTarget"]["Properties"]["TargetConfiguration"] + assert "Mcp" in target_config + assert "SmithyModel" in target_config["Mcp"] + assert "S3" in target_config["Mcp"]["SmithyModel"] + + +# =========================================================================== +# Gateway Execution Role tests (Requirements 3.1–3.6) +# =========================================================================== + +class TestGatewayExecutionRole: + """Verify GatewayExecutionRole IAM configuration.""" + + def setup_method(self): + self.role = RESOURCES["GatewayExecutionRole"]["Properties"] + # Collect all policy statements across all policies + self.all_statements = [] + for policy in self.role["Policies"]: + stmts = policy["PolicyDocument"]["Statement"] + self.all_statements.extend(stmts) + # Flatten all resource ARNs + self.all_resources = [] + for stmt in self.all_statements: + res = stmt.get("Resource", []) + if isinstance(res, str): + self.all_resources.append(res) + elif isinstance(res, list): + self.all_resources.extend(res) + # Flatten all actions + self.all_actions = [] + for stmt in self.all_statements: + actions = stmt.get("Action", []) + if isinstance(actions, str): + self.all_actions.append(actions) + elif isinstance(actions, list): + self.all_actions.extend(actions) + + def test_trust_policy_principal(self): + """Trust policy must allow bedrock-agentcore.amazonaws.com.""" + trust = self.role["AssumeRolePolicyDocument"] + principals = [] + for stmt in trust["Statement"]: + svc = stmt.get("Principal", {}).get("Service", "") + if isinstance(svc, str): + principals.append(svc) + elif isinstance(svc, list): + principals.extend(svc) + assert "bedrock-agentcore.amazonaws.com" in principals + + def test_token_vault_default_arn(self): + """Req 3.1: Must include token-vault/default ARN pattern.""" + assert any("token-vault/default" in r and "apikeycredentialprovider" not in r + for r in self.all_resources) + + def test_apikeycredentialprovider_wildcard_arn(self): + """Req 3.2: Must include apikeycredentialprovider/* ARN pattern.""" + assert any("apikeycredentialprovider/*" in r for r in self.all_resources) + + def test_workload_identity_directory_default_arn(self): + """Req 3.3: Must include workload-identity-directory/default ARN.""" + assert any( + "workload-identity-directory/default" in r + and "workload-identity" not in r.split("workload-identity-directory/default")[1][:1] + or r.endswith("workload-identity-directory/default") + for r in self.all_resources + ) + + def test_workload_identity_wildcard_arn(self): + """Req 3.4: Must include workload-identity-directory/default/workload-identity/{gateway}-* ARN.""" + assert any( + "workload-identity-directory/default/workload-identity/" in r + for r in self.all_resources + ) + + def test_bedrock_runtime_permissions(self): + """Req 3.5: Must include bedrock:InvokeModel.""" + required_actions = {"bedrock:InvokeModel"} + assert required_actions.issubset(set(self.all_actions)) + + def test_no_apigateway_get_permission(self): + """Req 3.6: Must NOT include apigateway:GET.""" + for action in self.all_actions: + assert "apigateway" not in action.lower() + + +# =========================================================================== +# Smithy model tests (Requirements 1.1–1.9) +# =========================================================================== + +class TestSmithyModel: + """Verify inline Smithy model structure.""" + + def setup_method(self): + self.shapes = SMITHY_MODEL["shapes"] + # Find service shape + self.service = None + for key, shape in self.shapes.items(): + if shape.get("type") == "service": + self.service = shape + self.service_key = key + break + + def test_smithy_version(self): + """Smithy model must be version 2.0.""" + assert SMITHY_MODEL["smithy"] == "2.0" + + def test_service_has_restjson1_trait(self): + """Req 1.1: Service must have aws.protocols#restJson1 trait.""" + assert "aws.protocols#restJson1" in self.service.get("traits", {}) + + def test_service_version(self): + """Service must have a version string.""" + assert self.service["version"] is not None + assert len(self.service["version"]) > 0 + + def test_defines_invoke_model_operation(self): + """Must define InvokeModel with POST method.""" + op = self.shapes["com.amazonaws.bedrockruntime#InvokeModel"] + http = op["traits"]["smithy.api#http"] + assert http["method"] == "POST" + assert "/model/" in http["uri"] + + def test_has_operations(self): + """Service must define operations (via resources).""" + resources = self.service.get("resources", []) + assert len(resources) > 0 + + def test_model_under_10mb(self): + """Req 1.8: Smithy model must be under 10MB.""" + assert len(_SMITHY_PAYLOAD.encode("utf-8")) < 10 * 1024 * 1024 + + +# =========================================================================== +# Lambda configuration tests (Requirements 5.1, 5.2, 5.6, 5.7) +# =========================================================================== + +class TestLambdaConfiguration: + """Verify Agent Lambda Function resource configuration.""" + + def setup_method(self): + self.lambda_props = RESOURCES["AgentLambdaFunction"]["Properties"] + self.lambda_role = RESOURCES["AgentLambdaRole"]["Properties"] + # Collect all actions from Lambda role policies + self.all_actions = [] + for policy in self.lambda_role["Policies"]: + for stmt in policy["PolicyDocument"]["Statement"]: + actions = stmt.get("Action", []) + if isinstance(actions, str): + self.all_actions.append(actions) + elif isinstance(actions, list): + self.all_actions.extend(actions) + + def test_runtime_python312(self): + """Req 5.1: Runtime must be python3.12.""" + assert self.lambda_props["Runtime"] == "python3.12" + + def test_architecture_x86_64(self): + """Req 5.1: Architecture must be x86_64.""" + assert "x86_64" in self.lambda_props["Architectures"] + + def test_timeout_at_least_120(self): + """Req 5.2: Timeout must be >= 120 seconds.""" + assert self.lambda_props["Timeout"] >= 120 + + def test_memory_at_least_1024(self): + """Req 5.2: MemorySize must be >= 1024 MB.""" + assert self.lambda_props["MemorySize"] >= 1024 + + def test_bedrock_invoke_permissions(self): + """Req 5.6: Must have bedrock:InvokeModel and related permissions.""" + required = { + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock:Converse", + "bedrock:ConverseStream", + } + assert required.issubset(set(self.all_actions)) + + def test_agentcore_invoke_gateway_permission(self): + """Req 5.7: Must have bedrock-agentcore:InvokeGateway permission.""" + assert "bedrock-agentcore:InvokeGateway" in self.all_actions + + def test_cloudwatch_logs_permissions(self): + """Lambda role must have CloudWatch Logs permissions.""" + required = {"logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"} + assert required.issubset(set(self.all_actions)) + + +# =========================================================================== +# Cognito configuration tests (Requirements 4.1, 4.2) +# =========================================================================== + +class TestCognitoConfiguration: + """Verify Cognito User Pool and Client configuration.""" + + def test_user_pool_exists(self): + """Req 4.1: CognitoUserPool resource must exist.""" + assert "CognitoUserPool" in RESOURCES + assert RESOURCES["CognitoUserPool"]["Type"] == "AWS::Cognito::UserPool" + + def test_user_pool_client_exists(self): + """Req 4.2: CognitoUserPoolClient resource must exist.""" + assert "CognitoUserPoolClient" in RESOURCES + assert RESOURCES["CognitoUserPoolClient"]["Type"] == "AWS::Cognito::UserPoolClient" + + def test_user_password_auth_flow(self): + """Req 4.1: Must include USER_PASSWORD_AUTH explicit auth flow.""" + client_props = RESOURCES["CognitoUserPoolClient"]["Properties"] + assert "USER_PASSWORD_AUTH" in client_props["ExplicitAuthFlows"] + + +# =========================================================================== +# No forbidden resources (Requirements 7.4, 7.5) +# =========================================================================== + +class TestNoForbiddenResources: + """Verify template does not contain forbidden resource types.""" + + def test_no_api_gateway_resources(self): + """Req 7.4: No AWS::ApiGateway::* or AWS::ApiGatewayV2::* resources.""" + for name, res in RESOURCES.items(): + rtype = res.get("Type", "") + assert not rtype.startswith("AWS::ApiGateway::"), ( + f"Found API Gateway v1 resource: {name}" + ) + assert not rtype.startswith("AWS::ApiGatewayV2::"), ( + f"Found API Gateway v2 resource: {name}" + ) + + def test_no_secrets_manager_resources(self): + """Req 7.5: No AWS::SecretsManager::Secret resources.""" + for name, res in RESOURCES.items(): + rtype = res.get("Type", "") + assert not rtype.startswith("AWS::SecretsManager::"), ( + f"Found Secrets Manager resource: {name}" + ) + + +# =========================================================================== +# Stack outputs (Requirement 7.6) +# =========================================================================== + +class TestStackOutputs: + """Verify required stack outputs are defined.""" + + def test_gateway_id_output(self): + assert "GatewayId" in OUTPUTS + + def test_cognito_user_pool_id_output(self): + assert "CognitoUserPoolId" in OUTPUTS + + def test_cognito_client_id_output(self): + assert "CognitoClientId" in OUTPUTS + + def test_lambda_function_name_output(self): + assert "LambdaFunctionName" in OUTPUTS diff --git a/strands-agentcore-smithy/tests/unit/test_deploy_script.py b/strands-agentcore-smithy/tests/unit/test_deploy_script.py new file mode 100644 index 0000000000..53ccaf8b76 --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_deploy_script.py @@ -0,0 +1,192 @@ +"""Unit tests for the SAM deploy script (scripts/deploy.sh) and Makefile build. + +After the migration from raw CloudFormation to AWS SAM: + - Orchestration (validate / build / deploy) lives in scripts/deploy.sh and + uses the `sam` CLI. + - Lambda packaging (the two-step pip3 install) lives in the root Makefile, + invoked by `sam build` via BuildMethod: makefile (no Docker required). +""" + +import os +import re + +_SCRIPT_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "deploy.sh" +) +_MAKEFILE_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "Makefile" +) + +with open(_SCRIPT_PATH) as _fh: + DEPLOY_CONTENT = _fh.read() + +with open(_MAKEFILE_PATH) as _fh: + MAKEFILE_CONTENT = _fh.read() + + +# ── Makefile: two-step pip3 install (packaging moved out of deploy.sh) ── + +class TestTwoStepPipInstall: + """Verify the two-step pip3 install pattern lives in the Makefile.""" + + def test_first_step_has_only_binary(self): + """First pip3 install uses --only-binary=:all:""" + first_pip = re.search( + r"pip3\s+install\b.*?--only-binary=:all:", MAKEFILE_CONTENT, re.DOTALL + ) + assert first_pip is not None, "First pip3 install must use --only-binary=:all:" + + def test_first_step_has_platform_targeting(self): + """First pip3 install targets manylinux2014_x86_64.""" + assert re.search( + r"pip3\s+install\b.*--platform\s+manylinux2014_x86_64", MAKEFILE_CONTENT, re.DOTALL + ), "First pip3 install must target --platform manylinux2014_x86_64" + + def test_first_step_has_python_version(self): + """First pip3 install specifies --python-version 3.12.""" + assert re.search( + r"pip3\s+install\b.*--python-version\s+3\.12", MAKEFILE_CONTENT, re.DOTALL + ), "First pip3 install must specify --python-version 3.12" + + def test_second_step_has_no_deps(self): + """Second pip3 install uses --no-deps for pure Python packages.""" + no_deps_match = re.search( + r"pip3\s+install\b.*?--no-deps", MAKEFILE_CONTENT, re.DOTALL + ) + assert no_deps_match is not None, "Second pip3 install must use --no-deps" + + def test_second_step_includes_pure_python_packages(self): + """Second step installs the expected pure Python packages.""" + no_deps_section = re.search( + r"pip3\s+install\b.*?--no-deps\b(.*)", MAKEFILE_CONTENT, re.DOTALL + ) + assert no_deps_section is not None + block = no_deps_section.group(0) + for pkg in ["requests", "urllib3", "charset-normalizer", "idna", "certifi", "PyJWT", "cryptography", "cffi"]: + assert pkg in block, f"Second pip3 install must include {pkg}" + + def test_at_least_two_pip3_install_commands(self): + """There should be at least two pip3 install invocations (binary + pure Python).""" + invocations = re.findall(r"\bpip3\s+install\b", MAKEFILE_CONTENT) + assert len(invocations) >= 2, f"Expected at least 2 pip3 install commands, found {len(invocations)}" + + +# ── No .dist-info removal ── + +class TestNoDistInfoRemoval: + def test_no_dist_info_removal(self): + """Neither the Makefile nor deploy.sh must remove .dist-info directories.""" + combined = MAKEFILE_CONTENT + "\n" + DEPLOY_CONTENT + assert not re.search(r"rm\s.*\.dist-info", combined), ( + "Must not remove .dist-info directories" + ) + assert not re.search(r"find.*\.dist-info.*-delete", combined), ( + "Must not delete .dist-info via find" + ) + + +# ── SAM build handles packaging + artifact upload ── + +class TestSamBuildAndDeploy: + def test_uses_sam_build(self): + """Script builds the Lambda with `sam build`.""" + assert re.search(r"\bsam\s+build\b", DEPLOY_CONTENT), "Script must call sam build" + + def test_uses_sam_deploy(self): + """Script deploys with `sam deploy`.""" + assert re.search(r"\bsam\s+deploy\b", DEPLOY_CONTENT), "Script must call sam deploy" + + def test_sam_deploy_has_named_iam_capability(self): + """SAM deploy must pass CAPABILITY_NAMED_IAM for the named roles.""" + assert "CAPABILITY_NAMED_IAM" in DEPLOY_CONTENT + + def test_sam_deploy_resolves_artifact_bucket(self): + """SAM deploy manages the artifact bucket via --resolve-s3.""" + assert "--resolve-s3" in DEPLOY_CONTENT + + def test_makefile_build_method(self): + """The Makefile exposes the SAM build target for the function.""" + assert "build-AgentLambdaFunction" in MAKEFILE_CONTENT + + +# ── Required dependencies ── + +class TestRequiredDependencies: + def test_requirements_txt_referenced(self): + """Makefile references requirements.txt for pip install.""" + assert "requirements.txt" in MAKEFILE_CONTENT + + +# ── Template validation before deployment ── + +class TestTemplateValidation: + def test_validate_template_before_deploy(self): + """Script validates the SAM template before deploying.""" + validate_pos = DEPLOY_CONTENT.find("sam validate") + deploy_pos = DEPLOY_CONTENT.find("sam deploy") + assert validate_pos != -1, "Script must call sam validate" + assert deploy_pos != -1, "Script must call sam deploy" + assert validate_pos < deploy_pos, "sam validate must come before sam deploy" + + def test_references_sam_template(self): + """Script references the SAM template path.""" + assert "infrastructure/template.yaml" in DEPLOY_CONTENT + + +# ── Smithy model upload (must precede stack deploy) ── + +class TestSmithyModelUpload: + def test_uploads_smithy_model_to_s3(self): + """Script uploads the Smithy model with `aws s3 cp`.""" + assert re.search(r"aws\s+s3\s+cp\b", DEPLOY_CONTENT), ( + "Script must upload the Smithy model with aws s3 cp" + ) + + def test_smithy_upload_before_deploy(self): + """Smithy model must be in S3 before the stack (and its target) deploys.""" + upload_pos = DEPLOY_CONTENT.find("aws s3 cp") + deploy_pos = DEPLOY_CONTENT.find("sam deploy") + assert upload_pos != -1 and deploy_pos != -1 + assert upload_pos < deploy_pos, "Smithy upload must precede sam deploy" + + +# ── Cognito test user creation ── + +class TestCognitoUser: + def test_creates_cognito_user(self): + """Script creates a Cognito test user.""" + assert "admin-create-user" in DEPLOY_CONTENT + assert "admin-set-user-password" in DEPLOY_CONTENT + + +# ── pip3 exclusively (in the Makefile) ── + +class TestPip3Exclusively: + def test_all_pip_commands_use_pip3(self): + """All pip install commands use pip3, not bare pip.""" + invocations = re.findall(r"\b(pip3?)\s+install\b", MAKEFILE_CONTENT) + assert len(invocations) > 0, "Must have pip install commands" + for inv in invocations: + assert inv == "pip3", f"Found bare '{inv}' instead of 'pip3'" + + +# ── No credential provider commands ── + +class TestNoForbiddenPatterns: + def test_no_credential_provider_commands(self): + """Script must not contain credential provider CLI commands.""" + forbidden = [ + "create-credential-provider", + "create-api-key-credential-provider", + "get-api-key", + ] + for pattern in forbidden: + assert pattern not in DEPLOY_CONTENT, ( + f"Script must not contain '{pattern}'" + ) + + def test_no_api_key_retrieval(self): + """Script must not retrieve API keys.""" + assert not re.search(r"get-api-key", DEPLOY_CONTENT), ( + "Script must not retrieve API keys" + ) diff --git a/strands-agentcore-smithy/tests/unit/test_error_utils.py b/strands-agentcore-smithy/tests/unit/test_error_utils.py new file mode 100644 index 0000000000..2b51210282 --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_error_utils.py @@ -0,0 +1,60 @@ +"""Unit tests for error_utils module. + +Requirements: 11.3 +""" + +import logging + +from shared.error_utils import ( + format_error_response, + format_unauthorized_response, + format_internal_error_response, +) + + +class TestFormatErrorResponse: + def test_returns_failed_response(self): + resp = format_error_response("something broke") + assert resp.success is False + assert resp.response == "" + assert resp.error == "something broke" + + def test_logs_server_error_at_error_level(self, caplog): + with caplog.at_level(logging.ERROR): + format_error_response("server fail", status_code=500) + assert "server fail" in caplog.text + + def test_logs_client_error_at_warning_level(self, caplog): + with caplog.at_level(logging.WARNING): + format_error_response("bad request", status_code=400) + assert "bad request" in caplog.text + + def test_custom_log_message(self, caplog): + with caplog.at_level(logging.ERROR): + format_error_response("user msg", status_code=500, log_message="internal detail") + assert "internal detail" in caplog.text + + +class TestFormatUnauthorizedResponse: + def test_default_message(self): + resp = format_unauthorized_response() + assert resp.success is False + assert resp.error == "Unauthorized" + + def test_custom_detail(self): + resp = format_unauthorized_response("Token expired") + assert resp.error == "Token expired" + + +class TestFormatInternalErrorResponse: + def test_default_message(self): + resp = format_internal_error_response() + assert resp.success is False + assert resp.error == "Internal server error" + + def test_with_exception(self, caplog): + exc = ValueError("division by zero") + with caplog.at_level(logging.ERROR): + resp = format_internal_error_response("calc failed", exception=exc) + assert resp.error == "calc failed" + assert "division by zero" in caplog.text diff --git a/strands-agentcore-smithy/tests/unit/test_handler.py b/strands-agentcore-smithy/tests/unit/test_handler.py new file mode 100644 index 0000000000..a7521abd69 --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_handler.py @@ -0,0 +1,131 @@ +"""Unit tests for the agent Lambda handler. + +Tests JWT extraction from events, validation error responses, +missing prompt handling, and successful delegation to agent_processor. + +Requirements: 5.1, 5.5, 5.6 +""" + +import sys +from types import ModuleType +from unittest.mock import patch, MagicMock + +import pytest + +# Mock third-party modules not installed in test env +_strands_mod = ModuleType("strands") +_strands_mod.Agent = MagicMock() +_mcp_mod = ModuleType("mcp") +_mcp_client_mod = ModuleType("mcp.client") +_mcp_http_mod = ModuleType("mcp.client.streamable_http") +_mcp_http_mod.streamablehttp_client = MagicMock() +_bedrock_mod = ModuleType("strands.models") +_bedrock_bedrock_mod = ModuleType("strands.models.bedrock") +_bedrock_bedrock_mod.BedrockModel = MagicMock() +_strands_tools_mod = ModuleType("strands.tools") +_strands_tools_mcp_mod = ModuleType("strands.tools.mcp") +_strands_tools_mcp_mod.MCPClient = MagicMock() + +sys.modules.setdefault("strands", _strands_mod) +sys.modules.setdefault("strands.models", _bedrock_mod) +sys.modules.setdefault("strands.models.bedrock", _bedrock_bedrock_mod) +sys.modules.setdefault("strands.tools", _strands_tools_mod) +sys.modules.setdefault("strands.tools.mcp", _strands_tools_mcp_mod) +sys.modules.setdefault("mcp", _mcp_mod) +sys.modules.setdefault("mcp.client", _mcp_client_mod) +sys.modules.setdefault("mcp.client.streamable_http", _mcp_http_mod) + +from agent.handler import lambda_handler, _extract_token +from shared.models import AgentResponse + + +# --- _extract_token --- + +class TestExtractToken: + def test_extracts_direct_token(self): + event = {"token": "my-jwt"} + assert _extract_token(event) == "my-jwt" + + def test_extracts_bearer_token_from_headers(self): + event = {"headers": {"Authorization": "Bearer abc123"}} + assert _extract_token(event) == "abc123" + + def test_direct_token_takes_precedence_over_header(self): + event = {"token": "direct", "headers": {"Authorization": "Bearer header"}} + assert _extract_token(event) == "direct" + + def test_returns_raw_auth_header_if_not_bearer(self): + event = {"headers": {"Authorization": "Basic creds"}} + assert _extract_token(event) == "Basic creds" + + def test_returns_none_when_no_token(self): + assert _extract_token({}) is None + + def test_returns_none_for_empty_auth_header(self): + event = {"headers": {"Authorization": ""}} + assert _extract_token(event) is None + + def test_returns_none_for_missing_headers(self): + event = {"headers": {}} + assert _extract_token(event) is None + + +# --- lambda_handler --- + +class TestLambdaHandler: + """Tests for the Lambda entry point.""" + + def test_missing_token_returns_unauthorized(self): + result = lambda_handler({}, None) + assert result["success"] is False + assert "authentication" in result["error"].lower() or "token" in result["error"].lower() + + @patch("agent.handler.validate_token") + def test_invalid_jwt_returns_unauthorized(self, mock_validate): + from src.shared.jwt_utils import JWTValidationError + mock_validate.side_effect = JWTValidationError("Token expired") + + result = lambda_handler({"token": "bad-jwt"}, None) + assert result["success"] is False + assert "expired" in result["error"].lower() + + @patch("agent.handler.extract_username") + @patch("agent.handler.validate_token") + def test_missing_prompt_returns_error(self, mock_validate, mock_extract): + mock_validate.return_value = {"cognito:username": "alice"} + mock_extract.return_value = "alice" + + result = lambda_handler({"token": "valid-jwt"}, None) + assert result["success"] is False + assert "prompt" in result["error"].lower() + + @patch("agent.handler.process_request") + @patch("agent.handler.extract_username") + @patch("agent.handler.validate_token") + def test_successful_request(self, mock_validate, mock_extract, mock_process): + mock_validate.return_value = {"cognito:username": "alice"} + mock_extract.return_value = "alice" + mock_process.return_value = AgentResponse(success=True, response="done") + + result = lambda_handler({"token": "jwt", "prompt": "hello"}, None) + assert result["success"] is True + assert result["response"] == "done" + assert result["error"] is None + + # Verify process_request was called with correct AgentRequest + call_args = mock_process.call_args[0][0] + assert call_args.prompt == "hello" + assert call_args.user_context.username == "alice" + assert call_args.user_context.token == "jwt" + + @patch("agent.handler.process_request") + @patch("agent.handler.extract_username") + @patch("agent.handler.validate_token") + def test_processor_exception_returns_internal_error(self, mock_validate, mock_extract, mock_process): + mock_validate.return_value = {"cognito:username": "alice"} + mock_extract.return_value = "alice" + mock_process.side_effect = RuntimeError("boom") + + result = lambda_handler({"token": "jwt", "prompt": "hello"}, None) + assert result["success"] is False + assert result["error"] is not None diff --git a/strands-agentcore-smithy/tests/unit/test_jwt_utils.py b/strands-agentcore-smithy/tests/unit/test_jwt_utils.py new file mode 100644 index 0000000000..9ae9f00df6 --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_jwt_utils.py @@ -0,0 +1,98 @@ +"""Unit tests for JWT validation edge cases. + +Requirements: 4.4, 4.5, 4.6 +""" + +import time + +import jwt as pyjwt +import pytest + +from shared.jwt_utils import validate_token, extract_username, JWTValidationError + + +def _encode(claims: dict, exp_offset: int = 3600) -> str: + payload = { + "iat": int(time.time()), + "exp": int(time.time()) + exp_offset, + **claims, + } + return pyjwt.encode(payload, "test-secret", algorithm="HS256") + + +# --- validate_token --- + +class TestValidateToken: + def test_accepts_token_use_access(self): + token = _encode({"token_use": "access"}) + claims = validate_token(token) + assert claims["token_use"] == "access" + + def test_accepts_token_use_id(self): + token = _encode({"token_use": "id"}) + claims = validate_token(token) + assert claims["token_use"] == "id" + + def test_accepts_missing_token_use(self): + """Tokens without token_use claim should still be accepted.""" + token = _encode({"sub": "user1"}) + claims = validate_token(token) + assert "token_use" not in claims + + def test_rejects_invalid_token_use(self): + token = _encode({"token_use": "refresh"}) + with pytest.raises(JWTValidationError, match="Unsupported token_use"): + validate_token(token) + + def test_rejects_empty_token(self): + with pytest.raises(JWTValidationError, match="empty or missing"): + validate_token("") + + def test_rejects_none_like_empty(self): + with pytest.raises(JWTValidationError): + validate_token("") + + def test_rejects_malformed_token(self): + with pytest.raises(JWTValidationError, match="Invalid token"): + validate_token("not.a.jwt.at.all") + + def test_rejects_expired_token(self): + token = _encode({"token_use": "id"}, exp_offset=-10) + with pytest.raises(JWTValidationError, match="expired"): + validate_token(token) + + def test_verify_aud_disabled_by_default(self): + """Tokens with an aud claim should not fail since verify_aud is False.""" + token = _encode({"token_use": "id", "aud": "some-client-id"}) + claims = validate_token(token) + assert claims["aud"] == "some-client-id" + + +# --- extract_username --- + +class TestExtractUsername: + def test_extracts_cognito_username(self): + claims = {"cognito:username": "alice"} + assert extract_username(claims) == "alice" + + def test_ignores_username_claim(self): + claims = {"cognito:username": "alice", "username": "bob"} + assert extract_username(claims) == "alice" + + def test_ignores_sub_claim(self): + claims = {"cognito:username": "alice", "sub": "sub-123"} + assert extract_username(claims) == "alice" + + def test_raises_when_cognito_username_missing(self): + claims = {"username": "bob", "sub": "sub-123"} + with pytest.raises(JWTValidationError, match="cognito:username"): + extract_username(claims) + + def test_raises_on_empty_claims(self): + with pytest.raises(JWTValidationError, match="cognito:username"): + extract_username({}) + + def test_raises_on_empty_string_cognito_username(self): + claims = {"cognito:username": ""} + with pytest.raises(JWTValidationError, match="cognito:username"): + extract_username(claims) diff --git a/strands-agentcore-smithy/tests/unit/test_logging_utils.py b/strands-agentcore-smithy/tests/unit/test_logging_utils.py new file mode 100644 index 0000000000..3e0be82327 --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_logging_utils.py @@ -0,0 +1,56 @@ +"""Unit tests for logging_utils module. + +Requirements: 11.3 +""" + +import logging +import uuid + +from shared.logging_utils import get_correlation_id, configure_logging, get_logger + + +class TestGetCorrelationId: + def test_extracts_request_id_from_event(self): + event = {"requestContext": {"requestId": "req-abc-123"}} + assert get_correlation_id(event) == "req-abc-123" + + def test_generates_uuid_when_no_event(self): + cid = get_correlation_id(None) + # Should be a valid UUID + uuid.UUID(cid) + + def test_generates_uuid_when_event_has_no_request_context(self): + cid = get_correlation_id({"body": "hello"}) + uuid.UUID(cid) + + def test_generates_uuid_when_request_id_missing(self): + event = {"requestContext": {}} + cid = get_correlation_id(event) + uuid.UUID(cid) + + def test_generates_uuid_for_empty_event(self): + cid = get_correlation_id({}) + uuid.UUID(cid) + + +class TestConfigureLogging: + def test_returns_root_logger(self): + logger = configure_logging() + assert isinstance(logger, logging.Logger) + + def test_sets_log_level(self): + logger = configure_logging(level=logging.DEBUG) + assert logger.level == logging.DEBUG + + def test_includes_correlation_id_in_format(self): + logger = configure_logging(correlation_id="corr-xyz") + handler = logger.handlers[-1] if logger.handlers else None + if handler: + assert "corr-xyz" in handler.formatter._fmt + + +class TestGetLogger: + def test_returns_named_logger(self): + logger = get_logger("my.module") + assert logger.name == "my.module" + assert isinstance(logger, logging.Logger) diff --git a/strands-agentcore-smithy/tests/unit/test_models.py b/strands-agentcore-smithy/tests/unit/test_models.py new file mode 100644 index 0000000000..78cdfc6985 --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_models.py @@ -0,0 +1,54 @@ +"""Unit tests for shared data models (UserContext, AgentRequest, AgentResponse). + +Requirements: 11.3 +""" + +from shared.models import UserContext, AgentRequest, AgentResponse + + +class TestUserContext: + def test_creation(self): + ctx = UserContext(username="alice", token="tok-123") + assert ctx.username == "alice" + assert ctx.token == "tok-123" + + def test_equality(self): + a = UserContext(username="bob", token="t") + b = UserContext(username="bob", token="t") + assert a == b + + def test_inequality(self): + a = UserContext(username="bob", token="t1") + b = UserContext(username="bob", token="t2") + assert a != b + + +class TestAgentRequest: + def test_creation(self): + ctx = UserContext(username="u", token="t") + req = AgentRequest(prompt="hello", user_context=ctx) + assert req.prompt == "hello" + assert req.user_context.username == "u" + + def test_equality(self): + ctx = UserContext(username="u", token="t") + a = AgentRequest(prompt="p", user_context=ctx) + b = AgentRequest(prompt="p", user_context=ctx) + assert a == b + + +class TestAgentResponse: + def test_success_response(self): + resp = AgentResponse(success=True, response="ok") + assert resp.success is True + assert resp.response == "ok" + assert resp.error is None + + def test_error_response(self): + resp = AgentResponse(success=False, response="", error="bad") + assert resp.success is False + assert resp.error == "bad" + + def test_error_defaults_to_none(self): + resp = AgentResponse(success=True, response="ok") + assert resp.error is None diff --git a/strands-agentcore-smithy/tests/unit/test_strands_client.py b/strands-agentcore-smithy/tests/unit/test_strands_client.py new file mode 100644 index 0000000000..8131119246 --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_strands_client.py @@ -0,0 +1,115 @@ +"""Unit tests for the Strands client factory functions. + +Tests MCP client creation and Bedrock model configuration. + +Requirements: 5.3, 5.4 +""" + +import sys +from types import ModuleType +from unittest.mock import patch, MagicMock + +# Mock third-party modules not installed in test env +_strands_mod = ModuleType("strands") +_strands_mod.Agent = MagicMock() +_mcp_mod = ModuleType("mcp") +_mcp_client_mod = ModuleType("mcp.client") +_mcp_http_mod = ModuleType("mcp.client.streamable_http") +_mcp_http_mod.streamablehttp_client = MagicMock() +_bedrock_mod = ModuleType("strands.models") +_bedrock_bedrock_mod = ModuleType("strands.models.bedrock") +_bedrock_bedrock_mod.BedrockModel = MagicMock() +_strands_tools_mod = ModuleType("strands.tools") +_strands_tools_mcp_mod = ModuleType("strands.tools.mcp") +_strands_tools_mcp_mod.MCPClient = MagicMock() + +sys.modules.setdefault("strands", _strands_mod) +sys.modules.setdefault("strands.models", _bedrock_mod) +sys.modules.setdefault("strands.models.bedrock", _bedrock_bedrock_mod) +sys.modules.setdefault("strands.tools", _strands_tools_mod) +sys.modules.setdefault("strands.tools.mcp", _strands_tools_mcp_mod) +sys.modules.setdefault("mcp", _mcp_mod) +sys.modules.setdefault("mcp.client", _mcp_client_mod) +sys.modules.setdefault("mcp.client.streamable_http", _mcp_http_mod) + +from agent.strands_client import ( + create_mcp_client, + create_bedrock_model, + DEFAULT_MODEL_ID, + DEFAULT_REGION, +) + + +class TestCreateMCPClient: + """Tests for the MCP client factory.""" + + @patch("agent.strands_client.MCPClient") + def test_returns_mcp_client_instance(self, mock_mcp_cls): + client = create_mcp_client("https://gw.example.com", "my-token") + mock_mcp_cls.assert_called_once() + assert client is mock_mcp_cls.return_value + + @patch("agent.strands_client.streamablehttp_client") + @patch("agent.strands_client.MCPClient") + def test_transport_factory_uses_bearer_token(self, mock_mcp_cls, mock_http): + """The transport factory lambda should pass the Bearer token header.""" + create_mcp_client("https://gw.example.com", "tok-abc") + + # Extract the transport factory lambda passed to MCPClient + transport_factory = mock_mcp_cls.call_args[0][0] + transport_factory() + + mock_http.assert_called_once_with( + "https://gw.example.com", + headers={"Authorization": "Bearer tok-abc"}, + ) + + @patch("agent.strands_client.streamablehttp_client") + @patch("agent.strands_client.MCPClient") + def test_different_urls_and_tokens(self, mock_mcp_cls, mock_http): + """Verify URL and token are correctly threaded through.""" + create_mcp_client("https://other.example.com", "other-token") + transport_factory = mock_mcp_cls.call_args[0][0] + transport_factory() + + mock_http.assert_called_once_with( + "https://other.example.com", + headers={"Authorization": "Bearer other-token"}, + ) + + +class TestCreateBedrockModel: + """Tests for the Bedrock model factory.""" + + @patch("agent.strands_client.BedrockModel") + def test_default_model_and_region(self, mock_bedrock_cls): + model = create_bedrock_model() + mock_bedrock_cls.assert_called_once_with( + model_id=DEFAULT_MODEL_ID, + region_name=DEFAULT_REGION, + ) + assert model is mock_bedrock_cls.return_value + + @patch("agent.strands_client.BedrockModel") + def test_custom_model_id(self, mock_bedrock_cls): + create_bedrock_model(model_id="custom-model") + mock_bedrock_cls.assert_called_once_with( + model_id="custom-model", + region_name=DEFAULT_REGION, + ) + + @patch("agent.strands_client.BedrockModel") + def test_custom_region(self, mock_bedrock_cls): + create_bedrock_model(region_name="eu-west-1") + mock_bedrock_cls.assert_called_once_with( + model_id=DEFAULT_MODEL_ID, + region_name="eu-west-1", + ) + + @patch("agent.strands_client.BedrockModel") + def test_custom_model_and_region(self, mock_bedrock_cls): + create_bedrock_model(model_id="my-model", region_name="ap-southeast-1") + mock_bedrock_cls.assert_called_once_with( + model_id="my-model", + region_name="ap-southeast-1", + ) diff --git a/strands-agentcore-smithy/tests/unit/test_test_script.py b/strands-agentcore-smithy/tests/unit/test_test_script.py new file mode 100644 index 0000000000..fc85a8724f --- /dev/null +++ b/strands-agentcore-smithy/tests/unit/test_test_script.py @@ -0,0 +1,142 @@ +"""Unit tests for the generated test script pattern in deploy.sh. + +The test script (scripts/test.sh) is generated by deploy.sh at runtime. +These tests validate the heredoc template embedded in deploy.sh. + +Validates: Requirements 10.1–10.5 +""" + +import os +import re + +import pytest + +_DEPLOY_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "deploy.sh" +) + +with open(_DEPLOY_PATH) as _fh: + DEPLOY_CONTENT = _fh.read() + +# Extract the test script template from deploy.sh (everything written to scripts/test.sh) +# The template spans from the first heredoc writing to test.sh until the chmod line. +_TEST_TEMPLATE_MATCH = re.search( + r"cat > scripts/test\.sh.*?chmod \+x scripts/test\.sh", + DEPLOY_CONTENT, + re.DOTALL, +) +assert _TEST_TEMPLATE_MATCH is not None, "Could not find test script template in deploy.sh" +TEST_TEMPLATE = _TEST_TEMPLATE_MATCH.group(0) + + +# ── Requirement 10.1: Executable permissions ── + +class TestExecutablePermissions: + def test_chmod_plus_x(self): + """Generated test script gets executable permissions.""" + assert "chmod +x scripts/test.sh" in DEPLOY_CONTENT + + +# ── Requirement 10.2: Optional prompt with default ── + +class TestDefaultPrompt: + def test_accepts_cli_argument_with_default(self): + """Test script accepts $1 with a default prompt value.""" + # Pattern: PROMPT="${1:-...}" or similar + assert re.search( + r'PROMPT="\$\{1:-[^}]+\}"', TEST_TEMPLATE + ), "Test script must accept optional CLI prompt with a default" + + def test_default_prompt_is_meaningful(self): + """Default prompt is a meaningful natural language query.""" + match = re.search(r'PROMPT="\$\{1:-([^}]+)\}"', TEST_TEMPLATE) + assert match is not None + default = match.group(1) + assert len(default) > 10, f"Default prompt too short: '{default}'" + + +# ── Requirement 10.3: Auth before invoke ── + +class TestAuthBeforeInvoke: + def test_cognito_auth_before_lambda_invoke(self): + """Test script authenticates with Cognito before invoking Lambda.""" + auth_pos = TEST_TEMPLATE.find("initiate-auth") + invoke_pos = TEST_TEMPLATE.find("lambda invoke") + assert auth_pos != -1, "Test script must call initiate-auth" + assert invoke_pos != -1, "Test script must call lambda invoke" + assert auth_pos < invoke_pos, ( + "initiate-auth must come before lambda invoke" + ) + + def test_uses_user_password_auth_flow(self): + """Test script uses USER_PASSWORD_AUTH flow.""" + assert "USER_PASSWORD_AUTH" in TEST_TEMPLATE + + +# ── Requirement 10.4: Lambda invocation with JWT ── + +class TestLambdaInvocation: + def test_passes_authorization_header(self): + """Test script passes Authorization header with Bearer token.""" + assert "Authorization" in TEST_TEMPLATE + assert "Bearer" in TEST_TEMPLATE + + def test_passes_prompt_in_body(self): + """Test script passes the prompt in the request body.""" + assert "prompt" in TEST_TEMPLATE + + +# ── Requirement 10.5: No nested JSON echo that breaks variable substitution ── + +class TestNoNestedJsonEcho: + def test_no_nested_echo_json_with_dollar_id_token(self): + """Test script must not use nested echo JSON that breaks ${ID_TOKEN} substitution. + + Bad pattern: echo '{"headers": {"Authorization": "Bearer '${ID_TOKEN}'"}}' + This breaks because of nested quoting. The script should use a temp file + or a heredoc approach instead. + """ + # Look for echo commands that try to inline JSON with variable substitution + bad_pattern = re.search( + r"""echo\s+['"]\s*\{.*Authorization.*\$\{?ID_TOKEN""", + TEST_TEMPLATE, + ) + assert bad_pattern is None, ( + "Test script must not use echo with inline JSON containing ${ID_TOKEN}" + ) + + def test_uses_temp_file_or_heredoc_for_payload(self): + """Test script should use a temp file or heredoc for the JSON payload.""" + uses_temp = "mktemp" in TEST_TEMPLATE or "PAYLOAD" in TEST_TEMPLATE + uses_heredoc = "<<" in TEST_TEMPLATE and "EOF" in TEST_TEMPLATE + assert uses_temp or uses_heredoc, ( + "Test script should use temp file or heredoc for JSON payload construction" + ) + + +# ── Requirement 10.1: Baked-in values ── + +class TestBakedInValues: + def test_gateway_id_baked_in(self): + """Deploy script bakes GATEWAY_ID into test script.""" + assert "GATEWAY_ID" in TEST_TEMPLATE + + def test_cognito_pool_id_baked_in(self): + """Deploy script bakes COGNITO_USER_POOL_ID into test script.""" + assert "COGNITO_USER_POOL_ID" in TEST_TEMPLATE + + def test_cognito_client_id_baked_in(self): + """Deploy script bakes COGNITO_CLIENT_ID into test script.""" + assert "COGNITO_CLIENT_ID" in TEST_TEMPLATE + + def test_lambda_function_name_baked_in(self): + """Deploy script bakes LAMBDA_FUNCTION_NAME into test script.""" + assert "LAMBDA_FUNCTION_NAME" in TEST_TEMPLATE + + def test_username_baked_in(self): + """Deploy script bakes TEST_USERNAME into test script.""" + assert "TEST_USERNAME" in TEST_TEMPLATE + + def test_password_baked_in(self): + """Deploy script bakes TEST_PASSWORD into test script.""" + assert "TEST_PASSWORD" in TEST_TEMPLATE From 324897614c9071e075191520da2712503284384f Mon Sep 17 00:00:00 2001 From: Mike Hume Date: Tue, 21 Jul 2026 16:59:58 +0100 Subject: [PATCH 2/2] Address PR #3221 review feedback - rename template, update tests and scripts --- strands-agentcore-smithy/.gitignore | 3 - .../agentcore-smithy-dynamodb/.config.kiro | 1 + .../specs/agentcore-smithy-dynamodb/design.md | 383 ++++++++++++++++++ .../agentcore-smithy-dynamodb/requirements.md | 163 ++++++++ .../specs/agentcore-smithy-dynamodb/tasks.md | 198 +++++++++ .../steering/agentcore-smithy-dynamodb.md | 264 ++++++++++++ strands-agentcore-smithy/Makefile | 31 -- strands-agentcore-smithy/README.md | 46 ++- strands-agentcore-smithy/example-pattern.json | 18 +- .../steering/agentcore-smithy-dynamodb.md | 196 +++++++++ .../strands-agentcore-smithy-handover.md | 130 ++++++ ...late.yaml => cloudformation-template.yaml} | 51 ++- strands-agentcore-smithy/scripts/deploy.sh | 186 +++++++-- strands-agentcore-smithy/scripts/test.sh | 6 +- strands-agentcore-smithy/src/agent/handler.py | 2 +- .../src/agent/strands_client.py | 10 +- strands-agentcore-smithy/tests/conftest.py | 4 +- .../test_cloudformation_properties.py | 2 +- .../property/test_deploy_script_properties.py | 42 +- .../unit/test_cloudformation_template.py | 2 +- .../tests/unit/test_deploy_script.py | 164 ++++---- 21 files changed, 1672 insertions(+), 230 deletions(-) create mode 100644 strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/.config.kiro create mode 100644 strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/design.md create mode 100644 strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/requirements.md create mode 100644 strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/tasks.md create mode 100644 strands-agentcore-smithy/.kiro/steering/agentcore-smithy-dynamodb.md delete mode 100644 strands-agentcore-smithy/Makefile create mode 100644 strands-agentcore-smithy/handoff/steering/agentcore-smithy-dynamodb.md create mode 100644 strands-agentcore-smithy/handoff/strands-agentcore-smithy-handover.md rename strands-agentcore-smithy/infrastructure/{template.yaml => cloudformation-template.yaml} (80%) diff --git a/strands-agentcore-smithy/.gitignore b/strands-agentcore-smithy/.gitignore index 6475b62f13..c507587e83 100644 --- a/strands-agentcore-smithy/.gitignore +++ b/strands-agentcore-smithy/.gitignore @@ -1,9 +1,6 @@ *-deps/ *-package/ *.zip -.aws-sam/ -samconfig.toml -.kiro/ .venv/ venv/ __pycache__/ diff --git a/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/.config.kiro b/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/.config.kiro new file mode 100644 index 0000000000..360eaaa6bb --- /dev/null +++ b/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/.config.kiro @@ -0,0 +1 @@ +{"specId": "5c21674c-a977-46ba-9d10-43757b0cff2d", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/design.md b/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/design.md new file mode 100644 index 0000000000..1b7ceeccd4 --- /dev/null +++ b/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/design.md @@ -0,0 +1,383 @@ +# Design Document: AgentCore Smithy DynamoDB + +## Overview + +This design describes a serverless AI agent that exposes DynamoDB operations (GetItem, PutItem, Query) through AWS Bedrock AgentCore Gateway using a Smithy model target. The agent uses the Strands SDK with Bedrock Claude 3 Sonnet to interpret natural language requests and translate them into DynamoDB operations via MCP protocol. Authentication is handled by Cognito JWT tokens validated by a CUSTOM_JWT authorizer on the gateway. The gateway uses GATEWAY_IAM_ROLE credentials to access DynamoDB directly, eliminating API keys or credential provider configuration. + +The key differentiator from the API Gateway variant is the Smithy model target type: instead of routing through API Gateway, the gateway interprets a Smithy 2.0 model definition to directly invoke DynamoDB operations. This simplifies the deployment (no API Gateway resources, no secrets, no credential provider dance) while keeping the agent code completely target-type agnostic. + +### Design Decisions + +1. **Inline Smithy Model**: The Smithy model is embedded as an InlinePayload in CloudFormation rather than uploaded to S3, since the DynamoDB operations model is well under the 10MB limit. This keeps deployment self-contained in a single template. + +2. **GATEWAY_IAM_ROLE over API_KEY**: Since DynamoDB is an AWS service using IAM auth, GATEWAY_IAM_ROLE is the natural credential type. This eliminates the credential provider CLI commands, Secrets Manager secrets, and stack re-update steps required by the API Gateway variant. + +3. **Reusable Agent Code**: The agent handler, processor, MCP client, and shared modules are identical to the API Gateway project. They connect to AgentCore Gateway via MCP and are unaware of the target type behind the gateway. + +4. **Single CloudFormation Template**: All infrastructure (DynamoDB table, AgentCore Gateway, GatewayTarget, Cognito, Lambda, IAM roles) is defined in one template for atomic deployment and teardown. + +## Architecture + +```mermaid +flowchart LR + User["User (test.sh)"] + Lambda["Agent Lambda\n(Strands SDK +\nClaude 3 Sonnet)"] + Cognito["Cognito\nUser Pool"] + Gateway["AgentCore Gateway\n(MCP, CUSTOM_JWT)"] + Smithy["Smithy Model Target\n(GetItem, PutItem, Query)"] + DynamoDB["DynamoDB\n(Product Table)"] + + User -->|"1. Auth (USER_PASSWORD_AUTH)"| Cognito + Cognito -->|"2. JWT ID Token"| User + User -->|"3. Invoke with JWT + prompt"| Lambda + Lambda -->|"4. MCP tools/list & tools/call"| Gateway + Gateway -->|"5. Validate JWT"| Cognito + Gateway -->|"6. Smithy → DynamoDB API"| Smithy + Smithy -->|"7. IAM Role auth"| DynamoDB +``` + +### Request Flow + +1. User authenticates with Cognito via `USER_PASSWORD_AUTH` to get a JWT ID token +2. User invokes the Agent Lambda with the JWT token and a natural language prompt +3. Lambda validates the JWT, creates a Strands Agent with MCP client connected to the gateway +4. The LLM (Claude 3 Sonnet) analyzes the prompt and selects the appropriate DynamoDB tool +5. The MCP client sends a `tools/call` request to the gateway +6. The gateway validates the JWT via CUSTOM_JWT authorizer (Cognito OIDC discovery) +7. The gateway translates the MCP call to a DynamoDB API request using the Smithy model +8. The gateway assumes the execution role (GATEWAY_IAM_ROLE) to authenticate with DynamoDB +9. DynamoDB returns the result, which flows back through the gateway → MCP → Agent → User + +## Components and Interfaces + +### 1. CloudFormation Template (`infrastructure/cloudformation-template.yaml`) + +Single template defining all resources: + +| Resource | Type | Purpose | +|----------|------|---------| +| `ProductTable` | `AWS::DynamoDB::Table` | Product data store with partition key + sort key | +| `AgentCoreGateway` | `AWS::BedrockAgentCore::Gateway` | MCP gateway with CUSTOM_JWT auth | +| `SmithyTarget` | `AWS::BedrockAgentCore::GatewayTarget` | Smithy model target for DynamoDB ops | +| `CognitoUserPool` | `AWS::Cognito::UserPool` | User authentication | +| `CognitoUserPoolClient` | `AWS::Cognito::UserPoolClient` | Client for USER_PASSWORD_AUTH flow | +| `AgentLambdaFunction` | `AWS::Lambda::Function` | Strands SDK agent | +| `AgentLambdaRole` | `AWS::IAM::Role` | Lambda execution role (Bedrock + AgentCore + Logs) | +| `GatewayExecutionRole` | `AWS::IAM::Role` | Gateway role (4 ARN patterns + DynamoDB) | + +### 2. Smithy Model (Inline in CloudFormation) + +The Smithy 2.0 JSON model defines three DynamoDB operations under the `aws.protocols#restJson1` protocol: + +``` +Service: example.dynamodb#DynamoDBProductService (restJson1) +├── GetItem — POST /getitem — Retrieve single item by TableName + Key +├── PutItem — POST /putitem — Create/update item by TableName + Item +└── Query — POST /query — Query by TableName + KeyConditionExpression +``` + +Each operation has: +- `smithy.api#http` trait (POST method, URI path) +- `smithy.api#documentation` traits on service, operations, and all members (for LLM tool selection) +- `smithy.api#required` traits on mandatory input members +- Input/output structures matching DynamoDB API shapes + +### 3. Agent Lambda (`src/agent/`) + +| Module | Responsibility | +|--------|---------------| +| `handler.py` | Lambda entry point; extracts JWT, creates UserContext, delegates to agent_processor | +| `agent_processor.py` | Creates MCP client, Strands Agent; manages session lifecycle in try/finally | +| `strands_client.py` | Factory functions for MCP client and Bedrock model configuration | + +All modules are target-type agnostic — reused from the API Gateway project without modification. + +### 4. Shared Modules (`src/shared/`) + +| Module | Responsibility | +|--------|---------------| +| `models.py` | Data classes: `UserContext`, `AgentRequest`, `AgentResponse` | +| `jwt_utils.py` | JWT validation: accepts `token_use: access` and `id`, disables `verify_aud`, extracts `cognito:username` | +| `error_utils.py` | Standardized error response formatting | +| `logging_utils.py` | Structured logging with correlation IDs | + +### 5. Deploy Script (`scripts/deploy.sh`) + +Orchestrates the full deployment lifecycle: + +``` +1. Validate CloudFormation template +2. Deploy stack (create or update, detect via DOES_NOT_EXIST) +3. Package Lambda (two-step pip3 install) +4. Deploy Lambda code (S3 fallback if >50MB) +5. Seed DynamoDB with sample products +6. Create Cognito test user +7. Generate scripts/test.sh with baked-in values +``` + +### 6. Test Script (`scripts/test.sh` — generated) + +Generated by deploy.sh with baked-in gateway endpoint, Cognito pool ID, client ID, username, and password. Accepts optional natural language prompt as CLI argument. + +### Interface: Agent Lambda ↔ AgentCore Gateway + +- Protocol: MCP (Model Context Protocol) +- Operations: `tools/list` (discover available tools), `tools/call` (invoke a tool) +- Authentication: JWT ID token passed in MCP session headers +- The gateway translates MCP tool calls to DynamoDB API calls using the Smithy model + +### Interface: AgentCore Gateway ↔ DynamoDB + +- Authentication: GATEWAY_IAM_ROLE (gateway assumes execution role) +- Operations: `dynamodb:GetItem`, `dynamodb:PutItem`, `dynamodb:Query` +- The Smithy model defines the request/response shapes; the gateway handles serialization + +## Data Models + +### DynamoDB Product Table Schema + +| Attribute | Type | Key | Description | +|-----------|------|-----|-------------| +| `category` | String (S) | Partition Key (HASH) | Product category (e.g., "Electronics", "Books") | +| `productId` | String (S) | Sort Key (RANGE) | Unique product identifier within category | +| `name` | String (S) | — | Product display name | +| `price` | Number (N) | — | Product price | +| `description` | String (S) | — | Product description | +| `inStock` | Boolean (BOOL) | — | Availability flag | + +Billing mode: `PAY_PER_REQUEST` + +### Sample Seed Data + +```json +[ + {"category": "Electronics", "productId": "ELEC-001", "name": "Wireless Mouse", "price": 29.99, "description": "Ergonomic wireless mouse", "inStock": true}, + {"category": "Electronics", "productId": "ELEC-002", "name": "USB-C Hub", "price": 49.99, "description": "7-port USB-C hub", "inStock": true}, + {"category": "Books", "productId": "BOOK-001", "name": "Python Cookbook", "price": 39.99, "description": "Advanced Python recipes", "inStock": true}, + {"category": "Books", "productId": "BOOK-002", "name": "Cloud Architecture", "price": 54.99, "description": "Designing cloud systems", "inStock": false} +] +``` + +This covers multiple categories (for Query demonstrations), multiple items per category (for GetItem demonstrations), and varied attributes (for PutItem demonstrations). + +### Smithy Model Input/Output Structures + +**GetItem**: +- Input: `TableName` (required, string), `Key` (required, map of string → AttributeValue) +- Output: `Item` (map of string → AttributeValue) + +**PutItem**: +- Input: `TableName` (required, string), `Item` (required, map of string → AttributeValue) +- Output: `Attributes` (map of string → AttributeValue) + +**Query**: +- Input: `TableName` (required, string), `KeyConditionExpression` (required, string), `ExpressionAttributeValues` (required, map of string → AttributeValue) +- Output: `Items` (list of map of string → AttributeValue), `Count` (integer) + +### Application Data Models (`src/shared/models.py`) + +```python +@dataclass +class UserContext: + username: str + token: str + +@dataclass +class AgentRequest: + prompt: str + user_context: UserContext + +@dataclass +class AgentResponse: + success: bool + response: str + error: Optional[str] = None +``` + +### IAM Role Structures + +**GatewayExecutionRole** trust policy: +- Principal: `bedrock-agentcore.amazonaws.com` +- Policies: + - AgentCore access (4 ARN patterns with `bedrock-agentcore:*`) + - DynamoDB access (`dynamodb:GetItem`, `dynamodb:PutItem`, `dynamodb:Query` on ProductTable ARN) + +**AgentLambdaRole** trust policy: +- Principal: `lambda.amazonaws.com` +- Policies: + - Bedrock access (`bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`, `bedrock:Converse`, `bedrock:ConverseStream`) + - AgentCore access (`bedrock-agentcore:InvokeGateway`) + - CloudWatch Logs (`logs:CreateLogGroup`, `logs:CreateLogStream`, `logs:PutLogEvents`) + + + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: Smithy operation structure completeness + +*For all* operations defined in the Smithy model, each operation must have a valid input structure with all mandatory members marked with the `smithy.api#required` trait, and a valid output structure that captures the DynamoDB response shape. + +**Validates: Requirements 1.5, 1.6** + +### Property 2: Smithy documentation coverage + +*For all* shapes in the Smithy model (service, operations, input/output structures, and their members), each shape must include a `smithy.api#documentation` trait with a non-empty string value. + +**Validates: Requirements 1.7** + +### Property 3: No streaming or custom protocols in Smithy model + +*For all* operations in the Smithy model, none shall have streaming traits (`smithy.api#streaming`) or use protocols other than `aws.protocols#restJson1`. + +**Validates: Requirements 1.9** + +### Property 4: JWT validation accepts both token_use values + +*For any* valid JWT token with `token_use` claim set to either `"access"` or `"id"`, the JWT validation function shall accept the token without raising a validation error. + +**Validates: Requirements 4.4** + +### Property 5: Username extraction from cognito:username claim + +*For any* JWT ID token containing a `cognito:username` claim, the username extraction function shall return the value of the `cognito:username` claim (not the `username` or `sub` claim). + +**Validates: Requirements 4.6** + +### Property 6: No API Gateway resources in CloudFormation template + +*For all* resources defined in the CloudFormation template, none shall have a resource type matching `AWS::ApiGateway::*` or `AWS::ApiGatewayV2::*`. + +**Validates: Requirements 7.4** + +### Property 7: Deploy script uses pip3 exclusively + +*For all* pip install commands in the deploy script, each command shall use `pip3` (not `pip`) as the executable name. + +**Validates: Requirements 9.7** + +### Property 8: Agent code is target-type agnostic + +*For all* source files in `src/agent/` and `src/shared/`, none shall contain references to DynamoDB-specific identifiers (e.g., `dynamodb`, `DynamoDB`, `GetItem`, `PutItem`, `Query`, `TableName`) or Smithy-specific identifiers (e.g., `smithy`, `SmithyModel`, `InlinePayload`). + +**Validates: Requirements 11.1** + +## Error Handling + +### Agent Lambda Error Handling + +| Error Scenario | Handling Strategy | +|----------------|-------------------| +| Missing/invalid JWT token | Return 401 with "Unauthorized" message; log the validation failure | +| Expired JWT token | Return 401 with "Token expired" message | +| MCP connection failure | Return 500 with "Gateway connection error"; log full exception in structured format | +| LLM invocation failure | Return 500 with "Model invocation error"; retry is handled by Strands SDK internally | +| MCP tools/call failure | Return 500 with "Tool execution error"; include tool name in error context | +| Missing environment variables | Fail fast at Lambda cold start with descriptive error message | +| Lambda timeout (>120s) | Strands SDK handles graceful shutdown; partial response returned if possible | + +### MCP Session Lifecycle + +The agent processor manages the MCP session in a `try/finally` block (not a `with` context manager): + +```python +mcp_client = create_mcp_client(gateway_url, token) +try: + agent = Agent(model=bedrock_model, tools=[mcp_client]) + result = agent(prompt) + return AgentResponse(success=True, response=str(result)) +finally: + mcp_client.cleanup() +``` + +This ensures cleanup happens even if the LLM or tool invocation raises an exception. + +### CloudFormation Deployment Errors + +| Error Scenario | Handling Strategy | +|----------------|-------------------| +| Template validation failure | Script exits with error message before attempting deployment | +| Stack creation failure | CloudFormation rolls back automatically; script reports the failure | +| Stack update with no changes | Script detects "No updates are to be performed" and continues | +| Lambda package >50MB | Script falls back to S3 upload; requires pre-existing S3 bucket | + +### DynamoDB Error Handling (Gateway-Side) + +The gateway handles DynamoDB errors and translates them to MCP error responses. The agent code does not handle DynamoDB errors directly since it communicates via MCP protocol only. + +## Testing Strategy + +### Dual Testing Approach + +This project uses both unit tests and property-based tests for comprehensive coverage: + +- **Unit tests**: Verify specific examples, edge cases, CloudFormation template structure, and script correctness +- **Property-based tests**: Verify universal properties across generated inputs (Smithy model validity, JWT handling, code agnosticism) + +### Property-Based Testing Configuration + +- **Library**: [Hypothesis](https://hypothesis.readthedocs.io/) for Python +- **Minimum iterations**: 100 per property test +- **Tag format**: `Feature: agentcore-smithy-dynamodb, Property {number}: {property_text}` +- Each correctness property is implemented by a single property-based test + +### Unit Tests + +| Test Area | What to Test | +|-----------|-------------| +| CloudFormation template structure | Gateway uses ProtocolType: MCP, CUSTOM_JWT authorizer, RoleArn (not ExecutionRoleArn), AllowedAudience (not Audience), CustomJWTAuthorizer casing, DiscoveryUrl suffix | +| Gateway execution role | All 4 ARN patterns present, DynamoDB permissions present, no apigateway:GET | +| Smithy model structure | Service has restJson1 trait, all 3 operations defined with POST method, InlinePayload nesting | +| Lambda configuration | Python 3.12, x86_64, timeout ≥120s, memory ≥1024MB, correct IAM permissions | +| Cognito configuration | USER_PASSWORD_AUTH flow, User Pool Client association | +| Deploy script | Two-step pip3 install, no .dist-info removal, DOES_NOT_EXIST check, S3 fallback logic, no credential provider commands | +| Test script | No nested JSON echo, executable permissions, default prompt handling, auth before invoke | +| JWT validation | Accepts token_use: id, accepts token_use: access, verify_aud disabled, cognito:username extraction | +| Data models | UserContext, AgentRequest, AgentResponse serialization | + +### Property-Based Tests + +| Property | Test Description | Generator Strategy | +|----------|-----------------|-------------------| +| Property 1: Smithy operation structure completeness | Parse Smithy model, verify all operations have valid input/output structures with required traits | Generate random operation names and member sets; verify structural invariants hold | +| Property 2: Smithy documentation coverage | Parse Smithy model, verify all shapes have documentation traits | Generate random Smithy shapes; verify documentation trait presence | +| Property 3: No streaming in Smithy model | Parse Smithy model, verify no streaming traits | Generate random operation trait sets; verify streaming traits are absent | +| Property 4: JWT accepts both token_use values | Generate JWTs with random claims including token_use: access or id | Generate random JWT payloads with varying claims; verify acceptance | +| Property 5: Username from cognito:username | Generate JWTs with cognito:username claim; verify extraction | Generate random usernames; verify cognito:username is extracted over username | +| Property 6: No API Gateway resources | Parse CloudFormation template; verify no API Gateway resource types | Generate random resource type strings; verify API Gateway types are filtered | +| Property 7: pip3 exclusively | Parse deploy script; verify all pip commands use pip3 | Generate random pip command variations; verify pip3 detection | +| Property 8: Agent code target-agnostic | Scan source files for DynamoDB/Smithy identifiers | Generate random identifier strings; verify DynamoDB/Smithy terms are detected | + +### Test File Organization + +``` +tests/ +├── unit/ +│ ├── test_cloudformation_template.py # Template structure validation +│ ├── test_smithy_model.py # Smithy model structure +│ ├── test_jwt_utils.py # JWT validation logic +│ ├── test_models.py # Data model tests +│ ├── test_deploy_script.py # Deploy script pattern checks +│ └── test_test_script.py # Generated test script checks +├── property/ +│ ├── test_smithy_properties.py # Properties 1, 2, 3 +│ ├── test_jwt_properties.py # Properties 4, 5 +│ ├── test_template_properties.py # Property 6 +│ ├── test_script_properties.py # Property 7 +│ └── test_agnostic_properties.py # Property 8 +└── conftest.py # Shared fixtures +``` + +### Integration Testing + +Integration tests are run manually via the generated `scripts/test.sh` after deployment. They exercise the full flow: +1. Authenticate with Cognito to get JWT ID token +2. Invoke Agent Lambda with natural language prompts +3. Verify the agent correctly routes to GetItem, PutItem, or Query operations +4. Verify DynamoDB data is returned/modified as expected + +Example test prompts: +- `"Show me all Electronics products"` → triggers Query +- `"Get details for product ELEC-001 in Electronics"` → triggers GetItem +- `"Add a new product called Headphones for $79.99 in Electronics"` → triggers PutItem diff --git a/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/requirements.md b/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/requirements.md new file mode 100644 index 0000000000..205782d255 --- /dev/null +++ b/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/requirements.md @@ -0,0 +1,163 @@ +# Requirements Document + +## Introduction + +This feature builds a serverless AI agent that exposes DynamoDB operations (GetItem, PutItem, Query) through AWS Bedrock AgentCore Gateway using a Smithy model target type. Users interact with the agent via natural language, and the Strands SDK with Claude 3 Sonnet determines which DynamoDB operation to invoke. Authentication is handled via Cognito JWT tokens with a CUSTOM_JWT authorizer on the gateway. The gateway uses GATEWAY_IAM_ROLE credentials to access DynamoDB directly, eliminating the need for API keys or credential provider configuration. The entire stack deploys via a single bash script and targets the us-east-1 region. + +## Glossary + +- **Agent_Lambda**: The AWS Lambda function (Python 3.12, x86_64) running the Strands SDK agent that processes user requests via Bedrock Claude 3 Sonnet and communicates with AgentCore Gateway over MCP protocol +- **AgentCore_Gateway**: The AWS Bedrock AgentCore Gateway resource configured with MCP protocol, CUSTOM_JWT authorizer, and a Smithy model target pointing to DynamoDB +- **Smithy_Model**: A JSON-format Smithy 2.0 API model using the aws.protocols#restJson1 trait that defines the DynamoDB operations (GetItem, PutItem, Query) exposed through the gateway +- **Cognito_Pool**: The Amazon Cognito User Pool and User Pool Client used to authenticate users and issue JWT ID tokens for the CUSTOM_JWT authorizer +- **Gateway_Execution_Role**: The IAM role assumed by AgentCore Gateway that includes the four core AgentCore ARN patterns and DynamoDB access permissions +- **Product_Table**: The DynamoDB table storing product data with a partition key and optional sort key, used as the target data store +- **Deploy_Script**: The bash script (scripts/deploy.sh) that orchestrates CloudFormation deployment, Lambda packaging, DynamoDB seeding, Cognito user creation, and test script generation +- **Test_Script**: The generated bash script (scripts/test.sh) with baked-in environment values for invoking the agent with natural language prompts +- **MCP_Client**: The Model Context Protocol client within the agent that connects to AgentCore Gateway to discover and invoke tools +- **Lambda_Package**: The deployment artifact containing the agent source code and pip-installed dependencies, packaged as a zip file with a two-step pip install process + +## Requirements + +### Requirement 1: Smithy Model Definition for DynamoDB Operations + +**User Story:** As a developer, I want a Smithy model that defines GetItem, PutItem, and Query operations for DynamoDB, so that AgentCore Gateway can translate MCP tool calls into DynamoDB API requests. + +#### Acceptance Criteria + +1. THE Smithy_Model SHALL define a service shape with the `aws.protocols#restJson1` trait and a version of "1.0.0" +2. THE Smithy_Model SHALL define a GetItem operation with an HTTP trait specifying a POST method and a URI path +3. THE Smithy_Model SHALL define a PutItem operation with an HTTP trait specifying a POST method and a URI path +4. THE Smithy_Model SHALL define a Query operation with an HTTP trait specifying a POST method and a URI path +5. THE Smithy_Model SHALL define input structures for each operation with required members marked using the `smithy.api#required` trait +6. THE Smithy_Model SHALL define output structures for each operation that capture the DynamoDB response shape +7. THE Smithy_Model SHALL include `smithy.api#documentation` traits on the service, each operation, and each input/output member to enable the LLM to select the correct tool +8. THE Smithy_Model SHALL be provided as an InlinePayload in the CloudFormation template and remain under 10MB in size +9. THE Smithy_Model SHALL NOT include streaming operations or custom protocols + +### Requirement 2: AgentCore Gateway Configuration + +**User Story:** As a developer, I want the AgentCore Gateway configured with MCP protocol, CUSTOM_JWT authentication, and a Smithy model target, so that the agent can securely invoke DynamoDB operations through the gateway. + +#### Acceptance Criteria + +1. THE AgentCore_Gateway SHALL use `ProtocolType: MCP` as the protocol configuration +2. THE AgentCore_Gateway SHALL use `AuthorizerType: CUSTOM_JWT` with a `CustomJWTAuthorizer` configuration block (all caps JWT) +3. WHEN configuring the CustomJWTAuthorizer, THE AgentCore_Gateway SHALL set the DiscoveryUrl to the Cognito User Pool OIDC endpoint ending with `/.well-known/openid-configuration` +4. WHEN configuring the CustomJWTAuthorizer, THE AgentCore_Gateway SHALL use `AllowedAudience` (not `Audience`) set to the Cognito User Pool Client ID +5. THE AgentCore_Gateway SHALL reference the Gateway_Execution_Role using the `RoleArn` property (not `ExecutionRoleArn`) +6. THE AgentCore_Gateway SHALL have a GatewayTarget resource with TargetConfiguration nested as `Mcp.SmithyModel.InlinePayload` +7. THE AgentCore_Gateway SHALL use `GATEWAY_IAM_ROLE` as the CredentialProviderType on the target (not `API_KEY`) + +### Requirement 3: Gateway Execution Role with DynamoDB Permissions + +**User Story:** As a developer, I want the gateway execution role to include all required AgentCore ARN patterns and DynamoDB permissions, so that tool invocations succeed without "Internal Error" responses. + +#### Acceptance Criteria + +1. THE Gateway_Execution_Role SHALL include a policy granting `bedrock-agentcore:*` access to the token-vault default ARN pattern (`arn:aws:bedrock-agentcore:{region}:{account}:token-vault/default`) +2. THE Gateway_Execution_Role SHALL include a policy granting access to the apikeycredentialprovider wildcard ARN pattern (`arn:aws:bedrock-agentcore:{region}:{account}:token-vault/default/apikeycredentialprovider/*`) +3. THE Gateway_Execution_Role SHALL include a policy granting access to the workload-identity-directory default ARN pattern (`arn:aws:bedrock-agentcore:{region}:{account}:workload-identity-directory/default`) +4. THE Gateway_Execution_Role SHALL include a policy granting access to the workload-identity-directory wildcard ARN pattern (`arn:aws:bedrock-agentcore:{region}:{account}:workload-identity-directory/default/workload-identity/{gateway-name}-*`) +5. THE Gateway_Execution_Role SHALL include a policy granting `dynamodb:GetItem`, `dynamodb:PutItem`, and `dynamodb:Query` actions on the Product_Table ARN +6. THE Gateway_Execution_Role SHALL NOT include `apigateway:GET` permissions + +### Requirement 4: Cognito Authentication + +**User Story:** As a developer, I want Cognito-based JWT authentication, so that only authenticated users can invoke the agent and access DynamoDB data through the gateway. + +#### Acceptance Criteria + +1. THE Cognito_Pool SHALL create a User Pool with `USER_PASSWORD_AUTH` as an explicit authentication flow +2. THE Cognito_Pool SHALL create a User Pool Client associated with the User Pool +3. WHEN a user authenticates, THE Cognito_Pool SHALL issue a JWT ID token that the CUSTOM_JWT authorizer validates +4. THE Agent_Lambda SHALL validate incoming JWT tokens by accepting both `token_use: access` and `token_use: id` claim values +5. THE Agent_Lambda SHALL disable audience verification (`verify_aud: False`) during JWT validation since the gateway handles audience validation via AllowedAudience +6. THE Agent_Lambda SHALL extract the username from the `cognito:username` claim (not the `username` claim) when processing ID tokens + +### Requirement 5: Agent Lambda with Strands SDK + +**User Story:** As a developer, I want a Lambda function running the Strands SDK with Claude 3 Sonnet that connects to AgentCore Gateway via MCP, so that users can interact with DynamoDB through natural language. + +#### Acceptance Criteria + +1. THE Agent_Lambda SHALL use Python 3.12 runtime with x86_64 architecture +2. THE Agent_Lambda SHALL have a minimum timeout of 120 seconds and minimum memory of 1024 MB +3. THE Agent_Lambda SHALL use the Strands SDK with Bedrock Claude 3 Sonnet as the LLM provider +4. THE Agent_Lambda SHALL connect to the AgentCore_Gateway using the MCP_Client to discover and invoke tools +5. THE Agent_Lambda SHALL NOT use a `with mcp_client:` context manager; the Agent manages the MCP session and cleanup occurs in a `finally` block +6. THE Agent_Lambda SHALL have IAM permissions for `bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`, `bedrock:Converse`, and `bedrock:ConverseStream` +7. THE Agent_Lambda SHALL have IAM permissions for `bedrock-agentcore:InvokeGateway` on the AgentCore_Gateway resource + +### Requirement 6: DynamoDB Table and Sample Data + +**User Story:** As a developer, I want a DynamoDB table with sample product data, so that the agent has data to query and demonstrate all three operations. + +#### Acceptance Criteria + +1. THE Product_Table SHALL be created as a DynamoDB table with a partition key and an optional sort key +2. THE Product_Table SHALL use PAY_PER_REQUEST billing mode +3. WHEN the Deploy_Script runs, THE Deploy_Script SHALL seed the Product_Table with sample product records covering multiple partition key values +4. THE Product_Table SHALL store items with attributes sufficient to demonstrate GetItem (single item retrieval), PutItem (item creation), and Query (partition key filtering) operations + +### Requirement 7: CloudFormation Infrastructure + +**User Story:** As a developer, I want all infrastructure defined in a single CloudFormation template, so that the entire stack can be deployed and torn down as a unit. + +#### Acceptance Criteria + +1. THE CloudFormation template SHALL define all resources in a single file at `infrastructure/cloudformation-template.yaml` +2. THE CloudFormation template SHALL deploy all resources in the us-east-1 region +3. THE CloudFormation template SHALL include the Product_Table, AgentCore_Gateway, GatewayTarget, Cognito_Pool, Agent_Lambda, and all IAM roles +4. THE CloudFormation template SHALL NOT include any API Gateway resources (RestApi, ApiKey, UsagePlan, stages, deployments, methods) +5. THE CloudFormation template SHALL NOT include Secrets Manager secrets for target authentication +6. THE CloudFormation template SHALL export the Gateway ID, Cognito User Pool ID, Cognito Client ID, and Lambda function name as stack outputs + +### Requirement 8: Lambda Packaging with Two-Step pip Install + +**User Story:** As a developer, I want Lambda dependencies packaged correctly using a two-step pip install, so that both binary and pure Python packages are included in the deployment artifact. + +#### Acceptance Criteria + +1. WHEN packaging the Lambda, THE Deploy_Script SHALL execute a first pip install step with `--only-binary=:all:` and `--platform manylinux2014_x86_64` and `--python-version 3.12` to install binary packages +2. WHEN packaging the Lambda, THE Deploy_Script SHALL execute a second pip install step with `--no-deps` to install pure Python packages that the first step silently skips (requests, urllib3, charset-normalizer, idna, certifi, PyJWT, cryptography, cffi) +3. THE Deploy_Script SHALL NOT remove `.dist-info` directories from the Lambda_Package (opentelemetry requires them) +4. IF the Lambda_Package zip file exceeds 50MB, THEN THE Deploy_Script SHALL upload the zip to S3 and update the Lambda function from the S3 location +5. THE Lambda_Package SHALL include all four dependencies: strands-agents, mcp, requests, and PyJWT with crypto extras + +### Requirement 9: Deployment Script + +**User Story:** As a developer, I want a single deploy script that handles the full deployment lifecycle, so that I can deploy the entire stack with one command. + +#### Acceptance Criteria + +1. THE Deploy_Script SHALL validate the CloudFormation template before deployment +2. THE Deploy_Script SHALL detect whether the CloudFormation stack exists and perform a create or update accordingly (checking for `DOES_NOT_EXIST` pattern) +3. THE Deploy_Script SHALL package and deploy the Lambda code after the CloudFormation stack is created +4. THE Deploy_Script SHALL seed the Product_Table with sample data after stack deployment +5. THE Deploy_Script SHALL create a test user in the Cognito_Pool with a predefined username and password +6. THE Deploy_Script SHALL generate the Test_Script at `scripts/test.sh` with baked-in values for gateway endpoint, Cognito pool ID, client ID, username, and password +7. THE Deploy_Script SHALL use `pip3` (not `pip`) for all package installation commands +8. THE Deploy_Script SHALL NOT include credential provider CLI commands, API key retrieval, or stack re-update steps + +### Requirement 10: Test Script Generation + +**User Story:** As a developer, I want a generated test script with baked-in configuration values, so that I can test the agent immediately after deployment without manual configuration. + +#### Acceptance Criteria + +1. WHEN the Deploy_Script completes, THE Deploy_Script SHALL generate `scripts/test.sh` with executable permissions +2. THE Test_Script SHALL accept an optional natural language prompt as a command-line argument and use a default prompt when none is provided +3. THE Test_Script SHALL authenticate with the Cognito_Pool to obtain a JWT ID token before invoking the agent +4. THE Test_Script SHALL invoke the Agent_Lambda with the JWT token and the user prompt +5. THE Test_Script SHALL NOT use nested JSON echo commands that break shell variable substitution for `${ID_TOKEN}` + +### Requirement 11: Reusable Agent Code + +**User Story:** As a developer, I want the agent code to be target-type agnostic, so that the same handler, agent processor, MCP client, and shared modules work regardless of whether the gateway target is API Gateway, Smithy, or another type. + +#### Acceptance Criteria + +1. THE Agent_Lambda handler, agent processor, MCP client factory, and shared modules SHALL NOT contain any DynamoDB-specific or Smithy-specific logic +2. THE Agent_Lambda source code SHALL be organized into `src/agent/` (handler.py, agent_processor.py, strands_client.py) and `src/shared/` (models.py, jwt_utils.py, error_utils.py, logging_utils.py) +3. THE Agent_Lambda shared modules SHALL include structured logging, error handling utilities, JWT validation, and data models for UserContext, AgentRequest, and AgentResponse diff --git a/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/tasks.md b/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/tasks.md new file mode 100644 index 0000000000..5447228cee --- /dev/null +++ b/strands-agentcore-smithy/.kiro/specs/agentcore-smithy-dynamodb/tasks.md @@ -0,0 +1,198 @@ +# Implementation Plan: AgentCore Smithy DynamoDB + +## Overview + +Build a serverless AI agent that exposes DynamoDB operations through AWS Bedrock AgentCore Gateway using a Smithy model target. Implementation proceeds bottom-up: shared modules first, then infrastructure, then agent code, then deployment/test scripts. Python 3.12 throughout. + +## Tasks + +- [x] 1. Create shared modules (`src/shared/`) + - [x] 1.1 Create `src/shared/__init__.py` and `src/shared/models.py` + - Define `UserContext`, `AgentRequest`, `AgentResponse` dataclasses + - `AgentResponse` includes optional `error` field + - _Requirements: 11.2, 11.3_ + + - [x] 1.2 Create `src/shared/jwt_utils.py` + - Implement JWT validation that accepts both `token_use: access` and `token_use: id` + - Disable audience verification (`verify_aud: False`) + - Extract username from `cognito:username` claim (not `username` or `sub`) + - _Requirements: 4.4, 4.5, 4.6, 11.3_ + + - [x] 1.3 Create `src/shared/error_utils.py` and `src/shared/logging_utils.py` + - Standardized error response formatting + - Structured logging with correlation IDs + - _Requirements: 11.3_ + + - [x] 1.4 Write property tests for JWT validation (Properties 4, 5) + - **Property 4: JWT validation accepts both token_use values** + - **Validates: Requirements 4.4** + - **Property 5: Username extraction from cognito:username claim** + - **Validates: Requirements 4.6** + + - [x] 1.5 Write unit tests for shared modules + - Test `UserContext`, `AgentRequest`, `AgentResponse` serialization + - Test JWT validation edge cases (missing claims, expired tokens) + - Test error_utils and logging_utils + - _Requirements: 4.4, 4.5, 4.6, 11.3_ + +- [x] 2. Create agent Lambda modules (`src/agent/`) + - [x] 2.1 Create `src/agent/__init__.py` and `src/agent/strands_client.py` + - Factory function for MCP client creation (gateway URL + token) + - Factory function for Bedrock model configuration (Claude 3 Sonnet) + - _Requirements: 5.3, 5.4, 11.1_ + + - [x] 2.2 Create `src/agent/agent_processor.py` + - Create MCP client, Strands Agent with Bedrock model + - Manage MCP session lifecycle in `try/finally` block (NOT `with` context manager) + - Return `AgentResponse` with success/error + - _Requirements: 5.4, 5.5, 11.1_ + + - [x] 2.3 Create `src/agent/handler.py` + - Lambda entry point: extract JWT from event, create `UserContext`, delegate to `agent_processor` + - Validate JWT, extract username, build `AgentRequest` + - Handle missing/invalid JWT with 401 response + - _Requirements: 5.1, 5.5, 5.6, 11.1_ + + - [x] 2.4 Write property test for agent code target-agnosticism (Property 8) + - **Property 8: Agent code is target-type agnostic** + - Scan all files in `src/agent/` and `src/shared/` for DynamoDB-specific or Smithy-specific identifiers + - **Validates: Requirements 11.1** + + - [x] 2.5 Write unit tests for agent modules + - Test handler JWT extraction and error responses + - Test agent_processor try/finally lifecycle + - Test strands_client factory functions + - _Requirements: 5.3, 5.4, 5.5_ + +- [x] 3. Checkpoint - Ensure all tests pass + - Ensure all tests pass, ask the user if questions arise. + +- [x] 4. Create CloudFormation template (`infrastructure/cloudformation-template.yaml`) + - [x] 4.1 Define DynamoDB ProductTable resource + - Partition key: `category` (S), Sort key: `productId` (S) + - Billing mode: `PAY_PER_REQUEST` + - _Requirements: 6.1, 6.2_ + + - [x] 4.2 Define Cognito UserPool and UserPoolClient resources + - UserPool with `USER_PASSWORD_AUTH` explicit auth flow + - UserPoolClient associated with the UserPool + - _Requirements: 4.1, 4.2_ + + - [x] 4.3 Define GatewayExecutionRole with all required permissions + - Trust policy for `bedrock-agentcore.amazonaws.com` + - All 4 AgentCore ARN patterns (`token-vault/default`, `apikeycredentialprovider/*`, `workload-identity-directory/default`, `workload-identity-directory/default/workload-identity/{gateway-name}-*`) + - DynamoDB permissions: `dynamodb:GetItem`, `dynamodb:PutItem`, `dynamodb:Query` on ProductTable ARN + - No `apigateway:GET` permissions + - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_ + + - [x] 4.4 Define AgentCore Gateway and GatewayTarget with inline Smithy model + - Gateway: `ProtocolType: MCP`, `AuthorizerType: CUSTOM_JWT` + - `CustomJWTAuthorizer` (all caps JWT) with `DiscoveryUrl` ending `/.well-known/openid-configuration` + - `AllowedAudience` (not `Audience`) set to Cognito Client ID + - `RoleArn` (not `ExecutionRoleArn`) referencing GatewayExecutionRole + - GatewayTarget: `CredentialProviderType: GATEWAY_IAM_ROLE` + - Target config nested as `Mcp.SmithyModel.InlinePayload` + - Smithy 2.0 JSON model with `aws.protocols#restJson1`, GetItem/PutItem/Query operations + - Each operation: POST method, URI path, `smithy.api#documentation` traits on all shapes + - Required members marked with `smithy.api#required` trait + - No streaming operations or custom protocols + - _Requirements: 1.1–1.9, 2.1–2.7_ + + - [x] 4.5 Define AgentLambdaRole and AgentLambdaFunction + - Lambda: Python 3.12, x86_64, timeout 120s, memory 1024MB + - IAM: `bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`, `bedrock:Converse`, `bedrock:ConverseStream` + - IAM: `bedrock-agentcore:InvokeGateway` on gateway resource + - IAM: CloudWatch Logs permissions + - _Requirements: 5.1, 5.2, 5.6, 5.7_ + + - [x] 4.6 Define stack outputs + - Export Gateway ID, Cognito User Pool ID, Cognito Client ID, Lambda function name + - _Requirements: 7.6_ + + - [x] 4.7 Write property tests for CloudFormation template (Properties 1, 2, 3, 6) + - **Property 1: Smithy operation structure completeness** + - **Validates: Requirements 1.5, 1.6** + - **Property 2: Smithy documentation coverage** + - **Validates: Requirements 1.7** + - **Property 3: No streaming or custom protocols in Smithy model** + - **Validates: Requirements 1.9** + - **Property 6: No API Gateway resources in CloudFormation template** + - **Validates: Requirements 7.4** + + - [x] 4.8 Write unit tests for CloudFormation template + - Verify gateway ProtocolType, AuthorizerType, CustomJWTAuthorizer casing, DiscoveryUrl suffix + - Verify AllowedAudience (not Audience), RoleArn (not ExecutionRoleArn) + - Verify GatewayExecutionRole has all 4 ARN patterns and DynamoDB permissions, no apigateway:GET + - Verify Lambda config: Python 3.12, x86_64, timeout ≥120s, memory ≥1024MB + - Verify Cognito USER_PASSWORD_AUTH flow + - Verify no API Gateway resources, no Secrets Manager secrets + - _Requirements: 1.1–1.9, 2.1–2.7, 3.1–3.6, 7.1–7.6_ + +- [x] 5. Checkpoint - Ensure all tests pass + - Ensure all tests pass, ask the user if questions arise. + +- [x] 6. Create deployment script (`scripts/deploy.sh`) + - [x] 6.1 Implement CloudFormation deployment logic + - Validate template before deployment + - Detect stack existence via `DOES_NOT_EXIST` pattern, create or update accordingly + - Handle "No updates are to be performed" gracefully + - _Requirements: 9.1, 9.2_ + + - [x] 6.2 Implement Lambda packaging with two-step pip install + - First step: `pip3 install --only-binary=:all: --platform manylinux2014_x86_64 --python-version 3.12` + - Second step: `pip3 install --no-deps` for pure Python packages (requests, urllib3, charset-normalizer, idna, certifi, PyJWT, cryptography, cffi) + - Do NOT remove `.dist-info` directories + - S3 fallback if zip exceeds 50MB + - Include strands-agents, mcp, requests, PyJWT[crypto] dependencies + - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5, 9.3_ + + - [x] 6.3 Implement DynamoDB seeding and Cognito user creation + - Seed ProductTable with sample product records across multiple categories + - Create test user in Cognito with predefined username/password + - _Requirements: 6.3, 6.4, 9.4, 9.5_ + + - [x] 6.4 Implement test script generation + - Generate `scripts/test.sh` with baked-in gateway endpoint, Cognito pool ID, client ID, username, password + - Set executable permissions on generated script + - Accept optional CLI prompt argument with default prompt + - Authenticate with Cognito before invoking Lambda + - No nested JSON echo commands that break `${ID_TOKEN}` substitution + - _Requirements: 9.6, 10.1, 10.2, 10.3, 10.4, 10.5_ + + - [x] 6.5 Ensure deploy script uses `pip3` exclusively and excludes forbidden patterns + - All pip commands use `pip3` (not `pip`) + - No credential provider CLI commands, API key retrieval, or stack re-update steps + - _Requirements: 9.7, 9.8_ + + - [x] 6.6 Write property test for deploy script (Property 7) + - **Property 7: Deploy script uses pip3 exclusively** + - **Validates: Requirements 9.7** + + - [x] 6.7 Write unit tests for deploy and test scripts + - Verify two-step pip3 install pattern, no .dist-info removal + - Verify DOES_NOT_EXIST check, S3 fallback logic + - Verify no credential provider commands + - Verify test script: no nested JSON echo, default prompt, auth before invoke + - _Requirements: 8.1–8.5, 9.1–9.8, 10.1–10.5_ + +- [x] 7. Create requirements.txt and test configuration + - [x] 7.1 Create `requirements.txt` with runtime dependencies + - strands-agents, mcp, requests, PyJWT[crypto] + - _Requirements: 8.5_ + + - [x] 7.2 Create `tests/conftest.py` with shared fixtures + - Fixtures for loading CloudFormation template, Smithy model, deploy script content + - Fixtures for sample JWT tokens and user contexts + - _Requirements: All_ + +- [x] 8. Final checkpoint - Ensure all tests pass + - Ensure all tests pass, ask the user if questions arise. + +## Notes + +- Tasks marked with `*` are optional and can be skipped for faster MVP +- Each task references specific requirements for traceability +- Checkpoints ensure incremental validation +- Property tests validate universal correctness properties from the design document +- Agent code in `src/agent/` and `src/shared/` must remain target-type agnostic (no DynamoDB/Smithy references) +- The Smithy model is embedded inline in the CloudFormation template, not in a separate file diff --git a/strands-agentcore-smithy/.kiro/steering/agentcore-smithy-dynamodb.md b/strands-agentcore-smithy/.kiro/steering/agentcore-smithy-dynamodb.md new file mode 100644 index 0000000000..4b6e5ddb40 --- /dev/null +++ b/strands-agentcore-smithy/.kiro/steering/agentcore-smithy-dynamodb.md @@ -0,0 +1,264 @@ +--- +inclusion: auto +--- + +# AgentCore Smithy Bedrock Runtime Agent — Implementation Guide + +This project builds a serverless AI agent using AWS Bedrock AgentCore Gateway with a Smithy model target type connecting to Bedrock Runtime. The agent uses Claude Sonnet as the main LLM and calls Claude Haiku as a tool via the gateway's Converse API. Follow these patterns strictly during implementation. + +## Architecture + +``` +User → Agent Lambda (Strands SDK / Bedrock Claude 3 Sonnet) + → Cognito JWT auth + → AgentCore Gateway (CUSTOM_JWT, Smithy model target) + → Bedrock Runtime (via GATEWAY_IAM_ROLE auth) +``` + +## Target Type: Smithy Model (NOT API Gateway, NOT OpenAPI) + +- GatewayTarget TargetConfiguration must be nested: `Mcp.SmithyModel` (not top-level) +- Use official AWS Smithy model from `github.com/aws/api-models-aws` — upload to S3 and reference via `SmithyModel.S3.Uri` +- Custom Smithy models do NOT work for AWS services (missing endpoint resolution metadata) +- Credential type is `GATEWAY_IAM_ROLE` (NOT `API_KEY`) — Bedrock uses IAM auth +- No credential provider CLI dance needed — IAM role handles auth +- No API Gateway, no API keys, no secrets for target auth + +## Smithy Model Rules + +1. Must use `aws.protocols#restJson1` trait on the service shape +2. Every operation needs `smithy.api#http` trait with method and URI +3. Input members use `smithy.api#httpQuery` for query params, `smithy.api#httpLabel` for path params +4. Use `smithy.api#required` trait for mandatory fields +5. Only RestJson protocol is supported +6. Max model size: 10MB +7. No streaming operations +8. Avoid dynamic endpoint patterns (security risk) +9. Must include `aws.auth#sigv4` trait on the service with the target service name (e.g. `"name": "bedrock"`) + +## KNOWN LIMITATION: Smithy Model Target + DynamoDB Protocol Mismatch + +The AgentCore Gateway's Smithy model target only supports `aws.protocols#restJson1` (path-based REST routing). DynamoDB uses `aws.protocols#awsJson1_0` (single POST endpoint with `X-Amz-Target` header). This protocol mismatch means the gateway cannot translate Smithy model tool calls into valid DynamoDB API requests — the last hop (gateway → DynamoDB) returns "internal error" even though the entire agent pipeline up to that point works correctly. + +This affects any AWS service that uses `awsJson1_0` or `awsJson1_1` protocols (DynamoDB, SQS, SNS, etc.). The Smithy model target type works for services with REST-style APIs (`restJson1`, `restXml`). + +Workaround options: +- Use the API Gateway target type with a REST API proxy in front of DynamoDB +- Use a Lambda target that calls DynamoDB directly +- Wait for AgentCore to add `awsJson1_0` protocol support for Smithy model targets + +## SOLUTION: Targeting Bedrock Runtime Instead of DynamoDB + +Since DynamoDB uses `awsJson1_0`, we pivoted to targeting Bedrock Runtime (which uses `restJson1`). The agent now calls another model (Claude Haiku) as a tool via the gateway. Here's what we learned: + +### 1. Custom Smithy Models Cannot Resolve AWS Service Endpoints + +A hand-written Smithy model with just `aws.auth#sigv4` and `aws.protocols#restJson1` is NOT enough for AWS services. The gateway derives the endpoint entirely from the model's built-in metadata: +- `aws.api#service` trait (with `sdkId`, `arnNamespace`, `endpointPrefix`) +- Full `smithy.rules#endpointRuleSet` with partition data, FIPS/DualStack logic + +A custom `endpointRuleSet` pointing to `https://bedrock-runtime.us-east-1.amazonaws.com` does NOT work — the gateway needs the complete partition data. + +**Fix:** Use the official AWS Smithy model from `github.com/aws/api-models-aws`. Upload to S3 and reference via `SmithyModel.S3.Uri` (not `S3Configuration`): +```yaml +SmithyModel: + S3: + Uri: !Sub 's3://${AWS::StackName}-smithy-models/bedrock-runtime-2023-09-30.json' +``` + +### 2. InvokeModel's Blob Body Doesn't Map to MCP Tool Inputs + +The official Bedrock Runtime model's `InvokeModel` operation uses `Blob` with `httpPayload` trait for the request body. When the gateway converts this to an MCP tool, the blob field disappears — there's no input parameter for the LLM to pass the actual prompt body. The LLM tries to pass it as `content` but that's not a recognized field. + +**Fix:** Use `Converse` instead of `InvokeModel`. Converse has structured `messages` and `modelId` fields that map cleanly to MCP tool input parameters. + +### 3. Gateway Execution Role Needs Broad Bedrock Permissions + +`bedrock:InvokeModel` on `arn:aws:bedrock:{region}::foundation-model/*` is not enough. Cross-region inference profiles (like `us.anthropic.claude-3-haiku-20240307-v1:0`) use different ARN patterns. + +**Fix:** Broad permissions on `*`: +```yaml +- PolicyName: BedrockRuntimeAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + - bedrock:Converse + - bedrock:ConverseStream + Resource: '*' +``` + +### 4. System Prompt Required for Correct Tool Usage + +The official Smithy model exposes 10+ tools (InvokeModel, Converse, ConverseStream, CountTokens, etc.) with complex schemas. Without guidance, the LLM guesses wrong model IDs and message formats. + +**Fix:** Add a system prompt to the Strands Agent telling it exactly which tool, model ID, and message format to use: +```python +SYSTEM_PROMPT = """Use the Converse tool (not InvokeModel). +- Set modelId to: us.anthropic.claude-3-haiku-20240307-v1:0 +- Set messages to: [{"role": "user", "content": [{"text": "YOUR PROMPT"}]}] +- Only try ONCE""" +agent = Agent(model=bedrock_model, tools=[mcp_client], system_prompt=SYSTEM_PROMPT) +``` + +## Bedrock Runtime Operations Exposed + +The official Smithy model exposes all Bedrock Runtime operations as MCP tools. The key ones: +- `Converse` — structured conversation API (USE THIS — maps cleanly to MCP tool inputs) +- `InvokeModel` — raw model invocation (DO NOT USE — Blob httpPayload doesn't map to MCP tool inputs) +- `ConverseStream`, `InvokeModelWithResponseStream` — streaming variants (not supported by Smithy targets) +- `CountTokens`, `ListAsyncInvokes`, etc. — utility operations + +The agent's system prompt directs it to use `Converse` with the correct model ID and message format. + +## AgentCore Gateway Resource (AWS::BedrockAgentCore::Gateway) + +Required properties (case-sensitive): +- `ProtocolType: MCP` — REQUIRED, not optional +- `RoleArn` — NOT `ExecutionRoleArn` (extraneous key error) +- `AuthorizerType: CUSTOM_JWT` +- `AuthorizerConfiguration.CustomJWTAuthorizer` — NOT `JwtConfiguration` (extraneous key error) + - Note: `CustomJWTAuthorizer` (all caps JWT), NOT `CustomJwtAuthorizer` + - `DiscoveryUrl` must end with `/.well-known/openid-configuration` + - `AllowedAudience` replaces the old `Audience` array + +## Gateway Execution Role — All 4 ARN Patterns + Bedrock Runtime + +Missing any of the 4 core ARN patterns causes "Internal Error" on `tools/call` while `tools/list` works fine: + +```yaml +Resource: + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:token-vault/default' + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:token-vault/default/apikeycredentialprovider/*' + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default' + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default/workload-identity/${GatewayName}-*' +``` + +Additionally, add Bedrock Runtime permissions (broad — cross-region inference profiles need `*`): +```yaml +- PolicyName: BedrockRuntimeAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + - bedrock:Converse + - bedrock:ConverseStream + Resource: '*' +``` + +Do NOT include `apigateway:GET` — no API Gateway in this project. + +## Credential Provider Configuration (Smithy/Bedrock Runtime) + +Use `GATEWAY_IAM_ROLE` — no API key credential provider needed: +```yaml +CredentialProviderConfigurations: + - CredentialProviderType: GATEWAY_IAM_ROLE +``` + +This means: +- No credential provider CLI commands +- No Secrets Manager secrets for target auth +- No stack re-update with credential provider ARN +- The Gateway execution role's Bedrock permissions handle everything + +## Agent Lambda — Strands SDK Rules + +- Runtime: `python3.12`, Architecture: `x86_64` +- Timeout: minimum 120 seconds, Memory: minimum 1024 MB +- IAM must include ALL four Bedrock actions: `InvokeModel`, `InvokeModelWithResponseStream`, `Converse`, `ConverseStream` +- Do NOT use `with mcp_client:` context manager — let Agent manage the session, clean up in `finally` block +- Do NOT remove `.dist-info` directories during packaging (opentelemetry needs them) +- If zip > 50MB, upload to S3 first then update Lambda from S3 + +## Lambda Dependencies (requirements.txt) + +All four are required: +``` +strands-agents>=1.0.0 +mcp>=1.0.0 +requests>=2.31.0 +PyJWT[crypto]>=2.8.0 +``` + +## Lambda Packaging — Two-Step pip Install (CRITICAL) + +`--only-binary=:all:` with `--platform` silently skips pure Python packages. You MUST use a two-step install: + +```bash +# Step 1: Binary packages +pip3 install --target $DIR --platform manylinux2014_x86_64 --python-version 3.12 --only-binary=:all: -r requirements.txt + +# Step 2: Pure Python packages (skipped by step 1) +pip3 install --target $DIR --platform manylinux2014_x86_64 --python-version 3.12 --only-binary=:all: --no-deps \ + requests urllib3 charset-normalizer idna certifi PyJWT cryptography cffi +``` + +## JWT Validation — ID Tokens, Not Access Tokens + +Cognito `USER_PASSWORD_AUTH` flow returns an ID token. The JWT validation code MUST: +- Accept both `token_use: access` and `token_use: id` +- Disable audience verification (`verify_aud: False`) — Gateway handles it via `AllowedAudience` +- Handle ID token claim names: `cognito:username` (not `username`), `aud` (not `client_id`) + +## Deployment Order + +1. Validate CloudFormation template +2. Upload official Smithy model to S3 +3. Deploy CloudFormation stack (AgentCore Gateway, Cognito, Lambda, IAM) +4. Package Lambda code (two-step pip install) +5. Deploy Lambda code (50MB S3 fallback) +6. Create test user in Cognito +7. Generate test script (`scripts/test.sh`) + +No secrets, no credential provider CLI, no stack re-update. Much simpler than the API Gateway project. + +## Deploy Script Pattern + +- Stack create vs update: check `DOES_NOT_EXIST` pattern +- Use `pip3` not `pip` +- Generate `scripts/test.sh` with baked-in values — do NOT echo nested JSON commands +- Use `stat -f%z` (macOS) with `stat -c%s` fallback (Linux) for file size check + +## Test Script + +Generated by deploy.sh with baked-in values. Usage: +```bash +./scripts/test.sh # default prompt +./scripts/test.sh 'Use the invoke model tool to ask Haiku to write a haiku about clouds' # invoke Haiku +``` + +Do NOT echo copy-paste test commands with nested JSON — shell escaping breaks `${ID_TOKEN}` substitution. + +## Existing Code (Reuse As-Is from apigw project) + +``` +src/agent/handler.py — Lambda entry point +src/agent/agent_processor.py — MCP client + Strands Agent lifecycle +src/agent/strands_client.py — Factory functions +src/shared/models.py — UserContext, AgentRequest, AgentResponse +src/shared/logging_utils.py — Structured logging +src/shared/error_utils.py — Error handling +src/shared/jwt_utils.py — JWT validation +``` + +These are target-type agnostic — they connect to AgentCore Gateway via MCP and don't care what's behind it. + +## Region + +All resources MUST be deployed in `us-east-1` (BedrockAgentCore availability). + +## Resources NOT Needed (from apigw project) + +Do NOT create any of these: +- `RestApi`, API Gateway resources, methods, deployments, stages +- `ApiKey`, `UsagePlan`, `UsagePlanKey` +- Secrets Manager secrets for API keys +- Credential provider CLI commands +- `apigateway:GET` IAM policy diff --git a/strands-agentcore-smithy/Makefile b/strands-agentcore-smithy/Makefile deleted file mode 100644 index 0a8a6589d6..0000000000 --- a/strands-agentcore-smithy/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -# ============================================================ -# AgentCore Smithy Bedrock - SAM Makefile build target -# ============================================================ -# Used by `sam build` via the AgentLambdaFunction resource's -# `Metadata: BuildMethod: makefile`. This replaces the previous -# manual packaging in deploy.sh and requires NO Docker. -# -# SAM invokes `make build-AgentLambdaFunction` with ARTIFACTS_DIR -# set to the directory SAM expects the built artifacts in. -# -# Two-step install reproduces the original packaging behaviour: -# 1. Install all dependencies as manylinux2014 x86_64 wheels -# 2. Re-install pure-Python packages with --no-deps so nothing -# is skipped by the binary-only constraint -# ============================================================ - -build-AgentLambdaFunction: - pip3 install \ - --target "$(ARTIFACTS_DIR)" \ - --platform manylinux2014_x86_64 \ - --python-version 3.12 \ - --only-binary=:all: \ - -r requirements.txt - pip3 install \ - --target "$(ARTIFACTS_DIR)" \ - --platform manylinux2014_x86_64 \ - --python-version 3.12 \ - --only-binary=:all: \ - --no-deps \ - requests urllib3 charset-normalizer idna certifi PyJWT cryptography cffi - cp -r src "$(ARTIFACTS_DIR)/" diff --git a/strands-agentcore-smithy/README.md b/strands-agentcore-smithy/README.md index d6e8b71889..a55ed4980d 100644 --- a/strands-agentcore-smithy/README.md +++ b/strands-agentcore-smithy/README.md @@ -1,6 +1,8 @@ -# Strands Agent on Lambda with AgentCore Smithy Bedrock Target +# AgentCore Smithy Amazon Bedrock Runtime -A serverless AI agent that uses AWS Bedrock AgentCore Gateway with a Smithy model target to call Bedrock Runtime. The agent (Claude Sonnet 4.6) calls another model (Claude Haiku 4.5) as a tool via the gateway's Converse API. +A serverless AI agent that uses Amazon Bedrock AgentCore Gateway with a Smithy model target to call Amazon Bedrock Runtime. The agent (Claude Sonnet 4.6) calls another model (Claude Haiku 4.5) as a tool via the gateway's Converse API. + +> **Important:** This application uses various AWS services and there are costs associated with these services after the Free Tier usage — please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. ## Architecture @@ -28,30 +30,29 @@ User → test.sh → Agent Lambda ──────────────→ 6. The gateway assumes its IAM execution role, signs the request, and calls Bedrock Runtime 7. Haiku's response flows back through the gateway → MCP → agent → user -## Deploy - -### Step 1: Prerequisites +## Prerequisites - AWS CLI v2 -- AWS SAM CLI - Python 3.12+ - pip3 -- `make` (used by the SAM Makefile build — no Docker required) -- AWS account with Bedrock and AgentCore enabled in `us-east-1` -- Bedrock model access enabled for Claude Sonnet 4.6 and Claude Haiku 4.5 +- AWS account with Amazon Bedrock and Amazon Bedrock AgentCore enabled in `us-east-1` +- Amazon Bedrock model access enabled for Claude Sonnet 4.6 and Claude Haiku 4.5 +- AWS credentials with permissions to create and manage: CloudFormation stacks, IAM roles, Lambda functions, S3 buckets, Cognito user pools, and Amazon Bedrock AgentCore Gateways -### Step 2: Open a Terminal +## Deploy + +### Step 1: Open a Terminal Open a terminal on your machine and navigate to where you want to clone the project. -### Step 3: Clone the Repository +### Step 2: Clone the Repository ```bash git clone https://github.com/aws-samples/serverless-patterns cd serverless-patterns/strands-agentcore-smithy ``` -### Step 4: Deploy +### Step 3: Deploy One command deploys everything: @@ -60,10 +61,10 @@ bash scripts/deploy.sh ``` This will: -1. Validate the SAM template (`sam validate`) +1. Validate the CloudFormation template 2. Download the official Bedrock Runtime Smithy model and upload to S3 -3. Build the Lambda with `sam build` (Makefile build — two-step pip3 install, no Docker) -4. Deploy the stack with `sam deploy` (Gateway, Cognito, Lambda, IAM roles) +3. Create/update the CloudFormation stack (Gateway, Cognito, Lambda, IAM roles) +4. Package and deploy the Lambda code (two-step pip3 install) 5. Create a Cognito test user 6. Generate `scripts/test.sh` with baked-in config values @@ -85,11 +86,10 @@ Or use the default prompt: ``` ├── infrastructure/ -│ ├── template.yaml # SAM template (all AWS resources) +│ ├── cloudformation-template.yaml # All AWS resources │ └── bedrock-runtime-2023-09-30.json # Official Smithy model (for tests) -├── Makefile # SAM Makefile build (no Docker) ├── scripts/ -│ ├── deploy.sh # Full deployment script (SAM) +│ ├── deploy.sh # Full deployment script │ └── test.sh # Generated by deploy.sh ├── src/ │ ├── agent/ @@ -165,6 +165,12 @@ Since the target is an AWS service (Bedrock Runtime), the gateway uses its IAM e ### Agent code is target-agnostic All code in `src/agent/` and `src/shared/` connects to the gateway via MCP and doesn't reference Bedrock, DynamoDB, or any specific target. The same code works with any gateway target type. +## Run Tests + +```bash +python3 -m pytest tests/ -v +``` + ## Teardown ```bash @@ -172,10 +178,10 @@ aws cloudformation delete-stack --stack-name agentcore-smithy-bedrock --region u aws s3 rb s3://agentcore-smithy-bedrock-smithy-models --force --region us-east-1 ``` -## Run Tests +The Lambda function's CloudWatch Logs log group is not deleted automatically. Remove it to avoid ongoing storage charges: ```bash -python -m pytest tests/ -v +aws logs delete-log-group --log-group-name /aws/lambda/agentcore-smithy-bedrock-AgentFunction --region us-east-1 ``` --- diff --git a/strands-agentcore-smithy/example-pattern.json b/strands-agentcore-smithy/example-pattern.json index 714e25a40c..620fb7ff47 100644 --- a/strands-agentcore-smithy/example-pattern.json +++ b/strands-agentcore-smithy/example-pattern.json @@ -1,18 +1,18 @@ { - "title": "Strands Agent on Lambda with AgentCore Smithy Bedrock Target", - "description": "Strands SDK agent on Lambda calls Bedrock Runtime via AgentCore Gateway using a Smithy model target, with Cognito JWT auth and IAM role signing.", + "title": "Strands Agent on AWS Lambda with Amazon Bedrock AgentCore Smithy Amazon Bedrock Target", + "description": "Strands SDK agent on AWS Lambda calls Amazon Bedrock Runtime via Amazon Bedrock AgentCore Gateway using a Smithy model target, with Cognito JWT auth and IAM role signing.", "language": "Python", "level": "300", - "framework": "SAM", + "framework": "CloudFormation", "introBox": { "headline": "How it works", "text": [ "The user authenticates with Amazon Cognito and receives a JWT token.", - "The JWT is passed to an Agent Lambda which uses the Strands Agents SDK to create an AI agent backed by Amazon Bedrock (Claude Sonnet 4.6 cross-region inference profile).", - "The Strands Agent connects to an AgentCore Gateway MCP endpoint, dynamically discovering available Bedrock Runtime operations via the MCP tools/list protocol.", - "The AgentCore Gateway validates the JWT token using a CUSTOM_JWT authorizer backed by Cognito, then routes MCP tool calls to the Smithy model target.", - "The gateway target uses the official Bedrock Runtime Smithy model (loaded from S3) to expose Bedrock Runtime operations as MCP tools.", - "The gateway uses its IAM execution role (GATEWAY_IAM_ROLE) to sign requests to Bedrock Runtime — no API keys or Secrets Manager secrets are required.", + "The JWT is passed to an AWS Lambda function which uses the Strands Agents SDK to create an AI agent backed by Amazon Bedrock (Claude Sonnet 4.6 cross-region inference profile).", + "The Strands Agent connects to an Amazon Bedrock AgentCore Gateway MCP endpoint, dynamically discovering available Amazon Bedrock Runtime operations via the MCP tools/list protocol.", + "The Amazon Bedrock AgentCore Gateway validates the JWT token using a CUSTOM_JWT authorizer backed by Amazon Cognito, then routes MCP tool calls to the Smithy model target.", + "The gateway target uses the official Amazon Bedrock Runtime Smithy model (loaded from S3) to expose Amazon Bedrock Runtime operations as MCP tools.", + "The gateway uses its IAM execution role (GATEWAY_IAM_ROLE) to sign requests to Amazon Bedrock Runtime — no API keys or Secrets Manager secrets are required.", "The outer agent (Claude Sonnet 4.6) calls the Converse tool, which invokes Claude Haiku 4.5 as the inner model to generate the response.", "The Strands SDK handles the full agentic loop: tool discovery, Claude tool selection, MCP tool execution, and response formatting — all in a single agent() call." ] @@ -22,7 +22,7 @@ "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/strands-agentcore-smithy", "templateURL": "serverless-patterns/strands-agentcore-smithy", "projectFolder": "strands-agentcore-smithy", - "templateFile": "infrastructure/template.yaml" + "templateFile": "infrastructure/cloudformation-template.yaml" } }, "resources": { diff --git a/strands-agentcore-smithy/handoff/steering/agentcore-smithy-dynamodb.md b/strands-agentcore-smithy/handoff/steering/agentcore-smithy-dynamodb.md new file mode 100644 index 0000000000..7789e540d4 --- /dev/null +++ b/strands-agentcore-smithy/handoff/steering/agentcore-smithy-dynamodb.md @@ -0,0 +1,196 @@ +--- +inclusion: auto +--- + +# AgentCore Smithy DynamoDB Agent — Implementation Guide + +This project builds a serverless AI agent using AWS Bedrock AgentCore Gateway with a Smithy model target type connecting to DynamoDB. Follow these patterns strictly during implementation. + +## Architecture + +``` +User → Agent Lambda (Strands SDK / Bedrock Claude 3 Sonnet) + → Cognito JWT auth + → AgentCore Gateway (CUSTOM_JWT, Smithy model target) + → DynamoDB (via GATEWAY_IAM_ROLE auth) +``` + +## Target Type: Smithy Model (NOT API Gateway, NOT OpenAPI) + +- GatewayTarget TargetConfiguration must be nested: `Mcp.SmithyModel` (not top-level) +- Use `InlinePayload` for the Smithy model JSON (or S3 for models >10MB) +- Smithy model MUST use `aws.protocols#restJson1` trait on the service +- Operations need `smithy.api#http` trait with method and URI +- Credential type is `GATEWAY_IAM_ROLE` (NOT `API_KEY`) — DynamoDB uses IAM auth +- No credential provider CLI dance needed — IAM role handles auth +- No API Gateway, no API keys, no secrets for target auth + +## Smithy Model Rules + +1. Must use `aws.protocols#restJson1` trait on the service shape +2. Every operation needs `smithy.api#http` trait with method and URI +3. Input members use `smithy.api#httpQuery` for query params, `smithy.api#httpLabel` for path params +4. Use `smithy.api#required` trait for mandatory fields +5. Only RestJson protocol is supported +6. Max model size: 10MB +7. No streaming operations +8. Avoid dynamic endpoint patterns (security risk) + +## DynamoDB Operations to Expose + +Expose these DynamoDB operations via the Smithy model: +- `GetItem` — retrieve a single item by key +- `PutItem` — create or update an item +- `Query` — query items by partition key with optional sort key conditions + +The LLM decides which operation to call based on the user's natural language prompt. + +## AgentCore Gateway Resource (AWS::BedrockAgentCore::Gateway) + +Required properties (case-sensitive): +- `ProtocolType: MCP` — REQUIRED, not optional +- `RoleArn` — NOT `ExecutionRoleArn` (extraneous key error) +- `AuthorizerType: CUSTOM_JWT` +- `AuthorizerConfiguration.CustomJWTAuthorizer` — NOT `JwtConfiguration` (extraneous key error) + - Note: `CustomJWTAuthorizer` (all caps JWT), NOT `CustomJwtAuthorizer` + - `DiscoveryUrl` must end with `/.well-known/openid-configuration` + - `AllowedAudience` replaces the old `Audience` array + +## Gateway Execution Role — All 4 ARN Patterns + DynamoDB + +Missing any of the 4 core ARN patterns causes "Internal Error" on `tools/call` while `tools/list` works fine: + +```yaml +Resource: + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:token-vault/default' + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:token-vault/default/apikeycredentialprovider/*' + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default' + - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default/workload-identity/${GatewayName}-*' +``` + +Additionally, add DynamoDB permissions for the target service: +```yaml +- PolicyName: DynamoDBAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:Query + Resource: !GetAtt ProductTable.Arn +``` + +Do NOT include `apigateway:GET` — no API Gateway in this project. + +## Credential Provider Configuration (Smithy/DynamoDB) + +Use `GATEWAY_IAM_ROLE` — no API key credential provider needed: +```yaml +CredentialProviderConfigurations: + - CredentialProviderType: GATEWAY_IAM_ROLE +``` + +This means: +- No credential provider CLI commands +- No Secrets Manager secrets for target auth +- No stack re-update with credential provider ARN +- The Gateway execution role's DynamoDB permissions handle everything + +## Agent Lambda — Strands SDK Rules + +- Runtime: `python3.12`, Architecture: `x86_64` +- Timeout: minimum 120 seconds, Memory: minimum 1024 MB +- IAM must include ALL four Bedrock actions: `InvokeModel`, `InvokeModelWithResponseStream`, `Converse`, `ConverseStream` +- Do NOT use `with mcp_client:` context manager — let Agent manage the session, clean up in `finally` block +- Do NOT remove `.dist-info` directories during packaging (opentelemetry needs them) +- If zip > 50MB, upload to S3 first then update Lambda from S3 + +## Lambda Dependencies (requirements.txt) + +All four are required: +``` +strands-agents>=1.0.0 +mcp>=1.0.0 +requests>=2.31.0 +PyJWT[crypto]>=2.8.0 +``` + +## Lambda Packaging — Two-Step pip Install (CRITICAL) + +`--only-binary=:all:` with `--platform` silently skips pure Python packages. You MUST use a two-step install: + +```bash +# Step 1: Binary packages +pip3 install --target $DIR --platform manylinux2014_x86_64 --python-version 3.12 --only-binary=:all: -r requirements.txt + +# Step 2: Pure Python packages (skipped by step 1) +pip3 install --target $DIR --platform manylinux2014_x86_64 --python-version 3.12 --only-binary=:all: --no-deps \ + requests urllib3 charset-normalizer idna certifi PyJWT cryptography cffi +``` + +## JWT Validation — ID Tokens, Not Access Tokens + +Cognito `USER_PASSWORD_AUTH` flow returns an ID token. The JWT validation code MUST: +- Accept both `token_use: access` and `token_use: id` +- Disable audience verification (`verify_aud: False`) — Gateway handles it via `AllowedAudience` +- Handle ID token claim names: `cognito:username` (not `username`), `aud` (not `client_id`) + +## Deployment Order (Simplified — No Credential Provider Dance) + +1. Validate CloudFormation template +2. Deploy CloudFormation stack (DynamoDB table, AgentCore Gateway, Cognito, Lambda, IAM) +3. Package Lambda code (two-step pip install) +4. Deploy Lambda code (50MB S3 fallback) +5. Seed DynamoDB table with sample data +6. Create test user in Cognito +7. Generate test script (`scripts/test.sh`) + +No secrets, no credential provider CLI, no stack re-update. Much simpler than the API Gateway project. + +## Deploy Script Pattern + +- Stack create vs update: check `DOES_NOT_EXIST` pattern +- Use `pip3` not `pip` +- Generate `scripts/test.sh` with baked-in values — do NOT echo nested JSON commands +- Use `stat -f%z` (macOS) with `stat -c%s` fallback (Linux) for file size check + +## Test Script + +Generated by deploy.sh with baked-in values. Usage: +```bash +./scripts/test.sh # default prompt +./scripts/test.sh 'Show me all products under $50' # query +./scripts/test.sh 'Get the details for product ABC123' # get item +./scripts/test.sh 'Add a new product called Widget for $29.99' # put item +``` + +Do NOT echo copy-paste test commands with nested JSON — shell escaping breaks `${ID_TOKEN}` substitution. + +## Existing Code (Reuse As-Is from apigw project) + +``` +src/agent/handler.py — Lambda entry point +src/agent/agent_processor.py — MCP client + Strands Agent lifecycle +src/agent/strands_client.py — Factory functions +src/shared/models.py — UserContext, AgentRequest, AgentResponse +src/shared/logging_utils.py — Structured logging +src/shared/error_utils.py — Error handling +src/shared/jwt_utils.py — JWT validation +``` + +These are target-type agnostic — they connect to AgentCore Gateway via MCP and don't care what's behind it. + +## Region + +All resources MUST be deployed in `us-east-1` (BedrockAgentCore availability). + +## Resources NOT Needed (from apigw project) + +Do NOT create any of these: +- `RestApi`, API Gateway resources, methods, deployments, stages +- `ApiKey`, `UsagePlan`, `UsagePlanKey` +- Secrets Manager secrets for API keys +- Credential provider CLI commands +- `apigateway:GET` IAM policy diff --git a/strands-agentcore-smithy/handoff/strands-agentcore-smithy-handover.md b/strands-agentcore-smithy/handoff/strands-agentcore-smithy-handover.md new file mode 100644 index 0000000000..5583f8e7ee --- /dev/null +++ b/strands-agentcore-smithy/handoff/strands-agentcore-smithy-handover.md @@ -0,0 +1,130 @@ +# Handover: strands-agentcore-smithy + +Build a serverless AI agent using AWS Bedrock AgentCore Gateway with a Smithy model target type. The agent (Claude 3 Sonnet) calls another model (Claude 3 Haiku) as a tool via the Bedrock Runtime Converse API through the gateway. + +## Architecture + +``` +User → Agent Lambda (Strands SDK + Bedrock Claude 3 Sonnet) + → Cognito JWT authentication (ID tokens) + → AgentCore Gateway (MCP protocol, CUSTOM_JWT authorizer) + → Smithy Model Target (official Bedrock Runtime model from S3) + → Bedrock Runtime Converse API → Claude 3 Haiku +``` + +## What is a Smithy Target? + +Smithy targets connect AgentCore Gateway to services defined using Smithy API models. AgentCore translates incoming MCP requests into API calls to those services and handles response formatting. + +Key constraints: +- For AWS services, you MUST use the official Smithy model from the AWS API Models repo +- Custom Smithy models cannot resolve AWS service endpoints (missing `aws.api#service` trait and full endpoint rules) +- Only RestJson protocol is supported (services using `awsJson1_0` like DynamoDB won't work) +- Max model size: 10MB +- No streaming operations + +## Deployment + +One command: + +```bash +bash scripts/deploy.sh +``` + +The script: +1. Validates the CloudFormation template +2. Downloads the official Bedrock Runtime Smithy model from GitHub and uploads to S3 +3. Creates/updates the CloudFormation stack +4. Packages Lambda code (two-step pip3 install for binary + pure Python packages) +5. Deploys Lambda code (S3 fallback if >50MB) +6. Creates a Cognito test user +7. Generates `scripts/test.sh` with baked-in values + +## Testing + +```bash +./scripts/test.sh "Ask Haiku to write a haiku about clouds" +./scripts/test.sh # uses default prompt +``` + +## Project Structure + +``` +├── infrastructure/ +│ ├── cloudformation-template.yaml # All AWS resources +│ └── bedrock-runtime-2023-09-30.json # Official Smithy model (local copy for tests) +├── scripts/ +│ ├── deploy.sh # Full deployment lifecycle +│ └── test.sh # Generated by deploy.sh +├── src/ +│ ├── agent/ +│ │ ├── handler.py # Lambda entry point +│ │ ├── agent_processor.py # Strands Agent + MCP lifecycle + system prompt +│ │ └── strands_client.py # MCP client + Bedrock model factories +│ └── shared/ +│ ├── models.py # UserContext, AgentRequest, AgentResponse +│ ├── jwt_utils.py # JWT validation +│ ├── error_utils.py # Error response formatting +│ └── logging_utils.py # Structured logging +├── tests/ +│ ├── unit/ # Unit tests +│ └── property/ # Property-based tests (Hypothesis) +└── requirements.txt +``` + +## CloudFormation Resources + +| Resource | Type | Purpose | +|----------|------|---------| +| `AgentCoreGateway` | `AWS::BedrockAgentCore::Gateway` | MCP gateway with CUSTOM_JWT auth | +| `SmithyTarget` | `AWS::BedrockAgentCore::GatewayTarget` | Smithy model target pointing to Bedrock Runtime via S3 | +| `CognitoUserPool` | `AWS::Cognito::UserPool` | User authentication | +| `CognitoUserPoolClient` | `AWS::Cognito::UserPoolClient` | Client for USER_PASSWORD_AUTH flow | +| `AgentLambdaFunction` | `AWS::Lambda::Function` | Strands SDK agent (Python 3.12, x86_64) | +| `AgentLambdaRole` | `AWS::IAM::Role` | Lambda execution role (Bedrock + AgentCore + Logs) | +| `GatewayExecutionRole` | `AWS::IAM::Role` | Gateway role (4 ARN patterns + Bedrock Runtime + S3) | + +## Key Lessons Learned + +### 1. Must Use Official AWS Smithy Models for AWS Services +A custom Smithy model with just `aws.auth#sigv4` and `aws.protocols#restJson1` fails silently — the gateway needs the full `aws.api#service` trait, endpoint rule sets with partition data, and FIPS/DualStack logic. Only the official models from `github.com/aws/api-models-aws` have this. + +### 2. Use Converse, Not InvokeModel +`InvokeModel` uses a `Blob` with `httpPayload` trait for the request body. When the gateway converts this to an MCP tool, the blob field disappears — there's no input parameter for the body. `Converse` has structured `messages` and `modelId` fields that map cleanly to MCP tool inputs. + +### 3. awsJson1_0 Services Don't Work +DynamoDB, SQS, SNS, and other services using `awsJson1_0` protocol are incompatible with Smithy model targets (which only support `restJson1`). Bedrock Runtime uses `restJson1` so it works. + +### 4. System Prompt Guides Tool Usage +The official Smithy model exposes ~10 tools with complex schemas. Without a system prompt, the LLM guesses wrong model IDs and message formats. The system prompt in `agent_processor.py` tells the agent to use Converse with the correct model ID and message structure. + +### 5. Broad Bedrock Permissions Required +`bedrock:InvokeModel` on `foundation-model/*` isn't enough — cross-region inference profiles use different ARN patterns. Use `Resource: '*'` with all four Bedrock actions. + +### 6. CloudFormation Property Casing Matters +- `CustomJWTAuthorizer` (all caps JWT) inside `AuthorizerConfiguration` +- `RoleArn` not `ExecutionRoleArn` +- `AllowedAudience` not `Audience` +- `CredentialProviderConfigurations` (array) not `CredentialProviderType` (top-level) +- `SmithyModel.S3.Uri` not `SmithyModel.S3Configuration.Uri` + +### 7. Lambda Packaging Two-Step pip Install +`--only-binary=:all:` with `--platform` silently skips pure Python packages. Must run a second `pip3 install --no-deps` for requests, urllib3, PyJWT, etc. + +### 8. Agent Code is Target-Agnostic +All code in `src/agent/` and `src/shared/` connects via MCP and doesn't reference any specific target. Same code works with API Gateway, Smithy, or Lambda targets. + +## Prerequisites + +- AWS CLI v2 +- Python 3.12+ +- pip3 +- AWS account with Bedrock and AgentCore enabled in `us-east-1` +- Bedrock model access for Claude 3 Sonnet and Claude 3 Haiku + +## Teardown + +```bash +aws cloudformation delete-stack --stack-name agentcore-smithy-bedrock --region us-east-1 +aws s3 rb s3://agentcore-smithy-bedrock-smithy-models --force --region us-east-1 +``` diff --git a/strands-agentcore-smithy/infrastructure/template.yaml b/strands-agentcore-smithy/infrastructure/cloudformation-template.yaml similarity index 80% rename from strands-agentcore-smithy/infrastructure/template.yaml rename to strands-agentcore-smithy/infrastructure/cloudformation-template.yaml index 8192d20cf0..89a98e80b3 100644 --- a/strands-agentcore-smithy/infrastructure/template.yaml +++ b/strands-agentcore-smithy/infrastructure/cloudformation-template.yaml @@ -1,9 +1,7 @@ AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 Description: > - AgentCore Smithy Bedrock Runtime - Serverless AI agent exposing Bedrock Runtime - operations through AWS Bedrock AgentCore Gateway using a Smithy model target. - Built and deployed with AWS SAM (Makefile build, no Docker required). + AgentCore Smithy Amazon Bedrock Runtime - Serverless AI agent exposing Amazon Bedrock Runtime + operations through Amazon Bedrock AgentCore Gateway using a Smithy model target. Parameters: GatewayName: @@ -12,6 +10,7 @@ Parameters: Description: Name for the AgentCore Gateway Resources: + # ============================================================ # ============================================================ # Cognito User Pool and Client # ============================================================ @@ -49,17 +48,30 @@ Resources: Principal: Service: bedrock-agentcore.amazonaws.com Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: !Ref AWS::AccountId + ArnLike: + aws:SourceArn: !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:*' Policies: - PolicyName: AgentCoreAccess PolicyDocument: Version: '2012-10-17' Statement: - - Effect: Allow + - Sid: TokenVaultApiKeyAccess + Effect: Allow Action: - - bedrock-agentcore:* + - bedrock-agentcore:GetResourceApiKey Resource: - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:token-vault/default' - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:token-vault/default/apikeycredentialprovider/*' + - Sid: WorkloadIdentityTokenAccess + Effect: Allow + Action: + - bedrock-agentcore:GetWorkloadAccessToken + - bedrock-agentcore:GetWorkloadAccessTokenForJWT + - bedrock-agentcore:GetWorkloadAccessTokenForUserId + Resource: - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default' - !Sub 'arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default/workload-identity/${GatewayName}-*' - PolicyName: BedrockRuntimeAccess @@ -73,9 +85,8 @@ Resources: - bedrock:Converse - bedrock:ConverseStream Resource: - - !Sub 'arn:aws:bedrock:${AWS::Region}::foundation-model/*' + - 'arn:aws:bedrock:*::foundation-model/*' - !Sub 'arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/*' - - '*' - PolicyName: SmithyModelS3Access PolicyDocument: Version: '2012-10-17' @@ -144,7 +155,9 @@ Resources: - bedrock:InvokeModelWithResponseStream - bedrock:Converse - bedrock:ConverseStream - Resource: '*' + Resource: + - !Sub 'arn:aws:bedrock:*::foundation-model/*' + - !Sub 'arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/*' - PolicyName: AgentCoreAccess PolicyDocument: Version: '2012-10-17' @@ -161,24 +174,26 @@ Resources: - Effect: Allow Action: - logs:CreateLogGroup + Resource: + - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-AgentFunction' + - Effect: Allow + Action: - logs:CreateLogStream - logs:PutLogEvents - Resource: '*' + Resource: + - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-AgentFunction:*' # ============================================================ - # Agent Lambda Function (SAM Serverless Function, Makefile build) + # Agent Lambda Function # ============================================================ AgentLambdaFunction: - Type: AWS::Serverless::Function - Metadata: - BuildMethod: makefile + Type: AWS::Lambda::Function Properties: FunctionName: !Sub '${AWS::StackName}-AgentFunction' - CodeUri: ../ - Handler: src.agent.handler.lambda_handler Runtime: python3.12 Architectures: - x86_64 + Handler: src.agent.handler.lambda_handler Role: !GetAtt AgentLambdaRole.Arn Timeout: 120 MemorySize: 1024 @@ -189,6 +204,10 @@ Resources: COGNITO_USER_POOL_ID: !Ref CognitoUserPool COGNITO_CLIENT_ID: !Ref CognitoUserPoolClient AWS_REGION_NAME: !Ref AWS::Region + Code: + ZipFile: | + def lambda_handler(event, context): + return {"statusCode": 200, "body": "Placeholder - deploy script will update with actual code"} # ============================================================ # Stack Outputs diff --git a/strands-agentcore-smithy/scripts/deploy.sh b/strands-agentcore-smithy/scripts/deploy.sh index 9d17a4df28..015b553e91 100755 --- a/strands-agentcore-smithy/scripts/deploy.sh +++ b/strands-agentcore-smithy/scripts/deploy.sh @@ -2,20 +2,21 @@ set -euo pipefail # ============================================================ -# AgentCore Smithy Bedrock - Deployment Script (AWS SAM) +# AgentCore Smithy Bedrock - Deployment Script # ============================================================ -# Orchestrates the full deployment lifecycle with AWS SAM: -# 1. Validate the SAM template -# 2. Upload the official Smithy model to S3 -# 3. Build the Lambda with SAM (Makefile build — no Docker) -# 4. Deploy the stack with SAM -# 5. Create a Cognito test user -# 6. Generate scripts/test.sh +# Orchestrates the full deployment lifecycle: +# 1. Validate CloudFormation template +# 2. Upload Smithy model to S3 +# 3. Deploy CloudFormation stack (create or update) +# 4. Package Lambda code (two-step pip3 install) +# 5. Deploy Lambda code (S3 fallback if >50MB) +# 6. Create Cognito test user +# 7. Generate scripts/test.sh # ============================================================ STACK_NAME="${STACK_NAME:-agentcore-smithy-bedrock}" REGION="${AWS_REGION:-us-east-1}" -TEMPLATE_PATH="infrastructure/template.yaml" +TEMPLATE_PATH="infrastructure/cloudformation-template.yaml" TEST_USERNAME="testuser" TEST_PASSWORD="TestPass123!" @@ -24,12 +25,12 @@ echo "Deploying stack: ${STACK_NAME} in region: ${REGION}" echo "============================================================" # ============================================================ -# Step 1: Validate the SAM template +# Step 1: Validate CloudFormation template # ============================================================ echo "" -echo ">>> Step 1: Validating SAM template..." -sam validate \ - --template-file "${TEMPLATE_PATH}" \ +echo ">>> Step 1: Validating CloudFormation template..." +aws cloudformation validate-template \ + --template-body "file://${TEMPLATE_PATH}" \ --region "${REGION}" > /dev/null echo "Template validation successful." @@ -48,28 +49,69 @@ aws s3 cp "${SMITHY_MODEL_FILE}" "s3://${SMITHY_BUCKET}/bedrock-runtime-2023-09- echo "Smithy model uploaded to s3://${SMITHY_BUCKET}/bedrock-runtime-2023-09-30.json" # ============================================================ -# Step 3: Build the Lambda with SAM (Makefile build, no Docker) +# Step 2: Deploy CloudFormation stack (create or update) # ============================================================ echo "" -echo ">>> Step 3: Building Lambda with SAM (Makefile build)..." -sam build \ - --template-file "${TEMPLATE_PATH}" -echo "SAM build complete." +echo ">>> Step 3: Deploying CloudFormation stack..." -# ============================================================ -# Step 4: Deploy the stack with SAM -# ============================================================ -echo "" -echo ">>> Step 4: Deploying stack with SAM..." -sam deploy \ +STACK_STATUS=$(aws cloudformation describe-stacks \ --stack-name "${STACK_NAME}" \ --region "${REGION}" \ - --capabilities CAPABILITY_NAMED_IAM \ - --resolve-s3 \ - --no-confirm-changeset \ - --no-fail-on-empty-changeset \ - --parameter-overrides "GatewayName=smithy-bedrock-gateway" -echo "SAM deploy complete." + --query "Stacks[0].StackStatus" \ + --output text 2>&1 || true) + +if echo "${STACK_STATUS}" | grep -qi "does.not.exist\|DOES_NOT_EXIST"; then + echo "Stack does not exist. Creating..." + aws cloudformation create-stack \ + --stack-name "${STACK_NAME}" \ + --template-body "file://${TEMPLATE_PATH}" \ + --capabilities CAPABILITY_NAMED_IAM \ + --region "${REGION}" + + echo "Waiting for stack creation to complete..." + aws cloudformation wait stack-create-complete \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" + echo "Stack creation complete." +elif echo "${STACK_STATUS}" | grep -q "ROLLBACK_COMPLETE"; then + echo "Stack is in ROLLBACK_COMPLETE state. Deleting before re-creating..." + aws cloudformation delete-stack \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" + aws cloudformation wait stack-delete-complete \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" + echo "Old stack deleted. Creating fresh stack..." + aws cloudformation create-stack \ + --stack-name "${STACK_NAME}" \ + --template-body "file://${TEMPLATE_PATH}" \ + --capabilities CAPABILITY_NAMED_IAM \ + --region "${REGION}" + + echo "Waiting for stack creation to complete..." + aws cloudformation wait stack-create-complete \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" + echo "Stack creation complete." +else + echo "Stack exists (status: ${STACK_STATUS}). Updating..." + UPDATE_OUTPUT=$(aws cloudformation update-stack \ + --stack-name "${STACK_NAME}" \ + --template-body "file://${TEMPLATE_PATH}" \ + --capabilities CAPABILITY_NAMED_IAM \ + --region "${REGION}" 2>&1 || true) + + if echo "${UPDATE_OUTPUT}" | grep -q "No updates are to be performed"; then + echo "No updates are to be performed. Continuing..." + else + echo "Waiting for stack update to complete..." + aws cloudformation wait stack-update-complete \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" + echo "Stack update complete." + fi +fi + # ============================================================ # Retrieve stack outputs @@ -106,11 +148,88 @@ echo "Cognito User Pool ID: ${COGNITO_USER_POOL_ID}" echo "Cognito Client ID: ${COGNITO_CLIENT_ID}" echo "Lambda Function: ${LAMBDA_FUNCTION_NAME}" +# ============================================================ +# Step 3: Package Lambda code (two-step pip3 install) +# ============================================================ +echo "" +echo ">>> Step 4: Packaging Lambda code..." + +PACKAGE_DIR=$(mktemp -d) +echo "Using temp directory: ${PACKAGE_DIR}" + +# Copy source code +cp -r src "${PACKAGE_DIR}/" + +# Step 3a: Install binary packages with platform targeting +echo "Installing binary dependencies (step 1 of 2)..." +pip3 install \ + --target "${PACKAGE_DIR}" \ + --platform manylinux2014_x86_64 \ + --python-version 3.12 \ + --only-binary=:all: \ + -r requirements.txt + +# Step 3b: Install pure Python packages that may have been skipped +echo "Installing pure Python dependencies (step 2 of 2)..." +pip3 install \ + --target "${PACKAGE_DIR}" \ + --platform manylinux2014_x86_64 \ + --python-version 3.12 \ + --only-binary=:all: \ + --no-deps \ + requests urllib3 charset-normalizer idna certifi PyJWT cryptography cffi + +# Create zip package +ZIP_FILE="/tmp/lambda-package-$$.zip" +rm -f "${ZIP_FILE}" +echo "Creating zip package..." +(cd "${PACKAGE_DIR}" && zip -r "${ZIP_FILE}" . -x "__pycache__/*" "*/__pycache__/*") + +# ============================================================ +# Step 4: Deploy Lambda code (S3 fallback if >50MB) +# ============================================================ +echo "" +echo ">>> Step 5: Deploying Lambda code..." + +# Get file size (macOS and Linux compatible) +ZIP_SIZE=$(stat -f%z "${ZIP_FILE}" 2>/dev/null || stat -c%s "${ZIP_FILE}") +MAX_SIZE=$((50 * 1024 * 1024)) + +echo "Package size: ${ZIP_SIZE} bytes" + +if [ "${ZIP_SIZE}" -gt "${MAX_SIZE}" ]; then + echo "Package exceeds 50MB. Using S3 fallback..." + S3_BUCKET="${STACK_NAME}-lambda-packages" + S3_KEY="lambda-package.zip" + + aws s3 mb "s3://${S3_BUCKET}" --region "${REGION}" 2>/dev/null || true + aws s3 cp "${ZIP_FILE}" "s3://${S3_BUCKET}/${S3_KEY}" --region "${REGION}" + + aws lambda update-function-code \ + --function-name "${LAMBDA_FUNCTION_NAME}" \ + --s3-bucket "${S3_BUCKET}" \ + --s3-key "${S3_KEY}" \ + --region "${REGION}" > /dev/null +else + echo "Package under 50MB. Uploading directly..." + aws lambda update-function-code \ + --function-name "${LAMBDA_FUNCTION_NAME}" \ + --zip-file "fileb://${ZIP_FILE}" \ + --region "${REGION}" > /dev/null +fi + +echo "Lambda code deployed successfully." + +# Cleanup temp files +rm -rf "${PACKAGE_DIR}" +rm -f "${ZIP_FILE}" + + # ============================================================ # Step 5: Create Cognito test user # ============================================================ echo "" -echo ">>> Step 5: Creating Cognito test user..." +echo ">>> Step 6: Creating Cognito test user..." aws cognito-idp admin-create-user \ --user-pool-id "${COGNITO_USER_POOL_ID}" \ @@ -128,11 +247,12 @@ aws cognito-idp admin-set-user-password \ echo "Cognito test user created (username: ${TEST_USERNAME})." + # ============================================================ -# Step 6: Generate test script (scripts/test.sh) +# Step 7: Generate test script (scripts/test.sh) # ============================================================ echo "" -echo ">>> Step 6: Generating test script..." +echo ">>> Step 7: Generating test script..." cat > scripts/test.sh << 'TESTSCRIPT_EOF' #!/usr/bin/env bash diff --git a/strands-agentcore-smithy/scripts/test.sh b/strands-agentcore-smithy/scripts/test.sh index bed5ae17b4..c3a974bde6 100755 --- a/strands-agentcore-smithy/scripts/test.sh +++ b/strands-agentcore-smithy/scripts/test.sh @@ -7,9 +7,9 @@ set -euo pipefail # ============================================================ REGION="us-east-1" -GATEWAY_ID="smithy-bedrock-gateway-2icnnwzik5" -COGNITO_USER_POOL_ID="us-east-1_d0B5LdMQN" -COGNITO_CLIENT_ID="c2ns12guk72f41kvsqs9oqs6u" +GATEWAY_ID="smithy-bedrock-gateway-8oh0kf7tje" +COGNITO_USER_POOL_ID="us-east-1_UNquXQKHg" +COGNITO_CLIENT_ID="2j56oaad81l9nid0lssick0tsl" LAMBDA_FUNCTION_NAME="agentcore-smithy-bedrock-AgentFunction" TEST_USERNAME="testuser" TEST_PASSWORD="TestPass123!" diff --git a/strands-agentcore-smithy/src/agent/handler.py b/strands-agentcore-smithy/src/agent/handler.py index 3db24815ae..99f90e9398 100644 --- a/strands-agentcore-smithy/src/agent/handler.py +++ b/strands-agentcore-smithy/src/agent/handler.py @@ -6,6 +6,7 @@ Lambda response serialization. """ +import json from dataclasses import asdict from src.agent.agent_processor import process_request @@ -87,7 +88,6 @@ def lambda_handler(event: dict, context) -> dict: if isinstance(body, dict): prompt = body.get("prompt", "") elif isinstance(body, str): - import json try: body = json.loads(body) prompt = body.get("prompt", "") diff --git a/strands-agentcore-smithy/src/agent/strands_client.py b/strands-agentcore-smithy/src/agent/strands_client.py index ccf4e73729..8b25a82c95 100644 --- a/strands-agentcore-smithy/src/agent/strands_client.py +++ b/strands-agentcore-smithy/src/agent/strands_client.py @@ -1,7 +1,7 @@ """Factory functions for Strands SDK client configuration. Target-type agnostic factories for creating MCP clients and Bedrock model -instances. These functions connect to AgentCore Gateway via MCP protocol +instances. These functions connect to Amazon Bedrock AgentCore Gateway via MCP protocol and are unaware of the target type behind the gateway. """ @@ -21,14 +21,14 @@ def create_mcp_client(gateway_url: str, token: str) -> MCPClient: - """Create an MCP client configured to connect to AgentCore Gateway. + """Create an MCP client configured to connect to Amazon Bedrock AgentCore Gateway. Uses streamable HTTP transport with a Bearer token for authentication. The returned client is NOT used as a context manager — the Agent manages the MCP session lifecycle, and cleanup should occur in a finally block. Args: - gateway_url: The AgentCore Gateway endpoint URL. + gateway_url: The Amazon Bedrock AgentCore Gateway endpoint URL. token: JWT token for gateway authentication. Returns: @@ -52,10 +52,10 @@ def create_bedrock_model( ) -> BedrockModel: """Create a Bedrock model instance for the Strands Agent. - Configures Claude 3 Sonnet as the LLM provider via AWS Bedrock. + Configures Claude Sonnet 4.6 as the LLM provider via AWS Bedrock. Args: - model_id: Bedrock model identifier. Defaults to Claude 3 Sonnet. + model_id: Bedrock model identifier. Defaults to Claude Sonnet 4.6. region_name: AWS region for Bedrock API calls. Defaults to us-east-1. Returns: diff --git a/strands-agentcore-smithy/tests/conftest.py b/strands-agentcore-smithy/tests/conftest.py index aec84a94fe..b73639f891 100644 --- a/strands-agentcore-smithy/tests/conftest.py +++ b/strands-agentcore-smithy/tests/conftest.py @@ -40,8 +40,8 @@ def _cfn_tag_constructor(loader, tag_suffix, node): @pytest.fixture(scope="session") def cfn_template(): - """Load and return the parsed SAM/CloudFormation template.""" - path = os.path.join(_ROOT, "infrastructure", "template.yaml") + """Load and return the parsed CloudFormation template.""" + path = os.path.join(_ROOT, "infrastructure", "cloudformation-template.yaml") with open(path) as fh: return yaml.load(fh, Loader=_CfnLoader) diff --git a/strands-agentcore-smithy/tests/property/test_cloudformation_properties.py b/strands-agentcore-smithy/tests/property/test_cloudformation_properties.py index fe5b75cf69..daa7b332d6 100644 --- a/strands-agentcore-smithy/tests/property/test_cloudformation_properties.py +++ b/strands-agentcore-smithy/tests/property/test_cloudformation_properties.py @@ -46,7 +46,7 @@ def _cfn_tag_constructor(loader, tag_suffix, node): # --------------------------------------------------------------------------- _TEMPLATE_PATH = os.path.join( - os.path.dirname(__file__), "..", "..", "infrastructure", "template.yaml" + os.path.dirname(__file__), "..", "..", "infrastructure", "cloudformation-template.yaml" ) with open(_TEMPLATE_PATH) as _fh: diff --git a/strands-agentcore-smithy/tests/property/test_deploy_script_properties.py b/strands-agentcore-smithy/tests/property/test_deploy_script_properties.py index f1f489ddf6..3ce68cabce 100644 --- a/strands-agentcore-smithy/tests/property/test_deploy_script_properties.py +++ b/strands-agentcore-smithy/tests/property/test_deploy_script_properties.py @@ -1,12 +1,8 @@ -"""Property-based tests for the Makefile build (Property 7). +"""Property-based tests for deploy script (Property 7). Feature: agentcore-smithy-bedrock -Property 7: Lambda packaging uses pip3 exclusively - Validates: pip3-only packaging - -After the migration to AWS SAM, Lambda packaging (the pip install steps) -lives in the root Makefile, invoked by `sam build` via BuildMethod: makefile. -This property verifies every pip install invocation uses pip3, not bare pip. +Property 7: Deploy script uses pip3 exclusively + Validates: Requirements 9.7 """ import os @@ -16,26 +12,34 @@ from hypothesis import strategies as st # --------------------------------------------------------------------------- -# Load Makefile content (packaging now lives here, not in deploy.sh) +# Load deploy script content # --------------------------------------------------------------------------- -_MAKEFILE_PATH = os.path.join( - os.path.dirname(__file__), "..", "..", "Makefile" +_SCRIPT_PATH = os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "deploy.sh" ) -with open(_MAKEFILE_PATH) as _fh: - _MAKEFILE_CONTENT = _fh.read() +with open(_SCRIPT_PATH) as _fh: + _SCRIPT_CONTENT = _fh.read() + +# Extract all lines that contain pip install commands (pip or pip3) +_PIP_COMMAND_PATTERN = re.compile(r"^\s*(pip3?|pip)\s+install\b", re.MULTILINE) +_PIP_LINES = [ + (i + 1, line) + for i, line in enumerate(_SCRIPT_CONTENT.splitlines()) + if _PIP_COMMAND_PATTERN.search(line) +] -# Find all pip/pip3 install invocations -_PIP_INVOCATIONS = re.findall(r"\b(pip3?)\s+install\b", _MAKEFILE_CONTENT) +# Also find continuation lines that start a pip/pip3 command +_PIP_INVOCATIONS = re.findall(r"\b(pip3?)\s+install\b", _SCRIPT_CONTENT) -assert len(_PIP_INVOCATIONS) > 0, "No pip install commands found in Makefile" +assert len(_PIP_INVOCATIONS) > 0, "No pip install commands found in deploy script" pip_invocation_st = st.sampled_from(_PIP_INVOCATIONS) # --------------------------------------------------------------------------- -# Property 7: Lambda packaging uses pip3 exclusively -# Tag: Feature: agentcore-smithy-bedrock, Property 7: Lambda packaging uses +# Property 7: Deploy script uses pip3 exclusively +# Tag: Feature: agentcore-smithy-bedrock, Property 7: Deploy script uses # pip3 exclusively # --------------------------------------------------------------------------- @@ -43,8 +47,8 @@ @given(invocation=pip_invocation_st) @settings(max_examples=100) def test_property7_all_pip_commands_use_pip3(invocation): - """For all pip install commands in the Makefile, each command shall + """For all pip install commands in the deploy script, each command shall use pip3 (not pip) as the executable name.""" assert invocation == "pip3", ( - "Found bare 'pip' instead of 'pip3' in Makefile" + f"Found bare 'pip' instead of 'pip3' in deploy script" ) diff --git a/strands-agentcore-smithy/tests/unit/test_cloudformation_template.py b/strands-agentcore-smithy/tests/unit/test_cloudformation_template.py index 883d26b10f..b18ad84182 100644 --- a/strands-agentcore-smithy/tests/unit/test_cloudformation_template.py +++ b/strands-agentcore-smithy/tests/unit/test_cloudformation_template.py @@ -34,7 +34,7 @@ def _cfn_tag_constructor(loader, tag_suffix, node): # --------------------------------------------------------------------------- _TEMPLATE_PATH = os.path.join( - os.path.dirname(__file__), "..", "..", "infrastructure", "template.yaml" + os.path.dirname(__file__), "..", "..", "infrastructure", "cloudformation-template.yaml" ) with open(_TEMPLATE_PATH) as _fh: diff --git a/strands-agentcore-smithy/tests/unit/test_deploy_script.py b/strands-agentcore-smithy/tests/unit/test_deploy_script.py index 53ccaf8b76..0cc5ee6b58 100644 --- a/strands-agentcore-smithy/tests/unit/test_deploy_script.py +++ b/strands-agentcore-smithy/tests/unit/test_deploy_script.py @@ -1,64 +1,59 @@ -"""Unit tests for the SAM deploy script (scripts/deploy.sh) and Makefile build. +"""Unit tests for deploy script (scripts/deploy.sh). -After the migration from raw CloudFormation to AWS SAM: - - Orchestration (validate / build / deploy) lives in scripts/deploy.sh and - uses the `sam` CLI. - - Lambda packaging (the two-step pip3 install) lives in the root Makefile, - invoked by `sam build` via BuildMethod: makefile (no Docker required). +Validates: Requirements 8.1–8.5, 9.1–9.8 """ import os import re +import pytest + _SCRIPT_PATH = os.path.join( os.path.dirname(__file__), "..", "..", "scripts", "deploy.sh" ) -_MAKEFILE_PATH = os.path.join( - os.path.dirname(__file__), "..", "..", "Makefile" -) with open(_SCRIPT_PATH) as _fh: DEPLOY_CONTENT = _fh.read() -with open(_MAKEFILE_PATH) as _fh: - MAKEFILE_CONTENT = _fh.read() - -# ── Makefile: two-step pip3 install (packaging moved out of deploy.sh) ── +# ── Requirement 8.1: First pip install step with --only-binary, --platform, --python-version ── class TestTwoStepPipInstall: - """Verify the two-step pip3 install pattern lives in the Makefile.""" + """Verify the two-step pip3 install pattern (Requirements 8.1, 8.2).""" def test_first_step_has_only_binary(self): """First pip3 install uses --only-binary=:all:""" + # pip3 install commands span multiple lines with backslash continuations first_pip = re.search( - r"pip3\s+install\b.*?--only-binary=:all:", MAKEFILE_CONTENT, re.DOTALL + r"pip3\s+install\b.*?--only-binary=:all:", DEPLOY_CONTENT, re.DOTALL ) assert first_pip is not None, "First pip3 install must use --only-binary=:all:" def test_first_step_has_platform_targeting(self): """First pip3 install targets manylinux2014_x86_64.""" assert re.search( - r"pip3\s+install\b.*--platform\s+manylinux2014_x86_64", MAKEFILE_CONTENT, re.DOTALL + r"pip3\s+install\b.*--platform\s+manylinux2014_x86_64", DEPLOY_CONTENT, re.DOTALL ), "First pip3 install must target --platform manylinux2014_x86_64" def test_first_step_has_python_version(self): """First pip3 install specifies --python-version 3.12.""" assert re.search( - r"pip3\s+install\b.*--python-version\s+3\.12", MAKEFILE_CONTENT, re.DOTALL + r"pip3\s+install\b.*--python-version\s+3\.12", DEPLOY_CONTENT, re.DOTALL ), "First pip3 install must specify --python-version 3.12" def test_second_step_has_no_deps(self): """Second pip3 install uses --no-deps for pure Python packages.""" + # pip3 install commands span multiple lines with backslash continuations no_deps_match = re.search( - r"pip3\s+install\b.*?--no-deps", MAKEFILE_CONTENT, re.DOTALL + r"pip3\s+install\b.*?--no-deps", DEPLOY_CONTENT, re.DOTALL ) assert no_deps_match is not None, "Second pip3 install must use --no-deps" def test_second_step_includes_pure_python_packages(self): """Second step installs the expected pure Python packages.""" + # Find the --no-deps pip3 install block and check for expected packages no_deps_section = re.search( - r"pip3\s+install\b.*?--no-deps\b(.*)", MAKEFILE_CONTENT, re.DOTALL + r"pip3\s+install\b.*?--no-deps\b(.*?)(?:\n\n|\Z)", DEPLOY_CONTENT, re.DOTALL ) assert no_deps_section is not None block = no_deps_section.group(0) @@ -67,110 +62,99 @@ def test_second_step_includes_pure_python_packages(self): def test_at_least_two_pip3_install_commands(self): """There should be at least two pip3 install invocations (binary + pure Python).""" - invocations = re.findall(r"\bpip3\s+install\b", MAKEFILE_CONTENT) + invocations = re.findall(r"\bpip3\s+install\b", DEPLOY_CONTENT) assert len(invocations) >= 2, f"Expected at least 2 pip3 install commands, found {len(invocations)}" -# ── No .dist-info removal ── +# ── Requirement 8.3: No .dist-info removal ── class TestNoDistInfoRemoval: def test_no_dist_info_removal(self): - """Neither the Makefile nor deploy.sh must remove .dist-info directories.""" - combined = MAKEFILE_CONTENT + "\n" + DEPLOY_CONTENT - assert not re.search(r"rm\s.*\.dist-info", combined), ( - "Must not remove .dist-info directories" - ) - assert not re.search(r"find.*\.dist-info.*-delete", combined), ( - "Must not delete .dist-info via find" + """Deploy script must NOT remove .dist-info directories.""" + assert ".dist-info" not in re.findall( + r"rm\s.*\.dist-info", DEPLOY_CONTENT + ), "Script must not remove .dist-info directories" + # Also check for find -delete patterns + assert not re.search( + r"find.*\.dist-info.*-delete", DEPLOY_CONTENT + ), "Script must not delete .dist-info via find" + + +# ── Requirement 8.4: S3 fallback if zip exceeds 50MB ── + +class TestS3Fallback: + def test_s3_fallback_logic_present(self): + """Script checks zip size and falls back to S3 if >50MB.""" + assert "50" in DEPLOY_CONTENT and "s3" in DEPLOY_CONTENT.lower(), ( + "Script must contain S3 fallback logic for packages >50MB" ) + def test_s3_upload_uses_s3_cp(self): + """S3 fallback uses 'aws s3 cp' to upload the zip.""" + assert re.search(r"aws\s+s3\s+cp\b", DEPLOY_CONTENT), ( + "S3 fallback must use 'aws s3 cp'" + ) -# ── SAM build handles packaging + artifact upload ── - -class TestSamBuildAndDeploy: - def test_uses_sam_build(self): - """Script builds the Lambda with `sam build`.""" - assert re.search(r"\bsam\s+build\b", DEPLOY_CONTENT), "Script must call sam build" - - def test_uses_sam_deploy(self): - """Script deploys with `sam deploy`.""" - assert re.search(r"\bsam\s+deploy\b", DEPLOY_CONTENT), "Script must call sam deploy" - - def test_sam_deploy_has_named_iam_capability(self): - """SAM deploy must pass CAPABILITY_NAMED_IAM for the named roles.""" - assert "CAPABILITY_NAMED_IAM" in DEPLOY_CONTENT - - def test_sam_deploy_resolves_artifact_bucket(self): - """SAM deploy manages the artifact bucket via --resolve-s3.""" - assert "--resolve-s3" in DEPLOY_CONTENT - - def test_makefile_build_method(self): - """The Makefile exposes the SAM build target for the function.""" - assert "build-AgentLambdaFunction" in MAKEFILE_CONTENT + def test_s3_fallback_uses_update_function_code_with_s3(self): + """S3 fallback updates Lambda with --s3-bucket and --s3-key.""" + assert re.search(r"--s3-bucket\b", DEPLOY_CONTENT), "S3 fallback must pass --s3-bucket" + assert re.search(r"--s3-key\b", DEPLOY_CONTENT), "S3 fallback must pass --s3-key" -# ── Required dependencies ── +# ── Requirement 8.5: Required dependencies ── class TestRequiredDependencies: def test_requirements_txt_referenced(self): - """Makefile references requirements.txt for pip install.""" - assert "requirements.txt" in MAKEFILE_CONTENT + """Deploy script references requirements.txt for pip install.""" + assert "requirements.txt" in DEPLOY_CONTENT -# ── Template validation before deployment ── +# ── Requirement 9.1: Template validation before deployment ── class TestTemplateValidation: def test_validate_template_before_deploy(self): - """Script validates the SAM template before deploying.""" - validate_pos = DEPLOY_CONTENT.find("sam validate") - deploy_pos = DEPLOY_CONTENT.find("sam deploy") - assert validate_pos != -1, "Script must call sam validate" - assert deploy_pos != -1, "Script must call sam deploy" - assert validate_pos < deploy_pos, "sam validate must come before sam deploy" - - def test_references_sam_template(self): - """Script references the SAM template path.""" - assert "infrastructure/template.yaml" in DEPLOY_CONTENT + """Script validates the CloudFormation template before deploying.""" + validate_pos = DEPLOY_CONTENT.find("validate-template") + create_pos = DEPLOY_CONTENT.find("create-stack") + assert validate_pos != -1, "Script must call validate-template" + assert validate_pos < create_pos, "validate-template must come before create-stack" -# ── Smithy model upload (must precede stack deploy) ── +# ── Requirement 9.2: DOES_NOT_EXIST detection ── -class TestSmithyModelUpload: - def test_uploads_smithy_model_to_s3(self): - """Script uploads the Smithy model with `aws s3 cp`.""" - assert re.search(r"aws\s+s3\s+cp\b", DEPLOY_CONTENT), ( - "Script must upload the Smithy model with aws s3 cp" +class TestStackExistenceDetection: + def test_does_not_exist_check(self): + """Script checks for DOES_NOT_EXIST to decide create vs update.""" + assert "DOES_NOT_EXIST" in DEPLOY_CONTENT, ( + "Script must check for DOES_NOT_EXIST pattern" ) - def test_smithy_upload_before_deploy(self): - """Smithy model must be in S3 before the stack (and its target) deploys.""" - upload_pos = DEPLOY_CONTENT.find("aws s3 cp") - deploy_pos = DEPLOY_CONTENT.find("sam deploy") - assert upload_pos != -1 and deploy_pos != -1 - assert upload_pos < deploy_pos, "Smithy upload must precede sam deploy" - + def test_create_stack_path(self): + """Script has a create-stack code path.""" + assert "create-stack" in DEPLOY_CONTENT -# ── Cognito test user creation ── + def test_update_stack_path(self): + """Script has an update-stack code path.""" + assert "update-stack" in DEPLOY_CONTENT -class TestCognitoUser: - def test_creates_cognito_user(self): - """Script creates a Cognito test user.""" - assert "admin-create-user" in DEPLOY_CONTENT - assert "admin-set-user-password" in DEPLOY_CONTENT + def test_no_updates_handled_gracefully(self): + """Script handles 'No updates are to be performed' gracefully.""" + assert "No updates are to be performed" in DEPLOY_CONTENT -# ── pip3 exclusively (in the Makefile) ── +# ── Requirement 9.7: pip3 exclusively ── class TestPip3Exclusively: def test_all_pip_commands_use_pip3(self): """All pip install commands use pip3, not bare pip.""" - invocations = re.findall(r"\b(pip3?)\s+install\b", MAKEFILE_CONTENT) + # Find all pip/pip3 install invocations + invocations = re.findall(r"\b(pip3?)\s+install\b", DEPLOY_CONTENT) assert len(invocations) > 0, "Must have pip install commands" for inv in invocations: assert inv == "pip3", f"Found bare '{inv}' instead of 'pip3'" -# ── No credential provider commands ── +# ── Requirement 9.8: No credential provider commands ── class TestNoForbiddenPatterns: def test_no_credential_provider_commands(self): @@ -190,3 +174,11 @@ def test_no_api_key_retrieval(self): assert not re.search(r"get-api-key", DEPLOY_CONTENT), ( "Script must not retrieve API keys" ) + + def test_no_stack_re_update(self): + """Script must not re-update the stack after initial deployment. + There should be at most one update-stack call.""" + update_calls = re.findall(r"\bupdate-stack\b", DEPLOY_CONTENT) + assert len(update_calls) <= 1, ( + f"Expected at most 1 update-stack call, found {len(update_calls)}" + )