diff --git a/apigw-lambda-websearch-bedrock-cdk/.gitignore b/apigw-lambda-websearch-bedrock-cdk/.gitignore new file mode 100644 index 0000000000..888fdf2586 --- /dev/null +++ b/apigw-lambda-websearch-bedrock-cdk/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +cdk.out/ +build/ +*.js +*.d.ts +cdk.context.json diff --git a/apigw-lambda-websearch-bedrock-cdk/README.md b/apigw-lambda-websearch-bedrock-cdk/README.md new file mode 100644 index 0000000000..d2720d7be4 --- /dev/null +++ b/apigw-lambda-websearch-bedrock-cdk/README.md @@ -0,0 +1,85 @@ +# Grounded AI Answers with Amazon Bedrock AgentCore Web Search and Amazon Bedrock (CDK) + +This pattern deploys an Amazon API Gateway REST API backed by an AWS Lambda function that searches the live web via Amazon Bedrock AgentCore Web Search, then uses Amazon Bedrock to synthesize accurate, cited answers grounded in current facts. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigw-lambda-websearch-bedrock-cdk + +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. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Node.js 20+](https://nodejs.org/en/download/) installed +* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) installed (`npm install -g aws-cdk`) +* CDK bootstrapped in your account/region (`cdk bootstrap`) +* Amazon Bedrock model access enabled for Claude Sonnet 4 in us-east-1 + +## Deployment Instructions + +1. Navigate to the pattern directory: + ```bash + cd apigw-lambda-websearch-bedrock-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Deploy: + ```bash + npx cdk deploy + ``` + +## How it works + +This pattern demonstrates **Retrieval-Augmented Generation (RAG) using live web data** instead of a static vector database: + +1. **Amazon API Gateway** receives user questions via POST /ask +2. **AWS Lambda** orchestrates the two-step process: + - Searches the live web via Amazon Bedrock AgentCore Gateway's Web Search connector (MCP protocol + SigV4) + - Passes search results as context to Amazon Bedrock (Claude Sonnet 4) for inference +3. **Amazon Bedrock** synthesizes a grounded answer with numbered citation references +4. Response includes the answer text and source URLs for verification + +This eliminates the need for maintaining a vector database, embedding pipeline, or data ingestion — the web IS the knowledge base, always current. + +## Testing + +```bash +curl -X POST https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/prod/ask \ + -H "Content-Type: application/json" \ + -d '{"question": "What was announced at AWS Summit NYC 2026?"}' +``` + +Or via AWS Lambda directly: +```bash +aws lambda invoke --function-name FUNCTION_NAME \ + --payload '{"body": "{\"question\": \"What is Amazon Aurora DSQL?\"}"}' \ + --cli-binary-format raw-in-base64-out output.json && cat output.json +``` + +## Example Response + +```json +{ + "answer": "Amazon Aurora DSQL is a serverless, distributed SQL database [1] that provides active-active multi-region support with PostgreSQL compatibility [2]...", + "sources": [ + {"title": "Amazon Aurora DSQL documentation", "url": "https://docs.aws.amazon.com/aurora-dsql/..."}, + {"title": "Aurora DSQL launch blog", "url": "https://aws.amazon.com/blogs/..."} + ] +} +``` + +## Cleanup + +```bash +npx cdk destroy +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-lambda-websearch-bedrock-cdk/bin/app.ts b/apigw-lambda-websearch-bedrock-cdk/bin/app.ts new file mode 100644 index 0000000000..4eefb26013 --- /dev/null +++ b/apigw-lambda-websearch-bedrock-cdk/bin/app.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { ApigwLambdaWebsearchBedrockStack } from '../lib/apigw-lambda-websearch-bedrock-stack'; + +const app = new cdk.App(); +new ApigwLambdaWebsearchBedrockStack(app, 'ApigwLambdaWebsearchBedrockStack', { + env: { region: 'us-east-1' }, +}); diff --git a/apigw-lambda-websearch-bedrock-cdk/cdk.json b/apigw-lambda-websearch-bedrock-cdk/cdk.json new file mode 100644 index 0000000000..a260472557 --- /dev/null +++ b/apigw-lambda-websearch-bedrock-cdk/cdk.json @@ -0,0 +1 @@ +{ "app": "node build/bin/app.js" } diff --git a/apigw-lambda-websearch-bedrock-cdk/example-pattern.json b/apigw-lambda-websearch-bedrock-cdk/example-pattern.json new file mode 100644 index 0000000000..ed2a948205 --- /dev/null +++ b/apigw-lambda-websearch-bedrock-cdk/example-pattern.json @@ -0,0 +1,96 @@ +{ + "title": "Grounded AI answers with Amazon Bedrock AgentCore Web Search and Amazon Bedrock", + "description": "Deploy an Amazon API Gateway REST API backed by an AWS Lambda function that searches the live web via Amazon Bedrock AgentCore Web Search, then uses Amazon Bedrock to synthesize accurate, cited answers grounded in current facts.", + "language": "TypeScript", + "level": "300", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "A user sends a question to the Amazon API Gateway REST API endpoint.", + "The AWS Lambda function searches the live web via Amazon Bedrock AgentCore Gateway's Web Search connector using the MCP protocol with SigV4 authentication.", + "Web search results (titles, URLs, snippets) are formatted as context and sent to Amazon Bedrock (Claude) for inference.", + "Amazon Bedrock synthesizes a grounded answer with numbered citation references, returned to the user with source URLs." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-lambda-websearch-bedrock-cdk", + "templateURL": "serverless-patterns/apigw-lambda-websearch-bedrock-cdk", + "projectFolder": "apigw-lambda-websearch-bedrock-cdk", + "templateFile": "lib/apigw-lambda-websearch-bedrock-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Bedrock AgentCore Web Search documentation", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" + }, + { + "text": "Amazon Bedrock InvokeModel API", + "link": "https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModel.html" + } + ] + }, + "deploy": { + "text": [ + "npx cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "npx cdk destroy" + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ], + "patternArch": { + "icon1": { + "x": 10, + "y": 50, + "service": "apigateway", + "label": "Amazon API Gateway" + }, + "icon2": { + "x": 35, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "icon3": { + "x": 60, + "y": 30, + "service": "bedrock", + "label": "AgentCore Web Search" + }, + "icon4": { + "x": 60, + "y": 70, + "service": "bedrock", + "label": "Amazon Bedrock" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "line2": { + "from": "icon2", + "to": "icon3" + }, + "line3": { + "from": "icon2", + "to": "icon4" + } + } +} diff --git a/apigw-lambda-websearch-bedrock-cdk/lib/apigw-lambda-websearch-bedrock-stack.ts b/apigw-lambda-websearch-bedrock-cdk/lib/apigw-lambda-websearch-bedrock-stack.ts new file mode 100644 index 0000000000..3e7a9f7916 --- /dev/null +++ b/apigw-lambda-websearch-bedrock-cdk/lib/apigw-lambda-websearch-bedrock-stack.ts @@ -0,0 +1,84 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as agentcore from 'aws-cdk-lib/aws-bedrockagentcore'; +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +export class ApigwLambdaWebsearchBedrockStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Amazon Bedrock AgentCore Gateway with Web Search connector + const gateway = new agentcore.Gateway(this, 'WebSearchGateway', { + gatewayName: 'grounded-search-gateway', + description: 'Amazon Bedrock AgentCore Gateway with Web Search for grounded AI answers', + authorizerConfiguration: agentcore.GatewayAuthorizer.usingAwsIam(), + }); + + // Web Search connector target + new cdk.CfnResource(this, 'WebSearchTarget', { + type: 'AWS::BedrockAgentCore::GatewayTarget', + properties: { + GatewayIdentifier: gateway.gatewayId, + Name: 'web-search', + TargetConfiguration: { + Mcp: { + Connector: { + Source: { ConnectorId: 'web-search' }, + Configurations: [{ Name: 'WebSearch', ParameterValues: {} }], + }, + }, + }, + CredentialProviderConfigurations: [ + { CredentialProviderType: 'GATEWAY_IAM_ROLE' }, + ], + }, + }); + + // Grant gateway role permission to invoke Web Search + gateway.role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['bedrock-agentcore:InvokeWebSearch'], + resources: [`arn:aws:bedrock-agentcore:${this.region}:aws:tool/web-search.v1`], + })); + + // AWS Lambda function that orchestrates Web Search + Amazon Bedrock inference + const fn = new lambda.Function(this, 'GroundedAnswerFunction', { + runtime: lambda.Runtime.PYTHON_3_12, + handler: 'index.handler', + code: lambda.Code.fromAsset('src/handler'), + timeout: cdk.Duration.seconds(60), + memorySize: 256, + environment: { + GATEWAY_ID: gateway.gatewayId, + GATEWAY_URL: gateway.gatewayUrl ?? '', + MODEL_ID: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + }, + }); + + // Grant the AWS Lambda function permission to invoke the gateway + gateway.grantInvoke(fn); + + // Grant the AWS Lambda function permission to invoke Amazon Bedrock models + fn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel'], + resources: [ + `arn:aws:bedrock:*::foundation-model/*`, + `arn:aws:bedrock:*:${this.account}:inference-profile/*`, + ], + })); + + // Amazon API Gateway REST API as the user-facing endpoint + const api = new apigateway.RestApi(this, 'GroundedSearchApi', { + restApiName: 'Grounded AI Search', + description: 'Ask questions grounded in live web data via Amazon Bedrock AgentCore Web Search and Amazon Bedrock', + }); + + const askResource = api.root.addResource('ask'); + askResource.addMethod('POST', new apigateway.LambdaIntegration(fn)); + + // Outputs + new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url }); + new cdk.CfnOutput(this, 'FunctionName', { value: fn.functionName }); + } +} diff --git a/apigw-lambda-websearch-bedrock-cdk/package.json b/apigw-lambda-websearch-bedrock-cdk/package.json new file mode 100644 index 0000000000..53fb67ef5e --- /dev/null +++ b/apigw-lambda-websearch-bedrock-cdk/package.json @@ -0,0 +1,8 @@ +{ + "name": "apigw-lambda-websearch-bedrock-cdk", + "version": "1.0.0", + "bin": { "app": "build/bin/app.js" }, + "scripts": { "build": "tsc", "synth": "cdk synth" }, + "dependencies": { "aws-cdk-lib": "^2.260.0", "constructs": "^10.3.0" }, + "devDependencies": { "aws-cdk": "^2.1128.0", "typescript": "~5.4.0" } +} diff --git a/apigw-lambda-websearch-bedrock-cdk/src/handler/index.py b/apigw-lambda-websearch-bedrock-cdk/src/handler/index.py new file mode 100644 index 0000000000..6e1209b718 --- /dev/null +++ b/apigw-lambda-websearch-bedrock-cdk/src/handler/index.py @@ -0,0 +1,143 @@ +""" +AWS Lambda function that grounds AI responses in live web data. +Orchestrates Amazon Bedrock AgentCore Web Search for facts, then +Amazon Bedrock for inference to produce cited, accurate answers. +""" + +import json +import os +import urllib.request +import boto3 + + +def search_web(gateway_url, region, query, max_results=5): + """Search the web via Amazon Bedrock AgentCore Gateway MCP protocol.""" + try: + import botocore.session + from botocore.auth import SigV4Auth + from botocore.awsrequest import AWSRequest + + session = botocore.session.get_session() + credentials = session.get_credentials().get_frozen_credentials() + + mcp_request = { + 'jsonrpc': '2.0', + 'id': '1', + 'method': 'tools/call', + 'params': { + 'name': 'web-search___WebSearch', + 'arguments': {'query': query[:200], 'maxResults': max_results}, + }, + } + + payload = json.dumps(mcp_request).encode('utf-8') + request = AWSRequest( + method='POST', url=gateway_url, data=payload, + headers={'Content-Type': 'application/json', 'Accept': 'application/json'}, + ) + SigV4Auth(credentials, 'bedrock-agentcore', region).add_auth(request) + + req = urllib.request.Request( + gateway_url, data=payload, headers=dict(request.headers), method='POST', + ) + with urllib.request.urlopen(req, timeout=15) as resp: + result = json.loads(resp.read().decode('utf-8')) + + # Extract search results from MCP response + content = result.get('result', {}).get('content', []) + if content: + return json.loads(content[0].get('text', '{}')) + return {} + except Exception as e: + return {'error': str(e)} + + +def invoke_bedrock(model_id, region, question, search_results): + """Invoke Amazon Bedrock to synthesize a grounded answer with citations.""" + try: + client = boto3.client('bedrock-runtime', region_name=region) + + # Format search results as context + sources = search_results.get('results', []) + context_parts = [] + for i, s in enumerate(sources[:5], 1): + context_parts.append( + f"[{i}] {s.get('title', 'Untitled')} ({s.get('url', '')})\n{s.get('text', '')[:500]}" + ) + context = '\n\n'.join(context_parts) + + prompt = f"""Answer the following question using ONLY the provided web search results. +Include citation numbers [1], [2], etc. for each fact you reference. +If the search results don't contain enough information, say so. + +Question: {question} + +Web Search Results: +{context} + +Answer (with citations):""" + + response = client.invoke_model( + modelId=model_id, + contentType='application/json', + accept='application/json', + body=json.dumps({ + 'anthropic_version': 'bedrock-2023-05-31', + 'max_tokens': 1024, + 'messages': [{'role': 'user', 'content': prompt}], + }), + ) + + body = json.loads(response['body'].read()) + answer = body['content'][0]['text'] + + return { + 'answer': answer, + 'sources': [{'title': s.get('title'), 'url': s.get('url')} for s in sources[:5]], + } + except Exception as e: + return {'error': str(e)} + + +def handler(event, context): + """Orchestrate web search + Bedrock inference for grounded AI answers.""" + try: + # Parse input from Amazon API Gateway + body = json.loads(event.get('body', '{}')) if isinstance(event.get('body'), str) else event + question = body.get('question', '') + + if not question: + return { + 'statusCode': 400, + 'headers': {'Content-Type': 'application/json'}, + 'body': json.dumps({'error': 'question field is required'}), + } + + gateway_url = os.environ['GATEWAY_URL'] + region = os.environ.get('AWS_REGION', 'us-east-1') + model_id = os.environ.get('MODEL_ID', 'us.anthropic.claude-sonnet-4-20250514-v1:0') + + # Step 1: Search the web for relevant facts + search_results = search_web(gateway_url, region, question) + if 'error' in search_results: + return { + 'statusCode': 502, + 'headers': {'Content-Type': 'application/json'}, + 'body': json.dumps({'error': f'Web search failed: {search_results["error"]}'}), + } + + # Step 2: Feed results to Amazon Bedrock for grounded inference + grounded_response = invoke_bedrock(model_id, region, question, search_results) + + return { + 'statusCode': 200, + 'headers': {'Content-Type': 'application/json'}, + 'body': json.dumps(grounded_response), + } + + except Exception as e: + return { + 'statusCode': 500, + 'headers': {'Content-Type': 'application/json'}, + 'body': json.dumps({'error': str(e)}), + } diff --git a/apigw-lambda-websearch-bedrock-cdk/tsconfig.json b/apigw-lambda-websearch-bedrock-cdk/tsconfig.json new file mode 100644 index 0000000000..2a8c474850 --- /dev/null +++ b/apigw-lambda-websearch-bedrock-cdk/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", "module": "commonjs", "lib": ["es2022"], + "declaration": true, "strict": true, "noImplicitAny": true, + "noImplicitReturns": true, "inlineSourceMap": true, "inlineSources": true, + "experimentalDecorators": true, "strictPropertyInitialization": false, + "outDir": "./build", "rootDir": ".", "skipLibCheck": true + }, + "exclude": ["node_modules", "cdk.out", "build"] +}