├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bedrock ├── knowledge-base-lex-langsmith │ ├── .env.sample │ ├── .gitignore │ ├── README.md │ ├── architecture_diagram.jpg │ ├── bin │ │ └── ai-app.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lambda │ │ ├── LexBedrockMessageProcessor.py │ │ ├── index.js │ │ └── index.py │ ├── langsmith_trace.png │ ├── lib │ │ ├── ai-stack.ts │ │ └── template.yml │ ├── package.json │ ├── test │ │ └── blog_post.test.ts │ └── tsconfig.json ├── langchain-agent │ ├── README.md │ ├── Serverless-Conversational-AI-Langchain-Agent-Bedrock.jpg │ ├── bin │ │ └── ai-app.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lambda │ │ ├── AIMessageProcessor.py │ │ ├── Agent.py │ │ ├── chat.py │ │ ├── config.py │ │ └── tools.py │ ├── lib │ │ └── ai-stack.ts │ ├── package.json │ ├── test │ │ └── blog_post.test.ts │ └── tsconfig.json └── langchain-js-stream-agent │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── bin │ └── langchain-js-stream-agent.ts │ ├── cdk.json │ ├── index.d.ts │ ├── jest.config.js │ ├── lambda │ └── agent.ts │ ├── lib │ └── langchain-js-stream-agent-stack.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── front-end ├── .gitignore ├── README.md ├── components │ ├── Button.tsx │ ├── Chat.tsx │ └── ChatLine.tsx ├── next-env.d.ts ├── package.json ├── pages │ ├── _app.tsx │ ├── globals.css │ └── index.tsx ├── postcss.config.js ├── public │ └── favicon.ico ├── tailwind.config.js └── tsconfig.json └── openai ├── README.md ├── architecture_diagram.png ├── bin └── ai-app.ts ├── cdk.json ├── jest.config.js ├── lambda ├── AIMessageProcessor.py ├── Agent.py ├── chat.py ├── config.py └── tools.py ├── lib └── ai-stack.ts ├── package.json ├── test └── blog_post.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules/ 2 | .DS_STORE 3 | *lock.json 4 | *cdk.out 5 | *amplify/ 6 | *.next/ 7 | *amplifyconfiguration.json 8 | *aws-exports.js 9 | *.env.local -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Serverless Conversational AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure Langchain Constructs on AWS with Different LLM Providers 2 | 3 | Find the LLM provider that best suits your needs. This repository contains the code for supporting the following LLM providers: 4 | 5 | - [AWS Bedrock](https://aws.amazon.com/bedrock/) 6 | - [Bedrock Knowledge Base, Lex, LangSmith Project](./bedrock/knowledge-base-lex-langsmith/) 7 | - [Bedrock LangChain Agent Project](./bedrock/langchain-agent/) 8 | - [Bedrock LangChain JS Stream Agent Project](./bedrock/langchain-js-stream-agent/) 9 | - [OpenAI Project](https://openai.com/) 10 | - [OpenAI LangChain Agent](./openai/) 11 | 12 | Currently the OpenAI stack includes a simple conversational Langchain agent running on AWS Lambda and using DynamoDB for memory that can be customized with tools and prompts. It also includes a simple web interface for interacting with the agent. 13 | 14 | The AWS Bedrock stack includes a conversational chain running on AWS Lambda, using DynamoDB for memory, and a Bedrock Knowledge Base for RAG. It is fronted through Amazon Lex and can be connected to Amazon Connect for a full call center experience. There is also a simple agent that can be deployed with Bedrock. 15 | 16 | ## Creating a new Lambda Layer with the latest Langchain SDK 17 | 18 | To create a new Lambda Layer compatible with the latest Python runtime and the latest langchain, boto3 or openai package you can: Go into Amazon Codebuild. [Pick the runtime you are trying to build the Lambda Layer for](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html). Set the source as none and in the buildspec add the following: 19 | 20 | ```yaml 21 | version: 0.2 22 | 23 | phases: 24 | build: 25 | commands: 26 | - pip config set global.target "" 27 | - mkdir -p package/python 28 | - pip install --target package/python boto3 29 | - pip install --target package/python numpy 30 | - pip install --target package/python langchain 31 | - cd package && zip ../package.zip * -r 32 | 33 | artifacts: 34 | files: 35 | - "package.zip" 36 | ``` 37 | 38 | For the artifact location pick the S3 bucket where you want the created zip file to go. Then just download that zip and use it to create the Lambda Layer. 39 | 40 | Note: This will create a zip file with the latest langchain, boto3 and numpy packages. To create one with openai instead swap boto3 for openai in the yaml commands. 41 | -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/.env.sample: -------------------------------------------------------------------------------- 1 | PROJECT_ID= 2 | LANGCHAIN_API_KEY= 3 | CDK_DEPLOY_ACCOUNT= -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | /cdk.out 5 | 6 | # Misc 7 | /.DS_Store 8 | 9 | package-lock.json 10 | 11 | layers/langchain-bedrock/python 12 | 13 | .env -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/README.md: -------------------------------------------------------------------------------- 1 | # Deploy Langchain using Claude on Bedrock, Amazon Lex, and LangSmith 2 | 3 | ## Requirements 4 | 5 | - [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. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. 6 | - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured 7 | - [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 8 | - [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (AWS CDK) v2 Installed 9 | - [NodeJS and NPM](https://nodejs.org/en/download/) Installed 10 | - [Bedrock Model Access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) Request Access for Anthropic Claude 3 Sonnet. 11 | - [Bedrock Knowledge Base](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html) Create a Knowledge Base, set a data source, and sync it 12 | - [Langsmith](https://python.langchain.com/docs/get_started/quickstart) Generate a Langchain API key (free) 13 | 14 | ## Architecture Overview 15 | ![Alt text](./architecture_diagram.jpg?raw=true "Architecture") 16 | 17 | ## Deployment Instructions 18 | 19 | 1. Clone the repository and navigate to it: 20 | 21 | ``` 22 | git clone https://github.com/aws-samples/langchain-agents.git 23 | cd langchain-agents/bedrock/knowledge-base-lex-langsmith 24 | ``` 25 | 26 | 2. Copy .env.sample to .env and update the values with your own: 27 | 28 | ``` 29 | cp .env.sample .env 30 | ``` 31 | 32 | Note: Project ID should be a descriptive title of the project you are building, i.e. ai-assistant. Deploy account ID should contain the AWS account ID you want to deploy to. If you want to proceed without a LangSmith API Key just fill in a dumby value such as NONE. 33 | 34 | If you have an existing knowledge base you want to reusue you can add the variable: 35 | 36 | ``` 37 | KNOWLEDGE_BASE_ID= 38 | ``` 39 | 40 | 3. Install the project dependencies: 41 | 42 | ``` 43 | npm install 44 | ``` 45 | 46 | 4. The package [generative-ai-cdk-constructs](https://github.com/awslabs/generative-ai-cdk-constructs) should be added to your package.json. 47 | 48 | ``` 49 | npm install @cdklabs/generative-ai-cdk-constructs 50 | ``` 51 | 52 | 5. Use AWS CDK to synthesize an AWS CloudFormation: 53 | 54 | ``` 55 | npx cdk synth 56 | ``` 57 | 58 | 6. Use AWS CDK to deploy the AWS resources for the pattern: 59 | 60 | ``` 61 | npx cdk deploy --require-approval never 62 | ``` 63 | 64 | ## Use the Pre-Built AWS CloudFormation Template 65 | 1. Log into the [AWS Console](https://us-east-1.console.aws.amazon.com/console/home?region=us-east-1) if you are not already. 66 | 2. Choose the Launch Stack button below for your desired AWS region to open the [AWS CloudFormation console](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks?filteringText=&filteringStatus=active&viewNested=true) and create a new stack. 67 | 3. Enter the following parameters: 68 | - StackName: Name your Stack, e.g. WhatsAppAIStack. 69 | - LangchainAPIKey: The API key generated through [LangChain](https://docs.smith.langchain.com/how_to_guides/setup/create_account_api_key). 70 | - Check the box to acknowledge creating IAM resources and click “Create Stack”. 71 | - Wait for the stack creation to complete. 72 | 73 | Region | Easy Deploy Button | Template URL - use to upgrade existing stack to a new release 74 | --- | --- | --- 75 | N. Virginia (us-east-1) | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?templateURL=https://aws-blogs-artifacts-public.s3.amazonaws.com/ML-16776/template.yml) | [YML](https://aws-blogs-artifacts-public.s3.amazonaws.com/ML-16776/template.yml) 76 | 77 | Note: Upload files to the data source (S3) created for WhatsApp. As soon as you upload a file, the data source will synchronize automatically. 78 | 79 | ## Testing 80 | 81 | 1. Navigate to Amazon Lex in your AWS account. 82 | 83 | 2. Select the bot that was created by the CDK deployment, LangchainBedrockExample. 84 | 85 | 3. Select the Test bot button. 86 | 87 | 4. Enter a message in the text box and press the Enter key. 88 | 89 | 5. Navigate to [Langsmith](https://smith.langchain.com) and check the trace of the app. 90 | 91 | ![Alt text](./langsmith_trace.png?raw=true "LangSmith trace") 92 | 93 | ## Cleanup 94 | 95 | 1. To delete the stack, run: 96 | 97 | ``` 98 | npx cdk destroy 99 | ``` 100 | 101 | ## Useful commands 102 | 103 | - `cdk ls` list all stacks in the app 104 | - `cdk synth` emits the synthesized CloudFormation template 105 | - `cdk deploy` deploy this stack to your default AWS account/region 106 | - `cdk diff` compare deployed stack with current state 107 | - `cdk docs` open CDK documentation 108 | 109 | ## Amazon Connect 110 | 111 | To connect the Amazon Lex bot to Amazon Connect, follow the instructions to [Add the Amazon Lex bot to your Amazon Connect instance](https://docs.aws.amazon.com/connect/latest/adminguide/amazon-lex.html#lex-bot-add-to-connect). -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/architecture_diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/langchain-agents/HEAD/bedrock/knowledge-base-lex-langsmith/architecture_diagram.jpg -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/bin/ai-app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { AIStack } from '../lib/ai-stack'; 5 | import { AwsSolutionsChecks } from 'cdk-nag' 6 | import { Aspects } from 'aws-cdk-lib'; 7 | 8 | const app = new cdk.App(); 9 | 10 | Aspects.of(app).add(new AwsSolutionsChecks({verbose: true})); 11 | new AIStack(app, 'ServerlessAIStack', { 12 | /* If you don't specify 'env', this stack will be environment-agnostic. 13 | * Account/Region-dependent features and context lookups will not work, 14 | * but a single synthesized template can be deployed anywhere. */ 15 | 16 | /* Uncomment the next line to specialize this stack for the AWS Account 17 | * and Region that are implied by the current CLI configuration. */ 18 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 19 | env: { 20 | account: process.env.CDK_DEPLOY_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT, 21 | region: process.env.CDK_DEPLOY_REGION || process.env.CDK_DEFAULT_REGION, 22 | }, 23 | 24 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 25 | }); -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/ai-app.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 24 | "@aws-cdk/aws-iam:minimizePolicies": true, 25 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 26 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 27 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 28 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 29 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 30 | "@aws-cdk/core:enablePartitionLiterals": true, 31 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 32 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 34 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/lambda/LexBedrockMessageProcessor.py: -------------------------------------------------------------------------------- 1 | from langchain_community.chat_models import BedrockChat 2 | from langchain_aws.retrievers import AmazonKnowledgeBasesRetriever 3 | from langchain.schema import HumanMessage, AIMessage 4 | from langchain_community.chat_message_histories import DynamoDBChatMessageHistory 5 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 6 | from langchain_core.runnables import RunnablePassthrough 7 | from langchain_core.output_parsers import StrOutputParser 8 | import boto3 9 | import os 10 | import json 11 | 12 | model_id = "anthropic.claude-3-haiku-20240307-v1:0" 13 | 14 | def lambda_handler(event, context): 15 | set_langchain_api_key() 16 | bedrock_client = boto3.client("bedrock-runtime") 17 | llm = BedrockChat(model_id=model_id, client=bedrock_client) 18 | session_id = event['sessionId'] 19 | history = DynamoDBChatMessageHistory( 20 | table_name=os.environ["CONVERSATION_TABLE_NAME"], 21 | session_id=session_id, 22 | ) 23 | retriever = AmazonKnowledgeBasesRetriever( 24 | knowledge_base_id=os.environ["KNOWLEDGE_BASE_ID"], 25 | retrieval_config={"vectorSearchConfiguration": {"numberOfResults": 3}} 26 | ) 27 | user_message = event["inputTranscript"] 28 | qa_system_prompt = """You are an assistant for question-answering tasks. \ 29 | Use the following pieces of retrieved context to answer the question. \ 30 | If you don't know the answer, just say that you don't know. \ 31 | Use three sentences maximum and keep the answer concise.\ 32 | 33 | {context}""" 34 | contextualize_q_system_prompt = """Given a chat history and the latest user question \ 35 | which might reference context in the chat history, formulate a standalone question \ 36 | which can be understood without the chat history. Do NOT answer the question, \ 37 | just reformulate it if needed and otherwise return it as is.""" 38 | contextualize_q_prompt = ChatPromptTemplate.from_messages( 39 | [ 40 | ("system", contextualize_q_system_prompt), 41 | MessagesPlaceholder(variable_name="chat_history"), 42 | ("human", "{question}"), 43 | ] 44 | ) 45 | contextualize_q_chain = contextualize_q_prompt | llm | StrOutputParser() 46 | qa_prompt = ChatPromptTemplate.from_messages( 47 | [ 48 | ("system", qa_system_prompt), 49 | MessagesPlaceholder(variable_name="chat_history"), 50 | ("human", "{question}"), 51 | ] 52 | ) 53 | 54 | 55 | def contextualized_question(input: dict): 56 | if input.get("chat_history"): 57 | return contextualize_q_chain 58 | else: 59 | return input["question"] 60 | 61 | 62 | rag_chain = ( 63 | RunnablePassthrough.assign( 64 | context=contextualized_question | retriever | format_docs 65 | ) 66 | | qa_prompt 67 | | llm 68 | ) 69 | response = rag_chain.invoke({"question": user_message, "chat_history": history.messages}) 70 | # Add user message and AI message 71 | history.add_user_message(user_message) 72 | history.add_ai_message(response) 73 | return lex_response(event, response.content) 74 | 75 | def lex_response(event, message): 76 | return { 77 | "sessionState":{ 78 | "sessionAttributes":event["sessionState"]["sessionAttributes"], 79 | "dialogAction":{ 80 | "type": "ElicitIntent", 81 | }, 82 | 'intent': {'name': event['sessionState']['intent']['name'], 'state': 'Fulfilled'} 83 | }, 84 | 'messages': [ 85 | { 86 | 'contentType': 'PlainText', 87 | 'content': message 88 | } 89 | ] 90 | } 91 | 92 | def set_langchain_api_key(): 93 | ssm = boto3.client('ssm') 94 | response = ssm.get_parameter(Name=os.environ["LANGCHAIN_API_KEY_PARAMETER_NAME"]) 95 | os.environ["LANGCHAIN_API_KEY"] = response['Parameter']['Value'] 96 | 97 | 98 | def format_docs(docs): 99 | return "\n\n".join(doc.page_content for doc in docs) 100 | -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/lambda/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | BedrockAgentClient, 3 | StartIngestionJobCommand, 4 | } = require("@aws-sdk/client-bedrock-agent"); 5 | const client = new BedrockAgentClient({ region: process.env.AWS_REGION }); 6 | 7 | exports.handler = async (event, context) => { 8 | const input = { 9 | knowledgeBaseId: process.env.KNOWLEDGE_BASE_ID, 10 | dataSourceId: process.env.DATA_SOURCE_ID, 11 | clientToken: context.awsRequestId, 12 | }; 13 | const command = new StartIngestionJobCommand(input); 14 | 15 | const response = await client.send(command); 16 | 17 | return JSON.stringify({ 18 | ingestionJob: response.ingestionJob, 19 | }); 20 | }; -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/lambda/index.py: -------------------------------------------------------------------------------- 1 | 2 | import boto3 3 | import os 4 | import json 5 | 6 | boto3_session = boto3.session.Session() 7 | region_name = boto3_session.region_name 8 | client = boto3_session.client('bedrock-agent', region_name=region_name) 9 | 10 | def lambda_handler(event, context): 11 | start_job_response = client.start_ingestion_job(knowledgeBaseId = os.environ["KNOWLEDGE_BASE_ID"], dataSourceId = os.environ["DATA_SOURCE_ID"]) 12 | return json.dumps(start_job_response["ingestionJob"]) -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/langsmith_trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/langchain-agents/HEAD/bedrock/knowledge-base-lex-langsmith/langsmith_trace.png -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/lib/ai-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 4 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 5 | import * as iam from 'aws-cdk-lib/aws-iam'; 6 | import * as ssm from 'aws-cdk-lib/aws-ssm'; 7 | import * as lex from 'aws-cdk-lib/aws-lex'; 8 | import { NagSuppressions } from 'cdk-nag'; 9 | import * as dotenv from 'dotenv'; 10 | import { Duration, CfnOutput, RemovalPolicy } from 'aws-cdk-lib'; 11 | import * as s3 from 'aws-cdk-lib/aws-s3'; 12 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 13 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 14 | import * as uuid from "uuid"; 15 | import { bedrock } from "@cdklabs/generative-ai-cdk-constructs"; 16 | import { S3EventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; 17 | import { join } from 'path'; 18 | 19 | dotenv.config(); 20 | 21 | let knowledge_base_id = process.env.KNOWLEDGE_BASE_ID!; 22 | 23 | export class AIStack extends cdk.Stack { 24 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 25 | super(scope, id, props); 26 | 27 | if (!knowledge_base_id) { 28 | 29 | const wspBucket = new s3.Bucket(this, "WhatsappBucket" + uuid.v4(), 30 | { 31 | lifecycleRules: [{ 32 | expiration: Duration.days(10), 33 | }], 34 | blockPublicAccess: { 35 | blockPublicAcls: true, 36 | blockPublicPolicy: true, 37 | ignorePublicAcls: true, 38 | restrictPublicBuckets: true, 39 | }, 40 | encryption: s3.BucketEncryption.S3_MANAGED, 41 | enforceSSL: true, 42 | removalPolicy: RemovalPolicy.DESTROY, 43 | autoDeleteObjects: true, 44 | }); 45 | 46 | // Add CDK Nag suppression for wildcard resource 47 | NagSuppressions.addResourceSuppressions(wspBucket, 48 | [ 49 | { 50 | id: 'AwsSolutions-S1', 51 | reason: 'The S3 Bucket has server access logs disabled.' 52 | } 53 | ] 54 | ); 55 | 56 | const wspKnowledgeBase = new bedrock.KnowledgeBase( 57 | this, 58 | "WhatsappKnowledgeBase", 59 | { 60 | embeddingsModel: bedrock.BedrockFoundationModel.TITAN_EMBED_TEXT_V1 61 | } 62 | ); 63 | 64 | NagSuppressions.addResourceSuppressionsByPath( 65 | this, 66 | '/ServerlessAIStack/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole', 67 | [ 68 | { 69 | id: 'AwsSolutions-IAM4', 70 | reason: 'CDK CustomResource LogRetention Lambda uses the AWSLambdaBasicExecutionRole AWS Managed Policy. Managed by CDK.', 71 | }, 72 | { 73 | id: 'AwsSolutions-IAM5', 74 | reason: 'CDK CustomResource LogRetention Lambda uses a wildcard to manage log streams created at runtime. Managed by CDK.', 75 | }, 76 | ], 77 | true, 78 | ); 79 | 80 | const docsDataSource = new bedrock.S3DataSource( 81 | this, 82 | "WhatsappDataSource", 83 | { 84 | bucket: wspBucket, 85 | knowledgeBase: wspKnowledgeBase, 86 | dataSourceName: "Whatsapp", 87 | chunkingStrategy: bedrock.ChunkingStrategy.fixedSize({ 88 | maxTokens: 1024, 89 | overlapPercentage: 20, 90 | }), 91 | } 92 | ); 93 | 94 | const s3PutEventSource = new S3EventSource(wspBucket, { 95 | events: [s3.EventType.OBJECT_CREATED_PUT], 96 | }); 97 | 98 | // Defines a lambda execution role 99 | const lambdaIngestionRole = new iam.Role(this, 'lambdaIngestionRole', { 100 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 101 | inlinePolicies: { 102 | 'LambdaBasicExecution': new iam.PolicyDocument({ 103 | statements: [ 104 | new iam.PolicyStatement({ 105 | effect: iam.Effect.ALLOW, 106 | actions: [ 107 | 'logs:CreateLogGroup', 108 | 'logs:CreateLogStream', 109 | 'logs:PutLogEvents' 110 | ], 111 | resources: [ 112 | `arn:aws:logs:${props?.env?.region}:${props?.env?.account}:log-group:/aws/lambda/start-ingestion-trigger:*` 113 | ] 114 | }), 115 | new iam.PolicyStatement({ 116 | actions: ["bedrock:StartIngestionJob"], 117 | resources: [wspKnowledgeBase.knowledgeBaseArn], 118 | }) 119 | ] 120 | }) 121 | } 122 | }); 123 | 124 | const lambdaIngestionJob = new NodejsFunction(this, 'IngestionJob', { 125 | runtime: Runtime.NODEJS_20_X, 126 | entry: join(__dirname, '../lambda/index.js'), 127 | functionName: `start-ingestion-trigger`, 128 | timeout: Duration.minutes(15), 129 | environment: { 130 | KNOWLEDGE_BASE_ID: wspKnowledgeBase.knowledgeBaseId, 131 | DATA_SOURCE_ID: docsDataSource.dataSourceId, 132 | }, 133 | role: lambdaIngestionRole, 134 | }); 135 | 136 | lambdaIngestionJob.addEventSource(s3PutEventSource); 137 | 138 | // Add CDK Nag suppression for wildcard resource 139 | NagSuppressions.addResourceSuppressions(lambdaIngestionRole, [{ 140 | id: 'AwsSolutions-IAM5', 141 | reason: 'Wildcard permission is needed to create custom Lambda execution role to write to CloudWatch Logs.' 142 | }], 143 | true 144 | ); 145 | 146 | NagSuppressions.addResourceSuppressionsByPath( 147 | this, 148 | '/ServerlessAIStack/BucketNotificationsHandler050a0587b7544547bf325f094a3db834/Role', 149 | [ 150 | { 151 | id: 'AwsSolutions-IAM4', 152 | reason: 'CDK CustomResource BucketNotifications Lambda uses the AWSLambdaBasicExecutionRole AWS Managed Policy. Managed by CDK.', 153 | }, 154 | { 155 | id: 'AwsSolutions-IAM5', 156 | reason: 'CDK CustomResource BucketNotifications Lambda uses a wildcard to manage log streams created at runtime. Managed by CDK.', 157 | }, 158 | ], 159 | true, 160 | ); 161 | 162 | knowledge_base_id = wspKnowledgeBase.knowledgeBaseId; 163 | 164 | new CfnOutput(this, "WhatsappBucketName", { 165 | value: wspBucket.bucketName, 166 | }); 167 | 168 | new CfnOutput(this, "WhatsappknowledgeBaseId", { 169 | value: wspKnowledgeBase.knowledgeBaseId, 170 | }); 171 | } 172 | 173 | // Defines a DynamoDB Table to store conversations 174 | const conversationTable = new dynamodb.Table(this, 'ConversationTable', { 175 | partitionKey: { name: 'SessionId', type: dynamodb.AttributeType.STRING }, 176 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 177 | // //Enable point-in-time recovery 178 | // pointInTimeRecovery: true, 179 | // //Delete the table after the stack is deleted 180 | // removalPolicy: cdk.RemovalPolicy.DESTROY, //or RETAIN in prod 181 | }); 182 | 183 | // Defines a lambda execution role 184 | const lambdaRole = new iam.Role(this, 'lambdaRole', { 185 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 186 | inlinePolicies: { 187 | 'LambdaBasicExecution': new iam.PolicyDocument({ 188 | statements: [ 189 | new iam.PolicyStatement({ 190 | effect: iam.Effect.ALLOW, 191 | actions: [ 192 | 'logs:CreateLogGroup', 193 | 'logs:CreateLogStream', 194 | 'logs:PutLogEvents' 195 | ], 196 | resources: [ 197 | `arn:aws:logs:${props?.env?.region}:${props?.env?.account}:log-group:/aws/lambda/LexBedrockMessageProcessor:*` 198 | ] 199 | }), 200 | new iam.PolicyStatement({ 201 | effect: iam.Effect.ALLOW, 202 | actions: [ 203 | 'bedrock:InvokeModel', 204 | ], 205 | resources: [ 206 | `arn:aws:bedrock:${props?.env?.region}::foundation-model/${bedrock.BedrockFoundationModel.ANTHROPIC_CLAUDE_HAIKU_V1_0}`, 207 | ] 208 | }), 209 | new iam.PolicyStatement({ 210 | effect: iam.Effect.ALLOW, 211 | actions: [ 212 | 'bedrock:Retrieve', 213 | ], 214 | resources: [ 215 | `arn:aws:bedrock:${props?.env?.region}:${props?.env?.account}:knowledge-base/${knowledge_base_id}`, 216 | ] 217 | }), 218 | ] 219 | }) 220 | } 221 | }); 222 | 223 | // Defines a Python Lambda resource AIMessageProcessor that has a function 224 | const LexMessageProcessor = new lambda.Function(this, 'LexBedrockMessageProcessor', { 225 | runtime: lambda.Runtime.PYTHON_3_12, 226 | functionName: 'LexBedrockMessageProcessor', 227 | code: lambda.Code.fromAsset('lambda'), 228 | handler: 'LexBedrockMessageProcessor.lambda_handler', 229 | timeout: cdk.Duration.seconds(120), 230 | architecture: lambda.Architecture.ARM_64, 231 | memorySize: 256, 232 | role: lambdaRole, 233 | }); 234 | 235 | LexMessageProcessor.grantInvoke(new iam.ServicePrincipal('lex.amazonaws.com')) 236 | 237 | // Add CDK Nag suppression for wildcard resource 238 | NagSuppressions.addResourceSuppressions(lambdaRole, [{ 239 | id: 'AwsSolutions-IAM5', 240 | reason: 'Wildcard permission is needed to create custom Lambda execution role to write to CloudWatch Logs' 241 | }], 242 | true 243 | ); 244 | 245 | // Build Langchain layer that includes Bedrock from layers/langchain-layer.zip 246 | const langchainBedrockLayer = lambda.LayerVersion.fromLayerVersionArn(this, 'LangChainLayer', 'arn:aws:lambda:us-east-1:049513818483:layer:langchain-layer:25') 247 | 248 | // Add Langchain layer to Lambda function 249 | LexMessageProcessor.addLayers(langchainBedrockLayer); 250 | 251 | // Grant Lambda function access to DynamoDB tables 252 | conversationTable.grantReadWriteData(LexMessageProcessor); 253 | 254 | const langchainAPIKeyParam = new cdk.CfnParameter(this, 'LangchainAPIKey', { 255 | type: 'String', 256 | description: 'Langsmith API Key.', 257 | default: process.env.LANGCHAIN_API_KEY || '', 258 | }); 259 | 260 | // Stores Langsmith API key in AWS SSM Parameter Store 261 | const langchainAPIKey = new ssm.StringParameter(this, 'LANGCHAIN_API_KEY', { 262 | description: 'Langsmith API Key', 263 | stringValue: langchainAPIKeyParam.valueAsString, 264 | }); 265 | 266 | langchainAPIKey.grantRead(LexMessageProcessor); 267 | 268 | LexMessageProcessor.addEnvironment('KNOWLEDGE_BASE_ID', knowledge_base_id); 269 | // Pass DynamoDB table names to Lambda function 270 | LexMessageProcessor.addEnvironment('CONVERSATION_TABLE_NAME', conversationTable.tableName); 271 | // For tracing 272 | LexMessageProcessor.addEnvironment('LANGCHAIN_TRACING_V2', "true"); 273 | LexMessageProcessor.addEnvironment('LANGSMITH_ENDPOINT', 'https://api.smith.langchain.com'); 274 | LexMessageProcessor.addEnvironment('LANGCHAIN_API_KEY_PARAMETER_NAME', langchainAPIKey.parameterName); 275 | LexMessageProcessor.addEnvironment('LANGCHAIN_PROJECT', `Claude-Agent-With-KB-${knowledge_base_id}`); 276 | 277 | const lexRole = new iam.Role(this, 'LexRole', { 278 | assumedBy: new iam.ServicePrincipal('lex.amazonaws.com'), 279 | inlinePolicies: { 280 | 'LexBasicExecution': new iam.PolicyDocument({ 281 | statements: [ 282 | new iam.PolicyStatement({ 283 | effect: iam.Effect.ALLOW, 284 | actions: [ 285 | 'polly:SynthesizeSpeech', 286 | ], 287 | resources: [ 288 | `*`, 289 | ] 290 | }), 291 | // Put intent 292 | new iam.PolicyStatement({ 293 | effect: iam.Effect.ALLOW, 294 | actions: [ 295 | 'lex:PutIntent', 296 | ], 297 | resources: [ 298 | `arn:aws:lex:${props?.env?.region}:${props?.env?.account}:intent:LangchainBedrockExample:*`, 299 | ] 300 | }), 301 | ] 302 | }) 303 | } 304 | }) 305 | 306 | // Add CDK Nag suppression for wildcard resource 307 | NagSuppressions.addResourceSuppressions(lexRole, [{ 308 | id: 'AwsSolutions-IAM5', 309 | reason: 'Wildcard permission is needed to create custom Lex execution role to use Polly Voices' 310 | }],); 311 | 312 | // Defines the Test Bot Alias 313 | const testBotAliasSettingsProperty: lex.CfnBot.TestBotAliasSettingsProperty = { 314 | botAliasLocaleSettings: [{ 315 | botAliasLocaleSetting: { 316 | enabled: true, 317 | codeHookSpecification: { 318 | lambdaCodeHook: { 319 | codeHookInterfaceVersion: '1.0', 320 | lambdaArn: LexMessageProcessor.functionArn, 321 | }, 322 | }, 323 | }, 324 | localeId: 'en_US', 325 | }], 326 | description: 'Langchain Bedrock Test Bot Alias', 327 | sentimentAnalysisSettings: { 328 | DetectSentiment: false, 329 | }, 330 | }; 331 | 332 | // Defines a Lex bot 333 | const bot = new lex.CfnBot(this, 'Bot', { 334 | name: 'LangchainBedrockExample', 335 | roleArn: lexRole.roleArn, 336 | idleSessionTtlInSeconds: 300, 337 | dataPrivacy: { 338 | ChildDirected: false, 339 | }, 340 | autoBuildBotLocales: true, 341 | testBotAliasSettings: testBotAliasSettingsProperty, 342 | botLocales: [{ 343 | localeId: 'en_US', 344 | nluConfidenceThreshold: 0.9, 345 | description: 'Langchain Bedrock Example Bot', 346 | voiceSettings: { 347 | voiceId: 'Danielle', 348 | }, 349 | intents: [{ 350 | name: 'Hello', 351 | sampleUtterances: [{ 'utterance': 'Hello' }], 352 | intentClosingSetting: { 353 | closingResponse: { 354 | messageGroupsList: [{ 355 | message: { 356 | plainTextMessage: { value: 'Hello. How can I help you?' }, 357 | }, 358 | }], 359 | }, 360 | nextStep: { 361 | 'dialogAction': { 362 | type: 'ElicitIntent', 363 | } 364 | } 365 | } 366 | }, 367 | // Fallback Intent 368 | { 369 | name: 'FallbackIntent', 370 | parentIntentSignature: 'AMAZON.FallbackIntent', 371 | description: 'Invokes LexMessageProcessor Lambda function', 372 | fulfillmentCodeHook: { 373 | enabled: true, 374 | }, 375 | }, 376 | ] 377 | }] 378 | }); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/lib/template.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1: 3 | Type: AWS::S3::Bucket 4 | Properties: 5 | BucketEncryption: 6 | ServerSideEncryptionConfiguration: 7 | - ServerSideEncryptionByDefault: 8 | SSEAlgorithm: AES256 9 | LifecycleConfiguration: 10 | Rules: 11 | - ExpirationInDays: 10 12 | Status: Enabled 13 | PublicAccessBlockConfiguration: 14 | BlockPublicAcls: true 15 | BlockPublicPolicy: true 16 | IgnorePublicAcls: true 17 | RestrictPublicBuckets: true 18 | Tags: 19 | - Key: aws-cdk:auto-delete-objects 20 | Value: "true" 21 | UpdateReplacePolicy: Delete 22 | DeletionPolicy: Delete 23 | Metadata: 24 | aws:cdk:path: ServerlessAIStack/WhatsappBucket425a0596-a19f-4c6b-a0e0-6602f1bbb9b8/Resource 25 | cdk_nag: 26 | rules_to_suppress: 27 | - reason: The S3 Bucket has server access logs disabled. 28 | id: AwsSolutions-S1 29 | WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b8Policy2A6CF9A7: 30 | Type: AWS::S3::BucketPolicy 31 | Properties: 32 | Bucket: 33 | Ref: WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 34 | PolicyDocument: 35 | Statement: 36 | - Action: s3:* 37 | Condition: 38 | Bool: 39 | aws:SecureTransport: "false" 40 | Effect: Deny 41 | Principal: 42 | AWS: "*" 43 | Resource: 44 | - Fn::GetAtt: 45 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 46 | - Arn 47 | - Fn::Join: 48 | - "" 49 | - - Fn::GetAtt: 50 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 51 | - Arn 52 | - /* 53 | - Action: 54 | - s3:DeleteObject* 55 | - s3:GetBucket* 56 | - s3:List* 57 | - s3:PutBucketPolicy 58 | Effect: Allow 59 | Principal: 60 | AWS: 61 | Fn::GetAtt: 62 | - CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092 63 | - Arn 64 | Resource: 65 | - Fn::GetAtt: 66 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 67 | - Arn 68 | - Fn::Join: 69 | - "" 70 | - - Fn::GetAtt: 71 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 72 | - Arn 73 | - /* 74 | Version: "2012-10-17" 75 | Metadata: 76 | aws:cdk:path: ServerlessAIStack/WhatsappBucket425a0596-a19f-4c6b-a0e0-6602f1bbb9b8/Policy/Resource 77 | WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b8AutoDeleteObjectsCustomResourceE0754E18: 78 | Type: Custom::S3AutoDeleteObjects 79 | Properties: 80 | ServiceToken: 81 | Fn::GetAtt: 82 | - CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F 83 | - Arn 84 | BucketName: 85 | Ref: WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 86 | DependsOn: 87 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b8Policy2A6CF9A7 88 | UpdateReplacePolicy: Delete 89 | DeletionPolicy: Delete 90 | Metadata: 91 | aws:cdk:path: ServerlessAIStack/WhatsappBucket425a0596-a19f-4c6b-a0e0-6602f1bbb9b8/AutoDeleteObjectsCustomResource/Default 92 | WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b8Notifications8F263CD3: 93 | Type: Custom::S3BucketNotifications 94 | Properties: 95 | ServiceToken: 96 | Fn::GetAtt: 97 | - BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691 98 | - Arn 99 | BucketName: 100 | Ref: WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 101 | NotificationConfiguration: 102 | LambdaFunctionConfigurations: 103 | - Events: 104 | - s3:ObjectCreated:Put 105 | LambdaFunctionArn: 106 | Fn::GetAtt: 107 | - IngestionJob878E4082 108 | - Arn 109 | Managed: true 110 | SkipDestinationValidation: false 111 | DependsOn: 112 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b8AllowBucketNotificationsToServerlessAIStackIngestionJobA8DEC2DE5348BDDB 113 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b8Policy2A6CF9A7 114 | Metadata: 115 | aws:cdk:path: ServerlessAIStack/WhatsappBucket425a0596-a19f-4c6b-a0e0-6602f1bbb9b8/Notifications/Resource 116 | WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b8AllowBucketNotificationsToServerlessAIStackIngestionJobA8DEC2DE5348BDDB: 117 | Type: AWS::Lambda::Permission 118 | Properties: 119 | Action: lambda:InvokeFunction 120 | FunctionName: 121 | Fn::GetAtt: 122 | - IngestionJob878E4082 123 | - Arn 124 | Principal: s3.amazonaws.com 125 | SourceAccount: 126 | Ref: AWS::AccountId 127 | SourceArn: 128 | Fn::GetAtt: 129 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 130 | - Arn 131 | Metadata: 132 | aws:cdk:path: ServerlessAIStack/WhatsappBucket425a0596-a19f-4c6b-a0e0-6602f1bbb9b8/AllowBucketNotificationsToServerlessAIStackIngestionJobA8DEC2DE 133 | CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092: 134 | Type: AWS::IAM::Role 135 | Properties: 136 | AssumeRolePolicyDocument: 137 | Version: "2012-10-17" 138 | Statement: 139 | - Action: sts:AssumeRole 140 | Effect: Allow 141 | Principal: 142 | Service: lambda.amazonaws.com 143 | ManagedPolicyArns: 144 | - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 145 | Metadata: 146 | aws:cdk:path: ServerlessAIStack/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role 147 | CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F: 148 | Type: AWS::Lambda::Function 149 | Properties: 150 | Code: 151 | S3Bucket: aws-blogs-artifacts-public 152 | S3Key: ML-16776/faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip 153 | Timeout: 900 154 | MemorySize: 128 155 | Handler: index.handler 156 | Role: 157 | Fn::GetAtt: 158 | - CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092 159 | - Arn 160 | Runtime: nodejs20.x 161 | Description: 162 | Fn::Join: 163 | - "" 164 | - - "Lambda function for auto-deleting objects in " 165 | - Ref: WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 166 | - " S3 bucket." 167 | DependsOn: 168 | - CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092 169 | Metadata: 170 | aws:cdk:path: ServerlessAIStack/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler 171 | aws:asset:path: asset.faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6 172 | aws:asset:property: Code 173 | WhatsappKnowledgeBaseRole55B2146F: 174 | Type: AWS::IAM::Role 175 | Properties: 176 | AssumeRolePolicyDocument: 177 | Statement: 178 | - Action: sts:AssumeRole 179 | Effect: Allow 180 | Principal: 181 | Service: bedrock.amazonaws.com 182 | - Action: sts:AssumeRole 183 | Condition: 184 | ArnLike: 185 | aws:SourceArn: 186 | Fn::Join: 187 | - "" 188 | - - "arn:aws:bedrock:" 189 | - Ref: AWS::Region 190 | - ":" 191 | - Ref: AWS::AccountId 192 | - :knowledge-base/* 193 | Effect: Allow 194 | Principal: 195 | Service: bedrock.amazonaws.com 196 | Version: "2012-10-17" 197 | ManagedPolicyArns: 198 | - Ref: WhatsappKnowledgeBaseKBVectorsAOSSApiAccessAll3CBE78F1 199 | RoleName: AmazonBedrockExecutionRoleForKnowledgeBaseServerldgeBase4D11C1DF 200 | Metadata: 201 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/Role/Resource 202 | cdk_nag: 203 | rules_to_suppress: 204 | - reason: The KB role needs read only access to all objects in the data source bucket. 205 | id: AwsSolutions-IAM5 206 | WhatsappKnowledgeBaseRoleDefaultPolicy3834F8A2: 207 | Type: AWS::IAM::Policy 208 | Properties: 209 | PolicyDocument: 210 | Statement: 211 | - Action: bedrock:InvokeModel 212 | Effect: Allow 213 | Resource: 214 | Fn::Join: 215 | - "" 216 | - - "arn:aws:bedrock:" 217 | - Ref: AWS::Region 218 | - ":" 219 | - :foundation-model/amazon.titan-embed-text-v1 220 | - Action: 221 | - s3:GetBucket* 222 | - s3:GetObject* 223 | - s3:List* 224 | Effect: Allow 225 | Resource: 226 | - Fn::GetAtt: 227 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 228 | - Arn 229 | - Fn::Join: 230 | - "" 231 | - - Fn::GetAtt: 232 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 233 | - Arn 234 | - /* 235 | Version: "2012-10-17" 236 | PolicyName: WhatsappKnowledgeBaseRoleDefaultPolicy3834F8A2 237 | Roles: 238 | - Ref: WhatsappKnowledgeBaseRole55B2146F 239 | Metadata: 240 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/Role/DefaultPolicy/Resource 241 | cdk_nag: 242 | rules_to_suppress: 243 | - reason: The KB role needs read only access to all objects in the data source bucket. 244 | id: AwsSolutions-IAM5 245 | WhatsappKnowledgeBaseKBVectorsEncryptionPolicy9326E8EA: 246 | Type: AWS::OpenSearchServerless::SecurityPolicy 247 | Properties: 248 | Name: encryptionpolicyservtorsd05ee8cf 249 | Policy: '{"Rules":[{"ResourceType":"collection","Resource":["collection/vectorstoreserverectorsd05ee8cf"]}],"AWSOwnedKey":true}' 250 | Type: encryption 251 | Metadata: 252 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/KBVectors/EncryptionPolicy 253 | WhatsappKnowledgeBaseKBVectorsNetworkPolicy10852A50: 254 | Type: AWS::OpenSearchServerless::SecurityPolicy 255 | Properties: 256 | Name: networkpolicyservectorsd05ee8cf 257 | Policy: '[{"Rules":[{"ResourceType":"collection","Resource":["collection/vectorstoreserverectorsd05ee8cf"]},{"ResourceType":"dashboard","Resource":["collection/vectorstoreserverectorsd05ee8cf"]}],"AllowFromPublic":true}]' 258 | Type: network 259 | Metadata: 260 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/KBVectors/NetworkPolicy 261 | WhatsappKnowledgeBaseKBVectorsVectorCollectionFFAFB7C1: 262 | Type: AWS::OpenSearchServerless::Collection 263 | Properties: 264 | Name: vectorstoreserverectorsd05ee8cf 265 | StandbyReplicas: ENABLED 266 | Type: VECTORSEARCH 267 | DependsOn: 268 | - WhatsappKnowledgeBaseKBVectorsEncryptionPolicy9326E8EA 269 | - WhatsappKnowledgeBaseKBVectorsNetworkPolicy10852A50 270 | Metadata: 271 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/KBVectors/VectorCollection 272 | WhatsappKnowledgeBaseKBVectorsAOSSApiAccessAll3CBE78F1: 273 | Type: AWS::IAM::ManagedPolicy 274 | Properties: 275 | Description: "" 276 | Path: / 277 | PolicyDocument: 278 | Statement: 279 | - Action: aoss:APIAccessAll 280 | Effect: Allow 281 | Resource: 282 | Fn::GetAtt: 283 | - WhatsappKnowledgeBaseKBVectorsVectorCollectionFFAFB7C1 284 | - Arn 285 | Version: "2012-10-17" 286 | Metadata: 287 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/KBVectors/AOSSApiAccessAll/Resource 288 | WhatsappKnowledgeBaseKBVectorsDataAccessPolicyB4EEFBF7: 289 | Type: AWS::OpenSearchServerless::AccessPolicy 290 | Properties: 291 | Name: dataaccesspolicyservtorsd05ee8cf 292 | Policy: 293 | Fn::Join: 294 | - "" 295 | - - '[{"Rules":[{"Resource":["collection/vectorstoreserverectorsd05ee8cf"],"Permission":["aoss:DescribeCollectionItems","aoss:CreateCollectionItems","aoss:UpdateCollectionItems"],"ResourceType":"collection"},{"Resource":["index/vectorstoreserverectorsd05ee8cf/*"],"Permission":["aoss:UpdateIndex","aoss:DescribeIndex","aoss:ReadDocument","aoss:WriteDocument","aoss:CreateIndex"],"ResourceType":"index"}],"Principal":["' 296 | - Fn::GetAtt: 297 | - WhatsappKnowledgeBaseRole55B2146F 298 | - Arn 299 | - '"],"Description":""}]' 300 | Type: data 301 | Metadata: 302 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/KBVectors/DataAccessPolicy 303 | Condition: WhatsappKnowledgeBaseKBVectorsIsDataAccessPolicyNotEmpty5660FCFC 304 | WhatsappKnowledgeBaseKBIndexManageIndexPolicy8FACAFD3: 305 | Type: AWS::OpenSearchServerless::AccessPolicy 306 | Properties: 307 | Name: manageindexpolicyserdex570a3090 308 | Policy: 309 | Fn::Join: 310 | - "" 311 | - - '[{"Rules":[{"Resource":["index/vectorstoreserverectorsd05ee8cf/*"],"Permission":["aoss:DescribeIndex","aoss:CreateIndex","aoss:DeleteIndex","aoss:UpdateIndex"],"ResourceType":"index"},{"Resource":["collection/vectorstoreserverectorsd05ee8cf"],"Permission":["aoss:DescribeCollectionItems"],"ResourceType":"collection"}],"Principal":["' 312 | - Fn::GetAtt: 313 | - OpenSearchIndexCRProviderCRRole466FC04E 314 | - Arn 315 | - '"],"Description":""}]' 316 | Type: data 317 | DependsOn: 318 | - WhatsappKnowledgeBaseKBVectorsAOSSApiAccessAll3CBE78F1 319 | - WhatsappKnowledgeBaseKBVectorsDataAccessPolicyB4EEFBF7 320 | - WhatsappKnowledgeBaseKBVectorsEncryptionPolicy9326E8EA 321 | - WhatsappKnowledgeBaseKBVectorsNetworkPolicy10852A50 322 | - WhatsappKnowledgeBaseKBVectorsVectorCollectionFFAFB7C1 323 | Metadata: 324 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/KBIndex/ManageIndexPolicy 325 | WhatsappKnowledgeBaseKBIndexVectorIndex11FF3A9B: 326 | Type: Custom::OpenSearchIndex 327 | Properties: 328 | ServiceToken: 329 | Fn::GetAtt: 330 | - OpenSearchIndexCRProviderframeworkonEvent6CAE4696 331 | - Arn 332 | Endpoint: 333 | Fn::Join: 334 | - "" 335 | - - Fn::GetAtt: 336 | - WhatsappKnowledgeBaseKBVectorsVectorCollectionFFAFB7C1 337 | - Id 338 | - "." 339 | - Ref: AWS::Region 340 | - "." 341 | - aoss.amazonaws.com 342 | IndexName: bedrock-knowledge-base-default-index 343 | VectorField: bedrock-knowledge-base-default-vector 344 | Dimensions: 1536 345 | MetadataManagement: 346 | - MappingField: AMAZON_BEDROCK_TEXT_CHUNK 347 | DataType: text 348 | Filterable: true 349 | - MappingField: AMAZON_BEDROCK_METADATA 350 | DataType: text 351 | Filterable: false 352 | DependsOn: 353 | - WhatsappKnowledgeBaseKBIndexManageIndexPolicy8FACAFD3 354 | - WhatsappKnowledgeBaseKBVectorsAOSSApiAccessAll3CBE78F1 355 | - WhatsappKnowledgeBaseKBVectorsDataAccessPolicyB4EEFBF7 356 | - WhatsappKnowledgeBaseKBVectorsEncryptionPolicy9326E8EA 357 | - WhatsappKnowledgeBaseKBVectorsNetworkPolicy10852A50 358 | - WhatsappKnowledgeBaseKBVectorsVectorCollectionFFAFB7C1 359 | UpdateReplacePolicy: Delete 360 | DeletionPolicy: Delete 361 | Metadata: 362 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/KBIndex/VectorIndex/Default 363 | WhatsappKnowledgeBaseMyCfnKnowledgeBase6D9710B9: 364 | Type: AWS::Bedrock::KnowledgeBase 365 | Properties: 366 | KnowledgeBaseConfiguration: 367 | Type: VECTOR 368 | VectorKnowledgeBaseConfiguration: 369 | EmbeddingModelArn: 370 | Fn::Join: 371 | - "" 372 | - - "arn:aws:bedrock:" 373 | - Ref: AWS::Region 374 | - ":" 375 | - :foundation-model/amazon.titan-embed-text-v1 376 | Name: KBServerlessAowledgeBase4D11C1DF 377 | RoleArn: 378 | Fn::GetAtt: 379 | - WhatsappKnowledgeBaseRole55B2146F 380 | - Arn 381 | StorageConfiguration: 382 | OpensearchServerlessConfiguration: 383 | CollectionArn: 384 | Fn::GetAtt: 385 | - WhatsappKnowledgeBaseKBVectorsVectorCollectionFFAFB7C1 386 | - Arn 387 | FieldMapping: 388 | MetadataField: AMAZON_BEDROCK_METADATA 389 | TextField: AMAZON_BEDROCK_TEXT_CHUNK 390 | VectorField: bedrock-knowledge-base-default-vector 391 | VectorIndexName: bedrock-knowledge-base-default-index 392 | Type: OPENSEARCH_SERVERLESS 393 | DependsOn: 394 | - WhatsappKnowledgeBaseKBCRPolicy3403943F 395 | - WhatsappKnowledgeBaseKBIndexManageIndexPolicy8FACAFD3 396 | - WhatsappKnowledgeBaseKBIndexVectorIndex11FF3A9B 397 | - WhatsappKnowledgeBaseRoleDefaultPolicy3834F8A2 398 | - WhatsappKnowledgeBaseRole55B2146F 399 | Metadata: 400 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/MyCfnKnowledgeBase 401 | WhatsappKnowledgeBaseKBCRPolicy3403943F: 402 | Type: AWS::IAM::Policy 403 | Properties: 404 | PolicyDocument: 405 | Statement: 406 | - Action: bedrock:CreateKnowledgeBase 407 | Effect: Allow 408 | Resource: "*" 409 | - Action: 410 | - bedrock:DeleteKnowledgeBase 411 | - bedrock:TagResource 412 | - bedrock:UpdateKnowledgeBase 413 | Effect: Allow 414 | Resource: 415 | Fn::Join: 416 | - "" 417 | - - "arn:aws:bedrock:" 418 | - Ref: AWS::Region 419 | - ":" 420 | - Ref: AWS::AccountId 421 | - :knowledge-base/* 422 | - Action: iam:PassRole 423 | Effect: Allow 424 | Resource: 425 | Fn::GetAtt: 426 | - WhatsappKnowledgeBaseRole55B2146F 427 | - Arn 428 | Version: "2012-10-17" 429 | PolicyName: WhatsappKnowledgeBaseKBCRPolicy3403943F 430 | Roles: 431 | - Ref: WhatsappKnowledgeBaseRole55B2146F 432 | Metadata: 433 | aws:cdk:path: ServerlessAIStack/WhatsappKnowledgeBase/KBCRPolicy/Resource 434 | cdk_nag: 435 | rules_to_suppress: 436 | - reason: Bedrock CreateKnowledgeBase can't be restricted by resource. 437 | id: AwsSolutions-IAM5 438 | OpenSearchIndexCRProviderCRRole466FC04E: 439 | Type: AWS::IAM::Role 440 | Properties: 441 | AssumeRolePolicyDocument: 442 | Statement: 443 | - Action: sts:AssumeRole 444 | Effect: Allow 445 | Principal: 446 | Service: lambda.amazonaws.com 447 | Version: "2012-10-17" 448 | ManagedPolicyArns: 449 | - Fn::Join: 450 | - "" 451 | - - "arn:" 452 | - Ref: AWS::Partition 453 | - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 454 | - Ref: WhatsappKnowledgeBaseKBVectorsAOSSApiAccessAll3CBE78F1 455 | Metadata: 456 | aws:cdk:path: ServerlessAIStack/OpenSearchIndexCRProvider/CRRole/Resource 457 | cdk_nag: 458 | rules_to_suppress: 459 | - reason: CDK CustomResource Lambda uses the AWSLambdaBasicExecutionRole AWS Managed Policy. 460 | id: AwsSolutions-IAM4 461 | OpenSearchIndexCRProviderCustomResourcesFunction4F9ADEA2: 462 | Type: AWS::Lambda::Function 463 | Properties: 464 | Code: 465 | S3Bucket: aws-blogs-artifacts-public 466 | S3Key: ML-16776/09d29c8955d507cea3bb83bd6d8f5a2aba803d1a07a3c9f76d00d7f3ad5a291d.zip 467 | Description: Custom Resource Provider 468 | Handler: custom_resources.on_event 469 | MemorySize: 128 470 | Role: 471 | Fn::GetAtt: 472 | - OpenSearchIndexCRProviderCRRole466FC04E 473 | - Arn 474 | Runtime: python3.12 475 | Timeout: 900 476 | DependsOn: 477 | - OpenSearchIndexCRProviderCRRole466FC04E 478 | Metadata: 479 | aws:cdk:path: ServerlessAIStack/OpenSearchIndexCRProvider/CustomResourcesFunction/Resource 480 | aws:asset:path: asset.09d29c8955d507cea3bb83bd6d8f5a2aba803d1a07a3c9f76d00d7f3ad5a291d 481 | aws:asset:is-bundled: false 482 | aws:asset:property: Code 483 | OpenSearchIndexCRProviderCustomResourcesFunctionLogRetentionC32546EF: 484 | Type: Custom::LogRetention 485 | Properties: 486 | ServiceToken: 487 | Fn::GetAtt: 488 | - LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A 489 | - Arn 490 | LogGroupName: 491 | Fn::Join: 492 | - "" 493 | - - /aws/lambda/ 494 | - Ref: OpenSearchIndexCRProviderCustomResourcesFunction4F9ADEA2 495 | RetentionInDays: 7 496 | Metadata: 497 | aws:cdk:path: ServerlessAIStack/OpenSearchIndexCRProvider/CustomResourcesFunction/LogRetention/Resource 498 | OpenSearchIndexCRProviderProviderRole87B4E831: 499 | Type: AWS::IAM::Role 500 | Properties: 501 | AssumeRolePolicyDocument: 502 | Statement: 503 | - Action: sts:AssumeRole 504 | Effect: Allow 505 | Principal: 506 | Service: lambda.amazonaws.com 507 | Version: "2012-10-17" 508 | ManagedPolicyArns: 509 | - Fn::Join: 510 | - "" 511 | - - "arn:" 512 | - Ref: AWS::Partition 513 | - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 514 | Metadata: 515 | aws:cdk:path: ServerlessAIStack/OpenSearchIndexCRProvider/ProviderRole/Resource 516 | cdk_nag: 517 | rules_to_suppress: 518 | - reason: CDK CustomResource Lambda uses the AWSLambdaBasicExecutionRole AWS Managed Policy. 519 | id: AwsSolutions-IAM4 520 | - reason: CDK CustomResource Provider has a wildcard to invoke any version of the specific Custom Resource function. 521 | id: AwsSolutions-IAM5 522 | applies_to: 523 | - regex: /^Resource:::\*$/g 524 | OpenSearchIndexCRProviderProviderRoleDefaultPolicy9810CB1F: 525 | Type: AWS::IAM::Policy 526 | Properties: 527 | PolicyDocument: 528 | Statement: 529 | - Action: lambda:InvokeFunction 530 | Effect: Allow 531 | Resource: 532 | - Fn::GetAtt: 533 | - OpenSearchIndexCRProviderCustomResourcesFunction4F9ADEA2 534 | - Arn 535 | - Fn::Join: 536 | - "" 537 | - - Fn::GetAtt: 538 | - OpenSearchIndexCRProviderCustomResourcesFunction4F9ADEA2 539 | - Arn 540 | - :* 541 | Version: "2012-10-17" 542 | PolicyName: OpenSearchIndexCRProviderProviderRoleDefaultPolicy9810CB1F 543 | Roles: 544 | - Ref: OpenSearchIndexCRProviderProviderRole87B4E831 545 | Metadata: 546 | aws:cdk:path: ServerlessAIStack/OpenSearchIndexCRProvider/ProviderRole/DefaultPolicy/Resource 547 | cdk_nag: 548 | rules_to_suppress: 549 | - reason: CDK CustomResource Provider has a wildcard to invoke any version of the specific Custom Resource function. 550 | id: AwsSolutions-IAM5 551 | applies_to: 552 | - regex: /^Resource:::\*$/g 553 | OpenSearchIndexCRProviderframeworkonEvent6CAE4696: 554 | Type: AWS::Lambda::Function 555 | Properties: 556 | Code: 557 | S3Bucket: aws-blogs-artifacts-public 558 | S3Key: ML-16776/4dc48ffba382f93077a1e6824599bbd4ceb6f91eb3d9442eca3b85bdb1a20b1e.zip 559 | Description: AWS CDK resource provider framework - onEvent (ServerlessAIStack/OpenSearchIndexCRProvider/Provider) 560 | Environment: 561 | Variables: 562 | USER_ON_EVENT_FUNCTION_ARN: 563 | Fn::GetAtt: 564 | - OpenSearchIndexCRProviderCustomResourcesFunction4F9ADEA2 565 | - Arn 566 | Handler: framework.onEvent 567 | Role: 568 | Fn::GetAtt: 569 | - OpenSearchIndexCRProviderProviderRole87B4E831 570 | - Arn 571 | Runtime: nodejs20.x 572 | Timeout: 900 573 | DependsOn: 574 | - OpenSearchIndexCRProviderProviderRoleDefaultPolicy9810CB1F 575 | - OpenSearchIndexCRProviderProviderRole87B4E831 576 | Metadata: 577 | aws:cdk:path: ServerlessAIStack/OpenSearchIndexCRProvider/Provider/framework-onEvent/Resource 578 | aws:asset:path: asset.4dc48ffba382f93077a1e6824599bbd4ceb6f91eb3d9442eca3b85bdb1a20b1e 579 | aws:asset:is-bundled: false 580 | aws:asset:property: Code 581 | cdk_nag: 582 | rules_to_suppress: 583 | - reason: Lambda runtime version is managed upstream by CDK. 584 | id: AwsSolutions-L1 585 | OpenSearchIndexCRProviderframeworkonEventLogRetention64E7FA2B: 586 | Type: Custom::LogRetention 587 | Properties: 588 | ServiceToken: 589 | Fn::GetAtt: 590 | - LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A 591 | - Arn 592 | LogGroupName: 593 | Fn::Join: 594 | - "" 595 | - - /aws/lambda/ 596 | - Ref: OpenSearchIndexCRProviderframeworkonEvent6CAE4696 597 | RetentionInDays: 7 598 | Metadata: 599 | aws:cdk:path: ServerlessAIStack/OpenSearchIndexCRProvider/Provider/framework-onEvent/LogRetention/Resource 600 | LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB: 601 | Type: AWS::IAM::Role 602 | Properties: 603 | AssumeRolePolicyDocument: 604 | Statement: 605 | - Action: sts:AssumeRole 606 | Effect: Allow 607 | Principal: 608 | Service: lambda.amazonaws.com 609 | Version: "2012-10-17" 610 | ManagedPolicyArns: 611 | - Fn::Join: 612 | - "" 613 | - - "arn:" 614 | - Ref: AWS::Partition 615 | - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 616 | Metadata: 617 | aws:cdk:path: ServerlessAIStack/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/Resource 618 | cdk_nag: 619 | rules_to_suppress: 620 | - reason: CDK CustomResource LogRetention Lambda uses the AWSLambdaBasicExecutionRole AWS Managed Policy. Managed by CDK. 621 | id: AwsSolutions-IAM4 622 | - reason: CDK CustomResource LogRetention Lambda uses a wildcard to manage log streams created at runtime. Managed by CDK. 623 | id: AwsSolutions-IAM5 624 | LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB: 625 | Type: AWS::IAM::Policy 626 | Properties: 627 | PolicyDocument: 628 | Statement: 629 | - Action: 630 | - logs:DeleteRetentionPolicy 631 | - logs:PutRetentionPolicy 632 | Effect: Allow 633 | Resource: "*" 634 | Version: "2012-10-17" 635 | PolicyName: LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB 636 | Roles: 637 | - Ref: LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB 638 | Metadata: 639 | aws:cdk:path: ServerlessAIStack/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy/Resource 640 | cdk_nag: 641 | rules_to_suppress: 642 | - reason: CDK CustomResource LogRetention Lambda uses the AWSLambdaBasicExecutionRole AWS Managed Policy. Managed by CDK. 643 | id: AwsSolutions-IAM4 644 | - reason: CDK CustomResource LogRetention Lambda uses a wildcard to manage log streams created at runtime. Managed by CDK. 645 | id: AwsSolutions-IAM5 646 | LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A: 647 | Type: AWS::Lambda::Function 648 | Properties: 649 | Handler: index.handler 650 | Runtime: nodejs20.x 651 | Timeout: 900 652 | Code: 653 | S3Bucket: aws-blogs-artifacts-public 654 | S3Key: ML-16776/4e26bf2d0a26f2097fb2b261f22bb51e3f6b4b52635777b1e54edbd8e2d58c35.zip 655 | Role: 656 | Fn::GetAtt: 657 | - LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB 658 | - Arn 659 | DependsOn: 660 | - LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB 661 | - LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB 662 | Metadata: 663 | aws:cdk:path: ServerlessAIStack/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Resource 664 | aws:asset:path: asset.4e26bf2d0a26f2097fb2b261f22bb51e3f6b4b52635777b1e54edbd8e2d58c35 665 | aws:asset:is-bundled: false 666 | aws:asset:property: Code 667 | WhatsappDataSourceC11AB03A: 668 | Type: AWS::Bedrock::DataSource 669 | Properties: 670 | DataSourceConfiguration: 671 | S3Configuration: 672 | BucketArn: 673 | Fn::GetAtt: 674 | - WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 675 | - Arn 676 | Type: S3 677 | KnowledgeBaseId: 678 | Fn::GetAtt: 679 | - WhatsappKnowledgeBaseMyCfnKnowledgeBase6D9710B9 680 | - KnowledgeBaseId 681 | Name: Whatsapp 682 | VectorIngestionConfiguration: 683 | ChunkingConfiguration: 684 | ChunkingStrategy: FIXED_SIZE 685 | FixedSizeChunkingConfiguration: 686 | MaxTokens: 1024 687 | OverlapPercentage: 20 688 | Metadata: 689 | aws:cdk:path: ServerlessAIStack/WhatsappDataSource/DataSource 690 | lambdaIngestionRole4FC7FBF9: 691 | Type: AWS::IAM::Role 692 | Properties: 693 | AssumeRolePolicyDocument: 694 | Statement: 695 | - Action: sts:AssumeRole 696 | Effect: Allow 697 | Principal: 698 | Service: lambda.amazonaws.com 699 | Version: "2012-10-17" 700 | Policies: 701 | - PolicyDocument: 702 | Statement: 703 | - Action: 704 | - logs:CreateLogGroup 705 | - logs:CreateLogStream 706 | - logs:PutLogEvents 707 | Effect: Allow 708 | Resource: 709 | Fn::Join: 710 | - "" 711 | - - "arn:aws:logs:" 712 | - Ref: AWS::Region 713 | - ":" 714 | - Ref: AWS::AccountId 715 | - :log-group:/aws/lambda/start-ingestion-trigger:* 716 | - Action: bedrock:StartIngestionJob 717 | Effect: Allow 718 | Resource: 719 | Fn::GetAtt: 720 | - WhatsappKnowledgeBaseMyCfnKnowledgeBase6D9710B9 721 | - KnowledgeBaseArn 722 | Version: "2012-10-17" 723 | PolicyName: LambdaBasicExecution 724 | Metadata: 725 | aws:cdk:path: ServerlessAIStack/lambdaIngestionRole/Resource 726 | cdk_nag: 727 | rules_to_suppress: 728 | - reason: Wildcard permission is needed to create custom Lambda execution role to write to CloudWatch Logs. 729 | id: AwsSolutions-IAM5 730 | IngestionJob878E4082: 731 | Type: AWS::Lambda::Function 732 | Properties: 733 | Code: 734 | S3Bucket: aws-blogs-artifacts-public 735 | S3Key: ML-16776/36cb4bb3e2a1530f17dbfde098db2da1fea6795ec206822f0bd77e62ce2456c6.zip 736 | Environment: 737 | Variables: 738 | KNOWLEDGE_BASE_ID: 739 | Fn::GetAtt: 740 | - WhatsappKnowledgeBaseMyCfnKnowledgeBase6D9710B9 741 | - KnowledgeBaseId 742 | DATA_SOURCE_ID: 743 | Fn::GetAtt: 744 | - WhatsappDataSourceC11AB03A 745 | - DataSourceId 746 | FunctionName: start-ingestion-trigger 747 | Handler: index.handler 748 | Role: 749 | Fn::GetAtt: 750 | - lambdaIngestionRole4FC7FBF9 751 | - Arn 752 | Runtime: nodejs20.x 753 | Timeout: 900 754 | DependsOn: 755 | - lambdaIngestionRole4FC7FBF9 756 | Metadata: 757 | aws:cdk:path: ServerlessAIStack/IngestionJob/Resource 758 | aws:asset:path: asset.36cb4bb3e2a1530f17dbfde098db2da1fea6795ec206822f0bd77e62ce2456c6 759 | aws:asset:is-bundled: true 760 | aws:asset:property: Code 761 | BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC: 762 | Type: AWS::IAM::Role 763 | Properties: 764 | AssumeRolePolicyDocument: 765 | Statement: 766 | - Action: sts:AssumeRole 767 | Effect: Allow 768 | Principal: 769 | Service: lambda.amazonaws.com 770 | Version: "2012-10-17" 771 | ManagedPolicyArns: 772 | - Fn::Join: 773 | - "" 774 | - - "arn:" 775 | - Ref: AWS::Partition 776 | - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 777 | Metadata: 778 | aws:cdk:path: ServerlessAIStack/BucketNotificationsHandler050a0587b7544547bf325f094a3db834/Role/Resource 779 | cdk_nag: 780 | rules_to_suppress: 781 | - reason: CDK CustomResource BucketNotifications Lambda uses the AWSLambdaBasicExecutionRole AWS Managed Policy. Managed by CDK. 782 | id: AwsSolutions-IAM4 783 | - reason: CDK CustomResource BucketNotifications Lambda uses a wildcard to manage log streams created at runtime. Managed by CDK. 784 | id: AwsSolutions-IAM5 785 | BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36: 786 | Type: AWS::IAM::Policy 787 | Properties: 788 | PolicyDocument: 789 | Statement: 790 | - Action: s3:PutBucketNotification 791 | Effect: Allow 792 | Resource: "*" 793 | Version: "2012-10-17" 794 | PolicyName: BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36 795 | Roles: 796 | - Ref: BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC 797 | Metadata: 798 | aws:cdk:path: ServerlessAIStack/BucketNotificationsHandler050a0587b7544547bf325f094a3db834/Role/DefaultPolicy/Resource 799 | cdk_nag: 800 | rules_to_suppress: 801 | - reason: CDK CustomResource BucketNotifications Lambda uses the AWSLambdaBasicExecutionRole AWS Managed Policy. Managed by CDK. 802 | id: AwsSolutions-IAM4 803 | - reason: CDK CustomResource BucketNotifications Lambda uses a wildcard to manage log streams created at runtime. Managed by CDK. 804 | id: AwsSolutions-IAM5 805 | BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691: 806 | Type: AWS::Lambda::Function 807 | Properties: 808 | Description: AWS CloudFormation handler for "Custom::S3BucketNotifications" resources (@aws-cdk/aws-s3) 809 | Code: 810 | ZipFile: |- 811 | import boto3 # type: ignore 812 | import json 813 | import logging 814 | import urllib.request 815 | 816 | s3 = boto3.client("s3") 817 | 818 | EVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration' 819 | CONFIGURATION_TYPES = ["TopicConfigurations", "QueueConfigurations", "LambdaFunctionConfigurations"] 820 | 821 | def handler(event: dict, context): 822 | response_status = "SUCCESS" 823 | error_message = "" 824 | try: 825 | props = event["ResourceProperties"] 826 | notification_configuration = props["NotificationConfiguration"] 827 | managed = props.get('Managed', 'true').lower() == 'true' 828 | skipDestinationValidation = props.get('SkipDestinationValidation', 'false').lower() == 'true' 829 | stack_id = event['StackId'] 830 | old = event.get("OldResourceProperties", {}).get("NotificationConfiguration", {}) 831 | if managed: 832 | config = handle_managed(event["RequestType"], notification_configuration) 833 | else: 834 | config = handle_unmanaged(props["BucketName"], stack_id, event["RequestType"], notification_configuration, old) 835 | s3.put_bucket_notification_configuration(Bucket=props["BucketName"], NotificationConfiguration=config, SkipDestinationValidation=skipDestinationValidation) 836 | except Exception as e: 837 | logging.exception("Failed to put bucket notification configuration") 838 | response_status = "FAILED" 839 | error_message = f"Error: {str(e)}. " 840 | finally: 841 | submit_response(event, context, response_status, error_message) 842 | 843 | def handle_managed(request_type, notification_configuration): 844 | if request_type == 'Delete': 845 | return {} 846 | return notification_configuration 847 | 848 | def handle_unmanaged(bucket, stack_id, request_type, notification_configuration, old): 849 | def get_id(n): 850 | n['Id'] = '' 851 | strToHash=json.dumps(n, sort_keys=True).replace('"Name": "prefix"', '"Name": "Prefix"').replace('"Name": "suffix"', '"Name": "Suffix"') 852 | return f"{stack_id}-{hash(strToHash)}" 853 | def with_id(n): 854 | n['Id'] = get_id(n) 855 | return n 856 | 857 | external_notifications = {} 858 | existing_notifications = s3.get_bucket_notification_configuration(Bucket=bucket) 859 | for t in CONFIGURATION_TYPES: 860 | if request_type == 'Update': 861 | old_incoming_ids = [get_id(n) for n in old.get(t, [])] 862 | external_notifications[t] = [n for n in existing_notifications.get(t, []) if not get_id(n) in old_incoming_ids] 863 | elif request_type == 'Delete': 864 | external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f"{stack_id}-")] 865 | elif request_type == 'Create': 866 | external_notifications[t] = [n for n in existing_notifications.get(t, [])] 867 | if EVENTBRIDGE_CONFIGURATION in existing_notifications: 868 | external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION] 869 | 870 | if request_type == 'Delete': 871 | return external_notifications 872 | 873 | notifications = {} 874 | for t in CONFIGURATION_TYPES: 875 | external = external_notifications.get(t, []) 876 | incoming = [with_id(n) for n in notification_configuration.get(t, [])] 877 | notifications[t] = external + incoming 878 | 879 | if EVENTBRIDGE_CONFIGURATION in notification_configuration: 880 | notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION] 881 | elif EVENTBRIDGE_CONFIGURATION in external_notifications: 882 | notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION] 883 | 884 | return notifications 885 | 886 | def submit_response(event: dict, context, response_status: str, error_message: str): 887 | response_body = json.dumps( 888 | { 889 | "Status": response_status, 890 | "Reason": f"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}", 891 | "PhysicalResourceId": event.get("PhysicalResourceId") or event["LogicalResourceId"], 892 | "StackId": event["StackId"], 893 | "RequestId": event["RequestId"], 894 | "LogicalResourceId": event["LogicalResourceId"], 895 | "NoEcho": False, 896 | } 897 | ).encode("utf-8") 898 | headers = {"content-type": "", "content-length": str(len(response_body))} 899 | try: 900 | req = urllib.request.Request(url=event["ResponseURL"], headers=headers, data=response_body, method="PUT") 901 | with urllib.request.urlopen(req) as response: 902 | print(response.read().decode("utf-8")) 903 | print("Status code: " + response.reason) 904 | except Exception as e: 905 | print("send(..) failed executing request.urlopen(..): " + str(e)) 906 | Handler: index.handler 907 | Role: 908 | Fn::GetAtt: 909 | - BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC 910 | - Arn 911 | Runtime: python3.11 912 | Timeout: 300 913 | DependsOn: 914 | - BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36 915 | - BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC 916 | Metadata: 917 | aws:cdk:path: ServerlessAIStack/BucketNotificationsHandler050a0587b7544547bf325f094a3db834/Resource 918 | ConversationTable75C14D21: 919 | Type: AWS::DynamoDB::Table 920 | Properties: 921 | AttributeDefinitions: 922 | - AttributeName: SessionId 923 | AttributeType: S 924 | BillingMode: PAY_PER_REQUEST 925 | KeySchema: 926 | - AttributeName: SessionId 927 | KeyType: HASH 928 | UpdateReplacePolicy: Retain 929 | DeletionPolicy: Retain 930 | Metadata: 931 | aws:cdk:path: ServerlessAIStack/ConversationTable/Resource 932 | lambdaRoleC844FDB1: 933 | Type: AWS::IAM::Role 934 | Properties: 935 | AssumeRolePolicyDocument: 936 | Statement: 937 | - Action: sts:AssumeRole 938 | Effect: Allow 939 | Principal: 940 | Service: lambda.amazonaws.com 941 | Version: "2012-10-17" 942 | Policies: 943 | - PolicyDocument: 944 | Statement: 945 | - Action: 946 | - logs:CreateLogGroup 947 | - logs:CreateLogStream 948 | - logs:PutLogEvents 949 | Effect: Allow 950 | Resource: 951 | Fn::Join: 952 | - "" 953 | - - "arn:aws:logs:" 954 | - Ref: AWS::Region 955 | - ":" 956 | - Ref: AWS::AccountId 957 | - :log-group:/aws/lambda/LexBedrockMessageProcessor:* 958 | - Action: bedrock:InvokeModel 959 | Effect: Allow 960 | Resource: 961 | Fn::Join: 962 | - "" 963 | - - "arn:aws:bedrock:" 964 | - Ref: AWS::Region 965 | - ":" 966 | - :foundation-model/anthropic.claude-3-haiku-20240307-v1:0 967 | - Action: bedrock:Retrieve 968 | Effect: Allow 969 | Resource: 970 | Fn::Join: 971 | - "" 972 | - - "arn:aws:bedrock:" 973 | - Ref: AWS::Region 974 | - ":" 975 | - Ref: AWS::AccountId 976 | - :knowledge-base/ 977 | - Fn::GetAtt: 978 | - WhatsappKnowledgeBaseMyCfnKnowledgeBase6D9710B9 979 | - KnowledgeBaseId 980 | Version: "2012-10-17" 981 | PolicyName: LambdaBasicExecution 982 | Metadata: 983 | aws:cdk:path: ServerlessAIStack/lambdaRole/Resource 984 | cdk_nag: 985 | rules_to_suppress: 986 | - reason: Wildcard permission is needed to create custom Lambda execution role to write to CloudWatch Logs 987 | id: AwsSolutions-IAM5 988 | lambdaRoleDefaultPolicyA63A8A92: 989 | Type: AWS::IAM::Policy 990 | Properties: 991 | PolicyDocument: 992 | Statement: 993 | - Action: 994 | - dynamodb:BatchGetItem 995 | - dynamodb:BatchWriteItem 996 | - dynamodb:ConditionCheckItem 997 | - dynamodb:DeleteItem 998 | - dynamodb:DescribeTable 999 | - dynamodb:GetItem 1000 | - dynamodb:GetRecords 1001 | - dynamodb:GetShardIterator 1002 | - dynamodb:PutItem 1003 | - dynamodb:Query 1004 | - dynamodb:Scan 1005 | - dynamodb:UpdateItem 1006 | Effect: Allow 1007 | Resource: 1008 | - Fn::GetAtt: 1009 | - ConversationTable75C14D21 1010 | - Arn 1011 | - Ref: AWS::NoValue 1012 | - Action: 1013 | - ssm:DescribeParameters 1014 | - ssm:GetParameter 1015 | - ssm:GetParameterHistory 1016 | - ssm:GetParameters 1017 | Effect: Allow 1018 | Resource: 1019 | Fn::Join: 1020 | - "" 1021 | - - "arn:aws:ssm:" 1022 | - Ref: AWS::Region 1023 | - ":" 1024 | - Ref: AWS::AccountId 1025 | - :parameter/ 1026 | - Ref: LANGCHAINAPIKEY80F88D14 1027 | Version: "2012-10-17" 1028 | PolicyName: lambdaRoleDefaultPolicyA63A8A92 1029 | Roles: 1030 | - Ref: lambdaRoleC844FDB1 1031 | Metadata: 1032 | aws:cdk:path: ServerlessAIStack/lambdaRole/DefaultPolicy/Resource 1033 | LexBedrockMessageProcessorDF2FE64F: 1034 | Type: AWS::Lambda::Function 1035 | Properties: 1036 | Architectures: 1037 | - arm64 1038 | Code: 1039 | S3Bucket: aws-blogs-artifacts-public 1040 | S3Key: ML-16776/d17b047f106c2e02e49fb198cc22fd124277aa66d49a8454990af2a147ad2393.zip 1041 | Environment: 1042 | Variables: 1043 | KNOWLEDGE_BASE_ID: 1044 | Fn::GetAtt: 1045 | - WhatsappKnowledgeBaseMyCfnKnowledgeBase6D9710B9 1046 | - KnowledgeBaseId 1047 | CONVERSATION_TABLE_NAME: 1048 | Ref: ConversationTable75C14D21 1049 | LANGCHAIN_TRACING_V2: "true" 1050 | LANGSMITH_ENDPOINT: https://api.smith.langchain.com 1051 | LANGCHAIN_API_KEY_PARAMETER_NAME: 1052 | Ref: LANGCHAINAPIKEY80F88D14 1053 | LANGCHAIN_PROJECT: 1054 | Fn::Join: 1055 | - "" 1056 | - - Claude-Agent-With-KB- 1057 | - Fn::GetAtt: 1058 | - WhatsappKnowledgeBaseMyCfnKnowledgeBase6D9710B9 1059 | - KnowledgeBaseId 1060 | FunctionName: LexBedrockMessageProcessor 1061 | Handler: LexBedrockMessageProcessor.lambda_handler 1062 | Layers: 1063 | - arn:aws:lambda:us-east-1:049513818483:layer:langchain-layer:25 1064 | MemorySize: 256 1065 | Role: 1066 | Fn::GetAtt: 1067 | - lambdaRoleC844FDB1 1068 | - Arn 1069 | Runtime: python3.12 1070 | Timeout: 120 1071 | DependsOn: 1072 | - lambdaRoleDefaultPolicyA63A8A92 1073 | - lambdaRoleC844FDB1 1074 | Metadata: 1075 | aws:cdk:path: ServerlessAIStack/LexBedrockMessageProcessor/Resource 1076 | aws:asset:path: asset.d17b047f106c2e02e49fb198cc22fd124277aa66d49a8454990af2a147ad2393 1077 | aws:asset:is-bundled: false 1078 | aws:asset:property: Code 1079 | LexBedrockMessageProcessorInvokeSebg4cOyIU3RZ822t7Fg39pQgQkdxMsqUbbNpnTP7c2B5532E3: 1080 | Type: AWS::Lambda::Permission 1081 | Properties: 1082 | Action: lambda:InvokeFunction 1083 | FunctionName: 1084 | Fn::GetAtt: 1085 | - LexBedrockMessageProcessorDF2FE64F 1086 | - Arn 1087 | Principal: lex.amazonaws.com 1088 | Metadata: 1089 | aws:cdk:path: ServerlessAIStack/LexBedrockMessageProcessor/InvokeSebg4cOyIU3RZ822t7Fg39pQgQkdxMsqUbbNpnTP+7c= 1090 | LANGCHAINAPIKEY80F88D14: 1091 | Type: AWS::SSM::Parameter 1092 | Properties: 1093 | Description: Langsmith API Key 1094 | Type: String 1095 | Value: 1096 | Ref: LangchainAPIKey 1097 | Metadata: 1098 | aws:cdk:path: ServerlessAIStack/LANGCHAIN_API_KEY/Resource 1099 | LexRoleD3586196: 1100 | Type: AWS::IAM::Role 1101 | Properties: 1102 | AssumeRolePolicyDocument: 1103 | Statement: 1104 | - Action: sts:AssumeRole 1105 | Effect: Allow 1106 | Principal: 1107 | Service: lex.amazonaws.com 1108 | Version: "2012-10-17" 1109 | Policies: 1110 | - PolicyDocument: 1111 | Statement: 1112 | - Action: polly:SynthesizeSpeech 1113 | Effect: Allow 1114 | Resource: "*" 1115 | - Action: lex:PutIntent 1116 | Effect: Allow 1117 | Resource: 1118 | Fn::Join: 1119 | - "" 1120 | - - "arn:aws:lex:" 1121 | - Ref: AWS::Region 1122 | - ":" 1123 | - Ref: AWS::AccountId 1124 | - :intent:LangchainBedrockExample:* 1125 | Version: "2012-10-17" 1126 | PolicyName: LexBasicExecution 1127 | Metadata: 1128 | aws:cdk:path: ServerlessAIStack/LexRole/Resource 1129 | cdk_nag: 1130 | rules_to_suppress: 1131 | - reason: Wildcard permission is needed to create custom Lex execution role to use Polly Voices 1132 | id: AwsSolutions-IAM5 1133 | Bot: 1134 | Type: AWS::Lex::Bot 1135 | Properties: 1136 | AutoBuildBotLocales: true 1137 | BotLocales: 1138 | - Description: Langchain Bedrock Example Bot 1139 | Intents: 1140 | - IntentClosingSetting: 1141 | ClosingResponse: 1142 | MessageGroupsList: 1143 | - Message: 1144 | PlainTextMessage: 1145 | Value: Hello. How can I help you? 1146 | NextStep: 1147 | DialogAction: 1148 | Type: ElicitIntent 1149 | Name: Hello 1150 | SampleUtterances: 1151 | - Utterance: Hello 1152 | - Description: Invokes LexMessageProcessor Lambda function 1153 | FulfillmentCodeHook: 1154 | Enabled: true 1155 | Name: FallbackIntent 1156 | ParentIntentSignature: AMAZON.FallbackIntent 1157 | LocaleId: en_US 1158 | NluConfidenceThreshold: 0.9 1159 | VoiceSettings: 1160 | VoiceId: Danielle 1161 | DataPrivacy: 1162 | ChildDirected: false 1163 | IdleSessionTTLInSeconds: 300 1164 | Name: LangchainBedrockExample 1165 | RoleArn: 1166 | Fn::GetAtt: 1167 | - LexRoleD3586196 1168 | - Arn 1169 | TestBotAliasSettings: 1170 | BotAliasLocaleSettings: 1171 | - BotAliasLocaleSetting: 1172 | CodeHookSpecification: 1173 | LambdaCodeHook: 1174 | CodeHookInterfaceVersion: "1.0" 1175 | LambdaArn: 1176 | Fn::GetAtt: 1177 | - LexBedrockMessageProcessorDF2FE64F 1178 | - Arn 1179 | Enabled: true 1180 | LocaleId: en_US 1181 | Description: Langchain Bedrock Test Bot Alias 1182 | SentimentAnalysisSettings: 1183 | DetectSentiment: false 1184 | Metadata: 1185 | aws:cdk:path: ServerlessAIStack/Bot 1186 | CDKMetadata: 1187 | Type: AWS::CDK::Metadata 1188 | Properties: 1189 | Analytics: v2:deflate64:H4sIAAAAAAAA/21Sy47bMAz8ltwVrpMA22t3XRQo+grioleDlhhXsSwtRNnZwPC/F5KdOCl64vCp4VBb2DxnkK3wzGupmrXRFQxFQNkIPHM58A6G1042FER+tDOazN4ZLS9LePYn5xWZRmGwrRTCkB/tnnyrmbWz4nNnZYggP9orHoXGFoaDMxTDyS4PzOg7WqxJLfGHwCjcG1km9PIPk+/JG2JOjxckO6/DZenMnTF0Y/EiJTFfx1SkvJNN6vxq3dmQqikuFEs/YcDCdV7SKHhXIjMFhpdohHE1wzdXHyiQTbMnAUrrFJ0YfiRzW19dLLZOVTD8wmraO4FRMLfxCl7beo8eWwrkkw5XZxSG3iFK78Io8o6Daw/EiVfS74YfUnvveq3Ip2US5yJgrW0t7ptzZ5VODGXqLv2cZLgOiGU/u/DWhX9oZbCB7Yfd6qNUjcGKn2qy5DHontao0xeTznLwnQwMw03qR52L3b3M/z3rb5LB+bszLlVln3LaKnqf675EPIp4BzjxU7/NYPMM2erEWq99Z4NuCQ6T/QvLSOHtEwMAAA== 1190 | Metadata: 1191 | aws:cdk:path: ServerlessAIStack/CDKMetadata/Default 1192 | Conditions: 1193 | WhatsappKnowledgeBaseKBVectorsIsDataAccessPolicyNotEmpty5660FCFC: 1194 | Fn::Not: 1195 | - Fn::Equals: 1196 | - 0 1197 | - 1 1198 | Outputs: 1199 | WhatsappBucketName: 1200 | Value: 1201 | Ref: WhatsappBucket425a0596a19f4c6ba0e06602f1bbb9b86ED5B3D1 1202 | WhatsappknowledgeBaseId: 1203 | Value: 1204 | Fn::GetAtt: 1205 | - WhatsappKnowledgeBaseMyCfnKnowledgeBase6D9710B9 1206 | - KnowledgeBaseId 1207 | Parameters: 1208 | LangchainAPIKey: 1209 | Type: String 1210 | Description: Langsmith API Key. -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-assistant-app", 3 | "version": "0.1.0", 4 | "bin": { 5 | "openai_plus_aws": "bin/ai-app.ts" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.2.4", 15 | "@types/node": "18.11.15", 16 | "@types/uuid": "^10.0.0", 17 | "aws-cdk": "^2.154.1", 18 | "cdk-nag": "2.28.17", 19 | "jest": "^29.3.1", 20 | "ts-jest": "^29.0.3", 21 | "ts-node": "^10.9.1", 22 | "typescript": "~4.9.4" 23 | }, 24 | "dependencies": { 25 | "@cdklabs/generative-ai-cdk-constructs": "^0.1.273", 26 | "aws-cdk-lib": "^2.154.1", 27 | "constructs": "^10.0.0", 28 | "dotenv": "^16.4.1", 29 | "source-map-support": "^0.5.21", 30 | "uuid": "^10.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/test/blog_post.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as OpenAIPlusAWS from '../lib/openai_plus_aws-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/openai_plus_aws-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new OpenAIPlusAWS.OpenAIPlusAWSStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /bedrock/knowledge-base-lex-langsmith/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/README.md: -------------------------------------------------------------------------------- 1 | # Secure AI Agents With Langchain, OpenAI’s GPT-4, and AWS 2 | 3 | ## Requirements 4 | 5 | - [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. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. 6 | - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured 7 | - [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 8 | - [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (AWS CDK) v2 Installed 9 | - [NodeJS and NPM](https://nodejs.org/en/download/) Installed 10 | - [Amplify CLI](https://docs.amplify.aws/cli/start/install/) installed and configured 11 | 12 | ## Architecture Overview 13 | ![Alt text](./Serverless-Conversational-AI-Langchain-Agent-Bedrock.jpg?raw=true "Architecture") 14 | 15 | ## Deployment Instructions 16 | 17 | 1. Clone the repository and navigate to it: 18 | 19 | ``` 20 | git clone https://github.com/aws-samples/langchain-agents.git 21 | cd langchain-agents/bedrock/langchain-agent 22 | ``` 23 | 24 | 2. Change directory to the backend directory: 25 | 26 | ``` 27 | cd back 28 | ``` 29 | 30 | 3. Install the project dependencies: 31 | 32 | ``` 33 | npm install 34 | ``` 35 | 36 | 4. The Cognito User Pool Domain must be unique. You can either go to the lib/ai-stack.ts and manually change the name or, if you are on a Mac, run the command below. 37 | 38 | ``` 39 | timestamp=$(date +%Y%m%d%H%M%S) 40 | sed -i '' "s/ai-domain/ai-domain-$timestamp/" lib/ai-stack.ts 41 | ``` 42 | 43 | Note: The first time you deploy an AWS CDK app into an environment (account/region), you’ll need to install a “bootstrap stack”. This stack includes resources that are needed for the toolkit’s operation. 44 | Use the following command to install the bootstrap stack into the environment: 45 | 46 | ``` 47 | npx cdk bootstrap 48 | ``` 49 | 50 | 5. Use AWS CDK to synthesize an AWS CloudFormation: 51 | 52 | ``` 53 | npx cdk synth 54 | ``` 55 | 56 | 6. Use AWS CDK to deploy the AWS resources for the pattern: 57 | 58 | ``` 59 | npx cdk deploy 60 | ``` 61 | 62 | During the deployment you will be asked to confirm the security-sensitive changes. Review it and enter y to deploy the stack and create the resources. 63 | 64 | 7. Save the following from the outputted values: 65 | 66 | a. UserPoolClientIdWeb. 67 | 68 | b. UserPoolId. 69 | 70 | c. FunctionUrl. 71 | 72 | 8. Navigate back to the root directory and change directory to the front-end directory: 73 | 74 | ``` 75 | cd ../../front-end 76 | ``` 77 | 78 | 9. Install the project dependencies: 79 | 80 | ``` 81 | npm install 82 | ``` 83 | 84 | 10. Configure a new AWS Amplify project: 85 | 86 | ``` 87 | amplify init 88 | ``` 89 | 90 | (Optional) If you are running into errors run: 91 | 92 | ``` 93 | amplify configure 94 | ``` 95 | 96 | 11. Import your existing auth resource to your local back-end 97 | 98 | ``` 99 | amplify import auth 100 | ``` 101 | 102 | Select the "Cognito User Pool and Identity Pool" option and select the values you saved previously. 103 | 104 | 12. Provision cloud back-end resources with the latest local changes: 105 | 106 | ``` 107 | amplify push 108 | ``` 109 | 110 | Create .env.local file to store environmental variable and replace the variables with the Lambda Function URL and corresponding AWS Region: 111 | 112 | ``` 113 | echo "NEXT_PUBLIC_LAMBDA_URL=" > .env.local 114 | echo "NEXT_PUBLIC_AWS_REGION=" >> .env.local 115 | ``` 116 | 117 | Run application: 118 | 119 | ``` 120 | npm run dev 121 | ``` 122 | 123 | ## Cleanup 124 | 125 | 1. To remove the Amplify auth, run: 126 | 127 | ``` 128 | amplify remove auth 129 | ``` 130 | 131 | 2. To delete the Amplify project and associated backend resources, run: 132 | 133 | ``` 134 | amplify delete 135 | ``` 136 | 137 | 3. To delete the stack, run: 138 | 139 | ``` 140 | npx cdk destroy 141 | ``` 142 | 143 | ## Useful commands 144 | 145 | - `cdk ls` list all stacks in the app 146 | - `cdk synth` emits the synthesized CloudFormation template 147 | - `cdk deploy` deploy this stack to your default AWS account/region 148 | - `cdk diff` compare deployed stack with current state 149 | - `cdk docs` open CDK documentation 150 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/Serverless-Conversational-AI-Langchain-Agent-Bedrock.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/langchain-agents/HEAD/bedrock/langchain-agent/Serverless-Conversational-AI-Langchain-Agent-Bedrock.jpg -------------------------------------------------------------------------------- /bedrock/langchain-agent/bin/ai-app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { LangchainAgentStack } from '../lib/ai-stack'; 5 | import { AwsSolutionsChecks } from 'cdk-nag' 6 | import { Aspects } from 'aws-cdk-lib'; 7 | 8 | const app = new cdk.App(); 9 | 10 | Aspects.of(app).add(new AwsSolutionsChecks({verbose: true})); 11 | new LangchainAgentStack(app, 'LangchainAgentStack', { 12 | /* If you don't specify 'env', this stack will be environment-agnostic. 13 | * Account/Region-dependent features and context lookups will not work, 14 | * but a single synthesized template can be deployed anywhere. */ 15 | 16 | /* Uncomment the next line to specialize this stack for the AWS Account 17 | * and Region that are implied by the current CLI configuration. */ 18 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /bedrock/langchain-agent/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/ai-app.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 24 | "@aws-cdk/aws-iam:minimizePolicies": true, 25 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 26 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 27 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 28 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 29 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 30 | "@aws-cdk/core:enablePartitionLiterals": true, 31 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 32 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 34 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/lambda/AIMessageProcessor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import boto3 4 | from langchain_community.chat_models import BedrockChat 5 | from chat import Chat 6 | from Agent import Agent 7 | from config import config 8 | 9 | conversation_table_name = config.CONVERSATION_TABLE_NAME 10 | 11 | 12 | def lambda_handler(event, context): 13 | print(event) 14 | chat = Chat(event) 15 | if is_user_request_to_start_new_conversation(event): 16 | print("Starting a new conversation") 17 | chat.create_new_chat() 18 | response_message = 'Your previous conversation has been saved. You are now ready to begin a new conversation.' 19 | return chat.http_response(response_message) 20 | user_message = get_user_message(event) 21 | llm = BedrockChat(model_id="anthropic.claude-3-sonnet-20240229-v1:0") 22 | langchain_agent = Agent(llm, chat.memory) 23 | message = langchain_agent.run(input=user_message) 24 | return chat.http_response(message) 25 | 26 | 27 | def is_user_request_to_start_new_conversation(event): 28 | user_message = get_user_message(event) 29 | return "start a new conversation" in user_message.strip().lower() 30 | 31 | 32 | def get_user_message(event): 33 | body = load_body(event) 34 | user_message_body = body['message'] 35 | return user_message_body 36 | 37 | 38 | def load_body(event): 39 | body = json.loads(event['body']) 40 | return body -------------------------------------------------------------------------------- /bedrock/langchain-agent/lambda/Agent.py: -------------------------------------------------------------------------------- 1 | from langchain.agents.tools import Tool 2 | from langchain.agents.conversational.base import ConversationalAgent 3 | from langchain.agents import AgentExecutor 4 | from tools import tools 5 | from datetime import datetime 6 | 7 | 8 | class Agent(): 9 | def __init__(self, llm, memory) -> None: 10 | self.prefix = "The following is a conversation between you, Agent, and a user. By the way, the date is " + \ 11 | datetime.now().strftime("%m/%d/%Y, %H:%M:%S") + "." 12 | self.ai_prefix = "Agent" 13 | self.human_prefix = "User" 14 | self.llm = llm 15 | self.memory = memory 16 | self.agent = self.create_agent() 17 | 18 | def create_agent(self): 19 | agent = ConversationalAgent.from_llm_and_tools( 20 | llm=self.llm, 21 | tools=tools, 22 | prefix=self.prefix, 23 | ai_prefix=self.ai_prefix, 24 | human_prefix=self.human_prefix 25 | ) 26 | agent_executor = AgentExecutor.from_agent_and_tools( 27 | agent=agent, tools=tools, verbose=True, memory=self.memory) 28 | return agent_executor 29 | 30 | def run(self, input): 31 | return self.agent.run(input=input) 32 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/lambda/chat.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | from boto3.dynamodb.types import TypeSerializer 4 | from langchain.memory.chat_message_histories import DynamoDBChatMessageHistory 5 | from langchain.memory import ConversationBufferMemory 6 | from config import config 7 | 8 | from datetime import datetime 9 | import json 10 | 11 | now = datetime.now() 12 | dynamodb = boto3.client('dynamodb') 13 | ts = TypeSerializer() 14 | 15 | conversation_table_name = config.CONVERSATION_TABLE_NAME 16 | chat_index_table_name = config.CHAT_INDEX_TABLE_NAME 17 | 18 | class Chat(): 19 | def __init__(self, event): 20 | self.set_user_identity(event) 21 | self.set_memory() 22 | self.set_chat_index() 23 | 24 | def set_memory(self): 25 | _id = self.user_id 26 | self.message_history = DynamoDBChatMessageHistory( 27 | table_name=conversation_table_name, session_id=_id) 28 | self.memory = ConversationBufferMemory( 29 | memory_key="chat_history", chat_memory=self.message_history, return_messages=True) 30 | 31 | def set_user_identity(self, event): 32 | body = json.loads(event['body']) 33 | self.user_id = body['userId'] 34 | 35 | 36 | def http_response(self, message): 37 | return { 38 | 'statusCode': 200, 39 | 'body': json.dumps(message) 40 | } 41 | 42 | def create_new_chat(self): 43 | self.increment_chat_index() 44 | 45 | def set_chat_index(self): 46 | self.chat_index = self.get_chat_index() 47 | 48 | def get_chat_index(self): 49 | key = {'UserId':self.user_id} 50 | chat_index = dynamodb.get_item(TableName=chat_index_table_name, Key=ts.serialize(key)['M']) 51 | if 'Item' in chat_index: 52 | return int(chat_index['Item']['chat_index']['N']) 53 | return 0 54 | 55 | def increment_chat_index(self): 56 | self.chat_index += 1 57 | input = { 58 | 'UserId': self.user_id, 59 | 'chat_index': self.chat_index, 60 | 'updated_at': str(now) 61 | } 62 | dynamodb.put_item(TableName=chat_index_table_name, Item=ts.serialize(input)['M']) -------------------------------------------------------------------------------- /bedrock/langchain-agent/lambda/config.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import os 3 | 4 | 5 | @dataclass(frozen=True) 6 | class Config: 7 | CONVERSATION_TABLE_NAME = os.environ['CONVERSATION_TABLE_NAME'] 8 | CHAT_INDEX_TABLE_NAME = os.environ['CHAT_INDEX_TABLE_NAME'] 9 | 10 | config = Config() 11 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/lambda/tools.py: -------------------------------------------------------------------------------- 1 | from langchain.agents.tools import Tool 2 | 3 | class Tools(): 4 | def __init__(self) -> None: 5 | self.tools = [ 6 | Tool( 7 | name="Hello World Tool", 8 | func=lambda x: "Hello world", 9 | description="useful for a simple hello world" 10 | ) 11 | ] 12 | 13 | tools = Tools().tools 14 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/lib/ai-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 4 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 5 | import * as iam from 'aws-cdk-lib/aws-iam'; 6 | import * as bedrock from 'aws-cdk-lib/aws-bedrock'; 7 | import * as cognito from 'aws-cdk-lib/aws-cognito'; 8 | import { NagSuppressions } from 'cdk-nag'; 9 | 10 | export class LangchainAgentStack extends cdk.Stack { 11 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 12 | super(scope, id, props); 13 | 14 | // Defines a DynamoDB to keep track of customer conversation 15 | const conversationIndexTable = new dynamodb.Table(this, 'ConversationIndexTable', { 16 | partitionKey: { name: 'UserId', type: dynamodb.AttributeType.STRING }, 17 | tableName: 'ConversationIndexTable', 18 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 19 | }); 20 | 21 | // Defines a DynamoDB Table to store conversations 22 | const conversationTable = new dynamodb.Table(this, 'ConversationTable', { 23 | partitionKey: { name: 'SessionId', type: dynamodb.AttributeType.STRING }, 24 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 25 | // //Enable point-in-time recovery 26 | // pointInTimeRecovery: true, 27 | // //Delete the table after the stack is deleted 28 | // removalPolicy: cdk.RemovalPolicy.DESTROY, //or RETAIN in prod 29 | }); 30 | 31 | // Defines a Cognito User Pool for user authentication 32 | const userPool = new cognito.UserPool(this, 'userPool', { 33 | userPoolName: 'user-pool', 34 | selfSignUpEnabled: true, 35 | removalPolicy: cdk.RemovalPolicy.DESTROY, //or RETAIN in prod 36 | // Enable MFA 37 | // mfa: cognito.Mfa.REQUIRED, 38 | accountRecovery: cognito.AccountRecovery.EMAIL_ONLY, 39 | mfaSecondFactor: { 40 | sms: false, 41 | otp: true, 42 | }, 43 | signInAliases: { 44 | phone: true, 45 | email: true, 46 | }, 47 | autoVerify: { 48 | email: true, 49 | }, 50 | keepOriginal: { 51 | email: true, 52 | }, 53 | standardAttributes: { 54 | phoneNumber: { 55 | required: true, 56 | }, 57 | email: { 58 | required: true, 59 | }, 60 | givenName: { 61 | required: true, 62 | }, 63 | }, 64 | enableSmsRole: false, 65 | passwordPolicy: { 66 | minLength: 8, 67 | requireLowercase: true, 68 | requireUppercase: true, 69 | requireDigits: true, 70 | requireSymbols: true, 71 | }, 72 | advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED 73 | }); 74 | 75 | //Defines the domain associated with the user pool 76 | new cognito.UserPoolDomain(this, 'userPoolDomain', { 77 | userPool, 78 | cognitoDomain: { 79 | domainPrefix: 'langchain-bedrock-test', //needs to be unqiue 80 | } 81 | }); 82 | 83 | //Defines a web client for the user pool without secret 84 | const userPoolClientWeb = new cognito.UserPoolClient(this, 'userPoolClientWeb', { 85 | userPool: userPool, 86 | userPoolClientName: 'user-pool-client-web', 87 | oAuth: { 88 | flows: { 89 | authorizationCodeGrant: true, 90 | implicitCodeGrant: false, 91 | }, 92 | scopes: [ 93 | cognito.OAuthScope.EMAIL, 94 | cognito.OAuthScope.OPENID, 95 | cognito.OAuthScope.PROFILE, 96 | cognito.OAuthScope.PHONE, 97 | cognito.OAuthScope.COGNITO_ADMIN 98 | ], 99 | callbackUrls: ['http://localhost:3000/'], 100 | logoutUrls: ['http://localhost:3000/'] 101 | } 102 | }); 103 | 104 | //Defines a native client for the user pool with secret 105 | const userPoolClientNative = new cognito.UserPoolClient(this, 'userPoolClientNative', { 106 | userPool: userPool, 107 | userPoolClientName: 'user-pool-client-native', 108 | oAuth: { 109 | flows: { 110 | authorizationCodeGrant: true, 111 | implicitCodeGrant: false, 112 | }, 113 | scopes: [ 114 | cognito.OAuthScope.EMAIL, 115 | cognito.OAuthScope.OPENID, 116 | cognito.OAuthScope.PROFILE, 117 | cognito.OAuthScope.PHONE, 118 | cognito.OAuthScope.COGNITO_ADMIN 119 | ], 120 | callbackUrls: ['http://localhost:3000/'], 121 | logoutUrls: ['http://localhost:3000/'] 122 | }, 123 | generateSecret: true 124 | }); 125 | 126 | 127 | // Defines an Identity Pool for user authorization 128 | const identityPool = new cognito.CfnIdentityPool(this, 'identityPool',{ 129 | identityPoolName: 'identity-pool', 130 | allowUnauthenticatedIdentities: false, 131 | cognitoIdentityProviders: [{ 132 | clientId: userPoolClientWeb.userPoolClientId, 133 | providerName:userPool.userPoolProviderName, 134 | }, 135 | { 136 | clientId: userPoolClientNative.userPoolClientId, 137 | providerName:userPool.userPoolProviderName, 138 | }] 139 | }); 140 | 141 | // Defines an IAM Role that will be assumed by authenticated users of User Pool 142 | const cognitoRole = new iam.Role(this, 'cognitoUserPoolRole', { 143 | assumedBy: new iam.FederatedPrincipal('cognito-identity.amazonaws.com', { 144 | 'StringEquals': { 145 | 'cognito-identity.amazonaws.com:aud' : identityPool.ref, 146 | }, 147 | "ForAnyValue:StringLike" : { 148 | "cognito-identity.amazonaws.com:amr" : "authenticated" 149 | }, 150 | }, 'sts:AssumeRoleWithWebIdentity'), 151 | }); 152 | 153 | // Defines an IAM Role that will be assumed by unauthenticated users of User Pool 154 | const cognitoUnauthRole = new iam.Role(this, 'cognitoUserPoolUnauthRole', { 155 | assumedBy: new iam.FederatedPrincipal('cognito-identity.amazonaws.com', { 156 | 'StringEquals': { 157 | 'cognito-identity.amazonaws.com:aud' : identityPool.ref, 158 | }, 159 | "ForAnyValue:StringLike" : { 160 | "cognito-identity.amazonaws.com:amr" : "unauthenticated" 161 | }, 162 | }, 'sts:AssumeRoleWithWebIdentity'), 163 | }); 164 | 165 | // Defines a lambda execution role 166 | const lambdaRole = new iam.Role(this, 'lambdaRole', { 167 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 168 | }); 169 | 170 | // Defines a Python Lambda resource AIMessageProcessor that has a function 171 | const AIMessageProcessor = new lambda.Function(this, 'AIMessageProcessor', { 172 | runtime: lambda.Runtime.PYTHON_3_12, 173 | code: lambda.Code.fromAsset('lambda'), 174 | handler: 'AIMessageProcessor.lambda_handler', 175 | timeout: cdk.Duration.seconds(120), 176 | architecture: lambda.Architecture.ARM_64, 177 | role: lambdaRole, 178 | memorySize: 256, 179 | }); 180 | 181 | // Defines a policy to the Lambda execution role that allows it to access CloudWatch Logs 182 | AIMessageProcessor.addToRolePolicy(new iam.PolicyStatement({ 183 | effect: iam.Effect.ALLOW, 184 | actions: [ 185 | 'logs:CreateLogGroup', 186 | 'logs:CreateLogStream', 187 | 'logs:PutLogEvents' 188 | ], 189 | resources: ['*'] 190 | })); 191 | 192 | 193 | // Invoke Bedrock permission 194 | AIMessageProcessor.addToRolePolicy(new iam.PolicyStatement({ 195 | effect: iam.Effect.ALLOW, 196 | actions: [ 197 | 'bedrock:InvokeModel' 198 | ], 199 | resources: [`arn:aws:bedrock:${cdk.Aws.REGION}::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0`] 200 | })); 201 | 202 | // Defines CDK Nag rule suppression for wildcard permission for lambda execution role 203 | NagSuppressions.addResourceSuppressions(lambdaRole, [{ 204 | id: 'AwsSolutions-IAM5', 205 | reason: 'Wildcild permission is needed to create custom Lambda execution role to write to CloudWatch Logs' 206 | }], 207 | true 208 | ); 209 | 210 | // Defines IAM permission to the functoin to allow Cognito Identity Pool to invoke it 211 | AIMessageProcessor.addPermission('allowCognitoIdentityPoolInvoke', { 212 | principal: new iam.ServicePrincipal('cognito-idp.amazonaws.com'), 213 | sourceArn: `arn:aws:cognito-identity:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:identitypool/${identityPool.ref}` 214 | }); 215 | 216 | // Defines a policy to the Cognito User Pool Role that allows it to invoke the AIMessageProcess Lambda function 217 | const authUserPolicy = new iam.PolicyStatement({ 218 | effect: iam.Effect.ALLOW, 219 | actions: ['lambda:InvokeFunctionUrl'], 220 | resources: [AIMessageProcessor.functionArn], 221 | }); 222 | 223 | cognitoRole.addToPolicy(authUserPolicy); 224 | 225 | // Add the policy to the authenticated role for the Identity Pool 226 | const authUserRole = new cognito.CfnIdentityPoolRoleAttachment(this, 'identityPoolRoleAttachment', { 227 | identityPoolId: identityPool.ref, 228 | roles: { 229 | authenticated: cognitoRole.roleArn, 230 | unauthenticated: cognitoUnauthRole.roleArn, 231 | }, 232 | }); 233 | 234 | authUserRole.addDependency(identityPool); 235 | 236 | // Defines a lambda function URL with secure authType 237 | const lambdaUrl = AIMessageProcessor.addFunctionUrl({ 238 | authType: lambda.FunctionUrlAuthType.AWS_IAM, 239 | cors: { 240 | allowCredentials: true, 241 | allowedOrigins: ['*'], 242 | allowedHeaders: ['*'], 243 | exposedHeaders: ['*'], 244 | maxAge: cdk.Duration.seconds(86400), 245 | } 246 | }); 247 | 248 | // Build Langchain layer that includes Bedrock from ../layers/langchain-aws.zip 249 | const langchainLayer = lambda.LayerVersion.fromLayerVersionArn(this, 'LangChainLayer', 'arn:aws:lambda:us-east-1:049513818483:layer:langchain-layer:25') 250 | 251 | AIMessageProcessor.addLayers(langchainLayer); 252 | 253 | // Grant Lambda function access to DynamoDB tables 254 | conversationIndexTable.grantReadWriteData(AIMessageProcessor); 255 | conversationTable.grantReadWriteData(AIMessageProcessor); 256 | 257 | // Pass DynamoDB table names to Lambda function 258 | AIMessageProcessor.addEnvironment('CHAT_INDEX_TABLE_NAME', conversationIndexTable.tableName); 259 | AIMessageProcessor.addEnvironment('CONVERSATION_TABLE_NAME', conversationTable.tableName); 260 | 261 | new cdk.CfnOutput(this, 'FunctionUrl', { 262 | value: lambdaUrl.url, 263 | }) 264 | new cdk.CfnOutput(this, 'IdentityPoolId', { 265 | value: identityPool.ref, 266 | }) 267 | new cdk.CfnOutput(this, 'UserPoolId', { 268 | value: userPool.userPoolId, 269 | }) 270 | new cdk.CfnOutput(this, 'UserPoolClientIdWeb', { 271 | value: userPoolClientWeb.userPoolClientId, 272 | }) 273 | new cdk.CfnOutput(this, 'UserPoolClientIdNative', { 274 | value: userPoolClientNative.userPoolClientId, 275 | }) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "langchain-bedrock-app", 3 | "version": "0.1.0", 4 | "bin": { 5 | "openai_plus_aws": "bin/ai-app.ts" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.2.4", 15 | "@types/node": "18.11.15", 16 | "aws-cdk": "^2.122.0", 17 | "jest": "^29.3.1", 18 | "ts-jest": "^29.0.3", 19 | "ts-node": "^10.9.1", 20 | "typescript": "~4.9.4", 21 | "cdk-nag": "2.28.17" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-apigateway": "^1.190.0", 25 | "@aws-cdk/aws-dynamodb": "^1.189.0", 26 | "@aws-cdk/aws-iam": "^1.189.0", 27 | "@aws-cdk/aws-lambda": "^1.189.0", 28 | "aws-cdk-lib": "2.184.0", 29 | "constructs": "^10.0.0", 30 | "source-map-support": "^0.5.21" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/test/blog_post.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as OpenAIPlusAWS from '../lib/openai_plus_aws-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/openai_plus_aws-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new OpenAIPlusAWS.OpenAIPlusAWSStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /bedrock/langchain-agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | !index.d.ts 5 | node_modules 6 | !package-lock.json 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/README.md: -------------------------------------------------------------------------------- 1 | # LangChain JS Stream Agent 2 | 3 | Serverless implementation of a LangChain agent using AWS Lambda and Bedrock. 4 | 5 | ## Getting Started 6 | 7 | This guide will walk you through the steps required to deploy the stack using the AWS Cloud Development Kit (CDK). 8 | 9 | ### Prerequisites 10 | 11 | - AWS CLI installed and configured 12 | - Node.js and npm installed 13 | - AWS CDK installed (`npm install -g aws-cdk`) 14 | - Docker (to use NodejsFunction) 15 | - Claude 3 Sonnet enabled in Bedrock 16 | 17 | ### Warning 18 | 19 | Please be aware that the URL of the Lambda function will be public after deployment. 20 | Secure this URL using an authentication mechanism to prevent unauthorized access. 21 | 22 | ### Deploy 23 | 24 | 1. Install dependencies: 25 | 26 | ```bash 27 | npm install 28 | ``` 29 | 30 | 2. Deploy the stack using CDK: 31 | 32 | ```bash 33 | cdk deploy 34 | ``` 35 | 36 | CDK will use Docker to build the Lambda function for the right architecture and runtime. 37 | 38 | Once the deployment is done, the Lambda URL will be displayed in the outputs: 39 | 40 | ``` 41 | Outputs: 42 | LangchainJsStreamAgentStack.agentFunctionUrlOutput = https://XXXXXXXXXX.lambda-url.us-east-1.on.aws/ 43 | ``` 44 | 45 | 3. Test the agent: 46 | 47 | ```bash 48 | curl https://XXXXXXXXXX.lambda-url.us-east-1.on.aws/ 49 | ``` 50 | 51 | The agent is streaming the response to the question: `What is 2 to the power of 8?` after using the calculator tool: 52 | 53 | ``` 54 | token: 55 | token: 56 | token: 57 | token: Thought 58 | token: : 59 | token: I 60 | token: now 61 | token: know 62 | token: the 63 | token: final 64 | token: answer 65 | token: 66 | Final 67 | token: Answer 68 | token: : 69 | token: 70 | token: 2 71 | token: 72 | token: to 73 | token: the 74 | token: power 75 | token: of 76 | token: 77 | token: 8 78 | token: 79 | token: is 80 | token: 81 | token: 256 82 | token: . 83 | token: 84 | token: 85 | ``` 86 | 87 | ### Cleanup 88 | 89 | When you are done, you can remove all the resources: 90 | 91 | ```bash 92 | cdk destroy 93 | ``` 94 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/bin/langchain-js-stream-agent.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from "aws-cdk-lib"; 3 | import "source-map-support/register"; 4 | import { LangchainJsStreamAgentStack } from "../lib/langchain-js-stream-agent-stack"; 5 | 6 | const app = new cdk.App(); 7 | new LangchainJsStreamAgentStack(app, "LangchainJsStreamAgentStack", { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | /* Uncomment the next line to specialize this stack for the AWS Account 12 | * and Region that are implied by the current CLI configuration. */ 13 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 14 | /* Uncomment the next line if you know exactly what Account and Region you 15 | * want to deploy the stack to. */ 16 | // env: { account: 'your-account-id', region: 'us-east-1' }, 17 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 18 | }); 19 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/langchain-js-stream-agent.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 24 | "@aws-cdk/aws-iam:minimizePolicies": true, 25 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 26 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 27 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 28 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 29 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 30 | "@aws-cdk/core:enablePartitionLiterals": true, 31 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 32 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 34 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 35 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 36 | "@aws-cdk/aws-route53-patters:useCertificate": true, 37 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 38 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 39 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 40 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 41 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 42 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 43 | "@aws-cdk/aws-redshift:columnId": true, 44 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 45 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 46 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 47 | "@aws-cdk/aws-kms:aliasNameRef": true, 48 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 49 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 50 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 51 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 52 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 53 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 54 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 55 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 56 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 57 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 58 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 59 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 60 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 61 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 62 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 63 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 64 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 65 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, 66 | "@aws-cdk/aws-stepfunctions-tasks:ecsReduceRunTaskPermissions": true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Context, Handler, LambdaFunctionURLEvent } from "aws-lambda"; 2 | import { Writable } from "stream"; 3 | 4 | /** 5 | * TypeScript Typings for the `awslambda` Module 6 | * 7 | * For more details, visit the GitHub repository: 8 | * https://github.com/llozano/lambda-stream-response/blob/main/src/%40types/awslambda/index.d.ts 9 | */ 10 | declare global { 11 | namespace awslambda { 12 | export function streamifyResponse( 13 | handler: ( 14 | evt: LambdaFunctionURLEvent & { body: string }, 15 | responseStream: Writable, 16 | context: Context 17 | ) => Promise 18 | ): Handler; 19 | export class HttpResponseStream { 20 | static from( 21 | responseStream: Writable, 22 | metadata: { statusCode: number; headers: Record } 23 | ): Writable; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | roots: ["/test"], 4 | testMatch: ["**/*.test.ts"], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/lambda/agent.ts: -------------------------------------------------------------------------------- 1 | import { BedrockChat } from "@langchain/community/chat_models/bedrock"; 2 | import { Calculator } from "@langchain/community/tools/calculator"; 3 | import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; 4 | import type { PromptTemplate } from "@langchain/core/prompts"; 5 | import { AgentExecutor, createReactAgent } from "langchain/agents"; 6 | import { pull } from "langchain/hub"; 7 | 8 | /** 9 | * AWS Lambda with Streaming Response 10 | * This functionality enables the AWS Lambda to send back a streaming response to the caller. 11 | * For more details, refer to the AWS documentation: 12 | * https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html 13 | * 14 | * LangChain Agent Creation 15 | * Utilizes the LangChain API to create a reactive agent equipped with various tools. 16 | * More information can be found at: 17 | * https://v02.api.js.langchain.com/functions/langchain_agents.createReactAgent.html 18 | */ 19 | export const handler = awslambda.streamifyResponse( 20 | async (_evt, responseStream, _context) => { 21 | // Set the response content type to text/event-stream 22 | // See https://github.com/serverless/serverless/discussions/12090#discussioncomment-6685223 23 | const metadata = { 24 | statusCode: 200, 25 | headers: { 26 | "Content-Type": "text/event-stream; charset=utf-8", 27 | }, 28 | }; 29 | responseStream = awslambda.HttpResponseStream.from( 30 | responseStream, 31 | metadata 32 | ); 33 | 34 | class CustomHandler extends BaseCallbackHandler { 35 | name = "custom_handler"; 36 | 37 | handleLLMNewToken(token: string) { 38 | responseStream.write(`token: ${token}\n`); 39 | } 40 | } 41 | 42 | const llm = new BedrockChat({ 43 | model: "anthropic.claude-3-sonnet-20240229-v1:0", 44 | region: "us-east-1", 45 | streaming: true, 46 | callbacks: [new CustomHandler()], 47 | }); 48 | 49 | const tools = [new Calculator()]; 50 | 51 | const prompt = await pull("hwchase17/react"); 52 | 53 | const agent = await createReactAgent({ 54 | llm, 55 | tools, 56 | prompt, 57 | }); 58 | 59 | const agentExecutor = new AgentExecutor({ 60 | agent, 61 | tools, 62 | }); 63 | 64 | await agentExecutor.invoke({ 65 | input: "What is 2 to the power of 8?", 66 | }); 67 | 68 | responseStream.end(); 69 | } 70 | ); 71 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/lib/langchain-js-stream-agent-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import * as iam from "aws-cdk-lib/aws-iam"; 3 | import * as lambda from "aws-cdk-lib/aws-lambda"; 4 | import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs"; 5 | import { Construct } from "constructs"; 6 | import { join } from "path"; 7 | 8 | export class LangchainJsStreamAgentStack extends cdk.Stack { 9 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | const agentFunction = new lambdaNodejs.NodejsFunction( 13 | this, 14 | "AgentFunction", 15 | { 16 | entry: join(__dirname, "..", "lambda", "agent.ts"), 17 | architecture: lambda.Architecture.ARM_64, 18 | runtime: lambda.Runtime.NODEJS_20_X, 19 | timeout: cdk.Duration.seconds(60), 20 | bundling: { 21 | externalModules: [], 22 | }, 23 | } 24 | ); 25 | 26 | // Warning: the Lambda URL is public and can be accessed by anyone 27 | // Use an authentication mechanism to secure the endpoint 28 | const agentFunctionUrl = agentFunction.addFunctionUrl({ 29 | authType: lambda.FunctionUrlAuthType.NONE, 30 | invokeMode: lambda.InvokeMode.RESPONSE_STREAM, 31 | }); 32 | 33 | const bedrockModelPolicy = new iam.PolicyStatement({ 34 | actions: ["bedrock:InvokeModelWithResponseStream"], 35 | effect: iam.Effect.ALLOW, 36 | resources: [ 37 | "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0", 38 | ], 39 | }); 40 | 41 | agentFunction.addToRolePolicy(bedrockModelPolicy); 42 | 43 | new cdk.CfnOutput(this, "agentFunctionUrlOutput", { 44 | value: agentFunctionUrl.url, 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "langchain-js-stream-agent", 3 | "version": "0.1.0", 4 | "bin": { 5 | "langchain-js-stream-agent": "bin/langchain-js-stream-agent.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/aws-lambda": "^8.10.141", 15 | "@types/jest": "^29.5.12", 16 | "@types/node": "20.14.9", 17 | "aws-cdk": "2.149.0", 18 | "jest": "^29.7.0", 19 | "ts-jest": "^29.1.5", 20 | "ts-node": "^10.9.2", 21 | "typescript": "~5.5.3" 22 | }, 23 | "dependencies": { 24 | "@langchain/aws": "^0.0.4", 25 | "@langchain/community": "^0.3.3", 26 | "@langchain/core": "^0.2.17", 27 | "aws-cdk-lib": "2.186.0", 28 | "constructs": "^10.0.0", 29 | "langchain": "^0.2.20", 30 | "source-map-support": "^0.5.21" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bedrock/langchain-js-stream-agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["es2020", "dom"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /front-end/.gitignore: -------------------------------------------------------------------------------- 1 | #amplify-do-not-edit-begin 2 | amplify/\#current-cloud-backend 3 | amplify/.config/local-* 4 | amplify/logs 5 | amplify/mock-data 6 | amplify/mock-api-resources 7 | amplify/backend/amplify-meta.json 8 | amplify/backend/.temp 9 | build/ 10 | dist/ 11 | node_modules/ 12 | aws-exports.js 13 | awsconfiguration.json 14 | amplifyconfiguration.json 15 | amplifyconfiguration.dart 16 | amplify-build-config.json 17 | amplify-gradle-config.json 18 | amplifytools.xcconfig 19 | .secret-* 20 | **.sample 21 | #amplify-do-not-edit-end -------------------------------------------------------------------------------- /front-end/README.md: -------------------------------------------------------------------------------- 1 | This repo is to interact with the agents deployed in openai/ or langchain-agent/openai. Instructions to set up the front-end are in the readmes of the aforementioned repositories. -------------------------------------------------------------------------------- /front-end/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | 3 | export function Button({ className, ...props }: any) { 4 | return ( 5 | 40 | 41 | ); 42 | 43 | export function Chat(user: any) { 44 | const initialMessage: Message[] = [ 45 | { 46 | who: "bot", 47 | message: `Hi ${ 48 | user?.attributes?.given_name ?? "User" 49 | }! How can I help you today?`, 50 | }, 51 | ]; 52 | const [messages, setMessages] = useState(initialMessage); 53 | const [input, setInput] = useState(""); 54 | const [loading, setLoading] = useState(false); 55 | const [cookie, setCookie] = useCookies([COOKIE_NAME]); 56 | 57 | useEffect(() => { 58 | if (!cookie[COOKIE_NAME]) { 59 | // generate a semi random short id 60 | const randomId = Math.random().toString(36).substring(7); 61 | setCookie(COOKIE_NAME, randomId); 62 | } 63 | }, [cookie, setCookie]); 64 | 65 | const sendMessage = async (message: string) => { 66 | setLoading(true); 67 | const newMessages = [ 68 | ...messages, 69 | { message: message, who: "user" } as Message, 70 | ]; 71 | setMessages(newMessages); 72 | const lastTenMessages = newMessages.slice(-10); 73 | 74 | const essentialCredentials = Auth.essentialCredentials( 75 | await Auth.currentCredentials() 76 | ); 77 | 78 | const url = process.env.NEXT_PUBLIC_LAMBDA_URL as string; 79 | const functionUrl = new URL(url); 80 | 81 | const sigv4 = new SignatureV4({ 82 | service: "lambda", 83 | region: process.env.NEXT_PUBLIC_AWS_REGION as string, 84 | credentials: { 85 | accessKeyId: essentialCredentials.accessKeyId, 86 | secretAccessKey: essentialCredentials.secretAccessKey, 87 | sessionToken: essentialCredentials.sessionToken, 88 | }, 89 | sha256: Sha256, 90 | }); 91 | 92 | const lastMessage = lastTenMessages.slice(-1)[0].message; 93 | 94 | // sign the request 95 | const signed = await sigv4.sign({ 96 | method: "POST", 97 | hostname: functionUrl.host, 98 | path: functionUrl.pathname, 99 | protocol: functionUrl.protocol, 100 | headers: { 101 | "Content-Type": "application/json", 102 | host: functionUrl.hostname, 103 | }, 104 | body: JSON.stringify({ 105 | message: lastMessage, 106 | user: user, 107 | userId: user?.attributes?.sub ?? cookie[COOKIE_NAME], 108 | }), 109 | }); 110 | 111 | delete signed.headers["host"]; 112 | 113 | try { 114 | const response = await axios.post(url, signed.body, { 115 | headers: signed.headers, 116 | }); 117 | 118 | const data = await response.data; 119 | 120 | setMessages([...newMessages, { message: data, who: "bot" } as Message]); 121 | } catch (error) { 122 | console.log(error); 123 | throw error; 124 | } 125 | 126 | setLoading(false); 127 | }; 128 | 129 | return ( 130 |
131 | {messages && 132 | messages.map(({ message, who }, index) => ( 133 | 134 | ))} 135 | 136 | {loading && } 137 | 138 | {messages.length < 2 && ( 139 | 140 | Type a message to start the conversation 141 | 142 | )} 143 | 148 |
149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /front-end/components/ChatLine.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import Balancer from 'react-wrap-balancer' 3 | 4 | // wrap Balancer to remove type errors :( - @TODO - fix this ugly hack 5 | const BalancerWrapper = (props: any) => 6 | 7 | export type Message = { 8 | who: 'bot' | 'user' | undefined 9 | message?: string 10 | } 11 | 12 | // loading placeholder animation for the chat line 13 | export const LoadingChatLine = () => ( 14 |
15 |
16 |
17 |

18 | 19 | Assistant 20 | 21 |

22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ) 33 | 34 | // util helper to convert new lines to
tags 35 | const convertNewLines = (text: string) => 36 | text.split('\n').map((line, i) => ( 37 | 38 | {line} 39 |
40 |
41 | )) 42 | 43 | export function ChatLine({ who = 'bot', message }: Message) { 44 | if (!message) { 45 | return null 46 | } 47 | const formatteMessage = convertNewLines(message) 48 | 49 | return ( 50 |
55 | 56 |
57 |
58 |
59 |

60 | 61 | {who == 'bot' ? 'Assistant' : 'You'} 62 | 63 |

64 |

70 | {formatteMessage} 71 |

72 |
73 |
74 |
75 |
76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /front-end/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /front-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": "https://github.com/vercel/examples.git", 3 | "license": "MIT", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@aws-amplify/ui-react": "^4.3.7", 13 | "@vercel/examples-ui": "^1.0.4", 14 | "aws-amplify": "^5.0.14", 15 | "clsx": "^1.2.1", 16 | "next": "latest", 17 | "react": "latest", 18 | "react-cookie": "^4.1.1", 19 | "react-dom": "latest", 20 | "react-wrap-balancer": "0.4.0" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^17", 24 | "@types/react": "latest", 25 | "autoprefixer": "^10.4.7", 26 | "eslint": "^8.11.0", 27 | "eslint-config-next": "^12.1.0", 28 | "postcss": "^8.4.14", 29 | "tailwindcss": "^3.0.24", 30 | "typescript": "^4.7.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /front-end/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import type { NextApiRequest } from 'next'; 3 | // import type { LayoutProps } from '@vercel/examples-ui/layout' 4 | import { Amplify, withSSRContext} from 'aws-amplify'; 5 | 6 | import { withAuthenticator } from '@aws-amplify/ui-react'; 7 | 8 | import '@aws-amplify/ui-react/styles.css'; 9 | import './globals.css' 10 | import awsExports from '../src/aws-exports'; 11 | 12 | Amplify.configure(awsExports); 13 | 14 | export async function getServerSideProps(req: any ) { 15 | const {Auth} = withSSRContext(req); 16 | try { 17 | const user = await Auth.currentAuthenticatedUser(); 18 | return { 19 | props: { 20 | user, 21 | }, 22 | }; 23 | } catch (err) { 24 | console.log(err); 25 | return { 26 | props: {}, 27 | }; 28 | } 29 | } 30 | 31 | function App({ Component, pageProps, user }: any) { 32 | return ( 33 | 34 | ) 35 | } 36 | 37 | export default withAuthenticator(App) 38 | -------------------------------------------------------------------------------- /front-end/pages/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /** 6 | * Chrome has a bug with transitions on load since 2012! 7 | * 8 | * To prevent a "pop" of content, you have to disable all transitions until 9 | * the page is done loading. 10 | * 11 | * https://lab.laukstein.com/bug/input 12 | * https://twitter.com/timer150/status/1345217126680899584 13 | */ 14 | body.loading * { 15 | transition: none !important; 16 | } 17 | 18 | *, 19 | *:before, 20 | *:after { 21 | box-sizing: inherit; 22 | } 23 | 24 | html { 25 | height: 100%; 26 | box-sizing: border-box; 27 | touch-action: manipulation; 28 | } 29 | 30 | body { 31 | position: relative; 32 | min-height: 100%; 33 | margin: 0; 34 | } 35 | 36 | html, 37 | body { 38 | font-family: theme(fontFamily.sans); 39 | text-rendering: optimizeLegibility; 40 | -webkit-font-smoothing: antialiased; 41 | -moz-osx-font-smoothing: grayscale; 42 | background-color: var(--accents-0); 43 | overscroll-behavior-x: none; 44 | } 45 | 46 | li { 47 | list-style: none; 48 | } 49 | -------------------------------------------------------------------------------- /front-end/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, Text, Page } from '@vercel/examples-ui' 2 | import { Chat } from '../components/Chat' 3 | 4 | function Home({user}: any) { 5 | return ( 6 | 7 |
8 | AI Assistant 9 | 10 | Test our secured AI Assistant powered by Generative AI! 11 | 12 |
13 | 14 |
15 | AI Assistant: 16 |
17 | 18 |
19 |
20 |
21 | ) 22 | } 23 | 24 | export default Home 25 | -------------------------------------------------------------------------------- /front-end/postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /front-end/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/langchain-agents/HEAD/front-end/public/favicon.ico -------------------------------------------------------------------------------- /front-end/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require('@vercel/examples-ui/tailwind')], 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx}', 5 | './components/**/*.{js,ts,jsx,tsx}', 6 | './node_modules/@vercel/examples-ui/**/*.js', 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /front-end/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /openai/README.md: -------------------------------------------------------------------------------- 1 | # Secure AI Agents With Langchain, OpenAI’s GPT-4, and AWS 2 | 3 | ## Requirements 4 | 5 | - [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. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. 6 | - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured 7 | - [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 8 | - [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (AWS CDK) v2 Installed 9 | - [NodeJS and NPM](https://nodejs.org/en/download/) Installed 10 | - [OpenAI API Key](https://platform.openai.com/account/api-keys) 11 | - [Amplify CLI](https://docs.amplify.aws/cli/start/install/) installed and configured 12 | 13 | ## Architecture Overview 14 | ![Alt text](./architecture_diagram.png?raw=true "Architecture") 15 | 16 | ## Deployment Instructions 17 | 18 | 1. Clone the repository and navigate it: 19 | 20 | ``` 21 | git clone https://github.com/aws-samples/langchain-agents.git 22 | cd langchain-agents/openai 23 | ``` 24 | 25 | 2. Install the project dependencies: 26 | 27 | ``` 28 | npm install 29 | ``` 30 | 31 | 3. Export the OpenAI API Key to store it in AWS SSM Parameter Store: 32 | 33 | ``` 34 | export OPENAI_API_KEY= 35 | ``` 36 | 37 | 4. The Cognito User Pool Domain must be unique. You can either go to the lib/ai-stack.ts and manually change the name or, if you are on a Mac, run the command below. 38 | 39 | ``` 40 | timestamp=$(date +%Y%m%d%H%M%S) 41 | sed -i '' "s/ai-domain/ai-domain-$timestamp/" lib/ai-stack.ts 42 | ``` 43 | 44 | Note: The first time you deploy an AWS CDK app into an environment (account/region), you’ll need to install a “bootstrap stack”. This stack includes resources that are needed for the toolkit’s operation. 45 | Use the following command to install the bootstrap stack into the environment: 46 | 47 | ``` 48 | npx cdk bootstrap 49 | ``` 50 | 51 | 5. Use AWS CDK to synthesize an AWS CloudFormation: 52 | 53 | ``` 54 | npx cdk synth 55 | ``` 56 | 57 | 6. Use AWS CDK to deploy the AWS resources for the pattern: 58 | 59 | ``` 60 | npx cdk deploy 61 | ``` 62 | 63 | During the deployment you will be asked to confirm the security-sensitive changes. Review it and enter y to deploy the stack and create the resources. 64 | 65 | 7. Save the following from the outputted values: 66 | 67 | a. UserPoolClientIdWeb. 68 | 69 | b. UserPoolId. 70 | 71 | c. FunctionUrl. 72 | 73 | 8. Navigate back to the root directory and change directory to the frontend directory: 74 | 75 | ``` 76 | cd ../front-end 77 | ``` 78 | 79 | 9. Install the project dependencies: 80 | 81 | ``` 82 | npm install 83 | ``` 84 | 85 | 10. Configure a new AWS Amplify project: 86 | 87 | ``` 88 | amplify init 89 | ``` 90 | 91 | (Optional) If you are running into errors run: 92 | 93 | ``` 94 | amplify configure 95 | ``` 96 | 97 | 11. Import your existing auth resource to your local back-end 98 | 99 | ``` 100 | amplify import auth 101 | ``` 102 | 103 | Select the "Cognito User Pool and Identity Pool" option and select the values you saved previously. 104 | 105 | 12. Provision cloud back-end resources with the latest local changes: 106 | 107 | ``` 108 | amplify push 109 | ``` 110 | 111 | Create .env.local file to store environmental variable and replace the variables with the Lambda Function URL and corresponding AWS Region: 112 | 113 | ``` 114 | echo "NEXT_PUBLIC_LAMBDA_URL=" > .env.local 115 | echo "NEXT_PUBLIC_AWS_REGION=" >> .env.local 116 | ``` 117 | 118 | Run application: 119 | 120 | ``` 121 | npm run dev 122 | ``` 123 | 124 | ## Cleanup 125 | 126 | 1. To remove the Amplify auth, run: 127 | 128 | ``` 129 | amplify remove auth 130 | ``` 131 | 132 | 2. To delete the Amplify project and associated backend resources, run: 133 | 134 | ``` 135 | amplify delete 136 | ``` 137 | 138 | 3. To delete the stack, run: 139 | 140 | ``` 141 | npx cdk destroy 142 | ``` 143 | 144 | ## Useful commands 145 | 146 | - `cdk ls` list all stacks in the app 147 | - `cdk synth` emits the synthesized CloudFormation template 148 | - `cdk deploy` deploy this stack to your default AWS account/region 149 | - `cdk diff` compare deployed stack with current state 150 | - `cdk docs` open CDK documentation 151 | -------------------------------------------------------------------------------- /openai/architecture_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/langchain-agents/HEAD/openai/architecture_diagram.png -------------------------------------------------------------------------------- /openai/bin/ai-app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { AIStack } from '../lib/ai-stack'; 5 | import { AwsSolutionsChecks } from 'cdk-nag' 6 | import { Aspects } from 'aws-cdk-lib'; 7 | 8 | const app = new cdk.App(); 9 | 10 | Aspects.of(app).add(new AwsSolutionsChecks({verbose: true})); 11 | new AIStack(app, 'ServerlessAIStack', { 12 | /* If you don't specify 'env', this stack will be environment-agnostic. 13 | * Account/Region-dependent features and context lookups will not work, 14 | * but a single synthesized template can be deployed anywhere. */ 15 | 16 | /* Uncomment the next line to specialize this stack for the AWS Account 17 | * and Region that are implied by the current CLI configuration. */ 18 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /openai/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/ai-app.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 24 | "@aws-cdk/aws-iam:minimizePolicies": true, 25 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 26 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 27 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 28 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 29 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 30 | "@aws-cdk/core:enablePartitionLiterals": true, 31 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 32 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 34 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /openai/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /openai/lambda/AIMessageProcessor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import boto3 4 | from langchain.chat_models import ChatOpenAI 5 | from chat import Chat 6 | from Agent import Agent 7 | from config import config 8 | 9 | conversation_table_name = config.CONVERSATION_TABLE_NAME 10 | openai_api_key_ssm_parameter_name = config.OPENAI_API_KEY_SSM_PARAMETER_NAME 11 | 12 | 13 | def lambda_handler(event, context): 14 | print(event) 15 | chat = Chat(event) 16 | set_openai_api_key() 17 | user_message = get_user_message(event) 18 | llm = ChatOpenAI(temperature=0, model_name="gpt-4") 19 | langchain_agent = Agent(llm, chat.memory) 20 | message = langchain_agent.run(input=user_message) 21 | return chat.http_response(message) 22 | 23 | 24 | def is_user_request_to_start_new_conversation(event): 25 | user_message = get_user_message(event) 26 | return "start a new conversation" in user_message.strip().lower() 27 | 28 | 29 | def get_user_message(event): 30 | body = load_body(event) 31 | user_message_body = body['message'] 32 | return user_message_body 33 | 34 | 35 | def load_body(event): 36 | body = json.loads(event['body']) 37 | return body 38 | 39 | 40 | def set_openai_api_key(): 41 | ssm = boto3.client('ssm') 42 | response = ssm.get_parameter(Name=openai_api_key_ssm_parameter_name) 43 | os.environ["OPENAI_API_KEY"] = response['Parameter']['Value'] 44 | -------------------------------------------------------------------------------- /openai/lambda/Agent.py: -------------------------------------------------------------------------------- 1 | from langchain.agents.tools import Tool 2 | from langchain.agents.conversational.base import ConversationalAgent 3 | from langchain.agents import AgentExecutor 4 | from tools import tools 5 | from datetime import datetime 6 | 7 | 8 | class Agent(): 9 | def __init__(self, llm, memory) -> None: 10 | self.prefix = "The following is a conversation between you, Agent, and a user. By the way, the date is " + \ 11 | datetime.now().strftime("%m/%d/%Y, %H:%M:%S") + "." 12 | self.ai_prefix = "Agent" 13 | self.human_prefix = "User" 14 | self.llm = llm 15 | self.memory = memory 16 | self.agent = self.create_agent() 17 | 18 | def create_agent(self): 19 | agent = ConversationalAgent.from_llm_and_tools( 20 | llm=self.llm, 21 | tools=tools, 22 | prefix=self.prefix, 23 | ai_prefix=self.ai_prefix, 24 | human_prefix=self.human_prefix 25 | ) 26 | agent_executor = AgentExecutor.from_agent_and_tools( 27 | agent=agent, tools=tools, verbose=True, memory=self.memory) 28 | return agent_executor 29 | 30 | def run(self, input): 31 | return self.agent.run(input=input) 32 | -------------------------------------------------------------------------------- /openai/lambda/chat.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | from boto3.dynamodb.types import TypeSerializer 4 | from langchain.memory.chat_message_histories import DynamoDBChatMessageHistory 5 | from langchain.memory import ConversationBufferMemory 6 | from config import config 7 | 8 | from datetime import datetime 9 | import json 10 | 11 | now = datetime.utcnow() 12 | dynamodb = boto3.client('dynamodb') 13 | ts = TypeSerializer() 14 | 15 | openai_api_key_ssm_parameter_name = config.OPENAI_API_KEY_SSM_PARAMETER_NAME 16 | conversation_table_name = config.CONVERSATION_TABLE_NAME 17 | 18 | 19 | class Chat(): 20 | def __init__(self, event): 21 | self.set_user_identity(event) 22 | self.set_memory() 23 | 24 | def set_memory(self): 25 | _id = self.user_id 26 | self.message_history = DynamoDBChatMessageHistory( 27 | table_name=conversation_table_name, session_id=_id) 28 | self.memory = ConversationBufferMemory( 29 | memory_key="chat_history", chat_memory=self.message_history, return_messages=True) 30 | 31 | def set_user_identity(self, event): 32 | body = json.loads(event['body']) 33 | self.user_id = body['userId'] 34 | 35 | 36 | def http_response(self, message): 37 | return { 38 | 'statusCode': 200, 39 | 'body': json.dumps(message) 40 | } 41 | -------------------------------------------------------------------------------- /openai/lambda/config.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import os 3 | 4 | 5 | @dataclass(frozen=True) 6 | class Config: 7 | CONVERSATION_TABLE_NAME = os.environ['CONVERSATION_TABLE_NAME'] 8 | OPENAI_API_KEY_SSM_PARAMETER_NAME = os.environ['OPENAI_API_KEY_SSM_PARAMETER_NAME'] 9 | 10 | 11 | config = Config() 12 | -------------------------------------------------------------------------------- /openai/lambda/tools.py: -------------------------------------------------------------------------------- 1 | from langchain.agents.tools import Tool 2 | 3 | class Tools(): 4 | def __init__(self) -> None: 5 | self.tools = [ 6 | Tool( 7 | name="Hello World Tool", 8 | func=lambda x: "Hello world", 9 | description="useful for a simple hello world" 10 | ) 11 | ] 12 | 13 | tools = Tools().tools 14 | -------------------------------------------------------------------------------- /openai/lib/ai-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 4 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 5 | import * as iam from 'aws-cdk-lib/aws-iam'; 6 | import * as ssm from 'aws-cdk-lib/aws-ssm'; 7 | import * as cognito from 'aws-cdk-lib/aws-cognito'; 8 | import { NagSuppressions } from 'cdk-nag'; 9 | 10 | export class AIStack extends cdk.Stack { 11 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 12 | super(scope, id, props); 13 | 14 | // Defines a DynamoDB Table to store conversations 15 | const conversationTable = new dynamodb.Table(this, 'ConversationTable', { 16 | partitionKey: { name: 'SessionId', type: dynamodb.AttributeType.STRING }, 17 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 18 | // //Enable point-in-time recovery 19 | // pointInTimeRecovery: true, 20 | // //Delete the table after the stack is deleted 21 | // removalPolicy: cdk.RemovalPolicy.DESTROY, //or RETAIN in prod 22 | }); 23 | 24 | // Defines a Cognito User Pool for user authentication 25 | const userPool = new cognito.UserPool(this, 'userPool', { 26 | userPoolName: 'user-pool', 27 | selfSignUpEnabled: true, 28 | removalPolicy: cdk.RemovalPolicy.DESTROY, //or RETAIN in prod 29 | // Enable MFA 30 | // mfa: cognito.Mfa.REQUIRED, 31 | accountRecovery: cognito.AccountRecovery.EMAIL_ONLY, 32 | mfaSecondFactor: { 33 | sms: false, 34 | otp: true, 35 | }, 36 | signInAliases: { 37 | phone: true, 38 | email: true, 39 | }, 40 | autoVerify: { 41 | email: true, 42 | }, 43 | keepOriginal: { 44 | email: true, 45 | }, 46 | standardAttributes: { 47 | phoneNumber: { 48 | required: true, 49 | }, 50 | email: { 51 | required: true, 52 | }, 53 | givenName: { 54 | required: true, 55 | }, 56 | }, 57 | enableSmsRole: false, 58 | passwordPolicy: { 59 | minLength: 8, 60 | requireLowercase: true, 61 | requireUppercase: true, 62 | requireDigits: true, 63 | requireSymbols: true, 64 | }, 65 | advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED 66 | }); 67 | 68 | //Defines the domain associated with the user pool 69 | new cognito.UserPoolDomain(this, 'userPoolDomain', { 70 | userPool, 71 | cognitoDomain: { 72 | domainPrefix: 'ai-domain', //needs to be unqiue 73 | } 74 | }); 75 | 76 | //Defines a web client for the user pool without secret 77 | const userPoolClientWeb = new cognito.UserPoolClient(this, 'userPoolClientWeb', { 78 | userPool: userPool, 79 | userPoolClientName: 'user-pool-client-web', 80 | oAuth: { 81 | flows: { 82 | authorizationCodeGrant: true, 83 | implicitCodeGrant: false, 84 | }, 85 | scopes: [ 86 | cognito.OAuthScope.EMAIL, 87 | cognito.OAuthScope.OPENID, 88 | cognito.OAuthScope.PROFILE, 89 | cognito.OAuthScope.PHONE, 90 | cognito.OAuthScope.COGNITO_ADMIN 91 | ], 92 | callbackUrls: ['http://localhost:3000/'], 93 | logoutUrls: ['http://localhost:3000/'] 94 | } 95 | }); 96 | 97 | //Defines a native client for the user pool with secret 98 | const userPoolClientNative = new cognito.UserPoolClient(this, 'userPoolClientNative', { 99 | userPool: userPool, 100 | userPoolClientName: 'user-pool-client-native', 101 | oAuth: { 102 | flows: { 103 | authorizationCodeGrant: true, 104 | implicitCodeGrant: false, 105 | }, 106 | scopes: [ 107 | cognito.OAuthScope.EMAIL, 108 | cognito.OAuthScope.OPENID, 109 | cognito.OAuthScope.PROFILE, 110 | cognito.OAuthScope.PHONE, 111 | cognito.OAuthScope.COGNITO_ADMIN 112 | ], 113 | callbackUrls: ['http://localhost:3000/'], 114 | logoutUrls: ['http://localhost:3000/'] 115 | }, 116 | generateSecret: true 117 | }); 118 | 119 | 120 | // Defines an Identity Pool for user authorization 121 | const identityPool = new cognito.CfnIdentityPool(this, 'identityPool',{ 122 | identityPoolName: 'identity-pool', 123 | allowUnauthenticatedIdentities: false, 124 | cognitoIdentityProviders: [{ 125 | clientId: userPoolClientWeb.userPoolClientId, 126 | providerName:userPool.userPoolProviderName, 127 | }, 128 | { 129 | clientId: userPoolClientNative.userPoolClientId, 130 | providerName:userPool.userPoolProviderName, 131 | }] 132 | }); 133 | 134 | // Defines an IAM Role that will be assumed by authenticated users of User Pool 135 | const cognitoRole = new iam.Role(this, 'cognitoUserPoolRole', { 136 | assumedBy: new iam.FederatedPrincipal('cognito-identity.amazonaws.com', { 137 | 'StringEquals': { 138 | 'cognito-identity.amazonaws.com:aud' : identityPool.ref, 139 | }, 140 | "ForAnyValue:StringLike" : { 141 | "cognito-identity.amazonaws.com:amr" : "authenticated" 142 | }, 143 | }, 'sts:AssumeRoleWithWebIdentity'), 144 | }); 145 | 146 | // Defines an IAM Role that will be assumed by unauthenticated users of User Pool 147 | const cognitoUnauthRole = new iam.Role(this, 'cognitoUserPoolUnauthRole', { 148 | assumedBy: new iam.FederatedPrincipal('cognito-identity.amazonaws.com', { 149 | 'StringEquals': { 150 | 'cognito-identity.amazonaws.com:aud' : identityPool.ref, 151 | }, 152 | "ForAnyValue:StringLike" : { 153 | "cognito-identity.amazonaws.com:amr" : "unauthenticated" 154 | }, 155 | }, 'sts:AssumeRoleWithWebIdentity'), 156 | }); 157 | 158 | // Defines a lambda execution role 159 | const lambdaRole = new iam.Role(this, 'lambdaRole', { 160 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 161 | }); 162 | 163 | // Defines a Python Lambda resource AIMessageProcessor that has a function 164 | const AIMessageProcessor = new lambda.Function(this, 'AIMessageProcessor', { 165 | runtime: lambda.Runtime.PYTHON_3_9, 166 | code: lambda.Code.fromAsset('lambda'), 167 | handler: 'AIMessageProcessor.lambda_handler', 168 | timeout: cdk.Duration.seconds(120), 169 | architecture: lambda.Architecture.ARM_64, 170 | role: lambdaRole, 171 | }); 172 | 173 | // Defines a policy to the Lambda execution role that allows it to access CloudWatch Logs 174 | AIMessageProcessor.addToRolePolicy(new iam.PolicyStatement({ 175 | effect: iam.Effect.ALLOW, 176 | actions: [ 177 | 'logs:CreateLogGroup', 178 | 'logs:CreateLogStream', 179 | 'logs:PutLogEvents' 180 | ], 181 | resources: ['*'] 182 | })); 183 | 184 | // Defines CDK Nag rule suppression for wildcard permission for lambda execution role 185 | NagSuppressions.addResourceSuppressions(lambdaRole, [{ 186 | id: 'AwsSolutions-IAM5', 187 | reason: 'Wildcild permission is needed to create custom Lambda execution role to write to CloudWatch Logs' 188 | }], 189 | true 190 | ); 191 | 192 | // Defines IAM permission to the functoin to allow Cognito Identity Pool to invoke it 193 | AIMessageProcessor.addPermission('allowCognitoIdentityPoolInvoke', { 194 | principal: new iam.ServicePrincipal('cognito-idp.amazonaws.com'), 195 | sourceArn: `arn:aws:cognito-identity:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:identitypool/${identityPool.ref}` 196 | }); 197 | 198 | // Defines a policy to the Cognito User Pool Role that allows it to invoke the AIMessageProcess Lambda function 199 | const authUserPolicy = new iam.PolicyStatement({ 200 | effect: iam.Effect.ALLOW, 201 | actions: ['lambda:InvokeFunctionUrl'], 202 | resources: [AIMessageProcessor.functionArn], 203 | }); 204 | 205 | cognitoRole.addToPolicy(authUserPolicy); 206 | 207 | // Add the policy to the authenticated role for the Identity Pool 208 | const authUserRole = new cognito.CfnIdentityPoolRoleAttachment(this, 'identityPoolRoleAttachment', { 209 | identityPoolId: identityPool.ref, 210 | roles: { 211 | authenticated: cognitoRole.roleArn, 212 | unauthenticated: cognitoUnauthRole.roleArn, 213 | }, 214 | }); 215 | 216 | authUserRole.addDependency(identityPool); 217 | 218 | // Defines a lambda function URL with secure authType 219 | const lambdaUrl = AIMessageProcessor.addFunctionUrl({ 220 | authType: lambda.FunctionUrlAuthType.AWS_IAM, 221 | cors: { 222 | allowCredentials: true, 223 | allowedOrigins: ['*'], 224 | allowedHeaders: ['*'], 225 | exposedHeaders: ['*'], 226 | maxAge: cdk.Duration.seconds(86400), 227 | } 228 | }); 229 | 230 | // Build Langchain layer that includes OpenAI from layers/langchain-layer.zip 231 | const langchainLayer = new lambda.LayerVersion.fromLayerVersionArn(this, 'LangchainLayer', 'arn:aws:lambda:us-east-1:049513818483:layer:openai:2'); 232 | AIMessageProcessor.addLayers(langchainLayer); 233 | 234 | // Grant Lambda function access to DynamoDB tables 235 | conversationTable.grantReadWriteData(AIMessageProcessor); 236 | 237 | // Stores OpenAI API key in AWS SSM Parameter Store 238 | const openAIKey = new ssm.StringParameter(this, 'OpenAIKey', { 239 | description: 'OpenAI API Key', 240 | stringValue: process.env.OPENAI_API_KEY!, 241 | }); 242 | 243 | // Grant Lambda function role access to read OpenAI API key from SSM Parameter Store 244 | const ssmPolicy = new iam.PolicyStatement({ 245 | effect: iam.Effect.ALLOW, 246 | actions: ['ssm:GetParameter'], 247 | resources: [openAIKey.parameterArn] 248 | }); 249 | 250 | AIMessageProcessor.addToRolePolicy(ssmPolicy); 251 | 252 | // Pass SSM parameter name to Lambda function 253 | AIMessageProcessor.addEnvironment('OPENAI_API_KEY_SSM_PARAMETER_NAME', openAIKey.parameterName); 254 | 255 | // Pass DynamoDB table names to Lambda function 256 | AIMessageProcessor.addEnvironment('CONVERSATION_TABLE_NAME', conversationTable.tableName); 257 | 258 | new cdk.CfnOutput(this, 'FunctionUrl', { 259 | value: lambdaUrl.url, 260 | }) 261 | new cdk.CfnOutput(this, 'IdentityPoolId', { 262 | value: identityPool.ref, 263 | }) 264 | new cdk.CfnOutput(this, 'UserPoolId', { 265 | value: userPool.userPoolId, 266 | }) 267 | new cdk.CfnOutput(this, 'UserPoolClientIdWeb', { 268 | value: userPoolClientWeb.userPoolClientId, 269 | }) 270 | new cdk.CfnOutput(this, 'UserPoolClientIdNative', { 271 | value: userPoolClientNative.userPoolClientId, 272 | }) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /openai/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-assistant-app", 3 | "version": "0.1.0", 4 | "bin": { 5 | "openai_plus_aws": "bin/ai-app.ts" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.2.4", 15 | "@types/node": "18.11.15", 16 | "aws-cdk": "2.59.0", 17 | "jest": "^29.3.1", 18 | "ts-jest": "^29.0.3", 19 | "ts-node": "^10.9.1", 20 | "typescript": "~4.9.4", 21 | "cdk-nag": "2.18.44" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-apigateway": "^1.190.0", 25 | "@aws-cdk/aws-dynamodb": "^1.189.0", 26 | "@aws-cdk/aws-iam": "^1.189.0", 27 | "@aws-cdk/aws-lambda": "^1.189.0", 28 | "aws-cdk-lib": "2.184.0", 29 | "constructs": "^10.0.0", 30 | "source-map-support": "^0.5.21" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /openai/test/blog_post.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as OpenAIPlusAWS from '../lib/openai_plus_aws-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/openai_plus_aws-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new OpenAIPlusAWS.OpenAIPlusAWSStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /openai/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------