From 15a2a816413c5b54d7cc64c0f887ce54a8582d42 Mon Sep 17 00:00:00 2001 From: mh Date: Sat, 27 Sep 2025 21:40:54 -0300 Subject: [PATCH] feat: lambda --- examples/with-aws-lambda/README.md | 261 +++++++++++ examples/with-aws-lambda/cdk.json | 63 +++ examples/with-aws-lambda/cdk/app.ts | 12 + .../with-aws-lambda/cdk/voltagent-stack.ts | 72 +++ examples/with-aws-lambda/example.env | 12 + examples/with-aws-lambda/package.json | 51 +++ examples/with-aws-lambda/samconfig.toml | 34 ++ examples/with-aws-lambda/serverless.yml | 55 +++ examples/with-aws-lambda/src/index.ts | 37 ++ examples/with-aws-lambda/src/lambda.ts | 6 + examples/with-aws-lambda/src/tools/index.ts | 29 ++ examples/with-aws-lambda/template.yaml | 62 +++ examples/with-aws-lambda/tsconfig.json | 17 + packages/serverless-hono/src/aws-lambda.ts | 134 ++++++ packages/serverless-hono/src/index.ts | 7 + website/docs/deployment/aws-lambda.md | 427 ++++++++++++++++++ website/docs/deployment/overview.md | 3 +- 17 files changed, 1281 insertions(+), 1 deletion(-) create mode 100644 examples/with-aws-lambda/README.md create mode 100644 examples/with-aws-lambda/cdk.json create mode 100644 examples/with-aws-lambda/cdk/app.ts create mode 100644 examples/with-aws-lambda/cdk/voltagent-stack.ts create mode 100644 examples/with-aws-lambda/example.env create mode 100644 examples/with-aws-lambda/package.json create mode 100644 examples/with-aws-lambda/samconfig.toml create mode 100644 examples/with-aws-lambda/serverless.yml create mode 100644 examples/with-aws-lambda/src/index.ts create mode 100644 examples/with-aws-lambda/src/lambda.ts create mode 100644 examples/with-aws-lambda/src/tools/index.ts create mode 100644 examples/with-aws-lambda/template.yaml create mode 100644 examples/with-aws-lambda/tsconfig.json create mode 100644 packages/serverless-hono/src/aws-lambda.ts create mode 100644 website/docs/deployment/aws-lambda.md diff --git a/examples/with-aws-lambda/README.md b/examples/with-aws-lambda/README.md new file mode 100644 index 000000000..3ee86ce42 --- /dev/null +++ b/examples/with-aws-lambda/README.md @@ -0,0 +1,261 @@ +# VoltAgent AWS Lambda Deployment Example + +This example shows how to deploy a VoltAgent agent to AWS Lambda using Amazon Bedrock with three different deployment tools: AWS SAM, AWS CDK, and Serverless Framework. + +## Features + +- ⚙️ Runs on AWS Lambda with Node.js 20 runtime +- 🤖 Uses Amazon Bedrock for AI model inference +- 🧰 Includes a sample weather tool +- 🛠️ Three deployment options: SAM, CDK, and Serverless Framework +- 🔗 Full VoltAgent HTTP API available through API Gateway +- 🔐 Automatic IAM permissions for Bedrock access + +## Prerequisites + +- Node.js 18+ +- AWS CLI configured with appropriate permissions +- Access to Amazon Bedrock models in your AWS account +- VoltOps keys (optional for observability) +- Choose one deployment tool: + - **AWS SAM**: Install [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + - **AWS CDK**: Install with `npm install -g aws-cdk` + - **Serverless Framework**: Install with `npm install -g serverless` + +## Setup + +1. Install dependencies: + + ```bash + npm install + ``` + +2. Copy the example environment file and add your values: + + ```bash + cp example.env .env + ``` + + Note: For Lambda deployment, AWS credentials are provided automatically via IAM roles. The `.env` file is only needed for local development. + +3. Build the TypeScript code: + + ```bash + npm run build + ``` + +## Deployment Options + +### Option 1: AWS SAM + +AWS SAM provides a simple way to deploy serverless applications with CloudFormation. + +#### Deploy with SAM + +```bash +# Build and deploy +npm run deploy:sam + +# Or step by step: +npm run build:sam +sam deploy --guided +``` + +The `--guided` flag walks you through the deployment configuration on first run. + +#### Local development with SAM + +```bash +# Start local API +npm run dev:sam + +# Test locally +curl http://localhost:3000/agents +``` + +### Option 2: AWS CDK + +AWS CDK provides infrastructure as code with TypeScript. + +#### Deploy with CDK + +```bash +# Bootstrap CDK (first time only) +cdk bootstrap + +# Deploy +npm run deploy:cdk + +# Optional: include VoltOps keys +npm run deploy:cdk -- \ + --context voltAgentPublicKey=pk-your-key-here \ + --context voltAgentSecretKey=sk-your-key-here +``` + +#### CDK development + +```bash +# Synthesize CloudFormation template +npm run build:cdk + +# View differences +cdk diff + +# Destroy the stack +cdk destroy +``` + +### Option 3: Serverless Framework + +Serverless Framework provides a developer-friendly deployment experience. + +#### Deploy with Serverless + +```bash +# Deploy +npm run deploy:serverless + +# Optional: include VoltOps keys +npm run deploy:serverless -- \ + --param="voltAgentPublicKey=pk-your-key-here" \ + --param="voltAgentSecretKey=sk-your-key-here" +``` + +#### Local development with Serverless + +```bash +npm run dev:serverless + +# Test locally +curl http://localhost:3000/agents +``` + +## Testing Your Deployment + +After deployment, each tool will output an API Gateway URL. Test your VoltAgent API: + +```bash +# Health check +curl https://your-api-gateway-url/ + +# List agents +curl https://your-api-gateway-url/agents + +# Invoke the assistant +curl -X POST https://your-api-gateway-url/agents/lambda-assistant/invoke \ + -H "Content-Type: application/json" \ + -d '{ + "messages": [{"role": "user", "content": "What is the weather in Berlin?"}] + }' + +# Check observability +curl https://your-api-gateway-url/observability/traces +``` + +## Project Structure + +``` +with-aws-lambda/ +├── src/ +│ ├── index.ts # VoltAgent configuration +│ ├── lambda.ts # AWS Lambda handler +│ └── tools/index.ts # Sample weather tool +├── cdk/ +│ ├── app.ts # CDK app entry point +│ └── voltagent-stack.ts # CDK stack definition +├── template.yaml # AWS SAM template +├── samconfig.toml # SAM configuration +├── serverless.yml # Serverless Framework config +├── cdk.json # CDK configuration +├── package.json +├── tsconfig.json +└── example.env +``` + +## Configuration Notes + +### Environment Variables + +All deployment methods support these environment variables: + +- `AWS_REGION` (automatic): Set by Lambda runtime, defaults to us-east-1 in code +- `VOLTAGENT_PUBLIC_KEY` (optional): VoltOps public key for observability +- `VOLTAGENT_SECRET_KEY` (optional): VoltOps secret key for observability + +### Amazon Bedrock Setup + +Before deploying, ensure you have: + +1. **Model Access**: Enable access to desired models in the Amazon Bedrock console +2. **Region Support**: Deploy in a region where Bedrock is available (us-east-1, us-west-2, etc.) +3. **IAM Permissions**: The deployment configurations automatically include necessary `bedrock:InvokeModel` permissions + +### Memory and Timeout + +Default Lambda configuration: +- **Memory**: 512 MB +- **Timeout**: 30 seconds +- **Runtime**: Node.js 20 + +Adjust these in your deployment configuration files as needed. + +## Cleanup + +To remove your deployment: + +```bash +# AWS SAM +sam delete + +# AWS CDK +cdk destroy + +# Serverless Framework +serverless remove +``` + +## Observability + +- In-memory span/log storage is active by default +- Observability endpoints available at `/observability/*` +- VoltOps integration uses HTTP polling (no WebSocket streaming on Lambda) +- View logs in AWS CloudWatch + +## Limitations + +- **MCP client/server**: Not available on Lambda (requires Node.js stdio/network APIs) +- **libSQL memory adapter**: Not supported (use InMemoryStorageAdapter or external databases) +- **WebSocket streaming**: Not available (VoltOps Console uses HTTP polling) + +## Troubleshooting + +### Build Issues + +```bash +# Clean and rebuild +rm -rf dist node_modules +npm install +npm run build +``` + +### Deployment Issues + +- Ensure AWS CLI is configured: `aws configure list` +- Check IAM permissions for Lambda, API Gateway, and CloudFormation +- Verify your API keys are correct + +### Runtime Issues + +- Check CloudWatch logs: `aws logs tail /aws/lambda/your-function-name --follow` +- Increase memory if you see timeout errors +- Verify Bedrock model access is enabled in your AWS account region +- Ensure IAM permissions include `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream` + +## Next Steps + +- Explore [VoltAgent documentation](https://docs.volt.ag) for advanced features +- Try different Amazon Bedrock models (Claude 3.5, Llama 3.2, Mistral, etc.) +- Add custom tools and memory adapters +- Configure external databases for persistent storage +- Set up CI/CD pipelines for automated deployments +- Integrate with other AWS services (S3, DynamoDB, etc.) \ No newline at end of file diff --git a/examples/with-aws-lambda/cdk.json b/examples/with-aws-lambda/cdk.json new file mode 100644 index 000000000..bb841a60e --- /dev/null +++ b/examples/with-aws-lambda/cdk.json @@ -0,0 +1,63 @@ +{ + "app": "npx ts-node --prefer-ts-exts cdk/app.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": false, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableLoggingIncludeExecutionData": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableLogging": true, + "@aws-cdk/aws-lambda:useNodeJs18Runtime": true, + "@aws-cdk/aws-s3:enforceSSL": true, + "@aws-cdk/aws-iot:mqttRetainAsReceived": true, + "@aws-cdk/aws-kinesisfirehose:useExplicitBucketGrantViaIam": true, + "@aws-cdk/aws-stepfunctions:useCfnOutputs": true + } +} \ No newline at end of file diff --git a/examples/with-aws-lambda/cdk/app.ts b/examples/with-aws-lambda/cdk/app.ts new file mode 100644 index 000000000..cdb19ca23 --- /dev/null +++ b/examples/with-aws-lambda/cdk/app.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { VoltAgentStack } from './voltagent-stack'; + +const app = new cdk.App(); + +new VoltAgentStack(app, 'VoltAgentStack', { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); \ No newline at end of file diff --git a/examples/with-aws-lambda/cdk/voltagent-stack.ts b/examples/with-aws-lambda/cdk/voltagent-stack.ts new file mode 100644 index 000000000..c86119c8d --- /dev/null +++ b/examples/with-aws-lambda/cdk/voltagent-stack.ts @@ -0,0 +1,72 @@ +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 { Construct } from 'constructs'; + +export class VoltAgentStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Get environment variables from context + const voltAgentPublicKey = this.node.tryGetContext('voltAgentPublicKey') || ''; + const voltAgentSecretKey = this.node.tryGetContext('voltAgentSecretKey') || ''; + + // Create the Lambda function + const voltAgentFunction = new lambda.Function(this, 'VoltAgentFunction', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'lambda.handler', + code: lambda.Code.fromAsset('dist'), + timeout: cdk.Duration.seconds(30), + memorySize: 512, + environment: { + VOLTAGENT_PUBLIC_KEY: voltAgentPublicKey, + VOLTAGENT_SECRET_KEY: voltAgentSecretKey, + }, + description: 'VoltAgent serverless function powered by Amazon Bedrock', + }); + + // Add Bedrock permissions to the Lambda function + voltAgentFunction.addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + resources: ['*'], + })); + + // Create API Gateway + const api = new apigateway.RestApi(this, 'VoltAgentApi', { + restApiName: 'VoltAgent Service', + description: 'API for VoltAgent Lambda function', + deployOptions: { + stageName: 'prod', + }, + }); + + // Create Lambda integration + const integration = new apigateway.LambdaIntegration(voltAgentFunction, { + requestTemplates: { 'application/json': '{ "statusCode": "200" }' }, + }); + + // Add root route + api.root.addMethod('ANY', integration); + + // Add proxy route for all paths + const proxyResource = api.root.addResource('{proxy+}'); + proxyResource.addMethod('ANY', integration); + + // Output the API URL + new cdk.CfnOutput(this, 'ApiUrl', { + value: api.url, + description: 'VoltAgent API Gateway URL', + }); + + // Output the function ARN + new cdk.CfnOutput(this, 'FunctionArn', { + value: voltAgentFunction.functionArn, + description: 'VoltAgent Lambda Function ARN', + }); + } +} \ No newline at end of file diff --git a/examples/with-aws-lambda/example.env b/examples/with-aws-lambda/example.env new file mode 100644 index 000000000..724efcc5b --- /dev/null +++ b/examples/with-aws-lambda/example.env @@ -0,0 +1,12 @@ +# Copy this file to .env and add your actual values + +# AWS Configuration (optional for Lambda deployment, required for local development) +# Lambda deployment will use IAM roles automatically +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID=your-access-key-id +AWS_SECRET_ACCESS_KEY=your-secret-access-key +AWS_SESSION_TOKEN=your-session-token-if-using-temporary-credentials + +# Optional: VoltOps observability keys +VOLTAGENT_PUBLIC_KEY=pk_your-voltagent-public-key-here +VOLTAGENT_SECRET_KEY=sk_your-voltagent-secret-key-here \ No newline at end of file diff --git a/examples/with-aws-lambda/package.json b/examples/with-aws-lambda/package.json new file mode 100644 index 000000000..383366181 --- /dev/null +++ b/examples/with-aws-lambda/package.json @@ -0,0 +1,51 @@ +{ + "name": "voltagent-example-aws-lambda", + "description": "VoltAgent example deployed to AWS Lambda", + "version": "1.0.0", + "dependencies": { + "@ai-sdk/amazon-bedrock": "^3.0.0", + "@aws-sdk/credential-providers": "~3.799.0", + "@voltagent/core": "^1.1.23", + "@voltagent/serverless-hono": "^1.0.3", + "ai": "^5.0.12", + "hono": "^4.7.7", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.145", + "@types/node": "^24.2.1", + "aws-cdk": "^2.173.4", + "aws-cdk-lib": "^2.173.4", + "constructs": "^10.4.2", + "serverless": "^4.6.7", + "serverless-offline": "^15.2.1", + "typescript": "^5.8.2" + }, + "engines": { + "node": ">=18" + }, + "keywords": [ + "agent", + "ai", + "aws", + "bedrock", + "lambda", + "serverless", + "voltagent" + ], + "license": "MIT", + "private": true, + "scripts": { + "build": "tsc --project tsconfig.json", + "build:sam": "pnpm build && sam build", + "build:cdk": "pnpm build && cdk synth", + "deploy:sam": "pnpm build:sam && sam deploy", + "deploy:cdk": "pnpm build:cdk && cdk deploy", + "deploy:serverless": "pnpm build && serverless deploy", + "dev:sam": "pnpm build && sam local start-api", + "dev:serverless": "pnpm build && serverless offline", + "typecheck": "tsc --noEmit", + "volt": "volt" + }, + "type": "module" +} \ No newline at end of file diff --git a/examples/with-aws-lambda/samconfig.toml b/examples/with-aws-lambda/samconfig.toml new file mode 100644 index 000000000..21c265a2d --- /dev/null +++ b/examples/with-aws-lambda/samconfig.toml @@ -0,0 +1,34 @@ +# Configuration file for AWS SAM CLI +# Documentation: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html + +version = 0.1 + +[default.global.parameters] +stack_name = "voltagent-lambda" + +[default.build.parameters] +cached = true +parallel = true + +[default.validate.parameters] +lint = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_IAM" +confirm_changeset = true +resolve_s3 = true +s3_prefix = "voltagent-lambda" +region = "us-east-1" +image_repositories = [] + +[default.package.parameters] +resolve_s3 = true + +[default.sync.parameters] +watch = true + +[default.local_start_api.parameters] +warm_containers = "EAGER" + +[default.local_start_lambda.parameters] +warm_containers = "EAGER" \ No newline at end of file diff --git a/examples/with-aws-lambda/serverless.yml b/examples/with-aws-lambda/serverless.yml new file mode 100644 index 000000000..860380d82 --- /dev/null +++ b/examples/with-aws-lambda/serverless.yml @@ -0,0 +1,55 @@ +service: voltagent-lambda + +frameworkVersion: '3' + +provider: + name: aws + runtime: nodejs20.x + stage: ${opt:stage, 'dev'} + region: ${opt:region, 'us-east-1'} + timeout: 30 + memorySize: 512 + environment: + VOLTAGENT_PUBLIC_KEY: ${param:voltAgentPublicKey, ""} + VOLTAGENT_SECRET_KEY: ${param:voltAgentSecretKey, ""} + iam: + role: + statements: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + +functions: + voltagent: + handler: dist/lambda.handler + description: VoltAgent serverless function powered by Amazon Bedrock + events: + - http: + path: / + method: ANY + cors: true + - http: + path: /{proxy+} + method: ANY + cors: true + +plugins: + - serverless-offline + +custom: + serverless-offline: + httpPort: 3000 + lambdaPort: 3002 + host: 0.0.0.0 + +package: + patterns: + - 'dist/**' + - '!src/**' + - '!cdk/**' + - '!.aws-sam/**' + - '!node_modules/**' + - '!*.ts' + - '!tsconfig.json' \ No newline at end of file diff --git a/examples/with-aws-lambda/src/index.ts b/examples/with-aws-lambda/src/index.ts new file mode 100644 index 000000000..1b9068297 --- /dev/null +++ b/examples/with-aws-lambda/src/index.ts @@ -0,0 +1,37 @@ +import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { Agent, VoltAgent, Memory, InMemoryStorageAdapter } from "@voltagent/core"; +import { serverlessHono } from "@voltagent/serverless-hono"; +import { weatherTool } from "./tools"; + +// Configure Amazon Bedrock provider +// Using AWS SDK Credentials Chain for Lambda (recommended) +// Lambda automatically provides IAM role credentials +const bedrock = createAmazonBedrock({ + region: process.env.AWS_REGION || "us-east-1", + credentialProvider: fromNodeProviderChain(), +}); + +// Memory configuration for Lambda +const memory = new Memory({ + storage: new InMemoryStorageAdapter({ + storageLimit: 50, + }), +}); + +const agent = new Agent({ + name: "lambda-assistant", + instructions: "Help the user quickly and call tools when needed. You are powered by Amazon Bedrock.", + model: bedrock("anthropic.claude-3-5-sonnet-20241022-v2:0"), + tools: [weatherTool], + memory, +}); + +const voltAgent = new VoltAgent({ + agents: { agent }, + serverless: serverlessHono(), +}); + +export function getVoltAgent(): VoltAgent { + return voltAgent; +} \ No newline at end of file diff --git a/examples/with-aws-lambda/src/lambda.ts b/examples/with-aws-lambda/src/lambda.ts new file mode 100644 index 000000000..e57548f4f --- /dev/null +++ b/examples/with-aws-lambda/src/lambda.ts @@ -0,0 +1,6 @@ +import { createAwsLambdaHandler } from "@voltagent/serverless-hono"; +import { getVoltAgent } from "./index"; + +const voltAgent = getVoltAgent(); + +export const handler = createAwsLambdaHandler(voltAgent); \ No newline at end of file diff --git a/examples/with-aws-lambda/src/tools/index.ts b/examples/with-aws-lambda/src/tools/index.ts new file mode 100644 index 000000000..942789e1d --- /dev/null +++ b/examples/with-aws-lambda/src/tools/index.ts @@ -0,0 +1,29 @@ +import { createTool } from "@voltagent/core"; +import z from "zod"; + +export const weatherTool = createTool({ + id: "get-weather", + name: "getWeather", + description: "Return a mock weather report for the requested location", + parameters: z.object({ + location: z.string().describe("City or location to look up"), + }), + execute: async ({ location }, context) => { + context?.logger.info(`Fetching weather for ${location}`); + + const mockWeatherData = { + location, + temperature: Math.floor(Math.random() * 30) + 5, + condition: ["Sunny", "Cloudy", "Rainy", "Snowy", "Partly Cloudy"][ + Math.floor(Math.random() * 5) + ], + humidity: Math.floor(Math.random() * 60) + 30, + windSpeed: Math.floor(Math.random() * 30), + }; + + return { + weather: mockWeatherData, + message: `Current weather in ${location}: ${mockWeatherData.temperature}°C and ${mockWeatherData.condition.toLowerCase()} with ${mockWeatherData.humidity}% humidity and wind speed of ${mockWeatherData.windSpeed} km/h.`, + }; + }, +}); \ No newline at end of file diff --git a/examples/with-aws-lambda/template.yaml b/examples/with-aws-lambda/template.yaml new file mode 100644 index 000000000..c088e596b --- /dev/null +++ b/examples/with-aws-lambda/template.yaml @@ -0,0 +1,62 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + VoltAgent AWS Lambda deployment example + +Parameters: + VoltAgentPublicKey: + Type: String + Description: VoltOps public key (optional) + Default: "" + VoltAgentSecretKey: + Type: String + Description: VoltOps secret key (optional) + NoEcho: true + Default: "" + +Globals: + Function: + Timeout: 30 + MemorySize: 512 + Runtime: nodejs20.x + Environment: + Variables: + VOLTAGENT_PUBLIC_KEY: !Ref VoltAgentPublicKey + VOLTAGENT_SECRET_KEY: !Ref VoltAgentSecretKey + +Resources: + VoltAgentFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: dist/ + Handler: lambda.handler + Description: VoltAgent serverless function powered by Amazon Bedrock + Policies: + - Statement: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + Events: + ApiEvent: + Type: Api + Properties: + Path: /{proxy+} + Method: ANY + RootEvent: + Type: Api + Properties: + Path: / + Method: ANY + +Outputs: + VoltAgentApi: + Description: "API Gateway endpoint URL for VoltAgent" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" + VoltAgentFunction: + Description: "VoltAgent Lambda Function ARN" + Value: !GetAtt VoltAgentFunction.Arn + VoltAgentFunctionIamRole: + Description: "Implicit IAM Role created for VoltAgent function" + Value: !GetAtt VoltAgentFunctionRole.Arn \ No newline at end of file diff --git a/examples/with-aws-lambda/tsconfig.json b/examples/with-aws-lambda/tsconfig.json new file mode 100644 index 000000000..0b9521a5e --- /dev/null +++ b/examples/with-aws-lambda/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["@types/node", "@types/aws-lambda"], + "baseUrl": ".", + "outDir": "dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src", "cdk"], + "exclude": ["node_modules", "dist", "cdk.out", ".aws-sam"] +} \ No newline at end of file diff --git a/packages/serverless-hono/src/aws-lambda.ts b/packages/serverless-hono/src/aws-lambda.ts new file mode 100644 index 000000000..e12247ee4 --- /dev/null +++ b/packages/serverless-hono/src/aws-lambda.ts @@ -0,0 +1,134 @@ +import { Buffer } from "node:buffer"; +import type { VoltAgent } from "@voltagent/core"; + +interface AwsLambdaEvent { + httpMethod?: string; + headers?: Record; + multiValueHeaders?: Record; + rawUrl?: string; + rawQueryString?: string; + rawPath?: string; + path?: string; + body?: string | null; + isBase64Encoded?: boolean; + requestContext?: { + protocol?: string; + domainName?: string; + stage?: string; + }; +} + +interface AwsLambdaResult { + statusCode: number; + headers?: Record; + multiValueHeaders?: Record; + body: string; + isBase64Encoded: boolean; +} + +interface AwsLambdaContext { + callbackWaitsForEmptyEventLoop?: boolean; + functionName?: string; + functionVersion?: string; + invokedFunctionArn?: string; + memoryLimitInMB?: string; + awsRequestId?: string; + logGroupName?: string; + logStreamName?: string; + identity?: unknown; + clientContext?: unknown; + getRemainingTimeInMillis?: () => number; +} + +type AwsLambdaHandler = ( + event: AwsLambdaEvent, + context: AwsLambdaContext, +) => Promise; + +const TEXT_BODY_METHODS = new Set(["GET", "HEAD"]); + +function buildUrl(event: AwsLambdaEvent): string { + const scheme = event.headers?.["x-forwarded-proto"] || "https"; + const host = event.headers?.host || event.requestContext?.domainName || "localhost"; + const stage = event.requestContext?.stage || ""; + const path = event.rawPath || event.path || "/"; + const query = event.rawQueryString ? `?${event.rawQueryString}` : ""; + + // Remove stage prefix from path if it exists + const cleanPath = stage && path.startsWith(`/${stage}`) ? path.slice(stage.length + 1) : path; + + return `${scheme}://${host}${cleanPath}${query}`; +} + +function createRequest(event: AwsLambdaEvent): Request { + const method = (event.httpMethod || "GET").toUpperCase(); + const headers = new Headers(); + + if (event.multiValueHeaders) { + for (const [key, values] of Object.entries(event.multiValueHeaders)) { + if (!values) continue; + for (const value of values) { + if (value !== undefined) { + headers.append(key, value); + } + } + } + } + + if (event.headers) { + for (const [key, value] of Object.entries(event.headers)) { + if (value !== undefined) { + headers.set(key, value); + } + } + } + + const url = event.rawUrl || buildUrl(event); + + const init: Record = { method, headers }; + + if (!TEXT_BODY_METHODS.has(method) && event.body) { + init.body = event.isBase64Encoded ? Buffer.from(event.body, "base64") : event.body; + } + + return new Request(url, init as RequestInit); +} + +function toAwsLambdaResponse(response: Response): Promise { + const single: Record = {}; + const multi: Record = {}; + + response.headers.forEach((value, key) => { + if (single[key]) { + multi[key] = [single[key], value]; + delete single[key]; + } else if (multi[key]) { + multi[key].push(value); + } else { + single[key] = value; + } + }); + + return response.arrayBuffer().then((buffer) => ({ + statusCode: response.status, + headers: Object.keys(single).length > 0 ? single : undefined, + multiValueHeaders: Object.keys(multi).length > 0 ? multi : undefined, + body: Buffer.from(buffer).toString("base64"), + isBase64Encoded: true, + })); +} + +export function createAwsLambdaHandler(voltAgent: VoltAgent): AwsLambdaHandler { + const provider = voltAgent.serverless(); + + return async (event, context) => { + // Prevent Lambda from waiting for empty event loop + context.callbackWaitsForEmptyEventLoop = false; + + const request = createRequest(event); + const response = await provider.handleRequest(request); + return toAwsLambdaResponse(response); + }; +} + +export type { AwsLambdaHandler, AwsLambdaEvent, AwsLambdaResult, AwsLambdaContext }; \ No newline at end of file diff --git a/packages/serverless-hono/src/index.ts b/packages/serverless-hono/src/index.ts index 897d5cc05..aebcdf037 100644 --- a/packages/serverless-hono/src/index.ts +++ b/packages/serverless-hono/src/index.ts @@ -15,4 +15,11 @@ export { type NetlifyFunctionHandler, type NetlifyFunctionResult, } from "./netlify-function"; +export { + createAwsLambdaHandler, + type AwsLambdaEvent, + type AwsLambdaHandler, + type AwsLambdaResult, + type AwsLambdaContext, +} from "./aws-lambda"; export default serverlessHono; diff --git a/website/docs/deployment/aws-lambda.md b/website/docs/deployment/aws-lambda.md new file mode 100644 index 000000000..17c5dce57 --- /dev/null +++ b/website/docs/deployment/aws-lambda.md @@ -0,0 +1,427 @@ +--- +title: AWS Lambda +description: Deploy VoltAgent to AWS Lambda using SAM, CDK, or Serverless Framework with Amazon Bedrock. +--- + +This guide explains how to deploy VoltAgent to AWS Lambda using various deployment tools with Amazon Bedrock as the LLM provider. AWS Lambda provides a serverless Node.js runtime that integrates seamlessly with Amazon Bedrock for AI model inference. + +## Prerequisites + +- Node.js 18+ +- `pnpm` or `npm` +- AWS CLI configured with appropriate permissions +- One of: AWS SAM CLI, AWS CDK, or Serverless Framework +- Access to Amazon Bedrock models in your AWS account +- Optional: `VOLTAGENT_PUBLIC_KEY` and `VOLTAGENT_SECRET_KEY` if you use VoltOps observability + +## 1. Generate project files + +### Option A: VoltAgent CLI + +```bash +npm run volt deploy --target aws-lambda +``` + +The CLI scaffolds deployment configuration files for your chosen tool (SAM, CDK, or Serverless Framework) along with the Lambda handler. + +### Option B: Manual setup + +1. Install your preferred deployment tool (`sam`, `aws-cdk`, or `serverless`). +2. Create the appropriate configuration file (see examples below). +3. Add a Lambda handler that bootstraps VoltAgent with `serverlessHono()`. + +## 2. Amazon Bedrock setup + +Before deploying, ensure you have access to Amazon Bedrock models: + +1. **Enable model access** in the AWS Bedrock console for your desired models +2. **Choose your region** - Bedrock is available in specific regions (us-east-1, us-west-2, etc.) +3. **IAM permissions** - Your Lambda execution role needs `bedrock:InvokeModel` permissions + +The deployment configurations below automatically include the necessary IAM permissions. + +## 3. Environment variables + +For VoltOps observability (optional), store them using your deployment tool's secret management: + +```bash +# AWS SAM +sam deploy --parameter-overrides VoltAgentPublicKey=pk_... VoltAgentSecretKey=sk_... + +# AWS CDK +cdk deploy --context voltAgentPublicKey=pk_... --context voltAgentSecretKey=sk_... + +# Serverless Framework +serverless deploy --param="voltAgentPublicKey=pk_..." --param="voltAgentSecretKey=sk_..." +``` + +## 4. Lambda handler + +Create a handler that converts AWS Lambda events to VoltAgent requests: + +```ts title="src/lambda.ts" +import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { VoltAgent, Agent, Memory, InMemoryStorageAdapter } from "@voltagent/core"; +import { serverlessHono, createAwsLambdaHandler } from "@voltagent/serverless-hono"; +import { weatherTool } from "./tools"; + +// Configure Amazon Bedrock provider +// Lambda automatically provides IAM role credentials +const bedrock = createAmazonBedrock({ + region: process.env.AWS_REGION || "us-east-1", + credentialProvider: fromNodeProviderChain(), +}); + +const memory = new Memory({ + storage: new InMemoryStorageAdapter({ + storageLimit: 50, + }), +}); + +const agent = new Agent({ + name: "lambda-assistant", + instructions: "Answer user questions quickly and efficiently. You are powered by Amazon Bedrock.", + model: bedrock("anthropic.claude-3-5-sonnet-20241022-v2:0"), + tools: [weatherTool], + memory, +}); + +const voltAgent = new VoltAgent({ + agents: { agent }, + serverless: serverlessHono(), +}); + +export const handler = createAwsLambdaHandler(voltAgent); +``` + +> Tip: On Lambda, WebSocket streaming is not available. VoltOps Console uses HTTP polling instead. + +## 5. Deployment configurations + +### AWS SAM + +```yaml title="template.yaml" +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Parameters: + VoltAgentPublicKey: + Type: String + Description: VoltOps public key (optional) + Default: "" + VoltAgentSecretKey: + Type: String + Description: VoltOps secret key (optional) + NoEcho: true + Default: "" + +Globals: + Function: + Timeout: 30 + MemorySize: 512 + Runtime: nodejs20.x + +Resources: + VoltAgentFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: dist/ + Handler: lambda.handler + Description: VoltAgent serverless function powered by Amazon Bedrock + Policies: + - Statement: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + Environment: + Variables: + VOLTAGENT_PUBLIC_KEY: !Ref VoltAgentPublicKey + VOLTAGENT_SECRET_KEY: !Ref VoltAgentSecretKey + Events: + ApiEvent: + Type: Api + Properties: + Path: /{proxy+} + Method: ANY + RootEvent: + Type: Api + Properties: + Path: / + Method: ANY + +Outputs: + VoltAgentApi: + Description: "API Gateway endpoint URL" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" +``` + +### AWS CDK + +```ts title="lib/voltagent-stack.ts" +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 { Construct } from 'constructs'; + +export class VoltAgentStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const voltAgentFunction = new lambda.Function(this, 'VoltAgentFunction', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'lambda.handler', + code: lambda.Code.fromAsset('dist'), + timeout: cdk.Duration.seconds(30), + memorySize: 512, + environment: { + VOLTAGENT_PUBLIC_KEY: this.node.tryGetContext('voltAgentPublicKey') || '', + VOLTAGENT_SECRET_KEY: this.node.tryGetContext('voltAgentSecretKey') || '', + }, + description: 'VoltAgent serverless function powered by Amazon Bedrock', + }); + + // Add Bedrock permissions + voltAgentFunction.addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + resources: ['*'], + })); + + const api = new apigateway.RestApi(this, 'VoltAgentApi', { + restApiName: 'VoltAgent Service', + description: 'API for VoltAgent Lambda function.', + }); + + const integration = new apigateway.LambdaIntegration(voltAgentFunction); + api.root.addMethod('ANY', integration); + + const proxyResource = api.root.addResource('{proxy+}'); + proxyResource.addMethod('ANY', integration); + + new cdk.CfnOutput(this, 'ApiUrl', { + value: api.url, + description: 'VoltAgent API URL', + }); + } +} +``` + +### Serverless Framework + +```yaml title="serverless.yml" +service: voltagent-lambda + +frameworkVersion: '3' + +provider: + name: aws + runtime: nodejs20.x + stage: dev + region: us-east-1 + timeout: 30 + memorySize: 512 + environment: + VOLTAGENT_PUBLIC_KEY: ${param:voltAgentPublicKey, ""} + VOLTAGENT_SECRET_KEY: ${param:voltAgentSecretKey, ""} + iam: + role: + statements: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + +functions: + voltagent: + handler: dist/lambda.handler + description: VoltAgent serverless function powered by Amazon Bedrock + events: + - http: + path: / + method: ANY + - http: + path: /{proxy+} + method: ANY + +plugins: + - serverless-offline + +custom: + serverless-offline: + httpPort: 3000 +``` + +## 6. Build and deploy + +### AWS SAM + +```bash +# Build TypeScript +pnpm build + +# Deploy +sam build +sam deploy --guided +``` + +### AWS CDK + +```bash +# Build TypeScript +pnpm build + +# Bootstrap CDK (first time only) +cdk bootstrap + +# Deploy +cdk deploy +``` + +### Serverless Framework + +```bash +# Build TypeScript +pnpm build + +# Deploy +serverless deploy +``` + +## 7. Test your deployment + +After deployment, test your VoltAgent API: + +```bash +# Health check +curl https://your-api-gateway-url/ + +# List agents +curl https://your-api-gateway-url/agents + +# Invoke the assistant +curl -X POST https://your-api-gateway-url/agents/lambda-assistant/invoke \ + -H "Content-Type: application/json" \ + -d '{ + "messages": [{"role": "user", "content": "What is the weather in Berlin?"}] + }' +``` + +## Observability notes + +- In-memory span/log storage is active by default. You can fetch traces through the `/observability` REST endpoints. +- If VoltOps credentials are present, the Lambda exports telemetry via OTLP fetch calls. These calls use Lambda's execution context properly. +- VoltOps Console falls back to HTTP polling. There is no WebSocket streaming on Lambda. +- The handler sets `callbackWaitsForEmptyEventLoop = false` to prevent Lambda from waiting for background tasks. + +## Feature limitations on AWS Lambda + +- **MCP client/server** are not available on Lambda. The current MCP implementation depends on Node.js stdio/network APIs that are not suitable for the Lambda execution model. +- **libSQL memory adapter** is not supported. Use the bundled `InMemoryStorageAdapter` or connect to an external database (RDS PostgreSQL/Aurora) via their HTTP clients. + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +### Memory configuration examples + + + + +```ts +import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { Memory, InMemoryStorageAdapter } from "@voltagent/core"; + +const bedrock = createAmazonBedrock({ + region: process.env.AWS_REGION || "us-east-1", + credentialProvider: fromNodeProviderChain(), +}); + +const memory = new Memory({ + storage: new InMemoryStorageAdapter({ + storageLimit: 50, + }), +}); + +const agent = new Agent({ + name: "lambda-assistant", + instructions: "Answer user questions quickly.", + model: bedrock("anthropic.claude-3-5-sonnet-20241022-v2:0"), + tools: [weatherTool], + memory, +}); +``` + + + + +```ts +import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { Memory } from "@voltagent/core"; +import { PostgresMemoryAdapter, PostgresVectorAdapter } from "@voltagent/postgres"; + +const bedrock = createAmazonBedrock({ + region: process.env.AWS_REGION || "us-east-1", + credentialProvider: fromNodeProviderChain(), +}); + +const memory = new Memory({ + storage: new PostgresMemoryAdapter({ + connectionString: process.env.RDS_POSTGRES_URL, + }), + vector: new PostgresVectorAdapter({ + connectionString: process.env.RDS_POSTGRES_URL, + }), +}); + +const agent = new Agent({ + name: "lambda-assistant", + instructions: "Answer user questions quickly.", + model: bedrock("anthropic.claude-3-5-sonnet-20241022-v2:0"), + tools: [weatherTool], + memory, +}); +``` + + + + +```ts +import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { Memory } from "@voltagent/core"; +import { SupabaseMemoryAdapter } from "@voltagent/supabase"; + +const bedrock = createAmazonBedrock({ + region: process.env.AWS_REGION || "us-east-1", + credentialProvider: fromNodeProviderChain(), +}); + +const memory = new Memory({ + storage: new SupabaseMemoryAdapter({ + url: process.env.SUPABASE_URL, + serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY, + }), +}); + +const agent = new Agent({ + name: "lambda-assistant", + instructions: "Answer user questions quickly.", + model: bedrock("anthropic.claude-3-5-sonnet-20241022-v2:0"), + tools: [weatherTool], + memory, +}); +``` + + + + +Monitor your deployment with AWS CloudWatch logs and adjust the Lambda configuration as needed. After these steps your VoltAgent app is live on AWS Lambda. \ No newline at end of file diff --git a/website/docs/deployment/overview.md b/website/docs/deployment/overview.md index d51f20f19..a33f2e5f8 100644 --- a/website/docs/deployment/overview.md +++ b/website/docs/deployment/overview.md @@ -9,7 +9,7 @@ You can run VoltAgent in classic Node.js servers or in serverless (edge) runtime - **Server (Node.js)** – use `@voltagent/server-hono` (or another HTTP layer) and deploy on any host such as Fly.io, Render, AWS, Railway. - **Serverless (edge runtimes)** – run VoltAgent on platforms like Cloudflare Workers, Vercel Edge, or Deno Deploy for low latency responses while using the shared serverless provider. -- **Serverless Functions** – deploy to Node-based functions such as Netlify Functions when you need Node compatibility but prefer managed cold starts over dedicated servers. +- **Serverless Functions** – deploy to Node-based functions such as AWS Lambda or Netlify Functions when you need Node compatibility but prefer managed cold starts over dedicated servers. - **Hybrid** – keep heavy work on a Node server and expose lightweight endpoints from the edge. ## When to pick which? @@ -25,6 +25,7 @@ You can run VoltAgent in classic Node.js servers or in serverless (edge) runtime ## Guides +- [AWS Lambda](./aws-lambda.md) - [Cloudflare Workers](./cloudflare-workers.md) - [Netlify Functions](./netlify-functions.md) - Vercel Edge and Deno Deploy guides will follow soon.