diff --git a/typescript/apigw-streaming-lambda-bedrock/README.md b/typescript/apigw-streaming-lambda-bedrock/README.md new file mode 100644 index 000000000..65ab74469 --- /dev/null +++ b/typescript/apigw-streaming-lambda-bedrock/README.md @@ -0,0 +1,48 @@ +# API Gateway REST Streaming with Lambda and Amazon Bedrock + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is an experimental example. It may not build out of the box** +> +> This example uses API Gateway REST response streaming (re:Invent 2025 launch) with `ResponseTransferMode: STREAM`. + +--- + + +This example creates an API Gateway REST API that streams responses from Amazon Bedrock (Claude) via Lambda response streaming, delivering tokens to the client as they are generated. + +## Architecture + +``` +Client → API Gateway (REST, streaming) → Lambda (streamifyResponse) → Bedrock (InvokeModelWithResponseStream) +``` + +## Build + +```bash +npm install +npm run build +``` + +## Deploy + +```bash +npx cdk deploy +``` + +## Test + +```bash +curl -N -X POST \ + -H "Content-Type: application/json" \ + -d '{"prompt":"Explain serverless in 3 sentences"}' +``` + +## Cleanup + +```bash +npx cdk destroy +``` diff --git a/typescript/apigw-streaming-lambda-bedrock/cdk.json b/typescript/apigw-streaming-lambda-bedrock/cdk.json new file mode 100644 index 000000000..bc47f5984 --- /dev/null +++ b/typescript/apigw-streaming-lambda-bedrock/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node index.ts" +} diff --git a/typescript/apigw-streaming-lambda-bedrock/index.ts b/typescript/apigw-streaming-lambda-bedrock/index.ts new file mode 100644 index 000000000..08a62f929 --- /dev/null +++ b/typescript/apigw-streaming-lambda-bedrock/index.ts @@ -0,0 +1,65 @@ +import * as cdk from "aws-cdk-lib"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as apigateway from "aws-cdk-lib/aws-apigateway"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as logs from "aws-cdk-lib/aws-logs"; +import { Construct } from "constructs"; + +export class ApigwStreamingLambdaBedrockStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const modelId = new cdk.CfnParameter(this, "BedrockModelId", { + type: "String", + default: "us.anthropic.claude-sonnet-4-20250514-v1:0", + description: "Bedrock inference profile model ID", + }); + + const fn = new lambda.Function(this, "StreamingBedrockFn", { + runtime: lambda.Runtime.NODEJS_20_X, + handler: "index.handler", + code: lambda.Code.fromAsset("src"), + timeout: cdk.Duration.minutes(5), + memorySize: 256, + environment: { MODEL_ID: modelId.valueAsString }, + logRetention: logs.RetentionDays.ONE_WEEK, + }); + + fn.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["bedrock:InvokeModelWithResponseStream"], + resources: [ + `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${modelId.valueAsString}`, + "arn:aws:bedrock:*::foundation-model/*", + ], + }) + ); + + const api = new apigateway.RestApi(this, "StreamingApi", { + restApiName: "Bedrock Streaming API", + description: "REST API with response streaming to Bedrock", + deployOptions: { stageName: "prod" }, + }); + + const chatResource = api.root.addResource("chat"); + const method = chatResource.addMethod( + "POST", + new apigateway.LambdaIntegration(fn, { timeout: cdk.Duration.minutes(5) }) + ); + + // Override to use streaming invocation path + const cfnMethod = method.node.defaultChild as apigateway.CfnMethod; + cfnMethod.addPropertyOverride( + "Integration.Uri", + `arn:aws:apigateway:${this.region}:lambda:path/2021-11-15/functions/${fn.functionArn}/response-streaming-invocations` + ); + cfnMethod.addPropertyOverride("Integration.ResponseTransferMode", "STREAM"); + cfnMethod.addPropertyOverride("Integration.TimeoutInMillis", 300000); + + new cdk.CfnOutput(this, "ApiEndpoint", { value: api.urlForPath("/chat") }); + new cdk.CfnOutput(this, "FunctionName", { value: fn.functionName }); + } +} + +const app = new cdk.App(); +new ApigwStreamingLambdaBedrockStack(app, "ApigwStreamingLambdaBedrockStack"); diff --git a/typescript/apigw-streaming-lambda-bedrock/package.json b/typescript/apigw-streaming-lambda-bedrock/package.json new file mode 100644 index 000000000..9ff0dd91f --- /dev/null +++ b/typescript/apigw-streaming-lambda-bedrock/package.json @@ -0,0 +1,11 @@ +{ + "name": "apigw-streaming-lambda-bedrock", + "version": "1.0.0", + "description": "API Gateway REST streaming with Lambda and Amazon Bedrock", + "private": true, + "scripts": { "build": "tsc", "watch": "tsc -w", "cdk": "cdk" }, + "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com", "organization": true }, + "license": "Apache-2.0", + "devDependencies": { "@types/node": "^22.0.0", "aws-cdk": "2.1119.0", "typescript": "~5.7.0" }, + "dependencies": { "aws-cdk-lib": "2.250.0", "constructs": "^10.0.0" } +} diff --git a/typescript/apigw-streaming-lambda-bedrock/src/index.js b/typescript/apigw-streaming-lambda-bedrock/src/index.js new file mode 100644 index 000000000..f29fc7d32 --- /dev/null +++ b/typescript/apigw-streaming-lambda-bedrock/src/index.js @@ -0,0 +1,42 @@ +const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require("@aws-sdk/client-bedrock-runtime"); + +const client = new BedrockRuntimeClient(); +const MODEL_ID = process.env.MODEL_ID; + +exports.handler = awslambda.streamifyResponse(async (event, responseStream, _context) => { + const body = JSON.parse(event.body || "{}"); + const prompt = body.prompt || "Hello"; + + responseStream = awslambda.HttpResponseStream.from(responseStream, { + statusCode: 200, + headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }, + }); + + try { + const response = await client.send( + new InvokeModelWithResponseStreamCommand({ + modelId: MODEL_ID, + contentType: "application/json", + accept: "application/json", + body: JSON.stringify({ + anthropic_version: "bedrock-2023-05-31", + max_tokens: 2048, + messages: [{ role: "user", content: prompt }], + }), + }) + ); + + for await (const event of response.body) { + if (event.chunk) { + const parsed = JSON.parse(new TextDecoder().decode(event.chunk.bytes)); + if (parsed.type === "content_block_delta" && parsed.delta?.text) { + responseStream.write(`data: ${JSON.stringify({ text: parsed.delta.text })}\n\n`); + } + } + } + responseStream.write("data: [DONE]\n\n"); + } catch (err) { + responseStream.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); + } + responseStream.end(); +}); diff --git a/typescript/apigw-streaming-lambda-bedrock/tsconfig.json b/typescript/apigw-streaming-lambda-bedrock/tsconfig.json new file mode 100644 index 000000000..5a9a7d6dd --- /dev/null +++ b/typescript/apigw-streaming-lambda-bedrock/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false + }, + "exclude": ["cdk.out"] +} diff --git a/typescript/lambda-durable-bedrock/README.md b/typescript/lambda-durable-bedrock/README.md new file mode 100644 index 000000000..8e0ee500a --- /dev/null +++ b/typescript/lambda-durable-bedrock/README.md @@ -0,0 +1,62 @@ +# Lambda Durable Functions with Amazon Bedrock + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is an experimental example. It may not build out of the box** +> +> This example uses AWS Lambda Durable Functions (re:Invent 2025 launch) which requires `nodejs24.x` runtime and the `@aws/durable-execution-sdk-js` SDK. + +--- + + +This example demonstrates AWS Lambda Durable Functions integrated with Amazon Bedrock to build a multi-step AI content generation workflow with automatic checkpointing. + +The Lambda function uses durable execution to: +1. Generate a blog outline using Bedrock (Claude) +2. Wait 5 seconds (simulating editorial review) +3. Expand the outline into a draft +4. Generate a summary + +Each step is automatically checkpointed — if the function fails mid-execution, it resumes from the last completed step rather than restarting. + +## Architecture + +``` +User → Lambda (Durable) → Step 1: Bedrock (outline) + → Wait (5s) + → Step 2: Bedrock (draft) + → Step 3: Bedrock (summary) + → Return result +``` + +## Build + +```bash +npm install +npm run build +``` + +## Deploy + +```bash +npx cdk deploy +``` + +## Test + +```bash +aws lambda invoke \ + --function-name \ + --qualifier \ + --payload '{"topic":"AWS Lambda Durable Functions"}' \ + output.json && cat output.json +``` + +## Cleanup + +```bash +npx cdk destroy +``` diff --git a/typescript/lambda-durable-bedrock/cdk.json b/typescript/lambda-durable-bedrock/cdk.json new file mode 100644 index 000000000..bc47f5984 --- /dev/null +++ b/typescript/lambda-durable-bedrock/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node index.ts" +} diff --git a/typescript/lambda-durable-bedrock/index.ts b/typescript/lambda-durable-bedrock/index.ts new file mode 100644 index 000000000..d887f7608 --- /dev/null +++ b/typescript/lambda-durable-bedrock/index.ts @@ -0,0 +1,60 @@ +import * as cdk from "aws-cdk-lib"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { Construct } from "constructs"; + +export class LambdaDurableBedrockStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const modelId = new cdk.CfnParameter(this, "BedrockModelId", { + type: "String", + default: "us.anthropic.claude-sonnet-4-20250514-v1:0", + description: "Bedrock inference profile model ID", + }); + + const fn = new lambda.Function(this, "DurableBedrockFn", { + runtime: lambda.Runtime.NODEJS_20_X, + handler: "index.handler", + code: lambda.Code.fromAsset("src"), + timeout: cdk.Duration.minutes(15), + memorySize: 256, + environment: { MODEL_ID: modelId.valueAsString }, + }); + + // Enable durable execution via escape hatch (no L2 yet) + const cfnFn = fn.node.defaultChild as lambda.CfnFunction; + cfnFn.addOverride("Properties.Runtime", "nodejs24.x"); + cfnFn.addOverride("Properties.DurableConfig", { + ExecutionTimeout: 900, + RetentionPeriodInDays: 14, + }); + + fn.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["bedrock:InvokeModel"], + resources: [ + `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${modelId.valueAsString}`, + "arn:aws:bedrock:*::foundation-model/*", + ], + }) + ); + + fn.role!.addManagedPolicy( + iam.ManagedPolicy.fromAwsManagedPolicyName( + "service-role/AWSLambdaBasicDurableExecutionRolePolicy" + ) + ); + + const cfnVersion = new lambda.CfnVersion(this, "DurableVersion", { + functionName: fn.functionName, + description: "Durable execution version", + }); + + new cdk.CfnOutput(this, "FunctionName", { value: fn.functionName }); + new cdk.CfnOutput(this, "VersionNumber", { value: cfnVersion.attrVersion }); + } +} + +const app = new cdk.App(); +new LambdaDurableBedrockStack(app, "LambdaDurableBedrockStack"); diff --git a/typescript/lambda-durable-bedrock/package.json b/typescript/lambda-durable-bedrock/package.json new file mode 100644 index 000000000..eb8eb2953 --- /dev/null +++ b/typescript/lambda-durable-bedrock/package.json @@ -0,0 +1,26 @@ +{ + "name": "lambda-durable-bedrock", + "version": "1.0.0", + "description": "Lambda Durable Functions with Amazon Bedrock for multi-step AI workflows", + "private": true, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.0.0", + "aws-cdk": "2.1119.0", + "typescript": "~5.7.0" + }, + "dependencies": { + "aws-cdk-lib": "2.250.0", + "constructs": "^10.0.0" + } +} diff --git a/typescript/lambda-durable-bedrock/src/index.js b/typescript/lambda-durable-bedrock/src/index.js new file mode 100644 index 000000000..7854456d7 --- /dev/null +++ b/typescript/lambda-durable-bedrock/src/index.js @@ -0,0 +1,44 @@ +const { withDurableExecution } = require("@aws/durable-execution-sdk-js"); +const { BedrockRuntimeClient, InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime"); + +const client = new BedrockRuntimeClient(); +const MODEL_ID = process.env.MODEL_ID; + +async function callBedrock(prompt) { + const res = await client.send( + new InvokeModelCommand({ + modelId: MODEL_ID, + contentType: "application/json", + accept: "application/json", + body: JSON.stringify({ + anthropic_version: "bedrock-2023-05-31", + max_tokens: 1024, + messages: [{ role: "user", content: prompt }], + }), + }) + ); + return JSON.parse(new TextDecoder().decode(res.body)).content[0].text; +} + +exports.handler = withDurableExecution(async (event, context) => { + const topic = event.topic || "Serverless computing"; + + const outline = await context.step(async (stepCtx) => { + stepCtx.logger.info(`Generating outline for: ${topic}`); + return callBedrock(`Create a concise blog post outline (5 sections max) about: ${topic}. Return only the outline.`); + }); + + await context.wait({ seconds: 5 }); + + const draft = await context.step(async (stepCtx) => { + stepCtx.logger.info("Expanding outline into draft"); + return callBedrock(`Expand this outline into a short blog draft (under 500 words):\n\n${outline}`); + }); + + const summary = await context.step(async (stepCtx) => { + stepCtx.logger.info("Generating summary"); + return callBedrock(`Summarize this blog post in 2-3 sentences:\n\n${draft}`); + }); + + return { topic, outline, draft, summary }; +}); diff --git a/typescript/lambda-durable-bedrock/tsconfig.json b/typescript/lambda-durable-bedrock/tsconfig.json new file mode 100644 index 000000000..5a9a7d6dd --- /dev/null +++ b/typescript/lambda-durable-bedrock/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false + }, + "exclude": ["cdk.out"] +} diff --git a/typescript/s3-vectors-lambda-bedrock/README.md b/typescript/s3-vectors-lambda-bedrock/README.md new file mode 100644 index 000000000..d9460cb2f --- /dev/null +++ b/typescript/s3-vectors-lambda-bedrock/README.md @@ -0,0 +1,55 @@ +# S3 Vectors RAG Pipeline with Lambda and Amazon Bedrock + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is an experimental example. It may not build out of the box** +> +> This example uses Amazon S3 Vectors (re:Invent 2025 GA) which requires `@aws-sdk/client-s3vectors` SDK. + +--- + + +This example builds a Retrieval Augmented Generation (RAG) pipeline using Amazon S3 Vectors for vector storage, Amazon Titan Embeddings for vectorization, and Claude on Amazon Bedrock for answer generation. + +## Architecture + +``` +Ingest: Documents → Lambda → Titan Embeddings → S3 Vectors (store) +Query: Question → Lambda → Titan Embeddings → S3 Vectors (search) → Claude (generate answer) +``` + +## Build + +```bash +npm install +npm run build +``` + +## Deploy + +```bash +npx cdk deploy +``` + +## Test + +```bash +# Ingest documents +aws lambda invoke --function-name \ + --payload '{"documents":[{"key":"doc1","text":"Lambda Durable Functions checkpoint state automatically."},{"key":"doc2","text":"S3 Vectors stores embeddings at 90% lower cost than specialized databases."}]}' \ + output.json && cat output.json + +# Query +aws lambda invoke --function-name \ + --payload '{"question":"How do durable functions handle state?"}' \ + output.json && cat output.json +``` + +## Cleanup + +```bash +npx cdk destroy +``` diff --git a/typescript/s3-vectors-lambda-bedrock/cdk.json b/typescript/s3-vectors-lambda-bedrock/cdk.json new file mode 100644 index 000000000..bc47f5984 --- /dev/null +++ b/typescript/s3-vectors-lambda-bedrock/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node index.ts" +} diff --git a/typescript/s3-vectors-lambda-bedrock/index.ts b/typescript/s3-vectors-lambda-bedrock/index.ts new file mode 100644 index 000000000..9c7e984da --- /dev/null +++ b/typescript/s3-vectors-lambda-bedrock/index.ts @@ -0,0 +1,123 @@ +import * as cdk from "aws-cdk-lib"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as logs from "aws-cdk-lib/aws-logs"; +import * as cr from "aws-cdk-lib/custom-resources"; +import { Construct } from "constructs"; + +export class S3VectorsLambdaBedrockStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vectorBucketName = new cdk.CfnParameter(this, "VectorBucketName", { + type: "String", + default: "rag-knowledge-base-vectors", + description: "Name for the S3 vector bucket", + }); + + const indexName = "knowledge-base"; + + const s3VectorsPolicy = new iam.PolicyStatement({ + actions: [ + "s3vectors:CreateVectorBucket", "s3vectors:DeleteVectorBucket", + "s3vectors:CreateVectorIndex", "s3vectors:DeleteVectorIndex", + "s3vectors:PutVectors", "s3vectors:QueryVectors", + "s3vectors:GetVectors", "s3vectors:DeleteVectors", + ], + resources: ["*"], // s3vectors does not support resource-level ARNs yet + }); + + const bedrockPolicy = new iam.PolicyStatement({ + actions: ["bedrock:InvokeModel"], + resources: [ + `arn:aws:bedrock:${this.region}::foundation-model/amazon.titan-embed-text-v2:0`, + `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/us.anthropic.claude-sonnet-4-20250514-v1:0`, + "arn:aws:bedrock:*::foundation-model/*", + ], + }); + + const sharedEnv = { + VECTOR_BUCKET_NAME: vectorBucketName.valueAsString, + INDEX_NAME: indexName, + EMBED_MODEL_ID: "amazon.titan-embed-text-v2:0", + GENERATION_MODEL_ID: "us.anthropic.claude-sonnet-4-20250514-v1:0", + }; + + const ingestFn = new lambda.Function(this, "IngestFn", { + runtime: lambda.Runtime.NODEJS_20_X, + handler: "ingest.handler", + code: lambda.Code.fromAsset("src"), + timeout: cdk.Duration.minutes(5), + memorySize: 256, + environment: sharedEnv, + logRetention: logs.RetentionDays.ONE_WEEK, + }); + ingestFn.addToRolePolicy(s3VectorsPolicy); + ingestFn.addToRolePolicy(bedrockPolicy); + + const queryFn = new lambda.Function(this, "QueryFn", { + runtime: lambda.Runtime.NODEJS_20_X, + handler: "query.handler", + code: lambda.Code.fromAsset("src"), + timeout: cdk.Duration.minutes(2), + memorySize: 256, + environment: sharedEnv, + logRetention: logs.RetentionDays.ONE_WEEK, + }); + queryFn.addToRolePolicy(s3VectorsPolicy); + queryFn.addToRolePolicy(bedrockPolicy); + + // Custom resource to create vector bucket and index on deploy + const setupRole = new iam.Role(this, "SetupRole", { + assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"), + ], + }); + setupRole.addToPolicy(s3VectorsPolicy); + + const createBucket = new cr.AwsCustomResource(this, "CreateVectorBucket", { + onCreate: { + service: "S3Vectors", + action: "createVectorBucket", + parameters: { vectorBucketName: vectorBucketName.valueAsString }, + physicalResourceId: cr.PhysicalResourceId.of("vector-bucket"), + }, + onDelete: { + service: "S3Vectors", + action: "deleteVectorBucket", + parameters: { vectorBucketName: vectorBucketName.valueAsString }, + }, + role: setupRole, + policy: cr.AwsCustomResourcePolicy.fromStatements([s3VectorsPolicy]), + }); + + const createIndex = new cr.AwsCustomResource(this, "CreateVectorIndex", { + onCreate: { + service: "S3Vectors", + action: "createVectorIndex", + parameters: { + vectorBucketName: vectorBucketName.valueAsString, + indexName, + dimension: 1024, + distanceMetric: "cosine", + }, + physicalResourceId: cr.PhysicalResourceId.of("vector-index"), + }, + onDelete: { + service: "S3Vectors", + action: "deleteVectorIndex", + parameters: { vectorBucketName: vectorBucketName.valueAsString, indexName }, + }, + role: setupRole, + policy: cr.AwsCustomResourcePolicy.fromStatements([s3VectorsPolicy]), + }); + createIndex.node.addDependency(createBucket); + + new cdk.CfnOutput(this, "IngestFunctionName", { value: ingestFn.functionName }); + new cdk.CfnOutput(this, "QueryFunctionName", { value: queryFn.functionName }); + } +} + +const app = new cdk.App(); +new S3VectorsLambdaBedrockStack(app, "S3VectorsLambdaBedrockStack"); diff --git a/typescript/s3-vectors-lambda-bedrock/package.json b/typescript/s3-vectors-lambda-bedrock/package.json new file mode 100644 index 000000000..b81fb73cf --- /dev/null +++ b/typescript/s3-vectors-lambda-bedrock/package.json @@ -0,0 +1,11 @@ +{ + "name": "s3-vectors-lambda-bedrock", + "version": "1.0.0", + "description": "S3 Vectors RAG pipeline with Lambda and Amazon Bedrock", + "private": true, + "scripts": { "build": "tsc", "watch": "tsc -w", "cdk": "cdk" }, + "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com", "organization": true }, + "license": "Apache-2.0", + "devDependencies": { "@types/node": "^22.0.0", "aws-cdk": "2.1119.0", "typescript": "~5.7.0" }, + "dependencies": { "aws-cdk-lib": "2.250.0", "constructs": "^10.0.0" } +} diff --git a/typescript/s3-vectors-lambda-bedrock/src/ingest.js b/typescript/s3-vectors-lambda-bedrock/src/ingest.js new file mode 100644 index 000000000..ceaf49a93 --- /dev/null +++ b/typescript/s3-vectors-lambda-bedrock/src/ingest.js @@ -0,0 +1,55 @@ +const { S3VectorsClient, PutVectorsCommand } = require("@aws-sdk/client-s3vectors"); +const { BedrockRuntimeClient, InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime"); + +const s3v = new S3VectorsClient(); +const bedrock = new BedrockRuntimeClient(); + +const VECTOR_BUCKET = process.env.VECTOR_BUCKET_NAME; +const INDEX_NAME = process.env.INDEX_NAME; +const EMBED_MODEL = process.env.EMBED_MODEL_ID; + +async function embed(text) { + const res = await bedrock.send( + new InvokeModelCommand({ + modelId: EMBED_MODEL, + contentType: "application/json", + accept: "application/json", + body: JSON.stringify({ inputText: text }), + }) + ); + return JSON.parse(new TextDecoder().decode(res.body)).embedding; +} + +exports.handler = async (event) => { + const documents = event.documents || []; + if (!documents.length) return { statusCode: 400, body: "No documents provided" }; + + // Generate embeddings for all documents + const vectors = []; + for (const doc of documents) { + const embedding = await embed(doc.text); + vectors.push({ + key: doc.key, + data: { float32: embedding }, + metadata: { + source_text: doc.text, + ingested_at: new Date().toISOString(), + ...(doc.metadata || {}), + }, + }); + } + + // Batch put into S3 Vectors + await s3v.send( + new PutVectorsCommand({ + vectorBucketName: VECTOR_BUCKET, + indexName: INDEX_NAME, + vectors, + }) + ); + + return { + statusCode: 200, + body: JSON.stringify({ ingested: vectors.length }), + }; +}; diff --git a/typescript/s3-vectors-lambda-bedrock/src/query.js b/typescript/s3-vectors-lambda-bedrock/src/query.js new file mode 100644 index 000000000..40e2501e1 --- /dev/null +++ b/typescript/s3-vectors-lambda-bedrock/src/query.js @@ -0,0 +1,80 @@ +const { S3VectorsClient, QueryVectorsCommand } = require("@aws-sdk/client-s3vectors"); +const { BedrockRuntimeClient, InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime"); + +const s3v = new S3VectorsClient(); +const bedrock = new BedrockRuntimeClient(); + +const VECTOR_BUCKET = process.env.VECTOR_BUCKET_NAME; +const INDEX_NAME = process.env.INDEX_NAME; +const EMBED_MODEL = process.env.EMBED_MODEL_ID; +const GEN_MODEL = process.env.GENERATION_MODEL_ID; + +async function embed(text) { + const res = await bedrock.send( + new InvokeModelCommand({ + modelId: EMBED_MODEL, + contentType: "application/json", + accept: "application/json", + body: JSON.stringify({ inputText: text }), + }) + ); + return JSON.parse(new TextDecoder().decode(res.body)).embedding; +} + +exports.handler = async (event) => { + const question = event.question; + if (!question) return { statusCode: 400, body: "No question provided" }; + + // Embed the question + const queryVector = await embed(question); + + // Search S3 Vectors for similar documents + const searchResult = await s3v.send( + new QueryVectorsCommand({ + vectorBucketName: VECTOR_BUCKET, + indexName: INDEX_NAME, + queryVector: { float32: queryVector }, + topK: 3, + returnMetadata: true, + returnDistance: true, + }) + ); + + const context = (searchResult.vectors || []) + .map((v) => v.metadata?.source_text || "") + .filter(Boolean) + .join("\n\n"); + + // Generate answer using retrieved context + const genRes = await bedrock.send( + new InvokeModelCommand({ + modelId: GEN_MODEL, + contentType: "application/json", + accept: "application/json", + body: JSON.stringify({ + anthropic_version: "bedrock-2023-05-31", + max_tokens: 1024, + messages: [ + { + role: "user", + content: `Answer the question based on the context below. If the context doesn't contain the answer, say so.\n\nContext:\n${context}\n\nQuestion: ${question}`, + }, + ], + }), + }) + ); + + const answer = JSON.parse(new TextDecoder().decode(genRes.body)).content[0].text; + + return { + statusCode: 200, + body: JSON.stringify({ + question, + answer, + sources: (searchResult.vectors || []).map((v) => ({ + key: v.key, + distance: v.distance, + })), + }), + }; +}; diff --git a/typescript/s3-vectors-lambda-bedrock/tsconfig.json b/typescript/s3-vectors-lambda-bedrock/tsconfig.json new file mode 100644 index 000000000..5a9a7d6dd --- /dev/null +++ b/typescript/s3-vectors-lambda-bedrock/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false + }, + "exclude": ["cdk.out"] +}